Skip to content

LV010-段的重定位

一、重定位的概念

1. 什么是重定位

接触过 S3C2440 的话应该会了解到,在程序运行之前我们需要手动将.bin 文件上的全部代码从 Nor Flash 或 Nand Flash 拷贝到 SDRAM 上。对于 imx6ull 来说,这部分拷贝代码的操作由 Boot Rom 自动完成,板子上电后 boot Rom 会将映像文件从启动设备(TF 卡、 eMMC)自动拷贝到 DDR3 内存上。上述拷贝代码的过程就是 重定位

那么 boot Rom 应该将映像文件拷贝到内存的哪个位置呢?这部分内容已经《25-裸机开发/05-imx6ull 启动/LV020-映像文件.md》中学习过了 。映像文件包含多个部分,其中.bin 文件的起始地址由地址 entry 决定,需要在 Makefile 中手动配置。

shell
./mkimage -n imximage.cfg.cfgtmp -T imximage -e 0x80100000 -d demo.bin demo.imx

按照上述的配置,整个映像文件被自动重定位到 DDR 内存上,其中.bin 文件的起始地址为 0x80100000。重定位结束后, CPU 会从这个地址读取第一条指令开始执行程序。

2. 为什么要重定位?

c
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-20260115125325448

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

2. 链接脚本修改

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

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

将.data 段的运行地址(runtime address)设定为 0x900000。加载地址由变量 data_load_addr 确定。这样设置后,在.bin 文件中“ .data”段仍旧存储在 “ .rodata ” 段之后。但在程序运行时, CPU 会从 0x900000 开始的空间内读取 “.data 段” 的值。

c
	.data 0x900000 : AT(data_load_addr)

下面我们将重定位后.data 段的起始地址存储在变量 data_start,重定位后的.data 段的结束地址存储在变量 data_end,这两个变量将供汇编文件调用。

c
    {
      data_start = . ;
      *(.data) 
      data_end = . ;
    }

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

c
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。我们可以将之前定义的各个变量的地址打印出来:

c
#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;
}

我们的链接文件如下:

c
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

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

c
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。设置完栈后直接跳转到 copy_data 函数重定位 data 段。

assembly
.text
.global  _start

_start: 				

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

	/* 重定位data段 */
	bl copy_data

	/* 清除bss段 */
	bl clean_bss

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

assembly
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

1.1 修改汇编文件

打开 start.S 将之前的汇编函数 copy_data, clean_bss 删除,改为直接调用 C 函数。在调用对应的 C 函数之前,需要通过寄存器 r0~r4 将 C 函数的参数准备好。

assembly
.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 函数 :

c
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

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

assembly
.text
.global  _start

_start: 				

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

	/* 重定位data段 */
	bl copy_data

	/* 清除bss段 */
	bl clean_bss

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

halt:
	b  halt

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

c
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

1.1 修改链接脚本

① 修改链接地址为 0x900000

② 删除与.data 段相关的链接脚本变量。

③ 添加变量_load_addr 并将它的值设置为 Makefile 中 entry 地址的值,供 C 函数调用。

assembly
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 函数中调用的外部变量。

c
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,跳转到重定位后的地址。

c
.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 行时,如下计算新地址:

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