简介:
LUA没有自带调试器,只提供了一套调试库,可以实现符合自己需要的调试器.晚上没事,改写了一下以前的一个GDB风格的LUA调试器,可嵌入到应用程序中,在需要的时候触发并调试,有需要的朋友可以参考下. 支持如下命令:
h 帮助信息
c 继续动行
s 单步运行(不跳过函数调用)
n 单步运行(跳过函数调用)
p var 打印变量值
b src:line 添加断点,注意src要写文件的绝对路径,例如 b script/main.lua:22
d num 删除断点
bl 列出所有断点
be num 启用一个断点
bd num 禁用一个断点
bt 打印调用栈
实现:
LUA支持用debug.sethook设置三种Hook:
一,每行代码执行时调用Hook函数
二,每个函数调用时执行Hook函数
三,每个函数返回时调用Hook函数
在Hook函数中,可以通过调用debug.getinfo得到当前LUA文件名,当前所执行代码的行号.所以,要实现针对行下断点,一个显尔易见的办法 就是在每个HOOK中判断当前文件和当前行号是否是一个断点,是的话就中断下来(调用io.read()来等待输入).这样当然会比较慢,因为每行代码执 行时都会去调用HOOK函数,但LUA一般是用来做逻辑而不是算术密集的操作,而且一般只是在开发期调试会使用hook,所以绝大部分情况下是足够用了. 特殊情况下有效率要求时,可以使用下文介绍的另一种办法.
但在实际开发中碰到了个问题:如何实现n命令(单步运行,跳过函数调用),LUA的HOOK是每行代码都会触发,可以取得当前函数,所以单步运行时很容易 知道是不是进入了一个新函数,但是如何在进入这个函数时不中断,在调用完成时才中断?最直观的想法是在n命令碰到函数时,自动在函数调用的下一行加入断点 并运行.但是实际情况要复杂得多,比如函数调用下一行是空行,函数调用当前行有return,比如:
if ( foo() ) return end
这样就要写很多代码分析的代码来保证正确性,逻辑就写复杂了,放弃这种做法.
另一种想法是下函数执行hook和函数返回hook,执行hook触发时增量某个变量,返回hook扫许时减量某个变量,这样变量为0时就是返回到调用函数了(调用栈平衡),但是程序逻辑也变得复杂了,放弃这种做法.
最直观的办法是检查调用栈深度,因为被调用函数返回时,调用栈深度总是不会变的.但是LUA debug库没有检查调用栈深度的函数,还好LUA是开源的,动手加一个即可.最后的代码见下面,如有更简单的办法请告之:
在lua(5.1.4)源代码文件 ldblib.c中添加返回调用栈深度的函数:
static int db_traceback_count (lua_State *L) {
lua_Debug ar;
int index = 1;
while (lua_getstack(L, index, &ar))
index++;
lua_pushnumber( L, index - 1 );
return 1;
}
在static const luaL_Reg dblib[] 数组中添加一行函数注册:
{"traceback_count", db_traceback_count},
重新编译LUA.
为了像GDB一样在单步调试时能显示当前源代码,在C/C++层注册一个读取指定文件指定行的函数:
int get_file_line( lua_State *L )
{
const char *file = luaL_checkstring( L, 1 );
int line = lua_tonumber( L, 2 );
if ( line < 1 ) line = 1;
int n = 1;
char src[2046];
FILE *f = fopen( file, "r" );
if ( f )
{
while ( fgets( src, 2046, f ) )
{
if ( n == line )
{
int last = strlen(src) - 1;
if ( src[last] == '/n' )
src[last] = '/0';
lua_pushstring( L, src );
fclose( f );
return 1;
}
n++;
}
lua_pushnil( L );
fclose( f );
return 1;
}
else
{
lua_pushnil( L );
return 1;
}
return 0;
}
在合适的地方注册即可,
lua_pushcfunction( L, get_file_line );
lua_setglobal( L, "get_file_line" );
运行时加载debug.lua(在后面给出)就可以了
使用:
lua本身要加载debug库
可以注册一个信号处理函数(linux)或热键处理函数(win32),在处理函数中调用
lua_getglobal( L, "begin_debug" );
int error = lua_pcall( L, 0, 0, 0 );
if ( expr )
{
printf( "%s/n", lua_tostring( L, -1 ) );
lua_pop( L, 1 );
}
如果是Win32 gui界面的程序,可以用AllocConsole函数分配一个控制台,在控制台中调试.
下面是debug.lua,还有很多可以优化和改进的地方,有需要的朋友可以自己修改
_DEBUG_FILE需要改成debug.lua在程序运行的相对路径
debug.lua
_DEBUG_FILE = "script/debug.lua"
debug.bps = {
max = 0,
trace = false,
last_cmd = "",
next = false,
cur_func = nil,
trace_count = 0,
var_tbl = nil,
}
function debug_log( log_str )
print( "(ldb) " .. log_str )
end
function debug_print_var( name, value, level )
local prefix = string.rep( " ", level )
local str = string.format( "%s%s = %s", prefix, name, tostring(value) )
if type( value ) == "table" then
if debug.var_tbl[value] then
--已在临时表中的,只打印表地址
print( str )
return
end
--加到临时表中,以免表出现循环引用时,打印也产生死循环
debug.var_tbl[value] = true
--打印表中所有数据
print( string.format( "%s%s = {", prefix, name ) )
for k, v in pairs( value ) do
--不打印 "_"开头的内部变量
if string.sub( k, 1, 1 ) ~= "_" then
debug_print_var( k, v, level + 1 )
end
end
print( prefix .. "}" )
elseif type( value ) == "string" then
print( str )
else
print( str )
end
end
function debug_print_expr( var )
--清空临时变量表
debug.var_tbl = {}
local index = 1
--找局部变量
while true do
local name, value = debug.getlocal( 4, index )
if not name then break end
index = index + 1
if name == var then
debug_print_var( var, value, 0 )
return
end
end
--找全局变量
if _G[var] ~= nil then
debug_print_var( var, _G[var], 0 )
return
end
debug_log( var .. " is invalid" )
end
function add_breakpoint( expr )
local si = string.find( expr, ":" )
if nil == si then
debug_log( "add breakpoint error, expr (" .. expr .. ") invalid" )
return
end
local line = string.sub( expr, si + 1 )
local line = tonumber( line )
local source = string.sub( expr, 1, si - 1 )
--先查找有不有相同断点
if ( debug.bps[line] ~= nil ) and ( debug.bps[line][source] ~= nil ) then
debug_log( string.format( "breakpoint %s:%d existed", source, line ) )
return
end
local tbl = {}
tbl.source = source
tbl.line = line
tbl.active = true
tbl.number = debug.bps.max + 1
if debug.bps[line] == nil then
debug.bps[line] = {}
end
debug.bps[line][source] = tbl
debug.bps.max = debug.bps.max + 1
end
function debug_show_bp()
for k, v in pairs( debug.bps ) do
if type( v ) == "table" then
for k1, v1 in pairs( v ) do
local str = string.format( "bp num:%d %s:%d active:",
v1.number,
v1.source,
v1.line )
if v1.active then
str = str .. "enable"
else
str = str .. "disable"
end
print( str )
end
end
end
end
function debug_del_bp( expr )
local number = tonumber( expr )
for k, v in pairs( debug.bps ) do
if type( v ) == "table" then
for k1, v1 in pairs( v ) do
if v1.number == number then
debug.bps[k][k1] = nil
debug_log( "remove bp:" .. number .. " ok" )
end
end
end
end
end
function debug_enable_bp( expr )
local number = tonumber( expr )
for k, v in pairs( debug.bps ) do
if type( v ) == "table" then
for k1, v1 in pairs( v ) do
if v1.number == number then
v1.active = true
debug_log( "enable bp:" .. number )
end
end
end
end
end
function debug_disable_bp( expr )
local number = tonumber( expr )
for k, v in pairs( debug.bps ) do
if type( v ) == "table" then
for k1, v1 in pairs( v ) do
if v1.number == number then
v1.active = false
debug_log( "disable bp:" .. number )
end
end
end
end
end
function debug_help()
print( "h help info" )
print( "c continue" )
print( "s trace" )
print( "n next" )
print( "p var print variable" )
print( "b src:line add breakpoint" )
print( "d num del breakpoint" )
print( "bl list breakpoint" )
print( "be num enable breakpoint" )
print( "bd num disable breakpoint" )
print( "bt print traceback" )
end
function debug_execute_cmd( env )
io.write( "(ldb) " )
local cmd = io.read()
--取上一次的命令,方便调试
if cmd ~= "" then
debug.bps.last_cmd = cmd
else
cmd = debug.bps.last_cmd
end
local c = cmd
local expr = ""
local si = string.find( cmd, " " )
if si ~= nil then
c = string.sub( cmd, 1, si - 1 )
expr = string.sub( cmd, string.find( cmd, " %w" ) + 1 )
end
if c == "c" then
debug.bps.trace = false
return true
elseif c == "s" then
debug.bps.trace = true
return true
elseif c == "n" then
debug.bps.trace = false
debug.bps.next = true
debug.bps.cur_func = env.func
debug.bps.trace_count = debug.traceback_count() - 1
return true
elseif c == "p" then
debug_print_expr( expr )
elseif c == "b" then
add_breakpoint( expr )
elseif c == "bl" then
debug_show_bp()
elseif c == "d" then
debug_del_bp( expr )
elseif c == "be" then
debug_enable_bp( expr )
elseif c == "bd" then
debug_disable_bp( expr )
elseif c == "bt" then
print( debug.traceback("", 3) )
elseif c == "h" then
debug_help()
else
debug_log( "invalid cmd:" .. cmd )
end
return false
end
function debug_trace( event, line )
local env = debug.getinfo( 2 )
if env.short_src == _DEBUG_FILE then
return
end
--判断是否在next调试
if debug.bps.next then
local trace_count = debug.traceback_count()
--函数返回了,调用栈数量就会比现在小
if trace_count < debug.bps.trace_count then
debug.bps.next = false
debug.bps.trace = true
elseif trace_count == debug.bps.trace_count then
if debug.bps.cur_func == env.func then
debug.bps.next = false
debug.bps.trace = true
end
end
end
--判断是否有断点
if ( not debug.bps.trace ) and ( debug.bps[line] ~= nil ) then
local tbl = debug.bps[line][env.short_src]
if ( tbl ~= nil ) and tbl.active then
--如果在next时,碰到断点了,就清除单步运行状态
debug.bps.next = false
debug.bps.trace = true
debug_log( "breakpoint " .. tbl.number )
end
end
if debug.bps.trace then
local src = get_file_line( env.short_src, line )
local funname = env.name or "unknow"
debug_log( string.format( "%s:%d(%s) %s", env.short_src, line, funname, src ) )
debug.bps.cur_file = env.short_src;
debug.bps.cur_line = line
while not debug_execute_cmd( env ) do
end
end
end
function begin_debug()
debug.bps.trace = true
debug.sethook( debug_trace, "l" )
end
--关闭debugger
function debug_close()
debug.bps.trace = false
debug.bps.next = false
debug.sethook()
end