Skip to content

Latest commit

 

History

History
304 lines (250 loc) · 11.2 KB

ch07-调试器.md

File metadata and controls

304 lines (250 loc) · 11.2 KB

###Lua调试器工作原理 要实现任何一个语言的调试器,大致需要有以下几个功能:

  • 如何让程序按照所下的断点终止程序?
  • 如何获得断点处程序相关的信息,比如调用堆栈,打印变量值?
  • 如何在断点处继续进入一个函数进行调试?

下面,来逐个分析Lua在实现调试器方面都提供了哪些支持以及如何使用它们来实现一个自己的Lua调试器.

####lua提供的hook功能 Lua为了支持调试,提供了hook功能,使用者可以根据需要添加不同的hook处理函数供条件触发时回调,包括以下几种: 具体包括以下几种hook类型:

(lua.h)
321 #define LUA_MASKCALL	(1 << LUA_HOOKCALL)
322 #define LUA_MASKRET		(1 << LUA_HOOKRET)
323 #define LUA_MASKLINE	(1 << LUA_HOOKLINE)
324 #define LUA_MASKCOUNT	(1 << LUA_HOOKCOUNT)

调用lua_sethook函数传入上面的参数,可以在对应的调用处调用注册的回调函数,其中:

  • LUA_MASKCALL在函数被调用时触发;
  • LUA_MASKRET在函数返回时被触发;
  • LUA_MASKLINE每执行一行代码时被触发;
  • LUA_MASKCOUNT每执行count条lua指令触发一次,这里的count是lua_sethook函数的第三个参数中传入,使用其它hook类型时该参数无效.

####如何得到当前程序的信息 Lua提供了lua_Debug结构体,里面的成员变量用来保存当前程序的一些信息:

(lua.h)
346 struct lua_Debug {
347   int event;
348   const char *name; /* (n) */
349   const char *namewhat; /* (n) `global', `local', `field', `method' */
350   const char *what; /* (S) `Lua', `C', `main', `tail' */
351   const char *source;   /* (S) */
352   int currentline;  /* (l) */
353   int nups;     /* (u) number of upvalues */
354   int linedefined;  /* (S) */
355   int lastlinedefined;  /* (S) */
356   char short_src[LUA_IDSIZE]; /* (S) */
357   /* private part */
358   int i_ci;  /* active function */
359 };

这些成员变量的含义分别是:

  • event:用于表示出发Hook的事件,事件类型就是前面提到的几个宏.
  • name:当前所在函数的名称.
  • namewhat:name域的含义。可能的取值为:“global”、“local”、“method”、“field”,或者空字符串.空字符串意味着Lua无法找到这个函数名.
  • what:函数类型。如果foo是普通的Lua函数,结果为“Lua”;如果是C函数,结果为“C”;如果是Lua的主代码段,结果为“main”
  • source:函数的定义位置。如果函数在字符串内被定义(通过loadstring 函数),source就是该字符串,如果函数在文件中被定义,source就是带“@”前缀的文件名。
  • currentline:当前所在行号.
  • nups:该函数的Upvalue的数量.
  • linedefined:source中函数被定义处的行号
  • lastlinedefined:该函数定义在源代码中的最后一行的行号.
  • short_src:source的简短版本(60个字符以内),对错误信息很有用.
  • i_ci:

可以通过lua_getinfo得到一些很重要的信息,它的调用方式是:lua_getinfo(state, params, ar),第二个参数是一个字符串,支持传入多个字母,而在前面lua_Debug结构体定义中的每个成员变量的注释中写在括号里面的字符,就是可以传入做为这个参数的变量,比如如果要得到name和what信息,需要传入"nS",依次类推.

####如何打印变量 变量分为局部和全局变量,因此搜索某个变量的时候是从内到外的方式搜索,搜索局部变量时,使用lua提供的API lua_getlocal函数:

LUA_API const char *lua_getlocal (lua_State *L, const lua_Debug *ar, int n);

这个函数的作用是在当前函数的locvars数组中依次查找变量,传入的n为数组索引,从1开始,当该索引找不到对应的数组元素时会返回NULL.另外需要注意的是,这个API会将查找结果压入栈中,如果查找不成功那么需要从栈中弹出前面压入的值恢复栈的结构,如下面这个函数做的这样:

static int
search_local_var(lua_State *state, lua_Debug *ar, const char* var) {
	int         i;
	const char *name;

	for(i = 1; (name = lua_getlocal(state, ar, i)) != NULL; i++) {
	  if(strcmp(var,name) == 0) {
	    return i;
	  }    
	  // not match, pop out the var's value
	  lua_pop(state, 1); 
	}
	return 0;
}

如果在局部变量中搜索不到,则还得使用lua_getglobal函数进行全局变量的搜索,这个API实际上是一个宏,会根据传入的key进入GLOBAL表查找变量,同样的在使用这个函数时也需要注意恢复栈结构:

static int
search_global_var(lua_State *state, lua_Debug *ar, const char* var) {
	lua_getglobal(state, var);

	if(lua_type(state, -1 ) == LUA_TNIL) {
	  lua_pop(state, 1);
	  return 0;
	}   

	return 1;
}

但是以上的过程仅仅还只能在相应的地方查找到同名的变量,在真正需要打印变量值的时候,还需要根据变量的类型具体来打印数据,如果是有其他数据关联的类型,如表,那么还需要遍历里面的关联成员进行打印(下面的代码引用自笔者自己实现的开源Lua调试器ldb的代码):

static void
print_var(lua_State *state, int si, int depth) {
  switch(lua_type(state, si)) {
  case LUA_TNIL:
    output("(nil)");
    break;

  case LUA_TNUMBER:
    output("%f", lua_tonumber(state, si));
    break;

  case LUA_TBOOLEAN:
    output("%s", lua_toboolean(state, si) ? "true":"false");
    break;

  case LUA_TFUNCTION:
    {
      lua_CFunction func = lua_tocfunction(state, si);
      if( func != NULL ) {
        output("(C function)0x%p", func);
      } else {
        output("(function)");
      }
    }
    break;

  case LUA_TUSERDATA:
    output("(user data)0x%p", lua_touserdata(state, si));
    break;

  case LUA_TSTRING:
    print_string_var(state, si, depth);
    break;
    
  case LUA_TTABLE:
    print_table_var(state, si, depth);
    break;

  default:
    break;
  }
}

static void
print_table_var(lua_State *state, int si, int depth) {
  int pos_si = si > 0 ? si : (si - 1);
  output("{");
  int top = lua_gettop(state);
  lua_pushnil(state);
  int empty = 1;
  while(lua_next(state, pos_si ) !=0) {
    if(empty) {
      output("\n");
      empty = 0;
    }

    int i;
    for(i = 0; i < depth; i++) {
      output("\t");
    }

    output( "[" );
    print_var(state, -2, -1);
    output( "] = " );
    if(depth > 5) {
      output("{...}");
    } else {
      print_var(state, -1, depth + 1);
    }
    lua_pop(state, 1);
    output(",\n");
  }

  if (empty) {
    output(" }");
  } else {
      int i;
    for (i = 0; i < depth - 1; i++) {
      output("\t");
    }
    output("}");
  }
  lua_settop(state, top);
}

static void
print_string_var(lua_State *state, int si, int depth) {
  output( "\"" );

  const char * val = lua_tostring(state, si);
  int vallen = lua_strlen(state, si);
  int i;
  const char spchar[] = "\"\t\n\r";
  for(i = 0; i < vallen; ) {
    if(val[i] == 0) {
      output("\\000");
      ++i;
    } else if (val[i] == '"') {
      output("\\\"");
      ++i;
    } else if(val[i] == '\\') {
      output("\\\\");
      ++i;
    } else if(val[i] == '\t') {
      output("\\t");
      ++i;
    } else if(val[i] == '\n') {
      output("\\n");
      ++i;
    } else if(val[i] == '\r') {
      output("\\r");
      ++i;
    } else {
      int splen = strcspn(val + i, spchar);

      output("%.*s", splen, val+i);
      i += splen;
    }
      }
  output("\"");
}

####如何查看文件的内容 查看文件的内容相对简单,因为当lua被Hook住的时候,可以通过lua_getinfo函数得到当前lua的一些信息,比如lua文件名,行号,在C实现的Lua调试器中,会维护一个已经读取过的文件列表,如果当前所在的文件还没有被读取到内存中,那么会读取到内存中,再根据所在的行号就可以得到文件内容的信息了。

####断点的添加 调试器的断点分为两种,一种是基于文件:行号形式的,一种则是基于函数调用形式的。 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,如果是的话再继续根据断点的函数名进行查找匹配。

####如何查看当前堆栈信息 也就是模拟gdb中的bt指令的功能。lua对这个获取某一层堆栈的信息已经提供了API lua_\getstack,这里要做的就是逐层调用该函数得到此时函数堆栈的信息:

static void
dump_stack(lua_State *state, int depth, int verbose) {
  lua_Debug ldb;
  int i;
  const char *name, *filename;

  for(i = depth; lua_getstack(state, i, &ldb) == 1; i++) {
    lua_getinfo(state, "Slnu", &ldb);
    name = ldb.name;
    if( !name ) {
      name = "";
    }
    filename = ldb.source;

    output("#%d: %s:'%s', '%s' line %d\n",
           i + 1 - depth, ldb.what, name,
           filename, ldb.currentline );
	}
}

####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指令可以继续执行下去。