1. 调试lua源码带来的麻烦
在之前的三篇文章中写了lua中最基本的数据类型和数据结构的实现,lua数据类型的实现、lua中string的实现、lua Table的实现。这些基础数据结构在lua源码中大量的使用,给调试带来了一定的困扰。
如TValue的实现,把一切数据类型集合在一起。如果我使用gdb去打印这个值我需要做的是,查看TValue的类型 tt_ 查看具体类型到底是int、float、还是string等数据类型,以此我们需要写一些脚本给我们的调试带来便利。
2. gdb的脚本编写
gdb脚本中可以使用gdb的所有指令,关于常用的指令可以参考gdb 调试c
gdb 默认加载脚本 ~/.gdbinit
其他要加载脚本可以 在~/.gdbinit里source其他脚本 或者 使用 gdb -x
#变量设置
set $val = 0
#函数定义
#define fun_test
语句
end
#函数参数 $arg0 $arg1..
#分支 (仅仅支持这种 没有elif else if)
if 条件
else
end
#循环
while 条件
end
gdb 还有一个比较牛逼的特性就是在gdb中可以调用c函数。
写一个demo编译
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <elf.h>
char *get_aaa()
{
return "aaa";
}
int main(int argc,char *argv[])
{
return 0;
}
gdb test
b main
r
p get_aaa()
可以看到成功打印了 ‘aaa’。我们可以使用p 去打印函数的返回值,一次如果想要看到函数的结果可以让函数返回 char *。之后我们会写这样的函数去方便调试。
这里推荐大家使用一款gdb脚本叫gef,下载完直接 ‘source 文件路径’就行了
3. TValue输出脚本
让我们看看TValue的定义
union GCUnion {
GCObject gc; /* common header */
struct TString ts;
struct Udata u;
union Closure cl;
struct Table h;
struct Proto p;
struct lua_State th; /* thread */
struct UpVal upv;
};
typedef union Value {
struct GCObject *gc; /* collectable objects */
void *p; /* light userdata */
lua_CFunction f; /* light C functions */
lua_Integer i; /* integer numbers */
lua_Number n; /* float numbers */
} Value;
#define TValuefields Value value_; lu_byte tt_
typedef struct TValue {
TValuefields;
} TValue;
好了看完是不是脑壳痛。TValue类型可以是lua支持的所以类型可以是int、float、string、table、指针等具体什么类型需要去看‘tt_’这个成员的值。如果我们调试遇到这个类型会怎么输出?查找tt_的值看什么什么类型然后去输出。每次这样下来异常的麻烦。那有没有好办法呢?
我们看一下lua脚本中输出的支持
a = 1
print(a)
a = 1.0
print(a)
a = "aaaa"
print(a)
a = {[2]="a"}
print(a)
输出:
1
1.0
aaaa
table: 0x56372ab1ff20
lua的print函数支持TValue的基本输出。以此我们抄print函数的实现即可
找到lbaselib.c文件,查找base_funcs变量
static const luaL_Reg base_funcs[] = {
{"assert", luaB_assert},
{"collectgarbage", luaB_collectgarbage},
{"dofile", luaB_dofile},
{"error", luaB_error},
{"getmetatable", luaB_getmetatable},
{"ipairs", luaB_ipairs},
{"loadfile", luaB_loadfile},
{"load", luaB_load},
{"next", luaB_next},
{"pairs", luaB_pairs},
{"pcall", luaB_pcall},
{"print", luaB_print},
{"warn", luaB_warn},
{"rawequal", luaB_rawequal},
{"rawlen", luaB_rawlen},
{"rawget", luaB_rawget},
{"rawset", luaB_rawset},
{"select", luaB_select},
{"setmetatable", luaB_setmetatable},
{"tonumber", luaB_tonumber},
{"tostring", luaB_tostring},
{"type", luaB_type},
{"xpcall", luaB_xpcall},
/* placeholders */
{LUA_GNAME, NULL},
{"_VERSION", NULL},
{NULL, NULL}
};
可以看到这些都是lua支持的全局函数和全局符号,我们需要‘print’对应的函数
static int luaB_print (lua_State *L) {
int n = lua_gettop(L); /* number of arguments */
int i;
for (i = 1; i <= n; i++) { /* for each argument */
size_t l;
const char *s = luaL_tolstring(L, i, &l); /* convert it to string */
if (i > 1) /* not the first element? */
lua_writestring("\t", 1); /* add a tab before it */
lua_writestring(s, l); /* print it */
lua_pop(L, 1); /* pop result */
}
lua_writeline();
return 0;
}
可以看到print函数的实现,这个函数的实现是多参数的。每次都是调用luaL_tolstring去转换,因此我们只要调用这个函数就行了
static int dbg_print (lua_State *L) {
size_t l;
dbg_tolstring(L, 1, &l);
return 1;
}
把luaL_tolstring改成了dbg_tolstring,这个函数很简单,但是这个函数是用来直接调用的,我们需要一函数返回char *并且用这个函数传递参数。
const char *print_value(lua_State *L,const TValue *o)
{
setfvalue(s2v(L->top), dbg_print);
api_incr_top(L);
setobj(L,s2v(L->top), o); //传参
api_incr_top(L);
lua_pcall(L,1,1,0); //调用dbg_print
const char *s = lua_tostring(L,-1); //接受返回值
lua_pop(L,1);
return s;
}
至于为什么要把luaL_tolstring改成了dbg_tolstring真是因为luaL_tolstring带来gc操作,回去释放内存。由于我踩过坑所以才改的.
//去gc操作
LUA_API const char *dbg_pushfstring (lua_State *L, const char *fmt, ...) {
const char *ret;
va_list argp;
lua_lock(L);
va_start(argp, fmt);
ret = luaO_pushvfstring(L, fmt, argp);
va_end(argp);
//luaC_checkGC(L);
lua_unlock(L);
return ret;
}
//去gc操作
LUA_API const char *dbg_pushstring (lua_State *L, const char *s) {
lua_lock(L);
if (s == NULL)
setnilvalue(s2v(L->top));
else {
TString *ts;
ts = luaS_new(L, s);
setsvalue2s(L, L->top, ts);
s = getstr(ts); /* internal copy's address */
}
api_incr_top(L);
//luaC_checkGC(L);
lua_unlock(L);
return s;
}
//去gc操作
LUALIB_API const char *dbg_tolstring (lua_State *L, int idx, size_t *len) {
if (luaL_callmeta(L, idx, "__tostring")) { /* metafield? */
if (!lua_isstring(L, -1))
luaL_error(L, "'__tostring' must return a string");
}
else {
switch (lua_type(L, idx)) {
case LUA_TNUMBER: {
if (lua_isinteger(L, idx))
dbg_pushfstring(L, "%I", (LUAI_UACINT)lua_tointeger(L, idx));
else
dbg_pushfstring(L, "%f", (LUAI_UACNUMBER)lua_tonumber(L, idx));
break;
}
case LUA_TSTRING:
lua_pushvalue(L, idx);
break;
case LUA_TBOOLEAN:
dbg_pushstring(L, (lua_toboolean(L, idx) ? "true" : "false"));
break;
case LUA_TNIL:
lua_pushliteral(L, "nil");
break;
default: {
int tt = luaL_getmetafield(L, idx, "__name"); /* try name */
const char *kind = (tt == LUA_TSTRING) ? lua_tostring(L, -1) :
luaL_typename(L, idx);
dbg_pushfstring(L, "%s: %p", kind, lua_topointer(L, idx));
if (tt != LUA_TNIL)
lua_remove(L, -2); /* remove '__name' */
break;
}
}
}
return lua_tolstring(L, -1, len);
}
把这三个函数的gc操作去调就行了
最后在gdb脚本中添加
define plua_Tval
p print_value($arg0,$arg1)
end
牛刀小试一下
我们断在luaV_execute (虚拟机执行函数)
b luaV_execute
4. Table输出脚本
Table的的输出相对简单一点,我们直接用luaH_next这个函数去遍历就行
const char *print_table(lua_State *L,Table *t)
{
static char buf[BUFFSIZE] = {0};
memset(buf,0,sizeof(buf));
int len = 0;
StackValue tem[2] = {0};
setnilvalue(s2v(tem));
while(luaH_next(L,t,tem)) {
len += snprintf(buf + len,BUFFSIZE - len,"%s",print_value(L,s2v(tem)));
len += snprintf(buf + len,BUFFSIZE - len,"\t");
len += snprintf(buf + len,BUFFSIZE - len,"%s",print_value(L,s2v(tem + 1)));
len += snprintf(buf + len,BUFFSIZE - len,"\n");
if (len >= BUFFSIZE) return buf;
}
return buf;
}
gdb脚本编写
define plua_Ttable
printf "%s",print_table($arg0,hvalue($arg1))
end
实验:
我们依然断在luaV_execute
b luaV_execute
5. string输出脚本
define plua_str
p (char *)(((TString *)$arg0)->contents)
end
这个相对简单一点
6. 字节码输出脚本
这个是重头戏,输出字节码有利于我们调试lua解析的整个阶段。
我们在使用 'luac -l -l test.lua’的时候可以看到字节码的输出。因此我们只要去找luac.c这个文件中字节码输出部分即可
static void PrintCode(const Proto* f) {
const Instruction* code=f->code;
int pc,n=f->sizecode;
for (pc=0; pc<n; pc++)
{
Instruction i=code[pc];
OpCode o=GET_OPCODE(i);
int a=GETARG_A(i);
int b=GETARG_B(i);
int c=GETARG_C(i);
int ax=GETARG_Ax(i);
int bx=GETARG_Bx(i);
int sb=GETARG_sB(i);
int sc=GETARG_sC(i);
int sbx=GETARG_sBx(i);
int isk=GETARG_k(i);
int line=luaG_getfuncline(f,pc);
printf("\t%d\t",pc+1);
if (line>0) printf("[%d]\t",line); else printf("[-]\t");
printf("%-9s\t",opnames[o]);
switch (o)
{
case OP_MOVE:
printf("%d %d",a,b);
break;
case OP_LOADI:
printf("%d %d",a,sbx);
break;
case OP_LOADF:
printf("%d %d",a,sbx);
break;
case OP_LOADK:
printf("%d %d",a,bx);
printf(COMMENT); PrintConstant(f,bx);
break;
...
}
可以看到PrintCode的实现是一个很大的switch-case。我们需要改的就是吧返回值改成char *。因此我们只要把printf改成sprintf就行。把结果输出在一个缓冲区里
const char *PrintCode(lua_State* L,const Proto* f)
{
int len = 0;
tmname=G(L)->tmname;
static char buf[BUFFSIZE] = {0};
memset(buf,0,BUFFSIZE);
const Instruction* code=f->code;
int pc,n=f->sizecode;
for (pc=0; pc<n; pc++)
{
Instruction i=code[pc];
OpCode o=GET_OPCODE(i);
int a=GETARG_A(i);
int b=GETARG_B(i);
int c=GETARG_C(i);
int ax=GETARG_Ax(i);
int bx=GETARG_Bx(i);
int sb=GETARG_sB(i);
int sc=GETARG_sC(i);
int sbx=GETARG_sBx(i);
int isk=GETARG_k(i);
int line=luaG_getfuncline(f,pc);
len += snprintf(buf + len,BUFFSIZE-len,"\t%d\t",pc+1);
if (line>0) len += snprintf(buf + len,BUFFSIZE-len,"[%d]\t",line); else len += snprintf(buf + len,BUFFSIZE-len,"[-]\t");
len += snprintf(buf + len,BUFFSIZE-len,"%-9s\t",opnames[o]);
switch (o)
{
case OP_MOVE:
len += snprintf(buf + len,BUFFSIZE-len,"%d %d",a,b);
break;
case OP_LOADI:
len += snprintf(buf + len,BUFFSIZE-len,"%d %d",a,sbx);
break;
case OP_LOADF:
len += snprintf(buf + len,BUFFSIZE-len,"%d %d",a,sbx);
break;
case OP_LOADK:
len += snprintf(buf + len,BUFFSIZE-len,"%d %d",a,bx);
len += snprintf(buf + len,BUFFSIZE-len,COMMENT); len += snprintf(buf + len,BUFFSIZE-len,"%s",PrintConstant(f,bx));
break;
...
脚本编写
define plua_code
printf "%s",PrintCode($arg0,$arg1)
end
//解析的时候常用的LexState结构
define plua_lscode
plua_code $arg0->L $arg0->fs.f
end
相应的把PrintConstant和PrintString函数也改成这个形式即可。
牛刀小试
现在我们断在lua解析的地方
b statlist
具体的实现参考
github
源码版本为lua5.4.3