ELF学习日记-Android SO库文件头分析
因为项目的需要,我对Android系统加载.so
文件有一些些研究,
把最近看过的一些大牛的分析和现状结合一下,
写篇东西做一下笔记。
.so
文件是什么
.so
文件是 Shared Object
文件的后缀,
直白的说就是Linux系统中的“动态链接库” ,
就和Windows系统下的 .dll
文件(Dynamic Link Library)类似。
.so
文件实则是 ELF
格式的文件,和Linux的可执行文件的格式一致。
ELF
文件结构
ELF
格式的文件中的“数据”实际上是以“段”(节,英文:Section)的形式存储的。
其顺序大致符合下面的排序:
- ELF Header:ELF头部
.dynsym
:保存动态链接相关符号,记录其偏移值.dynstr
:.dynsym
的辅助段.hash
:快速查找符号的哈希表,类似.dynstr
.rel.got
:数据引用修正,修正到.got
.rel.plt
:函数引用修正,修正到.got.plt
.text
:代码段- 其他自定义的代码段
.rodata
:字符串常量段.fini_array
:终止函数段.init_array
:初始化函数段.dynamic
:动态链接库特有,存储动态链接用到的表信息.got
:函数的绝对地址.data
:存放已经初始化的全局变量,静态内存分配相关.bss
:存放未初始化的全局变量,静态内存分配相关
ELF 头部:ELF Header
其中,一切的起点都在ELF头部,其偏移量(offset)为 0。
ELF头部的结构体为 elf32_hdr
或 elf64_hdr
,
在Android系统源代码的 /bionic/libc/kernel/uapi/linux/elf.h
可以找到。
以32位程序的ELF头部为例:
1 | 06#define EI_NIDENT 16 |
e_ident
ELF格式文件的识别区域,固定为 16Bytes 的字符串。
下面是这串字符串各个字节的含义。
EI_MAG
- Offset: 0 - 3 (
EI_MAG0
) - Length: 4 (
SELFMAG
) - Type:
String
ELF Identification 的前四个字节为魔数(Magic Number),
内容为 {0x7F, 'E', 'L', 'F'}
。
如果魔数不一致,在Android系统会报错“has bad ELF magic”,
终止 load_library
并APP闪退。
相关代码可以查询源码 /xref/bionic/linker/linker_phdr.cpp
EI_CLASS
- Offset: 4 (
EI_CLASS
) - Length: 1
- Type: Number
判断 ELF
文件是 32位的 还是 64位的。
常量定义:
ELFCLASSNONE
= 0:无定义【非法】ELFCLASS32
= 1:32位ELFCLASS64
= 2:64位ELFCLASSNUM
= 3:未知【非法】
在Android <5
的系统上,由于当年的系统不支持64位的指令集,
因此只要不是32位,就输出错误 “not 32-bit”,并APP闪退。
在Android >=5
系统上,已经出现了64位的指令集
,如 arm64_v8a
、x86_64
。
若32位的指令集遇到64位的SO库,
会输出错误 “is 64-bit instead of 32-bit”,
并APP闪退;
若32位的指令集遇到64位的SO库,
会输出错误 “is 32-bit instead of 64-bit”,
并APP闪退;
若出现非法的 ELFCLASS
,
会输出错误 “has unknown ELF class: ?”,
并APP闪退。
EI_DATA
- Offset: 5 (
EI_DATA
) - Length: 1
- Type: Number (unsigned char)
这个是判断 ELF
文件是 LSB
(Little-endian,低字节序)
还是 MSB
(Big-endian,高字节序)。
常量定义:
ELFDATANONE
= 0:无定义【非法】ELFDATA2LSB
= 1:LSB
ELFDATA2MSB
= 2:MSB
【非法】
安卓系统只允许 LSB
,因此只要不是 1
,
就输出错误 “not little-endian”,
并APP闪退。
EI_VERSION
- Offset: 6
- Length: 1
- Type: Number (unsigned char)
顾名思义,是 ELF
文件格式的版本号,默认是 EV_CURRENT
(= 1)。
Android系统不从这里检测 Version
,而在 e_version
上检测,因此修改无影响。
EI_OSABI
- Offset: 7
- Length: 1
- Type: Number (unsigned char)
指出来该 ELF
文件可以在什么操作系统运行,参考:
1 | #define ELFOSABI_NONE 0 /* UNIX System V ABI */ |
Android系统下的 SO文件 此处的值默认为 0
,
而且加载时不检测,修改无影响。
EI_ABIVERSION
- Offset: 8
- Length: 1
- Type: Number (unsigned char)
指出该 ELF
文件可以在哪个API版本下运行,Android下的默认值是 0
。
此处加载时不检测,修改无影响。
EL_PAD
- Offset: 9
- Length: 7
填充位,无检测,修改无影响。
技巧
Android >= 6
的系统版本只针对 0x00 - 0x05
之间的字节进行检测,0x06 - 0x0F
之间的字节(10 Bytes)无检测,
因此可以任意修改,存放想要存放的数据。
e_type
- Offset: 0x10
- Length: 2
- Type:
unsigned short
ELF文件类型,如:
1 | #define ET_NONE 0 /* 无定义【非法】 */ |
ET_REL
指的是 Relocatable file,
缺少 Program Header,
不可以加载到内存中,gcc
编译时候生成的 .o
文件就是 REL文件。
ET_EXEC
指的是可执行程序,
存在程序入口,
有 Program Header,
可以加载到内存中运行,
在 Linux
下的可执行程序都是这样的。
ET_DYN
特指动态链接库。
由于Android的SO库本质就是动态链接库,
因此SO库编译后 e_type = ET_DYN
。
Android系统会检测 e_type
。
若不为 ET_DYN
,则抛出错误 has unexpected e_type
,
并APP闪退。
e_machine
- Offset: 0x12
- Length: 2
- Type:
unsigned short
ELF
文件的CPU平台属性(指令集)。
参考:
1 | #if defined(__arm__) |
Android系统会使用 GetTargetElfMachine
函数
检测SO库的 e_machine
和现在的系统是否一致。
若不一致,则抛出错误 has unexpected e_machine
,
并APP闪退。
e_version
- Offset: 0x14
- Length: 4
- Type:
unsigned int
顾名思义,是 ELF
文件格式的版本号,默认是 EV_CURRENT
(= 1)。
Android系统会检测 e_version
。
若不为 EV_CURRENT
,则抛出错误 has unexpected e_version
,
并APP闪退。
e_entry
- Offset: 0x18
- Length: 4 (32bits)
- Type:
unsigned int
ELF程序的入口虚拟地址。仅用于可执行程序加载完成后,从此处开始执行进程指令。
动态链接库不存在入口地址,所以Android系统不检测。
e_phoff
- Offset: 0x1C
- Length: 4 (32bits)
- Type:
unsigned int
程序头表的偏移,涉及“连接视图”和“执行视图”。
与实际SO库中代码的指令执行相关,不允许修改。
e_shoff
- Offset: 0x20
- Length: 4 (32bits)
- Type:
unsigned int
段表在文件中的偏移,涉及读取段表。
Android <7
时,读取段表依靠视图,使用的是 e_phoff
,而非 e_shoff
,
因此可以随意修改。
涉及
linker_phdr.cpp
的phdr_table_get_dynamic_section
函数
Android >=7
时,读取段表依靠 e_shoff
,
修改为其他值会导致无法定位 .dynamic
段,
抛出错误 “.dynamic section header was not found”。
涉及
linker_phdr.cpp
的ElfReader::ReadDynamicSection
函数
为什么要强调这个?因为看雪里面的这个文章是不兼容新的OS的,内容过时了。
e_flags
- Offset: 0x24
- Length: 4 (32bits)
- Type:
unsigned int
ELF标志位,用来标志一些ELF文件平台相关的属性。
Android 系统不使用也不检测此参数。
e_ehsize
- Offset: 0x28
- Length: 2
- Type:
unsigned short
ELF头部的长度。
Android 系统不使用也不检测此参数。
e_phentsize
- Offset: 0x2A
- Length: 2
- Type:
unsigned short
程序头的大小。
Android 系统不使用也不检测此参数。
e_phnum
- Offset: 0x2C
- Length: 2
- Type:
unsigned short
在执行视图中,Segments的数量。
Android 系统对其进行检测,并且严格到实际的数目。
若不一致,则抛出错误 “has invalid e_phnum”、“has invalid phdr offset/size”
或者 “phdr mmap failed”等。
涉及函数
ElfReader::ReadProgramHeaders
和ElfReader::ReadProgramHeader
。
e_shentsize
- Offset: 0x2E
- Length: 2
- Type:
unsigned short
段表描述符的大小,= sizeof(ElfW(Shdr))
。
Android <= 6
系统不使用也不检测此参数。
Android >= 7
系统检测此参数。
若与 sizeof(ElfW(Shdr))
不相等,
则抛出错误 “has unsupported e_shentsize”。
e_shnum
- Offset: 0x30
- Length: 2
- Type:
unsigned short
段表描述符的数量。这个值等于ELF文件中拥有的段(section)的数量。
Android <= 6
系统不使用也不检测此参数。
Android >= 7
系统检测并使用此参数。
若为0,则抛出错误 “has no section headers”。
若超出文件大小范围,则抛出错误 “has invalid shdr offset/size”。
参考函数
ElfReader::ReadSectionHeaders
。
e_shstrndx
- Offset: 0x32
- Length: 2
- Type:
unsigned short
段表字符串表所在的段在段表中的下标,一般是 = e_shnum - 1
。
Android <= 6
系统不使用也不检测此参数。
Android >= 7
系统检测此参数。
若为0,则抛出错误 “has invalid e_shstrndx”。
参考函数
ElfReader::VerifyElfHeader
。
偷就完事了
在ELF头部,有这些地方可以疯狂偷空间:
0x06 - 0x0F
合计 10Bytes0x18 - 0x1B
合计 4Bytes0x24 - 0x2B
合计 8Bytes
无限制存储空间合计为 22Bytes。
以下地方,只有Android <=6
才可以偷空间存东西:
0x20 - 0x23
合计 4Bytes0x2E - 0x33
合计 6Bytes
存在版本限制存储空间合计为 10Bytes。
为什么要偷空间出来存数据?
主要还是为了方便SO库的打包工具做一些附加数据的存储,
偷出来的地方是随SO库加载而加载,可以在内存直接读取的。
值得学习
Android 系统源码提供了很多很好玩的设计思路,下面展示一些:
Code A
根据不同的指令集,使用不同的结构体:
1 | // 如果是 64bits 的系统 |
Code B
一些关于指令集的宏定义:
1 | // __arm__ -> armeabi, armeabi-v7a [32bits] |
除非注明,麦麦小家文章均为原创,转载请以链接形式标明本文地址。
版权声明:自由转载-非商用-非衍生-保持署名(创意共享3.0许可证)
本文地址:https://blog.micblo.com/2018/02/10/Android-SO%E5%BA%93%E6%96%87%E4%BB%B6%E5%A4%B4%E5%88%86%E6%9E%90/