前言
当我们在linux下使用c/c++开发时,可以通过gdb来调试我们编译后的elf文件。gdb支持了attch、单步运行(单行、单指令)、设置断点等非常实用的功能来辅助我们调试。当使用lua开发的时候,一般可能会使用print(打印到屏幕)或是输出日志等稍微简陋的调试方式,但如果日志输出不能满足我们需求时,比如我们需要类似断点、单步执行等更高级的调试功能,此时就必须借助第三方工具。
本文介绍了lua调试工具LuaPanda的使用,以及lua调试工具和gdb在实现上的一些区别。
gdb调试原理
先简单介绍一下gdb的原理。一般的我们将gdb这种调试进程称为tracer,被调试进程称为tracee。当进程被调试时(处于traced状态)时,每次收到任何除了SIGKILL以外的任何信号,都会暂停当前的执行,并且tracer进程可以通过waitpid来获取tracee的暂停原因。gdb使用ptrace系统调用来实现操作tracee进程
1. gdb附加到进程
当使用gdb附加到一个正在运行的进程(tracee)上时,gdb会执行类似下面的代码:
ptrace(PTRACE_ATTACH, pid, ...)
这里的pid是tracee的pid。系统调用执行后,os会给tracee进程发送一个SIGTRAP信号,然后tracee的执行将会暂停。最后gdb(tracer)可以通过系统调用waitpid来获取tracee的暂停原因,并且开始调试。
2. gdb单步执行
单步调试与上述attch类似,gdb通过下面的代码告诉tracee进程需要在运行完一个指令后暂停:
ptrace(PTRACE_SINGLESTEP, pid, ...)
当tracee执行完一个指令后,tracee也会因为收到os的SIGTRAP信号从而暂停执行。
3. gdb设置断点
设置断点稍微有点不同,首先gdb需要从调试程序的调试信息中根据行号(函数名)找到代码的内存地址,然后通过ptrace将tracee进程的代码替换成一个软中断指令:int 3。由于这个指令实际上会被编码为一个字节0xcc,因此可以很安全的与任何指令替换。
/* Look at the word at the address we're interested in */
unsigned addr = 0x8048096;
unsigned data = ptrace(PTRACE_PEEKTEXT, child_pid, (void*)addr, 0);
/* Write the trap instruction 'int 3' into the address */
unsigned data_with_trap = (data & 0xFFFFFF00) | 0xCC;
ptrace(PTRACE_POKETEXT, child_pid, (void*)addr, (void*)data_with_trap);
通过给ptrace指定PTRACE_PEEKTEXT、PTRACE_POKETEXT可以读写tracee进程的代码段的内存。最终当程序执行到int 3时,会触发一个软中断,os会给tracee进程发送SIGTRAP信号。当断点成功后,gdb会用相同的方法用原来的指令替换掉i