敘述 C語(yǔ)言編譯

@(C語(yǔ)言)[code]

用一段簡(jiǎn)單的代碼,探討下從C代碼到最終可執(zhí)行文件的編譯過(guò)程荠诬,追根究底汁蝶。
偶爾了解下底層,也就沒(méi)那么多莫名其妙了窃躲。

工作原因有時(shí)候會(huì)用python寫寫測(cè)試工具变秦,感受到其快速實(shí)現(xiàn)應(yīng)用的便利,但由于偏底層開發(fā)框舔,主力語(yǔ)言依然是C蹦玫。對(duì)于開發(fā)語(yǔ)言沒(méi)有什么優(yōu)劣概念,在特定的情景下哪種實(shí)現(xiàn)更佳就用哪種刘绣,工具合適才是最好的樱溉。

個(gè)人開發(fā)環(huán)境 ubuntu 14.04


編譯的作用

相比python,lua等腳本語(yǔ)言解釋執(zhí)行方式纬凤,編譯C是為了提高程序的運(yùn)行效率福贞。把對(duì)用戶友好的語(yǔ)言文本編譯成對(duì)機(jī)器友好的特定指令直接執(zhí)行,而不是執(zhí)行時(shí)一條一條通過(guò)解釋器解析執(zhí)行停士,很大地提高了執(zhí)行的效率挖帘。對(duì)應(yīng)C主要用于底層,系統(tǒng)層次恋技,追求高性能表現(xiàn)拇舀,亦或者,平臺(tái)資源限制蜻底。

編譯的過(guò)程

gcc 的編譯流程分為四個(gè)步驟:
計(jì)算機(jī)系統(tǒng)設(shè)計(jì)基本原則:層次化和抽象骄崩。

編譯flow

編寫一個(gè)最簡(jiǎn)單的程序 hello.c,以此為例薄辅,看看各個(gè)過(guò)程做了什么事情要拂。

#include<stdio.h>

#define NUM(x) ((x) + 1)
int main(void)
{
    printf("Hello world %d\\\\r\\\\n", NUM(1));
    return 0;
}

預(yù)處理(Pre-Processing)

預(yù)處理主要完成的工作:

  • 根據(jù)#if后面的條件決定需要編譯的代碼
  • 將源文件中#include格式包含的文件直接復(fù)制到編譯的源文件中
  • 用實(shí)際值替換用#define定義的字符串

對(duì)源代碼進(jìn)行預(yù)處理操作

$ gcc -E hello.c -o hello.i

使用編輯器打開輸出hello.i,一看嚇一跳站楚,原本7脱惰、8的代碼變成800多行
截取開頭結(jié)尾如下

# 1 "hello.c"
# 1 "<built-in>"
# 1 "<command-line>"
# 1 "/usr/include/stdc-predef.h" 1 3 4
# 1 "<command-line>" 2
...
...
int main(void)
{
 printf("Hello world %d\\\\r\\\\n", ((1) + 1));
 return 0;
}

我打開文件 stdio.h 對(duì)比發(fā)現(xiàn),hello.i 文件開頭多出來(lái)的一大堆東西窿春,就是stdio.h 經(jīng)過(guò)#if條件選擇后留下的(包括其他包含文件的展開拉一,同理)采盒。同時(shí)在最下面看到熟悉的printf函數(shù)中定義的宏被直接替換成對(duì)應(yīng)的文本。
在這里提出兩個(gè)問(wèn)題

  • 預(yù)處理宏展開可能陷入死循環(huán)?
    我修改了了代碼, 宏里面調(diào)用了自己舅踪,并且沒(méi)有遞歸退出條件
#include<stdio.h>

#define NUM(x) (NUM(x) + 1)
int main(void)
{
    printf("Hello world %d\\\\r\\\\n", NUM(1));
    return 0;
}

輸出hello.i可以看到纽甘,宏展開遇到自己就會(huì)停止良蛮,避免陷入死循環(huán)

int main(void)
{
 printf("Hello world %d\\\\r\\\\n", (NUM(1) + 1));
 return 0;
}
  • include 包含頭文件重復(fù)?

預(yù)處理會(huì)直接把對(duì)應(yīng)的頭問(wèn)題展開抽碌,如果包含的頭文件本身包含了自己,是否也會(huì)陷入死循環(huán)? 簡(jiǎn)單編寫文件測(cè)試

inc.h 文件

#include "inc.h"

inc.c 文件

#include "inc.h"

int main(void)
{
    return 0;
}

預(yù)處理結(jié)果出錯(cuò)决瞳,提示如下:

inc.h:1:17: error: #include nested too deeply
 #include "inc.h"

說(shuō)明對(duì)于文件的展開是可能出現(xiàn)重復(fù)货徙,遞歸的,也說(shuō)明了為什么在每個(gè)被包含的頭文件皮胡,需要添加如下代碼段痴颊。

#ifndef _XXX__XXX
#define _XXX_XXX

#endif

編譯(Compiling)

這一環(huán)節(jié),是把C代碼轉(zhuǎn)換為匯編代碼并根據(jù)需求進(jìn)行一定程度的優(yōu)化處理屡贺。
執(zhí)行命令進(jìn)行編譯

$ gcc -S hello.i -o hello.s
# gcc -S 實(shí)際調(diào)用cc1蠢棱,所以也可以直接使用cc1編譯

生成hello.s (AT&T 格式)
這代碼初看起來(lái)晦澀難懂,再細(xì)細(xì)看起來(lái)甩栈,還是很難懂泻仙。

    .file   "hello.c"
    .section    .rodata
.LC0:
    .string "Hello world %d\\\\r\\\\n"
    .text
    .globl  main
    .type   main, @function
main:
.LFB0:
    .cfi_startproc
    pushq   %rbp
    .cfi_def_cfa_offset 16
    .cfi_offset 6, -16
    movq    %rsp, %rbp
    .cfi_def_cfa_register 6
    
    movl    $2, %esi  # 編譯器直接替換為宏 NUM(1) 的結(jié)果
    movl    $.LC0, %edi # 設(shè)置字符串保存的地址
    movl    $0, %eax
    call    printf    
    #  調(diào)用printf子例程,只有一個(gè)參數(shù)的printf gcc
    #  會(huì)把它替換成_puts提高效率量没, 加-fno-builtin 取消
    
    movl    $0, %eax  # main return 0
    popq    %rbp
    .cfi_def_cfa 7, 8
    ret
    .cfi_endproc
.LFE0:
    .size   main, .-main
    .ident  "GCC: (Ubuntu 4.8.4-2ubuntu1~14.04.1) 4.8.4"
    .section    .note.GNU-stack,"",@progbits

編譯器的優(yōu)化

編譯會(huì)有一個(gè)中間過(guò)程玉转,進(jìn)行優(yōu)化(前端)后再最終輸出匯編代碼(后端), gcc 可以通過(guò)以下命令查看, 感覺(jué)不是給人類看的殴蹄。

$ gcc -S -fdump-rtl-expand hello.c

使用clang(<-編譯器)也可以查看輸出中間過(guò)程:

$ clang-3.5 -S -emit-llvm hello.c

clang 輸出的可讀性更強(qiáng)究抓,可以大概看出程序的面貌(因?yàn)檫@個(gè)程序很簡(jiǎn)單...)

; ModuleID = 'hello.c'
target datalayout = "e-m:e-i64:64-f80:128-n8:16:32:64-S128"
target triple = "x86_64-pc-linux-gnu"

@.str = private unnamed_addr constant [17 x i8] c"Hello world %d\\\\0D\\\\0A\\\\00", align 1

; Function Attrs: nounwind uwtable
define i32 @main() #0 {
  %1 = alloca i32, align 4
  store i32 0, i32* %1
  %2 = call i32 (i8*, ...)* @printf(i8* getelementptr inbounds ([17 x i8]* @.str, i32 0, i32 0), i32 2)
  ret i32 0
}

declare i32 @printf(i8*, ...) #1

attributes #0 = { nounwind uwtable "less-precise-fpmad"="false" "no-frame-pointer-elim"="true" "no-frame-pointer-elim-non-leaf" "no-infs-fp-math"="false" "no-nans-fp-math"="false" "stack-protector-buffer-size"="8" "unsafe-fp-math"="false" "use-soft-float"="false" }
attributes #1 = { "less-precise-fpmad"="false" "no-frame-pointer-elim"="true" "no-frame-pointer-elim-non-leaf" "no-infs-fp-math"="false" "no-nans-fp-math"="false" "stack-protector-buffer-size"="8" "unsafe-fp-math"="false" "use-soft-float"="false" }

!llvm.ident = !{!0}

!0 = metadata !{metadata !"Ubuntu clang version 3.5.0-4ubuntu2~trusty2 (tags/RELEASE_350/final) (based on LLVM 3.5.0)"}

我嘗試在hello.c 的源代碼中添加一個(gè)無(wú)用的循環(huán)

for (int i = 0; i < 10; ++i) {
    i = i;    
}

然后分別用以下兩個(gè)條命令編譯,查看輸出中間文件.ll (使用clang是因?yàn)檩敵鼋Y(jié)果比較適合閱讀)

# 默認(rèn)不優(yōu)化處理 -O0
$ clang-3.5 -S -emit-llvm hello.c
# 開啟代碼優(yōu)化
$ clang-3.5 -O3 -S -emit-llvm hello.c

第一種不優(yōu)化情況下袭灯,編譯器老老實(shí)實(shí)把我寫的"沒(méi)啥作用"的代碼原原本本的編譯出來(lái).
第二種進(jìn)行了優(yōu)化刺下, 那段代碼不見了......
我想起工作上遇到的,使用for 進(jìn)行簡(jiǎn)單延時(shí)匹配一些硬件操作的時(shí)序稽荧,悲劇了.
(輸出結(jié)果我就不貼上來(lái)了怠李。)

中間層優(yōu)化是和體系代碼無(wú)關(guān)的情況下進(jìn)行的,優(yōu)化后再調(diào)用對(duì)應(yīng)體系的后端生成匯編代碼蛤克。 M中體系都可以共用中間層優(yōu)化捺癞,而不是M中體系重新實(shí)現(xiàn)M中優(yōu)化。

匯編(Assembling)

這一步驟相對(duì)簡(jiǎn)單构挤,將匯編代碼轉(zhuǎn)換為對(duì)應(yīng)的機(jī)器執(zhí)行指令髓介,由于這一步丟失的信息很少,所以可以通過(guò)反匯編把機(jī)器碼還原為匯編代碼筋现,但是再進(jìn)一步還原到高級(jí)語(yǔ)言就不可能了唐础。

$ gcc -c hello.s -o hello.o
# 可以直接調(diào)用匯編器 as
$ as hello.s -o hello.o箱歧。

使用objdump對(duì)生成的ELF進(jìn)行反匯編

$ objdump  -S hello.o

hello.o:     file format elf64-x86-64


Disassembly of section .text:

0000000000000000 <main>:
   0:   55                      push   %rbp
   1:   48 89 e5                mov    %rsp,%rbp
   4:   be 02 00 00 00          mov    $0x2,%esi
   9:   bf 00 00 00 00          mov    $0x0,%edi
   e:   b8 00 00 00 00          mov    $0x0,%eax
  13:   e8 00 00 00 00          callq  18 <main+0x18> # 看這里
  18:   b8 00 00 00 00          mov    $0x0,%eax
  1d:   5d                      pop    %rbp
  1e:   c3                      retq   

看到 13行, 原本call printf 的那句被替換為一個(gè)跳轉(zhuǎn)一膨,而且跳轉(zhuǎn)到下一條指令呀邢。因?yàn)閜rintf是一個(gè)外部調(diào)用,這個(gè)地址需要下一步鏈接的時(shí)候才能確定豹绪,這時(shí)候只是一個(gè)占位价淌。

鏈接(Linking)

主要是在不同模塊間對(duì)符號(hào)進(jìn)行重定位

在ELF文件 hello.o 里保存一張重定位表(relocation table),保存了其他地方的函數(shù)瞒津、變量(統(tǒng)稱符號(hào))的名字和地址蝉衣。
可以通過(guò)readelf讀取出來(lái)

$ readelf --relocs hello.o

Relocation section '.rela.text' at offset 0x5a0 contains 2 entries:
  Offset          Info           Type           Sym. Value    Sym. Name + Addend
00000000000a  00050000000a R_X86_64_32       0000000000000000 .rodata + 0
000000000014  000a00000002 R_X86_64_PC32     0000000000000000 printf - 4

Relocation section '.rela.eh_frame' at offset 0x5d0 contains 1 entries:
  Offset          Info           Type           Sym. Value    Sym. Name + Addend
000000000020  000200000002 R_X86_64_PC32     0000000000000000 .text + 0

可以看到,匯編后巷蚪, printf的地址還是空的病毡,沒(méi)有填寫上對(duì)應(yīng)的地址。

使用nm可以查看文件的符號(hào)定義, 可以看到 "U", 表示該符號(hào)未定義屁柏。

$ nm hello.o
0000000000000000 T main
                 U printf

printf 是在lib.a庫(kù)(由多個(gè).O文件打包就成了.a庫(kù))里面實(shí)現(xiàn)所啦膜,所以查看下里面的定義,可以看到具體是到printf.o這個(gè)文件淌喻。

$ objdump  -t /usr/lib/x86_64-linux-gnu/libc.a | grep "printf"
...
printf.o:     file format elf64-x86-64
0000000000000000 g     F .text  000000000000009e __printf
0000000000000000         *UND*  0000000000000000 vfprintf
0000000000000000 g     F .text  000000000000009e printf
...

而當(dāng)我手動(dòng)嘗試鏈接的時(shí)候僧家,又被提示一堆未定義,而這些工作gcc會(huì)自動(dòng)遞歸查找去解決似嗤。

$ gcc -static hello.c
$ ./a.out 
Hello world 2
$ du -h a.out 
856K    a.out
$ nm a.out  | grep " printf"
0000000000407ea0 T printf

編譯后執(zhí)行啸臀,發(fā)現(xiàn)一切正常,printf已經(jīng)定義了烁落,但是一個(gè)簡(jiǎn)單的程序竟然是856K....

$ gcc hello.c
$ ./a.out 
Hello world 2
$ du -h a.out 
12K a.out
$ nm a.out  | grep " printf"
                 U printf@@GLIBC_2.2.5

采用動(dòng)態(tài)加載的模式編譯乘粒,應(yīng)用體積減小了很多,但是看到printf提示未定義伤塌,標(biāo)記改了灯萍,表示是一個(gè)動(dòng)態(tài)鏈接。
通過(guò)file也可以查看執(zhí)行文件是否動(dòng)態(tài)鏈接
dynamically linked 和 statically linked

$ gcc hello.c
$ file a.out 
a.out: ELF 64-bit LSB  executable, x86-64, version 1 (SYSV), dynamically linked (uses shared libs), for GNU/Linux 2.6.24, BuildID[sha1]=8bdbcefb6289597b2123017d2678b11a6f742f23, not stripped
$ gcc -static hello.c
$ file a.out 
a.out: ELF 64-bit LSB  executable, x86-64, version 1 (GNU/Linux), statically linked, for GNU/Linux 2.6.24, BuildID[sha1]=25ff17d24016dd4a453a5ac53e3a3fee0f00a5ec, not stripped

這就是動(dòng)態(tài)鏈接庫(kù)的好處了每聪,把共用的代碼加載到系統(tǒng)旦棉,每個(gè)程序需要用到時(shí)候直接調(diào)用,而不需要都包含到每個(gè)可執(zhí)行文件中药薯,減少開銷绑洛。在執(zhí)行的時(shí)候,通過(guò)加載器獲取實(shí)際地址執(zhí)行童本。

其實(shí)動(dòng)態(tài)鏈接庫(kù)是不知道自己會(huì)被加載到內(nèi)存哪個(gè)位置的真屯,所以對(duì)于這個(gè)種鏈接,程序在執(zhí)行的時(shí)候穷娱,才能獲取到實(shí)際的地址绑蔫,涉及到GOT和PLI运沦。
GOT中的信息需要在動(dòng)態(tài)鏈接庫(kù)被程序加載后立刻填寫正確。這就給采用動(dòng)態(tài)鏈接庫(kù)的程序在啟動(dòng)時(shí)帶來(lái)了一定額外開銷配深,從而減緩了啟動(dòng)速度携添。ELF采用了做延遲綁定的做法來(lái)解決這一問(wèn)題÷ㄒ叮基本思想就是通過(guò)增加另外一個(gè)間接層烈掠,使得函數(shù)第一次被用到時(shí)才進(jìn)行綁定,這就是PLT(Procedure Linkage Table)的作用澜共。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末向叉,一起剝皮案震驚了整個(gè)濱河市锥腻,隨后出現(xiàn)的幾起案子嗦董,更是在濱河造成了極大的恐慌,老刑警劉巖瘦黑,帶你破解...
    沈念sama閱讀 206,378評(píng)論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件京革,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡幸斥,警方通過(guò)查閱死者的電腦和手機(jī)匹摇,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,356評(píng)論 2 382
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)甲葬,“玉大人廊勃,你說(shuō)我怎么就攤上這事【眩” “怎么了坡垫?”我有些...
    開封第一講書人閱讀 152,702評(píng)論 0 342
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)画侣。 經(jīng)常有香客問(wèn)我冰悠,道長(zhǎng),這世上最難降的妖魔是什么配乱? 我笑而不...
    開封第一講書人閱讀 55,259評(píng)論 1 279
  • 正文 為了忘掉前任溉卓,我火速辦了婚禮,結(jié)果婚禮上搬泥,老公的妹妹穿的比我還像新娘桑寨。我一直安慰自己,他們只是感情好忿檩,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,263評(píng)論 5 371
  • 文/花漫 我一把揭開白布尉尾。 她就那樣靜靜地躺著,像睡著了一般休溶。 火紅的嫁衣襯著肌膚如雪代赁。 梳的紋絲不亂的頭發(fā)上扰她,一...
    開封第一講書人閱讀 49,036評(píng)論 1 285
  • 那天,我揣著相機(jī)與錄音芭碍,去河邊找鬼徒役。 笑死,一個(gè)胖子當(dāng)著我的面吹牛窖壕,可吹牛的內(nèi)容都是我干的忧勿。 我是一名探鬼主播,決...
    沈念sama閱讀 38,349評(píng)論 3 400
  • 文/蒼蘭香墨 我猛地睜開眼瞻讽,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼鸳吸!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起速勇,我...
    開封第一講書人閱讀 36,979評(píng)論 0 259
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤晌砾,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后烦磁,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體养匈,經(jīng)...
    沈念sama閱讀 43,469評(píng)論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 35,938評(píng)論 2 323
  • 正文 我和宋清朗相戀三年都伪,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了呕乎。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 38,059評(píng)論 1 333
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡陨晶,死狀恐怖猬仁,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情先誉,我是刑警寧澤湿刽,帶...
    沈念sama閱讀 33,703評(píng)論 4 323
  • 正文 年R本政府宣布,位于F島的核電站谆膳,受9級(jí)特大地震影響叭爱,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜漱病,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,257評(píng)論 3 307
  • 文/蒙蒙 一买雾、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧杨帽,春花似錦漓穿、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,262評(píng)論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至,卻和暖如春僚饭,著一層夾襖步出監(jiān)牢的瞬間震叮,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,485評(píng)論 1 262
  • 我被黑心中介騙來(lái)泰國(guó)打工鳍鸵, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留苇瓣,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 45,501評(píng)論 2 354
  • 正文 我出身青樓偿乖,卻偏偏與公主長(zhǎng)得像击罪,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子贪薪,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,792評(píng)論 2 345

推薦閱讀更多精彩內(nèi)容