LV04-06-重定位-02-实现段的重定位
本文主要是重定位——实现段的重定位的相关笔记,若笔记中有错误或者不合适的地方,欢迎批评指正😃。
点击查看使用工具及版本
Windows版本 | windows11 |
Ubuntu版本 | Ubuntu16.04的64位版本 |
VMware® Workstation 16 Pro | 16.2.3 build-19376536 |
终端软件 | MobaXterm(Professional Edition v23.0 Build 5042 (license)) |
Linux开发板 | 正点原子 i.MX6ULL Linux 阿尔法开发板 |
uboot | NXP官方提供的uboot,NXP提供的版本为uboot-imx-rel_imx_4.1.15_2.1.0_ga(使用的uboot版本为U-Boot 2016.03) |
linux内核 | linux-4.15(NXP官方提供) |
Win32DiskImager | Win32DiskImager v1.0 |
点击查看本文参考资料
分类 | 网址 | 说明 |
官方网站 | https://www.arm.com/ | ARM官方网站,在这里我们可以找到Cotex-Mx以及ARMVx的一些文档 |
https://www.nxp.com.cn/ | NXP官方网站 | |
https://www.nxpic.org.cn/ | NXP 官方社区 | |
https://u-boot.readthedocs.io/en/latest/ | u-boot官网 | |
https://www.kernel.org/ | linux内核官网 |
点击查看相关文件下载
分类 | 网址 | 说明 |
NXP | https://github.com/nxp-imx | NXP imx开发资源GitHub组织,里边会有u-boot和linux内核的仓库 |
https://elixir.bootlin.com/linux/latest/source | 在线阅读linux kernel源码 | |
nxp-imx/linux-imx/releases/tag/rel_imx_4.1.15_2.1.0_ga | NXP linux内核仓库tags中的rel_imx_4.1.15_2.1.0_ga | |
nxp-imx/uboot-imx/releases/tag/rel_imx_4.1.15_2.1.0_ga | NXP u-boot仓库tags中的rel_imx_4.1.15_2.1.0_ga | |
I.MX6ULL | i.MX 6ULL Applications Processors for Industrial Products | I.MX6ULL 芯片手册(datasheet,可以在线查看) |
i.MX 6ULL Applications ProcessorReference Manual | I.MX6ULL 参考手册(下载后才能查看,需要登录NXP官网) | |
ARM | Cortex-A7 MPCore Technical Reference Manual | Cortex-A7 MPCore技术参考手册 |
ARM Architecture Reference Manual ARMv7-A and ARMv7-R edition | ARM架构参考手册ARMv7-A和ARMv7-R版 | |
Arm Generic Interrupt Controller Architecture Specification- version 3 and version 4 | Arm通用中断控制器架构规范-版本3和版本4 | |
ARM Generic Interrupt Controller Architecture Specification - Version 2.0 | Arm通用中断控制器架构规范-版本2.0 | |
ARM Cortex-A Series Programmer's Guide for ARMv7-A | Cortex-A系列ARMv7-A编程指南 |
一、重定位的概念
1. 什么是重定位
接触过 S3C2440 的话应该会了解到,在程序运行之前我们需要手动将.bin文件上的全部代码从 Nor Flash 或 Nand Flash 拷贝到 SDRAM 上。对于 imx6ull来说,这部分拷贝代码的操作由 Boot Rom 自动完成,板子上电后 boot Rom 会将映像文件从启动设备(TF 卡、 eMMC)自动拷贝到 DDR3 内存上。上述拷贝代码的过程就是重定位。
那么 boot Rom 应该将映像文件拷贝到内存的哪个位置呢?这部分内容已经《LV04-01-IMX6ULL启动流程-03-映像文件》中学习过了 。映像文件包含多个部分,其中.bin 文件的起始地址由地址entry 决定,需要在 Makefile 中手动配置。
1 | ./mkimage -n imximage.cfg.cfgtmp -T imximage -e 0x80100000 -d demo.bin demo.imx |
按照上述的配置,整个映像文件被自动重定位到 DDR 内存上,其中.bin 文件的起始地址为 0x80100000。重定位结束后, CPU 会从这个地址读取第一条指令开始执行程序。
2. 为什么要重定位?
1 | int main(int argc, const char * argv[]) |
上述代码在程序运行时, CPU 需要不断地访问 DDR3 内存来获取 g_charA 的值,访问DDR3 会花费大量的时间,那么如何提升访问的效率呢?
答:在程序运行先前将 data 段的数据重定位到 imx6ull 的片内 RAM 上,因为CPU 访问片内 RAM 的速度远快于访问 DDR3 的速度。
二、汇编重定位data段
完整代码可以看这里:
1. 参考芯片手册确定片内RAM位置
我们可以查看参考手册的《Chapter 2: Memory Maps》 :
参考芯片手册得到片内 RAM 的地址为: 0x900000 ~ 0x91FFFF。所以我们将.data 段重定位后的地址设置为 0x900000。
2. 链接脚本修改
创建一个变量用来存储.data 段的起始加载地址。
1 | . = ALIGN(4); |
将.data 段的运行地址(runtime address)设定为 0x900000。加载地址由变量 data_load_addr 确定。这样设置后,在.bin 文件中“ .data”段仍旧存储在 “ .rodata ” 段之后。但在程序运行时, CPU 会从 0x900000 开始的空间内读取 “.data 段” 的值。
1 | .data 0x900000 : AT(data_load_addr) |
下面我们将重定位后.data 段的起始地址存储在变量 data_start,重定位后的.data 段的结束地址存储在变量 data_end,这两个变量将供汇编文件调用。
1 | { |
修改后的链接脚本如下所示:
1 | SECTIONS { |
通过上述操作, CPU 虽然会去片内 RAM 中读取.data 段数据,但实际上片内 RAM并没有准备好.data 段的数据,如下图所示。下面我们将通过汇编将 DDR 内存上的.data 段数据重定位到片内 RAM 上。
3. 修改汇编文件重定位.data 段
3.1 测试一下在DDR内部的data段重定位
这里完整的测试代码可以看这里:09_RELOCATION/03_without_relocation · sumumm/imx6ull-bare-demo - 码云 - 开源中国 (gitee.com)。我们可以将之前定义的各个变量的地址打印出来:
1 |
|
我们的链接文件如下:
1 | SECTIONS { |
可以看到打印信息如下:
然后我们修改链接文件如下:
1 | SECTIONS { |
会看到变量的地址也随着发生了变化:
3.2 重定位到片内RAM
完整代码可以看这里:09_RELOCATION/04_manual_relocate_data · sumumm/imx6ull-bare-demo - 码云 - 开源中国 (gitee.com)。设置完栈后直接跳转到 copy_data 函数重定位 data 段。
1 | .text |
燃弧我们来实现这个 copy_data 函数 :
1 | copy_data: |
3.3 测试结果
我们烧写到开发板,会有以下打印信息:
可以看到.data已经被定位到0x00900000。
三、C 函数重定位 data 段和清除 bss 段
目前为止我们已经通过汇编实现了重定位 data 段和清除 bss 段。为了让汇编程序更加简洁,这一节中我们将通过 C 语言实现重定位 data 段和清除 bss段。
1. 通过汇编传递链接脚本变量
这一部分我们来学习通过汇编文件获得链接脚本中的变量,再将这些变量传递给 C 函数。 完整代码可以看这里:09_RELOCATION/05_relocate_data_with_c_use_start · sumumm/imx6ull-bare-demo - 码云 - 开源中国 (gitee.com)
1.1 修改汇编文件
打开 start.S 将之前的汇编函数 copy_data, clean_bss 删除,改为直接调用C 函数。在调用对应的 C 函数之前,需要通过寄存器 r0~r4 将 C 函数的参数准备好。
1 | .text |
1.2 实现 copy_data, clean_bss 函数
我们创建程序文件 init.c ,在里面实现 copy_data, clean_bss 函数 :
1 | void copy_data (volatile unsigned int *src, volatile unsigned int *dest, unsigned int len) /* src, dest, len */ |
对于 copy_data 函数来说,参数 src, dest, len 分别对应汇编文件中r0, r1, r2 的值。
对于 clean_bss 函数来说,参数 start, end 分别对应汇编文件中 r0,r1 的值
1.3 修改 Makefile
修改 Makefile 文件,编译 init.c 并链接 init.o。
1.4 测试效果
我们编译并烧写到开发板中,看到以下打印信息说明我们在C语言实现了重定位和清除BSS段:
2. C函数直接调取链接脚本变量
上一节中 C 函数需要通过汇编文件传入参数,在这一节我们将进一步改进 C函数,使得 C 函数跳过汇编文件,直接从链接脚本中调用所需变量。完整代码可以看这里:09_RELOCATION/06_relocate_data_with_c_use_lds · sumumm/imx6ull-bare-demo - 码云 - 开源中国 (gitee.com)
2.1 修改汇编文件为直接调用 C 函数
1 | .text |
2.2 修改 init.c 通过函数来获取参数
1 | void copy_data (void) |
2.3 测试效果
我们编译并烧写到开发板中可以看到如下打印信息,说明我们重定位成功:
3. 总结:如何在 C 函数中使用链接脚本变量
我们来总结一下如何在 C 函数中使用链接脚本中定义的变量:
- (1)在 C 函数中声明该变量为外部变量,用 extern 修饰,例如: extern int _start;
- (2)使用取址符号(&)得到该变量的值,例如: int *p = &_start;//p 的值为 lds 文件中_start 的值。
为什么在汇编文件中可以直接使用链接脚本中的变量,而在 C 函数中需要加上取址符号呢?
原因: C 函数中定义一个全局变量 int g_i = 10;,程序中必然有 4 字节的空间留出来给这个变量 g_i,然而链接脚本中的变量并不像全局变量一样都保存在.bin 文件中。如果我们在 C 程序中只用到链接脚本变量 a1, a2, a3,那么程序中并不保存这 3 个变量。 但是这些变量的符号,都会保存在 symbol_table 符号表中,如下图所示
从上图中我们注意到:
- 对于全局变量, symbol table 里面存储的是变量的地址;可以通过&g_i得到变量的地址 addr。
- 对于链接脚本变量, symbol table 里面存储的是变量的值;为了取出这个值, C 代码要通过&a1。
四、C语言重定位全部代码
对于 imx6ull,它的 boot ROM 功能强大,会帮我们把程序重定位到 DDR3内存上。但对于一些采用其他芯片的板子,这一部分的操作可能需要我们手动去完成。例如 S3C2440 上电后,因为硬件的限制, .bin 文件的前 4k 程序需要将整个程序重定位到大小能够执行整个程序的 SDRAM 上。
为了学习代码重定位所需知识,在这一节中我们将重定位整个.bin 文件到片内 RAM 上。需要注意,虽然将全部代码重定位到片内 RAM 上可以加快命令的执行、数据的读取写入,但是这样的做法并不适合体积较大的程序,因为片内 RAM 只有128KB 空间。
1. 重定位步骤
完整代码可以看这里:09_RELOCATION/07_relocate_all_with_c · sumumm/imx6ull-bare-demo - 码云 - 开源中国 (gitee.com)
1.1 修改链接脚本
① 修改链接地址为 0x900000
② 删除与.data 段相关的链接脚本变量。
③ 添加变量_load_addr 并将它的值设置为 Makefile 中 entry 地址的值,供 C 函数调用。
1 | SECTIONS { |
1.2 修改 init.c
重定位全部代码和重定位.data 段原理相同。在这里只需要修改 copy_data函数中调用的外部变量。
1 | void copy_data (void) |
1.3 修改汇编文件
重定位之后,需要使用绝对跳转命令 ldr pc, = xxx,跳转到重定位后的地址。
1 | .text |
1.4 测试效果
我们能看到以下打印信息说明重定位成功,可以看到所有的变量都被重定义到片内RAM了。
2. 位置无关码
查看上述程序的反汇编发现,在重定位函数 copy_data 执行之前,已经涉及到了片内 RAM 上的地址,但此时片内 RAM 上并没有任何程序,那为什么程序还能正常运行呢?
dis 文件中左边的 90000xx 是链接地址,表示程序运行“应该位于这里”。但是实际上,我们一上电, boot ROM 把程序放到 0x80100000 去了。所以一开始运行这些指令时,它们是位于 DDR 里的。
第 9 行的 blx 命令,并不是跳到 0x9006a4。这要根据当前的 PC 值来计算,在 dis 里写成 0x9006a4,这只是表示“如果程序从 0x900000 开始运行的话,第9 行就会跳到 0x9006a4”。现在程序被 boot ROM 复制到 0x80100000,从0x80100000 开始运行,我们需要根据机器码来计算出实际跳转的地址。
blx 是相对跳转指令,要跳到“ pc + offset”这个地址去。程序从 0x8010000运行,运行到第 9 行时,如下计算新地址:
1 | PC = 当前地址+8=0x8010004+8=0x801000C |
在 0x80105C8 这个位置,确实存有 copy_data 函数(这里没看懂,从哪知道之类有这个函数的?反正韦东山的裸机开发教程这么写的,我反正没理解,后面明白了再补充吧),所以:即使程序并不在链接地址 0x900000 上,它也可以运行。因为 blx 是相对跳转指令,它用的不是链接地址,它是“位置无关”的。使用“位置无关码”写出的代码,它可以在任何位置上运行,不一定要在“链接地址”上运行。
下面我们来分析一下实际板子上电后,程序是如何执行的 :
(1)程序被 boot ROM 重定位到 0x80100000,并从这个地址开始执行第一条指令, 此时 pc = 0x80100000 + 8 = 0x80100008。
(2)执行到第 2 条指令“fa00016f”时,根据上述算法,它跳到地址0x80105C8 去执行 copy_data 函数。
(3)在执行完 copy_data 和 clean_bss 函数后,片内 RAM 0x900000 上已经有程序了。
(4)执行绝对跳转命令“ldr pc, =main”,它是一条伪指令,真实指令是“ldr pc, [pc, #4] ; 900018 <halt+0x8>”。
从 dis 文件里很容易看出,执行完这条指令后, pc 等于 dis 文件中“ 900018”上的值“009001d1”,所以程序跳到片内 RAM 去执行 main 函数了。
注意:在 dis 文件中, main 函数的链接地址是 0x009001d1,往 pc 寄存器里赋值 0x009001d1 时, bit0 为 1,表示 main 函数的代码是用 Thumb 指令写的。
3. 如何写位置无关码?
那么我们应该如何写位置无关码呢?答:使用相对跳转命令 b 或 bl,并注意:
- 重定位之前,不可使用绝对地址
(a) 不可访问全局类变量(全局变量或 static 修饰的局部变量)。
(b) 不可访问有初始值的数组(初始值放在 rodata 里,需要绝对地址来访问)。
- 重定位之后,使用 ldr pc = xxx,跳转到绝对地址( runtime address) 。