GCC學習筆記

来源:https://www.cnblogs.com/ayangs/archive/2018/09/15/9649655.html
-Advertisement-
Play Games

GCC(GNU Compiler Collection,GNU 編譯器套件)是由 GNU 開發的編程語言編譯器,支持C、C++、Objective-C、Fortran、Java、Ada和Go語言等多種預言的前端,以及這些語言的庫(如libstdc++、libgcj等等),它是以 GLP 許可證所發行... ...


目錄

導語

GCCGNU Compiler Collection,GNU 編譯器套件) 是由 GNU 開發的編程語言編譯器,支持C、C++、Objective-C、Fortran、Java、Ada和Go語言等多種預言的前端,以及這些語言的庫(如libstdc++、libgcj等等),它是以 GLP 許可證所發行的自由軟體,也是 GNU 計劃的關鍵部分。GCC 原本作為GNU操作系統的官方編譯器,現已被大多數類 Unix 操作系統(如Linux、BSD、Mac OS X 等)採納為標準的編譯器,GCC同樣適用於微軟的Windows 。
本文主要記錄 GCC 學習使用過程中的一些操作,便於後期使用時查閱,前期入門階段主要參考 An Introduction to GCC


1 編譯C程式

1.1 單個源文件

#include <stdio.h>
int main(void)
{
    printf("Hello, world!\n");
    return 0;
}

編譯源代碼:

$ gcc helloworld.c

未指定編譯輸出文件名則預設輸出 a.out

$ gcc -Wall helloworld.c -o helloworld

-o: output 該選項用於指定存儲機器碼的輸出文件;
-Wall: Warning all 該選項用於打開所有最常用的編譯警告;

$ ./helloworld
Hello, world!

1.2 多個源文件

如果將源碼分為多個源文件,我們也可以使用以下命令進行編譯:

$ gcc -Wall hello.c world.c hello_world.c -o helloworld

對於包含多個源文件的工程,我們可以將編譯過程分為兩個階段:

第一階段:所有 .c .h 源文件經過編譯,分別生成對應的 .o 對象文件;
第二階段:所有 .o 對象文件經過鏈接生成最終的可執行文件。

.o 對象文件包含機器碼,任何對其他文件中的函數或變數的記憶體地址的引用都留著沒有被解析,這些都留給鏈接器 GNU ld 生成可執行文件時再處理。

  • 源文件生成對象文件

    $ gcc -Wall -c hello.c world.c hello_world.c

    註意:這裡不需要使用 -o 來指定輸出文件名, -c 選項會自動生成與源文件同名的對象文件

  • 對象文件生成可執行文件

    $ gcc hello.o world.o hello_world.o -o hello_world

    註意:這裡不需要使用者-Wall ,因為源文件已經成功編譯成對象文件了,鏈接是一個要麼成功要麼失敗的過程。

通常,鏈接快於編譯,因此,對於大型項目,將不同功能在不同的源文件中進行實現,在修改功能時,可以只編譯被修改的文件,顯著節省時間。

1.3 靜態庫文件

編譯生成的 .o 對象文件可以通過歸檔器 GNU ar 打包成 .a 靜態庫文件,將某些功能提供給外部使用。在上述多個源文件的例子當中,我們可以將 hello.oworld.o 打包成靜態庫文件:

$ ar cr libhello.a hello.o world.o

這樣便生成了 .a 庫文件。在使用的時候,編譯器需要通過路徑找到對應的庫文件,而標準的系統庫,通常能在 /usr/lib/lib 目錄下找到,自己工程中生成的庫文件的位置需要通過編譯選項告知給編譯器。

一種直接的方式是在編譯命令中通過絕對路徑或者相對路徑指定靜態庫的位置,例如:

$ gcc -Wall hello_world.c ./libhello.a -o helloworld

實際上,為了避免使用長路徑名,我們可以使用 -l 來鏈接庫文件,例如:

$ gcc -Wall -L./ hello_world.c -lhello -o helloworld

兩種方式的效果應當是一致的,這裡 -L 選項指示庫文件的位置位於 ./ ,而 -lhello 選項會指示編譯器試圖鏈接 ./ 目錄下的文件名為 libhello.a 的靜態庫文件。


2 編譯選項

2.1 庫文件

外部庫文件包含2種類型:靜態庫和共用庫。

靜態庫文件格式為 .a ,文件包含對象文件的機器碼,鏈接靜態庫文件時,鏈接器將程式用到的外部函數的機器碼從靜態庫文件中提取出來,並複製到最終的可執行文件當中。
共用庫文件格式為 .so ,表示 shared object ,使用共用庫的可執行文件僅僅包它含用到的函數的表格,而不是外部函數所在對象文件的整個機器碼。共用庫在使用時需要先於可執行文件載入到記憶體,並支持同時被多個程式共用,這個過程稱為動態鏈接( dynamic linking )。

相比於靜態庫,共用庫具備如下優點:

  • 減少可執行程式文件大小;
  • 多個程式共用;
  • 庫文件升級無需重新編譯可執行程式

2.2 搜索路徑

在編譯過程中,如果找不到頭文件會報錯,預設情況下,GCC會在下麵的目錄中搜索頭文件,這些路徑稱為 include路徑

/usr/local/include/
/usr/include/

同理,在鏈接過程中,如果找不到庫文件也會報錯,預設情況下,GCC在下麵的目錄中搜索庫文件,這些路徑稱為 鏈接路徑

/usr/local/lib/
/usr/lib/

如果需要檢索其他路徑下的頭文件或者庫文件,可以通過 -I-L 的方式來分別擴展頭文件和庫文件的搜索路徑,這裡介紹2種搜索路徑的設置方法:

  1. 命令行選項

    $ gcc -Wall [-static] -I<INC_PATH> -L<LIB_PATH> <INPUT_FILES> -l<INPUT_LIBS> -O <OUTPUT_FILES>

    在前面生成的靜態庫文件的基礎上,我們可以進一步生成最終的可執行文件:

    $ gcc -Wall [-static] -I. -L. hello_world.c -lhello -o helloworld

    上述命令,-I 將指定頭文件位於當前路徑 .-L 將指定庫文件位於當前路徑 .-lhello 指定參與編譯的自定義的庫文件。

    需要註意的是,gcc 編譯器在使用 -l 選項時會預設優先鏈接到共用庫文件,如果確認使用靜態庫,則可以使用 -static 選項進行限制。

  2. 環境變數

    我們還可以在 shell 登錄文件(例如 .bashrc)中,預先擴展可能用到的頭文件目錄和庫文件目錄,這樣,每次登錄shell時,將會自動設置他們。

    對於C頭文件路徑,我們有環境變數 C_INCLUDE_PATH ,對於C++頭文件路徑,我們有環境變數 CPP_INCLUDE_PATH 。 例如:

    $ C_INCLUDE_PATH=./
    $ export C_INCLUDE_PATH

    對於靜態庫文件,我們有環境變數 LIBRARY_PATH

    $ LIBRARY_PATH=./
    $ export LIBRARY_PATH

    對於共用庫文件,我們有環境變數 LD_LIBRARY_PATH

    $ LD_LIBRARY_PATH=./
    $ export LD_LIBRARY_PATH

    上述目錄經過環境變數指定後,將在標準預設目錄之前搜索,後續編譯過程也無需在編譯命令中指定了。上面的編譯指令也可以進一步簡化:

    $ gcc -Wall hello_world.c -lhello -o helloworld

    對於多個搜索目錄,我們可以遵循標準Unix搜索路徑的規範,在環境變數中用冒號分割的列表進行表示:

    DIR1:DIR2:DIR3: ...

    DIR 可以用單個點 . 指示當前目錄。舉個例子:

    $ C_INCLUDE_PATH=.:/opt/gdbm-1.8.3/include:/net/include
    $ LIBRARY_PATH=.:/opt/gdbm-1.8.3/lib:/net/lib

    如果環境變數中已經包含路徑信息,則可以用以下語法進行擴展:

    $ C_INCLUDE_PATH= NEWPATH:$C_INCLUDE_PATH
    $ LIBRARY_PATH= NEWPATH:$LIBRARY_PATH
    $ LD_LIBRARY_PATH= NEWPATH:$LD_LIBRARY_PATH
  3. 搜索順序

    方式2和方式3本質上是同一種方法的不同表現方式。當環境變數和命令行選項被同時使用時,編譯器將按照下麵的順序搜索目錄:

    1. 從左到右搜索由命令行 -I-L 指定的目錄;
    2. 由環境變數 C_INCLUDE_PATH LIBRARY_PATH 指定的目錄;
    3. 預設的系統目錄。

2.3 C語言標準

預設情況下, gcc 編譯程式時使用的是GNU C 語法規則,而非 ANSI/ISO C 標準語法規則,GNU CANSI/ISO C 標準語法基礎上增加了一些對C語言的擴展功能,因此標準 C 源碼在 GCC 下一般來說是無需修改即可編譯的。

同時,GCC也提供了對 C 語言標準的控制選項,用以解決不同語法規則之間的衝突問題,最常用的是 -ansi-pedantic-std

-ansi:禁止與ANSI/ISO標準衝突的GNU擴展特性,包括對GNU C標準庫 glibc 的支持;
-pedantic:禁止與ANSI/ISO標準不符的GNU擴展特性,更加嚴格。
-std:

  • -ansi:相容 ANSI/ISO 標準

    一個合法的 ANSI/ISO C 程式,可能無法相容 GNU C 的擴展特性,可以通過 -ansi 選項禁用那些與 ANSI/ISO C 標準衝突的 GNU 擴展,即令 GCC 編譯器以相容 ANSI/ISO C 標準的方式編譯程式。例如:

    #include <stdio.h>
    int main(void)
    {
      const char asm[] = "6502";
      printf("the staring asm is '%s'\n", asm);
      return 0;
    }

    這裡,變數名 asmANSI/ISO 標準中是合法的,但 asmGNU 擴展中是關鍵詞,用於指示 C 函數中混編的彙編指令,直接編譯會出現錯誤:

    $ gcc -Wall ansi.c -o ansi

    使用 -ansi 選項後,即以 ANSI/ISO C 標準編譯,可成功編譯。

    $ gcc -Wall -ansi ansi.c -o ansi

    asm 類似的關鍵詞包括:inlinetypeofunixvax 等等,更多細節參考GCC參考手冊 “Using GCC”。

    -ansi 選項還會同時關閉 GNU C庫,對於 GNU C 庫中特有的變數、巨集定義、函數介面的調用都會出現未定義錯誤, GNU C 庫對外提供了一些功能特性的巨集開關,可以打開部分特性,例如,POSIX擴展(*_POSIX_C_SOURCE),BSD擴展(_BSD_SOURCE),SVID擴展(_SVID_SOURCE),XOPEN擴展(_XOPEN_SOURCE)和GNU擴展(_GNU_SOURCE*)。

    舉個例子,下麵的預定義M_PI 是 GNU C庫 math.h 的一部分,不在 ANSI/ISO C 標準庫中。

    #include <math.h>
    #include <stdio.h>
    int main(void)
    {
      printf("the value of pi is %f\n",M_PI);
      return 0;
    }

    如果強制使用 -ansi 編譯會出現未定義錯誤。

    $ gcc -Wall -ansi pi.c -o pi

    如果一定需要使用GNU C庫巨集定義,可以單獨打開對GNU C庫的擴展。

    $ gcc -Wall -ansi -D_GNU_SOURCE pi.c

    這裡*_GNU_SOURCE* 巨集打開所有的擴展,而 POSIX 擴展在這裡如果與其他擴展有衝突,則優先於其他擴展。有關特征測試巨集進一步信息可以參見 GNU C 庫參考手冊。

  • -pedantic:嚴格的 ANSI/ISO 標準

    同時使用 -ansi -pedantic 選項,編譯器將會以更加嚴格的標準檢查語法規則是否符合 ANSI/ISO C 標準,同時拒絕所有 GNU C 擴展語法規則。

    下麵是一個用到變長數組的程式,變長數組是 GNU C 擴展語法,但也不會妨礙合法的 ANSI/ISO 程式的編譯。

    int main(int argc, char *argv[])
    {
      int i, n = argc;
      double x[n];
      for (i = 0; i < n; i++)
          x[i] = i;
      return 0;
    }

    因此,使用 -ansi 不會出現相關編譯錯誤:

    $ gcc -Wall -ansi gnuarray.c -o gnuarray

    但是,使用 -ansi -pedantic 編譯,會出現違反 ANSI/ISO 標準的警告。

    $ gcc -Wall -ansi -pedantic gnuarray.c -o gnuarray
  • -std:指定標準

    可以通過 -std 選項來控制 GCC 編譯時採用的C語言標準。支持的可選項包括:

    • -std=c89 -std=iso9899:1990
    • -std=iso9899:199409
    • -std=c99 -std=iso9899:1999
    • -std=gnu89
    • -std=gnu99

2.4 編譯警告

  • -Wall

    -Wall 警告選項本身會打開很多常見錯誤的警告,這些錯誤通常總是有問題的代碼構造,或是很容易用明白無誤的方法改寫的錯,因此可以看作潛在嚴重問題的指示。這些錯誤主要包括:

    • -Wcomment:對嵌套註釋進行警告;

      /* commented out
          double x = 1.23; /* x-position*/
      */
    • -Wformat:對格式化字元串與對應函數參數的類型一致性進行警告;
    • -Wunused:對聲明但未使用的變數進行警告;
    • -Wimplicit:對未聲明就被使用的變數進行警告;
    • -Wreturn-type:對函數聲明返回類型與實際返回類型的一致性進行警告;

      int main(void)
      {
          printf("hello, world!\n");
          return;
      }

      -Wall 包含的警告選項都可以在GCC參考手冊 Using GCC 中找到。

  • 其他警告

    GCC提供了很多可選的警告選項,它們沒有包含在 -Wall 中,但仍然很有參考價值。這些警告選項可能會對合法代碼也報警,所以編譯時通常不需要長期開啟,建議周期性的使用,檢查輸出結果,或在某些程式和文件中打開,更加合適。

    • -W:對常見的編程錯誤進行報警,類似 -Wall ,也常和 -Wall 一起用。
    • -Wconversion:對可能引起意外結果的隱式類型轉換進行報警。
    • -Wshadow:對重覆定義同名變數進行報警。
    • -Wcast-qual:對可能引起移除修飾符特性的操作進行報警。
    • -Wwrite-strings:該選項隱含的使得所有字元串常量帶有 const 修飾符。
    • -Wtraditional:對那些在 ANSI/ISO 編譯器下和在 ANSI 之前的“傳統”譯器下編譯方式不同的代碼進行警告。
    • -Werror:將警告轉換為錯誤,一旦警告出現即停止編譯。

警告選項會產生診斷性的信息,但不會終止編譯過程,如果需要出現警告後停止編譯過程可以使用 -Werror


3 預處理選項

3.1 巨集定義

這裡主要介紹GNU C預處理器中巨集定義的常見用法。首先,看一個巨集定義的例子:

#include <stdio.h>
int main(void)
{
    #ifdef TESTNUM
        printf("TestMum is %d\n",TESTNUM);
    #endif
    #ifdef TESTMSG
        printf("TestMsg:%s\n",TESTMSG);
    #endif
    printf("Runing...\n");
    return 0;
}

如果在編譯命令中不加任何巨集定義選項,則編譯器會在預處理階段忽略 TESTNUM 巨集定義包裹的代碼:

$ gcc -Wall dtest.c -o dtest
$ ./dtest
Runing...

如果在編譯中增加 -D 選項,則編譯器會在預處理階段將 TESTNUM 巨集定義包裹的代碼進行編譯:

$ gcc -Wall -DTESTNUM dtest.c -o dtest
$ ./dtest
TestNum is 1
Runing...

如果對巨集定義進行巨集賦值,則編譯器會在預處理階段將賦值內容替換到 TESTNUM 巨集定義位置:

$ gcc -Wall -DTESTNUM=20 dtest.c -o dtest
$ ./dtest
TestNum is 20
Runing...

利用命令行上的雙引號,巨集可以被定義成字元串,字元串可以包含引號,需要用 \ 進行轉義:

$ gcc -Wall -DTESTMSG="\"Hello,World!\"" dtest.c -o dtest
$ ./dtest
Hello,World!
Runing...

上述字元串也可以定義成空值,例如:-DTESTMSG="" ,這樣的巨集還是會被 #ifdef 看作已定義,但該巨集會被展開為空。

3.2 預處理輸出

使用 -E 選項,GCC 可以只允許運行預處理器,並直接顯示預處理器對源代碼的處理結果,並且不會進行後續的編譯處理流程:

$ gcc -DTESTMSG="\"Hello,World!\"" -E dtest.c

預處理器會對巨集文件進行直接替換,並對頭文件進行展開,預處理器還會增加一些以 #line-number "source-file" 形式記錄源文件和行數,便於調試和編譯器輸出診斷信息。

被預處理的系統頭文件通常產生許多輸出,它們可以被重定向到文件中,或者使用 -save-temps 選項進行保存:

$ gcc -c -save-temps dtest.c

運行該命令之後,預處理過的輸出文件將被存儲到 .i 文件中,同時還會保存 .s 彙編文件和 .o 對象文件。

3.3 調試信息

通常,編譯器輸出的可執行文件只是一份作為機器碼的指令序列,而不包含源程式中的任何引用信息,例如變數名或者行號等,因此如果程式出現問題,我們將無法確定問題在哪裡。

  • 添加調試信息

    GCC 提供 -g 選項,可以在編譯生成可執行文件時添加另外的調試信息,這些信息可以在追蹤錯誤時從特定的機器碼指令對應到源代碼文件中的行,調試器可以在程式運行時檢查變數的值。

    $ gcc -Wall -g helloworld.c -o helloworld
    
  • 檢查core文件

    程式異常退出時,操作系統將程式崩潰瞬間的記憶體狀態寫入到 core 文件,結合 -g 選項生成的符號表中的信息,可以進一步確定程式崩潰時運行到的位置和此刻變數的值。

    但是,通常情況下操作系統配置在預設情況是下不寫 core文件 的,在開始之前,我們可以先查詢 core文件 的最大限定值:

    $ ulimit -c

    如果結果為0,則不會生成 core文件 ,我們可以擴大 core文件 上限,以便允許任何大小的 core 文件

    $ ulimit -c unlimited

    這裡,再準備一個包含非法記憶體錯誤的簡單程式,我們用它來生成 core 文件:

    int a(int *p)
    {
     int y = *p;
     return y;
    }
    int main(void)
    {
     int *p = 0;
     return a(p);
    }

    編譯生成帶調試信息的可執行文件:

    $ gcc -g null.c
    $ ./a.out
    Segmentation fault (core dumped)

    根據可執行文件和 core文件 即可利用 gdb 進行調試,定位錯誤位置:

    $ gdb a.out core
  • 回溯堆棧

    利用 gdbbacktrace 命令可以方便的顯示當前執行點的函數調用及其參數,並且利用 up down 命令在堆棧的不同層級之間移動,檢查變數變化。

    gdb 相關操作可以參考 “Debugging with GDB: The GNU Source-Level Debugger”


4 優化選項

編譯器的優化目標通常是 提高代碼的執行速度 或者 減少代碼體積

4.1 源碼級優化

  • 公共子表達式消除

    在優化功能打開之後,編譯器會自動對源代碼進行分析,使用臨時變數對多次重用的計算結果進行替代,減少重覆計算。例如:

    x = cos(v)*(l+sin(u/2)) + sin(w)*(l-sin(u/2))

    可以用臨時變數 t 替換 sin(u/2)

    t=sin(u/2)
    x = cos(v)*(l+t) + sin(w)*(l-t)
  • 函數內嵌

    函數調用過程中,需要花費一定的額外時間來實施調用過程(壓棧、跳轉、返回執行點等),而函數內嵌優化會將計算過程簡單但是調用頻繁的函數調用直接用函數體進行替換,提升那些被頻繁調用函數的執行效率。例如:

    double sq(double x)
    {
        return x * x;
    }
    int main(void)
    {
        double sum;
        for (int i = 0; i < 1000000; i++)
        {
            sum += sq(i + 5);
        }
    }

    經過嵌入優化後,大致會得到:

    int main(void)
    {
        double sum;
        for (int i = 0; i < 1000000; i++)
        {
            double t = (i + 5);
            sum += t * t;
        }
    }

    GCC 會使用一些啟髮式的方法選擇哪些函數要內嵌,比如函數要適當小。另外,嵌入優化方式只在單個對象文件基礎上實施。關鍵字 inline 可以顯示要求某個指定函數在用到的地方儘量內嵌。

4.2 速度-空間優化

編譯器會根據指定的優化條件,對可執行文件的執行速度和空間進行折中優化,使得最終結果可能會犧牲一些執行速度來節省文件大小,也可能會犧牲文件的空間占用來提升運行速度,或是在兩者之間取得一定平衡。

迴圈展開 即是一種以常見的空間換時間的優化方式,例如:

for(i = 0; i < 4; i++)
{
    y[i] = i;
}

直接將該迴圈展開後進行直接賦值,可以有效減少迴圈條件的判斷,減少運行時間:

y[0] = 0;
y[1] = 1;
y[2] = 2;
y[3] = 3;

對於支持並行處理的處理器,經過優化後的代碼可以使用並行運行,提高速度。對於未知邊界的迴圈,例如:

for(i = 0; i < n; i++)
{
    y[i] = i;
}

可能會被編譯器優化成這樣:

for(i = 0; i < (n % 2); i++)
{
    y[i] = i;
}
for(; i + 1 < n; i += 2)
{
   y[i] = i;
   y[i+1] = i+1;
}

上面第二個迴圈中的操作即可進行並行化處理。

4.3 指令調度優化

指令化調度是最底層的優化手段,由編譯器根據處理器特性決定各指令的最佳執行次序,以獲取最大的並行執行,指令調度沒有增加可執行文件大小,但改善了運行速度,對應的代價主要體現在編譯過程所需處理時間,以及編譯器占用的記憶體空間。

4.4 優化級別選項

GCC 編譯器為了生成滿足速度和空間要求的可執行文件,對優化級別使用 -O 選項進行定義。

  • -O0 或不指定 -O 選項(預設)
    不實施任何優化,源碼被儘量直接轉換到對應指令,編譯時間最少,適合調試使用。

  • -O1-O
    打開那些不需要任何速度-空間折衷的最常見形式的優化,對代碼大小和執行時間進行優化,生成的可執行文件更小、更快,編譯時間較少。

  • -O2
    在上一級優化的基礎上,增加指令調度優化,文件大小不會增加,編譯時間有所增加,它是各種 GNU 軟體發行包的預設優化級別。

  • -O3
    在上一級優化的基礎上,增加函數內嵌等深度優化,提升可執行文件的速度,但會增加它的大小,這一等級的優化可能會產生不利結果。

  • -Os
    該選項主要針對記憶體和磁碟空間受限的系統生成儘可能小的可執行文件。

  • -funroll-loops
    該選項獨立於上述優化選項,可以打開迴圈展開,增加可執行文件大小。

通常,開發調試過程可以使用 -O0 ,開發部署時可以用 -O2 ,優化等級也不是越多越好、越高越好,需要儘量根據程式差異和使用平臺的差異經過測試數據確定。

4.5 優化和編譯警告

開啟優化後,作為優化過程的一部分,編譯器檢查所有變數的使用和他們的初始值,稱為 數據流分析 。數據流分析的一個作用是檢查是否使用了未初始化的變數,在開啟優化之後,-Wall 中的 -Wuninitialized 選項會對未初始化變數的讀操作產生警告。因此,開啟優化後,GCC 會輸出一些額外的警告信息,而這些信息在不開啟優化時是不會產生的。


5 編譯過程

這一部分主要介紹 GCC 怎麼把源文件轉變成可執行文件。編譯過程是一個多階段的過程,涉及到多個工具,包括 GNU 編譯器(gcc 或 g++ 前端),GNU彙編器 as ,GNU 鏈接器 ld ,編譯過程中用到的整套工具被稱為工具鏈。

5.1 預處理過程

預處理過程是利用預處理器 cpp 來擴展巨集定義和頭文件,GCC 執行下麵的命令來實施這個步驟:

$ cpp hello.c > hello.i

該命令可以輸出經過預處理器處理輸出的源文件 hello.i

5.2 編譯過程

編譯過程是編譯器把預處理的源代碼經過翻譯處理成特定處理器的彙編語言,命令行 -S 選項可以將預處理過的 .i 源文件轉變成 .s 彙編文件。

$ gcc -Wall -S hello.i

該命令可以輸出經過編譯器處理輸出的彙編文件 hello.s

5.3 彙編過程

彙編過程是彙編器 as 把編譯處理的彙編文件轉變成機器碼,並生成對象文件,如果彙編文件中包含外部函數的調用,彙編器會保留外部函數的地址處於未定義狀態,留給後面的鏈接器填寫。

$ as hello.s -o hello.o

這裡, -o 選項用來指定輸出 .o 文件。

5.4 鏈接過程

鏈接過程是鏈接器 ld 將各對象文件鏈接到一起,生成可執行文件。在鏈接過程中,鏈接器會將彙編輸出的 .o 文件和系統中的 C 運行庫中必要的外部函數鏈接到一起。

$ gcc hello.o

鏈接器主要調用 ld 命令,也可以直接把對象文件與C標準庫鏈接,生成可執行文件。


6 編譯工具

6.1 歸檔工具 ar

GNU 歸檔工具 ar 用於把多個對象文件組合成歸檔文件,也被稱為庫,歸檔文件是多個對象文件打包在一起發行的簡便方法。

在上面的多個源文件例子中,假設有 hello.c world.c hello_world.c 三個程式, 我們可以現將三者編譯成對象文件:

$ gcc -Wall -c hello.c
$ gcc -Wall -c world.c
$ gcc -Wall -c hello_world.c

生成 hello.o world.o hello_world.o ,我們將兩個子函數打包成靜態文件庫:

$ ar cr libhello.a hello.o world.o

選項 cr 不需要 - ,代表 creat and replacelibhello.a 為目標文件,hello.o world.o 表示輸入文件。

也可以通過 t 選項,查看庫文件中包含的文件:

$ ar t libhello.a
hello.o
world.o

再利用 libhello.ahello_world.o 來鏈接生成可執行文件:

$ gcc -Wall hello_world.o libhello.a -o hello
$ ./hello
Hello, world

或者使用 -l 選項:

$ gcc -Wall -L. hello_world.o -lhello -o hello
$ ./hello
Hello, world

6.2 性能剖析器 gprof

GNU 性能剖析器 gprof 是衡量程式性能的有用工具,它可以記錄每個函數調用的次數和每個函數每次調用所花的時間。

這裡準備了一個數學上的 Collatz 猜想程式,我們用 gprof

來對其進行分析:

#include <stdio.h>
unsigned int step(unsigned int x)
{
    if(x % 2 == 0)
    {
        return (x / 2);
    }
    else
    {
        return (3 * x + 1);
    }
}
unsigned int nseq(unsigned int x0)
{
    unsigned int i = 1, x;
    if(x0 == 1 || x0 == 0)
        return i;
    x = step(x0);
    while(x != 1 && x != 0)
    {
        x = step(x);
        i++;
    }
    return i;
}
int main(void)
{
    unsigned int i, m = 0, im = 0;
    for(i = 1; i < 500000; i++)
    {
        unsigned int k = nseq(i);
        if(k > m)
        {
            m = k;
            im = i;
            printf("sequence length = %u for %u\n", m, im);
        }
    }
    return 0;
}

為了剖析性能,程式在編譯時需要用到 -pg 選項參與編譯鏈接:

$ gcc -Wall -c -pg collatz.c
$ gcc -Wall -pg collatz.o

這樣即可生成可分析的可執行文件,其包含有記錄每個函數所花時間的額外指令。

為了進行分析,需要先正常運行一次可執行文件:

$ ./a.out

運行結束後,會在本目錄下生成一個 gmon.out 文件。再以可執行文件名作為參數運行 gprof 就可以分析這些數據:

% cumulative self self total
time seconds seconds calls ns/call ns/call name
50.00 0.13 0.13 499999 260.00 500.00 nseq
46.15 0.25 0.12 62135400 1.93 1.93 step
3.85 0.26 0.01 frame_dummy

剖析數據的第一列顯示的是該程式的所有子函數的運行時間。

6.3 代碼覆蓋測試工具 gcov

GNU 代碼覆蓋測試工具 gcov 可以用於分析程式運行期間每一行代碼執行的次數,因此可以用於查找沒有用到的代碼區域。

我們準備下麵這個小程式來展示 gcov 的功能。

#include <stdio.h>
int main(void)
{
    int i;
    for(i = 1; i < 10; i++)
    {
        if(i % 3 == 0)
            printf("%d is divisible by 3\n",i);
        if(i % 11 == 0)
            printf("%d is divisible by 11\n",i);
    }
    return 0;
}

為了對該程式進行代碼覆蓋測試,編譯時必須攜帶 –fprofile-arcs–ftest-coverage 選項:

$ gcc -Wall -fprofile-arcs -ftest-coverage cov.c

其中,–fprofile-arcs 用於添加計數被執行到的行的次數,而 –ftest-coverage 被用與合併程式中每條分支中的用於計數的代碼。可執行程式只有在運行後才能生成代碼覆蓋測試數據:

$ ./a.out

.c 源文件為參數調用 gov 命令,命令會生成一個原始源碼文件的帶註釋信息的版本,其尾碼名為 gcov,包含執行到的每一行代碼的運行次數,沒有執行到的行數被用 ###### 標記上,根據註釋信息就可以看到該源文件的覆蓋情況。


7 文件信息

7.1 辨識文件

對於一個可執行命令執行 file 命令可以查看該文件的編譯環境信息。

$ file a.out
a.out: ELF 32-bit LSB executable, Intel 80386, version 1 (SYSV), dynamically linked, interpreter /lib/ld-linux.so.2, for GNU/Linux 2.6.32, BuildID[sha1]=121d574fcb968c6a83624f4d982eb74495951841, not stripped

下麵是輸出信息的解釋:

  • ELF :可執行文件的內部格式,ELF表示 Executable and Linking Format ,另外的格式還有 COFF(Common object File Format)
  • 32-bit :表示字的位寬,另外的位寬還有64-bit
  • LSB :表示該文件的大小端方式。
  • Intel 80386 :表示該文件適用的處理器。
  • version 1 (SYSV) :表示文件內部格式的版本
  • dynamically linked :表示文件會用到的共用庫,另外的還有 statically linked 表示程式是靜態鏈接的,比如用到 -static 選項 。
  • not stripped :表示可執行文件包含符號表。

7.2 符號映射表

符號映射表存儲了函數和命令變數的位置,用 nm 命令可以看到內容:

$ nm a.out
0804a01c B __bss_start
0804a01c b completed.7200
0804a014 D __data_start
0804a014 W data_start
……
0804840b T main
         U puts@@GLIBC_2.0
08048380 t register_tm_clones
08048310 T _start
0804a01c D __TMC_END__
08048340 T __x86.get_pc_thunk.bx

其中,T 表示這是定義在對象文件中的函數,U 表示這是本對象文件中沒有定義的函數(在其他對象文件中找到了)。

nm 命令最常用的用法是通過查找 T 項對應的函數名,檢查某個庫是否包含特定函數的定義。

7.3 動態鏈接庫

當程式用到 .so 動態鏈接庫時,需要在運行期間動態載入這些庫。 ldd 命令可以列出所有可執行文件依賴的共用庫文件。

$ ldd a.out
    linux-gate.so.1 =>  (0xb7749000)
    libc.so.6 => /lib/i386-linux-gnu/libc.so.6 (0xb7578000)
    /lib/ld-linux.so.2 (0x80017000)

ldd 命令也能夠用於檢查共用庫本身,可以跟蹤共用庫依賴鏈。


參考資料


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • Equals: 下麵的語句中,x、y 和 z 表示不為 null 的對象引用。* 除涉及浮點型的情況外,x.Equals(x) 都返回 true。 * x.Equals(y) 返回與 y.Equals(x) 相同的值。 * 如果 x 和 y 都為 NaN,則 x.Equals(y) 返回 true。 ...
  • C 基礎委托回顧 前言 快忘記了。 委托的特點 委托類似於 C++ 函數指針,但它們是類型安全的。 委托允許將方法作為參數進行傳遞。 委托可用於定義回調方法。 委托可以鏈接在一起;例如,可以對一個事件調用多個方法。 方法不必與委托簽名完全匹配。 委托是事件的基礎。 "官網介紹" 用法 1. dele ...
  • Visual Studio Code 是由微軟開發的一款免費、跨平臺的文本編輯器。由於其卓越的性能和豐富的功能,它很快就受到了大家的喜愛。 就像大多數 IDE 一樣,VSCode 也有一個擴展和主題市場,包含了數以千計質量不同的插件。為了幫助大家挑選出值得下載的插件,我們針對性的收集了一些實用、有趣 ...
  • 本文介紹從DDD(Domain-Driven Design[領域驅動設計])的角度來說說為什麼要使用Entity Framework(以下都會簡稱為EF),同時也看出類似Drapper之類的簡陋ORM不足的地方。 ...
  • 主要是通過一個WindowManager管理類,在window後臺代碼中通過WindowManager註冊需要彈出的窗體類型,在ViewModel通過WindowManager的Show方法,顯示出來。 WindowManager代碼如下: 做一個擴展方法,將子窗體註冊方法擴展到Window類型的對 ...
  • . 創建.net core web api 1.1 選擇一個empty 模式,裡面只有簡單的2個class 1.2 配置web api 的路由. 1.2.1 打開Startup.cs,首先引用config(Microsoft.Extensions.Configuration),創建一個構造函數,註入 ...
  • 在ASP.Net Core中,如果直接在Middleware中獲取RouteData返回的是空值,這是因為RouterMiddleware還沒執行。但有些情況下需要獲取RouteData,這要怎麼做呢? ...
  • 一、Webservice開發 1、在解決方案右鍵添加新建項目,新建空的web應用程式 2、在新建的項目右鍵添加新建項選擇web服務 3、這裡就是webservice 里的方法,可以添加自己需要的方法(方法前需要添加[WebMethod] 的特性,才可以被調用) 4、發佈iis即可訪問 5、需要身份驗 ...
一周排行
    -Advertisement-
    Play Games
  • 示例項目結構 在 Visual Studio 中創建一個 WinForms 應用程式後,項目結構如下所示: MyWinFormsApp/ │ ├───Properties/ │ └───Settings.settings │ ├───bin/ │ ├───Debug/ │ └───Release/ ...
  • [STAThread] 特性用於需要與 COM 組件交互的應用程式,尤其是依賴單線程模型(如 Windows Forms 應用程式)的組件。在 STA 模式下,線程擁有自己的消息迴圈,這對於處理用戶界面和某些 COM 組件是必要的。 [STAThread] static void Main(stri ...
  • 在WinForm中使用全局異常捕獲處理 在WinForm應用程式中,全局異常捕獲是確保程式穩定性的關鍵。通過在Program類的Main方法中設置全局異常處理,可以有效地捕獲並處理未預見的異常,從而避免程式崩潰。 註冊全局異常事件 [STAThread] static void Main() { / ...
  • 前言 給大家推薦一款開源的 Winform 控制項庫,可以幫助我們開發更加美觀、漂亮的 WinForm 界面。 項目介紹 SunnyUI.NET 是一個基於 .NET Framework 4.0+、.NET 6、.NET 7 和 .NET 8 的 WinForm 開源控制項庫,同時也提供了工具類庫、擴展 ...
  • 說明 該文章是屬於OverallAuth2.0系列文章,每周更新一篇該系列文章(從0到1完成系統開發)。 該系統文章,我會儘量說的非常詳細,做到不管新手、老手都能看懂。 說明:OverallAuth2.0 是一個簡單、易懂、功能強大的許可權+可視化流程管理系統。 有興趣的朋友,請關註我吧(*^▽^*) ...
  • 一、下載安裝 1.下載git 必須先下載並安裝git,再TortoiseGit下載安裝 git安裝參考教程:https://blog.csdn.net/mukes/article/details/115693833 2.TortoiseGit下載與安裝 TortoiseGit,Git客戶端,32/6 ...
  • 前言 在項目開發過程中,理解數據結構和演算法如同掌握蓋房子的秘訣。演算法不僅能幫助我們編寫高效、優質的代碼,還能解決項目中遇到的各種難題。 給大家推薦一個支持C#的開源免費、新手友好的數據結構與演算法入門教程:Hello演算法。 項目介紹 《Hello Algo》是一本開源免費、新手友好的數據結構與演算法入門 ...
  • 1.生成單個Proto.bat內容 @rem Copyright 2016, Google Inc. @rem All rights reserved. @rem @rem Redistribution and use in source and binary forms, with or with ...
  • 一:背景 1. 講故事 前段時間有位朋友找到我,說他的窗體程式在客戶這邊出現了卡死,讓我幫忙看下怎麼回事?dump也生成了,既然有dump了那就上 windbg 分析吧。 二:WinDbg 分析 1. 為什麼會卡死 窗體程式的卡死,入口門檻很低,後續往下分析就不一定了,不管怎麼說先用 !clrsta ...
  • 前言 人工智慧時代,人臉識別技術已成為安全驗證、身份識別和用戶交互的關鍵工具。 給大家推薦一款.NET 開源提供了強大的人臉識別 API,工具不僅易於集成,還具備高效處理能力。 本文將介紹一款如何利用這些API,為我們的項目添加智能識別的亮點。 項目介紹 GitHub 上擁有 1.2k 星標的 C# ...