谈谈程序链接及分段那些事
談談程序鏈接及分段那些事
如果讀過我之前的文章就會知道,程序構建大概需要經(jīng)歷四個過程:預處理、編譯、匯編、鏈接,這里主要介紹鏈接這一過程。
鏈接鏈的是什么?
鏈接鏈的就是目標文件,什么是目標文件?目標文件就是源代碼編譯后但未進行鏈接的那些中間文件,如Linux下的.o,它和可執(zhí)行文件的內(nèi)容和結構很相似,格式幾乎是一樣的,可以看成是同一種類型的文件,Linux下統(tǒng)稱為ELF文件,這里介紹下ELF文件標準:
- 可重定位文件:Linux中的.o,這類文件包含代碼和數(shù)據(jù),可被鏈接成可執(zhí)行文件或共享目標文件,例如靜態(tài)鏈接庫。
- 可執(zhí)行文件:可以直接執(zhí)行的文件,如/bin/bash文件。
- 共享目標文件:Linux中的.so,包含代碼和數(shù)據(jù),一種是鏈接器可以使用這種文件和其它的可重定位文件和共享目標文件鏈接,另一種是動態(tài)鏈接器可以將幾個這種共享目標文件和可執(zhí)行文件結合,作為進程映像的一部分來執(zhí)行。
- core dump文件:進程意外終止時,系統(tǒng)可以將該進程的地址空間的內(nèi)容和其它信息存到coredump文件用于調(diào)試,如gdb。
我們可以使用command file來查看文件的格式:
file test.o; file /bin/bash; file test.o test.o: ELF 64-bit LSB relocatable, x86-64, version 1 (SYSV), not stripped目標文件的構成
目標文件主要分為文件頭、代碼段、數(shù)據(jù)段和其它。
-
文件頭:描述整個文件的文件屬性(文件是否可執(zhí)行、是靜態(tài)鏈接還是動態(tài)鏈接、入口地址、目標硬件、目標操作系統(tǒng)等信息),還包括段表,用來描述文件中各個段的數(shù)組,描述文件中各個段在文件中的偏移位置和段屬性。
-
代碼段:程序源代碼編譯后的機器指令。
-
數(shù)據(jù)段:數(shù)據(jù)段分為.data段和.bss段。
-
.data段內(nèi)容:已經(jīng)初始化的全局變量和局部靜態(tài)變量
-
.bss段內(nèi)容:未初始化的全局變量和局部靜態(tài)變量,.bss段只是為未初始化的全局變量和局部靜態(tài)變量預留位置,本身沒有內(nèi)容,不占用空間。
-
除了代碼段和數(shù)據(jù)段,還有.rodata段、.comment、字符串表、符號表和堆棧提示段等等,還可以自定義段。
.bss段不占用存儲空間?
看下面代碼:
#include <stdio.h>int a[1000]; int b[1000] = {1};int main() {printf("程序喵\n");return 0; }我們查看下文件大小和各個段大小:
$ gcc testlink.c -o test $ ls -l test -rwxrwxrwx 1 wzq wzq 12368 May 30 08:48 test $ size test text data bss dec hex filename 1512 4616 4032 10160 27b0 test再看這段初始化的代碼:
#include <stdio.h>int a[1000] = {1}; int b[1000] = {1};int main() {printf("程序喵\n");return 0; }再查看下文件大小和各個段大小:
$ gcc testlink.c -o test $ ls -l test -rwxrwxrwx 1 wzq wzq 16368 May 30 08:49 test $ size test text data bss dec hex filename 1512 8616 8 10136 2798 test可以看到僅僅是做了一次初始化,文件大小就從12368變成了16368,正好是初始化了的那a[1000]的大小,這4000字節(jié)從.bss段移動到了.data段,程序大小增加了,這里可以看出.bss段不占據(jù)磁盤空間。
既然.bss段不占據(jù)空間,那它的大小和符號存在哪呢?
-
.bss段占據(jù)的大小存放在ELF文件格式中的段表(Section Table)中,段表存放了各個段的各種信息,比如段的名字、段的類型、段在elf文件中的偏移、段的大小等信息。同時符號存放在符號表.symtab中。
-
.bss不占據(jù)實際的磁盤空間,只在段表中記錄大小,在符號表中記錄符號。當文件加載運行時,才分配空間以及初始化。
其實程序里還有好多系統(tǒng)保留段,還可以自定義段,將某個變量放在自定義段,如下:
可以使用一些工具查看ELF文件頭以及各個段的內(nèi)容:
gcc -c testlink.c -o test.oreadelf -h test.o ELF 頭:Magic: 7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00 類別: ELF64數(shù)據(jù): 2 補碼,小端序 (little endian)版本: 1 (current)OS/ABI: UNIX - System VABI 版本: 0類型: REL (可重定位文件)系統(tǒng)架構: Advanced Micro Devices X86-64版本: 0x1入口點地址: 0x0程序頭起點: 0 (bytes into file)Start of section headers: 8768 (bytes into file)標志: 0x0本頭的大小: 64 (字節(jié))程序頭大小: 0 (字節(jié))Number of program headers: 0節(jié)頭大小: 64 (字節(jié))節(jié)頭數(shù)量: 13字符串表索引節(jié)頭: 12查看段表的方法:
使用objdump查看ELF文件中包含的關鍵的段:
objdump -h test.o test.o: 文件格式 elf64-x86-64節(jié): Idx Name Size VMA LMA File off Algn0 .text 00000017 0000000000000000 0000000000000000 00000040 2**0CONTENTS, ALLOC, LOAD, RELOC, READONLY, CODE1 .data 00001f40 0000000000000000 0000000000000000 00000060 2**5CONTENTS, ALLOC, LOAD, DATA2 .bss 00000000 0000000000000000 0000000000000000 00001fa0 2**0ALLOC3 .rodata 0000000a 0000000000000000 0000000000000000 00001fa0 2**0CONTENTS, ALLOC, LOAD, READONLY, DATA4 .comment 0000002a 0000000000000000 0000000000000000 00001faa 2**0CONTENTS, READONLY5 .note.GNU-stack 00000000 0000000000000000 0000000000000000 00001fd4 2**0CONTENTS, READONLY6 .eh_frame 00000038 0000000000000000 0000000000000000 00001fd8 2**3CONTENTS, ALLOC, LOAD, RELOC, READONLY, DATA readelf -S test.o readelf -S test.o There are 13 section headers, starting at offset 0x2240:節(jié)頭:[號] 名稱 類型 地址 偏移量大小 全體大小 旗標 鏈接 信息 對齊[ 0] NULL 0000000000000000 000000000000000000000000 0000000000000000 0 0 0[ 1] .text PROGBITS 0000000000000000 000000400000000000000017 0000000000000000 AX 0 0 1[ 2] .rela.text RELA 0000000000000000 000021900000000000000030 0000000000000018 I 10 1 8[ 3] .data PROGBITS 0000000000000000 000000600000000000001f40 0000000000000000 WA 0 0 32[ 4] .bss NOBITS 0000000000000000 00001fa00000000000000000 0000000000000000 WA 0 0 1[ 5] .rodata PROGBITS 0000000000000000 00001fa0000000000000000a 0000000000000000 A 0 0 1[ 6] .comment PROGBITS 0000000000000000 00001faa000000000000002a 0000000000000001 MS 0 0 1[ 7] .note.GNU-stack PROGBITS 0000000000000000 00001fd40000000000000000 0000000000000000 0 0 1[ 8] .eh_frame PROGBITS 0000000000000000 00001fd80000000000000038 0000000000000000 A 0 0 8[ 9] .rela.eh_frame RELA 0000000000000000 000021c00000000000000018 0000000000000018 I 10 8 8[10] .symtab SYMTAB 0000000000000000 000020100000000000000150 0000000000000018 11 9 8[11] .strtab STRTAB 0000000000000000 000021600000000000000030 0000000000000000 0 0 1[12] .shstrtab STRTAB 0000000000000000 000021d80000000000000061 0000000000000000 0 0 1 Key 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只能查看關鍵的段,而readelf可以查看所有段。
其中,.rela.text是針對.text段的重定位表,鏈接器在處理目標文件時,需要對目標文件中的某些部位進行重定位,即代碼段和數(shù)據(jù)段那些對絕對地址的引用的位置,這些重定位的信息都會放在.rela.text中,.rel開頭的都是用于重定位。
-
LINK表示符號表的下標,INFO表示它作用于哪個段,值是相應段的下標。
-
字符串表(.strtab):保存普通字符串,比如符號名字。
-
段表字符串表(.shstrtab):保存段表中用到的字符串,比如段名。
-
ELF文件頭和段表都有各自的結構體,這里不列舉,只需要知道它里面存儲的是什么東西就好。
程序為什么要分成數(shù)據(jù)段和代碼段
-
數(shù)據(jù)和指令被映射到兩個虛擬內(nèi)存區(qū)域,數(shù)據(jù)段對進程來說可讀寫,代碼段是只讀,這樣可以防止程序的指令被有意無意的改寫。
-
有利于提高程序局部性,現(xiàn)代CPU緩存一般被設計成數(shù)據(jù)緩存和指令緩存分離,分開對CPU緩存命中率有好處。
-
代碼段是可以共享的,數(shù)據(jù)段是私有的,當運行多個程序的副本時,只需要保存一份代碼段部分。
經(jīng)典語錄:真正了不起的程序員對自己程序的每一個字節(jié)都了如指掌。
鏈接器通過什么進行的鏈接
鏈接的接口是符號,在鏈接中,將函數(shù)和變量統(tǒng)稱為符號,函數(shù)名和變量名統(tǒng)稱為符號名。鏈接過程的本質(zhì)就是把多個不同的目標文件之間相互“粘”到一起,像玩具積木一樣各有凹凸部分,有固定的規(guī)則可以拼成一個整體。
可以將符號看作是鏈接中的粘合劑,整個鏈接過程基于符號才可以正確完成,符號有很多類型,主要有局部符號和外部符號,局部符號只在編譯單元內(nèi)部可見,對于鏈接過程沒有作用,在目標文件中引用的全局符號,卻沒有在本目標文件中被定義的叫做外部符號,以及定義在本目標文件中的可以被其它目標文件引用的全局符號,在鏈接過程中發(fā)揮重要作用。
可以使用一些命令來查看符號信息:
command nm:
command readelf:
readelf -s test.o Symbol table '.symtab' contains 14 entries:Num: Value Size Type Bind Vis Ndx Name0: 0000000000000000 0 NOTYPE LOCAL DEFAULT UND 1: 0000000000000000 0 FILE LOCAL DEFAULT ABS testlink.c2: 0000000000000000 0 SECTION LOCAL DEFAULT 1 3: 0000000000000000 0 SECTION LOCAL DEFAULT 3 4: 0000000000000000 0 SECTION LOCAL DEFAULT 4 5: 0000000000000000 0 SECTION LOCAL DEFAULT 5 6: 0000000000000000 0 SECTION LOCAL DEFAULT 7 7: 0000000000000000 0 SECTION LOCAL DEFAULT 8 8: 0000000000000000 0 SECTION LOCAL DEFAULT 6 9: 0000000000000000 4000 OBJECT GLOBAL DEFAULT 3 a10: 0000000000000fa0 4000 OBJECT GLOBAL DEFAULT 3 b11: 0000000000000000 23 FUNC GLOBAL DEFAULT 1 main12: 0000000000000000 0 NOTYPE GLOBAL DEFAULT UND _GLOBAL_OFFSET_TABLE_13: 0000000000000000 0 NOTYPE GLOBAL DEFAULT UND puts有些符號在程序中并沒有被定義,但是可以直接聲明并且引用的符號稱為特殊符號,這些符號其實是定義在ld鏈接器腳本中的,如下面代碼中的符號:
#include <stdio.h>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.out Executable Start 68800000 Text End 6880075D 6880075D 6880075D Data End 68A01010 68A01010 Executable End 68A01018 68A01018為什么需要extern “C”
C語言函數(shù)和變量的符號名基本就是函數(shù)名字變量名字,不同模塊如果有相同的函數(shù)或變量名字就會產(chǎn)生符號沖突無法鏈接成功的問題,所以C++引入了命名空間來解決這種符號沖突問題。同時為了支持函數(shù)重載C++也會根據(jù)函數(shù)名字以及命名空間以及參數(shù)類型生成特殊的符號名稱。
由于C語言和C++的符號修飾方式不同,C語言和C++的目標文件在鏈接時可能會報錯說找不到符號,所以為了C++和C兼容,引入了extern “C”,當引用某個C語言的函數(shù)時加extern "C"告訴編譯器對此函數(shù)使用C語言的方式來鏈接,如果C++的函數(shù)用extern "C"聲明,則此函數(shù)的符號就是按C語言方式生成的。
以memset函數(shù)舉例,C語言中以C語言方式來鏈接,但是在C++中以C++方式來鏈接就會找不到這個memset的符號,所以需要使用extern "C"方式來聲明這個函數(shù),為了兼容C和C++,可以使用宏來判斷,用條件宏判斷當前是不是C++代碼,如果是C++代碼則extern “C”。
#ifdef __cplusplus extern "C" { #endifvoid *memset(void *, int, size_t);#ifdef __cplusplus } #endif這種技巧幾乎在所有的系統(tǒng)頭文件中都會被用到。
強符號和弱符號
我們經(jīng)常編程中遇到的multiple definition of ‘xxx’,指的是多個目標中有相同名字的全局符號的定義,產(chǎn)生了沖突,這種符號的定義指的是強符號。有強符號自然就有弱符號,編譯器默認函數(shù)和初始化了的全局變量為強符號,未初始化的全局變量為弱符號。attribute((weak))可以定義弱符號。
extern int ext;int weak; // 弱符號 int strong = 1; // 強符號 __attribute__((weak)) int weak2 = 2; // 弱符號int main() {return 0; }鏈接器規(guī)則:
- 不允許強符號被多次定義,多次定義就會multiple definition of ‘xxx’
- 一個符號在一個目標文件中是強符號,在其它目標文件中是弱符號,選擇強符號
- 一個符號在所有目標文件中都是弱符號,選擇占用空間最大的符號,int類型和double類型選double類型
強引用和弱引用
一般引用了某個函數(shù)符號,而這個函數(shù)在任何地方都沒有被定義,則會報錯error: undefined reference to ‘xxx’,這種符號引用稱為強引用。與此對應的則有弱引用,鏈接器對強引用弱引用的處理過程幾乎一樣,只是對于未定義的弱引用,鏈接器不會報錯,而是默認其是一個特殊的值。
總結
以上是生活随笔為你收集整理的谈谈程序链接及分段那些事的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: linux中的nm命令
- 下一篇: Linux可执行文件如何装载进虚拟内存