深入LUA脚本语言,让你彻底明白调试原理

我们也可以自己写一个,如下:

// 引入Lua头文件

#include

#include

#include

int main(int argc, char *argv[])

{

    // 创建一个Lua虚拟机

    lua_State *L = luaL_newstate();


    // 打开LUA中的标准库

    luaL_openlibs(L);


    // 加载 test.lua 程序

    if (luaL_loadfile(L, "test.lua") || lua_pcall(L, 0, 0, 0))

    {

        printf("Error: %s \n", lua_tostring(g_lua_handle.L, -1));

        lua_close(g_lua_handle.L);

    }

    // 其他代码

}

2. Lua语法

在语法层面,Lua涵盖的内容还是比较全面的,它是一门动态类型语言,基本概念包括:八种基本数据类型,表是唯一的数据结构,环境与全局变量,元表及元方法,协程,闭包,错误处理,垃圾收集。具体的信息可以看一下Lua5.3参考手册。

这篇文章主要从调试器这个角度进行分析,因此我不会在这里详细的贴出很多代码细节,而只是把与调试有关的代码贴出来进行解释。

我之前在学习Lua源码时(5.3.5版本),在代码文件中记录了很多注释,可以很好的帮助理解,主要是因为我的忘性比较好。

其实我更建议大家自己去下载源码学习,经过自己的理解、加工,印象会更深刻。在之前的工作中,由于项目需要,我对源码进行了一些优化,这部分代码就不放出来了,添加注释的源码是完完全全的Lua5.3.5版本,大概是这个样子:

如果有小伙伴需要加了注释的源码,请在公众号(IOT物联网小镇)里留言给我。

四、Lua调试库相关

我们可以停下来稍微想一下,对一个程序进行调试,需要考虑的问题有3点:

如何让程序暂停执行?

如何获取程序的内部信息?

如果修改程序的内部信息?

带着这些问题,我们来逐个击破。

1. 钩子函数(Hook):让程序暂停执行

Lua虚拟机(也可称之为解释器)内部提供了一个接口:用户可以在应用程序中设置一个钩子函数(Hook),虚拟机在执行指令码的时候会检查用户是否设置了钩子函数,如果设置了,就调用这个钩子函数。本质上就是设置一个回调函数,因为都是用C语言来实现的,虚拟机中只要把这个钩子函数的地址记住,然后在某些场合回调这个函数就可以了。

那么,虚拟机在哪些场合回调用户设置的钩子函数呢?

我们在设置Hook函数的时候,可以通过mask参数来设置回调策略,也就是告诉虚拟机:在什么时候来回调钩子函数。mask参数可以是下列选项的组合操作:

LUA_MASKCALL:调用一个函数时,就调用一次钩子函数。

LUA_MASKRET:从一个函数中返回时,就调用一次钩子函数。

LUA_MASKLINE:执行一行指令时,就回调一次钩子函数。

LUA_MASKCOUNT:执行指定数量的指令时,就回调一次钩子函数。

设置钩子函数的基础API原型如下:

void lua_sethook (lua_State *L, lua_Hook f, int mask, int count);

第二个参数f需要指向我们自己定义的钩子函数,这个钩子函数原型为:

typedef void (*lua_Hook) (lua_State *L, lua_Debug *ar);

我们也可以通过下面即将介绍的调试库中的函数来设置钩子函数,效果是一样的,因为调试库函数的内部也是调用基础函数。

debug.sethook ([thread,] hook, mask [, count])

再来看一下虚拟机中的相关代码。

当执行完上一条指令,获取下一条指令之后,调用函数luaG_traceexec(lua_State *L):

void luaG_traceexec (lua_State *L) {

  // 获取mask掩码

  lu_byte mask = L->hookmask;

  int counthook = (--L->hookcount == 0 && (mask & LUA_MASKCOUNT));

  if (counthook)

    resethookcount(L);

  else if (!(mask & LUA_MASKLINE))

    return;

  if (counthook)

    luaD_hook(L, LUA_HOOKCOUNT, -1);  // 按指令次数调用钩子函数

  if (mask & LUA_MASKLINE) {

    Proto *p = ci_func(ci)->p;

    int npc = pcRel(ci->u.l.savedpc, p);

    int newline = getfuncline(p, npc);

    if (npc == 0 ||

        ci->u.l.savedpc <= L->oldpc ||

        newline != getfuncline(p, pcRel(L->oldpc, p)))

      luaD_hook(L, LUA_HOOKLINE, newline); // 按行调用钩子函数

  }

}

可以看到,当mask掩码中包含了LUA_MASKLINE时,就调用函数luaD_hook(),如下代码:

void luaD_hook (lua_State *L, int event, int line) {

  lua_Hook hook = L->hook;

  if (hook && L->allowhook) {

    // 为钩子函数准备参数,其中包括了各种调试信息

    lua_Debug ar;

    ar.event = event;

    ar.currentline = line;

    ar.i_ci = ci;

    // 调用钩子函数

    (*hook)(L, &ar);

  }

}

只要进入了用户设置的钩子函数,那么我们就可以在这个函数中为所欲为了。

比如:获取程序内部信息,读取、修改变量的值,查看函数调用栈信息等等,这就是下面要讲解的内容。

2. Lua调试库是什么?

首先说一下Lua中的标准库。

所谓的标准库就是Lua为开发者提供的一些有用的函数,可以提高开发效率,当然我们可以选择不使用标准库,或者只使用部分标准库,这是可以裁剪的。

这里我们只介绍一下基础库、操作系统库和调试库这3个家伙。

基础库

基础库提供了Lua核心函数,如果你不将这个库包含在你的程序中,就需要小心检查程序是否需要自己提供其中一些特性的实现,这个库一般都是需要使用的。

操作系统库

这个库提供与操作系统进行交互的功能,例如提供了函数:

os.date

os.time

os.execute

os.exit

os.getenv

调试库

先看一下库中提供的几个重要的函数:

debug.gethook

debug.sethook

debug.getinfo

debug.getlocal

debug.setlocal

debug.setupvalue

debug.traceback

debug.getregistry

上面已经说到,Lua给用户提供了设置钩子的API函数lua_sethook,用户可以直接调用这个函数,此时传入的钩子函数的定义格式需要满足要求。

为了简化用户编程,Lua还提供了调试库来帮助用户降低编程难度。调试库其实也就是把基础API函数进行封装了一下,我们以设置钩子函数debug.sethook为例:

文件ldblib.c中,定义了调试库支持的所有函数:

static int db_sethook (lua_State *L) {

  lua_sethook(L1, func, mask, count);

}

static const luaL_Reg dblib[] = {

  // 其他接口函数都删掉了,只保留这一个来讲解

  {"sethook", db_sethook},

  {NULL, NULL}

};

// 这个函数用来把调试库中的函数注册到全局变量表中

LUAMOD_API int luaopen_debug (lua_State *L) {

  luaL_newlib(L, dblib);

  return 1;

}

可以看到,调试库的debgu.sethook()函数最终也是调用基础API函数:lua_sethook()。

在后面的调试器开发讲解中,我就是用debug库来实现一个远程调试器。

3. 获取程序内部信息

在钩子函数中,可以通过如下API函数还获取程序内部的信息了:

int lua_getinfo (lua_State *L, const char *what, lua_Debug *ar);

在这个API函数中:

第二个参数用来告诉虚拟机我们想获取程序的哪些信息

第三个参数用来存储获取到的信息

结构体lua_Debug比较重要,成员变量如下:

typedef struct lua_Debug {

  int event;

  const char *name;          /* (n) */

  const char *namewhat;      /* (n) */

  const char *what;          /* (S) */

  const char *source;        /* (S) */

  int currentline;            /* (l) */

  int linedefined;            /* (S) */

  int lastlinedefined;        /* (S) */

  unsigned char nups;        /* (u) 上值的数量 */

  unsigned char nparams;      /* (u) 参数的数量 */

  char isvararg;              /* (u) */

  char istailcall;            /* (t) */

  char short_src[LUA_IDSIZE]; /* (S) */

  /* 私有部分 */

  其它域

} lua_Debug;

source:创建这个函数的代码块的名字。 如果 source 以 '@' 打头, 指这个函数定义在一个文件中,而 '@' 之后的部分就是文件名。

linedefined: 函数定义开始处的行号。

lastlinedefined: 函数定义结束处的行号。

currentline: 给定函数正在执行的那一行。

其他字段可以在参考手册中查询。

例如:如果想知道函数 f 是在哪一行定义的, 你可以使用下列代码:

lua_Debug ar;

lua_getglobal(L, "f");  /* 取得全局变量 'f' */

lua_getinfo(L, ">S", &ar);

printf("%d\n", ar.linedefined);

同样的,也可以调用调试库debug.getinfo()来达到同样的目的。

4. 修改程序内部信息

经过上面的讲解,已经看到我们获取程序信息都是通过Lua提供的API函数,或者是利用调试库提供的接口函数来完成的。那么修改程序内部信息也同样如此。

Lua提供了下面这2个API函数来修改函数中的变量:

修改当前活动记录总的局部变量的值:

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

设置闭包上值的值(上值upvalue就是闭包使用了外层的那些变量)

const char *lua_setupvalue (lua_State *L, int funcindex, int n);

同样的,也可以利用调试库中的debug.setlocal和debug.setupvalue来完成同样的功能。

5. 小结

到这里,我们就把Lua语言中与调试有关的机制和代码都理解清楚了,剩下的问题就是如何利用它提供的这些接口,来编写一个类似gdb一样的调试器。

就好比:Lua已经把材料(米、面、菜、肉、佐料)摆在我们的面前了,剩下的就需要我们把这些材料做成一桌美味佳肴。

龙华大道1号 http://www.kinghill.cn/Dynamics/2106.html

你可能感兴趣的:(深入LUA脚本语言,让你彻底明白调试原理)