天天看点

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

继续阅读