看CSAPP第七章 連結時,老是感覺迷迷糊糊的,所以實際看看程式的可重定位目標檔案和可執行目標檔案的內容。

靜態連結ELF檔案介紹

我這裡以書中的程式碼為例

//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頭:

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頭:

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 ……。。