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