重要 本文转载至:http://blog.jobbole.com/101619/css
飞机上面的黑匣子用于飞机失过后对事故的时候调查,同理,程序的黑匣子用于程序崩溃后对崩溃缘由进程定位。其实Linux提供的core dump机制就是一种黑匣子(core文件就是黑匣子文件)。可是core文件并不是在全部场景都适用,由于core文件是程序崩溃时的内存映像,若是程序使用的内存空间比较大,那产生的core文件也将会很是大,在64bit的操做系统中,该现象更为显著。可是,其实咱们定位程序崩溃的缘由通常只须要程序挂掉以前的堆栈信息、内存信息等就足够了。因此有的时候没有必要使用系统自带的core文件机制。linux
程序异常时,每每会产生某种信号,内核会对该信号进行处理。因此设计黑匣子程序的实质就是咱们定义本身的信号处理函数,来代替内核的默认处理。在咱们的信号处理函数中,咱们能够将咱们想要的信息保存下来(好比程序崩溃时的堆栈信息),以方便后面问题的定位。git
下面咱们先给出一个我写的程序,而后边分析程序边讲具体如何设计一个黑匣子程序:github
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
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
|
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <signal.h>
#include <unistd.h>
#include <time.h>
#include <sys/types.h>
#include <execinfo.h>
/* 定义一个数据结构用来保存信号 */
typedef
struct
sigInfo
{
int
signum
;
char
signame
[
20
]
;
}
sigInfo
;
/* 增长咱们想要捕捉的异常信号,这里列举了6个 */
sigInfo
sigCatch
[
]
=
{
{
1
,
"SIGHUP"
}
,
{
2
,
"SIGINT"
}
,
{
3
,
"SIGQUIT"
}
,
{
6
,
"SIGABRT"
}
,
{
8
,
"SIGFPE"
}
,
{
11
,
"SIGSEGV"
}
}
;
/* 咱们自定义的信号处理函数 */
void
blackbox_handler
(
int
sig
)
{
printf
(
"Enter blackbox_handler: "
)
;
printf
(
"SIG name is %s, SIG num is %d\n"
,
strsignal
(
sig
)
,
sig
)
;
// 打印堆栈信息
printf
(
"Stack information:\n"
)
;
int
j
,
nptrs
;
#define SIZE 100
void
*
buffer
[
100
]
;
char
*
*
strings
;
nptrs
=
backtrace
(
buffer
,
SIZE
)
;
printf
(
"backtrace() returned %d addresses\n"
,
nptrs
)
;
strings
=
backtrace_symbols
(
buffer
,
nptrs
)
;
if
(
strings
==
NULL
)
{
perror
(
"backtrace_symbol"
)
;
exit
(
EXIT_FAILURE
)
;
}
for
(
j
=
0
;
j
<
nptrs
;
j
++
)
printf
(
"%s\n"
,
strings
[
j
]
)
;
free
(
strings
)
;
_exit
(
EXIT_SUCCESS
)
;
}
/* 有bug的程序,调用该程序,将随机产生一些异常信号 */
void
bug_func
(
)
{
int
rand
;
struct
timeval
tpstart
;
pid_t
my_pid
=
getpid
(
)
;
// 产生随机数
gettimeofday
(
&
tpstart
,
NULL
)
;
srand
(
tpstart
.
tv_usec
)
;
while
(
(
rand
=
random
(
)
)
>
(
sizeof
(
sigCatch
)
/
sizeof
(
sigInfo
)
)
)
;
printf
(
"rand=%d\n"
,
rand
)
;
//随机产生异常信号
switch
(
rand
%
(
sizeof
(
sigCatch
)
/
sizeof
(
sigInfo
)
)
)
{
case
0
:
{
// SIGHUP
kill
(
my_pid
,
SIGHUP
)
;
break
;
}
case
1
:
{
// SIGINT
kill
(
my_pid
,
SIGINT
)
;
break
;
}
case
2
:
{
// SIGQUIT
kill
(
my_pid
,
SIGQUIT
)
;
break
;
}
case
3
:
{
// SIGABRT
abort
(
)
;
break
;
}
case
4
:
{
// SIGFPE
int
a
=
6
/
0
;
break
;
}
case
5
:
{
// SIGSEGV
kill
(
my_pid
,
SIGSEGV
)
;
break
;
}
default
:
return
;
}
}
int
main
(
)
{
int
i
,
j
;
struct
sigaction
sa
;
// 初始化信号处理函数数据结构
memset
(
&
sa
,
0
,
sizeof
(
sa
)
)
;
sa
.
sa_handler
=
blackbox_handler
;
sigemptyset
(
&
sa
.
sa_mask
)
;
sa
.
sa_flags
=
0
;
for
(
i
=
0
;
i
<
sizeof
(
sigCatch
)
/
sizeof
(
sigInfo
)
;
i
++
)
{
// 注册信号处理函数
if
(
sigaction
(
sigCatch
[
i
]
.
signum
,
&
sa
,
NULL
)
<
0
)
{
return
EXIT_FAILURE
;
}
}
bug_func
(
)
;
while
(
1
)
;
return
EXIT_SUCCESS
;
}
|
这里咱们定义了一个sigInfo的数据结构,用来保存信号。利用这个数据结构咱们能够将信号值与信号名映射起来。你能够在你的系统中使用 kill –l 命令去查看他们的对应关系。固然,在程序中,若是获得了信号值,也可使用Linux提供的API函数strsignal来获取信号的名字,其函数原型以下:web
1
2
3
|
#include <string.h>
char
*
strsignal
(
int
sig
)
;
|
以后定义了一个全局变量sigCatch来增长咱们想要处理的信号。ubuntu
在main函数里面,除了调用一些函数外,主要是注册了一下咱们要处理的信号。其实就是将特定的信号与某个信号处理函数关联起来。这里咱们所要捕获的信号的信号处理函数都是同一个blackbox_handler,由于咱们想在这些信号出现时保存堆栈信息,因此使用同一个函数彻底能够。这里须要介绍的是sigaction函数,其函数原型以下:数组
1
2
3
|
#include <signal.h>
int
sigaction
(
int
signum
,
const
struct
sigaction *
act
,
struct
sigaction *
oldact
)
;
|
使用该函数能够改变程序默认的信号处理函数。数据结构
第一个参数signum指明咱们想要改变其信号处理函数的信号值。注意,这里的信号不能是SIGKILL和SIGSTOP。这两个信号的处理函数不容许用户重写,由于它们给超级用户提供了终止程序的方法( SIGKILL and SIGSTOP cannot be caught, blocked, or ignored)。app
第二个和第三个参数是一个struct sigaction的结构体,该结构体在<signal.h>中定义,用来描述信号处理函数。若是act不为空,则其指向信号处理函数。若是oldact不为空,则以前的信号处理函数将保存在该指针中。若是act为空,则以前的信号处理函数不变。咱们能够经过将act置空,oldact非空来获取当前的信号处理函数。dom
咱们来看一下这个重要的结构体:
1
2
3
4
5
6
7
|
struct
sigaction
{
void
(
*
sa_handler
)
(
int
)
;
void
(
*
sa_sigaction
)
(
int
,
siginfo_t *
,
void
*
)
;
sigset_t
sa_mask
;
int
sa_flags
;
void
(
*
sa_restorer
)
(
void
)
;
// 该成员如今已废弃
}
;
|
能够看到,该结构体共有5个成员:
sa_handler是一个函数指针,指向咱们定义的信号处理函数,该值也能够是SIG_IGN(忽略信号)或者SIG_DEL(使用默认的信号处理函数)。
sa_mask字段说明了一个信号集,信号处理函数执行期间这一信号集要加到进程的信号屏蔽字中。仅当从信号处理函数返回时再将进程的信号屏蔽字复位为原先的值。这样在调用信号处理函数时就能阻塞某些信号。在信号处理函数被调用时,操做系统创建的新信号屏蔽字包括正在被递送的信号。所以保证了在处理一个给定信号时,若是这种信号再次发生,那么它会被阻塞到对前一个信号的处理结束为止。
sa_flags字段指定对信号处理的一些选项,经常使用的选项及其含义说明以下(在 <signal.h>中定义):
选项 |
含义 |
SA_INTERRUPT | 由此信号中断的系统调用不会自动重启 |
SA_NOCLDSTOP | 若signo是SIGCHLD,当子进程中止(做业控制)时,不产生此信号。当子进程终止时,仍产生此信号(参加SA_NOCLDWAIT说明)。若已设置此标志,则当中止的进程继续运行时,做为XSI扩展,不发送SIGCHLD信号。 |
SA_NOCLDWAIT | 若signo是SIGCHLD,则当调用进程的子进程终止时,不建立僵尸进程。若调用进程在后面调用wait,则调用进程阻塞,直到其全部子进程都终止,此时返回-1,并将errno设置为ECHILD。 |
SA_NODEFER | 当捕捉到此信号时,在执行其信号处理函数时,系统不自动阻塞此信号(除非sa_mask包括了此信号)。 |
SA_ONSTACK | 若用sigaltstack声明了以替换栈,则将此信号递送给替换栈上的进程。 |
SA_RESETHAND | 在此信号处理函数的入口处,将此信号的处理方式复位为SIG_DEF,并清除SA_SIGINFO标志。可是,不能自动复位SIGILL和SIGTRAP这两个信号的配置。设置此标志是sigaction的行为如同SA_NODEFER标志也设置了同样。 |
SA_RESTART | 由此信号中断的系统调用会自动重启动。 |
SA_SIGINFO | 此选项对信号处理程序提供了附加信息:一个指向siginfo结构的指针以及一个指向进程上下文标识符的指针。 |
sa_sigaction是一个替代的信号处理函数,当sa_flags字段设置为SA_SIGINFO时,使用该信号处理函数。须要注意的是,对于sa_sigaction和sa_handler字段,其实现可能使用同一存储区,因此应用程序只能一次使用这两个字段中的一个。一般,按以下方式调用信号处理函数:
1
|
void
handler
(
int
signo
)
;
|
可是,若是设置了SA_SIGINFO标志,则按照以下方式调用信号处理函数:
1
|
void
handler
(
int
signo
,
siginfo_t *
info
,
void
*
context
)
;
|
可见第二种方式比第一种方式多了后面两个参数。其中第二个参数为一个siginfo_t结构的指针,该结构描述了信号产生的缘由,该结构通常定义以下:
1
2
3
4
5
6
7
8
9
10
11
12
13
|
struct
siginfo_t
{
int
si_signo
;
// signal number
int
si_errno
;
// if nonzero, errno value from <errno.h>
int
si_code
;
// additional info (depends on signal)
pid_t
si_pid
;
// sending process ID
uid_t
si_uid
;
// sending process real user ID
void
*
si_addr
;
// address that cased the fault
int
si_status
;
// exit value or signal number
long
si_band
;
// band number for SIGPOLL
/* possibly other fileds also */
}
|
通常siginfo_t结构至少包含si_signo和si_code成员。第三个参数context是一个无类型的指针,它能够被强制转换为ucntext_t结构类型,用于标识信号传递时进程的上下文。
信号种类数目可能超过一个整型量所包含的位数,因此通常而言,不能用整型量中的一位表明一种信号,也就是不能用一个整型量表示信号集(使用信号集能够表示多个信号)。POSIX.1定义了数据结构sigset_t以包含一个信号集,而且定义了下面5个处理信号集的函数:
1
2
3
4
5
6
7
8
9
10
|
#include <signal.h>
/* 前四个函数成功返回0,失败返回-1 */
int
sigemptyset
(
sigset_t *
set
)
;
int
sigfillset
(
sigset_t *
set
)
;
int
sigaddset
(
sigset_t *
set
,
int
signum
)
;
int
sigdelset
(
sigset_t *
set
,
int
signum
)
;
/* 真返回1,假返回0,出错返回-1 */
int
sigismember
(
const
sigset_t *
set
,
int
signum
)
;
|
每个进程都有一个信号屏蔽字,它规定了当前要阻塞递送到该进程的信号集。对于每种可能的信号,该屏蔽字中都有一位与之对应。对于某种信号,若其对应为已设置,则它当前是被阻塞的。进程能够调用sigprocmask来检测和更改当前信号的屏蔽字。
函数sigemptyset初始化由set指向的信号集,清除其中全部的信号。函数sigfillset初始化由set指向的信号集,使其包括全部信号。全部应用程序在使用信号集前,要对该信号集调用sigemptyset或sigfillset一次。这是由于C编译器把未赋初值的外部和静态变量都初始化为0. 一旦已经初始化了一个信号集,之后就能够在该信号集中增、删特定的信号。函数sigaddset将一个信号添加到现有集中,sigdelset则从信号集中删除一个信号。
bug_func函数的做用是产生一些异常信号,用于咱们的测试。里面有两个注意点:(1)咱们使用微秒数来做为随机数种子,这样产生的伪随机数分布会比其余不少方式更均匀一些。(2)咱们调用了kill函数和abort函数来产生一些信号。其函数原型以下:
1
2
3
4
5
6
7
8
|
#include <signal.h>
int
kill
(
pid_t
pid
,
int
sig
)
;
int
raise
(
int
sig
)
;
#include <stdlib.h>
void
abort
(
void
)
;
|
kill函数将信号发送给进程或进程组。kill的pid参数有4种不一样的状况:
raise函数等价于kill(getpid(), signo).
abort函数会先清除对SIGABRT信号阻塞(若是有阻塞的话),而后调用raise函数向调用进程发送信号。注意:若是abort函数使得进程终止了,那终止前会刷新和关闭全部打开的流。
在黑匣子信号处理函数中咱们使用了backtrace和backtrace_symbols函数来获取进程崩溃时的堆栈信息。这两个函数的函数原型以下:
1
2
3
4
5
6
7
|
#include <execinfo.h>
int
backtrace
(
void
*
*
buffer
,
int
size
)
;
char
*
*
backtrace_symbols
(
void
*
const
*
buffer
,
int
size
)
;
void
backtrace_symbols_fd
(
void
*
const
*
buffer
,
int
size
,
int
fd
)
;
|
backtrace函数会返回进程的调用栈信息,并保存在buffer指向的二维数组中;size指明buffer中能够保存的最大栈帧数目,若是调用栈信息超过了size的值,则只会保存近期的调用栈信息。返回值是保存的栈帧数。
使用backtrace函数获得调用栈信息后,咱们就可使用backtrace_symbols函数将调用栈的地址信息翻译为用符号描述的信息,保存在返回值里面。须要注意的是咱们只须要定义返回值的指针,其空间由函数backtrace_symbols本身调用maolloc分配,可是使用完之后的空间由咱们负责释放。backtrace_symbols_fd没有返回值,它与backtrace_symbols的不一样之处在于它会将翻译的调用栈信息保存在文件里面。
注意:
在该黑匣子程序中,涉及到了不少Linux信号的知识,以及一些相关的数据结构和API,但愿对你们有用。但其实该黑匣子程序在有些极端状况下仍是有必定的问题,后面咱们会分析并进一步优化。
在前文中,咱们实现了一个黑匣子程序——在进程崩溃后,能够保存进程的调用栈。可是,在文章结尾咱们说程序有bug,那bug是什么呢?先看下面一个程序:
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
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
|
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <signal.h>
#include <unistd.h>
#include <time.h>
#include <sys/types.h>
#include <execinfo.h>
void
blackbox_handler
(
int
sig
)
{
printf
(
"Enter blackbox_handler: "
)
;
printf
(
"SIG name is %s, SIG num is %d\n"
,
strsignal
(
sig
)
,
sig
)
;
// 打印堆栈信息
printf
(
"Stack information:\n"
)
;
int
j
,
nptrs
;
#define SIZE 100
void
*
buffer
[
100
]
;
char
*
*
strings
;
nptrs
=
backtrace
(
buffer
,
SIZE
)
;
printf
(
"backtrace() returned %d addresses\n"
,
nptrs
)
;
strings
=
backtrace_symbols
(
buffer
,
nptrs
)
;
if
(
strings
==
NULL
)
{
perror
(
"backtrace_symbol"
)
;
exit
(
EXIT_FAILURE
)
;
}
for
(
j
=
0
;
j
<
nptrs
;
j
++
)
printf
(
"%s\n"
,
strings
[
j
]
)
;
free
(
strings
)
;
_exit
(
EXIT_SUCCESS
)
;
}
long
count
=
0
;
void
bad_iter
(
)
{
int
a
,
b
,
c
,
d
;
a
=
b
=
c
=
d
=
1
;
a
=
b
+
3
;
c
=
count
+
4
;
d
=
count
+
5
*
c
;
count
++
;
printf
(
"count:%ld\n"
,
count
)
;
bad_iter
(
)
;
}
int
main
(
)
{
struct
sigaction
sa
;
memset
(
&
sa
,
0
,
sizeof
(
sa
)
)
;
sa
.
sa_handler
=
blackbox_handler
;
sigemptyset
(
&
sa
.
sa_mask
)
;
sa
.
sa_flags
=
0
;
if
(
sigaction
(
SIGSEGV
,
&
sa
,
NULL
)
<
0
)
{
return
EXIT_FAILURE
;
}
bad_iter
(
)
;
while
(
1
)
;
return
EXIT_SUCCESS
;
}
|
该程序的执行结果以下:
1
2
3
4
5
6
7
8
9
|
.
.
.
.
.
.
count
:
261856
count
:
261857
count
:
261858
count
:
261859
count
:
261860
count
:
261861
Segmentation
fault
(
core
dumped
)
allan
@
ubuntu
:
temp
$
|
该程序是一种极端状况:咱们的程序中使用了无线层次的递归函数,致使栈空间被用尽,此时会产生SIGSEGV信号。可是从输出看,并无走到咱们的信号处理函数里面。这是由于但因为栈空间已经被用完,因此咱们的信号处理函数是无法被调用的,这种状况下,咱们的黑匣子程序是无法捕捉到异常的。
可是该问题也很好解决,咱们能够为咱们的信号处理函数在堆里面分配一块内存做为“可替换信号栈”。
使用可替换栈优化后的程序以下:
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
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
|
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <signal.h>
#include <unistd.h>
#include <time.h>
#include <sys/types.h>
#include <execinfo.h>
void
blackbox_handler
(
int
sig
)
{
printf
(
"Enter blackbox_handler: "
)
;
printf
(
"SIG name is %s, SIG num is %d\n"
,
strsignal
(
sig
)
,
sig
)
;
// 打印堆栈信息
printf
(
"Stack information:\n"
)
;
int
j
,
nptrs
;
#define SIZE 100
void
*
buffer
[
100
]
;
char
*
*
strings
;
nptrs
=
backtrace
(
buffer
,
SIZE
)
;
printf
(
"backtrace() returned %d addresses\n"
,
nptrs
)
;
strings
=
backtrace_symbols
(
buffer
,
nptrs
)
;
if
(
strings
==
NULL
)
{
perror
(
"backtrace_symbol"
)
;
exit
(
EXIT_FAILURE
)
;
}
for
(
j
=
0
;
j
<
nptrs
;
j
++
)
printf
(
"%s\n"
,
strings
[
j
]
)
;
free
(
strings
)
;
_exit
(
EXIT_SUCCESS
)
;
}
long
count
=
0
;
void
bad_iter
(
)
{
int
a
,
b
,
c
,
d
;
a
=
b
=
c
=
d
=
1
;
a
=
b
+
3
;
c
=
count
+
4
;
d
=
count
+
5
*
c
;
count
++
;
printf
(
"count:%ld\n"
,
count
)
;
bad_iter
(
)
;
}
int
main
(
)
{
stack_t
ss
;
struct
sigaction
sa
;
ss
.
ss_sp
=
malloc
(
SIGSTKSZ
)
;
ss
.
ss_size
=
SIGSTKSZ
;
ss
.
ss_flags
=
0
;
if
(
sigaltstack
(
&
ss
,
NULL
)
==
-
1
)
{
return
EXIT_FAILURE
;
}
memset
(
&
sa
,
0
,
sizeof
(
sa
)
)
;
sa
.
sa_handler
=
blackbox_handler
;
sigemptyset
(
&
sa
.
sa_mask
)
;
sa
.
sa_flags
=
SA_ONSTACK
;
if
(
sigaction
(
SIGSEGV
,
&
sa
,
NULL
)
<
0
)
{
return
EXIT_FAILURE
;
}
bad_iter
(
)
;
while
(
1
)
;
return
EXIT_SUCCESS
;
}
|
编译 gcc –rdynamic blackbox_overflow.c 后运行,输出为:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
|
.
.
.
.
.
.
count
:
261989
count
:
261990
count
:
261991
count
:
261992
Enter
blackbox_handler
:
SIG
name
is
Segmentation
fault
,
SIG
num
is
11
Stack
information
:
backtrace
(
)
returned
100
addresses
.
/
a
.
out
(
blackbox_handler
+
0x63
)
[
0x400c30
]
/
lib
/
x86_64
-
linux
-
gnu
/
libc
.
so
.
6
(
+
0x36ff0
)
[
0x7f6e68d74ff0
]
/
lib
/
x86_64
-
linux
-
gnu
/
libc
.
so
.
6
(
_IO_file_write
+
0xb
)
[
0x7f6e68db7e0b
]
/
lib
/
x86_64
-
linux
-
gnu
/
libc
.
so
.
6
(
_IO_do_write
+
0x7c
)
[
0x7f6e68db931c
]
/
lib
/
x86_64
-
linux
-
gnu
/
libc
.
so
.
6
(
_IO_file_xsputn
+
0xb1
)
[
0x7f6e68db84e1
]
/
lib
/
x86_64
-
linux
-
gnu
/
libc
.
so
.
6
(
_IO_vfprintf
+
|