在本文中,将探讨Linux下的调试器原理,并尝试编写一个简单的调试器(tracer)来调试一个示例程序(tracee)。调试器是程序员用来检查和调试程序的工具,它们通过设置断点、单步执行和查看调用栈等方式帮助开发者理解程序的运行状态。本文的目的是让读者了解调试器的内部机制,并希望读者能够编写一个易于使用的命令行调试器。
读者需要具备Linux操作系统的基础知识,特别是信号及其处理机制。调试器依赖于信号来从被调试程序(debuggee)接收通知(例如:SIGTRAP)。调试器总是通过wait函数等待来自tracee的某个信号。
断点允许用户在被调试程序的执行流程中设置一个中断点。用户可以这样做是为了在执行到这一点时评估某些条件。调试器会在被调试的可执行文件的进程空间中的特定地址(用户希望设置断点的地方)添加一条指令:
int 3 (opcode: 0xcc)
。当遇到这条指令时,EIP(指令指针)会被移动到中断服务例程(在这种情况下是int 3)。服务例程会保存CPU寄存器(所有中断服务例程都必须这样做),并向附加的调试器发送信号:调用了ptrace(PTRACE_ATTACH, pid, ...)的进程。
在阅读本文时,必须始终参考附带的代码。断点(opcode: 0xcc)是通过代码引入到debuggee中的:
C++
static unsigned char c[] = {0xcc, 0xc3, 0x12, 0x34, 0x45};
static void (*pfunc)() = (void(*)())c;
static int i = mprotect((unsigned long int)c & 0xfffffffffffff000, sizeof(c), PROT_EXEC | PROT_READ | PROT_WRITE);
pfunc();
使用mprotect(Windows中的virtualprotect的等价物)来为此内存提供执行访问权限。所有商业调试器都会使用ptrace(PTRACE_POKEDATA,...)注入断点(不带代码),这相当于Windows中的WriteProcessMemory,它们显然会在更改之前保存指令并在正确执行时恢复它。
Linux没有与Windows的StackWalk64等价的函数,尽管有第三方堆栈展开库(将使用)。当需要堆栈遍历时,调试器必须逐字节遍历堆栈(属于wait返回的threadID)。由于堆栈仅维护函数的返回地址,它必须检查前面的字节以确保调用已发出。为了确保被调用的函数确实是函数而不是标签,会采取额外的步骤,这是通过查看将帧指针推送到堆栈上的明显迹象来完成的。
C++
void Getbacktrace(int thetid) {
unw_cursor_t cursor;
unw_word_t ip;
unw_addr_space_t as;
struct UPT_info *ui = NULL;
as = unw_create_addr_space(&_UPT_accessors, 0);
ui = _UPT_create(thetid);
int rc = unw_init_remote(&cursor, as, ui);
while (unw_step(&cursor) > 0) {
//walk the stack one frame at a time
unw_word_t offset, pc;
unw_get_reg(&cursor, UNW_REG_IP, &pc);
char buffer[STR_MAX] = {};
if (0 == unw_get_proc_name(&cursor, buffer, sizeof(buffer), &offset))
//get mangled function names
printf("%s\n", buffer);
PrintFileAndLine(DEBUGGEE, pc);
//use addr2line
}
_UPT_destroy(ui); unw_destroy_addr_space(as);
}
如前所述,调试器将使用wait,在while循环中旋转(代码是自解释的),并在debuggee退出时退出(由于缺少退出部分,这是读者的练习):
C++
ptrace(PTRACE_ATTACH, pid, NULL); printf("error %u\n", errno);
//lets attach the process
while (1) {
pid_t tid = wait(&status);
if (WIFSTOPPED(status)) {
.....
ptrace(PTRACE_GETSIGINFO, pid, NULL, &siginfo);
ptrace(PTRACE_CONT, pid, NULL, siginfo.si_signo);
}
}