4. lua调试脚本编写

本文详细介绍了在调试Lua源码时遇到的挑战,如TValue类型的复杂性,以及如何通过编写GDB脚本来简化调试过程。作者分享了如何利用gdb编写脚本以方便地输出TValue、Table、string等类型,并展示了如何输出字节码以辅助理解Lua的执行阶段。此外,还推荐了gef作为增强gdb功能的工具。
摘要由CSDN通过智能技术生成

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

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值