【軟體開發底層知識修煉】五 gcc-C語言編譯器

楊柳_發表於2019-01-29

學習交流加

  • 個人qq: 1126137994
  • 個人微信: liu1126137994
  • 學習交流資源分享qq群: 962535112

1、GCC與gcc

  • GCC (GNU Compiler Collection)

    • GNU 編譯器集合,包含眾多語言的編譯器,包括C,C++,Java等等
    • GCC多用於嵌入式作業系統的編譯,如Linux,VxWorks,Android等等
  • gcc 單指GCC中的C語言編譯器

    • gcc 多用於核心開發以及少數應用程式開發

2、gcc的幕後工作

想了解更多更詳細的關於編譯連結深層次內容,請閱讀書籍《CSAPP》第7章與《程式設計師的自我修養》,因為這裡我的學習記錄只記錄結果與常用的幾個編譯方法。

我們先來看一個簡單的程式:

test.c源程式:

#include <stdio.h>
#include "func.h"

int g_global = 0;
int g_test = 1;

int main(int argc, char *argv[])
{
    func();
    
    printf("&g_global = %p\n", &g_global);
    printf("&g_test = %p\n", &g_test);
    printf("&func = %p\n", &func);
    printf("&main = %p\n", &main);
	
    return 0;
}

複製程式碼

func.h標頭檔案:

#include <stdio.h>

void func()
{
#ifdef TEST
    printf("TEST = %s\n", TEST);
#endif

    return;
}
複製程式碼

在Linux下使用gcc進行編譯:

gcc test.c -o test
複製程式碼

然後執行:

./test
複製程式碼

結果如下:

&g_global = 0x804a020
&g_test = 0x804a014
&func = 0x80483c4
&main = 0x80483c9
複製程式碼

很明顯,上述程式很簡單,大一的新生都知道為什麼。但是今天我們不是學習這個程式的,而是想要了解,執行 gcc test.c -o test 這個命令後,是如何一步一步生成可執行檔案test的。

實際上,上述C程式從原始檔到二進位制可執行檔案,有以下四個步驟:

  1. 預處理 cpp
  2. C編譯 cc
  3. 彙編 as
  4. 連結 ld

大概編譯一個源程式為二進位制檔案的過程如下圖所示:

在這裡插入圖片描述

當然,上面沒有列出連結器,在生成file.o後,還需要將file.o與系統的庫檔案進行連結,生成最終的可執行檔案。

從而,我們就知道了,gcc其實內部包含了前處理器,編譯器,彙編器,連結器這四部分。

這四部分這裡只是來簡單介紹一下(網上一大堆,本文側重點不在此):

  • 前處理器:預處理,將源程式的巨集定義與帶‘#’的部分展開
  • 編譯器:將預處理後得到的檔案進行第一次編譯得到對應的彙編源程式
  • 彙編器:將第二步得到的彙編源程式進行第二次編譯即彙編,得到二進位制檔案(可重定位檔案)
  • 連結器:將可重定位檔案與相應的庫進行連結生成最終的可執行檔案

3、實用的gcc選項

本文的重點來了,上述的內容過於簡單,而本節的內容雖然不難,但是並不被大多數人所瞭解,所以是本文的重點學習記錄。

下面將要學習的gcc選項,在工作中具有很強的實用性。

3.1、預處理選項-解決巨集錯誤

gcc -E file.c -o file.i
複製程式碼

實用上述編譯選項 -E 可以得到預處理後的檔案,有時候我們在程式中定義的巨集可能有錯誤,而這種錯誤又很難找,此時如果能得預處理後的檔案,就可以方便定位錯誤。

3.2、-S引數-輔助編寫彙編程式的好方法

寫彙編程式很難,但是如果先寫成C語言,再將這個C語言轉化成組合語言,就會很簡單。gcc編譯工具中,-S選項,可以達到這個目的。比如以下程式: foo.c程式:

#include <stdio.h>
 void foo(){
    printf("This is foo().\n");                                             
 }
複製程式碼

我們使用如下命令進行編譯:

gcc -S -O2 foo.c -o foo.s
複製程式碼

將會生成一個foo.c相同作用的彙編程式foo.s,如下:

	.file	"foo.c"
	.section	.rodata.str1.1,"aMS",@progbits,1
.LC0:
	.string	"This is foo().\n"
	.text
	.p2align 4,,15
.globl foo
	.type	foo, @function
foo:
	pushl	%ebp
	movl	%esp, %ebp
	subl	$24, %esp
	movl	$.LC0, 4(%esp)
	movl	$1, (%esp)
	call	__printf_chk
	leave
	ret
	.size	foo, .-foo
	.ident	"GCC: (Ubuntu/Linaro 4.4.4-14ubuntu5.1) 4.4.5"
	.section	.note.GNU-stack,"",@progbits

複製程式碼

使用-S 引數時,我們可以根據需要使用-O優化選項。從foo.s的內容可以看出,"This is foo().\n" 這個字串是放在.rodata段的。看來獲取C程式對應的彙編程式碼,對C語言實現方面的細節,也有所幫助。

3.3、獲取系統標頭檔案路徑

gcc -v file.c 
複製程式碼

獲取file.c使用的系統標頭檔案的位置

3.4、產生對映檔案

如果我們想要知道程式中各個符號的記憶體佈局的資訊,可以使用如下命令:

gcc -Wl,-Map=file.map file.c -o file
複製程式碼

3.5、通過選項定義巨集

有時候程式中需要的某一個常量會依賴工作環境的不同而改變,這個時候,我們可以將這個常量定義為巨集,但是這樣,我們還是需要每次都在源程式中將巨集的值改變,這也很麻煩,此時就可以利用編譯選項 -D,在編譯的命令列進行巨集定義。

還有就是程式中或許會存在下屬這樣的程式碼: test.c程式:

#include <stdio.h>
#include "func.h"

int g_global = 0;
int g_test = 1;

int main(int argc, char *argv[])
{
    func();
    
    printf("&g_global = %p\n", &g_global);
    printf("&g_test = %p\n", &g_test);
    printf("&func = %p\n", &func);
    printf("&main = %p\n", &main);
	
    return 0;
}
複製程式碼

test.h標頭檔案:

#include <stdio.h>

void func()
{
#ifdef TEST
    printf("TEST = %s\n", TEST);
#endif

    return;
}
複製程式碼

在標頭檔案中,有一處定義 # ifdef TEST ....

很明顯,上面的兩個檔案,都沒有定義這個TEST,所以程式執行結果如下:

  &g_global = 0x804a020
  &g_test = 0x804a014
  &func = 0x80483c4
  &main = 0x80483c9
複製程式碼

但是可能在某個場合,又必須要使用TEST定義,那麼此時,我們肯定不願意在程式中改來改去,此時就利用編譯器的 -D選項,來定義這個TEST。如下編譯命令:

gcc -D'TEST="test" ' test.c -o test
複製程式碼

執行程式後,結果如下:

TEST = test
&g_global = 0x804a020
&g_test = 0x804a014
&func = 0x80483c4
&main = 0x80483e1
複製程式碼

3.6、生成依賴關係

大多數人應該知道make,如果不知道也沒有關係。 在makefile中,make需要通過依賴關係來決定,每次構建時哪些檔案需要重新編譯。使用gcc的-M選項,可以得到make所需要的原始檔的依賴關係。-MM選項可以讓gcc生成不包含系統檔案的依賴關係。

比如有如下原始檔: main.c原始檔(main.h與foo.c的內容是什麼都行)

#include <stdio.h>
#include "main.h"
#include "foo.c"

int main(){
    printf("Hello world!\n");
    return 0;                                                                   
}

複製程式碼

對其進行如下編譯

gcc -M main.c
複製程式碼

將得到如下輸出:

在這裡插入圖片描述

可以看到,這句是make所需要的main.c的依賴關係。

如果使用如下命令的話:

gcc -MM main.c
複製程式碼

將得到如下輸出:

在這裡插入圖片描述
結果顯而易見!!!

3.7、指定連結庫

當一個可執行程式的生成,需要使用其他庫時,需要在連結時加以指定。這就需要用到gcc 的-l與-L選項。

假設一個程式叫做main.c,它編譯成可執行程式不光需要系統的標準庫,還需要一個庫:libfoo.a 且這個libfoo.a與main.c在同一個目錄,那麼在編譯main.c時,需要以下命令:

gcc -o main -L. main.c -lfoo
複製程式碼

注意:

  • -L告訴gcc編譯器,當前可以從哪個目錄查詢庫檔案,此處-L後面跟了一個**點‘.’**表示當前目錄。
  • -l選項,告訴編譯器需要連線的庫名。這裡並沒有寫“lib”字首和“.a字尾”。-lfoo就是代表指定libfoo.a庫參與連結。

更加詳細的內容參考《程式設計師的自我修養》

4、總結

今天學習了gcc的簡單概念,與gcc的常用的引數選項。

相關文章