Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Open
BigMadDonkey opened this issue May 14, 2024 · 5 comments
Open

Comments

@BigMadDonkey
Copy link

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):

  • OS: macOS
  • VSCode v1.89.1
  • LuaPanda Version 3.2.0 and 3.3.1
  • Framework xlua

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

@stuartwang
Copy link
Collaborator

我看了下代码
首先 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
Copy link
Author

我看了下代码
首先 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
Copy link
Author

@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
Copy link
Collaborator

stuartwang commented May 21, 2024

不过,我尝试在硬断点的位置查看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) 取到的函数起止行号有错。调试器会根据起止行号做性能优化,这个错误导致无法正确识别有断点的函数,导致无法停止。 关于在5.4下使用luapanda #167
  3. 其他可能性包括chook下路径处理异常。我觉得这种可能性不大,为了统一路径处理,c也是把路径交给lua层处理的,为例避免频繁调用还做了缓存。

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

@BigMadDonkey
Copy link
Author

很抱歉隔了这么久才回复...因为当时暂时解决了调试的问题,又短时间内看不出协程调试的端倪,所以就先搁置去忙其他工作了。最近又发现有时协程中断点不生效(时有时无,通常在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。

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants