歡迎來到Linux教程網
Linux教程網
Linux教程網
Linux教程網
您现在的位置: Linux教程網 >> UnixLinux >  >> Linux基礎 >> 關於Linux

LinuxELF文件格式詳解--Linux進程的管理與調度(十二)

對象文件


首先,你需要知道的是所謂對象文件(Object files)有三個種類:
可重定位的對象文件(Relocatable file)
可執行的對象文件(Executable file)
可被共享的對象文件(Shared object file)
可重定位的對象文件(Relocatable file)

適於鏈接的可重定位文件(relocatable file),包含二進制代碼和數據,能與其他可重定位對象文件在編譯時合並創建出一個可執行文件。

這是由匯編器匯編生成的 .o 文件。後面的鏈接器(link editor)拿一個或一些 Relocatable object files 作為輸入,經鏈接處理後,生成一個可執行的對象文件 (Executable file) 或者一個可被共享的對象文件(Shared object file)。我們可以使用 ar 工具將眾多的 .o Relocatable object files 歸檔(archive)成 .a 靜態庫文件。如何產生 Relocatable file,你應該很熟悉了,請參見我們相關的基本概念文章和JulWiki。另外,可以預先告訴大家的是我們的內核可加載模塊 .ko 文件也是 Relocatable object file。

可執行的對象文件(Executable file)

適於執行的可執行文件(executable file),包含可以直接拷貝進行內存執行的二進制代碼和數據。用於提供程序的進程映像,加載的內存執行。

這我們見的多了。文本編輯器vi、調式用的工具gdb、播放mp3歌曲的軟件mplayer等等都是Executable object file。你應該已經知道,在我們的 Linux 系統裡面,存在兩種可執行的東西。除了這裡說的 Executable object file,另外一種就是可執行的腳本(如shell腳本)。注意這些腳本不是 Executable object file,它們只是文本文件,但是執行這些腳本所用的解釋器就是 Executable object file,比如 bash shell 程序。

可被共享的對象文件(Shared object file)

共享目標文件(shared object file),一種特殊的可重定位對象文件,能在加載時或運行時,裝載進內存進行動態鏈接。連接器可將它與其它可重定位文件和共享目標文件連接成其它的目標文件,動態連接器又可將它與可執行文件和其它共享目標文件結合起來創建一個進程映像。

這些就是所謂的動態庫文件,也即 .so 文件。如果拿前面的靜態庫來生成可執行程序,那每個生成的可執行程序中都會有一份庫代碼的拷貝。如果在磁盤中存儲這些可執行程序,那就會占用額外的磁盤空間;另外如果拿它們放到Linux系統上一起運行,也會浪費掉寶貴的物理內存。如果將靜態庫換成動態庫,那麼這些問題都不會出現。動態庫在發揮作用的過程中,必須經過兩個步驟:

鏈接編輯器(link editor)拿它和其他Relocatable object file以及其他shared object file作為輸入,經鏈接處理後,生存另外的 shared object file 或者 executable file。

在運行時,動態鏈接器(dynamic linker)拿它和一個Executable file以及另外一些 Shared object file 來一起處理,在Linux系統裡面創建一個進程映像。

文件格式


本質上,對象文件只是保存在磁盤文件中的一串字節,每個系統的文件格式都不盡相同:

Bell實驗室的第一個Unix系統使用 a.out格式。

System V Unix的早期版本使用 Common Object File Format(COFF)。

Windows NT使用COFF的變種,叫做 Portable Executable(PE)。

現代Unix系統,包括Linux、新版System V、BSD變種、Solaris都使用 Executable and Linkable Format(ELF)。

ELF對象文件格式


ELF(Executable and Linking Format)是一種對象文件的格式,用於定義不同類型的對象文件(Object files)中都放了什麼東西、以及都以什麼樣的格式去放這些東西。它自最早在 System V 系統上出現後,被 xNIX 世界所廣泛接受,作為缺省的二進制文件格式來使用。可以說,ELF是構成眾多xNIX系統的基礎之一。

ELF代表Executable and Linkable Format。他是一種對可執行文件、目標文件和庫使用的文件格式。

他在Linux下成為標准格式已經很長時間, 代替了早年的a.out格式。ELF一個特別的優點在於, 同一文件格式可以用於內核支持的幾乎所有體系結構上, 這不僅簡化了用戶空間工具程序的創建, 也簡化了內核自身的程序設計。例如, 在必須為可執行文件生成裝載程序例程時。

但是文件格式相同並不意味著不同系統上的程序之間存在二進制兼容性, 例如, FreeBSD和Linux都使用ELF作為二進制格式。盡管二者在文件中組織數據的方式相同。但在系統調用機制以及系統調用的語義方面, 仍然有差別。這也是在沒有中間仿真層的情況下, FreeBSD程序不能在linux下運行的原因(反過來同樣是如此)。

有一點是可以理解的, 二進制程序不能在不同體系結構交換(例如, 為Alpha CPU編譯的Linux二進制程序不能在Sparc Linux上執行), 因為底層的體系結構是完全不同的。但是由於ELF的存在, 對所有體系結構而言, 程序本身的相關信息以及程序的各個部分在二進制文件中編碼的方式都是相同的。

Linux不僅將ELF用於用戶空間程序和庫, 還用於構建模塊。內核本身也是ELF格式。

ELF文件標准歷史


ELF是一種開放格式, 其規范可以自由獲取。

在ELF格式出來之後,TISC(Tool Interface Standard Committee)委員會定義了一套ELF標准。你可以從這裡(http://refspecs.freestandards.org/elf/)找到詳細的標准文檔

20世紀90年代,一些廠商聯合成立了一個委員會(TISC委員會),起草並發布了一個ELF文件格式標准供公開使用,並且希望所有人能夠遵循這項標准並且從中獲益。1993年,委員會發布了ELF文件標准。當時參與該委員會的有來自於編譯器的廠商,如Watcom和Borland;來自CPU的廠商如IBM和Intel;來自操作系統的廠商如IBM和Microsoft。1995年,委員會發布了ELF 1.2標准,自此委員會完成了自己的使命,不久就解散了。所以ELF文件格式標准的最新版本為1.2。

文件類型 e_type成員表示ELF文件類型,即前面提到過的3種ELF文件類型,每個文件類型對應一個常量。系統通過這個常量來判斷ELF的真正文件類型,而不是通過文件的擴展名。相關常量以“ET_”開頭,

TISC委員會前後出了兩個版本,v1.1和v1.2。兩個版本內容上差不多,但就可讀性上來講,我還是推薦你讀 v1.2的。因為在v1.2版本中,TISC重新組織原本在v1.1版本中的內容,將它們分成為三個部分(books):
a) Book I
介紹了通用的適用於所有32位架構處理器的ELF相關內容
b) Book II
介紹了處理器特定的ELF相關內容,這裡是以Intel x86 架構處理器作為例子介紹
c) Book III
介紹了操作系統特定的ELF相關內容,這裡是以運行在x86上面的 UNIX System V.4 作為例子介紹

值得一說的是,雖然TISC是以x86為例子介紹ELF規范的,但是如果你是想知道非x86下面的ELF實現情況,那也可以在http://refspecs.freestandards.org/elf/中找到特定處理器相關的Supplment文檔。比方ARM相關的,或者MIPS相關的等等。另外,相比較UNIX系統的另外一個分支BSD Unix,Linux系統更靠近 System V 系統。所以關於操作系統特定的ELF內容,你可以直接參考v1.2標准中的內容。

本文所使用的測試程序結構


add.c


#include 
#include 

// 不指定寄存器實現兩個整數相加
int Add(int a, int b)
{
    __asm__ __volatile__
    (
        //"lock;\n"
        "addl %1,%0;\n"
        : "=m"(a)
        : "r"(b), "m"(a)
      //  :
    );

    return a;
}

sub.c


#include 
#include 

// 不指定寄存器實現兩個參數相減
int Sub(int a, int b)
{
    __asm__ __volatile__
    (
        "subl %1, %0;"
        : "=m"(a)
        : "r"(b), "m"(a)
 //       :
    );

    return a;
}

testelf.c


#include 
#include 

int main(void)
{
    int a = 3, b = 5;

    printf("%d + %d = %d\n", a, b, Add(a, b));
    printf("%d - %d = %d\n", a, b, Sub(a, b));


    return EXIT_SUCCESS;
}

Makefile



target=testelf_normal testelf_dynamic testelf_static

MAIN_OBJS=testelf.o
SUBS_OBJS=add.o sub.o

DYNA_FILE=libtestelf.so
STAT_FILE=libtestelf.a

all:$(target)

%.o : %.c
    $(CC) -c $^ -o $@

clean :
    rm -rf $(MAIN_OBJS) $(SUBS_OBJS)
    rm -rf $(DYNA_FILE) $(STAT_FILE)
    rm -rf $(target)


# Complie the execute
testelf_normal:$(MAIN_OBJS) $(SUBS_OBJS)
    gcc $^ -o $@

testelf_dynamic:$(MAIN_OBJS) $(DYNA_FILE)
    gcc  $^ -o $@ -L./ -ltestelf

testelf_static:$(MAIN_OBJS) $(STAT_FILE)
    gcc  testelf.o -o $@ -static -L./ -ltestelf



# Complie the Dynamic Link Library libtestelf.so
libtestelf.so:$(SUBS_OBJS)
    gcc -fPCI -shared $^ -o $@

# Complie the Static Link Library libtestelf.so
libtestelf.a:$(SUBS_OBJS)
    ar -r $@ $^

我們編寫了兩個庫函數分別實現add和sub的功能, 然後編寫了一個測試代碼testelf.c調用了Add和Sub.

然後我們的Mmakefile為測試程序編寫了3分程序

普通的程序testelf_normal, 由add.o sub.o 和testelf.o直接鏈接生成

動態鏈接程序testelf_dynamic, 將add.o和sub.o先鏈接成動態鏈接庫libtestelf.so, 然後再動態鏈接生成testelf_dynamic

靜態鏈接程序testelf_static, 將add.o和sub.o先靜態鏈接成靜態庫libtestelf.a, 然後再靜態鏈接生成可執行程序testelf_staticke

我們在源代碼目錄執行make後會完成編譯, 編譯完成後

add.o, sub.o和testelf.o是可重定位的對象文件(Relocatable file)

libtestelf.so是可被共享的對象文件(Shared object file)

testelf_normal, testelf_dynamic和testelf_static是可執行的對象文件(Executable file)

如下圖所示

對象文件

ELF可執行與鏈接文件格式詳解


布局和結構


ELF文件由各個部分組成。

為了方便和高效,ELF文件內容有兩個平行的視角:一個是程序連接角度,另一個是程序運行角度

elf文件的布局和結構

首先圖的左邊部分,它是以鏈接視圖來看待elf文件的, 從左邊可以看出,包含了一個ELF頭部,它描繪了整個文件的組織結構。它還包括很多節區(section)。這些節有的是系統定義好的,有些是用戶在文件在通過.section命令自定義的,鏈接器會將多個輸入目標文件中的相同的節合並。節區部分包含鏈接視圖的大量信息:指令、數據、符號表、重定位信息等等。除此之外,還包含程序頭部表(可選)和節區 頭部表,程序頭部表,告訴系統如何創建進程映像。用來構造進程映像的目標文件必須具有程序頭部表,可重定位文件不需要這個表。而節區頭部表(Section Heade Table)包含了描述文件節區的信息,每個節區在表中都有一項,每一項給出諸如節區名稱、節區大小這類信息。用於鏈接的目標文件必須包含節區頭部表,其他目標文件可以有,也可以沒有這個表。

需要注意地是:盡管圖中顯示的各個組成部分是有順序的,實際上除了 ELF 頭部表以外,其他節區和段都沒有規定的順序。

右半圖是以程序執行視圖來看待的,與左邊對應,多了一個段(segment)的概念,編譯器在生成目標文件時,通常使用從零開始的相對地址,而在鏈接過程中,鏈接器從一個指定的地址開始,根據輸入目標文件的順序,以段(segment)為單位將它們拼裝起來。其中每個段可以包括很多個節(section)。

elf頭部

除了用於標識ELF文件的幾個字節外, ELF頭還包含了有關文件類型和大小的有關信息, 以及文件加載後程序執行的入口點信息

程序頭表(program header table)

程序頭表向系統提供了可執行文件的數據在進程虛擬地址空間中組織文件的相關信息。他還表示了文件可能包含的段數據、段的位置和用途

段segment

各個段保存了與文件愛你相關的各種形式的數據, 例如,符號表、實際的二進制碼、固定值(如字符串)活程序使用的數值常數

節頭表section

包含了與各段相關的附加信息。

ELF基本數據類型定義


在具體介紹ELF的格式之前,我們先來了解在ELF文件中都有哪些數據類型的定義:

ELF數據編碼順序與機器相關,為了使數據結構更加通用, linux內核自定義了幾種通用的數據, 使得數據的表示與具體體系結構分離

但是由於32位程序和64位程序所使用的數據寬度不同, 同時64位機必須兼容的執行32位程序, 因此我們所有的數據都被定義為32bit和64bit兩個不同類型的數據

常規定義在include/uapi/linux中, 各個結構也可以按照需求重新定義

32位機器上的定義

名稱 常規定義 大小 對齊 目的 Elf32_Addr __u32 4 4 無符號程序地址 Elf32_Half __u16 2 2 無符號中等整數 Elf32_Off __u32 4 4 無符號文件偏移 Elf32_SWord __u32 4 4 有符號大整數 Elf32_Word __u32 4 4 無符號大整數 unsigned char 無 1 1 無符號小整數

64位機器上的定義*

名稱 常規定義 大小 對齊 目的 Elf64_Addr __u64 8 8 無符號程序地址 Elf64_Half __u16 2 2 無符號小整數 Elf64_SHalf __s16 2 2 無符號小整數 Elf64_Off __u64 8 8 無符號文件偏移 Elf64_Sword __s32 4 4 有符號中等整數 Elf64_Word __u32 4 4 無符號中等整數 Elf64_Xword __u64 8 8 無符號大整數 Elf64_Sxword __s64 8 8 有符號大整數 unsigned char 無 1 1 無符號小整數

ELF頭部Elfxx_Ehdr


elf頭部用Elfxx_Ehdr結構(被定義在linux/uapi/linux/elf.h來表示, Elf32_Ehdr(32bit)和Elf64_Ehdr(64bit)

數據成員


內部成員, 如下

成員 類型 描述 e_ident[EI_NIDENT] unsigned char 目標文件標識信息, EI_NIDENT=16, 因此共占用128位 e_type Elf32_Half/Elf64_Half 目標文件類型 e_machine Elf32_Half/Elf64_Half 目標體系結構類型 e_version Elf32_Word/Elf64_Word 目標文件版本 e_entry Elf32_Addr/Elf64_Addr 程序入口的虛擬地址,若沒有,可為0 e_phoff Elf32_Off/Elf64_Off 程序頭部表格(Program Header Table)的偏移量(按字節計算),若沒有,可為0 e_shoff Elf32_Off/Elf64_Off 節區頭部表格(Section Header Table)的偏移量(按字節計算),若沒有,可為0 e_flags Elf32_Word/Elf64_Word 保存與文件相關的,特定於處理器的標志。標志名稱采用 EF_machine_flag的格式 e_ehsize Elf32_Half/Elf64_Half ELF 頭部的大小(以字節計算) e_phentsize Elf32_Half/Elf64_Half 程序頭部表格的表項大小(按字節計算) e_phnum Elf32_Half/Elf64_Half 程序頭部表格的表項數目。可以為 0 e_shentsize Elf32_Half/Elf64_Half 節區頭部表格的表項大小(按字節計算) e_shnum Elf32_Half/Elf64_Half 節區頭部表格的表項數目。可以為 0 e_shstrndx Elf32_Half/Elf64_Half 節區頭部表格中與節區名稱字符串表相關的表項的索引。如果文件沒有節區名稱字符串表,此參數可以為 SHN_UNDEF

ELF魔數e_ident


魔數

很多類型的文件,其起始的幾個字節的內容是固定的(或是有意填充,或是本就如此)。根據這幾個字節的內容就可以確定文件類型,因此這幾個字節的內容被稱為魔數 (magic number)。此外在一些程序代碼中,程序員常常將在代碼中出現但沒有解釋的數字常量或字符串稱為魔數 (magic number)或魔字符串。

ELF魔數 我們可以從前面readelf的輸出看到,最前面的”Magic”的16個字節剛好對應“Elf32_Ehdr”的e_ident這個成員。這16個字節被ELF標准規定用來標識ELF文件的平台屬性,比如這個ELF字長(32位/64位)、字節序、ELF文件版本

最開始的4個字節是所有ELF文件都必須相同的標識碼,分別為0x7F、0x45、0x4c、0x46
第一個字節對應ASCII字符裡面的DEL控制符,
後面3個字節剛好是ELF這3個字母的ASCII碼。這4個字節又被稱為ELF文件的魔數,幾乎所有的可執行文件格式的最開始的幾個字節都是魔數。

比如a.out格式最開始兩個字節為 0x01、0x07;

PE/COFF文件最開始兩個個字節為0x4d、0x5a,即ASCII字符MZ。

這種魔數用來確認文件的類型,操作系統在加載可執行文件的時候會確認魔數是否正確,如果不正確會拒絕加載。

接下來的一個字節是用來標識ELF的文件類的,0x01表示是32位的,0x02表示是64位的;第6個字是字節序,規定該ELF文件是大端的還是小端的(見附錄:字節序)。第7個字節規定ELF文件的主版本號,一般是1,因為ELF標准自1.2版以後就再也沒有更新了。後面的9個字節ELF標准沒有定義,一般填0,有些平台會使用這9個字節作為擴展標志。

各種魔數的由來

a.out格式的魔數為0x01、0x07,為什麼會規定這個魔數呢?

UNIX早年是在PDP小型機上誕生的,當時的系統在加載一個可執行文件後直接從文件的第一個字節開始執行,人們一般在文件的最開始放置一條跳轉(jump)指令,這條指令負責跳過接下來的7個機器字的文件頭到可執行文件的真正入口。而0x01 0x07這兩個字節剛好是當時PDP-11的機器的跳轉7個機器字的指令。為了跟以前的系統保持兼容性,這條跳轉指令被當作魔數一直被保留到了幾十年後的今天。

計算機系統中有很多怪異的設計背後有著很有趣的歷史和傳統,了解它們的由來可以讓我們了解到很多很有意思的事情。這讓我想起了經濟學裡面所謂的“路徑依賴”,其中一個很有意思的叫“馬屁股決定航天飛機”的故事在網上流傳很廣泛,有興趣的話你可以在google以“馬屁股”和“航天飛機”作為關鍵字搜索一下。
其中需要注意地是e_ident是一個16字節的數組,這個數組按位置從左到右都是有特定含義,每個數組元素的下標在標准中還存在別稱,如byte0的下標0別名為EI_MAG0,具體如下:

名稱 元素下標值 含義 EI_MAG0 0 文件標識 EI_MAG1 1 文件標識 EI_MAG2 2 文件標識 EI_MAG3 3 文件標識 EI_CLASS 4 文件類 EI_DATA 5 數據編碼 EI_VERSION 6 文件版本 EI_PAD 7 補齊字節開始處 EI_NIDENT 16 e_ident[]大小

e_ident[EI_MAG0]~e_ident[EI_MAG3]即e_ident[0]~e_ident[3]被稱為魔數(Magic Number),其值一般為0x7f,’E’,’L’,’F’

e_ident[EI_CLASS](即e_ident[4])識別目標文件運行在目標機器的類別,取值可為三種值:

名稱 元素下標值 含義 ELFCLASSNONE 0 非法類別 ELFCLASS32 1 32位目標 ELFCLASS64 2 64位目標

e_ident[EI_DATA](即e_ident[5]):給出處理器特定數據的數據編碼方式。即大端還是小端方式。取值可為3種:

名稱 元素下標值 含義 ELFDATANONE 0 非法數據編碼 ELFDATA2LSB 1 高位在前 ELFDATA2MSB 2 低位在前

目標文件類型e_type


e_type表示elf文件的類型

文件類型 e_type成員表示ELF文件類型,即前面提到過的3種ELF文件類型,每個文件類型對應一個常量。系統通過這個常量來判斷ELF的真正文件類型,而不是通過文件的擴展名。相關常量以“ET_”開頭

如下定義:

名稱 取值 含義 ET_NONE 0 未知目標文件格式 ET_REL 1 可重定位文件 ET_EXEC 2 可執行文件 ET_DYN 3 共享目標文件 ET_CORE 4 Core 文件(轉儲格式) ET_LOPROC 0xff00 特定處理器文件 ET_HIPROC 0xffff 特定處理器文件 ET_LOPROC~ET_HIPROC 0xff00~0xffff 特定處理器文件

目標體系結構類型e_machine


e_machine表示目標體系結構類型

名稱 取值 含義 EM_NONE 0 未指定 EM_M32 1 AT&T WE 32100 EM_SPARC 2 SPARC EM_386 3 Intel 80386 EM_68K 4 Motorola 68000 EM_88K 5 Motorola 88000 EM_860 7 Intel 80860 EM_MIPS 8 MIPS RS3000 others 9~ 預留

ELF版本e_version


這個用來區分ELF標准的各個修訂版本, 但是前面提到ELF最新版本就是1(1.2), 仍然是最新版, 因此目前不需要這個特性

另外ELF頭還包括了ELF文件的各個其他部分的長度和索引位置信息。因為這些部分的長度可能依程序而不同。所以在文件頭部必須提供相應的數據.

readelf -h查看elf頭部


可重定位的對象文件(Relocatable file)


readelf -h add.o

REL的elf文件頭

文件類型是, 說明是可重定位文件, 其代碼可以移動至任何位置.

該文件沒有程序頭表, 對需要進行鏈接的對象而言, 程序頭表是不必要的, 為此所有長度都設置為0

可執行的對象文件(Executable file)


readelf -h testelf_dynamic

exec的elf文件頭

可被共享的對象文件(Shared object file)


readelf -h libtestelf.so

dynamic的elf文件頭

程序頭部Elf32_phdr


以程序運行的角度看ELF文件, 就需要程序頭表,即要運行這個elf文件,需要將哪些東西載入到內存鏡像。而節區頭部表是以elf資源的角度來看待elf文件的,即這個elf文件到底存在哪些資源,以及這些資源之間的關聯關系,

程序頭部是一個表,它的起始地址在elf頭部結構中的e_phoff成員指定,數量由e_phnum表示,每個程序頭部表項的大小由e_phentsize指出。

可執行文件或者共享目標文件的程序頭部是一個結構數組,每個結構描述了一個段或者系統准備程序執行所必需的其它信息。目標文件的”段”包含一個或者多個”節區”,也就是”
段內容(Segment Contents)”。程序頭部僅對於可執行文件和共享目標文件有意義。

下面來看程序頭號部表項的數據結構

成員 類型 描述 p_type Elf32_Word/Elf64_Word 段類型 p_offset Elf32_Off/Elf64_Off 段位置 p_vaddr Elf32_Addr/Elf64_Addr 給出段的第一個字節將被放到內存中的虛擬地址 p_paddr Elf32_Addr/Elf64_Addr 僅用於與物理地址相關的系統中 p_filesz Elf32_Word/Elf64_Word 給出段在文件映像中所占的字節數 p_memsz Elf32_Word/Elf64_Word 給出段在內存映像中占用的字節數 p_flags Elf32_Word/Elf64_Word 與段相關的標志 p_align Elf32_Word/Elf64_Word 對齊

段類型p_type


名稱 取值 說明 PT_NULL 0 此數組元素未用。結構中其他成員都是未定義的 PT_DYNAMIC 2 數組元素給出動態鏈接信息 PT_INTERP 3 數組元素給出一個 NULL 結尾的字符串的位置和長度,該字符串將被當作解釋器調用。這種段類型僅對與可執行文件有意義(盡管也可能在共享目標文件上發生)。在一個文件中不能出現一次以上。如果存在這種類型的段,它必須在所有可加載段項目的前面 PT_NOTE 4 此數組元素給出附加信息的位置和大小 PT_SHLIB 5 此段類型被保留,不過語義未指定。包含這種類型的段的程序與 ABI不符 PT_PHDR 6 此類型的數組元素如果存在,則給出了程序頭部表自身的大小和位置,既包括在文件中也包括在內存中的信息。此類型的段在文件中不能出現一次以上。並且只有程序頭部表是程序的內存映像的一部分時才起作用。如果存在此類型段,則必須在所有可加載段項目的前面 PT_LOPROC~PT_HIPROC 0x70000000~0x7fffffff 此范圍的類型保留給處理器專用語義

readelf -l查看程序頭表

在程序頭表之後, 列出了6個段, 這些組成了最終在內存中執行的程序.

其還提供了各段在虛擬地址空間和物理空間的大小, 位置, 標志, 訪問權限和對齊方面的信息. 還指定了yui個類型來更精確的描述段.

各段的語義如下

段 描述 PHDR 保存了程序頭表 INTERP 指定在程序已經從可執行文件映射到內存之後, 必須調用的解釋器. 在這裡解釋器並不意味著二進制文件的內容必須解釋執行(比如Java的字節碼需要Java虛擬機解釋).它指的是這樣一個程序:通過鏈接其他庫, 來滿足未解決的引用.
通常/lib/ld-linux.so.2, /lib/ld-linux-ia-64.so.2等庫, 用於在虛擬地址空間中國插入程序運行所需的動態庫. 對幾乎所有的程序來說, 可能C標准庫都是必須映射的.還需要添加的各種庫, 如GTK, QT, 數學庫math, 線程庫pthread等等 LOAD 表示一個需要從二進制文件映射到虛擬地址的段. 其中保存了常量數據(如字符串), 程序的目標代碼等 DYNAMIC 該段保存了由動態鏈接器(即, INTERP中指定的解釋器)使用的信息 NOTE 保存了專有信息

可重定位的對象文件(Relocatable file)


readelf -l add.o

rel的elf程序頭表

可重定向文件, 是一個需要鏈接的對象, 程序頭表對其而言不是必要的, 因此這類文件一般沒有程序頭表

可執行的對象文件(Executable file)


readelf -l testelf_dynamic

exec的elf程序頭表

可被共享的對象文件(Shared object file)


readelf -l libtestelf.so

dyn的elf程序頭表

虛擬地址空間中的各個段, 填充了來自ELF文件中特定的段的數據. 因而readelf輸出的第二部分指定了那些節載入到哪些段(節段映射).

物理地址信息講被忽略, 因為該信息是由內盒根據物理頁幀到虛擬地址空間中相應位置的映射情況動態分配的.只有在沒有MMU(因而沒有虛擬內存)的系統上該信息才是由意義的

節區(Sections)


節區中包含目標文件中的所有信息

除了:ELF 頭部、程序頭部表格、節區頭部表格。

節區滿足以下條件:

目標文件中的每個節區都有對應的節區頭部描述它,反過來,有節區頭部不意味著有節區。

每個節區占用文件中一個連續字節區域(這個區域可能長度為 0)。

文件中的節區不能重疊,不允許一個字節存在於兩個節區中的情況發生。

目標文件中可能包含非活動空間(INACTIVE SPACE)。這些區域不屬於任何頭部和節區,其內容未指定。

節區頭部表格


ELF文件在描述各段的內容時, 是指定了哪些節的數據映射到段中. 因此需要一個結構來管理各個節的內容, 即節頭表

節區頭部表是以elf資源的角度來看待elf文件的,即這個elf文件到底存在哪些資源,以及這些資源之間的關聯關系,而前面提到的程序頭部表,則以程序運行來看elf文件的,即要運行這個elf文件,需要將哪些東西載入到內存鏡像。

ELF 頭部中,

e_shoff 成員給出從文件頭到節區頭部表格的偏移字節數;

e_shnum給出表格中條目數目;

e_shentsize 給出每個項目的字節數。

從這些信息中可以確切地定位節區的具體位置、長度。

從之前的描述中可知,每一項節區在節區頭部表格中都存在著一項元素與它對應,因此可知,這個節區頭部表格為一連續的空間,每一項元素為一結構體

那麼這個節區頭部由elfxx_shdr(定義在include/uapi/linux/elf.h), 32位elf32_shdr, 64位elf64_shdr

結構體的成員如下

成員 類型 描述 sh_name Elf32_Word/Elf64_Word 節區名,是節區頭部字符串表節區(Section Header String Table Section)的索引。名字是一個 NULL 結尾的字符串 sh_type Elf32_Word/Elf64_Word 節區類型 sh_flags Elf32_Word/Elf64_Word 節區標志 sh_addr Elf32_Addr/Elf64_Addr 如果節區將出現在進程的內存映像中,此成員給出節區的第一個字節應處的位置。否則,此字段為 0 sh_offset Elf32_Off/Elf64_Off 此成員的取值給出節區的第一個字節與文件頭之間的偏移 sh_size Elf32_Word/Elf64_Word 此成員給出節區的長度(字節數) sh_link Elf32_Word/Elf64_Word 此成員給出節區頭部表索引鏈接。其具體的解釋依賴於節區類型 sh_info Elf32_Word/Elf64_Word 此成員給出附加信息,其解釋依賴於節區類型 sh_addralign Elf32_Word/Elf64_Word 某些節區帶有地址對齊約束 sh_entsize Elf32_Word/Elf64_Word 某些節區中包含固定大小的項目,如符號表。對於這類節區,此成員給出每個表項的長度字節數

節區類型sh_type


sh_type的取值如下:

名稱 取值 說明 SHT_NULL 0 此值標志節區頭部是非活動的,沒有對應的節區。此節區頭部中的其他成員取值無意義 SHT_PROGBITS 1 此節區包含程序定義的信息,其格式和含義都由程序來解釋 SHT_SYMTAB 2 此節區包含一個符號表。目前目標文件對每種類型的節區都只能包含一個,不過這個限制將來可能發生變化
一般,SHT_SYMTAB 節區提供用於鏈接編輯(指 ld 而言)的符號,盡管也可用來實現動態鏈接 SHT_STRTAB 3 此節區包含字符串表。目標文件可能包含多個字符串表節區 SHT_RELA 4 此節區包含重定位表項,其中可能會有補齊內容(addend),例如 32 位目標文件中的 Elf32_Rela 類型。目標文件可能擁有多個重定位節區 SHT_HASH 5 此節區包含符號哈希表。所有參與動態鏈接的目標都必須包含一個符號哈希表。目前,一個目標文件只能包含一個哈希表,不過此限制將來可能會解除 SHT_DYNAMIC 6 此節區包含動態鏈接的信息。目前一個目標文件中只能包含一個動態節區,將來可能會取消這一限制 SHT_NOTE 7 此節區包含以某種方式來標記文件的信息 SHT_NOBITS 8 這種類型的節區不占用文件中的空間 , 其他方面和SHT_PROGBITS相似。盡管此節區不包含任何字節,成員sh_offset 中還是會包含概念性的文件偏移 SHT_REL 9 此節區包含重定位表項,其中沒有補齊(addends),例如 32 位目標文件中的 Elf32_rel 類型。目標文件中可以擁有多個重定位節區 SHT_SHLIB 10 此節區被保留,不過其語義是未規定的。包含此類型節區的程序與 ABI 不兼容 SHT_DYNSYM 11 作為一個完整的符號表,它可能包含很多對動態鏈接而言不必要的符號。因此,目標文件也可以包含一個 SHT_DYNSYM 節區,其中保存動態鏈接符號的一個最小集合,以節省空間 SHT_LOPROC X70000000 這一段(包括兩個邊界),是保留給處理器專用語義的 SHT_HIPROC OX7FFFFFFF 這一段(包括兩個邊界),是保留給處理器專用語義的 SHT_LOUSER 0X80000000 此值給出保留給應用程序的索引下界 SHT_HIUSER 0X8FFFFFFF 此值給出保留給應用程序的索引上界

節區標志sh_flags


sh_flag標志著此節區是否可以修改,是否可以執行,如下定義:

名稱 取值 含義 SHF_WRITE 0x1 節區包含進程執行過程中將可寫的數據 SHF_ALLOC 0x2 此節區在進程執行過程中占用內存。某些控制節區並不出現於目標文件的內存映像中,對於那些節區,此位應設置為 0 SHF_EXECINSTR 0x4 節區包含可執行的機器指令 SHF_MASKPROC 0xF0000000 所有包含於此掩碼中的四位都用於處理器專用的語義

sh_link和sh_info字段的具體含義依賴於sh_type的值

sh_type sh_link sh_info SHT_DYNAMIC 此節區中條目所用到的字符串表格的節區頭部索引 0 SHT_HASH 此哈希表所適用的符號表的節區頭部索引 0 SHT_REL
SHT_RELA 相關符號表的節區頭部索引 重定位所適用的節區的節區頭部索引 SHT_SYMTAB
SHT_DYNSYM 相關聯的字符串表的節區頭部索引 最後一個局部符號(綁定 STB_LOCAL)的符號表索引值加一 其它 SHN_UNDEF 0

特殊節區


有些節區是系統預訂的,一般以點開頭號,因此,我們有必要了解一些常用到的系統節區。

名稱 類型 屬性 含義 .bss SHT_NOBITS SHF_ALLOC +SHF_WRITE 包含將出現在程序的內存映像中的為初始化數據。根據定義,當程序開始執行,系統將把這些數據初始化為 0。此節區不占用文件空間 .comment SHT_PROGBITS (無) 包含版本控制信息 .data SHT_PROGBITS SHF_ALLOC + SHF_WRITE 這些節區包含初始化了的數據,將出現在程序的內存映像中 .data1 SHT_PROGBITS SHF_ALLOC + SHF_WRITE 這些節區包含初始化了的數據,將出現在程序的內存映像中 .debug SHT_PROGBITS (無) 此節區包含用於符號調試的信息 .dynamic SHT_DYNAMIC   此節區包含動態鏈接信息。節區的屬性將包含 SHF_ALLOC 位。是否 SHF_WRITE 位被設置取決於處理器 .dynstr SHT_STRTAB SHF_ALLOC 此節區包含用於動態鏈接的字符串,大多數情況下這些字符串代表了與符號表項相關的名稱 .dynsym SHT_DYNSYM SHF_ALLOC 此節區包含了動態鏈接符號表 .fini SHT_PROGBITS SHF_ALLOC + SHF_EXECINSTR 此節區包含了可執行的指令,是進程終止代碼的一部分。程序正常退出時,系統將安排執行這裡的代碼 .got SHT_PROGBITS   此節區包含全局偏移表 .hash SHT_HASH SHF_ALLOC 此節區包含了一個符號哈希表 .init SHT_PROGBITS SHF_ALLOC +SHF_EXECINSTR 此節區包含了可執行指令,是進程初始化代碼的一部分。當程序開始執行時,系統要在開始調用主程序入口之前(通常指 C 語言的 main 函數)執行這些代碼 .interp SHT_PROGBITS   此節區包含程序解釋器的路徑名。如果程序包含一個可加載的段,段中包含此節區,那麼節區的屬性將包含 SHF_ALLOC 位,否則該位為 0 .line SHT_PROGBITS (無) 此節區包含符號調試的行號信息,其中描述了源程序與機器指令之間的對應關系。其內容是未定義的 .note SHT_NOTE (無) 此節區中包含注釋信息,?9C?獨立的格式。 .plt SHT_PROGBITS   此節區包含過程鏈接表(procedure linkage table) .relname
.relaname SHT_REL
SHT_RELA   這些節區中包含了重定位信息。如果文件中包含可加載的段,段中有重定位內容,節區的屬性將包含 SHF_ALLOC 位,否則該位置 0。傳統上 name 根據重定位所適用的節區給定。例如 .text 節區的重定位節區名字將是:.rel.text 或者 .rela.text .rodata
.rodata1 SHT_PROGBITS SHF_ALLOC 這些節區包含只讀數據,這些數據通常參與進程映像的不可寫段 .shstrtab SHT_STRTAB   此節區包含節區名稱 .strtab SHT_STRTAB   此節區包含字符串,通常是代表與符號表項相關的名稱。如果文件擁有一個可加載的段,段中包含符號串表,節區的屬性將包含SHF_ALLOC 位,否則該位為 0 .symtab SHT_SYMTAB   此節區包含一個符號表。如果文件中包含一個可加載的段,並且該段中包含符號表,那麼節區的屬性中包含SHF_ALLOC 位,否則該位置為 0 .text SHT_PROGBITS SHF_ALLOC + SHF_EXECINSTR 此節區包含程序的可執行指令

readelf -S查看節區頭表


可重定位的對象文件(Relocatable file)


readelf -S add.o

rel的elf節區頭表

可重定向文件, 是一個需要鏈接的對象, 程序頭表對其而言不是必要的, 因此這類文件一般沒有程序頭表

可執行的對象文件(Executable file)


readelf -S testelf_dynamic

rel的elf節區頭表

可被共享的對象文件(Shared object file)


readelf -S libtestelf.so

dyn的elf程序頭表

字符串表


字符串表


首先要知道,字符串表它本身就是一個節區,從第二章描述中可知,每一個節區都存在一個節區頭部表項與之對應,所以字符串表這個節區也存在一個節區頭部表項對應,而在elf文件頭部結構中存在一個成員e_shstrndx給出這個節區頭部表項的索引位置。因此可以通過

shstrab  = (rt_uint8_t *)module_ptr +shdr[elf_module->e_shstrndx].sh_offset;

來得到字符串表的起始位置。
字符串表節區包含以 NULL(ASCII 碼 0)結尾的字符序列,通常稱為字符串。ELF目標文件通常使用字符串來表示符號和節區名稱。對字符串的引用通常以字符串在字符
串表中的下標給出。

一般,第一個字節(索引為 0)定義為一個空字符串。類似的,字符串表的最後一個字節也定義為 NULL,以確保所有的字符串都以 NULL 結尾。索引為 0 的字符串在
不同的上下文中可以表示無名或者名字為 NULL 的字符串。

允許存在空的字符串表節區,其節區頭部的 sh_size 成員應該為 0。對空的字符串表而言,非 0 的索引值是非法的。

例如:對於各個節區而言,節區頭部的 sh_name 成員包含其對應的節區頭部字符串表節區的索引,此節區由 ELF 頭的 e_shstrndx 成員給出。下圖給出了包含 25 個字節的一個字符串表,以及與不同索引相關的字符串。

字符

那麼上面字符串表包含以下字符串:

索引 字符串 0 (無) 1 name. 7 Variable 11 able 16 able 24 (空字符串)

符號表(Symbol Table)


首先,符號表同樣本身是一節區,也存在一對應節區頭部表項。

目標文件的符號表中包含用來定位、重定位程序中符號定義和引用的信息。

符號表索引是對此數組的索引。索引0表示表中的第一表項,同時也作為未定義符號的索引。

符號表是由一個個符號元素組成,用elfxx_sym來結構來表示, 定義在include/uapi/linux/elf.h, 同樣32位為elf32_sym, 64位對應elf64_sym

每個元素的數據結構如下定義:

成員 類型 描述 st_name Elf32_Word/Elf64_Word 名稱,索引到字符串表 st_value Elf32_AddrElf64_Addr 給出相關聯的符號的取值。依賴於具體的上下文 st_size Elf32_Word/Elf64_Word 相關的尺寸大小 st_info unsigned char 給出符號的類型和綁定屬性 st_other unsigned char 該成員當前包含 0,其含義沒有定義 st_shndx Elf32_Half/Elf64_Half 給出相關的節區頭部表索引。某些索引具有特殊含義

st_info給出符號的類型和綁定屬性


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_info 的高四位(ELF32_ST_BIND(i))

表示符號綁定,用於確定鏈接可見性和行為。具體的綁定類型如:

名稱 取值 說明 STB_LOCAL 0 局部符號在包含該符號定義的目標文件以外不可見。相同名稱的局部符號可以存在於多個文件中,互不影響 STB_GLOBAL 1 全局符號對所有將組合的目標文件都是可見的。一個文件中對某個全局符號的定義將滿足另一個文件對相同全局符號的未定義引用 STB_WEAK 2 弱符號與全局符號類似,不過他們的定義優先級比較低 STB_LOPROC 13 處於這個范圍的取值是保留給處理器專用語義的 STB_HIPROC 15 處於這個范圍的取值是保留給處理器專用語義的

全局符號與弱符號之間的區別主要有兩點:

當鏈接編輯器組合若干可重定 位 的 目 標 文 件 時 , 不 允 許 對 同 名 的STB_GLOBAL 符號給出多個定義。另一方面如果一個已定義的全局符號已經存在,出現一個同名的弱符號並不會產生錯誤。鏈接編輯器盡關心全局符號,忽略弱符號。類似地,如果一個公共符號(符號的 st_shndx 中包含 SHN_COMMON),那麼具有相同名稱的弱符號出現也不會導致錯誤。鏈接編輯器會采納公共定義,而忽略弱定義。

當鏈接編輯器搜索歸檔庫(archive libraries)時,會提取那些包含未定義全局符號的檔案成員。成員的定義可以是全局符號,也可以是弱符號。連接編輯器不會提取檔案成員來滿足未定義的弱符號。未能解析的弱符號取值為 0。

在每個符號表中,所有具有 STB_LOCAL 綁定的符號都優先於弱符號和全局符號。符號表節區中的 sh_info 頭部成員包含第一個非局部符號的符號表索引。

st_info的低四位ELF32_ST_TYPE(i)

定義如下

名稱 取值 說明 STT_NOTYPE 0 符號的類型沒有指定 STT_OBJECT 1 符號與某個數據對象相關,比如一個變量、數組等等 STT_FUNC 2 符號與某個函數或者其他可執行代碼相關 STT_SECTION 3 符號與某個節區相關。這種類型的符號表項主要用於重定位,通常具有 STB_LOCAL 綁定 STT_FILE 4 傳統上,符號的名稱給出了與目標文件相關的源文件的名稱。文件符號具有 STB_LOCAL 綁定,其節區索引是SHN_ABS,並且它優先於文件的其他 STB_LOCAL 符號(如果有的話) STT_LOPROC~STT_HIPROC 13~15 此范圍的符號類型值保留給處理器專用語義用途

在共享目標文件中的函數符號(類型為 STT_FUNC)具有特別的重要性。當其他目標文件引用了來自某個共享目標中的函數時,鏈接編輯器自動為所引用的符號創建過
程鏈接表項。類型不是 STT_FUNC 的共享目標符號不會自動通過過程鏈接表進行引用。

如果一個符號的取值引用了某個節區中的特定位置,那麼它的節區索引成員(st_shndx)包含了其在節區頭部表中的索引。當節區在重定位過程中被移動時,符號的取值也會隨之變化,對符號的引用始終會“指向”程序中的相同位置。

st_shndx


如前面所述,st_shndx給出相關的節區頭部表索引。但其值也存在一些特殊值,具有某些特殊的含義:

名稱 取值 說明 SHN_ABS   符號具有絕對取值,不會因為重定位而發生變化 SHN_COMMON   符號標注了一個尚未分配的公共塊。符號的取值給出了對齊約束,與節區的 sh_addralign成員類似。就是說,鏈接編輯器將為符號分配存儲空間,地址位於 st_value 的倍數處。符號的大小給出了所需要的字節數 SHN_UNDEF   此節區表索引值意味著符號沒有定義。當鏈接編輯器將此目標文件與其他定義了該符號的目標文件進行組合時,此文件中對該符號的引用將被鏈接到實際定義的位置

st_value


不同的目標文件類型中符號表項對 st_value 成員具有不同的解釋:

在可重定位文件中,st_value 中遵從了節區索引為 SHN_COMMON 的符號的對齊約束。

在可重定位的文件中,st_value 中包含已定義符號的節區偏移。就是說,st_value 是從 st_shndx 所標識的節區頭部開始計算,到符號位置的偏移。

在可執行和共享目標文件中,st_value 包含一個虛地址。為了使得這些文件的符號對動態鏈接器更有用,節區偏移(針對文 件的解釋)讓位於虛擬地址(針對內存的解釋),因為這時與節區號無關。

盡管符號表取值在不同的目標文件中具有相似的含義,適當的程序可以采取高效的數據訪問方式。

nm查看符號表


nm *.o

rel的elf符號表

可重定向文件, 是一個需要鏈接的對象, 程序頭表對其而言不是必要的, 因此這類文件一般沒有程序頭表

可執行的對象文件(Executable file)


nm testelf_dynamic

exec的elf符號表

可被共享的對象文件(Shared object file)


nm libtestelf.so

dyn的elf符號表

重定位信息


重定位是將符號引用與符號定義進行連接的過程。例如,當程序調用了一個函數時,相關的調用指令必須把控制傳輸到適當的目標執行地址。

重定位表項


可重定位文件必須包含如何修改其節區內容的信息,從而允許可執行文件和共享目標文件保存進程的程序映像的正確信息。重定位表項就是這樣一些數據。

可重定位表項的數據結構如下定義:

Elf32_Rel

成員 類型 描述 r_offset Elf32_Addr/Elf64_Addr 給出了重定位動作所適用的位置 r_info Elf32_Word/Elf64_Word 給出要進行重定位的符號表索引,以及將實施的重定位類型

Elf32_Rela

成員 類型 描述 r_offset Elf32_Addr/Elf64_Addr 給出了重定位動作所適用的位置 r_info Elf32_Word/Elf64_Word 給出要進行重定位的符號表索引,以及將實施的重定位類型 r_addend Elf32_Word 給出一個常量補齊,用來計算將被填充到可重定位字段的數值

重定位節區會引用兩個其它節區:符號表、要修改的節區。節區頭部的 sh_info 和sh_link 成員給出這些關系。不同目標文件的重定位表項對 r_offset 成員具有略微不同的解釋。
r_info通常分為高8位和低8位,分別表示不同的含義:

#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))

高8位用作要進行重定位的符號表索引,通過它可以得出一個符號表項,而低8位表示將實施的重定位類型,它是和處理器相關的。

ELF32_R_TYPE(i)


重定位表項描述如何修改後面的指令和數據字段。一般,共享目標文件在創建時,其基本虛擬地址是 0,不過執行地址將隨著動態加載而發生變化。

參考
六星經典CSAPP-筆記(7)加載與鏈接(上)

ELF文件結構

ELF文件格式

elf文件格式分析

ELF (文件格式)

淺談Linux的可執行文件格式ELF(轉帖)

ELF文件的加載和動態鏈接過程

可執行文件(ELF)格式的理解

elf文件格式

elf文件格式總結

elf文件格式與動態鏈接庫(非常之好)—–不可不看

Executable and Linkable Format (ELF) (這專門介紹ELF文件格式的ABI的好文章,網絡版在 www.skyfree.org/linux/references/ELF_Format.pdf可以得到)

Copyright © Linux教程網 All Rights Reserved