|
利用Frida实现lua热重载
背景与需求
对游戏进行修改时,尤其是代码逻辑在lua中,经常重启是一件很低效的事。所以考虑能不能有一款工具,不需要重启游戏就能让lua文件改动后立刻生效
非游戏开发,所以只有apk作为目标,无游戏工程代码。
设计原理与实现
主要分为两个模块:
- LuaFileWatcher,检测lua文件发生变化,当发生变化时调用Hotfix.lua进行重载模块的操作。
- Hotfix,重载lua模块
LuaFileWatcher
此处分享两种方法,一种使用Android的FileObserver(虽然本质上还是用的inotify),另一种直接使用 linux inotify。
android.os.FileObserver
关于该类的介绍,可以优先阅读:https://developer.android.com/reference/android/os/FileObserver
主要是通过frida js 实现 创建一个继承FileObserver的类,用于监控LUA_PATH下有哪些文件被打开了。
const LUA_PATH = "文件监控路径"
Java.performNow(function (){
var FileObserver = Java.use("android.os.FileObserver");
var LuaFileObserver = Java.registerClass({
name:'com.hotfix.LuaFileObserver',
superClass:FileObserver,
implements: [FileObserver],
methods:{
$init:[{
returnType: 'void',
arguments:['java.lang.String'],
implementation:function (p){
this.$super.$init(p)
}
}, {
returnType: 'void',
arguments:[''],
implementation:function (){
this.$super.$init()
}
}],
$new:{
returnType: 'void',
arguments:['java.lang.String'],
implementation:function (p){
this.$super.$new(p)
}
},
stopWatching:{
returnType: 'void',
arguments:[''],
implementation:function (){
this.$super.stopWatching()
}
},
startWatching:{
returnType: 'void',
arguments:[''],
implementation:function (){
this.$super.stopWatching()
}
},
finalize:{
returnType: 'void',
arguments:[''],
implementation:function (){
this.$super.stopWatching()
}
},
onEvent:function(event,path){
// console.log("event :"+event)
//console.log("path :"+path)
}
}
});
var FileWatcher = LuaFileObserver.$new(LUA_PATH)
FileObserver.onEvent.implementation = function (event,path){
console.log("event :"+event)
console.log("path :"+path)
if(event == 32)
doHotfix(path)
}
FileWatcher.startWatching()
})优点:简单
缺点:不稳定,很多机型会提示ClassNotFound。
inotify
需要先了解inotify是什么,有什么用,怎么使用:https://man7.org/linux/man-pages/man7/inotify.7.html
该功能主要是创建一个线程,不停的监控LUA_PATH下文件打开的情况。如果有文件被打开,则加载一段lua代码。
const LUA_PATH = "文件监控路径"
function LuaFileWatcher(){
var inotify_init = new NativeFunction(Module.findExportByName(null,"inotify_init"),'int',[])
var inotify_add_watch = new NativeFunction(Module.findExportByName(null,"inotify_add_watch"),'int',['int','pointer','int'])
const read = new NativeFunction(Module.findExportByName(null,"read"),'int',['int','pointer','int']);
var fd = inotify_init()
var wd = inotify_add_watch(fd,Memory.allocUtf8String(LUA_PATH),256) //ALL_EVENTS = 4095
console.log("fd "+fd+",wd "+wd)
const inotify_event_len = 0x10
var data = Memory.alloc(inotify_event_len*10);
while (1){
let readlen = read(fd,data,inotify_event_len*10-1)
if( readlen<0){
console.log(&#39;[+] Unable to read [!] &#39;);
continue
}
console.log(readlen,data)
// struct inotify_event {
// __s32 wd;
// __u32 ;
// __u32 cookie;
// __u32 len;
// char name[0];
// };
for (let i = 0; i < (readlen/0x10) ; i++) {
let readData = data.add(i*0x10)
let envent = []
envent.wd = readData.readS32();
envent.mask = readData.add(4).readU32();
envent.cookie = readData.add(8).readU32();
envent.len = readData.add(12).readU32();
envent.name = readData.add(16).readCString();
console.log(&#39;open file : &#39;,envent.name,envent.mask)
if(envent.mask!=256)
continue;
try{
console.log(&#39;----------------------&#39;)
let luaname = envent.name.replaceAll(&#34;_&#34;,&#34;.&#34;)
console.log(&#34;luaname&#34;+luaname)
var scr =&#39;if string.find(package.path,\&#34;&#39;+ LUA_PATH+&#39;\&#34;) == nil then\n&#39; +
&#39; package.path = package.path .. \&#34;;&#39;+LUA_PATH+&#39;/?\&#34;\n&#39; +
&#39;end\n&#39;+
&#39;require(\&#34;HotFixOOOK\&#34;)\n&#39;+
&#39;hotfix(\&#34;&#39;+luaname+&#39;\&#34;)&#39;
var luaL_loadstring_ret = luaL_loadstring(lua_State,Memory.allocUtf8String(scr))
console.log(&#34;luaL_loadstring_ret : &#34;+luaL_loadstring_ret)
send(&#34;load lua init ret &#34;+ lua_pcall(lua_State,0,0,0) + &#34; str:&#34;+lua_tolstring(lua_State, -1).readCString())
}catch (e) {
send(&#34;err:&#34;+e.toString())
}
}
}
}
var pthread_create = new NativeFunction(Module.findExportByName(null,&#34;pthread_create&#34;),&#39;int&#39;,[&#39;pointer&#39;,&#39;pointer&#39;,&#39;pointer&#39;,&#39;pointer&#39;])
var LuaFileWatcherNative = new NativeCallback(LuaFileWatcher,&#39;void&#39;,[&#39;void&#39;])
//启动新线程对目标目录进行文件监控。
pthread_create(Memory.alloc(16),new NativePointer(0),LuaFileWatcherNative,new NativePointer(0))package.path = package.path .. &#34;;LUA_PATH/?&#34;,这句的主要作用是将LUA_PATH添加到luaenv加载路径里面。不加会require不到用于热更的lua代码HotFixOOOK。
lua_State 通过hook libxlua.so的luaL_openlibs函数获取。
Hotfix
根据LuaRuntimeHotfix中的热更代码修改了一下。(默认apk使用的是xlua,如果不是xlua,删掉打印日志即可)
如果只修改函数,可以注释掉update_table.
function hotfix(filename)
CS.UnityEngine.Debug.LogError(&#34;start hotfix: &#34;..filename)
local dumpname = string.gsub(filename,&#34;%.&#34;,&#34;_&#34;)
local oldModule
if package.loaded[filename] then
oldModule = package.loaded[filename]
elseif package.loaded[dumpname] then
oldModule = package.loaded[dumpname]
else
CS.UnityEngine.Debug.LogError(&#39;this file nevev loaded: &#39;..filename)
require(filename)
end
package.loaded[filename] = nil
package.loaded[dumpname] = nil
CS.UnityEngine.Debug.LogError(&#39;loaded newMpdule &#39;..dumpname..&#39; ,oldModule: &#39;..filename)
local newModule = package.loaded[dumpname]
if newModule == nil then
-- try again
require(dumpname)
newModule = package.loaded[dumpname]
end
CS.UnityEngine.Debug.LogError(&#39;oldModule: &#39;.. tostring(oldModule)..&#39; ,newModule: &#39;..tostring(newModule))
-- 注释掉就只能修改函数
update_table(newModule, oldModule,updated_tables)
if newModule == nil then
package.loaded[filename] = oldModule
CS.UnityEngine.Debug.LogError(&#39;replaced faild !! &#39;)
return
end
package.loaded[filename] = newModule
CS.UnityEngine.Debug.LogError(&#39;replaced succeed&#39;)
end
function update_func(new_func, old_func)
assert(&#34;function&#34; == type(new_func))
assert(&#34;function&#34; == type(old_func))
-- Get upvalues of old function.
local old_upvalue_map = {}
local OldExistName = {}
for i = 1, math.huge do
local name, value = debug.getupvalue(old_func, i)
if not name then break end
old_upvalue_map[name] = value
OldExistName[name] = true
end
-- Update new upvalues with old.
for i = 1, math.huge do
local name, value = debug.getupvalue(new_func, i)
if not name then break end
--CS.UnityEngine.Debug.LogError(&#39;set up value: name:&#39;..name..&#39; typeof &#39;.. type(value))
if OldExistName[name] then
local old_value = old_upvalue_map[name]
if type(old_value) == &#34;function&#34; then
setfenv(new_func, getfenv(old_func))
debug.setupvalue(new_func, i, old_value)
else
debug.setupvalue(new_func, i, old_value)
end
else
-- 对新添加的upvalue设置正确的环境表
ResetENV(value,name)
end
end
end
function update_table(new_table, old_table, updated_tables)
assert(&#34;table&#34; == type(new_table))
assert(&#34;table&#34; == type(old_table))
-- Compare 2 tables, and update old table.
for key, value in pairs(new_table) do
local old_value = old_table[key]
local type_value = type(value)
if type_value == &#34;function&#34; then
update_func(value, old_value)
old_table[key] = value
elseif type_value == &#34;table&#34; then
if ( updated_tables[value] == nil ) then
updated_tables[value] = true
update_table(value, old_value,updated_tables)
end
end
end
---- Update metatable.
local old_meta = debug.getmetatable(old_table)
local new_meta = debug.getmetatable(new_table)
if type(old_meta) == &#34;table&#34; and type(new_meta) == &#34;table&#34; then
update_table(new_meta, old_meta,updated_tables)
end
end |
|