為什麼需要懂 ELF
- 可以了解程式是如何進行靜态連接配接和動态連接配接
- 從程序中擷取程式各種有用資訊,進而制作各種底層工具
ELF 檔案類型
ELF 對象檔案主要有3種類型:
- relocatable file:包含資料和代碼,需要連接配接其他檔案來生成 executable file 或 shared object file。
- executable file:包含了可執行的程式,指定了如何在運作時建立程序鏡像。
- shared object file:包含資料和代碼,可以聯合其他 relocatable file 和 shared object file 來生成新的 shared object file;或者在 executable file 運作時,和其他 shared object file 一起被合并進去,進而建立程序鏡像。
ELF 内容布局
我們主要關注 Linking View。
Linking View 主要分成4部分:
- ELF Header,用來描述該對象檔案的各項資訊
- Program header table:雖然叫 table,其實就是一個 Program header 的數組,所有 Program header 都等長。
- Section(s): 根據 Section Header 的不同,對應的 Section 内容也不同,而且各個 Section 的長度也不一樣。
- Section header table:雖然叫 table,其實就是一個 Section header 的數組,所有 Section header 都等長。Section header 包含了其對應 Section 的各項中繼資料,根據這些中繼資料,就能正确解讀 Section 的内容。
這4部分的布局順序除了 ELF Header 外,其他都不是固定的,因為包含了定位中繼資料,是以它們的順序可變。
ELF 資料類型
資料類型分了32位和64位,我們主要關注64位。
實驗準備
環境:
$ cat /etc/os-release
NAME="Linux Mint"
VERSION="18.3 (Sylvia)"
ID=linuxmint
ID_LIKE=ubuntu
PRETTY_NAME="Linux Mint 18.3"
VERSION_ID="18.3"
HOME_URL="http://www.linuxmint.com/"
SUPPORT_URL="http://forums.linuxmint.com/"
BUG_REPORT_URL="http://bugs.launchpad.net/linuxmint/"
VERSION_CODENAME=sylvia
UBUNTU_CODENAME=xenial
$ uname -a
Linux helowken-mint 4.10.0-38-generic #42~16.04.1-Ubuntu SMP Tue Oct 10 16:32:20 UTC 2017 x86_64 x86_64 x86_64 GNU/Linux
程式:
library.h
library.c
int a = 100;
int function(int input) {
return input + 10;
}
program.c
#include <stdio.h>
#include "library.h"
extern int a;
char c[10];
static char d = 'd';
char* f = (char*) function;
int main() {
int d = function(100) + a;
printf("a: %d\n", a);
printf("result: %d\n", d);
}
編譯:
生成 shared object file: libfunc.so
$ gcc -shared -fPIC -o libfunc.so library.c
生成 relocatable file:program.o
$ gcc -c program.c
生成 executable file 時會出錯:
$ gcc -o program program.o
program.o: In function `main':
program.c:(.text+0xe): undefined reference to `function'
program.c:(.text+0x16): undefined reference to `a'
program.c:(.text+0x21): undefined reference to `a'
program.o:(.data+0x8): undefined reference to `function'
collect2: error: ld returned 1 exit status
因為 program.o 中包含有未知的符号,是以需要增加 libfunc.so 的連接配接。(後面會詳細說明如何進行符号解析)
生成 executable file:program
$ gcc -o program program.o -L. -lfunc
但運作時會出錯
$ ./program
./program: error while loading shared libraries: libfunc.so: cannot open shared object file: No such file or directory
因為運作時,動态連接配接器(dynamic-linker)無法找到 libfunc.so,是以無法對符号進行解析。
增加 libfunc.so 的連接配接後,運作正常。
$ export LD_LIBRARY_PATH=.
$ ./program
a: 100
result: 210
目前為止,我們得到了3個檔案,分别是
- libfunc.so: shared object file
- program.o: relocatable file
- program: executable file
檢視 ELF Header
使用 readelf 這個指令,可以檢視 ELF 的頭部。
$ readelf -h program.o
ELF Header:
Magic: 7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00
Class: ELF64
Data: 2's complement, little endian
Version: 1 (current)
OS/ABI: UNIX - System V
ABI Version: 0
Type: REL (Relocatable file)
Machine: Advanced Micro Devices X86-64
Version: 0x1
Entry point address: 0x0
Start of program headers: 0 (bytes into file)
Start of section headers: 1048 (bytes into file)
Flags: 0x0
Size of this header: 64 (bytes)
Size of program headers: 0 (bytes)
Number of program headers: 0
Size of section headers: 64 (bytes)
Number of section headers: 14
Section header string table index: 11
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;
typedef struct {
unsigned char e_ident[EI_NIDENT]; //16 B (B for bytes)
Elf64_Half e_type; // 2 B
Elf64_Half e_machine; // 2 B
Elf64_Word e_version; // 4 B
Elf64_Addr e_entry; // 8 B
Elf64_Off e_phoff; // 8 B
Elf64_Off e_shoff; // 8 B
Elf64_Word e_flags; // 4 B
Elf64_Half e_ehsize; // 2 B
Elf64_Half e_phentsize; // 2 B
Elf64_Half e_phnum; // 2 B
Elf64_Half e_shentsize; // 2 B
Elf64_Half e_shnum; // 2 B
Elf64_Half e_shstrndx; // 2 B
} Elf64_Ehdr; // total size = 64 B
這是 ELF 頭部的定義,我們隻關注 Elf64_Ehdr (64位系統的定義)。
注釋部分是我添加上去的,可以看出整個 ELF Header 占據 64 bytes。
Elf64_Ehdr
e_ident[EI_NIDENT]
從定義可知:EI_NIDENT = 16
以下是這16個位元組的定義。
檢視這16個位元組:
$ hexdump -C -n16 program.o
00000000 7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00 |.ELF............|
00000010
index = [0, 3] 位元組: “7f 45 4c 46”,根據對照圖,應該是 “0x7f E L F”。
# 列印 index=[1,3] 這3個位元組的 ascii 形式
$ echo 0x45 0x4c 0x46 | xxd -r
ELF
index = 4 位元組: “02”,根據對照圖,可以得知,這是64位的對象檔案。這個位元組很重要,因為不論是資料類型還是 ELF Header 的定義,以及後面會講到的其他資訊都是劃分為32位和64位的,是以必須根據這個位元組來進行不同的解析。
index = 5 位元組: “01”,表示資料編碼。編碼方式有兩種(都是2的補碼形式)
- little endian: 低位在低位址,高位在高位址。(值為1)
- big endian: 高位在低位址,低位在高位址。(值為2)
這個位元組同樣很重要,直接影響解讀方式。由此可知 program.o 的資料編碼是 little endian。
index = 6 位元組: “01”,表示目前版本,一般用于判斷這個對象檔案是否有效,目前隻有等于1是有效的。
index = [7, 8] 位元組: “00 00”,表示系統擴充,ABI 擴充和 ABI 版本,我們先跳過。ABI 全稱 Application Binary Interface,針對不同系統有不同規範,比如 i386-ABI 對應的是 x86 系列,x86-64-ABI 對應 x86-64 系列。
index = [9, 15] 位元組: 這些位元組作為 padding 全部為0,用作以後的擴充,目前可以直接忽略。
e_type
表示對象檔案的類型。
資料類型:Elf64_Half (2 bytes)
# -C 表示16進制 + ascii 顯示, -s 表示 offset, -n 表示提取位元組數
# 這裡 -s16 表示跳過前面的 e_ident
$ hexdump -C -s16 -n2 program.o
00000010 01 00 |..|
00000012
因為 program.o 的資料編碼方式是 little endian,是以 “01 00” 應該看成 “0x0001”,也就是1。從對照圖可知,1 表示 Relocatable file。
e_machine
表示所需要的 CPU 架構。對照圖有很多很多不同的 CPU 架構,這裡隻列出一部分。
資料類型:Elf64_Half (2 bytes)
$ hexdump -C -s18 -n2 program.o
00000012 3e 00 |>.|
00000014
“3e 00” + little endian = 0x003e = 3 * 16 + 14 = 62。
從對照圖可知,program.o 需要 AMD x86-64 的架構。(雖然叫 AMD x86-64,實際上 Intel 的 CPU 也支援,隻不過64位架構是 AMD 先做出來,是以很多地方都會将其稱作 AMD x86-64)
e_version
表示對象檔案的版本,跟 e_ident 中的 EI_VERSION 等價。
資料類型:Elf64_Word (4 bytes)
$ hexdump -C -s20 -n4 program.o
00000014 01 00 00 00 |....|
00000018
“01 00 00 00” + little endian = 0x1
e_entry
系統第一次轉交控制的虛拟位址,也就是所謂的程式入口。我們寫的程式,如果要運作,都有一個 main 方法(C/C++/Java/Python),但這個并非是程式入口,真正的入口是 “_start”,後面會講到。
資料類型:Elf64_Addr (8 bytes)
hexdump -C -s24 -n8 program.o
00000018 00 00 00 00 00 00 00 00 |........|
00000020
這一串"00"表示無入口,因為 program.o 并非是可執行程式 (Executable file)。
再試試 prorgram (Executable file)
$ hexdump -C -s24 -n8 program
00000018 10 06 40 00 00 00 00 00 |..@.....|
00000020
可以看到入口位址為: “10 06 40 00 00 00 00 00” + little endian => 0x400610
可以通過 readelf 來驗證:
$ readelf -h program
ELF Header:
...
Entry point address: 0x400610
...
檢視一下該虛拟位址對應的彙編代碼:
$ objdump -D program | grep 400610
0000000000400610 <_start>:
400610: 31 ed xor %ebp,%ebp
可以看到這個位址對應的是一個叫 _start 的方法/過程。(後面會講到這個方法)
e_phoff
表示 Program header table 在檔案中的 offset,如果這個 table 不存在,則值為0。
資料類型:Elf64_Off (8 bytes)
$ hexdump -C -s32 -n8 program.o
00000020 00 00 00 00 00 00 00 00 |........|
00000028
因為 program.o 是 Relocatable file,是以沒有 Program header table.
$ hexdump -C -s32 -n8 program
00000020 40 00 00 00 00 00 00 00 |@.......|
00000028
因為 program 是 Executable file,是以可以看到 offset 為:
“40 00 00 00 00 00 00 00” + little endian = 0x40 = 4 * 16 = 64
再對照一下 Elf64_Ehdr 的定義,Elf64_Ehdr 長度為 64 bytes,也就是說,Program header table 緊貼着 ELF Header。
e_shoff
表示 Section header table 在檔案中的 offset,如果這個 table 不存在,則值為0。
資料類型:Elf64_Off (8 bytes)
$ hexdump -C -s40 -n8 program.o
00000028 18 04 00 00 00 00 00 00 |........|
00000030
可以看到 offset 為: “18 04 00 00 00 00 00 00” + little endian = 0x418 = 1048
可以通過 readelf 來驗證。
$ readelf -h program.o
ELF Header:
...
Start of section headers: 1048 (bytes into file)
...
e_flags
表示處理器特定的标志的字尾,我們不關心,跳過。
資料類型:Elf64_Word(4 bytes)
$ hexdump -C -s48 -n4 program.o
00000030 00 00 00 00 |....|
00000034
e_ehsize
表示 ELF Header 的大小。
資料類型:Elf64_Half(2 bytes)
$ hexdump -C -s52 -n2 program.o
00000034 40 00 |@.|
00000036
大小為: “40 00” + little endian = 0x40 = 64,跟 Elf64_Ehdr 的定義是一緻的。
e_phentsize
表示 Program header 的大小。(Program header 都是等長的)
資料類型:Elf64_Half(2 bytes)
$ hexdump -C -s54 -n2 program.o
00000036 00 00 |..|
00000038
因為 program.o 沒有 Program header table,是以這裡的大小為0。
$ hexdump -C -s54 -n2 program
00000036 38 00 |8.|
00000038
可以看到 program 的 Program header 大小為:
“38 00” + little endian = 0x38 = 56 (bytes)
e_phnum
表示有多少個 Program header。
(Program header table 的大小:e_phentsize * e_phnum)
資料類型:Elf64_Half(2 bytes)
$ hexdump -C -s56 -n2 program.o
00000038 00 00 |..|
0000003a
因為 program.o 沒有 Program header table,是以這裡的大小為0。
$ hexdump -C -s56 -n2 program
00000038 09 00 |..|
0000003a
可以看到 program 的 Program header 數目為:
“09 00” + little endian = 0x9 = 9
e_shentsize
表示 Section Header 的大小。(Section Header 都是等長的)
資料類型:Elf64_Half (2 bytes)
$ hexdump -C -s58 -n2 program.o
0000003a 40 00 |@.|
0000003c
大小為:“40 00” + little endian = 0x40 = 4 * 16 = 64 (bytes)
e_shnum
表示 Section Header 的數量。
(Section header table 的大小:e_shentsize * e_shnum)
資料類型:Elf64_Half (2 bytes)
$ hexdump -C -s60 -n2 program.o
0000003c 0e 00 |..|
0000003e
數量為:“0e 00” + little endian = 0xe = 14
e_shstrndx
每個 Section 都使用一個字元串作為其名字(後面會講到),這些字元串統一存放在同一個 Section 中,這個 Section 由專門的 Section Header 來描述它的位置和大小。而這個 Section Header 存放在 Section header table 中,為了快速檢索到這個 Section Header,于是使用 e_shstrndx 來儲存它在 Section header table 中的索引值。
資料類型:Elf64_Half (2 bytes)
$ hexdump -C -s62 -n2 program.o
0000003e 0b 00 |..|
00000040
索引值為:“0b 00” + little endian = 0xb = 11
PS:e_shnum 和 e_shstrndx 如果大于某個特定值時,會使用另外的存儲方式來儲存這2個值。鑒于大多數情況下,這2個值都不會過大,是以目前我們不讨論特殊情況。
到目前為止,我們已經探讨了 ELF Header 所包含的大部分資訊。
對照 readelf -h program.o 的輸出,可以加深對這些中繼資料的了解。
下一篇:ELF 詳解2 – Section Header & Section