vx公眾號(hào):CurryCoder的程序人生
業(yè)精于勤,荒于嬉;行成于思,毀于隨
1.問題引入
學(xué)過C語(yǔ)言的小伙伴們,基本上都知道從一個(gè)xxx.c的源文件到最后生成的可執(zhí)行文件,需要經(jīng)過預(yù)處理、編譯、匯編、鏈接這幾個(gè)步驟。但是,這幾個(gè)步驟詳細(xì)的過程我一直沒搞清楚,本文將深度剖析這幾個(gè)步驟。例如,在Windows/Linux系統(tǒng)中,一個(gè)C源文件從編寫完成到最終被CPU執(zhí)行,中間要經(jīng)歷一系列復(fù)雜而又漫長(zhǎng)的過程,如下圖所示:
2.編譯
編譯就是將程序員先的高級(jí)語(yǔ)言源代碼如xxx.c/xxx.cpp源文件轉(zhuǎn)化成對(duì)應(yīng)的目標(biāo)文件過程。一般來說,高級(jí)語(yǔ)言的編譯詳細(xì)流程需要經(jīng)過預(yù)處理、編譯和匯編這幾步。
2.1 預(yù)處理預(yù)處理過程主要是對(duì)源代碼做了如下操作:(1).刪除所有的代碼注釋信息 (2).刪除所有的#define,并展開所有的宏定義 (3).插入所有的#include頭文件的內(nèi)容到xxx.c/xxx.cpp源文件中的對(duì)應(yīng)位置(4).其他信息......例如,gcc編譯器可以使用gcc -E test.c -o test.i命令對(duì)源文件test.c進(jìn)行預(yù)編譯,并且把預(yù)編譯的結(jié)果輸出到test.i文件中。
[njust@njust Make_Tutorials]$ ls
test.c
[njust@njust Make_Tutorials]$ cat test.c
#include <stdio.h>
#define PI 3.14
int main() {
printf("hello world!\n");
return 0;
}
[njust@njust Make_Tutorials]$ gcc -E test.c -o test.i
[njust@njust Make_Tutorials]$ ls
test.c test.i
2.2 編譯
編譯就是將預(yù)處理后的文件進(jìn)行詞法分析、語(yǔ)法分析、語(yǔ)義分析并優(yōu)化后生成相應(yīng)的匯編文件。例如,使用命令gcc -S test.i -o test.s來編譯預(yù)處理階段生成的文件,或者也可以使用命令gcc -S test.c -o hello.s將預(yù)處理與編譯兩個(gè)步驟合二為一。
[njust@njust Make_Tutorials]$ ls
test.c test.i
[njust@njust Make_Tutorials]$ gcc -S test.i -o test.s
[njust@njust Make_Tutorials]$ ls
test.c test.i test.s
[njust@njust Make_Tutorials]$ gcc -S test.c -o test.s
[njust@njust Make_Tutorials]$ ls
test.c test.i test.s
匯編階段所生成的文件叫做目標(biāo)文件,目標(biāo)文件的結(jié)構(gòu)與可執(zhí)行文件的結(jié)構(gòu)是一致的,它們之間只存在一些細(xì)微的差異。目標(biāo)文件是無法被執(zhí)行的,它還需要經(jīng)過鏈接這一步操作后才能生成可執(zhí)行文件,最終被執(zhí)行。
3.目標(biāo)文件的格式
Linux系統(tǒng)中的目標(biāo)文件格式叫做ELF(Executable Linkable Format),ELF的格式如下圖所示:
ELF header是ELF文件中最重要的一個(gè)部分,header中保存了如下的內(nèi)容:
(1).ELF的magic number
(2).文件機(jī)器字節(jié)長(zhǎng)度
(3).操作系統(tǒng)平臺(tái)
(4).硬件平臺(tái)
(5).程序的入口地址
** (6).段表的位置和長(zhǎng)度**
(7).段的數(shù)量
(8).其他信息......
從header中我們可以獲取很多有用的信息,其中一種重要的信息就是段表的位置和長(zhǎng)度。通過這個(gè)信息我們可以從ELF文件中獲取到段表(Section Header Table),在ELF中段表的重要性僅次于header。段表中保存了ELF文件中所有的段的基本屬性(包括每個(gè)段的段名、段在ELF文件中的偏移、段的長(zhǎng)度及段的讀寫權(quán)限等),段表決定了整個(gè)ELF文件的結(jié)構(gòu)。
既然段表決定了所有的段的基本屬性,那么ELF文件中的段究竟是個(gè)啥呢?其實(shí)段只是對(duì)ELF文件內(nèi)不同類型數(shù)據(jù)的一種分類。例如,我們把所有的代碼(指令)放在同一個(gè)段中,并且給這個(gè)段起名為.text;把所有已初始化的數(shù)據(jù)放在.data段;把所有未初始化的數(shù)據(jù)放在.bss段;把所有只讀的數(shù)據(jù)放在.rodata段,.......等等。
為什么又要將數(shù)據(jù)(指令在ELF文件中也算是一種數(shù)據(jù),它是ELF文件的數(shù)據(jù)之一)分成不同的類型,然后分別存放在不同的段中呢?除了便于進(jìn)行區(qū)分外,還有如下幾個(gè)原因:
(1).便于給段設(shè)置讀寫權(quán)限,有的段只需要設(shè)置只讀權(quán)限;
(2).方便CPU緩存的生效;
(3).有利于節(jié)省內(nèi)存,例如程序有多個(gè)副本情況下,此時(shí)只需要一份代碼段即可;
用如下的hello.c程序?yàn)槔?,深入的分析一下ELF文件中的段信息,文件的內(nèi)容如下所示:
[njust@njust Make_Tutorials]$ cat hello.c
int printf(const char *format, ...);
int global_var_init_a = 84;
int global_var_uninit_b;
void bar(int i) {
printf("%d\n", i);
}
int main() {
static int static_var_a = 85;
static int static_var_b;
int a = 1;
int b;
bar(static_var_a + static_var_b + a + b);
return a;
}
使用命令gcc -c hello.c -o hello.o將源文件hello.c編譯成目標(biāo)文件hello.o。然后,再使用objdump命令查看ELF文件的內(nèi)部結(jié)構(gòu),-h表示顯示ELF文件的頭部信息得到如下結(jié)果:
[njust@njust Make_Tutorials]$ gcc -c hello.c -o hello.o
[njust@njust Make_Tutorials]$ ls
hello.c hello.o test.c test.i test.s
[njust@njust Make_Tutorials]$ objdump -h hello.o
hello.o: 文件格式 elf64-x86-64
節(jié):
Idx Name Size VMA LMA File off Algn
0 .text 00000054 0000000000000000 0000000000000000 00000040 2**0
CONTENTS, ALLOC, LOAD, RELOC, READONLY, CODE
1 .data 00000008 0000000000000000 0000000000000000 00000094 2**2
CONTENTS, ALLOC, LOAD, DATA
2 .bss 00000004 0000000000000000 0000000000000000 0000009c 2**2
ALLOC
3 .rodata 00000004 0000000000000000 0000000000000000 0000009c 2**0
CONTENTS, ALLOC, LOAD, READONLY, DATA
4 .comment 0000002e 0000000000000000 0000000000000000 000000a0 2**0
CONTENTS, READONLY
5 .note.GNU-stack 00000000 0000000000000000 0000000000000000 000000ce 2**0
CONTENTS, READONLY
6 .eh_frame 00000058 0000000000000000 0000000000000000 000000d0 2**3
CONTENTS, ALLOC, LOAD, RELOC, READONLY, DATA
從上面的輸出結(jié)果可以看到顯示了7個(gè)段,每個(gè)段都有一些屬性,下面解釋一下一些重要屬性的含義:
(1).Size:段的大小; (2).VMA:段的虛擬地址,因?yàn)槟繕?biāo)文件還沒有執(zhí)行鏈接操作,因此虛擬地址為0;(3).LMA:段被加載的地址,值為0(原因同上); (4).File off:段在ELF文件中的偏移地址; (5).CONTENTS:段存在于ELF文件中;需要重點(diǎn)關(guān)注的是.text、.data、.bss和.rodata這幾個(gè)段,這幾個(gè)段的詳細(xì)信息如下所示: .text段:保存程序中的所有指令信息,objdump的-s參數(shù)表示將段的內(nèi)容以十六進(jìn)制的方式打印出來,而-d參數(shù)會(huì)對(duì)所有包含指令的段進(jìn)行反匯編。于是,使用命令objdump -s -d hello.o就可以獲取代碼段的詳細(xì)信息;
[njust@njust Make_Tutorials]$ objdump -s -d hello.o
hello.o: 文件格式 elf64-x86-64
Contents of section .text:
0000 554889e5 4883ec10 897dfc8b 45fc89c6 UH..H....}..E...
0010 bf000000 00b80000 0000e800 000000c9 ................
0020 c3554889 e54883ec 10c745fc 01000000 .UH..H....E.....
0030 8b150000 00008b05 00000000 01c28b45 ...............E
0040 fc01c28b 45f801d0 89c7e800 0000008b ....E...........
0050 45fcc9c3 E...
Contents of section .data:
0000 54000000 55000000 T...U...
Contents of section .rodata:
0000 25640a00 %d..
Contents of section .comment:
0000 00474343 3a202847 4e552920 342e382e .GCC: (GNU) 4.8.
0010 35203230 31353036 32332028 52656420 5 20150623 (Red
0020 48617420 342e382e 352d3434 2900 Hat 4.8.5-44).
Contents of section .eh_frame:
0000 14000000 00000000 017a5200 01781001 .........zR..x..
0010 1b0c0708 90010000 1c000000 1c000000 ................
0020 00000000 21000000 00410e10 8602430d ....!....A....C.
0030 065c0c07 08000000 1c000000 3c000000 .\..........<...
0040 00000000 33000000 00410e10 8602430d ....3....A....C.
0050 066e0c07 08000000 .n......
Disassembly of section .text:
0000000000000000 <bar>:
0: 55 push %rbp
1: 48 89 e5 mov %rsp,%rbp
4: 48 83 ec 10 sub $0x10,%rsp
8: 89 7d fc mov %edi,-0x4(%rbp)
b: 8b 45 fc mov -0x4(%rbp),%eax
e: 89 c6 mov %eax,%esi
10: bf 00 00 00 00 mov $0x0,%edi
15: b8 00 00 00 00 mov $0x0,%eax
1a: e8 00 00 00 00 callq 1f <bar+0x1f>
1f: c9 leaveq
20: c3 retq
0000000000000021 <main>:
21: 55 push %rbp
22: 48 89 e5 mov %rsp,%rbp
25: 48 83 ec 10 sub $0x10,%rsp
29: c7 45 fc 01 00 00 00 movl $0x1,-0x4(%rbp)
30: 8b 15 00 00 00 00 mov 0x0(%rip),%edx # 36 <main+0x15>
36: 8b 05 00 00 00 00 mov 0x0(%rip),%eax # 3c <main+0x1b>
3c: 01 c2 add %eax,%edx
3e: 8b 45 fc mov -0x4(%rbp),%eax
41: 01 c2 add %eax,%edx
43: 8b 45 f8 mov -0x8(%rbp),%eax
46: 01 d0 add %edx,%eax
48: 89 c7 mov %eax,%edi
4a: e8 00 00 00 00 callq 4f <main+0x2e>
4f: 8b 45 fc mov -0x4(%rbp),%eax
52: c9 leaveq
53: c3 retq
.data段:保存已初始化的全局變量和局部靜態(tài)變量; .bss段:保存未初始化的全局變量和局部靜態(tài)變量;.rodata段:保存只讀數(shù)據(jù),例如字符串常量,被const修飾的變量;
4.重定位表與符號(hào)表
在ELF文件中還有兩個(gè)很重要的段,它們分別是重定位表與符號(hào)表。它們對(duì)后續(xù)的鏈接階段很重要。
4.1 重定位表
簡(jiǎn)單理解,編譯器將所有需要被重定位的數(shù)據(jù)存放在重定位表中,這樣鏈接器就能知道目標(biāo)文件中哪些數(shù)據(jù)是需要被重定位的。例如,我們有兩個(gè)源文件bar.c和foo.c,文件內(nèi)容如下所示:
[njust@njust Make_Tutorials]$ cat bar.c
extern int shared;
int main() {
int a = 100;
swap(&a,&shared);
}
[njust@njust Make_Tutorials]$ cat foo.c
int shared = 1;
void swap(int *a, int *b) {
*a ^= *b ^= *a ^= *b;
}
可以使用命令objdump -r bar.o來獲取重定位表的信息。此外,還可以使用命令readelf -S bar.o來詳細(xì)了解一個(gè)ELF文件。
[njust@njust Make_Tutorials]$ objdump -r bar.o
bar.o: 文件格式 elf64-x86-64
RELOCATION RECORDS FOR [.text]:
OFFSET TYPE VALUE
0000000000000014 R_X86_64_32 shared
0000000000000021 R_X86_64_PC32 swap-0x0000000000000004
RELOCATION RECORDS FOR [.debug_info]:
OFFSET TYPE VALUE
0000000000000006 R_X86_64_32 .debug_abbrev
000000000000000c R_X86_64_32 .debug_str+0x000000000000002d
0000000000000011 R_X86_64_32 .debug_str+0x0000000000000022
0000000000000015 R_X86_64_32 .debug_str+0x0000000000000007
0000000000000019 R_X86_64_64 .text
0000000000000029 R_X86_64_32 .debug_line
000000000000002e R_X86_64_32 .debug_str+0x000000000000008a
0000000000000038 R_X86_64_64 .text
000000000000005b R_X86_64_32 .debug_str+0x0000000000000028
0000000000000070 R_X86_64_32 .debug_str
RELOCATION RECORDS FOR [.debug_aranges]:
OFFSET TYPE VALUE
0000000000000006 R_X86_64_32 .debug_info
0000000000000010 R_X86_64_64 .text
RELOCATION RECORDS FOR [.debug_line]:
OFFSET TYPE VALUE
0000000000000029 R_X86_64_64 .text
RELOCATION RECORDS FOR [.eh_frame]:
OFFSET TYPE VALUE
0000000000000020 R_X86_64_PC32 .text
[njust@njust Make_Tutorials]$ readelf -S bar.o
共有 20 個(gè)節(jié)頭,從偏移量 0x678 開始:
節(jié)頭:
[號(hào)] 名稱 類型 地址 偏移量
大小 全體大小 旗標(biāo) 鏈接 信息 對(duì)齊
[ 0] NULL 0000000000000000 00000000
0000000000000000 0000000000000000 0 0 0
[ 1] .text PROGBITS 0000000000000000 00000040
0000000000000027 0000000000000000 AX 0 0 1
[ 2] .rela.text RELA 0000000000000000 00000450
0000000000000030 0000000000000018 I 17 1 8
[ 3] .data PROGBITS 0000000000000000 00000067
0000000000000000 0000000000000000 WA 0 0 1
[ 4] .bss NOBITS 0000000000000000 00000067
0000000000000000 0000000000000000 WA 0 0 1
[ 5] .debug_info PROGBITS 0000000000000000 00000067
000000000000007b 0000000000000000 0 0 1
[ 6] .rela.debug_info RELA 0000000000000000 00000480
00000000000000f0 0000000000000018 I 17 5 8
[ 7] .debug_abbrev PROGBITS 0000000000000000 000000e2
000000000000006f 0000000000000000 0 0 1
[ 8] .debug_aranges PROGBITS 0000000000000000 00000151
0000000000000030 0000000000000000 0 0 1
[ 9] .rela.debug_arang RELA 0000000000000000 00000570
0000000000000030 0000000000000018 I 17 8 8
[10] .debug_line PROGBITS 0000000000000000 00000181
000000000000003b 0000000000000000 0 0 1
[11] .rela.debug_line RELA 0000000000000000 000005a0
0000000000000018 0000000000000018 I 17 10 8
[12] .debug_str PROGBITS 0000000000000000 000001bc
000000000000008f 0000000000000001 MS 0 0 1
[13] .comment PROGBITS 0000000000000000 0000024b
000000000000002e 0000000000000001 MS 0 0 1
[14] .note.GNU-stack PROGBITS 0000000000000000 00000279
0000000000000000 0000000000000000 0 0 1
[15] .eh_frame PROGBITS 0000000000000000 00000280
0000000000000038 0000000000000000 A 0 0 8
[16] .rela.eh_frame RELA 0000000000000000 000005b8
0000000000000018 0000000000000018 I 17 15 8
[17] .symtab SYMTAB 0000000000000000 000002b8
0000000000000180 0000000000000018 18 13 8
[18] .strtab STRTAB 0000000000000000 00000438
0000000000000018 0000000000000000 0 0 1
[19] .shstrtab STRTAB 0000000000000000 000005d0
00000000000000a8 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)
上面的輸出結(jié)果中,以.rela開頭的就是重定位段,上面的.rela.text就存放了需要被重定位的指令信息,如果是需要被重定位的數(shù)據(jù)則對(duì)應(yīng)的段名為.rela.data。
上面的操作都是針對(duì)目標(biāo)文件bar.o進(jìn)行的,對(duì)目標(biāo)文件foo.o執(zhí)行上述命令可以發(fā)現(xiàn)它既不存在數(shù)據(jù)段的重定位表,也不存在代碼段的重定位表。這是因?yàn)閒oo.c中的變量shared和函數(shù)swap()都已經(jīng)明確知道了自己的地址,所以不需要重定位。
但是,bar.c文件則不一樣,因?yàn)閎ar.c中變量shared和函數(shù)swap()都沒有定義在當(dāng)前的文件中,因此編譯后產(chǎn)生的目標(biāo)文件不存在它們的地址信息,所以編譯器需要把它們放在重定位表中,等到鏈接的時(shí)候再到其他目標(biāo)文件中找到對(duì)應(yīng)的符號(hào)信息后對(duì)其進(jìn)行重定位。
4.2 符號(hào)表(.symtab)
目標(biāo)文件中的某些部分是在鏈接階段需要使用到的"粘合劑",這些部分稱為"符號(hào)",符號(hào)就保存在符號(hào)表中。符號(hào)表中保存的符號(hào)很多,其中最重要的就是定義在本目標(biāo)文件中并且可以被其它目標(biāo)文件所引用的符號(hào)、在本目標(biāo)文件中引用的全局符號(hào),這兩個(gè)符號(hào)呈現(xiàn)互補(bǔ)的關(guān)系。使用命令readelf -s可以查看符號(hào)表的內(nèi)容。具體的信息如下所示:
Num:符號(hào)表數(shù)組中的坐標(biāo)
Value:符號(hào)值
Size:符號(hào)大小
Type:符號(hào)類型
Bind:綁定信息
Name:符號(hào)的名稱
[njust@njust Make_Tutorials]$ readelf -s bar.o
Symbol table '.symtab' contains 16 entries:
Num: Value Size Type Bind Vis Ndx Name
0: 0000000000000000 0 NOTYPE LOCAL DEFAULT UND
1: 0000000000000000 0 FILE LOCAL DEFAULT ABS bar.c
2: 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 10
9: 0000000000000000 0 SECTION LOCAL DEFAULT 12
10: 0000000000000000 0 SECTION LOCAL DEFAULT 14
11: 0000000000000000 0 SECTION LOCAL DEFAULT 15
12: 0000000000000000 0 SECTION LOCAL DEFAULT 13
13: 0000000000000000 39 FUNC GLOBAL DEFAULT 1 main
14: 0000000000000000 0 NOTYPE GLOBAL DEFAULT UND shared
15: 0000000000000000 0 NOTYPE GLOBAL DEFAULT UND swap
[njust@njust Make_Tutorials]$ readelf -s foo.o
Symbol table '.symtab' contains 15 entries:
Num: Value Size Type Bind Vis Ndx Name
0: 0000000000000000 0 NOTYPE LOCAL DEFAULT UND
1: 0000000000000000 0 FILE LOCAL DEFAULT ABS foo.c
2: 0000000000000000 0 SECTION LOCAL DEFAULT 1
3: 0000000000000000 0 SECTION LOCAL DEFAULT 2
4: 0000000000000000 0 SECTION LOCAL DEFAULT 3
5: 0000000000000000 0 SECTION LOCAL DEFAULT 4
6: 0000000000000000 0 SECTION LOCAL DEFAULT 6
7: 0000000000000000 0 SECTION LOCAL DEFAULT 7
8: 0000000000000000 0 SECTION LOCAL DEFAULT 9
9: 0000000000000000 0 SECTION LOCAL DEFAULT 11
10: 0000000000000000 0 SECTION LOCAL DEFAULT 13
11: 0000000000000000 0 SECTION LOCAL DEFAULT 14
12: 0000000000000000 0 SECTION LOCAL DEFAULT 12
13: 0000000000000000 4 OBJECT GLOBAL DEFAULT 2 shared
14: 0000000000000000 74 FUNC GLOBAL DEFAULT 1 swap
命令nm也可以對(duì)符號(hào)進(jìn)行查看,其中D表示該符號(hào)是已經(jīng)初始化的變量,T表示該符號(hào)是指令,U表示符號(hào)尚未定義;
[njust@njust Make_Tutorials]$ nm bar.o
0000000000000000 T main
U shared
U swap
[njust@njust Make_Tutorials]$ nm foo.o
0000000000000000 D shared
0000000000000000 T swap
[njust@njust Make_Tutorials]$ nm result
0000000000601004 D __bss_start
0000000000601004 D _edata
0000000000601008 D _end
00000000004000e8 T main
0000000000601000 D shared
000000000040010f T swap
通過上面的舉例,我們知道重定位表與符號(hào)表之間是一種相互合作的關(guān)系,鏈接器首先會(huì)根據(jù)重定位表找到該目標(biāo)文件中需要被重定位的符號(hào),然后再根據(jù)符號(hào)表去其他的目標(biāo)文件中找到匹配的上的符號(hào)。最后,對(duì)本目標(biāo)文件中的符號(hào)進(jìn)行重定位。
5.(靜態(tài))鏈接
現(xiàn)代計(jì)算機(jī)的內(nèi)存和磁盤空間已經(jīng)足夠大,同時(shí)動(dòng)態(tài)鏈接對(duì)內(nèi)存和磁盤的節(jié)省十分有限,所以我們已經(jīng)可以忽略動(dòng)態(tài)鏈接在節(jié)省使用空間上的優(yōu)勢(shì)。此外,由于沒有了對(duì)動(dòng)態(tài)鏈接庫(kù)的依賴,不需要考慮動(dòng)態(tài)鏈接庫(kù)的不同版本,靜態(tài)鏈接的文件可以做到鏈接即可執(zhí)行,減少了運(yùn)維和部署上的復(fù)雜度,是非常的方便的,在有些新發(fā)明的語(yǔ)言(例如 go語(yǔ)言)中鏈接過程默認(rèn)已經(jīng)開始使用靜態(tài)鏈接。
5.1 靜態(tài)鏈接過程可細(xì)分為兩步:
(1).掃描所有的目標(biāo)文件,獲取它們每個(gè)段的長(zhǎng)度、位置和屬性,并把每個(gè)目標(biāo)文件中的符號(hào)表的符號(hào)定義與符號(hào)引用集中存放在一個(gè)全局符號(hào)表中,建立起可執(zhí)行文件到目標(biāo)文件的段映射關(guān)系;
(2).讀取目標(biāo)文件中的段數(shù)據(jù),并解析符號(hào)表信息。根據(jù)符號(hào)表信息進(jìn)行重定位、調(diào)整代碼中的地址等操作;
使用命令gcc -c bar.c foo.c -zexecstack -fno-stack-protector -g編譯源代碼得到目標(biāo)文件bar.o和foo.o,然后使用命令ld bar.o foo.o -e main -o result鏈接bar.o和foo.o目標(biāo)文件得到可執(zhí)行文件result。
[njust@njust Make_Tutorials]$ ls
bar.c foo.c hello.c hello.o test.c test.i test.s
[njust@njust Make_Tutorials]$ gcc -c bar.c foo.c -zexecstack -fno-stack-protector -g
[njust@njust Make_Tutorials]$ ls
bar.c bar.o foo.c foo.o hello.c hello.o test.c test.i test.s
[njust@njust Make_Tutorials]$ ld bar.o foo.o -e main -o result
[njust@njust Make_Tutorials]$ ls
bar.c bar.o foo.c foo.o hello.c hello.o result test.c test.i test.s
[njust@njust Make_Tutorials]$ cat bar.c
結(jié)合重定位表與符號(hào)表的知識(shí),我們可以知道鏈接器最終需要完成的工作有三個(gè):
(1).合并不同目標(biāo)文件中的同類型段;
(2).對(duì)目標(biāo)文件中的符號(hào)引用,在其它的目標(biāo)文件中找到引用的符號(hào);
(3).對(duì)目標(biāo)文件中的變量進(jìn)行重定位;
5.2 靜態(tài)庫(kù)的鏈接
操作系統(tǒng)一般都自帶有一些庫(kù)文件,linux中最有名的就是libc靜態(tài)庫(kù),它一般位于/usr/bin/libc.a中,libc.a是一個(gè)壓縮文件,它當(dāng)中包含了printf.o、scanf.o、malloc.o、read.o等庫(kù)文件。當(dāng)使用標(biāo)準(zhǔn)庫(kù)中的文件時(shí),鏈接器會(huì)對(duì)用戶目標(biāo)文件和標(biāo)準(zhǔn)庫(kù)文件進(jìn)行鏈接,得到最終的可執(zhí)行文件。
** 6.裝載**
完成鏈接步驟后,得到一個(gè)可執(zhí)行文件,在可執(zhí)行文件中包含了很多段,但是一旦這些段加載到內(nèi)存中后,我們就不需要再關(guān)心它們到底是什么類型的數(shù)據(jù)了,只需要關(guān)心這些數(shù)據(jù)在內(nèi)存中的讀寫權(quán)限??蓤?zhí)行文件被加載到內(nèi)存中的數(shù)據(jù)可分為:可讀不可寫和可讀可寫。 現(xiàn)代操作系統(tǒng)均采用分頁(yè)的方式來管理內(nèi)存,所以操作系統(tǒng)只需要讀取可執(zhí)行文件的文件頭,之后建立起可執(zhí)行文件到虛擬內(nèi)存的映射關(guān)系,不需要真正的將程序載入內(nèi)存。在程序的運(yùn)行過程中,CPU發(fā)現(xiàn)有些內(nèi)存頁(yè)在物理內(nèi)存中并不存在時(shí),會(huì)觸發(fā)缺頁(yè)異常,此時(shí)CPU將控制權(quán)限轉(zhuǎn)交給操作系統(tǒng)的異常處理函數(shù),操作系統(tǒng)負(fù)責(zé)將此內(nèi)存頁(yè)的數(shù)據(jù)從外存(磁盤)上讀取到物理內(nèi)存中。數(shù)據(jù)讀取完畢之后,操作系統(tǒng)讓CPU jmp到觸發(fā)了缺頁(yè)異常的那條指令處繼續(xù)執(zhí)行,此時(shí)指令執(zhí)行就不會(huì)再有缺頁(yè)異常了。忽略物理內(nèi)存地址以及缺頁(yè)異常的影響,一旦操作系統(tǒng)創(chuàng)建進(jìn)程(fork) 并載入了可執(zhí)行文件(exec),那么虛擬內(nèi)存的分布應(yīng)該如下圖所示??梢钥吹紼LF文件中的多個(gè)段在內(nèi)存中被合并為三個(gè)段。
上圖中,除了三個(gè)保存了ELF文件中的數(shù)據(jù)的段之外,還有其他幾部分。如下表所示:
| 名稱 | 描述 |
| Kernel Space | 內(nèi)核空間,用戶進(jìn)程無權(quán)訪問 |
| Stack | 實(shí)現(xiàn)函數(shù)調(diào)用 |
| Heap | 保存程序運(yùn)行時(shí)產(chǎn)生的全局變量 |
| Memory Map | 磁盤空間到內(nèi)存的映射 |
7.運(yùn)行
操作系統(tǒng)jmp到進(jìn)程的第一條指令并不是main方法,而是別的代碼。那些代碼負(fù)責(zé)初始化main方法執(zhí)行所需要的環(huán)境并調(diào)用main方法執(zhí)行,運(yùn)行這些代碼的函數(shù)被稱為入口函數(shù)或者入口點(diǎn)(Entry Point)。一個(gè)程序的執(zhí)行過程如下:
(1).操作系統(tǒng)在創(chuàng)建進(jìn)程之后,jmp到這個(gè)進(jìn)程的入口函數(shù)
(2).入口函數(shù)對(duì)程序運(yùn)行環(huán)境進(jìn)行初始化,包括堆、I/O、線程、全局變量的構(gòu)造等
(3).入口函數(shù)在完成初始化之后,調(diào)用main函數(shù),開始執(zhí)行程序的主體
(4).main函數(shù)執(zhí)行完畢之后返回到入口函數(shù),入口函數(shù)進(jìn)行清理工作,最后通過系統(tǒng)調(diào)用結(jié)束進(jìn)程