找回密码
 注册
关于网站域名变更的通知
查看: 357|回复: 2
打印 上一主题 下一主题

自助Linux之strace问题诊断工具

[复制链接]

该用户从未签到

跳转到指定楼层
1#
发表于 2020-3-10 10:15 | 只看该作者 回帖奖励 |倒序浏览 |阅读模式

EDA365欢迎您登录!

您需要 登录 才可以下载或查看,没有帐号?注册

x
. v- `9 W1 ]3 w' V2 o' H% f
引言& E+ G  {+ M, m0 t9 \$ `

# Y$ ?" @1 }$ X6 m+ U' R“Oops,系统挂死了..."
. K6 f; z! ]$ u+ _) j* W+ U; T; h7 P6 \* a6 q, d: n
“Oops,程序崩溃了..."
/ r; l/ n8 p& C  {/ @  f; n- R, x
: _7 o) d& U$ C+ l- R  e7 Y. F“Oops,命令执行报错..."6 {5 V+ M% Z/ [+ d% L3 V
& |; G) \# `' _0 j" t

- F6 J) ]4 F0 q; p3 Y; ?
! b7 `$ D. j, w) q+ A对于维护人员来说,这样的悲剧每天都在上演。理想情况下,系统或应用程序的错误日志提供了足够全面的信息,通过查看相关日志,维护人员就能很快地定位出问题发生的原因。但现实情况,许多错误日志打印模凌两可,更多地描述了出错时的现象(比如"could not open file","connect to XXX time out"),而非出错的原因。
6 W" i7 |& l. Q4 m2 P8 V
, Q3 p+ _- M# i' T# L9 V! [0 W
. j! f5 [6 r0 `. i5 C0 u1 n5 i
) w9 k4 d/ r. _4 N& q; W错误日志不能满足定位问题的需求,我们能从更“深层”的方面着手分析吗?程序或命令的执行,需要通过系统调用(system call)与操作系统产生交互,其实我们可以通过观察这些系统调用及其参数、返回值,界定出错的范围,甚至找出问题出现的根因。
, m" ~; O: P7 ~, c: h5 ~4 l
8 Z$ Q* D4 E- \0 U* ^
; B- E- b# f5 T1 U7 I) l
8 h1 u, g2 m0 o! b# Z. F! k在Linux中,strace就是这样一款工具。通过它,我们可以跟踪程序执行过程中产生的系统调用及接收到的信号,帮助我们分析程序或命令执行中遇到的异常情况。
/ m1 X5 z" ?4 c* Y3 V) V, d  u* q
, E; I' U$ m! h0 D% ?  u& | : A4 |7 h/ f0 y% U
) Y) r! Q) F7 ^
一个简单的例子$ K9 @+ Q3 V3 R. ^4 j3 ?
( @  {: A% n- K
如何使用strace对程序进行跟踪,如何查看相应的输出?下面我们通过一个例子来说明。) h! s6 G4 W! h9 |
+ Q0 ]. v# i: B  W1 q- c. M
1.被跟踪程序示例
- v" O) i' z4 w! U4 G/ F
0 a! d( e1 Y/ I3 n- M8 a! y
  • //main.c
  • #include <sys/types.h>
  • #include <sys/stat.h>
  • #include <fcntl.h>
  • int main( )
  • {
  •   int fd ;
  •   int i = 0 ;
  •   fd = open( “/tmp/foo”, O_RDONLY ) ;
  •   if ( fd < 0 )
  •     i=5;
  •   else
  •     i=2;
  •   return i;
  • }# A6 v  _: X1 p. U- y
* g) ~) A3 ?) q% W% x

: a* W+ l) c( n以上程序尝试以只读的方式打开/tmp/foo文件,然后退出,其中只使用了open这一个系统调用函数。之后我们对该程序进行编译,生成可执行文件:% M/ [. h) x3 `4 x: s
8 ]7 H% b) o: d' N$ u, X
  • lx@LX:~$ gcc main.c -o main
    ; v2 `8 q8 x# N
" i1 ?0 [8 ~: W

% `# t) r7 _% ~6 b4 d2.strace跟踪输出  q: I7 j) b- A4 B; C& N& E
. A+ k: k$ A( ^- Q, x
使用以下命令,我们将使用strace对以上程序进行跟踪,并将结果重定向至main.strace文件:
* L6 y. U6 z7 P9 |
5 O  n$ g; u! s* x6 {) V, F
  • lx@LX:~$ strace -o main.strace ./main0 @* m$ @7 P, X, h9 Z

- z! M8 ]6 \9 p6 y- P接下来我们来看main.strace文件的内容:
7 {9 J) t/ W8 j! @+ k6 f. b
. E/ y& i# A, l
  • lx@LX:~$ cat main.strace
  • 1 execve("./main", ["./main"], [/* 43 vars */]) = 0
  • 2 brk(0)                                  = 0x9ac4000
  • 3 access("/etc/ld.so.nohwcap", F_OK)      = -1 ENOENT (No such file or directory)
  • 4 mmap2(NULL, 8192, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0xb7739000
  • 5 access("/etc/ld.so.preload", R_OK)      = -1 ENOENT (No such file or directory)
  • 6 open("/etc/ld.so.cache", O_RDONLY)      = 3
  • 7 fstat64(3, {st_mode=S_IFREG|0644, st_size=80682, ...}) = 0
  • 8 mmap2(NULL, 80682, PROT_READ, MAP_PRIVATE, 3, 0) = 0xb7725000
  • 9 close(3)                                = 0
  • 10 access("/etc/ld.so.nohwcap", F_OK)      = -1 ENOENT (No such file or directory)
  • 11 open("/lib/i386-linux-gnu/libc.so.6", O_RDONLY) = 3
  • 12 read(3, "\177ELF\1\1\1\0\0\0\0\0\0\0\0\0\3\0\3\0\1\0\0\0\220o\1\0004\0\0\0"..., 512) = 512
  • 13 fstat64(3, {st_mode=S_IFREG|0755, st_size=1434180, ...}) = 0
  • 14 mmap2(NULL, 1444360, PROT_READ|PROT_EXEC, MAP_PRIVATE|MAP_DENYWRITE, 3, 0) = 0x56d000
  • 15 mprotect(0x6c7000, 4096, PROT_NONE)     = 0
  • 16 mmap2(0x6c8000, 12288, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_DENYWRITE, 3, 0x15a) = 0x6c8000
  • 17 mmap2(0x6cb000, 10760, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_ANONYMOUS, -1, 0) = 0x6cb000
  • 18 close(3)                                = 0
  • 19 mmap2(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0xb7724000
  • 20 set_thread_area({entry_number:-1 -> 6, base_addr:0xb77248d0, limit:1048575, seg_32bit:1, contents:0, read_exec_    only:0, limit_in_pages:1, seg_not_present:0, useable:1}) = 0
  • 21 mprotect(0x6c8000, 8192, PROT_READ)     = 0
  • 22 mprotect(0x8049000, 4096, PROT_READ)    = 0
  • 23 mprotect(0x4b0000, 4096, PROT_READ)     = 0
  • 24 munmap(0xb7725000, 80682)               = 0
  • 25 open("/tmp/foo", O_RDONLY)              = -1 ENOENT (No such file or directory)
  • 26 exit_group(5)                           = ?
  • //标红的行号为方便说明而添加,非strace执行输出  J- E) [! d9 c5 ^" o

0 k5 c) G3 l! q2 d' E9 `: o& F$ g3 Z3 T. J; Z# a7 t
看到这一堆输出,是否心生畏难情绪?不用担心,下面我们对输出逐条进行分析。
# w1 J% j  b! F$ N5 K" X, f( L( l
% d* w0 c5 O; M" |( `7 ]4 `; g

7 }$ n3 W% S+ x9 E5 E" a% l' xstrace跟踪程序与系统交互时产生的系统调用,以上每一行就对应一个系统调用,格式为:
+ V" N9 C! F7 e9 V7 ?5 w
. Y0 ^" s% K1 o系统调用的名称( 参数... ) = 返回值  错误标志和描述
2 \8 z$ E) \0 Q1 m! p1 ^& I: y  f2 d# r9 U9 ?- n/ x  j

. Q  \: w4 s* H6 d" qLine 1:  对于命令行下执行的程序,execve(或exec系列调用中的某一个)均为strace输出系统调用中的第一个。strace首先调用fork或clone函数新建一个子进程,然后在子进程中调用exec载入需要执行的程序(这里为./main)3 S9 I3 p# k8 T% v* m
, x: U2 o# C  V6 u
Line 2:  以0作为参数调用brk,返回值为内存管理的起始地址(若在子进程中调用malloc,则从0x9ac4000地址开始分配空间)
1 D  q% u) i1 b  h( Q3 R7 |% Z& _2 @' k' a, v0 X
Line 3:  调用access函数检验/etc/ld.so.nohwcap是否存在
1 s9 c3 s/ r# X9 |& n) `; e2 c9 \- u( Y( A* k  L# z, I: c% ]2 J# W
Line 4:  使用mmap2函数进行匿名内存映射,以此来获取8192bytes内存空间,该空间起始地址为0xb7739000,关于匿名内存映射,可以看这里' x4 w; H* \' Z0 E- X: c4 {; Z, ~
, u: f& i. ?. H+ i; c. W
Line 6:  调用open函数尝试打开/etc/ld.so.cache文件,返回文件描述符为3
9 b9 X! L# s" Y: @1 O  b: g: n9 s# G" `% z. H5 R
Line 7:  fstat64函数获取/etc/ld.so.cache文件信息' c2 R: f& T! r

" m) w, b; z% n% c2 h3 s' E/ cLine 8:  调用mmap2函数将/etc/ld.so.cache文件映射至内存,关于使用mmap映射文件至内存,可以看这里. a1 S1 m9 ~/ ]/ Q4 P
* {5 W+ F, u( \
Line 9:  close关闭文件描述符为3指向的/etc/ld.so.cache文件
; }8 ~; t3 }6 ~  G6 b) M
8 K: ^1 }/ k( s: _( D7 [4 P) U- lLine12:  调用read,从/lib/i386-linux-gnu/libc.so.6该libc库文件中读取512bytes,即读取ELF头信息8 x5 ?& }# q1 t: J

( A  B! Z* H6 |$ P0 xLine15:  使用mprotect函数对0x6c7000起始的4096bytes空间进行保护(PROT_NONE表示不能访问,PROT_READ表示可以读取)
; p" u/ N" e% o& R$ w+ T  Q5 K7 M7 ?% u' J8 j0 c7 U- e
Line24:  调用munmap函数,将/etc/ld.so.cache文件从内存中去映射,与Line 8的mmap2对应
4 Z$ ^1 j% _+ o  Y! A$ _! v- E+ |4 Z- J. Z0 E/ i9 U
Line25:  对应源码中使用到的唯一的系统调用——open函数,使用其打开/tmp/foo文件6 U. V1 R. R2 A8 R

7 X( s/ Y0 Z7 q. ^Line26:  子进程结束,退出码为5(为什么退出值为5?返回前面程序示例部分看看源码吧:)2 d$ U" g5 `' \5 {' t' K
3 ?7 M9 B. o0 _) f, \* @

* T6 _2 d$ F, A/ |3.输出分析
$ _/ m+ k: |5 B3 ^
0 X) p7 Q/ f  z呼呼!看完这么多系统调用函数,是不是有点摸不着北?让我们从整体入手,回到主题strace上来。" B" Z! H, q- a  {7 R
& E6 s* k! A: l8 a4 d2 F. P
从上面输出可以发现,真正能与源码对应上的只有open这一个系统调用(Line25),其他系统调用几乎都用于进行进程初始化工作:装载被执行程序、载入libc函数库、设置内存映射等。) h, I1 r& W: w# h( c0 f

, h& c3 |) T& r( S+ j  `1 \6 h+ ]- X, D# @
源码中的if语句或其他代码在相应strace输出中并没有体现,因为它们并没有唤起系统调用。strace只关心程序与系统之间产生的交互,因而strace不适用于程序逻辑代码的排错和分析。7 e% m5 i' N2 R1 t  Q5 D" `

, x: t* J& U( q4 J6 j
, k* y/ _# Z1 {0 }# E* q- ^8 X对于Linux中几百个系统调用,上面strace输出的几个只是冰山一角,想要更深入地了解Linux系统调用,那就man一下吧!
8 X5 n  J" I, r& Y0 e  x1 j* {% p3 B# ^. r9 ~* ]6 B
  • man 2 系统调用名称
  • man ld.so  //Linux动态链接的manpage
    + K* V/ Y6 ~& N0 c% _  V) e

/ d& M9 s. H( f9 D* p8 [  q/ o
1 K* K6 i! S* s' N3 d0 |strace常用选项
2 H/ I: y6 S( ]; S' ^5 N
6 K9 b$ G% s. u* Q3 b9 b该节介绍经常用到的几个strace命令选项,以及在何时使用这些选项合适。/ Z- Z% ~* k( }% j$ ?
; z% F& g& i" y+ p& {
1.跟踪子进程
( f: a' U; s2 g" f0 h# D; e2 A9 U6 e1 _9 k' n8 b: g7 U
默认情况下,strace只跟踪指定的进程,而不对指定进程中新建的子进程进行跟踪。使用-f选项,可对进程中新建的子进程进行跟踪,并在输出结果中打印相应进程PID:
( F+ \* |6 p, P' v" c+ Z
, k' @) p( ~# G+ {
  • mprotect(0x5b1000, 4096, PROT_READ)     = 0
  • munmap(0xb77fc000, 80682)               = 0
  • clone(Process 13600 attached
  • child_stack=0, flags=CLONE_CHILD_CLEARTID|CLONE_CHILD_SETTID|SIGCHLD, child_tidptr=0xb77fb938) = 13600
  • [pid 13599] fstat64(1, {st_mode=S_IFCHR|0620, st_rdev=makedev(136, 0), ...}) = 0
  • [pid 13600] fstat64(1, {st_mode=S_IFCHR|0620, st_rdev=makedev(136, 0), ...}) = 0
  • [pid 13599] mmap2(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0 <unfinished ...>
  • [pid 13600] mmap2(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0xb780f000
  • ……4 O) r- \8 x7 x2 `: J
4 v, k4 G" w: }5 d3 Q7 m
; `- u0 Q% W5 \9 E( i
对多进程程序、命令和脚本使用strace进行跟踪的时,一般打开-f选项。
" s7 O% b/ F* m3 {
% U$ v; C7 P- M9 t  p4 o3 X% M* k# w0 D) e7 [1 j" S2 N
2.记录系统调用时间0 a' b) }8 H: i, l

3 A6 D# Y& c0 M. V4 d% gstrace还可以记录程序与系统交互时,各个系统调用发生时的时间信息,有r、t、tt、ttt、T等几个选项,它们记录时间的方式为:
3 y. m* E9 Z; |4 `* c# K! q
  ~( t( g7 B. c1 X% @1 ?, w-T:   记录各个系统调用花费的时间,精确到微秒
9 f$ b* a2 _1 s& F/ A6 s8 X! e8 ~$ K1 B7 @# V1 e
-r:   以第一个系统调用(通常为execve)计时,精确到微秒2 p3 L" V1 v/ d

% b; u4 k6 M" Q( U5 ^' u0 T-t:   时:分:秒
3 p$ O2 W6 u# l3 t. z6 g# ]% H) V1 B0 X9 y7 A8 g8 G. X* o5 O$ ^0 [2 S
-tt:  时:分:秒 . 微秒
$ i1 ?8 k7 @( M% a. m+ G* x: T
' ?9 R' V: q6 M5 h& G) L. n-ttt: 计算机纪元以来的秒数 . 微秒
. h0 c3 ~( S/ k7 X" A0 C- r5 J- _- w3 Z  D6 M6 G
比较常用的为T选项,因为其提供了每个系统调用花费时间。而其他选项的时间记录既包含系统调用时间,又算上用户级代码执行用时,参考意义就小一些。对部分时间选项我们可以组合起来使用,例如:
8 }* C' x% P7 W1 T
" V1 D) H# v' N8 k; B8 @9 [4 Y
  • strace -Tr ./main
  • 0.000000 execve(“./main”, [“main”], [/* 64 vars */]) = 0
  • 0.000931 fcntl64(0, F_GETFD)= 0 <0.000012>
  • 0.000090 fcntl64(1, F_GETFD)= 0 <0.000022>
  • 0.000060 fcntl64(2, F_GETFD)= 0 <0.000012>
  • 0.000054 uname({sys=”Linux”, node=”ion”, ...}) = 0 <0.000014>
  • 0.000307 geteuid32()= 7903 <0.000011>
  • 0.000040 getuid32()= 7903 <0.000012>
  • 0.000039 getegid32()= 200 <0.000011>
  • 0.000039 getgid32()= 200 <0.000011>
  • ……
    5 d$ w+ ]& E2 W% f9 b4 M
: Q, J7 E3 h% H( l( A/ x

5 F, x" E, s  i6 z' Z* q" z- a最左边一列为-r选项对应的时间输出,最右边一列为-T选项对应的输出。2 `9 n$ L  x! l$ U0 H' e2 |
( h. n9 p5 }/ X2 e# C  |3 K1 Z" x

( g9 T6 x( M- I9 G* ]3.跟踪正在运行的进程
6 q7 e8 z9 n- u3 N7 F1 t0 S- y
/ N6 `; w/ J. ^" l4 x使用strace对运行中的程序进行跟踪,使用命令“strace -p PID”即可,命令执行之后,被跟踪的进程照常执行,strace的其他选项也适用于运行中的进程跟踪。
- W2 y! l7 ~/ @3 j5 D4 q
+ K6 W1 d* N2 V* k. x" k
" x" [( K* d3 |2 m0 s# H使用strace处理程序挂死
6 C, M1 ?" T! o) _
7 \1 R: P& j% l8 u最后我们通过一个程序示例,学习使用strace分析程序挂死的方法。8 X9 c% n" m8 L2 [
4 b  t. w8 n0 r* d1 E! Z2 J
1.挂死程序源码
2 u' P# t& z+ e) I" D
* M. _$ Q) j* R
  • //hang.c
  • #include <stdio.h>
  • #include <sys/types.h>
  • #include <unistd.h>
  • #include <string.h>
  • int main(int argc, char** argv)
  • {
  •     getpid(); //该系统调用起到标识作用
  •     if(argc < 2)
  •     {
  •         printf("hang (user|system)\n");
  •         return 1;
  •     }
  •     if(!strcmp(argv[1], "user"))
  •         while(1);
  •     else if(!strcmp(argv[1], "system"))
  •         sleep(500);
  •     return 0;
  • }
    # H! m* [8 D' E! z$ F! r

5 m) o. m; t2 ?( j$ B% O( Z# a
1 _8 H3 c: J& o7 k' n可向该程序传送user和system参数,以上代码使用死循环模拟用户态挂死,调用sleep模拟内核态程序挂死。6 v6 a& S, f) O0 m, S$ }

8 Y2 D! q( R! |' t/ P; p& O4 }: E+ Z/ o7 B3 W
2.strace跟踪输出; v: ]- N+ O2 U8 Q6 f

2 Y. f% C% h- \7 C, v6 q" j. {用户态挂死跟踪输出:2 @1 _* Q: L- T. y: R
4 k4 q' E) f1 m. X, C# `
  • lx@LX:~$ gcc hang.c -o hang
  • lx@LX:~$ strace ./hang user
  • ……
  • mprotect(0x8049000, 4096, PROT_READ)    = 0
  • mprotect(0xb59000, 4096, PROT_READ)     = 0
  • munmap(0xb77bf000, 80682)               = 0
  • getpid()                                = 14539
    2 m8 E! W1 W6 t( f9 ^

: L$ |% p% o, r- N9 Y. Y$ q' O# x3 c* I" ^, K7 k* H$ ~6 K
内核态挂死跟踪输出:  t5 N- v7 ^# B/ F% B% ~6 s

/ r% c/ p) A* o2 E0 E; ]  ^& {, _
  • lx@LX:~$ strace ./hang system
  • ……
  • mprotect(0x8049000, 4096, PROT_READ)    = 0
  • mprotect(0xddf000, 4096, PROT_READ)     = 0
  • munmap(0xb7855000, 80682)               = 0
  • getpid()                                = 14543
  • rt_sigprocmask(SIG_BLOCK, [CHLD], [], 8) = 0
  • rt_sigaction(SIGCHLD, NULL, {SIG_DFL, [], 0}, 8) = 0
  • rt_sigprocmask(SIG_SETMASK, [], NULL, 8) = 0
  • nanosleep({500, 0},* l0 M/ B0 F8 L# x$ T8 u3 j

9 H; b4 E* Y/ }
( p1 v& \# }. l2 M3.输出分析4 q! l9 ?: R+ v: K
( {5 i9 l; X/ s
用户态挂死情况下,strace在getpid()一行输出之后没有其他系统调用输出;进程在内核态挂死,最后一行的系统调用nanosleep不能完整显示,这里nanosleep没有返回值表示该调用尚未完成。
. v$ P$ l1 E) e" U1 a- |* X$ d. m( T
4 K$ Z1 k  ~0 L* j4 c
因而我们可以得出以下结论:使用strace跟踪挂死程序,如果最后一行系统调用显示完整,程序在逻辑代码处挂死;如果最后一行系统调用显示不完整,程序在该系统调用处挂死。" n  u7 S# W7 l8 g# j  {0 P
7 ^/ ?6 @9 G$ ^- x: P; l2 |

& q3 o- ~9 ~8 D) C. C# c当程序挂死在系统调用处,我们可以查看相应系统调用的man手册,了解在什么情况下该系统调用会出现挂死情况。另外,系统调用的参数也为我们提供了一些信息,例如挂死在如下系统调用:
1 e. g! G1 n9 E& \+ W3 _5 m
! ^% ~: m3 U$ U! s: p
  • read(16,4 K1 h  \3 t( t+ J( ?2 o/ M4 R) R2 W
+ v7 ?7 \' q/ G7 e8 C$ c  \) A
5 t8 n* D2 X" r9 T
那我们可以知道read函数正在对文件描述符为16的文件或socket进行读取,进一步地,我们可以使用lsof工具,获取对应于文件描述符为16的文件名、该文件被哪些进程占用等信息。8 w9 M1 w9 P6 x- E
& p% T: g, N/ F' V

! o+ e% c3 a5 ~! `( U2 e/ @小结: p' Z! ^0 b) S. T

8 A  l: Z: I5 K# E( M$ F+ K本文对Linux中常用的问题诊断工具strace进行了介绍,通过程序示例,介绍了strace的使用方法、输出格式以及使用strace分析程序挂死问题的方法,另外对strace工具的几个常用选项进行了说明,描述了这几个选项适用的场景。
+ e4 a3 w0 v, y1 a+ v2 o
3 K5 W+ C& F4 [( f& r4 I; a1 B下次再遇到程序挂死、命令执行报错的问题,如果从程序日志和系统日志中看不出问题出现的原因,先别急着google或找高手帮忙,别忘了一个强大的工具它就在那里,不离不弃,strace一下吧!
# G8 {/ W1 u% ^" J- j3 P$ R( M9 M

该用户从未签到

2#
发表于 2020-3-10 16:41 | 只看该作者
自助Linux之问题诊断工具strace

该用户从未签到

3#
发表于 2020-3-11 16:35 | 只看该作者
自助Linux之strace问题诊断工具
您需要登录后才可以回帖 登录 | 注册

本版积分规则

关闭

推荐内容上一条 /1 下一条

EDA365公众号

关于我们|手机版|EDA365电子论坛网 ( 粤ICP备18020198号-1 )

GMT+8, 2025-11-25 15:22 , Processed in 0.187500 second(s), 24 queries , Gzip On.

深圳市墨知创新科技有限公司

地址:深圳市南山区科技生态园2栋A座805 电话:19926409050

快速回复 返回顶部 返回列表