现在的位置: 首页 > 综合 > 正文

实现一个lua 调试器

2013年09月13日 ⁄ 综合 ⁄ 共 8030字 ⁄ 字号 评论关闭

简介:
  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

抱歉!评论已关闭.