近期在一直在做uboot的移植工作,uboot中有非常多值得学习的东西。之前总结过uboot的启动流程,但uboot一个非常核心的功能没有细致研究。就是uboot的relocation功能。

这几天研究下uboot的relocation功能,记录在此,跟大家共享。

自己辛苦编辑,转载请注明出处。谢谢!

所谓的relocation,就是重定位,uboot执行后会将自身代码复制到sdram的还有一个位置继续执行。这个在uboot启动流程分析中说过。

但基于曾经的理解,一个完整可执行的bin文件,link时指定的链接地址,load时的载入地址。执行时的执行地址。这3个地址应该是一致的

relocation后执行地址不同于载入地址 特别是链接地址,ARM的寻址会不会出现故障?

新版uboot跟老版uboot不太一样的地方在于新版uboot无论uboot的load addr(entry pointer)在哪里,启动后会计算出一个靠近sdram顶端的地址。将自身代码复制到该地址,继续执行。

个人感觉uboot这样改进用意有二,一是为kernel腾出低端空间。防止kernel解压覆盖uboot。二是对于由静态存储器(spiflash nandflash)启动,这个relocation是必须的。

可是这样会有一个问题。relocation后uboot的执行地址跟其链接地址不一致,compiler会在link时确定了当中变量以及函数的绝对地址,链接地址 载入地址 执行地址应该一致,

这样看来。arm在寻址这些变量 函数时找到的应该是relocation之前的地址。这样relocation就没有意义了!



当然uboot不会这样,我们来分析一下uboot下relocation之后是怎样寻址的,開始学习之前我是有3个疑问,例如以下

(1)怎样对函数进行寻址调用

(2)怎样对全局变量进行寻址操作(读写)

(3)对于全局指针变量中存储的其它变量或函数地址在relocation之后怎样操作

搞清楚这3个问题,对于我来说relocation的原理就算是搞明确了。

为了搞清楚这些。在uboot的某一个文件里增加例如以下代码

void test_func(void)
{
printf("test func\n");
} static void * test_func_val = test_func;
static int test_val = 10; void rel_dyn_test()
{
test_val = 20;
printf("test = 0x%x\n", test_func);
printf("test_func = 0x%x\n", test_func_val);
test_func();
}

rel_dyn_test函数中就包括了函数指针 变量赋值 函数调用这3种情况,寻址肯定要汇编级的追踪才干够,编译完毕后反汇编,得到u-boot.dump(objdump用-D选项,将全部section都disassemble出来)

找到rel_dyn_test函数,例如以下:

80e9d3cc <test_func>:
80e9d3cc: e59f0000 ldr r0, [pc, #0] ; 80e9d3d4 <test_func+0x8>
80e9d3d0: eaffc2fb b 80e8dfc4 <printf>
80e9d3d4: 80eb1c39 .word 0x80eb1c39 80e9d3d8 <rel_dyn_test>:
80e9d3d8: e59f202c ldr r2, [pc, #44] ; 80e9d40c <rel_dyn_test+0x34>
80e9d3dc: e3a03014 mov r3, #20 ; 0x14
80e9d3e0: e92d4010 push {r4, lr}
80e9d3e4: e59f1024 ldr r1, [pc, #36] ; 80e9d410 <rel_dyn_test+0x38>
80e9d3e8: e5823000 str r3, [r2]
80e9d3ec: e59f0020 ldr r0, [pc, #32] ; 80e9d414 <rel_dyn_test+0x3c>
80e9d3f0: ebffc2f3 bl 80e8dfc4 <printf>
80e9d3f4: e59f301c ldr r3, [pc, #28] ; 80e9d418 <rel_dyn_test+0x40>
80e9d3f8: e59f001c ldr r0, [pc, #28] ; 80e9d41c <rel_dyn_test+0x44>
80e9d3fc: e5931000 ldr r1, [r3]
80e9d400: ebffc2ef bl 80e8dfc4 <printf>
80e9d404: e8bd4010 pop {r4, lr}
80e9d408: eaffffef b 80e9d3cc <test_func>
80e9d40c: 80eb75c0 .word 0x80eb75c0
80e9d410: 80e9d3cc .word 0x80e9d3cc
80e9d414: 80eb1c44 .word 0x80eb1c44
80e9d418: 80eaa54c .word 0x80eaa54c
80e9d41c: 80eb1c51 .word 0x80eb1c51

。。

data段中

80eb75c0 <test_val>:
80eb75c0: 0000000a .word 0x0000000a

80eaa54c <test_func_val>:
80eaa54c: 80e9d3cc .word 0x80e9d3cc

rel_dyn_test反汇编后。最后多了一部分从0x80e9d40c開始的内存空间,对照发现这部分内存空间地址上的值居然是函数须要的变量test_val test_func_val的地址。

网上资料称这些函数末尾存储变量地址的内存空间为Label,(编译器自己主动分配)



一条条指令来分析。

ldr  r2,   [pc,  #44] ========> r2 = [pc + 0x2c]=======>r2 = [0x80e9d3e0 + 0x2c]=======>r2 =[0x80e9d40c]

须要注意。因为ARM的流水线机制。当前PC值为当前地址加8个字节

这样r2获取的是0x80e9d40c地址的值0x80eb75c0,这就是test_val的值嘛

mov r3, #20======> r3 = 20

相应C函数这应该是为test_val = 20做准备。先跳过后面2条指令,发现

str  r3,   [r2]

非常明显了,将马上数20存入0x80eb75c0中也就是test_val中。

这3条指令说明,ARM对于变量test_val的寻址例如以下:

(1)将变量test_val的地址存储在函数尾端的Label中(这段内存空间是由编译器自己主动分配的。而非人为)

(2)基于PC相对寻址获取函数尾端Label上的变量地址

(3)对test_val变量地址进行读写操作

再来看当中的几条指令

ldr   r3, [pc,   #28] =====> r3 = [0x80e9d3fc + 0x1c] =====> r3 = [0x80e9d418] ====> r3 = 0x80eaa54c

ldr   r1, [r3] =====> r1 = [0x80eaa54c] ======> r1 = 0x80e9d3cc

0x80e9d3cc这个地址能够看出是test_func的入口地址。这里是printf打印test_func_val的值

能够看出对于函数指针变量的寻址跟普通变量一样。

接下来来看函数的调用,能够看到对于printf以及test_func,使用的是指令bl以及b进行跳转。这2条指令都是相对寻址(pc + offset)

说明ARM调用函数使用的是相对寻址指令bl或b,与函数的绝对地址无关

对于这3种情况的寻址方法已经知道了,那就须要思考一下relocation之后会有什么变化。

将rel_dyn_test  relocation之后能够想象,函数的调用还是没有问题的,由于使用了bl或b相对跳转指令。

可是对于变量的寻址就有问题了。寻址的前2步没有问题。相对寻址获取尾部Label中的变量地址,但获取的变量地址是在 link时就确定下来的绝对地址啊!

而对于指针变量的寻址呢。问题很多其它了,

首先跟普通变量寻址一样,尾部内存空间的变量地址是link时的绝对地址。再者,指针变量存储的变量指针或者函数指针也是在link时确定的绝对地址,relocation之后这个值也变了!

那uboot是怎样来处理这些情况的呢?更准确的说应该是compiler和uboot怎样一起来处理这些情况的呢?

这里利用了PIC位置无关代码。通过为编译器指定编译选项-fpic或-fpie产生,

这样编译产生的目标文件包括了PIC所须要的信息。-fpic,-fpie是gcc的PIC编译选项。ld也有PIC连接选项-pie,要获得一个完整的PIC可执行文件,连接目标文件时必须为ld指定-pie选项,

察看uboot的编译选项发现,在arch/arm/config.mk,例如以下:

# needed for relocation
LDFLAGS_u-boot += -pie

uboot仅仅指定了-pie给ld,而没有指定-fPIC或-fPIE给gcc。

指定-pie后编译生成的uboot中就会有一个rel.dyn段。uboot就是靠rel.dyn段实现了完美的relocation!

察看u-boot.dump中的rel.dyn段。例如以下:

Disassembly of section .rel.dyn:

80eb7d54 <__rel_dyn_end-0x5c10>:
80eb7d54: 80e80020 rschi r0, r8, r0, lsr #32
80eb7d58: 00000017 andeq r0, r0, r7, lsl r0
80eb7d5c: 80e80024 rschi r0, r8, r4, lsr #32
80eb7d60: 00000017 andeq r0, r0, r7, lsl r0
80eb7d64: 80e80028 rschi r0, r8, r8, lsr #32
80eb7d68: 00000017 andeq r0, r0, r7, lsl r0
。 。。 <span style="color:#FF0000;">80eba944: 80e9d40c rschi sp, r9, ip, lsl #8
80eba948: 00000017 andeq r0, r0, r7, lsl r0
80eba94c: 80e9d410 rschi sp, r9, r0, lsl r4
80eba950: 00000017 andeq r0, r0, r7, lsl r0
80eba954: 80e9d414 rschi sp, r9, r4, lsl r4
80eba958: 00000017 andeq r0, r0, r7, lsl r0
80eba95c: 80e9d418 rschi sp, r9, r8, lsl r4
80eba960: 00000017 andeq r0, r0, r7, lsl r0
80eba964: 80e9d41c rschi sp, r9, ip, lsl r4
80eba968: 00000017 andeq r0, r0, r7, lsl r0</span>
。。 。。

有没有注意到,rel_dyn_test末尾存储全局变量地址的Label地址也存储在这里,那有什么用呢,那就来看一下uboot的核心函数relocate_code是怎样实现自身的relocation的,

在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 /*
* 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 relocate_done:

前半部分在uboot启动流程中讲过。将__image_copy_start到__image_copy_end之间的数据进行拷贝

来看一下arm的link script,在arch/arm/cpu/u-boot.lds,例如以下:

OUTPUT_FORMAT("elf32-littlearm", "elf32-littlearm", "elf32-littlearm")
OUTPUT_ARCH(arm)
ENTRY(_start)
SECTIONS
{
. = 0x00000000; . = ALIGN(4);
.text :
{
*(.__image_copy_start)
CPUDIR/start.o (.text*)
*(.text*)
} . = ALIGN(4);
.rodata : { *(SORT_BY_ALIGNMENT(SORT_BY_NAME(.rodata*))) } . = ALIGN(4);
.data : {
*(.data*)
} . = ALIGN(4); . = .; . = ALIGN(4);
.u_boot_list : {
<pre name="code" class="cpp"> KEEP(*(SORT(.u_boot_list*)));
} . = ALIGN(4); .image_copy_end :
{
*(.__image_copy_end)
} .rel_dyn_start :
{
*(.__rel_dyn_start)
} .rel.dyn : {
*(.rel*)
} .rel_dyn_end :
{
*(.__rel_dyn_end)
} .end :
{
*(.__end)
} _image_binary_end = .;

能够看出__image_copy_start---end之间包含了text data rodata段,可是没有包含rel_dyn。

继续看relocate_code函数。拷贝__image_copy_start----end之间的数据,但没有拷贝rel.dyn段。

首先获取__rel_dyn_start地址到r2。将start地址上连续2个4字节地址的值存在r0 r1中

推断r1中的值低8位,假设为0x17,则将r0中的值加relocation offset。

获取以此r0中值为地址上的值。存到r1中

将r1中值加relocation offset。再存回以r0中值为地址上。

以此循环,直到__rel_dyn_end。

这样读有些拗口。

来以咱们的rel_dyn_test举样例。

上面rel.dyn段中有一段例如以下:

80eba944:       80e9d40c        rschi   sp, r9, ip, lsl #8
80eba948: 00000017 andeq r0, r0, r7, lsl r0

依照上面的分析,推断第二个四字节为0x17,r0中存储为0x80e9d40c。这个是rel_dyn_test末尾Label的地址啊,

将r0加上relocation offset。则到了relocation之后rel_dyn_test的末尾Label。

获取r0为地址上的值到r1中,0x80eb75c0。能够看到,这个值就是变量test_val的首地址啊。

最后将r1加上relocation offset,写回以r0为地址上。

意思是将变量test_val地址加offset后写回到relocation之后rel_dyn_test的末尾Label中。

这样relocate_code完毕后,再来看对test_val的寻址。寻址第三步获取到的是改动之后的relocation addr啊,这样就能够获取到relocation之后的test_val值。

对于普通变量寻址是这样。那对于指针变量呢,如test_func_val呢?

获取test_func_val relocation后地址的步骤跟上面一样。可是我们在获取test_func_val的值时要注意。这个变量存储的是函数test_func指针,之前是0x80e9d3cc。relocation之后就变化了,所以test_func_val的值也应该变化,这个该怎么办?

方法是一样的,能够在rel.dyn段中找到例如以下一段:

80ebc18c:       80eaa54c        rschi   sl, sl, ip, asr #10
80ebc190: 00000017 andeq r0, r0, r7, lsl r0

这上面存储的是test_func_val的地址。依照relocate_code的操作。完毕后80eaa54c + offset上的值也应该+offset了。

这就攻克了,test_func_val的值也就是test_func的地址也被改动为relocation之后的地址了。

网上查阅资料。这里对于rel.dyn段中每个rel section(8个字节)第二个4字节。0x17,是一种label的类型R_ARM_RELATIVE。

经过上面uboot的relocate_code后,我们提出的3个问题的寻址都能够正常工作。

另一个疑问。是谁来决定哪些label放到rel.dyn中,特别是对于存储指针的变量,怎样分辨。这样看来。是compiler的ld来完毕的这个工作,将全部须要relocate的label放到rel.dyn段中,真是牛逼的compiler啊。

总结一下,能够看出,

使用-pie选项的compiler,将须要relocate的值(全局变量地址  函数入口地址)的地址存储在rel.dyn段中,uboot执行中relocate_code遍历rel.dyn段。依据rel.dyn中存储的值。对以(这些值+offset)为地址上的值进行了relocate。完毕对全部须要relocate的变量的改动!

。。还是有些拗口。。

须要注意的是,在uboot的整个relocate_code中rel.dyn不仅没有拷贝,也没有改动,改动仅仅是针对rel.dyn中值+offset为地址上的值。

查阅网上资料,compiler在cc时增加-fPIC或-fPIE选项,会在目标文件里生成GOT(global offset table),将本文件里须要relocate的值存放在GOT中。函数尾部的Label来存储GOT的offset以及当中变量的offset,变量寻址首先依据尾部Label相对寻址找到GOT地址,以及变量地址在GOT中的位置,从而确定变量地址。这样对于目标文件统一改动GOT中的值。就改动了变量地址的offset,完毕了relocation。

ld时增加-pie选项。就会将GOT并入到rel.dyn段中,uboot在relocate_code中统一依据rel.dyn段改动须要relocation的数值。

uboot中ld使用-pie而cc没有使用-fPIC或-fPIE,目标文件里就不会生成GOT,函数中寻址还是在尾部Label中直接存储变量的绝对地址。但这个Label相同存在rel.dyn中。uboot依据rel.dyn段改动Label上的值,就完毕了relocation。

这样不仅节省了每一个目标文件的GOT段,并且不须要去相对寻址GOT。直接改动函数尾部Label所存储的变量地址就能够啦!

uboot的relocation就是如此。

uboot的relocation原理具体分析的更多相关文章

  1. uboot的relocation原理详细分析

    转自:http://blog.csdn.net/skyflying2012/article/details/37660265 最近在一直在做uboot的移植工作,uboot中有很多值得学习的东西,之前 ...

  2. 20169212《Linux内核原理与分析》课程总结

    20169212<Linux内核原理与分析>课程总结 每周作业链接汇总 第一周作业:完成linux基础入门实验,了解一些基础的命令操作. 第二周作业:学习MOOC课程--计算机是如何工作的 ...

  3. 20169212《Linux内核原理与分析》第二周作业

    <Linux内核原理与分析>第二周作业 这一周学习了MOOCLinux内核分析的第一讲,计算机是如何工作的?由于本科对相关知识的不熟悉,所以感觉有的知识理解起来了有一定的难度,不过多查查资 ...

  4. u-boot中nandflash初始化流程分析(转)

    u-boot中nandflash初始化流程分析(转) 原文地址http://zhuairlunjj.blog.163.com/blog/static/80050945201092011249136/ ...

  5. AJAX练习(一):制作可以自动校验的表单(从原理上分析ajax的作用)

    继上文(AJAX(一)AJAX的简介和基础)作为联系. 传统网页在注册时检测用户名是否被占用,传统的校验显然缓慢笨拙. 当ajax出现后,这种体验有了很大的改观,因为在用户填写表单时,签名的表单项已经 ...

  6. 20169210《Linux内核原理与分析》第二周作业

    <Linux内核原理与分析>第二周作业 本周作业分为两部分:第一部分为观看学习视频并完成实验楼实验一:第二部分为看<Linux内核设计与实现>1.2.18章并安装配置内核. 第 ...

  7. wp7之换肤原理简单分析

    wp7之换肤原理简单分析 纠结很久...感觉勉强过得去啦.还望各位大牛指点江山 百度找到这篇参考文章http://www.cnblogs.com/sonyye/archive/2012/03/12/2 ...

  8. 2018-2019-1 20189221 《Linux内核原理与分析》第九周作业

    2018-2019-1 20189221 <Linux内核原理与分析>第九周作业 实验八 理理解进程调度时机跟踪分析进程调度与进程切换的过程 进程调度 进度调度时机: 1.中断处理过程(包 ...

  9. 2018-2019-1 20189221 《Linux内核原理与分析》第八周作业

    2018-2019-1 20189221 <Linux内核原理与分析>第八周作业 实验七 编译链接过程 gcc –e –o hello.cpp hello.c / gcc -x cpp-o ...

随机推荐

  1. OCP读书笔记(9) - 诊断数据库

    数据库恢复顾问 Data Recovery Advisor的命令行选项 1. 启动 RMAN 进程并连接到目标$ rman target=/ 2. 假设发生了某个错误,希望找出原因,使用 list f ...

  2. swift 笔记2

    swift交流群:342581988,欢迎增加. 今天真郁闷啊,把mac升级到10.10了.如今好了,曾经的程序都跑不了了.哎,不说了,让我郁闷会再. 说说条件推断吧,事实上这些基本的语法大家都知道肯 ...

  3. OC -- 第一个类

    OC -- 第一个类 类名:Car 属性:轮胎个数.时速 行为:跑 完整写一个类:类的声明和实现 1.    类的声明 代码: // NSObject 再Foundation框架中 #import & ...

  4. cf-#189-div 2

    A题: 从前往后暴搜就OK: B题: 每次询问的时候都bfs一次 C题: 异或运算,从后往前运算. -------------分割线--------------------- D题:对于第K个人来说, ...

  5. GridView的RowDataBound事件中获取当前行内容的几种方法

    1. Cells[x].Txt.    从列单元格的文本值获取.这种方法简单高率,最为常用,但是功能单纯.此法存在几个缺点:   (1)无法获取到设置了隐藏属性的数据列的值,所取到的值为“”(空).  ...

  6. red hat Linux 使用CentOS yum源更新

    red hat linux是商业版软件,没有经过注册是无法使用红帽 yum源更新软件的,使用CentOS源更新操作如下: 1.删除red hat linux 原有的yum 源 rpm -aq | gr ...

  7. jQuery拖动调整表格列宽度-resizableColumns

    实现鼠标可拖动调整表格列宽度 如图: 一.引入文件: <script src="/js/jquery-1.8.0.min.js" type="text/javasc ...

  8. pig中使用的一些实例语法

    在pig中, dump和store会分别完毕两个MR, 不会一起进行 1:载入名用正則表達式: LOAD '/user/wizad/data/wizad/raw/2014-0{6,7-0,7-1,7- ...

  9. Android多线程文件下载器

    本应用实现的是输入文件的网络的地址,点击button開始下载,下载过程中有进度条和后面的文本提示进度, 下载过程中button不可点击,防止反复的下载,完成下载后会进行Toast的提示显示, 而且回复 ...

  10. JVM必备指南(转)

    本文由 ImportNew - xiafei 翻译自 anturis.欢迎加入翻译小组.转载请见文末要求. 简介 Java虚拟机(JVM)是Java应用的运行环境,从一般意义上来讲,JVM是通过规范来 ...