編譯的整個過程:預編譯、編譯、匯編、鏈接


編譯分為四個步驟:

每個步驟將文件編譯成別的格式,如下:

詳解:

1.預編譯:

預編譯過程主要做4件事:
①展開頭文件
在寫有#include <filename>或#include "filename"的文件中,將文件filename展開,通俗來說就是將fiename文件中的代碼寫入到當前文件中;
②宏替換
③去掉注釋
④條件編譯
即對#ifndef #define #endif進行判斷檢查,也正是在這一步,#ifndef #define #endif的作用體現出來,即防止頭文件被多次重復引用

2.編譯

將代碼轉成匯編代碼,並且在這個步驟中做了兩件很重要的工作:
①編譯器在每個文件中保存一個函數地址符表,該表中存儲着當前文件內包含的各個函數的地址;
②因為這步要生成匯編代碼,即一條一條的指令,而調用函數的代碼會被編譯成一條call指令,call指令后面跟的是jmp指令的匯編代碼地址,而jmp指令后面跟的才是“被調用的函數編譯成匯編代碼后的第一條指令”的地址,但是給call指令后面補充上地址的工作是在鏈接的時候做的事情。

3.匯編

將匯編代碼轉成機器碼

4.鏈接

編譯器將生產的多個.o文件鏈接到一起生成一個可執行.exe文件;
但是在這個過程中,編譯器做的一個重要的事情是將每個文件中call指令后面的地址補充上;方式是從當前文件的函數地址符表中開始找,如果沒有,繼續向別的文件的函數地址符表中找,找到后填補在call指令后面,如果找不到,則鏈接失敗。

舉例:

說實話,很多人做了很久的C/C++,也用了很多IDE,但是對於可執行程序的底層生成一片茫然,這無疑是一種悲哀,可以想象到大公司面試正好被問到這樣的問題,有多悲催不言而喻,這里正由於換工作的緣故,所以打算系統的把之前用到的C/C++補一補。這里權且當做拋磚引玉,大神飄過。

【總述】

從一個源文件(.c)到可執行程序到底經歷了哪幾步,我想大多數的人都知道,到時到底每一步都做了什么,我估計也沒多少人能夠說得清清楚楚,明明白白。

其實總的流程是這樣的。

【第一步】編輯hello.c

復制代碼
1 #include <stdio.h>
2 #include <stdlib.h>
3 int main()
4 {
5         printf("hello world!\n");
6         return 0;
7 }
復制代碼

【第二步】預處理

預處理過程實質上是處理“#”,將#include包含的頭文件直接拷貝到hell.c當中;將#define定義的宏進行替換,同時將代碼中沒用的注釋部分刪除等

具體做的事兒如下:

(1)將所有的#define刪除,並且展開所有的宏定義。說白了就是字符替換

(2)處理所有的條件編譯指令,#ifdef #ifndef #endif等,就是帶#的那些

(3)處理#include,將#include指向的文件插入到該行處

(4)刪除所有注釋

(5)添加行號和文件標示,這樣的在調試和編譯出錯的時候才知道是是哪個文件的哪一行

(6)保留#pragma編譯器指令,因為編譯器需要使用它們。

 

gcc -E hello.c -o a.c可以生成預處理后的文件。通過查看文件內容和文件大小可以得知a.c講stdio.h和stdlib.h包含了進來。

【第三步】編譯

編譯的過程實質上是把高級語言翻譯成機器語言的過程,即對a.c做了這些事兒

(1)詞法分析,

(2)語法分析

(3)語義分析

(4)優化后生成相應的匯編代碼

從 高級語言->匯編語言->機器語言(二進制)

gcc -S hello.c -o a.s可以生成匯編代碼

匯編代碼如下。

復制代碼
 1         .file   "hello.c"
 2         .section        .rodata
 3 .LC0:
 4         .string "hello world!"
 5         .text
 6         .globl  main
 7         .type   main, @function
 8 main:
 9 .LFB0:
10         .cfi_startproc
11         pushl   %ebp
12         .cfi_def_cfa_offset 8
13         .cfi_offset 5, -8
14         movl    %esp, %ebp
15         .cfi_def_cfa_register 5
16         andl    $-16, %esp
17         subl    $16, %esp
18         movl    $.LC0, (%esp)
19         call    puts
20         movl    $0, %eax
21         leave
22         .cfi_restore 5
23         .cfi_def_cfa 4, 4
24         ret 
25         .cfi_endproc
26 .LFE0:
27         .size   main, .-main
28         .ident  "GCC: (Ubuntu/Linaro 4.6.3-1ubuntu5) 4.6.3"
29         .section        .note.GNU-stack,"",@progbits
復制代碼

gcc -c hello.c -o a.o將源文件翻譯成二進制文件。類Uinx系統編譯的結果生生成.o文件,Windows系統是生成.obj文件。

編譯的過程就是把hello.c翻譯成二進制文件

【第四步】鏈接

就像剛才的hello.c它使用到了C標准庫的東西“printf”,但是編譯過程只是把源文件翻譯成二進制而已,這個二進制還不能直接執行,這個時候就需要做一個動作,

將翻譯成的二進制與需要用到庫綁定在一塊。打個比方編譯的過程就向你對你老婆說,我要吃雪糕。你只是給你老婆發出了你要吃雪糕的訴求而已,但是雪糕還沒有到。

綁定就是說你要吃的雪糕你的老婆已經給你買了,你可以happy。

gcc hello.c -o a可以生成可執行程序。即gcc不帶任何參數。ldd就可以看到你的可執行程序依賴的庫。

可以看到a.o的大小是1.1k,畢竟他只是把源文件翻譯成二進制文件。a卻有7k,應該是他多了很多“繩子”吧。在運行的時候這些“繩子”就將對應的庫函數“牽過來”。很形象的比喻是不是?哈哈。libc.so.6 中就對咱們用的printf進行了定義。

這就是編寫的整個流程,(⊙o⊙)。謝謝各位看官。不足的地方請不吝賜教。

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM