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頭部為例:
06#define EI_NIDENT 16typedef 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閃退。
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
檔案可以在什麼作業系統執行,參考:
#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檔案型別,如:
#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平臺屬性(指令集)。
參考:
#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.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
根據不同的指令集,使用不同的結構體:
// 如果是 64bits 的系統#if defined(__LP64__)#define ElfW(type) Elf64_ ## type#else#define ElfW(type) Elf32_ ## type#endif// ElfW(Ehdr) === Elf32_Ehdr |
Code B
一些關於指令集的巨集定義:
// __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 |