使用ptrace跟踪进程
系统调用ptrace对gdb这种调试器来说是非常重要的,杯具的是,相关的文档却残缺不详–除非你觉得最好的文档就是内核源码!!下面,我会试着向大家展示ptrace在gdb这类工具中的作用.
1. 介绍
ptrace()是一个系统调用,它允许一个进程控制另外一个进程的执行.不仅如此,我们还可以借助于ptrace修改某个进程的空间(内存或寄存器),任何传递给一个进程(即被跟踪进程)的信号(除了会直接杀死进程的SIGKILL信号)都会使得这个进程进入暂停状态,这时系统通过wait()通知跟踪进程,这样,跟踪进程就可以修改被跟踪进程的行为了.
如果跟踪进程在被跟踪进程的内存中设置了相关的事件标志位,那么运行中被跟踪进程也可能因为特殊的事件而暂停.跟踪结束后,跟踪进程甚至可以通过设置被跟踪进程的退出码(exit code)来杀死它,当然也可以让它继续运行.
注意: ptrace()是高度依赖于底层硬件的.使用ptrace的程序通常不容易在个钟体系结构间移植.
2. 细节
ptrace的原型如下:
1.
#include
<sys/ptrace.h>
2.
long
int
ptrace(
enum
__ptrace_request
request, pid_t pid,
void
*
addr,
void
*
data)
我们可以看到,ptrace有4个参数,其中,request决定ptrace做什么,pid是被跟踪进程的ID,data存储从进程空间偏移量为addr的地方开始将被读取/写入的数据.
父进程可以通过将request设置为PTRACE_TRACEME,以此跟踪被fork出来的子进程,它同样可以通过使用PTRACE_ATTACH来跟踪一个已经在运行的进程.
2.1 ptrace如何工作
不管ptrace是什么时候被调用的,它首先做的就是锁住内核.在ptrace返回前,内核会被解锁.在这个过程中,ptrace是如何工作的呢,我们看看request值(译注: request的可选值值是定义在/usr/include/sys/ptrace.h中的宏)代表的含义吧.
PTRACE_TRACEME:
PTRACE_TRACEME是被父进程用来跟踪子进程的.正如前面所说的,任何信号(除了SIGKILL),不管是从外来的还是由exec系统调用产生的,都将使得子进程被暂停,由父进程决定子进程的行为.在request为PTRACE_TRACEME情况下,ptrace()只干一件事,它检查当前进程的ptrace标志是否已经被设置,没有的话就设置ptrace标志,除了request的任何参数(pid,addr,data)都将被忽略.
PTRACE_ATTACH:
request为PTRACE_ATTACH也就意味着,一个进程想要控制另外一个进程.需要注意的是,任何进程都不能跟踪控制起始进程init,一个进程也不能跟踪自己.某种意义上,调用ptrace的进程就成为了ID为pid的进程的’父’进程.但是,被跟踪进程的真正父进程是ID为getpid()的进程.
PTRACE_DETACH:
用来停止跟踪一个进程.跟踪进程决定被跟踪进程的生死.PTRACE_DETACH会恢复PTRACE_ATTACH和PTRACE_TRACEME的所有改变.父进程通过data参数设置子进程的退出状态(exit code).子进程的ptrace标志就被复位,然后子进程被移到它原来所在的任务队列中.这时候,子进程的父进程的ID被重新写回子进程的父进程标志位.可能被修改了的single-step标志位也会被复位.最后,子进程被唤醒,貌似神马都没有发生过;参数addr会被忽略.
PTRACE_PEEKTEXT, PTRACE_PEEKDATA, PTRACE_PEEKUSER:
这些宏用来读取子进程的内存和用户态空间(user space).PTRACE_PEEKTEXT和PTRACE_PEEKDATA从子进程内存读取数据,两者功能是相同的.PTRACE_PEEKUSER从子进程的user space读取数据.它们读一个字节的数据,保存在临时的数据结构中,然后使用put_user()(它从内核态空间读一个字符串到用户态空间)将需要的数据写入参数data,返回0表示成功.
对PTRACE_PEEKTEXT和PTRACE_PEEKDATA而言,参数addr是子进程内存中将被读取的数据的地址.对PTRACE_PEEKUSER来说,参数addr是子进程用户态空间的偏移量,此时data被无视.
PTRACE_POKETEXT, PTRACE_POKEDATA, PTRACE_POKEUSER:
这些宏行为与上面的几个是类似的.唯一的不同是它们用来写入data.(译注: 这段文字与上面的描述差不多,为免繁复,不译.)
PTRACE_SYSCALL, PTRACE_CONT:
这些宏用来唤醒暂停的子进程.在每次系统调用之后,PTRACE_SYSCALL使子进程暂停,而PTRACE_CONT让子进程继续运行.子进程的返回状态都是由ptrace()参数data设置的.但是,这只限于返回状态是有效的情况.ptrace()重置子进程的single-step位,设置/复位syscall-trace位,然后唤醒子进程;参数addr被无视.
PTRACE_SINGLESTEP:
PTRACE_SINGLESTEP的行为与PTRACE_SYSCALL无异,除了子进程在每次机器指令后都被暂停(PTRACE_SYSCALL是使子进程每次在系统调用后被暂停).single-step会被设置,跟PTRACE_SYSCALL一样,参数data包含返回状态,参数addr被无视.
PTRACE_KILL:
PTRACE_KILL被用来终止子进程.”谋杀”是这样进行的: 首先ptrace() 查看子进程是不是已经死了.如果不是, 子进程的返回码被设置为sigkill. single-step位被复位.然后子进程被唤醒,运行到返回码时子进程就死掉了.
2.2 更加依赖于硬件的调用.
上面讨论的request可选值是依赖于操作系统所在的体系结构和实现的.下面讨论的request可选值可以用来get/set子进程的寄存器,这更加依赖于系统架构.对寄存器的设置包括通用寄存器,浮点寄存器和扩展的浮点寄存器.
PTRACE_GETREGS, PTRACE_GETFPREGS, PTRACE_GETFPXREGS:
这些宏用来读子进程的寄存器.寄存器的值通过getreg()和__put_user()被读入data中;参数addr被无视.
PTRACE_SETREGS, PTRACE_SETFPREGS, PTRACE_SETFPXREGS:
跟上面的描述相反,这些宏被用来设置寄存器.
2.3 ptrace()的返回值
成功的ptrace()调用返回0.如果出错,将返回-1,errno也将被设置.PEEKDATA/PEEKTEXT,即使ptrace()调用成功,返回值也可能是-1,所以我们最好检查一下errno,它的可能值如下:
EPERM: 权限错误,进程无法被跟踪.
ESRCH: 目标进程不存在或者已经被跟踪.
EIO: 参数request的值无效,或者从非法的内存读/写数据.
EFAULT: 需要读/写数据的内存未被映射.
EIO和EFAULT真的很难区分,它们代表很严重的错误.
3. 小例子
如果你觉得上面的说明太枯燥了,好吧,我保证再也不这么干了.下面举个小例子,演示一下吧.
这是第一个,父进程对子进程中发生的每一次机器指令计数.
01.
#include
<stdio.h>
02.
#include
<stdlib.h>
03.
#include
<signal.h>
04.
#include
<syscall.h>
05.
#include
<sys/ptrace.h>
06.
#include
<sys/types.h>
07.
#include
<sys/wait.h>
08.
#include
<unistd.h>
09.
#include
<errno.h>
10.
11.
int
main(
void
)
12.
{
13.
long
long
counter
= 0;
/*
machine instruction counter */
14.
int
wait_val;
/*
child's return value */
15.
int
pid;
/*
child's process id */
16.
17.
puts
(
"Please
wait"
);
18.
19.
switch
(pid
= fork()) {
20.
case
-1:
21.
perror
(
"fork"
);
22.
break
;
23.
case
0:
/*
child process starts */
24.
ptrace(PTRACE_TRACEME,
0, 0, 0);
25.
/*
26.
*
must be called in order to allow the
27.
*
control over the child process
28.
*/
29.
execl(
"/bin/ls"
,
"ls"
,
NULL);
30.
/*
31.
*
executes the program and causes
32.
*
the child to stop and send a signal
33.
*
to the parent, the parent can now
34.
*
switch to PTRACE_SINGLESTEP
35.
*/
36.
break
;
37.
/*
child process ends */
38.
default
:
/*
parent process starts */
39.
wait(&wait_val);
40.
/*
41.
*
parent waits for child to stop at next
42.
*
instruction (execl())
43.
*/
44.
while
(wait_val
== 1407 ) {
45.
counter++;
46.
if
(ptrace(PTRACE_SINGLESTEP,
pid, 0, 0) != 0)
47.
perror
(
"ptrace"
);
48.
/*
49.
*
switch to singlestep tracing and
50.
*
release child
51.
*
if unable call error.
52.
*/
53.
wait(&wait_val);
54.
/*
wait for next instruction to complete */
55.
}
56.
/*
57.
*
continue to stop, wait and release until
58.
*
the child is finished; wait_val != 1407
59.
*
Low=0177L and High=05 (SIGTRAP)
60.
*/
61.
}
62.
printf
(
"Number
of machine instructions : %lld\n"
,
counter);
63.
return
0;
64.
}
运行一下代码吧(可能程序会很慢,哈哈.).
译注: 小小的解释下吧,子进程开始运行,调用exec后移花栽木,这时子进程的原进程(未调用exec之前的进程)因为要死了,会向父进程发送SIGTRAP信号.父进程一直阻塞等待(第一条wait(&wait_val);语句).父进程捕获到SIGTRAP信号,由此知道子进程已经结束了.接下来发生的就是最有趣的事情了,父进程通过request值为PTRACE_SINGLESTEP的ptrace调用,告诉操作系统,重新唤醒子进程,但是在每条机器指令运行之后暂停.再一次的,父进程阻塞等待子进程暂停(wait_val == 1407等价于WIFSTOPPED(wait_val) (个人看法,我还没有查到子进程状态的编码资料,求~))并计数,子进程结束(非暂停,对应的是WIFEXITED)后,父进程跳出loop循环.
4. 结论
ptrace()在调试器中是被用得很多的,它也可以被用来跟踪系统调用.调试器fork一个子进程并跟踪它,然后子进程exec调用要被调试的目标程序,在目标程序的每一次机器指令之后父进程都可以查看它的寄存器的值.在下一篇文章中,我会尽情展示ptrace的牛逼之处,春节快乐.