靜態連結ELF檔案介紹
看CSAPP第七章 連結時,老是感覺迷迷糊糊的,所以實際看看程式的可重定位目標檔案和可執行目標檔案的內容。
我這裡以書中的程式碼為例
//main。c
int
sum
(
int
*
a
,
int
n
);
int
array
[
2
]
=
{
1
,
2
};
int
main
(){
int
val
=
sum
(
array
,
2
);
return
val
;
}
//sum。c
int
sum
(
int
*
a
,
int
n
){
int
i
,
s
=
0
;
for
(
i
=
0
;
i
<
n
;
i
++
){
s
+=
a
[
i
];
}
return
s
;
}
然後在命令列中執行
gcc -Og -c main。c
可以得到
main。c
的可重定位目標檔案
main。o
,然後使用以下命令列檢視該ELF檔案
readelf -a -x 。text -x 。data main。o
可重定位目標檔案的ELF格式包含以下內容
我們依次檢視其中不同的資料節
ELF頭:
ELF Header:
Magic: 7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00
Class: ELF64
Data: 2‘s complement, little endian
Version: 1 (current)
OS/ABI: UNIX - System V
ABI Version: 0
Type: REL (Relocatable file)
Machine: Advanced Micro Devices X86-64
Version: 0x1
Entry point address: 0x0
Start of program headers: 0 (bytes into file)
Start of section headers: 704 (bytes into file)
Flags: 0x0
Size of this header: 64 (bytes)
Size of program headers: 0 (bytes)
Number of program headers: 0
Size of section headers: 64 (bytes)
Number of section headers: 12
Section header string table index: 11
可以看到ELF頭主要描述改檔案的生成資訊,並且包含了節頭部表的開始地址
Start of section headers
。
節頭部表:
Section Headers:
[Nr] Name Type Address Offset
Size EntSize Flags Link Info Align
[ 0] NULL 0000000000000000 00000000
0000000000000000 0000000000000000 0 0 0
[ 1] 。text PROGBITS 0000000000000000 00000040
000000000000001a 0000000000000000 AX 0 0 1
[ 2] 。rela。text RELA 0000000000000000 00000218
0000000000000030 0000000000000018 I 9 1 8
[ 3] 。data PROGBITS 0000000000000000 00000060
0000000000000008 0000000000000000 WA 0 0 8
[ 4] 。bss NOBITS 0000000000000000 00000068
0000000000000000 0000000000000000 WA 0 0 1
[ 5] 。comment PROGBITS 0000000000000000 00000068
000000000000002c 0000000000000001 MS 0 0 1
[ 6] 。note。GNU-stack PROGBITS 0000000000000000 00000094
0000000000000000 0000000000000000 0 0 1
[ 7] 。eh_frame PROGBITS 0000000000000000 00000098
0000000000000030 0000000000000000 A 0 0 8
[ 8] 。rela。eh_frame RELA 0000000000000000 00000248
0000000000000018 0000000000000018 I 9 7 8
[ 9] 。symtab SYMTAB 0000000000000000 000000c8
0000000000000120 0000000000000018 10 8 8
[10] 。strtab STRTAB 0000000000000000 000001e8
000000000000002d 0000000000000000 0 0 1
[11] 。shstrtab STRTAB 0000000000000000 00000260
0000000000000059 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)
可以看到,節頭部表中含有一個表,其中每個資料項含有以下內容(我只挑我會的說):
Nr
資料項的索引;
Name
資料項的名字,即該資料項是描述哪個資料節的;
Address
資料節所在的記憶體地址;
offset
資料節在目標檔案中的偏移量;
size
資料節的大小。
注意:
編譯器和彙編器會從地址0開始生成資料節,沒有考慮實際的記憶體地址,所以這裡的
address
都是0。
.text:
Hex dump of section ’。text‘:
NOTE: This section has relocations against it, but these have NOT been applied to this dump。
0x00000000 4883ec08 be020000 00488d3d 00000000 H……。。H。=。。。。
0x00000010 e8000000 004883c4 08c3 。。。。。H。。。。
該部分主要儲存程式碼片段,其中第一列為地址,後面的為十六進位制編碼的程式碼,我們可以透過以下命令列檢視
。text
對應的彙編程式碼
objdump -dx main。o
可以得到
。text
對應的彙編程式碼如下所示
Disassembly of section 。text:
0000000000000000
0: 48 83 ec 08 sub $0x8,%rsp
4: be 02 00 00 00 mov $0x2,%esi
9: 48 8d 3d 00 00 00 00 lea 0x0(%rip),%rdi # 10
c: R_X86_64_PC32 array-0x4
10: e8 00 00 00 00 callq 15
11: R_X86_64_PLT32 sum-0x4
15: 48 83 c4 08 add $0x8,%rsp
19: c3 retq
需要注意的一點是:編譯器和彙編器並不知道程式在執行時會放在記憶體的什麼位置,所以這裡會先使用佔位符。這裡的陣列
array
和函式
sum
的位置都不知道,可以看到第3行對
array
的引用的編碼
48 8d 3d 00 00 00 00
除了操作碼
48 8d 3d
以外都是0,來作為佔位符,以及第5行對
sum
的呼叫的編碼
e8 00 00 00 00
除了操作碼
e8
以外都是0,來作為佔位符。所以只有當分配了記憶體地址時,才能對這部分程式碼進行修改,也就是
重定位操作
。
這裡可以看到兩個重定位的型別,
R_X86_64_PC32
和
R_X86_64_PLT32
。
.rel.text:
Relocation section ’。rela。text‘ at offset 0x218 contains 2 entries:
Offset Info Type Sym。 Value Sym。 Name + Addend
00000000000c 000900000002 R_X86_64_PC32 0000000000000000 array - 4
000000000011 000b00000004 R_X86_64_PLT32 0000000000000000 sum - 4
該部分是彙編器對
。text
資料節生成的重定位條目,用來指示如何對
。text
節中地址未知的資料進行重定位操作。其中,
offset
表示要修改的位置位於
。text
中的偏移量,比如
array
中的
offset
為
0xc
,表示要從
。text
中偏移
0xc
處開始修改程式碼,即上面彙編程式碼中第3行的第4個位元組的位置;
type
表示重定位型別,決定了修改內容的計算;
Sym。Name
是要修改的對應的符號,當連結器賦予符號記憶體地址時,可以透過這個來獲取該符號在記憶體的位置。
Addend
修改內容的常量。
.data:
Hex dump of section ’。data‘:
0x00000000 01000000 02000000 ……。。
這裡主要儲存在程式碼中初始化的全域性變數,我們這裡只有一個
array
,這裡採用小端法,所以可以看到,第一列為資料項在
。data
中的偏移量,而後就是資料內容。
符號表:
Symbol table ’。symtab‘ contains 12 entries:
Num: Value Size Type Bind Vis Ndx Name
0: 0000000000000000 0 NOTYPE LOCAL DEFAULT UND
1: 0000000000000000 0 FILE LOCAL DEFAULT ABS main。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 6
6: 0000000000000000 0 SECTION LOCAL DEFAULT 7
7: 0000000000000000 0 SECTION LOCAL DEFAULT 5
8: 0000000000000000 26 FUNC GLOBAL DEFAULT 1 main
9: 0000000000000000 8 OBJECT GLOBAL DEFAULT 3 array
10: 0000000000000000 0 NOTYPE GLOBAL DEFAULT UND _GLOBAL_OFFSET_TABLE_
11: 0000000000000000 0 NOTYPE GLOBAL DEFAULT UND sum
彙編器構建了符號表,其中包含了
main。c
檔案中的所有符號,在重定位目標檔案中,
value
是該符號相對節位置的偏移量;
Size
為該符號對應資料的大小,可以透過
value
和
size
獲得該符號的內容;
Bind
表示符號的型別,非靜態的函式和全域性變數的符號為全域性符號,靜態的函式和全域性變數的符號為區域性符號,別的模組定義的全域性變數和函式的符號是外部符號,而外部符號也是全域性符號;
Ndx
表示該符號所在的資料節的索引,比如
main
函式就處在
。text
節,
array
陣列就處在
。data
節,而在可重定位目標檔案中,還有3個不在節頭部表的特殊偽節,
ABS
表示不要重定位的符號,
UND
表示未定義的符號,也就是在當前可重定位目標檔案中引用了,但是沒有找到對應定義的符號,這
也是連結器在符號解析中需要解決的符號
,
COMMON
儲存未初始化的全域性變數符號,因為未初始化的全域性變數符號是弱符號,所以在對當前檔案進行彙編時,彙編器不知道該符號最終會採用什麼定義,可能會被其他可重定位目標檔案修改,所以這裡就將這類符號儲存到
COMMON
中。
Name
就是符號。
當將可重定位目標檔案、靜態庫和動態庫透過連結器進行連結時
連結器會根據符號表檢視還有哪些符號是未定義的,然後根據其他檔案中對該符號的定義進行解析,使得每個符號引用都與一個符號定義相關聯。
連結器會將不同可重定位目標檔案和靜態庫中相同的資料節合併,然後分配記憶體地址,並且也對各個符號分配記憶體地址
連結器會根據分配好的地址,然後根據重定位表
。rel。text
和
。rel。data
中需要重定位的內容,對
。text
和
。data
進行重定位,使其指向當前的記憶體地址
接下來可以將
main。c
和
sum。c
編譯成可執行目標檔案,然後檢視該ELF中的內容
gcc -Og main。c sum。c
readelf -a -x 。text -x 。data a。out
可執行目標檔案的ELF格式包含以下內容
ELF頭:
ELF Header:
Magic: 7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00
Class: ELF64
Data: 2’s complement, little endian
Version: 1 (current)
OS/ABI: UNIX - System V
ABI Version: 0
Type: DYN (Shared object file)
Machine: Advanced Micro Devices X86-64
Version: 0x1
Entry point address: 0x4f0
Start of program headers: 64 (bytes into file)
Start of section headers: 6472 (bytes into file)
Flags: 0x0
Size of this header: 64 (bytes)
Size of program headers: 56 (bytes)
Number of program headers: 9
Size of section headers: 64 (bytes)
Number of section headers: 28
Section header string table index: 27
需要注意的是,這裡可執行目標檔案中多了一個段頭部表的起始位置
Start of program headers
段頭部表:
Program Headers:
Type Offset VirtAddr PhysAddr
FileSiz MemSiz Flags Align
PHDR 0x0000000000000040 0x0000000000000040 0x0000000000000040
0x00000000000001f8 0x00000000000001f8 R 0x8
INTERP 0x0000000000000238 0x0000000000000238 0x0000000000000238
0x000000000000001c 0x000000000000001c R 0x1
[Requesting program interpreter: /lib64/ld-linux-x86-64。so。2]
LOAD 0x0000000000000000 0x0000000000000000 0x0000000000000000
0x0000000000000810 0x0000000000000810 R E 0x200000
LOAD 0x0000000000000df0 0x0000000000200df0 0x0000000000200df0
0x0000000000000228 0x0000000000000230 RW 0x200000
DYNAMIC 0x0000000000000e00 0x0000000000200e00 0x0000000000200e00
0x00000000000001c0 0x00000000000001c0 RW 0x8
NOTE 0x0000000000000254 0x0000000000000254 0x0000000000000254
0x0000000000000044 0x0000000000000044 R 0x4
GNU_EH_FRAME 0x00000000000006b4 0x00000000000006b4 0x00000000000006b4
0x0000000000000044 0x0000000000000044 R 0x4
GNU_STACK 0x0000000000000000 0x0000000000000000 0x0000000000000000
0x0000000000000000 0x0000000000000000 RW 0x10
GNU_RELRO 0x0000000000000df0 0x0000000000200df0 0x0000000000200df0
0x0000000000000210 0x0000000000000210 R 0x1
Section to Segment mapping:
Segment Sections。。。
00
01 。interp
02 。interp 。note。ABI-tag 。note。gnu。build-id 。gnu。hash 。dynsym 。dynstr 。gnu。version 。gnu。version_r 。rela。dyn 。init 。plt 。plt。got 。text 。fini 。rodata 。eh_frame_hdr 。eh_frame
03 。init_array 。fini_array 。dynamic 。got 。data 。bss
04 。dynamic
05 。note。ABI-tag 。note。gnu。build-id
06 。eh_frame_hdr
07
08 。init_array 。fini_array 。dynamic 。got
段頭部表是可執行檔案必須包含的內容,
主要用來說明如何將可執行目標檔案對映到記憶體空間中
。可以注意到,它這裡將可執行目標檔案中不同的資料節根據不同讀寫執行的需要,分配成兩個段:資料段和程式碼段,透過
02 。interp 。note。ABI-tag 。note。gnu。build-id 。gnu。hash 。dynsym 。dynstr 。gnu。version 。gnu。version_r 。rela。dyn 。init 。plt 。plt。got 。text 。fini 。rodata 。eh_frame_hdr 。eh_frame
03 。init_array 。fini_array 。dynamic 。got 。data 。bss
可以知道每個段包含的資料節,其中第一行為程式碼段,第二行為資料段。而段頭部表會以段為單位將其對映到記憶體空間中。我們需要注意這兩行
Type Offset VirtAddr PhysAddr
FileSiz MemSiz Flags Align
LOAD 0x0000000000000000 0x0000000000000000 0x0000000000000000
0x0000000000000810 0x0000000000000810 R E 0x200000
LOAD 0x0000000000000df0 0x0000000000200df0 0x0000000000200df0
0x0000000000000228 0x0000000000000230 RW 0x200000
這裡說明了資料段和程式碼段對應的記憶體對映。其中,
offset
為可執行目標檔案中的偏移量,由此來定位到段的起始位置;
VirtAddr
為分配的虛擬記憶體地址;
PhysAddr
為分配的真實物理地址;
FileSiz
為要載入的資料節的大小;
MemSiz
表示申請的記憶體空間;
Flags
表示段的屬性,比如第一行程式碼段的
RE
表示可讀可執行,第二行資料段的
RW
表示可讀可寫。
節頭部表:
Section Headers:
[Nr] Name Type Address Offset
Size EntSize Flags Link Info Align
[ 0] NULL 0000000000000000 00000000
0000000000000000 0000000000000000 0 0 0
[ 1] 。interp PROGBITS 0000000000000238 00000238
000000000000001c 0000000000000000 A 0 0 1
[ 2] 。note。ABI-tag NOTE 0000000000000254 00000254
0000000000000020 0000000000000000 A 0 0 4
[ 3] 。note。gnu。build-i NOTE 0000000000000274 00000274
0000000000000024 0000000000000000 A 0 0 4
[ 4] 。gnu。hash GNU_HASH 0000000000000298 00000298
000000000000001c 0000000000000000 A 5 0 8
[ 5] 。dynsym DYNSYM 00000000000002b8 000002b8
0000000000000090 0000000000000018 A 6 1 8
[ 6] 。dynstr STRTAB 0000000000000348 00000348
000000000000007d 0000000000000000 A 0 0 1
[ 7] 。gnu。version VERSYM 00000000000003c6 000003c6
000000000000000c 0000000000000002 A 5 0 2
[ 8] 。gnu。version_r VERNEED 00000000000003d8 000003d8
0000000000000020 0000000000000000 A 6 1 8
[ 9] 。rela。dyn RELA 00000000000003f8 000003f8
00000000000000c0 0000000000000018 A 5 0 8
[10] 。init PROGBITS 00000000000004b8 000004b8
0000000000000017 0000000000000000 AX 0 0 4
[11] 。plt PROGBITS 00000000000004d0 000004d0
0000000000000010 0000000000000010 AX 0 0 16
[12] 。plt。got PROGBITS 00000000000004e0 000004e0
0000000000000008 0000000000000008 AX 0 0 8
[13] 。text PROGBITS 00000000000004f0 000004f0
00000000000001b2 0000000000000000 AX 0 0 16
[14] 。fini PROGBITS 00000000000006a4 000006a4
0000000000000009 0000000000000000 AX 0 0 4
[15] 。rodata PROGBITS 00000000000006b0 000006b0
0000000000000004 0000000000000004 AM 0 0 4
[16] 。eh_frame_hdr PROGBITS 00000000000006b4 000006b4
0000000000000044 0000000000000000 A 0 0 4
[17] 。eh_frame PROGBITS 00000000000006f8 000006f8
0000000000000118 0000000000000000 A 0 0 8
[18] 。init_array INIT_ARRAY 0000000000200df0 00000df0
0000000000000008 0000000000000008 WA 0 0 8
[19] 。fini_array FINI_ARRAY 0000000000200df8 00000df8
0000000000000008 0000000000000008 WA 0 0 8
[20] 。dynamic DYNAMIC 0000000000200e00 00000e00
00000000000001c0 0000000000000010 WA 6 0 8
[21] 。got PROGBITS 0000000000200fc0 00000fc0
0000000000000040 0000000000000008 WA 0 0 8
[22] 。data PROGBITS 0000000000201000 00001000
0000000000000018 0000000000000000 WA 0 0 8
[23] 。bss NOBITS 0000000000201018 00001018
0000000000000008 0000000000000000 WA 0 0 1
[24] 。comment PROGBITS 0000000000000000 00001018
000000000000002b 0000000000000001 MS 0 0 1
[25] 。symtab SYMTAB 0000000000000000 00001048
0000000000000600 0000000000000018 26 43 8
[26] 。strtab STRTAB 0000000000000000 00001648
0000000000000200 0000000000000000 0 0 1
[27] 。shstrtab STRTAB 0000000000000000 00001848
00000000000000f9 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)
這裡需要注意到,
Address
項已經不是0了,連結器已為所有資料節分配了對應的記憶體地址,所以
Address
就是其真實的記憶體地址。而我們這裡可以透過節頭部表中的
offset
和段頭部表中的
offset
將資料節對應起來。
其中多了幾個特殊的資料節,當載入器將可執行目標檔案根據段頭部表將對應的資料段和程式碼段載入到記憶體後,載入器會根據
。init
跳轉到由
ctrl。o
定義的
_start
函式,然後該函式會呼叫由
libc。so
定義的
__libc_start_main
函式來初始化執行環境,呼叫使用者層的
main
函式,所以
。init
是作為程式的入口點。
。interp
是包含動態連結器的路徑名,而動態聯結器本身就是共享目標(
ld-linux。so
),所以載入器可以載入和執行
。interp
中的動態連結器來完成動態連結。
並且我們可以看到,在可執行目標檔案中,已經不包含了重定位內容
。rel。text
和
。rel。data
。
.text:
我們可以透過
objdump -dx
來檢視
。text
中的彙編內容,我就挑選
main
和
sum
函式
00000000000005fa
5fa: 48 83 ec 08 sub $0x8,%rsp
5fe: be 02 00 00 00 mov $0x2,%esi
603: 48 8d 3d 06 0a 20 00 lea 0x200a06(%rip),%rdi # 201010
60a: e8 05 00 00 00 callq 614
60f: 48 83 c4 08 add $0x8,%rsp
613: c3 retq
0000000000000614
614: b8 00 00 00 00 mov $0x0,%eax
619: ba 00 00 00 00 mov $0x0,%edx
61e: eb 09 jmp 629
620: 48 63 ca movslq %edx,%rcx
623: 03 04 8f add (%rdi,%rcx,4),%eax
626: 83 c2 01 add $0x1,%edx
629: 39 f2 cmp %esi,%edx
62b: 7c f3 jl 620
62d: f3 c3 repz retq
62f: 90 nop
可以發現,原來在兩個可重定位目標檔案中的
main
和
sum
被合併到了可執行目標檔案中的同一個
。text
中了。然後原本是佔位符的編碼,現在也變成了真實的記憶體地址,比如
array
的
48 8d 3d 06 0a 20 00
,根據小端法可以得到
0x200a06
,由於這裡使用PC相對地址,所以可以根據下一條指令地址
0x60a
,計算出
array
的真實地址為
0x200a06+0x60a=0x201010
,也就能得到真實地址,根據
。data
的內容可知是正確的
Hex dump of section ‘。data’:
0x00201000 00000000 00000000 08102000 00000000 ……。。。。 。。。。。
0x00201010 01000000 02000000 ……。。