动手写调试器

环境配置

x86Linux

下载 Linenoise(命令行解析) 以及libelfin(分析调试信息).

软件架构

在运行调试器之前,我们首先需要运行被调试的程序.此外还需要调试器与被调试的进程之间进行通信.自然的,我们想到使用经典的fork-exec 模式进行软件开发.

GDB主要使用ptrace系统调用来完成对子进程状态的控制以及检查.

其接口设计较为繁琐,采用一个枚举类型的request值来表明欲要执行的操作, void*类型的addr 指明被调试进程的地址用于特殊操作. data 指向一些特殊操作所需资源

具体信息请查阅man pages.

子进程从fork返回后, 需要将request设置为PTRACE_TRACEME,告知父进程调试开始,系统调用的其余参数因无用均被忽略.

接着执行execlsyscall加载被调试的程序.至此,子进程执行调试代码,直至调试终止.

调试器设计

自此,我们已经成功运行了子进程,接下来的任务就是如何控制读取子进程的状态了.

为此,我们创建一个debugger类来实现这一操作.

调试器的设计遵循REPL模式. 在run函数中,父进程需要等待子进程启动完毕,在读取到EOF之前,一直在循环中不断执行指令并输出结果.

处理输入

该调试器的指令与gdb类似,在gdb中,我们使用continue cont c命令之一来恢复进程的执行, 在我的实现中,为了代码简单起见,只实现最简单的单个字母c. 其余命令类似.

continue_execution中,我们选择让进程继续执行.

断点

断点主要分为两种类型,软件和硬件.硬件断点主要是通过设置ISA指定的寄存器产生中断,然而软件断点包括修改正在运行的程序代码.在本教程中,我们只关心软件断点,因为他们更为简单,灵活,可以设置任意多的软件断点.在x86架构下,在同一时间仅能设置4个不同的硬件断点,但其功能更为强大,可以对指定的地址进行读写.

在上文中提到,软件断点需要通过对正在执行的代码进行修改而得到,这引出了如下问题:

解决第一个问题需要用到ptracesyscall,除了上文的作用外,其还可以读写特定的地址.

在x86架构下,为了让子进程在断点处停止(halt)并且通知父进程.需要将断点处的指令替换为int 3. 在x86架构中,操作系统启动时会初始化中断向量表,其中注册了对各种中断的处理函数,例如缺页,保护错,非法的错作数等等.

当CPU执行到int 3 这一指令时,控制权被转移给断点中断函数,其向进程发送SIGTRAP信号.从而产生断点.

如果你还记得上文中父进程通过waitpid等待子进程处理完成,那么我们也可以通过此方法通知父进程子进程踩到了断点.

实现软件断点

我们主要关注enable 以及disable这两个方法.

如前文所说,我们需要将断点处的指令替换为int 3其16进制值为0xcc.同时也需要存储原来地址处的指令以待之后恢复.

PTRACE_PEEKDATA表明当前syscall需要读取被调试进程的地址,并且返回从该地址起64bit的值.我们需要将返回值的最低字节hack为int 3 代表的值,最后写回对应的内存地址. //TODO 变长指令问题.

disable 函数将原指定写回

修改debugger类

//TODO

恢复执行

#TODO

测试

#TODO

内存和寄存器

我们首先需要向代码中添加x86各寄存器的枚举类型.(目前支持gpr和s(special)pr).由于兼容原因,x86允许用户访问一些通用寄存器的低8/16/32位,为了简单起见,我并不打算实现此模式.

 

现在你可以写一些函数来读写这些寄存器了

ptrace在这里用于读取寄存器的值.