[monitor] 9. Linux ptrace(程序调试器原理)
来源:互联网 发布:拼豆制作软件 编辑:程序博客网 时间:2024/05/21 17:16
1、ptrace概念
你一定知道linux下大名鼎鼎的程序调试工具gdb,但你可能没有听说过ptrace。Ptrace是linux一个标准系统调用,是gdb实现程序调试的核心。
ptrace能让一个进程实现对另一个进程的调试,主进程可以对被调试进程进行一系列控制动作:可以让被调试进程在进入/退出系统调用时断点,可以对被调试进程的任何位置插入调试断点,可以控制被调试进程单步执行,可以读取/写入被调试进程的寄存器,可以读取/写入被调试进程的堆栈内容。
根据ptrace提供的这一系列功能,gdb实现了各个调试功能。还有一系列的调试工具利用ptrace的功能,如strace、ltrace、pstask等等。
2、ptrace实现
ptrace的函数原型如下:
实现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模式。
2.2、PTRACE_ATTACH
PTRACE_TRACEME命令让被调试进程本身进入trace模式。PTRACE_ATTACH是调试进程让被调试进程进入trace模式。
2.3、PTRACE_SYSCALL
PTRACE_SYSCALL设置被调试进程在进入/退出系统调用时被断掉,即进程暂停运行并发送信号给调试主进程。
2.4、PTRACE_CONT
PTRACE_CONT使已经被调试器暂停掉或者断掉的进程继续执行。
2.5、PTRACE_SINGLESTEP
PTRACE_SINGLESTEP设置进程的标志寄存器为单步模式并让被调试进程继续执行。被调试进程执行完一条指令后,触发int1异常,并发信号给控制进程,把控制权交给主进程。
在内核探针kprobe中也使用了int1单步机制,可以参考相关实现。
下面是被调试进程单步执行完后,进入int1异常的处理过程。
在本进程的信号处理函数中将本进程置为停工,并发送信号给父进程。
2.6、PTRACE_PEEKDATA
PTRACE_PEEKDATA读取进程虚拟地址空间的任意数据。
2.7、PTRACE_POKEDATA
PTRACE_POKEDATA设置进程虚拟地址空间的任意数据。
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, ®s); /**//* 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, ®s); ptrace(PTRACE_DETACH, traced_process, NULL, NULL); return 0;}
上面的程序将把三个byte的内容进行替换以执行trap指令,等被调试进程停滞以后,我们把原指令再替换回来并把eip修改为原来的值。下面的图中演示了指令的执行过程
- 进程停滞后
- 替换入trap指令
- 3.断点成功,控制权交给了调试器
- 继续运行,将原指令替换回来并将eip复原
2.8、PTRACE_PEEKUSR
PTRACE_PEEKUSR读取单个寄存器。
2.9、PTRACE_POKEUSR
PTRACE_POKEUSR修改单个寄存器。
注意配置命令最好在进程暂停的情况下操作。
2.10、PTRACE_GETREGS
PTRACE_GETREGS获取所有cpu寄存器的内容。
2.11、PTRACE_SETREGS
PTRACE_SETREGS设置所有cpu寄存器的内容。
注意配置命令最好在进程暂停的情况下操作。
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。可以看到形如:
这就是在执行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程序。
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用来显示运行中函数的堆栈调用情况。
其是实质上也是用ptrace来实现的,首先用PTRACE_ATTACH停住被查看程序,然后尝试从”/proc/pid/exe”中解析出程序elf中的符号表,再通过PTRACE_PEEKUSER读出程序的堆栈指针,通过PTRACE_PEEKDATA读出堆栈的数据,根据堆栈数据在符号表中查询,解析出程序的整个堆栈调用关系。随后PTRACE_DETACH恢复程序的运行。
- [monitor] 9. Linux ptrace(程序调试器原理)
- 调试器原理之ptrace调用学习
- [Pthread] Linux上程序调试的基石(一)--ptrace
- [Pthread] Linux上程序调试的基石(一)--ptrace
- Linux上程序调试的基石(1)--ptrace
- Linux上程序调试的基石(1)--ptrace
- Linux上程序调试的基石(1)--ptrace
- Linux上程序调试的基石(1)--ptrace
- linux应用程序调试的基石---ptrace
- 调试器的原理-详解ptrace函数及fork父子进程跟踪实例
- linux之使用ptrace 跟踪多线程程序
- linux ptrace
- linux ptrace
- QTCreater调试提示ptrace
- ptrace基于地址调试
- ptrace基于行数调试
- ptrace 跟踪多线程程序
- linux ptrace函数
- Knn算法(机器学习)入门(Python实现)
- Integer.MIN_VALUE和Integer.MAX_VALUE
- 阿里云CentOS7中安装MySQL5.7.19
- maven nexus 和 热部署
- iOS 一一 触摸事件和手势
- [monitor] 9. Linux ptrace(程序调试器原理)
- 2017_10_13 Shell&基本数据类型
- PAT——1040. 有几个PAT
- 【20171014】python_语言设计(9)交互式图形编程
- Windows7的基础上安装CentOS双系统
- python2.7 MySQLdb库的安装
- poj 2349 Arctic Network
- 静态方法:无法在静态上下文中引用非静态
- python学习:列表