一、段錯誤信息的獲取
程序發生段錯誤時,提示信息很少,下面有幾種查看段錯誤的發生信息的途徑。
1、dmesg
dmesg 可以在應用程序崩潰時,顯示內存中保存的相關信息。
如下所示,通過 dmesg 命令可以查看發生段錯誤的程序名稱、引起段錯誤發生的內存地址、指令指針地址、堆棧指針地址、錯誤代碼、錯誤原因等。
root@#dmesg [ 6357.422282] a.out[3044]: segfault at 806851c ip b75cd668 sp bf8b2100 error 4 in libc-2.15.so[b7559000+19f000]
2、-g
使用gcc編譯程序的源碼時,加上 -g 參數,這樣可以使得生成的二進制文件中加入可以用於 gdb 調試的有用信息。
可產生供gdb調試用的可執行文件,大小明顯比只用-o選項編譯匯編連接后的文件大。
gdb的簡單使用:
(gdb)l 列表(list)
(gdb)r 執行(run)
(gdb)n 下一個(next)
(gdb)q 退出(quit)
(gdb)p 輸出(print)
(gdb)c 繼續(continue)
(gdb)b 4 設置斷點(break)
(gdb)d 刪除斷點(delete)
3、nm
使用 nm 命令列出二進制文件中符號表,包括符號地址、符號類型、符號名等。這樣可以幫助定位在哪里發生了段錯誤。
root@# nm a.out
4、ldd
使用 ldd 命令查看二進制程序的共享鏈接庫依賴,包括庫的名稱、起始地址,這樣可以確定段錯誤到底是發生在了自己的程序中還是依賴的共享庫中。
root@t# ldd a.out
5、dbx
可以在dbx命令下運行程序,這樣可以查看程序是否用完了堆棧
% dbx a.out
(dbx) catch SIGSEGV
(dbx) run
...
signal SEGV (segmentation violation in <some_routine> at 0xeff57708)
(dbx) where
如果現在可以看到調用鏈,那說明堆棧空間還未用完。但是如果是:
fetch at 0xeffe7a60 failed -- I/O error (dbx)
那么堆棧可能已經用完。上面這個十六進制的數就是可以提取或映射的堆棧地址。
可以嘗試在C-shell中調整堆棧段的大小限制。以下調整為10KB
limit stacksize 10
二、段錯誤信息的調試
接下來的講解是圍繞下面的代碼進行的:
#include <stdio.h> int main (void) { int *ptr = NULL; *ptr = 10; return 0; } 輸出結果: 段錯誤(核心已轉儲)
1、使用 printf 輸出信息
這個是看似最簡單,但往往很多情況下十分有效的調試方式,也許可以說是程序員用的最多的調試方式。
簡單來說,就是在程序的重要代碼附近加上像 printf 這類輸出信息,這樣可以跟蹤並打印出段錯誤在代碼中可能出現的位置。
為了方便使用這種方法,可以使用條件編譯指令 #define DEBUG 和 #endif 把 printf 函數包起來。
這樣在程序編譯時,如果加上 -DDEBUG 參數就可以查看調試信息;否則不加上參數就不會顯示調試信息。
2、使用 gcc 和 gdb
1)調試步驟
A、為了能夠使用 gdb 調試程序,在編譯階段加上 -g 參數。
root@# gcc -g test.c
B、使用 gdb 命令調試程序
root@# gdb a.out GNU gdb (Ubuntu/Linaro 7.4-2012.02-0ubuntu2) 7.4-2012.02 Copyright (C) 2012 Free Software Foundation, Inc. License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html> This is free software: you are free to change and redistribute it. There is NO WARRANTY, to the extent permitted by law. Type "show copying" and "show warranty" for details. This GDB was configured as "i686-linux-gnu". For bug reporting instructions, please see: <http://bugs.launchpad.net/gdb-linaro/>... Reading symbols from /home/tarena/project/c_test/a.out...done. (gdb)
C、進入 gdb 后,運行程序
(gdb) r Starting program: /home/tarena/project/c_test/a.out Program received signal SIGSEGV, Segmentation fault. 0x080483c4 in main () at test.c:6 6 *ptr = 10; (gdb)
從輸出看出,程序收到 SIGSEGV 信號,觸發段錯誤,並提示地址 0x080483c4、創建了一個空指針,然后試圖訪問它的值(讀值)。
可以通過man 7 signal查看SIGSEGV的信息
Signal Value Action Comment ────────────────────────────────────────────────────────────────────── SIGHUP 1 Term Hangup detected on controlling terminal or death of controlling process SIGINT 2 Term Interrupt from keyboard SIGQUIT 3 Core Quit from keyboard SIGILL 4 Core Illegal Instruction SIGABRT 6 Core Abort signal from abort(3) SIGFPE 8 Core Floating point exception SIGKILL 9 Term Kill signal SIGSEGV 11 Core Invalid memory reference SIGPIPE 13 Term Broken pipe: write to pipe with no readers SIGALRM 14 Term Timer signal from alarm(2) SIGTERM 15 Term Termination signal SIGUSR1 30,10,16 Term User-defined signal 1 SIGUSR2 31,12,17 Term User-defined signal 2 SIGCHLD 20,17,18 Ign Child stopped or terminated SIGCONT 19,18,25 Cont Continue if stopped SIGSTOP 17,19,23 Stop Stop process SIGTSTP 18,20,24 Stop Stop typed at tty SIGTTIN 21,21,26 Stop tty input for background process SIGTTOU 22,22,27 Stop tty output for background process The signals SIGKILL and SIGSTOP cannot be caught, blocked, or ignored.
D、完成調試后,輸入 q 命令退出 gdb
(gdb) q A debugging session is active. Inferior 1 [process 3483] will be killed. Quit anyway? (y or n) y
2)適用場景
A、僅當能確定程序一定會發生段錯誤的情況下適用。
B、當程序的源碼可以獲得的情況下,使用 -g 參數編譯程序
C、一般用於測試階段,生產環境下 gdb 會有副作用:使程序運行減慢,運行不夠穩定,等等。
D、即使在測試階段,如果程序過於復雜,gdb 也不能處理。
3、使用 core 文件和 gdb
上面有提到段錯誤觸發SIGSEGV信號,通過man 7 signal,可以看到SIGSEGV默認的處理程序(handler)會打印段錯誤信息,並產生 core 文件,由此我們可以借助於程序異常退出生成的 core 文件中的調試信息,使用 gdb 工具來調試程序中的段錯誤。
1)調試步驟
A、在一些Linux版本下,默認是不產生 core 文件的,首先可以查看一下系統 core 文件的大小限制:
root@# ulimit -c 0
B、可以看到默認設置情況下,本機Linux環境下發生段錯誤不會自動生成 core 文件,下面設置下 core 文件的大小限制(單位為KB)
root@# ulimit -c 1024 root@# ulimit -c 1024
C、運行程序,發生段錯誤生成的 core 文件
root@# ./a.out 段錯誤 (核心已轉儲)
D、加載 core 文件,使用 gdb 工具進行調試
root@ubuntu:/home/tarena/project/c_test# gdb a.out core GNU gdb (Ubuntu/Linaro 7.4-2012.02-0ubuntu2) 7.4-2012.02 Copyright (C) 2012 Free Software Foundation, Inc. License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html> This is free software: you are free to change and redistribute it. There is NO WARRANTY, to the extent permitted by law. Type "show copying" and "show warranty" for details. This GDB was configured as "i686-linux-gnu". For bug reporting instructions, please see: <http://bugs.launchpad.net/gdb-linaro/>... Reading symbols from /home/tarena/project/c_test/a.out...done. [New LWP 3491] warning: Can't read pathname for load map: 輸入/輸出錯誤. Core was generated by `./a.out'. Program terminated with signal 11, Segmentation fault. #0 0x080483c4 in main () at test.c:6 6 *ptr = 10; (gdb)
從輸出看出,可以顯示出異樣的段錯誤信息
E、完成調試后,輸入 q 命令退出 gdb
(gdb) q
2)適用場景
A、適合於在實際生成環境下調試程度的段錯誤(即在不用重新發生段錯誤的情況下重現段錯誤)
B、當程序很復雜,core 文件相當大時,該方法不可用
4、使用 objdump
1)調試步驟
A、使用 dmesg 命令,找到最近發生的段錯誤輸入信息
root@# dmesg [ 372.350652] a.out[2712]: segfault at 0 ip 080483c4 sp bfd1f7b8 error 6 in a.out[8048000+1000]
其中,對我們接下來的調試過程有用的是發生段錯誤的地址 0 和指令指針地址 080483c4。
有時候,“地址引起的錯”可以告訴你問題的根源。看到上面的例子,我們可以說,int *ptr = NULL; *ptr = 10;,創建了一個空指針,然后試圖訪問它的值(讀值)。
B、使用 objdump 生成二進制的相關信息,重定向到文件中
root@# objdump -d a.out > a.outDump root@# ls a.out a.outDump core test.c
其中,生成的 a.outDump 文件中包含了二進制文件的 a.out 的匯編代碼
C、在 a.outDump 文件中查找發生段錯誤的地址
root@ubuntu:/home/tarena/project/c_test# grep -n -A 10 -B 10 "0" a.outDump 1- 2-a.out: file format elf32-i386 118:080483b4 <main>: 119: 80483b4: 55 push %ebp 120: 80483b5: 89 e5 mov %esp,%ebp 121: 80483b7: 83 ec 10 sub $0x10,%esp 122: 80483ba: c7 45 fc 00 00 00 00 movl $0x0,-0x4(%ebp) 123: 80483c1: 8b 45 fc mov -0x4(%ebp),%eax 124: 80483c4: c7 00 0a 00 00 00 movl $0xa,(%eax) 125: 80483ca: b8 00 00 00 00 mov $0x0,%eax 126: 80483cf: c9 leave 127: 80483d0: c3 ret 128: 80483d1: 90 nop 129: 80483d2: 90 nop 130: 80483d3: 90 nop 131: 80483d4: 90 nop 132: 80483d5: 90 nop 133: 80483d6: 90 nop 134: 80483d7: 90 nop 135: 80483d8: 90 nop 136: 80483d9: 90 nop 137: 80483da: 90 nop 138: 80483db: 90 nop 139: 80483dc: 90 nop 140: 80483dd: 90 nop 141: 80483de: 90 nop 142: 80483df: 90 nop
通過對以上匯編代碼分析,得知段錯誤發生main函數,對應的匯編指令是movl $0xa,(%eax),接下來打開程序的源碼,找到匯編指令對應的源碼,也就定位到段錯誤了。
2)適用場景
A、不需要 -g 參數編譯,不需要借助於core文件,但需要有一定的匯編語言基礎。
B、如果使用 gcc 編譯優化參數(-O1,-O2,-O3)的話,生成的匯編指令將會被優化,使得調試過程有些難度。
5、使用catchsegv
catchsegv 命令專門用來補貨段錯誤,它通過動態加載器(ld-linux.so)的預加載機制(PRELOAD)把一個事先寫好的 庫(/lib/libSegFault.so)加載上,用於捕捉段錯誤的出錯信息。
root@t# catchsegv ./a.out Segmentation fault (core dumped) *** Segmentation fault Register dump: EAX: 00000000 EBX: b77a1ff4 ECX: bfd8a0e4 EDX: bfd8a074 ESI: 00000000 EDI: 00000000 EBP: bfd8a048 ESP: bfd8a038 EIP: 080483c4 EFLAGS: 00010282 CS: 0073 DS: 007b ES: 007b FS: 0000 GS: 0033 SS: 007b Trap: 0000000e Error: 00000006 OldMask: 00000000 ESP/signal: bfd8a038 CR2: 00000000 Backtrace: ??:0(main)[0x80483c4] /lib/i386-linux-gnu/libc.so.6(__libc_start_main+0xf3)[0xb761a4d3] ??:0(_start)[0x8048321] Memory map: 08048000-08049000 r-xp 00000000 08:01 2102158 /home/tarena/project/c_test/a.out 08049000-0804a000 r--p 00000000 08:01 2102158 /home/tarena/project/c_test/a.out 0804a000-0804b000 rw-p 00001000 08:01 2102158 /home/tarena/project/c_test/a.out 09467000-0948c000 rw-p 00000000 00:00 0 [heap] b75e1000-b75fd000 r-xp 00000000 08:01 1704884 /lib/i386-linux-gnu/libgcc_s.so.1 b75fd000-b75fe000 r--p 0001b000 08:01 1704884 /lib/i386-linux-gnu/libgcc_s.so.1 b75fe000-b75ff000 rw-p 0001c000 08:01 1704884 /lib/i386-linux-gnu/libgcc_s.so.1 b75ff000-b7601000 rw-p 00000000 00:00 0 b7601000-b77a0000 r-xp 00000000 08:01 1704863 /lib/i386-linux-gnu/libc-2.15.so b77a0000-b77a2000 r--p 0019f000 08:01 1704863 /lib/i386-linux-gnu/libc-2.15.so b77a2000-b77a3000 rw-p 001a1000 08:01 1704863 /lib/i386-linux-gnu/libc-2.15.so b77a3000-b77a6000 rw-p 00000000 00:00 0 b77b8000-b77bb000 r-xp 00000000 08:01 1704847 /lib/i386-linux-gnu/libSegFault.so b77bb000-b77bc000 r--p 00002000 08:01 1704847 /lib/i386-linux-gnu/libSegFault.so b77bc000-b77bd000 rw-p 00003000 08:01 1704847 /lib/i386-linux-gnu/libSegFault.so b77bd000-b77bf000 rw-p 00000000 00:00 0 b77bf000-b77c0000 r-xp 00000000 00:00 0 [vdso] b77c0000-b77e0000 r-xp 00000000 08:01 1704843 /lib/i386-linux-gnu/ld-2.15.so b77e0000-b77e1000 r--p 0001f000 08:01 1704843 /lib/i386-linux-gnu/ld-2.15.so b77e1000-b77e2000 rw-p 00020000 08:01 1704843 /lib/i386-linux-gnu/ld-2.15.so bfd6b000-bfd8c000 rw-p 00000000 00:00 0 [stack]
(C專家編程 7.7節)