本文是一系列探究調試器工做原理的文章的第一篇。我還不肯定這個系列須要包括多少篇文章以及它們所涵蓋的主題,但我打算從基礎知識開始提及。html
關於本文linux
我打算在這篇文章中介紹關於Linux下的調試器實現的主要組成部分——ptrace系統調用。本文中出現的代碼都在32位的Ubuntu系統上開發。請注意,這裏出現的代碼是同平臺緊密相關的,但移植到別的平臺上應該不會太難。ios
動機c++
要想理解咱們究竟要作什麼,試着想象一下調試器是如何工做的。調試器能夠啓動某些進程,而後對其進行調試,或者將本身自己關聯到一個已存在的進程之上。它能夠單步運行代碼,設置斷點而後運行程序,檢查變量的值以及跟蹤調用棧。許多調試器已經擁有了一些高級特性,好比執行表達式並在被調試進程的地址空間中調用函數,甚至能夠直接修改進程的代碼並觀察修改後的程序行爲。git
儘管現代的調試器都是複雜的大型程序,但使人驚訝的是構建調試器的基礎確是如此的簡單。調試器只用到了幾個由操做系統以及編譯器/連接器提供的基礎服務,剩下的僅僅就是簡單的編程問題了。(可查閱維基百科中關於這個詞條的解釋,做者是在反諷)程序員
Linux下的調試——ptracegithub
Linux下調試器擁有一個瑞士軍刀般的工具,這就是ptrace系統調用。這是一個功能衆多且至關複雜的工具,能容許一個進程控制另外一個進程的運行,並且能夠監視和滲入到進程內部。ptrace自己須要一本中等篇幅的書才能對其進行完整的解釋,這就是爲何我只打算經過例子把重點放在它的實際用途上。讓咱們繼續深刻探尋。web
遍歷進程的代碼redis
我如今要寫一個在「跟蹤」模式下運行的進程的例子,這裏咱們要單步遍歷這個進程的代碼——由CPU所執行的機器碼(彙編指令)。我會在這裏給出例子代碼,解釋每一個部分,本文結尾處你能夠經過連接下載一份完整的C程序文件,能夠自行編譯執行並研究。從高層設計來講,咱們要寫一個程序,它產生一個子進程用來執行一個用戶指定的命令,而父進程跟蹤這個子進程。首先,main函數是這樣的:shell
代碼至關簡單,咱們經過fork產生一個新的子進程。隨後的if語句塊處理子進程(這裏稱爲「目標進程」),而else if語句塊處理父進程(這裏稱爲「調試器」)。下面是目標進程:
這部分最有意思的地方在ptrace調用。ptrace的原型是(在sys/ptrace.h):
第一個參數是request,能夠是預約義的以PTRACE_打頭的常量值。第二個參數指定了進程id,第三以及第四個參數是地址和指向數據的指針,用來對內存作操做。上面代碼段中的ptrace調用使用了PTRACE_TRACEME請求,這表示這個子進程要求操做系統內核容許它的父進程對其跟蹤。這個請求在man手冊中解釋的很是清楚:
「代表這個進程由它的父進程來跟蹤。任何發給這個進程的信號(除了SIGKILL)將致使該進程中止運行,而它的父進程會經過wait()得到通知。另外,該進程以後全部對exec()的調用都將使操做系統產生一個SIGTRAP信號發送給它,這讓父進程有機會在新程序開始執行以前得到對子進程的控制權。若是不但願由父進程來跟蹤的話,那就不該該使用這個請求。(pid、addr、data被忽略)」
我已經把這個例子中咱們感興趣的地方高亮顯示了。注意,run_target在ptrace調用以後緊接着作的是經過execl來調用咱們指定的程序。這裏就會像咱們高亮顯示的部分所解釋的那樣,操做系統內核會在子進程開始執行execl中指定的程序以前中止該進程,併發送一個信號給父進程。
所以,是時候看看父進程須要作些什麼了:
經過上面的代碼咱們能夠回顧一下,一旦子進程開始執行exec調用,它就會中止而後接收到一個SIGTRAP信號。父進程經過第一個wait調用正在等待這個事件發生。一旦子進程中止(若是子進程因爲發送的信號而中止運行,WIFSTOPPED就返回true),父進程就去檢查這個事件。
父進程接下來要作的是本文中最有意思的地方。父進程經過PTRACE_SINGLESTEP以及子進程的id號來調用ptrace。這麼作是告訴操做系統——請從新啓動子進程,但當子進程執行了下一條指令後再將其中止。而後父進程再次等待子進程的中止,整個循環繼續得以執行。當從wait中獲得的不是關於子進程中止的信號時,循環結束。在正常運行這個跟蹤程序時,會獲得子進程正常退出(WIFEXITED會返回true)的信號。
icounter會統計子進程執行的指令數量。所以咱們這個簡單的例子實際上仍是作了點有用的事情——經過在命令行上指定一個程序名,咱們的例子會執行這個指定的程序,而後統計出從開始到結束該程序執行過的CPU指令總數。讓咱們看看實際運行的狀況。
實際測試
我編譯了下面這個簡單的程序,而後在咱們的跟蹤程序下執行:
令我驚訝的是,咱們的跟蹤程序運行了很長的時間而後報告顯示一共有超過100000條指令獲得了執行。僅僅只是一個簡單的printf調用,爲何會這樣?答案很是有意思。默認狀況下,Linux中的gcc編譯器會動態連接到C運行時庫。這意味着任何程序在運行時首先要作的事情是加載動態庫。這須要不少代碼實現——記住,咱們這個簡單的跟蹤程序會針對每一條被執行的指令計數,不只僅是main函數,而是整個進程。
所以,當我採用-static標誌靜態連接這個測試程序時(注意到可執行文件所以增長了500KB的大小,由於它靜態連接了C運行時庫),咱們的跟蹤程序報告顯示只有7000條左右的指令被執行了。這仍是很是多,但若是你瞭解到libc的初始化工做仍然先於main的執行,而清理工做會在main以後執行,那麼這就徹底說得通了。並且,printf也是一個複雜的函數。
咱們仍是不知足於此,我但願能看到一些可檢測的東西,例如我能夠從總體上看到每一條須要被執行的指令是什麼。這一點咱們能夠經過彙編代碼來獲得。所以我把這個「Hello,world」程序彙編(gcc -S)爲以下的彙編碼:
這就足夠了。如今跟蹤程序會報告有7條指令獲得了執行,我能夠很容易地從彙編代碼來驗證這一點。
深刻指令流
彙編碼程序得以讓我爲你們介紹ptrace的另外一個強大的功能——詳細檢查被跟蹤進程的狀態。下面是run_debugger函數的另外一個版本:
同前個版本相比,惟一的不一樣之處在於while循環的開始幾行。這裏有兩個新的ptrace調用。第一個讀取進程的寄存器值到一個結構體中。結構體user_regs_struct定義在sys/user.h中。這兒有個有趣的地方——若是你打開這個頭文件看看,靠近文件頂端的地方有一條這樣的註釋:
/* 本文件的惟一目的是爲GDB,且只爲GDB所用。對於這個文件,不要看的太多。除了GDB之外不要用於任何其餘目的,除非你知道你正在作什麼。*/
如今,我不知道你是怎麼想的,但我感受咱們正處於正確的跑道上。不管如何,回到咱們的例子上來。一旦咱們將全部的寄存器值獲取到regs中,咱們就能夠經過PTRACE_PEEKTEXT標誌以及將regs.eip(x86架構上的擴展指令指針)作參數傳入ptrace來調用。咱們所獲得的就是指令。讓咱們在彙編代碼上運行這個新版的跟蹤程序。
OK,因此如今除了icounter之外,咱們還能看到指令指針以及每一步的指令。如何驗證這是否正確呢?能夠經過在可執行文件上執行objdump –d來實現:
用這份輸出對比咱們的跟蹤程序輸出,應該很容易觀察到相同的地方。
關聯到運行中的進程上
你已經知道了調試器也能夠關聯到已經處於運行狀態的進程上。看到這裏,你應該不會感到驚訝,這也是經過ptrace來實現的。這須要經過PTRACE_ATTACH請求。這裏我不會給出一段樣例代碼,由於經過咱們已經看到的代碼,這應該很容易實現。基於教學的目的,這裏採用的方法更爲便捷(由於咱們能夠在子進程剛啓動時馬上將它中止)。
代碼
本文給出的這個簡單的跟蹤程序的完整代碼(更高級一點,能夠將具體指令打印出來)能夠在這裏找到。程序經過-Wall –pedantic –std=c99編譯選項在4.4版的gcc上編譯。
結論及下一步要作的
誠然,本文並無涵蓋太多的內容——咱們離一個真正可用的調試器還差的很遠。可是,我但願這篇文章至少已經揭開了調試過程的神祕面紗。ptrace是一個擁有許多功能的系統調用,目前咱們只展現了其中少數幾種功能。
可以單步執行代碼是頗有用處的,但做用有限。以「Hello, world」爲例,要到達main函數,須要先遍歷好幾千條初始化C運行時庫的指令。這就不太方便了。咱們所但願的理想方案是能夠在main函數入口處設置一個斷點,從斷點處開始單步執行。下一篇文章中我將向您展現該如何實現斷點機制。
參考文獻
寫做本文時我發現下面這些文章頗有幫助:
本文是關於調試器工做原理探究系列的第二篇。在開始閱讀本文前,請先確保你已經讀過本系列的第一篇(基礎篇)。
本文的主要內容
這裏我將說明調試器中的斷點機制是如何實現的。斷點機制是調試器的兩大主要支柱之一 ——另外一個是在被調試進程的內存空間中查看變量的值。咱們已經在第一篇文章中稍微涉及到了一些監視被調試進程的知識,但斷點機制仍然仍是個迷。閱讀完本文以後,這將再也不是什麼祕密了。
軟中斷
要在x86體系結構上實現斷點咱們要用到軟中斷(也稱爲「陷阱」trap)。在咱們深刻細節以前,我想先大體解釋一下中斷和陷阱的概念。
CPU有一個單獨的執行序列,會一條指令一條指令的順序執行。要處理相似IO或者硬件時鐘這樣的異步事件時CPU就要用到中斷。硬件中斷一般是一個專門的電信號,鏈接到一個特殊的「響應電路」上。這個電路會感知中斷的到來,而後會使CPU中止當前的執行流,保存當前的狀態,而後跳轉到一個預約義的地址處去執行,這個地址上會有一箇中斷處理例程。當中斷處理例程完成它的工做後,CPU就從以前中止的地方恢復執行。
軟中斷的原理相似,但實際上有一點不一樣。CPU支持特殊的指令容許經過軟件來模擬一箇中斷。當執行到這個指令時,CPU將其當作一箇中斷——中止當前正常的執行流,保存狀態而後跳轉到一個處理例程中執行。這種「陷阱」讓許多現代的操做系統得以有效完成不少複雜任務(任務調度、虛擬內存、內存保護、調試等)。
一些編程錯誤(好比除0操做)也被CPU當作一個「陷阱」,一般被認爲是「異常」。這裏軟中斷同硬件中斷之間的界限就變得模糊了,由於這裏很難說這種異常究竟是硬件中斷仍是軟中斷引發的。我有些偏離主題了,讓咱們回到關於斷點的討論上來。
關於int 3指令
看過前一節後,如今我能夠簡單地說斷點就是經過CPU的特殊指令——int 3來實現的。int就是x86體系結構中的「陷阱指令」——對預約義的中斷處理例程的調用。x86支持int指令帶有一個8位的操做數,用來指定所發生的中斷號。所以,理論上能夠支持256種「陷阱」。前32個由CPU本身保留,這裏第3號就是咱們感興趣的——稱爲「trap to debugger」。
很少說了,我這裏就引用「聖經」中的原話吧(這裏的聖經就是Intel’s Architecture software developer’s manual, volume2A):
「INT 3指令產生一個特殊的單字節操做碼(CC),這是用來調用調試異常處理例程的。(這個單字節形式很是有價值,由於這樣能夠經過一個斷點來替換掉任何指令的第一個字節,包括其它的單字節指令也是同樣,而不會覆蓋到其它的操做碼)。」
上面這段話很是重要,但如今解釋它仍是太早,咱們稍後再來看。
使用int 3指令
是的,懂得事物背後的原理是很棒的,可是這到底意味着什麼?咱們該如何使用int 3來實現斷點機制?套用常見的編程問答中出現的對話——請用代碼說話!
實際上這真的很是簡單。一旦你的進程執行到int 3指令時,操做系統就將它暫停。在Linux上(本文關注的是Linux平臺),這會給該進程發送一個SIGTRAP信號。
這就是所有——真的!如今回顧一下本系列文章的第一篇,跟蹤(調試器)進程能夠得到全部其子進程(或者被關聯到的進程)所獲得信號的通知,如今你知道咱們該作什麼了吧?
就是這樣,再沒有什麼計算機體系結構方面的東東了,該寫代碼了。
手動設定斷點
如今我要展現如何在程序中設定斷點。用於這個示例的目標程序以下:
我如今使用的是彙編語言,這是爲了不當使用C語言時涉及到的編譯和符號的問題。上面列出的程序功能就是在一行中打印「Hello,」,而後在下一行中打印「world!」。這個例子與上一篇文章中用到的例子很類似。
我但願設定的斷點位置應該在第一條打印以後,但剛好在第二條打印以前。咱們就讓斷點打在第一個int 0x80指令以後吧,也就是mov edx, len2。首先,我須要知道這條指令對應的地址是什麼。運行objdump –d:
經過上面的輸出,咱們知道要設定的斷點地址是0x8048096。等等,真正的調試器不是像這樣工做的,對吧?真正的調試器能夠根據代碼行數或者函數名稱來設定斷點,而不是基於什麼內存地址吧?很是正確。可是咱們離那個標準還差的遠——若是要像真正的調試器那樣設定斷點,咱們還須要涵蓋符號表以及調試信息方面的知識,這須要用另外一篇文章來講明。至於如今,咱們還必須得經過內存地址來設定斷點。
看到這裏我真的很想再扯一點題外話,因此你有兩個選擇。若是你真的對於爲何地址是0x8048096,以及這表明什麼意思很是感興趣的話,接着看下一節。若是你對此毫無興趣,只是想看看怎麼設定斷點,能夠略過這一部分。
題外話——進程地址空間以及入口點
坦白的說,0x8048096自己並無太大意義,這只不過是相對可執行鏡像的代碼段(text section)開始處的一個偏移量。若是你仔細看看前面objdump出來的結果,你會發現代碼段的起始位置是0x08048080。這告訴了操做系統要將代碼段映射到進程虛擬地址空間的這個位置上。在Linux上,這些地址能夠是絕對地址(好比,有的可執行鏡像加載到內存中時是不可重定位的),由於在虛擬內存系統中,每一個進程都有本身獨立的內存空間,並把整個32位的地址空間都看作是屬於本身的(稱爲線性地址)。
若是咱們經過readelf工具來檢查可執行文件的ELF頭,咱們將獲得以下輸出:
注意,ELF頭的「entry point address」一樣指向的是0x8048080。所以,若是咱們把ELF文件中的這個部分解釋給操做系統的話,就表示:
1. 將代碼段映射到地址0x8048080處
2. 從入口點處開始執行——地址0x8048080
可是,爲何是0x8048080呢?它的出現是因爲歷史緣由引發的。每一個進程的地址空間的前128MB被保留給棧空間了(注:這一部分緣由可參考Linkers and Loaders)。128MB恰好是0x80000000,可執行鏡像中的其餘段能夠從這裏開始。0x8048080是Linux下的連接器ld所使用的默認入口點。這個入口點能夠經過傳遞參數-Ttext給ld來進行修改。
所以,獲得的結論是這個地址並無什麼特別的,咱們能夠自由地修改它。只要ELF可執行文件的結構正確且在ELF頭中的入口點地址同程序代碼段(text section)的實際起始地址相吻合就OK了。
經過int 3指令在調試器中設定斷點
要在被調試進程中的某個目標地址上設定一個斷點,調試器須要作下面兩件事情:
1. 保存目標地址上的數據
2. 將目標地址上的第一個字節替換爲int 3指令
而後,當調試器向操做系統請求開始運行進程時(經過前一篇文章中提到的PTRACE_CONT),進程最終必定會碰到int 3指令。此時進程中止,操做系統將發送一個信號。這時就是調試器再次出馬的時候了,接收到一個其子進程(或被跟蹤進程)中止的信號,而後調試器要作下面幾件事:
1. 在目標地址上用原來的指令替換掉int 3
2. 將被跟蹤進程中的指令指針向後遞減1。這麼作是必須的,由於如今指令指針指向的是已經執行過的int 3以後的下一條指令。
3. 因爲進程此時仍然是中止的,用戶能夠同被調試進程進行某種形式的交互。這裏調試器可讓你查看變量的值,檢查調用棧等等。
4. 當用戶但願進程繼續運行時,調試器負責將斷點再次加到目標地址上(因爲在第一步中斷點已經被移除了),除非用戶但願取消斷點。
讓咱們看看這些步驟如何轉化爲實際的代碼。咱們將沿用第一篇文章中展現過的調試器「模版」(fork一個子進程,而後對其跟蹤)。不管如何,本文結尾處會給出完整源碼的連接。
這裏調試器從被跟蹤進程中獲取到指令指針,而後檢查當前位於地址0x8048096處的字長內容。運行本文前面列出的彙編碼程序,將打印出:
目前爲止一切順利,下一步:
注意看咱們是如何將int 3指令插入到目標地址上的。這部分代碼將打印出:
注意,「Hello,」在斷點以前打印出來了——同咱們計劃的同樣。同時咱們發現子進程已經中止運行了——就在這個單字節的陷阱指令執行以後。
這會使子進程打印出「world!」而後退出,同以前計劃的同樣。
注意,咱們這裏並無從新加載斷點。這能夠在單步模式下執行,而後將陷阱指令加回去,再作PTRACE_CONT就能夠了。本文稍後介紹的debug庫實現了這個功能。
更多關於int 3指令
如今是回過頭來講說int 3指令的好機會,以及解釋一下Intel手冊中對這條指令的奇怪說明。
「這個單字節形式很是有價值,由於這樣能夠經過一個斷點來替換掉任何指令的第一個字節,包括其它的單字節指令也是同樣,而不會覆蓋到其它的操做碼。」
x86架構上的int指令佔用2個字節——0xcd加上中斷號。int 3的二進制形式能夠被編碼爲cd 03,但這裏有一個特殊的單字節指令0xcc以一樣的做用而被保留。爲何要這樣作呢?由於這容許咱們在插入一個斷點時覆蓋到的指令不會多於一條。這很重要,考慮下面的示例代碼:
假設咱們要在dec eax上設定斷點。這剛好是條單字節指令(操做碼是0x48)。若是替換爲斷點的指令長度超過1字節,咱們就被迫改寫了接下來的下一條指令(call),這可能會產生一些徹底非法的行爲。考慮一下條件分支jz foo,這時進程可能不會在dec eax處中止下來(咱們在此設定的斷點,改寫了原來的指令),而是直接執行了後面的非法指令。
經過對int 3指令採用一個特殊的單字節編碼就能解決這個問題。由於x86架構上指令最短的長度就是1字節,這樣咱們能夠保證只有咱們但願中止的那條指令被修改。
封裝細節
前面幾節中的示例代碼展現了許多底層的細節,這些能夠很容易地經過API進行封裝。我已經作了一些封裝,使其成爲一個小型的調試庫——debuglib。代碼在本文末尾處能夠下載。這裏我只想介紹下它的用法,咱們要開始調試C程序了。
跟蹤C程序
目前爲止爲了簡單起見我把重點放在對彙編程序的跟蹤上了。如今升一級來看看咱們該如何跟蹤一個C程序。
其實事情並無很大的不一樣——只是如今有點難以找到放置斷點的位置。考慮以下這個簡單的C程序:
跟預計的狀況如出一轍!
代碼
這裏是完整的源碼。在文件夾中你會發現:
debuglib.h以及debuglib.c——封裝了調試器的一些內部工做。
bp_manual.c —— 本文一開始介紹的「手動」式設定斷點。用到了debuglib庫中的一些樣板代碼。
bp_use_lib.c—— 大部分代碼用到了debuglib,這就是本文中用於說明跟蹤一個C程序中的循環的示例代碼。
結論及下一步要作的
咱們已經涵蓋了如何在調試器中實現斷點機制。儘管實現細節根據操做系統的不一樣而有所區別,但只要你使用的是x86架構的處理器,那麼一切變化都基於相同的主題——在咱們但願中止的指令上將其替換爲int 3。
我敢確定,有些讀者就像我同樣,對於經過指定原始地址來設定斷點的作法不會感到很激動。咱們更但願說「在do_stuff上停住」,甚至是「在do_stuff的這一行上停住」,而後調試器就能照辦。在下一篇文章中,我將向您展現這是如何作到的。
本文是調試器工做原理探究系列的第三篇,在閱讀前請先確保已經讀過本系列的第一和第二篇。
本篇主要內容
在本文中我將向你們解釋關於調試器是如何在機器碼中尋找C函數以及變量的,以及調試器使用了何種數據可以在C源代碼的行號和機器碼中來回映射。
調試信息
現代的編譯器在轉換高級語言程序代碼上作得十分出色,可以將源代碼中漂亮的縮進、嵌套的控制結構以及任意類型的變量全都轉化爲一長串的比特流——這就是機器碼。這麼作的惟一目的就是但願程序能在目標CPU上儘量快的運行。大多數的C代碼都被轉化爲一些機器碼指令。變量散落在各處——在棧空間裏、在寄存器裏,甚至徹底被編譯器優化掉。結構體和對象甚至在生成的目標代碼中根本不存在——它們只不過是對內存緩衝區中偏移量的抽象化表示。
那麼當你在某些函數的入口處設置斷點時,調試器如何知道該在哪裏中止目標進程的運行呢?當你但願查看一個變量的值時,調試器又是如何找到它並展現給你呢?答案就是——調試信息。
調試信息是在編譯器生成機器碼的時候一塊兒產生的。它表明着可執行程序和源代碼之間的關係。這個信息以預約義的格式進行編碼,並同機器碼一塊兒存儲。許多年以來,針對不一樣的平臺和可執行文件,人們發明了許多這樣的編碼格式。因爲本文的主要目的不是介紹這些格式的歷史淵源,而是爲您展現它們的工做原理,因此咱們只介紹一種最重要的格式,這就是DWARF。做爲Linux以及其餘類Unix平臺上的ELF可執行文件的調試信息格式,現在的DWARF能夠說是無處不在。
ELF文件中的DWARF格式
根據維基百科上的詞條解釋,DWARF是同ELF可執行文件格式一同設計出來的,儘管在理論上DWARF也可以嵌入到其它的對象文件格式中。
DWARF是一種複雜的格式,在多種體系結構和操做系統上通過多年的探索以後,人們纔在以前的格式基礎上建立了DWARF。它確定是很複雜的,由於它解決了一個很是棘手的問題——爲任意類型的高級語言和調試器之間提供調試信息,支持任意一種平臺和應用程序二進制接口(ABI)。要徹底解釋清楚這個主題,本文就顯得太微不足道了。說實話,我也不理解其中的全部角落。本文我將採起更加實踐的方法,只介紹足量的DWARF相關知識,可以闡明實際工做中調試信息是如何發揮其做用的就能夠了。
ELF文件中的調試段
首先,讓咱們看看DWARF格式信息處在ELF文件中的什麼位置上。ELF能夠爲每一個目標文件定義任意多個段(section)。而Section header表中則定義了實際存在有哪些段,以及它們的名稱。不一樣的工具以各自特殊的方式來處理這些不一樣的段,好比連接器只尋找它關注的段信息,而調試器則只關注其餘的段。
咱們經過下面的C代碼構建一個名爲traceprog2的可執行文件來作下實驗。
每行的第一個數字表示每一個段的大小,而最後一個數字表示距離ELF文件開始處的偏移量。調試器就是利用這個信息來從可執行文件中讀取相關的段信息。如今,讓咱們經過一些實際的例子來看看如何在DWARF中找尋有用的調試信息。
定位函數
當咱們在調試程序時,一個最爲基本的操做就是在某些函數中設置斷點,指望調試器能在函數入口處將程序斷下。要完成這個功能,調試器必須具備某種可以從源代碼中的函數名稱到機器碼中該函數的起始指令間相映射的能力。
這個信息能夠經過從DWARF中的.debug_info段獲取到。在咱們繼續以前,先說點背景知識。DWARF的基本描述實體被稱爲調試信息表項(Debugging Information Entry —— DIE),每一個DIE有一個標籤——包含它的類型,以及一組屬性。各個DIE之間經過兄弟和孩子結點互相連接,屬性值能夠指向其餘的DIE。
咱們運行
沒錯,從反彙編結果來看0x8048604確實就是函數do_stuff的起始地址。所以,這裏調試器就同函數和它們在可執行文件中的位置確立了映射關係。
定位變量
假設咱們確實在do_stuff中的斷點處停了下來。咱們但願調試器可以告訴咱們my_local變量的值,調試器怎麼知道去哪裏找到相關的信息呢?這可比定位函數要難多了,由於變量能夠在全局數據區,能夠在棧上,甚至是在寄存器中。另外,具備相同名稱的變量在不一樣的詞法做用域中可能有不一樣的值。調試信息必須可以反映出全部這些變化,而DWARF確實能作到這些。
我不會涵蓋全部的可能狀況,做爲例子,我將只展現調試器如何在do_stuff函數中定位到變量my_local。咱們從.debug_info段開始,再次看看do_stuff這一項,這一次咱們也看看其餘的子項:
注意每個表項中第一個尖括號裏的數字,這表示嵌套層次——在這個例子中帶有<2>的表項都是表項<1>的子項。所以咱們知道變量my_local(以DW_TAG_variable做爲標籤)是函數do_stuff的一個子項。調試器一樣還對變量的類型感興趣,這樣才能正確的顯示變量的值。這裏my_local的類型根據DW_AT_type標籤可知爲<0x4b>。若是查看objdump的輸出,咱們會發現這是一個有符號4字節整數。
要在執行進程的內存映像中實際定位到變量,調試器須要檢查DW_AT_location屬性。對於my_local來講,這個屬性爲DW_OP_fberg: -20。這表示變量存儲在從所包含它的函數的DW_AT_frame_base屬性開始偏移-20處,而DW_AT_frame_base正表明了該函數的棧幀起始點。
函數do_stuff的DW_AT_frame_base屬性的值是0x0(location list),這表示該值必需要在location list段去查詢。咱們看看objdump的輸出:
關於位置信息,咱們這裏感興趣的就是第一個。對於調試器可能定位到的每個地址,它都會指定當前棧幀到變量間的偏移量,而這個偏移就是經過寄存器來計算的。對於x86體系結構,bpreg4表明esp寄存器,而bpreg5表明ebp寄存器。
讓咱們再看看do_stuff的開頭幾條指令:
注意,ebp只有在第二條指令執行後才與咱們創建起關聯,對於前兩個地址,基地址由前面列出的位置信息中的esp計算得出。一旦獲得了ebp的有效值,就能夠很方便的計算出與它之間的偏移量。由於以後ebp保持不變,而esp會隨着數據壓棧和出棧不斷移動。
那麼這到底爲咱們定位變量my_local留下了什麼線索?咱們感興趣的只是在地址0x8048610上的指令執行事後my_local的值(這裏my_local的值會經過eax寄存器計算,然後放入內存)。所以調試器須要用到DW_OP_breg5: 8 基址來定位。如今回顧一下my_local的DW_AT_location屬性:DW_OP_fbreg: -20。作下算數:從基址開始偏移-20,那就是ebp – 20,再偏移+8,咱們獲得ebp – 12。如今再看看反彙編輸出,注意到數據確實是從eax寄存器中獲得的,而ebp – 12就是my_local存儲的位置。
定位到行號
當我說到在調試信息中尋找函數時,我撒了個小小的謊。當咱們調試C源代碼並在函數中放置了一個斷點時,咱們一般並不會對第一條機器碼指令感興趣。咱們真正感興趣的是函數中的第一行C代碼。
這就是爲何DWARF在可執行文件中對C源碼到機器碼地址作了所有映射。這部分信息包含在.debug_line段中,能夠按照可讀的形式進行解讀:
不難看出C源碼同反彙編輸出之間的關係。第5行源碼指向函數do_stuff的入口點——地址0x8040604。接下第6行源碼,當在do_stuff上設置斷點時,這裏就是調試器實際應該停下的地方,它指向地址0x804860a——剛過do_stuff的開場白。這個行信息可以方便的在C源碼的行號同指令地址間創建雙向的映射關係。
1. 當在某一行上設定斷點時,調試器將利用行信息找到實際應該陷入的地址(還記得前一篇中的int 3指令嗎?)
2. 當某個指令引發段錯誤時,調試器會利用行信息反過來找出源代碼中的行號,並告訴用戶。
libdwarf —— 在程序中訪問DWARF
經過命令行工具來訪問DWARF信息這雖然有用但還不能徹底令咱們滿意。做爲程序員,咱們但願知道應該如何寫出實際的代碼來解析DWARF格式並從中讀取咱們須要的信息。
天然的,一種方法就是拿起DWARF規範開始鑽研。還記得每一個人都告訴你永遠不要本身手動解析HTML,而應該使用函數庫來作嗎?沒錯,若是你要手動解析DWARF的話狀況會更糟糕,DWARF比HTML要複雜的多。本文展現的只是冰山一角而已。更困難的是,在實際的目標文件中,這些信息大部分都以很是緊湊和壓縮的方式進行編碼處理。
所以咱們要走另外一條路,使用一個函數庫來同DWARF打交道。我知道的這類函數庫主要有兩個:
1. BFD(libbfd),GNU binutils就是使用的它,包括本文中屢次使用到的工具objdump,ld(GNU連接器),以及as(GNU彙編器)。
2. libdwarf —— 同它的老大哥libelf同樣,爲Solaris以及FreeBSD系統上的工具服務。
我這裏選擇了libdwarf,由於對我來講它看起來沒那麼神祕,並且license更加自由(LGPL,BFD是GPL)。
因爲libdwarf自身很是複雜,須要不少代碼來操做。我這裏不打算把全部代碼貼出來,但你能夠下載,而後本身編譯運行。要編譯這個文件,你須要安裝libelf以及libdwarf,並在編譯時爲連接器提供-lelf以及-ldwarf標誌。
這個演示程序接收一個可執行文件,並打印出程序中的函數名稱同函數入口點地址。下面是本文用以演示的C程序產生的輸出:
libdwarf的文檔很是好(見本文的參考文獻部分),花點時間看看,對於本文中提到的DWARF段信息你處理起來就應該沒什麼問題了。
結論及下一步
調試信息只是一個簡單的概念,具體實現細節可能至關複雜。但最終咱們知道了調試器是如何從可執行文件中找出同源代碼之間的關係。有了調試信息在手,調試器爲用戶所能識別的源代碼和數據結構同可執行文件之間架起了一座橋。
本文加上以前的兩篇文章總結了調試器內部的工做原理。經過這一系列文章,再加上一點編程工做就應該能夠在Linux下建立一個具備基本功能的調試器。
至於下一步,我還不肯定。也許我會就此終結這一系列文章,也許我會再寫一些高級主題好比backtrace,甚至Windows系統上的調試。讀者們也能夠爲從此這一系列文章提供意見和想法。不要客氣,請隨意在評論欄或經過Email給我提些建議吧。
調試(Debug)
軟件調試是在進行了成功的測試以後纔開始的工做,它與軟件測試不一樣,調試的任務是進一步診斷和改正程序中潛在的錯誤。
調試活動由兩部分組成:
u 肯定程序中可疑錯誤的確切性質和位置
u 對程序(設計,編碼)進行修改,排除這個錯誤
調試工做是一個具備很強技巧性的工做
軟件運行失效或出現問題,每每只是潛在錯誤的外部表現,而外部表現與內在緣由之間經常沒有明顯的聯繫,若是要找出真正的緣由,排除潛在的錯誤,不是一件易事。
能夠說,調試是經過現象,找出緣由的一個思惟分析的過程。
調試步驟:
(1) 從錯誤的外部表現形式入手,肯定程序中出錯位置
(2) 研究有關部分的程序,找出錯誤的內在緣由
(3) 修改設計代碼,以排除這個錯誤
(4) 重複進行暴露了這個錯誤的原始測試或某些有關測試。
從技術角度來看查找錯誤的難度在於:
u 現象與緣由所處的位置可能相距甚遠
u 當其餘錯誤獲得糾正時,這一錯誤所表現出的現象可能會暫時消失,但併爲實際排除
u 現象其實是由一些非錯誤緣由(例如,舍入不精確)引發的
u 現象多是因爲一些不容易發現的人爲錯誤引發的
u 錯誤是因爲時序問題引發的,與處理過程無關
u 現象是因爲難於精確再現的輸入狀態(例如,實時應用中輸入順序不肯定)引發
u 現象多是週期出現的,在軟,硬件結合的嵌入式系統中經常遇到
幾種主要的調試方法
調試的關鍵在於推斷程序內部的錯誤位置及緣由,能夠採用如下方法:
強行排錯
這種調試方法目前使用較多,效率較低,它不須要過多的思考,比較省腦筋。例如:
u 經過內存所有打印來調試,在這大量的數據中尋找出錯的位置。
u 在程序特定位置設置打印語句,把打印語句插在出錯的源程序的各個關鍵變量改變部位,重要分支部位,子程序調用部位,跟蹤程序的執行,監視重要變量的變化
u 自動調用工具,利用某些程序語言的調試功能或專門的交互式調試工具,分析程序的動態過程,而沒必要修改程序。
應用以上任一種方法以前,都應當對錯誤的徵兆進行全面完全的分析,得出對出錯位置及錯誤性質的推測,再使用一種適當的調試方法來檢驗推測的正確性。
回溯法調試
這是在小程序中經常使用的一種有效的調試方法,一旦發現了錯誤,人們先分析錯誤的徵兆,肯定最早發現「症狀「的位置
而後,人工沿程序的控制流程,向回追蹤源程序代碼,直到找到錯誤根源或肯定錯誤產生的範圍,
例如,程序中發現錯誤處是某個打印語句,經過輸出值可推斷程序在這一點上變量的值,再從這一點出發,回溯程序的執行過程,反覆思考:「若是程序在這一點上的狀態(變量的值)是這樣,那麼程序在上一點的狀態必定是這樣···「直到找到錯誤所在。
概括法調試
概括法是一種從特殊推斷通常的系統化思考方法,概括法調試的基本思想是:從一些線索(錯誤徵兆)着手,經過分析它們之間的關係來找出錯誤
u 收集有關的數據,列出全部已知的測試用例和程序執行結果,看哪些輸入數據的運行結果是正確的,哪些輸入數據的運行通過是有錯誤的
u 組織數據
因爲概括法是從特殊到通常的推斷過程,因此須要組織整理數據,以發現規律
常以3W1H形式組織可用的數據
「What「列出通常現象
「Where「說明發現現象的地點
「When「列出現象發生時全部已知狀況
「How「說明現象的範圍和量級
「Yes「描述出現錯誤的3W1H;
「No「做爲比較,描述了沒有錯誤的3W1H,經過分析找出矛盾來
u 提出假設
分析線索之間的關係,利用在線索結構中觀察到的矛盾現象,設計一個或多個關於出錯緣由的假設,若是一個假設也提不出來,概括過程就須要收集更多的數據,此時,應當再設計與執行一些測試用例,以得到更多的數據。
u 證實假設
把假設與原始線索或數據進行比較,若它能徹底解釋一切現象,則假設獲得證實,不然,認爲假設不合理,或不徹底,或是存在多個錯誤,以至只能消除部分錯誤
演繹法調試
演繹法是一種從通常原理或前提出發,通過排除和精華的過程來推導出結論的思考方法,演繹法排錯是測試人員首先根據已有的測試用例,設想及枚舉出全部可能出錯的緣由做爲假設,而後再用原始測試數據或新的測試,從中逐個排除不可能正確的假設,最後,再用測試數據驗證餘下的假設確是出錯的緣由。
u 列舉全部可能出錯緣由的假設,把全部可能的錯誤緣由列成表,經過它們,能夠組織,分析現有數據
u 利用已有的測試數據,排除不正確的假設
仔細分析已有的數據,尋找矛盾,力求排除前一步列出全部緣由,若是全部緣由都被排除了,則須要補充一些數據(測試用例),以創建新的假設。
u 改進餘下的假設
利用已知的線索,進一步改進餘下的假設,使之更具體化,以即可以精確地肯定出錯位置
u 證實餘下的假設
調試原則
n 在調試方面,許多原則本質上是心理學方面的問題,調試由兩部分組成,調試原則也分紅兩組。
n 肯定錯誤的性質和位置的原則
u 用頭腦去分析思考與錯誤徵兆有關的信息
u 避開死衚衕
u 只把調試工具當作輔助手段來使用,利用調試工具,能夠幫助思考,但不能代替思考
u 避免用試探法,最多隻能把它當作最後手段
n 修改錯誤的原則
u 在出現錯誤的地方,頗有可能還有別的錯誤
u 修改錯誤的一個常見失誤是只修改了這個錯誤的徵兆或這個錯誤的表現,而沒有修改錯誤的自己。
u 小心修正一個錯誤的同時有可能會引入新的錯誤
u 修改錯誤的過程將迫令人們暫時回到程序設計階段
u 修改源代碼程序,不要改變目標代碼
本文將從應用程序、編譯器和調試器三個層次來說解,在不一樣的層次,有不一樣的方法,這些方法有各本身的長處和侷限。瞭解這些知識,一方面知足一下新手的好奇心,另外一方面也可能有用得着的時候。
從應用程序的角度
最好的狀況是從設計到編碼都紮紮實實的,避免把錯誤引入到程序中來,這纔是解決問題的根本之道。問題在於,理想狀況並不存在,現實中存在着大量有內存錯誤的程序,若是內存錯誤很容易避免,JAVA/C#的優點將不會那麼突出了。
對於內存錯誤,應用程序本身能作的很是有限。但因爲這類內存錯誤很是典型,所佔比例很是大,所付出的努力與所得的回報相比是很是划算的,仍然值得研究。
前面咱們講了,堆裏面的內存是由內存管理器管理的。從應用程序的角度來看,咱們能作到的就是打內存管理器的主意。其實原理很簡單:
對付內存泄露。重載內存管理函數,在分配時,把這塊內存的記錄到一個鏈表中,在釋放時,從鏈表中刪除吧,在程序退出時,檢查鏈表是否爲空,若是不爲空,則說明有內存泄露,不然說明沒有泄露。固然,爲了查出是哪裏的泄露,在鏈表還要記錄是誰分配的,一般記錄文件名和行號就好了。
對付內存越界/野指針。對這二者,咱們只能檢查一些典型的狀況,對其它一些狀況無能爲力,但效果仍然不錯。其方法以下(源於《Comparing and contrasting the runtime error detection technologies》):
l 首尾在加保護邊界值
Header
Leading guard(0xFC)
User data(0xEB)
Tailing guard(0xFC)
在內存分配時,內存管理器按如上結構填充分配出來的內存。其中Header是管理器本身用的,先後各有幾個字節的guard數據,它們的值是固定的。當內存釋放時,內存管理器檢查這些guard數據是否被修改,若是被修改,說明有寫越界。
它的工做機制註定了有它的侷限性: 只能檢查寫越界,不能檢查讀越界,並且只能檢查連續性的寫越界,對於跳躍性的寫越界無能爲力。
l 填充空閒內存
空閒內存(0xDD)
內存被釋放以後,它的內容填充成固定的值。這樣,從指針指向的內存的數據,能夠大體判斷這個指針是不是野指針。
它一樣有它的侷限:程序要主動判斷才行。若是野指針指向的內存當即被從新分配了,它又被填充成前面那個結構,這時也沒法檢查出來。
從編譯器的角度
boundschecker和purify的實現均可以歸於編譯器一級。前者採用一種稱爲CTI(compile-time instrumentation)的技術。VC的編譯不是要分幾個階段嗎?boundschecker在預處理和編譯兩個階段之間,對源文件進行修改。它對全部內存分配釋放、內存讀寫、指針賦值和指針計算等全部內存相關的操做進行分析,並插入本身的代碼。好比:
Before
if (m_hsession) gblHandles->ReleaseUserHandle( m_hsession );
if (m_dberr) delete m_dberr;
After
if (m_hsession) {
_Insight_stack_call(0);
gblHandles->ReleaseUserHandle(m_hsession);
_Insight_after_call();
}
_Insight_ptra_check(1994, (void **) &m_dberr, (void *) m_dberr);
if (m_dberr) {
_Insight_deletea(1994, (void **) &m_dberr, (void *) m_dberr, 0);
delete m_dberr;
}
Purify則採用一種稱爲OCI(object code insertion)的技術。不一樣的是,它對可執行文件的每條指令進行分析,找出全部內存分配釋放、內存讀寫、指針賦值和指針計算等全部內存相關的操做,用本身的指令代替原始的指令。
boundschecker和purify是商業軟件,它們的實現是保密的,甚至擁有專利的,沒法對其研究,只能找一些皮毛性的介紹。不管是CTI仍是OCI這樣的名稱,多少有些神祕感。其實它們的實現原理並不複雜,經過對valgrind和gcc的bounds checker擴展進行一些粗淺的研究,咱們能夠知道它們的大體原理。
gcc的bounds checker基本上能夠與boundschecker對應起來,都是對源代碼進行修改,以達到控制內存操做功能,如malloc/free等內存管理函數、memcpy/strcpy/memset等內存讀取函數和指針運算等。Valgrind則與Purify相似,都是經過對目標代碼進行修改,來達到一樣的目的。
Valgrind對可執行文件進行修改,因此不須要從新編譯程序。但它並非在執行前對可執行文件和全部相關的共享庫進行一次性修改,而是和應用程序在同一個進程中運行,動態的修改即將執行的下一段代碼。
Valgrind是插件式設計的。Core部分負責對應用程序的總體控制,並把即將修改的代碼,轉換成一種中間格式,這種格式相似於RISC指令,而後把中間代碼傳給插件。插件根據要求對中間代碼修改,而後把修改後的結果交給core。core接下來把修改後的中間代碼轉換成原始的x86指令,並執行它。
因而可知,不管是boundschecker、purify、gcc的bounds checker,仍是Valgrind,修改源代碼也罷,修改二進制也罷,都是代碼進行修改。究竟要修改什麼,修改爲什麼樣子呢?別急,下面咱們就要來介紹:
管理全部內存塊。不管是堆、棧仍是全局變量,只要有指針引用它,它就被記錄到一個全局表中。記錄的信息包括內存塊的起始地址和大小等。要作到這一點並不難:對於在堆裏分配的動態內存,能夠經過重載內存管理函數來實現。對於全局變量等靜態內存,能夠從符號表中獲得這些信息。
攔截全部的指針計算。對於指針進行乘除等運算一般意義不大,最多見運算是對指針加減一個偏移量,如++p、p=p+n、p=a[n]等。全部這些有意義的指針操做,都要受到檢查。再也不是由一條簡單的彙編指令來完成,而是由一個函數來完成。
有了以上兩點保證,要檢查內存錯誤就很是容易了:好比要檢查++p是否有效,首先在全局表中查找p指向的內存塊,若是沒有找到,說明p是野指針。若是找到了,再檢查p+1是否在這塊內存範圍內,若是不是,那就是越界訪問,不然是正常的了。怎麼樣,簡單吧,不管是全局內存、堆仍是棧,不管是讀仍是寫,無一可以逃過出工具的法眼。
代碼賞析(源於tcc):
對指針運算進行檢查:
void *__bound_ptr_add(void *p, int offset)
{
unsigned long addr = (unsigned long)p;
BoundEntry *e;
#if defined(BOUND_DEBUG)
printf("add: 0x%x %d\n", (int)p, offset);
#endif
e = __bound_t1[addr >> (BOUND_T2_BITS + BOUND_T3_BITS)];
e = (BoundEntry *)((char *)e +
((addr >> (BOUND_T3_BITS - BOUND_E_BITS)) &
((BOUND_T2_SIZE - 1) << BOUND_E_BITS)));
addr -= e->start;
if (addr > e->size) {
e = __bound_find_region(e, p);
addr = (unsigned long)p - e->start;
}
addr += offset;
if (addr > e->size)
return INVALID_POINTER;
return p + offset;
}
static void __bound_check(const void *p, size_t size)
{
if (size == 0)
return;
p = __bound_ptr_add((void *)p, size);
if (p == INVALID_POINTER)
bound_error("invalid pointer");
}
重載內存管理函數:
void *__bound_malloc(size_t size, const void *caller)
{
void *ptr;
ptr = libc_malloc(size + 1);
if (!ptr)
return NULL;
__bound_new_region(ptr, size);
return ptr;
}
void __bound_free(void *ptr, const void *caller)
{
if (ptr == NULL)
return;
if (__bound_delete_region(ptr) != 0)
bound_error("freeing invalid region");
libc_free(ptr);
}
重載內存操做函數:
void *__bound_memcpy(void *dst, const void *src, size_t size)
{
__bound_check(dst, size);
__bound_check(src, size);
if (src >= dst && src < dst + size)
bound_error("overlapping regions in memcpy()");
return memcpy(dst, src, size);
}
從調試器的角度
如今有OS的支持,實現一個調試器變得很是簡單,至少原理再也不神祕。這裏咱們簡要介紹一下win32和linux中的調試器實現原理。
在Win32下,實現調試器主要經過兩個函數:WaitForDebugEvent和ContinueDebugEvent。下面是一個調試器的基本模型(源於: 《Debugging Applications for Microsoft .NET and Microsoft Windows》)
void main ( void )
{
CreateProcess ( ..., DEBUG_ONLY_THIS_PROCESS ,... ) ;
while ( 1 == WaitForDebugEvent ( ... ) )
{
if ( EXIT_PROCESS )
{
break ;
}
ContinueDebugEvent ( ... ) ;
}
}
由調試器起動被調試的進程,並指定DEBUG_ONLY_THIS_PROCESS標誌。按Win32下事件驅動的一向原則,由被調試的進程主動上報調試事件,調試器而後作相應的處理。
在linux下,實現調試器只要一個函數就好了:ptrace。下面是個簡單示例:(源於《Playing with ptrace》)。
#include <sys/ptrace.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
#include <linux/user.h>
int main(int argc, char *argv[])
{ pid_t traced_process;
struct user_regs_struct regs;
long ins;
if(argc != 2) {
printf("Usage: %s <pid to be traced>\n",
argv[0], argv[1]);
exit(1);
}
traced_process = atoi(argv[1]);
ptrace(PTRACE_ATTACH, traced_process,
NULL, NULL);
wait(NULL);
ptrace(PTRACE_GETREGS, traced_process,
NULL, ®s);
ins = ptrace(PTRACE_PEEKTEXT, traced_process,
regs.eip, NULL);
printf("EIP: %lx Instruction executed: %lx\n",
regs.eip, ins);
ptrace(PTRACE_DETACH, traced_process,
NULL, NULL);
return 0;
}
因爲篇幅有限,這裏對於調試器的實現不做深刻討論,主要是給新手指一個方向。之後如有時間,再寫個專題來介紹linux下的調試器和ptrace自己的實現方法。
嚴格的講,調試器是幫助程序員跟蹤,分隔和從軟件中移除bug的工具。它幫助程序員更進一步理解程序。一開始,主要是開發人員使用它,後來測試人員,維護人員也開始使用它。
調試器的發展歷程:
調試器的設計和開發要遵循四個關鍵的原則:
按照劃分的標準不一樣,調試器主要分爲一下幾類:
調試器之間的區別更多的是體如今他們展示給用戶的窗口。至於底層結構都是很相近的。下圖展現了調試器的整體架構:
調試器服務於全部的調試器視圖。包括進程控制,執行引擎,表達式計算,符號表管理四部分。
調試器內核爲了訪問被調試程序,必須使用操做系統提供的一系列例程。
調試器控制被調試程序的能力主要是依靠硬件支持和操做系統的調試機制。調試器須要最少三種的硬件功能的支持:
1. 提供設置斷點的方法;
2. 通知操做系統發生中斷或者陷阱的功能;
3. 當中斷或者陷阱發生時,直接讀寫寄存器,包括程序計數器。
通用的硬件調試機制
1. 斷點支持
斷點功能是經過特定的指令來實現的。對於變長指令的處理器,斷點指令一般是最短的指令,下圖給出了四個處理器的斷點指令:
2. 單步調試支持
單步調試是指執行一條指令就產生一次中斷,是用戶能夠查找每條指令的執行狀態。通常的處理器都提供一個模式位來實現單步調試功能。
3. 錯誤檢測支持
錯誤檢測功能是指當操做系統檢測到錯誤發生時,他通知調試器被它調試的程序發生了錯誤。
4. 檢測點支持
用來查看被調試程序的地址空間(數據空間)。
5. 多線程支持
6. 多處理器支持
調試器的操做系統支持功能
爲了控制一個被調試程序的過程,調試器須要一種機制去通知操做系統該可執行文件但願被控制。即一旦被調試程序因爲某些緣由中止的時候,調試器須要獲取詳細的信息使得他知道被調試程序是什麼緣由形成他中止的。
調試器是用戶級的程序,並非操做系統的一部分,並不能運行特權級指令,所以,它只能經過調用操做系統的系統調用來實現對特權級指令的訪問。
調試器運行被調試程序,並將控制權轉交給被調試程序,須要進行上下文切換。在一個簡單的斷點功能實現,有6個主要的轉換:
1. 當調試器運行到斷點指令的時候,產生陷阱跳轉到操做系統;
2. 經過操做系統,跳轉到調試器,調試器開始運行;
3. 調試器請求被調試程序的狀態信息,該請求送到操做系統進行處理;
4. 轉換到被調試程序文本以獲取信息,被調試程序激活;
5. 返回信息給操做系統;
6. 轉換到調試器以處理信息。
一旦使用圖形界面調試器,過程會更加的複雜。
對於多線程調試的支持;
l 一旦進程建立和刪除,操做系統必須通知調試器;
l 可以詢問和設置特定進程的進程狀態;
l 可以檢測到應用程序中止,或者線程中止。
例子:UNIX ptrace()
UNIX ptrace 是操做系統支持調試器的一個真實的API。
控制執行
調試器的核心是它的進程控制和運行控制。爲了可以調試程序,調試器必須可以對被調試程序進行狀態設置,斷點設置,運行進程,終止進程。
控制執行主要包含一下幾個功能:
1. 建立被調試程序
調試器作的第一件工做,就是建立被調試程序。通常經過兩種手段:一種是爲調試程序建立被調試進程,另外一種是將調試器附到被調試進程上。
2. 附到被調試進程
當一個進程發生錯誤異常,而且在被刷出(內存刷新)內存的時候,容許調試器掛到出錯進程以此來檢查內存鏡像。這個時候,用戶不能再繼續執行進程。
3. 設置斷點
設置斷點的功能是在可執行文本中插入特殊的指令來實現的。當程序執行到該特殊指令的時候,就產生陷阱,陷到操做系統。
4. 使被調試程序運行
當調試中斷產生的時候,調試器屬於激活進程,而被調試程序屬於未激活進程。調試器產生一個系統中斷請求恢復被調用函數的執行,操做系統對被調試程序進行上下文切換,恢復被調用程序的現場狀態,而後執行被調用程序。
執行區間的調試事件生成類型:
l 斷點,單步調試事件
l 線程建立/刪除事件
l 進程建立/刪除事件
l 檢測點事件
l 模塊加載/卸載事件
l 異常事件
l 其餘事件
斷點和單步調試
斷點一般須要兩層的表示:
l 邏輯表示:指在源代碼中設置的斷點,用來告訴用戶的;
l 物理表示:指真實的在機器碼中寫入,是用來告訴物理機器的。斷點必須存儲寫入位置的機器指令,以便可以在移除斷點的時候恢復原來的指令。
斷點存在條件斷點。
斷點存在多對一的關係,即多個用戶在同一個地方設置斷點(多個邏輯斷點對應一個物理斷點),固然也有多對多的關係。下圖展現了這樣的一個關係:
臨時斷點
臨時斷點是指只運行一次的斷點。
內部斷點
內部斷點對用戶是不可見的。他們是被調試器設置的。
通常主要用於:
l 單步調試:內部斷點和運行到內部斷點;
l 跳出函數:在函數返回地址設置內部斷點;
l 進入函數
查看程序的上下文信息
通常要查找程序的上下文信息主要有如下幾種方法:
經過源代碼查看程序執行到代碼的那一部分
程序堆棧是由硬件,操做系統和編譯器共同支持的:
硬件: 提供堆棧指針;
操做系統:爲每一個進程創建堆棧空間,並管理堆棧。一旦堆棧溢出,而產生一個錯誤;
$ 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
$ ulimit -c 0 <--------- c選項指定修改core文件的大小
$ ulimit -c 1000 <--------指定了core文件大小爲1000KB, 若是設置的大小小於core文件,則對core文件截取
$ ulimit -c unlimited <---------------對core文件的大小不作限制
$ echo "0" > /proc/sys/kernel/core_uses_pid
$ 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'
$ 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
$ gdb exec_file core_file
$ 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格式文件中沒有符號表信息
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 ()
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;
(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
(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
(gdb) x /b 0x7ffc4ad9d64c
0x7ffc4ad9d64c: Cannot access memory at address 0x7ffc4ad9d64c
一,什麼是coredump
咱們常常聽到你們說到程序core掉了,須要定位解決,這裏說的大部分是指對應程序因爲各類異常或者bug致使在運行過程當中異常退出或者停止,而且在知足必定條件下(這裏爲何說須要知足必定的條件呢?下面會分析)會產生一個叫作core的文件。
一般狀況下,core文件會包含了程序運行時的內存,寄存器狀態,堆棧指針,內存管理信息還有各類函數調用堆棧信息等,咱們能夠理解爲是程序工做當前狀態存儲生成第一個文件,許多的程序出錯的時候都會產生一個core文件,經過工具分析這個文件,咱們能夠定位到程序異常退出的時候對應的堆棧調用等信息,找出問題所在並進行及時解決。
二,coredump文件的存儲位置
core文件默認的存儲位置與對應的可執行程序在同一目錄下,文件名是core,你們能夠經過下面的命令看到core文件的存在位置:
cat /proc/sys/kernel/core_pattern
缺省值是core
注意:這裏是指在進程當前工做目錄的下建立。一般與程序在相同的路徑下。但若是程序中調用了chdir函數,則有可能改變了當前工做目錄。這時core文件建立在chdir指定的路徑下。有好多程序崩潰了,咱們卻找不到core文件放在什麼位置。和chdir函數就有關係。固然程序崩潰了不必定都產生 core文件。
以下程序代碼:則會把生成的core文件存儲在/data/coredump/wd,而不是你們認爲的跟可執行文件在同一目錄。
經過下面的命令能夠更改coredump文件的存儲位置,若你但願把core文件生成到/data/coredump/core目錄下:
echo 「/data/coredump/core」> /proc/sys/kernel/core_pattern
注意,這裏當前用戶必須具備對/proc/sys/kernel/core_pattern的寫權限。
缺省狀況下,內核在coredump時所產生的core文件放在與該程序相同的目錄中,而且文件名固定爲core。很顯然,若是有多個程序產生core文件,或者同一個程序屢次崩潰,就會重複覆蓋同一個core文件,所以咱們有必要對不一樣程序生成的core文件進行分別命名。
咱們經過修改kernel的參數,能夠指定內核所生成的coredump文件的文件名。例如,使用下面的命令使kernel生成名字爲core.filename.pid格式的core dump文件:
echo 「/data/coredump/core.%e.%p」 >/proc/sys/kernel/core_pattern
這樣配置後,產生的core文件中將帶有崩潰的程序名、以及它的進程ID。上面的%e和%p會被替換成程序文件名以及進程ID。
若是在上述文件名中包含目錄分隔符「/」,那麼所生成的core文件將會被放到指定的目錄中。 須要說明的是,在內核中還有一個與coredump相關的設置,就是/proc/sys/kernel/core_uses_pid。若是這個文件的內容被配置成1,那麼即便core_pattern中沒有設置%p,最後生成的core dump文件名仍會加上進程ID。
三,如何判斷一個文件是coredump文件?
在類unix系統下,coredump文件自己主要的格式也是ELF格式,所以,咱們能夠經過readelf命令進行判斷。
能夠看到ELF文件頭的Type字段的類型是:CORE (Core file)
能夠經過簡單的file命令進行快速判斷:
四,產生coredum的一些條件總結
1, 產生coredump的條件,首先須要確認當前會話的ulimit –c,若爲0,則不會產生對應的coredump,須要進行修改和設置。
ulimit -c unlimited (能夠產生coredump且不受大小限制)
若想甚至對應的字符大小,則能夠指定:
ulimit –c [size]
能夠看出,這裏的size的單位是blocks,通常1block=512bytes
如:
ulimit –c 4 (注意,這裏的size若是過小,則可能不會產生對應的core文件,筆者設置過ulimit –c 1的時候,系統並不生成core文件,並嘗試了1,2,3均沒法產生core,至少須要4才生成core文件)
但當前設置的ulimit只對當前會話有效,若想系統均有效,則須要進行以下設置:
Ø 在/etc/profile中加入如下一行,這將容許生成coredump文件
ulimit-c unlimited
Ø 在rc.local中加入如下一行,這將使程序崩潰時生成的coredump文件位於/data/coredump/目錄下:
echo /data/coredump/core.%e.%p> /proc/sys/kernel/core_pattern
注意rc.local在不一樣的環境,存儲的目錄可能不一樣,susu下可能在/etc/rc.d/rc.local
更多ulimit的命令使用,能夠參考:http://baike.baidu.com/view/4832100.htm
這些須要有root權限, 在ubuntu下每次從新打開中斷都須要從新輸入上面的ulimit命令, 來設置core大小爲無限.
2, 當前用戶,即執行對應程序的用戶具備對寫入core目錄的寫權限以及有足夠的空間。
3, 幾種不會產生core文件的狀況說明:
The core file will not be generated if
(a) the process was set-user-ID and the current user is not the owner of the program file, or
(b) the process was set-group-ID and the current user is not the group owner of the file,
(c) the user does not have permission to write in the current working directory,
(d) the file already exists and the user does not have permission to write to it, or
(e) the file is too big (recall the RLIMIT_CORE limit in Section 7.11). The permissions of the core file (assuming that the file doesn't already exist) are usually user-read and user-write, although Mac OS X sets only user-read.
五,coredump產生的幾種可能狀況
形成程序coredump的緣由有不少,這裏總結一些比較經常使用的經驗吧:
1,內存訪問越界
a) 因爲使用錯誤的下標,致使數組訪問越界。
b) 搜索字符串時,依靠字符串結束符來判斷字符串是否結束,可是字符串沒有正常的使用結束符。
c) 使用strcpy, strcat, sprintf, strcmp,strcasecmp等字符串操做函數,將目標字符串讀/寫爆。應該使用strncpy, strlcpy, strncat, strlcat, snprintf, strncmp, strncasecmp等函數防止讀寫越界。
2,多線程程序使用了線程不安全的函數。
應該使用下面這些可重入的函數,它們很容易被用錯:
asctime_r(3c) gethostbyname_r(3n) getservbyname_r(3n)ctermid_r(3s) gethostent_r(3n) getservbyport_r(3n) ctime_r(3c) getlogin_r(3c)getservent_r(3n) fgetgrent_r(3c) getnetbyaddr_r(3n) getspent_r(3c)fgetpwent_r(3c) getnetbyname_r(3n) getspnam_r(3c) fgetspent_r(3c)getnetent_r(3n) gmtime_r(3c) gamma_r(3m) getnetgrent_r(3n) lgamma_r(3m) getauclassent_r(3)getprotobyname_r(3n) localtime_r(3c) getauclassnam_r(3) etprotobynumber_r(3n)nis_sperror_r(3n) getauevent_r(3) getprotoent_r(3n) rand_r(3c) getauevnam_r(3)getpwent_r(3c) readdir_r(3c) getauevnum_r(3) getpwnam_r(3c) strtok_r(3c) getgrent_r(3c)getpwuid_r(3c) tmpnam_r(3s) getgrgid_r(3c) getrpcbyname_r(3n) ttyname_r(3c)getgrnam_r(3c) getrpcbynumber_r(3n) gethostbyaddr_r(3n) getrpcent_r(3n)
3,多線程讀寫的數據未加鎖保護。
對於會被多個線程同時訪問的全局數據,應該注意加鎖保護,不然很容易形成coredump
4,非法指針
a) 使用空指針
b) 隨意使用指針轉換。一個指向一段內存的指針,除非肯定這段內存原先就分配爲某種結構或類型,或者這種結構或類型的數組,不然不要將它轉換爲這種結構或類型的指針,而應該將這段內存拷貝到一個這種結構或類型中,再訪問這個結構或類型。這是由於若是這段內存的開始地址不是按照這種結構或類型對齊的,那麼訪問它時就很容易由於bus error而core dump。
5,堆棧溢出
不要使用大的局部變量(由於局部變量都分配在棧上),這樣容易形成堆棧溢出,破壞系統的棧和堆結構,致使出現莫名其妙的錯誤。
六,利用gdb進行coredump的定位
其實分析coredump的工具備不少,如今大部分類unix系統都提供了分析coredump文件的工具,不過,咱們常常用到的工具是gdb。
這裏咱們以程序爲例子來講明如何進行定位。
1, 段錯誤 – segmentfault
Ø 咱們寫一段代碼往受到系統保護的地址寫內容。
Ø 按以下方式進行編譯和執行,注意這裏須要-g選項編譯。
能夠看到,當輸入12的時候,系統提示段錯誤而且core dumped
Ø 咱們進入對應的core文件生成目錄,優先確認是否core文件格式並啓用gdb進行調試。
從紅色方框截圖能夠看到,程序停止是由於信號11,且從bt(backtrace)命令(或者where)能夠看到函數的調用棧,即程序執行到coremain.cpp的第5行,且裏面調用scanf 函數,而該函數其實內部會調用_IO_vfscanf_internal()函數。
接下來咱們繼續用gdb,進行調試對應的程序。
記住幾個經常使用的gdb命令:
l(list) ,顯示源代碼,而且能夠看到對應的行號;
b(break)x, x是行號,表示在對應的行號位置設置斷點;
p(print)x, x是變量名,表示打印變量x的值
r(run), 表示繼續執行到斷點的位置
n(next),表示執行下一步
c(continue),表示繼續執行
q(quit),表示退出gdb
啓動gdb,注意該程序編譯須要-g選項進行。
注: SIGSEGV 11 Core Invalid memoryreference
七,附註:
1, gdb的查看源碼
顯示源代碼
GDB 能夠打印出所調試程序的源代碼,固然,在程序編譯時必定要加上-g的參數,把源程序信息編譯到執行文件中。否則就看不到源程序了。當程序停下來之後,GDB會報告程序停在了那個文件的第幾行上。你能夠用list命令來打印程序的源代碼。仍是來看一看查看源代碼的GDB命令吧。
list<linenum>
顯示程序第linenum行的周圍的源程序。
list<function>
顯示函數名爲function的函數的源程序。
list
顯示當前行後面的源程序。
list -
顯示當前行前面的源程序。
通常是打印當前行的上5行和下5行,若是顯示函數是是上2行下8行,默認是10行,固然,你也能夠定製顯示的範圍,使用下面命令能夠設置一次顯示源程序的行數。
setlistsize <count>
設置一次顯示源代碼的行數。
showlistsize
查看當前listsize的設置。
list命令還有下面的用法:
list<first>, <last>
顯示從first行到last行之間的源代碼。
list ,<last>
顯示從當前行到last行之間的源代碼。
list +
日後顯示源代碼。
通常來講在list後面能夠跟如下這些參數:
<linenum> 行號。
<+offset> 當前行號的正偏移量。
<-offset> 當前行號的負偏移量。
<filename:linenum> 哪一個文件的哪一行。
<function> 函數名。
<filename:function>哪一個文件中的哪一個函數。
<*address> 程序運行時的語句在內存中的地址。
2, 一些經常使用signal的含義
SIGABRT:調用abort函數時產生此信號。進程異常終止。
SIGBUS:指示一個實現定義的硬件故障。
SIGEMT:指示一個實現定義的硬件故障。EMT這一名字來自PDP-11的emulator trap 指令。
SIGFPE:此信號表示一個算術運算異常,例如除以0,浮點溢出等。
SIGILL:此信號指示進程已執行一條非法硬件指令。4.3BSD由abort函數產生此信號。SIGABRT如今被用於此。
SIGIOT:這指示一個實現定義的硬件故障。IOT這個名字來自於PDP-11對於輸入/輸出TRAP(input/outputTRAP)指令的縮寫。系統V的早期版本,由abort函數產生此信號。SIGABRT如今被用於此。
SIGQUIT:當用戶在終端上按退出鍵(通常採用Ctrl-/)時,產生此信號,並送至前臺進
程組中的全部進程。此信號不只終止前臺進程組(如SIGINT所作的那樣),同時產生一個core文件。
SIGSEGV:指示進程進行了一次無效的存儲訪問。名字SEGV表示「段違例(segmentationviolation)」。
SIGSYS:指示一個無效的系統調用。因爲某種未知緣由,進程執行了一條系統調用指令,但其指示系統調用類型的參數倒是無效的。
SIGTRAP:指示一個實現定義的硬件故障。此信號名來自於PDP-11的TRAP指令。
SIGXCPUSVR4和4.3+BSD支持資源限制的概念。若是進程超過了其軟C P U時間限制,則產生此信號。
SIGXFSZ:若是進程超過了其軟文件長度限制,則SVR4和4.3+BSD產生此信號。
3, Core_pattern的格式
能夠在core_pattern模板中使用變量還不少,見下面的列表:
%% 單個%字符
%p 所dump進程的進程ID
%u 所dump進程的實際用戶ID
%g 所dump進程的實際組ID
%s 致使本次core dump的信號
%t core dump的時間 (由1970年1月1日計起的秒數)
%h 主機名
%e 程序文件名
利用Core Dump調試程序
[描述]
這裏介紹Linux環境下使用gdb結合core dump文件進行程序的調試和定位。
[簡介]
當用戶程序運行,可能會因爲某些緣由發生崩潰(crash),這個時候能夠產生一個Core Dump文件,記錄程序發生崩潰時候內存的運行情況。這個Core Dump文件,通常名稱爲core或者core.pid(pid就是應用程序運行時候的pid號),它能夠幫助咱們找出程序崩潰的緣由。
對於一個運行出錯的程序,咱們能夠有多種方法調試它,以便發生錯誤的緣由:a)經過閱讀代碼;b)經過在代碼中設置一些打印語句(插旗子);c)經過使用gdb設置斷點來跟蹤程序的運行。可是這些方法對於調試程序運行崩潰這樣相似的錯誤,定位都不夠迅速,若是程序代碼不少的話,顯然前面的方法有不少缺陷。在後面,咱們來看看另一種能夠定位錯誤的方法:d)使用gdb結合Core Dump文件來迅速定位到這個錯誤。這個方法,若是程序運行崩潰,那麼能夠迅速找到致使程序崩潰的緣由。
固然,調試程序,沒有哪一個方法是最好的,這裏只對最後一種方法重點講解,實際過程當中,每每根據須要和其餘方法結合使用。
[舉例]
下面,給出一個實際的操做過程,描述咱們使用gdb調試工具,結合Core Dump文件,定位程序崩潰的位置。
1、程序源代碼
下面是咱們的程序源代碼:
1 #include <iostream>
2 using std::cerr;
3 using std::endl;
4
5 void my_print(int d1, int d2);
6 int main(int argc, char *argv[])
7 {
8 int a = 1;
9 int b = 2;
10 my_print(a,b);
11 return 0;
12 }
13
14 void my_print(int d1, int d2)
15 {
16 int *p1=&d1;
17 int *p2 = NULL;
18 cerr<<"first is:"<<*p1<<",second is:"<<*p2<<endl;
19 }
這裏,程序代碼不多,咱們能夠直接經過代碼看到這個程序第17行的p2是NULL,而18行卻用*p2來進行引用,明顯這樣訪問一個空的地址是一個錯誤(也許咱們的初衷是使用p2來指向d2)。
咱們能夠有多種方法調試這個程序,以便發生上面的錯誤:a)經過閱讀代碼;b)經過在代碼中設置一些打印語句(插旗子);c)經過使用gdb設置斷點來跟蹤程序的運行。可是這些方法對於這個程序中相似的錯誤,定位都不夠迅速,若是程序代碼不少的話,顯然前面的方法有不少缺陷。在後面,咱們來看看另一種能夠定位錯誤的方法:d)使用gdb結合Core Dump文件來迅速定位到這個錯誤。
2、編譯程序:
編譯過程以下:
[root@lv-k test]# ls
main.cpp
[root@lv-k test]# g++ -g main.cpp
[root@lv-k test]# ls
a.out main.cpp
這樣,編譯main.cpp生成了可執行文件a.out,必定注意,由於咱們要使用gdb進行調試,因此咱們使用'g++'的'-g'選項。
3、運行程序
運行過程以下:
[root@lv-k test]# ./a.out
段錯誤
[root@lv-k test]# ls
a.out main.cpp
這裏,如咱們所指望的,會打印段錯誤的信息,可是並無生成Core Dump文件。
配置生成Core Dump文件的選項,並生成Core Dump:
[root@lv-k test]# ulimit -c unlimited
[root@lv-k test]# ./a.out
段錯誤 (core dumped)
[root@lv-k test]# ls
a.out core.30557 main.cpp
這裏,咱們看到,使用'ulimit'配置以後,程序崩潰的時候就會生成Core Dump文件了,這裏的文件是core.30557,文件名稱不一樣的系統生成的名稱有一點不一樣,這裏linux生成的名稱是:"core"+".pid"。
4、調試程序
使用Core Dump文件,結合gdb工具進行調試,過程以下:
1)初步定位:
[root@lv-k test]# gdb a.out core.30557
GNU gdb (GDB) Red Hat Enterprise Linux (7.0.1-23.el5_5.2)
Copyright (C) 2009 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 "i386-redhat-linux-gnu".
For bug reporting instructions, please see:
<http://www.gnu.org/software/gdb/bugs/>...
Reading symbols from /root/test/a.out...done.
Reading symbols from /usr/lib/libstdc++.so.6...(no debugging symbols found)...done.
Loaded symbols for /usr/lib/libstdc++.so.6
Reading symbols from /lib/libm.so.6...(no debugging symbols found)...done.
Loaded symbols for /lib/libm.so.6
Reading symbols from /lib/libgcc_s.so.1...(no debugging symbols found)...done.
Loaded symbols for /lib/libgcc_s.so.1
Reading symbols from /lib/libc.so.6...(no debugging symbols found)...done.
Loaded symbols for /lib/libc.so.6
Reading symbols from /lib/ld-linux.so.2...(no debugging symbols found)...done.
Loaded symbols for /lib/ld-linux.so.2
Core was generated by `./a.out'.
Program terminated with signal 11, Segmentation fault.
#0 0x0804870e in my_print (d1=1, d2=2) at main.cpp:18
18 cerr<<"first is:"<<*p1<<",second is:"<<*p2<<endl;
這裏,咱們就進入了gdb的調試交互界面,看到gdb直接定位到致使程序出錯的位置了。咱們還能夠使用以下命令:"#gdb a.out --core=core.30557"。
經過錯誤,咱們知道程序因爲"signal 11"致使終止,若是想要大體瞭解"signal 11",那麼咱們可查看signal的man手冊:
#man 7 signal
這樣,在輸出的信息中咱們能夠看見「SIGSEGV 11 Core Invalid memory reference」這樣的字樣,意思是說,signal(信號)11表示非法內存引用。注意這裏使用"man 7 signal"而不是"man signal",由於咱們要查看的不是signal函數或者signal命令,而是signal的其餘信息,其餘的信息在man手冊的第7節,具體須要瞭解一些使用man的命令。
2)查看具體調用關係
(gdb) bt
#0 0x0804870e in my_print (d1=1, d2=2) at main.cpp:18
#1 0x08048799 in main (argc=<value optimized out>, argv=<value optimized out>) at main.cpp:10
這裏,咱們經過backtrace(簡寫爲bt)命令能夠看到,致使崩潰那條語句是經過什麼調用途徑被調用到的。
3)設置斷點,並進行調試等:
(gdb) b main.cpp:10
Breakpoint 1 at 0x8048787: file main.cpp, line 10.
(gdb) r
Starting program: /root/test/a.out
Breakpoint 1, main (argc=<value optimized out>, argv=<value optimized out>) at main.cpp:10
10 my_print(a,b);
(gdb) s
my_print (d1=1, d2=2) at main.cpp:16
16 int *p1=&d1;
(gdb) n
17 int *p2 = NULL;
(gdb) n
18 cerr<<"first is:"<<*p1<<",second is:"<<*p2<<endl;
(gdb) p d1
$1 = 1
(gdb) p d2
$2 = 2
(gdb) p *p1
$1 = 1
(gdb) p *p2
Cannot access memory at address 0x0
(gdb) n
Program received signal SIGSEGV, Segmentation fault.
0x0804870e in my_print (d1=1, d2=2) at main.cpp:18
18 cerr<<"first is:"<<*p1<<",second is:"<<*p2<<endl;
這裏,咱們在開始初步的定位的基礎上,經過設置斷點(break),運行(run),gdb的單步跟進(step),單步跳過(next),變量的打印(print)等各類gdb命令,來了解產生崩潰時候的具體狀況,肯定產生崩潰的緣由。
4)退出gdb:
(gdb) q
A debugging session is active.
Inferior 3 [process 30584] will be killed.
Inferior 1 [process 1] will be killed.
Quit anyway? (y or n) y
Quitting: Couldn't get registers: 沒有那個進程.
[root@lv-k test]#
[root@lv-k test]# ls
a.out core.30557 core.30609 main.cpp
這裏,咱們看到又產生了一個core文件。由於剛纔調試,致使又產生了一個core文件。實際,若是咱們只使用"gdb a.out core.30557"初步定位以後,不進行調試就退出gdb的話,就不會再生成core文件。
5、修正錯誤
1)經過上面的過程咱們最終修正錯誤,獲得正確的源代碼以下:
1 #include <iostream>
2 using std::cerr;
3 using std::endl;
4
5 void my_print(int d1, int d2);
6 int main(int argc, char *argv[])
7 {
8 int a = 1;
9 int b = 2;
10 my_print(a,b);
11 return 0;
12 }
13
14 void my_print(int d1, int d2)
15 {
16 int *p1=&d1;
17 //int *p2 = NULL;//lvkai-
18 int *p2 = &d2;//lvkai+
19 cerr<<"first is:"<<*p1<<",second is:"<<*p2<<endl;
20 }
2)編譯並運行這個程序,最終產生結果以下:
[root@lv-k test]# g++ main.cpp
[root@lv-k test]# ls
a.out main.cpp
[root@lv-k test]# ./a.out
first is:1,second is:2
這裏,獲得了咱們預期的結果。
另外,有個小技巧,若是對Makefile有些瞭解的話能夠充分利用make的隱含規則來編譯單個源文件的程序,
過程以下:
[root@lv-k test]# ls
main.cpp
[root@lv-k test]# make main
g++ main.cpp -o main
[root@lv-k test]# ls
main main.cpp
[root@lv-k test]# ./main
first is:1,second is:2
這裏注意,make的目標參數必須是源文件"main.cpp"去掉後綴以後的"main",等價於"g++ main.cpp -o main",這樣編譯的命令比較簡單。
[其它]
其它內容有待添加。
認真地工做而且思考,是最好的老師。在工做的過程當中思考本身所缺少的技術,以及學習他人的經驗,才能在工做中有所收穫。這篇文章原來是工做中個人一個同事加朋友的經驗,我站在這樣的經驗的基礎上,進行了這樣總結。
一,什麼是coredump
咱們常常聽到你們說到程序core掉了,須要定位解決,這裏說的大部分是指對應程序因爲各類異常或者bug致使在運行過程當中異常退出或者停止,而且在知足必定條件下(這裏爲何說須要知足必定的條件呢?下面會分析)會產生一個叫作core的文件。
一般狀況下,core文件會包含了程序運行時的內存,寄存器狀態,堆棧指針,內存管理信息還有各類函數調用堆棧信息等,咱們能夠理解爲是程序工做當前狀態存儲生成第一個文件,許多的程序出錯的時候都會產生一個core文件,經過工具分析這個文件,咱們能夠定位到程序異常退出的時候對應的堆棧調用等信息,找出問題所在並進行及時解決。
二,coredump文件的存儲位置
core文件默認的存儲位置與對應的可執行程序在同一目錄下,文件名是core,你們能夠經過下面的命令看到core文件的存在位置:
cat /proc/sys/kernel/core_pattern
缺省值是core
注意:這裏是指在進程當前工做目錄的下建立。一般與程序在相同的路徑下。但若是程序中調用了chdir函數,則有可能改變了當前工做目錄。這時core文件建立在chdir指定的路徑下。有好多程序崩潰了,咱們卻找不到core文件放在什麼位置。和chdir函數就有關係。固然程序崩潰了不必定都產生 core文件。
以下程序代碼:則會把生成的core文件存儲在/data/coredump/wd,而不是你們認爲的跟可執行文件在同一目錄。
經過下面的命令能夠更改coredump文件的存儲位置,若你但願把core文件生成到/data/coredump/core目錄下:
echo 「/data/coredump/core」> /proc/sys/kernel/core_pattern
注意,這裏當前用戶必須具備對/proc/sys/kernel/core_pattern的寫權限。
缺省狀況下,內核在coredump時所產生的core文件放在與該程序相同的目錄中,而且文件名固定爲core。很顯然,若是有多個程序產生core文件,或者同一個程序屢次崩潰,就會重複覆蓋同一個core文件,所以咱們有必要對不一樣程序生成的core文件進行分別命名。
咱們經過修改kernel的參數,能夠指定內核所生成的coredump文件的文件名。例如,使用下面的命令使kernel生成名字爲core.filename.pid格式的core dump文件:
echo 「/data/coredump/core.%e.%p」 >/proc/sys/kernel/core_pattern
這樣配置後,產生的core文件中將帶有崩潰的程序名、以及它的進程ID。上面的%e和%p會被替換成程序文件名以及進程ID。
若是在上述文件名中包含目錄分隔符「/」,那麼所生成的core文件將會被放到指定的目錄中。 須要說明的是,在內核中還有一個與coredump相關的設置,就是/proc/sys/kernel/core_uses_pid。若是這個文件的內容被配置成1,那麼即便core_pattern中沒有設置%p,最後生成的core dump文件名仍會加上進程ID。
三,如何判斷一個文件是coredump文件?
在類unix系統下,coredump文件自己主要的格式也是ELF格式,所以,咱們能夠經過readelf命令進行判斷。
能夠看到ELF文件頭的Type字段的類型是:CORE (Core file)
能夠經過簡單的file命令進行快速判斷:
四,產生coredum的一些條件總結
1, 產生coredump的條件,首先須要確認當前會話的ulimit –c,若爲0,則不會產生對應的coredump,須要進行修改和設置。
ulimit -c unlimited (能夠產生coredump且不受大小限制)
若想甚至對應的字符大小,則能夠指定:
ulimit –c [size]
能夠看出,這裏的size的單位是blocks,通常1block=512bytes
如:
ulimit –c 4 (注意,這裏的size若是過小,則可能不會產生對應的core文件,筆者設置過ulimit –c 1的時候,系統並不生成core文件,並嘗試了1,2,3均沒法產生core,至少須要4才生成core文件)
但當前設置的ulimit只對當前會話有效,若想系統均有效,則須要進行以下設置:
Ø 在/etc/profile中加入如下一行,這將容許生成coredump文件
ulimit-c unlimited
Ø 在rc.local中加入如下一行,這將使程序崩潰時生成的coredump文件位於/data/coredump/目錄下:
echo /data/coredump/core.%e.%p> /proc/sys/kernel/core_pattern
注意rc.local在不一樣的環境,存儲的目錄可能不一樣,susu下可能在/etc/rc.d/rc.local
更多ulimit的命令使用,能夠參考:http://baike.baidu.com/view/4832100.htm
這些須要有root權限, 在ubuntu下每次從新打開中斷都須要從新輸入上面的ulimit命令, 來設置core大小爲無限.
2, 當前用戶,即執行對應程序的用戶具備對寫入core目錄的寫權限以及有足夠的空間。
3, 幾種不會產生core文件的狀況說明:
The core file will not be generated if
(a) the process was set-user-ID and the current user is not the owner of the program file, or
(b) the process was set-group-ID and the current user is not the group owner of the file,
(c) the user does not have permission to write in the current working directory,
(d) the file already exists and the user does not have permission to write to it, or
(e) the file is too big (recall the RLIMIT_CORE limit in Section 7.11). The permissions of the core file (assuming that the file doesn't already exist) are usually user-read and user-write, although Mac OS X sets only user-read.
五,coredump產生的幾種可能狀況
形成程序coredump的緣由有不少,這裏總結一些比較經常使用的經驗吧:
1,內存訪問越界
a) 因爲使用錯誤的下標,致使數組訪問越界。
b) 搜索字符串時,依靠字符串結束符來判斷字符串是否結束,可是字符串沒有正常的使用結束符。
c) 使用strcpy, strcat, sprintf, strcmp,strcasecmp等字符串操做函數,將目標字符串讀/寫爆。應該使用strncpy, strlcpy, strncat, strlcat, snprintf, strncmp, strncasecmp等函數防止讀寫越界。
2,多線程程序使用了線程不安全的函數。
應該使用下面這些可重入的函數,它們很容易被用錯:
asctime_r(3c) gethostbyname_r(3n) getservbyname_r(3n)ctermid_r(3s) gethostent_r(3n) getservbyport_r(3n) ctime_r(3c) getlogin_r(3c)getservent_r(3n) fgetgrent_r(3c) getnetbyaddr_r(3n) getspent_r(3c)fgetpwent_r(3c) getnetbyname_r(3n) getspnam_r(3c) fgetspent_r(3c)getnetent_r(3n) gmtime_r(3c) gamma_r(3m) getnetgrent_r(3n) lgamma_r(3m) getauclassent_r(3)getprotobyname_r(3n) localtime_r(3c) getauclassnam_r(3) etprotobynumber_r(3n)nis_sperror_r(3n) getauevent_r(3) getprotoent_r(3n) rand_r(3c) getauevnam_r(3)getpwent_r(3c) readdir_r(3c) getauevnum_r(3) getpwnam_r(3c) strtok_r(3c) getgrent_r(3c)getpwuid_r(3c) tmpnam_r(3s) getgrgid_r(3c) getrpcbyname_r(3n) ttyname_r(3c)getgrnam_r(3c) getrpcbynumber_r(3n) gethostbyaddr_r(3n) getrpcent_r(3n)
3,多線程讀寫的數據未加鎖保護。
對於會被多個線程同時訪問的全局數據,應該注意加鎖保護,不然很容易形成coredump
4,非法指針
a) 使用空指針
b) 隨意使用指針轉換。一個指向一段內存的指針,除非肯定這段內存原先就分配爲某種結構或類型,或者這種結構或類型的數組,不然不要將它轉換爲這種結構或類型的指針,而應該將這段內存拷貝到一個這種結構或類型中,再訪問這個結構或類型。這是由於若是這段內存的開始地址不是按照這種結構或類型對齊的,那麼訪問它時就很容易由於bus error而core dump。
5,堆棧溢出
不要使用大的局部變量(由於局部變量都分配在棧上),這樣容易形成堆棧溢出,破壞系統的棧和堆結構,致使出現莫名其妙的錯誤。
六,利用gdb進行coredump的定位
其實分析coredump的工具備不少,如今大部分類unix系統都提供了分析coredump文件的工具,不過,咱們常常用到的工具是gdb。
這裏咱們以程序爲例子來講明如何進行定位。
1, 段錯誤 – segmentfault
Ø 咱們寫一段代碼往受到系統保護的地址寫內容。
Ø 按以下方式進行編譯和執行,注意這裏須要-g選項編譯。
能夠看到,當輸入12的時候,系統提示段錯誤而且core dumped
Ø 咱們進入對應的core文件生成目錄,優先確認是否core文件格式並啓用gdb進行調試。
從紅色方框截圖能夠看到,程序停止是由於信號11,且從bt(backtrace)命令(或者where)能夠看到函數的調用棧,即程序執行到coremain.cpp的第5行,且裏面調用scanf 函數,而該函數其實內部會調用_IO_vfscanf_internal()函數。
接下來咱們繼續用gdb,進行調試對應的程序。
記住幾個經常使用的gdb命令:
l(list) ,顯示源代碼,而且能夠看到對應的行號;
b(break)x, x是行號,表示在對應的行號位置設置斷點;
p(print)x, x是變量名,表示打印變量x的值
r(run), 表示繼續執行到斷點的位置
n(next),表示執行下一步
c(continue),表示繼續執行
q(quit),表示退出gdb
啓動gdb,注意該程序編譯須要-g選項進行。
注: SIGSEGV 11 Core Invalid memoryreference
七,附註:
1, gdb的查看源碼
顯示源代碼
GDB 能夠打印出所調試程序的源代碼,固然,在程序編譯時必定要加上-g的參數,把源程序信息編譯到執行文件中。否則就看不到源程序了。當程序停下來之後,GDB會報告程序停在了那個文件的第幾行上。你能夠用list命令來打印程序的源代碼。仍是來看一看查看源代碼的GDB命令吧。
list<linenum>
顯示程序第linenum行的周圍的源程序。
list<function>
顯示函數名爲function的函數的源程序。
list
顯示當前行後面的源程序。
list -
顯示當前行前面的源程序。
通常是打印當前行的上5行和下5行,若是顯示函數是是上2行下8行,默認是10行,固然,你也能夠定製顯示的範圍,使用下面命令能夠設置一次顯示源程序的行數。
setlistsize <count>
設置一次顯示源代碼的行數。
showlistsize
查看當前listsize的設置。
list命令還有下面的用法:
list<first>, <last>
顯示從first行到last行之間的源代碼。
list ,<last>
顯示從當前行到last行之間的源代碼。
list +
日後顯示源代碼。
通常來講在list後面能夠跟如下這些參數:
<linenum> 行號。
<+offset> 當前行號的正偏移量。
<-offset> 當前行號的負偏移量。
<filename:linenum> 哪一個文件的哪一行。
<function> 函數名。
<filename:function>哪一個文件中的哪一個函數。
<*address> 程序運行時的語句在內存中的地址。
2, 一些經常使用signal的含義
SIGABRT:調用abort函數時產生此信號。進程異常終止。
SIGBUS:指示一個實現定義的硬件故障。
SIGEMT:指示一個實現定義的硬件故障。EMT這一名字來自PDP-11的emulator trap 指令。
SIGFPE:此信號表示一個算術運算異常,例如除以0,浮點溢出等。
SIGILL:此信號指示進程已執行一條非法硬件指令。4.3BSD由abort函數產生此信號。SIGABRT如今被用於此。
SIGIOT:這指示一個實現定義的硬件故障。IOT這個名字來自於PDP-11對於輸入/輸出TRAP(input/outputTRAP)指令的縮寫。系統V的早期版本,由abort函數產生此信號。SIGABRT如今被用於此。
SIGQUIT:當用戶在終端上按退出鍵(通常採用Ctrl-/)時,產生此信號,並送至前臺進
程組中的全部進程。此信號不只終止前臺進程組(如SIGINT所作的那樣),同時產生一個core文件。
SIGSEGV:指示進程進行了一次無效的存儲訪問。名字SEGV表示「段違例(segmentationviolation)」。
SIGSYS:指示一個無效的系統調用。因爲某種未知緣由,進程執行了一條系統調用指令,但其指示系統調用類型的參數倒是無效的。
SIGTRAP:指示一個實現定義的硬件故障。此信號名來自於PDP-11的TRAP指令。
SIGXCPUSVR4和4.3+BSD支持資源限制的概念。若是進程超過了其軟C P U時間限制,則產生此信號。
SIGXFSZ:若是進程超過了其軟文件長度限制,則SVR4和4.3+BSD產生此信號。
3, Core_pattern的格式
能夠在core_pattern模板中使用變量還不少,見下面的列表:
%% 單個%字符
%p 所dump進程的進程ID
%u 所dump進程的實際用戶ID
%g 所dump進程的實際組ID
%s 致使本次core dump的信號
%t core dump的時間 (由1970年1月1日計起的秒數)
%h 主機名
%e 程序文件名