最近在写一个 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 的情况,不处理堆栈的情况。终于知道原因了,原来几年前就遇到过。
hanxi/lua-seri#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