本文乃fireaxe原创,使用GPL发布,可以自由拷贝,转载。但转载请保持文档的完整性,并注明原作者及原链接。内容可任意使用,但对因使用该内容引起的后果不做任何保证。
作者:fireaxe.hq@outlook.com
博客:fireaxe.blog.chinaunix.net
1.1 原理介绍
u-boot通常都是存在ROM或者Flash上,以保证CPU启动后可以直接运行u-boot。但ROM的问题是只能读不能写,不利于程序的执行。如:全局变量读写,地址空间限制等问题。因此u-boot会先把自己拷贝到RAM中去执行。这一拷贝带来的问题是执行地址的混乱。代码的执行地址通常都是在编译时有链接地址指定的,如何保证拷贝前后都可以执行呢?
一个办法是使用拷贝到RAM后的地址作为编译时的链接地址,拷贝前所有函数与全局变量的调用都增加偏移量。(如VxWorks的bootloader)尽量减少拷贝前需要执行的代码量。
两一个地址是把image编译成与地址无关的程需,也就是PIC - Position independent code。编译器无法保证代码的独立性,它需要与加载器配合起来。U-boot自己加载自己,所以她自己就是加载器。
域代码无关代码依赖于下面两种技术:
1) 使用相对地址
2) 加载器可以自动更新涉及到绝对地址的指令
PIC的实现方式不止一种,不对CPU架构下的实现也有区别。这里主要结合搜啊ARM架构下u-boot中PIC的实现方式
1.2 一个简单例子
先通过一个简单例子介绍编译成PIC与非PIC的区别
1.2.1 源码
test0.1c |
int foo(int a);
int ttt = 1; int xxx = 5;
int boo() { foo(xxx); return foo(ttt); } |
这是一个简单的程序。Foo函数的实现在另一个文件,boo函数调用了foo函数与全局变量ttt与xxx。
1.2.2 Compile
对于PowerPC架构,u-boot只是在编译时使用了-fpic。这种方式会生成一个.got段来存储绝对地址符号。对与ARM架构,则是在编译时使用-mword-relocations,生成与位置无关代码;链接时使用-pie生成.rel.dyn段,该段中的每个条目被称为一个LABEL,用来存储绝对地址符号的地址。
1.2.3 PIC如何找到所有的绝对地址符号
Non-PIC |
PIC |
arm-qhao-linux-gnueabi-gcc -c test01.c arm-qhao-linux-gnueabi-ld -o a.out test01.o test02.o
|
arm-qhao-linux-gnueabi-gcc -c -mword-relocations test01.c arm-qhao-linux-gnueabi-ld -pie -o a.out test01.o test02.o
|
SYMBOL TABLE: 00008094 l d .text 00000000 .text 000100f8 l d .data 00000000 .data 000100fc g O .data 00000004 xxx 000100f8 g O .data 00000004 ttt
|
SYMBOL TABLE: 00000220 l d .rel.dyn 00000000 .rel.dyn 00000230 l d .text 00000000 .text 00008320 l d .data 00000000 .data 00008324 g O .data 00000004 xxx 00008320 g O .data 00000004 ttt
|
Disassembly of section .text:
00008094
8094: e92d4800 push {fp, lr} 8098: e28db004 add fp, sp, #4 809c: e30030fc movw r3, #252 ; 0xfc 80a0: e3403001 movt r3, #1 80a4: e5933000 ldr r3, [r3] 80a8: e1a00003 mov r0, r3
80ac: eb000007 bl 80d0
80b0: e30030f8 movw r3, #248 ; 0xf8 80b4: e3403001 movt r3, #1 80b8: e5933000 ldr r3, [r3] 80bc: e1a00003 mov r0, r3
80c0: eb000002 bl 80d0
80c4: e1a03000 mov r3, r0 80c8: e1a00003 mov r0, r3 80cc: e8bd8800 pop {fp, pc}
|
Disassembly of section .text:
00000230
230: e92d4800 push {fp, lr} 234: e28db004 add fp, sp, #4
238: e59f3024 ldr r3, [pc, #36] ; 264 23c: e5933000 ldr r3, [r3] 240: e1a00003 mov r0, r3
244: eb000008 bl 26c
248: e59f3018 ldr r3, [pc, #24] ; 268 24c: e5933000 ldr r3, [r3] 250: e1a00003 mov r0, r3
254: eb000004 bl 26c 258: e1a03000 mov r3, r0 25c: e1a00003 mov r0, r3 260: e8bd8800 pop {fp, pc} 264: 00008324 andeq r8, r0, r4, lsr #6 ; this is called as Lable 268: 00008320 andeq r8, r0, r0, lsr #6 |
Disassembly of section .data:
000100f8
100f8: 00000001 andeq r0, r0, r1
000100fc
100fc: 00000005 andeq r0, r0, r5
|
Disassembly of section .data:
00008320
8320: 00000001 andeq r0, r0, r1
00008324
8324: 00000005 andeq r0, r0, r5 |
|
Disassembly of section .rel.dyn:
00000220 <.rel.dyn>: 220: 00000264 andeq r0, r0, r4, ror #4 224: 00000017 andeq r0, r0, r7, lsl r0 228: 00000268 andeq r0, r0, r8, ror #4 22c: 00000017 andeq r0, r0, r7, lsl r0 |
1.2.4 加载器如何发现绝对地址符号
下图展示了加载器如何对代码进行重定向:
从前一节的表格中可以发现,PIC代码多了一个 。.rel.dyn段,该段有-pic参数产生,被称为LABLE表格,表格中每一项对应着一个函数后绝对地址表中的LABEL。程序被拷贝到新地址后,加载器通过.rel.dyn段找到所有的LABEL,利用新的启示地址来更新所有的LABEL。
1.3 U-boot中加载器的实现
u-boot加载自身的过程有被称为重定向(relocate)
下表中左侧是u-boot中的源码,右侧是用C语言写的伪代码。
Assembly |
Pseudo C code |
ENTRY(_main) ……
adr lr, here ldr r0, [r9, #GD_RELOC_OFF] /* r0 = gd->reloc_off */ add lr, lr, r0 ldr r0, [r9, #GD_RELOCADDR] /* r0 = gd->relocaddr */ b relocate here:
|
void _main(void) { lr = here; r0 = gd->relocaddr
relocate(here, gd->relocaddr) }
|
ENTRY(relocate_code) ldr r1, =__image_copy_start /* r1 <- SRC &__image_copy_start */ subs r4, r0, r1 /* r4 <- relocation offset */ beq relocate_done /* skip relocation */ ldr r2, =__image_copy_end /* r2 <- SRC &__image_copy_end */
copy_loop: ldmia r1!, {r10-r11} /* copy from source address [r1] */ stmia r0!, {r10-r11} /* copy to target address [r0] */ cmp r1, r2 /* until source end address [r2] */ blo copy_loop
/* * fix .rel.dyn relocations */ ldr r2, =__rel_dyn_start /* r2 <- __rel_dyn_start */ ldr r3, =__rel_dyn_end /* r3 <- __rel_dyn_end */ fixloop: ldmia r2!, {r0-r1} /* (r0,r1) <- (location,fixup) */ and r1, r1, #0xff cmp r1, #23 /* relative fixup? */ bne fixnext
/* relative fix: increase location by offset */ add r0, r0, r4 ldr r1, [r0] add r1, r1, r4 str r1, [r0] fixnext: cmp r2, r3 blo fixloop
relocate_done: bx lr
ENDPROC(relocate_code)
|
#define R_ARM_RELATIVE 0x17 typedef struct tagRelocItem { unsigned long address; unsigned long reloc_code; } RelocItem;
void relocate(int lr, int relocaddr) { RelocItem *relocItem; unsigned long offset; unsigned long addr; /* new address of Label */
/* copy image */ memcpy(__image_copy_start, __image_copy_end, (__image_copy_end - __image_copy_start));
/* fix .rel.dyn relocations */ relocItem = __rel_dyn_start; offset = relocaddr - __image_copy_start;
while (relocItem >= __rel_dyn_start) { if (relocItem->reloc_code == R_ARM_RELATIVE) { addr = relocItem ->address + offset; *(unsigned long *)addr += offset; }
relocItem++; }
goto lr; }
|
/* clear .bss segment */ ldr r0, =__bss_start /* this is auto-relocated! */ ldr r1, =__bss_end /* this is auto-relocated! */
mov r2, #0x00000000 /* prepare zero to clear BSS */
clbss_l: cmp r0, r1 /* while not at end of BSS */ strlo r2, [r0] /* clear 32-bit BSS word */ addlo r0, r0, #4 /* move to next */ blo clbss_l
|
memset(__bss_start, 0, __bss_end);
/* attention: that will cover .rel.dyn section */ |
/* call board_init_r(gd_t *id, ulong dest_addr) */ mov r0, r9 /* gd_t */ ldr r1, [r9, #GD_RELOCADDR] /* dest_addr */ /* call board_init_r */ ldr pc, =board_init_r /* this is auto-relocated! */ /* we should not return here. */ |
board_init_r(gd, gd->dest_addr);
|
上述实现的最后一步是.bss段清零。看下面的u-boot段列表:
6 .rel.dyn 000040f8 0003155c 0003155c 0002955c 2**2 8 .bss 000358a0 0003155c 0003155c 00000000 2**6 |
.rel.dyn与.bss段起始地址是相同的(通过链接脚本u-boot.lds实现)。这是因为.rel.dyn段只用于把u-boot自己加载到内存,之后就没有了。在.bss段清零时,实际上也就把.rel.dyn段去掉了。
这带来的一个问题是,u-boot在ROM运行时,.bss段是不为零的。.bss段存着未初始化的全局变量,因此此时使用变量时也不能假设变量初值为0。所幸大部分编程规范都要求全局变量初始化要显示初始化。
下面是加载示意图。可以看到加载前只使用.rel.dyn段,加载后只使用.bss段。