距离我上一篇研究ptrace的随笔http://www.cnblogs.com/zealotrouge/p/3544147.html已经过去半年了,最近不忙的时候抽空继续研究了下。同样,参考了Pradeep Padala的博文http://www.linuxjournal.com/article/6210,对其中断点部分比较感兴趣。因为从开始学习编程之日起,调试就是我们不可或缺的重要工具,而调试的基础就在于断点,那么,断点是如何让一个运行中的程序暂停的呢?背后的机制又是什么?为了探寻这个问题,我研究了上面的文章,并在自己的机器上(环境为Ubuntu12.04 + Intel x86_64 i5)动手实现了一下x64版本。
下面是整个问题研究的思路,按这个思路来给大家展示代码。首先,我们有一个简单的源程序tracedProcess.c,简单到仅仅是每隔1s输出一行"I'm running",这种简单的程序比较适合初学者分析汇编代码的语义,并找到容易设置断点的地方:
1 /* 2 tracedProcess.c 3 author: pengyiming 4 */ 5 6 #include <stdio.h> 7 8 void main() 9 { 10 while(1) 11 { 12 printf("I'm running "); 13 14 sleep(1); 15 } 16 }
gcc编译如上代码:
gcc -o tracedProcess.o tracedProcess.c
objdump分析目标文件tracedProcess.o,得到如下输出(截取main部分):
objdump -d tracedProcess.o 0000000000400544 <main>: 400544: 55 push %rbp 400545: 48 89 e5 mov %rsp,%rbp 400548: bf 5c 06 40 00 mov $0x40065c,%edi 40054d: e8 de fe ff ff callq 400430 <puts@plt> 400552: bf 01 00 00 00 mov $0x1,%edi 400557: b8 00 00 00 00 mov $0x0,%eax 40055c: e8 ef fe ff ff callq 400450 <sleep@plt> 400561: eb e5 jmp 400548 <main+0x4>
简单分析下:
0x400544 这个虚地址是main函数的入口
0x400544~0x400547 是所有函数的默认动作,新建一个函数栈
0x400548~0x40054c 将0x40065c传给edi寄存器,edi是字符串操作寄存器,存储的是字符串地址,0x40065c是只读区地址,用后面的getData()函数可以打出来,发现就是"I'm running"这个字符串
0x40054d~0x400551 callq执行一个函数调用,0x400430是此函数的入口,不难看出就是printf()函数链接到此目标文件的地址
0x400552~0x400556 清空edi寄存器
0x400557~0x40055b printf()无返回值,无需传递返回值地址给eax
0x40055c~0x400560 callq执行一个函数调用,sleep()
0x400561 跳转到0x400548进入下一个循环
分析完后,可以分析出,如果想把断点加在printf("I'm running ");这条语句上,我们可以把0x400548作为断点。
合适打断点的地址找到后,我们就要想想如何让程序暂停,继续参考上一篇博客中提到的Intel处理器开发手册,发现可以使用Trap指令使程序暂停运行。具体是使用int 0x80进入内核态,然后调用Trap指令——int3,只要CPU执行了这个指令,即可让程序暂停并处于一直等待状态,所以我们需要用ptrace在tracedProcess.o运行时,操作CPU寄存器和注入int 0x80 int3指令,一旦程序执行完int3,即可断点成功;当然,在断点后,我们希望程序能恢复运行,我们还需要备份CPU寄存器和原来代码段中的指令,以便之后的恢复。
小结一下,断点+恢复需要两次注入来实现:
一、断点注入步骤
(1)PTRACE_ATTACH附着被注入进程(会暂停被注入进程),备份当前的寄存器值
(2)备份注入地址处指令
(3)替换注入地址处指令为Trap指令
(4)PTRACE_CONT使被注入进程继续执行,直到执行完Trap指令
二、恢复注入步骤
(1)恢复之前的寄存器值(注:为了方便,所有的寄存器都备份了,实质上是为了恢复栈指针rsp&rbp和指令指针rip)
(2)恢复注入地址处指令
(3)PTRACE_DETACH使被注入进程继续执行,并脱离被注入进程
代码如下:
1 /* 2 ptrace4.c 3 author: pengyiming 4 description: 5 1, attach a test process, insert a break point 6 2, sleep for 5s then continue it 7 */ 8 9 #include <stdio.h> 10 #include <stdlib.h> 11 #include <string.h> 12 #include <sys/ptrace.h> 13 #include <sys/types.h> 14 #include <sys/wait.h> 15 #include <sys/reg.h> 16 #include <sys/user.h> 17 #include <sys/syscall.h> 18 #include <unistd.h> 19 20 #define WORD_SIZE sizeof(long) 21 22 static unsigned long injectAddress = 0x400548; 23 24 // converter long to char[] 25 union 26 { 27 long rawData; 28 char strData[WORD_SIZE]; 29 } converter; 30 31 void getData(pid_t pid, unsigned long dataAddr, unsigned long dataLen, char * const p_data) 32 { 33 // PEEKDATA counter 34 int counter = 0; 35 // PEEKDATA max count 36 int maxCount = dataLen / WORD_SIZE; 37 if (dataLen % WORD_SIZE != 0) 38 { 39 maxCount++; 40 } 41 // moving pointer 42 void * p_moving = p_data; 43 44 while (counter < maxCount) 45 { 46 memset(&converter, 0, WORD_SIZE); 47 converter.rawData = ptrace(PTRACE_PEEKDATA, pid, dataAddr + counter * WORD_SIZE, NULL); 48 49 memcpy(p_moving, converter.strData, WORD_SIZE); 50 p_moving += WORD_SIZE; 51 counter++; 52 } 53 p_data[dataLen] = '