|
|
EDA365欢迎您登录!
您需要 登录 才可以下载或查看,没有帐号?注册
x
\/ x/ v7 q4 {; a" T5 A
引言
3 W. o3 R% D3 o7 z5 p2 F: H* j# g; O8 u: n3 s9 Q8 M' ]8 O
“Oops,系统挂死了..."6 W( ^2 U& \% F/ \5 {; Q; f- S1 `
! I) C H9 f5 p- ?1 U
“Oops,程序崩溃了..."/ h+ f9 t( c% Y% ^2 z
& {: n% m8 R2 l“Oops,命令执行报错..."4 e: O! f& z1 ~) f* K4 V
1 i7 l6 Q; `+ P# w' ?7 X; x
1 _8 R6 E" r% R& Y/ r) u
) r( D8 i+ W/ I+ D a( M( B
对于维护人员来说,这样的悲剧每天都在上演。理想情况下,系统或应用程序的错误日志提供了足够全面的信息,通过查看相关日志,维护人员就能很快地定位出问题发生的原因。但现实情况,许多错误日志打印模凌两可,更多地描述了出错时的现象(比如"could not open file","connect to XXX time out"),而非出错的原因。
- W' `8 d. X4 F+ O' }3 X( C5 {2 o0 I
" F c7 v5 h1 S( n
3 {& x# K/ K; M# j9 T
错误日志不能满足定位问题的需求,我们能从更“深层”的方面着手分析吗?程序或命令的执行,需要通过系统调用(system call)与操作系统产生交互,其实我们可以通过观察这些系统调用及其参数、返回值,界定出错的范围,甚至找出问题出现的根因。2 H( [7 ]! O2 f" r
' L- R2 Z( B8 E, N" H5 L. [7 _' f
0 p: Z' ~9 _( W0 n- s* _1 i5 [% f1 a+ z6 @8 a# o' Y: i
在Linux中,strace就是这样一款工具。通过它,我们可以跟踪程序执行过程中产生的系统调用及接收到的信号,帮助我们分析程序或命令执行中遇到的异常情况。& ~1 S7 |4 w; [4 @
# h; \4 C. ?; ?7 p
6 t" w- F/ W- _; E W
' d3 n6 s8 O9 l% i, W一个简单的例子' n4 S9 A8 [7 T) f' v. H2 M4 S
( e* ~2 h. e: ^, G如何使用strace对程序进行跟踪,如何查看相应的输出?下面我们通过一个例子来说明。1 E& {. |( ~2 V
; a7 n7 i |: [! z. A. _
1.被跟踪程序示例' Q6 @4 ~8 \" N( B
, I% c2 ^) G7 u$ U& Z$ o( F
- //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;
- }
7 ?/ `" @% S5 J
: W4 t. Q$ p4 f; A* w# r/ u3 y, g; h
以上程序尝试以只读的方式打开/tmp/foo文件,然后退出,其中只使用了open这一个系统调用函数。之后我们对该程序进行编译,生成可执行文件:
4 `1 l- L# u/ i% e3 D
V W8 a6 l7 n6 n) u* c" _- lx@LX:~$ gcc main.c -o main4 U9 S+ M3 w9 O1 ^% @" A- W ]
+ e$ C- c5 A! F7 g
* s" D: U5 G* w2 r3 H. X% A2.strace跟踪输出+ R2 l/ ]9 H2 f+ v C( h6 u( ]
1 R5 g: G4 e4 S s" w# h& _
使用以下命令,我们将使用strace对以上程序进行跟踪,并将结果重定向至main.strace文件:
5 h) P8 q- J9 ?1 B7 e$ A6 D* l1 f3 L- {, n* s8 B
- lx@LX:~$ strace -o main.strace ./main
8 W5 k. X( }& q, o* Q( | : m9 v" u$ E2 @3 W4 [" Q
接下来我们来看main.strace文件的内容:# z8 c8 `- [9 `( _/ D2 R+ I
, a& ^; q' i1 N1 O5 k& x/ e* K6 |
- 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执行输出$ e2 Z# M5 ~( w1 Z! r" B2 }0 D9 _; G
3 ^) X; Q2 @( Y- x
4 L) q7 S3 P' ?! }' @" p4 H$ h1 [看到这一堆输出,是否心生畏难情绪?不用担心,下面我们对输出逐条进行分析。
0 W' u r9 T. U% E6 n
/ ]$ t( @4 Z) F( ]& Q+ y . F% M& G0 _* U8 S6 u
% y& h. `+ A$ W; K6 [
strace跟踪程序与系统交互时产生的系统调用,以上每一行就对应一个系统调用,格式为:
4 C4 S0 ?1 P5 X
. q9 L* q4 _& q系统调用的名称( 参数... ) = 返回值 错误标志和描述
% u; A1 Y5 I0 B, s: u8 n% w4 s: `2 F- {$ k
$ a- {$ c- r2 r# x5 g1 mLine 1: 对于命令行下执行的程序,execve(或exec系列调用中的某一个)均为strace输出系统调用中的第一个。strace首先调用fork或clone函数新建一个子进程,然后在子进程中调用exec载入需要执行的程序(这里为./main)
3 Y1 b* i: f5 K6 K% B5 @
) E2 m, G; h) J, A& x4 gLine 2: 以0作为参数调用brk,返回值为内存管理的起始地址(若在子进程中调用malloc,则从0x9ac4000地址开始分配空间)5 f" J4 A/ c( J+ M
1 n9 O, |! `! E: n
Line 3: 调用access函数检验/etc/ld.so.nohwcap是否存在0 ]" l; J: I: h( g Y, T% J7 p
7 ~5 M; A% N4 p
Line 4: 使用mmap2函数进行匿名内存映射,以此来获取8192bytes内存空间,该空间起始地址为0xb7739000,关于匿名内存映射,可以看这里2 m% z) h: M) u4 B9 p
I/ \3 ]* t( f; J' BLine 6: 调用open函数尝试打开/etc/ld.so.cache文件,返回文件描述符为3. w4 z6 _6 J+ Y3 ]( b M
2 p2 F+ E T) N# F. W* M' X: e4 o
Line 7: fstat64函数获取/etc/ld.so.cache文件信息
1 X4 ^* d- g- A2 Y2 {& i1 a
* {1 _2 O( \6 z8 pLine 8: 调用mmap2函数将/etc/ld.so.cache文件映射至内存,关于使用mmap映射文件至内存,可以看这里, T5 r6 P; ]2 f1 ^- N7 m
* g( l0 o7 O D0 ^- {0 a
Line 9: close关闭文件描述符为3指向的/etc/ld.so.cache文件, G" d7 o3 d, T5 L% k' B5 l
- q% C# j0 x* e' S; c6 b
Line12: 调用read,从/lib/i386-linux-gnu/libc.so.6该libc库文件中读取512bytes,即读取ELF头信息% v( d& y: E- [; T5 W5 A+ C& ^
3 R( l: d3 a3 W' u- rLine15: 使用mprotect函数对0x6c7000起始的4096bytes空间进行保护(PROT_NONE表示不能访问,PROT_READ表示可以读取)* k/ o) X+ ^0 W1 V
2 t: L2 u* b8 Y. Y7 J7 {& sLine24: 调用munmap函数,将/etc/ld.so.cache文件从内存中去映射,与Line 8的mmap2对应$ q2 |* v3 Y8 @0 y
7 d4 I; @* ~! h" }& N! h
Line25: 对应源码中使用到的唯一的系统调用——open函数,使用其打开/tmp/foo文件2 ^9 W; x5 ]* Q+ F8 y _$ I. p
! i1 l! v9 [1 B0 M9 M) T, _Line26: 子进程结束,退出码为5(为什么退出值为5?返回前面程序示例部分看看源码吧:)7 u' _/ _+ C6 n7 y1 v
0 @7 l2 @# ?$ X9 f5 X+ |( j# X- }; L# J: K* Z# V9 p+ N! \
3.输出分析# Q& @( d- X+ U0 j# w1 O: I5 ^7 p
; }6 w* V1 z+ R }4 j, s7 Q呼呼!看完这么多系统调用函数,是不是有点摸不着北?让我们从整体入手,回到主题strace上来。
5 b" x% T* ~5 s4 C7 {7 x3 S- X$ A
' k! d; D5 i0 j1 V' Q从上面输出可以发现,真正能与源码对应上的只有open这一个系统调用(Line25),其他系统调用几乎都用于进行进程初始化工作:装载被执行程序、载入libc函数库、设置内存映射等。# X& R: G5 `- A; N, N
- N% r6 c% B6 U: C
' g' S0 p7 |; _0 i5 i源码中的if语句或其他代码在相应strace输出中并没有体现,因为它们并没有唤起系统调用。strace只关心程序与系统之间产生的交互,因而strace不适用于程序逻辑代码的排错和分析。7 a# g) J: R7 A! k4 G* e3 H0 n
' o. l9 c! X# d. z* f* a+ Q
1 L1 J! S, m/ A* h. y对于Linux中几百个系统调用,上面strace输出的几个只是冰山一角,想要更深入地了解Linux系统调用,那就man一下吧!
) W @" x; Z- ]" l2 K
& l5 I1 `4 q) x! q7 B5 z1 f- man 2 系统调用名称
- man ld.so //Linux动态链接的manpage" ~& ^; D# g8 I$ D7 y
3 o, h! C' V. b3 G
+ |* u0 N) q: d1 h! S* ?" Rstrace常用选项7 ]2 |' P& T9 L3 Y1 C
& @+ a5 r! v7 D6 m4 R0 P! r4 A该节介绍经常用到的几个strace命令选项,以及在何时使用这些选项合适。, d" V" A# `6 V! M# H$ ?' q [2 N% b
$ @4 o1 c: _, O! f* T* S( U" O: @( ^1.跟踪子进程
: A/ N( w- ?0 \- U1 ~/ \1 K
# J3 {. P4 g" H5 \默认情况下,strace只跟踪指定的进程,而不对指定进程中新建的子进程进行跟踪。使用-f选项,可对进程中新建的子进程进行跟踪,并在输出结果中打印相应进程PID:
" E4 v; y) X$ A7 V7 ?
' j5 [- @; [( F7 J- 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
- ……2 w) l, z9 c% ^1 G+ r
& ?: |; D- Y3 _8 L4 ^) w# R: I) k
1 ^" H: b& q4 Q8 s; X对多进程程序、命令和脚本使用strace进行跟踪的时,一般打开-f选项。! q! C: C2 d# ?7 j( @1 z$ ~1 y
P& y; M8 m5 t Y. b. X, n u9 D: j$ M3 e2 r- z, B
2.记录系统调用时间
6 {; e# s! v/ d/ k+ H, n# f. k) N
strace还可以记录程序与系统交互时,各个系统调用发生时的时间信息,有r、t、tt、ttt、T等几个选项,它们记录时间的方式为:
- m! ]# u! j1 p$ D- [) ~5 U2 t7 b# a% Z9 ]; |# o/ b
-T: 记录各个系统调用花费的时间,精确到微秒3 k6 m; _" F$ F! e' S4 Y% g
9 K9 @2 \6 X- e( G4 I! E! D-r: 以第一个系统调用(通常为execve)计时,精确到微秒* P4 Z; |* ]: u* r3 L0 g- y
& Y8 A1 R5 B0 Z6 t+ r r" m3 M
-t: 时:分:秒9 e0 _( e/ U. s% ~/ o
$ W! P$ x' h4 \; M* P7 e! ]-tt: 时:分:秒 . 微秒
7 B4 F8 X$ B4 \: {' X B( S; f$ U! k2 d' `+ @+ _+ b9 `
-ttt: 计算机纪元以来的秒数 . 微秒# s; [9 ]; z- z. h' y! F d& h
+ X0 h" x, g8 }6 R
比较常用的为T选项,因为其提供了每个系统调用花费时间。而其他选项的时间记录既包含系统调用时间,又算上用户级代码执行用时,参考意义就小一些。对部分时间选项我们可以组合起来使用,例如:
3 I6 \( N; F3 p* e
% V$ M4 |) \+ a+ W- 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>
- ……0 r7 |5 A u' n; c: Z$ X
L' O1 |3 B" D6 q
! G2 P+ |0 |- D' v% ~/ [
最左边一列为-r选项对应的时间输出,最右边一列为-T选项对应的输出。* a; v: L6 e% ^7 }
1 H) ^' e1 ]' \. y, n( \: h4 e2 p& B5 ]8 h4 h: x
3.跟踪正在运行的进程9 d7 J) S6 b# m
% c& W: Q w5 V" L使用strace对运行中的程序进行跟踪,使用命令“strace -p PID”即可,命令执行之后,被跟踪的进程照常执行,strace的其他选项也适用于运行中的进程跟踪。1 Y# Z& c, U/ f, }7 ~9 ~
) D, W6 ]$ S* `3 ]/ }
8 ` A* t% H5 Y! S. z5 r
使用strace处理程序挂死
0 s: D8 E, J" Y& |0 G
1 {: R2 d% i% i# Y& ^; Z5 X# c2 X1 |最后我们通过一个程序示例,学习使用strace分析程序挂死的方法。. c5 f: r1 L6 U
9 W2 d# l! j2 D
1.挂死程序源码
5 r. g! p& p5 ^" z- O5 s$ z9 i( u5 I. L2 Q3 M
- //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;
- }
3 L, C, g7 p8 C& A1 l # z8 d- a- y- ?" k5 s7 v- O
* F- v J: y( v! A1 J) j
可向该程序传送user和system参数,以上代码使用死循环模拟用户态挂死,调用sleep模拟内核态程序挂死。, A; d6 T9 }: f: t
" n- K" A6 A$ Y! n* x, n5 t
# h% @: M, o3 ]- h
2.strace跟踪输出
4 H) D* N7 ^4 A: u% J6 c* [
6 k( a0 \9 z6 w6 I S用户态挂死跟踪输出:
8 h! l4 D7 `' f: U- W% C0 n* L2 f! Q% D& T9 z. v$ r) ^4 @
- 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
# o5 Q* B% X* l
6 `& `. d+ ~" t, F$ ?* f) _* Z R6 B1 `- c
内核态挂死跟踪输出:7 }3 p3 p/ m, \& h2 b" [$ e
+ e; [3 J9 G+ a7 c* X
- 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},- x0 {, g$ E' X1 m
1 p8 { O7 O# `* Y
, `1 H+ x u$ P' J6 _9 W( K
3.输出分析
) R6 ~# S( V6 M
8 J7 @! A# A \用户态挂死情况下,strace在getpid()一行输出之后没有其他系统调用输出;进程在内核态挂死,最后一行的系统调用nanosleep不能完整显示,这里nanosleep没有返回值表示该调用尚未完成。
4 I+ k. b7 Q; E5 y" _& @! m# J% n# w
/ B# K; T7 i$ r7 t3 N. Z& S# y; U! d# l: \+ N3 G
因而我们可以得出以下结论:使用strace跟踪挂死程序,如果最后一行系统调用显示完整,程序在逻辑代码处挂死;如果最后一行系统调用显示不完整,程序在该系统调用处挂死。8 x2 }& b' |! ~+ F9 J/ E3 |2 {
$ A2 d6 I+ U9 Z z, a: `0 S
$ g, W7 ^1 T) T' X) r
当程序挂死在系统调用处,我们可以查看相应系统调用的man手册,了解在什么情况下该系统调用会出现挂死情况。另外,系统调用的参数也为我们提供了一些信息,例如挂死在如下系统调用:% a0 o! B* Z5 I$ G0 B; ~2 [& r
& b5 E, g$ p9 k, a/ o8 {- read(16,
4 A- `0 `5 K* w5 m \) L6 x ( ]. {* A- p! k1 j) h
( P8 {* T( }$ Z5 L那我们可以知道read函数正在对文件描述符为16的文件或socket进行读取,进一步地,我们可以使用lsof工具,获取对应于文件描述符为16的文件名、该文件被哪些进程占用等信息。
0 X/ H8 M+ a0 A- k, l: U! L; U# @$ m z
6 W) Z* k% f( Z' ^5 p9 l( Q7 B5 N3 X
小结4 w% ^1 M' O6 w, L
: X3 C P( b; W8 m: b) ~本文对Linux中常用的问题诊断工具strace进行了介绍,通过程序示例,介绍了strace的使用方法、输出格式以及使用strace分析程序挂死问题的方法,另外对strace工具的几个常用选项进行了说明,描述了这几个选项适用的场景。* P$ }; w$ G* l. i
5 v3 t. Q) \3 t! T9 N5 P# c下次再遇到程序挂死、命令执行报错的问题,如果从程序日志和系统日志中看不出问题出现的原因,先别急着google或找高手帮忙,别忘了一个强大的工具它就在那里,不离不弃,strace一下吧!% V; E' L: g$ O/ l) `: c
|
|