背景
最近項(xiàng)目中用到了一個(gè)庫(kù)蜒什,在程序崩潰時(shí)可以生成exception
文件测秸,記錄程序崩潰時(shí)的調(diào)用信息,對(duì)于定位問題比較有價(jià)值灾常,因此整理下這個(gè)庫(kù)涉及到的知識(shí)點(diǎn)霎冯。相關(guān)測(cè)試代碼已經(jīng)放到github
可以下載調(diào)試。
基礎(chǔ)知識(shí)
maps
maps
用來描述進(jìn)程的虛擬地址空間是如何使用的钞瀑∷嗤恚總共包括六列,每列及其含義如下:
名字 | 含義 |
---|---|
address | 本段在虛擬內(nèi)存中的地址范圍仔戈。 |
perms | 本段的權(quán)限关串,r-讀,w-寫监徘,x-執(zhí)行晋修, p-私有,s-共享凰盔。 |
offset | 即本段映射地址在文件中的偏移墓卦。 |
dev | 主設(shè)備號(hào)與次設(shè)備號(hào):所映射的文件所屬設(shè)備的設(shè)備號(hào)。 |
inode | 文件索引節(jié)點(diǎn)號(hào)户敬。 |
pathname | 映射的文件名落剪。<br />對(duì)有名映射而言,是映射的文件名尿庐。<br />對(duì)匿名映射來說忠怖,是此段內(nèi)存在進(jìn)程中的作用。<br />[stack]表示本段內(nèi)存作為棧來使用抄瑟,[heap]作為堆來使用凡泣,其他情況則為無。 |
對(duì)于有名的內(nèi)存區(qū)間而言皮假,屬性為r--p
表示存放的是rodata
;屬性為rw-p
存放的是bss
和data
;屬性為r-xp
表示存放的是text
數(shù)據(jù)鞋拟。沒有文件名的內(nèi)存區(qū)間則表示用mmap
映射的匿名空間。
以下為./example/maps_test.c
編譯成的可執(zhí)行文件mapstest
的運(yùn)行結(jié)果:
code addr = 0x55e1df08d6da
A_global_addr = 0x55e1df28e034
B_global_init0_addr = 0x55e1df28e020
C_global_init_addr = 0x55e1df28e010
D_global_static_addr = 0x55e1df28e024
E_global_static_init0_addr = 0x55e1df28e028
F_global_static_init_addr = 0x55e1df28e014
G_global_const_addr = 0x55e1df08d998
a_addr = 0x7ffce299bb90
b_init0_addr = 0x7ffce299bb94
c_init_addr = 0x7ffce299bb98
d_static_addr = 0x55e1df28e02c
e_static_init0_addr = 0x55e1df28e030
f_static_init_addr = 0x55e1df28e018
g_const_addr = 0x7ffce299bb9c
h1_arr_addr = 0x7ffce299bbb2
h2_strconst_addr = 0x55e1df08d99c
h2_point_addr = 0x7ffce299bba0
i_malloc_addr = 0x55e1dfd1d260
ps -ef| grep mapstest
得到進(jìn)程對(duì)應(yīng)的pid
號(hào)惹资,maps文件如下:(路徑為/proc/{pid}/maps
)
55e1df08d000-55e1df08e000 r-xp 00000000 08:01 24786338 /mapstest #text
55e1df28d000-55e1df28e000 r--p 00000000 08:01 24786338 /mapstest #rodata
55e1df28e000-55e1df28f000 rw-p 00001000 08:01 24786338 /mapstest #bss data
55e1dfd1d000-55e1dfd3e000 rw-p 00000000 00:00 0 [heap] #堆
7f64881e5000-7f64883cc000 r-xp 00000000 08:01 10490449 /lib/x86_64-linux-gnu/libc-2.27.so
7f64883cc000-7f64885cc000 ---p 001e7000 08:01 10490449 /lib/x86_64-linux-gnu/libc-2.27.so
7f64885cc000-7f64885d0000 r--p 001e7000 08:01 10490449 /lib/x86_64-linux-gnu/libc-2.27.so
7f64885d0000-7f64885d2000 rw-p 001eb000 08:01 10490449 /lib/x86_64-linux-gnu/libc-2.27.so
7f64885d2000-7f64885d6000 rw-p 00000000 00:00 0
7f64885d6000-7f64885fd000 r-xp 00000000 08:01 10490421 /lib/x86_64-linux-gnu/ld-2.27.so
7f64887de000-7f64887e0000 rw-p 00000000 00:00 0
7f64887fd000-7f64887fe000 r--p 00027000 08:01 10490421 /lib/x86_64-linux-gnu/ld-2.27.so
7f64887fe000-7f64887ff000 rw-p 00028000 08:01 10490421 /lib/x86_64-linux-gnu/ld-2.27.so
7f64887ff000-7f6488800000 rw-p 00000000 00:00 0
7ffce297d000-7ffce299e000 rw-p 00000000 00:00 0 [stack] #棧
7ffce29ef000-7ffce29f2000 r--p 00000000 00:00 0 [vvar]
7ffce29f2000-7ffce29f4000 r-xp 00000000 00:00 0 [vdso]
ffffffffff600000-ffffffffff601000 r-xp 00000000 00:00 0 [vsyscall]
對(duì)應(yīng)地贺纲,我們可以找到每個(gè)變量在虛擬內(nèi)存中的地址范圍。其中動(dòng)態(tài)鏈接庫(kù)是程序運(yùn)行時(shí)動(dòng)態(tài)加載的而其加載地址也是每次可能不一樣的褪测。
signal
Linux
中的信號(hào)是一種消息處理機(jī)制, 它本質(zhì)上是一個(gè)整數(shù)猴誊,不同的信號(hào)對(duì)應(yīng)不同的值潦刃,由于信號(hào)的結(jié)構(gòu)簡(jiǎn)單所以天生不能攜帶很大的信息量,但是信號(hào)在系統(tǒng)中的優(yōu)先級(jí)是非常高的稠肘。
在Linux
中的很多常規(guī)操作中都會(huì)有相關(guān)的信號(hào)產(chǎn)生福铅,先從我們最熟悉的場(chǎng)景說起: 通過鍵盤操作產(chǎn)生了信號(hào)
:用戶按下Ctrl-C
萝毛,這個(gè)鍵盤輸入產(chǎn)生一個(gè)硬件中斷项阴,使用這個(gè)快捷鍵會(huì)產(chǎn)生信號(hào), 這個(gè)信號(hào)會(huì)殺死對(duì)應(yīng)的某個(gè)進(jìn)程。
通過shell命令產(chǎn)生了信號(hào)
:通過kill
命令終止某一個(gè)進(jìn)程笆包,kill -9 進(jìn)程PID
环揽。
通過函數(shù)調(diào)用產(chǎn)生了信號(hào)
:如果CPU當(dāng)前正在執(zhí)行這個(gè)進(jìn)程的代碼調(diào)用,比如函數(shù) sleep()庵佣,進(jìn)程收到相關(guān)的信號(hào)歉胶,被迫掛起。
通過對(duì)硬件進(jìn)行非法訪問產(chǎn)生了信號(hào)
:正在運(yùn)行的程序訪問了非法內(nèi)存巴粪,發(fā)生段錯(cuò)誤通今,進(jìn)程退出。
信號(hào)也可以實(shí)現(xiàn)進(jìn)程間通信肛根,但是信號(hào)能傳遞的數(shù)據(jù)量很少辫塌,不能滿足大部分需求,另外信號(hào)的優(yōu)先級(jí)很高派哲,并且它對(duì)應(yīng)的處理動(dòng)作是回調(diào)完成的臼氨,它會(huì)打亂程序原有的處理流程,影響到最終的處理結(jié)果芭届。因此非常不建議使用信號(hào)進(jìn)行進(jìn)程間通信储矩。
通過 kill -l
命令可以查看系統(tǒng)定義的信號(hào)列表。
進(jìn)程對(duì)信號(hào)的處理可以有以下三種措施:
忽略這個(gè)信號(hào)褂乍;
執(zhí)行用戶定義相應(yīng)操作持隧;
執(zhí)行默認(rèn)的操作;
SIGKILL
和SIGTSTOP
是不可以被信號(hào)處理函數(shù)捕捉或者忽略逃片。
常用接口:
函數(shù)名 | 備注 |
---|---|
signal |
信號(hào)安裝函數(shù)舆蝴,但是有消息重入問題,不建議使用 |
sigaction |
信號(hào)安裝函數(shù) |
kill |
給指定進(jìn)程發(fā)送信號(hào) |
raise |
給自己發(fā)送信號(hào)题诵,和kill(getpid( ),sig) 等價(jià) |
alarm |
設(shè)置定時(shí)器洁仗,定時(shí)器超時(shí)后,發(fā)送一個(gè)SIGALRM 信號(hào) |
例子:
#include <stdio.h>
#include <unistd.h>
#include <signal.h>
void catch_signal(int sig)
{
switch(sig)
{
case SIGINT:printf("get SIGINT signal\n");
}
}
int main (int argc,char *argv[ ])
{
signal(SIGINT,catch_signal);
int i = 0;
while(1)
{
sleep(100);//執(zhí)行完信號(hào)后sleep()立即返回性锭,不會(huì)一直休眠下去
printf("hello i = %d",i++);
}
return 0;
}
#include <stdio.h>
#include <unistd.h>
#include <signal.h>
int temp = 0;
void handler_sigint(int signo)
{
printf("recv SIGINT\n");
sleep(5);
temp += 1;
printf("the value of temp is:%d\n",temp);
printf("in handler_sigint, after sleep\n");
}
int main()
{
struct sigaction act;
act.sa_handler = handler_sigint;
act.sa_flags = SA_NOMASK;
sigaction(SIGINT, &act, NULL);
while(1);
return 0;
}
objdump
objdump -d
可以將目標(biāo)文件赠潦、動(dòng)態(tài)庫(kù)、可執(zhí)行文件反匯編草冈,如下:
00000000000006da <swap>:
6da: 55 push %rbp
6db: 48 89 e5 mov %rsp,%rbp
6de: 48 89 7d e8 mov %rdi,-0x18(%rbp)
6e2: 48 89 75 e0 mov %rsi,-0x20(%rbp)
6e6: 48 8b 45 e8 mov -0x18(%rbp),%rax
6ea: 8b 00 mov (%rax),%eax
6ec: 89 45 fc mov %eax,-0x4(%rbp)
6ef: 48 8b 45 e0 mov -0x20(%rbp),%rax
6f3: 8b 10 mov (%rax),%edx
6f5: 48 8b 45 e8 mov -0x18(%rbp),%rax
6f9: 89 10 mov %edx,(%rax)
6fb: 48 8b 45 e0 mov -0x20(%rbp),%rax
6ff: 8b 55 fc mov -0x4(%rbp),%edx
702: 89 10 mov %edx,(%rax)
704: 90 nop
705: 5d pop %rbp
706: c3 retq
00000000000006da
是函數(shù)的地址她奥,<swap>
是函數(shù)名瓮增,整個(gè)匯編文件分為三列,分別是指令地址哩俭、指令機(jī)器碼绷跑、指令機(jī)器碼反匯編得到的指令。
strip
實(shí)際項(xiàng)目中凡资,許多組件編譯后砸捏,都會(huì)使用strip
命令減小目標(biāo)文件的大小,處理后的文件依然可以正常運(yùn)行隙赁,但是其中的符號(hào)信息(比如函數(shù)名)會(huì)失去垦藏。出問題后不利于定位。
解決方法是在編譯(gcc -c
)階段加入-rdynamic
選項(xiàng)伞访,此方法會(huì)將函數(shù)名加入到*.dyn
節(jié)中掂骏,strip
對(duì)其無效。
backtrace
在Linux上的C/C++編程環(huán)境下厚掷,我們可以通過如下三個(gè)函數(shù)來獲取程序的調(diào)用棧信息弟灼。
#include <execinfo.h>
/* Store up to SIZE return address of the current program state in
ARRAY and return the exact number of values stored. */
int backtrace(void **array, int size);
/* Return names of functions from the backtrace list in ARRAY in a newly
malloc()ed memory block. */
char **backtrace_symbols(void *const *array, int size);
/* This function is similar to backtrace_symbols() but it writes the result
immediately to a file. */
void backtrace_symbols_fd(void *const *array, int size, int fd);
使用它們的時(shí)候有一下幾點(diǎn)需要我們注意的地方:
-
backtrace
的實(shí)現(xiàn)依賴于棧指針(fp
寄存器),在gcc
編譯過程中任何非零的優(yōu)化等級(jí)(-On
參數(shù))或加入了棧指針優(yōu)化參數(shù)-fomit-frame-pointer
后多將不能正確得到程序棧信息冒黑; -
backtrace_symbols
的實(shí)現(xiàn)需要符號(hào)名稱的支持田绑,在gcc
編譯過程中需要加入-rdynamic
參數(shù); - 內(nèi)聯(lián)函數(shù)沒有棧幀薛闪,它在編譯過程中被展開在調(diào)用的位置辛馆;
- 尾調(diào)用優(yōu)化(
Tail-call Optimization
)將復(fù)用當(dāng)前函數(shù)棧,而不再生成新的函數(shù)棧豁延,這將導(dǎo)致棧信息不能正確被獲取昙篙。
崩潰定位
在程序崩潰時(shí),系統(tǒng)會(huì)發(fā)送信號(hào)诱咏,在注冊(cè)的信號(hào)處理函數(shù)中苔可,將進(jìn)程的maps
文件保存下來,同時(shí)記錄此時(shí)的函數(shù)調(diào)用鏈袋狞,利用這些信息就可以進(jìn)行故障定位焚辅。前提是需要添加編譯選項(xiàng)-g
(不加也沒事,不過用addr2line
獲得崩潰代碼的行號(hào)需要)苟鸯,鏈接選項(xiàng)-rdynamic
(一定要加) 同蜻。
在可執(zhí)行文件中崩潰
在64位Linux
上編譯運(yùn)行example
下的test
,程序崩潰:
hello world
segmentfault addr 0x55daa333903e
=========>>>maps <<<=========
55daa3338000-55daa333a000 r-xp 00000000 08:01 24786361 /example/test
55daa3539000-55daa353a000 r--p 00001000 08:01 24786361 /example/test
55daa353a000-55daa353b000 rw-p 00002000 08:01 24786361 /example/test
55daa3e40000-55daa3e61000 rw-p 00000000 00:00 0 [heap]
7f09107eb000-7f09109d2000 r-xp 00000000 08:01 10490449 /lib/x86_64-linux-gnu/libc-2.27.so
7f09109d2000-7f0910bd2000 ---p 001e7000 08:01 10490449 /lib/x86_64-linux-gnu/libc-2.27.so
7f0910bd2000-7f0910bd6000 r--p 001e7000 08:01 10490449 /lib/x86_64-linux-gnu/libc-2.27.so
7f0910bd6000-7f0910bd8000 rw-p 001eb000 08:01 10490449 /lib/x86_64-linux-gnu/libc-2.27.so
7f0910bd8000-7f0910bdc000 rw-p 00000000 00:00 0
7f0910bdc000-7f0910c03000 r-xp 00000000 08:01 10490421 /lib/x86_64-linux-gnu/ld-2.27.so
7f0910de4000-7f0910de6000 rw-p 00000000 00:00 0
7f0910e03000-7f0910e04000 r--p 00027000 08:01 10490421 /lib/x86_64-linux-gnu/ld-2.27.so
7f0910e04000-7f0910e05000 rw-p 00028000 08:01 10490421 /lib/x86_64-linux-gnu/ld-2.27.so
7f0910e05000-7f0910e06000 rw-p 00000000 00:00 0
7ffc3e767000-7ffc3e788000 rw-p 00000000 00:00 0 [stack]
7ffc3e79e000-7ffc3e7a1000 r--p 00000000 00:00 0 [vvar]
7ffc3e7a1000-7ffc3e7a3000 r-xp 00000000 00:00 0 [vdso]
ffffffffff600000-ffffffffff601000 r-xp 00000000 00:00 0 [vsyscall]
=========>>>catch signal 11 <<<========= # 信號(hào)11是段錯(cuò)誤
Dump stack start...
backtrace() returned 7 addresses
[00] ./test(dump+0x2e) [0x55daa3338d98]
[01] ./test(signal_handler+0xb8) [0x55daa3338f2a]
[02] /lib/x86_64-linux-gnu/libc.so.6(+0x3ef20) [0x7f0910829f20]
[03] ./test(segmentfault+0x3a) [0x55daa3339078] #這里崩潰
[04] ./test(main+0x36) [0x55daa3338f9c]
[05] /lib/x86_64-linux-gnu/libc.so.6(__libc_start_main+0xe7) [0x7f091080cb97]
[06] ./test(_start+0x2a) [0x55daa3338c8a]
Dump stack end...
Segmentation fault (core dumped)
由于64位系統(tǒng)運(yùn)行的可執(zhí)行文件的符號(hào)表地址和實(shí)際運(yùn)行時(shí)地址差異甚大早处。
崩潰地址0x55daa3339078
是動(dòng)態(tài)映射的虛擬地址湾蔓,該虛擬地址是通過符號(hào)表地址+該代碼段映射區(qū)間(maps里面有)的地址得來的。
0x55daa3339078
落在區(qū)間55daa3338000-55daa333a000
得到真正的符號(hào)表地址0x55daa3339078
-0x55daa3338000
=0x1078
daniel@daniel:~/example$ addr2line -e test 1078
~/example/calc.c:44
32位系統(tǒng)顯示的是實(shí)際地址砌梆,可以不用轉(zhuǎn)換默责。
上面是獲得符號(hào)表地址的一種方法贬循,也可以使用objdump -d test
將test
反匯編找到segmentfault
的地址:
000000000000103e <segmentfault>:
103e: 55 push %rbp
103f: 48 89 e5 mov %rsp,%rbp
1042: 48 83 ec 10 sub $0x10,%rsp
1046: 48 8d 35 f1 ff ff ff lea -0xf(%rip),%rsi # 103e <segmentfault>
104d: 48 8d 3d b1 01 00 00 lea 0x1b1(%rip),%rdi # 1205 <_IO_stdin_used+0xe5>
1054: b8 00 00 00 00 mov $0x0,%eax
1059: e8 a2 fb ff ff callq c00 <printf@plt>
105e: c7 45 f0 0a 00 00 00 movl $0xa,-0x10(%rbp)
1065: c7 45 f4 00 00 00 00 movl $0x0,-0xc(%rbp)
106c: 48 c7 45 f8 00 00 00 movq $0x0,-0x8(%rbp)
1073: 00
1074: 48 8b 45 f8 mov -0x8(%rbp),%rax
1078: c7 00 01 00 00 00 movl $0x1,(%rax)
107e: 48 8b 45 f8 mov -0x8(%rbp),%rax
1082: 8b 10 mov (%rax),%edx
1084: 8b 45 f0 mov -0x10(%rbp),%eax
1087: 01 d0 add %edx,%eax
1089: 89 45 f4 mov %eax,-0xc(%rbp)
108c: 8b 45 f4 mov -0xc(%rbp),%eax
108f: c9 leaveq
1090: c3 retq
1091: 66 2e 0f 1f 84 00 00 nopw %cs:0x0(%rax,%rax,1)
1098: 00 00 00
109b: 0f 1f 44 00 00 nopl 0x0(%rax,%rax,1)
真正的符號(hào)表地址為000000000000103e
+ 0x3a
= 0x1078
在動(dòng)態(tài)庫(kù)中崩潰
hello world
add(1,2)=3
segmentfault addr 0x7f7335fb483a
=========>>>maps <<<=========
5631f8167000-5631f8169000 r-xp 00000000 08:01 24786364 /example/test_dynamic
5631f8368000-5631f8369000 r--p 00001000 08:01 24786364 /example/test_dynamic
5631f8369000-5631f836a000 rw-p 00002000 08:01 24786364 /example/test_dynamic
5631f8706000-5631f8727000 rw-p 00000000 00:00 0 [heap]
7f7335bc3000-7f7335daa000 r-xp 00000000 08:01 10490449 /lib/x86_64-linux-gnu/libc-2.27.so
7f7335daa000-7f7335faa000 ---p 001e7000 08:01 10490449 /lib/x86_64-linux-gnu/libc-2.27.so
7f7335faa000-7f7335fae000 r--p 001e7000 08:01 10490449 /lib/x86_64-linux-gnu/libc-2.27.so
7f7335fae000-7f7335fb0000 rw-p 001eb000 08:01 10490449 /lib/x86_64-linux-gnu/libc-2.27.so
7f7335fb0000-7f7335fb4000 rw-p 00000000 00:00 0
7f7335fb4000-7f7335fb5000 r-xp 00000000 08:01 24786363 /example/libcalc.so
7f7335fb5000-7f73361b4000 ---p 00001000 08:01 24786363 /example/libcalc.so
7f73361b4000-7f73361b5000 r--p 00000000 08:01 24786363 /example/libcalc.so
7f73361b5000-7f73361b6000 rw-p 00001000 08:01 24786363 /example/libcalc.so
7f73361b6000-7f73361dd000 r-xp 00000000 08:01 10490421 /lib/x86_64-linux-gnu/ld-2.27.so
7f73363bb000-7f73363be000 rw-p 00000000 00:00 0
7f73363db000-7f73363dd000 rw-p 00000000 00:00 0
7f73363dd000-7f73363de000 r--p 00027000 08:01 10490421 /lib/x86_64-linux-gnu/ld-2.27.so
7f73363de000-7f73363df000 rw-p 00028000 08:01 10490421 /lib/x86_64-linux-gnu/ld-2.27.so
7f73363df000-7f73363e0000 rw-p 00000000 00:00 0
7fffe6dfd000-7fffe6e1e000 rw-p 00000000 00:00 0 [stack]
7fffe6fd4000-7fffe6fd7000 r--p 00000000 00:00 0 [vvar]
7fffe6fd7000-7fffe6fd9000 r-xp 00000000 00:00 0 [vdso]
ffffffffff600000-ffffffffff601000 r-xp 00000000 00:00 0 [vsyscall]
=========>>>catch signal 11 <<<========= # 信號(hào)11是段錯(cuò)誤
Dump stack start...
backtrace() returned 7 addresses
[00] ./test_dynamic(dump+0x2e) [0x5631f8167cc8]
[01] ./test_dynamic(signal_handler+0xb8) [0x5631f8167e5a]
[02] /lib/x86_64-linux-gnu/libc.so.6(+0x3ef20) [0x7f7335c01f20]
[03] ./libcalc.so(segmentfault+0x3d) [0x7f7335fb4877] #在動(dòng)態(tài)庫(kù)里面崩潰
[04] ./test_dynamic(main+0x58) [0x5631f8167eee]
[05] /lib/x86_64-linux-gnu/libc.so.6(__libc_start_main+0xe7) [0x7f7335be4b97]
[06] ./test_dynamic(_start+0x2a) [0x5631f8167bba]
Dump stack end...
Segmentation fault (core dumped)
同樣地獲得符號(hào)表地址:0x7f7335fb4877 - 0x7f7335fb4000 = 0x877
daniel@daniel:~/example$ addr2line -e libcalc.so 877
/example/calc.c:44
objdump -d libcalc.so
獲得segmentfault
符號(hào)地址000000000000083a
,加上偏移0x3d
得到 0x877
000000000000083a <segmentfault>:
83a: 55 push %rbp
83b: 48 89 e5 mov %rsp,%rbp
83e: 48 83 ec 10 sub $0x10,%rsp
842: 48 8b 05 9f 07 20 00 mov 0x20079f(%rip),%rax # 200fe8 <segmentfault@@Base+0x2007ae>
849: 48 89 c6 mov %rax,%rsi
84c: 48 8d 3d 46 00 00 00 lea 0x46(%rip),%rdi # 899 <_fini+0x9>
853: b8 00 00 00 00 mov $0x0,%eax
858: e8 43 fe ff ff callq 6a0 <printf@plt>
85d: c7 45 f0 0a 00 00 00 movl $0xa,-0x10(%rbp)
864: c7 45 f4 00 00 00 00 movl $0x0,-0xc(%rbp)
86b: 48 c7 45 f8 00 00 00 movq $0x0,-0x8(%rbp)
872: 00
873: 48 8b 45 f8 mov -0x8(%rbp),%rax
877: c7 00 01 00 00 00 movl $0x1,(%rax)
87d: 48 8b 45 f8 mov -0x8(%rbp),%rax
881: 8b 10 mov (%rax),%edx
883: 8b 45 f0 mov -0x10(%rbp),%eax
886: 01 d0 add %edx,%eax
888: 89 45 f4 mov %eax,-0xc(%rbp)
88b: 8b 45 f4 mov -0xc(%rbp),%eax
88e: c9 leaveq
88f: c3 retq