好文轉載:ELF文件格式及程序加載執行過程總彙

ELF文件格式及程序加載執行過程總彙

好文轉自: http://www.linuxsir.org/bbs/printthread.php?t=206356
這是我這段時間學習elf文件格式蒐集的資料,其中的一些重量級文檔,比如linkers and loaders ,the executable and linkable format等等就不貼出來了,太大----


文章列表爲:

elf文件格式-- 1
elf文件格式-- 2
elf文件格式-- 3
Linux動態鏈接技術
Elf動態解析符號過程
Linux 動態函式庫解析
Before main() 分析
UNIX/LINUX 平臺可執行文件格式分析
分析ELF的加載過程
如何修改動態庫符號表


elf文件格式-- 1
這份文檔和原始的那份ELF文件格式的文檔有以下一個不同:

1. 忽略了分頁記數 。
2. 因爲上述原因,在這篇內容目錄中去掉了頁號,索引完全被忽略。
(不象Postscript文檔,txt文本可以用來搜索)
3. 頁標題的內容和文章的頁腳已經在開始的時候被換掉了。
4. 文章的排版也已經修正過了。
5. 如果必要,不同的字體已經被忽略了。大部分地方,這片文檔能讓你
充分的理解。然而,很小的地方,原始的文檔使用了斜體字來指出文
章中的字符變量。在那種情況下,本文使用<尖括號>。在原始的文檔
中沒有出現尖括號。
6. 原始的文檔有三個錯誤,如果你是不經意讀它的話,是不會明顯
就能找出的。但是在這裏,明確的被鑑別出來了。
我很冒昧的糾正了那些錯誤。在他們的位置用一個做上了標記。
可能還有其他我沒有看出來的的錯誤。

如果有如何其他的區別都是我的責任。這樣的錯誤請
mailto:[email protected].

Brian Raiter
[Last edited Fri Jul 23 1999]

________________________________________________________________


EXECUTABLE AND LINKABLE FORMAT (ELF)

Portable Formats Specification, Version 1.1
Tool Interface Standards (TIS)

________________________________________________________________


=========================== Contents 內容===========================


序言
1. OBJECT文件
導言
ELF頭(ELF Header)
Sections
String表(String Table)
Symbol表(Symbol Table)
重定位(Relocation)
2. 程序裝載與動態連接
導言
Program頭(Program Header)
Program裝載(Program Loading)
Dynamic連接(Dynamic Linking)
3. C LIBRARY
C Library

________________________________________________________________


導言

________________________________________________________________


ELF: 可執行連接格式

可執行連接格式是UNIX系統實驗室(USL)作爲應用程序二進制接口
(Application Binary Interface(ABI)而開發和發佈的。工具接口標準委
員會(TIS)選擇了正在發展中的ELF標準作爲工作在32位INTEL體系上不同操
作系統之間可移植的二進制文件格式。

假定開發者定義了一個二進制接口集合,ELF標準用它來支持流線型的軟件
發展。 應該減少不同執行接口的數量。因此可以減少重新編程重新編譯的
代碼。


關於這片文檔

這篇文檔是爲那些想創建目標文件或者在不同的操作系統上執行文件的開發
着準備的。它分以下三個部分:

* 第一部分, “目標文件Object Files”描述了ELF目標文件格式三種主要
的類型。
* 第二部分, “程序轉載和動態連接”描述了目標文件的信息和系統在創建
運行時程序的行爲。
* 第三部分, “C 語言庫”列出了所有包含在libsys中的符號,標準的ANSI C
和libc的運行程序,還有libc運行程序所需的全局的數據符號。

注意: 參考的X86體系已經被改成了Intel體系。

________________________________________________________________


1. 目標文件(Object file)

________________________________________________________________


========================= 序言 =========================


第一部分描述了iABI的object文件的格式, 被稱爲ELF(Executable
and Linking Format). 在object文件中有三種主要的類型。

* 一個可重定位(relocatable)文件保存着代碼和適當的數據,用來和其他的
object文件一起來創建一個可執行文件或者是一個共享文件。
* 一個可執行(executable)文件保存着一個用來執行的程序;該文件指出了
exec(BA_OS)如何來創建程序進程映象。
* 一個共享object文件保存着代碼和合適的數據,用來被下面的兩個鏈接器
鏈接。第一個是連接編輯器[請參看ld(SD_CMD)],可以和其他的可重定位和
共享object文件來創建其他的object。第二個是動態鏈接器,聯合一個
可執行文件和其他的共享object文件來創建一個進程映象。

一個object文件被彙編器和聯接器創建, 想要在處理機上直接運行的object
文件都是以二進制來存放的。那些需要抽象機制的程序,比如象shell腳本,
是不被接受的。

在介紹性的材料過後,第一部分主要圍繞着文件的格式和關於如何建立程序。
第二部分也描述了object文件的幾個組成部分,集中在執行程序所必須的信息上。


文件格式

Object文件參與程序的聯接(創建一個程序)和程序的執行(運行一個程序)。
object 文件格式提供了一個方便有效的方法並行的視角看待文件的內容,
在他們的活動中,反映出不同的需要。例 1-1圖顯示了一個object文件的
組織圖。

+ 圖1-1: Object文件格式

Linking 視角 Execution 視角
============ ==============
ELF header ELF header
Program header table (optional) Program header table
Section 1 Segment 1
... Segment 2
Section n ...
Section header table Section header table (optional)

一個ELF頭在文件的開始,保存了路線圖(road map),描述了該文件的組織情況。
sections保存着object 文件的信息,從連接角度看:包括指令,數據,
符號表,重定位信息等等。特別sections的描述會出項在以後的第一部分。
第二部分討論了段和從程序的執行角度看文件。

假如一個程序頭表(program header table)存在,那麼它告訴系統如何來創建一
個進程的內存映象。被用來建立進程映象(執行一個程序)的文件必須要有一個程
序頭表(program header table);可重定位文件不需要這個頭表。一個
section頭表(section header table)包含了描述文件sections的信息。每個
section在這個表中有一個入口;每個入口給出了該section的名字,大小,
等等信息。在聯接過程中的文件必須有一個section頭表;其他object文件可要
可不要這個section頭表。

注意: 雖然圖顯示出程序頭表立刻出現在一個ELF頭後,section頭表跟着其他
section部分出現,事實是的文件是可以不同的。此外,sections和段(segments)
沒有特別的順序。只有ELF頭(elf header)是在文件的固定位置。


數據表示
object文件格式支持8位、32位不同的處理器。不過,它試圖努力的在更大
或更小的體系上運行。因此,object文件描繪一些控制數據需要用與機器
無關的格式,使它儘可能的用一般的方法甄別object文件和描述他們的內容。
在object文件中剩餘的數據使用目標處理器的編碼方式,不管文件是在哪臺
機子上創建的。

+ 圖 1-2: 32-Bit Data Types

Name Size Alignment Purpose
==== ==== ========= =======
Elf32_Addr 4 4 Unsigned program address
Elf32_Half 2 2 Unsigned medium integer
Elf32_Off 4 4 Unsigned file offset
Elf32_Sword 4 4 Signed large integer
Elf32_Word 4 4 Unsigned large integer
unsigned char 1 1 Unsigned small integer

所有的object文件格式定義的數據結構是自然大小(natural size),爲相關
的類型調整指針。如果需要,數據結構中明確的包含了確保4字節對齊的填
充字段。來使結構大小是4的倍數。數據從文件的開始也有適當的對齊。
例如,一個包含了Elf32_Addr成員的結構將會在文件內對齊到4字節的邊界上。

因爲移植性的原因,ELF不使用位字段。


========================== ELF Header ==========================


一些object文件的控制結構能夠增長的,所以ELF頭包含了他們目前的大小。
假如object文件格式改變,程序可能會碰到或大或小他們不希望的控制結構。
程序也有可能忽略額外(extra)的信息。
對待來歷不明(missing)的信息依靠上下文來解釋,假如擴展被定義,它們
將會被指定。

+ 圖 1-3: ELF Header

#define EI_NIDENT 16

typedef struct {
unsigned char e_ident[EI_NIDENT];
Elf32_Half e_type;
Elf32_Half e_machine;
Elf32_Word e_version;
Elf32_Addr e_entry;
Elf32_Off e_phoff;
Elf32_Off e_shoff;
Elf32_Word e_flags;
Elf32_Half e_ehsize;
Elf32_Half e_phentsize;
Elf32_Half e_phnum;
Elf32_Half e_shentsize;
Elf32_Half e_shnum;
Elf32_Half e_shstrndx;
} Elf32_Ehdr;

* e_ident

這個最初的字段標示了該文件爲一個object文件,提供了一個機器無關
的數據,解釋文件的內容。在下面的ELF的鑑別(ELF Identification)
部分有更詳細的信息。

* e_type

該成員確定該object的類型。

Name Value Meaning
==== ===== =======
ET_NONE 0 No file type
ET_REL 1 Relocatable file
ET_EXEC 2 Executable file
ET_DYN 3 Shared object file
ET_CORE 4 Core file
ET_LOPROC 0xff00 Processor-specific
ET_HIPROC 0xffff Processor-specific

雖然CORE的文件內容未被指明,類型ET_CORE是保留的。
值從 ET_LOPROC 到 ET_HIPROC(包括ET_HIPROC)是爲特殊的處理器保留的。
如有需要,其他保留的變量將用在新的object文件類型上。

* e_machine

該成員變量指出了運行該程序需要的體系結構。

Name Value Meaning
==== ===== =======
EM_NONE 0 No machine
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 RS3000

如有需要,其他保留的值將用到新的機器類型上。特殊處理器名使用機器名來
區別他們。例如,下面將要被提到的成員flags使用前綴EF_;在一臺EM_XYZ機器
上,flag稱爲WIDGET,那麼就稱爲EF_XYZ_WIDGET。

* e_version

這個成員確定object文件的版本。

Name Value Meaning
==== ===== =======
EV_NONE 0 Invalid version
EV_CURRENT 1 Current version

值1表示原來的文件格式;創建新版本就用>1的數。EV_CURRENT值(上面給
出爲1)如果需要將指向當前的版本號。

* e_entry

該成員是系統第一個傳輸控制的虛擬地址,在那啓動進程。假如文件沒有
如何關聯的入口點,該成員就保持爲0。

* e_phoff

該成員保持着程序頭表(program header table)在文件中的偏移量(以字節計數)。
假如該文件沒有程序頭表的的話,該成員就保持爲0。

* e_shoff

該成員保持着section頭表(section header table)在文件中的偏移量(以字節
計數)。假如該文件沒有section頭表的的話,該成員就保持爲0。

* e_flags

該成員保存着相關文件的特定處理器標誌。
flag的名字來自於EF__。看下機器信息“Machine Information”
部分的flag的定義。

* e_ehsize

該成員保存着ELF頭大小(以字節計數)。

* e_phentsize

該成員保存着在文件的程序頭表(program header table)中一個入口的大小
(以字節計數)。所有的入口都是同樣的大小。

* e_phnum

該成員保存着在程序頭表中入口的個數。因此,e_phentsize和e_phnum
的乘機就是表的大小(以字節計數).假如沒有程序頭表(program header table),
e_phnum變量爲0。

* e_shentsize

該成員保存着section頭的大小(以字節計數)。一個section頭是在section
頭表(section header table)的一個入口;所有的入口都是同樣的大小。

* e_shnum

該成員保存着在section header table中的入口數目。因此,e_shentsize和
e_shnum的乘積就是section頭表的大小(以字節計數)。
假如文件沒有section頭表,e_shnum值爲0。

* e_shstrndx

該成員保存着跟section名字字符表相關入口的section頭表(section header
table)索引。假如文件中沒有section名字字符表,該變量值爲SHN_UNDEF。
更詳細的信息 看下面“Sections”和字符串表(“String Table”) 。


ELF 鑑別(Identification)

在上面提到的,ELF提供了一個object文件的框架結構來支持多種處理機,多
樣的數據編碼方式,多種機器類型。爲了支持這個object文件家族,最初的幾
個字節指定用來說明如何解釋該文件,獨立於處理器,與文件剩下的內容無關。

ELF頭(也就是object文件)最初的幾個字節與成員e_ident相一致。

+ 圖 1-4: e_ident[] Identification Indexes

Name Value Purpose
==== ===== =======
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_PAD 7 Start of padding bytes
EI_NIDENT 16 Size of e_ident[]

通過索引訪問字節,以下的變量被定義。

* EI_MAG0 to EI_MAG3

文件的前4個字符保存着一個魔術數(magic number),用來確定該文件是否
爲ELF的目標文件。

Name Value Position
==== ===== ========
ELFMAG0 0x7f e_ident[EI_MAG0]
ELFMAG1 ‘E‘ e_ident[EI_MAG1]
ELFMAG2 ‘L‘ e_ident[EI_MAG2]
ELFMAG3 ‘F‘ e_ident[EI_MAG3]

* EI_CLASS

接下來的字節是e_ident[EI_CLASS],用來確定文件的類型或者說是能力。

Name Value Meaning
==== ===== =======
ELFCLASSNONE 0 Invalid class
ELFCLASS32 1 32-bit objects
ELFCLASS64 2 64-bit objects

文件格式被設計成在不同大小機器中可移植的,在小型機上的不用大型機上
的尺寸。類型ELFCLASS32支持虛擬地址空間最大可達4GB的機器;使用上面
定義過的基本類型。

類型ELFCLASS64爲64位體系的機器保留。它的出現表明了object文件可能
改變,但是64位的格式還沒有被定義。如果需要,其他類型將被定義,會
有不同的類型和不同大小的數據尺寸。

* EI_DATA

字節e_ident[EI_DATA]指定了在object文件中特定處理器數據的編碼
方式。當前定義了以下編碼方式。

Name Value Meaning
==== ===== =======
ELFDATANONE 0 Invalid data encoding
ELFDATA2LSB 1 See below
ELFDATA2MSB 2 See below

更多的關於編碼的信息出現在下面。其他值保留,將被分配一個新的編碼
方式,當然如果必要的話。

* EI_VERSION

字節e_ident[EI_VERSION]表明了ELF頭的版本號。
現在這個變量的是一定要設爲EV_CURRENT,作爲上面e_version的解釋。

* EI_PAD

該變量標識了在e_ident中開始的未使用的字節。那些字節保留並被設置爲
0;程序把它們從object 文件中讀出但應該忽略。假如當前未被使用的字節
有了新的定義,EI_PAD變量將來會被改變。

一個文件的數據編碼指出瞭如何來解釋一個基本的object文件。在上述的
描述中,類型ELFCLAS32文件使用佔用1,2和4字節的目標文件。下面定義的
編碼方式,用下面的圖來描繪。數據出現在左上角。


ELFDATA2LSB編碼指定了2的補數值。
最小有意義的字節佔有最低的地址。

+ 圖1-5: Data Encoding ELFDATA2LSB

0------+
0x0102 | 01 |
+------+
0------1------+
0x010204 | 02 | 01 |
+------+------+
0------1------2------3------+
0x01020304 | 04 | 03 | 02 | 01 |
+------+------+------+------+

ELFDATA2LSB編碼指定了2的補數值。
最大有意義的字節佔有最低的地址。

+ 圖1-6: Data Encoding ELFDATA2MSB

0------+
0x0102 | 01 |
+------+
0------1------+
0x010204 | 01 | 02 |
+------+------+
0------1------2------3------+
0x01020304 | 01 | 02 | 03 | 04 |
+------+------+------+------+


機器信息

爲了確定文件,32位Intel體系結構的需要以下的變量。

+ 圖1-7: 32-bit Intel Architecture Identification, e_ident

Position Value
======== =====
e_ident[EI_CLASS] ELFCLASS32
e_ident[EI_DATA] ELFDATA2LSB

處理器確認ELF頭裏的e_machine成員,該成員必須爲EM_386。

ELF報頭裏的e_flags成員保存了和文件相關的位標記。32位Intel體系上未
定義該標記;所以這個成員應該爲0;


=========================== Sections ===========================


一個object文件的section header table可以讓我們定位所有的sections。
section header table是個Elf32_Shdr結構的數組(下面描述)。一個section
報頭表(section header table)索引是這個數組的下標。ELF header table
的e_shoff成員給出了section報頭表的偏移量(從文件開始的計數)。e_shnum
告訴我們section報頭表中包含了多少個入口;e_shentsize 給出了每個
入口的大小。

一些section報頭表索引是保留的;那些特別的索引在一個object文件中
將沒有與之對應sections。

+ 圖1-8: Special Section Indexes

Name Value
==== =====
SHN_UNDEF 0
SHN_LORESERVE 0xff00
SHN_LOPROC 0xff00
SHN_HIPROC 0xff1f
SHN_ABS 0xfff1
SHN_COMMON 0xfff2
SHN_HIRESERVE 0xffff

* SHN_UNDEF

該值表明沒有定義,缺少,不相關的或者其他涉及到的無意義的section。
例如,標號“defined”相對於section索引號SHN_UNDEF是一個沒有被
定義的標號。

注意: 雖然索引0保留作爲未定義的值,section報頭表包含了一個索引0的
入口。因此,假如ELF報頭說一個文件的section報頭表中有6個section入口
的話,e_shnum的值應該是從0到5。最初的入口的內容以後在這個section中
被指定。

* SHN_LORESERVE

該值指定保留的索引範圍的最小值。

* SHN_LOPROC through SHN_HIPROC

該值包含了特定處理器語意的保留範圍。


* SHN_ABS

該變量是相對於相應參考的絕對地址。
例如,section號的標號是絕對地址,不被重定位影響。

* SHN_COMMON

該section的標號是一個公共(common)的標號,就象FORTRAN COMMON
或者不允許的C擴展變量。

* SHN_HIRESERVE

該值指定保留的索引範圍的上限。系統保留的索引值是從SHN_LORESERVE
到SHN_HIRESERVE;該變量不涉及到section報頭表(section header table)。
因此,section報頭表不爲保留的索引值包含入口。

sections包含了在一個object文件中的所有信息,除了ELF報頭,程序報頭
表(program header table),和section報頭表(section header table)。
此外,object文件的sections滿足幾天條件:

* 每個在object文件中的section都有自己的一個section的報頭來描述它。
section頭可能存在但section可以不存在。
* 每個section在文件中都佔有一個連續順序的空間(但可能是空的)。
* 文件中的Sections不可能重複。文件中沒有一個字節既在這個section中
又在另外的一個section中。
* object文件可以有"非活動的"空間。不同的報頭和sections可以不覆蓋到
object文件中的每個字節。"非活動"數據內容是未指定的。

一個section頭有如下的結構。

+ 圖1-9: Section Header

typedef struct {
Elf32_Word sh_name;
Elf32_Word sh_type;
Elf32_Word sh_flags;
Elf32_Addr sh_addr;
Elf32_Off sh_offset;
Elf32_Word sh_size;
Elf32_Word sh_link;
Elf32_Word sh_info;
Elf32_Word sh_addralign;
Elf32_Word sh_entsize;
} Elf32_Shdr;

* sh_name

該成員指定了這個section的名字。它的值是section報頭字符表section的
索引。[看以下的“String Table”], 以NULL空字符結束。

* sh_type

該成員把sections按內容和意義分類。section的類型和他們的描述在下面。

* sh_flags

sections支持位的標記,用來描述多個屬性。
Flag定義出現在下面。

* sh_addr

假如該section將出現在進程的內存映象空間裏,該成員給出了一個該section
在內存中的位置。否則,該變量爲0。

* sh_offset

該成員變量給出了該section的字節偏移量(從文件開始計數)。SHT_NOBITS
類型的section(下面討論)在文件中不佔空間,它的sh_offset成員定位在
文件中的概念上的位置。

* sh_size
該成員給你了section的字節大小。除非這個section的類型爲SHT_NOBITS,
否則該section將在文件中將佔有sh_size個字節。SHT_NOBITS類型的section
可能爲非0的大小,但是不佔文件空間。

* sh_link

該成員保存了一個section報頭表的索引連接,它的解釋依靠該section的
類型。以下一個表描述了這些值。

* sh_info

該成員保存着額外的信息,它的解釋依靠該section的類型。以下一個表描
述了這些值。

* sh_addralign

一些sections有地址對齊的約束。例如,假如一個section保存着雙字,系統
就必須確定整個section是否雙字對齊。所以sh_addr的值以sh_addralign的值
作爲模,那麼一定爲0。當然的,僅僅0和正的2的次方是允許的。值0和1意味
着該section沒有對齊要求。

* sh_entsize

一些sections保存着一張固定大小入口的表,就象符號表。對於這樣一個
section來說,該成員給出了每個入口的字節大小。如果該section沒有
保存着一張固定大小入口的表,該成員就爲0。

section頭成員sh_type指出了section的語意。

+ 圖1-10: Section Types, sh_type

Name Value
==== =====
SHT_NULL 0
SHT_PROGBITS 1
SHT_SYMTAB 2
SHT_STRTAB 3
SHT_RELA 4
SHT_HASH 5
SHT_DYNAMIC 6
SHT_NOTE 7
SHT_NOBITS 8
SHT_REL 9
SHT_SHLIB 10
SHT_DYNSYM 11
SHT_LOPROC 0x70000000
SHT_HIPROC 0x7fffffff
SHT_LOUSER 0x80000000
SHT_HIUSER 0xffffffff

* SHT_NULL

該值表明該section頭是無效的;它沒有相關的section。
該section的其他成員的值都是未定義的。

* SHT_PROGBITS

該section保存被程序定義了的一些信息,它的格式和意義取決於程序本身。

* SHT_SYMTAB and SHT_DYNSYM

那些sections保存着一個符號表(symbol table)。一般情況下,一個
object文件每個類型section僅有一個,但是,在將來,這個約束可能被放寬。
典型的是,SHT_SYMTAB爲連接器提供標號,當然它也有可能被動態連接時使用。
作爲一個完整的符號表,它可能包含了一些動態連接時不需要的標號。
因此,一個object文件可能也包含了一個SHT_DYNSYM的section,它保存着
一個動態連接時所需最小的標號集合來節省空間。
看下面符號表“Symbol Table”的細節。

* SHT_STRTAB

該section保存着一個字符串表。一個object文件可以包含多個字符串表的
section。看下面字符串表“String Table”的細節。

* SHT_RELA

該section保存着具有明確加數的重定位入口。就象object文件32位的
Elf32_Rela類型。一個object文件可能有多個重定位的sections。具體細節
看重定位``Relocation‘‘部分。

* SHT_HASH

該標號保存着一個標號的哈希(hash)表。所有的參與動態連接的object
一定包含了一個標號哈希表(hash table)。當前的,一個object文件
可能只有一個哈希表。詳細細節看第二部分的哈希表"Hash Table"。

* SHT_DYNAMIC

該section保存着動態連接的信息。當前的,一個object可能只有一個動態
的section,但是,將來這個限制可能被取消。詳細細節看第二部分的動態
section(“Dynamic Section”)。

* SHT_NOTE

該section保存着其他的一些標誌文件的信息。詳細細節看第二部分的“Note
Section” 。

* SHT_NOBITS

該類型的section在文件中不佔空間,但是類似SHT_PROGBITS。儘管該section
不包含字節,sh_offset成員包含了概念上的文件偏移量。

* SHT_REL

該section保存着具有明確加數的重定位的入口。
就象object文件32位類型Elf32_Rel類型。一個object文件可能有多個
重定位的sections。具體細節看重定位``Relocation‘‘部分。

* SHT_SHLIB

該section類型保留但語意沒有指明。包含這個類型的section的程序
是不符合ABI的。

* SHT_LOPROC through SHT_HIPROC

在這範圍之間的值爲特定處理器語意保留的。

* SHT_LOUSER

該變量爲應用程序保留的索引範圍的最小邊界。

* SHT_HIUSER

該變量爲應用程序保留的索引範圍的最大邊界。在SHT_LOUSER和HIUSER的
section類型可能被應用程序使用,這和當前或者將來系統定義的section
類型是不矛盾的。

其他section類型的變量是保留的。前面提到過,索引0(SHN_UNDEF)的section
頭存在的,甚至索引標記的是未定義的section引用。這個入口保存着以下的
信息。

+ 圖1-11: Section Header Table Entry: Index 0

Name Value Note
==== ===== ====
sh_name 0 No name
sh_type SHT_NULL Inactive
sh_flags 0 No flags
sh_addr 0 No address
sh_offset 0 No file offset
sh_size 0 No size
sh_link SHN_UNDEF No link information
sh_info 0 No auxiliary information
sh_addralign 0 No alignment
sh_entsize 0 No entries

一個section報頭(section header table)的sh_flags成員保存着1位標記,
用來描述section的屬性。以下是定義的值;其他的值保留。

+ 圖1-12: Section Attribute Flags, sh_flags

Name Value
==== =====
SHF_WRITE 0x1
SHF_ALLOC 0x2
SHF_EXECINSTR 0x4
SHF_MASKPROC 0xf0000000

假如在sh_flags中的一個標記位被設置,該section相應的屬性也被打開。
否則,該屬性沒有被應用。未明的屬性就設爲0。

* SHF_WRITE

該section包含了在進程執行過程中可被寫的數據。

* SHF_ALLOC

該section在進程執行過程中佔據着內存。一些控制section不存在一個
object文件的內存映象中;對於這些sections,這個屬性應該關掉。

* SHF_EXECINSTR

該section包含了可執行的機器指令。

* SHF_MASKPROC

所有的包括在這掩碼中的位爲特定處理語意保留的。

在section報頭中,兩個成員sh_link和sh_info的解釋依靠該section的類型。

+ 圖1-13: sh_link and sh_info Interpretation

sh_type sh_link sh_info
======= ======= =======
SHT_DYNAMIC The section header index of 0
the string table used by
entries in the section.
SHT_HASH The section header index of 0
the symbol table to which the
hash table applies.
SHT_REL, The section header index of The section header index of
SHT_RELA the associated symbol table. the section to which the
relocation applies.
SHT_SYMTAB, The section header index of One greater than the symbol
SHT_DYNSYM the associated string table. table index of the last local
symbol (binding STB_LOCAL).
other SHN_UNDEF 0


Special Sections 特殊的Sections

不同的sections保存着程序和控制信息。下面列表中的section被系統使用,
指示了類型和屬性。

+ 圖1-14: Special Sections

Name Type Attributes
==== ==== ==========
.bss SHT_NOBITS SHF_ALLOC+SHF_WRITE
.comment SHT_PROGBITS none
.data SHT_PROGBITS SHF_ALLOC+SHF_WRITE
.data1 SHT_PROGBITS SHF_ALLOC+SHF_WRITE
.debug SHT_PROGBITS none
.dynamic SHT_DYNAMIC see below
.dynstr SHT_STRTAB SHF_ALLOC
.dynsym SHT_DYNSYM SHF_ALLOC
.fini SHT_PROGBITS SHF_ALLOC+SHF_EXECINSTR
.got SHT_PROGBITS see below
.hash SHT_HASH SHF_ALLOC
.init SHT_PROGBITS SHF_ALLOC+SHF_EXECINSTR
.interp SHT_PROGBITS see below
.line SHT_PROGBITS none
.note SHT_NOTE none
.plt SHT_PROGBITS see below
.rel SHT_REL see below
.rela SHT_RELA see below
.rodata SHT_PROGBITS SHF_ALLOC
.rodata1 SHT_PROGBITS SHF_ALLOC
.shstrtab SHT_STRTAB none
.strtab SHT_STRTAB see below
.symtab SHT_SYMTAB see below
.text SHT_PROGBITS SHF_ALLOC+SHF_EXECINSTR

* .bss

該sectiopn保存着未初始化的數據,這些數據存在於程序內存映象中。
通過定義,當程序開始運行,系統初始化那些數據爲0。該section不佔
文件空間,正如它的section類型SHT_NOBITS指示的一樣。

* .comment

該section保存着版本控制信息。

* .data and .data1
這些sections保存着初始化了的數據,那些數據存在於程序內存映象中。

* .debug

該section保存着爲標號調試的信息。該內容是未指明的。

* .dynamic

該section保存着動態連接的信息。該section的屬性將包括SHF_ALLOC位。
是否需要SHF_WRITE是跟處理器有關。第二部分有更詳細的信息。

* .dynstr

該section保存着動態連接時需要的字符串,一般情況下,名字字符串關聯着
符號表的入口。第二部分有更詳細的信息。

* .dynsym

該section保存着動態符號表,如“Symbol Table”的描述。第二部分有更
詳細的信息。

* .fini

該section保存着可執行指令,它構成了進程的終止代碼。
因此,當一個程序正常退出時,系統安排執行這個section的中的代碼。

* .got

該section保存着全局的偏移量表。看第一部分的“Special Sections”和
第二部分的“Global Offset Table”獲得更多的信息。

* .hash

該section保存着一個標號的哈希表。看第二部分的“Hash Table”獲得更多
的信息。

* .init

該section保存着可執行指令,它構成了進程的初始化代碼。
因此,當一個程序開始運行時,在main函數被調用之前(c語言稱爲main),
系統安排執行這個section的中的代碼。

* .interp

該section保存了程序的解釋程序(interpreter)的路徑。假如在這個section
中有一個可裝載的段,那麼該section的屬性的SHF_ALLOC位將被設置;否則,
該位不會被設置。看第二部分獲得更多的信息。

* .line

該section包含編輯字符的行數信息,它描述源程序與機器代碼之間的對於
關係。該section內容不明確的。

* .note

該section保存一些信息,使用“Note Section”(在第二部分)中提到的格式。

* .plt

該section保存着過程連接表(Procedure Linkage Table)。看第一部分的
``Special Sections‘‘和第二部分的“Procedure Linkage Table”。

* .rel and .rela

這些section保存着重定位的信息,看下面的``Relocation‘‘描述。
假如文件包含了一個可裝載的段,並且這個段是重定位的,那麼該section的
屬性將設置SHF_ALLOC位;否則該位被關閉。按照慣例,由重定位適用
的section來提供。因此,一個重定位的section適用的是.text,那麼該名字
就爲.rel.text或者是.rela.text。

* .rodata and .rodata1

這些section保存着只讀數據,在進程映象中構造不可寫的段。看第二部分的
``Program Header‘‘獲得更多的資料。

* .shstrtab

該section保存着section名稱。

* .strtab

該section保存着字符串,一般地,描述名字的字符串和一個標號的入口相關
聯。假如文件有一個可裝載的段,並且該段包括了符號字符串表,那麼section
的SHF_ALLOC屬性將被設置;否則不設置。

* .symtab

該section保存着一個符號表,正如在這個section裏``Symbol Table‘‘的
描述。假如文件有一個可裝載的段,並且該段包含了符號表,那麼section
的SHF_ALLOC屬性將被設置;否則不設置。

* .text

該section保存着程序的``text‘‘或者說是可執行指令。


前綴是點(.)的section名是系統保留的,儘管應用程序可以用那些保留的
section名。應用程序可以使用不帶前綴的名字以避免和系統的sections
衝突。object文件格式可以讓一個定義的section部分不出現在上面的列
表中。一個object文件可以有多個同樣名字的section。

爲處理器體系保留的section名的形成是通過置換成一個體系名的縮寫。
該名字應該取自體系名,e_machine使用的就是。例如,.Foo.psect就是
在FOO體系上定義的名字。

現存的擴展名是歷史遺留下來的。

Pre-existing Extensions
=======================
.sdata .tdesc
.sbss .lit4
.lit8 .reginfo
.gptab .liblist
.conflict


by xueyan(2005年06月11日,22時56

 

xueyan 05-06-24 03:03

elf文件格式-- 2
elf文件格式-- 2
=================== String Table 字符串表=========================


String table sections 保存着以NULL終止的一系列字符,一般我們稱爲字
符串。object文件使用這些字符串來描繪符號和section名。一個字符串的
參考是一個string table section的索引。第一個字節,即索引0,被定義保
存着一個NULL字符。同樣的,一個string table的最後一個字節保存着一個
NULL字符,所有的字符串都是以NULL終止。索引0的字符串是沒有名字或者說
是NULL,它的解釋依靠上下文。一個空的string table section是允許的;
它的section header的成員sh_size將爲0。對空的string table來說,非0的
索引是沒有用的。

一個 settion 頭的 sh_name 成員保存了一個對應於該 setion 頭字符表部分
的索引(就象ELF頭的 e_shstrndx 成員所特指的那樣。下表列出了一個有 25 字節
的字符串表(這些字符串和不同的索引相關聯):


Index +0 +1 +2 +3 +4 +5 +6 +7 +8 +9
===== == == == == == == == == == ==
0 n a m e . V a r
10 i a b l e a b l e
20 x x


+ Figure 1-15: String Table Indexes

Index String
===== ======
0 none
1 "name."
7 "Variable"
11 "able"
16 "able"
24 null string


如上所示,一個字符串表可能涉及該 section 中的任意字節。一個字符串可能
引用不止一次;引用子串的情況是可能存在的;一個字符串也可能被引用若干次;而
不被引用的字符串也是允許存在的。

==================== Symbol Table 符號表=========================


一個object文件的符號表保存了一個程序在定位和重定位時需要的定義和引用的信息。
一個符號表索引是相應的下標。0表項特指了該表的第一個入口,就象未定義的符號
索引一樣。初始入口的內容在該 section 的後續部分被指定。

Name Value
==== =====
STN_UNDEF 0

一個符號表入口有如下的格式:

+ Figure 1-16: Symbol Table Entry

typedef struct {
Elf32_Word st_name;
Elf32_Addr st_value;
Elf32_Word st_size;
unsigned char st_info;
unsigned char st_other;
Elf32_Half st_shndx;
} Elf32_Sym;

* st_name

該成員保存了進入該object文件的符號字符串表入口的索引(保留了符號名的表達字符)。
如果該值不爲 0 ,則它代表了給出符號名的字符串表索引。否則,該符號無名。

注意:External C 符號和object文件的符號表有相同的名稱。

* st_value

該成員給出了相應的符號值。它可能是絕對值或地址等等(依賴於上下文);
細節如下所述。

* st_size

許多符號和大小相關。比如,一個數據對象的大小是該對象所包含的字節數目。
如果該符號的大小未知或沒有大小則這個成員爲 0 。

* st_info

成員指出了符號的類型和相應的屬性。相應的列表如下所示。下面的代碼說明了
如何操作該值。

#define ELF32_ST_BIND(i) ((i)>>4)
#define ELF32_ST_TYPE(i) ((i)&0xf)
#define ELF32_ST_INFO(b, t) (((b)<<4)+((t)&0xf))

* st_other

該成員目前爲 0 ,沒有含義。

* st_shndx

每一個符號表的入口都定義爲和某些 section 相關;該成員保存了相關的 section
頭索引。就象 Figure 1-8 和相關的文字所描述的那樣,某些 section 索引
指出了特殊的含義。

一個符號的屬性決定了可鏈接性能和行爲。

+ Figure 1-17: Symbol Binding, ELF32_ST_BIND

Name Value
==== =====
STB_LOCAL 0
STB_GLOBAL 1
STB_WEAK 2
STB_LOPROC 13
STB_HIPROC 15

* STB_LOCAL

在包含了其定義的object文件之外的局部符號是不可見的。不同文件中的具有相同
名稱的局部符號並不相互妨礙。

* STB_GLOBAL

全局符號是對所有的object目標文件可見的。一個文件中的全局符號的定義可以
滿足另一個文件中對(該文件中)未定義的全局符號的引用。

* STB_WEAK

弱符號相似於全局符號,但是他們定義的優先級比較低一些。

* STB_LOPROC through STB_HIPROC

其所包含範圍中的值由相應的處理器語義所保留。

全局符號和弱符號的區別主要在兩個方面。

* 當鏈接器鏈接幾個可重定位的目標文件時,它不允許 STB_GLOBAL 符號的同名
多重定義。另一方面,如果一個全局符號的定義存在則具有相同名稱的弱符號名不會
引起錯誤。鏈接器將認可全局符號的定義而忽略弱符號的定義。與此相似,如果有一個
普通符號(比如,一個符號的 st_shndx 域包含 SHN_COMMON),則一個同名的弱符號
不會引起錯誤。鏈接器同樣認可普通符號的定義而忽略弱符號。

* 當鏈接器搜索檔案庫的時候,它選出包含了未定義的全局符號的存檔成員。該成員
的定義或者是全局的或者是一個弱符號。鏈接器不會爲了解決一個未定義的弱符號
選出存檔成員。未定義的弱符號具有 0 值。

在每一個符號表中,所有具有 STB_LOCAL 約束的符號優先於弱符號和全局符號。
就象上面 "sections" 中描述的那樣,一個符號表部分的 sh_info 頭中的成員
保留了第一個非局部符號的符號表索引。

符號的類型提供了一個爲相關入口的普遍分類。

+ Figure 1-18: Symbol Types, ELF32_ST_TYPE

Name Value
==== =====
STT_NOTYPE 0
STT_OBJECT 1
STT_FUNC 2
STT_SECTION 3
STT_FILE 4
STT_LOPROC 13
STT_HIPROC 15

* STT_NOTYPE

該符號的類型沒有指定。

* STT_OBJECT

該符號和一個數據對象相關,比如一個變量、一個數組等。

* STT_FUNC

該符號和一個函數或其他可執行代碼相關。

* STT_SECTION

該符號和一個 section 相關。這種類型的符號表入口主要是爲了重定位,一般的
具有 STB_LOCAL 約束。


* STT_FILE

按慣例而言,該符號給出了和目標文件相關的源文件名稱。一個具有 STB_LOCAL
約束的文件符號,其 section 索引爲 SHN_ABS ,並且它優先於當前對應該文件的
其他 STB_LOCAL 符號。

* STT_LOPROC through STT_HIPROC

該範圍中的值是爲處理器語義保留的。

共享文件中的函數符號(具有 STT_FUNC 類型)有特殊的意義。當其他的目標文件
從一個共享文件中引用一個函數時,鏈接器自動的爲引用符號創建一個鏈接表。除了
STT_FUNC 之外,共享的目標符號將不會自動的通過鏈接表引用。


如果一個符號涉及到一個 section 的特定定位,則其 section 索引成員 st_shndx
將保留一個到該 section 頭的索引。當該 section 在重定位過程中不斷
移動一樣,符號的值也相應變化,而該符號的引用在程序中指向同樣的定位。某些
特殊的 section 索引有其他的語義。


* SHN_ABS

該符號有一個不會隨重定位變化的絕對值。

* SHN_COMMON

該符號標識了一個沒有被分配的普通塊。該符號的值給出了相應的系統參數,就象
一個 section 的 sh_addralign 成員。也就是說,鏈接器將分配一個地址給
該符號,地址的值是 st_value 的倍數。該符號的大小指出了需要的字節數。


* SHN_UNDEF

該 section 表索引表明該符號是未定義的。當鏈接器將該目標文件和另一個定義
該符號的文件相裝配的時候,該文件內對該符號的引用將鏈接到當前實際的定義。

如上所述,符號表的 0 索引(STN_UNDEF)是保留的,它包含了如下內容:

+ Figure 1-19: Symbol Table Entry: Index 0

Name Value Note
==== ===== ====
st_name 0 No name
st_value 0 Zero value
st_size 0 No size
st_info 0 No type, local binding
st_other 0
st_shndx SHN_UNDEF No section


Symbol Values(符號值)

符號表入口對於不同的目標文件而言對 st_value 成員有一些不同的解釋。

* 在可重定位文件中, st_value 保存了 section 索引爲 SHN_COMMON 符號
的強制對齊值。

* 在可重定位文件中, st_value 保存了一個符號的 section 偏移。也就是說,
st_value 是從 st_shndx 定義的 section 開頭的偏移量。

* 在可執行的和可共享的目標文件中, st_value 保存了一個虛擬地址。爲了使
這些文件符號對於動態鏈接器更爲有效,文件層面上的 section 偏移讓位於內存
層面上的虛擬地址( section 編號無關的)。


儘管符號表值對於不同的目標文件有相似的含義,相應的程序還是可以有效地訪問數據。


====================== Relocation (重定位)==========================


重定位是連接符號引用和符號定義的過程。比如,當一個程序調用一個函數的時候,
相關的調用必須在執行時把控制傳送到正確的目標地址。換句話說,重定位文件應當
包含有如何修改他們的 section 內容的信息,從而允許可執行文件或共享目標文件
爲一個進程的程序映像保存正確的信息。重定位入口就是這樣的數據。

+ Figure 1-20: Relocation Entries

typedef struct {
Elf32_Addr r_offset;
Elf32_Word r_info;
} Elf32_Rel;

typedef struct {
Elf32_Addr r_offset;
Elf32_Word r_info;
Elf32_Sword r_addend;
} Elf32_Rela;

* r_offset

該成員給出了應用重定位行爲的地址。對於一個重定位文件而言,該值是從該
section 開始處到受到重定位影響的存儲單位的字節偏移量。對一個可執行文件
或一個共享目標而言,該值是受到重定位影響的存儲單位的虛擬地址。


* r_info

該成員給出了具有受重定位影響因素的符號表索引和重定位應用的類型。比如,
一個調用指令的重定位入口應當包含被調用函數的符號索引。如果該索引是
STN_UNDEF (未定義的符號索引),重定位將使用 0 作爲該符號的值。重定位
類型是和處理器相關的。當正文(text)引用到一個重定位入口的重定位類型或符
號表索引,它表明相應的應用 ELF32_R_TYPE或 ELF32_R_SYM 於入口的 r_info
成員。

#define ELF32_R_SYM(i) ((i)>>8)
#define ELF32_R_TYPE(i) ((unsigned char)(i))
#define ELF32_R_INFO(s, t) ((s)<<8+(unsigned char)(t))

* r_addend

該成員指定一個常量加數(用於計算將要存儲於重定位域中的值)。

如上所述,只有 Elf32_Rela 入口包含一個明確的加數。Elf32_Rel 類型
的入口在可以修改的地址中存儲一個隱含的加數。依賴於處理器結構,一種形式
或其他形式也許是必須的或更爲方便的。因此,特定機器的應用應當使用一種排他
性的形式或依賴於上下文的另一種形式。

一個重定位 section 關聯了兩個其他的 section :一個符號表和一個可修改
的 section 。該 section 頭的成員 sh_info 和 sh_link (在上文中的
“ section ”部分中有描述)指示了這種關係。重定位入口中的成員 r_offset
對於不同的目標文件有少許差異。

* 在可重定位文件中,r_offset 表示了一個 section 偏移。也就是說,重定位
section自己描述瞭如何修改其他在文件中的其他section; 重定位偏移量指
明瞭一個在第二個section中的存儲器單元。

* 在可執行和共享的目標文件中,r_offset 表示一個虛擬地址。爲了使得這些
文件的重定位入口更爲有用(對於動態鏈接器而言),該 section 偏移(文件
中)應當讓位於一個虛擬地址(內存中的)。

儘管爲了允許相關的程序更爲有效的訪問而讓 r_offset 的解釋對於不同的目標
文件有所不同,重定位類型的含義是相同的。


Relocation Types(重定位類型)

重定位入口描述了怎樣變更下面的指令和數據域(位數在表的兩邊角下)。

+ Figure 1-21: Relocatable Fields

+---------------------------+
| word32 |
31---------------------------0


* word32

指定一個以任意字節對齊方式佔用 4 字節的 32 位域。這些值使用與 32 位 Intel
體系相同的字節順序。

3------2------1------0------+
0x01020304 | 01 | 02 | 03 | 04 |
31------+------+------+------0

下面的計算假設正在將一個可重定位文件轉換爲一個可執行或共享的目標文件。
從概念上來說,鏈接器合併一個或多個可重定位文件來組成輸出。它首先決定
怎樣合併、定位輸入文件,然後更新符號值,最後進行重定位。對於可執行文件
和共享的目標文件而言,重定位過程是相似的並有相同的結果。下面的描述使用
如下的約定符號。

* A

表示用於計算可重定位的域值的加數。

* B

表示了在執行過程中一個共享目標被加載到內存時的基地址。一般情況下,一個
共享object文件使用的基虛地址爲0,但是一個可執行地址就跟共享object文件
不同了。

* G

表示了在執行過程中重定位入口符號駐留在全局偏移表中的偏移。請參閱
第二部分中的“ Global Offset Table (全局偏移表)”獲得更多
的信息。

* GOT

表示了全局偏移表的地址。請參閱第二部分中的“ Global Offset Table
(全局偏移表)”獲得更多的信息。

* L

表示一個符號的過程鏈接表入口的位置( section 偏移或地址)。一個過程
鏈接表入口重定位一個函數調用到正確的目的單元。鏈接器創建初始的鏈接表,
而動態鏈接器在執行中修改入口。
請參閱第二部分中的“ Procedure Linkage Table (過程鏈接表)”獲得更多
的信息

* P

表示(section 偏移或地址)被重定位的存儲單元位置(使用 r_offset 計算的)。

* S

表示索引駐留在重定位入口處的符號值。

一個重定位入口的 r_offset 值指定了受影響的存儲單元的首字節的偏移
或虛擬地址。重定位類型指定了哪一位(bit)將要改變,以及怎樣計算它們的值。
在 SYSTEM V 體系中僅僅使用 Elf32_Rel 重定位入口,將要被重定位的域中
保留了加數。在所有的情況下,加數和計算結果使用相同字節順序。

+ Figure 1-22(表 1-22): Relocation Types(重定位類型)

Name Value Field Calculation
==== ===== ===== ===========
R_386_NONE 0 none none
R_386_32 1 word32 S + A
R_386_PC32 2 word32 S + A - P
R_386_GOT32 3 word32 G + A - P
R_386_PLT32 4 word32 L + A - P
R_386_COPY 5 none none
R_386_GLOB_DAT 6 word32 S
R_386_JMP_SLOT 7 word32 S
R_386_RELATIVE 8 word32 B + A
R_386_GOTOFF 9 word32 S + A - GOT
R_386_GOTPC 10 word32 GOT + A - P

有的重定位類型有不同於簡單計算的語義。

* R_386_GOT32

這種重定位類型計算全局偏移表基地址到符號的全局偏移表
入口之間的間隔。這樣另外通知了 link editor 建立一個全局偏移表 。


* R_386_PLT32

這種重定位類型計算符號的過程鏈接表入口地址,並另外通知鏈接器建立一個
過程鏈接表。


* R_386_COPY

鏈接器創建該重定位類型用於動態鏈接。它的偏移成員涉及一個可寫段中的一個
位置。符號表索引指定一個可能存在於當前 object file 或在一個shared object
中的符號。在執行過程中,動態鏈接器把和 shared object 符號相關的數據
拷貝到該偏移所指定的位置。

* R_386_GLOB_DAT

這種重定位類型用於設置一個全局偏移表入口爲指定符號的地址。該特定的重定位
類型允許你決定符號和全局偏移表入口之間的一致性。


* R_386_JMP_SLOT

鏈接器創建該重定位類型用於動態鏈接。其偏移成員給出了一個過程鏈接表入口的
位置。動態鏈接器修改該過程鏈接表入口以便向特定的符號地址傳遞控制。
[參閱第二部分中的 "Procedure Linkage Table(過程鏈接表)"]

* R_386_RELATIVE

鏈接器創建該重定位類型用於動態鏈接。其偏移成員給出了包含表達相關地址值
的一個 shared object 中的位置。動態鏈接器計算相應的虛擬地址(把該
shared object 裝載地址和相對地址相加)。該類型的重定位入口必須爲
符號表索引指定爲 0 。


* R_386_GOTOFF

這種重定位類型計算符號值和全局偏移表地址之間的不同。另外還通知鏈接器
建立全局偏移表(GOT)。



* R_386_GOTPC

這種重定位類型類似於 R_386_PC32 ,不同的是它在計算中使用全局偏移表。
這種重定位中引用的符號通常是 _GLOBAL_OFFSET_TABLE_ ,該符號通知了
鏈接器建立全局偏移表(GOT)。

________________________________________________________________


2. PROGRAM LOADING AND DYNAMIC LINKING
程序裝入和動態鏈接
________________________________________________________________


======================== Introduction(介紹) =========================


第二部分描述了 object file 信息和創建運行程序的系統行爲。其中部分信息
適合所有的系統,其他信息是和特定處理器相關的。


可執行和共享的 object file 靜態的描繪了程序。爲了執行這樣的程序,系統
用這些文件創建動態的程序表現,或進程映像。一個進程映像有用於保存其代碼、
數據、堆棧等等的段。這個部分的主要章節討論如下的內容。


* 程序頭(Program header)。該章節補充第一部分,描述和程序運行相關的
object file 結構。即文件中主要的數據結構、程序頭表、定位段映像,也
包含了爲該程序創建內存映像所需要的信息。

* 載入程序(Program loading)。在給定一個 object file 時,系統爲了
讓它運行必須將它載入內存。

* 動態鏈接(Dynamic linking)。在載入了程序之後,系統必須通過解決組
成該進程的 object file之間的符號引用問題來完成進程映像的過程。

注意:指定了處理器範圍的 ELF 常量是有命名約定的。比如,DT_ , PT_ ,
用於特定處理器擴展名,組合了處理器的名稱(如 DT_M32_SPECIAL )。
沒有使用這種約定但是預先存在的處理器擴展名是允許的。


Pre-existing Extensions
(預先存在的擴展名)
=======================
DT_JMP_REL


====================== Program Header(程序頭) ======================


一個可執行的或共享的 object file 的程序頭表是一個結構數組,每一個
結構描述一個段或其他系統準備執行該程序所需要的信息。一個 object file
段包含一個或多個部分(就象下面的“段目錄”所描述的那樣)。程序頭僅僅對於
可執行或共享的 object file 有意義。一個文件使用 ELF 頭的 e_phentsize
和 e_phnum 成員來指定其擁有的程序頭大小。[參閱 第一部分中的 "ELF 頭"]


+ Figure 2-1: Program Header

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;

* p_type

該成員指出了這個數組的元素描述了什麼類型的段,或怎樣解釋該數組元素的信息。
類型值和含義如下所述。

* p_offset

該成員給出了該段的駐留位置相對於文件開始處的偏移。

* p_vaddr

該成員給出了該段在內存中的首字節地址。

* p_paddr

在物理地址定位有關聯的系統中,該成員是爲該段的物理地址而保留的。由於
System V 忽略了應用程序的物理地址定位,該成員對於可執行文件和共享的
object 而言是未指定內容的。


* p_filesz

該成員給出了文件映像中該段的字節數;它可能是 0 。

* p_memsz

該成員給出了內存映像中該段的字節數;它可能是 0 。

* p_flags

該成員給出了和該段相關的標誌。定義的標誌值如下所述。

* p_align

就象在後面“載入程序”部分中所說的那樣,可載入的進程段必須有合適的
p_vaddr 、 p_offset 值,取頁面大小的模。該成員給出了該段在內存和
文件中排列值。 0 和 1 表示不需要排列。否則, p_align 必須爲正的 2 的冪,
並且 p_vaddr 應當等於 p_offset 模 p_align 。


某些入口描述了進程段;其他的則提供補充信息並且無益於進程映像。已經
定義的入口可以以任何順序出現,除非是下面明確聲明的。後面是段類型值;
其他的值保留以便將來用於其他用途。


+ Figure 2-2: Segment Types, p_type

Name Value
==== =====
PT_NULL 0
PT_LOAD 1
PT_DYNAMIC 2
PT_INTERP 3
PT_NOTE 4
PT_SHLIB 5
PT_PHDR 6
PT_LOPROC 0x70000000
PT_HIPROC 0x7fffffff

* PT_NULL

該數組元素未使用;其他的成員值是未定義的。這種類型讓程序頭表忽略入口。

* PT_LOAD

該數組元素指定一個可載入的段,由 p_filesz 和 p_memsz 描述。文件中
字節被映射到內存段中。如果該段的內存大小( p_memsz )比文件大小( p_filesz )
要大,則多出的字節將象段初始化區域那樣保持爲 0 。文件的大小不會比內存大小值大。
在程序頭表中,可載入段入口是以 p_vaddr 的升序排列的。

* PT_DYNAMIC

該數組元素指定動態鏈接信息。參閱 後面的“動態部分”以獲得更多信息。

* PT_INTERP

該數組元素指定一個 null-terminated 路徑名的位置和大小(作爲解釋程序)。
這種段類型僅僅對可執行文件有意義(儘管它可能發生在一個共享 object 上);
它在一個文件中只能出現一次。如果它出現,它必須先於任何一個可載入段入口。
參閱 後面的“程序解釋器”(Program Interpreter)以獲得更多的信息。


* PT_NOTE

該數組元素指定輔助信息的位置和大小。參閱 後面的“注意部分”以獲得細節。

* PT_SHLIB

該段類型保留且具有未指定的語義。具有一個這種類型數組元素的程序並不
遵守 ABI 。

* PT_PHDR

該數組元素(如果出現),指定了程序頭表本身的位置和大小(包括在文件中
和在該程序的內存映像中)。更進一步來說,它僅僅在該程序頭表是程序內存映像
的一部分時纔有效。如果它出現,它必須先於任何可載入段入口。參閱 後面的
“程序解釋器”(Program Interpreter)以獲得更多的信息。


* PT_LOPROC through PT_HIPROC

該範圍中的值保留用於特定處理器的語義。

注意:除非在別處的特殊要求,所有的程序頭的段類型是可選的。也就是說,
一個文件的程序頭表也許僅僅包含和其內容相關的元素。



Base Address(基地址)

可執行和共享的 object file 有一個基地址,該基地址是與程序的 object file
在內存中映像相關的最低虛擬地址。基地址的用途之一是在動態鏈接過程中重定位
該程序的內存映像。


一個可執行的 object file 或 一個共享的 object file 的基地址是在
執行的時候從三個值計算而來的:內存載入地址、頁面大小的最大值 和 程序可
載入段的最低虛擬地址。就象在“程序載入”中所描述的那樣,程序頭中的虛擬地址
也許和程序的內存映像中實際的虛擬地址並不相同。爲了計算基地址,必須確定與
PT_LOAD 段 p_vaddr 的最小值相關的內存地址。獲得基地址的方法是將內存
地址截去最大頁面大小的最接近的整數倍。由於依賴載入內存中的文件類型,
該內存地址和 p_vaddr 值可能匹配也可能不匹配。


就象在第一部分中 "Section" 中描述的那樣, .bss section 具有 SHT_NOBITS
的類型。儘管在文件中不佔用空間,它在段的內存映像中起作用。通常,沒有初始化
的數據駐留在段尾,因此使得在相關的程序頭元素中的 p_memsz 比 p_filesz 大。


Note Section(註解部分)

有的時候供應商或系統設計者需要用特定的信息標記一個
object file 以便其他程序檢查其兼容的一致性,等等此類。 SHT_NOTE
類型的 section 和 PT_NOTE 類型的程序頭元素能夠被用於此目的。 section
和程序頭中的註解信息包含了任意數目的入口,每一個入口的格式都是對應於特定
處理器格式的 4-字節數組。下面的標籤有助於解釋註釋信息的組織形式,但是這些
標籤不是規格說明的一部分。


+ Figure 2-3: Note Information

namesz
descsz
type
name ...
desc ...

* namesz and name

名字中 namesz 的第一個字節包含了一個 null-terminated 字符
表達了該入口的擁有者或始發者。沒有正式的機制來避免名字衝突。從
慣例來說,供應商使用他們自己的名稱,比如 "XYZ Computer Company" ,
作爲標誌。如果沒有提供名字, namesz 值爲 0 。 如果有必要,確定
描述信息4-字節對齊。 這樣的填充信息並不包含在namesz 中。


* descsz and desc

desc 中 descsz 的首字節包含了註解描述符。ABI 不會在一個描述符內容中
放入任何系統參數。如果沒有描述符, descsz 將爲 0 。 如果有必要,確定
描述信息4-字節對齊。 這樣的填充信息並不包含在descsz中。


* type

該 word 給出了描述符的解釋。每一個創造着(originator) 控制着自己的類型;
對於單單一個類型值的多種解釋是可能存在的。因此,一個程序必須辨認出該名字
和其類型以便理解一個描述符。這個時候的類型必須是非負的。ABI 沒有定義
描述符的含義。

爲了舉例說明,下面的解釋段包含兩個入口。

+ Figure 2-4: Example Note Segment

+0 +1 +2 +3
-------------------
namesz 7
descsz 0 No descriptor
type 1
name X Y Z spc
C o pad
namesz 7
descsz 8
type 3
name X Y Z spc
C o pad
desc word0
word1

注意:系統保留的註解信息沒有名字 (namesz==0) ,有一個零長度的名字
(name[0]==‘‘) 現在還沒有類型爲其定義。所有其他的名字必須至少有
一個非空的字符。


注意:註解信息是可選的。註解信息的出現並不影響一個程序的 ABI 一致性,
前提是該信息不影響程序的執行行爲。否則,該程序將不遵循 ABI 並將出現
未定義的行爲。


===================== Program Loading(程序載入) =====================

當創建或增加一個進程映像的時候,系統在理論上將拷貝一個文件的段到一個虛擬
的內存段。系統什麼時候實際地讀文件依賴於程序的執行行爲,系統載入等等。一個
進程僅僅在執行時需要引用邏輯頁面的時候才需要一個物理頁面,實際上進程通常會
留下許多未引用的頁面。因此推遲物理上的讀取常常可以避免這些情況,改良系統的
特性。爲了在實踐中達到這種效果,可執行的和共享的 object file 必須具有
合適於頁面大小取模值的文件偏移和虛擬地址這樣條件的段映像。

虛擬地址和文件偏移在 SYSTEM V 結構的段中是模 4KB(0x1000) 或大的 2 的冪。
由於 4KB 是最大的頁面大小,因此無論物理頁面大小是多少,文件必須去適合頁面。


+ Figure 2-5: Executable File

File Offset File Virtual Address
=========== ==== ===============
0 ELF header
Program header table
Other information
0x100 Text segment 0x8048100
...
0x2be00 bytes 0x8073eff
0x2bf00 Data segment 0x8074f00
...
0x4e00 bytes 0x8079cff
0x30d00 Other information
...

+ Figure 2-6: Program Header Segments(程序頭段)

Member Text Data
====== ==== ====
p_type PT_LOAD PT_LOAD
p_offset 0x100 0x2bf00
p_vaddr 0x8048100 0x8074f00
p_paddr unspecified unspecified
p_filesz 0x2be00 0x4e00
p_memsz 0x2be00 0x5e24
p_flags PF_R+PF_X PF_R+PF_W+PF_X
p_align 0x1000 0x1000

儘管示例中的文件偏移和虛擬地址在文本和數據兩方面都適合模 4KB ,但是還有
4 個文件頁面混合了代碼和數據(依賴於頁面大小和文件系統塊的大小)。


* 第一個文本頁面包含了 ELF 頭、程序頭以及其他信息。
* 最後的文本頁包含了一個數據開始的拷貝。
* 第一個數據頁面有一個文本結束的拷貝。
* 最後的數據頁面也許會包含與正在運行的進程無關的文件信息。

理論上,系統強制內存中段的區別;段地址被調整爲適應每一個邏輯頁面在地址空間
中有一個簡單的准許集合。在上面的示例中,包含文本結束和數據開始的文件區域將
被映射兩次:在一個虛擬地址上爲文本而另一個虛擬地址上爲數據。


數據段的結束處需要對未初始化的數據進行特殊處理(系統定義的以 0 值開始)。
因此如果一個文件包含信息的最後一個數據頁面不在邏輯內存頁面中,則無關的
數據應當被置爲 0 (這裏不是指未知的可執行文件的內容)。在其他三個頁面中
"Impurities" 理論上並不是進程映像的一部分;系統是否擦掉它們是未指定的。
下面程序的內存映像假設了 4KB 的頁面。


+ Figure 2-7: Process Image Segments(進程映像段)

Virtual Address Contents Segment
=============== ======== =======
0x8048000 Header padding Text
0x100 bytes
0x8048100 Text segment
...
0x2be00 bytes
0x8073f00 Data padding
0x100 bytes
0x8074000 Text padding Data
0xf00 bytes
0x8074f00 Data segment
...
0x4e00 bytes
0x8079d00 Uninitialized data
0x1024 zero bytes
0x807ad24 Page padding
0x2dc zero bytes

可執行文件和共享文件在段載入方面有所不同。典型地,可執行文件段包含了
絕對代碼。爲了讓進程正確執行,這些段必須駐留在建立可執行文件的虛擬地址
處。因此係統使用不變的 p_vaddr 作爲虛擬地址。


另一方面,共享文件段包含與位置無關的代碼。這讓不同進程的相應段虛擬地址
各不相同,且不影響執行。雖然系統爲各個進程選擇虛擬地址,它還要維護各個
段的相對位置。因爲位置無關的代碼在段間使用相對定址,故而內存中的虛擬地址
的不同必須符合文件中虛擬地址的不同。下表給出了幾個進程可能的共享對象虛擬
地址的分配,演示了不變的相對定位。該表同時演示了基地址的計算。


+ Figure 2-8: Example Shared Object Segment Addresses

Sourc Text Data Base Address
===== ==== ==== ============
File 0x200 0x2a400 0x0
Process 1 0x80000200 0x8002a400 0x80000000
Process 2 0x80081200 0x800ab400 0x80081000
Process 3 0x900c0200 0x900ea400 0x900c0000
Process 4 0x900c6200 0x900f0400 0x900c6000

 

xueyan 05-06-24 03:04

elf文件格式-- 3
elf文件格式-- 3

==================== Dynamic Linking (動態鏈接) =====================


一個可執行文件可能有一個 PT_INTERP 程序頭元素。在 exec(BA_OS) 的
過程中,系統從 PT_INTERP 段中取回一個路徑名並由解釋器文件的段創建初始的
進程映像。也就是說,系統爲解釋器“編寫”了一個內存映像,而不是使用原始
的可執行文件的段映像。此時該解釋器就負責接收系統來的控制並且爲應用程序
提供一個環境變量。


解釋器使用兩種方法中的一種來接收系統來的控制。首先,它會接收一個文件描述符
來讀取該可執行文件,定位於開頭。它可以使用這個文件描述符來讀取 並且(或者)
映射該可執行文件的段到內存中。其次,依賴於該可執行文件的格式,系統會載入
這個可執行文件到內存中而不是給該解釋器一個文件描述符。伴隨着可能的文件描述符
異常的情況,解釋器的初始進程聲明應匹配該可執行文件應當收到的內容。解釋器本身
並不需要第二個解釋器。一個解釋器可能是一個共享對象也可能是一個可執行文件。


* 一個共享對象(通常的情況)在被載入的時候是位置無關的,各個進程可能不同;
系統在 mmap(KE_OS) 使用的動態段域爲它創建段和相關的服務。因而,一個
共享對象的解釋器將不會和原始的可執行文件的原始段地址相沖突。


* 一個可執行文件被載入到固定地址;系統使用程序頭表中的虛擬地址爲其創建段。
因而,一個可執行文件解釋器的虛擬地址可能和第一個可執行文件相沖突;這種
衝突由解釋器來解決。



Dynamic Linker(動態鏈接器)

當使用動態鏈接方式建立一個可執行文件時,鏈接器把一個 PT_INTERP 類型
的元素加到可執行文件中,告訴系統把動態鏈接器做爲該程序的解釋器。

注意:由系統提供的動態鏈接器是和特定處理器相關的。

Exec(BA_OS) 和動態鏈接器合作爲程序創建進程,必須有如下的動作:

* 將可執行文件的內存段加入進程映像中;
* 將共享對象的內存段加入進程映像中;
* 爲可執行文件和它的共享對象進行重定位;
* 如果有一個用於讀取可執行文件的文件描述符傳遞給了動態鏈接器,那麼關閉它。
* 向程序傳遞控制,就象該程序已經直接從 exec(BA_OS) 接收控制一樣。

鏈接器同時也爲動態鏈接器構建各種可執行文件和共享對象文件的相關數據。就象
在上面“程序頭”中說的那樣,這些數據駐留在可載入段中,使得它們在執行過程
中有效。(再一次的,要記住精確的段內容是處理器相關的。可以參閱相應處理器
的補充說明來獲得詳盡的信息。)


* 一個具有 SHT_DYNAMIC 類型的 .dynamic section 包含各種數據。駐留在
section 開頭的結構包含了其他動態鏈接信息的地址。

* SHT_HASH 類型的 .hash section 包含了一個 symbol hash table.

* SHT_PROGBITS 類型的 .got 和 .plt section 包含了兩個分離的 table:
全局偏移表和過程鏈接表。 下面的 section 演示了動態鏈接器使用和改變
這些表來爲 object file 創建內存映像。


由於每一個遵循 ABI 的程序從一個共享對象庫中輸入基本的系統服務,因此動態
鏈接器分享於每一個遵循 ABI 的程序的執行過程中。


就象在處理器補充說明的“程序載入”所解釋的那樣,共享對象也許會佔用與記錄在
文件的程序頭表中的地址不同的虛擬內存地址。動態鏈接器重定位內存映像,在應用程序
獲得控制之前更新絕對地址。儘管在庫被載入到由程序頭表指定的地址的情況下絕對地址
應當是正確的,通常的情況卻不是這樣。


如果進程環境 [see exec(BA_OS)] 包含了一個非零的 LD_BIND_NOW 變量,
動態鏈接器將在控制傳遞到程序之前進行所有的重定位。舉例而言,所有下面的
環境入口將指定這種行爲。


* LD_BIND_NOW=1
* LD_BIND_NOW=on
* LD_BIND_NOW=off

其他情況下, LD_BIND_NOW 或者不在環境中或者爲空值。動態鏈接器可以不急於
處理過程鏈接表入口,因而避免了對沒有調用的函數的符號解析和重定位。參閱
"Procedure Linkage Table"獲取更多的信息。



Dynamic Section(動態section)


假如一個object文件參與動態的連接,它的程序頭表將有一個類型爲PT_DYNAMIC
的元素。該“段”包含了.dynamic section。一個_DYNAMIC特別的符號,表明了
該section包含了以下結構的一個數組。

+ Figure 2-9: Dynamic Structure

typedef struct {
Elf32_Sword d_tag;
union {
Elf32_Sword d_val;
Elf32_Addr d_ptr;
} d_un;
} Elf32_Dyn;

extern Elf32_Dyn _DYNAMIC[];

對每一個有該類型的object,d_tag控制着d_un的解釋。

* d_val

那些Elf32_Word object描繪了具有不同解釋的整形變量。

* d_ptr

那些Elf32_Word object描繪了程序的虛擬地址。就象以前提到的,在執行時,
文件的虛擬地址可能和內存虛擬地址不匹配。當解釋包含在動態結構中的地址
時是基於原始文件的值和內存的基地址。爲了一致性,文件不包含在
重定位入口來糾正在動態結構中的地址。


以下的表格總結了對可執行和共享object文件需要的tag。假如tag被標爲
mandatory,ABI-conforming文件的動態連接數組必須有一個那樣的入口。
同樣的,“optional”意味着一個可能出現tag的入口,但是不是必須的。

+ Figure 2-10: Dynamic Array Tags, d_tag

Name Value d_un Executable Shared Object
==== ===== ==== ========== =============
DT_NULL 0 ignored mandatory mandatory
DT_NEEDED 1 d_val optional optional
DT_PLTRELSZ 2 d_val optional optional
DT_PLTGOT 3 d_ptr optional optional
DT_HASH 4 d_ptr mandatory mandatory
DT_STRTAB 5 d_ptr mandatory mandatory
DT_SYMTAB 6 d_ptr mandatory mandatory
DT_RELA 7 d_ptr mandatory optional
DT_RELASZ 8 d_val mandatory optional
DT_RELAENT 9 d_val mandatory optional
DT_STRSZ 10 d_val mandatory mandatory
DT_SYMENT 11 d_val mandatory mandatory
DT_INIT 12 d_ptr optional optional
DT_FINI 13 d_ptr optional optional
DT_SONAME 14 d_val ignored optional
DT_RPATH 15 d_val optional ignored
DT_SYMBOLIC 16 ignored ignored optional
DT_REL 17 d_ptr mandatory optional
DT_RELSZ 18 d_val mandatory optional
DT_RELENT 19 d_val mandatory optional
DT_PLTREL 20 d_val optional optional
DT_DEBUG 21 d_ptr optional ignored
DT_TEXTREL 22 ignored optional optional
DT_JMPREL 23 d_ptr optional optional
DT_LOPROC 0x70000000 unspecified unspecified unspecified
DT_HIPROC 0x7fffffff unspecified unspecified unspecified

* DT_NULL

一個DT_NULL標記的入口表示了_DYNAMIC數組的結束。

* DT_NEEDED

這個元素保存着以NULL結尾的字符串表的偏移量,那些字符串是所需庫的名字。
該偏移量是以DT_STRTAB 爲入口的表的索引。看“Shared Object Dependencies”
關於那些名字的更多信息。動態數組可能包含了多個這個類型的入口。那些
入口的相關順序是重要的,雖然它們跟其他入口的關係是不重要的。

* DT_PLTRELSZ

該元素保存着跟PLT關聯的重定位入口的總共字節大小。假如一個入口類型
DT_JMPREL存在,那麼DT_PLTRELSZ也必須存在。

* DT_PLTGOT

該元素保存着跟PLT關聯的地址和(或者)是GOT。具體細節看處理器補充
(processor supplement)部分。

* DT_HASH

該元素保存着符號哈希表的地址,在“哈希表”有描述。該哈希表指向
被DT_SYMTAB元素引用的符號表。

* DT_STRTAB

該元素保存着字符串表地址,在第一部分有描述,包括了符號名,庫名,
和一些其他的在該表中的字符串。

* DT_SYMTAB

該元素保存着符號表的地址,在第一部分有描述,對32-bit類型的文件來
說,關聯着一個Elf32_Sym入口。

* DT_RELA

該元素保存着重定位表的地址,在第一部分有描述。在表中的入口有明確的
加數,就象32-bit類型文件的Elf32_Rela。一個object文件可能好多個重定位
section。當爲一個可執行和共享文件建立重定位表的時候,連接編輯器連接
那些section到一個單一的表。儘管在object文件中那些section是保持獨立的。
動態連接器只看成是一個簡單的表。當動態連接器爲一個可執行文件創建一個
進程映象或者是加一個共享object到進程映象中,它讀重定位表和執行相關的
動作。假如該元素存在,動態結構必須也要有DT_RELASZ和DT_RELAENT元素。
當文件的重定位是mandatory,DT_RELA 或者 DT_REL可能出現(同時出現是
允許的,但是不必要的)。


* DT_RELASZ

該元素保存着DT_RELA重定位表總的字節大小。

* DT_RELAENT

該元素保存着DT_RELA重定位入口的字節大小。


* DT_STRSZ

該元素保存着字符串表的字節大小。

* DT_SYMENT

該元素保存着符號表入口的字節大小。

* DT_INIT

該元素保存着初始化函數的地址,在下面“初始化和終止函數”中討論。

* DT_FINI

該元素保存着終止函數的地址,在下面“初始化和終止函數”中討論。

* DT_SONAME

該元素保存着以NULL結尾的字符串的字符串表偏移量,那些名字是共享
object的名字。偏移量是在DT_STRTAB入口記錄的表的索引。關於那些名字看
Shared Object Dependencies 部分獲得更多的信息。

* DT_RPATH

該元素保存着以NULL結尾的搜索庫的搜索目錄字符串的字符串表偏移量。
在共享object依賴關係(Shared Object Dependencies)中有討論

* DT_SYMBOLIC

在共享object庫中出現的該元素爲在庫中的引用改變動態連接器符號解析的算法。
替代在可執行文件中的符號搜索,動態連接器從它自己的共享object開始。假如
一個共享的object提供引用參考失敗,那麼動態連接器再照常的搜索可執行文件
和其他的共享object。


* DT_REL

該元素相似於DT_RELA,除了它的表有潛在的加數,正如32-bit文件類型的
Elf32_Rel一樣。假如這個元素存在,它的動態結構必須也同時要有DT_RELSZ
和DT_RELENT的元素。

* DT_RELSZ

該元素保存着DT_REL重定位表的總字節大小。

* DT_RELENT

該元素保存着DT_RELENT重定爲入口的字節大小。

* DT_PLTREL

該成員指明瞭PLT指向的重定位入口的類型。適當地, d_val成員保存着
DT_REL或DT_RELA。在一個PLT中的所有重定位必須使用相同的轉換。

* DT_DEBUG

該成員被調試使用。它的內容沒有被ABI指定;訪問該入口的程序不是
ABI-conforming的。

* DT_TEXTREL

如在程序頭表中段許可所指出的那樣,這個成員的缺乏代表沒有重置入
口會引起非寫段的修改。假如該成員存在,一個或多個重定位入口可能
請求修改一個非寫段,並且動態連接器能因此有準備。


* DT_JMPREL

假如存在,它的入口d_ptr成員保存着重定位入口(該入口單獨關聯着
PLT)的地址。假如lazy方式打開,那麼分離它們的重定位入口讓動態連接
器在進程初始化時忽略它們。假如該入口存在,相關聯的類型入口DT_PLTRELSZ
和DT_PLTREL一定要存在。

* DT_LOPROC through DT_HIPROC

在該範圍內的變量爲特殊的處理器語義保留。除了在數組末尾的DT_NULL元素,
和DT_NEEDED元素相關的次序,入口可能出現在任何次序中。在表中不出
現的Tag值是保留的。


Shared Object Dependencies(共享Object的依賴關係)

當連接器處理一個文檔庫時,它取出庫中成員並且把它們拷貝到一個輸出的
object文件中。當運行時沒有包括一個動態連接器的時候,那些靜態的連接服
務是可用的。共享object也提供服務,動態連接器必須把正確的共享object
文件連接到要實行的進程映象中。因此,可執行文件和共享的object文件之間
存在着明確的依賴性。

當動態連接器爲一個object文件創建內存段時,依賴關係(在動態結構的
DT_NEEDED入口中記錄)表明需要哪些object來爲程序提供服務。通過
重複的連接參考的共享object和他們的依賴關係,動態連接器可以建造一個
完全的進程映象。當解決一個符號引用的時候,動態連接器以寬度優先搜索
(breadth-first)來檢查符號表,換句話說,它先查看自己的可實行程序
中的符號表,然後是頂端DT_NEEDED入口(按順序)的符號表,再接下來是
第二級的DT_NEEDED入口,依次類推。共享object文件必須對進程是可讀的;
其他權限是不需要的。

注意:即使當一個共享object被引用多次(在依賴列關係表中),動態連接器
只把它連接到進程中一次。

在依賴關係列表中的名字既被DT_SONAME字符串拷貝,又被建立object文件
時的路徑名拷貝。例如,動態連接器建立一個可執行文件(使用帶DT_SONAME
入口的lib1共享文件)和一個路徑名爲/usr/lib/lib2的共享object庫,
那麼可執行文件將在它自己的依賴關係列表中包含lib1和/usr/bin/lib2。

假如一個共享object名字有一個或更多的反斜槓字符(/)在這名字的如何地方,
例如上面的/usr/lib/lib2文件或目錄,動態連接器把那個字符串自己做爲路徑名。
假如名字沒有反斜槓字符(/),例如上面的lib1,三種方法指定共享文件的
搜索路徑,如下:

* 第一,動態數組標記DT_RPATH保存着目錄列表的字符串(用冒號(:)分隔)。
例如,字符串/home/dir/lib:/home/dir2/lib:告訴動態連接器先搜索
/home/dir/lib,再搜索/home/dir2/lib,再是當前目錄。

* 第二,在進程環境中(see exec(BA_OS)),有一個變量稱爲LD_LIBRARY_PATH
可以保存象上面一樣的目錄列表(隨意跟一個分號(;)和其他目錄列表)。
以下變量等於前面的例子:
LD_LIBRARY_PATH=/home/dir/lib:/home/dir2/lib:
LD_LIBRARY_PATH=/home/dir/lib;/home/dir2/lib:
LD_LIBRARY_PATH=/home/dir/lib:/home/dir2/lib:;
所以的LD_LIBRARY_PATH目錄在DT_RPATH指向的目錄之後被搜索。儘管一些
程序(例如連接編輯器)不同的處理分號前和分號後的目錄,但是動態連接
不會。不過,動態連接器接受分號符號,具體語意在如上面描述。

* 最後,如果上面的兩個目錄查找想要得到的庫失敗,那麼動態連接器搜索
/usr/lib.

注意:出於安全考慮,動態連接器忽略set-user和set-group的程序的
LD_LIBRARY_PATH所指定的搜索目錄。但它會搜索DT_RPATH指明的目錄和
/usr/lib。


Global Offset Table(GOT全局偏移量表)

一般情況下,位置無關的代碼不包含絕對的虛擬地址。全局偏移量表在私有數據
中保存着絕對地址,所以應該使地址可用的,而不是和位置無關性和程序代碼段
共享能力妥協。一個程序引用它的GOT(全局偏移量表)來使用位置無關的地址並且
提取絕對的變量,所以重定位位置無關的參考到絕對的位置。

初始時,GOT(全局偏移量表)保存着它重定位入口所需要的信息 [看第一部分的
“Relocation”]。在系統爲一個可裝載的object文件創建內存段以後,動態
連接器處理重定位入口,那些類型爲R_386_GLOB_DAT的指明瞭GOT(全局偏移量表)。
動態連接器決定了相關的標號變量,計算他們的絕對地址,並且設置適當的內存
表入口到正確的變量。雖然當連接編輯器建造object文件的時候,絕對地址
是不知道,連接器知道所以內存段的地址並且能夠因此計算出包含在那裏的
標號地址。

假如程序需要直接訪問符號的絕對地址,那麼這個符號將有一個GOT(全局偏移量表)
入口。因爲可執行文件和共享文件有獨立的GOT(全局偏移量表),一個符號地址
可能出現在不同的幾個表中。在交給進程映象的代碼控制權以前,動態連接器處
理所有的重定位的GOT(全局偏移量表),所以在執行時,確認絕對地址是可用的。

該表的入口0是爲保存動態結構地址保留的(參考_DYNAMIC標號)。這允許
象動態連接程序那樣來找出他們自己的動態結構(還沒有處理他們的重
定向入口)。這些對於動態連接器是重要的,因爲它必要初始化自己而不
能依賴於其他程序來重定位他們的內存映象。在32位Interl系統結構中,在
GOT中的人口1和2也是保留的,具體看以下的過程連接表(Procedure Linkage
Table)。

系統可以爲在不同的程序中相同的共享object選擇不同的內存段;它甚至可以
爲相同的程序不同的進程選擇不同的庫地址。雖然如此,一旦進程映象被建立
以後,內存段不改變地址。只要一個進程存在,它的內存段駐留在固定的虛擬
地址。

GOT表的格式和解釋是處理器相關的。在32位Intel體系結構下,標號
_GLOBAL_OFFSET_TABLE_可能被用來訪問該表。

+ Figure 2-11: Global Offset Table

extern Elf32_Addr _GLOBAL_OFFSET_TABLE_[];

標號_GLOBAL_OFFSET_TABLE_可能駐留在.got section的中間,允許負的和非負
的下標索引這個數組。


Procedure Linkage Table(PLT過程連接表)

就象GOT重定位把位置無關的地址計算成絕對地址一樣,PLT過程連接表重定位
位置無關的函數調用到絕對的地址。從一個可執行或者共享的object文件到另外的,
連接編輯器不解析執行的傳輸(例如函數的調用)。因此,連接編輯器安排程序
的傳遞控制到PLT中的入口。在SYSTEM V體系下,PLT存在共享文本中,但是它們
使用的地址是在私有的GOT中。符號連接器決定了目標的絕對地址並且修改GOT的
內存映象。因此,在沒有危及到位置無關、程序文本的共享能力的情況下。動態
連接器能重定位人口。


+ Figure 2-12: Absolute Procedure Linkage Table
絕對的過程連接表

.PLT0:pushl got_plus_4
jmp *got_plus_8
nop; nop
nop; nop
.PLT1:jmp *name1_in_GOT
pushl $offset
jmp .PLT0@PC
.PLT2:jmp *name2_in_GOT
pushl $offset
jmp .PLT0@PC
...

+ Figure 2-13: Position-Independent Procedure Linkage Table
位置無關(或者說位置獨立)的過程連接表
.PLT0:pushl 4(%ebx)
jmp *8(%ebx)
nop; nop
nop; nop
.PLT1:jmp *name1@GOT(%ebx)
pushl $offset
jmp .PLT0@PC
.PLT2:jmp *name2@GOT(%ebx)
pushl $offset
jmp .PLT0@PC
...

注意:如圖所示,PLT的指令使用了不同的操作數地址方式,對絕對代碼和
對位置無關的代碼。但是,他們的界面對於動態連接器是相同的。

以下的步驟,動態連接器和程序協作(cooperate)通過PLT和GOT來解析符號
引用。

1. 當第一次創建程序的內存映象時,動態連接器爲在GOT中特別的變量設置
第二次和第三次的入口。下面關於那些變量有更多的解釋。

2. 假如PLT是位置無關的,那麼GOT的地址一定是保留在%ebx中的。每個在進程
映象**享的object文件有它自己的PLT,並且僅僅在同一個object文件中,
控制傳輸到PLT入口。從而,要調用的函數有責任在調用PLT入口前,設置PLT
地址到寄存器中。

3. 舉例說明,假如程序調用函數name1,它的傳輸控制到標號.PLT1.

4. 第一個指令跳到在GOT入口的name1地址。初始話時,GOT保存着緊跟着的push1
指令的地址,而不是真實的name1的地址。

5. 因此,程序在堆棧中壓入(push)一個重定位的偏移量。重定位的偏移量是
一個32位,非負的字節偏移量(從定位表算起)。指派的重定位入口將是
一個R_386_JMP_SLOT類型,它的偏移量指明瞭GOT入口(在前面的jmp指令中
被使用)。該重定位入口也包含一個符號表的索引,因此告訴動態連接器
哪個符號要被引用,在這裏是name1。

6. 在壓入(push)一個重定位的偏移量後,程序跳到.PLT0,在PLT中的第一個入口。
push1指令在堆棧中放置第二個GOT入口(got_plus_4 or 4(%ebx))的值,
因此,給動態連接器一個word的鑑別信息。然後程序跳到第三個GOT入口
(got_plus_8 or 8(%ebx)),它傳輸控制到動態連接器。

7. 當動態連接器接到控制權,它展開堆棧,查看指派的重定位入口,尋找符號的
值,在GOT入口中存儲真實的name1地址,然後傳輸控制想要目的地。

8. PLT入口的併發執行將直接傳輸控制到name1,而不用第二次調用動態連接器
了。所以,在.PLT1中的jmp指令將轉到name1,代替“falling through”
轉到pushl指令。

LD_BIND_NOW環境變量能改變動態連接器的行爲。假如這個變量爲非空,動態
連接器在傳輸控制到程序前計算PLT入口。換句話說,動態連接器處理重定位
類型爲R_386_JMP_SLOT的入口在進程初始化時。否則,動態連接器計算PLT入口
懶惰的,推遲到符號解析和重定位直到一個表入口的第一次執行。


注意:一般來說,以懶惰(Lazy)方式綁定是對全應用程序執行的改進。
因爲不使用的符號就不會招致動態連接器做無用功。然而,對一些應用程序,
兩種情況使用懶惰(Lazy)方式是不受歡迎的。

第一 初始的引用一個共享object函數比後來的調用要花的時間長,因爲動
態連接器截取調用來解析符號。一些應用程序是不能容忍這樣的。
第二 假如這個錯誤發生並且動態連接器不能解析該符號,動態連接器將終止
程序。在懶惰(Lazy)方式下,這可能發生在任意的時候。一再的,一
些應用程序是不能容忍這樣的。通過關掉懶惰(Lazy)方式,在應用程
序接到控制前,當在處理初始話時發生錯誤,動態連接器強迫程序,使
之失敗。

Hash Table(哈希表)

Elf32_Word object的哈希表支持符號表的訪問。
標號出現在下面幫助解釋哈希表的組織,但是它們不是規範的一部分。

+ Figure 2-14: Symbol Hash Table

nbucket
nchain
bucket[0]
...
bucket[nbucket - 1]
chain[0]
...
chain[nchain - 1]


bucket數組包含了nbucket入口,並且chain數組包含了nchain個入口;索引從0開始。
bucket和chain保存着符號表的索引。Chain表入口類似於符號表。符號表入口的
數目應該等於nchain;所以符號表的索引也選擇chain表的入口。
一個哈希函數(如下的)接受一個符號名並且返回一個可以被計算機使用的bucket索引
的值。因此,假如一個哈希函數返回一些名字的值爲X,那麼bucket[x%nbucket]
將給出一個索引y(既是符號表和chain表的索引)。假如符號表入口不是期望的,
chain[y]給出下一個符號表的入口(使用相同的哈希變量)。可以沿着chain
鏈直到選擇到了期望名字的符號表入口或者是碰到了STN_UNDEF的入口。

+ Figure 2-15: Hashing Function

unsigned long
elf_hash(const unsigned char *name)
{
unsigned long h = 0, g;

while (*name) {
h = (h << 4) + *name++;
if (g = h & 0xf0000000)
h ^= g >> 24;
h &= ~g;
}
return h;
}


Initialization and Termination Functions
初始化和終止函數


在動態連接妻建立進程映象和執行重定位以後,每一個共享object得到適當
的機會來執行一些初始話代碼。初始化函數不按特別的順序被調用,但是
所有的共享object初始化發生在執行程序獲得控制之前。

類似地,共享的object可能包含終止函數,它們在進程本身開始它的終止之後
被執行(以atexit(BA_OS)的機制)。

共享object通過設置在動態結構中的DT_INIT和DT_FINI入口來指派它們的初始化
和終止函數,如上動態section(Dynamic Section)部分描述。典型的,那些函數
代碼存在.init和.fini section中,第一部分的“section”已經提到過。

注意:儘管atexit(BA_OS)的終止處理一般可可正常完成,但是不保證在死進程上
被執行。特別的,假如_exit被調用(看exit(BA_OS))或者假如進程死掉,那麼
進程是不執行終止處理的。因爲它收到一個信號,該信號可捕獲或忽略。

________________________________________________________________


3. C LIBRARY

________________________________________________________________


========================== C Library ===========================

C庫,libc,包含了所有的符號(包含在libsys),另外,包含在在下面兩個
表中列出的運行函數。第一個表中的運行函數是ANSI C標準的。

+ Figure 3-1: libc Contents, Names without Synonyms

abort fputc isprint putc strncmp
abs fputs ispunct putchar strncpy
asctime fread isspace puts strpbrk
atof freopen isupper qsort strrchr
atoi frexp isxdigit raise strspn
atol fscanf labs rand strstr
bsearch fseek ldexp rewind strtod
clearerr fsetpos ldiv scanf strtok
clock ftell localtime setbuf strtol
ctime fwrite longjmp setjmp strtoul
difftime getc mblen setvbuf tmpfile
div getchar mbstowcs sprintf tmpnam
fclose getenv mbtowc srand tolower
feof gets memchr sscanf toupper
ferror gmtime memcmp strcat ungetc
fflush isalnum memcpy strchr vfprintf
fgetc isalpha memmove strcmp vprintf
fgetpos iscntrl memset strcpy vsprintf
fgets isdigit mktime strcspn wcstombs
fopen isgraph perror strlen wctomb
fprintf islower printf strncat

再加上, libc 保存着以下的服務。

+ Figure 3-2: libc Contents, Names with Synonyms

__assert getdate lockf ** sleep tell **
cfgetispeed getopt lsearch strdup tempnam
cfgetospeed getpass memccpy swab tfind
cfsetispeed getsubopt mkfifo tcdrain toascii
cfsetospeed getw mktemp tcflow _tolower
ctermid hcreate monitor tcflush tsearch
cuserid hdestroy nftw tcgetattr _toupper
dup2 hsearch nl_langinfo tcgetpgrp twalk
fdopen isascii pclose tcgetsid tzset
__filbuf isatty popen tcsendbreak _xftw
fileno isnan putenv tcsetattr
__flsbuf isnand ** putw tcsetpgrp
fmtmsg ** lfind setlabel tdelete

** = Function is at Level 2 in the SVID Issue 3 and therefore at
Level 2 in the ABI.

包括上面同義(Synonyms)表列出的標號,對於 入口已經存在的_
形式(帶一個下劃線,上面沒有列出來)優先權高於它們的名字。所以,例如,
libc同時包含了getopt和_getopt。

在常規的上列中,其他地方以下沒有被定義。

int __filbuf(FILE *f);
This function returns the next input character for f, filling
its buffer as appropriate. It returns EOF if an error occurs.

int __flsbuf(int x, FILE *f);
This function flushes the output characters for f as if
putc(x, f) had been called and then appends the value of x to
the resulting output stream. It returns EOF if an error occurs
and x otherwise.

int _xftw(int, char *, int (*)(char *, struct stat *, int), int);
Calls to the ftw(BA_LIB) function are mapped to this function
when applications are compiled. This function is identical to
ftw(BA_LIB), except that _xftw() takes an interposed first
argument, which must have the value 2.


要了解更多的關於SVID,ANSI C,POSIX的知識,可看該章節其他的庫section部分。
該節“System Data Interfaces”後有更多的描述。

Global Data Symbols
全局數據符號


libc庫需要一些外部的全局數據符號(爲了它自己的常規工作而定義的)。
所有向libsys庫請求的數據符號一定要讓libc提供,就象下面表中的數據符號。

正式定義的數據object被他們的符號描述,看System V接口定義,第三版本或者第6章節的數據定義(Data Definitions)section(在適當的處理器補充到System V ABI)。

在下面表中的入口有-_的形式。一對符號都代表了一些數據。
下劃線的synonyms假設滿足ANSI C標準。


+ Figure 3-3: libc Contents, Global External Data Symbols

getdate_err optarg
_getdate_err opterr
__iob optind
optopt

 

xueyan 05-06-24 03:05

Linux動態鏈接技術(轉載)
Linux動態鏈接技術(轉載)

在動態鏈接的應用程序或共享庫中,ELF的程序頭描述表具有一個PT_DYNAMIC類型的描述符,它指?br>雋?dynamic段的位 置,dynamic段用來描述動態鏈接過程。當應用程序調用的共享庫函數時,要通過.plt段進行跳轉。plt段又稱爲過程連接表,它是連接器ld所生成 的一組靜態的trampline,是隻讀的可執行的段,包含在.text段一起映射到內存。plt每16個字節爲一個槽位,plt的第1個槽位保留給動態 解析器使用,其餘的槽位表示對不同共享庫函數的調用。plt依賴於全局偏移量表(.got段),GOT表是一可寫的數據段,包含在.data段中一起映射 到內存,用來存放共享符號的絕對地址。?br>τ貿絛虻饔黴蠶碸夂褪峭ü齪lt槽位上的一條jmp指令跳轉到GOT表所指的一個共享函數指針。這 樣,共享庫的重定位就化爲對GOT表項的重定位。GOT表的第1個指針指向.dynamic段,第2、3個?br>剛胗雙lt段的第1個槽位對應, 用來安裝動態解析器。爲了少做無用功,Linux採用了動態解析技術,就是說在加載共享庫時,並不進行函數的解析,而是安裝動態解析器,讓共享庫調用指向 解析器,只有當函數調用發生時才進行解析。爲此,在ld在生成可執行程序時,讓其GOT共享函數指針指?br>蚋髯詐lt槽位上的兩條指令,一條是 pushl指令,將該函數所對應GOT重定位表的索引作爲參數壓入堆棧,然後通過另一條jmp指令跳轉到plt槽位1,它再跳轉到GOT表第3個指針所表 示的動態解析器?br>肟凇U庋狽⑸⒊曬饢瞿勘旰詮蠶碸庵械牡刂肥保煤誄絛騁OT表中的指針就被實際的地址刷新。


下面是基於動態解釋器ld.so-1.9.9版本的簡單分析

簡單的測試文件testso.c:

int x = 0;

int test()
{
return x;
}

用gcc -S -fPIC testso.c編繹成的彙編代碼:

.globl x
.data
.align 4
.type x,@object
.size x,4
x:
.long 0
.text
.align 4
.globl test
.type test,@function
test:
pushl %ebp
movl %esp,%ebp
pushl %ebx
call .L2
.L2:
popl %ebx # 取標號.L2所在的地址
addl $_GLOBAL_OFFSET_TABLE_+[.-.L2],%ebx #_GLOBAL_OFFSET_TABLE_爲當前地址到GOT表的偏移
movl x@GOT(%ebx),%eax # ebx在-fPIC編繹的函數中用於指向本模塊的GOT表
movl (%eax),%eax # x@GOT表示符號x在GOT表中的索引
movl -4(%ebp),%ebx
leave
ret
.Lfe1:
.size test,.Lfe1-test

用gcc -shared testso.s -o testso.so生成共享庫的反彙編的有關輸出:

Disassembly of section .plt:

00000258 <.plt>:
258: ff b3 04 00 00 pushl 0x4(%ebx) #GOT表的第2個指針,對-fPIC編繹的函數,ebx總是指向GOT表
25d: 00
25e: ff a3 08 00 00 jmp *0x8(%ebx) #跳轉到GOT表的第3個指針,調用__dl_linux_resolover
263: 00
264: 00 00 addb %al,(%eax)
266: 00 00 addb %al,(%eax)
268: ff a3 0c 00 00 jmp *0xc(%ebx) # 跳轉到共享函數test()所在的GOT的指針
26d: 00
26e: 68 00 00 00 00 pushl x0 # test()所在GOT指針的初始入口
273: e9 e0 ff ff ff jmp 258 <_init+0x8> # 跳轉到plt槽位1

Disassembly of section .text:

000002d8 :
2d8: 55 pushl %ebp
2d9: 89 e5 movl %esp,%ebp
2db: 53 pushl %ebx
2dc: e8 00 00 00 00 call 2e1
2e1: 5b popl %ebx
2e2: 81 c3 ab 10 00 addl x10ab,%ebx # 取GOT表指針
2e7: 00
2e8: 8b 83 10 00 00 movl 0x10(%ebx),%eax # 從GOT表中取變量x的地址
2ed: 00
2ee: 8b 00 movl (%eax),%eax
2f0: 8b 5d fc movl 0xfffffffc(%ebp),%ebx
2f3: c9 leave
2f4: c3 ret


引用testso的應用程序文件test.c:
main()
{
printf("%d/n",test());
}
用gcc test.c testso.so -o test生成可執行文件的反彙編輸出:

Disassembly of section .plt:

08048398 <.plt>:
8048398: ff 35 54 95 04 pushl 0x8049554 # GOT表的第2個指針
804839d: 08
804839e: ff 25 58 95 04 jmp *0x8049558 #GOT表的第3個指針,運行_dl_linux_resolver
80483a3: 08
80483a4: 00 00 addb %al,(%eax)
80483a6: 00 00 addb %al,(%eax)
80483a8: ff 25 5c 95 04 jmp *0x804955c # printf()在plt的入口
80483ad: 08
80483ae: 68 00 00 00 00 pushl x0
80483b3: e9 e0 ff ff ff jmp 8048398 <_init+0x8>
80483b8: ff 25 60 95 04 jmp *0x8049560
80483bd: 08
80483be: 68 08 00 00 00 pushl x8
80483c3: e9 d0 ff ff ff jmp 8048398 <_init+0x8>
80483c8: ff 25 64 95 04 jmp *0x8049564 # test()在plt段的調用點
80483cd: 08 # [0x8048564]初始時指向0x80483ce
80483ce: 68 10 00 00 00 pushl x10 # test()在GOT表重定位表.rel.got中的索引
80483d3: e9 c0 ff ff ff jmp 8048398 <_init+0x8> # 跳轉到plt的第1槽位
80483d8: ff 25 68 95 04 jmp *0x8049568
80483dd: 08
80483de: 68 18 00 00 00 pushl x18
80483e3: e9 b0 ff ff ff jmp 8048398 <_init+0x8>
80483e8: ff 25 6c 95 04 jmp *0x804956c
80483ed: 08
80483ee: 68 20 00 00 00 pushl x20
80483f3: e9 a0 ff ff ff jmp 8048398 <_init+0x8>
80483f8: ff 25 70 95 04 jmp *0x8049570
80483fd: 08
80483fe: 68 28 00 00 00 pushl x28
8048403: e9 90 ff ff ff jmp 8048398 <_init+0x8>

Disassembly of section .text:

080484c8 :
80484c8: 55 pushl %ebp
80484c9: 89 e5 movl %esp,%ebp
80484cb: e8 f8 fe ff ff call 80483c8 <_init+0x38> #
80484d0: 89 c0 movl %eax,%eax
80484d2: 50 pushl %eax
80484d3: 68 38 85 04 08 pushl x8048538
80484d8: e8 cb fe ff ff call 80483a8 <_init+0x18>
80484dd: 83 c4 08 addl x8,%esp
80484e0: c9 leave
80484e1: c3 ret

ld.so-1.9.9/d-link/i386/resolve.S

#define ALIGN 4
#define RUN linux_run
#define RESOLVE _dl_linux_resolve
#define RESOLVER _dl_linux_resolver
#define EXIT _interpreter_exit
#define INIT __loader_bootstrap

.text
.align ALIGN
.align 16

.globl RESOLVE
.type RESOLVE,@function
RESOLVE:
pusha
lea 0x20(%esp),%eax /* eax = tpnt and reloc_entry params */
pushl 4(%eax) /* push copy of reloc_entry param */
pushl (%eax) /* push copy of tpnt param */
pushl %eax /* _dl_linux_resolver expects a dummy
* param - this could be removed */
#ifdef __PIC__
call .L24
.L24:
popl %ebx
addl $_GLOBAL_OFFSET_TABLE_+[.-.L24],%ebx
movl RESOLVER@GOT(%ebx),%ebx /* eax = resolved func */
call *%ebx
#else
call RESOLVER
#endif
movl %eax,0x2C(%esp) /* store func addr over original
* tpnt param */
addl xC,%esp /* remove copy parameters */
popa /* restore regs */
ret /* jump to func removing original
* reloc_entry param from stack */
.LFE2:
.size RESOLVE,.LFE2-RESOLVE

d-link/i386/elfinterp.c:

unsigned int _dl_linux_resolver(int dummy, int i)
{
unsigned int * sp;
int reloc_entry;
int reloc_type;
struct elf32_rel * this_reloc;
char * strtab;
struct elf32_sym * symtab;
struct elf32_rel * rel_addr;
struct elf_resolve * tpnt;
int symtab_index;
char * new_addr;
char ** got_addr;
unsigned int instr_addr;
sp = &i;
reloc_entry = sp[1];
tpnt = (struct elf_resolve *) sp[0];

rel_addr = (struct elf32_rel *) (tpnt->dynamic_info[DT_JMPREL] +
tpnt->loadaddr); 取可執行程序的GOT重定位表

this_reloc = rel_addr + (reloc_entry >> 3);
reloc_type = ELF32_R_TYPE(this_reloc->r_info);
symtab_index = ELF32_R_SYM(this_reloc->r_info);

symtab = (struct elf32_sym *) (tpnt->dynamic_info[DT_SYMTAB] + tpnt->loadaddr);
strtab = (char *) (tpnt->dynamic_info[DT_STRTAB] + tpnt->loadaddr);


if (reloc_type != R_386_JMP_SLOT) {
_dl_fdprintf(2, "%s: Incorrect relocation type in jump relocations/n",
_dl_progname);
_dl_exit(1);
};

/* Address of jump instruction to fix up */
instr_addr = ((int)this_reloc->r_offset + (int)tpnt->loadaddr);
got_addr = (char **) instr_addr;

#ifdef DEBUG
_dl_fdprintf(2, "Resolving symbol %s/n",
strtab + symtab[symtab_index].st_name);
#endif

/* Get the address of the GOT entry */
new_addr = _dl_find_hash(strtab + symtab[symtab_index].st_name,
tpnt->symbol_scope, (int) got_addr, tpnt, 0);
if(!new_addr) {
_dl_fdprintf(2, "%s: can't resolve symbol '%s'/n",
_dl_progname, strtab + symtab[symtab_index].st_name);
_dl_exit(1);
};
/* #define DEBUG_LIBRARY */
#ifdef DEBUG_LIBRARY
if((unsigned int) got_addr < 0x40000000) {
_dl_fdprintf(2, "Calling library function: %s/n",
strtab + symtab[symtab_index].st_name);
} else {
*got_addr = new_addr;
}
#else
*got_addr = new_addr; 更新GOT函數指針
#endif
return (unsigned int) new_addr;
}

 

xueyan 05-06-24 03:06

Elf動態解析符號過程(轉載)
本篇文章以linux爲平臺爲例,演示ELF動態解析符號的過程


本篇文章以linux爲平臺爲例,演示ELF動態解析符號的過程。
不正之處,還請斧正。

通常,ELF解析符號方式稱爲lazy MODE裝載的。這種裝載技術是ELF平臺上
默認的方式。在不同的體系平臺在實現這種機制也是不同的。但是i386和SPARC
在大部分上是相同的。

動態連接器(rtld)提供符號的動態連接,裝載共享objects和解析標號的引用。
通常是ld.so,它可以是一個共享object也可以是個可執行的文件。


★★ 符號表(symbol table)

每個object要想使它對其他的ELF文件可用,就要用到符號表(symbol table)中
symbol entry.事實上,一個symbol entry 是個symbol結構,它描述了這個
symbol的名字和該symbol的value.symbol name被編碼作爲dynamic string
table的索引(index). The value of a symbol是在ELF OBJECT文件內該
symbol的地址。該地址通常需要被重新定位(加上該object裝載到內存的基地址
(base load address)). 從而構成該symbol在內存中的絕對地址。
一個符號表入口有如下的格式:
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; /* No defined meaning, 0 */
Elf32_Section st_shndx; /* Section index */
} Elf32_Sym;

可執行文件他們知道運行時刻他們的地址,所以他們內部的引用符號在編譯時候就已
經被重定位了。


★★ GOT(global offset table)

GOT是一個數組,存在ELF image的數據段中,他們是一些指向objects的指針(通常
是數據objects).動態連接器將重新修改那些編譯時還沒有確定下來地址的符號的
GOT入口。所以說GOT在i386動態連接中扮演着重要的角色。


★★ PLT(procedure linkage table)

PLT是一個這樣的結構,它的entries包含了一些代碼片段用來傳輸控制到外部的過程。
在i386體系下,PLT和他的代碼片段entries有如下格式:

PLT0:
push GOT[1] ; word of identifying information
jmp GOT[2] ; pointer to rtld function nop
...
PLTn:
jmp GOT[x + n] ; GOT offset of symbol address
push n ; relocation offset of symbol
jmp PLT0 ; call the rtld
PLTn + 1
jmp GOT[x +n +1]; GOT offset of symbol address
push n +1 ; relocation offset of symbol
jmp PLT0 ; call the rtld

當傳輸控制到一個外部的函數時,它傳輸執行到PLT 中跟該symbol相關的那個entry
(是在編譯時候連接器安裝的)。在PLT entry中第一條指令將jump到一個存儲在GOT
中的一個指針地址;假如符號還沒有被解析,該GOT中存放着的是該PLT entry中的
下一條指令地址。該指令push一個在重定位表中的偏移量到stack,然後下一條指令
傳輸控制到PLT[0]入口。該PLT[0]包含了調用RTLD解析符號的函數代碼。該
解析符號函數地址由程序裝載器已經插入到GOT[2]中了。

動態連接器將展開stack並且獲取需要解析符號在重定位表地址信息。重定位入口、
符號表和字符串表共同決定着PLT entry引用的那個符號和在進程內存中符號應該
存放的地址。假如可能的話,該符號將被解析出來,它的地址將被存放在被該
PLT entry使用的GOT entry中。下一次該符號被請求時,與之對應的GOT已經包
含了該符號的地址了。所以,所有後來的調用將直接通過GOT傳輸控制。動態連接器
只解析第一次被二進制文件所引用的符號;這種引用方式就是我們上面所說的
lazy MODE。


★★ 哈希表和鏈(hash table and chain)

除了符號表(symbol table),GOT(global offset table),PLT(procedure
linkage table),字符串表(string table),ELF objects還可以包含一個
hash table和chain(用來使動態連接器解析符號更加容易)。hash table和chain
通常被用來迅速判定在符號表中哪個entry可能符合所請求的符號名。hash table(總
是伴隨着chain的)被作爲整型數組存放。在hash表中,一半位置是留給那些buckets的,
另一半是留給在chain中的元素(element)的. hash table直接反映了symbol table
的元素數目和他們的次序。

動態連接器結構提供了所有動態連接的執行是以透明方式訪問動態連接器.
然而,明確訪問也是可用的。動態連接(裝載共享objects和解析符號),
可以通過直接訪問RTLD的那些函數來完成:dlopen() , dlsym() and
dlclose() .這些函數被包含在動態連接器本身中。爲了訪問那些函數,
連接時需要把動態連接函數庫(libdl)連接進去。該庫包含了一些stub函數
允許編譯時候連接器解析那些函數的引用;然而那些stub函數只簡單的返回0。
因爲事實上函數駐留在動態連接器中,假如從靜態連接的ELF文件中調用
那些函數,共享object的裝載將會失敗。

對於執行動態連接器所必須的是:hash table,hash table元素的數目,
chain,dynamic string table和dynamic symbol talbe。滿足了
這些條件,下面算法適用任何symbol的地址計算:

1. hn = elf_hash(sym_name) % nbuckets;
2. for (ndx = hash[ hn ]; ndx; ndx = chain[ ndx ]) {
3. symbol = sym_tab + ndx;
4. if (strcmp(sym_name, str_tab + symbol->st_name) == 0)
5. return (load_addr + symbol->st_value); }

hash號是elf_hash()的返回值,在ELF規範的第4部分有定義,以hash table中元素
個數取模。該號被用來做hash table的下表索引,求得hash值,找出與之匹配的符號
名的chain的索引(line 3)。使用該索引,符號從符號表中獲得(line 3).比較獲得
的符號名和請求的符號名是否相同(line 5).使用這個算法,就可以簡單解析任何符號了。


★★ 演示

#include
int main(int argc, char *argv[])
{
printf("Hello, world/n");
return 0;
}


Relocation section '.rel.plt' at offset 0x278 contains 4 entries:
Offset Info Type Symbol's Value Symbol's Name
0804947c 00107 R_386_JUMP_SLOT 080482d8 __register_frame_info
08049480 00207 R_386_JUMP_SLOT 080482e8 __deregister_frame_info
08049484 00307 R_386_JUMP_SLOT 080482f8 __libc_start_main
08049488 00407 R_386_JUMP_SLOT 08048308 printf
只有R_386_JUMP_SLOT的纔會出現在GOT中

Symbol table '.dynsym' contains 7 entries:
Num: Value Size Type Bind Ot Ndx Name
0: 0 0 NOTYPE LOCAL 0 UND
1: 80482d8 116 FUNC WEAK 0 UND __register_frame_info@GLIBC_2.0 (2)
2: 80482e8 162 FUNC WEAK 0 UND __deregister_frame_info@GLIBC_2.0 (
2)
3: 80482f8 261 FUNC GLOBAL 0 UND __libc_start_main@GLIBC_2.0 (2)
4: 8048308 41 FUNC GLOBAL 0 UND printf@GLIBC_2.0 (2)
5: 804843c 4 OBJECT GLOBAL 0 14 _IO_stdin_used
6: 0 0 NOTYPE WEAK 0 UND __gmon_start__


[alert7@redhat]$ gcc -o test test.c
[alert7@redhat]$ ./test
Hello, world
[alert7@redhat]$ gdb -q test
(gdb) disass main
Dump of assembler code for function main:
0x80483d0 : push %ebp
0x80483d1 : mov %esp,%ebp
0x80483d3 : push x8048440
0x80483d8 : call 0x8048308
0x80483dd : add x4,%esp
0x80483e0 : xor %eax,%eax
0x80483e2 : jmp 0x80483e4
0x80483e4 : leave
0x80483e5 : ret
...
0x80483ef : nop
End of assembler dump.
(gdb) b * 0x80483d8
Breakpoint 1 at 0x80483d8
(gdb) r
Starting program: /home/alert7/test

Breakpoint 1, 0x80483d8 in main ()
(gdb) disass 0x8048308 ① ⑴
Dump of assembler code for function printf:
/****************************************/ //PLT4:
0x8048308 : jmp *0x8049488 //jmp GOT[6]
//此時,GOT[6]中存在的是0x804830e
0x804830e : push x18 //x18爲printf入口在GOT的偏移量
0x8048313 : jmp 0x80482c8 <_init+48> //jmp PLT0
//PLT0處存放着調用RTLD函數的指令
//當函數返回時候,把GOT[6]修改爲真正的
//printf函數地址,然後直接跳到printf函數
//執行。
該部分爲PLT的一部分
/****************************************/
End of assembler dump.
(gdb) x 0x8049488
0x8049488 <_GLOBAL_OFFSET_TABLE_+24>: 0x0804830e
080482c8 <.plt>: ② //PLT0:
80482c8: ff 35 74 94 04 08 pushl 0x8049474 //pushl GOT[1]地址
//GOT[1]是一個鑑別信息,是link_map類型的一個指針

80482ce: ff 25 78 94 04 08 jmp *0x8049478 //JMP GOT[2]
//跳到動態連接器解析函數執行
80482d4: 00 00 add %al,(%eax)
80482d6: 00 00 add %al,(%eax)

80482d8: ff 25 7c 94 04 08 jmp *0x804947c //PLT1:
80482de: 68 00 00 00 00 push x0
80482e3: e9 e0 ff ff ff jmp 80482c8 <_init+0x30>

80482e8: ff 25 80 94 04 08 jmp *0x8049480 //PLT2:
80482ee: 68 08 00 00 00 push x8
80482f3: e9 d0 ff ff ff jmp 80482c8 <_init+0x30>

80482f8: ff 25 84 94 04 08 jmp *0x8049484 //PLT3:
80482fe: 68 10 00 00 00 push x10
8048303: e9 c0 ff ff ff jmp 80482c8 <_init+0x30>

8048308: ff 25 88 94 04 08 jmp *0x8049488 //PLT4:
804830e: 68 18 00 00 00 push x18
8048313: e9 b0 ff ff ff jmp 80482c8 <_init+0x30>

(gdb) b * 0x80482c8
Breakpoint 2 at 0x80482c8
(gdb) c
Continuing.

Breakpoint 2, 0x80482c8 in _init ()
(gdb) x/8x 0x8049470
0x8049470 <_GLOBAL_OFFSET_TABLE_>: 0x08049490 0x40013ed0 0x4000a960 0x400fa550
0x8049480 <_GLOBAL_OFFSET_TABLE_+16>: 0x080482ee 0x400328cc 0x0804830e 0x00000000
(gdb) x/50x 0x40013ed0 ( * link_map類型)
0x40013ed0: 0x00000000 0x40010c27 0x08049490 0x400143e0
0x40013ee0: 0x00000000 0x40014100 0x00000000 0x08049490
0x40013ef0: 0x080494e0 0x080494d8 0x080494a8 0x080494b0
0x40013f00: 0x080494b8 0x00000000 0x00000000 0x00000000
0x40013f10: 0x080494c0 0x080494c8 0x08049498 0x080494a0
0x40013f20: 0x00000000 0x00000000 0x00000000 0x080494f8
0x40013f30: 0x08049500 0x08049508 0x080494e8 0x080494d0
0x40013f40: 0x00000000 0x080494f0 0x00000000 0x00000000
0x40013f50: 0x00000000 0x00000000 0x00000000 0x00000000
0x40013f60: 0x00000000 0x00000000 0x00000000 0x00000000
(gdb) disass 0x4000a960 ③
Dump of assembler code for function _dl_runtime_resolve:
0x4000a960 <_dl_runtime_resolve>: push %eax
0x4000a961 <_dl_runtime_resolve+1>: push %ecx
0x4000a962 <_dl_runtime_resolve+2>: push %edx
0x4000a963 <_dl_runtime_resolve+3>: mov 0x10(%esp,1),%edx
0x4000a967 <_dl_runtime_resolve+7>: mov 0xc(%esp,1),%eax
0x4000a96b <_dl_runtime_resolve+11>: call 0x4000a740
//調用真正的解析函數fixup(),修正GOT[6],使它指向真正的printf函數地址
0x4000a970 <_dl_runtime_resolve+16>: pop %edx
0x4000a971 <_dl_runtime_resolve+17>: pop %ecx
0x4000a972 <_dl_runtime_resolve+18>: xchg %eax,(%esp,1)
0x4000a975 <_dl_runtime_resolve+21>: ret x8 //跳到printf函數地址執行
0x4000a978 <_dl_runtime_resolve+24>: nop
0x4000a979 <_dl_runtime_resolve+25>: lea 0x0(%esi,1),%esi
End of assembler dump.
(gdb) b * 0x4000a972
Breakpoint 4 at 0x4000a972: file dl-runtime.c, line 182.
(gdb) c
Continuing.

Breakpoint 4, 0x4000a972 in _dl_runtime_resolve () at dl-runtime.c:182
182 in dl-runtime.c
(gdb) i reg $eax $esp
eax 0x4006804c 1074167884
esp 0xbffffb64 -1073743004
(gdb) b *0x4000a975
Breakpoint 5 at 0x4000a975: file dl-runtime.c, line 182.
(gdb) c
Continuing.

Breakpoint 5, 0x4000a975 in _dl_runtime_resolve () at dl-runtime.c:182
182 in dl-runtime.c
(gdb) si
printf (format=0x1
) at printf.c:26
26 printf.c: No such file or directory.
(gdb) disass ④ ⑵
Dump of assembler code for function printf:
0x4006804c : push %ebp
0x4006804d : mov %esp,%ebp
0x4006804f : push %ebx
0x40068050 : call 0x40068055
0x40068055 : pop %ebx
0x40068056 : add xa2197,%ebx
0x4006805c : lea 0xc(%ebp),%eax
0x4006805f : push %eax
0x40068060 : pushl 0x8(%ebp)
0x40068063 : mov 0x81c(%ebx),%eax
0x40068069 : pushl (%eax)
0x4006806b : call 0x400325b4
0x40068070 : mov 0xfffffffc(%ebp),%ebx
0x40068073 : leave
0x40068074 : ret
End of assembler dump.
(gdb) x/8x 0x8049470
0x8049470 <_GLOBAL_OFFSET_TABLE_>: 0x08049490 0x40013ed0 0x4000a960 0x400fa550
0x8049480 <_GLOBAL_OFFSET_TABLE_+16>: 0x080482ee 0x400328cc 0x4006804c 0x00000000

GOT[6]已經被修正爲0x4006804c了

第一次調用printf()的時候需要經過①->②->③->④
以後調用printf()的時候就不需要這麼複雜了,只要經過⑴->⑵就可以了

link_map結構說明如下:
/* Structure describing a loaded shared object. The `l_next' and `l_prev'
members form a chain of all the shared objects loaded at startup.

These data structures exist in space used by the run-time dynamic linker;
modifying them may have disastrous results.

This data structure might change in future, if necessary. User-level
programs must avoid defining objects of this type. */


★★ glibc中動態解析符號的源代碼(glibc 2.1.3的實現)

.text
.globl _dl_runtime_resolve
.type _dl_runtime_resolve, @function
.align 16
_dl_runtime_resolve:
pushl %eax # Preserve registers otherwise clobbered.
pushl %ecx
pushl %edx
movl 16(%esp), %edx # Copy args pushed by PLT in register. Note
movl 12(%esp), %eax # that `fixup' takes its parameters in regs.
call fixup # Call resolver.
popl %edx # Get register content back.
popl %ecx
xchgl %eax, (%esp) # Get %eax contents end store function address.
ret # Jump to function address.

static ElfW(Addr) __attribute__ ((unused))
fixup (
# ifdef ELF_MACHINE_RUNTIME_FIXUP_ARGS
ELF_MACHINE_RUNTIME_FIXUP_ARGS,
# endif
struct link_map *l, ElfW(Word) reloc_offset)
{
const ElfW(Sym) *const symtab
= (const void *) l->l_info[DT_SYMTAB]->d_un.d_ptr;
const char *strtab = (const void *) l->l_info[DT_STRTAB]->d_un.d_ptr;

const PLTREL *const reloc /*計算函數重定位人口*/
= (const void *) (l->l_info[DT_JMPREL]->d_un.d_ptr + reloc_offset);

const ElfW(Sym) *sym = &symtab[ELFW(R_SYM) (reloc->r_info)];/*計算函數的符號表入口*/
void *const rel_addr = (void *)(l->l_addr + reloc->r_offset);/*GOT[n]的地址*/
ElfW(Addr) value;

/* The use of `alloca' here looks ridiculous but it helps. The goal is
to prevent the function from being inlined and thus optimized out.
There is no official way to do this so we use this trick. gcc never
inlines functions which use `alloca'. */
alloca (sizeof (int));

/* Sanity check that we're really looking at a PLT relocation. */
assert (ELFW(R_TYPE)(reloc->r_info) == ELF_MACHINE_JMP_SLOT);/*健壯性檢查*/

/* Look up the target symbol. */
switch (l->l_info[VERSYMIDX (DT_VERSYM)] != NULL)
{
default:
{
const ElfW(Half) *vernum =
(const void *) l->l_info[VERSYMIDX (DT_VERSYM)]->d_un.d_ptr;
ElfW(Half) ndx = vernum[ELFW(R_SYM) (reloc->r_info)];
const struct r_found_version *version = &l->l_versions[ndx];

if (version->hash != 0)
{
value = _dl_lookup_versioned_symbol(strtab + sym->st_name,
&sym, l->l_scope, l->l_name,
version, ELF_MACHINE_JMP_SLOT);
break;
}
}
case 0:
value = _dl_lookup_symbol (strtab + sym->st_name, &sym, l->l_scope,
l->l_name, ELF_MACHINE_JMP_SLOT);
}
/*此時value爲object裝載的基地址*/
/* Currently value contains the base load address of the object
that defines sym. Now add in the symbol offset. */

value = (sym ? value + sym->st_value : 0);/*在object中函數的絕對地址*/

/* And now perhaps the relocation addend. */
value = elf_machine_plt_value (l, reloc, value);/*可能還需要一下重定位*/

/* Finally, fix up the plt itself. */
elf_machine_fixup_plt (l, reloc, rel_addr, value);/*修正GOT[n]*/

return value;
}


static inline Elf32_Addr
elf_machine_plt_value (struct link_map *map, const Elf32_Rela *reloc,
Elf32_Addr value)
{
return value + reloc->r_addend;
}


/* Fixup a PLT entry to bounce directly to the function at VALUE. */
static inline void
elf_machine_fixup_plt (struct link_map *map, const Elf32_Rel *reloc,
Elf32_Addr *reloc_addr, Elf32_Addr value)
{
*reloc_addr = value;
}

 

xueyan 05-06-24 03:08

Linux 動態函式庫解析[轉]
在本文的這個部分,針對 Linux 系統是如何來辨別這些不同的可執行檔,以及整體的執行流程來作一個說明。
By Wing



  程序啓動的流程

  在 linux 的環境中最常見的可執行檔的種類包括了 Script 檔、Aout 格式的執行檔、ELF 格式的執行檔。在本文的這個部分,我會針對 Linux 系統是如何來辨別這些不同的可執行檔,以及整體的執行流程來作一個說明。

  我在此大略說明一下程序啓動的流程,當我們在 shell 中輸入指令時,會先去系統的路徑中來尋找是否有該可執行檔存在,如果找不到的話,就會顯示出找不到該可執行檔的訊息。如果找到的話,就會去呼叫 execve()來執行該檔案,接下來 execve() 會呼叫 System Call sys_execv(),這是在Linux 中 User Mode 透過 80 號中斷(int 80 ah=11)進入 Kernel Mode 所執行的第一個指令,之後在 Kernel 中陸續執行 do_exec()、 prepare_binprm()、read_exec()、search_binary_handler(),而在 search_binary_handler() 函式中,會逐一的去檢查目前所執行檔案的型態(看看是否爲Script File、aout 或 ELF 檔),不過 Linux 所採用的方式是透過各個檔案格式的處理程序來決定目前的執行檔所屬的處理程序。

  如下圖,會先去檢驗檔案是否爲 Script 檔,若是直進入 Script 檔的處理程序。若不是,則再進入 Aout 檔案格式的處理程序,若該執行檔爲 Aout 的檔案格式便交由 Aout檔案格式的處理程序來執行。如果仍然不是的話,便再進入 ELF 檔案格式的處理程序,如果都找不到的話,則傳回錯誤訊息。


  由這種執行的流程來看的話,如果 Linux Kernel 想要加入其他的執行檔格式的話,就要在 search_binary_handler() 加入新的執行檔的處理程序,這樣一旦新的執行檔格式產生後,在 Linux 下要執行時,因爲在do_load_script、do_load_aout_binary、do_load_elf_binary都會傳回錯誤,因此只 有我們自己的 do_load_xxxx_binary 函式可以正確的接手整個執行檔的處理流程,因此便可以達成新的檔案格式置入的動作哩。

  在函式 do_load_elf_binary () 執行時,首先會去檢視目前的檔案是否爲 ELF 格式,如下程序碼


if (elf_ex.e_ident[0] != 0x7f' '
strncmp(&elf_ex.e_ident[1], "ELF", 3) != 0)
goto out;



  便是去檢查該檔的前四個 bytes 是否爲 0x7f 加上 “ELF” (0x 45 0x4c 0x46),若非,則結束 do_load_elf_binary 的執行。之後,便是去檢視我們之前提過的 e_type 屬性,來得知是否爲 ET_EXEC(Executable File) 或是ET_DYN(Shared Object File) 這兩個值的其中之一


if (elf_ex.e_type != ET_EXEC && elf_ex.e_type != ET_DYN)
goto out;



  如果都不是這兩個值之一,便結束 do_load_elf_binary 的執行之後便是一連串讀取 ELF 檔表格的動作,在此就不多說,有興趣的讀者可以自行參閱/usr/src/linux/fs/binfmt_elf.c 的內容即可。

  在此我們檢視一個執行檔由啓動到結束的完整流程,首先這個執行檔具有如下的程序碼


#include
int main()
{
printf(" test ");
}



  然後,透過如下的編程過程


gcc test.c ˉo test



  我們如果檢視執行檔的 ELF Header 可以得知它主要呼叫了 /lib/libc.so.6函式庫中以下的函式


printf
__deregister_frame_info
__libc_start_main
__register_frame_info



  接下來,我們便把程序的執行流程大略整理如下,而 execve("./test", ["./test"], []) 執行的流程,就是剛剛我們所提到的內容,若不熟悉的讀者,可以再回頭看看剛剛的內容,即可對 execve("./test", ["./test"], []) 的執行流程有大略的瞭解。在這裏,我們會把整個執行流程更完整的來檢視一遍。

  首先,我們所在的執行環境會透過 execve("./test", ["./test"], []) 的函式呼叫來啓動 test 執行檔。

  呼叫 open("/etc/ld.so.cache", O_RDONLY),以唯讀模式開啓 ld.so.cache,這個檔案的功能是作爲動態函式庫的快取,它會記錄了目前系統中所存在的動態函式庫的資訊以及這些函式庫所存在的位置。所以說,如 果我們在系統中安裝了新的函式庫時,我們便需要去更新這個檔案的內容,以使新的函式庫可以在我們的 Linux 環境中發生作用,我們可以透過 ldconfig 這個指令來更新 ld.so.cache 的內容。

  呼叫 mmap(0, 9937, PROT_READ, MAP_PRIVATE, 3, 0),把 ld.so.cache 檔案映射到記憶體中,mmap 函式的宣告爲 mmap(void *start, size_t length, int prot , int flags, int fd, off_t offset),在筆者的電腦上 ld.so.cache 的檔案大小爲 9937 bytes,PROT_READ代表這塊記憶體位置是可讀取的,MAP_PRIVATE 則表示產生一個行程私有的 copy-on-write 映射,因此這個呼叫會把整個 ld.so.cache 檔案映射到記憶體中,在筆者電腦上所傳回的映射記憶體起始位置爲 0x40013000。

  注: mmap(void *start, size_t length, int prot , int flags, int fd, off_t offset)代表我們要求在檔案 fd中,起始位置爲offset去映射 length 長度的資料,到記憶體位置 start ,而 prot 是用來描述該記憶體位置的保護權限(例如:讀、寫、執行),flags用來定義所映射物件的型態,例如這塊記憶體是否允許多個 Process 同時映射到,也就是說一旦有一個 Process 更改了這個記憶體空間,那所有映射到這塊記憶體的Process 都會受到影響,或是 flag 設定爲 Process 私有的記憶體映射,這樣就會透過 copy-on-write 的機制,當這塊記憶體被別的 Process 修改後,會自動配置實體的記憶體位置,讓其他的 Process 所映射到的記憶體內容與原本的相同。(有關mmap的其它應用,可參考本文最後的注一)

  呼叫 open("/lib/libc.so.6", O_RDONLY),開啓 libc.so.6。

  呼叫 read(3, "177ELF111331250202"..., 4096) 讀取libc.so.6的檔頭。

  呼叫 mmap(0, 993500, PROT_READ|PROT_EXEC, MAP_PRIVATE, 3, 0),把 libc.so.6 映射到記憶體中,由檔頭開始映射 993500 bytes,若是使用 RedHat 6.1(或其它版本的 RedHat)的讀者或許會好奇 libc.so.6 所 link 到的檔案 libc-2.1.2.so 大小不是 4118715 bytes 嗎? 其實原本 RedHat 所附的 libc.so.6 動態函式庫是沒有經過 strip 過的,如果經過 strip 後,大小會變爲 1052428 bytes,而 libc.so.6 由檔頭開始在 993500 bytes 之後都是一些版本的資訊,筆者猜想應該是這樣的原因,所以在映射檔時,並沒有把整個 libc.so.6 檔案映射到記憶體中,只映射前面有意義的部分。與映射 ld.so.cache 不同的是,除了 PROT_READ 屬性之外,libc.so.6 的屬性還多了PROT_EXEC,這代表了所映射的這塊記憶體是可讀可執行的。在筆者的電腦中,libc.so.6 所映射到的記憶體起始位置爲 0x40016000。

  呼叫 mprotect(0x40101000, 30940, PROT_NONE),用來設定記憶體的使用權限,而 PROT_NONE 屬性是代表這塊記憶體區間(0x40101000—0x401088DC)是不能讀取、寫入與執行的。

  呼叫 mmap(0x40101000, 16384, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED, 3, 0xea000),映射 libc.so.6 由起始位置 0xea000 映射 16384bytes 到記憶體位置 0x40101000。

  呼叫 mmap(0x40105000, 14556, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_ANONYMOUS, -1, 0),MAP_ANONYMOUS 表示沒有檔案被映射,且產生一個初始值全爲 0 的記憶體區塊。

  呼叫 munmap(0x40013000, 9937),把原本映射到 ld.so.cache 的記憶體解除映射(此時已把執行檔所需的動態函式庫都映射到記憶體中了)。

  呼叫 personality(0),可以設定目前 Process 的執行區間(execution domain),換個說法就是 Linux 支援了多個執行區間,而我們所設定的執行區間會告訴 Linux 如何去映射我們的訊息號碼(signal numbers)到各個不同的訊息動作(signal actions)中。這執行區間的功能,允許 Linux 對其它 Unix-Like 的操作系統,提供有限度的二進位檔支援。如這個例子中,personality(0) 的參數爲 0,就是指定爲 PER_LINUX 的執行區間(execution domain)。


#define PER_MASK (0x00ff)
#define PER_LINUX (0x0000)
#define PER_LINUX_32BIT (0x0000 | ADDR_LIMIT_32BIT)
#define PER_SVR4 (0x0001 | STICKY_TIMEOUTS)
#define PER_SVR3 (0x0002 | STICKY_TIMEOUTS)
#define PER_SCOSVR3 (0x0003 | STICKY_TIMEOUTS | WHOLE_SECONDS)
#define PER_WYSEV386 (0x0004 | STICKY_TIMEOUTS)
#define PER_ISCR4 (0x0005 | STICKY_TIMEOUTS)
#define PER_BSD (0x0006)
#define PER_XENIX (0x0007 | STICKY_TIMEOUTS)
#define PER_LINUX32 (0x0008)
#define PER_IRIX32 (0x0009 | STICKY_TIMEOUTS) /* IRIX5 32-bit */
#define PER_IRIXN32 (0x000a | STICKY_TIMEOUTS) /* IRIX6 new 32-bit */
#define PER_IRIX64 (0x000b | STICKY_TIMEOUTS) /* IRIX6 64-bit */



  呼叫 getpid(),取得目前 Process 的 Process ID。

  呼叫 mmap(0, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0),傳回值爲 0x400130,MAP_ANONYMOUS 表示沒有檔案被映射,且產生一個初始值全爲 0 的記憶體區塊。

  呼叫 write(1, " test ", 6),顯示字串在畫面上。

  呼叫 munmap(0x40013000, 4096),解除記憶體位置0x40013000的記憶體映射。

  呼叫 _exit(6),結束程序執行。

  在這段所舉的例子,只用到了一個函式庫 libc.so.6,我們可以舉像是 RedHat 中 Telnet 指令爲例,首先檢視他的 ELF Header


==>libncurses.so.4
tgetent
==>libc.so.6
strcpy
ioctl
printf
cfgetospeed
recv
connect
............┅
sigsetmask
__register_frame_info
close
free



  它主要呼叫了函式庫 libncurses.so.4 的函式 tgetent,以及函式庫 libc.so.6 中爲數不少的函式,當然我們也可以去檢視它執行的流程,與之前只呼叫了 libc.so.6 的printf 函式來比較,我們可以發現它主要的不同就是去載入了 libncurses.so.4


open("/usr/lib/libncurses.so.4", O_RDONLY) ;
fstat(3, {st_mode=S_IFREG|0755, st_size=274985, ...}) ;
read(3, "177ELF111331340335"..., 4096) ;
mmap(0, 254540, PROT_READ|PROT_EXEC, MAP_PRIVATE, 3, 0);
mprotect(0x40048000, 49740, PROT_NONE);
mmap(0x40048000, 36864, PROT_READ|PROT_WRITE,
MAP_PRIVATE|MAP_FIXED, 3, 0x31000);
mmap(0x40051000, 12876, PROT_READ|PROT_WRITE,
MAP_PRIVATE|MAP_FIXED|MAP_ANONYMOUS, -1, 0) ;
close(3);

 

xueyan 05-06-24 03:09

Before main() 分析[轉]


本文分析了在main()之前的ELF程序流程,試圖讓您更清楚的把握程序的流程的脈絡走向。
從而更深入的瞭解ELF。不正確之處,還請斧正。



★ 綜述

ELF的可執行文件與共享庫在結構上非常類似,它們具有一張程序段表,用來描述這些段如何映射到進程空間.
對於可執行文件來說,段的加載位置是固定的,程序段表中如實反映了段的加載地址.對於共享庫來說,段的加
載位置是浮動的,位置無關的,程序段表反映的是以0作爲基準地址的相對加載地址.儘管共享庫的連接是不
充分的,爲了便於測試動態鏈接器,Linux允許直接加載共享庫運行.如果應用程序具有動態鏈接器的描述段,
內核在完成程序段加載後,緊接着加載動態鏈接器,並且啓動動態鏈接器的入口.如果沒有動態鏈接器的描述段,
就直接交給用戶程序入口。
上述這部分請參考:linuxforum論壇上opera寫的《分析ELF的加載過程》

在控制權交給動態鏈接器的入口後,首先調用_dl_start函數獲得真實的程序入口(注:該入口地址
不是main的地址,也就是說一般程序的入口不是main),然後循環調用每個共享object的初始化函數,
接着跳轉到真實的程序入口,一般爲_start(程序中的_start)的一個例程,該例程壓入一些參數到堆棧,
就直接調用__libc_start_main函數。在__libc_start_main函數中替動態連接器和自己程序安排
destructor,並運行程序的初始化函數。然後才把控制權交給main()函數。



★ main()之前流程

下面就是動態鏈接器的入口。
/* Initial entry point code for the dynamic linker.
The C function `_dl_start' is the real entry point;
its return value is the user program's entry point. */

#define RTLD_START asm ("/
.text/n/
.globl _start/n/
.globl _dl_start_user/n/
_start:/n/
pushl %esp/n/
call _dl_start/n//*該函數返回時候,%eax中存放着user entry point address*/
popl %ebx/n//*%ebx放着是esp的內容*/
_dl_start_user:/n/
# Save the user entry point address in %edi./n/
movl %eax, %edi/n//*入口地址放在%edi*/

# Point %ebx at the GOT.
call 0f/n/
0: popl %ebx/n/
addl $_GLOBAL_OFFSET_TABLE_+[.-0b], %ebx/n/

# Store the highest stack address/n/
movl __libc_stack_end@GOT(%ebx), %eax/n/
movl %esp, (%eax)/n//*把棧頂%esp放到GOT的__libc_stack_end中*/

# See if we were run as a command with the executable file/n/
# name as an extra leading argument./n/
movl _dl_skip_args@GOT(%ebx), %eax/n/
movl (%eax), %eax/n/

# Pop the original argument count./n/
popl %ecx/n/

# Subtract _dl_skip_args from it./n/
subl %eax, %ecx/n/

# Adjust the stack pointer to skip _dl_skip_args words./n/
leal (%esp,%eax,4), %esp/n/

# Push back the modified argument count./n/
pushl %ecx/n/

# Push the searchlist of the main object as argument in/n/
# _dl_init_next call below./n/
movl _dl_main_searchlist@GOT(%ebx), %eax/n/
movl (%eax), %esi/n/
0: movl %esi,%eax/n/

# Call _dl_init_next to return the address of an initializer/n/
# function to run./n/
call _dl_init_next@PLT/n//*該函數返回初始化函數的地址,返回地址放在%eax中*/

# Check for zero return, when out of initializers./n/
testl %eax, %eax/n/
jz 1f/n/

# Call the shared object initializer function./n/
# NOTE: We depend only on the registers (%ebx, %esi and %edi)/n/
# and the return address pushed by this call;/n/
# the initializer is called with the stack just/n/
# as it appears on entry, and it is free to move/n/
# the stack around, as long as it winds up jumping to/n/
# the return address on the top of the stack./n/
call *%eax/n//*調用共享object初始化函數*/

# Loop to call _dl_init_next for the next initializer./n/
jmp 0b/n/

1: # Clear the startup flag./n/
movl _dl_starting_up@GOT(%ebx), %eax/n/
movl , (%eax)/n/

# Pass our finalizer function to the user in %edx, as per ELF ABI./n/
movl _dl_fini@GOT(%ebx), %edx/n/

# Jump to the user's entry point./n/
jmp *%edi/n/
.previous/n/
");


sysdeps/i386/start.s中
user's entry也就是下面的_start例程

/* This is the canonical entry point, usually the first thing in the text
segment. The SVR4/i386 ABI (pages 3-31, 3-32) says that when the entry
point runs, most registers' values are unspecified, except for:

%edx Contains a function pointer to be registered with `atexit'.
This is how the dynamic linker arranges to have DT_FINI
functions called for shared libraries that have been loaded
before this code runs.

%esp The stack contains the arguments and environment:
0(%esp) argc
4(%esp) argv[0]
...
(4*argc)(%esp) NULL
(4*(argc+1))(%esp) envp[0]
...
NULL
*/

.text
.globl _start
_start:
/* Clear the frame pointer. The ABI suggests this be done, to mark
the outermost frame obviously. */
xorl %ebp, %ebp

/* Extract the arguments as encoded on the stack and set up
the arguments for `main': argc, argv. envp will be determined
later in __libc_start_main. */
popl %esi /* Pop the argument count. */
movl %esp, %ecx /* argv starts just at the current stack top.*/

/* Before pushing the arguments align the stack to a double word
boundary to avoid penalties from misaligned accesses. Thanks
to Edward Seidl <[email protected]> for pointing this out. */
andl xfffffff8, %esp
pushl %eax /* Push garbage because we allocate
28 more bytes. */

/* Provide the highest stack address to the user code (for stacks
which grow downwards). */
pushl %esp

pushl %edx /* Push address of the shared library
termination function. */

/* Push address of our own entry points to .fini and .init. */
pushl $_fini
pushl $_init

pushl %ecx /* Push second argument: argv. */
pushl %esi /* Push first argument: argc. */

pushl $main

/* Call the user's main function, and exit with its value.
But let the libc call main. */
call __libc_start_main

hlt /* Crash if somehow `exit' does return. */



__libc_start_main在sysdeps/generic/libc_start.c中
假設定義的是PIC的代碼。
struct startup_info
{
void *sda_base;
int (*main) (int, char **, char **, void *);
int (*init) (int, char **, char **, void *);
void (*fini) (void);
};

int
__libc_start_main (int argc, char **argv, char **envp,
void *auxvec, void (*rtld_fini) (void),
struct startup_info *stinfo,
char **stack_on_entry)
{

/* the PPC SVR4 ABI says that the top thing on the stack will
be a NULL pointer, so if not we assume that we're being called
as a statically-linked program by Linux... */
if (*stack_on_entry != NULL)
{
/* ...in which case, we have argc as the top thing on the
stack, followed by argv (NULL-terminated), envp (likewise),
and the auxilary vector. */
argc = *(int *) stack_on_entry;
argv = stack_on_entry + 1;
envp = argv + argc + 1;
auxvec = envp;
while (*(char **) auxvec != NULL)
++auxvec;
++auxvec;
rtld_fini = NULL;
}

/* Store something that has some relationship to the end of the
stack, for backtraces. This variable should be thread-specific. */
__libc_stack_end = stack_on_entry + 4;

/* Set the global _environ variable correctly. */
__environ = envp;

/* Register the destructor of the dynamic linker if there is any. */
if (rtld_fini != NULL)
atexit (rtld_fini);/*替動態連接器安排destructor*/

/* Call the initializer of the libc. */

__libc_init_first (argc, argv, envp);/*一個空函數*/

/* Register the destructor of the program, if any. */
if (stinfo->fini)
atexit (stinfo->fini);/*安排程序自己的destructor*/

/* Call the initializer of the program, if any. */

/*運行程序的初始化函數*/
if (stinfo->init)
stinfo->init (argc, argv, __environ, auxvec);

/*運行程序main函數,到此,控制權才交給我們一般所說的程序入口*/
exit (stinfo->main (argc, argv, __environ, auxvec));

}



void
__libc_init_first (int argc __attribute__ ((unused)), ...)
{
}

int
atexit (void (*func) (void))
{
struct exit_function *new = __new_exitfn ();

if (new == NULL)
return -1;

new->flavor = ef_at;
new->func.at = func;
return 0;
}


/* Run initializers for MAP and its dependencies, in inverse dependency
order (that is, leaf nodes first). */

ElfW(Addr)
internal_function
_dl_init_next (struct r_scope_elem *searchlist)
{
unsigned int i;

/* The search list for symbol lookup is a flat list in top-down
dependency order, so processing that list from back to front gets us
breadth-first leaf-to-root order. */

i = searchlist->r_nlist;
while (i-- > 0)
{
struct link_map *l = searchlist->r_list[i];

if (l->l_init_called)
/* This object is all done. */
continue;

if (l->l_init_running)
{
/* This object's initializer was just running.
Now mark it as having run, so this object
will be skipped in the future. */
l->l_init_running = 0;
l->l_init_called = 1;
continue;
}

if (l->l_info[DT_INIT]
&& (l->l_name[0] != '' || l->l_type != lt_executable))
{
/* Run this object's initializer. */
l->l_init_running = 1;

/* Print a debug message if wanted. */
if (_dl_debug_impcalls)
_dl_debug_message (1, "/ncalling init: ",
l->l_name[0] ? l->l_name : _dl_argv[0],
"/n/n", NULL);

/*共享庫的基地址+init在基地址中的偏移量*/
return l->l_addr + l->l_info[DT_INIT]->d_un.d_ptr;

}

/* No initializer for this object.
Mark it so we will skip it in the future. */
l->l_init_called = 1;
}


/* Notify the debugger all new objects are now ready to go. */
_r_debug.r_state = RT_CONSISTENT;
_dl_debug_state ();

return 0;
}
在main()之前的程序流程看試有點簡單,但正在運行的時候還是比較複雜的
(自己用GBD跟蹤下就知道了),因爲一般的程序都需要涉及到PLT,GOT標號的
重定位。弄清楚這個對ELF由爲重要,以後有機會再補上一篇吧。


★ 手動確定程序和動態連接器的入口

[alert7@redhat62 alert7]$ cat helo.c
#include <stdio.h>
int main(int argc,char **argv)
{
printf("hello/n");
return 0;
}

[alert7@redhat62 alert7]$ gcc -o helo helo.c
[alert7@redhat62 alert7]$ readelf -h helo
ELF Header:
Magic: 7f 45 4c 46 01 01 01 00 00 00 00 00 00 00 00 00
Class: ELF32
Data: 2's complement, little endian
Version: 1 (current)
OS/ABI: UNIX - System V
ABI Version: 0
Type: EXEC (Executable file)
Machine: Intel 80386
Version: 0x1
Entry point address: 0x8048320
Start of program headers: 52 (bytes into file)
Start of section headers: 8848 (bytes into file)
Flags: 0x0
Size of this header: 52 (bytes)
Size of program headers: 32 (bytes)
Number of program headers: 6
Size of section headers: 40 (bytes)
Number of section headers: 29
Section header string table index: 26
在這裏我們看到程序的入口爲0x8048320,可以看看是否爲main函數。

[alert7@redhat62 alert7]$ gdb -q helo
(gdb) disass 0x8048320
Dump of assembler code for function _start:
0x8048320 <_start>: xor %ebp,%ebp
0x8048322 <_start+2>: pop %esi
0x8048323 <_start+3>: mov %esp,%ecx
0x8048325 <_start+5>: and xfffffff8,%esp
0x8048328 <_start+8>: push %eax
0x8048329 <_start+9>: push %esp
0x804832a <_start+10>: push %edx
0x804832b <_start+11>: push x804841c
0x8048330 <_start+16>: push x8048298
0x8048335 <_start+21>: push %ecx
0x8048336 <_start+22>: push %esi
0x8048337 <_start+23>: push x80483d0
0x804833c <_start+28>: call 0x80482f8 <__libc_start_main>
0x8048341 <_start+33>: hlt
0x8048342 <_start+34>: nop
End of assembler dump.
呵呵,不是main吧,程序的入口是個_start例程。

再來看動態連接器的入口是多少
[alert7@redhat62 alert7]$ ldd helo
libc.so.6 => /lib/libc.so.6 (0x40018000)
/lib/ld-linux.so.2 => /lib/ld-linux.so.2 (0x40000000)
動態連接器ld-linux.so.2加載到進程地址空間0x40000000。

[alert7@redhat62 alert7]$ readelf -h /lib/ld-linux.so.2
ELF Header:
Magic: 7f 45 4c 46 01 01 01 00 00 00 00 00 00 00 00 00
Class: ELF32
Data: 2's complement, little endian
Version: 1 (current)
OS/ABI: UNIX - System V
ABI Version: 0
Type: DYN (Shared object file)
Machine: Intel 80386
Version: 0x1
Entry point address: 0x1990
Start of program headers: 52 (bytes into file)
Start of section headers: 328916 (bytes into file)
Flags: 0x0
Size of this header: 52 (bytes)
Size of program headers: 32 (bytes)
Number of program headers: 3
Size of section headers: 40 (bytes)
Number of section headers: 23
Section header string table index: 20
共享object入口地址爲0x1990。加上整個ld-linux.so.2被加載到進程地址空間0x40000000。
那麼動態連接器的入口地址爲0x1990+0x40000000=0x40001990。

用戶空間執行的第一條指令地址就是0x40001990,既上面#define RTLD_START的開始。

 

xueyan 05-06-24 03:10

UNIX/LINUX 平臺可執行文件格式分析[轉】
本文討論了 UNIX/LINUX 平臺下三種主要的可執行文件格式:a.out(assembler and link editor output 彙編器和鏈接編輯器的輸出)、COFF(Common Object File Format 通用對象文件格式)、ELF(Executable and Linking Format 可執行和鏈接格式)。首先是對可執行文件格式的一個綜述,並通過描述 ELF 文件加載過程以揭示可執行文件內容與加載運行操作之間的關係。隨後依此討論了此三種文件格式,並着重討論 ELF 文件的動態連接機制,其間也穿插了對各種文件格式優缺點的評價。最後對三種可執行文件格式有一個簡單總結,並提出作者對可文件格式評價的一些感想。



UNIX/LINUX 平臺可執行文件格式分析
stack 發表於 2005-4-17 22:03:00
轉載自:IBM developerWorks 中國網站

本文討論了 UNIX/LINUX 平臺下三種主要的可執行文件格式:a.out(assembler and link editor output 彙編器和鏈接編輯器的輸出)、COFF(Common Object File Format 通用對象文件格式)、ELF(Executable and Linking Format 可執行和鏈接格式)。首先是對可執行文件格式的一個綜述,並通過描述 ELF 文件加載過程以揭示可執行文件內容與加載運行操作之間的關係。隨後依此討論了此三種文件格式,並着重討論 ELF 文件的動態連接機制,其間也穿插了對各種文件格式優缺點的評價。最後對三種可執行文件格式有一個簡單總結,並提出作者對可文件格式評價的一些感想。
可執行文件格式綜述
相對於其它文件類型,可執行文件可能是一個操作系統中最重要的文件類型,因爲它們是完成操作的真正執行者。可執行文件的大小、運行速度、資源佔用情況以及 可擴展性、可移植性等與文件格式的定義和文件加載過程緊密相關。研究可執行文件的格式對編寫高性能程序和一些黑客技術的運用都是非常有意義的。

不管何種可執行文件格式,一些基本的要素是必須的,顯而易見的,文件中應包含代碼和數據。因爲文件可能引用外部文件定義的符號(變量和函數),因此重定位 信息和符號信息也是需要的。一些輔助信息是可選的,如調試信息、硬件信息等。基本上任意一種可執行文件格式都是按區間保存上述信息,稱爲段 (Segment)或節(Section)。不同的文件格式中段和節的含義可能有細微區別,但根據上下文關係可以很清楚的理解,這不是關鍵問題。最後,可 執行文件通常都有一個文件頭部以描述本文件的總體結構。

相對可執行文件有三個重要的概念:編譯(compile)、連接(link,也可稱爲鏈接、聯接)、加載(load)。源程序文件被編譯成目標文件,多個 目標文件被連接成一個最終的可執行文件,可執行文件被加載到內存中運行。因爲本文重點是討論可執行文件格式,因此加載過程也相對重點討論。下面是 LINUX平臺下ELF文件加載過程的一個簡單描述。

1:內核首先讀ELF文件的頭部,然後根據頭部的數據指示分別讀入各種數據結構,找到標記爲可加載(loadable)的段,並調用函數 mmap()把段內容加載到內存中。在加載之前,內核把段的標記直接傳遞給 mmap(),段的標記指示該段在內存中是否可讀、可寫,可執行。顯然,文本段是隻讀可執行,而數據段是可讀可寫。這種方式是利用了現代操作系統和處理器 對內存的保護功能。著名的Shellcode(參考資料 17)的編寫技巧則是突破此保護功能的一個實際例子。

2:內核分析出ELF文件標記爲 PT_INTERP 的段中所對應的動態連接器名稱,並加載動態連接器。現代 LINUX 系統的動態連接器通常是 /lib/ld-linux.so.2,相關細節在後面有詳細描述。

3:內核在新進程的堆棧中設置一些標記-值對,以指示動態連接器的相關操作。

4:內核把控制傳遞給動態連接器。

5:動態連接器檢查程序對外部文件(共享庫)的依賴性,並在需要時對其進行加載。

6:動態連接器對程序的外部引用進行重定位,通俗的講,就是告訴程序其引用的外部變量/函數的地址,此地址位於共享庫被加載在內存的區間內。動態連接還有一個延遲(Lazy)定位的特性,即只在"真正"需要引用符號時才重定位,這對提高程序運行效率有極大幫助。

7:動態連接器執行在ELF文件中標記爲 .init 的節的代碼,進行程序運行的初始化。在早期系統中,初始化代碼對應函數 _init(void)(函數名強制固定),在現代系統中,則對應形式爲

void
__attribute((constructor))
init_function(void)
{
……
}




其中函數名爲任意。

8:動態連接器把控制傳遞給程序,從 ELF 文件頭部中定義的程序進入點開始執行。在 a.out 格式和ELF格式中,程序進入點的值是顯式存在的,在 COFF 格式中則是由規範隱含定義。

從上面的描述可以看出,加載文件最重要的是完成兩件事情:加載程序段和數據段到內存;進行外部定義符號的重定位。重定位是程序連接中一個重要概念。我們知 道,一個可執行程序通常是由一個含有 main() 的主程序文件、若干目標文件、若干共享庫(Shared Libraries)組成。(注:採用一些特別的技巧,也可編寫沒有 main 函數的程序,請參閱參考資料 2)一個 C 程序可能引用共享庫定義的變量或函數,換句話說就是程序運行時必須知道這些變量/函數的地址。在靜態連接中,程序所有需要使用的外部定義都完全包含在可執 行程序中,而動態連接則只在可執行文件中設置相關外部定義的一些引用信息,真正的重定位是在程序運行之時。靜態連接方式有兩個大問題:如果庫中變量或函數 有任何變化都必須重新編譯連接程序;如果多個程序引用同樣的變量/函數,則此變量/函數會在文件/內存中出現多次,浪費硬盤/內存空間。比較兩種連接方式 生成的可執行文件的大小,可以看出有明顯的區別。

a.out 文件格式分析
a.out 格式在不同的機器平臺和不同的 UNIX 操作系統上有輕微的不同,例如在 MC680x0 平臺上有 6 個 section。下面我們討論的是最"標準"的格式。

a.out 文件包含 7 個 section,格式如下:

exec header(執行頭部,也可理解爲文件頭部)
text segment(文本段)
data segment(數據段)
text relocations(文本重定位段)
data relocations(數據重定位段)
symbol table(符號表)
string table(字符串表)

執行頭部的數據結構:

struct exec {
unsigned long a_midmag; /* 魔數和其它信息 */
unsigned long a_text; /* 文本段的長度 */
unsigned long a_data; /* 數據段的長度 */
unsigned long a_bss; /* BSS段的長度 */
unsigned long a_syms; /* 符號表的長度 */
unsigned long a_entry; /* 程序進入點 */
unsigned long a_trsize; /* 文本重定位表的長度 */
unsigned long a_drsize; /* 數據重定位表的長度 */
};




文件頭部主要描述了各個 section 的長度,比較重要的字段是 a_entry(程序進入點),代表了系統在加載程序並初試化各種環境後開始執行程序代碼的入口。這個字段在後面討論的 ELF 文件頭部中也有出現。由 a.out 格式和頭部數據結構我們可以看出,a.out 的格式非常緊湊,只包含了程序運行所必須的信息(文本、數據、BSS),而且每個 section 的順序是固定的。這種結構缺乏擴展性,如不能包含"現代"可執行文件中常見的調試信息,最初的 UNIX 黑客對 a.out 文件調試使用的工具是 adb,而 adb 是一種機器語言調試器!

a.out 文件中包含符號表和兩個重定位表,這三個表的內容在連接目標文件以生成可執行文件時起作用。在最終可執行的 a.out 文件中,這三個表的長度都爲 0。a.out 文件在連接時就把所有外部定義包含在可執行程序中,如果從程序設計的角度來看,這是一種硬編碼方式,或者可稱爲模塊之間是強藕和的。在後面的討論中,我們 將會具體看到ELF格式和動態連接機制是如何對此進行改進的。

a.out 是早期UNIX系統使用的可執行文件格式,由 AT&T 設計,現在基本上已被 ELF 文件格式代替。a.out 的設計比較簡單,但其設計思想明顯的被後續的可執行文件格式所繼承和發揚。可以參閱參考資料 16 和閱讀參考資料 15 源代碼加深對 a.out 格式的理解。參考資料 12 討論瞭如何在"現代"的紅帽LINUX運行 a.out 格式文件。

COFF 文件格式分析
COFF 格式比 a.out 格式要複雜一些,最重要的是包含一個節段表(section table),因此除了 .text,.data,和 .bss 區段以外,還可以包含其它的區段。另外也多了一個可選的頭部,不同的操作系統可一對此頭部做特定的定義。

COFF 文件格式如下:

File Header(文件頭部)
Optional Header(可選文件頭部)
Section 1 Header(節頭部)
………
Section n Header(節頭部)
Raw Data for Section 1(節數據)
Raw Data for Section n(節數據)
Relocation Info for Sect. 1(節重定位數據)
Relocation Info for Sect. n(節重定位數據)
Line Numbers for Sect. 1(節行號數據)
Line Numbers for Sect. n(節行號數據)
Symbol table(符號表)
String table(字符串表)

文件頭部的數據結構:

struct filehdr
{
unsigned short f_magic; /* 魔數 */
unsigned short f_nscns; /* 節個數 */
long f_timdat; /* 文件建立時間 */
long f_symptr; /* 符號表相對文件的偏移量 */
long f_nsyms; /* 符號表條目個數 */
unsigned short f_opthdr; /* 可選頭部長度 */
unsigned short f_flags; /* 標誌 */
};




COFF 文件頭部中魔數與其它兩種格式的意義不太一樣,它是表示針對的機器類型,例如 0x014c 相對於 I386 平臺,而 0x268 相對於 Motorola 68000系列等。當 COFF 文件爲可執行文件時,字段 f_flags 的值爲 F_EXEC(0X00002),同時也表示此文件沒有未解析的符號,換句話說,也就是重定位在連接時就已經完成。由此也可以看出,原始的 COFF 格式不支持動態連接。爲了解決這個問題以及增加一些新的特性,一些操作系統對 COFF 格式進行了擴展。Microsoft 設計了名爲 PE(Portable Executable)的文件格式,主要擴展是在 COFF 文件頭部之上增加了一些專用頭部,具體細節請參閱參考資料 18,某些 UNIX 系統也對 COFF 格式進行了擴展,如 XCOFF(extended common object file format)格式,支持動態連接,請參閱參考資料 5。

緊接文件頭部的是可選頭部,COFF 文件格式規範中規定可選頭部的長度可以爲 0,但在 LINUX 系統下可選頭部是必須存在的。下面是 LINUX 下可選頭部的數據結構:

typedef struct
{
char magic[2]; /* 魔數 */
char vstamp[2]; /* 版本號 */
char tsize[4]; /* 文本段長度 */
char dsize[4]; /* 已初始化數據段長度 */
char bsize[4]; /* 未初始化數據段長度 */
char entry[4]; /* 程序進入點 */
char text_start[4]; /* 文本段基地址 */
char data_start[4]; /* 數據段基地址 */
}
COFF_AOUTHDR;




字段 magic 爲 0413 時表示 COFF 文件是可執行的,注意到可選頭部中顯式定義了程序進入點,標準的 COFF 文件沒有明確的定義程序進入點的值,通常是從 .text 節開始執行,但這種設計並不好。

前面我們提到,COFF 格式比 a.out 格式多了一個節段表,一個節頭條目描述一個節數據的細節,因此 COFF 格式能包含更多的節,或者說可以根據實際需要,增加特定的節,具體表現在 COFF 格式本身的定義以及稍早提及的 COFF 格式擴展。我個人認爲,節段表的出現可能是 COFF 格式相對 a.out 格式最大的進步。下面我們將簡單描述 COFF 文件中節的數據結構,因爲節的意義更多體現在程序的編譯和連接上,所以本文不對其做更多的描述。此外,ELF 格式和 COFF格式對節的定義非常相似,在隨後的 ELF 格式分析中,我們將省略相關討論。

struct COFF_scnhdr
{
char s_name[8]; /* 節名稱 */
char s_paddr[4]; /* 物理地址 */
char s_vaddr[4]; /* 虛擬地址 */
char s_size[4]; /* 節長度 */
char s_scnptr[4]; /* 節數據相對文件的偏移量 */
char s_relptr[4]; /* 節重定位信息偏移量 */
char s_lnnoptr[4]; /* 節行信息偏移量 */
char s_nreloc[2]; /* 節重定位條目數 */
char s_nlnno[2]; /* 節行信息條目數 */
char s_flags[4]; /* 段標記 */
};




有一點需要注意:LINUX系統中頭文件coff.h中對字段s_paddr的註釋是"physical address",但似乎應該理解爲"節被加載到內存中所佔用的空間長度"。字段s_flags標記該節的類型,如文本段、數據段、BSS段等。在 COFF的節中也出現了行信息,行信息描述了二進制代碼與源代碼的行號之間的對映關係,在調試時很有用。

參考資料 19是一份對 COFF格式詳細描述的中文資料,更詳細的內容請參閱參考資料 20。

ELF文件格式分析
ELF 文件有三種類型:可重定位文件:也就是通常稱的目標文件,後綴爲.o。共享文件:也就是通常稱的庫文件,後綴爲.so。可執行文件:本文主要討論的文件格 式,總的來說,可執行文件的格式與上述兩種文件的格式之間的區別主要在於觀察的角度不同:一種稱爲連接視圖(Linking View),一種稱爲執行視圖(Execution View)。

首先看看ELF文件的總體佈局:

ELF header(ELF頭部)
Program header table(程序頭表)
Segment1(段1)
Segment2(段2)
………
Sengmentn(段n)
Setion header table(節頭表,可選)

段由若干個節(Section)構成,節頭表對每一個節的信息有相關描述。對可執行程序而言,節頭表是可選的。參考資料 1中作者談到把節頭表的所有數據全部設置爲0,程序也能正確運行!ELF頭部是一個關於本文件的路線圖(road map),從總體上描述文件的結構。下面是ELF頭部的數據結構:

typedef struct
{
unsigned char e_ident[EI_NIDENT]; /* 魔數和相關信息 */
Elf32_Half e_type; /* 目標文件類型 */
Elf32_Half e_machine; /* 硬件體系 */
Elf32_Word e_version; /* 目標文件版本 */
Elf32_Addr e_entry; /* 程序進入點 */
Elf32_Off e_phoff; /* 程序頭部偏移量 */
Elf32_Off e_shoff; /* 節頭部偏移量 */
Elf32_Word e_flags; /* 處理器特定標誌 */
Elf32_Half e_ehsize; /* ELF頭部長度 */
Elf32_Half e_phentsize; /* 程序頭部中一個條目的長度 */
Elf32_Half e_phnum; /* 程序頭部條目個數 */
Elf32_Half e_shentsize; /* 節頭部中一個條目的長度 */
Elf32_Half e_shnum; /* 節頭部條目個數 */
Elf32_Half e_shstrndx; /* 節頭部字符表索引 */
} Elf32_Ehdr;




下面我們對ELF頭表中一些重要的字段作出相關說明,完整的ELF定義請參閱參考資料 6和參考資料7。

e_ident [0]-e_ident[3]包含了ELF文件的魔數,依次是0x7f、'E'、'L'、'F'。注意,任何一個ELF 文件必須包含此魔數。參考資料 3中討論了利用程序、工具、/Proc文件系統等多種查看ELF魔數的方法。e_ident[4]表示硬件系統的位數,1代表32位,2代表64位。 e_ident[5] 表示數據編碼方式,1代表小印第安排序(最大有意義的字節佔有最低的地址),2代表大印第安排序(最大有意義的字節佔有最高的地址)。e_ident [6]指定ELF頭部的版本,當前必須爲1。e_ident[7]到e_ident[14]是填充符,通常是0。ELF格式規範中定義這幾個字節是被忽略 的,但實際上是這幾個字節完全可以可被利用。如病毒Lin/Glaurung.676/666(參考資料 1)設置 e_ident[7]爲0x21,表示本文件已被感染;或者存放可執行代碼(參考資料 2)。ELF頭部中大多數字段都是對子頭部數據的描述,其意義相對比較簡單。值得注意的是某些病毒可能修改字段e_entry(程序進入點)的值,以指向 病毒代碼,例如上面提到的病毒Lin/Glaurung.676/666。

一個實際可執行文件的文件頭部形式如下:(利用命令readelf)

ELF Header:
Magic: 7f 45 4c 46 01 01 01 00 00 00 00 00 00 00 00 00
Class: ELF32
Data: 2's complement, little endian
Version: 1 (current)
OS/ABI: UNIX - System V
ABI Version: 0
Type: EXEC (Executable file)
Machine: Intel 80386
Version: 0x1
Entry point address: 0x80483cc
Start of program headers: 52 (bytes into file)
Start of section headers: 14936 (bytes into file)
Flags: 0x0
Size of this header: 52 (bytes)
Size of program headers: 32 (bytes)
Number of program headers: 6
Size of section headers: 40 (bytes)
Number of section headers: 34
Section header string table index: 31




緊接ELF頭部的是程序頭表,它是一個結構數組,包含了ELF頭表中字段e_phnum定義的條目,結構描述一個段或其他系統準備執行該程序所需要的信息。

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;




在詳細討論可執行文件程序頭表之前,首先查看一個實際文件的輸出:

Program Headers:
Type Offset VirtAddr PhysAddr FileSiz MemSiz *** Align
PHDR 0x000034 0x08048034 0x08048034 0x000c0 0x000c0 R E 0x4
INTERP 0x0000f4 0x080480f4 0x080480f4 0x00013 0x00013 R 0x1
[Requesting program interpreter: /lib/ld-linux.so.2]
LOAD 0x000000 0x08048000 0x08048000 0x00684 0x00684 R E 0x1000
LOAD 0x000684 0x08049684 0x08049684 0x00118 0x00130 RW 0x1000
DYNAMIC 0x000690 0x08049690 0x08049690 0x000c8 0x000c8 RW 0x4
NOTE 0x000108 0x08048108 0x08048108 0x00020 0x00020 R 0x4

Section to Segment mapping:
Segment Sections...
00
01 .interp
02 .interp .note.ABI-tag .hash .dynsym .dynstr .gnu.version .gnu.version_r .rel.dyn .rel.plt .init .plt .text .fini .rodata .eh_frame
03 .data .dynamic .ctors .dtors .jcr .got .bss
04 .dynamic
05 .note.ABI-tag

Section Headers:
[Nr] Name Type Addr Off Size ES *** Lk Inf Al
[ 0] NULL 00000000 000000 000000 00 0 0 0
[ 1] .interp PROGBITS 080480f4 0000f4 000013 00 A 0 0 1
[ 2] .note.ABI-tag NOTE 08048108 000108 000020 00 A 0 0 4
[ 3] .hash HASH 08048128 000128 000040 04 A 4 0 4
[ 4] .dynsym DYNSYM 08048168 000168 0000b0 10 A 5 1 4
[ 5] .dynstr STRTAB 08048218 000218 00007b 00 A 0 0 1
[ 6] .gnu.version VERSYM 08048294 000294 000016 02 A 4 0 2
[ 7] .gnu.version_r VERNEED 080482ac 0002ac 000030 00 A 5 1 4
[ 8] .rel.dyn REL 080482dc 0002dc 000008 08 A 4 0 4
[ 9] .rel.plt REL 080482e4 0002e4 000040 08 A 4 b 4
[10] .init PROGBITS 08048324 000324 000017 00 AX 0 0 4
[11] .plt PROGBITS 0804833c 00033c 000090 04 AX 0 0 4
[12] .text PROGBITS 080483cc 0003cc 0001f8 00 AX 0 0 4
[13] .fini PROGBITS 080485c4 0005c4 00001b 00 AX 0 0 4
[14] .rodata PROGBITS 080485e0 0005e0 00009f 00 A 0 0 32
[15] .eh_frame PROGBITS 08048680 000680 000004 00 A 0 0 4
[16] .data PROGBITS 08049684 000684 00000c 00 WA 0 0 4
[17] .dynamic DYNAMIC 08049690 000690 0000c8 08 WA 5 0 4
[18] .ctors PROGBITS 08049758 000758 000008 00 WA 0 0 4
[19] .dtors PROGBITS 08049760 000760 000008 00 WA 0 0 4
[20] .jcr PROGBITS 08049768 000768 000004 00 WA 0 0 4
[21] .got PROGBITS 0804976c 00076c 000030 04 WA 0 0 4
[22] .bss NOBITS 0804979c 00079c 000018 00 WA 0 0 4
[23] .comment PROGBITS 00000000 00079c 000132 00 0 0 1
[24] .debug_aranges PROGBITS 00000000 0008d0 000098 00 0 0 8
[25] .debug_pubnames PROGBITS 00000000 000968 000040 00 0 0 1
[26] .debug_info PROGBITS 00000000 0009a8 001cc6 00 0 0 1
[27] .debug_abbrev PROGBITS 00000000 00266e 0002cc 00 0 0 1
[28] .debug_line PROGBITS 00000000 00293a 0003dc 00 0 0 1
[29] .debug_frame PROGBITS 00000000 002d18 000048 00 0 0 4
[30] .debug_str PROGBITS 00000000 002d60 000bcd 01 MS 0 0 1
[31] .shstrtab STRTAB 00000000 00392d 00012b 00 0 0 1
[32] .symtab SYMTAB 00000000 003fa8 000740 10 33 56 4
[33] .strtab STRTAB 00000000 0046e8 000467 00 0 0 1




對一個ELF可執行程序而言,一個基本的段是標記p_type爲PT_INTERP的段,它表明了運行此程序所需要的程序解釋器(/lib/ld- linux.so.2),實際上也就是動態連接器(dynamic linker)。最重要的段是標記p_type爲PT_LOAD的段,它表明了爲運行程序而需要加載到內存的數據。查看上面實際輸入,可以看見有兩個可 LOAD段,第一個爲只讀可執行(***爲R E),第二個爲可讀可寫(***爲RW)。段1包含了文本節.text,注意到ELF文件頭部中程序進入點的值爲0x80483cc,正好是指向節. text在內存中的地址。段二包含了數據節.data,此數據節中數據是可讀可寫的,相對的只讀數據節.rodata包含在段1中。ELF格式可以比 COFF格式包含更多的調試信息,如上面所列出的形式爲.debug_xxx的節。在I386平臺LINUX系統下,用命令file查看一個ELF可執行 程序的可能輸出是:a.out: ELF 32-bit LSB executable, Intel 80386, version 1 (SYSV), for GNU/Linux 2.2.5, dynamically linked (uses shared libs), not stripped。

ELF文件中包含了動態連接器的全路徑,內核定位"正確"的動態連接器在內存中的地址是"正確"運行可執行文件的保證,參考資料 13討論瞭如何通過查找動態連接器在內存中的地址以達到顛覆(Subversiver)動態連接機制的方法。

最後我們討論ELF文件的動態連接機制。每一個外部定義的符號在全局偏移表(Global Offset Table GOT)中有相應的條目,如果符號是函數則在過程連接表(Procedure Linkage Table PLT)中也有相應的條目,且一個PLT條目對應一個GOT條目。對外部定義函數解析可能是整個ELF文件規範中最複雜的,下面是函數符號解析過程的一個 描述。

1:代碼中調用外部函數func,語句形式爲call 0xaabbccdd,地址0xaabbccdd實際上就是符號func在PLT表中對應的條目地址(假設地址爲標號.PLT2)。

2:PLT表的形式如下

.PLT0: pushl 4(%ebx) /* GOT表的地址保存在寄存器ebx中 */
jmp *8(%ebx)
nop; nop
nop; nop
.PLT1: jmp *name1@GOT(%ebx)
pushl $offset
jmp .PLT0@PC
.PLT2: jmp *func@GOT(%ebx)
pushl $offset
jmp .PLT0@PC




3:查看標號.PLT2的語句,實際上是跳轉到符號func在GOT表中對應的條目。

4:在符號沒有重定位前,GOT表中此符號對應的地址爲標號.PLT2的下一條語句,即是pushl $offset,其中$offset是符號func的重定位偏移量。注意到這是一個二次跳轉。

5:在符號func的重定位偏移量壓棧後,控制跳到PLT表的第一條目,把GOT[1]的內容壓棧,並跳轉到GOT[2]對應的地址。

6:GOT[2]對應的實際上是動態符號解析函數的代碼,在對符號func的地址解析後,會把func在內存中的地址設置到GOT表中此符號對應的條目中。

7:當第二次調用此符號時,GOT表中對應的條目已經包含了此符號的地址,就可直接調用而不需要利用PLT表進行跳轉。

動態連接是比較複雜的,但爲了獲得靈活性的代價通常就是複雜性。其最終目的是把GOT表中條目的值修改爲符號的真實地址,這也可解釋節.got包含在可讀可寫段中。

動態連接是一個非常重要的進步,這意味着庫文件可以被升級、移動到其他目錄等等而不需要重新編譯程序(當然,這不意味庫可以任意修改,如函數入參的個數、 數據類型應保持兼容性)。從很大程度上說,動態連接機制是ELF格式代替a.out格式的決定性原因。如果說面對對象的編程本質是面對接口 (interface)的編程,那麼動態連接機制則是這種思想的地一個非常典型的應用,具體的講,動態連接機制與設計模式中的橋接(BRIDGE)方法比 較類似,而它的LAZY特性則與**(PROXY)方法非常相似。動態連接操作的細節描述請參閱參考資料 8,9,10,11。通過閱讀命令readelf、objdump 的源代碼以及參考資料 14中所提及的相關軟件源代碼,可以對ELF文件的格式有更徹底的瞭解。

總結
不同時期的可執行文件格式深刻的反映了技術進步的過程,技術進步通常是針對解決存在的問題和適應新的環境。早期的UNIX系統使用a.out格式,隨着操 作系統和硬件系統的進步,a.out格式的侷限性越來越明顯。新的可執行文件格式COFF在UNIX System VR3中出現,COFF格式相對a.out格式最大變化是多了一個節頭表(section head table),能夠在包含基礎的文本段、數據段、BSS段之外包含更多的段,但是COFF對動態連接和C++程序的支持仍然比較困難。爲了解決上述問題, UNIX系統實驗室(UNIX SYSTEM Laboratories USL) 開發出ELF文件格式,它被作爲應用程序二進制接口(Application binary Interface ABI)的一部分,其目的是替代傳統的a.out格式。例如,ELF文件格式中引入初始化段.init和結束段.fini(分別對應構造函數和析構函數) 則主要是爲了支持C++程序。1994年6月ELF格式出現在LINUX系統上,現在ELF格式作爲UNIX/LINUX最主要的可執行文件格式。當然我 們完全有理由相信,在將來還會有新的可執行文件格式出現。

上述三種可執行文件格式都很好的體現了設計思想中分層的概念,由一個總的頭部刻畫了文件的基本要素,再由若干子頭部/條目刻畫了文件的若干細節。比較一下 可執行文件格式和以太數據包中以太頭、IP頭、TCP頭的設計,我想我們能很好的感受分層這一重要的設計思想。參考資料 21從全局的角度討論了各種文件的格式,並提出一個比較誇張的結論:Everything Is Byte!

最後的題外話:大多數資料中對a.out格式的評價較低,常見的詞語有黑暗年代(dark ages)、醜陋(ugly)等等,當然,從現代的觀點來看,的確是比較簡單,但是如果沒有曾經的簡單何來今天的精巧?正如我們今天可以評價石器時代的技 術是ugly,那麼將來的人們也可以嘲諷今天的技術是非常ugly。我想我們也許應該用更平和的心態來對曾經的技術有一個公正的評價。

參考資料


《LINUX VIRUSES - ELF FILE FORMAT》 Marius Van Oers
《A Whirlwind Tutorial on Creating Really Teensy ELF Executables for Linux 》 breadbox
《The Linux Virus Writing And Detection HOWTO》Alexander Bartolich
《從程序員角度看ELF》Hongjiu Lu alert7(譯)
《XCOFF Object File Format》
《Executable and Linkable Format(ELF)》

《elf文件格式 --另一文本方式的elf文檔》alert7(譯)
《如何修改動態庫符號表》wangdb
《分析ELF的加載過程》opera
《Before main() 分析》 alert7
《Linkers & Loaders》John R. Levine
《Running a.out executables on modern Red Hat Linux》
《Cheating the ELF》
《ELF Binary Analysis Tools》
《dbxread.c》
《Manual Reference Pages - A.OUT (5)》
《Linux 下緩衝區溢出攻擊的原理及對策》
《Microsoft Portable Executable and Common Object File Format Specification》
《COFF的文件結構》redleaves
《Common Object File Format (COFF)》
《Everything Is Byte》 mala

 

xueyan 05-06-24 03:11

分析ELF的加載過程
對於可執行文件來說,段的加載位置是固定的,程序段表中如實反映了段的加載地址.對於共享庫來?段的加載位置是浮動的,位置無關的,程序段表反映的是以0 作爲基準地址的相對加載地址.儘管共享庫的連接是不充分的,爲了便於測試動態鏈接器,Linux允許直接加載共享庫運行.如果應用程序具有動態鏈接器的描 述段,內核在完成程序段加載後,緊接着加載動態鏈接器,並且啓動動態鏈接器的

ELF的可執行文件與共享庫在結構上非常類似,它們具有一張程序段表,用來描述這些段如何映射到?br>炭佔?
對於可執行文件來說,段的加載位置是固定的,程序段表中如實反映了段的加載地址.對於共享庫來?br>?段的加載位置是浮動的,位置無關的,程序段 表反映的是以0作爲基準地址的相對加載地址.儘管共享庫的連接是不充分的,爲了便於測試動態鏈接器,Linux允許直接加載共享庫運行.如果應用程序具有 動態鏈接器的描述段,內核在完成程序段加載後,緊接着加載動態鏈接器,並且啓動動態鏈接器的?br>肟?

typedef struct elf32_hdr{
unsigned char e_ident[EI_NIDENT];
Elf32_Half e_type; /* ET_EXEC ET_DYN 等 */
Elf32_Half e_machine;
Elf32_Word e_version;
Elf32_Addr e_entry; /* Entry point */
Elf32_Off e_phoff; 程序段描述表的位置
Elf32_Off e_shoff; 一般段描述表的位置
Elf32_Word e_flags;
Elf32_Half e_ehsize; ELF頭的大小
Elf32_Half e_phentsize; 程序段描述表單元的大小
Elf32_Half e_phnum; 程序段描述表單元的個數
Elf32_Half e_shentsize; 一般段描述表的單元大小
Elf32_Half e_shnum; 一般段描述表單元的個數
Elf32_Half e_shstrndx;
} Elf32_Ehdr;

typedef struct elf32_phdr{
Elf32_Word p_type; PT_INTERP,PT_LOAD,PT_DYNAMIC等
Elf32_Off p_offset; 該程序段在ELF文件中的位置
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;

struct linux_binprm{
char buf[BINPRM_BUF_SIZE]; 預先讀入的ELF文件頭
struct page *page[MAX_ARG_PAGES];
unsigned long p; /* current top of mem */
int sh_bang;
struct file * file;
int e_uid, e_gid;
kernel_cap_t cap_inheritable, cap_permitted, cap_effective;
int argc, envc;
char * filename; /* Name of binary */
unsigned long loader, exec;
};
static int load_elf_binary(struct linux_binprm * bprm, struct pt_regs * regs)
{
struct file *interpreter = NULL; /* to shut gcc up */
unsigned long load_addr = 0, load_bias;
int load_addr_set = 0;
char * elf_interpreter = NULL;
unsigned int interpreter_type = INTERPRETER_NONE;
unsigned char ibcs2_interpreter = 0;
mm_segment_t old_fs;
unsigned long error;
struct elf_phdr * elf_ppnt, *elf_phdata;
unsigned long elf_bss, k, elf_brk;
int elf_exec_fileno;
int retval, size, i;
unsigned long elf_entry, interp_load_addr = 0;
unsigned long start_code, end_code, start_data, end_data;
struct elfhdr elf_ex;
struct elfhdr interp_elf_ex;
struct exec interp_ex;
char passed_fileno[6];

/* Get the exec-header */
elf_ex = *((struct elfhdr *) bprm->buf);

retval = -ENOEXEC;
/* First of all, some simple consistency checks */
if (memcmp(elf_ex.e_ident, ELFMAG, SELFMAG) != 0)
goto out; 文件頭標記是否匹配

if (elf_ex.e_type != ET_EXEC && elf_ex.e_type != ET_DYN)
goto out; 文件類型是否爲可執行文件或共享庫
if (!elf_check_arch(&elf_ex))
goto out;
if (!bprm->file->f_op||!bprm->file->f_op->mmap)
goto out; 所在的文件系統是否具有文件映射功能

/* Now read in all of the header information */

retval = -ENOMEM;
size = elf_ex.e_phentsize * elf_ex.e_phnum; 求程序段表總長度
if (size > 65536)
goto out;
elf_phdata = (struct elf_phdr *) kmalloc(size, GFP_KERNEL); 分配程序段表空間
if (!elf_phdata)
goto out;

; 讀入程序段表
retval = kernel_read(bprm->file, elf_ex.e_phoff, (char *) elf_phdata, size);
if (retval < 0)
goto out_free_ph;

retval = get_unused_fd(); 取可用進程文件表的自由槽位
if (retval < 0)
goto out_free_ph;
get_file(bprm->file);
fd_install(elf_exec_fileno = retval, bprm->file); 將打開的文件安裝到進程文件表

elf_ppnt = elf_phdata; 指向程序段表
elf_bss = 0; bss段的起始地址
elf_brk = 0; bss段的終止地址

start_code = ~0UL; 代碼段的開始
end_code = 0; 代碼段的終止
start_data = 0; 數據段的開始
end_data = 0; 數據段的終止
; 掃描ELF程序段表,搜尋動態鏈接器定義
for (i = 0; i < elf_ex.e_phnum; i++) {
if (elf_ppnt->p_type == PT_INTERP) {
retval = -EINVAL;
if (elf_interpreter)
goto out_free_dentry; 如果包含多個動態鏈接器描述項

/* This is the program interpreter used for
* shared libraries - for now assume that this
* is an a.out format binary
*/

retval = -ENOMEM; 爲動態鏈接器名稱字符串分配空間
elf_interpreter = (char *) kmalloc(elf_ppnt->p_filesz,
GFP_KERNEL);
if (!elf_interpreter)
goto out_free_file;
; 將動態鏈接器的文件名讀入內存
retval = kernel_read(bprm->file, elf_ppnt->p_offset,
elf_interpreter,
elf_ppnt->p_filesz);
if (retval < 0)
goto out_free_interp;
/* If the program interpreter is one of these two,
* then assume an iBCS2 image. Otherwise assume
* a native linux image.
*/
if (strcmp(elf_interpreter,"/usr/lib/libc.so.1") == 0 ||
strcmp(elf_interpreter,"/usr/lib/ld.so.1") == 0)
ibcs2_interpreter = 1; 說明應用程序是IBCS2仿真代碼

interpreter = open_exec(elf_interpreter); 打開動態鏈接器文件
retval = PTR_ERR(interpreter);
if (IS_ERR(interpreter))
goto out_free_interp;
retval = kernel_read(interpreter, 0, bprm->buf, BINPRM_BUF_SIZE);
; 讀入動態鏈接器文件頭
if (retval < 0)
goto out_free_dentry;

/* Get the exec headers */
interp_ex = *((struct exec *) bprm->buf); 假定爲aout格式的文件頭結構
interp_elf_ex = *((struct elfhdr *) bprm->buf); 假定爲ELF文件頭結構
}
elf_ppnt++; 下一片段目錄項
}

/* Some simple consistency checks for the interpreter */
if (elf_interpreter) {
; 如果定義了動態鏈接器,分析其格式類型
interpreter_type = INTERPRETER_ELF | INTERPRETER_AOUT;


/* Now figure out which format our binary is */
if ((N_MAGIC(interp_ex) != OMAGIC) &&
(N_MAGIC(interp_ex) != ZMAGIC) &&
(N_MAGIC(interp_ex) != QMAGIC))
interpreter_type = INTERPRETER_ELF; 如果不是AOUT標識

if (memcmp(interp_elf_ex.e_ident, ELFMAG, SELFMAG) != 0)
interpreter_type &= ~INTERPRETER_ELF; 如果沒有ELF標識

retval = -ELIBBAD;
if (!interpreter_type) 不能識別動態鏈接器類型
goto out_free_dentry;

/* Make sure only one type was selected */
if ((interpreter_type & INTERPRETER_ELF) &&
interpreter_type != INTERPRETER_ELF) {
printk(KERN_WARNING "ELF: Ambiguous type, using ELF/n");
interpreter_type = INTERPRETER_ELF;
}
}

/* OK, we are done with that, now set up the arg stuff,
and then start this sucker up */

if (!bprm->sh_bang) {
char * passed_p;

if (interpreter_type == INTERPRETER_AOUT) {
sprintf(passed_fileno, "%d", elf_exec_fileno);
passed_p = passed_fileno;

if (elf_interpreter) {
retval = copy_strings_kernel(1,&passed_p,bprm);
; 將程序的文件描述符壓入參數堆棧,準備傳遞給aout格式的動態鏈接器
if (retval)
goto out_free_dentry;
bprm->argc++; bprm->page[]中參數的數目
}
}
}

/* Flush all traces of the currently running executable */
retval = flush_old_exec(bprm);
if (retval)
goto out_free_dentry;

/* OK, This is the point of no return */
current->mm->start_data = 0;
current->mm->end_data = 0;
current->mm->end_code = 0;
current->mm->mmap = NULL;
current->flags &= ~PF_FORKNOEXEC;
elf_entry = (unsigned long) elf_ex.e_entry; 應用程序的入口地址

/* Do this immediately, since STACK_TOP as used in setup_arg_pages
may depend on the personality. */
SET_PERSONALITY(elf_ex, ibcs2_interpreter);
; 如果是ibcs2_interpreter非0,置PER_SVR4代碼個性,否則置PER_LINUX代碼個性

/* Do this so that we can load the interpreter, if need be. We will
change some of these later */
current->mm->rss = 0;
setup_arg_pages(bprm); /* XXX: check error */
; 建立描述堆棧參數頁的初始虛存範圍結構
current->mm->start_stack = bprm->p;

/* Try and get dynamic programs out of the way of the default mmap
base, as well as whatever program they might try to exec. This
is because the brk will follow the loader, and is not movable. */

load_bias = ELF_PAGESTART(elf_ex.e_type==ET_DYN ? ELF_ET_DYN_BASE : 0);
; 如果需要加載的是共享庫,則設置共享庫的加載偏置爲ELF_ET_DYN_BASE
/* Now we do a little grungy work by mmaping the ELF image into
the correct location in memory. At this point, we assume that
the image should be loaded at fixed address, not at a variable
address. */

old_fs = get_fs();
set_fs(get_ds());
; 再次掃描程序段描述表,映射其中的程序段到進程空間
for(i = 0, elf_ppnt = elf_phdata; i < elf_ex.e_phnum; i++, elf_ppnt++) {
int elf_prot = 0, elf_flags;
unsigned long vaddr;

if (elf_ppnt->p_type != PT_LOAD)
continue; 如果程序段不可加載

if (elf_ppnt->p_flags & PF_R) elf_prot |= PROT_READ;
if (elf_ppnt->p_flags & PF_W) elf_prot |= PROT_WRITE;
if (elf_ppnt->p_flags & PF_X) elf_prot |= PROT_EXEC;

elf_flags = MAP_PRIVATE|MAP_DENYWRITE|MAP_EXECUTABLE;
; 根據程序段描述設置相應的mmap參數

vaddr = elf_ppnt->p_vaddr; 段起始地址
if (elf_ex.e_type == ET_EXEC || load_addr_set) {
; 對於可執行程序,使用固定映射
elf_flags |= MAP_FIXED;
}

error = elf_map(bprm->file, load_bias + vaddr, elf_ppnt, elf_prot, elf_flags);
; 將該程序段[eppnt->p_offset,eppnt->p_filesz]映射到虛存(load_bias+vaddr)開始的區域

if (!load_addr_set) {
load_addr_set = 1;
load_addr = (elf_ppnt->p_vaddr - elf_ppnt->p_offset);
; 求出該ELF文件在用戶虛存中的起始地址
if (elf_ex.e_type == ET_DYN) {
load_bias += error -
ELF_PAGESTART(load_bias + vaddr);
load_addr += error;
}
}

k = elf_ppnt->p_vaddr;
if (k < start_code) start_code = k; 取最小的段地址作爲代碼段起始
if (start_data < k) start_data = k; 取最大的段地址作爲數據段起始

k = elf_ppnt->p_vaddr + elf_ppnt->p_filesz; 這時k指向文件段尾

if (k > elf_bss)
elf_bss = k; 取最大文件段尾作爲BSS段起始
if ((elf_ppnt->p_flags & PF_X) && end_code < k)
end_code = k; 取最大可執行的文件段尾作爲可執行段的終止
if (end_data < k)
end_data = k; 取最大的文件段尾作爲數據段的終止
k = elf_ppnt->p_vaddr + elf_ppnt->p_memsz; 這時k指向內存段尾
if (k > elf_brk)
elf_brk = k; 取最大的內存段尾作爲BSS段的終止
}
set_fs(old_fs);
elf_entry += load_bias;
elf_bss += load_bias;
elf_brk += load_bias;
start_code += load_bias;
end_code += load_bias;
start_data += load_bias;
end_data += load_bias;

if (elf_interpreter) {
if (interpreter_type == INTERPRETER_AOUT)
elf_entry = load_aout_interp(&interp_ex,
interpreter);
else
elf_entry = load_elf_interp(&interp_elf_ex, 動態鏈接器的文件頭
interpreter, 動態鏈接器打開的文件結構
&interp_load_addr); 輸出鏈接器的加載地址
; 可執行程序的入口變爲動態鏈接器的入口
allow_write_access(interpreter);
fput(interpreter);
kfree(elf_interpreter);

if (elf_entry == ~0UL) {
printk(KERN_ERR "Unable to load interpreter/n");
kfree(elf_phdata);
send_sig(SIGSEGV, current, 0);
return 0;
}
}

kfree(elf_phdata); 釋放程序段表

if (interpreter_type != INTERPRETER_AOUT)
sys_close(elf_exec_fileno);

set_binfmt(&elf_format); 增加ELF內核模塊的引用計數

compute_creds(bprm);
current->flags &= ~PF_FORKNOEXEC;
bprm->p = (unsigned long) 建立入口函數參數表
create_elf_tables((char *)bprm->p,
bprm->argc,
bprm->envc,
(interpreter_type == INTERPRETER_ELF ? &elf_ex : NULL),
load_addr, load_bias,
interp_load_addr,
(interpreter_type == INTERPRETER_AOUT ? 0 : 1));
/* N.B. passed_fileno might not be initialized? */
if (interpreter_type == INTERPRETER_AOUT)
current->mm->arg_start += strlen(passed_fileno) + 1;
current->mm->start_brk = current->mm->brk = elf_brk;
current->mm->end_code = end_code;
current->mm->start_code = start_code;
current->mm->start_data = start_data;
current->mm->end_data = end_data;
current->mm->start_stack = bprm->p;

/* Calling set_brk effectively mmaps the pages that we need
* for the bss and break sections
*/
set_brk(elf_bss, elf_brk); 建立bss的虛存映射,elf_bss是bss的開始,elf_brk是bss的結束

padzero(elf_bss); 如果bss不起始於頁連界上,說明與data段有重疊,則將該頁bss區域清0

#if 0
printk("(start_brk) %lx/n" , (long) current->mm->start_brk);
printk("(end_code) %lx/n" , (long) current->mm->end_code);
printk("(start_code) %lx/n" , (long) current->mm->start_code);
printk("(start_data) %lx/n" , (long) current->mm->start_data);
printk("(end_data) %lx/n" , (long) current->mm->end_data);
printk("(start_stack) %lx/n" , (long) current->mm->start_stack);
printk("(brk) %lx/n" , (long) current->mm->brk);
#endif

if ( current->personality == PER_SVR4 )
{
/* Why this, you ask??? Well SVr4 maps page 0 as read-only,
and some applications "depend" upon this behavior.
Since we do not have the power to recompile these, we
emulate the SVr4 behavior. Sigh. */
/* N.B. Shouldn't the size here be PAGE_SIZE?? */
down(¤t->mm->mmap_sem);
error = do_mmap(NULL, 0, 4096, PROT_READ | PROT_EXEC,
MAP_FIXED | MAP_PRIVATE, 0);
up(¤t->mm->mmap_sem);
}

#ifdef ELF_PLAT_INIT
/*
* The ABI may specify that certain registers be set up in special
* ways (on i386 %edx is the address of a T_FINI function, for
* example. This macro performs whatever initialization to
* the regs structure is required.
*/
ELF_PLAT_INIT(regs);
#endif

start_thread(regs, elf_entry, bprm->p); 將返回的eip設爲elf_entry,esp設爲bprm->p
if (current->ptrace & PT_PTRACED)
send_sig(SIGTRAP, current, 0); 如果進程處於跟蹤狀態,則生成SIGTRAP信號
retval = 0;
out:
return retval;

/* error cleanup */
out_free_dentry:
allow_write_access(interpreter);
fput(interpreter);
out_free_interp:
if (elf_interpreter)
kfree(elf_interpreter);
out_free_file:
sys_close(elf_exec_fileno);
out_free_ph:
kfree(elf_phdata);
goto out;
}
static unsigned long load_elf_interp(struct elfhdr * interp_elf_ex,
struct file * interpreter,
unsigned long *interp_load_addr)
{
struct elf_phdr *elf_phdata;
struct elf_phdr *eppnt;
unsigned long load_addr = 0;
int load_addr_set = 0;
unsigned long last_bss = 0, elf_bss = 0;
unsigned long error = ~0UL;
int retval, i, size;

/* First of all, some simple consistency checks */
if (interp_elf_ex->e_type != ET_EXEC &&
interp_elf_ex->e_type != ET_DYN)
goto out;
if (!elf_check_arch(interp_elf_ex))
goto out;
if (!interpreter->f_op || !interpreter->f_op->mmap)
goto out;

/*
* If the size of this structure has changed, then punt, since
* we will be doing the wrong thing.
*/
if (interp_elf_ex->e_phentsize != sizeof(struct elf_phdr))
goto out;

/* Now read in all of the header information */

size = sizeof(struct elf_phdr) * interp_elf_ex->e_phnum;
if (size > ELF_MIN_ALIGN)
goto out; 如果動態鏈接器的程序段表的尺寸大於1個頁面
elf_phdata = (struct elf_phdr *) kmalloc(size, GFP_KERNEL);
if (!elf_phdata)
goto out;

retval = kernel_read(interpreter,interp_elf_ex->e_phoff,(char *)elf_phdata,size);
error = retval;
if (retval < 0)
goto out_close;

eppnt = elf_phdata;
for (i=0; ie_phnum; i++, eppnt++) {
if (eppnt->p_type == PT_LOAD) {
int elf_type = MAP_PRIVATE | MAP_DENYWRITE;
int elf_prot = 0;
unsigned long vaddr = 0;
unsigned long k, map_addr;

if (eppnt->p_flags & PF_R) elf_prot = PROT_READ;
if (eppnt->p_flags & PF_W) elf_prot |= PROT_WRITE;
if (eppnt->p_flags & PF_X) elf_prot |= PROT_EXEC;
vaddr = eppnt->p_vaddr;
if (interp_elf_ex->e_type == ET_EXEC || load_addr_set)
elf_type |= MAP_FIXED;

map_addr = elf_map(interpreter, load_addr + vaddr, eppnt, elf_prot, elf_type);

if (!load_addr_set && interp_elf_ex->e_type == ET_DYN) {
load_addr = map_addr - ELF_PAGESTART(vaddr);
load_addr_set = 1;
}

/*
* Find the end of the file mapping for this phdr, and keep
* track of the largest address we see for this.
*/
k = load_addr + eppnt->p_vaddr + eppnt->p_filesz;
if (k > elf_bss)
elf_bss = k;

/*
* Do the same thing for the memory mapping - between
* elf_bss and last_bss is the bss section.
*/
k = load_addr + eppnt->p_memsz + eppnt->p_vaddr;
if (k > last_bss)
last_bss = k;
}
}

/* Now use mmap to map the library into memory. */

/*
* Now fill out the bss section. First pad the last page up
* to the page boundary, and then perform a mmap to make sure
* that there are zero-mapped pages up to and including the
* last bss page.
*/
padzero(elf_bss);
elf_bss = ELF_PAGESTART(elf_bss + ELF_MIN_ALIGN - 1); /* What we have mapped so far*/

/* Map the last of the bss segment */
if (last_bss > elf_bss)
do_brk(elf_bss, last_bss - elf_bss);

*interp_load_addr = load_addr;
error = ((unsigned long) interp_elf_ex->e_entry) + load_addr;

out_close:
kfree(elf_phdata);
out:
return error;
}
static inline unsigned long
elf_map (struct file *filep, unsigned long addr, struct elf_phdr *eppnt, int prot, inttype)
{
unsigned long map_addr;

down(¤t->mm->mmap_sem);
map_addr = do_mmap(filep, ELF_PAGESTART(addr),
eppnt->p_filesz + ELF_PAGEOFFSET(eppnt->p_vaddr), prot, type,
eppnt->p_offset - ELF_PAGEOFFSET(eppnt->p_vaddr));

up(¤t->mm->mmap_sem);
return(map_addr);
}
void set_binfmt(struct linux_binfmt *new)
{
struct linux_binfmt *old = current->binfmt;
if (new && new->module)
__MOD_INC_USE_COUNT(new->module);
current->binfmt = new;
if (old && old->module)
__MOD_DEC_USE_COUNT(old->module);
}
static void set_brk(unsigned long start, unsigned long end)
{
start = ELF_PAGEALIGN(start);
end = ELF_PAGEALIGN(end);
if (end <= start)
return;
do_brk(start, end - start);
}
static void padzero(unsigned long elf_bss)
{
unsigned long nbyte;

nbyte = ELF_PAGEOFFSET(elf_bss);
if (nbyte) {
nbyte = ELF_MIN_ALIGN - nbyte;
clear_user((void *) elf_bss, nbyte);
}
}

 

xueyan 05-06-24 03:12

如何修改動態庫符號表[轉]
ELF 文件中代碼、連接信息和註釋是以節(section)爲單位存放的,並存有一
個節頭表(section header)。對每一節,在節頭表中都有一個表項(節頭表項)
與之對應,表項記錄了該節的一些信息,例如該節在文件中的位置信息和該節
的字節長度信息。

一、ELF 文件和有關術語

Unix 系統的可執行文件和動態庫文件是以 ELF 格式存放的。爲使下面的敘述
清晰而沒有伎義,先簡要介紹一下 ELF 文件格式,並約定一些術語。關於ELF
文件格式的詳細情況請參看有關文獻。

ELF 文件中代碼、連接信息和註釋是以節(section)爲單位存放的,並存有一
個節頭表(section header)。對每一節,在節頭表中都有一個表項(節頭表項)
與之對應,表項記錄了該節的一些信息,例如該節在文件中的位置信息和該節
的字節長度信息。

程序運行讀入內存時,是以程序段(program segment)爲單位讀入的。在 ELF
文件中有一個程序頭表(program header table),每個程序段在程序頭表中有
一個表項(程序頭表項)與之對應,表項記錄了該程序段的有關信息,例如該程
序段在文件中的位置信息和該程序段的字節長度信息。程序段的內容由若干節
組成,節的內容組合在一起連成一片構成程序段的內容。

在所有這些節中,有一節的內容由字符串構成,這些字符串是各節的名稱,叫
節名。下面稱這一節爲節名錶。另有一節,節名爲".dynsym",它的內容爲符
號表,符號表的每一表項記錄了一個符號的有關信息,例如該符號對應的代碼
的地址值。還有一節,節名爲".dynstr",它的內容由字符串構成。大多數符
號在該節中有一個字符串與之對應,這個字符串是該符號的符號名。而每一函
數對應一個符號,函數名即爲符號名。下面稱被某一函數對應的符號爲函數符
號。

ELF 文件開始處的一段叫 ELF 文件頭。它記錄了程序頭表在文件中的偏移、
程序頭表的表項數目、程序頭表每一表項的字節長度、節頭表在文件中的偏移、
節頭表的表項數目、節頭表每個表項的字節長度。它還記錄了節名錶所在的節
的索引序號。

二、動態庫符號表修改方法

修改動態庫符號表的方法和步驟如下:

第一步:

讀取 ELF 文件頭,取出

(1) 程序頭表在文件中的偏移,獲取程序頭表在文件中的位置;
(2) 程序頭表的表項數目和程序頭表每一表項的字節長度;
(3) 節頭表在文件中的偏移,獲取節頭表在文件中的位置;
(4) 節頭表的表項數目和節頭表每個表項的字節長度;
(3) 節名錶所在的節的索引序號。

ELF 文件頭在文件中的偏移爲零,即起始於 ELF 文件開頭的第一字節,它的
數據結構爲:

#define EI_NIDENT (16)

typedef uint16_t Elf32_Half;
typedef uint32_t Elf32_Word;
typedef uint32_t Elf32_Addr;
typedef uint32_t Elf32_Off;
typedef uint16_t Elf32_Section;

typedef struct
{
unsigned char e_ident[EI_NIDENT]; /* Magic number及其他信息 */
Elf32_Half e_type; /* ELF 文件類型 */
Elf32_Half e_machine; /* 機器型號 */
Elf32_Word e_version; /* 版本 */
Elf32_Addr e_entry; /* 程序入口虛地址 */
Elf32_Off e_phoff; /* 程序頭表在文件中的偏移 */
Elf32_Off e_shoff; /* 節頭表在文件中的偏移 */
Elf32_Word e_flags; /* 處理器標誌 */
Elf32_Half e_ehsize; /* ELF 文件頭長度 */
Elf32_Half e_phentsize; /* 程序頭表每個表項長度 */
Elf32_Half e_phnum; /* 程序頭表的表項總數 */
Elf32_Half e_shentsize; /* 節頭表每個表項長度 */
Elf32_Half e_shnum; /* 節頭表的表項總數 */
Elf32_Half e_shstrndx; /* 節名錶所在的節的表項索引號 */
} Elf32_Ehdr;

第二步:

依據節頭表在文件中的偏移和節名錶所在的節的索引序號,定出節名錶在節頭
表中對應的表項的文件偏移,即定出該表項在文件中的位置。讀取該表項,取
出節名錶在文件中的偏移和該節在文件中的字節長度。節頭表由若干表項組成,
每個表項的內容按下面的數據結構來組織:

typedef struct
{
Elf32_Word sh_name; /* 節名索引號 */
Elf32_Word sh_type; /* 節類型 */
Elf32_Word sh_flags; /* 節標誌 */
Elf32_Addr sh_addr; /* 執行時該節虛地址 */
Elf32_Off sh_offset; /* 在文件中的偏移 */
Elf32_Word sh_size; /* 節長度 */
Elf32_Word sh_link; /* 到其他節的連接 */
Elf32_Word sh_info; /* 其他信息 */
Elf32_Word sh_addralign; /* alignment */
Elf32_Word sh_entsize; /* 如內容爲表,每個表項的長度 */
} Elf32_Shdr;

第三步:

按節名錶在文件中的偏移和該節的長度讀取節名錶,並緩存在一個緩衝區中。

第四步:

依據節頭表在文件中的偏移、節頭表的表項總數、以及節頭表每個表項長度搜
索節頭表。對每個節頭表項,讀出節名索引號,由節名索引號從上面緩存在緩
衝區中節名錶得出該節頭表項對應的節的名字。如果名字爲".dynsym",記錄
該節的在文件中的偏移和字節長度。名字爲 ".dynsym" 的節的內容是符號表,
除了記錄它在文件中的偏移和字節長度外,還要記下它的每個表項的長度。每
個表項即是對一個符號所記錄的信息,表項的數據結構爲:

typedef struct
{
Elf32_Word st_name; /* 符號名索引號 */
Elf32_Addr st_value; /* 符號地址值 */
Elf32_Word st_size; /* 符號對應的代碼長度 */
unsigned char st_info; /* 符號類型和梆定信息 */
unsigned char st_other; /* 未用,值爲 0 */
Elf32_Section st_shndx; /* 所在節的節索引號 */
} Elf32_Sym;

在搜索節頭表時,除了找 ".dynsym" 節外,還要找到名字爲 ".dynstr" 的節,
記下它的在文件中的偏移和字節長度。由該結在文件中的偏移和字節長度讀取
它的內容,並緩存在一個緩衝區中。

第五步:

按第四步中得到的 ..dynsym節的字節長度和符號表的表項的長度算出符號表表
項數目,也即符號的數目。然後依據第四步中得到的符號表(即.dynsym節)在
文件中的偏移把文件指針打到符號表所在的位置,檢索符號表找到要修改的符
號。方法是從符號表表項中讀出符號名索引號(st_name)的值,這個值即是該
表項記錄其信息的符號的符號名字符串在 .dynstr 節中的偏移,由這個值在
第四步中緩存 .dynstr 節的緩衝區中取出符號名,把符號名和要找的符號的
符號名進行比較。

第六步:

第五步中找到了要修改的符號,現在可以進行修改了。所謂修改符號就是修改
該符號在符號表(.dynsym 節)中表項,因爲表項的內容是對該符號的有關信
息的記錄。hook 時需要關注的是符號地址值(st_value)和符號對應的代碼長
度(st_size)。可以把符號地址值(st_value)和符號對應的代碼長度(st_size)
修改爲動態庫中另一符號的相應的 st_value 和 st_size 值。通常修改的是
函數符號。如果是函數符號,那麼修改後當調用該函數時,實際調用的是上面
修改時取其 st_value 和 st_size 值的另一符號所對應的函數。也可以向ELF
文件中加入的一段幾十字節的 shellcode 或其他代碼,修改符號表時把所修
改的符號在符號表中的表項的 st_value 值指向這段 shellcode 或其他代碼,
st_size 的值置爲這段代碼的字節長度。那麼,程序調用被修改的符號所對應
的函數時,實際調用的是這段 shellcode 或其他代碼。

三、示例程序

爲對上面所說的進行解釋,下面的給出一個示例程序。它打印出 ELF 文件的
有關信息,然後通過修改符號表把動態庫的函數1 hook 到函數2。這樣做以後,
如果某程序編譯時與該動態庫連接了,它調用函數1時,程序運行時實際調用
的是函數2。函數2可以是動態庫本身就有的函數,也可以是你向 ELF 文件中
偷偷加入的一段幾十字節的 shellcode。這是向系統置入後門的一個方法,特
別是 hook 經常被調用的動態庫。

測試時做了一個小動態庫,它僅有兩個函數 haha() 和 huhu():
________________________________________________________
/* haha.c */

#include <stdio.h>

void haha(void)
{
printf(" --- haha/n");
return;
}
________________________________________________________
/* huhu.c */

#include <stdio.h>

void huhu(void)
{
printf(" --- huhu/n");
return;
}
________________________________________________________

[wangdb@redhat62 exploit]$ gcc -c -fPIC -O3 haha.c huhu.c
[wangdb@redhat62 exploit]$ gcc -shared haha.o huhu.o -o libtst.so.1.0
[wangdb@redhat62 exploit]$ ln -s libtst.so.1.0 libtst.so

程序 m.c 調用 huhu() 和 haha():
________________________________________________________
/* m.c */
int main()
{
haha();
huhu();
return 0;
}
________________________________________________________

[wangdb@redhat62 exploit]$ gcc m.c -L. -ltst -o ttt
[wangdb@redhat62 exploit]$ gcc -O3 hook_elf.c -o elf_hook
[wangdb@redhat62 exploit]$ ./ttt
--- haha
--- huhu
[wangdb@redhat62 exploit]$ ./elf_hook libtst.so huhu haha
.
.
.
[wangdb@redhat62 exploit]$ ./ttt
--- haha
--- haha
[wangdb@redhat62 exploit]$

下面是 hook_elf.c 程序:
________________________________________________________________________________
/*
* C Program File: hook_elf.c ---
*
* Description: This program read and print relevant information of ELF
* File, then hook function fun1 to fun2. After hooking, when
* some program call function fun1, actually it is fun2 being
* called.
* Usage:
* hook_elf <lib***.so> <dst_sym> <src_sym>
* (Note: when dst_sym == src_sym, ELF file is not changed.)
*
* Author: wangdb ([email protected])
*/

#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdint.h>

#define EI_NIDENT (16)

typedef uint16_t Elf32_Half;
typedef uint32_t Elf32_Word;
typedef uint32_t Elf32_Addr;
typedef uint32_t Elf32_Off;
typedef uint16_t Elf32_Section;

/* 下面的數據結構定義取自 elf.h 頭文件 */

/* The ELF file header. This appears at the start of every ELF file. */

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 */
} myElf32_Ehdr;

/* Program segment header. */

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 */
} myElf32_Phdr;

/* Section header. */

typedef struct
{
Elf32_Word sh_name; /* Section name (string tbl index) */
Elf32_Word sh_type; /* Section type */
Elf32_Word sh_flags; /* Section flags */
Elf32_Addr sh_addr; /* Section virtual addr at execution */
Elf32_Off sh_offset; /* Section file offset */
Elf32_Word sh_size; /* Section size in bytes */
Elf32_Word sh_link; /* Link to another section */
Elf32_Word sh_info; /* Additional section information */
Elf32_Word sh_addralign; /* Section alignment */
Elf32_Word sh_entsize; /* Entry size if section holds table */
} myElf32_Shdr;

/* 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; /* No defined meaning, 0 */
Elf32_Section st_shndx; /* Section index */
} myElf32_Sym;

/* The syminfo section if available contains additional information about
every dynamic symbol. */

typedef struct
{
Elf32_Half si_boundto; /* Direct bindings, symbol bound to */
Elf32_Half si_flags; /* Per symbol flags */
} myElf32_Syminfo;


/* Main routine */

int main(int argc, char *argv[])
{
myElf32_Ehdr *e_hdr_ptr;
myElf32_Phdr *p_hdr_ptr;
myElf32_Shdr *s_hdr_ptr;

myElf32_Sym *symptr;
myElf32_Syminfo *HashSymPtr;

int fd, i;
unsigned char buf[256];

unsigned int ProHdrFileOffset;
unsigned int SecHdrFileOffset;
unsigned int NamStrSecTblIndex;
unsigned int ProHdrTblEntrNum;
unsigned int SecHdrTblEntrNum;
unsigned int ProHdrTblEntrSize;
unsigned int SecHdrTblEntrSize;

unsigned int SecNamStrTblFileOffset = 0;
char SecNameStrTable[1024];
unsigned int SecNameIndex = 0;

unsigned char SymTblEntry[16];

unsigned int DebugInfoFileOffset = 0;
int DebugInfoSymTblNum = 0;
unsigned int DebugInfoStrTblFileOffset = 0;
char DebugInfoStrTable[4096];
unsigned int DebugInfoStrTblSize = 0;

unsigned int SymTblFileOffset = 0;
int SymTblNum = 0;
unsigned int SymNamStrTblFileOffset = 0;
char SymNamStrTable[2048];
unsigned int SymNamStrTblSize = 0;

unsigned int HashOffset = 0;
int HashTblNum = 0;

unsigned char src_sym[16], dst_sym[16];
unsigned char tmp_sym_addr[4];
unsigned char tmp_sym_size[4];
unsigned int src_sym_tbl = 0, dst_sym_tbl = 0;

if (argc < 4) {
fprintf(stderr, "Usage: %s <object_file> <dst_sym> <src_sym>/n", argv[0]);
exit(1);
}

if ( (fd = open(argv[1], O_RDONLY)) == -1 ) {
fprintf(stderr, "Can't open file /"%s/"./n", argv[1]);
exit(1);
}

fprintf(stdout, "Dump content of the ELF file '%s'/n", argv[1]);

fprintf(stdout, "Part I: ELF File Header.../n");

/* 讀取 ELF 文件頭 */
if ( read(fd, buf, 52) != 52 ) {
fprintf(stderr, "read error/n");
close(fd); exit(1);
}

e_hdr_ptr = (myElf32_Ehdr *)buf;

fprintf(stdout, "(Magic number and other info)e_ident: %s/n",
e_hdr_ptr->e_ident);
fprintf(stdout, "(Object file type)e_type: 0x%04X/n",
e_hdr_ptr->e_type);
fprintf(stdout, "(Architecture)e_machine: 0x%04X/n",
e_hdr_ptr->e_machine);
fprintf(stdout, "(Object file version)e_version: 0x%08X/n",
e_hdr_ptr->e_version);
fprintf(stdout, "(Entry point virtual address)e_entry: 0x%08X/n",
e_hdr_ptr->e_entry);
fprintf(stdout, "(Program header table file offset)e_phoff: 0x%08X/n",
e_hdr_ptr->e_phoff);
fprintf(stdout, "(Section header table file offset)e_shoff: 0x%08X/n",
e_hdr_ptr->e_shoff);
fprintf(stdout, "(Processor-specific flags)e_flags: 0x%08X/n",
e_hdr_ptr->e_flags);
fprintf(stdout, "(ELF header size in bytes)e_ehsize: 0x%04X/n",
e_hdr_ptr->e_ehsize);
fprintf(stdout, "(Program header table entry size)e_phentsize: 0x%04X/n",
e_hdr_ptr->e_phentsize);
fprintf(stdout, "(Program header table entry count)e_phnum: 0x%04X/n",
e_hdr_ptr->e_phnum);
fprintf(stdout, "(Section header table entry size)e_shentsize: 0x%04X/n",
e_hdr_ptr->e_shentsize);
fprintf(stdout, "(Section header table entry count)e_shnum: 0x%04X/n",
e_hdr_ptr->e_shnum);
fprintf(stdout, "(Section header string table index)e_shstrndx: 0x%04X/n",
e_hdr_ptr->e_shstrndx);

/* 記下程序頭表在文件中的偏移、節頭表在文件中的偏移、
節名錶所在的節的索引序號、程序頭表表項字節長度、程序頭表表項數目、
節頭表表項字節長度、節頭表表項數目。*/
ProHdrFileOffset = (unsigned int)e_hdr_ptr->e_phoff;
SecHdrFileOffset = (unsigned int)e_hdr_ptr->e_shoff;
NamStrSecTblIndex = (unsigned int)e_hdr_ptr->e_shstrndx;
ProHdrTblEntrNum = (unsigned int)e_hdr_ptr->e_phnum;
SecHdrTblEntrNum = (unsigned int)e_hdr_ptr->e_shnum;
ProHdrTblEntrSize = (unsigned int)e_hdr_ptr->e_phentsize;
SecHdrTblEntrSize = (unsigned int)e_hdr_ptr->e_shentsize;

fprintf(stdout, "Part II: Program Header Table.../n");

if ( lseek(fd, (off_t)ProHdrFileOffset, SEEK_SET) != ProHdrFileOffset ) {
fprintf(stderr, "lseek to program header error./n");
close(fd); exit(1);
}

for (i = 0; i < (int)ProHdrTblEntrNum; i++) {
if ( read(fd, buf, (size_t)ProHdrTblEntrSize) !=
(ssize_t)ProHdrTblEntrSize ) {
fprintf(stderr, "read error/n");
close(fd); exit(1);
}
fprintf(stdout, "Program Header Entry for Segment %d:/n", i + 1);
p_hdr_ptr = (myElf32_Phdr *)buf;
fprintf(stdout, "(Segment type)p_type: 0x%08X/n",
p_hdr_ptr->p_type);
fprintf(stdout, "(Segment flags)p_flags: 0x%08X/n",
p_hdr_ptr->p_flags);
fprintf(stdout, "(Segment file offset)p_offset: 0x%08X/n",
p_hdr_ptr->p_offset);
fprintf(stdout, "(Segment virtual address)p_vaddr: 0x%08X/n",
p_hdr_ptr->p_vaddr);
fprintf(stdout, "(Segment physical address)p_paddr: 0x%08X/n",
p_hdr_ptr->p_paddr);
fprintf(stdout, "(Segment size in file)p_filesz: 0x%08X/n",
p_hdr_ptr->p_filesz);
fprintf(stdout, "(Segment size in memory)p_memsz: 0x%08X/n",
p_hdr_ptr->p_memsz);
fprintf(stdout, "(Segment alignment)p_align: 0x%08X/n",
p_hdr_ptr->p_align);
}

fprintf(stdout, "Part III: Section Header Table.../n");

/* 定出節名錶所在的節在節頭表中對應的表項的文件偏移。*/
SecNamStrTblFileOffset = SecHdrFileOffset + NamStrSecTblIndex * 40;
if ( lseek(fd, (off_t)SecNamStrTblFileOffset, SEEK_SET) !=
SecNamStrTblFileOffset || SecNamStrTblFileOffset == 0 ) {
fprintf(stderr,
"lseek to Section Table Entry for Section Name String Table error./n");
close(fd); exit(1);
}
if ( read(fd, buf, (size_t)SecHdrTblEntrSize) != (ssize_t)SecHdrTblEntrSize ) {
fprintf(stderr, "read error/n");
close(fd); exit(1);
}
s_hdr_ptr = (myElf32_Shdr *)buf;
SecNamStrTblFileOffset = (unsigned int)s_hdr_ptr->sh_offset;

/* 讀取節名錶,並緩存在一個緩衝區中。*/
if ( lseek(fd, (off_t)SecNamStrTblFileOffset, SEEK_SET) !=
SecNamStrTblFileOffset || SecNamStrTblFileOffset == 0 ) {
fprintf(stderr, "lseek to Section Name String Table error./n");
close(fd); exit(1);
}
if ( read(fd, SecNameStrTable, (size_t)s_hdr_ptr->sh_size) !=
(ssize_t)s_hdr_ptr->sh_size ) {
fprintf(stderr, "read error/n");
close(fd); exit(1);
}

if ( lseek(fd, (off_t)SecHdrFileOffset, SEEK_SET) != SecHdrFileOffset ||
SecHdrFileOffset == 0 ) {
fprintf(stderr, "lseek to section header error./n");
close(fd); exit(1);
}

/* 記錄符號表(即.dynsym節)在文件中的偏移,由它的字節長度和每個表項的
長度算出符號表的表項數目。同時記下.dynstr節在文件中的偏移和字節長度。*/
for (i = 0; i < (int)SecHdrTblEntrNum; i++) {
if ( read(fd, buf, (size_t)SecHdrTblEntrSize) !=
(ssize_t)SecHdrTblEntrSize ) {
fprintf(stderr, "read error/n");
close(fd); exit(1);
}
s_hdr_ptr = (myElf32_Shdr *)buf;
/*if ( s_hdr_ptr->sh_type == 0x3 && s_hdr_ptr->sh_name == 0x11 ) {
SecNamStrTblFileOffset = (unsigned int)s_hdr_ptr->sh_offset;
}*/
if ( strcmp(SecNameStrTable + s_hdr_ptr->sh_name, ".symtab") == 0 ) {
DebugInfoFileOffset = (unsigned int)s_hdr_ptr->sh_offset;
DebugInfoSymTblNum = (int)((s_hdr_ptr->sh_size)/(s_hdr_ptr->sh_entsize));
}
if ( strcmp(SecNameStrTable + s_hdr_ptr->sh_name, ".strtab") == 0 ) {
DebugInfoStrTblFileOffset = (unsigned int)s_hdr_ptr->sh_offset;
DebugInfoStrTblSize = (unsigned int)s_hdr_ptr->sh_size;
}
if ( strcmp(SecNameStrTable + s_hdr_ptr->sh_name, ".dynsym") == 0 ) {
SymTblFileOffset = (unsigned int)s_hdr_ptr->sh_offset;
SymTblNum = (int)((s_hdr_ptr->sh_size)/(s_hdr_ptr->sh_entsize));
}
if ( strcmp(SecNameStrTable + s_hdr_ptr->sh_name, ".dynstr") == 0 ) {
SymNamStrTblFileOffset = (unsigned int)s_hdr_ptr->sh_offset;
SymNamStrTblSize = (unsigned int)s_hdr_ptr->sh_size;
}
if ( strcmp(SecNameStrTable + s_hdr_ptr->sh_name, ".hash") == 0 ) {
HashOffset = (unsigned int)s_hdr_ptr->sh_offset;
HashTblNum = (int)((s_hdr_ptr->sh_size)/(s_hdr_ptr->sh_entsize));
}
}

if ( lseek(fd, (off_t)SecHdrFileOffset, SEEK_SET) != SecHdrFileOffset ) {
fprintf(stderr, "lseek to section header error./n");
close(fd); exit(1);
}

for (i = 0; i < (int)SecHdrTblEntrNum; i++) {
if ( read(fd, buf, (size_t)SecHdrTblEntrSize) !=
(ssize_t)SecHdrTblEntrSize ) {
fprintf(stderr, "read error/n");
close(fd); exit(1);
}
s_hdr_ptr = (myElf32_Shdr *)buf;
fprintf(stdout, "Section %d:/n", i);
SecNameIndex = (unsigned int)s_hdr_ptr->sh_name;
fprintf(stdout, "(Section name (string tbl index))sh_name: 0x%08X -> %s/n",
s_hdr_ptr->sh_name, SecNameStrTable + SecNameIndex);
fprintf(stdout, "(Section type)sh_type: 0x%08X/n",
s_hdr_ptr->sh_type);
fprintf(stdout, "(Section flags)sh_flags: 0x%08X/n",
s_hdr_ptr->sh_flags);
fprintf(stdout, "(Section virtual addr at execution)sh_addr: 0x%08X/n",
s_hdr_ptr->sh_addr);
fprintf(stdout, "(Section file offset)sh_offset: 0x%08X/n",
s_hdr_ptr->sh_offset);
fprintf(stdout, "(Section size in bytes)sh_size: 0x%08X/n",
s_hdr_ptr->sh_size);
fprintf(stdout, "(Link to another section)sh_link: 0x%08X/n",
s_hdr_ptr->sh_link);
fprintf(stdout, "(Additional section information)sh_info: 0x%08X/n",
s_hdr_ptr->sh_info);
fprintf(stdout, "(Section alignment)sh_addralign: 0x%08X/n",
s_hdr_ptr->sh_addralign);
fprintf(stdout, "(Entry size if section holds table)sh_entsize: 0x%08X/n",
s_hdr_ptr->sh_entsize);
}

fprintf(stdout,
"************************************************************************/n");

if ( lseek(fd, (off_t)DebugInfoStrTblFileOffset, SEEK_SET) !=
DebugInfoStrTblFileOffset || DebugInfoStrTblFileOffset == 0 ) {
fprintf(stderr, "lseek to Debug Info String Table error./n");
goto next;
}
read(fd, DebugInfoStrTable, (size_t)(DebugInfoStrTblSize + 1));

if ( lseek(fd, (off_t)DebugInfoFileOffset, SEEK_SET) !=
DebugInfoFileOffset || DebugInfoFileOffset == 0 ) {
fprintf(stderr, "lseek to Debug Info Symbol Table error./n");
goto next;
}
for (i = 0; i < DebugInfoSymTblNum; i++) {
read(fd, SymTblEntry, (size_t)16);
symptr = (myElf32_Sym *)SymTblEntry;
fprintf(stdout, "Debug Infomation -> Symbol ID: %d/n", i);
fprintf(stdout, "Symbol_index_and_name: 0x%08X -> %s/n",
symptr->st_name, DebugInfoStrTable + symptr->st_name);
fprintf(stdout, "Symbol_value: 0x%08X/n", symptr->st_value);
fprintf(stdout, "Symbol_size: 0x%08X/n", symptr->st_size);
fprintf(stdout, "Symbol_type_and_binding: 0x%02X/n", symptr->st_info);
fprintf(stdout, "Section_index: 0x%04X/n", symptr->st_shndx);
fprintf(stdout,
"--------------------------------------------------------/n");
}

fprintf(stdout,
"************************************************************************/n");

next:

/* 讀取 .dynstr 節的內容,並緩存在一個緩衝區中。*/
if ( lseek(fd, (off_t)SymNamStrTblFileOffset, SEEK_SET) !=
SymNamStrTblFileOffset || SymNamStrTblFileOffset == 0 ) {
fprintf(stderr, "lseek to Dynamical symbol name string error./n");
close(fd); exit(1);
}
read(fd, SymNamStrTable, (size_t)(SymNamStrTblSize + 1));
if ( lseek(fd, (off_t)SymTblFileOffset, SEEK_SET) != SymTblFileOffset ||
SymTblFileOffset == 0 ) {
fprintf(stderr, "lseek to Dynamical symbol Table error./n");
close(fd); exit(1);
}
for (i = 0; i < SymTblNum; i++) {
read(fd, SymTblEntry, (size_t)16);
symptr = (myElf32_Sym *)SymTblEntry;
fprintf(stdout, "Symbol ID: %d/n", i);
fprintf(stdout, "Symbol_index_and_name: 0x%08X -> %s/n",
symptr->st_name, SymNamStrTable + symptr->st_name);
fprintf(stdout, "Symbol_value: 0x%08X/n", symptr->st_value);
fprintf(stdout, "Symbol_size: 0x%08X/n", symptr->st_size);
fprintf(stdout, "Symbol_type_and_binding: 0x%02X/n", symptr->st_info);
fprintf(stdout, "Section_index: 0x%04X/n", symptr->st_shndx);
fprintf(stdout,
"--------------------------------------------------------/n");
}

fprintf(stdout,
"************************************************************************/n");

if ( lseek(fd, (off_t)HashOffset, SEEK_SET) != HashOffset ||
HashOffset == 0 ) {
fprintf(stderr, "lseek to hash table error./n");
close(fd); exit(-1);
}
for (i = 0; i < HashTblNum; i++) {
fprintf(stdout, "Hash Table ID: %d/n", i);
read(fd, SymTblEntry, (size_t)4);
HashSymPtr = (myElf32_Syminfo *)SymTblEntry;
fprintf(stdout, "Direct_bindings, symbol_bound_to: 0x%04X/n",
HashSymPtr->si_boundto);
fprintf(stdout, "Per_symbol_flags: 0x%04X/n", HashSymPtr->si_flags);
fprintf(stdout,
"--------------------------------------------------------/n");
}

close(fd); /* End of Printing */

/* Change symbol value, hook it */

if ( (fd = open(argv[1], O_RDWR)) < 0 ) {
fprintf(stderr, "Can't open object file '%s'./n", argv[1]);
exit(-1);
}

/* 檢索符號表搜索要修改的符號。*/
if ( lseek(fd, (off_t)SymTblFileOffset, SEEK_SET) != SymTblFileOffset ) {
fprintf(stderr, "lseek error./n");
close(fd); exit(-1);
}

for (i = 0; i < SymTblNum; i++) {
read(fd, SymTblEntry, (size_t)16);
symptr = (myElf32_Sym *)SymTblEntry;
if ( strcmp(argv[2], SymNamStrTable + symptr->st_name) == 0 ) {
memcpy(dst_sym, SymTblEntry, 16);
dst_sym_tbl = (unsigned int)lseek(fd, (off_t)0, SEEK_CUR) - 16;
}
if ( strcmp(argv[3], SymNamStrTable + symptr->st_name) == 0 ) {
memcpy(src_sym, SymTblEntry, 16);
src_sym_tbl = (unsigned int)lseek(fd, (off_t)0, SEEK_CUR) - 16;
}
}

/* 修改符號表中要修改的符號所對應的表項的 st_value 和 st_size 值。*/
symptr = (myElf32_Sym *)src_sym;
memcpy(tmp_sym_addr, &symptr->st_value, 4);
memcpy(tmp_sym_size, &symptr->st_size, 4);
symptr = (myElf32_Sym *)dst_sym;
memcpy(&symptr->st_value, tmp_sym_addr, 4);
memcpy(&symptr->st_size, tmp_sym_size, 4);

if ( dst_sym_tbl == 0 || src_sym_tbl == 0 ||
lseek(fd, (off_t)dst_sym_tbl, SEEK_SET) != dst_sym_tbl ) {
fprintf(stderr, "lseek error./n");
close(fd); exit(-1);
}
if ( write(fd, dst_sym, (size_t)16) != (ssize_t)16 ) {
fprintf(stderr, "write error/n");
close(fd); exit(-1);
}

close(fd);
return 0;
}

/* EOF */
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章