linux系列目录:
linux基础篇(一)——GCC和Makefile编译过程 linux基础篇(二)——静态和动态链接 ARM裸机篇(一)——i.MX6ULL启动过程 ARM裸机篇(二)——i.MX6ULL第一个裸机程序 ARM裸机篇(三)——重定位和地址无关码
一、将程序重映射到RAM
在上一节中,我们将第一个裸机程序编译后,然后将映像文件重定位到了DDR3内存上,其中.bin文件的起始地址为0x80000000。重定位结束后,CPU会从这个地址读取第一条指令开始执行程序。
在链接之前查看代码.text段的地址信息如下:可以发现所有段的起始地址都为0 链接之后的.text段地址:.text段的地址被重映射到了0x80000000.
这一节我们将代码重映射到RAM中执行.
- 修改链接脚本
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 = .;
}
- 修改汇编部分:
.text
.global _start
.thumb
_start:
ldr sp,=0x80200000
bl copy_data
bl clean_bss
ldr pc, =main
halt:
b halt
- 修改C部分
#define CCM_CCGR1 (volatile unsigned long*)0x20C406C
#define IOMUXC_SW_MUX_CTL_PAD_GPIO1_IO04 (volatile unsigned long*)0x20E006C
#define IOMUXC_SW_PAD_CTL_PAD_GPIO1_IO04 (volatile unsigned long*)0x20E02F8
#define GPIO1_GDIR (volatile unsigned long*)0x0209C004
#define GPIO1_DR (volatile unsigned long*)0x0209C000
#define uint32_t unsigned int
void copy_data (void)
{
extern int _load_addr, _start, __bss_start;
volatile unsigned int *dest = (volatile unsigned int *)&_start;
volatile unsigned int *end = (volatile unsigned int *)&__bss_start;
volatile unsigned int *src = (volatile unsigned int *)&_load_addr;
while (dest < end)
{
*dest++ = *src++;
}
}
void clean_bss(void)
{
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;
}
}
void delay(uint32_t count)
{
volatile uint32_t i = 0;
for (i = 0; i < count; ++i)
{
__asm("NOP");
}
}
int main()
{
*(CCM_CCGR1) = 0xFFFFFFFF;
*(IOMUXC_SW_MUX_CTL_PAD_GPIO1_IO04) = 0x5;
*(IOMUXC_SW_PAD_CTL_PAD_GPIO1_IO04) = 0x1F838;
*(GPIO1_GDIR) = 0x10;
*(GPIO1_DR) = 0x0;
while(1)
{
*(GPIO1_DR) = 0x0;
delay(0xFFFFF);
*(GPIO1_DR) = 1<<4;
delay(0xFFFFF);
}
return 0;
}
- 编译烧录
可以发现程序定位到片内RAM后,LED的闪烁速度明显变快。因为CPU读取RAM的速度比读取DDR的速度快的多。
二、 代码分析
对led.elf进行反汇编,生成反汇编文件imx6ull.lds。
查看上述程序的反汇编发现,在重定位函数copy_data执行之前,已经涉及到了片内RAM上的地址,但此时片内RAM上并没有任何程序,那为什么程序还能正常运行呢?
dis文件中左边的90000xx是链接地址,表示程序运行“应该位于这里”。但是实际上,我们一上电,boot ROM把程序放到0x80100000去了。所以一开始运行这些指令时,它们是位于DDR里的。 第9行的blx命令,并不是跳到0x9005c8。这要根据当前的PC值来计算,在dis里写成9005c8,这只是表示“如果程序从0x900000开始运行的话,第9行就会跳到0x9005c8”。现在程序被boot ROM复制到0x80100000,从0x80100000开始运行,我们需要根据机器码来计算出实际跳转的地址。 blx是相对跳转指令,要跳到“pc + offset”这个地址去。程序从0x8010000运行,运行到第9行时,如下计算新地址: PC=当前地址+8=0x8010004+8=0x801000C offset=机器码“fa00016f”里的bit[23:0]4=0x16f4=0x5BC 新PC=PC + offset = 0x80105C8 在0x80105C8这个位置,确实存有copy_data函数,所以:即使程序并不在链接地址0x900000上,它也可以运行。因为blx是相对跳转指令,它用的不是链接地址,它是“位置无关”的。使用“位置无关码”写出的代码,它可以在任何位置上运行,不一定要在链接地址 上运行。
下面我们来分析一下实际板子上电后,程序是如何执行的:
- 程序被boot ROM重定位到0x80100000,并从这个地址开始执行第一条指令:此时pc = 0x80100000 + 8 = 0x80100008。
- 执行到第2条指令“fa00016f”时,根据上述算法,它跳到地址0x80105C8去执行copy_data函数
- 在执行完copy_data和clean_bss函数后,片内RAM 0x900000上已经有程序了。
- 执行绝对跳转命令“ldr pc, =main”,它是一条伪指令,真实指令是“ldr pc, [pc, #4] ; 900018 <halt+0x8>”:
从dis文件里很容易看出,执行完这条指令后,pc等于dis文件中“900018”上的值“009001b3”,所以程序跳到片内RAM去执行main函数了。
注意: 在dis文件中,main函数的链接地址是0x009001b2,往pc寄存器里赋值0x009001b3时,bit0为1,表示main函数的代码是用Thumb指令写的。
- 重定位之前,不可使用绝对地址
a) 不可访问全局类变量(全局变量或static修饰的局部变量) b) 不可访问有初始值的数组(初始值放在rodata里,需要绝对地址来访问) - 重定位之后,使用ldr pc = xxx,跳转到绝对地址(runtime address)
三、 总结
重定位: 编译器和汇编器生成从地址 0 开始的代码和数据段。链接器通过把每个符号定义与一个内存位置关联起来,从而重定位这些段,然后修改所有对这些符号的引用,使得它们指向这个内存位置 。
地址无关代码: 利用相对跳转指令,根据当前PC值自动计算出跳转地址,而不是利用的链接地址,所以这类代码都是地址无关的,它可以在任何位置上运行。
编译器还利用代码段中任何指令和数据段中任何变量之间的距离都是一个运行时常量,与代码段和数据段的绝对内存位置无关这个事实,制作位置无关的共享库。
扩展阅读:linux基础篇(二)——静态和动态链接
|