当我们执行一个可执行程序的时候, 内核会list_for_each_entry遍历所有注册的linux_binfmt对象, 对其调用load_binrary方法来尝试加载, 直到加载成功为止。上面代码可以看倒,ELF中加载程序即为load_elf_binary,内核中已经注册的可运行文件结构linux_binfmt会让其所属的加载程序load_binary逐一前来认领需要运行的程序binary,如果某个格式的处理程序发现相符后,便执行该格式映像的装入和启动。
https://github.com/novelinux/linux-4.x.y/tree/master/fs/exec.c/sys_execve.md
$ readelf -l app_process32
Elf file type is DYN (Shared object file)
Entry point 0x1739
There are 9 program headers, starting at offset 52
Program Headers:
Type Offset VirtAddr PhysAddr FileSiz MemSiz Flg Align
PHDR 0x000034 0x00000034 0x00000034 0x00120 0x00120 R 0x4
INTERP 0x000154 0x00000154 0x00000154 0x00013 0x00013 R 0x1
[Requesting program interpreter: /system/bin/linker]
LOAD 0x000000 0x00000000 0x00000000 0x05b87 0x05b87 R E 0x1000
LOAD 0x005c98 0x00006c98 0x00006c98 0x00368 0x01205 RW 0x1000
DYNAMIC 0x005d4c 0x00006d4c 0x00006d4c 0x00160 0x00160 RW 0x4
NOTE 0x000168 0x00000168 0x00000168 0x00038 0x00038 R 0x4
GNU_STACK 0x000000 0x00000000 0x00000000 0x00000 0x00000 RW 0x10
EXIDX 0x0050ec 0x000050ec 0x000050ec 0x00220 0x00220 R 0x4
GNU_RELRO 0x005c98 0x00006c98 0x00006c98 0x00368 0x00368 RW 0x4
Section to Segment mapping:
Segment Sections...
00
01 .interp
02 .interp .note.android.ident .note.gnu.build-id .dynsym .dynstr .gnu.hash .gnu.version .gnu.version_r .rel.dyn .rel.plt .plt .text .ARM.exidx .ARM.extab .rodata
03 .preinit_array .init_array .fini_array .data.rel.ro .dynamic .got .bss
04 .dynamic
05 .note.android.ident .note.gnu.build-id
06
07 .ARM.exidx
08 .preinit_array .init_array .fini_array .data.rel.ro .dynamic .got
前面的步骤已经完成了目标映像和解释器的加载,并且将目标程序的各个段家在近内存,但是,一个程序成功执行,操作系统还需要知道程序的入口地址,才能开始执行加载好的映像。如果需要动态链接,就通过load_elf_interp装入解释器映像, 并把将来进入用户空间的入口地址设置成load_elf_interp()的返回值,即解释器映像的入口地址。而若不需要装入解释器,那么这个入口地址就是目标映像本身的入口地址。
1.解释器(也可以叫动态链接器)首先检查可执行程序所依赖的共享库,并在需要的时候对其进行加载。ELF 文件有一个特别的节区: .dynamic,它存放了和动态链接相关的很多信息,例如动态链接器通过它找到该文件使用的动态链接库。找到动态链接库后,就可以将其加载到内存中。
$ arm-linux-androideabi-readelf -S app_process32
There are 27 section headers, starting at offset 0x6d38:
Section Headers:
[Nr] Name Type Addr Off Size ES Flg Lk Inf Al
[ 0] NULL 00000000 000000 000000 00 0 0 0
[ 1] .interp PROGBITS 00000154 000154 000013 00 A 0 0 1
[ 2] .note.android.ide NOTE 00000168 000168 000018 00 A 0 0 4
[ 3] .note.gnu.build-i NOTE 00000180 000180 000020 00 A 0 0 4
[ 4] .dynsym DYNSYM 000001a0 0001a0 000550 10 A 5 1 4
[ 5] .dynstr STRTAB 000006f0 0006f0 0007e5 00 A 0 0 1
[ 6] .gnu.hash GNU_HASH 00000ed8 000ed8 000048 04 A 4 0 4
[ 7] .gnu.version VERSYM 00000f20 000f20 0000aa 02 A 4 0 2
[ 8] .gnu.version_r VERNEED 00000fcc 000fcc 000060 00 A 5 2 4
[ 9] .rel.dyn REL 0000102c 00102c 000158 08 A 4 0 4
[10] .rel.plt REL 00001184 001184 000240 08 AI 4 21 4
[11] .plt PROGBITS 000013c4 0013c4 000374 00 AX 0 0 4
[12] .text PROGBITS 00001738 001738 0039b1 00 AX 0 0 4
[13] .ARM.exidx ARM_EXIDX 000050ec 0050ec 000220 08 AL 12 0 4
[14] .ARM.extab PROGBITS 0000530c 00530c 000030 00 A 0 0 4
[15] .rodata PROGBITS 00005340 005340 000847 00 A 0 0 8
[16] .preinit_array PREINIT_ARRAY 00006c98 005c98 000008 04 WA 0 0 4
[17] .init_array INIT_ARRAY 00006ca0 005ca0 00000c 04 WA 0 0 4
[18] .fini_array FINI_ARRAY 00006cac 005cac 000008 04 WA 0 0 4
[19] .data.rel.ro PROGBITS 00006cb4 005cb4 000098 00 WA 0 0 4
[20] .dynamic DYNAMIC 00006d4c 005d4c 000160 08 WA 5 0 4
[21] .got PROGBITS 00006eac 005eac 000154 00 WA 0 0 4
[22] .bss NOBITS 00007000 006000 000e9d 00 WA 0 0 16
[23] .note.gnu.gold-ve NOTE 00000000 006000 00001c 00 0 0 4
[24] .ARM.attributes ARM_ATTRIBUTES 00000000 00601c 000040 00 0 0 1
[25] .gnu_debugdata PROGBITS 00000000 00605c 000bb8 00 0 0 1
[26] .shstrtab STRTAB 00000000 006c14 000123 00 0 0 1
Key to Flags:
W (write), A (alloc), X (execute), M (merge), S (strings)
I (info), L (link order), G (group), T (TLS), E (exclude), x (unknown)
O (extra OS processing required) o (OS specific), p (processor specific)
2.解释器对程序的外部引用进行重定位,并告诉程序其引用的外部变量/函数的地址,此地址位于共享库被加载在内存的区间内。动态链接还有一个延迟定位的特性,即只有在“真正”需要引用符号时才重定位,这对提高程序运行效率有极大帮助。(如果设置了 LD_BIND_NOW 环境变量,这个动作就会直接进行)下面具体说明符号重定位的过程。
首先了解几个概念。 符号,也就是可执行程序代码段中的变量名、函数名等。重定位是将符号引用与符号定义进行链接的过程,对符号的引用本质是对其在内存中具体地址的引用,所以本质上来说,符号重定位要解决的是当前编译单元如何访问「外部」符号这个问题。动态链接是在程序运行时对符号进行重定位,也叫运行时重定位(而静态链接则是在编译时进行,也叫链接时重定位)现代操作系统中,二进制映像的代码段不允许被修改,而数据段能被修改。
但对于动态链接来说,有两个不同的地方:
*(1)因为不允许对可执行文件的代码段进行加载时符号重定位,因此如果可执行文件引用了动态库中的数据符号,则在该可执行文件内对符号的重定位必须在链接阶段完成,为做到这一点,链接器在构建可执行文件的时候,会在当前可执行文件的数据段里分配出相应的空间来作为该符号真正的内存地址,等到运行时加载动态库后,再在动态库中对该符号的引用进行重定位:把对该符号的引用指向可执行文件数据段里相应的区域。
*(2)ELF 文件对调用动态库中的函数采用了所谓的"延迟绑定"(lazy binding)策略, 只有当该函数在其第一次被调用发生时才最终被确认其真正的地址,因此我们不需要在调用动态库函数的地方直接填上假的地址,而是使用了一些跳转地址作为替换,这样一来连修改动态库和可执行程序中的相应代码都不需要进行了,当然延迟绑定的目的不是为了这个,具体先不细说。
可执行程序对符号的访问又分为模块内和模块间的访问,这里只介绍模块间的访问,也就是访问动态链接库中的符号。
PLT就是程序链接表(Procedure Link Table),属于代码段。用于把位置独立的函数调用重定向到绝对位置。每个动态链接的程序和共享库都有一个PLT,PLT表的每一项都是一小段代码,从对应的GOT表项中读取目标函数地址。程序对某个函数的第一次访问都被调整为对 PLT入口也就是PLT0的访问,也就是说所有的PLT首次执行时,最后都会跳转到第一个PLT中执行。PLT0是一段访问动态链接器的特殊代码,是动态链接做符号解析和重定位的公共入口。这样做的好处是不用每个PLT表都有重复的一份指令,可以减少PLT指令条数。
PLT表结构如下图所示
0000142c <strncmp@plt>:
142c: e28fc600 add ip, pc, #0, 12
1430: e28cca05 add ip, ip, #20480 ; 0x5000
1434: e5bcfac8 ldr pc, [ip, #2760]! ; 0xac8
00001438 <strlen@plt>:
1438: e28fc600 add ip, pc, #0, 12
143c: e28cca05 add ip, ip, #20480 ; 0x5000
1440: e5bcfac0 ldr pc, [ip, #2752]! ; 0xac0
可以看到,PLT会先执行ldr pc指令跳转到某一个地址,而这个地址就对应的GOT表项。
GOT就是全局偏移表(Global Offset Table),属于数据段。为了能使得代码段里对数据及函数的引用与具体地址无关,只能再作一层跳转,ELF 的做法是在动态库的数据段中加一个表项,也就是GOT 。GOT表格中放的是数据全局符号的地址,该表项在动态库被加载后由动态加载器进行初始化,动态库内所有对数据全局符号的访问都到该表中来取出相应的地址,即可做到与具体地址了,而该表作为动态库的一部分,访问起来与访问模块内的数据是一样的。