PIC(与位置无关代码)在u-boot上的实现

2019-04-15 12:55发布

1.1 原理介绍

u-boot通常都是存在ROM或者Flash上,以保证CPU启动后可以直接运行u-boot。但ROM的问题是只能读不能写,不利于程序的执行。如:全局变量读写,地址空间限制等问题。因此u-boot会先把自己拷贝到RAM中去执行。这一拷贝带来的问题是执行地址的混乱。代码的执行地址通常都是在编译时有链接地址指定的,如何保证拷贝前后都可以执行呢?一个办法是使用拷贝到RAM后的地址作为编译时的链接地址,拷贝前所有函数与全局变量的调用都增加偏移量。(如VxWorksbootloader)尽量减少拷贝前需要执行的代码量。两一个地址是把image编译成与地址无关的程需,也就是PIC - Position independent code。编译器无法保证代码的独立性,它需要与加载器配合起来。U-boot自己加载自己,所以她自己就是加载器。域代码无关代码依赖于下面两种技术:1) 使用相对地址2) 加载器可以自动更新涉及到绝对地址的指令PIC的实现方式不止一种,不对CPU架构下的实现也有区别。这里主要结合搜啊ARM架构下u-bootPIC的实现方式

1.2 一个简单例子

先通过一个简单例子介绍编译成PIC与非PIC的区别

1.2.1 源码

test0.1cint foo(int a); int ttt = 1;int xxx = 5; int boo(){    foo(xxx);    return foo(ttt);}这是一个简单的程序。Foo函数的实现在另一个文件,boo函数调用了foo函数与全局变量tttxxx

1.2.2 Compile

对于PowerPC架构,u-boot只是在编译时使用了-fpic。这种方式会生成一个.got段来存储绝对地址符号(位置无关(PIC)代码原理剖析)。对与ARM架构,则是在编译时使用-mword-relocations,生成与位置无关代码;链接时使用-pie生成.rel.dyn段,该段中的每个条目被称为一个LABEL,用来存储绝对地址符号的地址。

1.2.3 PIC如何找到所有的绝对地址符号 

Non-PICPICarm-qhao-linux-gnueabi-gcc -c test01.carm-qhao-linux-gnueabi-ld -o a.out test01.o test02.o arm-qhao-linux-gnueabi-gcc -c -mword-relocations test01.carm-qhao-linux-gnueabi-ld -pie -o a.out  test01.o test02.o SYMBOL TABLE:00008094 l  d  .text  00000000 .text000100f8 l  d  .data  00000000 .data000100fc g   O .data  00000004 xxx000100f8 g   O .data  00000004 ttt SYMBOL TABLE:00000220 l  d  .rel.dyn  00000000 .rel.dyn00000230 l  d  .text  00000000 .text00008320 l  d  .data  00000000 .data00008324 g   O .data  00000004 xxx00008320 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 #6Disassembly 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以符号ttt为例,在non-PIC中,#809c#80a通过绝对地址获得xxx的地址,如果要搬移到新地址,则必须找到并修改调用绝对地址的指令。PIC中,每个函数在最后都附加了绝对地址表,#238通过PC寄存器加偏移的方式找到地址表,根据地址表中存储的绝对地址找到xxx实际的地址。如果要搬移到新地址,需要修改每一个函数后面的绝对地址表。

1.2.4  加载器如何发现绝对地址符号

下图展示了加载器如何对代码进行重定向:
从前一节的表格中可以发现,PIC代码多了一个 .rel.dyn段,该段有-pic参数产生,被称为LABLE表格,表格中每一项对应着一个函数后绝对地址表中的LABEL。程序被拷贝到新地址后,加载器通过.rel.dyn段找到所有的LABEL,利用新的启示地址来更新所有的LABEL

1.3 U-boot中加载器的实现

u-boot加载自身的过程有被称为重定向(relocate下表中左侧是u-boot中的源码,右侧是用C语言写的伪代码。  
AssemblyPseudo C codeENTRY(_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  relocatehere: 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 0x17typedef 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-bootROM运行时,.bss段是不为零的。.bss段存着未初始化的全局变量,因此此时使用变量时也不能假设变量初值为0。所幸大部分编程规范都要求全局变量初始化要显示初始化。下面是加载示意图。可以看到加载前只使用.rel.dyn段,加载后只使用.bss段。

本文乃fireaxe原创,使用GPL发布,可以自由拷贝,转载。但转载请保持文档的完整性,并注明原作者及原链接。内容可任意使用,但对因使用该内容引起的后果不做任何保证。作者:fireaxe.hq@outlook.com博客:fireaxe.blog.chinaunix.net