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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
int main(int argc, const char * argv[])
{
Uart_Init(); //初始化uart串口

printf("\n\r");
/* 在串口上输出g_charA */
while (1)
{
PutChar(g_charA);
g_charA++;
delay(1000000);
}

return 0;
}

上述代码在程序运行时, CPU 需要不断地访问 DDR3 内存来获取 g_charA 的值,访问DDR3 会花费大量的时间,那么如何提升访问的效率呢?

答:在程序运行先前将 data 段的数据重定位到 imx6ull 的片内 RAM 上,因为CPU 访问片内 RAM 的速度远快于访问 DDR3 的速度。

二、汇编重定位data段

完整代码可以看这里:

1. 参考芯片手册确定片内RAM位置

我们可以查看参考手册的《Chapter 2: Memory Maps》 :

image-20230929093431743

参考芯片手册得到片内 RAM 的地址为: 0x900000 ~ 0x91FFFF。所以我们将.data 段重定位后的地址设置为 0x900000。

2. 链接脚本修改  

创建一个变量用来存储.data 段的起始加载地址。

1
2
3
4
   . = ALIGN(4);
.rodata : { *(.rodata) }
. = ALIGN(4);
data_load_addr = .;//将当前地址存储在变量中(大概的值为 0x8880xxxx)

将.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
2
3
4
5
{
data_start = . ;
*(.data)
data_end = . ;
}

修改后的链接脚本如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
SECTIONS {
. = 0x80100000;

. = ALIGN(4);
.text :
{
*(.text)
}

. = ALIGN(4);
.rodata : { *(.rodata) }

. = ALIGN(4);

data_load_addr = .;
.data 0x900000 : AT(data_load_addr)
{
data_start = . ;
*(.data)
data_end = . ;
}

. = ALIGN(4);
__bss_start = .;
.bss : { *(.bss) *(.COMMON) }
__bss_end = .;
}

通过上述操作, CPU 虽然会去片内 RAM 中读取.data 段数据,但实际上片内 RAM并没有准备好.data 段的数据,如下图所示。下面我们将通过汇编将 DDR 内存上的.data 段数据重定位到片内 RAM 上。

image-20230929095552392

3. 修改汇编文件重定位.data 段

3.1 测试一下在DDR内部的data段重定位

这里完整的测试代码可以看这里:09_RELOCATION/03_without_relocation · sumumm/imx6ull-bare-demo - 码云 - 开源中国 (gitee.com)。我们可以将之前定义的各个变量的地址打印出来:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
#include "uart.h"
#include "my_printf.h"

char g_charA = 'A'; //存储在 .data段
const char g_charB = 'B'; //存储在 .rodata段
const char g_charC; //存储在 .bss段
int g_intA = 0; //存储在 .bss段
int g_intB; //存储在 .bss段

void delay (volatile int time)
{
while(time--);
}

int main(int argc, const char * argv[])
{
Uart_Init(); //初始化uart串口

printf("g_charA=%d &g_charA=0x%x\n\r", g_charA, &g_charA);
printf("g_charB=%d &g_charB=0x%x\n\r", g_charB, &g_charB);
printf("g_charC=%d &g_intA=0x%x\n\r", g_charC, &g_charC);
printf("g_intA=%d &g_intA=0x%x\n\r", g_intA, &g_intA);
printf("g_intB=%d &g_intB=0x%x\n\r", g_intB, &g_intB);

/* 在串口上输出g_charA */
while (1)
{
//printf("g_intA=%d &g_intA=0x%x\n\r", g_intA, &g_intA);
g_charA++;
delay(1000000);
}

return 0;
}

我们的链接文件如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
SECTIONS {
. = 0x80100000;

. = ALIGN(4);
.text :
{
*(.text)
}

. = ALIGN(4);
.rodata : { *(.rodata) }

. = ALIGN(4);
.data 0x80200000 : { *(.data) }

. = ALIGN(4);
__bss_start = .;
.bss : { *(.bss) *(.COMMON) }
__bss_end = .;
}

可以看到打印信息如下:

image-20240119232309998

然后我们修改链接文件如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
SECTIONS {
. = 0x80100000;

. = ALIGN(4);
.text :
{
*(.text)
}

. = ALIGN(4);
.rodata : { *(.rodata) }

. = ALIGN(4);
.data 0x80140000 : { *(.data) }

. = ALIGN(4);
__bss_start = .;
.bss : { *(.bss) *(.COMMON) }
__bss_end = .;
}

会看到变量的地址也随着发生了变化:

image-20240119232813611

3.2 重定位到片内RAM

完整代码可以看这里:09_RELOCATION/04_manual_relocate_data · sumumm/imx6ull-bare-demo - 码云 - 开源中国 (gitee.com)。设置完栈后直接跳转到 copy_data 函数重定位 data 段。

1
2
3
4
5
6
7
8
9
10
11
12
13
.text
.global _start

_start:

/* 设置栈 */
ldr sp,=0x80200000

/* 重定位data段 */
bl copy_data

/* 清除bss段 */
bl clean_bss

燃弧我们来实现这个 copy_data 函数 :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
copy_data:
/* 重定位data段 */
ldr r1, =data_load_addr /* data段的加载地址 (0x8010....) */
ldr r2, =data_start /* data段重定位地址, 0x900000 */
ldr r3, =data_end /* data段结束地址(重定位后地址 0x90....) */
cpy:
ldr r4, [r1] /* 从r1读到r4 */
str r4, [r2] /* r4存放到r2 */
add r1, r1, #4 /* r1+1 */
add r2, r2, #4 /* r2+1 */
cmp r2, r3 /* r2 r3比较 */
bne cpy /* 如果不等则继续拷贝 */

mov pc, lr

3.3 测试结果

我们烧写到开发板,会有以下打印信息:

image-20240119073639853

可以看到.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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
.text
.global _start

_start:

/* 设置栈 */
ldr sp,=0x80200000

/* 重定位data段 */
ldr r0, =data_load_addr /* data段的加载地址 (0x8010....) */
ldr r1, =data_start /* data段重定位地址, 0x900000 */
ldr r2, =data_end /* data段结束地址(重定位后地址 0x90....) */
sub r2, r2, r1 /* r2的值为data段的长度 */

bl copy_data /* 跳转到函数copy_data并将r0,r1,r2作为函数参数传入 */

/* 清除bss段 */
ldr r0, =__bss_start
ldr r1, =__bss_end

bl clean_bss /* 跳转到函数clean_bss并将r0, r1作为函数参数传入*/

/* 跳转到主函数 */
bl main

halt:
b halt

1.2 实现 copy_data, clean_bss 函数

我们创建程序文件 init.c ,在里面实现 copy_data, clean_bss 函数 :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
void copy_data (volatile unsigned int *src, volatile unsigned int *dest, unsigned int len)  /* src, dest, len */
{
unsigned int i = 0;

while (i < len)
{
*dest++ = *src++;
i += 4;
}
}

void clean_bss (volatile unsigned int *start, volatile unsigned int *end) /* start, end */
{
while (start <= end)
{
*start++ = 0;
}
}

  • 对于 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段:

image-20240119075018893

2. C函数直接调取链接脚本变量

上一节中 C 函数需要通过汇编文件传入参数,在这一节我们将进一步改进 C函数,使得 C 函数跳过汇编文件,直接从链接脚本中调用所需变量。完整代码可以看这里:09_RELOCATION/06_relocate_data_with_c_use_lds · sumumm/imx6ull-bare-demo - 码云 - 开源中国 (gitee.com)

2.1 修改汇编文件为直接调用 C 函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
.text
.global _start

_start:

/* 设置栈 */
ldr sp,=0x80200000

/* 重定位data段 */
bl copy_data

/* 清除bss段 */
bl clean_bss

/* 跳转到主函数 */
bl main

halt:
b halt

2.2 修改 init.c 通过函数来获取参数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
void copy_data (void)
{
/* 从链接脚本中获得参数 data_load_addr, data_start, data_end */
extern int data_load_addr, data_start, data_end;

volatile unsigned int *dest = (volatile unsigned int *)&data_start;
volatile unsigned int *end = (volatile unsigned int *)&data_end;
volatile unsigned int *src = (volatile unsigned int *)&data_load_addr;

/* 重定位数据 */
while (dest < end)
{
*dest++ = *src++;
}
}

void clean_bss(void)
{
/* 从lds文件中获得 __bss_start, __bss_end */
extern int __bss_end, __bss_start;

volatile unsigned int *start = (volatile unsigned int *)&__bss_start;
volatile unsigned int *end = (volatile unsigned int *)&__bss_end;

while (start <= end)
{
*start++ = 0;
}
}

2.3 测试效果

我们编译并烧写到开发板中可以看到如下打印信息,说明我们重定位成功:

image-20240119230542564

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 符号表中,如下图所示

image-20240119224923460

从上图中我们注意到:

  • 对于全局变量, 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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
SECTIONS {
_load_addr = 0x80100000;

. = 0x900000;

. = ALIGN(4);
.text :
{
*(.text)
}

. = ALIGN(4);
.rodata : { *(.rodata) }

. = ALIGN(4);
.data : { *(.data) }

. = ALIGN(4);
__bss_start = .;
.bss : { *(.bss) *(.COMMON) }
__bss_end = .;
}

1.2 修改 init.c

重定位全部代码和重定位.data 段原理相同。在这里只需要修改 copy_data函数中调用的外部变量。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
void copy_data (void)
{
/* 从链接脚本中获得参数 _start, __bss_start, */
extern int _load_addr, _start, __bss_start;

volatile unsigned int *dest = (volatile unsigned int *)&_start; //_start = 0x900000
volatile unsigned int *end = (volatile unsigned int *)&__bss_start; //__bss_start = 0x9xxxxx
volatile unsigned int *src = (volatile unsigned int *)&_load_addr; //_load_addr = 0x80100000

/* 重定位数据 */
while (dest < end)
{
*dest++ = *src++;
}
}

void clean_bss(void)
{
/* 从lds文件中获得 __bss_start, __bss_end */
extern int __bss_end, __bss_start;

volatile unsigned int *start = (volatile unsigned int *)&__bss_start;
volatile unsigned int *end = (volatile unsigned int *)&__bss_end;

while (start <= end)
{
*start++ = 0;
}
}

1.3 修改汇编文件

重定位之后,需要使用绝对跳转命令 ldr pc, = xxx,跳转到重定位后的地址。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
.text
.global _start

_start:

/* 设置栈 */
ldr sp,=0x80200000

/* 重定位text, rodata, data段 */
bl copy_data

/* 清除bss段 */
bl clean_bss

/* 跳转到主函数 */
// bl main /* 相对跳转,程序仍在DDR3内存中执行 */
ldr pc, =main /* 绝对跳转,程序在片内RAM中执行 */

halt:
b halt

1.4 测试效果

我们能看到以下打印信息说明重定位成功,可以看到所有的变量都被重定义到片内RAM了。

image-20240119231520558

2. 位置无关码

查看上述程序的反汇编发现,在重定位函数 copy_data 执行之前,已经涉及到了片内 RAM 上的地址,但此时片内 RAM 上并没有任何程序,那为什么程序还能正常运行呢?

image-20240119233245643

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
2
3
PC     = 当前地址+8=0x8010004+8=0x801000C
offset = 机器码 fa00016f 里的 bit[23:0]*4=0x16f*4=0x5BC
new PC = PC + offset = 0x80105C8

在 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) 。