[monitor] 9. Linux ptrace(程序调试器原理)

来源:互联网 发布:拼豆制作软件 编辑:程序博客网 时间:2024/05/21 17:16

1、ptrace概念

你一定知道linux下大名鼎鼎的程序调试工具gdb,但你可能没有听说过ptrace。Ptrace是linux一个标准系统调用,是gdb实现程序调试的核心。

1

ptrace能让一个进程实现对另一个进程的调试,主进程可以对被调试进程进行一系列控制动作:可以让被调试进程在进入/退出系统调用时断点,可以对被调试进程的任何位置插入调试断点,可以控制被调试进程单步执行,可以读取/写入被调试进程的寄存器,可以读取/写入被调试进程的堆栈内容。
根据ptrace提供的这一系列功能,gdb实现了各个调试功能。还有一系列的调试工具利用ptrace的功能,如strace、ltrace、pstask等等。

2、ptrace实现

ptrace的函数原型如下:

2

实现ptrace的不同功能是由request命令字来指定的,常用的命令字有PTRACE_TRACEME、PTRACE_ATTACH、PTRACE_PEEKUSR、PTRACE_PEEKDATA、PTRACE_POKEDATA、PTRACE_SYSCALL、PTRACE_CONT、PTRACE_SINGLESTEP、PTRACE_GETFPREGS、PTRACE_SETFPREGS。

我们逐条来分析ptrace命令字的功能实现。

2.1、PTRACE_TRACEME

一个进程接受ptrace命令调试之前必须先进入trace模式,PTRACE_TRACEME命令让被调试进程本身进入trace模式。

3
4

2.2、PTRACE_ATTACH

PTRACE_TRACEME命令让被调试进程本身进入trace模式。PTRACE_ATTACH是调试进程让被调试进程进入trace模式。

5
6
7
8

2.3、PTRACE_SYSCALL

PTRACE_SYSCALL设置被调试进程在进入/退出系统调用时被断掉,即进程暂停运行并发送信号给调试主进程。

9
10
11
12
13
14
15
16
17

2.4、PTRACE_CONT

PTRACE_CONT使已经被调试器暂停掉或者断掉的进程继续执行。

18
19

2.5、PTRACE_SINGLESTEP

PTRACE_SINGLESTEP设置进程的标志寄存器为单步模式并让被调试进程继续执行。被调试进程执行完一条指令后,触发int1异常,并发信号给控制进程,把控制权交给主进程。

在内核探针kprobe中也使用了int1单步机制,可以参考相关实现。

20
21

下面是被调试进程单步执行完后,进入int1异常的处理过程。

22
23
24
25

在本进程的信号处理函数中将本进程置为停工,并发送信号给父进程。

26
27

2.6、PTRACE_PEEKDATA

PTRACE_PEEKDATA读取进程虚拟地址空间的任意数据。

28
29
30

2.7、PTRACE_POKEDATA

PTRACE_POKEDATA设置进程虚拟地址空间的任意数据。

31

2.7.1、设置断点

PTRACE_POKEDATA可以用来实现gdb的设置断点功能,具体的设置方法可以参考如下方法。

调试器是怎么设置断点的呢?通常是将当前将要执行的指令替换成trap指令,于是被调试的程序就会在这里停滞,这时调试器就可以察看被调试程序的信息了。被调试程序恢复运行以后调试器会把原指令再放回来。这里是一个例子:

#include sys/ptrace.h>#include sys/types.h>#include sys/wait.h>#include unistd.h>#include linux/user.h>const int long_size = sizeof(long);void getdata(pid_t child, long addr,             char *str, int len){    char *laddr;    int i, j;    union u ...{            long val;            char chars[long_size];    }data;    i = 0;    j = len / long_size;    laddr = str;    while(i  j) ...{        data.val = ptrace(PTRACE_PEEKDATA, child,                          addr + i * 4, NULL);        memcpy(laddr, data.chars, long_size);        ++i;        laddr += long_size;    }    j = len % long_size;    if(j != 0) ...{        data.val = ptrace(PTRACE_PEEKDATA, child,                          addr + i * 4, NULL);        memcpy(laddr, data.chars, j);    }    str[len] = '';}void putdata(pid_t child, long addr,             char *str, int len){    char *laddr;    int i, j;    union u ...{            long val;            char chars[long_size];    }data;    i = 0;    j = len / long_size;    laddr = str;    while(i  j) ...{        memcpy(data.chars, laddr, long_size);        ptrace(PTRACE_POKEDATA, child,               addr + i * 4, data.val);        ++i;        laddr += long_size;    }    j = len % long_size;    if(j != 0) ...{        memcpy(data.chars, laddr, j);        ptrace(PTRACE_POKEDATA, child,               addr + i * 4, data.val);    }}int main(int argc, char *argv[]){    pid_t traced_process;    struct user_regs_struct regs, newregs;    long ins;    /**//* int 0x80, int3 */    char code[] = ...{0xcd,0x80,0xcc,0};    char backup[4];    if(argc != 2) ...{        printf("Usage: %s  ",               argv[0], argv[1]);        exit(1);    }    traced_process = atoi(argv[1]);    ptrace(PTRACE_ATTACH, traced_process,           NULL, NULL);    wait(NULL);    ptrace(PTRACE_GETREGS, traced_process,           NULL, &regs);    /**//* Copy instructions into a backup variable */    getdata(traced_process, regs.eip, backup, 3);    /**//* Put the breakpoint */    putdata(traced_process, regs.eip, code, 3);    /**//* Let the process continue and execute       the int 3 instruction */    ptrace(PTRACE_CONT, traced_process, NULL, NULL);    wait(NULL);    printf("The process stopped, putting back "           "the original instructions ");    printf("Press  to continue ");    getchar();    putdata(traced_process, regs.eip, backup, 3);    /**//* Setting the eip back to the original       instruction to let the process continue */    ptrace(PTRACE_SETREGS, traced_process,           NULL, &regs);    ptrace(PTRACE_DETACH, traced_process,           NULL, NULL);    return 0;}

上面的程序将把三个byte的内容进行替换以执行trap指令,等被调试进程停滞以后,我们把原指令再替换回来并把eip修改为原来的值。下面的图中演示了指令的执行过程

    1. 进程停滞后
    1. 替换入trap指令

32

  • 3.断点成功,控制权交给了调试器
    1. 继续运行,将原指令替换回来并将eip复原

33

2.8、PTRACE_PEEKUSR

PTRACE_PEEKUSR读取单个寄存器。

34
35
36

2.9、PTRACE_POKEUSR

PTRACE_POKEUSR修改单个寄存器。
注意配置命令最好在进程暂停的情况下操作。

37
38

2.10、PTRACE_GETREGS

PTRACE_GETREGS获取所有cpu寄存器的内容。

39
40

2.11、PTRACE_SETREGS

PTRACE_SETREGS设置所有cpu寄存器的内容。
注意配置命令最好在进程暂停的情况下操作。

41
42

3、ptrace应用

3.1、gdb

从上面的实现看到ptrace可以对被调试进程进行一系列控制动作:可以让被调试进程在进入/退出系统调用时断点,可以对被调试进程的任何位置插入调试断点,可以控制被调试进程单步执行,可以读取/写入被调试进程的寄存器,可以读取/写入被调试进程的堆栈内容。

gdb利用ptrace的这些特性实现了对进程的调试功能。

3.2、strace

strace也是一个常用的调试工具,strace的功能是追踪程序的系统调用。我们不去分析strace的源码,而是用一段简单代码来说明strace的实现原理。如有这么一段程序:

HelloWorld.c:#include <stdio.h>int main(){    printf("Hello World!/n");    return 0;}

编译后,用strace跟踪: strace ./HelloWorld。可以看到形如:

43

这就是在执行HelloWorld中,系统所执行的系统调用,以及他们的返回值。

下面我们用ptrace来研究一下它是怎么实现的。

    switch(pid = fork())    {    case -1:        return -1;    case 0: //子进程        ptrace(PTRACE_TRACEME,0,NULL,NULL);        execl("./HelloWorld", "HelloWorld", NULL);    default: //父进程        wait(&val); //等待并记录execve        if(WIFEXITED(val))            return 0;        syscallID=ptrace(PTRACE_PEEKUSER, pid, ORIG_EAX*4, NULL);        printf("Process executed system call ID = %ld/n",syscallID);        ptrace(PTRACE_SYSCALL,pid,NULL,NULL);        while(1)        {            wait(&val); //等待信号            if(WIFEXITED(val)) //判断子进程是否退出                return 0;            if(flag==0) //第一次(进入系统调用),获取系统调用的参数            {                syscallID=ptrace(PTRACE_PEEKUSER, pid, ORIG_EAX*4, NULL);                printf("Process executed system call ID = %ld ",syscallID);                flag=1;            }            else //第二次(退出系统调用),获取系统调用的返回值            {                returnValue=ptrace(PTRACE_PEEKUSER, pid, EAX*4, NULL);                printf("with return value= %ld/n", returnValue);                flag=0;            }            ptrace(PTRACE_SYSCALL,pid,NULL,NULL);        }    }

在上面的程序中,fork出的子进程先调用了ptrace(PTRACE_TRACEME)表示子进程让父进程跟踪自己。然后子进程调用execl加载执行了HelloWorld。而在父进程中则使用wait系统调用等待子进程的状态改变。子进程因为设置了PTRACE_TRACEME而在执行系统调用被系统停止(设置为TASK_TRACED),这时父进程被唤醒,使用ptrace(PTRACE_PEEKUSER,pid,…)分别去读取子进程执行的系统调用ID(放在ORIG_EAX中)以及系统调用返回时的值(放在EAX中)。然后使用 ptrace(PTRACE_SYSCALL,pid,…)指示子进程运行到下一次执行系统调用的时候(进入或者退出),直到子进程退出为止。

程序的执行结果如下:

Process executed system call ID = 11Process executed system call ID = 45 with return value= 134520832Process executed system call ID = 192 with return value= -1208934400Process executed system call ID = 33 with return value= -2Process executed system call ID = 5 with return value= -2

其中,11号系统调用就是execve,45号是brk,192是mmap2,33是access,5是open…经过比对可以发现,和strace的输出结果一样。当然strace进行了更详尽和完善的处理,我们这里只是揭示其原理,感兴趣的同学可以去研究一下strace的实现。

3.3、ltrace

ltrace用来最终程序运行过程中对库函数的调用。我们用ltrace来调试上一个HelloWorld程序。

44

ltrace其实也是基于ptrace。我们知道,ptrace能够主要是用来跟踪系统调用,那么它是如何跟踪库函数呢?

首先ltrace打开elf文件,对其进行分析。在elf文件中,出于动态连接的需要,需要在elf文件中保存函数的符号,供连接器使用。具体格式,大家可以参考elf文件的格式。这样ltrace就能够获得该文件中,所有系统调用的符号,以及对应的执行指令。然后,ltrace将该指令所对应的4个字节,替换成断点。这样在进程执行到相应的库函数后,就可以通知到了ltrace,ltrace将对应的库函数打印出来之后,继续执行子进程。实际上ltrace与strace使用的技术大体相同,但ltrace在对支持fork和clone方面,不如strace。strace在收到frok和clone等系统调用后,做了相应的处理,而ltrace没有。

3.4、pstack

pstack用来显示运行中函数的堆栈调用情况。

45

其是实质上也是用ptrace来实现的,首先用PTRACE_ATTACH停住被查看程序,然后尝试从”/proc/pid/exe”中解析出程序elf中的符号表,再通过PTRACE_PEEKUSER读出程序的堆栈指针,通过PTRACE_PEEKDATA读出堆栈的数据,根据堆栈数据在符号表中查询,解析出程序的整个堆栈调用关系。随后PTRACE_DETACH恢复程序的运行。

原创粉丝点击