Tencent / LuaPanda

lua debug and code tools for VS Code
Other
1.27k stars 356 forks source link

[求助] LuaPanda不能停在红点断点 硬断点可以生效 #184

Open BigMadDonkey opened 6 months ago

BigMadDonkey commented 6 months ago

Describe the bug 之前我用LuaHelper插件 + LuaPanda 3.2.0 在项目中进行调试,持续使用了1年左右,都十分正常(我在配置launch.json时 useCHookfalse所以跟VSCode的升级可能没关系)。最近发现在编辑器里插入的红点断点总是不生效,参照FAQ尝试用硬断点然后执行LuaPanda.testBreakpoint发现输出如图所示:

image

并且硬断点有时会停在我们框架内扩展的可复用coroutine的逻辑附近而不是调用LuaPanda.BP()的位置。

之后尝试改用最新的LuaPanda插件 + LuaPanda3.3.1,但是红点断点仍然不生效,但在不生效位置加硬断点之后,LuaPanda.testBreakpoint输出变成下图的样子:

image

Desktop (please complete the following information):

Additional context 其实我推测可能与项目框架内对coroutine的修改有关,但是我一时实在是看不出来,故想请教一下开发者,3.2.0版本testBreakpoint中formatted 路径正常(但还是停不到断点上),而LuaPanda3.3.1 formatted显示为coroutine,可能导致这种情况的原因是什么? 十分感谢!

stuartwang commented 6 months ago

我看了下代码 首先 GetInfo 和 Normalized 都是来源于 debug.getInfo 的返回,所以这个数据是实时取到的。

image

lastRunFunction的赋值是在 function this.real_hook_process(info) 这个函数中,它会记录当前用户函数的 getInfo 信息以及 event(call/line/return) 状态

image

Formated 数据来源于 lastRunFunction["source"],这个数据是最后一次进入 debug.sethook 设置的钩子函数时获取到的. 理论上来讲,这两个数据应该是一致的,预期的结果如下

GetInfo:    @c:/Users/xxx/Desktop/luaTest2/ae.lua
Normalized: c:/users/xxx/desktop/luatest2/ae.lua
Formated:   ae.lua

我另外对比了下 3.2.0和3.3.1的 LuaPanda.lua 代码,处理协程的部分确实有所修改 3.2.0是在调试器启动连接后对协程进行了hook

        --协程调试
        if coroutineCreate == nil and type(coroutine.create) == "function" then
            this.printToConsole("change coroutine.create");
            coroutineCreate = coroutine.create;
            coroutine.create = function(...)
                local co =  coroutineCreate(...)
                table.insert(coroutinePool,  co);
                --运行状态下,创建协程即启动hook
                this.changeCoroutineHookState();
                return co;
            end
        else
            this.printToConsole("restart coroutine");
            this.changeCoroutineHookState();
        end

而3.3.1是在 LuaPanda.lua 被加载时就进行了协程的hook(其实就是重写了协程的创建函数,在创建的协程上加个钩子) image


这里我的建议是在 this.debug_hook 函数末尾打印下 getinfo 获取到的信息,看是否符合预期。因为Formated数据其实是从这里取的。 我最近也会再测试下协程的hook

BigMadDonkey commented 6 months ago

我看了下代码 首先 GetInfo 和 Normalized 都是来源于 debug.getInfo 的返回,所以这个数据是实时取到的。

image

lastRunFunction的赋值是在 function this.real_hook_process(info) 这个函数中,它会记录当前用户函数的 getInfo 信息以及 event(call/line/return) 状态

image

Formated 数据来源于 lastRunFunction["source"],这个数据是最后一次进入 debug.sethook 设置的钩子函数时获取到的. 理论上来讲,这两个数据应该是一致的,预期的结果如下

GetInfo:    @c:/Users/xxx/Desktop/luaTest2/ae.lua
Normalized: c:/users/xxx/desktop/luatest2/ae.lua
Formated:   ae.lua

我另外对比了下 3.2.0和3.3.1的 LuaPanda.lua 代码,处理协程的部分确实有所修改 3.2.0是在调试器启动连接后对协程进行了hook

        --协程调试
        if coroutineCreate == nil and type(coroutine.create) == "function" then
            this.printToConsole("change coroutine.create");
            coroutineCreate = coroutine.create;
            coroutine.create = function(...)
                local co =  coroutineCreate(...)
                table.insert(coroutinePool,  co);
                --运行状态下,创建协程即启动hook
                this.changeCoroutineHookState();
                return co;
            end
        else
            this.printToConsole("restart coroutine");
            this.changeCoroutineHookState();
        end

而3.3.1是在 LuaPanda.lua 被加载时就进行了协程的hook(其实就是重写了协程的创建函数,在创建的协程上加个钩子) image


这里我的建议是在 this.debug_hook 函数末尾打印下 getinfo 获取到的信息,看是否符合预期。因为Formated数据其实是从这里取的。 我最近也会再测试下协程的hook

感谢回复,下周我会去公司环境下试验一下的 到时候更新一下状态

BigMadDonkey commented 6 months ago

@stuartwang 问题似乎解决了,还真是coroutine hook没挂上的问题,是我们内部框架的改动,导致连接调试器的位置之前有些地方创建了可复用的协程(用完之后会被缓存在pool中),后面再用到协程时用的是这些在没挂上hook时创建的可复用协程,导致hook不上。保证debugger最先require就好了。非常感谢!

不过,我尝试在硬断点的位置查看lastRunFunction["source"]似乎复现不出formatted和getInfo不同的情况了,目前不知道这种情况的具体诱因。

另外我还有个疑问:debugger源码中设置coroutine hook的位置会判断是否有hookLib,为什么hookLib存在时就不用coroutine hook了呢?如果hookLib.lua_set_hookstate就可以对所有的协程都hook,那么这个由于“调试器连接晚于部分可复用协程创建”导致的无法hook的问题,应该只会发生在useCHook:false(不使用CHook)的情况下,不会影响使用CHook时的调试,但是我使用旧的写法在创建可复用协程后再连接调试器,即使设置了useCHook:true还是没有停在断点上。

stuartwang commented 6 months ago

不过,我尝试在硬断点的位置查看lastRunFunction["source"]似乎复现不出formatted和getInfo不同的情况了,目前不知道这种情况的具体诱因。

按照最初的设计想法,formatted 就是规范化后的路径,只是 lastRunFunction["source"] 恰好有这个数据,就拿过来用了,理论上二者应该是能匹配上的。我觉得之前出现二者不匹配的问题还是和协程有关,调试器关于协程的处理可能测试还不够充分,导致以某些特定场景下的异常。

另外我还有个疑问:debugger源码中设置coroutine hook的位置会判断是否有hookLib,为什么hookLib存在时就不用coroutine hook了呢?

指的是如下代码中的 if hookLib == nil then 判断对吧。我的理解是,lua 层的 debug.sethook ([thread,] hook, mask [, count]) 接口只针对特定的协程设置钩子函数,每个协程都要单独设置。而luac接口 void lua_sethook (lua_State *L, lua_Hook f, int mask, int count); 是可以进程中包括协程函数一起设置钩子。所以 coroutineCreate 时如果使用的是 hookLib ,就无须对每个协程单独设置了。(这里时间比较久了,我记得是这样的,当然也可以写Demo验证如上规则。如果有错误欢迎指出)

function this.replaceCoroutineFuncs()
    if hookLib == nil then
        if coroutineCreate == nil and type(coroutine.create) == "function" then
            this.printToConsole("change coroutine.create");
            coroutineCreate = coroutine.create;
            coroutine.create = function(...)
                local co =  coroutineCreate(...)
                table.insert(coroutinePool,  co);
                --运行状态下,创建协程即启动hook
                this.changeCoroutineHookState(co, currentHookState);
                return co;
            end
        end
    end
end

如果hookLib.lua_set_hookstate就可以对所有的协程都hook,那么这个由于“调试器连接晚于部分可复用协程创建”导致的无法hook的问题,应该只会发生在useCHook:false(不使用CHook)的情况下,不会影响使用CHook时的调试,但是我使用旧的写法在创建可复用协程后再连接调试器,即使设置了useCHook:true还是没有停在断点上。

这里我们的理解是一致的,hookLib.lua_set_hookstate 是调用了luac接口进行hook的,理论上可以hook包括协程在内的所有函数。如果没有停止,有以下可能性

  1. 因为某些原因chook并没有实际加载上,仍然用的是luahook (BP() 中使用LuaPanda.getInfo()可以看到实际是否加载)
  2. lua 5.4.6 版本中发现使用 lua_getinfo(L, "Slf", ar) 取到的函数起止行号有错。调试器会根据起止行号做性能优化,这个错误导致无法正确识别有断点的函数,导致无法停止。 #167
  3. 其他可能性包括chook下路径处理异常。我觉得这种可能性不大,为了统一路径处理,c也是把路径交给lua层处理的,为例避免频繁调用还做了缓存。

目前想到的就是这些,有问题欢迎交流

BigMadDonkey commented 2 months ago

很抱歉隔了这么久才回复...因为当时暂时解决了调试的问题,又短时间内看不出协程调试的端倪,所以就先搁置去忙其他工作了。最近又发现有时协程中断点不生效(时有时无,通常在VSCode侧,已被执行过的代码行尝试增删断点几次会修复这个问题),所以又研究了一下,发现协程hook可能还是有些问题。

之前说问题是由于我这边使用了可复用协程,协程pool的创建先于require LuaPanda导致有一部分pool的协程没有执行被修改的coroutine.create进而没挂上hook,但是修改完之后发现还是有时会有上述问题。我分析了一下情况(useCHook:true, attachMode):

首先Unity进入PlayMode,require LuaPanda并开始监听(并override coroutine.create),然后框架创建可复用协程,由于此时没有连接调试器,所以hookLib == nil,因此这时创建的协程都加了lua侧的debug.setHook,并且currentHookState是disconnect。

后面运行过程中连接调试器,调用changeHookState,但此时hookLib已经获取,因此后续给协程修改hookState时不走lua侧的逻辑改为走cHook,这导致已有的可复用协程同时被加了lua侧的hook和CHook,我猜测这可能是导致异常状况的原因。 后续再从VScode添加/删除断点,有时就能修复,这个我还不太清楚是什么原因。

不过,如果先在VSCode侧运行调试,然后Unity进PlayMode,这样创建协程时hookLib已经是存在的,就不会有这种情况了,我自测这样做协程中都是能正常断点断到的。

先打开调试再进PlayMode时,还发现了一个比较奇怪的地方

image

按我理解这里不应该有最底下一行log的,但是似乎coroutine.create还是被修改了。

image

change coroutine.create的log却又没有。这一点我也很疑惑,暂时没看出来怎么回事

但是有一点是可以确定的,那就是hookLib是有可能先为nil后不为nil的,这就可能导致对同一个协程hook的操作方式先后有不同。我个人觉得是否使用hookLib,亦即是否useCHook,至少应该是每一次Unity PlayMode都固定的,最好在开始确定好,然后在整个Unity play过程中不修改吧?感觉放到launch.json里配置未必是好做法,应该很少有这种需要动态切换hook方式的使用场景吧。

如果用attachMode去调试一个在连接调试器前打开的协程,这个协程就可能先以lua方式hook,再被CHook。