前言
在現網環境下,程序奔潰后不一定會留下core文件,原因有很多,比如存儲空間不足就是其中一個常見的原因。此時我們只能依據linux記錄的錯誤日志來定位問題。
涉及linux命令
本文涉及以下幾條命令
1. dmesg命令,用於獲取程序出錯時的堆棧地址
1)dmesg |grep -E 'segfault|general'
可以通過該命令過濾出發生崩潰的程序,以及對應的堆棧信息。之前看網上的其他文章僅過濾segfault,但我在實踐中發現"general protection"的提示信息也在告訴我們進程崩了。目前我只遇到segfault和general這兩種情況,如果還有其他的過濾條件可以給我留言。
舉例:
[root@vmware ~] dmesg |grep -E 'segfault|general'
[ 374.549753] a.out[57228]: segfault at 0 ip 00000000004004fd sp 00007ffe7296f610 error 6 in a.out[400000+1000]
[ 429.110096] b.out[96783]: segfault at 0 ip 00000000004004fd sp 00007ffcc3e697c0 error 6 in b.out[400000+1000]
字段說明:
1)ip:指令指針寄存器,字段后面的數字就是test程序出錯時程序執行的位置
2)sp:堆棧指針寄存器
3)error:錯誤碼,由三個字位組成的,從高到底分別為bit2 bit1和bit0
bit2: 值為1表示是用戶態程序內存訪問越界,值為0表示是內核態程序內存訪問越界
bit1: 值為1表示是寫操作導致內存訪問越界,值為0表示是讀操作導致內存訪問越界
bit0: 值為1表示沒有足夠的權限訪問非法地址的內容,值為0表示訪問的非法地址根本沒有對應的頁面,也就是無效地址
4)b.out后面緊跟着基地址(這里是400000),后面的+10000不太明白是什么,知道的給我留言
2)dmesg |grep 進(線)程名
通過進程或線程名來過濾。這里之所以強調線程,因為我在實踐中發現dmesg里的信息可能只有線程名,所以推薦在給線程取名時使用統一前綴,比如你的主進程為Test,那么線程可以取Test_A,Test_A,這樣過濾時 grep Test就能過濾出所有想要的信息
舉例:
[root@vmware ~] dmesg |grep a.out
[ 374.549753] a.out[57228]: segfault at 0 ip 00000000004004fd sp 00007ffe7296f610 error 6 in a.out[400000+1000]
3)dmesg -C
dmesg命令查看到的信息在重啟后將會被清空,若當前錯誤信息太多也可以通過該命令手動清空dmesg信息,以便下次問題的定位。-C(大寫)參數為靜默清空,如果清空前還想打印一次,可以通過-c(小寫)參數。
注:
cat /var/log/messages |grep xxx
這里也保存進程奔潰信息,且重啟后依然存在。
舉例:
[root@vmware ~] cat /var/log/messages|grep b.out
May 8 09:24:04 vmware kernel: b.out[96783]: segfault at 0 ip 00000000004004fd sp 00007ffcc3e697c0 error 6 in b.out[400000+1000]
May 8 09:24:04 vmware abrt-hook-ccpp: Process 96783 (b.out) of user 0 killed by SIGSEGV - dumping core
May 8 09:24:05 vmware abrt-server: Executable '/root/b.out' doesn't belong to any package and ProcessUnpackaged is set to 'no'
2. date 用於轉換dmesg信息里的時間
date -d "1970-01-01 UTC `echo "$(date +%s)-$(cat /proc/uptime|cut -f 1 -d' ')+時間"|bc `seconds"
舉例:
[ 672.091250] a.out[26520]: segfault at 0 ip 00000000004004fd sp 00007ffe51b27fe0 error 6 in a.out[400000+1000]
[root@vmware ~] date -d "1970-01-01 UTC `echo "$(date +%s)-$(cat /proc/uptime|cut -f 1 -d' ')+672.091250"|bc `seconds"
2019年 05月 08日 星期三 09:40:02 CST
3. ldd 用於獲取進程所依賴的動態庫,以及所在位置
舉例:
~ $ ldd a.out
linux-vdso.so.1 (0x00007ffc24b9a000)
libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007fde40b63000)
/lib64/ld-linux-x86-64.so.2 (0x00007fde41104000)
4. addr2line,將dmesg獲取到的地址轉換為代碼中發成錯誤的文件、行號及函數名
奔潰發生的位置不同,該命令的使用方式也有所不同。區別主要在於傳入的地址參數。
addr2line -e 進程名 IP指令地址 -f
addr2line -e 進程名 IP指令地址-基地址 -f
在本例1中,addr2line的參數為IP的地址。而在其它例子里,需要將“IP地址-基地址作”的值作為參數。原本我以為區別在與是在主進程中奔潰,還是在動態庫中奔潰,但我將例1放在虛擬機+debian9.11上進行測試,發現也需要傳入“IP地址-基地址”作為參數。
目前我的做法是(供參考):
如果基地址顯示的是400000,我就不用IP地址減它。
如果IP地址根據經驗看特別長,就需要減去基地址。(見下文實例分析)
多試是最靠譜的,最多2次就出結果了。
下面舉一些實際的例子:
1)addr2line -e 進程名 IP指令地址 -f
注:本例在虛擬機+centos7.3中測試
舉例:
在主程序中奔潰
#include <stdio.h> int main() { int *p = NULL; *p = 0; return 0; }
[root@vmware ~] gcc a.c -g
[root@vmware ~] ./a.out
段錯誤(吐核)
[root@vmware ~] dmesg |grep a.out
[ 1310.167335] a.out[122089]: segfault at 0 ip 00000000004004fd sp 00007ffcf08f3ab0 error 6 in a.out[400000+1000]
[root@vmware ~] addr2line -e a.out 00000000004004fd -f
func
/root/a.c:5
通過該例子我們可以看到,程序發生段錯誤的函數以及具體位置。
需要注意的是如果編譯程序時沒有加上-g參數,就只能顯示出函數名,顯示不出具體所在文件的位置.
2)addr2line -e 進程名 IP指令地址-基地址 -f
舉例-1:
在主程序中奔潰
注:本例在虛擬機+debian9.11中測試,代碼與例1相同,但addr2line傳入的地址參數不同。
#include <stdio.h> int main() { int *p = NULL; *p = 0; return 0; }
[root@ ~] $ dmesg |grep a.out
[1281876.289005] a.out[1794281]: segfault at 0 ip 0000561801e55670 sp 00007ffec7a47c20 error 6 in a.out[561801e55000+1000]
[root@ ~] $ addr2line -e ./a.out 0x670 -f
main
/root/test.c:5
這里0x670是0000561801e55670-561801e55000的值。在前面我提到說,如果IP地址根據經驗看特別長,就需要減去基地址。那么一般是什么樣的地址?我們可以通過readelf命令查看一下,我們可以看到執行代碼從第三行開始,對應的地址是0x660。0x670在0x66c~0x676之間,也是第5行代碼。和addr2line分析的結果一樣。
[root@ ~] $readelf -w a.out |grep opcode
[0x000000bd] Extended opcode 2: set Address to 0x660
[0x000000c8] Special opcode 7: advance Address by 0 to 0x660 and Line by 2 to 3
[0x000000c9] Special opcode 62: advance Address by 4 to 0x664 and Line by 1 to 4
[0x000000ca] Special opcode 118: advance Address by 8 to 0x66c and Line by 1 to 5
[0x000000cb] Special opcode 147: advance Address by 10 to 0x676 and Line by 2 to 7
[0x000000cc] Special opcode 76: advance Address by 5 to 0x67b and Line by 1 to 8
[0x000000cf] Extended opcode 1: End of Sequence
舉例-2:
在動態庫中奔潰
#include<stdio.h> #include<string.h> void func() { int *p = NULL; memcpy(p, "test", 4); } int main() { func(); return 0; }
[root@vmware ~] dmesg |grep a.out
[ 6807.501481] a.out[72684]: segfault at 0 ip 00007f6559bc7463 sp 00007fff80625b18 error 6 in libc-2.17.so[7f6559a7c000+1b6000]
[root@vmware ~] ldd a.out
linux-vdso.so.1 => (0x00007ffc643f6000)
libc.so.6 => /lib64/libc.so.6 (0x00007f83ef206000)
/lib64/ld-linux-x86-64.so.2 (0x00007f83ef5e2000)
[root@vmware ~] addr2line -e /lib64/libc.so.6 14B463 -f
__memcpy_ssse3_back
:?
這個這個例子我們可以看到,段錯誤發生的位置是在a.out進程調用的libc庫里,我們先使用ldd找到動態庫的位置,addr2line傳入的地址參數使用14B463(00007f6559bc7463 - 7f6559a7c000),這里我們看到的是__memcpy_ssse3_back導致的,在glibc源碼中查找,確定了是memcpy干的。
同樣的代碼在虛擬機+debian9.11中測試,並不能看出是memcpy,為了確定結果是正確的,我們使用gdb啟動,發現段錯誤的位置是一樣的。至少說明我們定位的方法是正確的,對於glibc這樣的庫定位起來是有些費勁。
[1288936.502170] a.out[1874406]: segfault at 0 ip 00007f9dce8800ba sp 00007ffe0a299bc8 error 6 in libc-2.24.so[7f9dce757000+195000]
[root@ ~] $ addr2line -e /lib/x86_64-linux-gnu/libc-2.24.so 0x1290BA -f
__nss_passwd_lookup
/build/glibc-77giwP/glibc-2.24/string/../sysdeps/x86_64/multiarch/memmove-vec-unaligned-erms.S:301
5、catchsegv,捕獲段錯誤
catchsegv命令專門用來撲獲段錯誤,它通過動態加載器(ld-linux.so)的預加載機制(PRELOAD),把一個事先寫好的庫(/lib/libSegFault.so)加載上,用於捕捉斷錯誤的出錯信息。需要在啟動程序時就使用該命令,因此比較適合在復現問題時使用。此時各種堆棧等調試信息會打印屏幕上(類似於我們在程序里寫代碼捕獲段錯誤信號,再將堆棧打印到日志里),需要注意種方法dmesg就看不到段錯誤信息了。
[root@ ~] $ catchsegv ./a.out
我這里這列出了堆棧信息:
[root@ ~] $ addr2line -e ./a.out 0x6f8 -f
func
/root/a.c:7
[root@ ~] $ addr2line -e /lib/x86_64-linux-gnu/libc-2.24.so 0x1290ba -f
__nss_passwd_lookup
/build/glibc-77giwP/glibc-2.24/string/../sysdeps/x86_64/multiarch/memmove-vec-unaligned-erms.S:301
附1:常見段錯誤產生的原因
1)訪問不存在的內存地址
#include<stdio.h> #include<stdlib.h> void main() { int *ptr = NULL; *ptr = 0; }
2)訪問系統保護的內存地址
#include<stdio.h> #include<stdlib.h> void main() { int *ptr = (int *)0; *ptr = 100; }
3)訪問只讀的內存地址
#include<stdio.h> #include<stdlib.h> #include<string.h> void main() { char *ptr = "test"; strcpy(ptr, "TEST"); }
4)棧溢出
#include<stdio.h>
#include<stdlib.h>
void main()
{
main();
}
附2:其他定位問題可能用到的工具
eu-readelf、objdump、nm