因为项目的需要,我对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_hdrelf64_hdr
在Android系统源代码的 /bionic/libc/kernel/uapi/linux/elf.h 可以找到。

以32位程序的ELF头部为例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
06#define EI_NIDENT 16
typedef struct elf32_hdr {
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;

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_v8ax86_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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#define ELFOSABI_NONE       0   /* UNIX System V ABI */
#define ELFOSABI_SYSV 0 /* Alias. */
#define ELFOSABI_HPUX 1 /* HP-UX */
#define ELFOSABI_NETBSD 2 /* NetBSD. */
#define ELFOSABI_LINUX 3 /* Linux. */
#define ELFOSABI_SOLARIS 6 /* Sun Solaris. */
#define ELFOSABI_AIX 7 /* IBM AIX. */
#define ELFOSABI_IRIX 8 /* SGI Irix. */
#define ELFOSABI_FREEBSD 9 /* FreeBSD. */
#define ELFOSABI_TRU64 10 /* Compaq TRU64 UNIX. */
#define ELFOSABI_MODESTO 11 /* Novell Modesto. */
#define ELFOSABI_OPENBSD 12 /* OpenBSD. */
#define ELFOSABI_ARM_AEABI 64 /* ARM EABI */
#define ELFOSABI_ARM 97 /* ARM */
#define ELFOSABI_STANDALONE 255 /* Standalone (embedded) application */

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
2
3
4
#define ET_NONE 0 /* 无定义【非法】 */
#define ET_REL 1 /* 已编译未链接 */
#define ET_EXEC 2 /* 已编译已链接的可执行程序 */
#define ET_DYN 3 /* 已编译已链接的动态链接库 */

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
2
3
4
5
6
7
8
9
10
11
#if defined(__arm__)
return EM_ARM;
#elif defined(__aarch64__)
return EM_AARCH64;
#elif defined(__i386__)
return EM_386;
#elif defined(__mips__)
return EM_MIPS;
#elif defined(__x86_64__)
return EM_X86_64;
#endif

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.cppphdr_table_get_dynamic_section 函数

Android >=7 时,读取段表依靠 e_shoff
修改为其他值会导致无法定位 .dynamic 段,
抛出错误 “.dynamic section header was not found”。

涉及 linker_phdr.cppElfReader::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::ReadProgramHeadersElfReader::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头部,有这些地方可以疯狂偷空间:

  1. 0x06 - 0x0F 合计 10Bytes
  2. 0x18 - 0x1B 合计 4Bytes
  3. 0x24 - 0x2B 合计 8Bytes

无限制存储空间合计为 22Bytes。

以下地方,只有Android <=6 才可以偷空间存东西:

  1. 0x20 - 0x23 合计 4Bytes
  2. 0x2E - 0x33 合计 6Bytes

存在版本限制存储空间合计为 10Bytes。

为什么要偷空间出来存数据?
主要还是为了方便SO库的打包工具做一些附加数据的存储,
偷出来的地方是随SO库加载而加载,可以在内存直接读取的。

值得学习

Android 系统源码提供了很多很好玩的设计思路,下面展示一些:

Code A

根据不同的指令集,使用不同的结构体:

1
2
3
4
5
6
7
8
// 如果是 64bits 的系统
#if defined(__LP64__)
#define ElfW(type) Elf64_ ## type
#else
#define ElfW(type) Elf32_ ## type
#endif

// ElfW(Ehdr) === Elf32_Ehdr

Code B

一些关于指令集的宏定义:

1
2
3
4
5
6
7
8
9
// __arm__ -> armeabi, armeabi-v7a [32bits]
// __aarch64__ -> arm64-v8a [64bits]
// __i386__ -> x86 [32bits]
// __mips__ -> mips, mips64 [32 / 64bits]
// __x86_64__ -> x86_64 [64bits]

#if defined(__arm__)
printf("This is ARM Architecture!");
#endif

除非注明,麦麦小家文章均为原创,转载请以链接形式标明本文地址。

版权声明:自由转载-非商用-非衍生-保持署名(创意共享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/