Linux ELF 详解1 -- ELF Header【转】

转自:https://blog.csdn.net/helowken2/article/details/113739946

为什么需要懂 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

int function(int);
1
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
1
生成 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
1
但运行时会出错

$ ./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
1
2
3
index = [0, 3] 字节: “7f 45 4c 46”,根据对照图,应该是 “0x7f E L F”。

# 打印 index=[1,3] 这3个字节的 ascii 形式
$ echo 0x45 0x4c 0x46 | xxd -r
ELF
1
2
3
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
1
2
3
“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
1
2
3
“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
1
2
3
这一串"00"表示无入口,因为 program.o 并非是可执行程序 (Executable file)。
再试试 prorgram (Executable file)

$ hexdump -C -s24 -n8 program
00000018 10 06 40 00 00 00 00 00 |..@.....|
00000020
1
2
3
可以看到入口地址为: “10 06 40 00 00 00 00 00” + little endian => 0x400610
可以通过 readelf 来验证:

$ readelf -h program
ELF Header:
...
Entry point address: 0x400610
...
1
2
3
4
5
查看一下该虚拟地址对应的汇编代码:

$ objdump -D program | grep 400610
0000000000400610 <_start>:
400610: 31 ed xor %ebp,%ebp
1
2
3
可以看到这个地址对应的是一个叫 _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
1
2
3
因为 program.o 是 Relocatable file,所以没有 Program header table.

$ hexdump -C -s32 -n8 program
00000020 40 00 00 00 00 00 00 00 |@.......|
00000028
1
2
3
因为 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
1
2
3
可以看到 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)
...
1
2
3
4
5
e_flags
表示处理器特定的标志的后缀,我们不关心,跳过。

数据类型:Elf64_Word(4 bytes)

$ hexdump -C -s48 -n4 program.o
00000030 00 00 00 00 |....|
00000034
1
2
3
e_ehsize
表示 ELF Header 的大小。

数据类型:Elf64_Half(2 bytes)

$ hexdump -C -s52 -n2 program.o
00000034 40 00 |@.|
00000036
1
2
3
大小为: “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
1
2
3
因为 program.o 没有 Program header table,所以这里的大小为0。

$ hexdump -C -s54 -n2 program
00000036 38 00 |8.|
00000038
1
2
3
可以看到 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
1
2
3
因为 program.o 没有 Program header table,所以这里的大小为0。

$ hexdump -C -s56 -n2 program
00000038 09 00 |..|
0000003a
1
2
3
可以看到 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
1
2
3
大小为:“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
1
2
3
数量为:“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
1
2
3
索引值为:“0b 00” + little endian = 0xb = 11

PS:e_shnum 和 e_shstrndx 如果大于某个特定值时,会使用另外的存储方式来保存这2个值。鉴于大多数情况下,这2个值都不会过大,所以当前我们不讨论特殊情况。

到目前为止,我们已经探讨了 ELF Header 所包含的大部分信息。
对照 readelf -h program.o 的输出,可以加深对这些元数据的理解。

下一篇:ELF 详解2 – Section Header & Section
————————————————
版权声明:本文为CSDN博主「懒惰的劳模」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/helowken2/article/details/113739946

posted @ 2022-01-18 23:55  Sky&Zhang  阅读(288)  评论(0编辑  收藏  举报