C語言學習總結(二)——GCC和GDB

一、GCC是什麼

GCC是什麼,GCC是一個軟件,GNU編譯器套件(GNU Compiler Collection),最初由GNU組織爲C語言開發,之後發展爲一個套件。


二、GCC編譯過程

1、預處理

預處理的過程是編譯器去將頭文件進行包含,將宏定義進行替換,在這個過程中,inlude和define命令生效。
預處理過程中的常用知識點有:

  1. 頭文件未包含,<> 和 “” 的使用範圍是不一樣的,對於用戶自定義的頭文件,需要用 “” 指出路徑。 可以使用 -I 命令指定頭文件的目錄路徑
  2. 宏的重定義,因爲不同頭文件可能會重複引用同樣的宏,會出現類似的問題,所以,使用
    #ifndef XXX
    #define XXX

    #endif
    來避免重複定義宏,這種寫法同樣可以用來進行條件編譯(-D XXX)。
  3. include與define不是關鍵字,他們是GCC可以識別的標識碼。
  4. 宏定義中可以進行函數替換,但是必須是在同一行,多行函數一般使用do{ … }while(0)
  5. # 和 ## 是預處理運算符

    define ABC(x) #x 字符串化
    define ABC(x) day##x 字符連接
  6. 常見的預定義宏

    • FILE代表當前源文件的文件名
    • LINE代表展開成當前代碼行的行號,是編譯器內建的特殊宏定義
    • FUNCTION : 函數名

最後的問題,在預處理的過程中,頭文件中的定義是如何被包含在文件中的。

2. 編譯

編譯是將C語言程序編譯成彙編文件的一個過程。
編譯的過程主要經過了六步:掃描(詞法分析)、語法分析、語義分析、源代碼優化、代碼生成、目標代碼優化。
在編譯過程中可能出現的問題有:

  1. 語法錯誤,根據錯誤提示進行修改代碼
  2. 優化錯誤,在嵌入式系統中會出現編譯器的過度優化,有時候需要設定優化選項

3. 彙編

彙編器是將彙編代碼轉變成機器可以執行的命令,每一個彙編語句幾乎都對應一條機器指令。彙編相對於編譯過程比較簡單,根據彙編指令和機器指令的對照表一一翻譯即可。

4. 鏈接

通過調用鏈接器ld來鏈接程序運行需要的一大堆目標文件,以及所依賴的其它庫文件,最後生成可執行文件。鏈接的過程主要分爲:地址和空間分配(Address and Storage Allocation),符號決議(Symbol Resolution),重定位(Relocation)等。
鏈接分爲靜態鏈接和動態鏈接:

  1. 靜態鏈接是指在編譯階段直接把靜態庫加入到可執行文件中去,這樣可執行文件會比較大。

    • 編譯靜態鏈接庫
      1. 生成目標文件 .o
      2. ar crv [.a] [.o]
    • 調用靜態鏈接庫
      1. gcc -o [file] [file.c] -L. [file.a]
  2. 動態鏈接是指鏈接階段僅僅只加入一些描述信息,而程序執行時再從系統中把相應動態庫加載到內存中去。

    • 首先、在任何給定的文件系統中,對於一個庫只有一個.so文件
    • 第二、所有引用該庫德可執行目標文件共享這個.so文件中的代碼和數據
    • 第三、在存儲器中,一個共享庫的.text節只有一個副本可以被不同的正在運行的進程共享.

      1. 編譯動態鏈接庫
        • 生成位置無關的目標代碼 gcc-fPIC -c [*.c]
        • gcc -shared -o [.so] [.o]
      2. 調用動態鏈接庫(與靜態鏈接庫類似)
        • gcc -o [file] [file.c] -L. [file.so]

鏈接過程詳解

目標文件

目標文件分爲三種:

  1. 可重定位的目標文件:
    包含二進制代碼和數據,其形式可以再編譯時與其他可定位目標文件合併起來,創建一個可執行目標文件。
  2. 可執行目標文件:
    包含二進制代碼和數據,其形式可以被直接拷貝到存儲器並執行.
  3. 共享目標文件:
    一種特殊的可重定位目標文件,可以再加載或運行時,被動態地夾在到存儲器並執行.
    編譯器和彙編器生成可重定位目標文件(包括共享目標文件),鏈接器生成可執行目標文件.

    可重定位的目標文件的結構如下:

標識 作用
.text: 已編譯程序的機器代碼
.rodata: 只讀數據
.data: 已初始化的全局C變量
.bss: 未初始化的全局C變量.在目標文件中這個節不佔實際空間,僅是一個佔位符.
.sysmtab: 一個符號表,存放在程序中被定義和引用的函數和全局變量的信息.
.rel.text: 當鏈接器把這個目標文件和其他文件結合時,.text節中的許多位置都需要修改.一般而言,任何調用外部函數或者引用全局變量的指令都要修改.另一個方面,調用本地函數的指令則不需要修改.
.rel.data: 被模塊定義或引用的任何全局變量的信息.
.debug: 一個調試符號表
.line: 原始C源程序中的行號和.text節中機器指令之間的映射.
.strtab: 一個字符串表,其中內容包括.symtab和.debug節中的符號表,以及節頭部中的節名字.

一個m文件的符號表有三種不同的符號

類型 作用
由m定義並能被其他模塊引用的全局符號. 1.m定義的非靜態的C函數 2.m定義的不帶static屬性的全局變量.
由其他模塊定義並被模塊m引用的全局符號.這些符號成爲外部符號, 在其他模塊中的C函數和變量.
只被模塊m定義和引用的本地符號. 由m定義的帶static屬性的C函數和全局變量

符號解析

將每個引用和它輸入的可重定位目標文件按的符號表中的一個確定的符號定義聯繫起來.

  1. 局部和static變量等:
    對於那些和引用定義在相同模塊的本地符號的引用,符號解析式非常簡單明瞭的.編譯器只允許每個模塊中的每個本地符號只有一個定義.編譯器還確保靜態本地變量,它們會有本地鏈接器符號,擁有唯一的名字.
  2. 外部引用:對於全局符號的引用解析,當編譯器遇到一個不是在當前模塊中定義的符號(變量或函數名)時,它會假設該符號式在其他某個模塊中定義的,生成一個鏈接器符號表表目,並把它交給鏈接器處理.如果鏈接器在它的任何輸入模塊中都找不到這個被引用的符號,它就輸出一條錯誤信息並終止.
  3. 在編譯時,編譯器輸出的每個全局符號給彙編器,或者是強,或者是弱,而彙編器把這個信息隱含地編碼在可重定位目標文件的符號表中.函數和以初始化的全局變量是強符號,未初始化的全局變量是弱符號.
    編譯系統提供一種機制,將所有相關的目標模塊打包爲一個單獨的文件,稱爲靜態庫,它可以用做鏈接器的輸入.當鏈接器構造一個輸出的可執行文件時,它只拷貝靜態庫裏被應用程序引用的目標模塊。
    輸入命令時要考慮到,靜態庫和目標文件的位置,庫文件放在目標文件的後面,如果庫文件之間有引用關係,則被引用的庫放在後面。

鏈接過程常見錯誤:

  • 原材料不夠 undefined reference to fun
    尋找標籤是否實現了,鏈接時是否加入一起鏈接
  • 原材料重複 multiple definition of fun
    多次實現了標籤,只保留一個標籤實現

5. GCC的主要選項

選項 含義
編譯過程
預處理 -E 不經過編譯預處理程序的輸出而輸送至標準輸出
編譯 -S 要求編譯程序生成來自源代碼的彙編程序輸出
彙編 -c 通知GCC取消鏈接步驟,即編譯源碼並在最後生成目標文件
鏈接 -o 生成可執行文件
調試 -g 包含調試信息
頭文件 -I 包含指定路徑的頭文件
警告選項
-v 啓動所有警報
-Wall 在發生警報時取消編譯操作,即將警報看作是錯誤
-w 禁止所有的報警
庫文件
-static 靜態編譯
-shared 1、生成動態庫文件 2、進行動態編譯
-L dir 庫文件搜索中添加路徑
-fPIC 生成使用相對位置無關的目標代碼(Position Independent Code)然後通常用於使用gcc的-static選項從該PIC目標文件生成動態庫文件
特定功能
-D 定義指定的宏,使它能夠通過源碼中的#ifdef進行檢驗;
-O、 -O2、-O3 將優化狀態打開,該選項不能與-g選項聯合使用;

三、GDB的使用

GDB是GNU開源組織發佈的一個強大的UNIX下的程序調試工具。一般來說,GDB主要幫忙你完成下面四個方面的功能:

  • 啓動你的程序,可以按照你的自定義的要求隨心所欲的運行程序。
  • 可讓被調試的程序在你所指定的調置的斷點處停住。(斷點可以是條件表達式)
  • 當程序被停住時,可以檢查此時你的程序中所發生的事。
  • 動態的改變你程序的執行環境。

1. 啓動調試

首先,要進行調試,需要在生成可執行文件的時候使用gcc -g選項:
-g[level] 默認爲2

  • 0 表示不生成任何調試信息
  • 1 表示生成最少的調試信息,不提供局部變量及源代碼行列等信息。
  • 2 標準模式
  • 3 較2而言,包含了宏定義等額外信息

然後有兩種方式啓動調試
方式一: 直接在命令行下輸入gdb ./[filename]
方式二:運行gdb後輸入file ./[filename]

2. 設置斷點

除了斷點外還有Watchpoints(觀測點)及Catchpoints (異常捕捉點)
1. 設置斷點
輸入b或break加上斷點位置或斷點函數名,如

b main               \\在main函數入口設置斷點
b text2bin.c:50      \\在源代碼第50行設置斷點
info breakpoints     \\查看斷點信息
clear                \\清除所有斷點
clear text2bin.c:50    \\清除特定的斷點
clear main 
disable及enable  \\開關某個特定的斷點
b +5 及b -5, \\表示在當前行的後五行及前五行位置設置斷點

對於觀測點(Watchpoints),是指在某個條件下觸發的斷點,如text2bin中77行:

Buffer2[nCount++] = ConvertTextToInt(sData); 

我們要查看當nCount爲10時的運行狀況,我們可以通過下面的步驟完成:

  1. 執行b 77,返回這個斷點號是3
  2. 執行condition 3 nCount=10

這樣就可以控制當nCount爲10時在77行處中斷.
2. 設置捕捉點
你可設置捕捉點來補捉程序運行時的一些事件。如:載入共享庫(動態鏈接庫)或是C++的異常。設置捕捉點的格式爲:catch
3. 其它相關命令

命令 含義
info b 查看所設斷點
break 行號或函數名 <條件表達式> 設置斷點
tbreak 行號或函數名 <條件表達式> 設置臨時斷點,到達後被自動刪除
delete [斷點號] 刪除指定斷點,若缺省斷點號則刪除所有斷點
disable [斷點號]] 停止指定斷點,使用”info b”仍能查看此斷點。
enable [斷點號] 激活指定斷點,即激活被disable停止的斷點
condition [斷點號] <條件表達式> 修改對應斷點的條件
ignore [斷點號] 在程序執行中,忽略對應斷點num次
step 單步恢復程序運行,且進入函數調用
next 單步恢復程序運行,但不進入函數調用
finish 運行程序,直到當前函數完成返回
c 繼續執行函數,直到函數結束或遇到新的斷點

3. 檢查程序

可以使用以下命令查詢程序

命令 含義
list<行號> <函數名>
forward-search 正則表達式 源代碼前向搜索
reverse-search 正則表達式 源代碼後向搜索
dir dir 停止路徑名
show directories 顯示定義了的源文件搜索路徑
info line 顯示加載到Gdb內存中的代碼
print 表達式 變量
x 查看內存變量內容。其中n爲整數表示顯示內存的長度,f表示顯示的格式,u表示從當前地址往後請求顯示的字節數
display 表達式 設定在單步運行或其他情況中,自動顯示的對應表達式的內容
info thread 查看當前有多少線程
thread # 切換到指定線程
set print thread-events on/off 設定是否打印線程狀態
b/break [location] thread # 當設置中斷時,也可以專爲某個線程設置

print詳解

  1. 操作符:
    • @ 是一個和數組有關的操作符,在後面會有更詳細的說明。
    • :: 指定一個在文件或是一個函數中的變量。
    • {} 表示一個指向內存地址的類型爲type的一個對象。
  2. 程序變量
    在GDB中,你可以隨時查看以下三種變量的值:
    • 全局變量(所有文件可見的)
    • 靜態全局變量(當前文件可見的)
    • 局部變量(當前Scope可見的)
  3. 數組
    “@”的左邊是第一個內存的地址的值,“@”的右邊則你你想查看內存的長度。
    例如,你的程序中有這樣的語句:
    int *array = (int *) malloc (len * sizeof (int));
    於是,在GDB調試過程中,你可以以如下命令顯示出這個動態數組的取值:
    p *array@len
  4. 輸出格式
    x 按十六進制格式顯示變量。
    d 按十進制格式顯示變量。
    u 按十六進制格式顯示無符號整型。
    o 按八進制格式顯示變量。
    t 按二進制格式顯示變量。
    a 按十六進制格式顯示變量。
    c 按字符格式顯示變量。
    f 按浮點數格式顯示變量。

4. 改變程序的執行

  1. 修改變量值
    修改被調試程序運行時的變量值,在GDB中很容易實現,使用GDB的print命令即可完成。如:
    (gdb) print x=4
  2. 跳轉執行
    一般來說,被調試程序會按照程序代碼的運行順序依次執行。GDB提供了亂序執行的功能,也就是說,GDB可以修改程序的執行順序,可以讓程序執行隨意跳躍。這個功能可以由GDB的jump命令來完:
    jump 指定下一條語句的運行點。可以是文件的行號,可以是file:line格式,可以是+num這種偏移量格式。表式着下一條運行語句從哪裏開始。
  3. 產生信號量
    使用singal命令,可以產生一個信號量給被調試的程序。如:中斷信號Ctrl+C。這非常方便於程序的調試,可以在程序運行的任意位置設置斷點,並在該斷點用GDB產生一個信號量,這種精確地在某處產生信號非常有利程序的調試。

    single命令和shell的kill命令不同,系統的kill命令發信號給被調試程序時,是由GDB截獲的,而single命令所發出一信號則是直接發給被調試程序的。

  4. 強制函數返回
    如果你的調試斷點在某個函數中,並還有語句沒有執行完。你可以使用return命令強制函數忽略還沒有執行的語句並返回。
    使用return命令取消當前函數的執行,並立即返回,如果指定了,那麼該表達式的值會被認作函數的返回值。

  5. 強制調用函數
    call表達式中可以一是函數,以此達到強制調用函數的目的。並顯示函數的返回值,如果函數返回值是void,那麼就不顯示。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章