天天看點

Linux ELF 詳解1 -- ELF Header

為什麼需要懂 ELF

  1. 可以了解程式是如何進行靜态連接配接和動态連接配接
  2. 從程序中擷取程式各種有用資訊,進而制作各種底層工具

ELF 檔案類型

ELF 對象檔案主要有3種類型:

  1. relocatable file:包含資料和代碼,需要連接配接其他檔案來生成 executable file 或 shared object file。
  2. executable file:包含了可執行的程式,指定了如何在運作時建立程序鏡像。
  3. shared object file:包含資料和代碼,可以聯合其他 relocatable file 和 shared object file 來生成新的 shared object file;或者在 executable file 運作時,和其他 shared object file 一起被合并進去,進而建立程序鏡像。

ELF 内容布局

Linux ELF 詳解1 -- ELF Header

我們主要關注 Linking View。

Linking View 主要分成4部分:

  1. ELF Header,用來描述該對象檔案的各項資訊
  2. Program header table:雖然叫 table,其實就是一個 Program header 的數組,所有 Program header 都等長。
  3. Section(s): 根據 Section Header 的不同,對應的 Section 内容也不同,而且各個 Section 的長度也不一樣。
  4. Section header table:雖然叫 table,其實就是一個 Section header 的數組,所有 Section header 都等長。Section header 包含了其對應 Section 的各項中繼資料,根據這些中繼資料,就能正确解讀 Section 的内容。

這4部分的布局順序除了 ELF Header 外,其他都不是固定的,因為包含了定位中繼資料,是以它們的順序可變。

ELF 資料類型

Linux ELF 詳解1 -- ELF Header

資料類型分了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個檔案,分别是

  1. libfunc.so: shared object file
  2. program.o: relocatable file
  3. 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個位元組的定義。

Linux ELF 詳解1 -- ELF Header

檢視這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的補碼形式)

  1. little endian: 低位在低位址,高位在高位址。(值為1)
  2. 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

表示對象檔案的類型。

Linux ELF 詳解1 -- ELF Header

資料類型: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 架構,這裡隻列出一部分。

Linux ELF 詳解1 -- ELF Header

資料類型: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

繼續閱讀