ptrace函数
ptrace 是 Linux 和其他类 Unix 系统中用于进程调试和跟踪的强大系统调用。它允许一个进程(调试器)监视和控制另一个进程(被调试进程)的执行,修改其内存和寄存器,是实现 GDB、Valgrind 等调试工具的基础。
函数原型
#include <sys/ptrace.h>
long ptrace(enum __ptrace_request request, pid_t pid,
void *addr, void *data);
request:操作类型,如 PTRACE_TRACEME、PTRACE_PEEKDATA 等
pid:目标进程的 PID
addr:内存地址或寄存器号,取决于操作类型
data:输入 / 输出数据,取决于操作类型
操作类型中,主要用到 PTRACE_TRACEME、PTRACE_ATTACH 、PTRACE_SINGLESTEP。
例子
以PTRACE_TRACEME为例,当子进程执行ptrace(PTRACE_TRACEME,……)时,内核会把子进程的PCB(进程控制块,在linux中是task_struct)中的ptrace标志位置为PT_PTRACED,然后继续执行子进程,当该进程遇见特殊中断(int 3)或者异常的时候,就不会像往常一样交由内核处理了,而是直接转交给监视它的父进程来进行处理。
PTRACE_ATTACH也是如此,当进程A想要调式进程B,执行ptrace(PTRACE_ATTACH,1111(进程B的Pid),……)时,内核会先给进程B发送SIGSTOP让其停止,然后把进程B的ptrace标志位置为PT_PTRACED(这样以后的特殊中断和异常就会转交给父进程处理),接着通知进程A,进程A就可以通过各种ptrace命令来给进程B发号施令了。
所以ptrace的核心作用可以概括为,把某个进程(被调试者)的特殊中断和异常处理权从内核手中交给另外一个进程(调试者)处理。当然,除此之外ptrace还有一些单步执行的功能,其实就是把对应标志位置1,这样cpu在执行一步之后就会直接触发中断,通知调试者,这里不展开叙述。
windows
windows处理调试进程的逻辑跟linux差不多,也是用到了PEB(类似linux的PCB)的DEBUG_PROCESS 标志,只不过把ptrace这个函数给细化,切割成了很多函数来实现,下面是表格:
| 功能场景 | Linux ptrace 操作 | Windows 对应 API / 机制 |
|---|---|---|
| 关联被调试进程(创建) | fork + ptrace(PTRACE_TRACEME) | CreateProcess(带 DEBUG_PROCESS 标志) |
| 关联被调试进程(附加) | ptrace(PTRACE_ATTACH, pid) | DebugActiveProcess |
| 接收调试事件 | waitpid(配合信号拦截) | WaitForDebugEvent |
| 继续被调试进程执行 | ptrace(PTRACE_CONT) | ContinueDebugEvent |
| 读取 / 修改目标进程内存 | ptrace(PTRACE_PEEKDATA/POKEDATA) | ReadProcessMemory / WriteProcessMemory |
| 读取 / 修改寄存器状态 | ptrace(PTRACE_GETREGS/SETREGS) | GetThreadContext / SetThreadContext |
| 设置断点 / 单步执行 | ptrace(PTRACE_SINGLESTEP) | 异常断点(INT 3)+ 单步标志(CONTEXT_SINGLESTEP) |
| 脱离调试关联 | ptrace(PTRACE_DETACH) | DebugActiveProcessStop |
gdb
gdb的两种使用方式,一种是直接调试——gdb启动后先fork一个子进程,子进程中执行ptrace(PTRACE_TRACEME,……)成为父进程的调试进程,然后执行目标程序,即可通过gdb父进程实现对目标程序的调试。
另外一种是附加调试,gdb中直接执行ptrace(PTRACE_ATTACH,目标pid,……)即可。
着重说一下下断点的方式,在用户执行 ”break 断点位置“之后,gdb会先把断点位置的指令存起来,然后改成int 3(0xCC),这样在运行到这行代码时候就会触发异常中断,把处理权交给调试者(即父进程gdb),继续的话,就会执行被换掉的那条命令,执行完之后再换回int 3,以便下一次触发断点,实际上,windows的调试器的断点也是这种思路。