引言
在《I/O的效率比較》中,我們在修改圖1程序的BUF_SIZE為
8388608時,運行程序出現崩潰,如下圖1:

圖1. 段錯誤
一般而言,導致程序段錯誤的原因如下:
- 內存訪問出錯,這類問題的典型代表就是數組越界。
- 非法內存訪問,出現這類問題主要是程序試圖訪問內核段內存而產生的錯誤。
- 棧溢出, Linux默認給一個進程分配的棧空間大小為8M,因此你的數組開得過大的話會出現這種問題。
首先我們先看一下系統默認分配的資源:
$ ulimit -a
core file size (blocks, -c) unlimited
data seg size (kbytes, -d) unlimited
scheduling priority (-e) 0
file size (blocks, -f) unlimited
pending signals (-i) 7884
max locked memory (kbytes, -l) 64
max memory size (kbytes, -m) unlimited
open files (-n) 1024
pipe size (512 bytes, -p) 8
POSIX message queues (bytes, -q) 819200
real-time priority (-r) 0
stack size (kbytes, -s) 8192
cpu time (seconds, -t) unlimited
max user processes (-u) 7884
virtual memory (kbytes, -v) unlimited
file locks (-x) unlimited
可以看到默認分配的棧大小為8M。而剛好我們的代碼里的棧大小調到了8M,因此出現了段錯誤。
那么有沒有一種更直接明了的方法來識別和分析應用程序崩潰產生的bug呢?
有,那就是通過程序崩潰后產生的core文件。
core文件
何為core文件.
core dump又叫內核轉儲, 在Unix系統中,核心映像(core image)就是“進程”執行當時的內存內容,當進程發生錯誤或收到“信號”而終止執行時,系統會將核心映像寫入一個文件,以作為調試之用,這就是所謂的核心轉儲(core dump)。而core文件一般產生在進程的當前工作目錄下。
所以core文件中只是程序的內存映像, 如果在編譯時加入調試信息的話,那么還會有調試信息。
如何產生core文件
我們運行了a.out程序出現了“段錯誤”,但沒有產生core文件。這是因為系統默認core文件的大小為0,所以沒有創建。可以用ulimit命令查看和修改core文件的大小。
$ ulimit -c 0 <--------- c選項指定修改core文件的大小
$ ulimit -c 1000 <--------指定了core文件大小為1000KB, 如果設置的大小小於core文件,則對core文件截取
$ ulimit -c unlimited <---------------對core文件的大小不做限制
如果想讓修改永久生效,則需要修改配置文件,如.bash_profile、/etc/profile或/etc/security/limits.conf
我們回到上面的代碼演示,把core文件的大小調成不限制,再執行a.out,就可以在當前目錄看到core文件了。
另外補充一些資料,說明一些情況也不會產生core文件。
- 進程是設置-用戶-ID,而且當前用戶並非程序文件的所有者;
- 進程是設置-組-ID,而且當前用戶並非該程序文件的組所有者;
- 用戶沒有寫當前工作目錄的許可權;
- 文件太大。core文件的許可權(假定該文件在此之前並不存在)通常是用戶讀/寫,組讀和其他讀。
為什么需要core文件
關於core產生的原因很多,比如過去一些Unix的版本不支持現代Linux上這種gdb直接附着到進程上進行調試的機制,需要先向進程發送終止信號,然后用工具閱讀core文件。在Linux上,我們就可以使用kill向一個指定的進程發送信號或者使用gcore命令來使其主動出core並退出。
如果從淺層次的原因上來講,出core意味着當前進程存在BUG,需要程序員修復。
從深層次的原因上講,是當前進程觸犯了某些OS層級的保護機制,逼迫OS向當前進程發送諸如SIGSEGV(即signal 11)之類的信號, 例如訪問空指針或數組越界出core,實際上是觸犯了OS的內存管理,訪問了非當前進程的內存空間,OS需要通過出core來進行警示,這就好像一個人身體內存在病毒,免疫系統就會通過發熱來警示,並導致人體發燒是一個道理(有意思的是,並不是每次數組越界都會出Core,這和OS的內存管理中虛擬頁面分配大小和邊界有關,即使不出core,也很有可能讀到臟數據,引起后續程序行為紊亂,這是一種很難追查的BUG)。
core文件的名稱和生成路徑
默認情況下core的文件名叫"core"
/proc/sys/kernel/core_uses_pid可以控制core文件的文件名中是否添加pid作為擴展
- 文件內容為1,表示添加pid作為擴展名,生成的core文件格式為core.PID
- 為0則表示生成的core文件統一命名為core.
如何修改這個文件的內容?
$ echo "0" > /proc/sys/kernel/core_uses_pid
/proc/sys/kernel/core_pattern文件用於定制core的文件名,一般使用%配合不同的字符:
- %p 出core進程的PID
- %u 出core進程的UID
- %s 造成core的signal號
- %t 出core的時間,從1970-01-0100:00:00開始的秒數
- %e 出core進程對應的可執行文件名
如何閱讀core文件
產生了core文件之后,就是如何查看core文件,並確定問題所在,進行修復。為此,我們不妨先來看看core文件的格式,多了解一些core文件。
$ file core.4244
core.4244: ELF 64-bit LSB core file x86-64, version 1 (SYSV), SVR4-style, from '/home/fireway/study/temp/a.out'
首先可以明確一點,core文件的格式ELF格式,通過使用readelf -h命令來查看更詳細內容
$ readelf -h core.4244
ELF 頭:
Magic: 7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00
Class: ELF64
Data: 2's complement, little endian
Version: 1 (current)
OS/ABI: UNIX - System V
ABI Version: 0
Type: CORE (Core 文件)
Machine: Advanced Micro Devices X86-64
Version: 0x1
入口點地址: 0x0
程序頭起點: 64 (bytes into file)
Start of section headers: 0 (bytes into file)
標志: 0x0
本頭的大小: 64 (字節)
程序頭大小: 56 (字節)
Number of program headers: 19
節頭大小: 0 (字節)
節頭數量: 0
字符串表索引節頭: 0
了解了這些之后,我們來看看如何閱讀core文件,並從中追查BUG。在Linux下,一般讀取core的命令為:
$ gdb exec_file core_file
使用gdb,先從可執行文件中讀取符號表信息,然后讀取core文件。如果不與可執行文件攪合在一起可以嗎?答案是不行,因為core文件中沒有符號表信息,無法進行調試,可以使用如下命令來驗證:
$ objdump -x core.4244 | tail
26 load16 00001000 00007ffff7ffe000 0000000000000000 0003f000 2**12
CONTENTS, ALLOC, LOAD
27 load17 00801000 00007fffff7fe000 0000000000000000 00040000 2**12
CONTENTS, ALLOC, LOAD
28 load18 00001000 ffffffffff600000 0000000000000000 00841000 2**12
CONTENTS, ALLOC, LOAD, READONLY, CODE
SYMBOL TABLE:
no symbols <----------------- 表明當前的ELF格式文件中沒有符號表信息
結合上面知識點,我們分別編譯帶-g的目標可執行mycat_debug和不帶-g的目標可執行mycat,會發現mycat_debug的文件大小稍微大一些。使用readelf命令得出的結果比較報告,詳細見附件-readelf報告.html
各自執行產生的core文件,再使用objdump命令得出的結果比較報告,詳細見附件-objdump報告.html
最后我們各自使用gdb讀取core文件,得出的結果比較報告,詳細見附件-gdb_core報告.html
如果我們強制使用gdb mycat, 接着是帶有調試信息的core文件,gdb會有什么提示呢?
Reading symbols from mycat...(no debugging symbols found)...done.
warning: core file may not match specified executable file.
[New LWP 2037]
Core was generated by `./mycat_debug'.
Program terminated with signal SIGSEGV, Segmentation fault.
#0 0x0000000000400957 in main ()
接下來重點來看,為啥產生段錯誤?
使用gdb mycat_debug core.2037可見:
GNU gdb (Ubuntu 7.7.1-0ubuntu5~14.04.2) 7.7.1
Copyright (C) 2014 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 "x86_64-linux-gnu".
Type "show configuration" for configuration details.
For bug reporting instructions, please see:
<http://www.gnu.org/software/gdb/bugs/>.
Find the GDB manual and other documentation resources online at:
<http://www.gnu.org/software/gdb/documentation/>.
For help, type "help".
Type "apropos word" to search for commands related to "word"...
Reading symbols from mycat_debug...done.
[New LWP 2037]
Core was generated by `./mycat_debug'.
Program terminated with signal SIGSEGV, Segmentation fault.
#0 main () at io1.c:16
16 int n = 0;
可知程序段錯誤,代碼是int n = 0;這一句,我們來看當前棧信息:
(gdb) info f
Stack level 0, frame at 0x7ffc4b59d670:
rip = 0x400957 in main (io1.c:16); saved rip = 0x7fc5c0d5aec5
source language c.
Arglist at 0x7ffc4b59d660, args:
Locals at 0x7ffc4b59d660, Previous frame's sp is 0x7ffc4b59d670
Saved registers:
rbp at 0x7ffc4b59d660, rip at 0x7ffc4b59d668
其中可見指令指針rip指向地址為0x400957, 我們用x命令來查看內存地址中的值。具體幫助查看
gdb調試 - 查看內存一節
(gdb) x/5i 0x400957 或者x/5i $rip
=> 0x400957 <main+26>:movl $0x0,-0x800014(%rbp)
0x400961 <main+36>:lea -0x800010(%rbp),%rax
0x400968 <main+43>:mov $0x800000,%edx
0x40096d <main+48>:mov $0x0,%esi
0x400972 <main+53>:mov %rax,%rdi
這條movl指令要把立即數0送到
-0x800014
(%rbp)這個地址去,其中rbp存儲的是幀指針,其地址是 0x7ffc4b59d660
,而
-0x800014顯然是個負數,十進制是
8388628
,且棧空間是由高地址向低地址延伸,見圖2,那么n的棧地址就是
-0x800014(%rbp)
,也就是$rbp-
8388628
。當我們嘗試訪問此地址時

圖2. 典型的存儲空間安排
(gdb) x /b 0x7ffc4ad9d64c
0x7ffc4ad9d64c: Cannot access memory at address 0x7ffc4ad9d64c
可以看到無法訪問此內存地址,這是因為它已經超過了OS允許的范圍。
ulimit命令參數及用法
功能說明:控制shell程序的資源。
補充說明:ulimit為shell內建指令,可用來控制shell執行程序的資源。
參 數:
- -a 顯示目前資源限制的設定。
- -c 設定core文件的最大值,單位為KB。
- -d <數據節區大小> 程序數據節區的最大值,單位為KB。
- -f <文件大小> shell所能建立的最大文件,單位為區塊。
- -H 設定資源的硬性限制,也就是管理員所設下的限制。
- -m <內存大小> 指定可使用內存的上限,單位為KB。
- -n <文件數目> 指定同一時間最多可開啟的文件數。
- -p <緩沖區大小> 指定管道緩沖區的大小,單位512字節。
- -s <堆疊大小> 指定堆疊的上限,單位為KB。
- -S 設定資源的彈性限制。
- -t 指定CPU使用時間的上限,單位為秒。
- -u <程序數目> 用戶最多可開啟的程序數目。
- -v <虛擬內存大小> 指定可使用的虛擬內存上限,單位為KB。
參考
附件列表