//-----中斷,異常,系統(tǒng)調(diào)用 : 開始
1)在用戶空間發(fā)生中斷時,CPU會自動在內(nèi)核空間保存用戶堆棧的SS, 用戶堆棧的ESP, EFLAGS, 用戶空間的CS, EIP, 中斷號 - 256
| 用戶堆棧的SS | 用戶堆棧的ESP | EFLAGS | 用戶空間的CS | EIP | 中斷號 - 256
進入內(nèi)核后,會進行一個SAVE_ALL,這樣內(nèi)核棧上的內(nèi)容為:
| 用戶堆棧的SS | 用戶堆棧的ESP | EFLAGS | 用戶空間的CS | EIP | 中斷號 - 256 | ES | DS | EAX | EBP | EDI | ESI | EDX | ECX | EBX
好了,一切都處理完時,內(nèi)核jmp到RESTORE_ALL(它是一個宏,例:在x86_32體系結(jié)構(gòu)下,/usr/src/kernel/arch/286/kernel/entry_32.S文件里包含該宏的定義)
RESTORE做的工作,從它的代碼里就可以看出來了:
首先把棧上的 ES | DS | EAX | EBP | EDI | ESI | EDX | ECX | EBX pop到對應(yīng)的寄存器里
然后將esp + 4 把 “中斷號 - 256” pop掉
此時內(nèi)核棧上的內(nèi)容為:
| 用戶堆棧的SS | 用戶堆棧的ESP | EFLAGS | 用戶空間的CS | EIP
最后執(zhí)行iret指令,此時CPU會從內(nèi)核棧上取出SS, ESP, ELFGAS, CS, EIP,然后接著運行。
2) 在用戶空間發(fā)生異常時,CPU自動保存在內(nèi)核棧的內(nèi)容為:
| 用戶堆棧的SS | 用戶堆棧的ESP | EFLAGS | 用戶空間的CS | EIP | 出錯代碼 error_code
(注:CPU只是在進入異常時才知道是否應(yīng)該把出錯代碼壓入堆棧(為什么?),而從異常處理通過iret指令返回時已經(jīng)時過境遷,CPU已經(jīng)無從知當(dāng)初發(fā)生異常的原因,因此不會自動跳過這一項,而要靠相應(yīng)的異常處程序?qū)Χ褩<右哉{(diào)整,使得在CPU開始執(zhí)行iret指令時堆棧頂部是返回地址)
進入內(nèi)核后,沒有進行SAVE_ALL,而是進入相應(yīng)的異常處理函數(shù)(這個函數(shù)是包裝后的,真正的處理函數(shù)在后面)(在此函數(shù)里會把真正的處理函數(shù)的地址push到棧上),然后jmp到各種異常處理所共用的程序入口error_code,它會像SAVE_ALL那樣保存相應(yīng)的寄存器(沒有保存ES),此時內(nèi)核空間上的內(nèi)容為:
| 用戶堆棧的SS | 用戶堆棧的ESP | EFLAGS | 用戶空間的CS | EIP | 出錯代碼 error_code | 相應(yīng)異常處理函數(shù)入口 | DS | EAX | EBP | EDI | ESI | EDX | ECX | EBX
(注:如果沒有出錯代碼,則此值為0)
最后結(jié)束時與中斷類似(RESTORE_ALL)。
3) 發(fā)生系統(tǒng)調(diào)用時,CPU自動保存在內(nèi)核棧的內(nèi)容為:
| 用戶堆棧的SS | 用戶堆棧的ESP | EFLAGS | 用戶空間的CS | EIP
為了與中斷和異常的棧一致,在進入系統(tǒng)調(diào)用入口(ENTRY(system_call))后會首先push %eax,然后進行SAVE_ALL,此時內(nèi)核棧上的內(nèi)容為
| 用戶堆棧的SS | 用戶堆棧的ESP | EFLAGS | 用戶空間的CS | EIP | EAX | ES | DS | EAX | EBP | EDI | ESI | EDX | ECX | EBX
最后結(jié)束時與中斷類似(RESTORE_ALL)。
//-----中斷,異常,系統(tǒng)調(diào)用 : 結(jié)束
debug可以幫助熟悉系統(tǒng),可是時間長了會很疲卷,特別是機械的調(diào)試,如果還要面對雜亂的代碼,更是雪上加霜。所以要學(xué)著從debug中鉆探快樂,在系統(tǒng)的調(diào)試過程中發(fā)揮想象,嘗試不同的debug方法。
最近看了《軟件調(diào)試實戰(zhàn)》,結(jié)合自己的經(jīng)歷,總結(jié)了一下:
1. 與測試用例相關(guān)
a. 如果不能達(dá)到“測試先行”,至少應(yīng)該在寫完代碼后有相對完整的測試用例。對于正確性的保證和以后重構(gòu)代碼都是有好處的。
b. 每次添加新功能或修復(fù)了一個bug時,都應(yīng)該增加測試用例!A歷經(jīng)千辛萬苦終于fix 了一個bug,很久很久以后,B覺得這段代碼需要改改,于是改了改,后來的結(jié)果還是改了,而且順利提交到了庫里(因為A當(dāng)時遇到的bug 并沒有出現(xiàn)!)
c. 回歸測試
修改代碼后進行回歸測試。每次提交一個版本后自動進行回歸測試,保證庫里的代碼的正確性。
d. 簡化測試用例
好處:可以排除不起作用的因素;減少測試用例的運行時間;最重要的是,使用測試用例更容易調(diào)試(誰愿意處理那些填充了數(shù)百或數(shù)千項的數(shù)據(jù)容器呢?)
方法如: 如果測試?yán)颖容^好改,可以將其改小;將輸入集改小
e. 完成代碼,清理后重新運行所有測試用例。
2. 關(guān)于程序的編譯
a. 重視編譯期間的warning,最好把warning數(shù)減為0. 不要忽略編譯器警告,即使它們可能是無害的。
eg:
int add(int a,int b){
return a +b ;
}
結(jié)果頭文件里聲明成了 extern int add(long a,int b)
會調(diào)試死人啊,調(diào)程序的時候一看程序定義是對的啊,怎么傳的參數(shù)一下就變了;
b. 如果出現(xiàn)莫名其妙的錯誤
如果是用Makefile組織工程時,考慮make clean,有可能修改數(shù)據(jù)結(jié)構(gòu)或頭文件后改變了一些東西,但是由于一些未知原因該文件并未重新編譯。如果函數(shù)是C函數(shù),有可能調(diào)用者和被 調(diào)用者的參數(shù)的成員和類型不同。如果一個類方法,則訪問任何類成員 都將發(fā)生錯誤,因為這兩個類的內(nèi)存而已幾乎是完全不同的。這可能導(dǎo)致Segmentation falut,或是很久之后才能檢測到的內(nèi)存破壞。
3. 關(guān)于鏈接
a. 鏈接器的基本工作原理
編譯器或匯編程序?qū)⒃创a轉(zhuǎn)換為機器代碼,并輸出對象誰的。對象文件中包含符號(函數(shù)或變量),這些符號有的在本模塊定義的,有的在其他模塊定義的,鏈接器就在鏈接對象文件時把這些未定義的符號與定義它的模塊對應(yīng)起來。
b. 鏈接順序
有庫和歸檔文件時 鏈接算法是不一樣的。
鏈接器參數(shù)順序很重要,對于編譯單元(如對象文件和庫)和搜索路徑來說都是如此。
c. C++中使用C代碼時,用extern c{} 把C代碼包裝一下。
關(guān)于 c++符號和名稱改編:C++允許重載函數(shù),為了生成C++代碼元素的唯一符號,編譯器使一種稱為名稱改編(name mangling)的技術(shù),它將對象的準(zhǔn)確規(guī)格說明(如會員名空間和函數(shù)參數(shù)的個數(shù)及類型)編碼到符號中。(可以用c++filt解析出來~ eg: c++filt _Z9factoriali的結(jié)果為factorial(int))
d. 環(huán)境變量
LD_LIBRARY_PATH會影響動態(tài)加載的庫,用LDD可以看到程序依賴哪個動態(tài)庫
4. 自動化測試
讓一切自動化起來。如果重復(fù)的做一件事,就很有必要考慮自動化了。
5. 關(guān)于那些怪異的錯誤
在一些顯而易見有內(nèi)存問題的情況下,如:間歇故障和無法解釋的隨機行為,這時考慮使用內(nèi)存調(diào)試器了!
如valgrind,很好用,也很簡單。
valgrind –tool=massif your_program 進行內(nèi)存剖析(檢測內(nèi)存分配情況,優(yōu)化內(nèi)存使用)
valgrind –tool=memcheck your_program 進行內(nèi)存檢查(檢測無效的寫訪問,檢測對未初始化的內(nèi)存的讀取操作,檢測內(nèi)存泄露等)
valgrind –tool=helgrind your_program 查找競爭條件,可以用來輔助調(diào)試多線程程序
valgrid –-db-attac=yes的功能很好用,可以將內(nèi)存高度器和源代碼測試器(如gdb)結(jié)合起來,這樣就可以即時查看當(dāng)時的變量的值,很好用!
6. 靜態(tài)檢查器
作為常規(guī)軟件構(gòu)建過程中的一部分運行,用于查找一些可通過靜態(tài)源代碼分析發(fā)現(xiàn)的特定bug。
7. 關(guān)于運行時剖析工具
不要編寫自己的運行時剖析時工具:自己霞友云朋一的剖析 工具通常使用系統(tǒng)調(diào)用time()或ctime()來測量時間。這些系統(tǒng)調(diào)用的問題是開銷很高,而且準(zhǔn)確度低。另處在剖析期間要收集大量數(shù)據(jù),可能會影響程序本身的行為。
8. 環(huán)境變量
如程序的行為可能 依賴于當(dāng)前工作目錄。在linux上,目錄被注冊到環(huán)境變量CWD上。這個bug碰到過,還導(dǎo)致了死鎖。
9. 讀取恰當(dāng)?shù)腻e誤消息
某個地方出錯時,滿屏都是錯誤消息時,應(yīng)該重點關(guān)注哪些消息?
Answer: 首先出現(xiàn)的那些消息!因為后面的消息有可能是前面導(dǎo)致的。這和編譯出錯時的情景一致:編譯錯誤有很多,我們肯定會直覺地去尋找第一個出錯的 地方,誰知道是不是少了個括號導(dǎo)致后面一連串的錯誤。
10. bug不會自動消失
如果某個版本有bug,update后,bug消失了,“真好!”,一定要弄清楚bug出現(xiàn)的原因是什么。以前遇到過一個bug,增加一條printf語句后,bug消失了!最后發(fā)現(xiàn)問題是數(shù)組越界了,而修改源代碼會導(dǎo)致代碼段,數(shù)據(jù)段的布局等改變,所以會導(dǎo)致偶爾對。(這種情況可以求助于內(nèi)存調(diào)試工具或者靜態(tài)檢查的工具)
11. 學(xué)習(xí)使用gcc, gdb,strace 等工具。(熟悉以后可以再挖掘挖掘,可能有驚喜)
12. cvs/svn commit之前一定要diff一下,看做了哪些修改,以避免不小心刪掉一些東西后,然后”被提交”了。
最后,最強大的工具不在計算機中,而是調(diào)試者的判斷力和分析技巧。
參考資料:
1. 《軟件調(diào)試實戰(zhàn)》:http://book.douban.com/subject/4231293/
if語句里的var變量經(jīng)替換后變?yōu)?if [ = "y" ],些時當(dāng)然會出錯。hex108@Gentoo ~ $ cat test.sh #!/bin/sh var= if [ $var = "y" ] ;then echo "yes" fi
hex108@Gentoo ~ $ ./test.sh
./test.sh: line 3: [: =: unary operator expected
ps:現(xiàn)在寫腳本的時候傾向于使用perl,而較少使用shell ,因為對于經(jīng)常使用的腳本,可能會經(jīng)常需要對它不停地進行改進,慢慢的,程序越來越大,該考慮重構(gòu)了, 此時才會發(fā)現(xiàn)perl(python等“真正的”腳本語言)比shell相對來說更好重構(gòu)。