GCC使用具體教程以及例子

編譯第一個C程序

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

使用gcc命令將hello.c編譯成可執行程序 a.out,並運行:

 將源文件hello.c編譯爲一個指定名稱的可執行文件:hello,可以通過gcc -o參數來完成

GCC 編譯過程分析

 

以demo.c爲例:從一個C語言源文件,到生成最後的可執行文件,GCC編譯過程的基本流程如下:

 

  • C 源文件: 編寫一個簡單的hello world程序
  • 預處理:生成預處理後的C源文件 hello.i
  • 編譯:將C源文件翻譯成彙編文件 hello.s
  • 彙編:將彙編文件彙編成目標文件 hello.o
  • 鏈接:將目標文件鏈接成可執行文件

 

 

gcc命令是GCC編譯器裏的一個前端程序,用來控制整個編譯過程:分別調用預處理器、編譯器和彙編器,完成編譯的每一個過程,最後調用鏈接器,生成可執行文件:a.out

默認情況下,gcc命令會自動完成上述的整個編譯過程。當然,gcc還提供了一系列參數,使用這個參數,可以讓用戶精準控制每一個編譯過程。

  • -E :只做預處理,不編譯
  • -S :只編譯,將C程序編譯爲彙編文件
  • -c :只彙編,不鏈接。
  • -o :指定輸出的文件名

GCC -E 參數

如果只對一段C語言程序做預處理操作,而不進行編譯,可以通過gcc -E 參數來完成。如下面的一段程序,在程序中分別使用#include包含頭文件,使用#define定義宏,使用#ifdef條件編譯。

#include <stdio.h>
#define PI  3.14
int main(void)
{
    printf("hello world!\n");
    printf("PI = %f\n", PI);
#ifdef DEBUG
    printf("debug mode\n");
#else
    printf("release mode\n");
#endif
    return 0;
}

 上面的C源程序使用gcc -E進行預處理,就可以生成原汁原味的C程序

 

 通過預處理後的C程序,使用#include包含的的頭文件就地展開,我們可以看到stdio.h頭文件中printf函數的聲明。程序中使用#define定義的宏PI,也會在實際使用的地方展開爲實際的值。使用#ifdef定義的條件編譯,會根據條件判斷,選擇實際要編譯的代碼分支。

GCC -S 參數

如果只對C源程序做編譯處理,不彙編,可以使用gcc -S 參數:會gcc會將C源程序做預處理、編譯操作,生成對應的彙編文件,不再做進一步的彙編和鏈接操作。

也可以 利用上次的

gcc -S demo.i

GCC -c 參數

 如果只想對一個C程序做彙編操作,不進行鏈接,可以使用gcc -c 來完成:

 

gcc只對源文件做預處理、編譯和彙編操作,不會做鏈接操作。在當前目錄下,我們可以看到demo.c經過彙編編譯,生成的對應的demo.o目標文件。

當然,gcc -c 選項,也可以對上幾節生成的 demo.i、demo.s文件直接彙編,生成對應的目標文件

 

默認情況下,gcc會將demo.c生成對應的demo.o目標文件。當然,我們也可以通過 -o 輸出選項,生成指定的目標文件:

GCC 靜態鏈接庫

我們也可以通過gcc命令,將自己實現的一些函數封裝成庫,提供給其他開發者使用。

製作靜態鏈接庫

假如現在有add.c和sub.c 源文件,分別實現了加法函數add()和減法函數sub():

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

將它們編譯生成一個靜態庫libmath.a,供其他程序調用:

# gcc -c add.c 
# gcc -c sub.c 
# ls
add.c  add.o  sub.c  sub.o
# ar rcs libmath.a add.o sub.o
# ls
add.c  add.o  libmath.a  sub.c  sub.o

生成的libmath.a就是一個靜態庫,裏面包含了我們實現的add()函數和sub()函數:

# ar t libmath.a 
add.o
sub.o
# nm libmath.a 
add.o:
0000000000000000 T add
sub.o:
0000000000000000 T sub

使用靜態鏈接庫

接下來,我們就可以編寫一個main()函數,然後在main函數裏調用它們。

int add(int a, int b);
int sub(int a, int b);
int main(void)
{
    add(1, 2);
    sub(4, 3);
    return 0;
}

在編譯mainc源文件時,因爲調用了libmath.a庫中的add和sub函數,編譯時要使用gcc -l指定庫的名字,使用-L指定庫的路徑:

# ls
libmath.a  main.c
# gcc main.c -L./ -lmath
# ls
a.out  libmath.a  main.c

GCC -I 參數

按照C語言的傳統,調用函數之前,要先聲明,然後才能使用。對add和sub函數的聲明,可以放到C源文件裏聲明,也可以單獨放到一個頭文件裏聲明,任何使用add和sub函數的源文件,直接包含這個頭文件就可以了。

# tree
.
├── inc
│   ├── add.h
│   └── sub.h
├── libmath.a
└── main.c
# cat inc/add.h 
int add(int a, int b);
# cat inc/sub.h 
int sub(int a, int b);
# cat main.c 
#include "add.h"
#include "sub.h"
int main(void)
{
    add(1, 2);
    sub(4, 3);
    return 0;
}

因爲頭文件 add.h 和 sub.h 統一放到了inc目錄下,編譯器在預處理時,要告訴編譯器這個路徑,否則編譯器就會找不到這些頭文件報錯。通過 gcc -I參數可以告訴編譯器,這些頭文件的所在路徑:

# ls
inc  libmath.a  main.c
# gcc main.c -L./ -lmath -I inc/
# ls
a.out  inc  libmath.a  main.c

GCC 動態鏈接庫

靜態庫裏實現的函數,可能被多個應用程序調用,那麼在鏈接時,被調用的這個函數可能就會多次鏈接到不同的應用程序中。

 

比如C標準庫的printf函數,可能被一個應用程序調用多次,被不同的應用程序調用,當這些應用程序加載到內存運行時,內存中也就存在多個printf函數代碼的副本,太浪費了內存空間。而且,對於應用程序來說,每一個調用的庫函數都被鏈接進來,自身的文件體積也會大增。是可忍孰不可忍,動態鏈接此時就粉墨登場了。

動態鏈接跟靜態鏈接相比,具有以下優勢

  • 庫的代碼不會鏈接到應用程序裏
  • 同一份代碼(如printf代碼)可以被多個應用程序共享使用
  • 大大節省了內存空間

但動態庫也有缺點,發佈軟件時,動態庫需要和應用程序一起發佈,否則你編譯的應用程序到了一個新的平臺可能就無法運行。

製作一個動態庫

gcc  -shared -fPIC -o libmymath.dylib add.c sub.c

 

其中的參數說明:

  • -shared :動態庫編譯,鏈接動態庫
  • -fPIC(或fpic) :生成使用相對地址無關的目標代碼
  • -Ldir :在動態庫的搜索路徑中增加dir目錄
  • -lname :鏈接靜態庫(libname.a)或動態庫(libname.so)的庫

使用動態鏈接

將動態鏈接庫拷貝到main.c的同一目錄下

# tree
.
├── inc
│   ├── add.h
│   └── sub.h
├── libmymath.so
└── main.c

在編譯main.c,鏈接動態庫libmymath.so的時候,直接指定當前目錄下的libmymath.dylib文件:

# gcc main.c ./libmymath.dylib -I inc/
# ls
a.out  inc  libmymath.dylib  main.c

在當前目錄下運行生成的可執行文件a.out,可以正常運行。將a.out拷貝到其他目錄,比如/opt目錄下 會報錯

我們需要將對應的libmymath.dylib文件拷貝到a.out的同一目錄下,然後a.out才能正常運行。

Linux的動態鏈接庫一般都放在/lib官方默認目錄下。如果想讓a.out放在任何路徑下都可以運行,我們可以把libmymath.dylib動態庫拷貝到/lib下,然後在編譯應用程序時,通過-L參數指定動態鏈接庫的搜索路徑。編譯生成的a.out在運行時,就會到指定的/lib目錄下去加載動態庫libmyamth.dylib:

GCC -std標準

同樣一段C程序,使用GCC的不同標準去編譯,編譯的結果可能不相同。使用gcc -std參數可以指定GCC編譯時的標準,常用的標準如下:

  • c89
  • c99
  • c11
  • gnu89、gnu90、gnu99、gnu11

gnu89和c89標準的區別是:gnu89除了支持和兼容c89標準外,在c89的基礎上進行了語法擴展。

同樣一段C程序,使用GCC不同的C標準去編譯,結果可能就不一樣:

# cat hello.c 
#include <stdio.h>
int main(void)
{
    int a[10];
    for(int i = 0; i < 10; i++)
        a[i] = i;
    return 0;
}

 

如果根據提示,使用C99、C11、gnu99等標準編譯,就不會報錯,可以正常編譯:

GCC -Wall 參數

 GCC編譯器的-Wall參數用於顯示所有的警告信息。大家在編寫程序時,不要以爲編譯通過,程序可以運行就萬事大吉了,任何一個隱藏的警告信息都可以對軟件的穩定運行帶來隱患。因此,我們不要放過任何一個警告信息,使用GCC編譯器的-Wall參數,可以開啓警告信息,顯示所有的警告信息。

GCC -g 參數

程序的編譯一般分爲兩種模式:debug模式和release模式。

軟件在開發階段,需要不斷地調試,甚至源碼級單步調試,此時建議使用debug模式編譯,生成的可執行文件中包含了大量的調試信息,可以方便我們使用gdb等調試器進行源碼級地調試:比如單步、在源碼中設置斷點等。

使用調試模式生成的可執行文件因爲包含大量的調試信息,所以文件體系會比release模式大。等開發結束,發佈軟件時,建議使用release模式,生成的可執行文件體積就比較小了。

使用gcc -g 選項,可以生成一個debug模式的可執行文件

使用debug模式生成的可執行文件裏,會有一些單獨的section保存各種符號及調試信息,這些信息包含了二進制代碼和源碼之間的一一對應關係。通過這種對應關係,我們就可以實現源碼級的單步調試和設置斷點。

對比一下,release模式的可執行文件,你會發現:debug模式的可執行文件裏有35個section,而release模式的可執行文件裏只有30個section:

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章