【學(xué)習(xí)筆記】程序的編譯和鏈接

一舆声、前言

在開發(fā)程序的過程中,編譯和鏈接是一定會經(jīng)歷但很少被重視的步驟柳爽,通常這兩個步驟會被 IDE 封裝媳握,開發(fā)者只需一鍵構(gòu)建即可,但遇到錯誤(尤其是鏈接相關(guān)的錯誤)時磷脯,如果不了解編譯和鏈接的原理蛾找,就很難定位并解決問題。本文則嘗試分析并記錄程序編譯和鏈接的整個過程赵誓。

1.png

二打毛、什么是編譯?

編譯的過程其實(shí)是將我們程序的源代碼翻譯成CPU能夠直接運(yùn)行的機(jī)器代碼俩功。

image.png
#include <stdio.h>

int add(int a, int b);

int main() {
    printf("Hello World!\n");
    int result = add(5, 5);
    return 0;
}

其中打印一行文本幻枉,并調(diào)用了一個 add() 函數(shù),該函數(shù)被定義在另一個源文件 math.c 中诡蜓,代碼如下:

int add(int a, int b) {
    return a + b;
}

2.1 編譯源碼

接下來熬甫,我們可以調(diào)用 gcc -c 來分別編譯這兩個文件,命令如下:

gcc -c main.c
gcc -c math.c

備注:常用的 C/C++ 編譯器除了 gcc 還有 clang万牺、msvc 等等罗珍。

需要注意的是洽腺,編譯永遠(yuǎn)都是以單個源文件為單位的脚粟。在實(shí)際開發(fā)中覆旱,我們通常會將不同功能的代碼分散到不同的源文件,一方面方便代碼的閱讀和維護(hù)核无,同時也提升了軟件構(gòu)建的速度扣唱。

比如,我們修改了其中某一個源文件团南,那么只需要單獨(dú)編譯它這一個文件即可噪沙,不需要浪費(fèi)時間重新編譯整個工程。

2.2 二進(jìn)制的目標(biāo)文件

執(zhí)行上述的 gcc 編譯命令后吐根,可以看到正歼,在編譯之后會生成兩個擴(kuò)展名為 .o 的文件,它們被稱作目標(biāo)文件拷橘。

目標(biāo)文件是一個二進(jìn)制的文件局义,文件的格式是 ELF(Executable and Linkable Format),Linux 下所有可執(zhí)行文件的通用格式冗疮。相應(yīng)的 Windows 使用的是另一種格式 PE萄唇,它們雖然互不兼容,但在結(jié)構(gòu)上非常相似术幔,都是對二進(jìn)制代碼的一種封裝另萤。

我們可以在文件頭部找到可執(zhí)行文件的基本信息,比如支持的操作系統(tǒng)诅挑、計器類型等等四敞,執(zhí)行以下命令查看:

readelf -h main.o

輸出結(jié)果:

ELF 頭:
  Magic:   7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00 
  類別:                              ELF64
  數(shù)據(jù):                              2 補(bǔ)碼,小端序 (little endian)
  版本:                              1 (current)
  OS/ABI:                            UNIX - System V
  ABI 版本:                          0
  類型:                              REL (可重定位文件)
  系統(tǒng)架構(gòu):                          Advanced Micro Devices X86-64
  版本:                              0x1
  入口點(diǎn)地址:               0x0
  程序頭起點(diǎn):          0 (bytes into file)
  Start of section headers:          744 (bytes into file)
  標(biāo)志:             0x0
  本頭的大邪瓮住:       64 (字節(jié))
  程序頭大心垦:       0 (字節(jié))
  Number of program headers:         0
  節(jié)頭大小:         64 (字節(jié))
  節(jié)頭數(shù)量:         13
  字符串表索引節(jié)頭: 10

文件后面則是一系列的區(qū)塊毒嫡,里面有我們的計器代碼還有程序的數(shù)據(jù)等等癌蚁,查看區(qū)塊/段(Sections):

readelf -S main.o

輸出結(jié)果:

共有 13 個節(jié)頭,從偏移量 0x2e8 開始:

節(jié)頭:
  [號] 名稱              類型             地址              偏移量
       大小              全體大小          旗標(biāo)   鏈接   信息   對齊
  [ 0]                   NULL             0000000000000000  00000000
       0000000000000000  0000000000000000           0     0     0
  [ 1] .text             PROGBITS         0000000000000000  00000040
       000000000000002b  0000000000000000  AX       0     0     1
  [ 2] .rela.text        RELA             0000000000000000  00000220
       0000000000000048  0000000000000018   I      11     1     8
  [ 3] .data             PROGBITS         0000000000000000  0000006b
       0000000000000000  0000000000000000  WA       0     0     1
  [ 4] .bss              NOBITS           0000000000000000  0000006b
       0000000000000000  0000000000000000  WA       0     0     1
  [ 5] .rodata           PROGBITS         0000000000000000  0000006b
       000000000000000d  0000000000000000   A       0     0     1
  [ 6] .comment          PROGBITS         0000000000000000  00000078
       0000000000000036  0000000000000001  MS       0     0     1
  [ 7] .note.GNU-stack   PROGBITS         0000000000000000  000000ae
       0000000000000000  0000000000000000           0     0     1
  [ 8] .eh_frame         PROGBITS         0000000000000000  000000b0
       0000000000000038  0000000000000000   A       0     0     8
  [ 9] .rela.eh_frame    RELA             0000000000000000  00000268
       0000000000000018  0000000000000018   I      11     8     8
  [10] .shstrtab         STRTAB           0000000000000000  00000280
       0000000000000061  0000000000000000           0     0     1
  [11] .symtab           SYMTAB           0000000000000000  000000e8
       0000000000000120  0000000000000018          12     9     8
  [12] .strtab           STRTAB           0000000000000000  00000208
       0000000000000016  0000000000000000           0     0     1
Key to Flags:
  W (write), A (alloc), X (execute), M (merge), S (strings), l (large)
  I (info), L (link order), G (group), T (TLS), E (exclude), x (unknown)
  O (extra OS processing required) o (OS specific), p (processor specific)

在以上信息中兜畸,經(jīng)常提到有:

  • .text 代碼區(qū)努释,里面是之前編譯好的機(jī)器代碼;
  • .data 數(shù)據(jù)區(qū)咬摇,里面保存了我們初始化的全局變量伐蒂、局部靜態(tài)變量等等;

需要注意的是肛鹏,目標(biāo)文件雖然包含了編譯之后的機(jī)器代碼逸邦,但它并不能夠直接執(zhí)行恩沛,操作系統(tǒng)也不允許你區(qū)執(zhí)行它。因?yàn)槲覀冊诰幾g的過程中缕减,用到了尚未定義的 add() 函數(shù)雷客。

在主程序中 add() 其實(shí)只是一句聲明而已,它被定義在另一個模塊 math.c 中桥狡,這同樣也包括我們用到的標(biāo)準(zhǔn)庫中的 printf() 函數(shù)搅裙,如果我們?nèi)ゲ榭?stdio.h 頭文件,其中的 printf() 也只是一個函數(shù)聲明而已裹芝。

換句話說部逮,我們在編譯 main.c 時,編譯器完全不知道 printf()add() 函數(shù)的存在嫂易,比如它們位于內(nèi)存的哪個區(qū)塊兄朋、代碼長什么樣,都是不知道的怜械。因此編譯器只能將這個兩個函數(shù)的跳轉(zhuǎn)地址暫時先設(shè)為 0颅和,隨后在鏈接的時候再去修正它。

比如宫盔,我們來看一下 main.o 這個目標(biāo)文件中的內(nèi)容:

objdump -s -d main.o

輸出結(jié)果:

main.o:     文件格式 elf64-x86-64

Contents of section .text:
 0000 554889e5 4883ec10 bf000000 00e80000  UH..H...........
 0010 0000be05 000000bf 05000000 e8000000  ................
 0020 008945fc b8000000 00c9c3             ..E........     
Contents of section .rodata:
 0000 48656c6c 6f20576f 726c6421 00        Hello World!.   
Contents of section .comment:
 0000 00474343 3a202855 62756e74 7520352e  .GCC: (Ubuntu 5.
 0010 342e302d 36756275 6e747531 7e31362e  4.0-6ubuntu1~16.
 0020 30342e31 32292035 2e342e30 20323031  04.12) 5.4.0 201
 0030 36303630 3900                        60609.          
Contents of section .eh_frame:
 0000 14000000 00000000 017a5200 01781001  .........zR..x..
 0010 1b0c0708 90010000 1c000000 1c000000  ................
 0020 00000000 2b000000 00410e10 8602430d  ....+....A....C.
 0030 06660c07 08000000                    .f......        

Disassembly of section .text:

0000000000000000 <main>:
   0:   55                      push   %rbp
   1:   48 89 e5                mov    %rsp,%rbp
   4:   48 83 ec 10             sub    $0x10,%rsp
   8:   bf 00 00 00 00          mov    $0x0,%edi
   d:   e8 00 00 00 00          callq  12 <main+0x12>
  12:   be 05 00 00 00          mov    $0x5,%esi
  17:   bf 05 00 00 00          mov    $0x5,%edi
  1c:   e8 00 00 00 00          callq  21 <main+0x21>
  21:   89 45 fc                mov    %eax,-0x4(%rbp)
  24:   b8 00 00 00 00          mov    $0x0,%eax
  29:   c9                      leaveq 
  2a:   c3                      retq 

上述的 0000000000000000 <main> 是指編譯后的主函數(shù)代碼融虽,在下面的內(nèi)容中,左邊的是機(jī)器代碼灼芭,右邊是對應(yīng)的反匯編有额,可以看到反匯編中的兩個 call 指令,它們分別對應(yīng)之前調(diào)用的 printf()add() 函數(shù)彼绷,我們發(fā)現(xiàn)巍佑,它們的跳轉(zhuǎn)地址都被設(shè)成了 0,而這里的 0 在后面鏈接的時候會被修正寄悯。

另外萤衰,為了讓鏈接器能夠定位到這些需要被修正的地址,在代碼塊中猜旬,我們還可以找到一個重定位表(Reloction Table)脆栋,命令如下:

objdump -r main.o

輸出結(jié)果:

main.o:     文件格式 elf64-x86-64

RELOCATION RECORDS FOR [.text]:
OFFSET           TYPE              VALUE 
0000000000000009 R_X86_64_32       .rodata
000000000000000e R_X86_64_PC32     puts-0x0000000000000004
000000000000001d R_X86_64_PC32     add-0x0000000000000004


RELOCATION RECORDS FOR [.eh_frame]:
OFFSET           TYPE              VALUE 
0000000000000020 R_X86_64_PC32     .text

比如在 .text 區(qū)塊中,需要被重定位的兩個函數(shù) printf()add()洒擦,它們分別位于偏移量 0x0e0x1d 的位置椿争,后面的 R_X86_64_PC32 是地址的類型和長度,這和我們之前看到的機(jī)器代碼是一一對應(yīng)的熟嫩。

備注:這里的 printf() 函數(shù)顯示為 puts() 是因?yàn)?main.c 中調(diào)用 printf() 函數(shù)時秦踪,傳入的參數(shù)無需解析,即不含類似 %s 之類的 format 時,編譯器會將其轉(zhuǎn)換為 puts() 函數(shù)椅邓,進(jìn)行簡化柠逞。

三、什么是鏈接景馁?

我們將編譯生成的 main.omath.o 鏈接生成一個獨(dú)立的可執(zhí)行文件板壮,命令如下:

gcc main.o math.o -o demo

執(zhí)行完成后,在終端目錄下裁僧,我們可以找到生成的 demo 文件个束,該文件可以直接運(yùn)行慕购。

./demo

輸出結(jié)果:

Hello World!

鏈接其實(shí)將編譯之后的所有目標(biāo)文件聊疲,連同用到的一些靜態(tài)庫、運(yùn)行時庫(動態(tài)庫)組合拼裝成一個獨(dú)立的可執(zhí)行文件沪悲。其中就包括我們之前提到的地址修正获洲。

在這個時候,鏈接器會根據(jù)目標(biāo)文件或者靜態(tài)庫中的重定位表殿如,找到那些需要被重定位的函數(shù)贡珊、全局變量,從而修正它們的地址涉馁。

2.png

但如果我們在鏈接的時候忘記提供必須的目標(biāo)文件门岔,比如這里的 math.o 由于鏈接器找不到 add() 函數(shù)的實(shí)現(xiàn),就會報錯"引用未定義"(undefined reference to 'xxx')烤送,或者有的編譯器也叫"符號未定義"(undefined symbols to 'xxx')寒随,意思就是我們的代碼中用到了 xxx,但鏈接器卻無法找到它的定義帮坚。

gcc main.o -o demo

輸出結(jié)果:

main.o:在函數(shù)‘main’中:
main.c:(.text+0x1d):對‘a(chǎn)dd’未定義的引用
collect2: error: ld returned 1 exit status

四妻往、構(gòu)建工具

本文到這里,相信大家對編譯和鏈接已經(jīng)有一定了解了试和,但如果我們每次都手動編譯再鏈接顯然不夠高效讯泣,實(shí)際開發(fā)也沒有人會這么做,通常我們都是用各種各樣的 IDE 或者構(gòu)建工具幫我們自動化整個流程阅悍。

我自己工作中常用到的構(gòu)建工具主要有 Makefile 和 SCons好渠,這里用最常見的 Makefile 來舉例。

可能很多人對 Makefile 的印象是很古老节视,但其實(shí) Makefile 除了軟件構(gòu)建之外還有許多其他的奇技淫巧拳锚,比如用它來自動生成文檔等等,像很多現(xiàn)代的項(xiàng)目也都還在用它肴茄,譬如 Android OS 的構(gòu)建等等晌畅。

Makefile 的核心是對"依賴"的管理,比如要構(gòu)建上文中的可以執(zhí)行程序 demo寡痰,則需要 main.omath.o 文件同時執(zhí)行 gcc 鏈接指令抗楔,構(gòu)建 main.o 又需要 main.c 這個文件同時執(zhí)行 gcc 編譯命令棋凳,依此類推,我們可以發(fā)現(xiàn)连躏,makefile 其實(shí)就是在定義一棵依賴樹剩岳。

1.png

我們需要構(gòu)建最右側(cè)的這個目標(biāo)文件就需要提供左邊那些節(jié)點(diǎn)文件,然后這樣層層遞歸下去入热,代碼如下:

all: demo

demo: main.o math.o
                gcc main.o math.o -o demo

main.o: main.c
                gcc -c main.c

math.o: math.c
                gcc -c math.c

clean:
                rm demo main.o math.o

有了 makefile 以后我們可以調(diào)用 make 命令拍棕,后面跟上目標(biāo)的名稱 demo,它會自動根據(jù)我們"依賴樹"遞歸地去構(gòu)建這個可執(zhí)行文件:

make demo

第一次運(yùn)行由于所有葉子節(jié)點(diǎn)都不存在勺良,make 會自動構(gòu)建所有的依賴绰播,包括其中的目標(biāo)文件:

tree

輸出結(jié)果

.
├── main
├── main.c
├── main.o
├── makefile
├── math.c
└── math.o

0 directories, 6 files

如果我們再運(yùn)行一次 make,由于所有的文件都已經(jīng)存在尚困,并且是最新的蠢箩,make 就不會再重復(fù)構(gòu)建了。

make: 'demo' is up to date.

如果后續(xù)我們修改了 main.c 文件事甜,由于 main.c 只會影響 main.o谬泌,從而影響最后的可執(zhí)行文件 demo,所以 make 只會去重新生成這兩個相關(guān)的文件逻谦,從而避免了其他不必要的文件編譯掌实。

其實(shí)所有的現(xiàn)代化構(gòu)建工具都用到了相同的原理——對依賴的管理,只不過加入了一些更實(shí)用的功能邦马,比如腳本語言的支持(SCons就是基于Python實(shí)現(xiàn)的贱鼻,因此支持Python語言的功能)、第三方庫的管理等等勇婴。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末忱嘹,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子耕渴,更是在濱河造成了極大的恐慌拘悦,老刑警劉巖,帶你破解...
    沈念sama閱讀 219,539評論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件橱脸,死亡現(xiàn)場離奇詭異础米,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)添诉,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,594評論 3 396
  • 文/潘曉璐 我一進(jìn)店門屁桑,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人栏赴,你說我怎么就攤上這事蘑斧。” “怎么了?”我有些...
    開封第一講書人閱讀 165,871評論 0 356
  • 文/不壞的土叔 我叫張陵竖瘾,是天一觀的道長沟突。 經(jīng)常有香客問我,道長捕传,這世上最難降的妖魔是什么惠拭? 我笑而不...
    開封第一講書人閱讀 58,963評論 1 295
  • 正文 為了忘掉前任,我火速辦了婚禮庸论,結(jié)果婚禮上职辅,老公的妹妹穿的比我還像新娘。我一直安慰自己聂示,他們只是感情好域携,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,984評論 6 393
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著催什,像睡著了一般涵亏。 火紅的嫁衣襯著肌膚如雪宰睡。 梳的紋絲不亂的頭發(fā)上蒲凶,一...
    開封第一講書人閱讀 51,763評論 1 307
  • 那天,我揣著相機(jī)與錄音拆内,去河邊找鬼旋圆。 笑死,一個胖子當(dāng)著我的面吹牛麸恍,可吹牛的內(nèi)容都是我干的灵巧。 我是一名探鬼主播,決...
    沈念sama閱讀 40,468評論 3 420
  • 文/蒼蘭香墨 我猛地睜開眼抹沪,長吁一口氣:“原來是場噩夢啊……” “哼刻肄!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起融欧,我...
    開封第一講書人閱讀 39,357評論 0 276
  • 序言:老撾萬榮一對情侶失蹤敏弃,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后噪馏,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體麦到,經(jīng)...
    沈念sama閱讀 45,850評論 1 317
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,002評論 3 338
  • 正文 我和宋清朗相戀三年欠肾,在試婚紗的時候發(fā)現(xiàn)自己被綠了瓶颠。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 40,144評論 1 351
  • 序言:一個原本活蹦亂跳的男人離奇死亡刺桃,死狀恐怖粹淋,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤桃移,帶...
    沈念sama閱讀 35,823評論 5 346
  • 正文 年R本政府宣布欢搜,位于F島的核電站,受9級特大地震影響谴轮,放射性物質(zhì)發(fā)生泄漏炒瘟。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,483評論 3 331
  • 文/蒙蒙 一第步、第九天 我趴在偏房一處隱蔽的房頂上張望疮装。 院中可真熱鬧,春花似錦粘都、人聲如沸廓推。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,026評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽樊展。三九已至,卻和暖如春堆生,著一層夾襖步出監(jiān)牢的瞬間专缠,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,150評論 1 272
  • 我被黑心中介騙來泰國打工淑仆, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留涝婉,地道東北人。 一個月前我還...
    沈念sama閱讀 48,415評論 3 373
  • 正文 我出身青樓蔗怠,卻偏偏與公主長得像墩弯,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子寞射,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,092評論 2 355

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