硬件平台:jz2440
软件平台:Ubuntu16.04 arm-linux-gcc-3.4.5
源码位置: https://github.com/lian494362816/C/tree/master/2440/012_relocate/008
承接上1篇博客 s3c2440学习之路-010 sdram, sdram已经初始化完毕,现在可以正式的发挥SDRAM的价值了。(SDRAM 是可以随意读写的,后续的代码都会放到上面来运行)
一个程序编译后,会有代码段、数据段、只读数据段、bss段和注释段
名称 | 作用 |
---|---|
.txt 代码段 | 存储代码 |
.data 数据段 | 存储数据 |
.rodata | 存储只读数据段,主要是使用const修饰过的变量 |
bss | 未初始化和初始化为0的全局(static 修饰的局部)变量 (减小bin文件的大小) |
.comment 注释段 | 注释作用,不占用bin文件的大小 |
这里主要讲一下bss段为什么可以减小bin文件的大小。bbs主要记录未初始化和初始化为0的全局(static 修饰的局部)变量的位置,而不会记录其具体数据,因为这部分的数值会默认设置为0。
这里看2个小程序
test1.c
#include
char a[1000];
int main(int argc, char *argv)
{
return 0;
}
test2.c
#include
char a[1000] = {1};
int main(int argc, char *argv)
{
return 0;
}
test1.c 定一个了1个char[1000]的数组,没有对其进行初始化,保存在bss段,最后编译出来的bin档大小为8756B
test2.c同样定义了1个chart1000]的数组,但是对其进行了初始化,保存在data段,最后编译出来的bin档大小为9592B
因为test1.c的数据保存在bss段,而bss只会记录其位置,所以不需要很大的存储空间。而test2.c需要把整个数组的值全部保存在数据段,所以至少需要比test1.c的bin大1000Byte。 这里要注意,减小的只是bin文件的大小,代码实际运行起来时消耗的内存大小是一样的。
当使用nand启动时,因为2440的特性,只能运行前4K的代码(前4K会自动拷贝到内部的sram里),因此想运行1个100K+ 的uboot.bin是不可以能的,所以需要把代码重定位到外部的SDRAM里。
当使用nor启动时,jz2440开发板上nor为2MB,足够放一般的程序。但是nor 可以随意读无法随意写,这意味着全局变量是不可修改的。因为修改全局变量需要把数值重新写到nor 里面,而nor 是不可随意写的,所以写数值会失败。因此也需要把代码重定位到外部的SDRAM里。(局部变量是可以修改的,因为局部变量是放在栈里面,栈一般会设置在2440内部的sram)
实现重定位很简单,第1把代码全部拷贝上SDRAM上,第2把代码跳到SDRAM上运行,也就是修改PC的值。
要把代码拷贝到SDRAM上,就需要弄清楚从哪里开始拷,拷多大,有什么顺序或规则吗。
1)代码是放在nor /nand里面,所以是从nor/nand开始拷。而烧写代码时都是从nor/nand的0地址开始的,所以要从nor/nand的0地址开始拷。
2)程序分成了代码段、数据段、只读数据段、bss段和注释段,因此要把这些数据全部拷贝到SDRAM里面,但是具体的大小是多少,后面会简单的讲一下lsd链接脚本,通过这个脚本可以知道程序的大小。
3)顺序可以通过lds链接脚本来设置,不过一般的顺序为代码段->只读数据段->数据段->bss段。
因为对lsd链接脚本没有做深入的研究,所以只懂得简单的运用,详细的使用请看 http://ftp.gnu.org/old-gnu/Manuals/ld-2.9.1/html_mono/ld.es.html 官方说明文档。这里只对自己使用到的lds链接脚本做注释。
test.lds
SECTIONS {
// .代表当前的位置, 把当前的位置设置为0x30000000, 0x30000000就是SDRAM的起始地址
. = 0x30000000 ;
// 4字节对齐
. = ALIGN(4) ;
//定义了一个变量 __copy_code_start = 当前的位置,也就是0x30000000
__copy_code_start = . ;
//代码段
.text :
{
//先存放start.o的代码段,因为start.o是第1个运行的文件
start.o
//*表示所有,所有文件的代码段
*(.text)
}
. = ALIGN(4) ;
//所有文件的只读数据段
.rodata : { *(.rodata) }
. = ALIGN(4) ;
//所有文件的数据段
.data : { *(.data) }
. = ALIGN(4) ;
//定义变量__bss_start=当前位置,当前的位置等于0x30000000+所有文件的代码段+所有文件的只读数据段+所有文件的数据段
__bss_start = . ;
//文件文件的bss段和注释段
.bss : { *(.bss) *(.COMMON) }
//定义变量__end = 当前的位置,当前的位置等于__bss_start+所有文件的bss段和注释段
__end = . ;
}
通过test.lds脚本可以知道,程序的存储顺序为:start.o的text->text->rodata->data->bss。总共定义了3个变量,__copy_code_start、__bss_start 、__end。
在编译的时候,使用-T就可以使用链接脚本了。因为链接脚本第1句“ . = 0x30000000 ;”,把当前的位置设置为0x30000000,所以反汇编看到dis文件的起始位置就是0x30000000。当你把生成的bin档烧录到nor/nand 里面时,还是会从nor/nand的0位置开始拷贝进去。复位后PC=0, 程序依旧是从0地址开始运行。因此dis文件中的0x30000000不会对代码产生任何影响。其中的__copy_code_start、__bss_start 、__end才是后面编程需要用到的。
start.s
/* 初始化外部的sdram */
bl bank6_sdram_init
/* 将代码从nor 拷贝到 sdram */
/* copy .data .rodata. .text to SDRAM */
bl copy2sdram
/* 清空bss段 */
/* clear bss segment */
bl clear_bss
/* 初始化串口 */
bl uart0_init
/* 打印位置的数值 */
bl print_positon
/* 跳到main去执行 */
ldr pc, =main /* abs jump to main */
start.s 前面会做一些其他的初始化,不过全部略去了,只看最重要的几个部分。SDRAM的初始化在上一篇博客讲了,这里就不做介绍了。
init.c
void copy2sdram(void)
{
extern int __copy_code_start, __bss_start;
volatile unsigned int *src = (unsigned int *)0;
volatile unsigned int *dst = (unsigned int *)&__copy_code_start;
volatile unsigned int *end = (unsigned int *)&__bss_start;
while(dst < end)
{
*dst++ = *src++;
}
}
__copy_code_start, __bss_start 是链接脚本里面的变量,引用外部变量,所以需要extern。现在的程序只支持nor 的重定位,而2440可以直接访问nor, 所以src=0 表示nor的0地址。&__copy_code_start表示取变量__copy_code_start的数值,也是0x30000000。这里的表达跟C语言不一样,C语言是取变量的地址,而__copy_code_start是链接脚本的变量,因此使用起来也不一样。可以把这个当做一种规则来记忆。同理,end = &__bss_start 也是取__bss_start的数值0x30001000。
这段代码的主要作用就是把nor 0地址的数据拷贝到0x30000000的地址去,长度为end - dst = &__bss_start - &__copy_code_start = 0x30001000 - 0x30000000 = 0x1000。这里只拷贝了text、data、rodata,bss段没有拷贝,接下来就处理bss段。(0x30000000就是SDRAM的起始地址,拷贝到这个地址就是拷贝到SDRAM里面,不理解的麻烦看看上一篇博客)
init.c
void clear_bss(void)
{
extern int __bss_start, __end;
volatile unsigned int *src = (unsigned int *)&__bss_start;
volatile unsigned int *end = (unsigned int *)&__end;
while(src <= end)
{
*src ++ = 0;
}
}
同理,引用 __bss_start, __end链接脚本变量需要使用extern。这里的&__bss_start 和 &__end也是获取其数值。src = &__bss_start = 0x30001000, end = &__end = 30001008。这段代码的主要作用就是把0x30001000~0x30001008上的数据清空。而拷贝代码时不要拷贝bss段,因此这段数据都是0,所以只需要记录bss的起始位置和终止位置,然后将其清空即可。这也照应了前面说的,bss段可以减小bin文件的大小。
init.c
void print_positon(void)
{
extern int __copy_code_start, __bss_start, __end;
volatile unsigned int *code_start = (unsigned int *)&__copy_code_start;
volatile unsigned int *bss_start= (unsigned int *)&__bss_start;
volatile unsigned int *end= (unsigned int *)&__end;
printf("code_start:%x\n", code_start);
printf("bss_start:%x\n", bss_start);
printf("end:%x\n", end);
}
print_positon 的作用就是打印__copy_code_start、__bss_start、__end的数值,因此需要先调用bl uart0_init来初始化串口。
start.s 最后执行 ldr pc, =main 将pc赋值成main的数值。通过dis文件可以看到, ldr pc, =main对应的语句为ldr pc, [pc, #16]。pc + 16 = 0x30000088, 0x30000088位置上的数值为0x30000af8,刚好main的数值就是0x30000af8。因此ldr pc, =main就等于pc=0x30000af8,此时程序就开始运行在SDRAM上了。
这里要特别说明,在执行ldr pc, =main之前,程序都是运行在nor上的,所以程序的地址不可能超过2M。虽然看到程序的起始地址是0x30000000,不过实际PC的值都需要减去0x30000000。虽然显示的地址为0x30000000+,不过程序是从nor的0地址开始烧录。复位后程序从nor 0地址开始取指令,也是对应的0x30000000。因此PC是从0开始往上增加,而不是从0x30000000开始往上增加,直到执行ldr pc, =main之后PC的值才真正的等于dis文件显示的。
main.c
#include "uart.h"
//#include "key.h"
//#include "led.h"
#include "delay.h"
#include "my_printf.h"
#include "init.h"
//2018.9.1 测试bss 段的初始数据是否是0
char _g_char1 = 'A';
char _g_char2 = 'a';
int _g_a;
int _g_b = 0;
const char _g_const_char = 'B';
int main(int argc, char *argv[])
{
uart0_init();
printf("_g_a:%d\n", _g_a);
printf("_g_b:%d\n", _g_b);
while(1)
{
printf("%c", _g_char1);
_g_char1 ++;
printf("%c ", _g_char2);
_g_char2 ++;
delay(500000);
}
return 0
}
程序定义了_g_char1和 _g_char2 两个全局变量,如果程序可以正常的跑到main函数并且这个2个变量的数值可以正常的增加,说明重定位成功。(目前只能测试nor)
看完后需要懂得以下几个问题才算是弄懂了代码重定位: