Segmetation fault你來的真不是時候
問題是這樣的,今天一個簡單的C程序,用gcc編譯成彙編語言後,原本想在裏面改點東西,結果運行時就報了「Segmetation fault」。它丫來的還真不是時候,恰好最近正好煩它呢,誰知本身倒送上門來了。OK,擇日不如撞日,今兒就拿你開刀了。
源代碼以下:
- /*littletrick.c*/
- #include <stdio.h>
- int main()
- {
- int a = 100;
- int b = 25;
- if (a > b)
- {
- return a;
- }
- else
- {
- return b;
- }
- }
用gcc將其編譯成彙編源文件:
還沒來得及在裏面作修改,三條命令下去,結果「Segmetation fault」了:
可能有些人曾經遇到過這樣的問題,或許有些人未來可能會遇到。不知道你們對這個問題有什麼感想?這裏說說我本身的分析、追蹤和解決過程,也都是一些片湯話,順便和你們分享分享。
當初學C語言了,老師就說過main()函數是C語言的入口函數,因此你寫的C程序裏必定要以main()做爲函數入口。注意這裏說的是「main()函數是C庫的入口函數」。
而在學習彙編語言的時候,老師又說過「彙編語言源程序的入口點是_start",因此當咱們寫彙編源程序時須要一個_start標記,指明程序的入口地方。
有了這兩點基礎知識,咱們必定不會有main()或者_start就是進程入口點的錯誤觀念了。關於main()函數以前,阿彬有兩篇人氣爆高、超經典的博文,想繼續探究這些問題的盆友們請點「man函數以前」和「北極以北 main函數以前」。
回到咱們的問題上來,咱們是從C語言源程序裏生成的彙編源代碼的,所以gcc在將C文件編譯成彙編語言源程序時就默認滴認爲咱們的程序最終是經過C庫(不論是靜態連接仍是動態連接)來調用main()函數,因此看彙編出來的文件最末尾有兩條指令leave和ret:
- //省略部分代碼
- .L3:
- leave
- ret
- .size main, .-main
- .ident "GCC: (GNU) 4.4.7 20120313 (Red Hat 4.4.7-3)"
- .section .note.GNU-stack,"",@progbits
leave指令其實和enter指令是配對使用,它們主要用來聲明C語言風格的函數,其中enter是函數的「前言」(prologues),leave是函數的「結尾」(epilogues)。也就是說這兩條指令是AT&T爲C語言函數設計的「開頭」和「結尾」的模板,並真心地但願每個彙編源文件面的函數們開頭都放一條enter指令,末尾都放一條leave指令。然而,這並非一個強制的約定,因此不少時候你會看到一些彙編源代碼的函數裏只有leave沒有enter,或者,即便人家是個函數,開頭既沒有enter,結尾也沒有leave。這裏咱們看到,GCC也沒有嚴格按照這樣的風格來實現。想一想當初提出這個機制的人內心會是何其的悲催啊。閒話不表,接下來是ret指令,咱們都知道,在彙編語言裏它一般都是和call指令配合使用,完成函數調用功能。這兄弟倆關係還算比較好,一對好基友。通常見到call的地方,在虛擬世界的某個地方大多數狀況下(注意不是必定)均可以找到一個ret與它惺惺相惜,隔江相望。
(關於call和ret的更多故事,請繼續關注本博客後續的相關係列博文)
要說明白咱們遇到的這個問題的原因還得稍微擺擺call和ret指令的故事。
call是彙編語言中函數調用最多見的指令,它一般會完成兩件事:
第一:當call指令
執行時
(注意用詞,不是
執行前
),它會首先將指令寄存器EIP的值保存在棧裏,這一步是自動完成的。
第二:修改當前的EIP值,讓它指向要跳轉的函數地址處。也就是接下來當即是要調用的函數的入口地址處。
當被調用的函數執行完,返回時,其末尾一般都會有一句ret指令。而該指令的做用就是自動到棧上面將被call指令存入的EIP的值恢復到EIP寄存器裏,使得進程能夠繼續往下執行。這裏咱們差很少能夠猜到,段錯誤的緣由確定是EIP的值混亂所致,但這只是猜測,待會咱們還要進一步分析,EIP是怎麼混亂的?爲何會混亂?怎麼解決的問題。
先反編譯一下咱們最終的可執行文件littletrick:
你們應該看出來了,咱們最終的可執行文件並無經過C庫來啓動,而是直接赤裸裸滴就只有一個可執行的代碼段,並且指明該程序的入口點就是main。最後一句是ret可是死活找不到call在哪裏,問題偏偏恰好出在沒有call和ret配對這個關鍵點
上。咱們用GDB調試運行一下看看詳細過程,是否是如咱們猜想的那樣。從新編譯littletrick.s時加-gstabs(別忘了從新連接)參數以讓其支持GDB調試。
程序剛開始運行時,咱們在main的地方打個斷點:
看看棧、還有各個寄存器裏的值:
咱們看到EIP的值0x8048074就是接下來要執行的指令,也就是main函數入口的地址。這和咱們上面反編譯出來的main函數在虛擬地址空間的值是一致的。此時,棧頂指針ESP的值是0xbffff7a0,說明從0xbffff7a0到0xbfffffff的棧空間裏已經有些一些數據,簡單看一下這些數據裏前128字節都長啥樣子:
至於這些數據是什麼,之後的博文會詳細解釋,這裏只要知道當進程run起來的時候,棧上已經有了部分初始化數據就OK了。咱們一直往下執行:
在執行ret指令前,能夠看到EIP和ESP的值分別是0x8048099和0xbffff7a0。對照反彙編的結果0x8048099恰好就是ret指令的虛擬地址,而當執行完前面的leave指令時,棧上的局部變量a和b都已經被「彈」出去了。也就是說此時棧又恢復到了進程執行時的初始模樣。前面咱們也說過,ret會自動到棧上去取原來的EIP的值把它設置到EIP寄存器裏,而此時棧頂的位置由ESP裏的值0xbffff7a0來指定,從該地址開始4字節(由於EIP是32位寄存器),小端字節序的值是0x00000001。因此,當ret執行完後EIP裏的值就錯誤的被設置成了0x00000001:
很顯然,對進程來講,這是一個非法的訪問地址,操做系統不容許它直接訪問,所以就像上一篇博文所說的,給了一個"Segmetation fault"的錯誤提示信息。這裏,關於ret指令咱們還明確到一點,那就是ret不是從棧上取(retrieve)數據,而是彈(pop)數據出來,這會影響棧頂寄存器ESP的值。
好的,到這裏問題明白了,緣由也清楚了:
某些版本的gcc在將C語言源程序編譯成彙編源代碼時,會在主函數main的末尾,放置一條ret指令。當咱們想用gcc生成彙編模板時,若是用as命令(而不是用gcc提供的-c或者-o控制選項)去彙編*.s文件,而後用ld進行連接成可執行程序,運行時就必定會報「Segmetation fault」。至於GCC在經過C源代碼生成彙編時在main函數末尾加不加ret這也和gcc的版本有關,有些版本的gcc關於C語言的return語句人家就用了exit系統調用來處理。若是你的GCC在C語言源程序編譯出來的彙編代碼裏,在
main函數末尾加了一個ret,而你也和我同樣喜歡折騰,那麼這裏就須要注意了。
問題弄明白了,至於解決辦法也就簡單多了。既然問題是ret和call不配對所致,那麼這裏彙編出來的ret就是多餘的,因此將它刪掉就能夠了。固然爲了嚴緊起見,咱們將它改爲linux系統調用的exit函數,讓它對人家操做系統總得有個交代才行。最後改過的版本:
新增的第一條movl指令是將系統調用的返回結果保存到ebx寄存器,在shell裏咱們能夠經過檢查變量"$?"來查看執行結果;第二條movl指令是將exit的系統調用號1送到eax寄存器裏;第三條int $0x80就是陷入內核,執行Linux的exit系統調用。若是想深刻了解系統調用的童鞋,請猛戳這裏或者這裏都行。
編譯、連接再運行:
結果很愉快了,而後該幹啥就幹啥了。 PS:對從*.c生成的彙編語言源程序*.s,若是想繼續用C庫,那麼你能夠用「gcc -c」來編譯*.s文件,而後用「gcc -o 」生成最終的可執行文件。這樣一來,你就不會遇到本文所提到的煩人的"Segmetation fault"了。
歡迎關注本站公眾號,獲取更多信息