ptrace使用

ptrace系统调用提供了一种方法让追踪者tracer对被追踪者tracee进行观察与控制,我们通过一些例子来进行讲解。

首先,我们看这么一段代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
#include <sys/ptrace.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
#include <sys/reg.h>
#include <stdio.h>
#include <signal.h>

int main()
{
pid_t pid;

pid = fork();

if (pid == 0) { // child process
printf("child process pid %ld\n", getpid());

char *argv[] = {"ls", NULL};
execv("/bin/ls", argv);
}
else { // parent process
printf("parent process pid %ld\n", getpid());

wait(NULL);
printf("parent process exit\n");
}

return 0;
}

大概说说这段代码做的事情。首先创建一个子进程,父进程调用wait函数等待子进程退出。子进程调用execv创建一个新的进程,并且在很短的时间内执行ls命令。子进程退出后,父进程跟着退出了。

所以,输出结果如下:

1
2
3
4
5
6
7
[root@97043d024896 test]# gcc ptrace.c
[root@97043d024896 test]# ./a.out
parent process pid 42037
child process pid 42038
a.out ptrace.c
parent process exit
[root@97043d024896 test]#

我们来看看strace信息:

1
2
3
4
5
6
7
8
9
10
strace -f ./a.out

[pid 49922] exit_group(0) = ?
[pid 49922] +++ exited with 0 +++
<... wait4 resumed>NULL, 0, NULL) = 49922
--- SIGCHLD {si_signo=SIGCHLD, si_code=CLD_EXITED, si_pid=49922, si_uid=0, si_status=0, si_utime=0, si_stime=0} ---
write(1, "parent process exit\n", 20parent process exit
) = 20
exit_group(0) = ?
+++ exited with 0 +++

其中,49922这个是子进程。可以看到,子进程退出前,父进程处于wait状态,当父进程收到SIGCHLD信号之后,父进程resume了,然后父进程退出了。

现在,让我们使用ptrace,看看进程的状态会发送什么变化:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
#include <sys/ptrace.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
#include <sys/reg.h>
#include <stdio.h>
#include <signal.h>

int main()
{
pid_t pid;

pid = fork();

if (pid == 0) { // child process
ptrace(PTRACE_TRACEME, 0, NULL, NULL);
printf("child process pid %ld\n", getpid());

char *argv[] = {"ls", NULL};
execv("/bin/ls", argv);
}
else { // parent process
printf("parent process pid %ld\n", getpid());

wait(NULL);
wait(NULL);

printf("parent process exit\n");
}

return 0;
}

我们在子进程里面调用了ptrace函数,并且设置了PTRACE_TRACEME,表示希望父进程跟踪这个子进程。运行结果如下:

1
2
3
4
[root@97043d024896 test]# ./a.out
parent process pid 43160
child process pid 43161

我们发现,子进程调用了execv之后,不会立马执行ls命令了。我们来看看进程的状态:

查看父进程的状态:

1
2
3
[root@97043d024896 test]# ps -aux | grep -w "43160" | grep -v "grep"
root 43160 0.0 0.0 4228 680 pts/7 S+ 13:16 0:00 ./a.out
[root@97043d024896 test]#

此时父进程的状态是S+,表示处于睡眠状态。

查看子进程的状态:

1
2
3
[root@97043d024896 test]# ps -aux | grep -w "43161" | grep -v "grep"
root 43161 0.0 0.0 416 4 pts/7 t+ 13:16 0:00 ls
[root@97043d024896 test]#

此时子进程的状态是t+,表示处于暂停状态。要么是因为一个job control信号,要么是因为它正在被追踪。我们这个场景就是属于被追踪。

strace信息如下:

1
2
3
4
5
6
7
8
child process pid 51875
fstat(1, {st_mode=S_IFCHR|0620, st_rdev=makedev(136, 7), ...}) = 0
mmap(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7fb5a89b1000
--- SIGCHLD {si_signo=SIGCHLD, si_code=CLD_TRAPPED, si_pid=51875, si_uid=0, si_status=SIGTRAP, si_utime=0, si_stime=0} ---
write(1, "parent process pid 51874\n", 25parent process pid 51874
) = 25
wait4(-1, NULL, 0, NULL) = 51875
wait4(-1,

此时,没有子进程退出的状态。并且,父进程也处于wait信号的状态。

那么,如何让子进程恢复运行呢?代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
#include <sys/ptrace.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
#include <sys/reg.h>
#include <stdio.h>
#include <signal.h>

int main()
{
pid_t pid;

pid = fork();

if (pid == 0) { // child process
ptrace(PTRACE_TRACEME, 0, NULL, NULL);
printf("child process pid %ld\n", getpid());

char *argv[] = {"ls", NULL};
execv("/bin/ls", argv);
}
else { // parent process
printf("parent process pid %ld\n", getpid());

wait(NULL);

ptrace(PTRACE_CONT, pid, NULL, NULL);

wait(NULL);

printf("parent process exit\n");
}

return 0;
}

结果如下:

1
2
3
4
5
[root@97043d024896 test]# ./a.out
parent process pid 49313
child process pid 49314
a.out ptrace.c
parent process exit

此时,子进程成功的执行了ls命令。

strace信息如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
child process pid 52573
getpid() = 52572
--- SIGCHLD {si_signo=SIGCHLD, si_code=CLD_TRAPPED, si_pid=52573, si_uid=0, si_status=SIGTRAP, si_utime=0, si_stime=0} ---
fstat(1, {st_mode=S_IFCHR|0620, st_rdev=makedev(136, 7), ...}) = 0
mmap(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7f6eb99d8000
write(1, "parent process pid 52572\n", 25parent process pid 52572
) = 25
wait4(-1, NULL, 0, NULL) = 52573
ptrace(PTRACE_CONT, 52573, NULL, SIG_0) = 0
wait4(-1, a.out ptrace.c
NULL, 0, NULL) = 52573
--- SIGCHLD {si_signo=SIGCHLD, si_code=CLD_EXITED, si_pid=52573, si_uid=0, si_status=0, si_utime=0, si_stime=0} ---
write(1, "parent process exit\n", 20parent process exit
) = 20
exit_group(0) = ?
+++ exited with 0 +++

我们发现,父进程收到了两次SIGCHLD信号,说明子进程执行完ls命令之后退出了。

那么,这里子进程暂停的原理是什么呢?在ptrace_event这个函数里面:

1
2
3
4
5
6
7
8
9
10
11
static inline void ptrace_event(int event, unsigned long message)
{
if (unlikely(ptrace_event_enabled(current, event))) {
current->ptrace_message = message;
ptrace_notify((event << 8) | SIGTRAP);
} else if (event == PTRACE_EVENT_EXEC) {
/* legacy EXEC report via SIGTRAP */
if ((current->ptrace & (PT_PTRACED|PT_SEIZED)) == PT_PTRACED)
send_sig(SIGTRAP, current, 0);
}
}

我们发现,如果是exec的话,就会发一个SIGTRAP信号给当前进程。