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

时间:2023-01-12 10:55:59

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.1c

int 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-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

以符号ttt为例,在non-PIC中,#809c#80a通过绝对地址获得xxx的地址,如果要搬移到新地址,则必须找到并修改调用绝对地址的指令。PIC中,每个函数在最后都附加了绝对地址表,#238通过PC寄存器加偏移的方式找到地址表,根据地址表中存储的绝对地址找到xxx实际的地址。如果要搬移到新地址,需要修改每一个函数后面的绝对地址表。

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

下图展示了加载器如何对代码进行重定向:
PIC(与位置无关代码)在u-boot上的实现

从前一节的表格中可以发现,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-bootROM运行时,.bss段是不为零的。.bss段存着未初始化的全局变量,因此此时使用变量时也不能假设变量初值为0。所幸大部分编程规范都要求全局变量初始化要显示初始化。

下面是加载示意图。可以看到加载前只使用.rel.dyn段,加载后只使用.bss段。
PIC(与位置无关代码)在u-boot上的实现

本文乃fireaxe原创,使用GPL发布,可以*拷贝,转载。但转载请保持文档的完整性,并注明原作者及原链接。内容可任意使用,但对因使用该内容引起的后果不做任何保证。

作者:fireaxe.hq@outlook.com
博客:fireaxe.blog.chinaunix.net