概述
文章目录
- 前言
- 生成 so 文件
- 相关工具
- objdump
- readelf
- 整体结构图
- 头部结构
- 段表结构
- 字符串表结构
- 程序表结构
- 符号表结构
- 重定位表结构
- 其他结构
- 解析代码
- 打开 ELF 文件
- 检查 ELF 文件
- 解析 ELF 头部结构
- 解析段描述表结构
- 解析字符串表
- 打印段描述表结构
- 解析符号字符串表
- 解析程序头表
- 解析段
- 解析符号表
- 解析重定位表
- 测试
- 解析源码
- 参考
前言
ELF 是一种可执行文件的格式,全称是 Executable and Linkable Format,即可执行和链接格式,它是 Unix/Linux 系统下的二进制文件的标准格式,与之对应的是 Windows 系统的 PE(Portable Executable)可执行文件格式,它们都是由 COFF(Common Object File Format,通用对象文件格式)文件格式发展而来。
so 文件是 Unix/Linux 系统中的动态库文件,被称为共享目标文件(Shared Object File),后缀名为 .so
,它是 ELF 的一种,另外属于 ELF 类型的还有可重定位文件(Relocatable File)以及核心转储文件(Core Dump File)。
ELF 文件类型 | 说明 | 实例 |
---|---|---|
可重定位文件(Relocatable File) | 这类文件包含了代码和数据,可以被用来链接成可执行文件或共享目标文件,静态链接库也可以归为这一类 | Linux 的 .o ;Windows 的 .obj |
共享目标文件(Shared Object File) | 这种文件包含了代码和数据,可以在以下两种情况中使用,一种是链接器可以使用这种文件跟其他的可重定位文件和共享目标文件链接,产生新的目标文件,第二种是动态连接器可以将几个这种共享目标文件与可执行文件结合,作为进程映像的一部分来运行 | Linux 的 .so ,如 /lib/glibc-2.5.so ,Windows 的 DLL |
核心转储文件(Core Dump File) | 当进程意外终止时,系统可以将该进程的地址空间的内容及终止时的一些其他信息转储到核心转储文件 | Linux 下的 Core Dump |
Android 是基于 Linux 内核开发的操作系统,所以 Android 平台上的可执行文件格式和 Unix/Linux 是一致的。
下面以 Android 平台下的 so 文件为例子对 ELF 这种文件格式进行解析。
生成 so 文件
为了对 so 文件进行解析,首先需要生成一个 so 文件。
NDK 构建可参考:Android NDK 指南
首先建立一个最基本的 NDK 开发工程,创建 Java 类 NativeHandler
:
// NativeHandler.java
package io.l0neman.nativetproject;
public class NativeHandler {
static {
// 加载 libfoo.so 库
System.loadLibrary("foo");
}
public static native String getHello();
}
编写 C++ 代码文件,为了稍微显得没有那么简单,加入一些变量和简单函数:
// foo.h
#ifndef NATIVETPROJECT_FOO_H
#define NATIVETPROJECT_FOO_H
#include <jni.h>
extern "C" {
JNIEXPORT jstring JNICALL
Java_io_l0neman_nativetproject_NativeHandler_getHello(JNIEnv *env, jclass clazz);
}
#endif //NATIVETPROJECT_FOO_H
// foo.cpp
#include "foo.h"
#include <cstdio>
#include <jni.h>
int global_init_var = 84;
int global_uninit_var;
void func1(int i) {
printf("%d", i);
}
int test() {
static int static_var = 85;
static int static_var2;
int a = 1;
int b;
func1(static_var + static_var2 + a + b);
return a;
}
extern "C" {
jstring Java_io_l0neman_nativetproject_NativeHandler_getHello(JNIEnv *env, jclass clazz) {
test();
return env->NewStringUTF("hello");
}
}
mk 文件:
# Application.mk
APP_ABI := armeabi-v7a arm64-v8a
APP_OPTIM := release
# Android.mk
LOCAL_PATH := $(call my-dir)
include $(CLEAR_VARS)
LOCAL_MODULE := foo
LOCAL_SRC_FILES := foo.cpp
LOCAL_CFLAGS := -g
include $(BUILD_SHARED_LIBRARY)
这些文件在 src/main/jni
目录中,进入 jni
目录,然后执行 ndk-build
命令,将编译出 armeabi-v7a
和 arm64-v8a
架构的 libfoo.so
文件,它们的位置在 src/main/jni/libs/armxxx/libfoo.so
。
有了文件,下面开始进行解析。
相关工具
在解析之前介绍两个用于解析 ELF 文件的工具,它们通常是 Linux 系统中自带的软件,可直接使用。
如果想要在 Windows 系统中使用,推荐使用 Windows 子系统(Windows Subsystem for Linux)。
objdump
可解析目标文件的工具,可显示 ELF 文件的概要信息。常用选项如下:
-h 显示所有节的信息
-x 显示所有节的内容
-d 显示可执行节的汇编程序内容
-D --disassemble-all 显示所有节的汇编程序内容
-s --full-contents 显示所有节内容
-t --syms 显示符号表的内容
-T --dynamic-syms 显示动态符号表的内容
-r --reloc 显示文件中的重定位条目
-R --dynamic-reloc 显示文件中的动态重定位条目
readelf
用于解析 ELF 文件的工具,可以详细的输出 ELF 文件的信息。常用选项如下:
-a 等效于:-h -l -S -s -r -d -V -A -I
-h --file-header 显示 ELF 文件头
-l --program-headers 显示程序头
-s --syms 显示符号表
--dyn-syms 显示动态符号表
-n --notes 显示核心注释
-r --relocs 显示重定位
-u --unwind 显示展开信息
-d --dynamic 显示动态部分
在下面的解析过程中,可使用这两个工具对解析结果进行参考和对照。
整体结构图
上图反映了一个 ELF 文件的典型结构,首先是一个 ELF 文件的头结构,根据 ELF 所支持的目标执行平台不同,分为 32 位和 64 位的 ELF 文件,32 位 ELF 文件的头结构使用一个 Elf32_Ehdr
结构体描述,ELF 头结构描述了整个 ELF 文件的属性,包括文件类型(可执行文件、可重定位、共享目标文件等)、虚拟入口地址、段表偏移、文件本身的大小等。
文件头下面的就是 ELF 文件的主要内容了,ELF 文件由若干个段(Section)组成,它们的结构各不相同,在 ELF 文件中扮演不同的角色,有各自的分工。通常 ELF 文件包含若干遵循 ELF 结构规范的段。如上图所示,左边带有 .
前缀的为段名,上面几个深蓝色的矩形为 ELF 文件标准结构,它们有明确的结构定义,在 Android 9.0 系统源代码中,可在 art/runtime/elf.h
文件中找到它们对应的定义,在下面的分析中会一一解释它们的含义。
除了具有标准结构的 ELF 段,还有一些常用段以及程序自定义段名的段。下面列举一些常用段的含义:
段名 | 说明 |
---|---|
.rodata | Read only Data,存放的是只读数据,比如字符串、全局 const 变量等 |
.comment | 存放编译器版本信息,例如 GCC:(GNU)4.2.0 |
.debug | 调试信息 |
.dynamic | 动态链接信息 |
.hash | 符号哈希表 |
.line | 调试时的行号表,即源代码与行号与编译后指令的对应表 |
.notes | 额外的编译器信息,比如程序的公司名、发布版本等 |
.strtab | String Table,字符串表,用于存储 ELF 文件中用到的字符串 |
.symtab | Symbol Table,符号表,存放 ELF 文件的内部符号 |
.shstrtab | Section String Table,段名字符串表 |
.plt .got | 动态链接的跳转表和全局入口表 |
.init .fini | 程序初始化终结代码段 |
.interp | 存放动态链接器路径 |
.text | 代码段,存放机器指令,是 ELF 文件的主要内容 |
.bss | 为未初始化的全局变量和局部静态变量预留位置,没有内容,不占空间 |
.data | 数据段,存全局变量和局部静态变量 |
.dynstr | 动态符号字符串表 |
对于任意的 ELF 文件,它的结构可能不会像上面图中一样完整,根据实际情况,编译器编译生成的 ELF 文件会根据实际代码来增加或减少相应的段,顺序也可能和上图不同,但 ELF 文件的头部结构和那几个标准格式段的结构是一致的。
头部结构
首先是 ELF 文件的头部结构,32 位 ELF 文件的头部结构定义如下:
typedef uint32_t Elf32_Addr; // 表示程序地址
typedef uint32_t Elf32_Off; // 表示文件偏移
typedef uint16_t Elf32_Half;
typedef uint32_t Elf32_Word;
typedef int32_t Elf32_Sword;
EI_NIDENT = 16
struct Elf32_Ehdr {
unsigned char e_ident[EI_NIDENT]; // 文件标识
Elf32_Half e_type; // 文件类型
Elf32_Half e_machine; // ELF 文件的 CPU 平台属性,相关常量以 EM_ 开头
Elf32_Word e_version; // ELF 版本号,一般为常数 1
Elf32_Addr e_entry; // 入口地址,规定 ELF 程序的入口虚拟地址,操作系统在加载完该程序后从这个地址开始执行进程的指令
Elf32_Off e_phoff; // Program header 表的文件偏移字节
Elf32_Off e_shoff; // 段表在文件中的偏移
Elf32_Word e_flags; // LF 标志位,用来标识一些 ELF 文件平台相关的属性。相关常量格式一般为 EF_machine_flag,machine 为平台,flag 为标志
Elf32_Half e_ehsize; // ELF 文件头本身的大小
Elf32_Half e_phentsize; // Program header 表的大小
Elf32_Half e_phnum; // Program header 表的数量
Elf32_Half e_shentsize; // 段表描述符的大小,这个一般等于一节
Elf32_Half e_shnum; // 段表描述符数量。这个值等于 ELF 文件中拥有段的数量
Elf32_Half e_shstrndx; // 段表字符串表所在的段在段表中的下标
};
- e_ident(ELF 魔数)
e_ident
是文件标识字段,也就是魔数,32 位 ELF 文件的文件标识为 16 个字节,每个字节含义如下:
7f 45 4c 46 01 01 01 00 00 00 00 00 00 00 00 00
^ ^ ^ ^ ^ ^
E L F | |
/
[ ELF 文件类型 ] [ 字节序 ]
0 无效文件 0 无效格式
1 32 位 ELF 文件 1 小端格式
2 64 位 ELF 文件 2 大端格式
- e_type(文件类型)
e_type
成员表示 ELF 文件类型,系统通过这个常量来判断 ELF 文件类型,而不是文件扩展名。
常量 | 值 | 含义 |
---|---|---|
ET_NONE | 0 | 无类型 |
ET_REL | 1 | 可重定位文件,一般为 .o 文件 |
ET_EXEC | 2 | 可执行文件 |
ET_DYN | 3 | 共享目标文件,一般为 .so 文件 |
ET_CORE | 4 | 核心文件 |
- e_machine(机器类型)
ELF 文件被设计成可以在多个平台下使用,但并不表示同一个 ELF 文件可以在不同的平台下使用,而是表示不同平台下的 ELF 文件都遵循同一套 ELF 标准。e_machine
成员就表示该属性。
相关常量以“EM”开头,例如:
常量 | 值 | 含义 |
---|---|---|
EM_M32 | 1 | AT&T WE 32100 |
EM_SPARC | 2 | SPARC |
EM_386 | 3 | Intel x86 |
EM_68K | 4 | Motorola 68000 |
EM_88K | 5 | Motorola 88000 |
EM_860 | 6 | Intel 80860 |
… |
完整列表可以参考 elf.h
文件中的相关常量定义。
ELF 文件头结构中其他字段上面的注释中已经能够说明对应的含义,部分字段描述了子结构的偏移,包括 Program Header 表和 Section Header 表以及字符串表,这个在解析对应的结构时会用到。
使用 readelf
工具解析 armeabi-v7a/libfoo.so
头结构结果如下:
ELF Header:
Magic: 7f 45 4c 46 01 01 01 00 00 00 00 00 00 00 00 00
Class: ELF32
Data: 2's complement, little endian
Version: 1 (current)
OS/ABI: UNIX - System V
ABI Version: 0
Type: DYN (Shared object file)
Machine: ARM
Version: 0x1
Entry point address: 0x0
Start of program headers: 52 (bytes into file)
Start of section headers: 12920 (bytes into file)
Flags: 0x5000200, Version5 EABI, soft-float ABI
Size of this header: 52 (bytes)
Size of program headers: 32 (bytes)
Number of program headers: 8
Size of section headers: 40 (bytes)
Number of section headers: 27
Section header string table index: 26
段表结构
段表由 Elf32_Shdr
结构体数组描述,每个 Elf32_Shdr
描述一个段。
段表的描述结构体数组在文件中的偏移存放在 ELF 文件头中的 e_shoff
字段中,e_shentsize
和 e_shnum
字字段分别为数组的大小和数量。
struct Elf32_Shdr {
Elf32_Word sh_name; // 段名,位于 .shstrtab 的字符串表。sh_name 是段名在其中的偏移
Elf32_Word sh_type; // 段类型(SHT_*)
Elf32_Word sh_flags; // 段标志位(SHF_*)
Elf32_Addr sh_addr; // 段的虚拟地址,前提是该段可被加载,否则为 0
Elf32_Off sh_offset; // 段偏移,前提是该段存在于文件中,否则无意义
Elf32_Word sh_size; // 段的长度
Elf32_Word sh_link; // 段的链接信息
Elf32_Word sh_info; // 段的额外信息
Elf32_Word sh_addralign; // 段地址对齐
Elf32_Word sh_entsize; // 项的长度
};
- sh_type(段的类型)
段的名字只在编译和链接过程中有意义,无法真正表示段的类型,决定段的属性和类型的是段的类型(sh_type)和段的属性(sh_flag)
段类型的相关常量以 SHT
开头,例如:
常量 | 值 | 含义 |
---|---|---|
SHT_NULL | 0 | 无效段 |
SHT_PROGBITS | 1 | 程序段、代码段、数据段都是这种类型 |
SHT_SYMTAB | 2 | 表示该段的内容为符号表 |
SHT_STRTAB | 3 | 表示该段的内容为字符串表 |
SHT_RELA | 4 | 重定位表,该段包含了重定位信息 |
SHT_HASH | 5 | 符号表的哈希表 |
SHT_DYNAMIC | 6 | 动态链接信息 |
SHT_NOTE | 7 | 提示性信息 |
SHT_NOBITS | 8 | 表示该段在文件中没有内容,比如 .bss 段 |
SHT_REL | 9 | 该段包含了重定位信息 |
SHT_SHLIB | 10 | 保留 |
SHT_DNYSYM | 11 | 动态链接的符号表 |
… |
完整列表可以参考 elf.h
文件中的相关常量定义。
- sh_flag(段的标志)
段的标志位表示该段在进程虚拟地址空间中的属性,比如是否可写,是否可执行等。
常见值如下:
常量 | 值 | 含义 |
---|---|---|
SHF_WRITE | 0x1 | 表示该段在进程空间中可写 |
SHF_ALLOC | 0x2 | 表示该段在进程空间中需要分配空间。有些包含指示或者控制信息的段不需要在进程空间中被分配空间,它们一般不会有这个标志。像代码段、数据段和 .bss 段一般都会有这个标志位 |
SHF_EXECINSTR | 0x4 | 表示该段在进程空间中可以被执行,一般指代码段 |
… |
完整列表可以参考 elf.h
文件中的相关常量定义。
系统保留段相关标志位如下:
Name | sh_type | sh_flags |
---|---|---|
.bss | SHT_NOBITS | SHF_ALLOC + SHF_WRITE |
.comment | SHT_PROGBITS | none |
.data | SHT_PROGBITS | SHF_ALLOC + SHF_WRITE |
.data1 | SHT_PROGBITS | SHF_ALLOC + SHF_WRITE |
.debug | SHT_PROGBITS | none |
.dynamic | SHT_DYNAMIC | SHF_ALLOC + SHF_WRITE 有些系统下 .dynamic 段可能是只读的,所以没有 SHF_WRITE 标志位 |
.hash | SHT_HASH | SHF_ALLOC |
.line | SHT_PROGBITS | none |
.note | SHT_NOTE | none |
.rodata | SHT_PROGBITS | SHF_ALLOC |
.rodata1 | SHT_PROGBITS | SHF_ALLOC |
.shstrtab | SHT_STRTAB | none |
.strtab | SHT_STRTAB | 如果该 ELF 文件中有可装载的段需要用到该字符串表,那么字符串表也将被装载的到内存空间,则有 SHF_ALLOC 标志位 |
.symtab | SHT_SYMTAB | 同字符串表 |
.text | SHT_PROGBITS | SHF_ALLOC + SHF_WRITE |
- sh_link、sh_info(段的链接信息)
段的类型必须是链接相关的(动态或静态),比如重定位表、符号表等。否则这两个成员无意义。
sh_type | sh_link | sh_info |
---|---|---|
SHT_DYNAMIC | 该段所使用的字符串表在段表中的下标 | 0 |
SHT_HASH | 该段所使用的符号表在段表中的下标 | 0 |
SHT_REL SHT_RELA | 该段所使用的相应符号表在段表中的下标 | 该重定位表所作用的段在段表中的下标 |
SHT_SYMTAB SHT_DYNSYM | 操作系统相关的 | 操作系统相关的 |
other | SHN_UNDEF | 0 |
使用 readelf
工具解析 libfoo.so
段表描述结果如下:
Section Headers:
[Nr] Name Type Addr Off Size ES Flg Lk Inf Al
[ 0] NULL 00000000 000000 000000 00 0 0 0
[ 1] .note.android.ide NOTE 00000134 000134 000098 00 A 0 0 4
[ 2] .note.gnu.build-i NOTE 000001cc 0001cc 000024 00 A 0 0 4
[ 3] .dynsym DYNSYM 000001f0 0001f0 000350 10 A 4 1 4
[ 4] .dynstr STRTAB 00000540 000540 000373 00 A 0 0 1
[ 5] .gnu.hash GNU_HASH 000008b4 0008b4 00015c 04 A 3 0 4
[ 6] .hash HASH 00000a10 000a10 000170 04 A 3 0 4
[ 7] .gnu.version VERSYM 00000b80 000b80 00006a 02 A 3 0 2
[ 8] .gnu.version_d VERDEF 00000bec 000bec 00001c 00 A 4 1 4
[ 9] .gnu.version_r VERNEED 00000c08 000c08 000040 00 A 4 2 4
[10] .rel.dyn REL 00000c48 000c48 0000c8 08 A 3 0 4
[11] .rel.plt REL 00000d10 000d10 0000f0 08 AI 3 20 4
[12] .plt PROGBITS 00000e00 000e00 00017c 00 AX 0 0 4
[13] .text PROGBITS 00000f7c 000f7c 0016b4 00 AX 0 0 4
[14] .ARM.exidx ARM_EXIDX 00002630 002630 0001a0 08 AL 13 0 4
[15] .ARM.extab PROGBITS 000027d0 0027d0 000180 00 A 0 0 4
[16] .rodata PROGBITS 00002950 002950 000497 01 AMS 0 0 1
[17] .fini_array FINI_ARRAY 00003e08 002e08 000008 04 WA 0 0 4
[18] .data.rel.ro PROGBITS 00003e10 002e10 000048 00 WA 0 0 4
[19] .dynamic DYNAMIC 00003e58 002e58 000110 08 WA 4 0 4
[20] .got PROGBITS 00003f68 002f68 000098 00 WA 0 0 4
[21] .data PROGBITS 00004000 003000 00000c 00 WA 0 0 4
[22] .bss NOBITS 0000400c 00300c 000005 00 WA 0 0 4
[23] .comment PROGBITS 00000000 00300c 000109 01 MS 0 0 1
[24] .note.gnu.gold-ve NOTE 00000000 003118 00001c 00 0 0 4
[25] .ARM.attributes ARM_ATTRIBUTES 00000000 003134 000034 00 0 0 1
[26] .shstrtab STRTAB 00000000 003168 00010f 00 0 0 1
字符串表结构
字符串表存放 ELF 文件内需要被使用的字符串,它是由多个字符串首尾相连组成,是一段连续的字节。
通常一个 ELF 文件包含多个字符串表,存放段名和存放符号名的字符串表不是同一个。
其他 ELF 结构包含字符串时,只需提供一个所属字符串表中的索引值。
下面是 libfoo.so 文件的一个字符串表一部分的 16 进制视图,字符串表的第一个字节是