|
1.背景介紹 2.程序中常見(jiàn)的bug分類 3.程序調(diào)試器(如gdb)有什么用 4.段錯(cuò)誤(Segmental fault)介紹 5.gdb調(diào)試入門(mén) 一、背景介紹 這個(gè)筆記主要介紹開(kāi)源的程序調(diào)試器(gdb)的入門(mén)知識(shí),目的是使unix/linux環(huán)境的編程新手能夠快速學(xué)會(huì)使用gdb調(diào)試程序的方法,同時(shí)也是對(duì)我使用gdb的一個(gè)經(jīng)驗(yàn)總結(jié)。 本文假設(shè)你能使用簡(jiǎn)單的unix/linux命令并能用gcc(GNU C Compiler, GNU C 語(yǔ)言編譯器)編譯程序,當(dāng)然有編程經(jīng)驗(yàn)更好。:) 為幫助你理解和操作,我將使用我遇到過(guò)的真實(shí)事例來(lái)演示使用gdb調(diào)試有缺陷(bug)的程序過(guò)程,你看過(guò)這篇筆記后能自己動(dòng)手練一下最好。 二、程序中常見(jiàn)的缺陷(bug)分類 程序(編譯型程序,perl、python,php等腳本程序除外)中常見(jiàn)的bug通常分為兩類: 語(yǔ)法錯(cuò)誤和邏輯錯(cuò)誤,或者編譯時(shí)錯(cuò)誤和運(yùn)行是錯(cuò)誤。 語(yǔ)法錯(cuò)誤(編譯時(shí)錯(cuò)誤)是我們?cè)诰帉?xiě)源代碼時(shí)沒(méi)有按照相關(guān)的語(yǔ)言規(guī)范(如ANSI C標(biāo)準(zhǔn))導(dǎo)致編譯時(shí)出錯(cuò),編譯失敗。這種錯(cuò)誤的檢查和調(diào)試一般是比較簡(jiǎn)單和直接的:因?yàn)榫幾g器(如gcc)通常會(huì)明確告訴你錯(cuò)誤的原因和大致的范圍(注意不一定是準(zhǔn)確的錯(cuò)誤行)。例如下面的一個(gè)簡(jiǎn)單demo.c程序的第8行缺失了一個(gè)分號(hào),gcc指示第10行前少了一個(gè)分號(hào)。這就是一個(gè)典型的語(yǔ)法錯(cuò)誤。 geekard@geekard:~/test$ cat -n demo.c 1 #include<stdio.h> 2 3 int 4 main(){ 5 6 int n; 7 8 printf("the n is:%c", n) 9 10 return 0; 11 } geekard@geekard:~/test$ gcc demo.c -o demo demo.c: In function ‘main’:demo.c:10: error: expected ‘;’ before ‘return’ 添加了分號(hào)再編譯一次,這下沒(méi)有出現(xiàn)問(wèn)題,運(yùn)行程序的結(jié)果如下: geekard@geekard:~/test$ ./demo the n is:6680564 另外注意這個(gè)程序中的變量n,我定義其為整型變量但并沒(méi)有對(duì)其初始化賦值,這就是一個(gè)邏輯錯(cuò)誤:編譯器不會(huì)指示這個(gè)錯(cuò)誤,只有在實(shí)際運(yùn)行或測(cè)試時(shí)才能發(fā)現(xiàn)。 這個(gè)小程序只是一個(gè)故意的編造,但在實(shí)際編程中無(wú)論你多高明,經(jīng)驗(yàn)多豐富,難免會(huì)在此處犯些小錯(cuò)誤(想想吧:當(dāng)你需要編寫(xiě)或維護(hù)一個(gè)成千上萬(wàn)行的代碼,這種小概率事件就是確定事件了,:)),而通常這些錯(cuò)誤又是那么的淺顯而易于消除,但是手工“除蟲(chóng)”(debug),往往是效率低下且讓人厭煩的,本文將就"段錯(cuò)誤"這個(gè)內(nèi)存訪問(wèn)越界的錯(cuò)誤談?wù)勅绾问褂胓db快速定位這些"段錯(cuò)誤"的語(yǔ)句。 三、程序調(diào)試器(如gdb)有什么用?(參考自gdb的在線幫助手冊(cè), 可用命令:man gdb, 或 info gdb查看) 程序調(diào)試器(如gdb)的主要目的是讓你能夠查看正在執(zhí)行的程序其內(nèi)部特性(如執(zhí)行流程、變量值、函數(shù)調(diào)用、堆棧等),也可以程序崩潰時(shí)刻或以前都發(fā)生了什么。 Gdb對(duì)程序的調(diào)試能力主要體現(xiàn)在以下四個(gè)方面(當(dāng)然不止這些): . 啟動(dòng)你的程序,可以帶任何影響其功能(或稱行為)的參數(shù)。 . 能夠使你的程序在指定條件下在指定的地方(斷點(diǎn))停止運(yùn)行。 . 當(dāng)你的程序在斷點(diǎn)處停止時(shí),你可以查看已執(zhí)行的結(jié)果(如變量的值,函數(shù)之間的調(diào)用情況,執(zhí)行到那一行代碼,下一步該執(zhí)行哪行代碼) . 改變你的程序中,你可以實(shí)驗(yàn)這種改變所帶來(lái)的影響(如bug消除了,或者情況變得更糟糕) 使用gdb,你可以調(diào)試C,C++,以及Modula-2語(yǔ)言編寫(xiě)的程序。 四、段錯(cuò)誤(Segmental fault)介紹 在用C/C++語(yǔ)言寫(xiě)程序的時(shí)侯,內(nèi)存管理的絕大部分工作都是需要我們來(lái)做的。實(shí)際上,內(nèi)存管理是一個(gè)比較繁瑣的工作,所以像java和c#等語(yǔ)言采用了內(nèi)存自動(dòng)回收機(jī)制,避免了內(nèi)存泄漏。如果程序試圖往內(nèi)存地址0處寫(xiě)東西時(shí),內(nèi)核就會(huì)向其發(fā)送段錯(cuò)誤信號(hào),如果程序沒(méi)有捕獲該信號(hào),默認(rèn)的操作時(shí)內(nèi)核終止該程序的運(yùn)行,例如我寫(xiě)的一個(gè)myls程序就遭遇了這種情況: luck@geekard:~/codes/12.21$ ./myls -ld . longlist 1, typelist 0, dirlist 1, filename . Segmentation fault luck@geekard:~/codes/12.21$ 常見(jiàn)的段錯(cuò)誤原因如下: 1)往受到系統(tǒng)保護(hù)的內(nèi)存地址寫(xiě)數(shù)據(jù)有些內(nèi)存是內(nèi)核占用的或者是其他程序正在使用,為了保證系統(tǒng)正常工作,所以會(huì)受到系統(tǒng)的保護(hù),而不能任意訪問(wèn) .2)內(nèi)存越界(數(shù)組越界,變量類型不一致等) 下面我以上面的myls程序出現(xiàn)的錯(cuò)誤為例介紹用gdb進(jìn)行調(diào)試的方法和過(guò)程。 五、gdb調(diào)試入門(mén) 5.1 調(diào)試前的準(zhǔn)備 我們首先要啟動(dòng)linux內(nèi)核提供核心轉(zhuǎn)儲(chǔ)(core dump)機(jī)制:當(dāng)程序中出現(xiàn)內(nèi)存操作錯(cuò)誤時(shí),會(huì)發(fā)生崩潰并產(chǎn)生核心文件(core文件)。使用GDB可以對(duì)產(chǎn)生的核心文件進(jìn)行分析,找出程序是在什么時(shí)候崩潰的和在崩潰之前程序都做了些什么。 首先,你的Segmentation Fault錯(cuò)誤必須要能重現(xiàn)(廢話…)。 然后,依參照下面的步驟來(lái)操作: 1)無(wú)論你是用Makefile來(lái)編譯,還是直接在命令行手工輸入命令來(lái)編譯,都應(yīng)該加上 -g 選項(xiàng)。如: luck@geekard:~/codes/12.21$ ls myls-0.0.c myls-1.0.c myls-2.0.c luck@geekard:~/codes/12.21$ gcc -g -o myls myls-0.0.c luck@geekard:~/codes/12.21$ ls myls myls-0.0.c myls-1.0.c myls-2.0.c 加了-g選項(xiàng)后,gcc就會(huì)在生成的可執(zhí)行文件(這里-o myls表示輸出(output)的可執(zhí)行文件名時(shí)myls)里添加一些調(diào)試符號(hào)(debugging symbols),有了這些調(diào)試符號(hào)后就可以在稍后用gdb調(diào)試時(shí)列出執(zhí)行的程序的C源代碼了。-g選項(xiàng)增大了文件體積,一般只是在剛開(kāi)發(fā)出的程序調(diào)試時(shí)使用,當(dāng)確定無(wú)誤編譯出實(shí)際使用的可執(zhí)行文件時(shí)就不需要-g選項(xiàng)了。 2)一般來(lái)說(shuō),在默認(rèn)情況下,在程序崩潰時(shí),core文件是不生成的(很多Linux發(fā)行版在默認(rèn)時(shí)禁止生成核心文件)。所以,你必須修改這個(gè)默認(rèn)選項(xiàng),在命令行執(zhí)行: ulimit -c unlimited //unlimited 表示不限制生成的core文件的大小。 3)運(yùn)行你的程序,不管用什么方法,使之重現(xiàn)Segmentation Fault錯(cuò)誤。 luck@geekard:~/codes/12.21$ ./myls -ld . longlist 1, typelist 0, dirlist 1, filename . Segmentation fault (core dumped) 4)這時(shí),你會(huì)發(fā)現(xiàn)在你程序同一目錄下,生成了一個(gè)文件名為 core的文件,即核心文件。 luck@geekard:~/codes/12.21$ ls core myls myls-0.0.c myls-1.0.c myls-2.0.cluck@geekard:~/codes/12.21$ 5)用GDB調(diào)試它,在命令行執(zhí)行: luck@geekard:~/codes/12.21$ gdb ./myls 或者先啟動(dòng)gdb然后在gdb命令提示符中輸入這兩個(gè)文件: luck@geekard:~/codes/12.21$ gdb //不帶參數(shù)啟動(dòng)gdb調(diào)試程序 GNU gdb (GDB) 7.2-ubuntu Copyright (C) 2010 Free Software Foundation, Inc. License GPLv3+: GNU GPL version 3 or later <http:///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 "i686-linux-gnu". For bug reporting instructions, please see: <http://www./software/gdb/bugs/>. (gdb) file ./myls //輸入file命令和你的可執(zhí)行文件名和路徑,這里為當(dāng)前目錄下的myls文件 Reading symbols from /home/luck/codes/12.21/myls...done. (gdb) run -ld ./ //帶參數(shù)(這里為 -ld ./)運(yùn)行r(run)程序,這和在bash命令行上執(zhí)行:./myls -ld ./效果時(shí)一致的。 Starting program: /home/luck/codes/12.21/myls -ld ./ longlist 1, typelist 0, dirlist 1, filename ./ //myls程序的輸出 Program received signal SIGSEGV, Segmentation fault. //出錯(cuò)后退出 0x0016e78f in vfprintf () from /lib/libc.so.6 (gdb) 從這里我們還發(fā)現(xiàn)進(jìn)程是由于收到了SIGSEGV信號(hào)而結(jié)束的。通過(guò)進(jìn)一步的查閱文檔(man 7 signal),我們知道SIGSEGV默認(rèn)handler的動(dòng)作是打印”段錯(cuò)誤"的出錯(cuò)信息,并產(chǎn)生Core文件。 查看一下我的當(dāng)前目錄,果然有core文件。 luck@geekard:~/codes/12.21$ ls core myls myls-0.0.c myls-1.0.c myls-2.0.c 下面我們就用剛才生成的分段錯(cuò)誤產(chǎn)生的核心轉(zhuǎn)儲(chǔ)文件(core)再次調(diào)試程序。接著上一步的(gdb) 提示符,輸入以下命令: (gdb) core core //輸入core命令和分段錯(cuò)誤產(chǎn)生的核心轉(zhuǎn)儲(chǔ)文件,這里為當(dāng)前目錄下的core文件 A program is being debugged already. Kill it? (y or n) y //按y,重新調(diào)試 [New Thread 24884] warning: Can't read pathname for load map: Input/output error. 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 `./myls -ld .'. //core文件記錄了發(fā)生錯(cuò)誤的程序執(zhí)行的命令行參數(shù) Program terminated with signal 11, Segmentation fault. #0 0x002bb78f in vfprintf () from /lib/libc.so.6 //core文件記錄了發(fā)生錯(cuò)誤時(shí)程序的退出狀態(tài) (gdb) 從標(biāo)號(hào)為0的行我們并不能看出程序到底在哪出錯(cuò),所以下一步需要確定發(fā)生錯(cuò)誤前程序中函數(shù)之間的調(diào)用關(guān)系 (gdb) backtrace //顯示程序的堆棧信息 #0 0x0014f78f in vfprintf () from /lib/libc.so.6 #1 0x0016f4dc in vsprintf () from /lib/libc.so.6 #2 0x00157b4b in sprintf () from /lib/libc.so.6 #3 0x08048c56 in finalprt (file=0x8a9b02b "..", dirlist=1, typelist=0, longlist=1) at myls-0.0.c:261 #4 0x080487c3 in detailList (file=0xbfab684d ".", dirlist=1, typelist=0, longlist=1) at myls-0.0.c:132 #5 0x08048712 in main (argc=3, argv=0xbfab4804) at myls-0.0.c:89 (gdb) 可以看出myls程序的函數(shù)調(diào)用關(guān)系為: main() ---> detailList() ---> finalprt 然后在標(biāo)號(hào)為0-2的行進(jìn)入了系統(tǒng)的C庫(kù)函數(shù),所以產(chǎn)生錯(cuò)誤的可能在標(biāo)號(hào)3、4、5指明的函數(shù)中。 我們先看一下最后調(diào)用finalprt()函數(shù)時(shí)可能發(fā)生錯(cuò)誤的代碼行: (gdb) frame 3 //上面以#開(kāi)頭的行稱為幀(frame),這里指定查看第3幀 #3 0x08048c56 in finalprt (file=0x8a9b02b "..", dirlist=1, typelist=0, longlist=1) at myls-0.0.c:261 261 sprintf(str, "%c%d %d,%d %d %d %s", filetype, permission, uid, gid, size, mdate, file); 可以看到在調(diào)用sprintf()函數(shù)時(shí)可能發(fā)生了分段錯(cuò)誤(由非法引用內(nèi)存引起),而sprintf()的原型為: int sprintf(char *str, const char *format, ...); 最有可能引起錯(cuò)誤的地方是其第一個(gè)參數(shù):char *str,一個(gè)指向字符串?dāng)?shù)組的指針,我們先把疑點(diǎn)放在這,接下來(lái)看一下函數(shù)之間相互調(diào)用時(shí)傳遞的參數(shù)值和函數(shù)的內(nèi)部變量值: (gdb) backtrace full //full參數(shù)表示完全顯示函數(shù)之間相互調(diào)用時(shí)傳遞的參數(shù)值和函數(shù)的內(nèi)部變量值 #0 0x0014f78f in vfprintf () from /lib/libc.so.6 No symbol table info available. #1 0x0016f4dc in vsprintf () from /lib/libc.so.6 No symbol table info available. #2 0x00157b4b in sprintf () from /lib/libc.so.6 No symbol table info available. #3 0x08048c56 in finalprt (file=0x8a9b02b "..", dirlist=1, typelist=0, longlist=1) at myls-0.0.c:261 str = 0x4d11faec <Address 0x4d11faec out of bounds> flag = 65 #4 0x080487c3 in detailList (file=0xbfab684d ".", dirlist=1, typelist=0, longlist=1) at myls-0.0.c:132 ptr = 0x8048e44 "longlist %d, typelist %d, dirlist %d, filename %s\n" dirp = 0x8a9b008 direntp = 0x8a9b020 #5 0x08048712 in main (argc=3, argv=0xbfab4804) at myls-0.0.c:89 file = 0xbfab684d "." ptr = 0x8048d30 "U\211\345WVS\350O" i = 3 j = 3 longlist = 1 dirlist = 1 typelist = 0 請(qǐng)注意序號(hào)3中的內(nèi)部變量str的值 <Address 0x4d11faec out of bounds>,這表示發(fā)生了數(shù)組越界,難怪發(fā)生了段錯(cuò)誤! 現(xiàn)在我們找到原因了:finalprt()中的第261行調(diào)用函數(shù)sprintf()時(shí)向其傳遞的第一個(gè)參數(shù)str發(fā)生里越界存取,于是內(nèi)核終止程序的運(yùn)行。 下面我們要驗(yàn)證這個(gè)判斷:在261處設(shè)置一個(gè)斷點(diǎn),程序運(yùn)行到斷點(diǎn)后單步執(zhí)行,觀察是否會(huì)發(fā)生錯(cuò)誤。 (gdb) stop //停止當(dāng)前調(diào)試 (gdb) break 261 //在第261行設(shè)置一個(gè)斷點(diǎn) Breakpoint 1 at 0x8048bf1: file myls-0.0.c, line 261. (gdb) run -ld ./ //帶參數(shù)運(yùn)行程序(myls) The program being debugged has been started already. Start it from the beginning? (y or n) y //當(dāng)然yes Starting program: /home/luck/codes/12.21/myls -ld ./ longlist 1, typelist 0, dirlist 1, filename ./ Breakpoint 1, finalprt (file=0x804c02b "..", dirlist=1, typelist=0, longlist=1) //可以看到程序在第261行停止 at myls-0.0.c:261 261 sprintf(str, "%c%d %d,%d %d %d %s", filetype, permission, uid, gid, size, mdate, file); (gdb) where //顯示目前函數(shù)之間的調(diào)用情況與breaktrace命令功能相似 #0 finalprt (file=0x804c02b "..", dirlist=1, typelist=0, longlist=1) at myls-0.0.c:261 #1 0x080487c3 in detailList (file=0xbffff830 "./", dirlist=1, typelist=0, longlist=1) at myls-0.0.c:132 #2 0x08048712 in main (argc=3, argv=0xbffff6e4) at myls-0.0.c:89 (gdb) printf "%d\n",filetype //打印處函數(shù)中的變量filetype的值 100 (gdb) list //列出斷點(diǎn)處前后的相關(guān)代碼 256 // if(filetype == 'd') 257 sprintf(str, "%s\n", file); 258 break; 259 case 0101: 260 // if(filetype == 'd') 261 sprintf(str, "%c%d %d,%d %d %d %s", filetype, permission, uid, gid, size, mdate, file); 262 break; 263 case 0110: 264 // if(filetype == 'd') 265 sprintf(str, "%s%c", file, filetype); (gdb) n //然后單步執(zhí)行代碼,立即發(fā)生了錯(cuò)誤 Program received signal SIGSEGV, Segmentation fault. 0x0016e78f in vfprintf () from /lib/libc.so.6 可見(jiàn)在線調(diào)試驗(yàn)證了我們的假設(shè),的確時(shí)261行的sprintf語(yǔ)句有問(wèn)題,下面我們看一下261所在的函數(shù)finalprt()中變量str的類型 (gdb) list finalprt //列出函數(shù)finalprt()入口附近的源代碼 225 *mdate_s = fstat.st_mtime; 226 return 0; 227 } 228 229 /*this function prints all the information*/ 230 static char *finalprt(char *file, int dirlist, int typelist, int longlist){ 231 232 char *str; 233 int flag = 0000; 234 (gdb) 注意第232行的變量定義:str被錯(cuò)誤的定義個(gè)指向char的指針,而sprintf()的第一個(gè)參數(shù)要求為一字符型數(shù)組的首地址,所以sprintf()調(diào)用時(shí)會(huì)發(fā)生內(nèi)存越界的錯(cuò)誤。 接著考慮下去,以前用windows系統(tǒng)下的ie的時(shí)侯,有時(shí)打開(kāi)某些網(wǎng)頁(yè),會(huì)出現(xiàn)“運(yùn)行時(shí)錯(cuò)誤”,這個(gè)時(shí)侯如果恰好你的機(jī)器上又裝有windows的編譯器的話,他會(huì)彈出來(lái)一個(gè)對(duì)話框,問(wèn)你是否進(jìn)行調(diào)試,如果你選擇是,編譯器將被打開(kāi),并進(jìn)入調(diào)試狀態(tài),開(kāi)始調(diào)試。 Linux下如何做到這些呢? 我們可以在要調(diào)試的程序中定義一個(gè)分段錯(cuò)誤信號(hào)(SIGSEGV)的處理函數(shù)(handler),在該函數(shù)中中調(diào)用gdb,這樣當(dāng)段錯(cuò)誤發(fā)生時(shí)程序就會(huì)自動(dòng)啟動(dòng)gdb進(jìn)行調(diào)試,一個(gè)簡(jiǎn)單的示例代碼如下: /** *段錯(cuò)誤時(shí)啟動(dòng)調(diào)試 */ #include <stdio.h> #include <stdlib.h> #include <signal.h> #include <string.h> void dump(int signo){ char buf[1024]; char cmd[1024]; FILE *fh; snprintf(buf, sizeof(buf), "/proc/%d/cmdline", getpid()); //取得進(jìn)程的命令行文件地址 if(!(fh = fopen(buf, "r"))) //打開(kāi)該文件 exit(0); if(!fgets(buf, sizeof(buf), fh)) //將其內(nèi)容讀到buf數(shù)組中 exit(0); fclose(fh); if(buf[strlen(buf) - 1] == '\n') //刪除獨(dú)到的字符串中最后的還行符并保證字符串以空字符結(jié)尾 buf[strlen(buf) - 1] = '\0'; snprintf(cmd, sizeof(cmd), "gdb %s %d", buf, getpid()); //合并命令行參數(shù) system(cmd); //執(zhí)行cmd字符竄 代表的命令 exit(0); } void dummy_function (void){ //測(cè)試函數(shù) unsigned char *ptr = 0x00; *ptr = 0x00; //向內(nèi)存中0x00地址寫(xiě)數(shù)據(jù),產(chǎn)生段錯(cuò)誤 } int main (void) { signal(SIGSEGV, &dump); //捕獲信號(hào)SIGSEGV,當(dāng)接收到內(nèi)核發(fā)送的SIGSEGV信號(hào)時(shí)調(diào)用處理函數(shù)dump() dummy_function (); return 0; } 編譯運(yùn)行效果如下: luck@geekard test $ gcc -g -rdynamic f.c luck@geekard test $ ./a.out GNU gdb 6.5 Copyright (C) 2006 Free Software Foundation, Inc. 。。。。省略。。。。 0xffffe410 in __kernel_vsyscall () (gdb) bt #0 0xffffe410 in __kernel_vsyscall () #1 0xb7ee4b53 in waitpid () from /lib/libc.so.6 #2 0xb7e925c9 in strtold_l () from /lib/libc.so.6 #3 0x08048830 in dump (signo=11) at f.c:22 #4 <signal handler called> #5 0x0804884c in dummy_function () at f.c:31 #6 0x08048886 in main () at f.c:38 第3個(gè)frame指示發(fā)生錯(cuò)誤的行為f.c中的22行,即為*ptr = 0x00;行。 好了,以上就是這篇筆記的主要內(nèi)容,下面總結(jié)一下gdb的主要命令: ulimit -c unlimited //打開(kāi)內(nèi)核的核心轉(zhuǎn)儲(chǔ)機(jī)制 gcc -g -o outPutName sourceCodeName.c //編譯時(shí)加-g選項(xiàng),使生成的可執(zhí)行文件中包含調(diào)試信息 gdb outPutName core //啟動(dòng)gdb,可以咋命令行上指定要調(diào)試程序 or: gdb file outPutName //也可以在gdb命令提示符中輸入要調(diào)試的程序名 core core //指定程序執(zhí)行錯(cuò)誤時(shí)內(nèi)核生成的轉(zhuǎn)儲(chǔ)文件 list [function]|[row-number] //查看源代碼,可以跟函數(shù)名或行號(hào) break [function]|[row-number] //設(shè)置斷點(diǎn),可以跟函數(shù)名或行號(hào) clear [function]|[row-number] //清除斷點(diǎn),可以跟函數(shù)名或行號(hào)或斷點(diǎn)號(hào) r [paramiters] / |
|
|
來(lái)自: 昵稱17328427 > 《Linux》