这段时间在qnode项目中新增了一个叫ldb的子项目,它的作用是使用C语言实现了一个lua调试器,后面将会在qnode中嵌入对调试lua脚本的支持。
先来简单提一下ldb的用法,在ldb目录的子目录test中,有一个main.c文件,其中使用ldb库提供的API实现对lua脚本的调试演示:
#include <stdio h="">
#include "ldb.h"
ldb_t *ldb;
static int
c_break(lua_State *state) {
ldb_step_in(state, 1);
return 0;
}
int main() {
int i;
const char *file = "my.lua";
lua_State *L = lua_open();
luaL_openlibs(L);
lua_register(L, "c_break", c_break);
ldb = ldb_new(L);
luaL_dofile(L, file);
ldb_destroy(ldb);
return 0;
}
</stdio>
在这里,ldb提供了创建和销毁ldb库的API,分别是ldb_new和ldb_destroy。另外提供了一个API ldb_step_in,main文件中通过向lua层提供一个C api c_debug,在该函数中调用这个API,实现对lua的调试。
main函数将读取my.lua文件来执行,它的内容如下:
function test()
local ab = 2034
print("test")
end
local t = {out=10}
local a = 1014
b = 2024
print("before debug")
c_break()
test()
print("after debug")
来看看ldb库当前实现的调试功能有哪些:
(ldb) h
Lua debugger written by Lichuang(2013)
cmd:
help(h) : print help info
print(p) <varname> : print var value
backtrace(bt) : print backtrace info
list(l) : list file source
step(s) : one instruction exactly
next(n) :next line
break(b) [function|filename:line] : break at function or line in a file
disable(dis) breakpoint : disable a breakpoint
enable(en) breakpoint : enable a breakpoint
delete(del) breakpoint : delete a breakpoint
info(i) :show all break info
continue(c) : continue execute when hit a break point
</varname>
括号中的都是该命令的缩写。当前支持添加/禁止断点,打印变量,查看文件内容,查看当前调用栈,step in模式支持逐行执行的调试,以及next模式会跳过函数执行等最基本的调试命令。
介绍完简单的使用和功能,下面来介绍一下lua中为支持调试功能提供了哪些API,以及通过这些API如何实现一个lua调试器。
1)lua提供的hook功能
lua为了支持调试,提供了hook功能,使用者可以根据需要添加不同的hook处理函数供条件触发时回调,包括以下几种:
具体包括以下几种hook类型:
#define LUA_HOOKCALL 0
#define LUA_HOOKRET 1
#define LUA_HOOKLINE 2
#define LUA_HOOKCOUNT 3
#define LUA_HOOKTAILRET 4
这里用的最多的是LUA_HOOKCALL,LUA_HOOKLINE,如果注册了这两个类型的HOOK函数,则会分别在调用某函数和执行每一行代码之后调用注册的HOOK函数。
使用lua中提供的API,可以如下方式注册HOOK函数:
int mask;
mask = lua_gethookmask(state);
if (enable) {
lua_sethook(state, all_hook, mask | LUA_MASKLINE, 0);
} else {
lua_sethook(state, all_hook, mask & ~LUA_MASKLINE, 0);
}
有了HOOK函数,就可以在lua代码每次执行的时候做一些动作了。
除了HOOK函数之外,lua自身还提供了lua_Debug结构体,这个结构体包括以下成员:
source 函数的定义位置。如果函数在字符串内被定义(通过loadstring 函数),source就 是该字符串,如果函数在文件中被定义,source就是带“@”前缀的文件名。 short_src source的简短版本(60个字符以内),对错误信息很有用。 linedefined source中函数被定义处的行号。 what 函数类型。如果foo是普通的Lua函数,结果为“Lua”;如果是C函数,结果为“C”; 如果是Lua的主代码段,结果为“main”。 name 函数的名称。 namewhat name域的含义。可能的取值为:“global”、“local”、“method”、“field”,或者空 字符串。空字符串意味着Lua无法找到这个函数名。 nups 函数中的Upvalues的个数。 func 函数本身。稍后介绍。
可以通过lua_getinfo得到一些很重要的信息,它的调用方式是:
lua_getinfo(state, params, ar),第二个参数是一个字符串,支持传入多个字母,每个字母有不同的含义:
'n': name,namewhat
'f': func
'S': source,short_src,what,linedefined
'l': currentline
'u': nup
2) 如何打印变量?
变量分为局部和全局变量,因此搜索某个变量的时候是从内到外的方式搜索,搜索局部变量时,使用lua提供的API lua_getlocal函数,逐个搜索,具体可以看ldb.c中的search_local_var函数。
如果在局部变量中搜索不到,则还得使用lua_getglobal函数进行全局变量的搜索,具体见ldb.c中的search_global_var函数。
但是以上的过程仅仅还只能在相应的地方查找到同名的变量,在真正需要打印变量值的时候,还需要根据变量的类型具体来打印数据,代码太多,不在这里列出,见ldb.c中的print_var函数。
3)如何查看文件的内容
查看文件的内容相对简单,因为当lua被HOOK住的时候,可以通过lua_getinfo函数得到当前lua的一些信息,比如lua文件名,行号,在C实现的Lua调试器中,会维护一个已经读取过的文件列表,如果当前所在的文件还没有被读取到内存中,那么会读取到内存中,再根据所在的行号就可以得到文件内容的信息了。
4)断点的添加
调试器的断点分为两种,一种是基于文件:行号形式的,一种则是基于函数调用形式的。
C实现的调试器中,首先需要定义一个数据结构类型,用于表示断点:
typedef struct ldb_breakpoint_t {
unsigned int available:1;
char *file;
char *func;
const char *type;
int line;
unsigned int active:1;
int index;
int hit;
} ldb_breakpoint_t;
这些信息包括断点所在的文件,行号,函数名,当前是否被激活,被触发的计数,等等。
先来看第一种形式断点的实现。这种形式的断点相对简单。做法是创建一个新的断点数据结构,保存下文件和行号,在每次HOOK函数中都去根据当前的文件和行号信息去查找是否匹配了某个断点的信息,当然,当前的实现中这个查找是线性的,可能对性能有一定影响。
来看第二种形式断点的实现。由于函数在lua中也是一种类型的变量,既然是变量那么就涉及到作用域。比如你在A模块中断点时,想给B模块的fun函数下断点,那么就不能简单的写”b func”,而应该是”b B.func”。所以在实现对函数进行断点的时候要注意这一点。另外,当给某一个函数下断点时,还需要添加LUA_HOOKCALL类型的HOOK,也就是在函数调用时被触发。之所以这么做,是因为在查找断点时,当首先使用文件名和行号都查找不到时,会判断一下当前这次的HOOK调用,是不是一个函数调用触发的HOOK,如果是的话再继续根据断点的函数名进行查找匹配。
5)如何查看当前堆栈信息
也就是模拟gdb中的bt指令的功能。lua对这个已经有支持了,可以使用lua_getstack函数,具体见ldb.c中的dump_stack函数。
6)step和next指令的实现
首先来看一个子问题,如何得到当前的函数堆栈数量,或者说当前的调用层次,通过反复调用前面提到的lua_getstack函数可以获取到:
static int
get_calldepth(lua_State *state) {
int i;
lua_Debug ar;
for (i = 0; lua_getstack(state, i + 1, &ar ) != 0; i++)
;
return i;
}
这两个指令的实现稍微有点难度,所以放在最后一个讲解。step就是逐行执行代码,即使调用函数的时候也会跟进该函数中,而next指令会在调用函数的时候不跟进函数的调用。所以这两者的区别在于调用时的函数堆栈,因此这两个指令的区别仅在于遇到函数的时候是不是继续跟进去。所以我的做法是新增一个变量用于保存当前的函数栈索引,当step模式时将这个值置为-1,next模式时只会保存为当前的函数栈索引,如果某个指令是调用一个函数时,这时通过get_calldepth函数获得的函数栈就会比之前的大,这样就可以知道当前的这个指令是不是调用一个函数了,next指令可以在这个时候返回不做任何处理,而step指令可以继续执行下去。
以上就是简单的原理性介绍,如果想真正了解lua调试器的实现,还是具体看看代码,自己跑一下测试demo吧。