上一篇介紹了編譯連接的過程,提到了目標(biāo)文件是通過匯編過程生成的,最終鏈接生成可執(zhí)行文件,這篇介紹一下目標(biāo)文件里面到底有什么。
本文導(dǎo)圖

格式概述
目標(biāo)文件相對于最終的可執(zhí)行文件而言,結(jié)構(gòu)上已經(jīng)和可執(zhí)行文件的結(jié)構(gòu)基本一樣了。只是還沒有經(jīng)過鏈接過程,某些符號、地址還沒被重定位,大部分內(nèi)容都已經(jīng)具備了。
可執(zhí)行文件的格式在Windows(PE-Portable Executable)和Linux(ELF- Executable Linkable Format)、Mac(Mach-O)不同,前兩者都是基于COFF(Common file format)格式的,除此之外還有其他不常見的,如Intel/Microsoft的OMF、Unix a.out、MS-Doc .Com格式等。
廣義上將,目標(biāo)文件與可執(zhí)行文件格式幾乎一樣,可以看成同一種類型的文件。
動態(tài)鏈接庫(window .dll、Linux .so)及靜態(tài)鏈接庫(window lib、Linux .a)、Mac(dylb,tbd)都是按照可執(zhí)行文件的格式存儲。靜態(tài)庫稍微不同,因為他是把很多目標(biāo)文件集合在一起,需要在頭部加上一些索引。也就是包含了很多目標(biāo)文件的一個目標(biāo)文件包。
ELF格式文件更可以做如下歸納(目標(biāo)文件.o 靜態(tài)庫、可執(zhí)行文件、動態(tài)庫):


查看格式
file命令查看相應(yīng)的文件格式。
Mac下

Linux下

下面以ELF結(jié)構(gòu)作為分析
淺析目標(biāo)文件內(nèi)部結(jié)構(gòu)
目標(biāo)文件至少包含編譯后的機(jī)器指令代碼、數(shù)據(jù)、和鏈接所需要的信息比如、符號表、調(diào)試信息、字符串等,下面的內(nèi)容圍繞著這幾個部分進(jìn)行總結(jié)。
按照信息的不同屬性以節(jié)(Section)也叫段(Segment)的形式存儲,表示一個一定長度 的區(qū)域。這些區(qū)域里面分別存儲了上訴編譯后的信息。
- 代碼段:編譯之后的機(jī)器指令放在代碼段(Code Section),一般是叫
.code或.text。 - 數(shù)據(jù)段:全局變量和靜態(tài)變量數(shù)據(jù)放在數(shù)據(jù)段(Data Section),一般叫
.data - BBS段:未初始化的全局變量、未初始化靜態(tài)變量放在
.bbs段。
具體來講,下面代碼與目標(biāo)文件對應(yīng)關(guān)系:

- 文件頭:ELF文件有個文件頭,描述整個文件的文件屬性,如是否可執(zhí)行、靜態(tài)庫還是動態(tài)庫、及入口地址(可執(zhí)行文件)、目標(biāo)硬件、操作系統(tǒng)等。其中還包含一個段表,段表示描述各個段的一個數(shù)組,包含各個段在文件的偏移位置、段的屬性。文件頭后面就是各個段的內(nèi)容,比如代碼段保存指令,數(shù)據(jù)段保存數(shù)據(jù)?!渲卸伪矸浅V匾?/li>
未初始化的全局變量、局部靜態(tài)變量雖然默認(rèn)值都是0,但是沒必要在
.data段為它們分配空間(浪費),存放0是沒必要的。所以放在.bbs段,也就給他們預(yù)留個位置而已,沒有內(nèi)容,所以在文件中不占空間。
總體來說,程序代碼被編譯后分成兩種段,程序指令(代碼段)和程序數(shù)據(jù)(數(shù)據(jù)端及.BSS)
指令和數(shù)據(jù)分開的好處:
- 指令和數(shù)據(jù)分別映射到兩個虛擬區(qū)域,數(shù)據(jù)可讀可寫,而指令是只讀的,可以設(shè)值區(qū)域的權(quán)限,防止惡意修改程序指令。——權(quán)限,安全
- 分開有利于程序的局部性(在一段時間內(nèi),整個程序的執(zhí)行僅限于程序中的某一部分。相應(yīng)地,執(zhí)行所訪問的存儲空間也局限于某個內(nèi)存區(qū)域。)CPU有幾大的緩存體系?!植啃栽?/li>
- 復(fù)用,如果系統(tǒng)有多個程序的副本,那么指令都是一樣的。這個也是動態(tài)庫非常大的優(yōu)勢,節(jié)省內(nèi)存開銷——復(fù)用、復(fù)用?。。?/li>
實驗?zāi)繕?biāo)文件內(nèi)容
實驗代碼:


使用objdump -h 查看目標(biāo)文件內(nèi)部結(jié)構(gòu)。
在Mac上實驗結(jié)果
objdump -h hello.o
hello.o: file format Mach-O 64-bit x86-64
Sections:
Idx Name Size Address Type
0 __text 00000067 0000000000000000 TEXT
1 __data 00000008 0000000000000068 DATA
2 __cstring 00000004 0000000000000070 DATA
3 __bss 00000004 0000000000000120 BSS
4 __compact_unwind 00000040 0000000000000078 DATA
5 __eh_frame 00000068 00000000000000b8 DATA
在Linux實驗結(jié)果

除了之前講到的三個段,這里多了只讀數(shù)據(jù)端,注釋端,堆棧提示段。其中包含了一些屬性關(guān)鍵詞,如長度(size)、端的位置(File off)、該段是否在文件中存在(Contents)、段的各種屬性(Alloc)。可以看到.bbs段沒有contents所以在文件中沒有內(nèi)容。而堆棧提示段size為0。
將上面的內(nèi)容整理一下:

可以通過size命令查看可執(zhí)行文件的各個端大?。ㄗ⒁鈊ec所有段的十進(jìn)制和,hex是16進(jìn)制的和)
Mac上實驗(文件格式是Mach-o)

Linux上實驗(文件格式是ELF)

深入目標(biāo)文件內(nèi)部結(jié)構(gòu)
下面將深入了解目標(biāo)文件中的各個段,他們的作用及含義。
核心段
上面提到過核心段有代碼段、數(shù)據(jù)端、BBS段。下面分別介紹!
代碼段
代碼段可以使用objdump的-s參數(shù)打印出來,-d可以將包含指令的段反匯編信息。
- 最左邊是偏移地址,緊跟著的是16進(jìn)制信息(每兩個數(shù)字為一個字節(jié),一般都是以字節(jié)位單位查看。)
Mac上實驗(文件格式是Mach-o):最前面的部分是反匯編內(nèi)容,接下來是各個段的內(nèi)容。
$ objdump -s -d hello.o
hello.o: file format Mach-O 64-bit x86-64
Disassembly of section __TEXT,__text:
_fun1:
0: 55 pushq %rbp
1: 48 89 e5 movq %rsp, %rbp
4: 48 83 ec 10 subq $16, %rsp
8: 48 8d 05 61 00 00 00 leaq 97(%rip), %rax
f: 89 7d fc movl %edi, -4(%rbp)
12: 8b 75 fc movl -4(%rbp), %esi
15: 48 89 c7 movq %rax, %rdi
18: b0 00 movb $0, %al
1a: e8 00 00 00 00 callq 0 <_fun1+0x1F>
1f: 89 45 f8 movl %eax, -8(%rbp)
22: 48 83 c4 10 addq $16, %rsp
26: 5d popq %rbp
27: c3 retq
28: 0f 1f 84 00 00 00 00 00 nopl (%rax,%rax)
_main:
30: 55 pushq %rbp
31: 48 89 e5 movq %rsp, %rbp
34: 48 83 ec 10 subq $16, %rsp
38: c7 45 fc 00 00 00 00 movl $0, -4(%rbp)
3f: c7 45 f8 6f 00 00 00 movl $111, -8(%rbp)
46: 8b 05 00 00 00 00 movl (%rip), %eax
4c: 03 05 00 00 00 00 addl (%rip), %eax
52: 03 45 f8 addl -8(%rbp), %eax
55: 03 45 f4 addl -12(%rbp), %eax
58: 89 c7 movl %eax, %edi
5a: e8 00 00 00 00 callq 0 <_main+0x2F>
5f: 31 c0 xorl %eax, %eax
61: 48 83 c4 10 addq $16, %rsp
65: 5d popq %rbp
66: c3 retq
Contents of section __text:
0000 554889e5 4883ec10 488d0561 00000089 UH..H...H..a....
0010 7dfc8b75 fc4889c7 b000e800 00000089 }..u.H..........
0020 45f84883 c4105dc3 0f1f8400 00000000 E.H...].........
0030 554889e5 4883ec10 c745fc00 000000c7 UH..H....E......
0040 45f86f00 00008b05 00000000 03050000 E.o.............
0050 00000345 f80345f4 89c7e800 00000031 ...E..E........1
0060 c04883c4 105dc3 .H...].
Contents of section __data:
0068 54000000 55000000 T...U...
Contents of section __cstring:
0070 25640a00 %d..
Contents of section __bss:
<skipping contents of bss section at [0120, 0124)>
Contents of section __compact_unwind:
0078 00000000 00000000 28000000 00000001 ........(.......
0088 00000000 00000000 00000000 00000000 ................
0098 30000000 00000000 37000000 00000001 0.......7.......
00a8 00000000 00000000 00000000 00000000 ................
Contents of section __eh_frame:
00b8 14000000 00000000 017a5200 01781001 .........zR..x..
00c8 100c0708 90010000 24000000 1c000000 ........$.......
00d8 28ffffff ffffffff 28000000 00000000 (.......(.......
00e8 00410e10 8602430d 06000000 00000000 .A....C.........
00f8 24000000 44000000 30ffffff ffffffff $...D...0.......
0108 37000000 00000000 00410e10 8602430d 7........A....C.
0118 06000000 00000000 ........
# instanza @ InstanzadeMacBook-Pro in ~/Desktop/Temp [12:12:42]
$
Linux實驗(文件格式是ELF)

-
contents of section .text就是數(shù)據(jù)已十六進(jìn)制打印出來的內(nèi)容。中間4列是16進(jìn)制內(nèi)容,最右邊是.text段的ASCII碼形式。 -
.text段包含是.c文件兩個函數(shù)的指令,比如第一個字節(jié)是0x55,就是func1()函數(shù)第一條push %ebp指令,最后一個0xc3代表main函數(shù)最后一個指令ret
數(shù)據(jù)段
.data段保存了初始化的全局靜態(tài)變量和局部靜態(tài)變量。上面代碼中有兩個這樣的變量,每個變量4個字節(jié)一共8個字節(jié)。所以.data這個段大小為8個字節(jié)?!?strong>這是個非常準(zhǔn)確的計算,不會存在多一個、少一個字節(jié)的情況
注意printf的時候,有一個字符串常量“%d\n”。在linux中放到了.rodata段,分別有四個字符%、d、\n(換行符)、空字符。一個字符一個字節(jié)(8位ASCII碼),所以一共四個字節(jié)。在Mac上放到了字符串常量區(qū)。
以Mac下例子為例:
Contents of section __cstring:
0070 25640a00 %d..
通過查ASCII表,得到對應(yīng)情況:%——25,d——64,\n(換行符)——0a,空字符(NULL)——00。剛好和字符串常量區(qū)對應(yīng)
在來看一起.data段(Mac下):
0060 c04883c4 105dc3 .H...].
Contents of section __data:
0068 54000000 55000000 T...U...
根據(jù)偏移范圍可以知道.data端一共8個字節(jié)。和前面用size看到的不一樣,size看到的是12個字節(jié)。
Linux下

可以看到Linux也是8個字節(jié)。字節(jié)從低到高分別是0x54、0x00、0x00、0x00。剛好是十進(jìn)制的84,特別注意這里的順序,字節(jié)從低到高還是從高到底排列涉及到CPU的字節(jié)序問題,也就是大端小端。后面四個字節(jié)也剛好是85
BBS段
Mac中的bss段如下:
Contents of section __bss:
<skipping contents of bss section at [0120, 0124)>
可以知道具體的值放到了0120到0124之間,一共四個字節(jié)。
Linux下

同樣也是四個字節(jié)。和global_uninit_var、static_var2合起來大小8個字節(jié)不符。
BBS段最終是通過符號表決定的。在Linux下只有static_var2存放到了bbs段。而global_uninit_var沒有存放到任何段。只是一個未定義的common符號,這和語言及編譯器實現(xiàn)有關(guān)。有一點可以明確——編譯單元內(nèi)部可見的靜態(tài)變量是存在bbs段的。
其他段
ELF中除了.text、.data、.bbs三個最常用的段還有其他段。具體如下:

以
.開頭的都是系統(tǒng)保留的。如果想要自定義端,則不能以.開頭。
GCC提供了擴(kuò)展機(jī)制,可以將變量或者代碼放到你指定的段中去。用以下方式實現(xiàn):

以上只是ELF文件的輪廓,下面用一張圖總結(jié):

幾點說明一下:
- 文件頭:描述怎個文件的基本屬性,文件版本,目標(biāo)機(jī)器型號,程序入口地址。
- 段表:描述了ELF文件包含所有段的信息,比如每個段的段名,長度,文件中的偏移,讀寫權(quán)限,和其他屬性。
下面開始從文件頭開始
文件頭
Linux用readelf來查看文件頭,Mac用otool查看(otool -h hello.o)
在Linux中


系統(tǒng)定義文件頭數(shù)據(jù)結(jié)構(gòu):
#define EI_NIDENT 16
typedef struct {
unsigned char e_ident[EI_NIDENT];
Elf32_Half e_type;
Elf32_Half e_machine;
Elf32_Word e_version;
Elf32_Addr e_entry;
Elf32_Off e_phoff;
Elf32_Off e_shoff;
Elf32_Word e_flags;
Elf32_Half e_ehsize;
Elf32_Half e_phentsize;
Elf32_Half e_phnum;
Elf32_Half e_shentsize;
Elf32_Half e_shnum;
Elf32_Half e_shstrndx;
} Elf32_Ehdr;
各個字段代表什么意思根據(jù)名稱就知道了。關(guān)鍵要知道在哪里定義這些常數(shù)的。ELF文件頭定義在user/include/elf。mach.o類型的目標(biāo)文件也有對應(yīng)的頭文件。
下圖是對readelf和ELF頭文件定義的各個成員的含義解釋。這里先大致了解下,后面會詳解!


ELF魔數(shù)(e_ident前四個字節(jié))
通過readelf之后看到最前面的16個字節(jié)剛好是e_ident,這16個ELF標(biāo)準(zhǔn)用來標(biāo)識ELF文件平臺屬性,比如ELF字長(32位、64位),字節(jié)序、文件版本等。
下表是對e_ident數(shù)組各個成員的說明:

- 前四個字節(jié)是所有ELF文件必須相同的標(biāo)識碼:0x7F、0x45、0x4c、0x46。第一個字節(jié)賭贏DEL控制符,后三個對應(yīng)ELF。所有的可執(zhí)行文件最開始的幾個字節(jié)都是魔數(shù),用來確認(rèn)文件類型,操作系統(tǒng)在加載的時候會確認(rèn)魔數(shù)是否正確,不正確就不會加載。

- 下一個字節(jié)標(biāo)識文件類型;0x01標(biāo)識32,0x02標(biāo)識64
- 第六個字節(jié)是字節(jié)序
- 第七個字節(jié):規(guī)定ELF文本版本
- 后面9個字節(jié):標(biāo)準(zhǔn)還沒有定義,一般填0
文件類型
e_type表示文件類型,之前提到過3種ELF文件類型。如下表

機(jī)器類型
e_machine表示平臺屬性

段表
ELF最終段結(jié)構(gòu)由段表決定,編譯器、鏈接器和 裝載器都是依賴段表來定位和訪問各個端的屬性。段表的位置有ELF文件頭的e_shoff決定,如上面的例子段表的偏移為0x118。
直接來查看所有的段信息

這里的數(shù)據(jù)其實在代碼層面來講都是由對應(yīng)的數(shù)據(jù)結(jié)構(gòu)的。無論是mach.o還是ELF。比如這里的Elf32_Shdr結(jié)構(gòu)體是對一個段的封裝,段表也就是由Elf32_Shdr組成的數(shù)組。對上圖而言一個有11個這樣的元素。
段表元素數(shù)據(jù)結(jié)構(gòu)

typedef struct {
Elf32_Word sh_name;
Elf32_Word sh_type;
Elf32_Word sh_flags;
Elf32_Addr sh_addr;
Elf32_Off sh_offset;
Elf32_Word sh_size;
Elf32_Word sh_link;
Elf32_Word sh_info;
Elf32_Word sh_
各個字段的含義

現(xiàn)在把整個.o文件所有段的位置及長度都分析清楚了。
- 段表長度為0x1b8,一共440個字節(jié),包含11個段描述符,每個段描述符為40個字節(jié),這個長短剛好是結(jié)構(gòu)Elf32_Shdr的長度。
- 文件最后一段.re.text結(jié)束后,長度為0x450也就是1104個字節(jié),剛好是.o文件的大小。
中間有段表和.rel.text都因為對齊的原因,與前面的短之間有一個字節(jié)和兩個字節(jié)的間隔。注意這里是為了對齊。

段類型(sh_type、sh_flags)
段名(sh_name)只有在編譯、鏈接過程有意義,但不能真正表示段的類型,段名(sh_name)其實只是一個索引指向字符串表(SHT_STRTAB)的某個位置,在編譯器和鏈接器中起作用的是段類型字段和段標(biāo)志位字段。
段類型:

段標(biāo)志位

系統(tǒng)保留段


段的鏈接信息(sh_link、sh_info)
段的類型與鏈接相關(guān)的話都需要sh_link、sh_info,無論是動態(tài)庫還是靜態(tài)庫。比如重定位表,符號表。對于其他段這兩個成員沒意義

重定位表(段類型SHT_REL)
在剛才的段中,有一個.rel.text的短,類型(sh_type)為SHT_REL,也就是這個段包含的是重定位表。
鏈接器在處理目標(biāo)文件的時候,需要對目標(biāo)文件某些部分重定位,雖然在代碼段和數(shù)據(jù)端中用的是絕對地址的引用位置。重定位信息都記錄在重定位表里面,每一個需要重定位的代碼段或數(shù)據(jù)段都會有一個相應(yīng)的重定位表,比如上面的
.rel.text就是對.text的重定位表。因為.text段有一個絕對地址的引用,就是printf函數(shù)的調(diào)用。這里的決定地址引用也就是寫死的地址。而.data段沒有絕對地址引用。
字符串表(段類型SHT_STRTAB)
ELF用到很多字符串 ,如段名、變量名、因為字符串長度不確定,所以固定的解構(gòu)表示比較困難。一種很常見做法就是把字符串集中放到一個表中,使用字符串的時候在表中的偏移來引用字符串。如下圖

字符串表一般有兩種一種用來保存普通的字符串,比如符號的名字;另一種段表字符串用來來保存段表中用到的字符串,比如段表名。字符串表中包含有若干個以’null’結(jié)尾的字符序列。
只要分析ELF頭文件,就可以得到段表和段表字符串表的位置,從而解析整個ELF文件。
符號表(symbol table)
符號
假如B文件要用到A文件的函數(shù)foo,那么在目標(biāo)文件A定義了函數(shù)foo,目標(biāo)文件B引用了A中的foo函數(shù)。每個函數(shù)、變量都有自己的名字,將函數(shù)名、變量名統(tǒng)稱為符號名。
每一個目標(biāo)文件都會一個相應(yīng)的符號表,記錄了目標(biāo)文件所要用到的符號,每個定義的符號有一個對應(yīng)的值,叫做符號值,對于變量和函數(shù)來說,符號值就是他們的地址。出了變量和函數(shù)外還有其他幾種符號,可以將符號表中的符號進(jìn)行分類。
符號表包含的信息用于定位和重定位程序中的符號定義和引用。目標(biāo)文件的其它部分通過一個符號在這個表中的索引值來使用 該符號。索引值從 0 開始計數(shù),但值為 0 的表項(即第一項)并沒有實際的意義, 它表示未定義的符號。這里用常量 STN_UNDEF 來表示未定義的符號。
符號一般有如下類型:
- 定義在目標(biāo)文件的全局符號:可以被其他目標(biāo)文件引用。如上面的func1,main,global_init_var
- 引用其他目標(biāo)文件中的全局符號:一般叫做外部符號,如 printf
- 段名:由編譯器產(chǎn)生,它的值就是該段的其實起始地址。如
.text、.data。 - 局部符號:只在編譯單元內(nèi)部可見,如static_var、static_var2。調(diào)試器可以是用哪個這些符號來分析程序。對于鏈接沒什么作用,鏈接器也會忽略他們。
- 行號符號:目標(biāo)文件指令與源代碼中代碼行的對應(yīng)關(guān)系。
可以使用nm查看符號表的內(nèi)容
Mac上

Linux上

符號表結(jié)構(gòu)(SHT_SYMTAB)
符號表示文件中的一個段,段名.symtab。符號表數(shù)據(jù)結(jié)構(gòu)比較簡單Elf32_sym(符號表項),每個結(jié)構(gòu)體對應(yīng)一個符號。
其結(jié)構(gòu)體如下:
typedef struct {
Elf32_Word st_name;
Elf32_Addr st_value;
Elf32_Word st_size;
unsigned char st_info;
unsigned char st_other;
Elf32_Half st_shndx;

各個字段的含義

符號類型和屬性(st_info)
由低4位表示符號類型,高28位表示符號屬性信息。

符號所在段(st_shndx)
如果符號定義在本目標(biāo)文件,則這個成員表示符號所載的段在段表中的下標(biāo)。因為符號是為段而定義,在段中被引用。本數(shù)據(jù)成員即指明了相關(guān)聯(lián)的段。本數(shù)據(jù)成員是一個索引值,它指向相關(guān)聯(lián)的段在段表中的索引。在重定位過程中,段的位置會改變,本數(shù)據(jù)成員的值也隨之改變,繼續(xù)指向段的新位置。
如果沒有定義在本目標(biāo)文件,則sh_shndx有些特殊。

符號值(st_value)
每個符號都有一個對應(yīng)的值。如果是一個變量或者函數(shù),符號值就是其地址。總的來說有以下幾種情況:
- 如果是符號定義且不是
Common塊類型(st_shndx不為SHN_COMMON),則st_value表示該符號在段中的偏移,即符號所對應(yīng)的函數(shù)、變量位于st_shndx指定段。這種是最常見的,比如func1、main、global_init_var。 - 如果是COMMON塊的類型,則st_value表示該符號的對齊屬性,如global_uninit_var
- 在可執(zhí)行文件中,st_value表示符號的虛擬地址。
符號表內(nèi)容解析
內(nèi)容如下:

- 第一列表示符號表數(shù)組下標(biāo),從0開始共15個符號
- 第二列就是符號值——st_value
- 第三列Size為符號大小——st_size
- 第四列、第五列分別為符號類型和綁定信息,對應(yīng)st_info的低四位和高28位
- 第六列不知道
- 第七列表示符號所屬的段——st_shndx
- 第八列表示符號名稱
根據(jù)前面的內(nèi)容做出如下解析:
- func1、main是函數(shù)所有在代碼段,代碼段Ndx為1,所以.text為1.并且類型是STT_Func,并且是全局可見,所以是STB——GLOBAL。
- global_uninit_var是一個SHN_COMMON類型的符號,本身并沒有在BSS段。
- ....
- 依次類推可以把各個符號都解析出來
需要說明的:static_var和static_var2變成了static_var.1533和static_var.1534,是因為進(jìn)行符號修正。其次綁定的屬性是STB_LOCAL,表示只在編譯單元可見。類型是STT_SECTION類型的符號,表示下標(biāo)為Ndx段的短名。但是符號沒有顯示。比如2號符號Ndx為1那么就是.text段。那么符號名字就是.text??梢允褂?code>objdump -t查看段名符號。

特殊符號
當(dāng)使用鏈接器鏈接的時候,鏈接器會定義很多特殊符號,這些特殊符號沒有在程序定義,但是可以直接聲明并應(yīng)用它。鏈接器將這些特殊符號放在了鏈接腳本中,鏈接器會在程序最終鏈接為可執(zhí)行文件的時候,將其解析為正確的值。
常見特殊符號

符號修飾、函數(shù)簽名(防止沖突)
最開始編譯器產(chǎn)生目標(biāo)文件的時候,符號名和相應(yīng)的變量函數(shù)名一樣。但是如果已經(jīng)定義這些符號就會產(chǎn)生目標(biāo)文件沖突。為了解決目標(biāo)文件沖突,就在對應(yīng)的符號名簽名加一些字符以示區(qū)分。如在符號名前、后加上_。
如果模塊較多,命名規(guī)范不嚴(yán)格,同樣可能導(dǎo)致沖突。于是就增加了命名空間的方法來解決。
所以看到上面的static_var和static_var2變成了static_var.1533和static_var.1534。也就是進(jìn)行了一次符號修飾。
C++符號修飾
C++強(qiáng)大而復(fù)雜,為了支持C++特性,發(fā)明了符號修飾和符號改編機(jī)制。
對于不同類,同名函數(shù),引入了一個函數(shù)簽名的概念。函數(shù)簽名包含一個函數(shù)的信息,函數(shù)名,參數(shù)類型及所在的命名空間、其他細(xì)心,用于識別不同的函數(shù)。
編譯器在鏈接處理符號的時候,會使用名稱修飾的方法,使得每個函數(shù)簽名對應(yīng)一個修飾后的名稱。如下圖:

弱符號、強(qiáng)符號()
經(jīng)常在編程中將一個符號重復(fù)定義,如果出現(xiàn)定義錯誤則說明這種事強(qiáng)符號。有些符號可以定義為弱符號,比如未初始化的全局變量,也可以使用__attribut__((weak))定義一個強(qiáng)符號為弱符號。
它們的規(guī)則如下:
- 不允許強(qiáng)符號被多次定義,也就是不同目標(biāo)文件不能有相當(dāng)?shù)膹?qiáng)符號(iOS中經(jīng)常出現(xiàn)的符號沖突就是這個意思)
- 如果符號在某個目標(biāo)文件是強(qiáng)符號,其他文件是弱符號,那么選擇強(qiáng)符號
- 如果在所有文件中都是弱符號則選擇其中占空間最大的一個
弱引用、強(qiáng)引用
注意不是iOS中強(qiáng)弱引用
如果沒有找到該符號的定義,鏈接器就會報未定義錯誤這種成為強(qiáng)引用。與之相對的是弱引用,如果是弱引用,鏈接器不認(rèn)為是個錯誤,一般對未定義的弱引用,鏈接器默認(rèn)為0,或者其他值,以便程序識別。在動態(tài)庫中使用到,和COMMON塊概念聯(lián)系很緊密
可以使用__attribute__((weakref))擴(kuò)展自聲明對一個外部函數(shù)為弱引用。如果把它編譯為可執(zhí)行文件,并不會報鏈接錯誤。但是當(dāng)運行的時候,就會發(fā)生非法地址訪問。
可以用if加以判斷,防止這種情況。

這種弱符號和弱引用對于動態(tài)庫來講非常非常有用,比如動態(tài)庫中定義的弱符號可以被用戶定義的強(qiáng)符號覆蓋,使得程序可以使用自定義版的庫函數(shù)。
調(diào)試信息
目標(biāo)文件還可能保存調(diào)試信息,比如在函數(shù)里面設(shè)置斷點,可以監(jiān)視變量變化,可以單步行進(jìn)等,前提都是編譯器必須提前將源代碼與目標(biāo)代碼之間的關(guān)系(如目標(biāo)代碼中的地址對應(yīng)源代碼中的哪一行、函數(shù)和變量的類你先給,結(jié)構(gòu)體的定義,字符串保存到目標(biāo)文件里面)保存到目標(biāo)文件。
如果在gcc 中加入-g參數(shù)就可以在目標(biāo)文件里面加上調(diào)試信息。如gcc -c -g hello.c
比如:
objdump -h hello.o
hello.o: file format Mach-O 64-bit x86-64
Sections:
Idx Name Size Address Type
0 __text 00000067 0000000000000000 TEXT
1 __data 00000008 0000000000000068 DATA
2 __cstring 00000004 0000000000000070 DATA
3 __bss 00000004 00000000000004e4 BSS
4 __debug_str 0000009e 0000000000000074 DATA
5 __debug_loc 00000000 0000000000000112 DATA
6 __debug_abbrev 00000087 0000000000000112 DATA
7 __debug_info 000000e0 0000000000000199 DATA
8 __debug_ranges 00000000 0000000000000279 DATA
9 __debug_macinfo 00000001 0000000000000279 DATA
10 __apple_names 000000c8 000000000000027a DATA
11 __apple_objc 00000024 0000000000000342 DATA
12 __apple_namespac 00000024 0000000000000366 DATA
13 __apple_types 00000047 000000000000038a DATA
14 __compact_unwind 00000040 00000000000003d8 DATA
15 __eh_frame 00000068 0000000000000418 DATA
16 __debug_line 00000062 0000000000000480 DATA
ELF采用一個DWARF的標(biāo)準(zhǔn)的調(diào)試信息格式。在Xcode中也有DWARF的身影。

調(diào)試信息在目標(biāo)文件和可執(zhí)行文件中占用很大的空間,往往比程序的代碼和數(shù)據(jù)大很多倍(這一點在iOS開發(fā)中用Instrument調(diào)試的時候最終就是用得這個文件實現(xiàn)的。)
小結(jié)
這一部分內(nèi)容比較多,主要是解析目標(biāo)文件格式。如文件頭、段、段表、重定位表、符號表、字符串表各自的數(shù)據(jù)結(jié)構(gòu)是如何的。其實對ELF文件解析的內(nèi)容內(nèi)容遠(yuǎn)遠(yuǎn)不止這些,也有專門的官方文檔對ELF格式記性詳細(xì)的說明。
最重要的是明白各個部分的關(guān)系
擴(kuò)展閱讀
可執(zhí)行文件格式
理解ELF文件格式
ELF文件格式分析
Comparison of executable file formats
MachOView