1. 程式人生 > >GCC的編譯和除錯--入門介紹

GCC的編譯和除錯--入門介紹

編譯與除錯
1.1編譯的概念和理解
在進行C程式開發時,編譯就是將編寫的C語言程式碼變成可執行程式的過程,這一過程
是由編譯器來完成的。編譯器就是完成程式編譯工作的軟體,在進行程式編譯時完成了一系
列複雜的過程。
1.1.1程式編譯的過程
在執行這一操作時,程式完成了複雜的過程。一個程式的編譯,需要完成詞法分析、語
法分析、中間程式碼生成、程式碼優化、目的碼生成。本章將講解這些步驟的作用與原理。
(1)詞法分析。指的是對由字元組成的單詞進行處理,從左至右逐個字元地對源程式進
行掃描,產生一個個的單詞符號。然後把字串的源程式改造成為單詞符號串的中間程式。
在編譯程式時,這一過程是自動完成的。編譯程式會對程式碼的每一個單詞進行檢查。如果單
詞發生錯誤,編譯過程就會停止並顯示錯誤。這時需要對程式中的錯誤進行修改。
(2)語法分析。語法分析器以單詞符號作為輸入,分析單詞符號串是否形成符合語法規
則的語句。例如,需要檢查表示式、賦值、迴圈等結構是否完整和符合使用規則。在語法分
析時,會分析出程式中錯誤的語句,並顯示出結果。如果語法發生錯誤,編譯任務是不能完
成的。
(3)中間程式碼生成。中間程式碼是源程式的一種內部表示,或稱中間語言。程式進行詞法
分析和語法分析以後,將程式轉換成中間程式碼。這一轉換的作用是使程式的結構更加簡單和
規範。中間程式碼生成操作是一箇中間過程,與使用者是無關的。
(4)程式碼優化。程式碼優化是指對程式進行多種等價變換,使得從變換後的程式能生成更
有效的目的碼。使用者可以在編譯程式時設定程式碼優化的引數,可以針對不同的環境和設定
進行優化。
(5)目的碼生成。目的碼生成指的是產生可以執行的應用程式,這是編譯的最後一
個步驟。生成的程式是二進位制的機器語言,使用者只能執行這個程式,而不能開啟這個檔案查
看程式的程式碼。
1.1.2編譯器
所謂編譯器,是將編寫出的程式程式碼轉換成計算機可以執行的程式的軟體。在進行C程
序開發時,編寫出的程式碼是源程式的程式碼,是不能直接執行的。需要用編譯器編譯成可以運
行的二進位制程式。
在不同的作業系統下面有不同的編譯器。C程式是可以跨平臺執行的。但並不是說
Windows系統下C語言編寫的程式可以直接在Linux下面執行。Windows下面C語言編寫的
程式,被編譯成exe檔案。這樣的程式只能在Windows系統下執行。如果需要在Linux系統
下執行,需要將這個程式的原始碼在Linux系統重新編譯。不同的作業系統下面有不同的編
譯器。Linux系統下面編譯生成的程式是不能在Windows系統上執行的。
1.2 gcc編譯器
gcc是Linux下的C程式編譯器,具有非常強大的程式編譯功能。在Linux系統下,C語
言編寫的程式程式碼一般需要通過gcc來編譯成可執行程式。
1.2.1 gcc編譯器簡介
Linux系統下的gcc編譯器(GNU C Compiler)是一個功能強大、效能優越的編譯器。gcc
支援多種平臺的編譯,是Linux系統自由軟體的代表作品。gcc本來只是C編譯器的,但是後
來發展為可在多種硬體平臺上編譯出可執行程式的超級編譯器。各種硬體平臺對gcc的支援
使得其執行效率與一般的編譯器相比平均效率要高20%~30%。gcc編譯器能將C、C++源程
序、匯程語言和目標程式進行編譯連結成可執行檔案。通過支援make工具,gcc可以實施項
目管理和批量編譯。
經過多年的發展,gcc已經發生了很大的變化。gcc已經不僅僅能支援C語言,還支援Ada
語言、C++語言、Java語言、Objective C語言、Pascal語言、COBOL語言等更多的語言集的編
譯。gcc幾乎支援所有的硬體平臺,使得gcc對於特定的平臺可以編譯出更高效的機器碼。
gcc在編譯一個程式時,一般需要完成預處理(preprocessing)、編譯(compilation)、匯
編(assembly)和連結(linking)過程。使用gcc編譯C程式時,這些過程是使用預設的設定
自動完成的,但是使用者可以對這些過程進行設定,控制這些操作的詳細過程。
1.2.2 gcc對源程式副檔名的支援
副檔名指的是檔名中最後一個點的這個點以後的部分。例如下面是一個C程式原始檔
的副檔名。
5.1.c
那麼這個檔案的檔名是“5.1.c”,副檔名是“.c”。通常來說,原始檔的副檔名標識源文
件所使用的程式語言。例如C程式原始檔的副檔名一般是“.c”。對編譯器來說,副檔名控制
著預設語言的設定。在預設情況下,gcc通過副檔名來區分原始檔的語言型別。然後根據
這種語言型別進行不同的編譯。gcc對原始檔的副檔名約定如下所示。
.c為副檔名的檔案,為C語言原始碼檔案。
.a為副檔名的檔案,是由目標檔案構成的庫檔案。
.C,.cc或.cpp為副檔名的檔案,標識為C++原始碼檔案。
.h為副檔名的檔案,說明檔案是程式所包含的標頭檔案。
.i為副檔名的檔案,標識檔案是已經預處理過的C原始碼檔案,一般為中間程式碼檔案。
.ii為副檔名的檔案,是已經預處理過的C++原始碼檔案,同上也是中間程式碼檔案。
.o為副檔名的檔案,是編譯後的目標檔案,原始檔生成的中間目標檔案。
.s為副檔名的檔案,是組合語言原始碼檔案。
.S為副檔名的檔案,是經過預編譯的組合語言原始碼檔案。
.o為副檔名的檔案,是編譯以後的程式目標檔案(Object file),目標檔案經過連線成
可執行檔案
此外,對於gcc編譯器提供兩種顯示的編譯命令,分別對應於編譯C和C++源程式的
編譯命令。
1.3 C程式的編譯
本章以一個例項講述如何用gcc編譯C程式。在編譯程式之前,需要用VIM編寫一個簡
單的C程式。在編譯程式時,可以對gcc命令進行不同的設定。
1.3.1編寫第一個C程式
本節將編寫第一個C程式。程式實現一句文字的輸出和判斷兩個整數的大小關係。本書
中編寫程式使用的編輯器是VIM。程式編寫步驟如下所示。
開啟系統的終端。單擊“主選單”|“系統工具”|“終端”命令,開啟一個系統
終端。
在終端中輸入下面的命令,在使用者根目錄“root”中建立一個目錄。
mkdir c
在終端介面中輸入“vim”命令,然後按“Enter”鍵,系統會啟動VIM。
在VIM中按“i”鍵,進入到插入模式。然後在VIM中輸入下面的程式程式碼。
#include<stdio.h>
int max(int i,int j)
{
if(i>j)
{
return(i);
}
else
{
return(j);
}
}
void main()
{
int i,j,k;
i=3;
j=5;
printf("hello,Linux./n”);
k=max(i,j);
printf("%d/n",k);
}
程式碼輸入完成以後,按“Esc”鍵,返回到普通模式。然後輸入下面的命令,儲存檔案。
:w/root/c/a.c
這時,VIM會把輸入的程式儲存到c目錄下的檔案a.c中。
再輸入“:q”命令,退出VIM。這時,已經完成了這個C程式的編寫。
1.3.2用gcc編譯程式
上面編寫的C程式,只是一個原始碼檔案,還不能作為程式來執行。需要用gcc將這個
原始碼檔案編譯成可執行檔案。編譯檔案的步驟如下所示。
開啟系統的終端。單擊“主選單”|“系統工具”|“終端”命令,開啟一個系統
終端。這時進入的目錄是使用者根目錄“/root”。然後輸入下面的命令,進入到c目錄。
cd c
上一節編寫的程式就存放在這個目錄中。輸入“ls”命令可以檢視這個目錄下的檔案。
顯示的結果如下所示。
輸入下面的命令,將這個程式碼檔案編譯成可執行程式。
gcc a.c
檢視已經編譯的檔案。在終端中輸入“ls”命令,顯示的結果如下所示。
a.c a.out
輸入下面的命令對這個程式新增可執行許可權。
chmod+x a.out
輸入下面的命令,執行這個程式。
./a.out
程式的執行結果如下所示。
hello,Linux.
5
從上面的操作可知,用gcc可以將一個C程式原始檔編譯成一個可執行程式。編譯以
後的程式需要新增可執行的許可權才可以執行。在實際操作中,還需要對程式的編譯進行各
種設定。
1.3.3檢視gcc的引數
gcc在編譯程式時可以有很多可選引數。在終端中輸入下面的命令,可以檢視gcc的這些
可選引數。
gcc--help
在終端中顯示的gcc的可選引數如下所示。進行程式編譯時,可以設定下面的這些引數。
用法:gcc[選項]檔案...
選項:
-pass-exit-codes:在某一階段退出時返回最高的錯誤碼
--help:顯示此幫助說明
--target-help:顯示目標機器特定的命令列選項
-dumpspecs:顯示所有內建spec字串
-dumpversion:顯示編譯器的版本號
-dumpmachine:顯示編譯器的目標處理器
-print-search-dirs:顯示編譯器的搜尋路徑
-print-libgcc-file-name:顯示編譯器伴隨庫的名稱
-print-file-name=<庫>:顯示<庫>的完整路徑
-print-prog-name=<程式>:顯示編譯器元件<程式>的完整路徑
-print-multi-directory:顯示不同版本libgcc的根目錄
-print-multi-lib:顯示命令列選項和多個版本庫搜尋路徑間的對映
-print-multi-os-directory:顯示作業系統庫的相對路徑
-Wa,<選項>:將逗號分隔的<選項>傳遞給彙編器
-Wp,<選項>:將逗號分隔的<選項>傳遞給前處理器
-Wl,<選項>:將逗號分隔的<選項>傳遞給連結器
-Xassembler<引數>:將<引數>傳遞給彙編器
-Xpreprocessor<引數>:將<引數>傳遞給前處理器
-Xlinker<引數>:將<引數>傳遞給連結器
-combine:將多個原始檔一次性傳遞給彙編器
-save-temps:不刪除中間檔案
-pipe:使用管道代替臨時檔案
-time:為每個子程序計時
-specs=<檔案>:用<檔案>的內容覆蓋內建的specs檔案
-std=<標準>:指定輸入原始檔遵循的標準
--sysroot=<目錄>:將<目錄>作為標頭檔案和庫檔案的根目錄
-B<目錄>:將<目錄>新增到編譯器的搜尋路徑中
-b<機器>:為gcc指定目標機器(如果有安裝)
-V<版本>:執行指定版本的gcc(如果有安裝)
-v:顯示編譯器呼叫的程式
-###:與-v類似,但選項被引號括住,並且不執行命令
-E:僅作預處理,不進行編譯、彙編和連結
-S:編譯到組合語言,不進行彙編和連結
-c:編譯、彙編到目的碼,不進行連結
-o<檔案>:輸出到<檔案>
-x<語言>:指定其後輸入檔案的語言。允許的語言包括c、c++、assembler等。
以-g、-f、-m、-O、-W或--param開頭的選項將由gcc自動傳遞給其呼叫的不同子程序。若要
向這些程序傳遞其他選項,必須使用-W<字母>選項。
1.3.4設定輸出的檔案
在預設情況下,gcc編譯出的程式為當前目錄下的檔案a.out。-o引數可以設定輸出的目
標檔案。例如下面的命令,可以設定將程式碼編譯成可執行程式do。
gcc a.c-o do
也可以設定輸出目錄檔案為不同的目錄。例如下面的命令,是將目錄檔案設定成/tmp目
錄下的檔案do。
gcc a.c-o/tmp/do
輸入下面的命令,檢視生成的目錄檔案。結果如下所示,在編譯程式時生成的目錄為/tmp
目錄下的檔案do。
-rwxrwxr-x 1 root root 5109 12-28 13:33/tmp/do
1.3.5檢視編譯過程
引數-v可以檢視程式的編譯過程和顯示已經呼叫的庫。輸入下面的命令,在編譯程式時
輸出編譯過程。
gcc-v a.c
顯示的結果如下所示。
使用內建specs。
目標:i386-redhat-linux
配置為:../configure--prefix=/usr--mandir=/usr/share/man
--infodir=/usr/share/info--enable-shared--enable-threads=posix
--enable-checking=release--with-system-zlib--enable-__cxa_atexit
--disable-libunwind-exceptions
--enable-languages=c,c++,objc,obj-c++,java,fortran,ada
--enable-java-awt=gtk--disable-dssi--enable-plugin
--host=i386-redhat-linux
執行緒模型:posix
gcc版本4.1.2 20070925(Red Hat 4.1.2-33)
/usr/libexec/gcc/i386-redhat-linux/4.1.2/cc1
-quiet-v a.c-quiet-dumpbase a.c-mtune=generic-auxbase a-version
-o/tmp/cc8P7rzb.s
忽略不存在的目錄“/usr/lib/gcc/i386-redhat-linux/4.1.2/../../../../i386-
redhat-linux/include”
#include"..."搜尋從這裡開始:
#include<...>搜尋從這裡開始:
/usr/local/include
/usr/lib/gcc/i386-redhat-linux/4.1.2/include
/usr/include
搜尋列表結束。
GNU C版本4.1.2 20070925(Red Hat 4.1.2-33)(i386-redhat-linux)
由GNU C版本4.1.2 20070925(Red Hat 4.1.2-33)編譯。
GGC準則:--param ggc-min-expand=64--param ggc-min-heapsize=64394
Compiler executable checksum:ab322ce5b87a7c6c23d60970ec7b7b31
a.c:In function‘main’:
a.c:16:警告:‘main’的返回型別不是‘int’
as-V-Qy-o/tmp/ccEFPrYh.o/tmp/cc8P7rzb.s
GNU assembler version 2.17.50.0.18(i386-redhat-linux)using BFD version
version 2.17.50.0.18-1 20070731
/usr/libexec/gcc/i386-redhat-linux/4.1.2/collect2
--eh-frame-hdr--build-id-m elf_i386--hash-style=gnu-dynamic-linker
/lib/ld-linux.so.2/usr/lib/gcc/i386-redhat-linux/4.1.2/../../../crt1.o
從顯示的編譯過程可知,gcc自動載入了系統的預設配置,呼叫系統的庫函式完成了程式
的編譯過程。
1.3.6設定編譯的語言
gcc可以對多種語言編寫的原始碼。如果原始碼的副檔名不是預設的副檔名,gcc就
無法編譯這個程式。可以用-x選擇來設定程式的語言。可以用下面的步驟來練習這一操作。
輸入下面的命令,將C程式檔案複製一份。
cp a.c a.u
複製出的檔案a.u是一個C程式檔案,但副檔名不是預設的副檔名。這時輸入下面的
命令編譯這個程式。
gcc a.u
顯示的結果如下所示,表明檔案的格式不能識別。
a.u:file not recognized:File format not recognized
collect2:ld返回1
這時,用-x引數設定編譯的語言,命令如下所示。這樣就可以正常地編譯檔案a.u。
gcc-x'c'a.u
需要注意的是,這裡的c需要用單引號擴起來。當編譯副檔名不是.c的C程式時,需要
使用-x引數。
1.3.7-asci設定ANSIC標準
ANSIC是American National Standards Institute(ANSI:美國標準協會)出版的C語言
標準。使用這種標準的C程式可以在各種編譯器和系統下執行通過。gcc可以編譯ANSIC的
程式,但是gcc中的很多標準並不被ANSIC所支援。在gcc編譯程式時,可以用-ansic來設定
程式使用ANSIC標準。例如下面的命令,在設定程式編譯時,用ANSIC標準進行編譯。
gcc-asci a.out
5.3.8
1.3.8 g++編譯C++程式
gcc可以編譯C++程式。編譯C程式和C++程式時,使用的是不同的命令。編譯C++程
序時,使用的命令是g++。該命令的使用方法與gcc是相似的。下面是使用g++命令編譯C++
程式的例項。
下面是一個C++程式的程式碼,實現與1.3.1節程式同樣的功能。C++程式的程式碼與C程式
的程式碼非常相似。
#include<iostream>
int max(int i,int j)
{
if(i>j)
{
return(i);
}
else
{
return(j);
}
}
int main()
{
int i,j,k;
i=3;
j=5;
printf("hello,Linux./n");
k=max(i,j);
printf("%d/n",k);
return(0);
}
輸入下面的命令,編譯這個C++程式。
g++5.2.cpp–o 5.2.out
輸入下面的命令,對這個程式新增可執行許可權。
chmod+x 5.2.ou
輸入下面的命令,執行這個程式,程式的程式碼如下所示。
hello,Linux.
5
從結果可知,這個程式與1.3.1節中的程式執行結果是相同的。
1.4編譯過程的控制
編譯過程指的是gcc對一個程式進行編譯時完成的內部處理和步驟。編譯程式時會自動
完成預處理(Preprocessing)、編譯(Compilation)、彙編(Assembly)和連結(Linking)4個
步驟。本節將講解如何對這4個步驟進行控制。
1.4.1編譯過程簡介
gcc把一個程式的原始檔,編譯成一個可執行檔案,中間包括很多複雜的過程。可以用圖
1-1來表示編譯中4個步驟的作用和關係。
原始檔
彙編檔案
目標檔案
可執行檔案
使用-E選項進行預處理
使用-S選項彙編
-c選項生成目標檔案
連結目標檔案和庫
生成可執行檔案
預處理檔案
圖1-1 gcc編譯原始檔到可執行檔案的過程
在4個過程中,每一個操作都完成了不同的功能。編譯過程的功能如下所示。
預處理:在預處理階段,主要完成對原始碼中的預編譯語句(如巨集定義define等)和
檔案包含進行處理。需要完成的工作是對預編譯指令進行替換,把包含檔案放置到需
要編譯的檔案中。完成這些工作後,會生成一個非常完整的C程式原始檔。
編譯:gcc對預處理以後的檔案進行編譯,生成以.s為字尾的組合語言檔案。該彙編語
言檔案是編譯原始碼得到的組合語言程式碼,接下來交給彙編過程進行處理。組合語言
是一種比C語言更低階的語言,直接面對硬碟進行操作。程式需要編譯成彙編指令以
後再編譯成機器程式碼。
彙編:彙編過程是處理組合語言的階段,主要調用匯編處理程式完成將組合語言彙編
成二進位制機器程式碼的過程。通常來說,彙編過程是將.s的組合語言程式碼檔案彙編為.o
的目標檔案的過程。所生成的目標檔案作為下一步連結過程的輸入檔案。
連結:連結過程就是將多個彙編生成的目標檔案以及引用的庫檔案進行模組連結生成
一個完整的可執行檔案。在連結階段,所有的目標檔案被安排在可執行程式中的適當
的位置。同時,該程式所呼叫到的庫函式也從各自所在的函式庫中連結到程式中。經
過了這個過程以後,生成的檔案就是可執行的程式。
1.4.2控制預處理過程
引數-E可以完成程式的預處理工作而不進行其他的編譯工作。下面的命令,可以將本章
編寫的程式進行預處理,然後儲存到檔案a.cxx中。
gcc-E-o a.cxx a.c
輸入下面的命令,檢視經過預處理以後的a.cxx檔案。
vim a.cxx
可以發現,檔案a.cxx約有800行程式碼。程式中預設包含的標頭檔案已經被展開寫到到這個
預處理檔案
彙編檔案
目標檔案
可執行檔案
連結目標檔案和庫
生成可執行檔案
-c選項生成目標檔案
使用-s選項彙編
使用-E選項進行預處理
檔案中。顯示的檔案a.cxx前幾行程式碼如下所示。可見,在程式編譯時,需要呼叫非常多的頭
檔案和系統庫函式。
#1"a.c"
#1"<built-in>"
#1"<command line>"
#1"a.c"
#1"/usr/include/stdio.h"1 3 4
#28"/usr/include/stdio.h"3 4
#1"/usr/include/features.h"1 3 4
#335"/usr/include/features.h"3 4
#1"/usr/include/sys/cdefs.h"1 3 4
#360"/usr/include/sys/cdefs.h"3 4
#1"/usr/include/bits/wordsize.h"1 3 4
#361"/usr/include/sys/cdefs.h"2 3 4
#336"/usr/include/features.h"2 3 4
#359"/usr/include/features.h"3 4
#1"/usr/include/gnu/stubs.h"1 3 4
#1"/usr/include/bits/wordsize.h"1 3 4
#5"/usr/include/gnu/stubs.h"2 3 4
#1"/usr/include/gnu/stubs-32.h"1 3 4
#8"/usr/include/gnu/stubs.h"2 3 4
#360"/usr/include/features.h"2 3 4
#29"/usr/include/stdio.h"2 3 4
1.4.3生成彙編程式碼
引數-S可以控制gcc在編譯C程式時只生成相應的彙編程式檔案,而不繼續執行後面的
編譯。下面的命令,可以將本章中的C程式編譯成一個彙編程式。
gcc-S-o a.s a.c
輸入下面的命令,檢視彙編檔案a.s。可以發現這個檔案一共有60行程式碼。這些程式碼是
這個程式的彙編指令。部分彙編程式程式碼如下所示。
.file"a.c"
.text
.globl max
.type max,@function
max:
pushl%ebp
movl%esp,%ebp
subl$4,%esp
movl 8(%ebp),%eax
cmpl 12(%ebp),%eax
jle.L2
movl 8(%ebp),%eax
movl%eax,-4(%ebp)
jmp.L4
.L2:
movl 12(%ebp),%eax
movl%eax,-4(%ebp)
.L4:
movl-4(%ebp),%eax
leave
ret
.size max,.-max
.section.rodata
.LC0:
.string"hello,Linux."
.LC1:
.string"%d/n"
.text
.globl main
.type main,@function
main:
leal 4(%esp),%ecx
andl$-16,%esp
.......
popl%ebp
leal-4(%ecx),%esp
ret
.size main,.-main
.ident"GCC:(GNU)4.1.2 20070925(Red Hat 4.1.2-33)"
.section.note.GNU-stack,"",@progbits
1.4.4生成目的碼
引數-c可以使得gcc在編譯程式時只生成目錄程式碼而不生成可執行程式。輸入下面的命
令,將本章中的程式編譯成目錄程式碼。
gcc-c-o a.o a.c
輸入下面的命令,檢視這個目錄程式碼的資訊。
file a.o
顯示檔案a.o的結果如下所示,顯示檔案a.o是一個可重定位的目的碼檔案。
a.o:ELF 32-bit LSB relocatable,Intel 80386,version 1(SYSV),not stripped
1.4.5連結生成可執行檔案
gcc可以把上一步驟生成的目錄程式碼檔案生成一個可執行檔案。在終端中輸入下面的命令。
gcc a.o-o aa.out
這時生成一個可執行檔案aa.out。輸入下面的命令檢視這個檔案的資訊。
file aa.out
顯示的結果如下所示,表明這個檔案是可在Linux系統下執行的程式檔案。
aa.out:ELF 32-bit LSB executable,Intel 80386,version 1(SYSV),dynamically
linked(uses shared libs),for GNU/Linux 2.6.9,not stripped
1.5 gdb除錯程式
所謂除錯,指的是對編好的程式用各種手段進行查錯和排錯的過程。進行這種查錯處理
時,並不僅僅是執行一次程式檢查結果,而是對程式的執行過程、程式中的變數進行各種分
析和處理。本節將講解使用gdb進行程式的除錯。
1.5.1 gdb簡介
gdb是一個功能強大的除錯工具,可以用來除錯C程式或C++程式。在使用這個工具進
行程式除錯時,主要使用gdb進行下面5個方面的操作。
啟動程式:在啟動程式時,可以設定程式執行環境。
設定斷點:斷點就是可以在程式設計時暫停程式執行的標記。程式會在斷點處停止,
使用者便於檢視程式的執行情況。這裡的斷點可以是行數、程式名稱或條件表示式。
檢視資訊:在斷點停止後,可以檢視程式的執行資訊和顯示程式變數的值。
分步執行:可以使程式一個語句一個語句的執行,這時可以及時地檢視程式的資訊。
改變環境:可以在程式執行時改變程式的執行環境和程式變數。
1.5.2在程式中加入除錯資訊
為了使用gdb進行程式除錯,需要在編譯程式中加入供gdb使用的除錯資訊。方法是在
編譯程式時使用一個-g引數。在終端中輸入下面的命令,在編譯程式時加入除錯資訊。
gcc-g-o a.debug a.c
這時,編譯程式a.c,生成一個a.bedug的可執行程式。這個可執行程式中加入了供除錯
所用的資訊。
1.5.3啟動gdb
在除錯檔案以前,需要啟動gdb。在終端中輸入下面的命令。
gdb
這時,gdb的啟動資訊如下所示。這些提示顯示了gdb的版本和版權資訊。
GNU gdb Red Hat Linux(6.6-35.fc8rh)
Copyright(C)2006 Free Software Foundation,Inc.
GDB is free software,covered by the GNU General Public License,and you are
welcome to change it and/or distribute copies of it under certain conditions.
Type"show copying"to see the conditions.
There is absolutely no warranty for GDB.Type"show warranty"for details.
This GDB was configured as"i386-redhat-linux-gnu".
(gdb)
1.5.4在gdb中載入需要除錯的程式
使用gdb除錯一個程式之前,需要載入這個程式。載入程式的命令是file。在(gdb)提示
符後面輸入下面的命令載入程式a.debug。
file a.debug
命令的執行結果如下所示,顯示已經載入了這個檔案,並且使用了系統庫檔案。
Reading symbols from/root/c/a.debug...done.
Using host libthread_db library"/lib/libthread_db.so.1".
1.5.5在gdb中檢視程式碼
用gcc命令編譯程式加入了-g命令以後,編譯後的a.debug程式中加入了斷點。可以用list
命令顯示程式的原始碼和斷點。下面的步驟是檢視加入斷點以後的程式碼。
在(gdb)提示符後面輸入下面的命令。
list 1
這時,gdb會顯示第一個斷點以前的程式碼。顯示的程式碼如下所示。
1#include<stdio.h>
2
3 int max(int i,int j)
4{
5 if(i>j)
6{
7 return(i);
8}
9 else
10{
(gdb)
這時,按“Enter”鍵,顯示下一個斷點以前的程式碼。結果如下所示。
11 return(j);
12}
13}
14
15 void main()
16{
17 int i,j,k;
18 i=3;
19 j=5;
20 printf("hello,Linux./n");
(gdb)
按“Enter”鍵,顯示下一個斷點以前的程式碼。結果如下所示。
21 k=max(i,j);
22 printf("%d/n",k);
23}
(gdb)
1.5.6在程式中加入斷點
程式會執行到斷點的位置停止下來,等待使用者處理資訊或者檢視中間變數。如果自動設
置的斷點不能滿足除錯要求,可以用break命令增加程式的斷點。例如需要在程式的第6行增
加一個斷點,可以輸入下面的命令。
break 6
這時gdb顯示的結果如下所示。
Breakpoint 1 at 0x8048402:file a.c,line 6.
輸入下面的命令,在程式的第18行、19行、21行增加斷點。
break 18
break 19
break 21
1.5.7檢視斷點
命令info breakpoint可以檢視程式中設定的斷點。輸入“info breakpoint”命令,結果如
下所示。顯示程式中所有的斷點。
1 breakpoint keep y 0x08048402 in max at a.c:6
2 breakpoint keep y 0x08048426 in main at a.c:18
3 breakpoint keep y 0x0804842d in main at a.c:19
4 breakpoint keep y 0x08048440 in main at a.c:21
加上相應的斷點編號,可以檢視這一個斷點的資訊。例如下面的命令就是檢視第二個斷點。
info breakpoint 2
顯示的結果如下所示。
2 breakpoint keep y 0x08048426 in main at a.c:18
1.5.8執行程式
gdb中的run命令可以使這個程式以除錯的模式執行。下面的步驟是分步執行程式,對程
序進行除錯。
在(ddb)提示符後輸入“run”命令,顯示的結果如下所示。
Starting program:/root/c/a.debug
warning:Missing the separate debug info file:
/usr/lib/debug/.build-id/ac/2eeb206486bb7315d6ac4cd64de0cb50838ff6.debug
warning:Missing the separate debug info file:
/usr/lib/debug/.build-id/ba/4ea1118691c826426e9410cafb798f25cefad5.debug
Breakpoint 2,main()at a.c:18
18 i=3;
結果顯示了程式中的異常,並將異常記錄到了系統檔案中。然後在程式的第二個斷
點的位置第18行停下。
這時輸入“next”命令,程式會在下一行停下,結果如下所示。
19 j=5;
輸入“continue”命令,程式會在下一個斷點的位置停下。結果如下所示。
Continuing.
Breakpoint 3,main()at a.c:19
21 k=max(i,j);
輸入“continue”命令,程式執行到結束。結果如下所示,表明程式已經執行完畢正
常退出。
5
Program exited with code 02.
step命令與next命令的作用相似,對程式實現單步執行。不同之處是,在遇上函式
呼叫時,step函式可以進行到函式內部。而next函式只是一步完成函式的呼叫。
1.5.9變數的檢視
print命令可以在程式的執行中檢視一個變數的值。本節將用下面的步驟來講解變數的查
看方法。
輸入下面的命令,執行程式。
run
程式在第一個斷點位置停下。顯示的結果如下所示。
Breakpoint 2,main()at a.c:18
18 i=3;
程式進入第18行之前停下,並沒有對i進行賦值。可以用下面的命令來檢視i的值。
print i
顯示的結果如下所示,表示i現在只是一個任意值。
$5=-1076190040
輸入下面的命令,使程式執行一步。
step
顯示的結果如下所示。
19 j=5;
這時程式在19行以前停下,這時輸入下面的命令,檢視i的值。
print i
這時顯示的i的結果如下所示。表明i已經賦值為3。
$6=3
這時輸入“step”命令,再次輸入“step”命令,顯示的結果如下所示。
21 k=max(i,j);
這時輸入“step”命令,會進入到子函式中,結果如下所示。這時,顯示了傳遞給函
數的變數和值。
max(i=3,j=5)at a.c:5
5 if(i>j)
這時,輸入“step”命令,顯示的結果如下所示,表明函式會返回變數j。
11 return(j);
輸入下面的命令,檢視j的值。
print j
顯示的結果如下所示,表明j的值為5。
$7=5
這時再執行兩次“step”命令,顯示的結果如下所示。
22 printf("%d/n",k);
這時,輸入下面的命令,檢視k的值。
print k
顯示的結果如下所示,表明k的值為5。
$8=5
17完成了程式的除錯執行以後,輸入“q”命令,退出gdb。
1.6程式除錯例項
本節講解一個程式除錯例項。先編寫一個程式,在程式執行時,發現結果與預想結果有
些不同。然後用gdb工具進行除錯,通過對單步執行和變數的檢視,查找出程式的錯誤。
1.6.1編寫一個程式
本節將編寫一個程式,要求程式執行時可以顯示下面的結果。
1+1=2
2+1=3 2+2=4
3+1=4 3+2=5 3+3=6
4+1=5 4+2=6 4+3=7 4+4=8
很明顯,這個程式是通過兩次迴圈與一次判斷得到的。程式中需要定義三個變數。下面
用這個思路來編寫這個程式。
開啟一個終端。在終端中輸入“vim”命令,開啟VIM。
在VIM中按“i”鍵,進入到插入模式。然後在VIM中輸入下面的程式碼。
#include<stdio.h>
main()
{
int i,j,k;
for(i=1;i<=4;i++)
{
for(j=1;j<=4;j++);
{
if(i>=j)
{
k=i+j;
printf("%d+%d=%d",i,j,k);
}
}
printf("/n");
}
}
在VIM中按“Esc”鍵,返回到普通模式。然後輸入下面的命令,儲存這個檔案。
:w/root/c/test.c
輸入“:q”命令退出VIM。很容易發現,在第二個迴圈後
1.6.2編譯檔案
本節將對上一節編寫的程式進行編譯和執行。在執行程式時,會發現程式有錯誤。
在終端中輸入下面的命令,編譯這個程式。
gcc/root/c/test.c
程式可以正常編譯通過,輸入下面的命令,執行這個程式。
/root/c/a.out
程式的顯示結果是4個空行,並沒有按照預想的要求輸出結果。
輸入下面的命令,對這個程式進行編譯。在編譯加入-g引數,為gdb除錯做準備。
gcc-g-o test.debug 6.2.c
這時,程式可以正常編譯通過。輸出的檔案是test.debug。這個檔案中加入了檔案調
試需要的資訊。
1.6.3程式的除錯
本節將講述使用gdb對上一節編寫的程式進行除錯,查找出程式中的錯誤。
在終端中輸入“gdb”命令,進入到gdb,顯示的結果如下所示。
GNU gdb Red Hat Linux(6.6-35.fc8rh)
Copyright(C)2006 Free Software Foundation,Inc.
GDB is free software,covered by the GNU General Public License,and you are
welcome to change it and/or distribute copies of it under certain conditions.
Type"show copying"to see the conditions.
There is absolutely no warranty for GDB.Type"show warranty"for details.
This GDB was configured as"i386-redhat-linux-gnu".
匯入檔案。在gdb中輸入下面的命令。
file/root/c/test.debug
這時顯示的結果如下所示。表明已經成功載入了這個檔案。
Reading symbols from/root/c/test.debug...(no debugging symbols
found)...done.
Using host libthread_db library"/lib/libthread_db.so.1".
檢視檔案。在終端中輸入下面的命令。
list
顯示的檔案檢視結果如下所示。
1#include<stdio.h>
2
3 main()
4{
5 int i,j,k;
6 for(i=1;i<=4;i++)
7{
8 for(j=1;j<=4;j++);
9{
10 if(i>=j)
(gdb)
11{
12 k=i+j;
13 printf("%d+%d=%d",i,j,k);
14}
15}
16 printf("/n");
17}
18}
(gdb)
Line number 19 out of range;6.2.c has 18 lines.
在程式中加入斷點。從顯示的程式碼可知,需要在第6行、第11行、第12行和第13
行加入斷點。在gdb中輸入下面的命令。
break 6
break 11
break 12
break 13
gdb顯示的新增斷點的結果如下所示。
Breakpoint 1 at 0x8048405:file 6.2.c,line 6.
Breakpoint 2 at 0x8048429:file 6.2.c,line 11.
Breakpoint 3 at 0x8048429:file 6.2.c,line 12.
Breakpoint 4 at 0x8048432:file 6.2.c,line 13.
輸入下面的命令,執行這個程式。
run
執行到第一個斷點顯示的結果如下所示。
Breakpoint 1,main()at 6.2.c:6
6 for(i=1;i<=4;i++)
輸入“step”命令,程式執行一步,結果如下所示。
8 for(j=1;j<=4;j++);
這說明程式已經進入了for迴圈。這時輸入下面命令,檢視i的值。
print i
顯示的結果如下所示。
$2=1
這時再輸入“step”命令,顯示的結果如下所示。
10 if(i>=j)
這時再輸入“step”命令,顯示的結果如下所示。
16 printf("/n");
這表明,在進行j的for迴圈時,沒有反覆執行迴圈體。這時再輸入“step”命令,
顯示的結果如下所示。
for(i=1;i<=4;i++)
這表明,程式正常的進行了i的for迴圈。這是第二次執行for迴圈。
17輸入“step”命令,顯示的結果如下所示。
8 for(j=1;j<=4;j++);
18這表明,程式執行到for迴圈。這時再次輸入“step”命令,顯示的結果如下所示。
10 if(i>=j)
19輸入“step”命令,顯示的結果如下所示。
16 printf("/n");
20輸入“step”命令,顯示的結果如下所示。
6 for(i=1;i<=4;i++)
21這說明,程式正常的進行了i的for迴圈,但是沒有執行j的for迴圈。這一定是j的
for迴圈語句有問題。這時就不難發現j的for迴圈後面多了一個分號。
22輸入“q”命令,退出gdb。
1.6.4 gdb幫助的使用
gdb有非常多的命令。輸入“help”命令可以顯示這些命令的幫助資訊。本節將講解幫助
資訊的使用。
在gdb輸入“help”命令,顯示的幫助資訊如下所示。
List of classes of commands:
aliases--Aliases of other commands
breakpoints--Making program stop at certain points
data--Examining data
files--Specifying and examining files
internals--Maintenance commands
obscure--Obscure features
running--Running the program
stack--Examining the stack
status--Status inquiries
support--Support facilities
tracepoints--Tracing of program execution without stopping the program
user-defined--User-defined commands
Type"help"followed by a class name for a list of commands in that class.
Type"help all"for the list of all commands.
Type"help"followed by command name for full documentation.
Type"apropos word"to search for commands related to"word".
Command name abbreviations are allowed if unambiguous.
上面的幫助資訊顯示,輸入“help all”會輸出所有幫助資訊。
在“help”命令後面加上一個命令名稱,可以顯示這個命令的幫助資訊。例如輸入
“help file”,顯示的file命令幫助資訊如下所示。
Use FILE as program to be debugged.
It is read for its symbols,for getting the contents of pure memory,
and it is the program executed when you use the`run'command.
If FILE cannot be found as specified,your execution directory path
($PATH)is searched for a command of that name.
No arg means to have no executable file and no symbols.
1.7 gdb常用命令
除了前面講述的gdb命令以外,gdb還有很多種命令。這些命令可以完成程式除錯的各
種功能。其他的常用命令含義如下所示。
backtrace:顯示程式中的當前位置和表示如何到達當前位置的棧跟蹤(同義詞:where)。
breakpoint:在程式中設定一個斷點。
cd:改變當前工作目錄。
clear:刪除剛才停止處的斷點。
commands:命中斷點時,列出將要執行的命令。
continue:從斷點開始繼續執行。
delete:刪除一個斷點或監測點,也可與其他命令一起使用。
display:程式停止時顯示變數和表示式。
down:下移棧幀,使得另一個函式成為當前函式。
frame:選擇下一條continue命令的幀。
info:顯示與該程式有關的各種資訊。
info break:顯示當前斷點清單,包括到達斷點處的次數等。
info files:顯示被除錯檔案的詳細資訊。
info func:顯示所有的函式名稱。
info local:顯示當函式中的區域性變數資訊。
info prog:顯示被除錯程式的執行狀態。
info var:顯示所有的全域性和靜態變數名稱。
jump:在源程式中的另一點開始執行。
kill:異常終止在gdb控制下執行的程式。
list:列出相應於正在執行的程式的原始檔內容。
next:執行下一個源程式行,從而執行其整體中的一個函式。
print:顯示變數或表示式的值。
pwd:顯示當前工作目錄。
pype:顯示一個數據結構(如一個結構或C++類)的內容。
quit:退出gdb。
reverse-search:在原始檔中反向搜尋正規表示式。
run:執行該程式。
search:在原始檔中搜索正規表示式。
set variable:給變數賦值。
signal:將一個訊號傳送到正在執行的程序。
step:執行下一個源程式行,必要時進入下一個函式。
undisplay display:命令的反命令,不要顯示錶達式。
until:結束當前迴圈。
up:上移棧幀,使另一函式成為當前函式。
watch:在程式中設定一個監測點(即資料斷點)。
whatis:顯示變數或函式型別。
1.8編譯程式常見的錯誤與問題
在編寫程式時,無論是邏輯上還是語法上,不可能一次做到完全正確。於是在編譯程式
時,就會發生編譯錯誤。本節將講述程式編譯時常見的錯誤型別與處理方法。
1.8.1邏輯錯誤與語法錯誤
在程式設計時,出現的錯誤可能有邏輯錯誤和語法錯誤兩種。這兩種錯誤的發生原因和處理
方法是不同的。本節將講述這兩種錯誤的處理方法。
邏輯錯誤指的是程式的設計思路發生了錯誤。這種錯誤在程式中是致命的,程式可能
正常編譯通過,但是結果是錯誤的。當程式正常執行而結果錯誤時,一般都是程式設計的
思路錯誤。這時,需要重新考慮程式的運算方法與資料處理流程是否正確。
語法錯誤:語法錯誤指的是程式的思路正確,但是在書寫語句時,發生了語句錯誤。
這種錯誤一般是程式設計時不小心或是對語句的錯誤理解造成的。在發生語句錯誤時,程
序一般不能正常編譯通過。這時會提示錯誤的型別和錯誤的位置,按照這些提示改正
程式的語法錯誤即可完成錯誤的修改。
1.8.2 C程式中的錯誤與異常
C程式中的錯誤,根據嚴重程式的不同,可以分為異常與警誤兩類。在編譯程式時,這
兩種情況對編譯的影響是不同的,對錯誤與異常的處理方式是不同的。
1.什麼是異常
異常指的是程式碼中輕微的錯誤,這些錯誤一般不會影響程式的正常執行,但是不完全符
合編程的規範。在編譯程式時,會產生一個“警告”,但是程式會繼續編譯。下面的程式會使
程式發生異常,在編譯時產生一個警告錯誤。
在除法中,0作除數。
在開方運算時,對負數開平方。
程式的主函式沒有宣告型別。
程式的主函式沒有返回值。
程式中定義了一個變數,但是沒有使用這個變數。
變數的儲存發生了溢位。
2.什麼是錯誤
錯誤指的是程式的語法出現問題,程式編譯不能正常完成,產生一個錯誤資訊。這時會
顯示錯誤的型別與位置。根據這些資訊可以對程式進行修改。
1.8.3編譯中的警告提示
在編譯程式時,如果發生了不嚴重的異常,會輸出一個錯告錯誤,然後完成程式的編譯。
例如下面的內容是一個程式在編譯時產生的警告。
5.1.c:In function'main':
5.1.c:16:警告:‘main’的返回型別不是‘int’
5.1.c:18:警告:被零除
這些的含義如下所示。
(1)“In function'main':”表示發生的異常在main函式內。
(2)“5.1.c:16:”表示發生異常的檔案是5.1.c,位置是第16行。
(3)下面的資訊是第16行的異常,表明程式的返回型別不正確。
‘main’的返回型別不是‘int’
(4)下面的警告資訊表明程式的第18行有除數為0的錯誤。
5.1.c:18:警告:被零除
1.8.4找不到包含檔案的錯誤
程式中的包含檔案在系統或工程中一定要存在,否則程式編譯時會發生致命錯誤。例如
下面的語句包含了一個不正確的標頭檔案。
#include<stdio1.h>
編譯程式時,會發生錯誤,錯誤資訊如下所示。
5.1.c:2:20:錯誤:stdio2.h:沒有那個檔案或目錄
1.8.5錯誤地使用逗號
程式中逗號的含義是並列幾個內容,形成某種演算法或結構。程式中如果錯誤地使用逗號,會
使程式在編譯時發生致命錯誤。例如下面的程式碼,是程式中的if語句後面有一個錯誤的逗號。
int max(int i,int j)
{
if(i>j),
{
return(i);
}
else
{
return(j);
}
}
程式編譯時輸出的錯誤資訊如下所示。表明max函式中逗號前面的表示式有錯誤,實際
上的錯誤是多一個逗號。
5.1.c:In function‘max’:
5.1.c:4:錯誤:expected expression before‘,’token
5.1.c:In function‘max’:
1.8.6括號不匹配錯誤
程式中的引號、單引號、小括號、中括號、大括號等符號必須成對出現。這方面的錯誤
會使程式發生符號不匹配的錯誤。發生這種錯誤後,編譯程式往往不能理解程式碼的含義,也
不能準確顯示錯誤的位置,而是顯示錶達式錯誤。例如下面的程式碼,在最後一行上了一個花
括號。
int max(int i,int j)
{
if(i>j)
{
return(i);
}
else
{
return(j);
}
編譯程式時,會顯示下面的錯誤資訊。
5.1.c:22:錯誤:expected declaration or statement at end of input
1.8.7小括號不匹配錯誤
程式中的小括號一般在一行內成對出現並且相匹配。小括號不匹配時,程式發生致命錯
誤。例如下面的程式碼,第一行多了一個右半邊括號。
if(i>j))
{
return(i);
}
else
{
return(j);
}
程式設計程式時,會發生下面的錯誤。顯示括號前面有錯誤,並且導致下面的else語句也有
錯誤。
5.1.c:4:錯誤:expected statement before‘)’token
5.1.c:8:錯誤:expected expression before‘else’
1.8.8變數型別或結構體宣告錯誤
程式中的變數或結構體的名稱必須正確,否則程式會發生未宣告的錯誤。例如下面的代
碼,用一個不存在的型別來宣告一個變數。
ch a;
程式在執行時,會顯示出這個變數錯誤,並且會顯示有其他的錯誤。
5.1.c:17:錯誤:‘ch’未宣告(在此函式內第一次使用)
5.1.c:17:錯誤:(即使在一個函式內多次出現,每個未宣告的識別符號在其
5.1.c:17:錯誤:所在的函式內只報告一次。)
5.1.c:17:錯誤:expected‘;’before‘a’
1.8.9使用不存在的函式的錯誤
如果程式引用了一個不存在的函式,會使用程式發生嚴重的錯誤。例如下面的程式碼,引
用了一個不存在的函式add。
k=add(i,j);
程式顯示的錯誤資訊如下所示,表明在main函式中的add函式沒有定義。
/tmp/ccYQfDJy.o:In function`main':
5.1.c:(.text+0x61):undefined reference to`add'
collect2:ld返回1
5.8.10大小寫錯誤
C程式對程式碼的大小寫是敏感的,不同的大小寫代表不同的內容。例如下面的程式碼,將
小寫的“int”錯誤的寫成了“Int”。
Int t;
程式顯示的錯誤資訊如下所示,表明“Int”型別不存在或未宣告。發生這個錯誤時,會
輸出多行錯誤提示。
5.1.c:16:錯誤:‘Int’未宣告(在此函式內第一次使用)
5.1.c:16:錯誤:(即使在一個函式內多次出現,每個未宣告的識別符號在其
5.1.c:16:錯誤:所在的函式內只報告一次。)
5.1.c:16:錯誤:expected‘;’before‘t’
1.8.11資料型別的錯誤
程式中的某些運算,必須針對相應的資料型別,否則這個運算會發生資料型別錯誤。例
如下面的程式碼,錯誤地將兩個整型數進行求餘運算。
float a,b;
a=a%b;
程式編譯時,輸出下面的錯誤,表明“%”運算子的運算元無效。
5.1.c:19:錯誤:雙目運算子%運算元無效
1.8.12賦值型別錯誤
任何一個變數,在賦值時必須使用相同的資料型別。例如下面的程式碼,錯誤地將一個字
符串賦值給一個字元。
char c;
c="a";
程式編譯時的結果如下所示,表明賦值時資料型別錯誤。
5.1.c:19:警告:賦值時將指標賦給整數,未作型別轉換
1.8.13迴圈或判斷語句中多加分號
分號在程式中的作用是表示一個語句結束。在程式的語句中用一個單獨的分號表示一個
空語句。但是在迴圈或判斷結構的後面,一個分號會導致程式的邏輯發生錯誤。關於這些結
構的使用方法,後面的章節將會詳細講到。下面的程式,在for語句的後面,錯誤的添加了一
個分號,導致程式不能正常地進行迴圈。
#include<stdio.h>
main()
{
int sum,j;
sum=0;
for(j=0;j<11;j++);
{
sum=sum+j;
}
printf(“%d”,sum);
}
這個程式的本意是要求出10以內的整數和。但是在for語句的後面,錯誤地使用了一個
分號。這時,程式不能正確地進行迴圈,而是把分號作為一個語句進行迴圈,所以程式輸出
的結果為“11”。
1.9小結
程式的編譯和除錯是程式設計的一個重要環節。本章講解了Linux系統中C程式設計的編譯器gcc
和編譯器gdb的使用。使用gcc時,需要對編譯進行各種設定,需要理解gcc各項引數的作
用。gdb的學習重點是gdb單步執行程式的理解,通過程式的單步執行發現程式中的問題。