GCC
gcc是linux系統集成的編譯器。在linux環境下編輯程序,首先需要克服的便是沒有集成開發環境的一鍵式操作所帶來的麻煩。這其中涉及命令行操作、編譯選項的設定、文件依賴關系的書寫(makefile)等問題。這里主要介紹的是關於gcc的常用命令行參數及其相應的作用。(若編譯C++文件,則只需將下列命令的 gcc 換為 g++,源文件的后綴應為 .C/.cpp/.c++/.cc等)
基本格式:gcc [options] file1 file2... //若不加入參數,則按默認參數依次執行編譯、匯編和鏈接操作,生成的可執行文件名為 a.out 常用參數:-E //只執行預處理操作,直接輸出到標准輸出(可通過 -o 命令指定輸出到文件中 ) -S //只執行到編譯操作完成,不進行匯編操作,生成的是匯編文件(.s 或 .asm),內容為匯編語言 -c //執行編譯和匯編,但不進行鏈接,即只生成可重定位目標文件(.o),為二進制文件,不生成完整的可執行文件 -o filename //將操作后的內容輸出到filename指定的文件中 -static //對於支持動態鏈接的系統,使用靜態鏈接而不是動態鏈接進行鏈接操作 -g //編譯時生成debug有關的程序信息(供gdb使用) --save-temps //生成編譯過程的中間結果文件(包括預處理文件(x.ii)、匯編代碼(x.s)、目標文件(x.o)和最終的可執行文件) -I PATH //在PATH指定的目錄下尋找相關的include文件,參數中間不加空格 -lxx //其中xx為指定函數庫,對於Linux環境下的函數庫,靜態庫后綴為.a,動態庫后綴為.so,一般庫名為libxx.a或libxx.so,如加入libm.so庫,則使用參數-lm(去除lib和后綴.a\so) -L PATH //在PATH指定的目錄下尋找相關的庫文件,即-lxx指定待鏈接的庫,-L指定尋找該庫的路徑。不指定時搜索默認的庫函數路徑。
-std=xx //指定編譯使用的語言標准,如 -std=c++11 使用 c++11 標准 -x language //指定待編譯文件的語言,而不是由編譯器根據文件后綴自行判斷。即默認情況下gcc根據文件后綴判斷使用的編程語言。例如使用文件名hello作為源文件名是不合適的,應使用hello.c -Wall //輸出一些簡單的錯誤以及一些可能存在問題的警告 -Wextra //輸出-Wall不包含的警告等 -Werror //將警告視為錯誤輸出
-Wl,option //通過該選項將參數 option 作為后續鏈接器 ld 使用的參數
-Wl,rpath=/path/to/lib //為鏈接器指定一個非默認的運行時庫的搜索路徑,運行采用了該選項編譯的程序時,鏈接器會在-rpath 指定的目錄中搜索所需的 so 庫文件,以將其載入內存中
-D name=definition //加入宏定義,若不指定def,則默認為1 -O1、-O2 //規定編譯器的優化等級,優化級數越高執行效率一般越好,但是優化會改變原有程序結構,使得其匯編不易理解 //一些進行緩沖區溢出實驗時可能需要的選項 -fstack-protector\-fno-stack-protector //是否開啟堆棧保護,這里的保護是在返回地址之前加入一個驗證值來確保返回地址不被破壞 -z execstack //啟用可執行棧,默認是禁用的 //(echo 0 >/proc/sys/kernel/randomize_va_space 關閉地址隨機化,這是一個單獨的命令,操作需要root權限)
上述編譯使用的參數一般在直接使用 gcc / g++ 時作為命令行參數指定。如果想要在某些調用了 gcc / g++ 的編譯過程中加入所需的編譯參數,即無法直接通過命令行參數的方式指定編譯參數時,可以通過全局變量的方式( Linux 環境下 )指定所需的編譯參數。具體而言,使用 CXXFLAGS 指定 g++ 編譯參數,使用 CFLAGS 指定 gcc 編譯參數。
export CXXFLAGS="-std=c++11" //通過全局變量指定額外的編譯參數 export CFLAGS="-std=c99"
舉例說明
(1)將源文件編輯為可執行文件
gcc hello.c //默認生成名為a.out的可執行文件,這樣若在同一文件夾下編譯另一個程序,則會a.out會被后來文件覆蓋
(2)編譯文件,並輸出到hello.s
gcc -S -o hello.s hello.c
(3)生成兩個可重定位目標文件
gcc -c hello.c world.c //生成hello.o與world.o,不進行鏈接操作,即僅進行預處理、編譯、匯編,而不進行鏈接
(4)對庫文件、目標文件進行連接操作
gcc -static hello.o world.o -lm -L /usr/lib //以靜態鏈接的方式,將hello.o、world.o以及libm.a庫中的相關目標文件鏈接,在/usr/lib文件夾下尋找目標庫
GDB
gdb是Linux下一款功能強大的調試工具,它既能在反匯編過程中充當一件稱手的工具,也能在程序debug過程中為為程序員提供幫助,其唯一美中不足的是在Linux環境下沒有圖像界面(當然沒有功能的封裝也是其功能強大的原因之一,而且現在的ddd也提供了GUI)。這里主要記錄筆者從一些學習指導中學習的關於gdb命令和用法的總結。
為什么要使用GDB?
1.在Windows環境下,許多IDE以圖形界面提供類似gdb的功能,一般也較為好用。但是一方面,gdb提供給使用者更大的自由,另一方面gdb也是目前幾乎所有Linux發行版本的自帶軟件,簡單易得;
2.調試程序時盡量減少對諸如printf等輸出函數的依賴。許多作者給出的解釋是重新修改代碼和編譯是一件麻煩的差事。這一點筆者起初也並不理解,覺得上述操作確實不算麻煩(...)。后來發現,對於一個單一文件,代碼不超過100行的文件,上述操作確實在可接受范圍。但對於文件眾多,工程量巨大的項目,修改代碼、重新編譯文件是一件極其耗時且麻煩的操作。如果在Windows環境下進行大工程的debug所需要的修改、重編譯所帶來的頻繁鼠標或快捷鍵操作還不能使你回心轉意的話,相信我,在Linux的命令行模式下進行相同的操作會讓你有所改變的;
3.習慣是逐漸養成的,不論好壞都是。或許只有逐漸在看起來不那么方便的GDB中鍛煉起來,你才能在無論什么編譯環境中debug的得心應手,可能那時,你會嫌棄圖形界面提供的工具不夠給力的;
調試策略
無論進行何種調試工作,大體的調試策略都類似:使用二分法的方式對錯誤地點進行定位;使用斷點(breakpoint),使程序運行至斷點處時停止以便觀察程序狀態;使用單步執行,使程序運行一條指令后停止,從而觀察數據的變化情況和程序控制流;對一個變量預設特定的值,跟蹤其在程序運行中的變化規律等等。根據二八定律,使用20%的GDB指令,一般就可以解決80%的程序bug。這里介紹的是能夠常規使用GDB的命令,更多高級或特殊指令,可以參考GDB官方文檔Degugging with GDB。
為了更好的使用gdb的調試功能,在編譯程序時需加入 -g 選項,由編譯器生成某些用於調試的信息。
GDB常用命令(此部分譯自 Guide to Faster,Less Frustrating Debugging,細節有改動)
開始/結束gdb
使用 gdb filename 啟動gdb,其中 filename 應為可執行文件。
gdb a.out //使用gdb對a.out進行調試
gdb以命令行環境運行,進入gdb后,程序會等待用戶的指令並執行,直至用戶選擇退出。使用 q 或 Ctrl + d 退出。
運行(r)指令
使用命令 r 運行(run)程序,另外也可以加入程序運行所需要的參數,若原命令行模式下的運行指令為 ./a.out > test.txt ,則在gdb運行時應為 r > test.txt。且如果在同一調試過程中需要多次運行程序(run),后續再執行時便可直接使用 r 指令,系統會默認使用之前的參數。
r //運行程序 r [options] arguments //帶參數運行程序,參數與命令行環境下一致,使用 r 替換源程序文件即可
List( l )指令
可以使用指令 l 來列出源文件中的部分源代碼。(需要編譯時加入 -g 選項生成對應的編譯符號)
l 10 //輸出源程序10行及前后幾行的源碼,可以方便進行調試。若要繼續查看,按回車鍵會繼續向下顯示。
對於多個文件的而言,可以通過 l source_file_name.c:col (l 源文件名:行號)來指定所需查看的源代碼
l hello.c:10 //輸出hello.c在10行前后的代碼
也可以以函數為整體進行輸出,命令格式為 l function_name
l main //輸出main函數的源代碼
斷點(b)和繼續執行(c)指令
指令 b 可以在需要地方放置斷點,使得程序在指令的位置停止運行,指令格式為 b 斷點位置。其中,斷點位置可以是行號,也可以是函數名(指定方式與 l 指令類似),也可以是地址。
b 10 //在源代碼10行處放置斷點 b main //在main函數開始處放置斷點 b *0x80480000 //在存放在0x80480000處的指令處放置斷點,直接使用地址時需要使用 *地址 的格式 b 10 if a<10 //可以在斷點中加入中斷執行的條件,表示當a < 10 時才會中斷程序執行
在斷點處檢查完畢后,可以使用 c 指定繼續指令的執行。使用指令 disable/enable 斷點號 可以啟用/停用某斷點。使用指令 d 可刪除所有的斷點,d 1 刪除breakpoint 1.
disable/enable n //停用/啟用編號為n的斷點 d //刪除所有斷點 d n //刪除標號為n的斷點
觀測點(watch)指令
指令watch可以為某一表達式設置觀察點,當程序執行過程中,當表達式的值發生改變時,則 gdb 會中斷程序執行,並顯示表達式的變化情況。
watch a //當變量 a 的值發生變化時,中斷程序執行 watch -l a // watch指令指定了 -l 參數時,會將指令所接的表達式的計算結果作為地址,觀察該地址處的值的變化情況 rwatch a // 當 a 的值被讀取時,中斷表達式的執行
顯示(disp)和打印(p)指令
disp指令(display)可以在每次程序暫定時顯示指定變量的值,指令格式為 disp 變量名。若輸入的變量為數組名,則每次顯示數組的所有元素,若為結構體,則輸出結構體的所有成員的值。
disp temp //在每次程序暫停時輸出指定的變量的值(確保程序在指定變量的作用域內執行,如某個在特定函數中的局部變量在程序進入該函數執行之前是無法被顯示的) undisplay //取消所有disp指定的自動顯示變量
p指定(print)同樣將變量的值打印出來,用法與diap類似,但結果只顯示一次。
除變量外,p指令還可以輸出給定寄存器、給定地址處的值。同時,可以通過一些參數對打印格式進行規定,如 /x 表示以16進制格式打印值,/t表示以二進制格式打印值。
p $eax //打印寄存器%eax存儲的值,注意使用$標志寄存器名稱 p /x ($ebp + 8) //以十六進制的格式打印%ebp + 8 的值 p /t 100 //以二進制格式輸出100的值 p *0x08048000 //輸出位於0x08048000處的數據(此處實際存放的是機器代碼),注意地址需使用 * 標志,否則會被默認為常數 p *(int *)0xxxxxxxx //將指定地址處數據按照整數格式輸出,這里一般需要指出指針類型方便gdb解釋數據
其他顯示類info命令
info reg //輸出所有寄存器的當前值 info frame //輸出棧幀的使用情況 info b n //其中 n 為指定的斷點號,顯示指定斷點的狀態信息,不加參數 n 時,會顯示所有的斷點的信息
內存檢查(examine)指令
x 指令用於檢查內存中某一區域的值,指令格式為 :x fmt address 。其中address為內存地址的表達式,fmt由 /重復次數+格式化字符+尺寸字符 組成。格式化字符有o(octal,八進制),x(hex,十六進制), d(decimal,十進制),u(unsigned decimal,無符號十進制),t(binary,二進制),f(float,浮點),a(address,地址),i(instruction,指令),c(char,字符),s(string,字符串).尺寸字符有 b(byte),h(halfword), w(word), g(giant, 8 bytes)
x /4xb *0xxxxxx //將指定地址區域連續的四個字節以十六進制的格式輸出,一般內存地址均使用 * 標識
格式化輸出(printf)指令
該指令的使用方法與C語言中的格式化輸出函數相似
printf" %d , %d \n",X,Y //對於兩個變量整形X,Y進行輸出
使用指令whatis可以方便的得知所需對象的類型,如 whatis temp 會顯示出temp的類型定義,在調試時有用。
執行(s與n)指令
s 與 n 指令都是表示執行下一條指令指令的意思。但是,當遇到函數調用時,s 指令會進入函數調用內部進行執行,即下一步為被調函數的第一指令,而 n 指令不進入函數調用內部,會將整個函數的執行過程當作一步執行。
回溯(bt)指令
回溯指令(backtrace)可以查看程序內存訪問越界等錯誤信息,顯示程序出錯的位置,從而幫助定位程序錯誤。
設置(set)指令
設置指令 set 可以將指定的變量的值修改為調試所需要的值。如對於一個int型的變量X,可以使用 set X = 12 將變量的值進行設置。
使用宏定義
可以使用宏定義對一些常用指令進行定義。指令格式 :define 宏名,並根據提示輸入宏定義,以end作為結尾標志。
另外,在使用gdb進行調試過程中,可能免不了需要重新編譯程序,這時不必將gdb退出,只需待程序重新編譯后使用 r 指令重新運行程序,gdb會自動更新程序狀態,這樣可以節約時間。