应用程序及共享库的动态链接

加载原理

应用程序与动态链接库的加载

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 

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的方式访问,编译器实现起来也简单

你可能感兴趣的:(编译与链接,动态链接)