連結裝載與庫 第4章 靜態連結
靜態連結
/* a.c */
extern int shared;
int main()
{
int a = 100;
swap( &a, &shared);
}
/* b.c */
int shared = 1;
void swap(int* a, int* b)
{
*a ^= *b ^= *a ^= *b;
}
4.1 空間與地址分配
對於多個輸入檔案,連結器如何將它們各個段合併到輸出檔案?必須要為這些段分配空間。
4.1.1 按序疊加
也就是將各個目標檔案依次合併。 缺點: 由於空間對齊而產生可觀的浪費。
4.1.2 相似段合併
將相同的段合併到一起,併為合併後的段分配空間。
現在的連結器基本都採用相似段合併的方式。整個連結過程分兩步:1. 空間與地址分配 2.符號解析與重定位。
- 空間與地址分配 掃描所有輸入目標檔案,獲得它們各個段的長度,屬性和位置,並且將他們合併,計算出輸出檔案中各個段合併後的長度與位置,並建立對映關係(對映關係由連結器儲存?)。同時將所有的符號定義和符號引用(為什麼需要符號引用)收集起來,統一放到一個全域性符號表中。
- **符號解析與重定位 ** 讀取輸入檔案中段的資料,重定位資訊,進行符號解析與重定位,調整程式碼中的地址。
[email protected]:~/compileLinkLoad# objdump -h a.o
a.o: file format elf32-i386
Sections:
Idx Name Size VMA LMA File off Algn
0 .group 00000008 00000000 00000000 00000034 2**2
CONTENTS, READONLY, EXCLUDE, GROUP, LINK_ONCE_DISCARD
1 .text 0000004a 00000000 00000000 0000003c 2**0
CONTENTS, ALLOC, LOAD, RELOC, READONLY, CODE
2 .data 00000000 00000000 00000000 00000086 2**0
CONTENTS, ALLOC, LOAD, DATA
3 .bss 00000000 00000000 00000000 00000086 2**0
ALLOC
4 .text.__x86.get_pc_thunk.ax 00000004 00000000 00000000 00000086 2**0
CONTENTS, ALLOC, LOAD, READONLY, CODE
5 .comment 0000002e 00000000 00000000 0000008a 2**0
CONTENTS, READONLY
6 .note.GNU-stack 00000000 00000000 00000000 000000b8 2**0
CONTENTS, READONLY
7 .eh_frame 00000060 00000000 00000000 000000b8 2**2
CONTENTS, ALLOC, LOAD, RELOC, READONLY, DATA
[email protected]:~/compileLinkLoad# objdump -h b.o
b.o: file format elf32-i386
Sections:
Idx Name Size VMA LMA File off Algn
0 .group 00000008 00000000 00000000 00000034 2**2
CONTENTS, READONLY, EXCLUDE, GROUP, LINK_ONCE_DISCARD
1 .text 00000043 00000000 00000000 0000003c 2**0
CONTENTS, ALLOC, LOAD, RELOC, READONLY, CODE
2 .data 00000004 00000000 00000000 00000080 2**2
CONTENTS, ALLOC, LOAD, DATA
3 .bss 00000000 00000000 00000000 00000084 2**0
ALLOC
4 .text.__x86.get_pc_thunk.ax 00000004 00000000 00000000 00000084 2**0
CONTENTS, ALLOC, LOAD, READONLY, CODE
5 .comment 0000002e 00000000 00000000 00000088 2**0
CONTENTS, READONLY
6 .note.GNU-stack 00000000 00000000 00000000 000000b6 2**0
CONTENTS, READONLY
7 .eh_frame 0000004c 00000000 00000000 000000b8 2**2
CONTENTS, ALLOC, LOAD, RELOC, READONLY, DATA
[email protected]:~/compileLinkLoad# objdump -h ab
ab: file format elf32-i386
Sections:
Idx Name Size VMA LMA File off Algn
0 .text 00000091 08048094 08048094 00000094 2**0
CONTENTS, ALLOC, LOAD, READONLY, CODE
1 .eh_frame 00000080 08048128 08048128 00000128 2**2
CONTENTS, ALLOC, LOAD, READONLY, DATA
2 .got.plt 0000000c 0804a000 0804a000 00001000 2**2
CONTENTS, ALLOC, LOAD, DATA
3 .data 00000004 0804a00c 0804a00c 0000100c 2**2
CONTENTS, ALLOC, LOAD, DATA
4 .comment 0000002d 00000000 00000000 00001010 2**0
CONTENTS, READONLY
VMA表示虛擬地址,LMA表示載入地址,一般這兩個值相等。在有些嵌入式系統中不相同。 (具體理解有待補充)
4.1.3 符號地址的確定
經過前面的空間分配之後,每個輸入檔案的每個段在輸出檔案的虛擬地址都已經確定。而每個符號相對於段的偏移也是確定的,所以每個符號在輸出檔案的虛擬地址也就確定了。
符號解析與重定位
4.2.1 重定位
將原始檔a.c編譯成a.o的時候,並不知道shared和main地址。那麼a.o中,是怎麼儲存這兩個變數的地址的呢?
[email protected]:~/compileLinkLoad# objdump -d a.o
a.o: file format elf32-i386
Disassembly of section .text:
00000000 <main>:
0: 8d 4c 24 04 lea 0x4(%esp),%ecx
4: 83 e4 f0 and $0xfffffff0,%esp
7: ff 71 fc pushl -0x4(%ecx)
a: 55 push %ebp
b: 89 e5 mov %esp,%ebp
d: 53 push %ebx
e: 51 push %ecx
f: 83 ec 10 sub $0x10,%esp
12: e8 fc ff ff ff call 13 <main+0x13>
17: 05 01 00 00 00 add $0x1,%eax
1c: c7 45 f4 64 00 00 00 movl $0x64,-0xc(%ebp)
23: 83 ec 08 sub $0x8,%esp
26: 8b 90 00 00 00 00 mov 0x0(%eax),%edx
2c: 52 push %edx
2d: 8d 55 f4 lea -0xc(%ebp),%edx
30: 52 push %edx
31: 89 c3 mov %eax,%ebx
33: e8 fc ff ff ff call 34 <main+0x34>
38: 83 c4 10 add $0x10,%esp
3b: b8 00 00 00 00 mov $0x0,%eax
40: 8d 65 f8 lea -0x8(%ebp),%esp
43: 59 pop %ecx
44: 5b pop %ebx
45: 5d pop %ebp
46: 8d 61 fc lea -0x4(%ecx),%esp
49: c3 ret
Disassembly of section .text.__x86.get_pc_thunk.ax:
00000000 <__x86.get_pc_thunk.ax>:
0: 8b 04 24 mov (%esp),%eax
3: c3 ret
可以看出,偏移為0x27的位置,shared變數的地址為0。偏移為0x34的位置,swap函式的地址為0xfffffffc(小端)。
連結的時候,這兩個地址會被修改為正確的值:
[email protected]:~/compileLinkLoad# objdump -d ab
ab: file format elf32-i386
Disassembly of section .text:
08048094 <main>:
8048094: 8d 4c 24 04 lea 0x4(%esp),%ecx
8048098: 83 e4 f0 and $0xfffffff0,%esp
804809b: ff 71 fc pushl -0x4(%ecx)
804809e: 55 push %ebp
804809f: 89 e5 mov %esp,%ebp
80480a1: 53 push %ebx
80480a2: 51 push %ecx
80480a3: 83 ec 10 sub $0x10,%esp
80480a6: e8 33 00 00 00 call 80480de <__x86.get_pc_thunk.ax>
80480ab: 05 55 1f 00 00 add $0x1f55,%eax
80480b0: c7 45 f4 64 00 00 00 movl $0x64,-0xc(%ebp)
80480b7: 83 ec 08 sub $0x8,%esp
80480ba: c7 c2 0c a0 04 08 mov $0x804a00c,%edx
80480c0: 52 push %edx
80480c1: 8d 55 f4 lea -0xc(%ebp),%edx
80480c4: 52 push %edx
80480c5: 89 c3 mov %eax,%ebx
80480c7: e8 16 00 00 00 call 80480e2 <swap>
80480cc: 83 c4 10 add $0x10,%esp
80480cf: b8 00 00 00 00 mov $0x0,%eax
80480d4: 8d 65 f8 lea -0x8(%ebp),%esp
80480d7: 59 pop %ecx
80480d8: 5b pop %ebx
80480d9: 5d pop %ebp
80480da: 8d 61 fc lea -0x4(%ecx),%esp
80480dd: c3 ret
....
shared變數地址為0x804a00c,函式地址為0x00000016(近址相對位移)
4.2.2 重定位表
重定位表儲存與重定位相關的資訊,供連結器在重定位的時候使用。
[email protected]:~/compileLinkLoad# objdump -r a.o
a.o: file format elf32-i386
RELOCATION RECORDS FOR [.text]:
OFFSET TYPE VALUE
00000013 R_386_PC32 __x86.get_pc_thunk.ax
00000018 R_386_GOTPC _GLOBAL_OFFSET_TABLE_
00000028 R_386_GOT32X shared
00000034 R_386_PLT32 swap
RELOCATION RECORDS FOR [.eh_frame]:
OFFSET TYPE VALUE
00000020 R_386_PC32 .text
00000054 R_386_PC32 .text.__x86.get_pc_thunk.ax
每個重定位表是elf檔案的一個段,是一個Elf32_Rel結構的陣列。
typedef struct
{
Elf32_Addr r_offset; /* Address */
Elf32_Word r_info; /* Relocation type and symbol index */
} Elf32_Rel;
r_offset 所要修正的位置的第一個位元組相對於段起始的偏移 r_info 重定位入口的型別和符號。低8位表示型別。高24位表示重定位入口的符號在符號表中的下標。
4.2.3 符號解析
當連結器需要對某個符號的引用進行重定位時,連結器就會去查的由所有輸入目標檔案的符號表組成的全域性符號表,找到相應的符號後進行重定位。 如果某個符號無法在全域性符號表中找到,連結器就會報符號未定義錯誤。
### 4.2.4 指令修正方式 不同的處理器定址方式千差萬別。對於32位的x86平臺下的ELF檔案的重定位入口所修正的指令定址方式只有2種: 絕對近址32位定址 相對近址32位定址 R_386_32 1 絕對定址修正 S + A R_386_PC32 2 相對定址修正 S+A-P A = 被儲存在修正位置的值 P = 被修正的位置 S = 符號的實際地址
4.3 common塊
對於c/c++來說,編譯器預設函式和初始化了的全域性變數為強符號,未初始化的全域性變數為弱符號。非全域性變數沒有強弱符號的概念。 針對強弱符號,連結器會按如下規則處理與選擇被多次定義的全域性符號:
- 不允許強符號多次定義。
- 強符號出現一次,弱符號出現一次或多次,選擇強符號
- 都是弱符號,選擇佔用空間最大的一個
強引用:如果找不到對應的符號,連結器就會報錯 弱引用:如果找不到對應的符號,連結器不會報錯
為什麼在目標檔案中,編譯不直接把未初始化的全域性變數也當做未初始化的區域性靜態變數一樣處理,為它在bss段分配空間,而是標記為一個common型別的變數。 這是因為未初始化的全域性變數是一個弱符號,經過連結之後,最終佔用的空間是未知的,所以無法放到BSS段。但是連結完成之後,最終還是放到了BSS段
4.4 c++相關問題
略
4.5 靜態庫連結
一個靜態庫可以簡單的看成一組目標檔案的集合,即很多目標檔案經過壓縮打包後形成的一個檔案。一般使用ar將多個目標檔案壓縮到一起,並進行編號和索引,以便於查詢和檢索。 用ar工具檢視靜態庫包含哪些檔案:
ar -t /usr/lib/i386-linux-gnu/libc.a
ld連結器要做的事情,就是在靜態庫中尋找所有需要的符號以及它們所在的目標檔案,將這些目標檔案解壓出來,連結在一起成為可執行檔案。
gcc -static --verbose -fno-builtin hello.c -static 表示靜態連結 –verbose 顯示詳細過程 -fno-builtin 關閉內建函式優化選項 可以看出整個過程分為3步:
- 編譯
- 彙編
- 連結
連結器在連結靜態庫的時候是以目標檔案為單位的,所以一個目標檔案只包含一個函式有助於減小生成檔案的大小。
4.6 連結過程控制
一些特殊的程式,如作業系統核心,BIOS,可能需要指定輸出檔案的各個段的邪氣地址,段的名稱,段存放的順序等。預設的連結規則無法滿足需求。
4.6.1 連結控制指令碼
在使用ld連結器的時候,沒有指定連結指令碼時會使用預設的連結指令碼,存放到/usr/lib/ldscripts/目錄下。 指定指令碼語法:
ld -T link.script
4.6.2 最小的程式“hello world”
- 不使用c語言庫
- 不使用main作為入口(貌似只是為了炫技)
/*TinyHelloWorld.c*/
char* str="Hello world!\n";
void print()
{
asm("movl $13,%%edx \n\t"
"movl %0,%%ecx \n\t"
"movl $0,%%ebx \n\t"
"movl $4,%%eax \n\t"
"int $0x80 \n\t"
::"r"(str):"edx","ecx","ebx");
}
void exit()
{
asm("movl $42,%ebx \n\t"
"movl $1,%eax \n\t"
"int $0x80 \n\t");
}
void nomain()
{
print();
exit();
}
編譯連結:
gcc -c -fno-builtin TinyHelloWorld.c
ld -static -e nomain -o TinyHelloWorld TinyHelloWorld.o
生成的TinyHelloWorld居然有5000個位元組(作者書上寫的是924)。而且有很多的段,顯然還不夠小。這個時候該輪到連結器指令碼上場了。
4.6.3 使用ld連結指令碼
/*TinyHelloWorld.lds*/
ENTRY(nomain)
SECTIONS
{
.= 0x08048000 + SIZEOF_HEADERS;
tinytext : { *(.text) *(.data) *(.rodata)}
/DISCARD/ :{ *(.comment) }
}
重新連結:
ld -static -T TinyHelloWorld.lds -o TinyHelloWorld TinyHelloWorld.o
刪除這些多餘的段之後,大小直接變為了1160位元組,果然厲害。
4.6.4 ld連結指令碼語法簡介
略。將來有需要再細學
4.7 BFD庫
現代的硬體和軟體平臺各類繁多,導致編譯器和連結器很難處理不同平臺之間的目標檔案。
BFD(binary file descriptor library)是一個GNU專案,致力於規則一種統一的介面來處理不同目標檔案格式之間的差異。 現在GCC,ld,GDB及binutils的其他工具都通過BFD庫來處理目標檔案,而不直接操作目標檔案。從而將編譯器,連結器本身同具體的目標檔案格式隔離開來。