hanxi / blog

涵曦的博客
https://blog.hanxi.cc
56 stars 5 forks source link

调试 Lua 的 C 库报错记录 #88

Open hanxi opened 1 year ago

hanxi commented 1 year ago

最近在写一个 Lua 库,用于打印报错堆栈上的函数参数和 upvalue,最开始用 Lua 实现了一个版本,运行效果还可以。想着用 C 来实现一遍,经过一段时间的奋战,终于能跑了。

运行效果展示

测试代码:

local tb = require "traceback.c"
debug.traceback = tb.traceback

local a = "test upvalue a"

local function f(a, b, c, d, e)
    print("in f")
    local x = a
    error("my test error")
end

local function f1(a, b, c, d, e)
    print("in f1")
    f(a,b,c,d,e)
end

local c = {2,2.22}
local a = {
    b = {"x","y"},
    c = c,
}
a.b.xx = c
a.c.yy = a.b
a.c.zz = a.b.xx

local ok, msg = xpcall(f1, debug.traceback, a, "xs", 1.22, 100)
print(msg)

运行效果大概是这样的:

test.lua:9: my test error
stack traceback:
        [C]: in function 'error'
        test.lua:9: in upvalue 'f'
                <arg 1> a = {['c']={[1]=2,[2]=2.22,,['yy']={...}},}
                        a['c']['zz'] = a['c']
                        a['b'] = a['c']['yy']
                <arg 2> b = "xs"
                <arg 3> c = 1.22
                <arg 4> d = 100
                <arg 5> e = nil
                <upv 1> _ENV = {['os']={['difftime']=function: 0x560853d17e20,['date']=function: 0x560853d18250,['setlocale']=function: 0x560853d17bf0,['clock']=function: 0x560853d17e70...},['rawlen']=function: 0x560853d11520,['tostring']=function: 0x560853d112e0,['warn']=function: 0x560853d115d0,['select']=function: 0x560853d11310...}
        test.lua:14: in function <test.lua:12>
                <arg 1> a = {['c']={[1]=2,[2]=2.22,,['yy']={...}},}
                        a['c']['zz'] = a['c']
                        a['b'] = a['c']['yy']
                <arg 2> b = "xs"
                <arg 3> c = 1.22
                <arg 4> d = 100
                <arg 5> e = nil
                <upv 1> _ENV = {['os']={['difftime']=function: 0x560853d17e20,['date']=function: 0x560853d18250,['setlocale']=function: 0x560853d17bf0,['clock']=function: 0x560853d17e70...},['rawlen']=function: 0x560853d11520,['tostring']=function: 0x560853d112e0,['warn']=function: 0x560853d115d0,['select']=function: 0x560853d11310...}
                <upv 2> f = function: 0x560853f186f0
        [C]: in function 'xpcall'
        test.lua:26: in main chunk
                <upv 1> _ENV = {['os']={['difftime']=function: 0x560853d17e20,['date']=function: 0x560853d18250,['setlocale']=function: 0x560853d17bf0,['clock']=function: 0x560853d17e70...},['rawlen']=function: 0x560853d11520,['tostring']=function: 0x560853d112e0,['warn']=function: 0x560853d115d0,['select']=function: 0x560853d11310...}
        [C]: in ?

遇到问题

但是会偶然的出现 core dumped 。由于是偶现的, gdb 好像也不好调试,就只能加打印,批量重试了。 写个脚本批量运行,把 core 文件和日志文件对应起来。

while true; do
    ../lua-5.4.4/src/lua test.lua > tmp.log 2>&1 &
    pid=$!
    wait $!
    err=$?
    if [ $err -ne 0 ]; then
        mv tmp.log log.$pid
        exit 1
    fi
    sleep 2
done

遇到报错就停下来。 然后也借助 gdb 来查看 core 文件,发现 gdb 竟然有个 TUI 界面。使用 tui enable 开启 TUI 界面,界面是会自动显示当前代码文件的,按方向键上下滚动代码。 ![[Pasted image 20230211143947.png]] 使用 up/down 切换堆栈层级,代码会跟着切换。

排除法定位问题

怀疑是 lua_Buffer 的问题,注释部分代码试试看。怀疑 luaL_tolstring 函数会影响,先修改不调用这个函数。

怀疑 lua_Buffer 的问题,可以把 lua_Buffer 注释掉,只打印序列化出的数据,或者用其他方法来存储序列化后的数据。怀疑 lua_Buffer 的原因是调试的时候发现 lua_pushvalue 经常会把 buffer 的数据弄成乱码,可能是 _ENV 里有特殊字符。

通过注释 lua_Buffer 相关代码,目前跑了一天还没遇到过报错。

或许是不是不能同时使用 2 个 buffer 呢?可以研究一下 lua_Buffer 的代码。

通过排除法,已经定位到是 Buffer 的问题,具体是加了什么特殊字符问题引起的还没定位到。特殊字符可能是 _ENV 里的。

再使用排除法,只保留

static void
seri_other(lua_State *L, int index, luaL_Buffer *b) {
    size_t len;
    const char *str = luaL_tolstring(L, index, &len);
    fprintf(stderr, "seri_other:%s\n", str);
    luaL_addvalue(b);
}

// 另外保留一些插入明显是字符或者字符串的
luaL_addstring(b, "...");
luaL_addchar(b, '"');

已经重现到。

再试试只序列化 ENV 的情况,不处理堆栈的情况。终于知道原因了,原来几年前就遇到过。 https://github.com/hanxi/lua-seri/issues/1

During its normal operation, a string buffer uses a variable number of stack slots. So, while using a buffer, you cannot assume that you know where the top of the stack is. 一般的操作过程中,字符串缓存会使用不定量的栈槽。 因此,在使用缓存中,你不能假定目前栈顶在哪。 在对缓存操作的函数调用间,你都可以使用栈,只需要保证栈平衡即可; 即,在你做一次缓存操作调用时,当时的栈位置和上次调用缓存操作后的位置相同。 (对于 luaL_addvalue 是个唯一的例外。) 在调用完 luaL_pushresult 后, 栈会恢复到缓存初始化时的位置上,并在顶部压入最终的字符串。

终于理解这句话的意思了,就是每次操作 buffer 前后,需要保持栈不变。所以解决方法也就是 issues 里提出的 2 个,自己实现一个 buffer 或者把数据缓存到一个表里,最后才使用 buffer 合并。甚至包括 luaL_bufferinit ,这也就说明,必须保证 buffer 在栈顶了。因为无法使用此办法来序列化了。

还特意去 Lua 邮件列表里问了这个 buffer 的用法: http://lua-users.org/lists/lua-l/2023-02/msg00082.html

最终的解决办法是使用 luaL_buffinitsize 申请一段固定长度的 buffer, 采用 memcpy 写入数据。

最终成果: https://github.com/hanxi/ltraceback

顺便把之前写的一个序列化 lua 数据的库的 bug 修复好了。 https://github.com/hanxi/lua-seri