深入理解計算機第七章筆記-鏈接

何為鏈接

鏈接就是將不一樣部分的代碼和數據收集,然後再合成一個單一的文件,該文件可以被載入到內存並且被執行。鏈接可以在編譯時、載入時或運行時執行。

本文章涵蓋的內容有目標文件種類、ELF格式、PLT(Procedure Linkage Table,程序鏈接表)、GOT(Global Offset Table,全局偏移表)、靜態鏈接、載入時和運行時的動態鏈接。

在了解鏈接的過程之前,我們必須先對ELF格式和目標文件有個概念才能更加容易對鏈接有深刻的理解。

目標文件

目標文件是源代碼編譯後,但未進行鏈接的那些中間文件(Windows裡面是.obj,Linux裡面是.o),也可以說是位元組塊的集合。而連結器的作用就是要將這些元組塊連接起來,確定被連結塊的運行時位置,並修改代碼和數據塊中的各種位置。

目標文件的三種形式為:

  • 可重定位目標文件(Relocatable File):包含二進位制代碼和數據,可以在編譯時和其他可重定位目標文件或共享目標文件合拼,創建可執行目標文件。File Extension通常為.o。
  • 可執行目標文件(Executable File):包含二進位制代碼和數據,可以被直接複製到存儲器並執行。Linux裡面的一般沒有File Extension。
  • 共享目標文件(Shared Object File:特殊類型的可重定位目標文件,可在載入或運行時被動態地載入到存儲器並連結。通常以 .so 结尾。一般情况下,它有以下两种使用情景:
    (1)链接器(Link editor, ld)会处理它和其它可重定位文件以及共享目标文件,生成另外一个目标文件。
    (2)動態連結器(Dynamic Linker)將它與可執行文件以及其它共享目標合拼在一起生成進程映像。

目标文件由匯編器和鏈接器創建,是可以在处理器上直接運行的二进制程序。那些需要虛擬機才能够執行的程序,如shell脚本,並不属于这一范围。

初步認識ELF文件

而以上三種形式在Linux裡面都主要稱為ELF文件格式,因為ELF其實本身就是程序的載體,只要通過編譯後的代碼,最終都會被轉成ELF文件格式然後再讓系統根據所定義出來的ELF格式文件來進行所謂的靜態鏈接或動態鏈接(待查證)。

那麼一個ELF文件是由以下幾個部分所組成:

  • File header(文件頭)
  • Section header table(節區頭部表:通常可重定位目標文件需要這部分,而可執行目標文件則不一定要包含這部分。)
  • Program header table(程序頭部表:通常可執行目標文件需要這部分,而是可重定位目標文件則不一定要包含這部分。)
  • Contents of the sections or segments(節或段落裡面的內容)

ELF文件格式

英文版本的Elf文件格式

當然ELF文件一般會看成這2種視角。

Elf頭部

Elf頭部用來描述文件的基本屬性。例如:目標機器型號,程序入口地址和ELF文件版本等等。

ELF文件頭結構以及裡面的相關常數會被定義在/usr/include/elf.h裡面。這裡是elf.h的內容。

elf.h裡面ELF文件頭的定義部分是這樣的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
/* The ELF file header. This appears at the start of every ELF file.  */

#define EI_NIDENT (16)

/*32位的版本*/

typedef struct
{
unsigned char e_ident[EI_NIDENT]; /* Magic number and other info */
Elf32_Half e_type; /* Object file type */
Elf32_Half e_machine; /* Architecture */
Elf32_Word e_version; /* Object file version */
Elf32_Addr e_entry; /* Entry point virtual address */
Elf32_Off e_phoff; /* Program header table file offset */
Elf32_Off e_shoff; /* Section header table file offset */
Elf32_Word e_flags; /* Processor-specific flags */
Elf32_Half e_ehsize; /* ELF header size in bytes */
Elf32_Half e_phentsize; /* Program header table entry size */
Elf32_Half e_phnum; /* Program header table entry count */
Elf32_Half e_shentsize; /* Section header table entry size */
Elf32_Half e_shnum; /* Section header table entry count */
Elf32_Half e_shstrndx; /* Section header string table index */
} Elf32_Ehdr;

/*64位的版本*/

typedef struct
{
unsigned char e_ident[EI_NIDENT]; /* Magic number and other info */
Elf64_Half e_type; /* Object file type */
Elf64_Half e_machine; /* Architecture */
Elf64_Word e_version; /* Object file version */
Elf64_Addr e_entry; /* Entry point virtual address */
Elf64_Off e_phoff; /* Program header table file offset */
Elf64_Off e_shoff; /* Section header table file offset */
Elf64_Word e_flags; /* Processor-specific flags */
Elf64_Half e_ehsize; /* ELF header size in bytes */
Elf64_Half e_phentsize; /* Program header table entry size */
Elf64_Half e_phnum; /* Program header table entry count */
Elf64_Half e_shentsize; /* Section header table entry size */
Elf64_Half e_shnum; /* Section header table entry count */
Elf64_Half e_shstrndx; /* Section header string table index */
} Elf64_Ehdr;

/* Fields in the e_ident array. The EI_* macros are indices into the
array. The macros under each EI_* macro are the values the byte
may have. */

以下是ELF頭的例子。

而我用來查看SimpleSection.o的Elf頭。源文件代碼如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
/* SimpleSection.c */

int printf(const char* format,...);

int global_init_var =84; //
int global_uninit_var;

void func1(int i)
{
printf("%d\n",i);
}
int main(void)
{
static int static_var =85;
static int static_var2;
int a = 1;
int b;
funcl1(static_var + static_var2 + a + b);
}

然後用gcc -c SimpleSection.c後你會得到一個SimpleSection.o的二進制文件,這時你就可以用readelf -h SimpleSection.o來查看Elf頭。這裡 -c 的意思為不進行鏈接。

ELF頭部成員

e_ident

主要是用來判定該文件為ELF目標文件,同時提供關於用於解碼和解釋文件結構有關的數據表示方式(Data Representation of a data structure)。這個數組對於不同的下標的含義如下:

宏名稱 取值 目的
EI_MAG0 0 文件標識(File identification)
EI_MAG1 1 文件標識(File identification)
EI_MAG2 2 文件標識(File identification)
EI_MAG3 3 文件標識(File identification)
EI_CLASS 4 文件類別(File Class)
EI_DATA 5 數據編碼(Data Encoding)
EI_VERSION 6 文件版本(File Version)
EI_OSABI 7 OS/ABI 標識(OS/ABI)
EI_ABIVERSION 8 ABI版本
EI_PAD 9 補齊字節開始處(Start of padding bytes)
EI_NIDENT 16 e_ident部分的大小(Size of e-ident[])

其中EI_MAG0EI_MAG3會顯示出這個文件是ELF文件,也叫做魔數(Magic Number)。

名稱 位置
ELFMAG0 0x7f e_ident[EI_MAG0]
ELFMAG1 ‘E’ e_ident[EI_MAG1]
ELFMAG2 ‘L’ e_ident[EI_MAG2]
ELFMAG3 ‘F’ e_ident[EI_MAG3]

e_ident[EI_CLASS]e_ident[EI_MAG3]的下一个字節,專用來表示文件的類型或容量。

名稱 意義
ELFCLASSNONE 0 無效類型
ELFCLASS32 1 32位文件
ELFCLASS64 2 64位文件

這兩個類型的差別在於ELFCLASS32類型支持文件大小和虚拟地址空間上限为4GB的機器而ELFCLASS64類型被保留用于64位架構。它表明目標文件能会改變。在必要时,会定義附带有不同的基本類型目標文件數據大小的其他類型。

e_ident[EI_DATA]位元組給出了目標文件中的特定處理器數據的編碼方式。下面是目前已定義的編碼:

名稱 意義
ELFDATANONE 0 無效數據編碼
ELFDATA2LSB 1 小端
ELFDATA2MSB 2 大端
  • 大端模式,是指數據的高位元組保存在記憶體的低地址中,而數據的低位元組保存在記憶體的高地址中,這樣的存儲模式有點類似於把數據當作字串順序處理:地址由小向大增加,而數據從高位往低位放;這和我們的閱讀習慣一致。

  • 小端模式,是指數據的高位元組保存在記憶體的高地址中,而數據的低位元組保存在記憶體的低地址中,這種存儲模式將地址的高低和數據位權有效地結合起來,高地址部分權值高,低地址部分權值低。

其它值被保留,在未來必要時將被賦予新的編碼。

e_ident[EI_VERSION]給出了ELF頭的版本號。目前這個值必須是EV_CURRENT,詳情請看等下的e_version

e_ident[EI_PAD]給出了e_ident中未使用位元組的開始地址。這些位元組被保留並置為0。

處理目標文件的程序應該忽略它們。如果之後這些位元組被使用,EI_PAD的值就會改變。

e_type

用來定義文件的類型。

名稱 意義
ET_NONE 0 無文件類型
ET_REL 1 可重定位文件
ET_EXEC 2 可執行文件
ET_DYN 3 共享目標文件
ET_CORE 4 核心轉儲文件
ET_LOOS 0xfe00 操作系統指定
ET_HIOS 0xfeff 操作系統指定
ET_LOPROC 0xff00 處理器指定
ET_HIPROC 0xffff 處理器指定

雖然核心轉儲文件的內容沒有被詳細說明,但ET_CORE還是被保留用於標誌此類文件。從ET_LOOSET_HIOS(包括邊界)會被保留用於操作系統處理的場景。而從ET_LOPROCET_HIPROC(包括邊界)則會被保留用於處理器指定的場景。其它值在未來必要時可被賦予新的目標文件類型。

e_machine

用來定義當前文件的機械架構。

名稱|值|意義|
|:——|:——|:——|
|EM_NONE|0|無機械類型|
|EM_M32|1|AT&T WE 32100|
|EM_SPARC|2|SPARC|
|EM_386|3|Intel 80386|
|EM_68K|4|Motorola 68000|
|EM_88K|5|Motorola 88000|
|EM_860|7|Intel 80860|
|EM_MIPS|8|MIPS I Architecture|

當然還有其他類別的機械結構。

e_version

用來顯示文件當前版本。

名稱|值|意義|
|:——|:——|:——|
|EV_NONE|0|無效版本|
|EV_CURRENT|1|目前版本|

e_entry

這一項給系統轉交控制權給ELF中的代碼的虛擬地址。如果沒有相關入口項(Associated entry point),則這一項為0。

e_phoff

這一項給出程序頭部表在文件中的位元組偏移(Program Header table Offset)。如果文件中沒有程序頭部表,則為0。

e_shoff

這一項給出節頭表在文件中的位元組偏移(Section Header table Offset)。如果文件中沒有節頭表,則為0。

e_flags

這一項給出文件中與特定處理器相關的標誌,這些標誌命名格式為EF_machine_flag

e_ehsize

這一項給出ELF文件頭部的位元組長度(ELF Header Size)。

e_phentsize

這一項給出程序頭部表中每個表項的位元組長度(Program Header Entry Size)。每個表項的大小相同。

e_phnum

這一項給出程序頭部表的項數(Program Header entry Number)。因此,e_phnume_phentsize的乘積即為程序頭部表的位元組長度。如果文件中沒有程序頭部表,則該項值為0。

e_shentsize

這一項給出節頭的位元組長度(Section Header Entry Size)。一個節頭是節頭表中的一項;節頭表中所有項占據的空間大小相同。

e_shnum

這一項給出節頭表中的項數(Section Header Number)。因此,e_shnume_shentsize的乘積即為節頭表的位元組長度。如果文件中沒有節頭表,則該項值為0。

e_shstrndx

這一項給出節頭表中與節名字串表相關的表項的索引值(Section Header Table Index related with Section Name String Table)。如果文件中沒有節名字串表,則該項值為SHN_UNDEF。關於細節的介紹,請參考後面的「節」和「字串表」部分。

Elf程序頭(Program Header)

程序的頭部只有對於可執行文件和共享目標文件有意義。其中,ELF文件的頭中的e_phentsizee_phnum項指定了相應的程序頭的大小。程序頭的數據結構如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
/* Program segment header.  */

/*32位的版本*/

typedef struct
{
Elf32_Word p_type; /* Segment type */
Elf32_Off p_offset; /* Segment file offset */
Elf32_Addr p_vaddr; /* Segment virtual address */
Elf32_Addr p_paddr; /* Segment physical address */
Elf32_Word p_filesz; /* Segment size in file */
Elf32_Word p_memsz; /* Segment size in memory */
Elf32_Word p_flags; /* Segment flags */
Elf32_Word p_align; /* Segment alignment */
} Elf32_Phdr;

/*64位的版本*/

typedef struct
{
Elf64_Word p_type; /* Segment type */
Elf64_Word p_flags; /* Segment flags */
Elf64_Off p_offset; /* Segment file offset */
Elf64_Addr p_vaddr; /* Segment virtual address */
Elf64_Addr p_paddr; /* Segment physical address */
Elf64_Xword p_filesz; /* Segment size in file */
Elf64_Xword p_memsz; /* Segment size in memory */
Elf64_Xword p_align; /* Segment alignment */
} Elf64_Phdr;

Elf程序頭成員

字段 說明
p_type 表明了對應數組元素的類型或表面了如何解釋該數組元素,具體信息可看待會描述的p_type的段類型
p_offset 此字段為文件開始到段開頭第一字節的偏移
p_vaddr 第一字節內存的虛擬地址
p_paddr 用於物理地址尋址相關系統裡面,由於System V忽略了應用程序的物理尋址,因此可執行文件和共享文件的該項內容並沒有被限定
p_filesz 表明文件鏡像中該字段的大小,可以為0
p_memsz 表明內存鏡像中該字段的大小,可以為0
p_flags 給出和段相關的標記,具體信息可看待會的描述
p_align 需加載程序段的p_vaddr以及p_offset的大小為page的整數倍。該成員給出了段文件和內存中對齊方式。若該數值是0或者1,表示不需要對齊。此外,p_align是2的整數指數次方,並且p_vaddrp_offset在Modulo p_align的意義下應該相等。詳情請看待會描述

p_type的段類型

數值|名字|意義|
|:——|:——|:——|
|0x00000000|PT_NULL|表明段還沒被使用|
|0x00000001|PT_LOAD|是可加載的段,大小由p_fileszp_memsz來決定。文件中的位元組被映射到相應記憶體段開始處。如果p_memsz大於p_filesz,剩餘」的位元組都會變成0。p_filesz不能大於p_memsz。可載入的段在程序頭部中按照p_vaddr的升序排列。|
|0x00000002|PT_DYNAMIC|和動態鏈接信息有關|
|0x00000003|PT_INTERP|給出以NULL為结尾的字符串的位置和長度。該字串將被當作解釋器調用。這種段類型僅對可執行文件有意義(也可能出現在共享目標文件中)。此外,這種段在一個文件中最多出現一次。而且這種類型的段存在的話,它必須在所有可載入段項的前面。
|0x00000004|PT_NOTE|給出附加信息的位置和大小。|
|0x00000005|PT_SHLIB|該段類型被保留,不過語義未指定。而且,包含這種類型的段的程序不符合ABI標準。|
|0x00000006|PT_PHDR|該段類型的數組元素如果存在的話,則給出了程序頭部表自身的大小和位置,既包括在文件中也包括在記憶體中的訊息。此類型的段在文件中最多出現一次。此外,只有程序頭部表是程序的記憶體映像的一部分時,它才會出現。如果此類型段存在,則必須在所有可載入段項目的前面。|
|0x70000000|PT_LOPROC|略|
|0x7FFFFFFF|PT_HIPROC|略|

基地址-Base Address

用途:在動態連結期間重新定位程序。

1.程序頭部的虛擬地址可能並不是程序記憶體鏡像中實際的虛擬地址。可執行程序通常都會包含絕對地址的代碼。
2.為了使得程序可以正常執行,段必須在相應的虛擬地址處。
3.共享目標文件通常來說包含與地址無關的代碼。這可以使得共享目標文件可以被多個進程載入,同時保持程序執行的正確性。
4.系統會為不同的進程選擇不同的虛擬地址,也會保留段的相對地址,因為地址無關代碼使用段之間的相對地址來進行定址,記憶體中的虛擬地址之間的差必須與文件中的虛擬地址之間的差相匹配。
5.記憶體中任何段的虛擬地址與文件中對應的虛擬地址之間的差值對於任何一個可執行文件或共享對象來說是一個單一常量值。這個差值就是基地址。

可執行文件或者共享目標文件的基地址是在執行過程中由以下三個數值計算的。

  • 虛擬記憶體載入地址
  • 最大頁面大小
  • 程序可載入段的最低虛擬地址

要計算基地址,首先要確定可載入段中p_vaddr最小的記憶體虛擬地址,之後把該記憶體虛擬地址縮小為與之最近的最大頁面的整數倍即是基地址。根據要載入到記憶體中的文件的類型,記憶體地址可能與p_vaddr相同也可能不同。

段權限

節頭部表

ELF可重定位文件裡面有不同大小的條目(entry)。這些條目組成了節頭部表(Section Header Table)。如下圖所示:

在linux裡面查看Elf Section Header的方式是輸入readelf -S SimpleSection.o

在這裡我們可以看到一共有13個條目(節)。以下我會說明這些節的作用。

.text

存放已編譯程序的代碼。

.rodata

用來只讀數據

.data

存放已初始化的全局和靜態C變量。而局部的C變量則會在運行時保存在棧中。

.bss

和上面相反,只存放未初始化的全局C變量。在目標文件中,该節只是佔位符,不占用實際空間。

.symtab

符號表,存放程序中被定義和引用的函數和全局變量的信息。另外,它不包含局部變量的表目。

.rel.text

和其他文件連結時,.text節中任何調用外部函數或引用全局變數的指令都需要修改(調用本地函數的指令不需要修改)。

.rel.data

被模組定義或引用的任何全局變數的訊息,.data節中任何已初始化全局變數的初始值是全局變數或外部定義函數的地址都需要修改。

可執行目標文件不需要重定位訊息,通常省略.rel.text.rel.data

.debug

調校符號表,以-g選項編譯時得到。包含如定義的局部變數和類型定義,定義和引用的全局變數,原始的C源文件。

.line

原始的C源文件中的行號和.text節中機器指令之間的映射,以-g選項編譯時得到。

.strtab

字串表,是以null結尾的字串序列,包含.symtab和.debug節中的符號表,節頭部中的節名字。

.line

原始的C源文件中的行號和.text節中機器指令之間的映射,以-g選項編譯時得到。

.strtab

字符串表,是以null結尾的字符串序列,包含.symtab.debug節中的符號表,節頭部中的節名字。

符號和符號表

有3種符號:

  • 可重定位目標模塊定義並能被其他模塊引用的全局符號,對應於非靜態屬性的C函數和全局變量。
  • 其他模塊定義並被當前模塊引用的全局符號,稱為外部符號,對應於定義在其他模塊的非静态C函數和全局變量。
  • 只被當前模塊定義和引用的本地符號(局部符号),對應於有靜態屬性的C函數和全局變量。

符號表是匯編器構造的,它是一個包含了以下條目的數組。

1
2
3
4
5
6
7
8
9
typedef struct {
int name; /* String table offset */
char type:4, /* Function or data */
binding:4; /* Local or global */
char reserverd; /* Unused */
short section; /* Section hearder index */
long value; /* Section offset or absolute address */
long size; /* Object Size in bytes*/
} Elf64_Symbol;

/ Symbol table entry. /

typedef struct
{
Elf32_Word st_name; / Symbol name (string tbl index) /
Elf32_Addr st_value; / Symbol value /
Elf32_Word st_size; / Symbol size /
unsigned char st_info; / Symbol type and binding /
unsigned char st_other; / Symbol visibility /
Elf32_Section st_shndx; / Section index /
} Elf32_Sym;

typedef struct
{
Elf64_Word st_name; / Symbol name (string tbl index) /
unsigned char st_info; / Symbol type and binding /
unsigned char st_other; / Symbol visibility /
Elf64_Section st_shndx; / Section index /
Elf64_Addr st_value; / Symbol value /
Elf64_Xword st_size; / Symbol size /
} Elf64_Sym;

0%