C代碼是如何變成程序的
C語言是一門典型的編譯語言鸽粉,源代碼文件需要編譯成目標(biāo)代碼文件才能運(yùn)行杖剪。可以認(rèn)為程序文件就是編譯好的目標(biāo)代碼文件梨睁。以GCC的編譯過程為例。GCC的翻譯過程可以分成四個(gè)階段:預(yù)處理器娜饵、編譯器坡贺、匯編器、鏈接器箱舞,執(zhí)行這四個(gè)階段的程序一起構(gòu)成了一個(gè)編譯系統(tǒng)遍坟。
預(yù)處理器
預(yù)處理器(cpp)負(fù)責(zé)對(duì)源代碼進(jìn)行文本處理。它根據(jù)以字符#開頭的命令晴股,修改原始的C代碼愿伴。如:
-
include <stdio.h> 從編譯器的內(nèi)置查找路徑的根部開始查找stdio.h文件,讀取其內(nèi)容电湘,并把它直接插入到程序文本中公般。
-
include ”my_header.h” 與上條的區(qū)別就是查找路徑是從當(dāng)前代碼文件所在目錄開始万搔。
-
define MACRO_NAME CONTEXT 將原始代碼中所有的MACRO_NAME文本都替換成CONTEXT,這種替換可能會(huì)引起很多難以理解的錯(cuò)誤官帘。
-
define FUNC_NAME(PARA_LIST) CONTEXT 與上條類似,區(qū)別在于會(huì)在查找到FUNC_NAME的地方進(jìn)行參數(shù)匹配昧谊,并將CONTEXT中出現(xiàn)的參數(shù)名稱用對(duì)應(yīng)的文本進(jìn)行替換刽虹。
-
define MACRO_NAME #undef MACRO_NAME 前者用于單純的宏定義,后者用于取消宏定義呢诬。
-
ifdef #ifndef #else #endif 這幾個(gè)都是用于條件編譯的命令涌哲,用于決定被包括的文本是否加入到處理后的文本中。
常用的預(yù)處理命令就是這些尚镰,處理后就得到了另一個(gè)C代碼文件阀圾,一般用.i作為擴(kuò)展名。這部分有一個(gè)常用的技巧:header guard狗唉,用于防止頭文件被重復(fù)加載初烘。假設(shè)一個(gè)場(chǎng)景,某個(gè)工程中的3個(gè)文件:main.c分俯、a.h肾筐、b.h,其中每個(gè)文件的開頭有這樣的文本:
//main.c
#include ”a.h”
#include ”b.h”
//a.h
#include ”b.h”
void func_a();
//b.h
void func_b();
上面提到了預(yù)處理器在處理#include時(shí)是直接的文本插入缸剪,處理后的main.i文件的內(nèi)容是:
//main.i
void func_b();
void func_a();
void func_b();
b.h的內(nèi)容被載入了兩次吗铐!這個(gè)例子足夠簡(jiǎn)單,出現(xiàn)這種問題不會(huì)發(fā)生錯(cuò)誤杏节,但如果b.h文件很大唬渗,重復(fù)加載后可能會(huì)出現(xiàn)很多問題,還會(huì)導(dǎo)致編譯時(shí)間的延長(zhǎng)奋渔。這種情況下我們可以使用header guard來防止頭文件被重復(fù)加載镊逝,中間省略的部分即頭文件的正式內(nèi)容:
#ifndef XXX_YYY_ZZZ
#define XXX_YYY_ZZZ
...
#endif
其中XXX_YYY_ZZZ是你自定義的宏名字。如果為每個(gè)頭文件選擇一個(gè)不重復(fù)的宏名字卒稳,這個(gè)宏組合保證了每個(gè)頭文件只會(huì)被一個(gè)代碼文件載入一次蹋半,因?yàn)榈诙屋d入時(shí)XXX_YYY_ZZZ宏已經(jīng)定義過了,就直接跳到了#endif的后面充坑。
編譯階段
編譯器(ccl)將文本文件hello.i翻譯成文本文件hello.s减江,它包含一個(gè)匯編語言程序。匯編語言程序中的每條語句都以一種標(biāo)準(zhǔn)的文本格式確切地描述了一條低級(jí)機(jī)器語言指令捻爷。匯編語言為不同高級(jí)語言的不同編譯器提供了通用的輸出語言辈灼,例如C編譯器和Fortran編譯器產(chǎn)生的輸出文件用的都是一樣的匯編語言。
例如也榄,hello.c為:
#include <stdio.h>
int main(int argc, char *argv[])
{
printf("hello world\n");
return 0;
}
運(yùn)行g(shù)cc –S hello.c可以得到hello.s文件巡莹,其內(nèi)容為:
.file "hello.c"
.def ___main; .scl 2; .type 32; .endef
.section .rdata,"dr"
LC0:
.ascii "hello world\0"
.text
.globl _main
.def _main; .scl 2; .type 32; .endef
_main:
LFB6:
.cfi_startproc
pushl %ebp
.cfi_def_cfa_offset 8
所有以字符.開頭的行都是指導(dǎo)匯編器和鏈接器的命令司志,其它行則是被翻譯成匯編語言的代碼。
匯編階段
接下來降宅,匯編器(as)將hello.s翻譯成機(jī)器語言指令骂远,把這些指令打包成一種叫做可重定位目標(biāo)程序的格式,并將結(jié)果保存在目標(biāo)文件hello.o中腰根。hello.o文件是一個(gè)二進(jìn)制文件激才,它的字節(jié)編碼是機(jī)器語言指令而不是字符,如果我們?cè)谖谋揪庉嬈髦写蜷_hello.o文件额嘿,看到的將是一堆亂碼瘸恼。運(yùn)行g(shù)cc –c hello.c可以得到hello.o文件,它是二進(jìn)制格式册养,無法直接查看东帅,可以用反匯編器來查看它的編碼:objdump –d code.o以一種典型的可重定位目標(biāo)格式ELF為例。ELF文件的頭部數(shù)據(jù)包含了:
- 生成該文件的系統(tǒng)的字的大小和字節(jié)順序球拦。
- 幫助鏈接器語法分析和解釋目標(biāo)文件信息的數(shù)據(jù)靠闭。ELF文件中包含的數(shù)據(jù)可分成幾個(gè)節(jié),每個(gè)節(jié)的位置和大小是由節(jié)頭部表描述的:
- text 機(jī)器代碼
- rodata 只讀數(shù)據(jù)刘莹,比如雙引號(hào)括起的字符串等阎毅。
- data 已初始化的全局變量。
- bss 未初始化的全局變量点弯。在ELF文件中它只是占位符扇调,在目標(biāo)文件中不占據(jù)實(shí)際的空間。
- symtab 一個(gè)符號(hào)表抢肛,存放在程序中定義和引用的函數(shù)和全局變量的信息狼钮。
- rel.text 一個(gè).text節(jié)中位置的列表,當(dāng)鏈接器進(jìn)行鏈接時(shí)捡絮,需要修改這些位置熬芜。
- rel.data 被引用或定義的全局變量的重定位信息,依賴于其它模塊信息的已初始化的全局變量福稳,其值在鏈接時(shí)需要被修改涎拉。
- debug 調(diào)試符號(hào)表。
- line 機(jī)器代碼與源文件行號(hào)的對(duì)應(yīng)關(guān)系的圆,只有在-g選項(xiàng)時(shí)才會(huì)產(chǎn)生鼓拧。
- .strtab 一個(gè)字符串表,包括.symtab和.debug中的符號(hào)表越妈,以及每個(gè)節(jié)的名字季俩。
鏈接階段
鏈接器(ld)負(fù)責(zé)將多個(gè)可重定位目標(biāo)文件(.o文件)合并為一個(gè)可執(zhí)行文件,如hello程序文件就是由hello.o和printf.o文件合并得來的梅掠。合并過程中鏈接器負(fù)責(zé)解析符號(hào)表酌住,并修改不同編譯模塊間的引用信息店归,如hello.o的main函數(shù)調(diào)用printf函數(shù)時(shí),機(jī)器代碼的跳轉(zhuǎn)位置直到鏈接階段才會(huì)確定酪我,鏈接器會(huì)將跳轉(zhuǎn)位置修改為printf函數(shù)的入口位置消痛。鏈接器解析本地符號(hào)的引用是非常簡(jiǎn)單的。編譯器只允許每個(gè)模塊中每個(gè)本地符號(hào)只有一個(gè)定義都哭。不過肄满,對(duì)全局符號(hào)的解析就很復(fù)雜。如果鏈接器在所有模塊中都找不到某個(gè)符號(hào)時(shí)质涛,它就輸出”undefined reference”錯(cuò)誤信息并終止。如果所有符號(hào)的解析都順利完成掰担,鏈接器最后會(huì)輸出所有符號(hào)的引用位置都確定了的可執(zhí)行文件汇陆。