ELF 文件、鏡像(Image)文件、可執行文件、對象文件詳解

作者:ZC·Shou

https://itexp.blog.csdn.net/article/details/100048461

ELF 文件規範

ELF(Executable and Linking Format)是一個二進制文件規範。用於定義不同類型的對象文件(Object files)中都放了什麼東西、以及都以什麼樣的格式去放這些東西。

現在流行的二進制可執行文件格式 (Executable File Format),主要是 Windows 下的 PE(Portable Executable)和 Linux 的 ELF(Executable and Linking Format)可執行和鏈接格式)。他們都是 COFF(Common Object File Format)的變種。ARM 體系中採用的也是 ELF 文件格式。

COFF 是在 Unix System V Release 3 時由 UNIX 系統實驗室(UNIX System Laboratories, USL)首先提出並且使用的文件規範,後來微軟公司基於 COFF 格式,制定了 PE 格式標準,並將其用於當時的 Windows NT 系統。在 System V Release 4 時,UNIX 系統實驗室在 COFF 的基礎上,開發和發佈了 ELF 格式,作爲應用程序二進制接口 (Application Binary Interface,ABI)。

此後,工具接口標準委員會(Tool Interface Standard Committee,TISC)選擇了正在發展中的 ELF 標準作爲工作在 32 位 INTEL 體系上不同操作系統之間可移植的二進制文件格式。可以找到詳細的標準文檔 [1]。如下圖:

TISC 共出過兩個版本(v1.1 和 v1.2)的標準文檔。兩個版本內容上差不多,但 v1.2 版本重新組織了原本在 v1.1 版本中的內容。可讀性更高。兩個版本的目錄如下所示:

  由於 TISC 的 v1.2 比較老舊,且後續沒有再更新,尤其是在 64 位出現之後,原來的 ELF v1.2 已經不再試用,因此,System V 對 ELF v1.2 進行了擴展,這個擴展就是 System V Application Binary Interface AMD64 Architecture Processor Supplement,實際了 Unix 系統與 類 Unix 系統都使用 System V 擴展的這個版本。

在 ELF 文件規範中,把系統中採用 ELF 格式的文件(規範中稱爲對象文件(Object File))歸類爲以下三種:

在 Linux 系統中,還有一類文件,被稱爲核心轉儲文件(Core Dump File) ,當進程意外終止,系統可以將該進程地址空間的內容及終止時的一些信息轉存到核心轉儲文件。 對應 Linux 下的 core dump。

對象文件參與程序鏈接(構建程序)和程序執行(運行程序)。 爲了方便和高效,對象文件(Object File)格式提供文件內容的並行視圖,反映了這些活動的不同需求。 下圖顯示了對象文件(Object File)的組織。

其中,各部分的含義都是規範定義好的!

數據表示法

對象文件(Object File)格式支持具有 8 位字節和 32 位體系結構的各種處理器。 然而,它旨在可擴展到更大(或更小)的體系結構。 因此,對象文件(Object File)用一種與機器無關的格式表示一些控制數據,從而可以識別對象文件(Object File)並以通用方式解釋它們的內容。 目標處理器中的剩餘數據使用目標處理器的編碼,而不管創建文件的機器如何。出於可移植性的原因,ELF 不使用位字段。

U9QFEu

對象文件格式定義的所有數據結構都遵循相關類的自然大小和對齊準則。如果需要,數據結構包含顯式填充,以確保 4 字節對象的 4 字節對齊,強制結構大小爲 4 的倍數,以此類推。數據從文件開始也有適當的對齊。因此,例如,包含 Elf32 Addr 成員的結構將在文件中的 4 字節邊界上對齊。

字符表示法

ELF 中對於符號的字符編碼也有一定的要求。當 ELF 接口文檔提到字符常量時,例如’/‘或’\ n’,它們的數值應遵循 7 位 ASCII 準則。 對於先前的字符常量,單字節值分別爲 47 和 10。
  根據字符編碼,在 0 到 127 範圍之外的字符值可以佔用一個或多個字節。 應用程序可以根據需要使用不同語言的不同字符集擴展來控制自己的字符集。 儘管 TIS - 一致性 不限制字符集,但它們通常應遵循一些簡單的指導原則:

關於 ELF 文件規範這裏就不多做詳細介紹了,感興趣的可以去 Linux 基金會的官方網站下載規範來看看!

ARM ELF 文件格式

ARM 體系中,所有文件均採用的 ELF 文件格式。我們可以在 ARM 的官網找到 ARM 關於 ARM ELF 文件格式的說明文檔。後文參考部分的下載中是目前可以從 ARM 官網找到的所有和 ARM ELF 相關的 PDF 文檔。

目前,我們可以找到的 ARM ELF 相關的文檔主要有 4 個:《ARM ELF File Format》、《ELF for the ARM® Architecture》、《ARM ELF》以及 ARM 的鏈接器手冊。其中,《ARM ELF File Format》是比較早期的文檔,針對於 ARM SDT 時代的 ELF 文件,有點過時了;後者三個則是最新的介紹文檔,《ELF for the ARM® Architecture》 僅僅是對 ARM ELF 取值的一些特殊說明,是在讀者先了解 ELF 文件規範的基礎上進行的說明。

ARM 中的各種源文件(包括彙編文件,C 語言程序及 C++ 程序等)經過 ARM 編譯器編譯後生成 ELF 格式的對象文件(Object File)(.o 文件)。這些對象文件(Object File)和相應的 C/C++ 運行時用到的庫經過 ARM 連接器處理後,生成 ELF 格式的鏡像文件(image),這種 ELF 格式的映像文件是一種可執行文件,可被寫入嵌入式設備的 ROM 中。

在 ARM 體系中,所有的二進制文件均被稱爲對象文件。其中,鏈接器最終生成的 ELF 格式的可執行文件又被稱爲鏡像文件(Image file)。ARM ELF 鏡像文件或者對象文件由輸入節(Input Sections)輸出節(Output Sections)域(Regions)段(Segments) 組成,每個鏈接階段都有不同的鏡像視圖。如下圖所示:

When describing a memory view:

  1. The term root region means a region that has the same load and execution addresses. 術語根區域是指具有相同加載和執行地址的區域。

  2. Load regions are equivalent to ELF segments. 加載區域等效於 ELF 段。

輸入節 Input section

一個輸入節就是是輸入對象文件中的一個獨立的部分。 它包含代碼,初始化數據,或着是描述未初始化或必須在鏡像文件執行前設置爲零的內存片段。 這些屬性由 RO,RW,XO 和 ZI 等屬性表示。 armlink 使用這些屬性將輸入節分組爲更大的構建塊,稱爲輸出節和域。

輸出節 Output section

一個輸出節就是一組輸入節的組合,它們具有相同的 RO,RW,XO 或 ZI 屬性,並且由鏈接器連續放置在存儲器中。 輸出節與組成它的輸入節具有相同的屬性。 在輸出節中,輸入節根據節放置規則進行排序。

域 Region

一個域最多包含四個輸出節,具體取決於內容和具有不同屬性的節的數量。 默認情況下,域中的輸出節根據其屬性進行排序。 首先是 XO 屬性的輸出節,然後是 RO 屬性的輸出節,再然後是 RW 屬性的輸出節,最後是 ZI 屬性的輸出節。 域通常會映射到物理存儲設備,例如 ROM,RAM 或外圍設備。 您可以使用分散加載文件來更改輸出節的順序。

程序段 Program segment

一個程序段對應於一個加載域,並且包含執行域。 程序段包含文本和數據等信息。

存在 XO( execute-only)節時的注意事項

  1. 您可以在同一執行域中混合 XO 和 非 XO 節。 但是,輸出的結果是一個 RO 節。

  2. 如果輸入文件具有一個或多個 XO 節,則鏈接器將生成單獨的 XO ELF 段。 在最終鏡像中,除非使用分散加載文件或 --xo-base 選項另有指定,否則 XO 段緊接在 RO 段之前。

鏡像的加載視圖和執行視圖

鏡像的域在加載時放置在系統存儲器映射中。 內存中域的位置可能會在執行期間發生變化。在執行鏡像之前,可能必須將鏡像的某些域移動到其執行地址並創建 ZI 輸出節。 例如,初始化的 RW 數據可能必須從其 ROM 中的加載地址複製到 RAM 中的執行地址。鏡像的內存映射具有以下不同視圖:

加載視圖 Load view

根據鏡像加載到內存中時所處的地址,即鏡像執行開始前的位置,描述每個鏡像的域和節。

執行視圖 Execution view

根據鏡像執行期間所在的地址描述每個鏡像的域和節。

下圖顯示了沒有(XO)節的鏡像的這些視圖:

下圖顯示了具有 XO 節的鏡像的加載和執行視圖:

Image entry points

鏡像中的入口點就是鏡像中的一個位置(地址),該位置(地址)會被加載到 PC 寄存器。 它是程序執行開始的位置。 雖然鏡像中可以有多個入口點,但在鏈接時只能指定一個入口點。並非每個 ELF 文件都必須有入口點。 不允許在單個 ELF 文件中存在多個入口點。

對於嵌入式 Cortex-M 核的程序,程序的執行是從復位向量所在的位置(地址)開始執行。復位向量會被加載到 PC 寄存器中,且復位向量的位置(地址)並不固定。 通常,復位向量指向 CMSIS Reset_Handler 函數。
有兩種不同類型的入口點:

如果加載程序要使用嵌入式的映像,則它必須在標頭中指定一個初始入口點。 使用--entry命令行選項選擇入口點。

ARM ELF 文件實例

與標準的 ELF 文件相比,ARM ELF 的某些值比較特殊,下面以實際文件來說明一下每個部分。編譯工具如下圖:

編譯後,會在對應目錄下生成 .o 文件和 .axf 文件,爲了分析 ELF 文件,我們將使用 readelf 工具。在詳細解析之前,先用 Winhex 直接打開生成的 .o 文件,可以看到文件開頭有 ELF 字樣。表明它是一個 ELF 文件。如下:

注意:.o 不是 ARM 的可執行文件!axf 爲可執行文件。以下用兩種程序作對比。

一個簡單的可執行 ARM ELF 文件的概念佈局如下圖所示。請注意,文件中各部分的實際排序可能與下圖中的順序不同,因爲只有 ELF Header 在文件中具有固定位置。

注意,針對目前最新版本的 ARM ELF,上圖有點過時!

ELF Header

ELF Header 描述了體系結構和操作系統等基本信息,並指出 Section Header Table 和 Program Header Table 在文件中的什麼位置。實際文件中,只有 ELF Header 位置是絕對的,且只能在最開始,其他部分部分的位置順序並不一定完全相同。

Program Header Table 在彙編和鏈接過程中沒有用到,所以在重定位文件中可以沒有;Section Header Table 中保存了所有 Section 的描述信息,Section Header Table 在加載過程中沒有用到,對於可執行文件,可以沒有該部分。當然,對於某些類型的文件來說,可以同時擁有 Program header table 和 Section Header Table,這樣 load 完後還可以重定位。(例如:shared objects)

ELF Header 可以使用如下數據結構表示:

#define EI_NIDENT 16

typedef struct {
    unsigned char   e_ident[EI_NIDENT]; // Magic
    Elf32_Half      e_type;             // Type
    Elf32_Half      e_machine;          // Machine
    Elf32_Word      e_version;          // Version
    Elf32_Addr      e_entry;            // Entry point address
    Elf32_Off       e_phoff;            // Start of program headers
    Elf32_Off       e_shoff;            // Start of section headers
    Elf32_Word      e_flags;            // Flags    
    Elf32_Half      e_ehsize;           // Size of this header
    Elf32_Half      e_phentsize;        // Size of program headers
    Elf32_Half      e_phnum;            // Number of program headers
    Elf32_Half      e_shentsize;        // Size of section headers
    Elf32_Half      e_shnum;            // Number of section headers
    Elf32_Half      e_shstrndx;         // Section header string table index
} Elf32_Ehdr;

下面兩幅圖分別顯示了不同文件的 ELF Header。以上數據結構中的註釋,即對應於下圖中的各部分字段。
.o 文件 ELF Header 如下圖所示:

.axf 文件 ELF Header 如下圖所示:

下面對以上兩幅圖中的內容做一下詳細介紹:

kZm0DX

`e_ident[EI_MAG0] ~ e_ident[EI_MAG3]`:包含了 ELF 文件的魔數,依次固定是 0x7f 和 ‘E’、‘L’、‘F’。  
`e_ident[EI_CLASS]`:取值如下

DkAYoR

ARM ELF 文件應包含 ELFCLASS32。  
`e_ident[EI_DATA]`

52gEmj

選擇將由執行環境中的默認數據順序控制。 在以 BE8 模式運行的 Architecture v6 處理器上,所有的指令均爲小端格式。 適合在此模式下操作的可執行文件將在 e_flags 字段中設置 EF_ARM_BE8。  
`e_ident[EI_VERSION]`:指定 ELF 頭部的版本,當前必須爲 1。  
`e_ident[7]~e_ident[15]`:是填充符,通常是 0

L7dtRH

ktsXTO

目前沒有特定於 ARM 的對象文件類型。 `ET_LOPROC``ET_HIPROC` 之間的所有值都保留給本規範的未來版本。

sv8pTh

6OMMUA

ftK5X0

注意:以上部分與 ARM 早期文檔是有區別的,很多值已經不同

注意:實際文件中,每一部分的位置順序並不一定完全相同,只有 ELF Header 位置是絕對的,且只能在最開始。

Section Header(節頭)

節頭表提供了對 ELF 文件中所有節的訪問。節中包含對象文件(Object File)中的所有信息,除了:ELF 頭部、程序頭部表格、節頭部 表格。節滿足以下條件:

  1. 對象文件(Object File)中的每個節都有對應的節頭部描述它,反過來,有節頭部不意 味着有節。

  2. 每個節佔用文件中一個連續字節域(這個區域可能長度爲 0)。

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

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

ELF 頭部中,e_shoff 成員給出從文件頭到節頭部表格的偏移字節數;e_shnum 給出表格中條目數目;e_shentsize 給出每個項目的字節數。從這些信息中可以確切地定位節的具體位置、長度。節頭部表格中比較特殊的幾個下標如下:

xem8re

介於 SHN_LORESERVESHN_HIRESERVE 之間的表項不會出現在節頭部表中。

.o 文件 Section Header(部分)

.axf 文件 Section Header

上圖中的表頭可以用如下數據結構描述(對應關係見註釋):

typedef struct {
    Elf32_Word sh_name;         // name
    Elf32_Word sh_type;         // Type
    Elf32_Word sh_flags;        // Flg
    Elf32_Addr sh_addr;         // Addr
    Elf32_Off sh_offset;        // Off
    Elf32_Word sh_size;         // Size
    Elf32_Word sh_link;         // Lk
    Elf32_Word sh_info;         // Inf
    Elf32_Word sh_addralign;    // Al
    Elf32_Word sh_entsize;      // ES
} Elf32_Shdr;

psibdA

除了以上標準節類型外,ARM 架構下,還有以下特殊的類型:

3ZBtdK

AXRTYb

ARM 中的特殊取值如下:

a7zldI

qVkH8I

注意:

  1. 保留給處理器體系結構的節名稱一般構成爲:處理器體系結構名稱簡寫 + 節名稱。且處理器名稱應該與 e_machine 中使用的名稱相同。例如:上圖最後的 .ARM.attributes

  2. 對象文件(Object File)中也可以包含多個名字相同的節。

  3. 上圖節名 ER_IROM1、RW_IRAM1、RW_IRAM 是由連接器的分散加載文件指定的名稱。可以根據需要自行修改。

ARM 節名稱是以下面列出的具有預定義含義的標準前綴之一開始的名稱,或者是包含美元($)字符的名稱。 在 ARM EABI 下沒有其他具有特殊意義的段名稱。

fylUhS

除了以上標準節外,ARM 架構下,還有以下特殊的節:

tERql0

這裏需要注意一下 Debug Sections。Debug Sections 僅在調試時使用,稍微複雜一些。ARM 可執行 ELF 文件的調試節中包含多種類型的調試信息,ELF 可執行文件的使用者(如 armlink)可以通過檢查可執行文件的節表來區分這些種類型的調試信息。

ARM 系列的開發工具在不同的發展時期,採用的調試信息是有區別的,後來統一採用 DWARP。目前採用的應該是 3.0 版本。具體如下:

eANwva

1Ui7fU

關於 DWARF 調試標準詳見:http://www.dwarfstd.org/。目前最新版本是 The DWARF Debugging Standard Version 5

Program Headers(程序頭)

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

程序頭可以使用如下數據結構來表示(對應關係見註釋):

typedef struct {
Elf32_Word    p_type;       // Type
Elf32_Off     p_offset;     // Offset
Elf32_Addr    p_vaddr;      // VirtAddr
Elf32_Addr    p_paddr;      // PhyAddr
Elf32_Word    p_filesz;     // FileSiz
Elf32_Word    p_memsz;      // MemSiz
Elf32_Word    p_flags;      // Flg
Elf32_Word    p_align;      // Align
} Elf32_Phdr;

xqPHSW

BMrEnn

Symbol table(符號表)

一個對象文件的符號表保存了定位和重定位所在程序的符號定義和引用所需的信息。符號表以數組的下標進行索引。0 指定表中的第一個條目,並用作未定義的符號索引。ARM 結構中,符號表與標準的 ELF 文件沒有任何區別。

圖 12 .o 文件 Symbol table(部分)

  在 C 語言中,符號表保存了程序實現或使用的所有全局變量和函數,如果程序引用一個自身代碼未定義的符號,則稱之爲未定義符號,這類引用必須在靜態鏈接期間用其他目標模塊或庫解決,或在加載時通過動態鏈接解決。

符號表可以使用以下數據結構表示:

typedef struct {
Elf32_Word      st_name;    // Name
Elf32_Addr      st_value;   // Value
Elf32_Word      st_size;    // Size
unsigned char   st_info;    //
unsigned char   st_other;   
Elf32_Half      st_shndx;   // Ndx
} Elf32_Sym;

ANWQmg

In each symbol table, all symbols with STB_LOCAL binding precede the weak and global symbols. A symbol’s type provides a general classification for the associated entity. Figure 3-17, Symbol Types, ELF32_ST_TYPE

RCFkqx

The symbols in ELF object files convey specific information to the linker and loader. See section 4, ARM- and Thumb-Specific Definitions, for a description of the actual linking model used in the system.

As mentioned above, the symbol table entry for index 0 (STN_UNDEF) is reserved. It is shown in Figure 3-18. Figure 3-18, Symbol Table Entry: Index 0

6sW5Fq

String table(字符串表)

字符串表節包含以 NULL(ASCII 碼 0)結尾的字符序列,通常稱爲字符串。ELF 對象文件(Object File)通常使用字符串來表示符號和節名稱。對字符串的引用通常以字符串在字符 串表中的下標給出。ARM 結構中,字符串表與標準的 ELF 文件沒有任何區別。

axf 文件

axf 文件是 ARM 的調試文件,其格式符合上一節講的對象文件(Object File)格式(ELF)。其中除了包含了完整的 bin 文件外,還附加了其他的調試信息。在調試的時候,這些調試信息是不必下到 RAM 中去的,真正下到 RAM 中的信息僅僅是可執行代碼。下圖爲 axf 文件的頭部。

  通過直接查看完整的 axf 文件可以看出,axf 中絕大多數都是和調試相關的內容。真正的 Bin 只是其中的一小部分。Bin 的結尾處在 axf 文件中也很容易找到,再次就不在贅述。
  既然前面我們說了,axf 文件就是 ELF 文件格式,那麼我們可以使用 readelf 工具,具體查看一下 axf 文件。下圖是一個 axf 文件的節表

Bin 文件

bin 文件是 ARM 的可執行文件,是最純粹的二進制機器代碼。與 HEX 文件包括地址信息的不同,BIN 文件格式只包括了數據本身。在燒寫或下載 HEX 文件的時候,一般都不需要用戶指定地址,因爲 HEX 文件內部的信息已經包括了地址。而燒寫 BIN 文件的時候,用戶是一定需要指定地址信息的。
  ARM 的 Bin 文件就是 axf 的精華部分(掐掉 ELF 頭,去掉 .symtab、.debug 和. symtab 區裏的信息)。下圖是筆者使用 Winhex 截取的 ARM 的 Bin 文件的開頭和結尾的示意圖。

hex 文件

首先,hex 文件最初由 Intel 提出。在 Intel HEX 文件中,每一行是一個 HEX 記錄,由十六進制數組成的機器碼或者數據常量,Intel HEX 文件經常被用於將程序或數據傳輸存儲到 ROM、EPROM,大多數編程器和模擬器使用 Intel HEX 文件。
  hex 文件全部由可打印的 ASCII 字符組成。如下圖就是 ARM-MDK5.22 生成的一個 hex 文件(部分)

從上圖不難看出,hex 文件就是一個個的十六進制的字符串。實際上,一個 Intel HEX 文件可以包含任意多的十六進制記錄,每條記錄有五個域,每條記錄都由一個冒號 ":" 打頭。一個數據記錄以一個回車和一個換行結束。其格式如下:
:CCAAAARR[DD...]ZZ
其中:

舉例如下:
:10400000781A00203D420008034C0008D14B0008FC

參考

  1. Tool Interface Standard (TIS) Executable and Linking Format (ELF) Specification Version 1.2

  2. ARM® Compiler v5.06 for µVision® Version 5 armlink User Guide

  3. ARM® Compiler v5.06 for µVision® Version 5 armcc User Guide

  4. ARM ELF File Format ARM DUI 00101-A

  5. ARM ELF Development Systems Business Unit Engineering Software Group

  6. ELF for the ARM® Architecture

參考資料

[1] 標準文檔: https://refspecs.linuxbase.org/elf/elf.pdf

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/bLnaDOA0yindJQWUiRzSgw