程序分析利器strace 简介 strace可以通过捕捉记录正在运行进程的系统调用和信号来进行程序分析,既然是系统调用,因此shell脚本和应用程序都可以通过strace来调试分析。
strace是一个非常方便有用的诊断,调试工具,我们不需要重新编译程序,通过strace追踪便可获得系统底层发生的文件管理,进程管理,内存分配,信号捕捉等事件。比如有的时候内存泄露我们无法从日志中发现异常,因此需要更加底层的内存分配管理调用来分析。
打印 在strace捕捉到每行记录中都包含系统调用名字,参数和返回值,也可以通过参数打印很多的调用细节,比如所属进程,调用开始和消耗时间。捕捉到的所有信息我们可以输出到标准输出,也可以输出到指定文件。
系统调用生命周期标记 在追踪多个进程和线程的系统调用过程中,当A系统调用执行一段时候后,此时B系统调用开始执行,我们通过unfinished
标记调用的开始,resumed
来标记调用的结束
1 2 3 16308 14 :47 :31.092116 mmap2(NULL , 19480576 , PROT_NONE, MAP_PRIVATE|MAP_ANONYMOUS|MAP_NORESERVE, -1 , 0 <unfinished ...>15606 14 :47 :31.092137 clock_gettime(CLOCK_REALTIME, <unfinished ...>16308 14 :47 :31.092153 <... mmap2 resumed> ) = 0x842e9000 <0.000022 >
结构体参数/返回值打印 结构体使用花括号括起来,并使用空格分离
比如clock_gettime
,函数原型为:
1 int clock_gettime (clockid_t clk_id, struct timespec *tp) ;
系统调用显示为:
1 2 3 15606 14 :47 :31.093987 clock_gettime(CLOCK_REALTIME, <unfinished ...>16308 14 :47 :31.094002 <... futex resumed> ) = 1 <0.000023 >15606 14 :47 :31.094019 <... clock_gettime resumed> {tv_sec=403766673093154435 , tv_nsec=7593356726744084332 }) = 0 <0.000023 > >>返回结果
最终timespec结构体结果打印为
1 {tv_sec=403766673093154435 , tv_nsec=7593356726744084332 }
数组成员打印 数组使用方括号括起来,并使用空格分离
比如execve
1 execve("/bin/sh", ["sh", "-c", "sleep 2"], [/* 30 vars */]) = 0 <0.000135>
使用 执行方式 strace有两种执行方式
第一种strace + 程序
,比如
1 strace sleep 5 >>strace +shell命令
或者
1 $ strace test-run >>strace +可执行二进制程序
或者
1 strace ./test.sh >>strace +shell脚本
第二种为 strace -p PID
,比如
有一个正在运行的程序./test-run
, 获取该程序的进程号
1 2 test/ $ pidof test-run 66725 66723
之后追踪此进程
1 2 3 $ sudo strace -p 66723 [10:26:57] strace: Process 66723 attached restart_syscall(<... resuming interrupted nanosleep ...>) = 0 write(1, "in father pid: 66723 to sleep\n", 31) = 31
选项 下面是最常用的使用搭配
1 strace -o output.txt -T -tt -p 28979 -f
按照使用频率来进行记录
-o 文件 指定日志输出文件,实际项目追踪过程中几分钟内会产生上百万行日志,如果输出到屏幕,可能会导致键盘控制无效或者控制终端崩溃等,也不利于浏览和过滤关键信息,因此最好将系统调用记录输出重定向到文件,但是进程执行的输出依旧显示在屏幕。
-T 在最右端打印系统调用时间
1 2 3 4 test/ $ strace -T sleep 2 ... nanosleep({2, 0}, NULL) = 0 <2.000731> >>系统调用时间为2.000731s ...
-tt 在最左端打印调用时的时间戳,微妙级
1 2 3 4 test/ $ strace -T -tt sleep 2 10:54:49.461790 nanosleep({2, 0}, NULL) = 0 <2.000594> 10:54:51.462440 close(1) = 0 <0.000016> 10:54:51.462500 close(2) = 0 <0.000014>
-f/-F -F同-f功能一致,都是打印进程号,包括fork的子进程和线程
1 2 3 4 5 6 7 8 9 10 test/ $ strace -T -tt -f test-run 11:10:23.994700 clone(child_stack=0, flags=CLONE_CHILD_CLEARTID|CLONE_CHILD_SETTID|SIGCHLD, child_tidptr=0x7f3fa30db9d0) = 70544 <0.000051> >>通过fork创建子进程,并返回子进程号 11:10:23.994772 getpid() = 70543 <0.000006> >>父进程获取自己的进程号 11:10:23.994794 write(1, "in father pid: 70543 start to sl"..., 36in father pid: 70543 start to sleep ) = 36 <0.000076> strace: Process 70544 attached >>strace开始追踪子进程70544 [pid 70543] 11:10:23.994981 nanosleep({10, 0}, <unfinished ...> >>父进程开始睡眠 [pid 70544] 11:10:23.995047 write(1, "in child pid: 70544 start to sle"..., 35in child pid: 70544 start to slepp >>子进程系统调用 ) = 35 <0.000024>
-ff 将追踪的多进程的系统调用记录重定向到不同的文件,需要搭配-o 使用,比如指定 -o output.txt, 会生成output.txt.PID,但是进程执行的输出依旧显示在屏幕
1 2 3 4 5 6 7 test/ $ strace -T -tt -f -o output.txt test-run >>进程打印 START TO RUN SUB_MODULE: fork >>开始fork子进程 in father pid: 71278 start to sleep >>父进程pid为71278 in child pid: 71279 start to sleep >>子进程pid为71279 in child pid: 71279 end to sleep in father pid: 71278 to sleep
最终生成文件为output.txt.71278
和output.txt.71279
-i 打印系统调用时的指令地址
1 2 3 4 5 test/ $ strace -i sleep 2 [00007f067d4d4370] nanosleep({2 , 0 }, NULL) = 0 [00007f067d48035b] close(1 ) = 0 [00007f067d48035b] close(2 ) = 0
-b -b syscall
,当指定的系统调用被捕捉时,将该进程从strace中脱离,目前只支持execve, 对带有-f 的选项的有用,这样可以不看子进程的调用, 但是可以是看多线程的系统调用。比如system函数的调用
1 2 3 4 5 6 int main () { printf ("hello world start \n" ); system("sleep 2" ); printf ("hello world start \n" ); return 0 ; }
比如以上代码system函数在fork子进程之后通过execve来执行 sleep 2
这条shell命令
1 2 3 4 5 6 7 8 9 14 :35 :16.278634 clone(child_stack=0 , flags=CLONE_PARENT_SETTID|SIGCHLD, parent_tidptr=0x7fff0f8f096c ) = 156737 <0.000051 > >>fork通过clone创建子进程14 :35 :16.278699 wait4(156737 , strace: Process 156737 attached <unfinished ...> >>strace捕捉子进程syscall [pid 156737] 14 :35 :16.278755 rt_sigaction(SIGINT, {SIG_DFL, [], SA_RESTORER, 0x7fbedb1de4c0 }, NULL, 8 ) = 0 <0.000007 >[pid 156737] 14 :35 :16.278776 rt_sigaction(SIGQUIT, {SIG_DFL, [], SA_RESTORER, 0x7fbedb1de4c0 }, NULL, 8 ) = 0 <0.000006 >[pid 156737] 14 :35 :16.278795 rt_sigprocmask(SIG_SETMASK, [], NULL, 8 ) = 0 <0.000007 >[pid 156737] 14 :35 :16.278820 execve("/bin/sh" , ["sh" , "-c" , "sleep 2" ], []strace: Process 156737 detached >>捕捉到子进程的execve信号,然后脱离进程 <detached ...> 14 :35 :18.281459 <... wait4 resumed> [{WIFEXITED(s) && WEXITSTATUS(s) == 0 }], 0 , NULL) = 156737 <2.002757 >
-e -e expr 指定捕捉指定类型的系统调用
表达式 说明 -e trace=set 跟踪指定的系统调用 -e trace=file 跟踪文件相关的系统调用(文件名作参数) -e trace=process 跟踪进程相关的系统调用 -e trace=network 跟踪网络相关的系统调用 -e trace=ipc 跟踪进程通讯相关的系统调用 -e trace=desc 根据文件描述符的系统调用 -e trace=memory 跟踪内存相关的系统调用 -e raw=set 将制定的系统调用参数16进制打印出来 实践ok -e signal=set 追踪指定的信号 未实验成功 -e signal=SIGCHLD -e read=3,5 输出从指定文件句柄read出的内容 实践ok -e write=set 输出写入到指定文件句柄的内容 实践ok
1 2 3 4 5 6 7 test/ $ strace -e write=1,2 -T -tt -f ./hello-world ... 15:16:44.543954 write(1, "hello world start \n", 19hello world start ) = 19 <0.000009> | 00000 68 65 6c 6c 6f 20 77 6f 72 6c 64 20 73 74 61 72 hello world star | | 00010 74 20 0a ...
-c/-C 统计系统调用时间,但是不会打印普通的输出, -C既有普通的输出,也会在最后进行系统调用个数统计
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 test/ $ sudo strace -c sleep 2 [15:23:57] % time seconds usecs/call calls errors syscall ------ ----------- ----------- --------- --------- ---------------- 0.00 0.000000 0 1 read 0.00 0.000000 0 3 open 0.00 0.000000 0 5 close 0.00 0.000000 0 3 fstat 0.00 0.000000 0 8 mmap 0.00 0.000000 0 4 mprotect 0.00 0.000000 0 1 munmap 0.00 0.000000 0 3 brk 0.00 0.000000 0 3 3 access 0.00 0.000000 0 1 nanosleep 0.00 0.000000 0 1 execve 0.00 0.000000 0 1 arch_prctl ------ ----------- ----------- --------- --------- ---------------- 100.00 0.000000 34 3 total
access系统调用失败是我本地电脑没有一些系统库
-w 统计系统调用开始和结束的时间差,和-c搭配使用
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 test/ $ sudo strace -c -w sleep 2 [15:28:41] % time seconds usecs/call calls errors syscall ------ ----------- ----------- --------- --------- ---------------- 99.98 2.000670 2000670 1 nanosleep 0.00 0.000096 96 1 execve 0.00 0.000058 7 8 mmap 0.00 0.000032 6 5 close 0.00 0.000031 8 4 mprotect 0.00 0.000025 8 3 open 0.00 0.000021 7 3 3 access 0.00 0.000020 7 3 brk 0.00 0.000018 6 3 fstat 0.00 0.000010 10 1 munmap 0.00 0.000006 6 1 read 0.00 0.000006 6 1 arch_prctl ------ ----------- ----------- --------- --------- ---------------- 100.00 2.000993 34 3 total
能发现进程大部分时间都用在nanosleep上,可以用来检查性能瓶颈并针对优化
-S 对-c统计的数据进行排序,比如默认是时间,也可以是名字和系统调用次数,时间
比如对调用次数进行排序
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 strace -c -w -S calls sleep 2 [15:28:50] % time seconds usecs/call calls errors syscall ------ ----------- ----------- --------- --------- ---------------- 0.00 0.000057 7 8 mmap 0.00 0.000042 8 5 close 0.00 0.000033 8 4 mprotect 0.00 0.000026 9 3 open 0.00 0.000020 7 3 fstat 0.00 0.000019 6 3 brk 0.00 0.000021 7 3 3 access 0.00 0.000007 7 1 read 0.00 0.000010 10 1 munmap 99.98 2.000387 2000387 1 nanosleep 0.00 0.000090 90 1 execve 0.00 0.000006 6 1 arch_prctl ------ ----------- ----------- --------- --------- ---------------- 100.00 2.000718 34 3 total
对syscall所用时间排序
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 test/ $ strace -C -w -S time sleep 2 % time seconds usecs/call calls errors syscall ------ ----------- ----------- --------- --------- ---------------- 99.98 2.001845 2001845 1 nanosleep 0.01 0.000108 108 1 execve 0.00 0.000057 7 8 mmap 0.00 0.000049 10 5 close 0.00 0.000030 8 4 mprotect 0.00 0.000030 10 3 3 access 0.00 0.000024 8 3 open 0.00 0.000019 6 3 fstat 0.00 0.000019 6 3 brk 0.00 0.000010 10 1 munmap 0.00 0.000006 6 1 read 0.00 0.000006 6 1 arch_prctl ------ ----------- ----------- --------- --------- ---------------- 100.00 2.002203 34 3 total
应用 N项目遇到超夜拍照内存泄露,对比检查进程smap发现泄露地址,通过地址对比strace抓取mmap系统调用记录,获取该地址内存申请线程和上下文执行状态,确定内存泄露程序范围。