天天看點

GNU編譯器gcc/g++1 簡介2 編譯過程3 所支援檔案類型4 編譯選項4 gcc 庫選項5 常用警告選項6 優化選項7 調試選項

  版權聲明:本文章參考《Linux man pages》做了修改,增添了一部分内容。未經作者允許,嚴禁用于商業出版,否則追究法律責任。網絡轉載請注明出處,這是對原創者的起碼的尊重!!!

1 簡介

  • 對于 C 檔案,可以采用 gcc 或 g++編譯
  • 對于C++檔案,應該采用 g++進行編譯。 g++是一個調用GCC并自動指定與C ++庫連結的程式。除非使用-x選項,否則它會将.c,.h和.i檔案視為C ++檔案而不是C檔案。

2 編譯過程

  • 編譯過程:預編譯—>編譯–>彙編–>連結
  • 所用工具:預處理器—>編譯器 —>彙編器 as—>連接配接器
  • 一個編譯過程包括下面幾個階段:
    • 預處理:預處理器将對源檔案中的宏和頭檔案進行展開。
    • 編譯:檢查代碼規範性、文法錯誤等,在檢查無誤後把代碼翻譯成彙編語言
    • 彙編:将彙編檔案編譯成機器碼。
    • 連結:将目标檔案和外部符号進行連接配接,得到一個可執行二進制檔案。
下面以一個很簡單的 test.c 來探讨這個過程。
#include <stdio.h>


#defineNUMBER (1+2)

int main()
{
  int x =NUMBER;
  return ;
}
           
  • 預處理:

    gcc –E test.c -o test.i

    ,我們用

    cat

    檢視

    test.i

    的内容如下:
    ...頭檔案展開部分省略...
    int main() 
    {
       int x=(+); 
       return ;
    }
               
    可以看到,檔案中宏定義

    NUMBER

    出現的位置被(1+2)替換掉了,頭檔案也被展開,其它的内容保持不變。宏與

    typedef

    差別就在于typedef隻是為對象去取一個别名,使用别名和原名效果一樣,但不會替換。
  • 編譯:

    gcc –S test.i –o test.s

    ,通過

    cat test.s

    檢視

    test.s

    的内容為彙編代碼。
  • 彙編:

    as test.s -o test.o

    , 利用as将彙編檔案編譯成機器碼。得到輸出檔案為 test.o。test.o 中為目标機器上的二進制檔案. 用

    nm

    檢視檔案中的符号:

    nm test.o

    輸出如下:

    00000000 T main

    。有的編譯器上會顯示:

    00000000 b .bss 00000000 d.data 00000000 t .text U___main U__alloca 00000000 T _main

    既然已經是二進制目标檔案了,能不能執行呢?試一下

    ./test.o

    ,提示

    can not execute binary file

    。原來

    ___main

    前面的

    U

    表示這個符号的位址還沒有定下來,

    T

    表示這個符号屬于代碼。
  • 連結:

    gcc test.o –o test.exe

    ,将所有的.o 檔案連結起來生産可執行程式。

3 所支援檔案類型

字尾 含義 字尾 含義
.c C源檔案 .i 經過預處理的C檔案
.cc/.cp/.cxx/.cpp/.CPP/.c++/.C C++源檔案 .ii 經過預處理的C++檔案
.m obje-C源檔案 .mi 經過預處理的obj-C檔案
.mm/.M obj-C++源檔案 .mii 經過預處理的obj-C++檔案
.F/.FOR/.fpp/.FPP/.FTN 固定的Fortran源檔案 .f/.for/.ftn 固定的經過預處理的Fortran檔案
.F90/.F95/.F03/.F08 自由的Fortran源檔案 .f90/.f95/.f03/.f08 自由的經過預處理的Fortran檔案
.go Go 源檔案
.ads Ada的規範源檔案 .adb Ada的主體源檔案。
.S/.sx 彙編源檔案 .s 經過預處理的彙編檔案
.o 未連結的二進制檔案 .exe/ 經過連結的二進制檔案
.hh/.H/.hp/.hxx/.hpp/.HPP/.h++/.tcc C++頭檔案 .h C/C++/Obj-C/Obj-C++頭檔案

4 編譯選項

選項 含義 輸入 輸出
-E 隻進行預處理,預設輸出到螢幕 源檔案 經過預處理的檔案
-S 隻編譯不彙編,預設輸出同名.s檔案 源檔案和經過預處理的檔案 經過預處理的彙編檔案
-c 隻編譯彙編不連結,預設輸出同名.o檔案 源檔案和經過預處理的檔案、彙編檔案 未連結的二進制檔案
選項 含義
@file 編譯選項由檔案file讀入
-o file 指定輸出檔案
-v 列印出編譯器内部編譯各過程的指令行資訊和編譯器的版本
-I dir 在頭檔案的搜尋清單中添加dir 目錄
-D macro 傳入宏定義
-x language 指定源檔案語言,如c,c++
-std=standard 指定标準,如c99,c++11
-M 生成依賴關系,包括庫檔案
-MM 生成依賴關系,不包括庫檔案
#示例:四種編譯過程

#include <stdio.h>
#define MAX100
#define max(a,b) ((a)>(b)?(a):(b)) //宏定義,執行-E 之後被替換
int main()
{
        printf("MAX=%d\n",MAX);
        printf("max(3,4)=%d\n",max(,));
        return ;
}

//法一:(了解原理用,四步:.c-->.i-->.s->.o->.exe)
gcc –E project1.c –o project1.i  //預編譯,生成已預處理過的 *.i檔案
gcc –S project1.i–o project1.s   //編譯,生成*.s檔案
as project1.s –o project1.o       //彙編,生成未連結的.o二進制檔案
gcc project1.o –o project1.exe    //連結,生成可執行程式

//法二:(了解原理用,三步: .c-->.i-->.s-->.exe)
gcc –E project1.c –o project1.i  //預編譯,生成已已預處理過的 *.i檔案
gcc –S project1.i–o project1.s   //編譯,生成彙編語言原始程式*.s
gcc project1.s  –o project1.exe  //彙編并連結,生成可執行程式

//法三:(常用,兩步.c-->.o -->.exe)
gcc –c project1.c –o project1.o   //預處理,編譯,彙編
gcc project1.o –o project1.exe    //連結

//法四:(常用,一步:.c-->.exe)
gcc project1.c –o project1.exe     //編譯并連結

//法五:(常用,一步:.c-->.exe)
gcc project1.c                     //預設生成a.out可執行檔案
           
//示列: gcc傳遞宏定義選項 –D 的作用,就是定義宏的另一種方式
#include <stdio.h>
main()
{
#ifdef cjy   //表示如果定義了 cjy,即指令行參數傳了cjy,就執行下面的輸出
    printf("cjy is defined!\n");
#else
    printf("cjy is not defined!\n");
#endif
    printf("main exit\n");
}

gcc –E project2.c –o project2.i –D cjy //條件編譯,用-D 傳遞,如果沒有傳cjy則執行#else
gcc–S project2.i –o project2.s
gcc–o project2.exe project2.s

或:

gcc project2.c –o project2  –D cjy//條件編譯,用-D 傳遞,如果沒有傳cjy則執行#else
           

4 gcc 庫選項

選項 含義
-static 連結靜态庫,禁止使用動态庫.用于最後連結階段
-shared 1.可以生成動态庫檔案 2.進行動态編譯,盡可能地連結動态庫,隻有沒有動态庫時才會連接配接同名靜态庫(預設選項,可省略)
-L dir 在庫檔案的搜尋路徑清單中添加dir目錄
-lname 連結稱為libname.a(靜态庫)或者libname.so(動态庫)的庫檔案若兩個庫都存在則根據編譯方式時-static還是-shared來進行連結
-fPIC或-fpic 生成使用相對位址的與位置無關的目标代碼。通常使用gcc的-shared選項從該PIC目标檔案生成動态庫檔案

4.1 -fPIC 功能

  -fPIC 作用于編譯階段,告訴編譯器産生與位置無關代碼(Position-Independent Code),則産生的代碼中,沒有絕對位址,全部使用相對位址,故而代碼可以被加載器加載到記憶體的任意位置,都可以正确的執行。這正是共享庫所要求的,共享庫被加載時,在記憶體的位置不是固定的。

  

gcc -shared -fPIC -o 1.so 1.c

。這裡有一個-fPIC參數,PIC就是position independent code。PIC使.so檔案的代碼段變為真正意義上的共享,如果不加-fPIC,則加載.so檔案的代碼段時,代碼段引用的資料對象需要重定位, 重定位會修改代碼段的内容,這就造成每個使用這個.so檔案代碼段的程序在核心裡都會生成這個.so檔案代碼段的副本.每個副本都不一樣,取決于這個.so檔案代碼段和資料段記憶體映射的位置.不加fPIC編譯出來的so,是要再加載時根據加載到的位置再次重定位的.(因為它裡面的代碼并不是位置無關代碼),如果被多個應用程式共同使用,那麼它們必須每個程式維護一份so的代碼副本了.(因為so被每個程式加載的位置都不同,顯然這些重定位後的代碼也不同,當然不能共享),我們總是用fPIC來生成動态庫檔案,從來不用fPIC來生成靜态庫檔案.fPIC與動态連結可以說基本沒有關系,libc.so一樣可以不用fPIC編譯,隻是這樣的so必須要在加載到使用者程式的位址空間時重定向所有表目.是以,不用fPIC編譯so并不總是不好.如果你滿足以下4個需求/條件:

  (1)該庫可能需要經常更新

  (2)該庫需要非常高的效率(尤其是有很多全局量的使用時)

  (3)該庫并不很大.

  (4)該庫基本不需要被多個應用程式共享

就可以不适用-fpic來編譯so檔案。如果用沒有加這個參數的編譯後的共享庫,也可以使用的話,可能是兩個原因:

  (1)gcc預設開啟-fPIC選項

  (2)loader使你的代碼位置無關

從GCC來看,shared應該是包含fPIC選項的,但似乎不是是以系統都支援,是以最好顯式加上fPIC選項。

4.2 靜态庫和動态庫

  靜态庫是目标檔案.a的歸檔檔案(格式為libname.a)。如果在編譯某個程式時連結靜态庫,則連結器将會搜尋靜态庫并直接拷貝到該程式的可執行二進制檔案到目前檔案中;

  動态庫(格式為libname.so[.主版本号.次版本号.發行号])在程式編譯時并不會被連結到目标代碼中,而是在程式運作時才被加載器載入。跳轉到動态庫所在的位址。

4.2.1 建立靜态庫

$ gcc -c add.c //編譯 add.c 源檔案預設生成 add.o 目标檔案
$ ar rcsv libadd.a add.o //對目标檔案*.o 進行歸檔,生成 lib*.a,此處 lib 要寫
$ gcc -o main main.c –L ./ –ladd –I ./ //不要忘記-L 後面的那個. (即在庫檔案的搜尋   路徑中添加目前路徑, -ladd 表示連結庫檔案libadd.a/.so, -I./表示包含在目前目錄中的頭檔案)
$./main  //因為是靜态編譯,生成的執行檔案可以獨立于.a檔案運作。
           

  指令:ar rcsv libxxx.a xx1.o

  描述:建立、删除、管理靜态庫檔案。

常用參數 意義
r 在庫中插入子產品(替換)。當插入的子產品名已經在庫中存在,則替換同名的子產品。如果若幹子產品中有一個子產品在庫中不存在,ar顯示一個錯誤消息,并不替換其他同名子產品。預設的情況下,新的成員增加在庫的結尾處,可以使用其他任選項來改變增加的位置。
c 建立一個庫。不管庫是否存在,都将建立。
s 建立目标檔案索引,這在建立較大的庫時能加快時間。(補充:如果不需要建立索引,可改成大寫S參數;如果.a檔案缺少索引,可以使用ranlib指令添加)
v 顯示生成結果
t 顯示庫目錄
    

  指令:nm -s libxxx.a

  描述:顯示庫檔案中的索引表。

  指令:ranlib libxxx.a

  描述:為庫檔案建立索引表。

4.2.2 建立動态庫

分三步:
$ gcc -fPIC -Wall -c add.c      //編譯 add.c 源檔案生成 add.o 目标檔案
$ gcc -shared -o libadd.so add.o //對目标檔案*.o 進行歸檔,生成 lib*.so,此lib 要寫
$ gcc -o main main.c –L . –ladd –I . //.和./等效

分兩步:
gcc -fPIC  -Wall  -shared  add.c  -o  libadd.so //生成動态庫檔案
gcc -o main main.c –L ./ –ladd                  //編譯時連結動态庫
           

  建立動态連結庫之後,以後就可以使用該動态連結庫了。例如在 test.c 裡面調用了原來庫中的函數,則編譯時執行

gcc–o test test.c –L ./ –ladd

就可以了。

  注意:在運作 main 前,需要注冊動态庫的路徑。方法有 3 種:

  (1)修改/etc/ld.so.conf

  (2)修改LD_LIBRARY_PATH 環境變量,LD_LIBRARY_PATH=. ./main

  (3)将庫檔案拷貝到/lib 或者/usr/lib 下(系統預設搜尋庫路徑)。

$cp libadd.so /lib //通常采用的方法, cp lib*.so /lib或者cp libadd.so  /usr/lib
$./main     //運作main
           

  如果不拷貝,生成.so之後還有種方法:采用dlopen,dlclose,dlerror,dlsym加載動态連結庫。

  為了使程式友善擴充,具備通用性,可以采用插件形式。采用異步事件驅動模型,保證主程式邏輯不變,将各個業務已動态連結庫的形式加載進來,這就是所謂的插件。linux提供了加載和處理動态連結庫的系統調用,非常友善。

#include <dlfcn.h>
void *dlopen(const char *filename, int flag);
char *dlerror(void);
void *dlsym(void *handle, const char *symbol);
int dlclose(void *handle);
           
  • dlopen以指定模式打開指定的動态連接配接庫檔案,并傳回一個句柄(指針)給調用程序,打開模式如下:
    • RTLD_LAZY 暫緩決定,等有需要時再解出符号
    • RTLD_NOW 立即決定,傳回前解除所有未決定的符号。
  • dlerror傳回出現的錯誤,
  • dlsym通過句柄和連接配接符名稱擷取函數名或者變量名,
  • dlclose解除安裝打開的庫。
//例:将如下程式編譯為動态連結庫libcaculate.so,程式如下:

int add(int a,int b)
{
    return (a + b);
}
int sub(int a, int b)
{
    return (a - b);
}

int mul(int a, int b)
{
    return (a * b);
}

int div(int a, int b)
{
    return (a / b);
}

//編譯如下: gcc -fPIC -shared caculate.c -o libcaculate.so
//采用上面生成的libcaculate.so,寫個測試程式如下:

#include <stdio.h>
#include <stdlib.h>
#include <dlfcn.h>

//動态連結庫路徑
 #define LIB_CACULATE_PATH "./libcaculate.so"

//函數指針
typedef int (*CAC_FUNC)(int, int);

int main()
{
    void *handle;
    char *error;
    CAC_FUNC cac_func = NULL;

   //打開動态連結庫
    handle = dlopen(LIB_CACULATE_PATH, RTLD_LAZY);
    if (!handle) 
    {
        fprintf(stderr, "%s\n", dlerror());
        exit(EXIT_FAILURE);
    }

    //清除之前存在的錯誤
    dlerror();

    //擷取一個函數
    *(void **) (&cac_func) = dlsym(handle, "add");
    if ((error = dlerror()) != NULL)  
    {
        fprintf(stderr, "%s\n", error);
        exit(EXIT_FAILURE);
    }
    printf("add: %d\n", (*cac_func)(,));

    cac_func = (CAC_FUNC)dlsym(handle, "sub");
    printf("sub: %d\n", cac_func(,));

    cac_func = (CAC_FUNC)dlsym(handle, "mul");
    printf("mul: %d\n", cac_func(,));

    cac_func = (CAC_FUNC)dlsym(handle, "div");
    printf("div: %d\n", cac_func(,));

    //關閉動态連結庫
    dlclose(handle);
    exit(EXIT_SUCCESS);
}

//編譯選項如下:gcc -rdynamic -o main main.c –ldl
           

4.3 靜态庫與動态庫的比較

  動态庫隻在執行時才被連結使用,不是直接編譯為可執行檔案,并且一個動态庫可以被多個程式使用故可稱為共享庫。

  靜态庫将會整合到程式中,在程式執行時不用加載靜态庫。 是以,靜态庫會使你的程式臃腫并且難以更新,但比較容易部署。而動态庫會使你的程式輕便易于更新但難以部署。

5 常用警告選項

選項 含義
-ansi 生成标準文法(ANSI C 标準)所要求的警告資訊(并不列出所有警告)
-pedantic 列出 ANSI C 标準的全部警告資訊。
-Wall 列出所有的警告資訊(常用)
–Werror 要求gcc将所有的警告都當成錯誤來處理

6 優化選項

  gcc 對代碼進行優化通過選項“-On”來控制優化級别(不是零時大寫的o,n 是整數)。不同的優化級别對應不同的優化處理工作。如使用優化選項:

常用選項 描述
-O1 主要進行線程跳轉和延遲退棧兩種優化。
-O2 除了完成所有“-O1”級别的優化之外,還要進行一些額外調整工作,如處理其指令排程等。
-O3 則還包括循環展開或其他一些與處理器特性相關的優化工作。

  雖然優化選項可以加速代碼的運作速度,但對于調試而言将是一個很大的挑戰。因為代碼在經過優化之後,原先在源程式中聲明和使用的變量很可能不再使用,控制流也可能會突然跳轉到意外的地方,循環語句也有可能因為循環展開而變得到處都有,所有這些對調試來講都是不好的。是以在調試的時候最好不要使用任何的優化選項,隻有當程式在最終發行的時候才考慮對其進行優化。通常用的是-O2。

7 調試選項

選項 描述
-g 在可執行程式中包含标準調試資訊
-g0 等于不加-g。即不包含任何資訊
-g1 隻包含最小資訊,一般來說隻有你不需要debug,隻需要backtrace資訊,并且真的很在意程式大小,或者有其他保密/特殊需求時才會使用-g1。
–g2 gdb預設等級,包含絕大多數你需要的資訊。
–g3 包含一些額外資訊,例如包含宏定義資訊。當你需要調試宏定義時,請使用-g3
  

  版權聲明:本文章參考《Linux man pages》做了修改,增添了一部分内容。未經作者允許,嚴禁用于商業出版,否則追究法律責任。網絡轉載請注明出處,這是對原創者的起碼的尊重!!!