Professional Documents
Culture Documents
第 5 章、連結與載入
作者:陳鍾誠
旗標出版社
第 5 章、連結與載入
5.1 簡介
5.2 函式庫
5.3 目的檔
5.4 連結器
5.5 載入器
5.6 動態連結
5.7 實務案例
5.7.1 GNU 連結工具
5.7.2 目的檔格式 – a.out
5.7.3 目的檔格式 – ELF
5.1 簡介
目的檔 v.s 可執行檔
連結器
絕對載入器
5.1 目的檔 v.s. 可執行檔
當組譯器組譯完成之後,所輸出的檔案,通常稱為目的
檔。
假如目的檔中沒有未連結的外部變數,而且已經指定了
起始位址,那麼,這種目的檔通常被稱為執行檔。
此時,載入器可以將執行檔載入到記憶體當中執行。
但是,如果有外部變數尚未被連結,那麼,目的檔就必
須被連結成執行檔後,才能被放入到記憶體中執行。
連結器
連結器必須計算出區段與變數的記憶體位址,並利
用修正記錄進行修正後,才能消除這些外部變數,
以便將目的檔轉換成執行檔。
Windows
目的檔: *.obj
執行檔: *.exe
Linux
目的檔: *.o
執行檔:預設 a.out
絕對載入器
最簡單的載入器,是採用絕對定位方式的載入器,
也就是在目的檔當中直接指定所要載入的記憶體位
址,然後,載入器就將程式載入到對應的記憶體位
址執行之。
載入器與目的檔
載入器的設計與目的檔格式有關
不同的目的檔格式之載入器將會不同
在本節中
A 格式 Loader_A
只有一筆 T 記錄
直接將 T 記錄載入到指定位址即可
B 格式 Loader_B
包含 T 記錄與 B 記錄
必須用迴圈讀取 T, B 記錄,放到指定位址
圖 5.1 格式 A 的執行檔
T{
001F0010 001F0020 31000020 013F0008
00000001 00000007 00000003 00000006
00000000 00000004 12400000 12500000
00600024 10520000 0D000010 13445000
13556000 0EFFFFFF0 32000000 00000000
00000000 00000001
}
圖 5.2 格式 A 的載入器的演算
法
Algorithm Loader_A
Input objFile
fileHeader = objFile.readHeader();
memory = allocateMemory(objFile. memorySize);
section = objFile.readSection(T)
put section.objCode into
memory(memory.startAddress)
PC = memory.startAddress
End
圖 5.3 < 圖 5.1> 的目的檔載入記憶
體後的情況
記憶體 記憶體內容
位址
1200 001F0010 001F0020 31000020 013F0008
1210 00000001 00000007 00000003 00000006
1220 00000000 00000004 12400000 12500000
1230 00600024 10520000 0D000010 13445000
1240 13556000 0EFFFFF0 32000000 00000000
1250 00000000 00000001 XXXXXXXX XXXXXXXX
1260 XXXXXXXX XXXXXXXX XXXXXXXX XXXXXXXX
圖 5.4 格式 B 的執行檔
T(0000) {
001F0010 001F0020 31000020 013F0008
00000001 00000007 00000003 00000006
}
B(0020,0004)
T(0024) {
00000004 12400000 12500000 00600024
10520000 0D000010 13445000 13556000
0EFFFFF0 32000000
}
B(004C,0004)
T(0050) {
00000000 00000001
}
圖 5.5 格式 B 的載入器演算法
演算法 說明
Algorithm Loader_B B 格式載入器
Input objFile 輸入為目的檔
objFile.open(); 開啟目的檔
memory = allocateMemory(objFile. 分配記憶空間
memorySize); 讀取目的檔
while not objFile.isEnd() 讀取下一筆區段
section = objFile.readSection(); 如果是 T 區段
if section.type() is T
put section.objCode into
memory(section.startAddress)
else if sectin.type() is B
do nothing // 或者 fill memory(section) with
0
end if
end foreach
PC = memory.startAddress
圖 5.6 < 圖 5.4> 的 B 格式目的檔
載入記憶體的情況
記憶體 記憶體內容
位址
1200 001F0010 001F0020 31000020 013F0008
1210 00000001 00000007 00000003 00000006
1220 XXXXXXXX 00000004 12400000 12500000
1230 00600024 10520000 0D000010 13445000
1240 13556000 0EFFFFF0 32000000 XXXXXXXX
1250 00000000 00000001 XXXXXXXX XXXXXXXX
1260 XXXXXXXX XXXXXXXX XXXXXXXX XXXXXXXX
5.2 函式庫
函式庫會先被組譯成目的檔。
主程式才被組譯為目的檔。
最後再經由連結器連結成完整的執行檔。
外部引用
組譯器允許外部函數的存在
在連結的時候,才去解決這些外部引用的情況。
範例 5.1 具有交互引用的 C 語言程
式 ( 實作堆疊功能 )
StackType.c StackFunc.c StackMain.c
int stack[128]; extern int extern void
int top = 0; stack[]; push(int x);
extern int top; extern int pop();
void push(int x) int main() {
{ int x;
stack[top++] = push(3);
x; x= pop();
} return 0;
int pop() { }
return stack[--
top];
}
範例 5.2 具有交互引用的組合語言
程式 ( 實作堆疊功能 ) ( 頁 1)
檔名: StackType.s 檔名: StackMain.s
.bss .text
.global stack .extern push
stack RESW 512 .extern pop
.data .global main
.global top main:
top WORD 0 LDI R1, #3
PUSH R1
JSUB push
JSUB pop
ST R1, x
LDI R1, #0
RET
x RESW 1
範例 5.2 具有交互引用的組合語言
程式 ( 實作堆疊功能 ) ( 頁 2)
檔名: StackFunc.s 續…
.extern stack .global pop
.extern top pop:
.text LD R2, top // R2=top
.global push LD R3, stack // R3=stack
push : LDI R4, #4 // R4=4
POP R1 // R1=x LDI R5, #1 // R5 = 1
LD R2, top MUL R5, R2, R4 // R5 =
LD R3, stack top*4
LDI R4, #4 LDR R1,
LDI R5, #1 [R3+R5]//R1=stack[top]
MUL R5, R2, R4 SUB R2, R2, R5 // R2=R2-1
STR R1, [R3+R5] // ST R2, top // top = R2
stack[top]=x RET
ADD R2, R2, R5
ST R2, top
RET
範例 5.3 堆疊型態 StackType.s
及其目的碼
位址 組合語言檔: 目的碼 說明
StackType.s
.bss BSS 段開始
.global stack stack 是全域變數
B 0000 stack RESW B(0200),S(B,0000,stac 保留 512 個 bytes
512 k) 給 BSS 段
DATA 段開始
.data top 是全域變數
D 0000 .global top D(00000000),S(D,000 top 的位址是
top WORD 0 0,top) DATA 段的 0000
範例 5.4 堆疊函數 StackFunc.s
及其目的碼 ( 第 1 頁 )
組合語言檔: 目的碼 說明
StackFunc.s
.data 資料段開始
.extern stack S(U,,stack) stack 為外部變數
.extern top S(U,,top) top 為外部變數
.text 程式段開始
.global push push 為全域變數
push : S(T,0000, push)
T 0000 POP R1 // R1=x T(31100000)
T 0004 LD R2, top T(002F0000), 使用 PC 相對定址
T 0008 LD R3, stack M(T,0004,top,pc) 使用 PC 相對定址
T 000C LDI R4, #4 T(003F0000),
T 0010 LDI R5, #1 M(T,0008,stack,pc)
T 0014 MUL R5, R2, R4 T(08400004)
T 0018 STR R1, T(08500001)
T 001C [R3+R5] T(15524000)
T 0020 ADD R2, R2, R5 T(05135000)
T 0024 ST R2, top T(13225000)
RET T(01200000),
M(T,0020,top,pc)
T(2C000000)
範例 5.4 堆疊函數 StackFunc.s
及其目的碼 ( 第 2 頁 )
組合語言檔: 目的碼 說明
StackFunc.s
… … … …
T 0028 .global pop S(T,0028, pop) pop 為全域變數
T 002C pop: pop 函數開始
T 0030 LD R2, top T(002F0000), 使用 PC 相對定址
T 0034 LD R3, stack M(T,0028,top,pc) 使用 PC 相對定址
T 0038 LDI R4, #4 T(003F0000),
T 003C LDI R5, #1 M(T,002C,stack,pc)
T 0040 MUL R5, R2, R4 T(08400004)
T 0044 LDR R1, T(08500001)
T 0048 [R3+R5] T(15524000)
SUB R2, R2, R5 T(04135000) 使用 PC 相對定址
ST R2, top T(14225000)
RET T(012F0000),M(T,0044,top,pc
)
T(2C000000)
範例 5.5 堆疊主程式 StackMain.s
及其目的碼
組合語言檔 目的碼
:
StackMain.
.extern
s S(U,,push) push 為外部變數
T push S(U,,push) pop 為外部變數
0000 .extern S(T,0000,main) 程式 (Text) 段
T pop T(08100003) 開始
0004 .text T(30100000) main 為全域變
T .global T(2BF00000), 數
0008 main M(T,0008,push,pc) main 程式開始
T main: T(2BF00000),
000C LDI R1, M(T,000C,pop,pc) 使用 PC 相對定
T #3 T(01100008) 址
0010 PUSH R1 T(08100000)
T JSUB T(2C000000) 使用 PC 相對定
0014 push T(00000000) 址
T JSUB pop
5.3 目的檔
程式段 (.text, 以 T 開頭 )
資料段 (.data ,以 D 開頭 )
BSS 段 (.bss, 以 B 開頭 )
.global :符號表記錄 (S 記錄 )
.extern :修改記錄 (M 記錄 )
圖 5.7 目的檔中的記錄與範例
記錄 程式範例 目的碼 說明
T JSUB push T(2BF00000) 程式段的目的碼
D top WORD 0 D(00000000) 資料段的目的碼
B stack RESW B(400) 保留 BSS 段的空間
M 400 push
JSUB M(T,0008,push, 修改記錄 ( 或稱重定位記
pc) 錄 ) :採用相對於 PC 的位
址計算方式
S .global pop S(T,0028, pop) 符號記錄:記錄全域變數的
位址
S .extern stack S(U,,stack) 符號記錄:記錄外部變數
圖 5.8 重定位記錄 (M 記錄 ) 的表
示法
#define TYPE_ABSOLUTE 0
#define TYPE_PC_RELATIVE 1
…
typedef struct{
int section; // 段代號。
int offset; // 待修改位址。
int symbol; // 符號代碼 ( 用以取得修改值 ) 。
int type; // 修改記錄類型 (0. 絕對 1. 相對於 PC 、
2. …)
} RelocationRecord; // 修改記錄 (M 記錄 )
圖 5.9 符號記錄 (S 記錄 ) 的表
示法
#define SYM_TYPE_DATA 0
#define SYM_TYPE_FUNC 1
#define SYM_TYPE_SECT 2
#define SYM_TYPE_FILE 3
…
typedef struct {
int name; // 符號在字串表中的位址。
int section; // 定義該符號的分段
int value; // 符號的值 ( 通常為一個位址 ) 。
} SymbolRecord; // 符號記錄 (S 記錄 )
圖 5.10 字串表的結構
字串: ".text\0.data\0.bss\0stack\0top\0push\0pop\0main\0"
位址 0 1 2 3 4 5 6 7 9 9 A B C D E F
0 . T E X T \0 . D A T A \0 . B S S
000
0 \0 S T A C K \0 T O P \0 P U S H \0
010
0 P O P \0 M A I N \0
020
圖 5.11 目的檔 a.out 的格式與
範例
檔頭 (H 記錄 ) H(StackFunc.s, 各段的長度 )
資料段 (D 記錄 )
程式重定位資訊 M { (0004,top,pc) (0008,stack,pc) (0020,top,pc) …}
(M 記錄 )
資料重定位資訊
(M 記錄 )
符號表 S { (T,0000,push) (T,0028,pop) (U,,stack) (U,,top) }
(S 記錄 )
字串表 .text\0.data\0.bss\0top\0stack\0push\0pop\0
String Table
輸出入
輸入是一群目的檔
輸出是一個執行檔
區段合併
除了解決外部引用的問題之外,連結器通常還必須
對程式進行區段合併的動作。
常見區段
程式段 (text section)
資料段 (data section)
BSS 段 (Block Started by Symbol)
一種特殊的資料段,用來存放那些未設定初值的變數。
圖 5.13 連結器的輸入與輸出
函式庫 目的檔 目的檔
lib obj obj
連結器
linker
.text
.data .data
.bss
.bss
.text
.data
.bss
圖 5.15 連結器輸出的執行檔,以
stack.o 為例
T{
// 來源: StackMain.o 的程式段 (T 記錄 )
08100003 30100000 2B000014
2B000028
01100008 08100000 2C000000
00000000
// 來源: StackFunc.o 的程式段 (T 記錄 )
31100000 00200000 00300000
08400004
08500001 15524000 05135000
13225000
01200000 2C000000 00200000
00300000
08400004 08500001 15524000
04135000
14225000 01200000 2C000000
}
B { 0200 }
D指令: ld -o Stack.o
{ 00000000 } StackFunc.o StackType.o StackMain.o
說明:連結 StackFunc.o StackType.o StackMain.o 三個檔案以形成執行檔
Stack.o
目的檔: StackMain.o 圖 5.16 連結過程
T,0000
程式段: T {
08100003 30100000 2B000000 2B000000 圖
01100008 08100000 2C000000 00000000
T,001F }
T,000 執行檔: Stack.o
M { (T,0008,push,pc), (T,000C,pop,pc) } 0
S { (U,,push), (U,,pop), (T,0000, main) } T,001
程式段: T{
0
08100003 30100000 2B000014 2B000028
T,002
01100008 08100000 2C000000 00000000
目的檔 : 0
31100000 00200000 00300000 08400004
StackFunc.o T,003
08500001 15524000 05135000 13225000
程式段: T{ 0
T,0000 01200000 2C000000 00200000 00300000
31100000 00200000 00300000 08400004 T,004
08400004 08500001 15524000 04135000
08500001 15524000 05135000 13225000 0
14225000 01200000 2C000000
01200000 2C000000 00200000 00300000 T,005
}
08400004 08500001 15524000 04135000 0
14225000 01200000 2C000000 T,006
D,0000
} 0 資料段: D { 00000000 }
T,004C D,0004
S { (T,0000,push)(T,0028,pop)(U,,stack)(U,,top) } B,0000
M { (T,0004,top,pc)(T,0008,stack,pc)(T,0020,top,pc) BSS 段: B { 0200}
B,0200
(T,0028,top,pc)(T,002C,stack,pc)(T,0044,top,pc)
}
目的檔: StackType.o
D,0000
資料段: D { 00000000 }
D,0004
B,0000
BSS 段: B { 0200}
B,0200
S { (B, 0000, stack) (D,0000,top) }
5.5 載入器
將目的檔載入到記憶體,然後開始執行該目的檔
必須處理分段資訊與修改記錄等結構。
載入器必須讀取執行檔,組合其中的分段,放入到
記憶體中,然後根據修改記錄修正記憶體中的指令
區與資料區,最後才將程式計數器 PC 設定為起
始指令的位址,開始執行該程式。
圖 5.17 執行中程式的記憶體配置情
況 - Stack.o 載入後的情況
0000 1200 08100003 30100000 2B000014 2B000028
01100008 08100000 2C000000 00000000
31100000 00200044 00300044 08400004
程式段 (.text) 08500001 15524000 05135000 13225000
01200028 2C000000 00200020 00300020
08400004 08500001 15524000 04135000
14225000 01200004 2C000000
126C
資料段 (.data) 00000000
1270
BSS 段 (.bss) XXXXXXXX …
1470
堆積段 (.heap)
堆疊段 (.stack)
size 2000
(a) 載入到記憶體後的分段情況 (b) 執行檔 Stack.o 被載入記憶體後的情況
圖 5.18 目的檔 Stack.o 載入記憶體
後的情況
位址 記憶體內容
1200 08100003 30100000 2B000014 2B000028
1210 01100008 08100000 2C000000 00000000
1220 31100000 00200044 00300044 08400004
1230 08500001 15524000 05135000 13225000
1240 01200028 2C000000 00200020 00300020 Top:12
6C
1250 08400004 08500001 15524000 04135000
1260 14225000 01200004 2C000000 00000000
1270 XXXXXXXX XXXXXXXX XXXXXXXX XXXXXXXX
符號表 : S { (T,0000,main) (T,0020,push) (T,0048,pop)(B,0000, stack) (D,0000, top)
stack:127 }
0 修正記錄 : M { (T,0024,top,pc) (T,0028,stack,pc) (T,0040,top,pc) (T,0048,top,pc)
(T,004C,stack,pc) (T,0064,top,pc) }
圖 5.19 載入器的演算法 ( 頁 1)
載入器的演算法 說明
1 Algorithm Loader 載入器
2 Input objFile 輸入檔:執行檔
3 symbolTable = new SymbolTable() 建立符號表
4 sectionTable = new SectionTable(); 建立分段表
5 fileHeader = objFile.readHeader(); 讀取檔頭
6 memory = allocateMemory(objFile. 分配執行空間
7 memorySize); 取得開頭位址
8 address = memory.address 對於每個段頭
9 foreach sectionHeader in fileHeader 建立分段物件
10 section = new Section(sectionHeader); 放入分段表
11 sectionTable[section.name] = section 如果是 {T,D,B} 記錄
12 if section.type() in {T,D,B} 設定分段位址
13 section.startAddress = address 設定下一個分段位址
14 address = address +
15 section.memorySize;
… end if
end foreach
圖 5.19 載入器的演算法 ( 頁 2)
載入器的演算法 說明
… … …
16 foreach section se in sectionTable 對於每個分段 se
17 if section.type() in {T, D} 如果是 {T,D} 類型分段
18 put se.objCode into memory(se.startAddress) 將目的碼放入記憶體
19 else if section.type() in {S} 如果是 {S} 類型分段
20 foreach symbolRecord smb in section 取出其中的符號 smb
21 smb.address += se.startAddress 設定符號位址
22 end foreach
23 end if
24 end foreach
25 foreach section in sectionTable 對於每一個分段
26 if section.type() in {M} 如果是 {M} 類型分段
27 foreach modifyRecord m in section 對於每個修改記錄 m
28 modifyMemoryAt(m.address, 根據 m 修改記憶體
29 symbol[m.symbolID], m.type)
30 end for
31 end if
32 end foreach 將程式計數器 PC 設定為起
33 PC = memory.startAddress 始位址,開始執行。
34 End algorithm
5.6 動態連結
靜態連結器
如果再程式開始執行之前,連結器就給定了外部參考
的記憶體位址,則該連結器就被稱為靜態連結器
動態連結器
如果連結器在執行期間才決定外部參考的記憶體位址
,則被稱為動態連結器。
靜態連結 v.s 動態連結
靜態連結
傳統的連結器會將所有程式連結成單一的執行檔,在
執行時只需要該執行檔就能順利執行。
動態連結
使用動態連結機制時,函式庫可以先不需要被連結進
來,而是在需要的時候才透過動態連結器 (Dynamic
Linker) 尋找並連結函式庫,這種方式可以不用載入
全部的程式,因此可以節省記憶體。當很多執行中的
程式共用到函式庫時,動態連結所節省的記憶體數量
就相當可觀。
圖 5.20 動態連結機制的實作方
式
主程式 動態連結符號區
LD R1, var3@GOT
PUSH R1
CALL f2@PLT
PLT : 動態連結函數區
(Stub)
f1: f2_in_meory
LD PC, Ptr_f1@GOT
DL_f1: CALL DLinker
f2:
LD PC, Ptr_f2@GOT
DL_f2: CALL Dlinker
…
f2_in_memory:
….
動態連結器 ….
DLinker :
尋找 f1, f2, f3 … 對應的函數,
然後將函數位址填入 Ptr_f1,
Ptr_f2, Ptr_f3, …
動態連結函式庫的特性
動態連結函式庫通常是與位置無關的程式碼
(Position Independent Code) ,使用相對定址的方
式。否則,如果在動態連結時利用修改記錄修正函
式庫的記憶體內容,會造成每個程式都在修正函式
庫,就可能造成不一致的窘境。
動態連結函式庫的優缺點
動態函式庫的優點是可任意抽換,不需刻意更新舊
程式,系統隨時保持最新狀態。但是,這也可能造
成『動態連結地獄』 (DLL hell) 的困境,因為,
如果新的函式庫有錯,或者與舊的主程式不相容,
那麼,原本執行正常的程式會突然出現錯誤,甚至
無法使用。
Windows 與 Linux 中的動態連
結
在 Windows 中
稱為 DLLs (Dynamic Linking Libraries)
其附檔名通常為 .dll
在 UNIX/Linux 中
被稱為 Share Objects
其附檔名通常是 .so 。
動態載入技術
動態連結
編譯時就已經知道動態函數的名稱與參數類型,因此
,編譯器可以事先檢查函數型態的相容性。
動態載入
允許程式設計人員在程式執行的過程中,動態決定要
載入哪個函式庫,要執行哪個函數,這種技術比動態
連結更具有彈性,靈活度也更高。其方法是讓程式可
以呼叫載入器,以便動態的載入程式,因此才被稱為
動態載入技術。
動態載入技術
簡化版
Linux 中的 execve
強化版
Linux 中的 dl 函式庫
範例 5.6 使用 execve 呼叫載入器的範
例
#include <unistd.h>
int main()
{
char *argv[]={"ls","-al","/etc/passwd",(char
*)0};
char *envp[]={"PATH=/bin",0};
execve("/bin/ls",argv,envp);
}
表格 5.1 Linux 與 MS. Windows
中動態載入函式庫的對照比較表
UNIX/Linux Windows
#include
引入檔 #include <dlfcn.h>
<windows.h>
函式庫檔 libdl.so Kernel32.dll
LoadLibrary
載入功能 dlopen
LoadLibraryEx
取得函數 dlsym GetProcAddress
關閉功能 dlclose FreeLibrary
範例 5.7 Linux 動態載入函式庫的
範例
程式: dlcall.c 說明
編譯指令: gcc -o dl_call dl_call.c –ldl
#include <dlfcn.h> 引用 dlfcn.h 動態函式庫
int main(void) {
void *handle = dlopen ("libm.so", 開啟 shared library 'libm'
RTLD_LAZY); 宣告 cos() 函數的變數
double (*cosine)(double); 找出 cos() 函數的記憶體位址
cosine = dlsym(handle, "cos"); 呼叫 cos() 函數
printf ("%f\n", (*cosine)(2.0)); 關閉函式庫
dlclose(handle);
return 0;
}
5.7 實務案例
5.7.1 GNU 連結工具
$ ./stack 連結三者成為執行檔
x=3
執行 stack 檔
結果輸出 3 。
範例 5.10 將 < 範例 5.1> 的 C 語
言程式編譯為 IA32 組合語言
Cygwin 中的指令與執行結果 說明
$ gcc -S StackType.c -o 編譯 StackType.c ,輸出組合
StackType.s -I . 語言檔 StackType.s
編譯 StackFunc.c ,輸出組合
$ gcc -S StackFunc.c -o 語言檔 StackFunc.s
StackFunc.s -I . 編譯 StackMain.c ,輸出組合
語言檔 StackFunc.s
$ gcc -S StackMain.c -o
StackMain.s -I .
檔案: StackType.s
.file "StackType.c"
.globl _top
.bss
.align 4
_top:
.space 4
.comm _stack, 400 #
400
檔案: StackFunc.s
. .section .rdata,"dr" .section .rdata,"dr"
LC0: .ascii "top < STACK_SIZE\0" LC2: .ascii "top > 0\0"
LC1: .ascii "StackFunc.c\0" .text
.text .globl _pop
.globl _push .def _pop;.scl 2;.type
.def _push;.scl 2;.type 32;.endef
32;.endef _pop: pushl %ebp
_push: movl %esp, %ebp
pushl %ebp subl $24, %esp
movl %esp, %ebp cmpl $0, _top
subl $24, %esp jg L6
cmpl $99, _top movl $LC2, 8(%esp)
jle L3 movl $10, 4(%esp)
movl $LC0, 8(%esp) movl $LC1, (%esp)
movl $5, 4(%esp) call ___assert
movl $LC1, (%esp) L6: decl _top
call ___assert movl _top, %eax
L3: movl _top, %eax movl _stack(,%eax,4),
movl %eax, %edx %eax
movl 8(%ebp), %eax leave
movl %eax, ret
_stack(,%edx,4) .def ___assert;.scl 3;.type
incl _top 32;.endef
leave
檔案: StackMain.s
.file "StackMain.c" movl %eax, -8(%ebp)
.def ___main;.scl 2;.type 32;.endef movl -8(%ebp), %eax
.section .rdata,"dr" call __alloca
LC0: call ___main
.ascii "x=%d\12\0" movl $3, (%esp)
.text call _push
.globl _main call _pop
.def _main; .scl 2; movl %eax, -4(%ebp)
.type 32; .endef movl -4(%ebp), %eax
_main: movl %eax, 4(%esp)
pushl %ebp movl $LC0, (%esp)
movl %esp, %ebp call _printf
subl $24, %esp movl $0, %eax
andl $-16, %esp leave
movl $0, %eax ret
addl $15, %eax .def _printf;.scl 3;.type 32;.endef
addl $15, %eax .def _pop;.scl 3;.type 32;.endef
shrl $4, %eax .def _push;.scl 3;.type 32;.endef
sall $4, %eax
範例 5.12 使用 nm 指令觀看目的檔
的連結地圖
Cygwin 當中的操作與結果 說明
$ nm StackType.o 顯示 StackType.o 的連結地圖
00000000 b .bss (map)
00000000 d .data bss 段 (b: 未初始化變數 )
00000000 t .text data 段 (d: 已初始化資料 )
00000190 C _stack text 段 (t: 程式段 )
00000000 B _top int stack[] 的定義
$ nm StackFunc.o (C:Common)
00000000 b .bss int top=0 的定義 (B:bss)
00000000 d .data 顯示 StackType.o 的連結地圖
00000000 r .rdata (map)
00000000 t .text bss 段 (b: 未初始化變數 )
U ___assert data 段 (d: 已初始化資料 )
00000044 T _pop rdata 段 (r: 唯讀資料段 )
00000000 T _push text 段 (t: 程式段 )
U _stack 未定義 (U:___assert)
U _top int stack[] 的定義
(C:Common)
int top=0 的定義 (B:bss)
範例 5.13 用 strings 指令檢視目的
檔中的字串表
Cygwin 的操作
$ strings StackType.o
.text
0`.data
.bss
.file
StackType.c
.text
.data
.bss
_top
_stack
範例 5.14 用 size 指令檢視目的檔
中分段大小
Cygwin 的操作
$ size stack.exe
text data bss dec hex filename
1140 416 468 2024 7e8 stack.exe
製作靜態函式庫
GNU 工具的 ar 指令
建立函式庫
新增函式
取出函式
範例:
建立函式庫
ar -r lib/libstack.a StackFunc.o StackType.o
連結函式庫
gcc -o stack.o StackMain.c -lstack -L lib -I inc
表格 C.6 函式庫指令 ar 的參數表
參數 範例 說明
-r ar -r libx.a a.o 將 a.o 與 b.o 包裝為函式庫
-tv b.o-tv libx.a
ar libx.alibx.a 函式庫的內容
查看
-x ar -x libx.a a.o 取出 libx.a 中的目的檔 a.o
範例 5.15 使用 ar 指令建立函式
庫
Cygwin 當中的操作與結果 說明
$ ar -r lib/libstack.a StackFunc.o 建立 lib/libstack.a 函式
StackType.o 庫
ar: creating lib/libstack.a
$ gcc -o stack.o StackMain.c -lstack 編譯 StackMain.c 並連結
-L lib -I inc libstack.a 函式庫,輸
出執
行檔 stack.o
$ ./stack.o 執行 stack.o
x=3 結果輸出 3 。
$ ar -tv lib/libstack.a 顯示函式庫 libstack.a
rw-r--r-- … 略… 30 12:59 2009 包含 StackFunc.o
StackFunc.o 包含 StackType.o
rw-r--r-- … 略… 30 12:59 2009 從 lib/libstack.a 中取出
StackType.o StackFunc.o
$ ar -x lib/libstack.a StackFunc.o
製作動 態函式庫
建立動態函式庫
gcc -shared a.o b.o c.o -o libabc.so
連結動態函式庫
gcc -labc test.o -o test
GNU 目的檔工具
GNU 的目的檔工具可分為
觀察工具 :
objdump 、 nm 、 strings
修改工具 :
objcopy 、 strip
主要用法
用 objdump 觀察目的檔
用 objcopy 轉換目的檔
範例 5.16 使用 objdump 反組譯目的
檔
指令: objdump -d StackFunc.o
StackFunc.o: file format pe-i386
Disassembly of section .text:
00000000 <_push>:
0: 55 push %ebp
1: 89 e5 mov %esp,%ebp
3: 83 ec 18 sub $0x18,%esp
6: 83 3d 00 00 00 00 63 cmpl $0x63,0x0
d: 7e 1c jle 2b <_push+0x2b>
f: c7 44 24 08 00 00 00 movl $0x0,0x8(%esp)
16: 00
17: c7 44 24 04 05 00 00 movl $0x5,0x4(%esp)
1e: 00
1f: c7 04 24 11 00 00 00 movl $0x11,(%esp)
26: e8 00 00 00 00 call 2b <_push+0x2b>
2b: a1 00 00 00 00 mov 0x0,%eax
…
範例 5.17 使用 objdump 觀察目的
檔 ( 頁 1)
指令: objdump -x StackFunc.o
StackFunc.o: file format pe-i386
StackFunc.o
architecture: i386, flags 0x00000039:
HAS_RELOC, HAS_DEBUG, HAS_SYMS, HAS_LOCALS
start address 0x00000000
…
Sections:
Idx Name Size VMA LMA File off Algn
0 .text 00000084 00000000 00000000 000000b4 2**2
CONTENTS, ALLOC, LOAD, RELOC, READONLY, CODE
1 .data 00000000 00000000 00000000 00000000 2**2
ALLOC, LOAD, DATA
2 .bss 00000000 00000000 00000000 00000000 2**2
ALLOC
3 .rdata 00000028 00000000 00000000 00000138 2**2
CONTENTS, ALLOC, LOAD, READONLY, DATA
SYMBOL TABLE:
[ 0](sec -2)(fl 0x00)(ty 0)(scl 103) (nx 1) 0x00000000 StackFunc.c
File
[ 2](sec 1)(fl 0x00)(ty 20)(scl 2) (nx 1) 0x00000000 _push
AUX tagndx 0 ttlsiz 0x0 lnnos 0 next 0
範例 5.17 使用 objdump 觀察目的
檔 ( 頁 2)
…
RELOCATION RECORDS FOR [.text]:
OFFSET TYPE VALUE
00000008 dir32 _top
00000013 dir32 .rdata
00000022 dir32 .rdata
00000027 DISP32 ___assert
0000002c dir32 _top
00000038 dir32 _stack
0000003e dir32 _top
0000004c dir32 _top
00000057 dir32 .rdata
00000066 dir32 .rdata
0000006b DISP32 ___assert
00000071 dir32 _top
00000076 dir32 _top
0000007d dir32 _stack
目的檔修改工具 - objcopy
GNU 的主要目的檔複製工具 – Objcopy
該工具不只可以進行複製
還可以對目的檔進行修改與格式轉換等處理
Cygwin 使用過程
$ objcopy --readonly-text -I binary -O elf32-i386 -B i386 ccc.jpg ccc.o
$ ls -all
total 8
drwxrwxrwx+ 2 ccc None 0 May 15 13:11 .
drwxrwxrwx+ 8 ccc None 0 May 15 13:07 ..
-rwxrwxrwx 1 ccc None 3183 Jul 3 2008 ccc.jpg
-rw-r--r-- 1 ccc None 3612 May 15 13:11 ccc.o
$ objdump -x ccc.o | grep ccc
ccc.o: file format elf32-i386
ccc.o
00000000 g .data 00000000 _binary_ccc_jpg_start
00000c6f g .data 00000000 _binary_ccc_jpg_end
00000c6f g *ABS* 00000000 _binary_ccc_jpg_size
範例 5.19 使用外部變數引用目的檔
中的影像區塊
extern char *_binary_ccc_jpg_start;
extern char *_binary_ccc_jpg_end;
extern char *_binary_ccc_jpg_size;
範例 5.19 的作法,在嵌入式系統中相當常見,因為嵌
入式系統當中很可能沒有檔案系統,因此無法使用像
fopen() 這種檔案函數。此時,如果要顯示影像,就可
以直接將影像放入記憶體,然後取得其記憶體區塊,直
接在程式中使用。
範例 5.18 使用 objcopy 把影像檔
轉換為目的檔
Cygwin 使用過程
$ objcopy --readonly-text -I binary -O elf32-i386 -B i386 ccc.jpg ccc.o
$ ls -all
total 8
drwxrwxrwx+ 2 ccc None 0 May 15 13:11 .
drwxrwxrwx+ 8 ccc None 0 May 15 13:07 ..
-rwxrwxrwx 1 ccc None 3183 Jul 3 2008 ccc.jpg
-rw-r--r-- 1 ccc None 3612 May 15 13:11 ccc.o
$ objdump -x ccc.o | grep ccc
ccc.o: file format elf32-i386
ccc.o
00000000 g .data 00000000 _binary_ccc_jpg_start
00000c6f g .data 00000000 _binary_ccc_jpg_end
00000c6f g *ABS* 00000000 _binary_ccc_jpg_size
專案建置工具 - make
當程式越來越多時,編譯、連結與測試的動作會變
得相當繁瑣,此時就必須使用專案建置工具
$ make 進行專案編譯
gcc -c StackType.c -o StackType.o -I inc -L 編譯 StackType 中…
lib 編譯 StackFunc 中…
gcc -c StackFunc.c -o StackFunc.o -I inc -L 建立函式庫 lib/libstack.a 中…
lib 函式庫建立完成
ar -r lib/libstack.a StackType.o StackFunc.o 編譯主程式,並連結函式庫
ar: creating lib/libstack.a 輸出執行檔 stack
gcc StackMain.c -lstack -o stack -I inc -L lib
啟動執行檔 stack
輸出結果 x=3
$ ./stack
x=3
5.7.2 目的檔格式 – a.out
真正的目的檔,為了效能的緣故,通常不會儲存為
文字格式,而是儲存為二進位格式。像是 DOS
當中的 .com 檔案, Windows 當中的 .exe 檔案
,與 Linux 早期的 a.out 格式,還有近期的 ELF
格式,都是常見的目的檔格式。
為了讓讀者更清楚目的檔的格式,在本節中,我們
將以 Linux 早期使用的 a.out 這個格式,作為範
例,以便詳細說明目的檔的格式。
圖 5.22 兩種目的檔的格式 – a.out 與
ELF
檔頭 檔頭
a.out header ELF header
程式表頭
程式段
Program Header
Text Section
Table
資料段 第 1 段
Data Section Section 1
程式重定位資訊 第 2 段
Text Relocation Section 2
資料重定位資訊
…
Data Relocation
符號表 第 n 段
Symbol Table Section n
字串表 分段表頭
String Table Section Header Table
檔頭結構
struct exec { // a.out 的檔頭結構
unsigned long a_magic; // 執行檔魔數
// OMAGIC:0407, NMAGIC:0410,ZMAGIC:0413
unsigned a_text; // 程式段長度
unsigned a_data; // 資料段長度
unsigned a_bss; // 檔案中的未初始化資料區長度
unsigned a_syms; // 檔案中的符號表長度
unsigned a_entry; // 執行起始位址
unsigned a_trsize; // 程式重定位資訊長度
unsigned a_drsize; // 資料重定位資訊長度
};
圖 5.23 目的檔 a.out 的資料結構 ( 第
2頁)
重定位結構
struct relocation_info { // a.out 的重定位結構
int r_address; // 段內需要重定位的位址
unsigned int r_symbolnum:24; // r_extern=1 時 : 符號的序號值 ,
// r_extern=0 時 : 段內需要重定位的位址
unsigned int r_pcrel:1; // PC 相關旗標
unsigned int r_length:2; // 被重定位的欄位長度 (2 的次方 )
/ 2^0=1,2^1=2,2^2=4,2^3=8 bytes) 。
unsigned int r_extern:1; // 外部引用旗標。 1- 以外部符號重定位
// 0- 以段的地址重定位。
unsigned int r_pad:4; // 最後補滿的 4 個位元 ( 沒有使用到的部分 ) 。
};
圖 5.23 目的檔 a.out 的資料結構 ( 第
3頁)
符號表結構
struct nlist { // a.out 的符號表結構
union { //
char *n_name; // 字串指標,
struct nlist *n_next; // 或者是指向另一個符號項結構的指標,
long n_strx; // 或者是符號名稱在字串表中的位元組偏移值。
} n_un; //
unsigned char n_type; // 符號類型 N_ABS/N_TEXT/N_DATA/N_BSS 等。
char n_other; // 通常不用
short n_desc; // 保留給除錯程式用
unsigned long n_value; // 含有符號的值,對於代碼、資料和 BSS 符號,
// 通常是一個位址。
};
圖 5.24 目的檔a.out 各區段所對應
的資料結構
檔頭
struct exec {…}
header
程式段
0101…..
Text Section
資料段
資料結構 0101…..
Data Section
程式重定位資訊 struct relocation_info {…} ( 很多
Text Relocation 個)
資料重定位資訊 struct relocation_info {…} ( 很多
Data Relocation 個)
符號表
struct nlist {…} ( 很多個 )
Symbol Table
字串表 \0.bss\0.comment\0.data\0.text\
String Table 0stack\0ListA\0ListB\0 ….
是 UNIX/Linux 系統中較先進的目的檔格式。
發展出來的。
主要文件在『 System V Application Binary
格式的存放方式。
並且在第五章的 Program Loading and Dynamic
Linking 當中,說明了動態連結與載入的設計方法。
讀者可於下列網址下載到 System V 的二進位應用介
。
各家 CPU 廠商會自行補充與處理器有關的 ELF 規格書
ELF 的功用
ELF 可用來記錄
目的檔 (object file)
執行檔 (executable file)
動態連結檔 (share object)
核心傾印 (core dump) 檔
ELF 支援
動態連結與載入功能
ELF 的兩種不同時期之觀點
連結時期觀點 (Linking View)
一個區通常是數個分段的組合體
像是與程式有關的段落,包含程式段、程式重定位等,在執
行時期會被組合為一個分區。
圖 5.26 目的檔 ELF 的兩種不同觀點
檔頭 檔頭
ELF header ELF header
p_offset
程式表頭 ( 非必要 ) 程式表頭
Program Header Table Program Header Table
第 1 段 (Section 1)
第 2 段 (Section 2) 第 1 區 (Segment 1)
…
第 k 段 (Section k)
… 第 2 區 (Segment 2)
…
… …
ELF 的程式表頭記錄 說明
typedef struct {
Elf32_Word p_type; 分區類型
Elf32_Off p_offset; 分區位址 ( 在目的檔中 )
Elf32_Addr p_vaddr; 分區的虛擬記憶體位址
Elf32_Addr p_paddr; 分區的實體記憶體位址
Elf32_Word p_filesz; 分區在檔案中的大小
Elf32_Word p_memsz; 分區在記憶體中的大小
Elf32_Word p_flags; 分區旗標
Elf32_Word p_align; 分區的對齊資訊
} Elf32_Phdr;
圖 5.32 目的檔 ELF 的程式表頭
檔頭
ELF header
程式表頭 Program Header Table 區塊 1 的表頭
(ELF32_Phdr[0..m-1]) Elf32_Phdr[0]
區塊 2 的表頭
區塊 1 (Segment 1)
Elf32_Phdr[1]
區塊 2 (Segment 2) …
區塊 n 的表頭
…
Elf32_Phdr[m-1]
區塊 m (Segment m) Elf32_Phdr[i].p_offset
分段表頭
Section Header Table \0.bss\0.comment\0.data\0.text\0.got
\0stack\0LISTA\0LISTB\0Var1\0….
表格 5.3 目的檔ELF 的常見區塊列表
Segment Sections
說明
區塊型態 分段
參考文獻
http://www.jollen.org/EmbeddedLinux/Program_Loading.html
ELF 動態連結的作法
執行 ELF 載入動作時,使用的是以區塊為主的執行時
器是作業系統的一部分。
Linux 中的動態載入功能
1. Kernel 將 ELF 檔案中的所有 PT_LOAD 型態的區塊載入到記憶體,
這些區塊包含程式區塊與資料區塊。
4. Kernel 將動態連結器載入到記憶體,並將其映射到該行程的虛擬位址
空間中,然後啟動『動態連結器』。
5. 目的程式開始執行,在呼叫動態函數時,『動態連結器』根據需要,
決定出正確的連結順序,然後對該程式與動態函數進行重定位的動作
,再將控制權轉移到動態函數中。
Linux 與動態連結
動態連結器的英文名稱為 Dynamic Linker ,但在
ELF 文件中被稱為 Program Interpreter 。
ELF 的重定位結構 說明
typedef struct {
Elf32_Sword d_tag; 標記
union {
Elf32_Word d_val; 值 ( 用途很多樣 )
Elf32_Addr d_ptr; 指標 ( 程式的虛擬位址 )
} d_un;
} Elf32_Dyn;
extern Elf32_Dyn _DYNAMIC[]; 動態連結陣列
第 5 章、連結與載入 ( 本章摘要
)
5.1 簡介
目的檔、連結器、絕對載入器
5.2 函式庫
外部引用
5.3 目的檔
記錄: T,D,B,M,S ( 程式、資料、 BSS 、修改、符
號)
第 5 章、連結與載入 ( 本章摘要
)
5.4 連結器
將區段合併後,消除外部引用。
5.5 載入器
將執行檔載入記憶體,並利用修改記錄 M 進行修改
5.6 動態連結
在執行期間才決定外部參考的記憶體位址,則被稱為
動態連結器。
使用動態跳轉機制與 Stub 程式區。
第 5 章、連結與載入 ( 本章摘要
)
5.7 實務案例
5.7.1 GNU 連結工具
gcc, ld ( 連結器 ) 、 objdump ( 目的檔列印 ) 、 objcopy( 目的
檔複製 ) 、 nm ( 符號表列印 ) 、 ar ( 函式庫建立 ) 、 make
( 專案建置 )