一篇掌握GDB调试程序的核心技术-ptrace系统调用与使用示例( 二 )

 
三 , ptrace实现原理

本文使用的 Linux 2.4.16 版本的内核
  • 看懂本文需要的基础:进程调度 , 内存管理和信号处理等相关知识 。
  • 调用 ptrace() 系统函数时会触发调用内核的 sys_ptrace() 函数 , 由于不同的原因 CPU 架构有着不同的调试方式 , 所以 Linux 为每种不同的 CPU 架构实现了不同的 sys_ptrace() 函数 , 而本文主要介绍的是 X86 CPU 的调试方式 , 所以 sys_ptrace() 函数所在文件是 linux-2.4.16/arch/i386/kernel/ptrace.c 。
sys_ptrace() 函数的主体是一个 switch 语句 , 会传入的 request 参数不同进行不同的操作 , 如下:asmlinkage int sys_ptrace(long request, long pid, long addr, long data){struct task_struct *child;struct user *dummy = NULL;int i, ret;...read_lock(&tasklist_lock);child = find_task_by_pid(pid); // 获取 pid 对应的进程 task_struct 对象if (child)get_task_struct(child);read_unlock(&tasklist_lock);if (!child)goto out;if (request == PTRACE_ATTACH) {ret = ptrace_attach(child);goto out_tsk;}...switch (request) {case PTRACE_PEEKTEXT:case PTRACE_PEEKDATA:...case PTRACE_PEEKUSR:...case PTRACE_POKETEXT:case PTRACE_POKEDATA:...case PTRACE_POKEUSR:...case PTRACE_SYSCALL:case PTRACE_CONT:...case PTRACE_KILL:...case PTRACE_SINGLESTEP:...case PTRACE_DETACH:...}out_tsk:free_task_struct(child);out:unlock_kernel();return ret;}
  • 从上面的代码可以看出 , sys_ptrace() 函数首先根据进程的 pid 获取到进程的 task_struct 对象 。然后根据传入不同的 request 参数在 switch 语句中进行不同的操作 。
ptrace() 支持的所有 request 操作定义在 linux-2.4.16/include/linux/ptrace.h 文件中 , 如下:#define PTRACE_TRACEME0#define PTRACE_PEEKTEXT1#define PTRACE_PEEKDATA2#define PTRACE_PEEKUSR3#define PTRACE_POKETEXT4#define PTRACE_POKEDATA5#define PTRACE_POKEUSR6#define PTRACE_CONT7#define PTRACE_KILL8#define PTRACE_SINGLESTEP9#define PTRACE_ATTACH0x10#define PTRACE_DETACH0x11#define PTRACE_SYSCALL24#define PTRACE_GETREGS12#define PTRACE_SETREGS13#define PTRACE_GETFPREGS14#define PTRACE_SETFPREGS15#define PTRACE_GETFPXREGS18#define PTRACE_SETFPXREGS19#define PTRACE_SETOPTIONS21
  • 由于 ptrace() 提供的操作比较多 , 所以本文只会挑选一些比较有代表性的操作进行解说 , 比如 PTRACE_TRACEME、PTRACE_SINGLESTEP、PTRACE_PEEKTEXT、PTRACE_PEEKDATA 和 PTRACE_CONT 等 , 而其他的操作 , 有兴趣的朋友可以自己去分析其实现原理 。
进入被追踪模式(PTRACE_TRACEME操作)
  • 当要调试一个进程时 , 需要使进程进入被追踪模式 , 怎么使进程进入被追踪模式呢?有两个方法:
  1. 被调试的进程调用 ptrace(PTRACE_TRACEME, ...) 来使自己进入被追踪模式 。
  2. 调试进程(如GDB)调用 ptrace(PTRACE_ATTACH, pid, ...) 来使指定的进程进入追踪模式 。
  • 第一种方式是进程自己主动进入被追踪模式 , 而第二种是进程被动进入被追踪模式 。
  • 被调试的进程必须进入追踪模式才能进行调试 , 因为 Linux 会对被追踪的进程进行一些特殊的处理 。下面我们主要介绍第一种进入追踪模式的实现 , 就是 PTRACE_TRACEME 操作过程 , 代码如下:
asmlinkage int sys_ptrace(long request, long pid, long addr, long data){...if (request == PTRACE_TRACEME) {if (current->ptrace & PT_PTRACED)goto out;current->ptrace |= PT_PTRACED; // 标志 PTRACE 状态ret = 0;goto out;}...}
  • 从上面的代码可以发现 , ptrace() 对 PTRACE_TRACEME 的处理就是把当前进程标志为 PTRACE 状态 。
  • 当然事情不会这么简单 , 因为当一个进程被标记为 PTRACE 状态后 , 当调用 exec() 函数去执行一个外部程序时 , 将会暂停当前进程的运行 , 并且发送一个 SIGCHLD 给父进程 。父进程接收到 SIGCHLD 信号后就可以对被调试的进程进行调试 。
  • 我们来看看 exec() 函数是怎样实现上述功能的 , exec() 函数的执行过程为 sys_execve() -> do_execve() -> load_elf_binary():
static int load_elf_binary(struct linux_binprm * bprm, struct pt_regs * regs){...if (current->ptrace & PT_PTRACED)send_sig(SIGTRAP, current, 0);...}