什么是strace?
根据strace官网的描述,strace是一个Linux用户空间跟踪器,可用于诊断、调试和教学。我们用它来监控用户空间进程和内核之间的交互,比如系统调用、信令、进程状态变化等等。
strace 使用内核的 ptrace 特性来实现其功能。
在运维的日常工作中,故障处理和问题诊断是主要内容和必备技能。strace 可以作为跟踪故障过程的有效工具。就像侦探通过系统调用的痕迹告诉你异常的真相。
这次想分享一个实用的东西,就是写一个strace工具。
用过strace的同学都知道,strace是用来跟踪进程调用的系统调用的,还可以统计进程调用对系统调用的统计。strace有两种使用方式,如下:
strace的过程
strace -p 进程 pid
第一个用于跟踪要执行的程序,而第二个用于跟踪正在运行的进程。
下图是用 strace 跟踪 ls 命令的结果
BASH
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
|
$ strace ls
--- Process 15332 created
--- Process 15332 loaded C:\Windows\System32\ntdll.dll at 00007ffdd6fd0000
--- Process 15332 loaded C:\Windows\System32\kernel32.dll at 00007ffdd4660000
--- Process 15332 loaded C:\Windows\System32\KernelBase.dll at 00007ffdd40b0000
--- Process 15332 thread 2208 created
--- Process 15332 thread 9072 created
--- Process 15332 loaded D:\Program Files (x86)\cygwin64\bin\cygwin1.dll at 0000000180040000
--- Process 15332 loaded D:\Program Files (x86)\cygwin64\bin\cygintl-8.dll at 00000003c0ba0000
--- Process 15332 thread 13508 created
--- Process 15332 loaded D:\Program Files (x86)\cygwin64\bin\cygiconv-2.dll at 00000003cf8b0000
0 0 [main] ls (15332) **********************************************
42 42 [main] ls (15332) Program name: D:\Program Files (x86)\cygwin64\bin\ls.exe (windows pid 15332)
27 69 [main] ls (15332) OS version: Windows NT-10.0
24 93 [main] ls (15332) **********************************************
--- Process 15332 loaded C:\Windows\System32\advapi32.dll at 00007ffdd4500000
--- Process 15332 loaded C:\Windows\System32\msvcrt.dll at 00007ffdd6830000
--- Process 15332 loaded C:\Windows\System32\sechost.dll at 00007ffdd4a50000
--- Process 15332 loaded C:\Windows\System32\rpcrt4.dll at 00007ffdd48d0000
A lot is omitted here
19 24022 [main] ls 588 proc_terminate: nprocs 0
19 24041 [main] ls 588 proc_terminate: leaving
25 24066 [main] ls 588 pinfo::exit: Calling dlls.cleanup_forkables n 0x0, exitcode 0x0
21 24087 [main] ls 588 pinfo::exit: Calling ExitProcess n 0x0, exitcode 0x0
--- Process 15332 (pid: 588) thread 9072 exited with status 0x0
--- Process 15332 (pid: 588) thread 2208 exited with status 0x0
--- Process 15332 (pid: 588) thread 6340 exited with status 0x0
--- Process 15332 (pid: 588) thread 13508 exited with status 0x0
--- Process 15332 (pid: 588) exited with status 0x0
|
自己写一个 strace 的第一步是了解 ptrace() 系统调用的使用。我们来看看ptrace()系统调用的定义
BASH
1
|
int ptrace(long request, long pid, long addr, long data);
|
ptrace() 系统调用用于跟踪进程的运行状态。下面介绍其参数的含义:
请求:指定要跟踪的操作。换句话说,可以通过传入不同的请求参数,对进程进行不同的跟踪。可选值是:
PTRACE_TRACEME
PTRACE_PEEKTEXT
PTRACE_POKETEXT
PTRACE_CONT
PTRACE_SINGLESTEP
…
PID:指定要跟踪的进程的PID。
addr:指定要读取或修改的内存地址。
数据:对于不同的请求操作,数据有不同的作用,下面会分别介绍。
如前所述,有两种方法可以使用 strace 跟踪进程。一种是通过strace命令启动进程,另一种是通过-p指定要跟踪的进程。
ptrace() 系统调用还提供了两种“请求”来实现上述两种方式:
第一个是通过 ptrace_Trace
第二个是通过 PTRACE_ATTACH
本文主要介绍第一种方法。由于第一种方法使用跟踪器启动被跟踪程序,因此需要启动两个进程。通常,可以使用 fork() 系统调用来创建一个新进程,所以我们自然也使用了 fork() 系统调用。
我们新建一个文件strace.c,输入代码如下:
BASH
1
2
3
4
5
6
7
8
9
10
|
int main(int argc, char *argv[]){
pid_t child;
child = fork();
if (child == 0) {
// Child process
} else {
// Parent process
}
return 0;
}
|
上面的代码通过调用fork()创建了一个子进程,但是什么都不做。之后,我们将在子进程中运行被跟踪的程序,在父进程中运行跟踪进程代码。
运行跟踪的程序
如前所述,被跟踪的程序需要在子进程中运行。要运行程序,您可以调用 execl() 系统。因此,您可以通过以下代码在子进程中运行 ls 命令:
BASH
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
#include <unistd.h>
#include <stdlib.h>
int main(int argc, char *argv[]){
pid_t child;
child = fork();
if (child == 0) {
execl("/bin/ls", "/bin/ls", NULL);
exit(0);
} else {
// Parent process
}
return 0;
}
|
execl() 用于执行指定的程序。如果执行成功,则不会返回。因此,EXECL(…) 的下一行代码 exit(0) 将不会被执行。
由于我们需要跟踪ls命令,所以在执行ls命令之前必须调用ptrace(ptrace_trace,0,null,null)来告诉系统需要跟踪进程。代码如下:
BASH
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
#include <sys/ptrace.h>
#include <unistd.h>
#include <stdlib.h>
int main(int argc, char *argv[])
{
pid_t child;
child = fork();
if (child == 0) {
ptrace(PTRACE_TRACEME, 0, NULL, NULL);
execl("/bin/ls", "/bin/ls", NULL);
exit(0);
} else {
// Parent process
}
return 0;
}
|
这样就完成了被跟踪进程部分的代码,然后启动跟踪进程部分的代码。
编写跟踪过程代码
如果你编译运行上面的代码,你会发现没有任何效果。这是因为当子进程调用 ptrace (ptrace_trace, 0, null, null) 并调用 execl() 系统调用时,子进程会向父进程(跟踪进程)发送 SIGCHLD 信号并停止运行本身。直到父进程发送调试命令,它才会继续运行。
因为在上面的代码中,父进程(跟踪进程)并没有发送任何调试命令退出,所以子进程(被跟踪进程)没有运行就随父进程退出,所以你不会看到任何效果。
现在让我们开始编写代码来跟踪这个过程。
由于被跟踪进程会向跟踪进程发送 SIGCHLD 消息,所以我们必须首先在跟踪进程的代码中接收到 SIGCHLD 信号。接收信号通过“wait()”系统调用完成,代码如下:
BASH
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
|
#include <sys/ptrace.h>
#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>
int main(int argc, char *argv[])
{
pid_t child;
int status;
child = fork();
if (child == 0) {
ptrace(PTRACE_TRACEME, 0, NULL, NULL);
execl("/bin/ls", "/bin/ls", NULL);
exit(0);
} else {
wait(&status); // Receive SIGCHLD signal sent by subprocess
}
return 0;
}
|
上述代码调用“wait()”系统调