1. 程式人生 > >C語言編譯過程(自己用)

C語言編譯過程(自己用)

我現在一般都是用gcc,所以自然以GCC編譯hellworld為例,簡單總結如下。
 

hello.c原始碼如下:

1
2
3
4
5
6
  1. <span style="color:#339933">#include <stdio.h></span>  
  2. <span style="color:#993333">int</span> main<span style="color:#009900">(</span><span style="color:#009900">)</span>  
  3. <span style="color:#009900"
    >{</span>  
  4.     <a href="http://www.opengroup.org/onlinepubs/009695399/functions/printf.html"><span style="color:#000066">printf</span></a><span style="color:#009900">(</span>“Hello<span style="color:#339933">,</span> world.\n”<span style="color:#009900">)</span><span style=
    "color:#339933">;</span>  
  5.     <span style="color:#b1b100">return</span> <span style="color:#0000dd">0</span><span style="color:#339933">;</span>  
  6. <span style="color:#009900">}</span>  

通常我們使用gcc來生成可執行程式,命令為:gcc hello.c,預設生成可執行檔案a.out

其實編譯(包括連結)的命令:gcc hello.c 可分解為如下4個大的步驟:

  • 預處理(Preprocessing)
  • 編譯(Compilation)
  • 彙編(Assembly)
  • 連結(Linking)
gcc compilation

gcc compilation



 

1.       預處理(Preproceessing)

預處理的過程主要處理包括以下過程:

  • 將所有的#define刪除,並且展開所有的巨集定義
  • 處理所有的條件預編譯指令,比如#if #ifdef #elif #else #endif等
  • 處理#include 預編譯指令,將被包含的檔案插入到該預編譯指令的位置。
  • 刪除所有註釋 “//”和”/* */”.
  • 新增行號和檔案標識,以便編譯時產生除錯用的行號及編譯錯誤警告行號。
  • 保留所有的#pragma編譯器指令,因為編譯器需要使用它們

通常使用以下命令來進行預處理:

gcc -E hello.c -o hello.i

引數-E表示只進行預處理 或者也可以使用以下指令完成預處理過程

cpp hello.c > hello.i      /*  cpp – The C Preprocessor  */

直接cat hello.i 你就可以看到預處理後的程式碼

2.       編譯(Compilation)

編譯過程就是把預處理完的檔案進行一系列的詞法分析,語法分析,語義分析及優化後生成相應的彙編程式碼。

$gcc –S hello.i –o hello.s

或者

$ /usr/lib/gcc/i486-linux-gnu/4.4/cc1 hello.c

注:現在版本的GCC把預處理和編譯兩個步驟合成一個步驟,用cc1工具來完成。gcc其實是後臺程式的一些包裝,根據不同引數去呼叫其他的實際處理程式,比如:預編譯編譯程式cc1、彙編器as、聯結器ld

可以看到編譯後的彙編程式碼(hello.s)如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
    .file   "hello.c"
    .section    .rodata
.LC0:
    .string "Hello, world."
    .text
.globl main
    .type   main, @function
main:
    pushl   %ebp
    movl    %esp, %ebp
    andl    $-16, %esp
    subl    $16, %esp
    movl    $.LC0, (%esp)
    call    puts
    movl    $0, %eax
    leave
    ret
    .size   main, .-main
    .ident  "GCC: (Ubuntu 4.4.3-4ubuntu5) 4.4.3"
    .section    .note.GNU-stack,"",@progbits

3.       彙編(Assembly)

彙編器是將彙編程式碼轉變成機器可以執行的命令,每一個彙編語句幾乎都對應一條機器指令。彙編相對於編譯過程比較簡單,根據彙編指令和機器指令的對照表一一翻譯即可。

$ gcc –c hello.c –o hello.o

或者

$ as hello.s –o hello.co

由於hello.o的內容為機器碼,不能以普通文字形式的檢視(vi 開啟看到的是亂碼)。

4.       連結(Linking)

通過呼叫連結器ld來連結程式執行需要的一大堆目標檔案,以及所依賴的其它庫檔案,最後生成可執行檔案。

ld -static crt1.o crti.o crtbeginT.o hello.o -start-group -lgcc -lgcc_eh -lc-end-group crtend.o crtn.o (省略了檔案的路徑名)。

helloworld的大體編譯和連結過程就是這樣了,那麼編譯器和連結器到底做了什麼呢?

編譯過程可分為6步:掃描(詞法分析)、語法分析、語義分析、原始碼優化、程式碼生成、目的碼優化。

詞法分析:掃描器(Scanner)將源代的字元序列分割成一系列的記號(Token)。lex工具可實現詞法掃描。

語法分析:語法分析器將記號(Token)產生語法樹(Syntax Tree)。yacc工具可實現語法分析(yacc: Yet Another Compiler Compiler)。

語義分析:靜態語義(在編譯器可以確定的語義)、動態語義(只能在執行期才能確定的語義)。

原始碼優化:原始碼優化器(Source Code Optimizer),將整個語法書轉化為中間程式碼(Intermediate Code)(中間程式碼是與目標機器和執行環境無關的)。中間程式碼使得編譯器被分為前端和後端。編譯器前端負責產生機器無關的中間程式碼;編譯器後端將中間程式碼轉化為目標機器程式碼。

目的碼生成:程式碼生成器(Code Generator).

目的碼優化:目的碼優化器(Target Code Optimizer)。

連結的主要內容是把各個模組之間相互引用的部分處理好,使得各個模組之間能夠正確地銜接。

連結的主要過程包括:地址和空間分配(Address and Storage Allocation),符號決議(Symbol Resolution),重定位(Relocation)等。

連結分為靜態連結和動態連結。

靜態連結是指在編譯階段直接把靜態庫加入到可執行檔案中去,這樣可執行檔案會比較大。

動態連結則是指連結階段僅僅只加入一些描述資訊,而程式執行時再從系統中把相應動態庫載入到記憶體中去。

靜態連結的大致過程如下圖所示:

static linking

static linking