4. 跟踪系统调用——strace¶
在前面的文章我们了解了gdb调试工具,今天我们来了解一下新的调试工具,它虽然不能与 gdb一样跟踪程序的运行,调试程序运行时的环境,如查看栈的内容与函数内部的变量等信 息,但它依旧是一个非常强大的工具,我们一般用来跟踪任何程序的系统调用,没错,是任何 程序,比如Linux系统中自带的命令,或者是其他不开源的可执行文件,strace都可以跟踪他 们的系统调用,下面我们就来简单了解一下strace这个工具。
4.1. strace简介¶
strace命令是一个集诊断、调试、统计与一体的工具,我们用它来监控用户空间进程和内核 的交互,比如对应用程序的系统调用、信号传递与进程状态变更等进行跟踪与分析,以达到解 决问题或者是了解应用工作过程的目的。这个简单而又强大的工具几乎在所有的Linux操作系 统上都可用,足以见证其强大之处。
我们知道Linux的前身是Unix,Unix的设计由大量的函数调用组成,比如包括一些简单的操 作,打开一个文件,读写文件,关闭文件等,这写就是系统调用,而Linux也是如此,拥有大 量系统提供的函数接口。在Linux世界,进程不能直接访问硬件设备,当进程需要访问硬件设 备(比如读取磁盘文件,接收网络数据等等)时,必须由用户态模式切换至内核态模式,通过 系统调用访问硬件设备。所有的Linux应用程序都是通过调用操作系统提供的函数来完成它们 的任务,这也是我们常说的应用编程,我们不需要去了解操作系统到底在干嘛,我们只需要知 道通过这些系统调用就可以完成我们的操作,比如向串口写入数据,那么就将串口设备打开, 然后写入数据,最后关闭串口设备等。
那么如果程序出现问题,我们不知道问题出现在哪里,而strace工具就派上用场了,使用 strace工具,我们可以清楚地看到这些系统调用的过程、使用的参数、返回值与执行消耗的 时间等,还可以了解它们与操作系统之间的底层交互。
ps:strace底层使用内核的ptrace特性来实现其功能,有兴趣的同学可以去了解一下ptrace。
4.2. strace命令¶
strace是系统自带的命令,在绝大部分的linux系统中可以用它来调试应用程序,只需我们运 行一下strace命令就可以跟踪某个应用程序,在应用程序运行结束后它也会自动结束跟踪, 在命令执行的过程中,strace会记录和解析命令进程的所有系统调用以及这个进程所接收到 的所有的信号值,通过这些跟踪到的信息,你可以知道程序执行了那些系统调用,以何种顺序 执行,收到了什么信号等。
在linux下运行 strace -h
命令就可以看到strace命令用法的介绍,还有很多参数,由于太
多就不一一列举出来。
➜ ~ strace -h
usage: strace [-CdffhiqrtttTvVwxxy] [-I n] [-e expr]...
[-a column] [-o file] [-s strsize] [-P path]...
-p pid... / [-D] [-E var=val]... [-u username] PROG [ARGS]
or: strace -c[dfw] [-I n] [-e expr]... [-O overhead] [-S sortby]
-p pid... / [-D] [-E var=val]... [-u username] PROG [ARGS]
strace命令输出的格式参数:
ps:这些参数挑一些常用的记就好了,因为如果不记得运行
strace -h
命令就知道这些 参数的作用了,而以下的参数描述都是根据strace帮助文档整理的。
[-o file]:将strace的输出写入指定的文件。
[-s strsize]:将打印字符串的长度限制为strsize个字符(默认值为32)。
-d:输出strace关于标准错误的调试信息。
-r:打印出相对时间关于每一个系统调用。
-f:跟踪由fork()调用所产生的子进程。
-t:在输出中的每一行前加上时间信息。
-tt:在输出中的每一行前加上时间信息(微秒级)。
-T:打印在每个系统调用中花费的时间。
-r:打印相对时间戳。
-x:以十六进制打印非ascii字符串。
-xx:以十六进制打印所有字符串。
-y:打印与文件描述符参数关联的路径。
-yy:打印与套接字文件描述符关联的协议特定信息。
strace命令统计参数
-c:统计每一系统调用的所执行的时间,次数和出错的次数等。
-C:像-c一样,也可以打印常规输出。
-w:汇总系统调用延迟(默认为系统时间)
[-O overhead]:设置将系统调用跟踪到OVERUSE的开销
[-S sortby]:按以下顺序对系统调用计数:时间,调用,名称等
strace命令过滤参数
[-e expr]…:指定一个表达式,用来控制如何跟踪。格式如下:
[qualifier=][!]value1[,value2]...
。描述说明:
qualifier
只能是trace、abbrev、verbose、raw、signal、read、write
其中之一,value是用来限定的符号或数字,默认的 qualifier是trace。感叹号是否定符号,例如:-e open
等价于-e trace=open
,表示只跟踪open
调用;而-etrace!=open
表示跟踪除了open以外的其他调用,还有两个特殊的符号all
和none
,它们分别代表所有选项与没有选项。
strace命令过滤的例子:
-e trace=set
只跟踪指定的系统调用,例如:-e trace=open,close,rean,write
表示只跟踪这四个系统调用,默认的为set=all
。-e trace=file
只跟踪有关文件操作的系统调用。-e trace=process
只跟踪有关进程控制的系统调用。-e trace=network
跟踪与网络有关的所有系统调用。-e strace=signal
跟踪所有与信号有关的系统调用。-e trace=ipc
跟踪所有与进程通讯有关的系统调用。-e raw=set
将指定的系统调用的参数以十六进制显示。-e signal=se
t 指定跟踪的系统信号。默认为all(所有信号)。如signal=!SIGIO
或者signal=!io
,表示不跟踪SIGIO
信号。-e read=set
输出从指定文件中读出的数据。-e write=set
输出写入到指定文件中的数据。
4.3. strace实战¶
4.3.1. 跟踪ls命令¶
学习了那么多基本的知识,大家是不是迫不及待想试一下了?我们就跟踪一下基本的ls命令吧,它在我们linux系统 中应该是用得最多的命令了,我们可以看看在敲下ls命令的时候,系统做了什么东西:
友情提示一下,因为ls命令是列出当前路径下的所有文件,而strace命令会默认跟踪所有的系统调用,如果当前路径下有很多文件的话,可能会输出非常非常多的内容,不利于观察,我们可以新建一个测试的空目录,在空目录下跟踪ls命令到底做了什么事情。
# 创建空的测试目录并进入
➜ ~ mkdir mytest
➜ ~ cd mytest
# 跟踪ls命令
➜ mytest strace ls
# 输出内容
execve("/bin/ls", ["ls"], 0x7fffe987f830 /* 34 vars */) = 0
brk(NULL) = 0x55769774d000
access("/etc/ld.so.nohwcap", F_OK) = -1 ENOENT (No such file or directory)
access("/etc/ld.so.preload", R_OK) = -1 ENOENT (No such file or directory)
openat(AT_FDCWD, "/etc/ld.so.cache", O_RDONLY|O_CLOEXEC) = 3
fstat(3, {st_mode=S_IFREG|0644, st_size=95106, ...}) = 0
mmap(NULL, 95106, PROT_READ, MAP_PRIVATE, 3, 0) = 0x7efd08f12000
close(3) = 0
access("/etc/ld.so.nohwcap", F_OK) = -1 ENOENT (No such file or directory)
openat(AT_FDCWD, "/lib/x86_64-linux-gnu/libselinux.so.1", O_RDONLY|O_CLOEXEC) = 3
read(3, "\177ELF\2\1\1\0\0\0\0\0\0\0\0\0\3\0>\0\1\0\0\0\20b\0\0\0\0\0\0"..., 832) = 832
fstat(3, {st_mode=S_IFREG|0644, st_size=154832, ...}) = 0
mmap(NULL, 8192, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7efd08f10000
mmap(NULL, 2259152, PROT_READ|PROT_EXEC, MAP_PRIVATE|MAP_DENYWRITE, 3, 0) = 0x7efd08adb000
mprotect(0x7efd08b00000, 2093056, PROT_NONE) = 0
mmap(0x7efd08cff000, 8192, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_DENYWRITE, 3, 0x24000) = 0x7efd08cff000
mmap(0x7efd08d01000, 6352, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_ANONYMOUS, -1, 0) = 0x7efd08d01000
# 省略n行
close(3) = 0
close(1) = 0
close(2) = 0
exit_group(0) = ?
+++ exited with 0 +++
是不是没想到,这么简单的一个ls命令,内核居然敢了这么多事情,这还是省略了绝大部分的输出,大家可以自己参考终端的输出内容。
4.3.2. 跟踪并统计ls命令¶
或者我们可以统计一下ls命令调用了什么函数,可以使用以下命令:
➜ mytest strace -c ls
% time seconds usecs/call calls errors syscall
------ ----------- ----------- --------- --------- ----------------
0.00 0.000000 0 7 read
0.00 0.000000 0 11 close
0.00 0.000000 0 9 fstat
0.00 0.000000 0 17 mmap
0.00 0.000000 0 12 mprotect
0.00 0.000000 0 1 munmap
0.00 0.000000 0 3 brk
0.00 0.000000 0 2 rt_sigaction
0.00 0.000000 0 1 rt_sigprocmask
0.00 0.000000 0 2 ioctl
0.00 0.000000 0 8 8 access
0.00 0.000000 0 1 execve
0.00 0.000000 0 2 getdents
0.00 0.000000 0 2 2 statfs
0.00 0.000000 0 1 arch_prctl
0.00 0.000000 0 1 set_tid_address
0.00 0.000000 0 9 openat
0.00 0.000000 0 1 set_robust_list
0.00 0.000000 0 1 prlimit64
------ ----------- ----------- --------- --------- ----------------
100.00 0.000000 91 10 total
这里的输出很清楚的告诉我们调用了那些系统函数,调用次数多少,消耗了多少时间等等,这些信息对我们分析一个程序来说是非常有用的。
4.3.3. 重定向输出信息¶
strace命令在终端输出的信息太多了,我们想要将它重定向输出某个文件中(使用 [-o filename]
参数),
然后对文件进行分析,这样子的操作就比在终端上分析要好得多,但是需要注意的是,因为输出的信息很多,
生成的日志文件可能会很大,所以在日常使用中要注意设置过滤,不需要完全跟踪所有的内容。
重定向输出信息命令如下:
➜ mytest strace -o ls.log ls
ls.log
➜ mytest strace -c -o ls.log ls
ls.log
生成ls.log文件位于当前路径下,可以使用cat命令打开。
ps:-o filename 参数中的文件名filename可以随意命名,无需后缀也是可以的,毕竟Linux一切皆文件。
4.3.4. 跟踪自己的代码¶
我们可以使用核心转储章节的代码,代码位置: embed_linux_tutorial/base_code/linux_debug/core_dump
,
编译后使用strace命令去跟踪它,看看遇到错误是怎么样的情况。
操作如下:
# 编译
➜ core_dump git:(dev_jie) ✗ make
gcc -o core_dump.o -c -g -Werror -I. -Iinclude -static core_dump.c -g -MD -MF .core_dump.o.d
gcc -o targets core_dump.o -g -Werror -I. -Iinclude -static
# 跟踪
➜ core_dump git:(dev_jie) ✗ strace ./targets
execve("./targets", ["./targets"], 0x7ffe4c007260 /* 34 vars */) = 0
brk(NULL) = 0x1d57000
brk(0x1d581c0) = 0x1d581c0
arch_prctl(ARCH_SET_FS, 0x1d57880) = 0
uname({sysname="Linux", nodename="embedfire_dev", ...}) = 0
readlink("/proc/self/exe", "/home/xxxxx/embed_linux_tutoria"..., 4096) = 73
brk(0x1d791c0) = 0x1d791c0
brk(0x1d7a000) = 0x1d7a000
access("/etc/ld.so.nohwcap", F_OK) = -1 ENOENT (No such file or directory)
fstat(1, {st_mode=S_IFCHR|0620, st_rdev=makedev(136, 0), ...}) = 0
write(1, "\350\277\231\346\230\257\344\270\200\344\270\252\351\224\231\350\257\257\n", 19这是一个错误
) = 19
--- SIGSEGV {si_signo=SIGSEGV, si_code=SEGV_MAPERR, si_addr=NULL} ---
+++ killed by SIGSEGV +++
[1] 12576 segmentation fault strace ./targets
从日志信息可以看出,它是被SIGSEGV终止的,并且产生了段错误 segmentation fault
。除此之外,
代码的运行调用了一些系统调用,比如调用execve()函数创建了一个新的进程 ./targets
、查看了当前系统的信息,
系统名,主机名、打开文件描述符编号为1的输出文件,并且向该文件描述符中写入打印的内容等。
我们也能使用 -T
参数查看每个系统调用的时间(在输出的最右边 <>
的内容就是时间),当然大家可以用 -t -tt
等参数来测试一下:
➜ core_dump git:(dev_jie) ✗ strace -T ./targets
execve("./targets", ["./targets"], 0x7ffc6c9cda78 /* 34 vars */) = 0 <0.000119>
brk(NULL) = 0x1d65000 <0.000006>
brk(0x1d661c0) = 0x1d661c0 <0.000007>
arch_prctl(ARCH_SET_FS, 0x1d65880) = 0 <0.000005>
uname({sysname="Linux", nodename="embedfire_dev", ...}) = 0 <0.000005>
readlink("/proc/self/exe", "/home/jiejie/embed_linux_tutoria"..., 4096) = 73 <0.000023>
# 省略后续输出
本章跟踪进程的讲解就到此结束,更多实战的内容大家可以自行去尝试。