关于重定位的思考

  一个简单的重定位例子

       重定位顾名思义就是重新分配代码和数据的物理位置,通俗来说就是把一块数据,代码由一个地方搬到另一个地方,通常是从ROM搬到RAM。为什么?因为ROM为只读存储器其写入速度很慢,读出速度很快,可以用来存放代码并执行,但并不适合用来存放数据。若想写程序中的变量,就需要在RAM中进行,因此进行搬运。

重定位代码

      如上图,我举一个简单的例子来说明重定位的原理。我们希望把程序从0x0000-0x1000的ROM地址拷贝到RAM中进行运行,这个时候就需要重定位函数。其一般放在程序的开头如图所示,上电后从0x0000开始运行,执行重定位代码。

重定位带代码

上面的代码为ARM程序重定位代码,比较困惑的就是_start和_end。怎么理解呢?可以把他们理解成链接标号,表示关键的地址位置。

链接脚本

如上这些标号是在连接脚本中定义的,然后在链接的时候赋给程序中的常量const_start和const_end。const_start和const_end也被叫做占位符,编译的时候并没有明确的值,连接后才有。 连接后,const_start的值为0x1000。const_end的值为0x1999,他们两个表示程序重定位的目标地址使用ldr加载。而当前程序的地址使用adr来获取。

重定位例子

   上面只是举了一个简单的例子来说明重定位实际上的重定位要比这个复杂的多。

stm32的重定位

   先介绍stm32的重定位,因为下面的我们没进行学习过。其实stm32的重定位又叫做分散加载。也就是它只重定位了.data数据而没有对其他程序段进行重定位。

stm32分散加载
 

     在stm32术语体系里.data被称作RW section。如上图,stm32在上电后会把RW程序段拷贝到SRAM里,从而实现代码和常量数据运行在flash里而变量数据和堆栈在SDRAM里。重定位的代码位于__main里面,这个函数找不到源码,官方文档里说这里面完成了RW section的分散加载,以及ZI取的初始化。显然这些都离不开,链接脚本。lds,在STM32的语境下被称为SCT分散加载文件。

     SCT文件规划了 Code、RO-data、RW-data 及 ZI-data 这些区域的地址,链接器根据这些地址将子程序组织在一块。SCT文件中主要包含描述加载域及执行域的部分,一个文件中可包含有多个加载域,而一个加载域可由多个部分的执行域组成。同等级的域之间使用花括号“{}”分隔开,最外层的是加载域,第二层“{}”内的是执行域。

SCT文件示例

  如上图为一个默认生成的SCT文件,有一个加载域和两个执行域。第一个执行域里面将start.S里的Reset区域放在了起始位置0x80000000,Reset区域实际上就是向量表。对了讲的有点远了,其实根据上述SCT文件我们就可以确定源地址为:0x2000+0x800000000。目标地址为:0x200000000。拷贝尺寸为 :0x5000。

uboot的重定位

   uboot是用来加载启动启动linux内核的,它包含镜像烧写,网络通信等功能,是一个比较大的裸机程序。

(1) 从NorFlah启动时,由于NorFlash写入速度慢,因此不适应变量的写入,因此需要将uboot代码从NorFlash重定位到SDRAM。

uboot NorFlash启动

如上图所示,复位后CPU会将NorFlash映射为0x00000000并从这里启动。重定位代码根据连接脚本里的地址标识(_start,_end,_textbase)将代码从NorFlash搬运到SDRAM。代码如下:

地址准备
内存拷贝

(2)从NandFlash启动

     如上代码,我们可以发现从NandFlash或NorFlash使用不同的代码进行拷贝,Nor_Flash可以直接访问,而Nand_Flash需要进行初始化以及使用指令进行内存读取。

uboot Nand Flash启动

  上图展现了从NandFlash启动的过程,当从Nand Flash启动的时候CPU会将内部的Stepstone映射为0x00000000地址,并从这里开始启动。当然了这里是有一部分代码的,这些代码是复位后硬件从NandFlash自动加载过来的4KB程序,里面包含了重定位代码。

  

### 关于重定位信息与重定位过程的概念及实现细节 #### 一、概念定义 重定位是指在程序加载过程中修改程序中的地址引用,使得这些引用指向正确的内存位置的过程。当编译器生成目标文件时,并不知道最终的加载地址其他模块的位置,因此会保留一些占位符用于后续修正。 对于不同的架构支持特性,存在多种类型的重定位条目来适应特定需求。例如,在x86_64体系结构下的`R_X86_64_PLT32`就是一种针对PLT(Procedure Linkage Table)表项偏移量计算而设计的相对寻址模式[^1]。 #### 二、具体实例分析 考虑C语言源码经过预处理器、编译器转换成汇编代码之后,再由汇编器转化为机器指令形式的目标文件阶段。此时虽然已经得到了可以直接被CPU识别并执行的一系列操作数序列,但由于尚未确定实际运行环境里的确切布局情况,所以还需要进一步处理才能正常使用。 假设有一个简单的函数调用场景: ```c extern void multiple(int); int main() { ... multiple(5); // 调用外部声明的multiple函数 } ``` 在这个例子中,如果`multiple()`不是静态链接而是动态解析的话,那么它的绝对地址就不能提前知道;于是编译工具链会在`.plt`节创建相应的存根以便间接访问该符号所代表的真实实体,并通过设置合适的重定位记录告知连接器如何调整这条分支指令的操作数部分以匹配真实入口点的实际距离差值。 #### 三、链接期间的工作流程 链接器负责收集多个输入对象文件以及库资源并将它们组合起来形成完整的可执行映像或共享库单元。在此过程中涉及到的关键活动之一便是解决所有未决的名字绑定关系——即将各个地方提到却暂时未知的具体位置填充进去。 为了支持这种灵活性,ELF格式规定了一套标准的数据结构用来描述待修补的信息片段及其预期行为方式。每种平台可能都有自己特有的编码约定,但总体思路都是相似的:每当遇到需要后期填补的内容就记下一个标记告诉将来谁应该在这里做什么改变。 举个例子来说,某些旧版本的C编译器可能会自动给全局变量名前面加上下划线作为内部表示方法的一部分[^2],这同样属于广义上的名称修饰范畴之内,尽管严格意义上讲并不完全等同于经典的“重定位”。 #### 四、操作系统层面的应用案例 以内核启动为例可以更直观地理解整个机制的作用范围有多广泛。U-Boot作为嵌入式系统的引导加载程序承担着至关重要的职责,其中包括准备必要的硬件条件并向后继OS传递控制权。由于Linux内核镜像是按照固定的基址构建出来的,所以在真正开始之前必须先将其迁移到物理RAM里指定的地方去等待激活时刻的到来。 特别值得注意的是,即便是在此之前完成了初步安置工作,也并不代表万事俱备只欠东风了。因为从这一刻起直到正式进入用户空间之前的这段时间里还存在着许多潜在的变化因素会影响最终形态的确立。为此专门安排了一个名为`__primary_switch`的小段子程序来进行最后几步关键性的准备工作,包括但不限于开启MMU服务从而建立起有效的页表映射关系,进而确保接下来的一切都能够顺利开展下去[^3]。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值