加载原理
应用程序与动态链接库的加载
SylixOS中的应用程序与Linux并不相同,Linux每个进程拥有独立的虚拟地址空间(32位机空间为0 - -3GB),SylixOS的内核与应用共享整个虚拟空间,这样的话就要求不同的应用程序不能占有相同的虚拟地址空间,SylixOS中的应用程序可以理解为与.so
共享库一个道理,并没有执行链接操作,应用程序与.so
具有两个相同的特点:
- 两者都是位置无关码,编译时加入
-PIC
参数,且elf
头类型均为DYN
- 当一个应用程序被加载多次,或者多个进程使用同一个
.so
,都需要做成是共享代码段,并拥有各自的数据段
__execShell
+
|
|
|
v
vprocRun
+ +--->elfLoadReloc(.ko)
| |
+---->API_ModuleLoadEx |
| + |
| +--------->__elfListLoad+--+--->__elfLoad+---+--->elfLoadExec(app and .so)+----->elfPhdrRead+------->dynPhdrParse
| |
| |
+---->pfunEntry +--->elfPhdrRelocate+-------->archElfRelocateRel
这里只讨论APP
或者.so
的加载,对于.ko
的实现方式不同。调用过程中,函数__elfListLoad
实现了elf文件的装载与代码重定位。该函数中,遍历模块本身及依赖模块,通过__elfLoad
函数加载elf
到内存,加载主要过程在elfPhdrRead
函数中。这里需要注意的是,对于APP
或者.so
的代码段是共享的,对于数据段是私有的(保证每个进程拥有私有数据),目前SylixOS的实现中,对于数据段目前没有采用copy-on-write
的方式,而是在加载模块时,直接分配出数据空间来。dynPhdrParse
函数主要作用是解析dynamic
段。
elfPhdrRelocate
函数主要功能就是实现程序的重定位,通过解析prelTable
或者prelaTable
表中需要重定位符号,如果符号是未定的的,需要查找符号表找到符号的地址(查找符号表的顺序是优先查找当前进程及依模块的模块,然后查找内核全局符号表),然后通过archElfRelocateRel
将查找后的地址填入到.got
表中,这里的具体实现与ARCH相关,以ARM平台举例,简单阐述动态加载原理。
测试代码包含了对全局变量_G_uiValue
的引用及外部函数printf
的调用,代码如下(使用Debug版本编译,便于阅读):
#include <stdio.h>
unsigned int _G_uiValue = 0;
int main (int argc, char **argv)
{
_G_uiValue = 10;
printf("SylixOS Loader Test %d\n", _G_uiValue);
return (0);
}
编译并反汇编应用程序,反汇编代码如下,这里只列出相关代码:
Disassembly of section .plt:
00000280 <.plt>:
280: e52de004 push {lr} ; (str lr, [sp, #-4]!)
284: e59fe004 ldr lr, [pc, #4] ; 290 <main-0x10>
288: e08fe00e add lr, pc, lr
28c: e5bef008 ldr pc, [lr, #8]!
290: 0000812c andeq r8, r0, ip, lsr #2
294: e28fc600 add ip, pc, #0, 12 ;ip = 294 + 8 = 0x29c
298: e28cca08 add ip, ip, #8, 20 ; 0x8000 ;ip = 29c + 0x8000 = 0x829c
29c: e5bcf12c ldr pc, [ip, #300]! ; 0x12c ;83c8
Disassembly of section .text:
000002a0 <main>:
2a0: e92d4800 push {fp, lr}
2a4: e28db004 add fp, sp, #4
2a8: e24dd008 sub sp, sp, #8
2ac: e50b0008 str r0, [fp, #-8]
2b0: e50b100c str r1, [fp, #-12]
2b4: e59f3044 ldr r3, [pc, #68] ; 300 <main+0x60> r3 = 0x80fc
2b8: e08f3003 add r3, pc, r3 ; r3 = 80fc + 2b8 + 8 = 0x83bc
2bc: e59f2040 ldr r2, [pc, #64] ; 304 <main+0x64> r2 = 0x10
2c0: e7932002 ldr r2, [r3, r2] ; r2 = [0x83cc] = 0
2c4: e3a0100a mov r1, #10
2c8: e5821000 str r1, [r2]
2cc: e59f2030 ldr r2, [pc, #48] ; 304 <main+0x64>
2d0: e7933002 ldr r3, [r3, r2]
2d4: e5933000 ldr r3, [r3]
2d8: e59f2028 ldr r2, [pc, #40] ; 308 <main+0x68> r2 = 0x28
2dc: e08f2002 add r2, pc, r2 ; r2 = 0x28 + 0x2dc + 8 = 0x30c
2e0: e1a00002 mov r0, r2
2e4: e1a01003 mov r1, r3
2e8: ebffffe9 bl 294 <main-0xc>
2ec: e3a03000 mov r3, #0
2f0: e1a00003 mov r0, r3
2f4: e24bd004 sub sp, fp, #4
2f8: e8bd4800 pop {fp, lr}
2fc: e12fff1e bx lr
300: 000080fc strdeq r8, [r0], -ip
304: 00000010 andeq r0, r0, r0, lsl r0
308: 00000028 andeq r0, r0, r8, lsr #32
Disassembly of section .rodata:
0000030c <.rodata>:
30c: 696c7953 stmdbvs ip!, {r0, r1, r4, r6, r8, fp, ip, sp, lr}^ ; "SylixOS xxx"
310: 20534f78 subscs r4, r3, r8, ror pc
314: 64616f4c strbtvs r6, [r1], #-3916 ; 0xf4c
318: 54207265 strtpl r7, [r0], #-613 ; 0x265
31c: 20747365 rsbscs r7, r4, r5, ror #6
320: 000a6425 andeq r6, sl, r5, lsr #8
Disassembly of section .got:
000083bc <.got>:
83bc: 00008324 andeq r8, r0, r4, lsr #6
...
83c8: 00000280 andeq r0, r0, r0, lsl #5
83cc: 00000000 andeq r0, r0, r0
Disassembly of section .data:
000083d0 <__data_start>:
83d0: 2e372e31 mrccs 14, 1, r2, cr7, cr1, {1}
83d4: Address 0x000083d4 is out of bounds.
Disassembly of section .bss:
000083d8 <_G_uiValue>:
83d8: 00000000 andeq r0, r0, r0
这里先一步步看,先分析主干,毕竟C语言的代码主要逻辑就是赋值、函数调用。从
main
函数的2b4
行开始看,这里先将PC + #68
地址内容传给R3
,需要注意的是由于流水线机制,目前的PC
值为2b4 + 8
,所以R3
的值即为300
处的内容000080fc
。然后继续执行代码,这里为方便理解,将上面列出的部分代码单独拿出来讲解。
2c0: e7932002 ldr r2, [r3, r2] ; r2 = [0x83cc] = 0
2c4: e3a0100a mov r1, #10
2c8: e5821000 str r1, [r2] ; r1的值传入到0x83cc
在C语言中,我们将_G_uiValue
赋值为10,和上面的后两行代码操作很像,但是最后一句将10
这个值存储在了0地址
处,并不是_G_uiValue
变量地址000083d8
,这里就涉及到了重定位的作用,当应用程序加载时,操作系统根据elf
文件中解析出的信息修改.got
表。继续看代码:
2d8: e59f2028 ldr r2, [pc, #40] ; 308 <main+0x68> r2 = 0x28
2dc: e08f2002 add r2, pc, r2 ; r2 = 0x28 + 0x2dc + 8 = 0x30c
这两行主要目的是"SylixOS Loader Test"
字符串首地址当作参数传给函数printf
, r2 = 0x30c
,其中0x30c
就是
.rodata
段首地址,这里放的就是"SylixOS Loader Test"
字符串,不信的话你可以比对一下ascii码。现在函数的参数也都有了,那么最后一个问题就是怎么调用外部函数printf
。
Disassembly of section .plt:
00000280 <.plt>:
280: e52de004 push {lr} ; (str lr, [sp, #-4]!)
284: e59fe004 ldr lr, [pc, #4] ; 290 <main-0x10>
288: e08fe00e add lr, pc, lr
28c: e5bef008 ldr pc, [lr, #8]!
290: 0000812c andeq r8, r0, ip, lsr #2
294: e28fc600 add ip, pc, #0, 12 ;ip = 294 + 8 = 0x29c
298: e28cca08 add ip, ip, #8, 20 ;ip = 29c + 0x8000 = 0x829c
29c: e5bcf12c ldr pc, [ip, #300]! ;83c8内存中的值传给pc
....
2e0: e1a00002 mov r0, r2 ; 传递参数
2e4: e1a01003 mov r1, r3 ;传递参数
2e8: ebffffe9 bl 294 <main-0xc> ;相对跳转
最后一句是跳转到.plt
中的294
处,然后对ip
寄存器进行了一系列计算,最终将83c8
地址的内容传给了寄存器pc
,83c8
也属于.got
段,也是在加载应用程序时,将printf
函数的地址放入到83c8
处。这里假设应用程序被放在了虚拟地址为0x60010000
空间处,此时的所有代码地址都要加上基地址0x60010000
,当操作系统加载应用程序后,.got
表如下:
000083bc <.got>:
83bc: 00008324 andeq r8, r0, r4, lsr #6
...
83c8: 600xxxxx ;这里装的就是printf的地址
83cc: 600183d8 ;这里装的就是_G_uiValue的地址
这样就能访问到外部函数printf
和_G_uiValue
变量了。
总结与问题
- 通过
PIC
编译选项,对于全局符号及外部符号的引用,会通过.plt
与.got
来访问,而访问这两个表的过程中都是通过相对寻址方式。仔细观察你就会发现,之所以称为相对寻址,就是在访问数据或者执行外部函数时,都利用了pc
寄存器,这样无论代码放在哪里,由于相对位置没有改变,再通过对当前pc
做偏移,就可以正常运行代码,所以称之为位置无关码。 - SylixOS与Linux实现方式不同,操作系统刚加载应用时,就会将所有的符号找到,并填写到
.got
中,这样可以保证时间确定性,而Linux是延迟加载,用到的时候才看查找符号并修改.got
- 这里有个疑问,我的理解是对于模块内部的符号,比如例子中的
_G_uiValue
变量,感觉没有必要通过.got
和.plt
的方式访问,可以利用pc
相对跳转实现。我的看法是,首先肯定不能用绝对跳转,不然就不是位置无关码了,利用相对跳转的话就有了跳转范围限制,索性这里就都利用.got
和.plt
的方式访问,编译器实现起来也简单