« Lua 中写 C 扩展库时用到的一些技巧 | 返回首页 | Lua Debugger »

lua 代码的断点调试

Lua 5.1 带了一个 debug 库,把所有的 C API 中的 debug 相关 api 都导出了。作为独立的语言使用的话,这些足够搭建一套方便的调试库。

说到最常用的断点调试法,我们能想到的最直接的方法就是利用 lua debug 库中的 hook ,然后记录一张断点位置表,设置行模式的 hook ,每次进入 hook 都检查是否是断点处,若是就停下来等待交互调试。

这个方法很有效,但是很消耗 cpu 。因为每进入一个新的代码行,都需要回调一个函数。当这个函数本身又是用 lua 写的时候,效率更低。

本文提供另一种思路,换一个方法设置断点,让没有断点时不影响运行效率。

简单的说就是在代码中插入硬断点。

C 语言调试时,很多调试器其实是动态的在进程中的程序码中插入 int 3 ,让程序执行到断点处可以触发调试中断,恢复运行时只需要把 int 3 抹去换上原本的机器码即可。

lua 虚拟机并没有提供调试中断指令,向 byte code 中插入代码也没有合适的途径。但是不要忘了,动态语言最大的好处之一就是解释执行,不需要编译链接。所以,我们把调试中断的调用硬编码到代码中也不是难事。调试之后去掉这些多余的代码即可。只要有硬编码进的调试断点,我们再为这些断点写动态的开关就是很简单的事情了。

今天我实现了这么一套简单的调试库,完全用 lua 本身实现的,只需要用 require "bp" 就可以加载进来使用。源代码我贴在了 wiki 上

下面讲讲简单的思路:

我把断点分成两类,一类是匿名断点,一类是具名断点。分别用 bp.bp() 和 bp.bp "name" 来设置。 匿名断点在第一次设置时初始化,并激活。也就是说,程序运行到 bp.bp() 处就会断下来。系统会为这个断点分配一个唯一 id 号。以后可以通过 id 来激活和关闭这个断点。方法是 bp.trigger(id,true/false) 默认 bp.trigger(id) 是激活断点。

这样的断点内部是用 closure 的对象地址做标识的,也就是说,同一个 function 的不同实例可以有相互独立的断点 id 。这样比靠源代码定位的方式更加灵活。closure 在动态语言中的使用非常广泛,相同一份代码可能在不同状态下干着不同的事情,我们可以为特定的 closure 设中断了。

当 closure 垃圾回收后,断点也会被回收。

具名断点适合对一批断点同时开关,因为有字符串名字,所以可以在断点运行到之前就设置好状态。有更高的可控性。

在云风的这个系统中,可以用 bp.list "name" 列出所有命名为 "name" 的断点所在代码位置,也就是说,可以给无限个断点起相同的名字,一致的控制。

bp.list() 可以列出系统中已有的所有断点。

为了配合这套断点调试系统,我附带做了几个小东西。一个就是递归打印 table ,函数名为 bp.print_r(tbl,limit=64)

由于 lua 的 table 有可能循环引用,或是元素太多,我在这里设置上输出上限,默认是 64 。超过上限的以 ... 结束。

另外,这里给 debug.debug() 这个调试控制台增加了方便的 local 变量以及 upvalue 的控制方法。原有的 debug.getlocal 和 debug.getupvalue 实在难用了点。

现在进入控制台后,可以通过 _L 和 _U 两张表,分别控制 local 和 upvalue 了。

可以在控制台试试这样一个例子:

require "bp"

function foo(arg) bp.bp() -- 设置一个匿名断点 return arg end

=foo(0) -- 输出 foo(0)

运行后我们发现立刻进入了调试环境,输出:

break point:    1       on      =stdin(2)
_L=     {arg=0,}
_U=     {}

表示 1 号断点中断,源码行是 stdin 的第 2 行。局部变量有 arg=0 一个,upvalue 没有。 现在可以输入 _L.arg=1 ,然后输入 cont 继续运行程序。你会发现屏幕输出是 1 而不是 0 。arg 变量已经被修改了。


大体上就是这样了,周末如果有空,我会把单步运行加上,基本就可以及的上简单的 gdb 功能了 :D

Comments

可惜没有下文了,好奇这种思路如何加如单步调试。在进入端点之后,设置钩子来做么?结束断点之后清除钩子?
你写的很好,很想跟你学学关于断点测试的技巧,谢谢!可以加我qq:529426924
Hi all! You are The Best!!! Bye
yield 是在 coroutine 中用的吧。在两个执行线路中切换。 这里并没有多个执行线路,进入调试控制台是lua 原本就有的功能,整个代码就塞在那里了。只是可以利用调试 api 访问不属于自己堆栈层次的数据而已。 如果要实现一个 ide 的话,那么要就用 coroutine 要就直接用多线程,甚至多进程了。 例如 http://www.keplerproject.org/remdebug/ 这个调试解决方案。
有点像没有返回功能的python里面的yield。

Post a comment

非这个主题相关的留言请到:留言本