近期学习uboot,在relocation部分看了不少大神的分析,下面将自己的理解写下来,也是对此部分功能的学习巩固。
1.1 为什么要relocation
uboot从SDRAM低位拷贝到高位后,各指令存储地址改变,对位置无法指令的执行,不受影响;但位置相关指令的执行,比如说读取一个全局变量(存储在一个绝对地址处),按照之前的寻址无法找到拷贝后的变量值,导致程序执行错误,这时即需要relocation来解决。
通过下面这个例子说明:
Static int G_test = 1;Uboot中执行函数:
Void Fun_test(void)
{
G_test = 0xff;
}
图中标识说明:
Fun_test: uboot中函数;
G_test: 全局变量,被函数Fun_test使用;
G_test_lable: 函数Fun_test尾端lable,其值是变量G_test的地址。
Fun_test_addr: 函数Fun_test拷贝前存储地址;
G_test_lable_addr:G_test_lable拷贝前存储地址;
G_test_addr:变量G_test拷贝前存储地址;
Fun_test_rel_addr:函数Fun_test拷贝后存储地址;
G_test_lable_rel_addr:G_test_lable拷贝后存储地址;
G_test_rel_addr:变量G_test拷贝后存储地址;
此处插入一个知识点,arm如何对变量G_test进行寻址:
(1)将变量G_test的地址存储在函数尾端的Label中(这段内存空间是由编译器自动分配的,而非人为);
(2)基于PC相对寻址获取函数尾端Label上的变量地址;
(3)对G_test变量地址进行读写操作。
下面看uboot在拷贝前函数Fun_test的执行:
1、调用函数Fun_test;
2、通过函数尾端lable:G_test_lable(存储地址是G_test_lable_addr)得到变量G_test存储地址G_test_addr,即[G_test_lable_addr] = G_test_addr;
3、根据变量G_test的存储地址对其进行读写操作,例子中将其赋值为0xFF。
4、函数执行结束,返回。
再看uboot拷贝后函数Fun_test的执行:
1、调用函数Fun_test;
2、通过函数尾端lable:G_test_lable(存储地址是G_test_lable_rel_addr)得到变量G_test存储地址仍然是G_test_addr,而不是我们期望的G_test_rel_addr,这样既无法准确获取变量的存储地址,导致程序执行出错。
relocation就是解决这个问题,使得uboot在拷贝后,仍能准确的寻址,避免程序执行出错,这就是relocation的原因。
由上面描述我们可以看出,问题出现在变量G_test的寻址上,不难看出只要将拷贝后的G_test_lable保存的值更新成拷贝后的变量存储地址G_test_rel_addr,问题就解决了,那如何操作呢,下面就是如何进行relocation。
1.2 如何relocation
直接看源码(uboot2014.10 arch/arm/lib/relocate.S)
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
/*-------------------------------------dividing line-------------------------------------------*/
/*
* fix .rel.dyn relocations
*/
ldr r2, =__rel_dyn_start /* r2 <- SRC &__rel_dyn_start */
ldr r3, =__rel_dyn_end /* r3 <- SRC &__rel_dyn_end */
fixloop:
ldmia r2!, {r0-r1} /* (r0,r1) <- (SRC 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
分界线前面是uboot数据拷贝,后面是relocation,从代码可以看到,relocation对__rel_dyn_start与__rel_dyn_end之间数据进行了处理,专为relocation使用,其结构如下:
连续2个4字节(共8字节)组成一relocation处理部分,其中前4字节保存需要relocation的存储地址,后面4个字节是一个lable,标记前一地址需要relocation。__rel_dyn_start与__rel_dyn_end这段数据编译器在编译时产生,编译器讲uboot拷贝后需要relocation的存储地址(即例中的G_test_lable_addr)均记录在此段中,然后由上述代码进行遍历,实现relocation,下面看代码实现:
ldr r2, =__rel_dyn_start /* r2 <- SRC &__rel_dyn_start */
ldr r3, =__rel_dyn_end /* r3 <- SRC &__rel_dyn_end */
fixloop:
ldmia r2!, {r0-r1} //从__rel_dyn_start开始,每次连续读取8字节
and r1, r1, #0xff
cmp r1, #23 //判断后面四字节低字节是否为0x17
bne fixnext //不是不进行relocation,跳至fixnext
/* relative fix: increase location by offset */
//是0x17,开始进行relocation
//结合前面举例分析此段代码
//r4保存拷贝偏移量offset
add r0, r0, r4 //r0=r0+offset,即例中,r0=G_test_lable_rel_addr
ldr r1, [r0] //将r0中的值(变量拷贝前的存储地址G_test_addr)赋值r1
add r1, r1, r4 //将存储地址+offset,即r1=G_test_addr+offset=G_test_rel_addr
str r1, [r0] //将变量存储地址存储到lable中(G_test_lable_rel_addr)
fixnext:
cmp r2, r3 //判断遍历是否结束
blo fixloop
从上面代码可以看出,relocation处理即是例中所说,将拷贝后的变量lable存储的变量地址更新成拷贝后的变量存储地址。
除了全局变量以外,还有存储变量或函数的指针变量也需要relocation,其实道理是一样的,只是指针变量处理起来复杂一点。
1)首先根据上面方法将指针变量的lable存储的地址更新为拷贝后的存储地址;
2)再将存储地址的值(即变量或函数的地址)更新为拷贝后的存储地址。
此处的更新即是在原先的存储地址增加偏移量offset,类似于拷贝中的处理。
下面引用一位大神的总结:
总结一下,可以看出,
使用-pie选项的compiler,将需要relocate的值(全局变量地址 函数入口地址)的地址存储在rel.dyn段中,uboot运行中relocate_code遍历rel.dyn段,根据rel.dyn中存储的值,对以(这些值+offset)为地址上的值进行了relocate,完成对所有需要relocate的变量的修改!
需要注意的是,在uboot的整个relocate_code中rel.dyn不仅没有拷贝,也没有修改,修改只是针对rel.dyn中值+offset为地址上的值!
更详细的分析请看此篇博客。