关于 ELF 格式文件的笔记
ELF
的全称是 Executable and Linkable Format
,直接翻译过来就是可执行的和可链接的格式,在 Linux
/ Android
中,他们的可执行文件也是 ELF
格式,执行的程序可能还会依赖别的函数库,在 Andorid
中我们称为 native
库,通常分为静态库(以 .a
结尾,该库不会再依赖其他的库)和动态链接库(以 .so
结尾,全称 Shared Object
,在程序运行时动态链接加载到内存中,它本身还有可能依赖其他库)。无论是静态库还是动态库他们也都是 ELF
格式的文件,根据它的命名就很好理解可以执行文件就是 Executable
的,而在运行过程中动态加载的库就是 Linkable
的。学习和理解 ELF
格式的文件对理解 Linux
操作系统和其对应的程序进程执行的方式有很大的帮助,对崩溃的方法栈回溯和 PLT Hook
也有全新的认识。
我自己在 C
语言和 Linux
系统方面也是小白,也看了别人介绍的文章,经常看得云里雾里的,这次我来记录一下最近学习到的东西,可能有的不对,谨慎参考。
C 编译过程
在这之前我们先来看看 C
的源代码是如何编译成可执行文件的,先看图,我再解释。
我们通常是有多个源码文件,但是只有一个源码文件以 main()
函数作为程序执行的入口函数,在 main()
函数中通常他会依赖其他源码文件中的代码(可能还会依赖上面讲到静态库和动态库),当然其他源码文件可能也有互相的依赖。在 C
语言中通过头文件来表示这种依赖关系(无论是库还是不同源码文件的依赖),通常在头文件中会定义一些结构体,全局变量和未实现的方法等等,而源码文件中就是方法的的真正实现,在开始编译开始前就需要先处理这些头文件,编译器会把源代码中依赖的头文件全部合并到源码文件中去,这个过程中还可以执行一些宏脚本来控制这个合并过程,这个过程称为 预处理。
在预处理完成后,编译器会根据合并后的源代码,在编译成汇编代码,这个过程称为编译。
接着我们还需要对上一个步骤生成的汇编代码再编译,生成对应设备能够执行的机器码指令,生成的文件是以 .o
结尾的文件,它的文件格式也是 ELF
的,它是一个可链接的文件类型,这个过程称为汇编。
讲道理汇编后的的 .o
文件中已经是机器码了,是不是就可以直接执行了?emmm,讲道理没有依赖的话确实可以直接执行了。不过大部分情况下都有依赖关系,前面不是说到了吗 .o
文件是一个可链接类型的文件,这个时候 Linker
就会把上述所有源码生成的 .o
文件链接合并成一个可执行的文件,这个可执行文件就会以 main()
函数的地址作为执行的入口地址,最后输出的文件就是可以单独执行的程序了,这个过程称为 链接。
上面图中的编译过程包含我上面描述的预处理,编译,汇编等过程。
程序运行
上面编译出来的可执行程序就一定能够正常的运行吗?不一定,因为我们上面有提到可能还会有依赖的库(静态库和动态链接库),如果我们的设备上缺少这些库也就会报错,后续我们以动态链接库分析。
还记得我还是学生的时候就经常去下载一些盗版游戏玩,有的时候就能遇到 "缺少 xxxx.dll "的问题,.dll
文件就是 Windows
系统上的动态链接库,然后我们就去网上去下载缺少的库文件,然后根据教程把它放在特定的位置,然后游戏就可以正常运行啦。
参考下图:
我们想象一下没有动态链接库的依赖程序会变成什么样?每个程序中都有都有一些非常通用的逻辑,比如说求平方根、基础图形渲染和声音播放等等很多程序都会用到的逻辑,那么每个程序都包含他们用到的很通用的代码,那么他们的可执行文件就非常大,当程序运行时这些庞大的执行文件就会被加载到内存中,由于每个程序都这个干就会导致内存的占用就特别多。但是我们仔细想想,每个程序用到的基础逻辑都是一样的,也就是我们的内存中占用了大量的重复的基础逻辑,这是对内存的严重浪费。
当我们把这些通用的逻辑独立出来,单独编译成一个个动态链接库,然后把他们放在操作系统的特定目录下,我们的可执行程序就不需要包含这部分代码,当我们的程序加载到内存中后,系统就会根据我们程序的声明去查找这些库,然后把这些库加载到内存中供我们使用,这样不就是太酷啦。在 Linux
中会把动态加载到内核空间,然后映射到应用进程的地址上,如果还有别的进程也需要这个库,由于加载过了就不会再重复加载,只需要把地址映射上去就好了,也就是所有的进程都共享同一份内存中的代码,这也许就是叫做 Shared Object
的原因吧,通过这样的方式也大大降低了代码对于内存的占用。
在调用动态链接库的方法最主要的一步就是如何找到我们所调用的方法对应的代码的地址,在 ELF
文件中定义了很多的信息来帮助我们找到我们所需要方法的地址,我们继续来看看 ELF
文件中有哪些信息。
ELF 文件基本结构
主要结构包含 ELF
头,Program
头表(段表),Section
(节),Section
头表(节表), 其中最最最重要的就是 Section
,Section
中包含了我们代码中的所有的信息,比如机器码指令,字符表,符号表(也就是我们定义的变量,方法名等等),动态链接信息(依赖的动态链接库和如何找到相关方法的信息等等)。
查看 ELF
我使用的是 readelf
工具,你也可以找找其他工具。
ELF 头
text
ELF 头:
Magic: 7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00
类别: ELF64
数据: 2 补码,小端序 (little endian)
Version: 1 (current)
OS/ABI: UNIX - System V
ABI 版本: 0
类型: DYN (共享目标文件)
系统架构: AArch64
版本: 0x1
入口点地址: 0x19280
程序头起点: 64 (bytes into file)
Start of section headers: 246208 (bytes into file)
标志: 0x0
Size of this header: 64 (bytes)
Size of program headers: 56 (bytes)
Number of program headers: 9
Size of section headers: 64 (bytes)
Number of section headers: 27
Section header string table index: 26
这里面的内容很简单描述了基本文件类型,入口程序位置等等,重要的信息是段表头和节表头中 Item 的数量,表头大小和开始的位置,根据这些信息我们就能够很容易地获取段表头和节表头。
节和节头
先看看节表头:
text
节头:
[号] 名称 类型 地址 偏移量
大小 全体大小 旗标 链接 信息 对齐
[ 0] NULL 0000000000000000 00000000
0000000000000000 0000000000000000 0 0 0
[ 1] .note.androi[...] NOTE 0000000000000238 00000238
0000000000000098 0000000000000000 A 0 0 4
[ 2] .note.gnu.bu[...] NOTE 00000000000002d0 000002d0
0000000000000024 0000000000000000 A 0 0 4
[ 3] .dynsym DYNSYM 00000000000002f8 000002f8
0000000000002910 0000000000000018 A 8 1 8
[ 4] .gnu.version VERSYM 0000000000002c08 00002c08
000000000000036c 0000000000000002 A 3 0 2
[ 5] .gnu.version_r VERNEED 0000000000002f74 00002f74
00000000000000e0 0000000000000000 A 8 7 4
[ 6] .gnu.hash GNU_HASH 0000000000003058 00003058
0000000000000acc 0000000000000000 A 3 0 8
[ 7] .hash HASH 0000000000003b24 00003b24
0000000000000db8 0000000000000004 A 3 0 4
[ 8] .dynstr STRTAB 00000000000048dc 000048dc
0000000000002451 0000000000000000 A 0 0 1
[ 9] .rela.dyn RELA 0000000000006d30 00006d30
0000000000008178 0000000000000018 A 3 0 8
[10] .rela.plt RELA 000000000000eea8 0000eea8
0000000000000c18 0000000000000018 AI 3 22 8
[11] .rodata PROGBITS 000000000000fac0 0000fac0
0000000000003b7c 0000000000000000 AMS 0 0 16
[12] .gcc_except_table PROGBITS 000000000001363c 0001363c
00000000000003bc 0000000000000000 A 0 0 4
[13] .eh_frame_hdr PROGBITS 00000000000139f8 000139f8
000000000000117c 0000000000000000 A 0 0 4
[14] .eh_frame PROGBITS 0000000000014b78 00014b78
00000000000046fc 0000000000000000 A 0 0 8
[15] .text PROGBITS 0000000000019280 00019280
000000000001ef10 0000000000000000 AX 0 0 16
[16] .plt PROGBITS 0000000000038190 00038190
0000000000000830 0000000000000000 AX 0 0 16
[17] .data.rel.ro PROGBITS 00000000000399c0 000389c0
0000000000002ea0 0000000000000000 WA 0 0 8
[18] .fini_array FINI_ARRAY 000000000003c860 0003b860
0000000000000010 0000000000000000 WA 0 0 8
[19] .init_array INIT_ARRAY 000000000003c870 0003b870
0000000000000008 0000000000000000 WA 0 0 8
[20] .dynamic DYNAMIC 000000000003c878 0003b878
0000000000000250 0000000000000010 WA 8 0 8
[21] .got PROGBITS 000000000003cac8 0003bac8
00000000000000b8 0000000000000000 WA 0 0 8
[22] .got.plt PROGBITS 000000000003cb80 0003bb80
0000000000000420 0000000000000000 WA 0 0 8
// ...
节表头中描述了有哪些节,节的名字,类型,大小,地址等等。
不同的节存储了不同的重要信息,我简单列举一些:
- .text:存储机器码指令
- .dynstr 和 .shstrtab:字符表,存储程序中用到的字符串,比如方法名变量名,别的地方如果用到了
offset
来代表,在这个表中就能够查到对应的值,类似于Java
中常量池中的字符串。 - .dynsym: 符号表,也就是存储我们代码中定义的方法名,全局变量名等等。
- .dynamic:动态链接表,存储动态链接库相关的信息,比如库的名字等等。
- .got 和 .got.plt:和动态链接库的地址定位相关,目前我也没有看懂。
其他的我就先不列举了,大家可以去找找资料看看。
我这里以符号表为例子给大家看看:
less
Symbol table '.dynsym' contains 438 entries:
Num: Value Size Type Bind Vis Ndx Name
// ...
89: 0000000000000000 0 FUNC GLOBAL DEFAULT UND pthread[...]@LIBC (2)
90: 0000000000000000 0 FUNC GLOBAL DEFAULT UND dl_iter[...]@LIBC (8)
91: 0000000000000000 0 FUNC GLOBAL DEFAULT UND pthread[...]@LIBC (2)
92: 0000000000000000 0 FUNC GLOBAL DEFAULT UND fwrite@LIBC (2)
93: 000000000001abb8 8 FUNC GLOBAL DEFAULT 15 Java_com_tans_tm[...]
94: 0000000000012eda 4 OBJECT GLOBAL DEFAULT 11 _ZTSPKb
95: 0000000000012f02 2 OBJECT GLOBAL DEFAULT 11 _ZTSs
96: 000000000003c200 16 OBJECT GLOBAL DEFAULT 17 _ZTIe
97: 0000000000012fa4 3 OBJECT GLOBAL DEFAULT 11 _ZTSDi
98: 00000000000338d4 8 FUNC GLOBAL DEFAULT 15 _ZNSt9exceptionD2Ev
99: 000000000001305d 13 OBJECT GLOBAL DEFAULT 11 _ZTSSt9bad_alloc
100: 000000000001aef4 24 FUNC GLOBAL DEFAULT 15 Java_com_tans_tm[...]
101: 0000000000012eec 4 OBJECT GLOBAL DEFAULT 11 _ZTSPKc
102: 0000000000012f0b 2 OBJECT GLOBAL DEFAULT 11 _ZTSt
103: 000000000003bf40 32 OBJECT GLOBAL DEFAULT 17 _ZTIPl
104: 000000000003c160 16 OBJECT GLOBAL DEFAULT 17 _ZTIf
105: 000000000001ac64 32 FUNC GLOBAL DEFAULT 15 Java_com_tans_tm[...]
106: 000000000003396c 8 FUNC GLOBAL DEFAULT 15 _ZNSt9type_infoD2Ev
107: 000000000003bf90 32 OBJECT GLOBAL DEFAULT 17 _ZTIPm
108: 0000000000012f76 4 OBJECT GLOBAL DEFAULT 11 _ZTSPKd
109: 000000000003c250 16 OBJECT GLOBAL DEFAULT 17 _ZTIg
110: 0000000000012fb0 34 OBJECT GLOBAL DEFAULT 11 _ZTSN10__cxxabiv[...]
111: 000000000001303e 13 OBJECT GLOBAL DEFAULT 11 _ZTSSt9exception
112: 000000000001adc0 40 FUNC GLOBAL DEFAULT 15 Java_com_tans_tm[...]
113: 000000000001b04c 8 FUNC WEAK DEFAULT 15 _ZdlPvRKSt9nothrow_t
114: 000000000003baa8 24 OBJECT GLOBAL DEFAULT 17 _ZTIN10__cxxabiv[...]
115: 0000000000012ec0 2 OBJECT GLOBAL DEFAULT 11 _ZTSv
116: 000000000003bd50 16 OBJECT GLOBAL DEFAULT 17 _ZTIh
117: 000000000003c080 32 OBJECT GLOBAL DEFAULT 17 _ZTIPn
118: 0000000000012f7f 4 OBJECT GLOBAL DEFAULT 11 _ZTSPKe
119: 0000000000012ede 2 OBJECT GLOBAL DEFAULT 11 _ZTSw
120: 000000000003be90 16 OBJECT GLOBAL DEFAULT 17 _ZTIi
121: 000000000003c0d0 32 OBJECT GLOBAL DEFAULT 17 _ZTIPo
122: 0000000000012f6d 4 OBJECT GLOBAL DEFAULT 11 _ZTSPKf
123: 000000000001ac5c 8 FUNC GLOBAL DEFAULT 15 Java_com_tans_tm[...]
124: 0000000000012ec9 3 OBJECT GLOBAL DEFAULT 11 _ZTSDn
125: 0000000000012e29 37 OBJECT GLOBAL DEFAULT 11 _ZTSN10__cxxabiv[...]
126: 000000000003bee0 16 OBJECT GLOBAL DEFAULT 17 _ZTIj
127: 0000000000012f38 2 OBJECT GLOBAL DEFAULT 11 _ZTSx
128: 0000000000012f88 4 OBJECT GLOBAL DEFAULT 11 _ZTSPKg
129: 00000000000338e4 16 FUNC GLOBAL DEFAULT 15 _ZNKSt9exception[...]
130: 000000000003399c 44 FUNC GLOBAL DEFAULT 15 _ZNSt8bad_castD0Ev
131: 000000000003c750 40 OBJECT GLOBAL DEFAULT 17 _ZTVSt10bad_typeid
132: 000000000001aeec 8 FUNC GLOBAL DEFAULT 15 Java_com_tans_tm[...]
133: 000000000001b198 8 FUNC WEAK DEFAULT 15 _ZdlPvSt11align_[...]
134: 0000000000012ef5 4 OBJECT GLOBAL DEFAULT 11 _ZTSPKh
135: 0000000000012f41 2 OBJECT GLOBAL DEFAULT 11 _ZTSy
136: 000000000003395c 16 FUNC GLOBAL DEFAULT 15 _ZNKSt20bad_arra[...]
137: 000000000001ac14 8 FUNC GLOBAL DEFAULT 15 Java_com_tans_tm[...]
138: 000000000003bac0 24 OBJECT GLOBAL DEFAULT 17 _ZTIN10__cxxabiv[...]
139: 0000000000012f19 4 OBJECT GLOBAL DEFAULT 11 _ZTSPKi
140: 000000000003bf30 16 OBJECT GLOBAL DEFAULT 17 _ZTIl
141: 000000000001abe8 8 FUNC GLOBAL DEFAULT 15 Java_com_tans_tm[...]
142: 000000000001b1a0 12 FUNC WEAK DEFAULT 15 _ZdlPvmSt11align[...]
143: 000000000003dfd0 8 OBJECT GLOBAL DEFAULT 23 __cxa_terminate_[...]
144: 0000000000012de3 34 OBJECT GLOBAL DEFAULT 11 _ZTSN10__cxxabiv[...]
145: 000000000003be00 32 OBJECT GLOBAL DEFAULT 17 _ZTIPs
146: 0000000000012f22 4 OBJECT GLOBAL DEFAULT 11 _ZTSPKj
147: 000000000003bf80 16 OBJECT GLOBAL DEFAULT 17 _ZTIm
148: 0000000000012f63 5 OBJECT GLOBAL DEFAULT 11 _ZTSPKDh
149: 000000000003be50 32 OBJECT GLOBAL DEFAULT 17 _ZTIPt
150: 000000000003c070 16 OBJECT GLOBAL DEFAULT 17 _ZTIn
151: 0000000000012fab 5 OBJECT GLOBAL DEFAULT 11 _ZTSPKDi
152: 0000000000013090 12 OBJECT GLOBAL DEFAULT 11 _ZTSSt8bad_cast
153: 0000000000012f2b 4 OBJECT GLOBAL DEFAULT 11 _ZTSPKl
154: 000000000003c0c0 16 OBJECT GLOBAL DEFAULT 17 _ZTIo
155: 0000000000012f98 3 OBJECT GLOBAL DEFAULT 11 _ZTSDs
156: 0000000000019a5c 16 FUNC GLOBAL DEFAULT 15 _ZN19tMediaPlaye[...]
// ...
数据有点多,里面描述了符号的类型,大小,对应的值等等信息。并不是所有的节都是这么容易看的,不同的节内容差别很大,比如代码相关的 .text 节就很抽象,它是机器码,通常需要反编译成汇编来看。
段
很多人都会把节和段搞混淆,在系统加载程序的过程中会为不同的节按照功能分类,同一类的节就分配到同一段空间中,这个分类就是段,也就是一个段包含若干的节,参考下图:
段表中描述了对节的映射,还定义了段的大小,我们看看段表:
text
程序头:
Type Offset VirtAddr PhysAddr
FileSiz MemSiz Flags Align
PHDR 0x0000000000000040 0x0000000000000040 0x0000000000000040
0x00000000000001f8 0x00000000000001f8 R 0x8
LOAD 0x0000000000000000 0x0000000000000000 0x0000000000000000
0x00000000000389c0 0x00000000000389c0 R E 0x1000
LOAD 0x00000000000389c0 0x00000000000399c0 0x00000000000399c0
0x00000000000035e0 0x00000000000035e0 RW 0x1000
LOAD 0x000000000003bfa0 0x000000000003dfa0 0x000000000003dfa0
0x0000000000000068 0x0000000000000b30 RW 0x1000
DYNAMIC 0x000000000003b878 0x000000000003c878 0x000000000003c878
0x0000000000000250 0x0000000000000250 RW 0x8
GNU_RELRO 0x00000000000389c0 0x00000000000399c0 0x00000000000399c0
0x00000000000035e0 0x0000000000003640 R 0x1
GNU_EH_FRAME 0x00000000000139f8 0x00000000000139f8 0x00000000000139f8
0x000000000000117c 0x000000000000117c R 0x4
GNU_STACK 0x0000000000000000 0x0000000000000000 0x0000000000000000
0x0000000000000000 0x0000000000000000 RW 0x0
NOTE 0x0000000000000238 0x0000000000000238 0x0000000000000238
0x00000000000000bc 0x00000000000000bc R 0x4
Section to Segment mapping:
段节...
00
01 .note.android.ident .note.gnu.build-id .dynsym .gnu.version .gnu.version_r .gnu.hash .hash .dynstr .rela.dyn .rela.plt .rodata .gcc_except_table .eh_frame_hdr .eh_frame .text .plt
02 .data.rel.ro .fini_array .init_array .dynamic .got .got.plt
03 .data .bss
04 .dynamic
05 .data.rel.ro .fini_array .init_array .dynamic .got .got.plt
06 .eh_frame_hdr
07
08 .note.android.ident .note.gnu.build-id
最后
到这里算是对 ELF
有了一个基本的认识,关于具体如何定位动态链接库等等问题还需要继续的深入学习。