GCC編譯器基礎入門

導語

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前端

編譯C程序

單個源文件

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

編譯源代碼:linux

$ gcc helloworld.c

未指定編譯輸出文件名則默認輸出 a.outc++

$ gcc -Wall helloworld.c -o helloworld

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

$ ./helloworld
Hello, world!

多個源文件

若是將源碼分爲多個源文件,咱們也可使用如下命令進行編譯:編程

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

對於包含多個源文件的工程,咱們能夠將編譯過程分爲兩個階段:小程序

第一階段:全部 .c .h 源文件通過編譯,分別生成對應的 .o 對象文件;
第二階段:全部 .o 對象文件通過連接生成最終的可執行文件。數組

.o 對象文件包含機器碼,任何對其餘文件中的函數或變量的內存地址的引用都留着沒有被解析,這些都留給連接器 GNU ld 生成可執行文件時再處理。bash

  • 源文件生成對象文件編程語言

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

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

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

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

    注意:這裏不須要使用者-Wall ,由於源文件已經成功編譯成對象文件了,連接是一個要麼成功要麼失敗的過程。

一般,連接快於編譯,所以,對於大型項目,將不一樣功能在不一樣的源文件中進行實現,在修改功能時,能夠只編譯被修改的文件,顯著節省時間。

靜態庫文件

編譯生成的 .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種類型:靜態庫和共享庫。

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

相比於靜態庫,共享庫具有以下優勢:

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

搜索路徑

在編譯過程當中,若是找不到頭文件會報錯,默認狀況下,GCC會在下面的目錄中搜索頭文件,這些路徑稱爲 include路徑

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

同理,在連接過程當中,若是找不到庫文件也會報錯,默認狀況下,GCC在下面的目錄中搜索庫文件,這些路徑稱爲 連接路徑

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

若是須要檢索其餘路徑下的頭文件或者庫文件,能夠經過 -I-L 的方式來分別擴展頭文件和庫文件的搜索路徑,這裏介紹2種搜索路徑的設置方法:

-命令行選項

$ 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 選項進行限制。

-環境變量

咱們還能夠在 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

-搜索順序

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

  1. 從左到右搜索由命令行 -I-L 指定的目錄;
  2. 由環境變量 C_INCLUDE_PATH LIBRARY_PATH 指定的目錄;
  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

編譯警告

  • -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

預處理選項

宏定義

這裏主要介紹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 看做已定義,但該宏會被展開爲空。

預處理輸出

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

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

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

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

$ gcc -c -save-temps dtest.c

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

調試信息

一般,編譯器輸出的可執行文件只是一份做爲機器碼的指令序列,而不包含源程序中的任何引用信息,例如變量名或者行號等,所以若是程序出現問題,咱們將沒法肯定問題在哪裏。

  • 添加調試信息

    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」

優化選項

編譯器的優化目標一般是 提升代碼的執行速度 或者 減小代碼體積

源碼級優化

  • 公共子表達式消除

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

    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 能夠顯示要求某個指定函數在用到的地方儘可能內嵌。

速度-空間優化

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

循環展開 便是一種以常見的空間換時間的優化方式,例如:

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;
}

上面第二個循環中的操做便可進行並行化處理。

指令調度優化

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

優化級別選項

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

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

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

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

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

  • -Os
    該選項主要針對內存和磁盤空間受限的系統生成儘量小的可執行文件。

  • -funroll-loops
    該選項獨立於上述優化選項,能夠打開循環展開,增長可執行文件大小。

一般,開發調試過程可使用 -O0 ,開發部署時能夠用 -O2 ,優化等級也不是越多越好、越高越好,須要儘可能根據程序差別和使用平臺的差別通過測試數據肯定。

優化和編譯警告

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

編譯過程

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

預處理過程

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

$ cpp hello.c > hello.i

該命令能夠輸出通過預處理器處理輸出的源文件 hello.i

編譯過程

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

$ gcc -Wall -S hello.i

該命令能夠輸出通過編譯器處理輸出的彙編文件 hello.s

彙編過程

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

$ as hello.s -o hello.o

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

連接過程

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

$ gcc hello.o

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

編譯工具

歸檔工具 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

性能剖析器 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

剖析數據的第一列顯示的是該程序的全部子函數的運行時間。

代碼覆蓋測試工具 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,包含執行到的每一行代碼的運行次數,沒有執行到的行數被用 ###### 標記上,根據註釋信息就能夠看到該源文件的覆蓋狀況。

文件信息

辨識文件

對於一個可執行命令執行 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 :表示可執行文件包含符號表。

符號映射表

符號映射表存儲了函數和命令變量的位置,用 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 項對應的函數名,檢查某個庫是否包含特定函數的定義。

動態連接庫

當程序用到 .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 命令也可以用於檢查共享庫自己,能夠跟蹤共享庫依賴鏈。

參考資料

相關文章
相關標籤/搜索