談?wù)劤绦蜴溄蛹胺侄文切┦?/h1>
時(shí)間:2020-07-06 09:32:42
手機(jī)看文章掃描二維碼
隨時(shí)隨地手機(jī)看文章
掃描二維碼
隨時(shí)隨地手機(jī)看文章
如果讀過(guò)我之前的文章就會(huì)知道,程序構(gòu)建大概需要經(jīng)歷四個(gè)過(guò)程:預(yù)處理、編譯、匯編、鏈接,這里主要介紹鏈接這一過(guò)程。
鏈接鏈的是什么?
鏈接鏈的就是目標(biāo)文件,什么是目標(biāo)文件?目標(biāo)文件就是源代碼編譯后但未進(jìn)行鏈接的那些中間文件,如Linux下的.o,它和可執(zhí)行文件的內(nèi)容和結(jié)構(gòu)很相似,格式幾乎是一樣的,可以看成是同一種類(lèi)型的文件,Linux下統(tǒng)稱(chēng)為ELF文件,這里介紹下ELF文件標(biāo)準(zhǔn):
可重定位文件:Linux中的.o,這類(lèi)文件包含代碼和數(shù)據(jù),可被鏈接成可執(zhí)行文件或共享目標(biāo)文件,例如靜態(tài)鏈接庫(kù)。
可執(zhí)行文件:可以直接執(zhí)行的文件,如/bin/bash文件。
共享目標(biāo)文件:Linux中的.so,包含代碼和數(shù)據(jù),一種是鏈接器可以使用這種文件和其它的可重定位文件和共享目標(biāo)文件鏈接,另一種是動(dòng)態(tài)鏈接器可以將幾個(gè)這種共享目標(biāo)文件和可執(zhí)行文件結(jié)合,作為進(jìn)程映像的一部分來(lái)執(zhí)行。
core dump文件:進(jìn)程意外終止時(shí),系統(tǒng)可以將該進(jìn)程的地址空間的內(nèi)容和其它信息存到coredump文件用于調(diào)試,如gdb。
我們可以使用command file來(lái)查看文件的格式:
file test.o; file /bin/bash;
目標(biāo)文件的構(gòu)成
目標(biāo)文件主要分為文件頭、代碼段、數(shù)據(jù)段和其它。
文件頭:描述整個(gè)文件的文件屬性(文件是否可執(zhí)行、是靜態(tài)鏈接還是動(dòng)態(tài)鏈接、入口地址、目標(biāo)硬件、目標(biāo)操作系統(tǒng)等信息),還包括段表,用來(lái)描述文件中各個(gè)段的數(shù)組,描述文件中各個(gè)段在文件中的偏移位置和段屬性。
代碼段:程序源代碼編譯后的機(jī)器指令。
數(shù)據(jù)段:數(shù)據(jù)段分為.data段和.bss段。
.data段內(nèi)容:已經(jīng)初始化的全局變量和局部靜態(tài)變量
.bss段內(nèi)容:未初始化的全局變量和局部靜態(tài)變量,.bss段只是為未初始化的全局變量和局部靜態(tài)變量預(yù)留位置,本身沒(méi)有內(nèi)容,不占用空間。
除了代碼段和數(shù)據(jù)段,還有.rodata段、.comment、字符串表、符號(hào)表和堆棧提示段等等,還可以自定義段。
.bss段不占用存儲(chǔ)空間?
看下面代碼:
int a[1000];int b[1000] = {1};int main() {printf("程序喵\n");return 0;}
我們查看下文件大小和各個(gè)段大小:
gcc testlink.c -o testls -l test-rwxrwxrwx 1 wzq wzq 12368 May 30 08:48 testsize testtext data bss dec hex filename1512 4616 4032 10160 27b0 test
再看這段初始化的代碼:
int a[1000] = {1};int b[1000] = {1};int main() {printf("程序喵\n");return 0;}
再查看下文件大小和各個(gè)段大?。?/span>
gcc testlink.c -o testls -l test-rwxrwxrwx 1 wzq wzq 16368 May 30 08:49 testsize testtext data bss dec hex filename1512 8616 8 10136 2798 test
可以看到僅僅是做了一次初始化,文件大小就從12368變成了16368,正好是初始化了的那a[1000]的大小,這4000字節(jié)從.bss段移動(dòng)到了.data段,程序大小增加了,這里可以看出.bss段不占據(jù)磁盤(pán)空間。
既然.bss段不占據(jù)空間,那它的大小和符號(hào)存在哪呢?
.bss段占據(jù)的大小存放在ELF文件格式中的段表(Section Table)中,段表存放了各個(gè)段的各種信息,比如段的名字、段的類(lèi)型、段在elf文件中的偏移、段的大小等信息。同時(shí)符號(hào)存放在符號(hào)表.symtab中。
.bss不占據(jù)實(shí)際的磁盤(pán)空間,只在段表中記錄大小,在符號(hào)表中記錄符號(hào)。當(dāng)文件加載運(yùn)行時(shí),才分配空間以及初始化。
其實(shí)程序里還有好多系統(tǒng)保留段,還可以自定義段,將某個(gè)變量放在自定義段,如下:
__attribute__((section("Custom"))) int global = 1;
可以使用一些工具查看ELF文件頭以及各個(gè)段的內(nèi)容:
查看文件頭:
$ readelf -h test.oELF Header:Magic: 7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00Class: ELF64Data: 2's complement, little endianVersion: 1 (current)OS/ABI: UNIX - System VABI Version: 0Type: REL (Relocatable file)Machine: Advanced Micro Devices X86-64Version: 0x1Entry point address: 0x0Start of program headers: 0 (bytes into file)Start of section headers: 720 (bytes into file)Flags: 0x0Size of this header: 64 (bytes)Size of program headers: 0 (bytes)Number of program headers: 0Size of section headers: 64 (bytes)Number of section headers: 13Section header string table index: 12
可以使用readelf查看文件頭:ELF魔數(shù)、文件機(jī)器字節(jié)長(zhǎng)度、數(shù)據(jù)存儲(chǔ)方式、版本、運(yùn)行平臺(tái)、ABI版本、ELF重定位類(lèi)型、硬件平臺(tái)、硬件平臺(tái)版本、入口地址、程序頭入口和長(zhǎng)度、段表的位置和長(zhǎng)度和段的數(shù)量。
查看段表的方法:
使用objdump查看ELF文件中包含的關(guān)鍵的段:
objdump -h test.o: file format elf64-x86-64Sections:Idx Name Size VMA LMA File off Algn0 .text 00000017 0000000000000000 0000000000000000 00000040 2**0ALLOC, LOAD, RELOC, READONLY, CODE1 .data 00000000 0000000000000000 0000000000000000 00000057 2**0ALLOC, LOAD, DATA2 .bss 00000000 0000000000000000 0000000000000000 00000057 2**0ALLOC3 .rodata 00000010 0000000000000000 0000000000000000 00000057 2**0ALLOC, LOAD, READONLY, DATA4 .comment 0000002a 0000000000000000 0000000000000000 00000067 2**0READONLY5 .note.GNU-stack 00000000 0000000000000000 0000000000000000 00000091 2**0READONLY6 .eh_frame 00000038 0000000000000000 0000000000000000 00000098 2**3ALLOC, LOAD, RELOC, READONLY, DATA
使用readelf查看ELF文件中包含的段:
readelf -S test.oThere are 13 section headers, starting at offset 0x2d0:Section Headers:Name Type Address OffsetSize EntSize Flags Link Info Align0] NULL 0000000000000000 000000000000000000000000 0000000000000000 0 0 01] .text PROGBITS 0000000000000000 000000400000000000000017 0000000000000000 AX 0 0 12] .rela.text RELA 0000000000000000 000002200000000000000030 0000000000000018 I 10 1 83] .data PROGBITS 0000000000000000 000000570000000000000000 0000000000000000 WA 0 0 14] .bss NOBITS 0000000000000000 000000570000000000000000 0000000000000000 WA 0 0 15] .rodata PROGBITS 0000000000000000 000000570000000000000010 0000000000000000 A 0 0 16] .comment PROGBITS 0000000000000000 00000067000000000000002a 0000000000000001 MS 0 0 17] .note.GNU-stack PROGBITS 0000000000000000 000000910000000000000000 0000000000000000 0 0 18] .eh_frame PROGBITS 0000000000000000 000000980000000000000038 0000000000000000 A 0 0 89] .rela.eh_frame RELA 0000000000000000 000002500000000000000018 0000000000000018 I 10 8 8.symtab SYMTAB 0000000000000000 000000d00000000000000120 0000000000000018 11 9 8.strtab STRTAB 0000000000000000 000001f0000000000000002b 0000000000000000 0 0 1.shstrtab STRTAB 0000000000000000 000002680000000000000061 0000000000000000 0 0 1Key to Flags:W (write), A (alloc), X (execute), M (merge), S (strings), I (info),L (link order), O (extra OS processing required), G (group), T (TLS),C (compressed), x (unknown), o (OS specific), E (exclude),l (large), p (processor specific)
objdump只能查看關(guān)鍵的段,而readelf可以查看所有段。
其中,.rela.text是針對(duì).text段的重定位表,鏈接器在處理目標(biāo)文件時(shí),需要對(duì)目標(biāo)文件中的某些部位進(jìn)行重定位,即代碼段和數(shù)據(jù)段那些對(duì)絕對(duì)地址的引用的位置,這些重定位的信息都會(huì)放在.rela.text中,.rel開(kāi)頭的都是用于重定位。
LINK表示符號(hào)表的下標(biāo),INFO表示它作用于哪個(gè)段,值是相應(yīng)段的下標(biāo)。
字符串表(.strtab):保存普通字符串,比如符號(hào)名字。
段表字符串表(.shstrtab):保存段表中用到的字符串,比如段名。
ELF文件頭和段表都有各自的結(jié)構(gòu)體,這里不列舉,只需要知道它里面存儲(chǔ)的是什么東西就好。
程序?yàn)槭裁匆殖蓴?shù)據(jù)段和代碼段
數(shù)據(jù)和指令被映射到兩個(gè)虛擬內(nèi)存區(qū)域,數(shù)據(jù)段對(duì)進(jìn)程來(lái)說(shuō)可讀寫(xiě),代碼段是只讀,這樣可以防止程序的指令被有意無(wú)意的改寫(xiě)。
有利于提高程序局部性,現(xiàn)代CPU緩存一般被設(shè)計(jì)成數(shù)據(jù)緩存和指令緩存分離,分開(kāi)對(duì)CPU緩存命中率有好處。
代碼段是可以共享的,數(shù)據(jù)段是私有的,當(dāng)運(yùn)行多個(gè)程序的副本時(shí),只需要保存一份代碼段部分。
經(jīng)典語(yǔ)錄:真正了不起的程序員對(duì)自己程序的每一個(gè)字節(jié)都了如指掌。
鏈接器通過(guò)什么進(jìn)行的鏈接
鏈接的接口是符號(hào),在鏈接中,將函數(shù)和變量統(tǒng)稱(chēng)為符號(hào),函數(shù)名和變量名統(tǒng)稱(chēng)為符號(hào)名。鏈接過(guò)程的本質(zhì)就是把多個(gè)不同的目標(biāo)文件之間相互“粘”到一起,像玩具積木一樣各有凹凸部分,有固定的規(guī)則可以拼成一個(gè)整體。
可以將符號(hào)看作是鏈接中的粘合劑,整個(gè)鏈接過(guò)程基于符號(hào)才可以正確完成,符號(hào)有很多類(lèi)型,主要有局部符號(hào)和外部符號(hào),局部符號(hào)只在編譯單元內(nèi)部可見(jiàn),對(duì)于鏈接過(guò)程沒(méi)有作用,在目標(biāo)文件中引用的全局符號(hào),卻沒(méi)有在本目標(biāo)文件中被定義的叫做外部符號(hào),以及定義在本目標(biāo)文件中的可以被其它目標(biāo)文件引用的全局符號(hào),在鏈接過(guò)程中發(fā)揮重要作用。
可以使用一些命令來(lái)查看符號(hào)信息:
command nm:
nm test.oU _GLOBAL_OFFSET_TABLE_0000000000000000 T mainU puts
command objdump:
objdump -t test.otest.o: file format elf64-x86-64SYMBOL TABLE:0000000000000000 l df *ABS* 0000000000000000 test_c.cc0000000000000000 l d .text 0000000000000000 .text0000000000000000 l d .data 0000000000000000 .data0000000000000000 l d .bss 0000000000000000 .bss0000000000000000 l d .rodata 0000000000000000 .rodata0000000000000000 l d .note.GNU-stack 0000000000000000 .note.GNU-stack0000000000000000 l d .eh_frame 0000000000000000 .eh_frame0000000000000000 l d .comment 0000000000000000 .comment0000000000000000 g F .text 0000000000000017 main0000000000000000 *UND* 0000000000000000 _GLOBAL_OFFSET_TABLE_0000000000000000 *UND* 0000000000000000 puts
command readelf:
readelf -s test.oSymbol table '.symtab' contains 12 entries:Num: Value Size Type Bind Vis Ndx Name0: 0000000000000000 0 NOTYPE LOCAL DEFAULT UND1: 0000000000000000 0 FILE LOCAL DEFAULT ABS test_c.cc2: 0000000000000000 0 SECTION LOCAL DEFAULT 13: 0000000000000000 0 SECTION LOCAL DEFAULT 34: 0000000000000000 0 SECTION LOCAL DEFAULT 45: 0000000000000000 0 SECTION LOCAL DEFAULT 56: 0000000000000000 0 SECTION LOCAL DEFAULT 77: 0000000000000000 0 SECTION LOCAL DEFAULT 88: 0000000000000000 0 SECTION LOCAL DEFAULT 69: 0000000000000000 23 FUNC GLOBAL DEFAULT 1 main10: 0000000000000000 0 NOTYPE GLOBAL DEFAULT UND _GLOBAL_OFFSET_TABLE_11: 0000000000000000 0 NOTYPE GLOBAL DEFAULT UND puts
有些符號(hào)在程序中并沒(méi)有被定義,但是可以直接聲明并且引用的符號(hào)稱(chēng)為特殊符號(hào),這些符號(hào)其實(shí)是定義在ld鏈接器腳本中的,如下面代碼中的符號(hào):
extern char __executable_start[];extern char etext[], _etext[], __etext[];extern char edata[], _edata[];extern char end[], _end[];int main() {printf("Executable Start %X \n", __executable_start);printf("Text End %X %X %X \n", etext, _etext, __etext);printf("Data End %X %X \n", edata, _edata);printf("Executable End %X %X \n", end, _end);return 0;}
輸出:
$ ./a.outExecutable Start 68800000Text End 6880075D 6880075D 6880075DData End 68A01010 68A01010Executable End 68A01018 68A01018
為什么需要extern "C"
C語(yǔ)言函數(shù)和變量的符號(hào)名基本就是函數(shù)名字變量名字,不同模塊如果有相同的函數(shù)或變量名字就會(huì)產(chǎn)生符號(hào)沖突無(wú)法鏈接成功的問(wèn)題,所以C++引入了命名空間來(lái)解決這種符號(hào)沖突問(wèn)題。同時(shí)為了支持函數(shù)重載C++也會(huì)根據(jù)函數(shù)名字以及命名空間以及參數(shù)類(lèi)型生成特殊的符號(hào)名稱(chēng)。
由于C語(yǔ)言和C++的符號(hào)修飾方式不同,C語(yǔ)言和C++的目標(biāo)文件在鏈接時(shí)可能會(huì)報(bào)錯(cuò)說(shuō)找不到符號(hào),所以為了C++和C兼容,引入了extern "C",當(dāng)引用某個(gè)C語(yǔ)言的函數(shù)時(shí)加extern "C"告訴編譯器對(duì)此函數(shù)使用C語(yǔ)言的方式來(lái)鏈接,如果C++的函數(shù)用extern "C"聲明,則此函數(shù)的符號(hào)就是按C語(yǔ)言方式生成的。
以memset函數(shù)舉例,C語(yǔ)言中以C語(yǔ)言方式來(lái)鏈接,但是在C++中以C++方式來(lái)鏈接就會(huì)找不到這個(gè)memset的符號(hào),所以需要使用extern "C"方式來(lái)聲明這個(gè)函數(shù),為了兼容C和C++,可以使用宏來(lái)判斷,用條件宏判斷當(dāng)前是不是C++代碼,如果是C++代碼則extern "C"。
extern "C" {void *memset(void *, int, size_t);}
這種技巧幾乎在所有的系統(tǒng)頭文件中都會(huì)被用到。
強(qiáng)符號(hào)和弱符號(hào)
我們經(jīng)常編程中遇到的multiple definition of 'xxx',指的是多個(gè)目標(biāo)中有相同名字的全局符號(hào)的定義,產(chǎn)生了沖突,這種符號(hào)的定義指的是強(qiáng)符號(hào)。有強(qiáng)符號(hào)自然就有弱符號(hào),編譯器默認(rèn)函數(shù)和初始化了的全局變量為強(qiáng)符號(hào),未初始化的全局變量為弱符號(hào)。__attribute__((weak))可以定義弱符號(hào)。
extern int ext;int weak; // 弱符號(hào)int strong = 1; // 強(qiáng)符號(hào)__attribute__((weak)) int weak2 = 2; // 弱符號(hào)int main() {return 0;}
鏈接器規(guī)則:
不允許強(qiáng)符號(hào)被多次定義,多次定義就會(huì)multiple definition of 'xxx'
一個(gè)符號(hào)在一個(gè)目標(biāo)文件中是強(qiáng)符號(hào),在其它目標(biāo)文件中是弱符號(hào),選擇強(qiáng)符號(hào)
一個(gè)符號(hào)在所有目標(biāo)文件中都是弱符號(hào),選擇占用空間最大的符號(hào),int類(lèi)型和double類(lèi)型選double類(lèi)型
強(qiáng)引用和弱引用
一般引用了某個(gè)函數(shù)符號(hào),而這個(gè)函數(shù)在任何地方都沒(méi)有被定義,則會(huì)報(bào)錯(cuò)error: undefined reference to 'xxx',這種符號(hào)引用稱(chēng)為強(qiáng)引用。與此對(duì)應(yīng)的則有弱引用,鏈接器對(duì)強(qiáng)引用弱引用的處理過(guò)程幾乎一樣,只是對(duì)于未定義的弱引用,鏈接器不會(huì)報(bào)錯(cuò),而是默認(rèn)其是一個(gè)特殊的值。
__attribute__ ((weak)) void foo();int main() {foo();return 0;}
這里可以編譯鏈接成功,運(yùn)行此可執(zhí)行程序,會(huì)報(bào)非法地址錯(cuò)誤,所以可以做下面的改進(jìn):
__attribute__ ((weak)) void foo();int main() {if (foo) {foo();}return 0;}
這種強(qiáng)引用弱引用對(duì)于庫(kù)來(lái)說(shuō)十分有用,庫(kù)中的弱引用可以被用戶(hù)定義的強(qiáng)引用所覆蓋,這樣程序就可以使用自定義版本的庫(kù)函數(shù),可以將引用定義為弱引用,如果去掉了某個(gè)功能,也可以正常連接接,想增加相應(yīng)功能還可以直接增加強(qiáng)引用,方便程序的裁剪和組合。
如下:
// test2.cvoid foo() {printf("foo2\n");}
// test3.cvoid foo() {printf("foo3\n");}
使用如下方式鏈接:
gcc test.c -o a.out./a.out什么都不會(huì)輸出gcc test.c test2.c -o a.out./a.outfoo2gcc test.c test3.c -o a.out./a.outfoo3
對(duì)于弱符號(hào)和弱引用,其都僅是GNU工具鏈GCC對(duì)C語(yǔ)言語(yǔ)法的擴(kuò)展,并不是C本身的語(yǔ)言特性。
參考
《程序員的自我修養(yǎng)---鏈接、裝載與庫(kù)》
https://blog.csdn.net/guyongqiangx/article/details/53067434?locationNum=6&fps=1
https://blog.csdn.net/Move_now/article/details/69307890
免責(zé)聲明:本文內(nèi)容由21ic獲得授權(quán)后發(fā)布,版權(quán)歸原作者所有,本平臺(tái)僅提供信息存儲(chǔ)服務(wù)。文章僅代表作者個(gè)人觀(guān)點(diǎn),不代表本平臺(tái)立場(chǎng),如有問(wèn)題,請(qǐng)聯(lián)系我們,謝謝!






