上一节完成了SDRAM的控制,现在我们可以对SDRAM进行读写操作了,因此用SDRAM做一下代码重定位的内容。一些内容已经在https://blog.csdn.net/G_METHOD/article/details/104508545中提及,如重定位是什么,为何要进行重定位,还有链接脚本中使用到的地址的区别,因此此处会简单一些描述。
当我们的程序不能在原本的目标地址执行的时候,就需要进行代码重定位,而代码重定位做的就是把程序从当前不能正常运行的地址处搬到运行时地址达到程序正常运行的目的。
重定位一般包含段复制以及bss段清除,bss段是全局变量中无初始值或者初始值为0的变量合集,作为bss段而非data段存在可以节省bin文件大小。而为重定位做导航的是链接脚本,如源地址,目标地址等都可以由链接脚本给出,同时链接脚本还能做内存映射安排,随心所欲安排段的位置以及内存分布的相对位置,留下自己所以希望的空洞,链接脚本具体内容可以细致读一读http://ftp.gnu.org/old-gnu/Manuals/ld-2.9.1/html_mono/ld.html#IDX237,可以更加清晰的认识和使用链接脚本,这里精简地说一下最核心的部分。
SECTIONS {
...
secname start BLOCK(align) (NOLOAD) : AT ( ldadr )
{ contents } >region :phdr =fill
...
}
这是我们要用到的链接脚本最核心的地方,SECTIONS为固定格式,一个链接脚本必须有且只能至多一个,SECTION内我们可以自由安排段的内存映射。secname是段名,一般有.data,.text,.rodata,.bss等,必须为链接器认识的段名字。start处填写我们期望程序的运行时地址,如果未指定,则按顺序以及前面的段长度直接给定。BLOCK(align)只我们可设置该段的起始地址处于对齐地址处,从而保证程序的绝对正常运行,这里的对齐不是唯一的方式后文将提及另外一种方式。NOLOAD表示该段处于可执行文件中,读取elf可以查看到该段,但是程序运行时不加载入内存中,由于我们是裸板程序,存储位置处即加载地址,因此没有说不加载这一概念,个人认为这里只针对elf等应用层加载。需要注意的是 :AT ,AT前面的 : 两侧是必须要有空格的。AT(ldadr)指示程序的加载地址,在bin文件中,这一特性可以利用来控制bin的大小,如text和data以及bss紧挨着排列放置,但是重定位时各自到各自的运行时地址,如果没有指定AT(ldadr)这一地址,加载地址默认与运行时地址start一致。contents是我们自由安排相同段内内容顺序的地方。后面的region和phdr以及fill由于未使用这里就不多赘述,在上述第二个网站链接的文档中有细致描述。
以JZ2440为例输出如下链接脚本,SDRAM的起始地址在0x30000000,将其作为起始地址,这里用到了 . 符号,. 符号表示当前位置的地址,仅在SECTIONS内可用,作为右值使用则将当前地址赋值给变量,如下面的链接脚本中的__copy_start = . 。当作为左值使用时,用于指定当前地址,需要注意的是相关赋值操作都需要加上 ;符号结尾。ALGIN(4) 表示地址按照4字节对齐,由于arm以4字节为基准步进取值解码执行,当地址不为4字节对齐时,强制进行对齐,如果我们不进行对齐,则可能出现数据段在0x3002处,此时需要对该数据区域写,直接进行对齐变成对地址0x3000执行赋值操作,此时如果0x3000地址存在有效的代码时,将会覆盖,从而导致程序运行异常,因此保险起见,应该对每个段进行对齐。
linker.lds
-------------------------
SECTIONS{
. = 0x30000000;
. = ALIGN(4);
_copy_start = .;
.text : { *(.text) }
. = ALIGN(4);
.rodata :{ *(.rodata) }
. = ALIGN(4);
.data : { *(.data) }
. = ALIGN(4);
_copy_end = .;
_bss_start = . ;
.bss : {
*(.bss)
*(.COMMON)
}
_bss_end = .;
}
链接脚本的contents部分描述我们将什么内容放置在段中,通配符 * 表示所有的文件,这里的文件是指执行 ld -T linker.lds xxx.o yyy.o 指定的目标文件,各个目标文件在内存中的位置和顺序与ld执行时指定目标文件的顺序有关,如果按照上述方式排列,则顺序为xxx.o 再yyy.o,当然也可以在contents中指定目标文件顺序,比如我们需要将Start.S这一启动文件放置在其他文件之前,可以这样
.text : { Start.o(.text) *(.text) }
中间使用空格分隔开。
这里示例程序的代码重定位涉及到段复制和bss段清除,因此在对应段的位置定义相应的变量以供重定位程序使用。需要再说的是在SECTIONS中定义的变量,在elf中是以符号表中没有值的符号存在的,拓展一下,全局变量和函数都会在符号表中存储,符号表中对应符号存在对应地址,该地址指向的区域是变量的初始值,函数的情况较为复杂这里不做拓展,而链接脚本的变量在这里存储,但是也有相应的地址,但是没有值,相应的,地址就是链接脚本变量的值,因此我们使用的时候应该将其当做一个外部符号来使用,但是仅仅对其取地址,如下,使用32位指针类型指向该地址进行相应操作可以较快完成该阶段(硬件特性和软件层面的实现决定)。
extern uint32_t _bss_start;
uint32_t *target_start_address = &_bss_start;
关于符号表和链接脚本变量及C语言中如何使用链接脚本变量的详细知识可以查看https://sourceware.org/ml/binutils/2007-07/msg00154.html,基于上述知识,输出代码如下:
Start.S
----------------
.text
.global _start
_start:
MOV R0,#0
LDR R1,[R0]
STR R0,[R0]
LDR R2,[R0]
CMP R2,R0
LDR SP,=0x40000000+4096
MOVEQ SP,#4096
STREQ R1,[R0]
bl HardwareInitAll //相对跳转
ldr pc,=main //绝对跳转,到SDRAM上运行
halt:
b halt
s3c2440.c
------------------------
#include "s3c2440.h"
....
static void CopySegmentForReloacte(void)
{
extern uint32_t _copy_start,_copy_end;
uint32_t *source_start = 0;
uint32_t *target_start = &_copy_start;
uint32_t *target_end = &_copy_end;
while(target_start <= target_end)
{
*target_start++ = *source_start++;
}
}
static void CleanBssSegment(void)
{
extern uint32_t _bss_start,_bss_end;
uint32_t *target_start_address = &_bss_start;
uint32_t *target_end = &_bss_end;
while(target_start_address <= target_end)
{
*target_start_address++ = 0;
}
}
void HardwareInitAll(void)
{
WatchDogDisable();
ClockDevideConfig();
ChangeModeToAsynchronous();
MPLLConfig();
MemoryControllerInit();
CopySegmentForReloacte();
CleanBssSegment();
}
main.c
------
#include <stdint.h>
#include "s3c2440.h"
#include "led.h"
#include "uart.h"
char test_char = 'A';
int main()
{
UartInit();
while(1)
{
putc(test_char);
if(++test_char > 'Z') { test_char = 'A'; }
Delay(1000);
}
return 0;
}
makefile中,使用 -T选项指定链接脚本,如下
al:
arm-linux-gcc -o Start.o -c Start.S
arm-linux-gcc -o main.o -c main.c
arm-linux-gcc -o s3c2440.o -c s3c2440.c
arm-linux-gcc -o uart.o -c uart.c
arm-linux-gcc -o led.o -c led.c
arm-linux-gcc -o key.o -c key.c
arm-linux-ld -T linker.lds -o led.elf Start.o s3c2440.o led.o key.o uart.o main.o
arm-linux-objcopy -O binary -S led.elf led.bin
arm-linux-objdump -D led.elf > led.dis
clean:
rm *.dis *.o *.bin *.elf
编译后烧写到NorFlash中,如果串口正常依次输出A-Z即重定位成功。
Start.S中bl相对跳转到初始化函数中进行初始化和重定位,因为未完成重定位前运行时地址没有可执行的有效指令数据,智能通过相对跳转执行NorFlash上的程序,当重定位程序把运行时地址即SDRAM上的数据都准备好后,采用ldr绝对跳转正式跳转到SDRAM上运行。由此可以总结出,重定位代码前禁止使用位置相关代码,只能使用位置无关码进行程序的执行。
此处留空,以后补充位置无关码的判别。