重要 本文轉載至: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
+
|