<返回更多

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

2022-04-08    linux上的码农
加入收藏
一篇掌握GDB调试程序的核心技术-ptrace系统调用与使用示例

 

 

前言:在程序出现bug的时候,最好的解决办法就是通过 GDB 调试程序,然后找到程序出现问题的地方。比如程序出现 段错误(内存地址不合法)时,就可以通过 GDB 找到程序哪里访问了不合法的内存地址而导致的。 本文不是介绍GDB不是使用方式,而是大概介绍 GDB 的实现原理,当然是 GDB 是一个庞大而复杂的项目,不可能只通过一篇文章就能解释清楚,所以本文主要是介绍 GDB 使用的核心的技术 - ptrace。

 

一,ptrace系统调用

long ptrace(enum __ptrace_request request,  pid_t pid, void *addr,  void *data);

下面解释一下 ptrace() 各个参数的作用:

  1. request:指定调试的指令,指令的类型很多,如:PTRACE_TRACEME、PTRACE_PEEKUSER、PTRACE_CONT、PTRACE_GETREGS等等,下面会介绍不同指令的作用。
  2. pid:进程的ID(这个不用解释了)。
  3. addr:进程的某个地址空间,可以通过这个参数对进程的某个地址进行读或写操作。
  4. data:根据不同的指令,有不同的用途,下面会介绍。

二,ptrace使用示例

#include <sys/ptrace.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
#include <sys/user.h>
#include <stdio.h>
int main()
{   pid_t child;
    struct user_regs_struct regs;

    child = fork();  // 创建一个子进程
    if(child == 0) { // 子进程
        ptrace(PTRACE_TRACEME, 0, NULL, NULL); // 表示当前进程进入被追踪状态
        execl("/bin/ls", "ls", NULL);          // 执行 `/bin/ls` 程序
    } 
    else { // 父进程
        wait(NULL); // 等待子进程发送一个 SIGCHLD 信号
        ptrace(PTRACE_GETREGS, child, NULL, ®s); // 获取子进程的各个寄存器的值
        printf("Register: rdi[%ld], rsi[%ld], rdx[%ld], rax[%ld], orig_rax[%ld]n",
                regs.rdi, regs.rsi, regs.rdx,regs.rax, regs.orig_rax); // 打印寄存器的值
        ptrace(PTRACE_CONT, child, NULL, NULL); // 继续运行子进程
        sleep(1);
    }
    return 0;
}
Register: rdi[0], rsi[0], rdx[0], rax[0], orig_rax[59]
ptrace  ptrace.c

 

更多linux内核视频教程文档资料免费领取后台私信【内核】自行获取.

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

 

Linux内核源码/内存调优/文件系统/进程管理/设备驱动/网络协议栈-学习视频教程-腾讯课堂

 

下面解释一下上面程序的执行流程:

  1. 主进程调用 fork() 系统调用创建一个子进程。
  2. 的进程调用 ptrace(PTRACE_TRACEME,...) 把自己设置为被追踪状态,并且调用 execl() 执行 /bin/ls 程序。
  3. 被设置为追踪(TRACE)状态的子进程执行 execl() 的程序后,会向父进程发送 SIGCHLD 信号,并且暂停自身的执行。
  4. 父进程通过调用 wait() 接收子进程发送过来的信号,并且开始追踪子进程。
  5. 父进程通过调用 ptrace(PTRACE_GETREGS, child, ...) 来获取到子进程各个寄存器的值,并且打印寄存器的值。
  6. 父进程通过调用 ptrace(PTRACE_CONT, child, ...) 让子进程继续执行下去。

 

三,ptrace实现原理

本文使用的 Linux 2.4.16 版本的内核

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;
}

ptrace() 支持的所有 request 操作定义在 linux-2.4.16/include/linux/ptrace.h 文件中,如下:

#define PTRACE_TRACEME         0
#define PTRACE_PEEKTEXT        1
#define PTRACE_PEEKDATA        2
#define PTRACE_PEEKUSR         3
#define PTRACE_POKETEXT        4
#define PTRACE_POKEDATA        5
#define PTRACE_POKEUSR         6
#define PTRACE_CONT            7
#define PTRACE_KILL            8
#define PTRACE_SINGLESTEP      9
#define PTRACE_ATTACH       0x10
#define PTRACE_DETACH       0x11
#define PTRACE_SYSCALL        24
#define PTRACE_GETREGS        12
#define PTRACE_SETREGS        13
#define PTRACE_GETFPREGS      14
#define PTRACE_SETFPREGS      15
#define PTRACE_GETFPXREGS     18
#define PTRACE_SETFPXREGS     19
#define PTRACE_SETOPTIONS     21

进入被追踪模式(PTRACE_TRACEME操作)

  1. 被调试的进程调用 ptrace(PTRACE_TRACEME, ...) 来使自己进入被追踪模式。
  2. 调试进程(如GDB)调用 ptrace(PTRACE_ATTACH, pid, ...) 来使指定的进程进入追踪模式。
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;
    }
    ...
}
static int load_elf_binary(struct linux_binprm * bprm, struct pt_regs * regs)
{
    ...
    if (current->ptrace & PT_PTRACED)
        send_sig(SIGTRAP, current, 0);
    ...
}

int do_signal(struct pt_regs *regs, sigset_t *oldset) 
{
    for (;;) {
        unsigned long signr;

        spin_lock_irq(¤t->sigmask_lock);
        signr = dequeue_signal(¤t->blocked, &info);
        spin_unlock_irq(¤t->sigmask_lock);

        // 如果进程被标记为 PTRACE 状态
        if ((current->ptrace & PT_PTRACED) && signr != SIGKILL) {
            /* 让调试器运行  */
            current->exit_code = signr;
            current->state = TASK_STOPPED;   // 让自己进入停止运行状态
            notify_parent(current, SIGCHLD); // 发送 SIGCHLD 信号给父进程
            schedule();                      // 让出CPU的执行权限
            ...
        }
    }
}

上面的代码主要做了3件事:

  1. 如果当前进程被标记为 PTRACE 状态,那么就使自己进入停止运行的状态。
  2. 发送 SIGCHLD 信号给父进程。
  3. 让出 CPU 的执行权限,使 CPU 执行其他进程。
一篇掌握GDB调试程序的核心技术-ptrace系统调用与使用示例

 

 

asmlinkage int sys_ptrace(long request, long pid, long addr, long data)
{
    ...
    switch (request) {
    case PTRACE_PEEKTEXT:
    case PTRACE_PEEKDATA: {
        unsigned long tmp;
        int copied;

        copied = access_process_vm(child, addr, &tmp, sizeof(tmp), 0);
        ret = -EIO;
        if (copied != sizeof(tmp))
            break;
        ret = put_user(tmp, (unsigned long *)data);
        break;
    }
    ...
}
struct mm_struct {
    ...
    pgd_t *pgd; /* 页目录指针 */
    ...
}
一篇掌握GDB调试程序的核心技术-ptrace系统调用与使用示例

 

单步调试模式(PTRACE_SINGLESTEP)

asmlinkage int sys_ptrace(long request, long pid, long addr, long data)
{
    ...
    switch (request) {
    case PTRACE_SINGLESTEP: {  /* set the trap flag. */
        long tmp;
        ...
        tmp = get_stack_long(child, EFL_OFFSET) | TRAP_FLAG;
        put_stack_long(child, EFL_OFFSET, tmp);
        child->exit_code = data;
        /* give it a chance to run. */
        wake_up_process(child);
        ret = 0;
        break;
    }
    ...
}
一篇掌握GDB调试程序的核心技术-ptrace系统调用与使用示例

 

tmp = get_stack_long(child, EFL_OFFSET) | TRAP_FLAG;
put_stack_long(child, EFL_OFFSET, tmp);
  1. 获取进程的 eflags 寄存器的值,并且设置 Trap Flag 标志。
  2. 把新的值设置到进程的 eflags 寄存器中。
一篇掌握GDB调试程序的核心技术-ptrace系统调用与使用示例

 

 

四,小结

声明:本站部分内容来自互联网,如有版权侵犯或其他问题请与我们联系,我们将立即删除或处理。
▍相关推荐
更多资讯 >>>