摘 要:Linux中的段錯誤是編程中經(jīng)常遇到的問題,往往導致程序崩潰。本文對段錯誤產(chǎn)生的原因,結合程序運行的過程,對段錯誤進行量化分析。
關鍵詞:段錯誤;量化分析;程序調試
中圖分類號:TP316.81
在Linux系統(tǒng)上做過程序開發(fā)的人一定都遇到過“段錯誤”(Segmentation fault),隨之程序異常退出。初學編程的人往往對此束手無策,不知道發(fā)生了什么事情,應該如何進行調試。其實Linux下的段錯誤和Windows平臺上臭名昭著的“該程序執(zhí)行了非法操作,即將被關閉”錯誤本質上是相同的,絕大部分都是對內存的非法訪問而導致的。
很多新手程序員對于段錯誤往往無從下手,或是只能通過原始的方式,例如在程序中添加許多的printf語句來跟蹤程序的執(zhí)行。這樣往往效率低下,因此掌握一些調試技巧對于提高調試效率而言是十分重要的,使用正確的調試工具和方法往往能夠事半功倍,幫助準確定位程序出錯的地方,從而找到引發(fā)該錯誤的根本原因(root cause)。
1 段錯誤產(chǎn)生的原因
在Linux下程序崩潰基本上都是由于內存非法訪問造成的,當內存非法訪問發(fā)生時,CPU會產(chǎn)生一個軟中斷信號,如SIGSEGV,而該軟中斷信號的默認處理就是程序退出并產(chǎn)生一個core dump文件,該文件保存了程序崩潰時的現(xiàn)場,包括CPU寄存器的值,內存棧和堆里的數(shù)據(jù)。這些數(shù)據(jù)加上程序的二進制文件(即編譯后的可執(zhí)行文件)和程序源代碼就是我們進行分析的基礎。
2 程序的運行過程
在調試程序之前我們需要了解一下我們的程序是怎么執(zhí)行的。我們寫的C源碼經(jīng)過編譯鏈接后生成機器代碼,也就是匯編指令組成的可執(zhí)行文件,在Linux中是ELF(Executable and Linkable Format)格式的可執(zhí)行文件。匯編指令對內存和寄存器進行操作。而在X86所有的寄存器中,EAX,EBP,ESP,EIP是幾個最重要的寄存器。
EAX:通用寄存器,并用于保存函數(shù)返回值。被調函數(shù)返回時將返回值放入EAX,調用者從EAX中獲取返回值。
ESP:棧頂寄存器,指向工作棧的棧頂。每當進入一個函數(shù)時,會通過修改ESP在棧中開辟一塊空間供本函數(shù)使用。當退出一個函數(shù)時,ESP會恢復原值。
EBP:棧底寄存器,指向當前函數(shù)的棧底。每當進入一個函數(shù)時,該函數(shù)會將原來的(即調用它的函數(shù)的)EBP保存在棧中,然后將原來的ESP作為新的EBP,即EBP指向當前函數(shù)的棧底。
EIP:當前正在執(zhí)行的匯編指令的地址。
函數(shù)的進入和退出都對應著對程序工作棧的修改,需要特別注意的是在X86中,棧是往低地址方向增長。所以進入一個函數(shù)分配??臻g是對ESP進行減操作(sub),而退出一個函數(shù)時是進行加(add)操作。每個函數(shù)在棧上都有自己一塊空間,稱為該函數(shù)的棧幀(stack frame)。如果函數(shù)f1()調用了f2(),目前正在執(zhí)行函數(shù)f2()中的代碼,那么工作棧將會有如圖1的布局:
圖1
表中的內存位置的寫法是x86的基址尋址的表達方式(采用GDB使用的ATT格式),例如-4(%esp)代表的是地址為ESP寄存器的值減去4的內存單元的值。
3 實例分析
我們來看一個經(jīng)過簡化的例子。我們有一個程序執(zhí)行時出現(xiàn)崩潰,產(chǎn)生了core dump文件。用gdb調試工具打開coredump文件可以看到如圖2輸出:
圖2
可以看出該程序發(fā)生了段錯誤,收到了一個SIGSEGV。同時GDB還指出了出錯的指令位于f2()函數(shù)的0x08048426地址。我們通過disassemble命令查看f2()的匯編代碼如圖3:
圖3
可以看到0x08048426的指令是mov(%eax),%edx,其含義是將EAX寄存器當作指針使用,將其所指向的內存的內容取到EDX中。這句指令出錯意味著EAX寄存器中存放的是非法的內存地址,該地址不可讀。我們可以通過info registers命令來查看EAX以及其它寄存器的值(部分)如圖4:
圖4
結果顯示EAX的值是0,即空指針NULL,顯然該地址是不可訪問的,所以CPU產(chǎn)生了一個軟中斷信號SIGSEGV。由此我們從匯編代碼的層次找到了程序崩潰的直接原因,但這還不夠,我們需要繼續(xù)分析為什么EAX寄存器是0。我們順藤摸瓜,查看EAX的值是從何而來。我們繼續(xù)查看f2()的匯編代碼可以發(fā)現(xiàn)上一條指令0x08048423即mov0x8(%ebp),%eax這條指令給EAX寄存器賦了值。我們知道m(xù)ov是一條賦值指令,0x8(%ebp)我們已經(jīng)講到,是f1()傳遞給f2()的第1個參數(shù),由此可以知道f2()的第一個參數(shù)的值為0,即p為空指針NULL,因此此處程序崩潰的原因是傳遞給f2()的參數(shù)為空指針,而f2()在使用前未對其進行檢查導致程序崩潰。
4 其它可能導致段錯誤的情形
上面例子是由于訪問非法指針引起的段錯誤,是在編程中,特別是初學者常犯的一種錯誤。除了非法指針外還有一些其他類型的段錯誤,比如:(1)寫局部變量數(shù)組時越界。由于局部變量數(shù)組是在棧上的,越界意味著覆蓋棧的其他部分,導致程序無法繼續(xù)執(zhí)行;(2)棧溢出。程序的棧的空間是有限的,如果函數(shù)嵌套層次太多,例如遞歸調用層數(shù)過多,每次調用都會分配一塊??臻g,導致棧溢出;(3)修改內存只讀區(qū)的內容,雙引號中的字符串,例如”abcd”是存放在只讀區(qū)中的,如果你嘗試通過指針去修改字符串的內容就會導致段錯誤。
5 結束語
本文介紹的調試方法雖然是基于Linux和x86的,但其思想同樣適用于其他操作系統(tǒng)和硬件平臺。另外,掌握程序的調試技巧固然十分重要,但更重要的提高自身的編程水平和養(yǎng)成良好的編程習慣,這樣才能寫出高質量的程序。畢竟程序調試是一種逆向工程,引入一個bug十分容易,而找到它往往需要付出很大的時間和精力的成本。
參考文獻:
[1]The Santa Cruz Operation.Inc.System V Application Binary Interface Intel386 Architecture Processor Supplement Fourth Edition[M],1997.
[2]Randal E.Bryant.David R.O’Hallaron.Computer Systems A Programmer’s Perspective,Pittsburgh[M],2001.
作者簡介:徐伶伶(1981.09-),女,江蘇太倉人,研究生,講師,計算機應用技術;趙靜女(1981-),山東青島人,研究生,講師,計算機應用技術。
作者單位:青島工學院,山東青島 266300
基金項目: “基于本體的教育信息化共享平臺研制”(項目編號:2012KY009)。