1. 程式人生 > >:程式是怎樣被連結和載入的?

:程式是怎樣被連結和載入的?

個真實的例子
我們通過一個簡小的連結例項來結束對連結過程的介紹。圖 3 所示為一對 C 語言原始碼
檔案,m.c 中的主程式呼叫了一個名為 a 的例程,而呼叫了庫例程 strlen 和 write 的 a 例程
bbs.theithome.com
在 a.c 中。
---------------------------------------------------------------------------------------------
圖 1-3 源程式
源程式 m.c
extern void a(char *);
int main(int ac, char **av)
{
static char string[] = "Hello, world!\n";
a(string);
}
源程式 a.c
#include <unistd.h>
#include <string.h>
void a(char *s)
{
write(1, s, strlen(s));
}
---------------------------------------------------------------------------------------------
如圖 4 所示,主程式 m.c 在我的 Pentium 機器上用 gcc 編譯成一個典型 a.out 目的碼
格式長度為 165 位元組的目標檔案。該目標檔案包含一個固定長度的頭部,16 個位元組的“文字
”段,包含只讀的程式程式碼,16 個位元組的資料段,包含字串。其後是兩個重定位項,其
中一個標明 pushl 指令將字串 string 的地址放置在棧上為呼叫例程 a 作準備,另一個標明
call 指令將控制轉移到例程 a。符號表分別匯出和匯入了_main 與_a 的定義,以及偵錯程式需
要的其它一系列符號(每一個全域性符號都會以下劃線做為字首,第 5 章中將會講述原因)。
注意由於和字串 string 在同一個檔案中,pushl 指令引用了 string 的臨時地址 0x10,而
由於_a 的地址是未知的所以 call 指令引用的地址為 0x0。
---------------------------------------------------------------------------------------------
圖 1-4 m.o 的目的碼
Sections:
Idx Name Size VMA LMA File off Algn
0 .text 00000010 00000000 00000000 00000020 2**3
1 .data 00000010 00000010 00000010 00000030 2**3
Disassembly of section .text:
00000000 <_main>:
0: 55 pushl %ebp
1: 89 e5 movl %esp,%ebp
bbs.theithome.com
3: 68 10 00 00 00 pushl $0x10
4: 32 .data
8: e8 f3 ff ff ff call 0
9: DISP32 _a
d: c9 leave
e: c3 ret
...
---------------------------------------------------------------------------------------------
如圖 5 所示,子程式檔案 a.c 編譯成一個長度為 160 位元組的目標檔案,包括頭部, 28
位元組的文字段,無資料段。兩個重定位項標記了對 strlen 和 write 的 call 指令,符號表中
匯出_a 並匯入了_strlen 和_write。
---------------------------------------------------------------------------------------------
圖 1-5 a.c 的目的碼
Sections:
Idx Name Size VMA LMA File off Algn
0 .text 0000001c 00000000 00000000 00000020 2**2
CONTENTS, ALLOC, LOAD, RELOC, CODE
1 .data 00000000 0000001c 0000001c 0000003c 2**2
CONTENTS, ALLOC, LOAD, DATA
Disassembly of section .text:
00000000 <_a>:
0: 55 pushl %ebp
1: 89 e5 movl %esp,%ebp
3: 53 pushl %ebx
4: 8b 5d 08 movl 0x8(%ebp),%ebx
7: 53 pushl %ebx
8: e8 f3 ff ff ff call 0
9: DISP32 _strlen
d: 50 pushl %eax
e: 53 pushl %ebx
f: 6a 01 pushl $0x1
11: e8 ea ff ff ff call 0
12: DISP32 _write
16: 8d 65 fc leal -4(%ebp),%esp
19: 5b popl %ebx
1a: c9 leave
1b: c3 ret
---------------------------------------------------------------------------------------------
bbs.theithome.com
為了產生一個可執行程式,連結器將這兩個目標檔案,以及一個標準的 C 程式啟動初始
化例程,和必要的 C 庫例程整合到一起,產生一個部分如圖 6 所示的可執行檔案。
---------------------------------------------------------------------------------------------
圖 1-6 可執行程式的部分程式碼
Sections:
Idx Name Size VMA LMA File off Algn
0 .text 00000fe0 00001020 00001020 00000020 2**3
1 .data 00001000 00002000 00002000 00001000 2**3
2 .bss 00000000 00003000 00003000 00000000 2**3
Disassembly of section .text:
00001020 <start-c>:
...
1092: e8 0d 00 00 00 call 10a4 <_main>
...
000010a4 <_main>:
10a4: 55 pushl %ebp
10a5: 89 e5 movl %esp,%ebp
10a7: 68 24 20 00 00 pushl $0x2024
10ac: e8 03 00 00 00 call 10b4 <_a>
10b1: c9 leave
10b2: c3 ret
...
000010b4 <_a>:
10b4: 55 pushl %ebp
10b5: 89 e5 movl %esp,%ebp
10b7: 53 pushl %ebx
10b8: 8b 5d 08 movl 0x8(%ebp),%ebx
10bb: 53 pushl %ebx
10bc: e8 37 00 00 00 call 10f8 <_strlen>
10c1: 50 pushl %eax
10c2: 53 pushl %ebx
10c3: 6a 01 pushl $0x1
10c5: e8 a2 00 00 00 call 116c <_write>
10ca: 8d 65 fc leal -4(%ebp),%esp
10cd: 5b popl %ebx
10ce: c9 leave
bbs.theithome.com
10cf: c3 ret
...
000010f8 <_strlen>:
...
0000116c <_write>:
...
---------------------------------------------------------------------------------------------
連結器將每個輸入檔案中相應的段合併在一起,故只存在一個合併後的文字段,一個
合併後的資料段和一個 bss 段(兩個輸入檔案不會使用的,被初始化為 0 的資料段)。由於
每個段都會被填充為 4K 對齊以滿足 x86 的頁尺寸,因此文字段為 4K(減去檔案中 20 位元組長
度的 a.out 頭部,邏輯上它並不屬於該段),資料段和 bss 段每個同樣也是 4K 位元組。
合併後的文字段包含名為 start-c 的庫啟動程式碼,由 m.o 重定位到 0x10a4 的程式碼,重
定位到 0x10b4 的 a.o,以及被重定位到文字段更高地址從 C 庫中連結來的例程。資料段,沒
有顯示在這裡,按照和文字段相同的順序包含了合併後的資料段。由於_main 的程式碼被重定
位到地址 0x10a4,所以這個程式碼要被修改到 start-c 程式碼的 call 指令中。在 main 例程內部,
對字串 string 的引用被重定位到 0x2024,這是 string 在資料段最終的位置,並且 call
指令中地址修改為 0x10b4,這是_a 最終確定的地址。在_a 內部,對_strlen 和_write 的 cal
l 指令也要修改為這兩個例程的最終地址。
可執行程式中仍然有很多其它的 C 庫例程,沒有顯示在這裡,它們由啟動程式碼和_write
(在稍後例子中的出錯處理例程)直接或間接的呼叫。由於可執行程式的檔案格式不是可以重
連結的,且作業系統從已知的固定位置載入它,因此它不包含重定位資料。它帶有一個有助
於偵錯程式(debugger)工作的符號表,儘管這個程式沒有使用這個符號表並且可以將其刪除
以節省空間。
在這個例子中,從庫中連結的程式碼明顯要多於程式本身的程式碼。這是很正常的,尤其
當程式使用大的圖形庫或視窗庫,這就促進了共享庫的出現,詳見第 9 章和第 10 章。這個
連結好的程式大小為 8K,但若使用共享庫連結則同樣的程式大小僅為 264 位元組。當然這是一
個像玩具一樣的例子,但真實程式經常也會採用同樣的方法節省空間。