C語言編譯過程詳解

前言

C語言程序從源代碼到二進制行程序都經歷了那些過程?本文以Linux下C語言的編譯過程爲例,講解C語言程序的編譯過程。html

編寫hello world C程序:linux

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

編譯過程只需:shell

$ gcc hello.c # 編譯
$ ./a.out # 執行
hello world!

這個過程如此熟悉,以致於你們以爲編譯事件很簡單的事。事實真的如此嗎?咱們來細看一下C語言的編譯過程究竟是怎樣的。ubuntu

上述gcc命令其實依次執行了四步操做:1.預處理(Preprocessing), 2.編譯(Compilation), 3.彙編(Assemble), 4.連接(Linking)。編輯器

C_complie

示例

爲了下面步驟講解的方便,咱們須要一個稍微複雜一點的例子。假設咱們本身定義了一個頭文件mymath.h,實現一些本身的數學函數,並把具體實現放在mymath.c當中。而後寫一個test.c程序使用這些函數。程序目錄結構以下:ide

├── test.c
└── inc
    ├── mymath.h
    └── mymath.c

程序代碼以下:函數

// test.c
#include <stdio.h>
#include "mymath.h"// 自定義頭文件
int main(){
    int a = 2;
    int b = 3;
    int sum = add(a, b); 
    printf("a=%d, b=%d, a+b=%d\n", a, b, sum);
}

頭文件定義:rest

// mymath.h
#ifndef MYMATH_H
#define MYMATH_H
int add(int a, int b);
int sum(int a, int b);
#endif

頭文件實現:code

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

1.預處理(Preprocessing)

預處理用於將全部的#include頭文件以及宏定義替換成其真正的內容,預處理以後獲得的仍然是文本文件,但文件體積會大不少。gcc的預處理是預處理器cpp來完成的,你能夠經過以下命令對test.c進行預處理:htm

gcc -E -I./inc test.c -o test.i

或者直接調用cpp命令

$ cpp test.c -I./inc -o test.i

上述命令中-E是讓編譯器在預處理以後就退出,不進行後續編譯過程;-I指定頭文件目錄,這裏指定的是咱們自定義的頭文件目錄;-o指定輸出文件名。

通過預處理以後代碼體積會大不少:

X 文件名 文件大小 代碼行數
預處理前 test.c 146B 9
預處理後 test.i 17691B 857

預處理以後的程序仍是文本,能夠用文本編輯器打開。

2.編譯(Compilation)

這裏的編譯不是指程序從源文件到二進制程序的所有過程,而是指將通過預處理以後的程序轉換成特定彙編代碼(assembly code)的過程。編譯的指定以下:

$ gcc -S -I./inc test.c -o test.s

上述命令中-S讓編譯器在編譯以後中止,不進行後續過程。編譯過程完成後,將生成程序的彙編代碼test.s,這也是文本文件,內容以下:

// test.c彙編以後的結果test.s
    .file   "test.c"
    .section    .rodata
.LC0:
    .string "a=%d, b=%d, a+b=%d\n"
    .text
    .globl  main
    .type   main, @function
main:
.LFB0:
    .cfi_startproc
    pushl   %ebp
    .cfi_def_cfa_offset 8
    .cfi_offset 5, -8
    movl    %esp, %ebp
    .cfi_def_cfa_register 5
    andl    $-16, %esp
    subl    $32, %esp
    movl    $2, 20(%esp)
    movl    $3, 24(%esp)
    movl    24(%esp), %eax
    movl    %eax, 4(%esp)
    movl    20(%esp), %eax
    movl    %eax, (%esp)
    call    add 
    movl    %eax, 28(%esp)
    movl    28(%esp), %eax
    movl    %eax, 12(%esp)
    movl    24(%esp), %eax
    movl    %eax, 8(%esp)
    movl    20(%esp), %eax
    movl    %eax, 4(%esp)
    movl    $.LC0, (%esp)
    call    printf
    leave
    .cfi_restore 5
    .cfi_def_cfa 4, 4
    ret 
    .cfi_endproc
.LFE0:
    .size   main, .-main
    .ident  "GCC: (Ubuntu 4.8.2-19ubuntu1) 4.8.2"
    .section    .note.GNU-stack,"",@progbits

請不要問我上述代碼是什麼意思!-_-

3.彙編(Assemble)

彙編過程將上一步的彙編代碼轉換成機器碼(machine code),這一步產生的文件叫作目標文件,是二進制格式。gcc彙編過程經過as命令完成:

$ as test.s -o test.o

等價於:

gcc -c test.s -o test.o

這一步會爲每個源文件產生一個目標文件。所以mymath.c也須要產生一個mymath.o文件

4.連接(Linking)

連接過程將多個目標文以及所需的庫文件(.so等)連接成最終的可執行文件(executable file)

命令大體以下:

$ ld -o test.out test.o inc/mymath.o ...libraries...

結語

通過以上分析,咱們發現編譯過程並不像想象的那麼簡單,而是要通過預處理、編譯、彙編、連接。儘管咱們平時使用gcc命令的時候沒有關心中間結果,但每次程序的編譯都少不了這幾個步驟。也不用爲上述繁瑣過程而煩惱,由於你仍然能夠:

$ gcc hello.c # 編譯
$ ./a.out # 執行

參考文獻

1.https://www3.ntu.edu.sg/home/ehchua/programming/cpp/gcc_make.html 2.http://www.trilithium.com/johan/2005/08/linux-gate/ 3.https://gcc.gnu.org/onlinedocs/gccint/Collect2.html

相關文章
相關標籤/搜索