一、概述
JZ2440外接了各种控制器,它可以直接访问SDRAM、NOR Flash、SRAM(片内4K内存),以及各种控制器(包括Nand Flash控制器),但是不能直接访问Nand Flash。
我们程序可以放在Nor Flash上,也可以放在SDRAM上,CPU可以直接进行运行。但是如果我们将程序放到Nand Flash上,CPU就无法直接进行访问。那我们怎么通过Nand方式启动的呢?
Nand Flash启动时,物理硬件会将Nand Flash前4K内存copy到SRAM(片内4K内存),此时SRAM基地址为0,CPU从SRAM开始运行。
此时就涉及一个问题:如果我们程序大于4k,那如何进行Nand启动?
Nand Flash前4k内存,需要将所有程序放到SDRAM中,然后程序从SDRAM开始执行。
当我们通过Nor Flash启动时,虽然可以CPU可以直接访问超过4k的内容,但是由于Nor Flash的特性:能像普通内存那样直接读,但不能像普通内存那样直接写
。这就意味着:
通过Nor Flash启动后,需要把全局变量和静态变量重定位到SDRAM里。
所以,无论是Nor启动,还是Nand启动,我们都需要代码重定位。
把一个程序从一个位置移动到另一个位置,称之为重定位
二、测试Nor Flash写入
main.c
#include "uart.h"
void delay(volatile int d)
{
while (d--);
}
char g_Char = 'A'; //定义一个全局变量
const char g_Char2 = 'B'; //定义固定的全局变量
int g_A = 0;
int g_B;
int main(void)
{
uart0_init();
while(1)
{
putchar(g_Char);
g_Char++;
delay(1000000);
}
return 0;
}
其余的Makefile、start.S、uart.c、uart.h都与前章基本一致。此时编译出的bin文件大小有33k,显然是不对的。
查看反编译dis文件:
代码中,各个段的含义:
- text段:保存可执行的代码。
- data段:保存全局变量和静态(局部/全局)变量
- rodata段:固定的全局变量
- bss段:保存无初值或初值为0的全局变量。
- comment段:注释,如gcc编译信息。
我们可以清楚地发现:text段到data段之间,有一片很大的空白区域,我们称之为黑洞区
。
为了减少黑洞区
的大小,使得bin文件小于4k,让其能够支持Nand启动,只需要修改Makefile,手动指定data段的位置为0x800:
all:
arm-linux-gcc -c -o uart.o uart.c
arm-linux-gcc -c -o main.o main.c
arm-linux-gcc -c -o start.o start.S
arm-linux-ld -Ttext 0 -Tdata 0x800 start.o uart.o main.o -o sdram.elf
arm-linux-objcopy -O binary -S sdram.elf sdram.bin
arm-linux-objdump -D sdram.elf > sdram.dis
clean:
rm *.bin *.o *.elf *.dis
然后编译,发现bin文件变为3k大小,如何分别烧写到Nor Flash和Nand Flash,并启动,观察打印内容:
Nor启动时,打印AAAA......
Nand启动时,打印ABCD......
可见,Nor Flash只能像普通内存那样读,而不能像普通内存那样写。
三、链接脚本
为什么通过Nor启动和Nand启动打印不一样呢?我们来分析一下原因。
- Nor启动
当通过Nor启动时,0地址为Nor Flash的基地址,全局变量g_Char被放在0x800的地方,也属于Nor Flash的范围,所以此时g_Char++会无效。
- Nand启动
当通过Nand启动时,0地址为SRAM(片内4k内存)的基地址,CPU上电后,硬件会将Nand Flash前4k内容全部拷贝到SRAM中,CPU开始从SRAM执行。而此时全局变量g_Char也被拷贝到SRAM中,所以此时g_Char++会有效。
为了解决Nor Flash里面的变量不能写的问题,我们需要把变量所在的数据段放在SDRAM中。 可是如果只是简单的修改Makefile,指定数据段为0x30000000,如下:
arm-linux-ld -Ttext 0 -Tdata 0x30000000 start.o uart.o main.o -o sdram.elf
编译出了的bin文件,大小高达700多M,这么大的黑洞区
,谁都难以忍受。
那如何才能将数据段在bin文件中,紧靠代码段放置,而在运行时,放到0x30000000的地方?
有以下两个办法:
- 第一个方法
把数据段的g_Char和代码段靠在一起;
烧写在Nor Flash上面;
运行时把g_char(全局变量)复制到SDRAM,即0x3000000位置(重定位);
- 第二个方法
让文件直接从0x30000000开始,全局变量在0x3......;
烧写Nor Flash上 0地址处;
运行会把整个代码段数据段(整个程序)从0地址复制到SDRAM的0x30000000(重定位);
上面两个方法的区别在于:前者只重定位了数据段,后者重定位了整个程序。
3.1、链接脚本的引入
如果想使用重定位,需要使用链接脚本 Using LD, the GNU linker
如果我们不使用链接脚本,bin文件中,代码存放的顺序,是按照链接顺序来存放的。
修改Makefile
all:
arm-linux-gcc -c -o init.o init.c
arm-linux-gcc -c -o uart.o uart.c
arm-linux-gcc -c -o main.o main.c
arm-linux-gcc -c -o start.o start.S
#arm-linux-ld -Ttext 0 -Tdata 0x800 start.o uart.o main.o -o sdram.elf
arm-linux-ld -T sdram.lds start.o init.o uart.o main.o -o sdram.elf
arm-linux-objcopy -O binary -S sdram.elf sdram.bin
arm-linux-objdump -D sdram.elf > sdram.dis
clean:
rm *.bin *.o *.elf *.dis
创建链接脚本sdram.lds:
SECTIONS {
.text 0 : { *(.text) }
.rodata : { *(.rodata) }
.data 0x30000000 : AT(0x800) { *(.data) }
.bss : { *(.bss) *(.COMMON)}
}
SECTIONS中,表示数据的组织形式:
1、
.text
表示存放代码段,0
表示将内容放入0地址,{*(.text)}
表示所有文件的代码段。所以这句话的意思是:把所有文件的代码段放到0地址。
2、第二句也类似,.rodata
表示存放只读数据段,紧跟在.text
内容后面,{ *(.rodata) }
将表示所有文件的只读数据段。
3、第三句,bin文件中0x800的位置存放所有文件的数据段,这些内容理论上应该放到0x30000000。
4、第三句,将所有文件的bss段和common段,放到bss段。
我们编译源码,查看反编译内容:
Disassembly of section .rodata:
000003a0 :
3a0: Address 0x000003a0 is out of bounds.
Disassembly of section .data:
30000000 :
30000000: Address 0x30000000 is out of bounds.
Disassembly of section .bss:
30000004 :
30000004: 00000000 andeq r0, r0, r0
30000008 :
30000008: 00000000 andeq r0, r0, r0
可以看到,数据段中的g_Char变量,地址为0x30000000,可是我们bin文件只有3k大小。也就是说:
这些代码虽然存放在bin文件的0x800的位置,但是,后面这些代码会被在0x30000000位置进行执行。
那如何将这些存放在0x800的代码"搬运"到0x30000000呢?
这就需要我们手动去完成,修改start.S:
.text
.global _start
_start:
/* 省略以下代码:
1、关闭看门狗
2、设置时钟
3、设置栈指针
*/
bl sdram_init
/* 重定位data段,只copy 32位(4字节) */
mov r1, #0x800
ldr r0, [r1]
mov r1, #0x30000000
str r0, [r1]
bl main
halt:
b halt
由于需要从data段拷贝4字节到SDRAM,所以需要提前初始化SDRAM。
再次编译,然后烧写到Nor和Nand Flash中,程序都正常执行。
3.2、链接脚本优化
上个程序中,我们只重定位data段的4个字节,但是,当我们程序有多个全局变量,那就需要重定位多个字节:
main.c
#include "uart.h"
void delay(volatile int d)
{
while (d--);
}
char g_Char1 = 'A'; //定义一个全局变量
char g_temp1 = '-';
char g_temp2 = '-';
char g_Char2 = 'a';
char g_Char3 = '1'; //定义一个全局变量
// //由于只copy 4字节,所以g_Char3无效
const char g_Char = 'B'; //定义固定的全局变量
int g_A = 0;
int g_B;
int main(void)
{
uart0_init();
while(1)
{
putchar(g_Char1);
putchar(g_Char2);
putchar(g_Char3);
g_Char1++;
g_Char2++;
g_Char3++;
delay(1000000);
}
return 0;
}
此时,我们需要修改链接脚本,使得其更加通用。
sdram.lds
SECTIONS {
.text 0 : { *(.text) }
.rodata : { *(.rodata) }
.data 0x30000000 : AT(0x800)
{
data_load_addr = LOADADDR(.data);
data_start = . ;
*(.data)
data_end = . ;
}
.bss : { *(.bss) *(.COMMON)}
}
LOADADDR
宏,可以得到data段在bin文件的地址,即加载地址。
而data_start = .
,表示运行地址。
也就是说,我们只需将data_load_addr
的内容copy到data_start
上,拷贝长度为data_end - data_start
。
重新修改start.S重定位的内容:
.text
.global _start
_start:
/* 省略以下代码:
1、关闭看门狗
2、设置时钟
3、设置栈指针
*/
bl sdram_init
/* 重定位data段 */
ldr r1, =data_load_addr /* data段在bin文件中的地址,加载地址 */
ldr r2, =data_start /* data段在重定位地址,运行时的地址 */
ldr r3, =data_end /* data段结束地址 */
cpy:
ldrb r4, [r1]
strb r4, [r2]
add r1, r1, #1
add r2, r2, #1
cmp r2, r3
bne cpy
bl main
halt:
b halt
编译并烧写,运行后,程序输出预期结果。
3.3、链接脚本解析
链接脚本的通用格式如下:
SECTIONS {
...
secname start BLOCK(align) (NOLOAD) : AT ( ldadr )
{ contents } >region :phdr =fill
...
}
secname
,段名,这里可以随便取
start
,起始地址:运行时地址,重定位地址
ldadr
,加载地址,默认对于运行时地址。
contents
,内容:
可以直接指定某个文件(如
start.o
),把整个文件放在整个段中。
也可以指定某个段(如*(.text)
),把所有文件的这个段放到该段中。
或者start.o *(.text)
,先存放start.o整个文件,再将其余文件的代码段存入。
- 1、链接到elf文件,包含地址信息(如:加载地址)。
- 2、使用加载器,把elf文件读入内存
对应裸板,是JTAG调试工具
对于app,加载器也是app
- 3、运行
- 4、如果load addr != runtime addr,程序本身要重定位。
四、清除BSS段
BSS段:用来存放为无初值或初值为0的全局变量。
上节中,我们重定位了数据段,但没有清除BSS段。这会导致我们访问这些变量时,会得到脏数据,所以我们需要手动清除BSS段。
注意:bss段并不会保存在bin文件和elf文件中,因为这样做是毫无意义的。
sdram.lds
SECTIONS {
.text 0 : { *(.text) }
.rodata : { *(.rodata) }
.data 0x30000000 : AT(0x800)
{
data_load_addr = LOADADDR(.data);
. = ALIGN(4);
data_start = . ;
*(.data)
data_end = . ;
}
. = ALIGN(4);
bss_start = .;
.bss : { *(.bss) *(.COMMON)}
bss_end = .;
}
start.S
.text
.global _start
_start:
/* 省略以下代码:
1、关闭看门狗
2、设置时钟
3、设置栈指针
*/
bl sdram_init
/* 重定位data段 */
ldr r1, =data_load_addr /* data段在bin文件中的地址,加载地址 */
ldr r2, =data_start /* data段在重定位地址,运行时的地址 */
ldr r3, =data_end /* data段结束地址 */
cpy:
ldr r4, [r1]
str r4, [r2]
add r1, r1, #4
add r2, r2, #4
cmp r2, r3
ble cpy
/* 清除BSS段 */
ldr r1, =bss_start
ldr r2, =bss_end
mov r3, #0
clean:
str r3, [r1]
add r1, r1, #4
cmp r1, r2
ble clean
bl main
halt:
b halt
这里我们做了以下优化:
- 1、重定位和清除bss段的代码中,将原先每次操作1字节,改为每次操作4字节。
- 2、做了4字节对齐操作。
假如我们不做4字节对齐操作,那么在清除bss段时,由于假如清除的地址不是4字节的整数倍,CPU会向下取整,可能会清除别的段中的数据。
所以在链接脚本中,我们添加了以下代码,用于段的4字节对齐。
. = ALIGN(4);
打印bss段中的变量,看看清除是否有效:
uart.c
//......
void printHex(unsigned int val)
{
int i;
unsigned char arr[8];
/* 先取每一位值 */
for (i = 0; i < 8; i++)
{
arr[i] = val & 0xf;
val >>= 4;
}
/* 打印 */
puts("0x");
for (i = 7; i >=0; i--)
{
if (arr[i] <= 9)
{
putchar(arr[i] + '0');
}
else
{
putchar(arr[i] - 10 + 'A');
}
}
}
main.c
#include "uart.h"
void delay(volatile int d)
{
while (d--);
}
char g_Char1 = 'A'; //定义一个全局变量
char g_temp1 = '-';
char g_temp2 = '-';
char g_Char2 = 'a';
char g_Char3 = '1'; //定义一个全局变量
//由于只copy 4字节,所以g_Char3无效
const char g_Char = 'B'; //定义固定的全局变量
int g_A = 0;
int g_B;
int main(void)
{
uart0_init();
puts("\n\rg_A = ");
printHex(g_A);
puts("\n\r");
while(1)
{
putchar(g_Char1);
putchar(g_Char2);
putchar(g_Char3);
g_Char1++;
g_Char2++;
g_Char3++;
delay(1000000);
}
return 0;
}
运行后,结果与预期一致:g_A = 0x00000000
。
五、代码重定位与位置无关码
之前也介绍过,解决黑洞区有2种方法:
1、只重定位数据段。
2、重定位整个程序。
在前面章节中,我们都是使用的第一种方法,但第二种方法其实对应Linux开发板来说,更加适用,原因如下:
1、Linux不同于单片机,它对内存大小没那么高的要求。
2、第一种方式只适用于能够运行程序的Flash,假如从Nand Flash、SD卡加载运行,就只能用第二种方式,将程序拷贝到内存中运行。
3、JTAG调试工具只支持第二种链接脚本,不支持第一种分体的链接脚本。
5.1、重定位整个程序
sdram.lds
SECTIONS
{
. = 0x30000000;
. = ALIGN(4);
.text :
{
*(.text)
}
. = ALIGN(4);
.rodata : { *(.rodata) }
. = ALIGN(4);
.data : { *(.data) }
. = ALIGN(4);
__bss_start = .;
.bss : { *(.bss) *(.COMMON) }
_end = .;
}
这里直接指定程序开始的运行地址为0x30000000。
start.S
.text
.global _start
_start:
/* 省略以下代码:
1、关闭看门狗
2、设置时钟
3、设置栈指针
*/
bl sdram_init
/* 重定位text, rodata, data段 */
mov r1, #0
ldr r2, =_start /* 第1条指令运行时的地址 */
ldr r3, =__bss_start /* bss段的起始地址 */
cpy:
ldr r4, [r1]
str r4, [r2]
add r1, r1, #4
add r2, r2, #4
cmp r2, r3
ble cpy
/* 清除BSS段 */
ldr r1, =__bss_start
ldr r2, =_end
mov r3, #0
clean:
str r3, [r1]
add r1, r1, #4
cmp r1, r2
ble clean
bl main
halt:
b halt
上述的代码在运行时,前面部分会将整个程序复制到SDRAM,并清除BSS段。但是在编译脚本里,整个程序的运行地址被指定到SDRAM上,这就引出一个问题:
为什么当我们通过Nor启动或Nand启动时,运行地址为SDRAM的程序,也能在Nor Flash或SRAM上正常运行?
此时CPU依旧运行在原先的内存芯片上,并未在SDRAM上执行。也就是说:
- 当Nor启动时,CPU在Nor上执行代码,然后去SDRAM上读写全局变量。
- 当Nand启动时,CPU在SRAM上执行代码,然后去SDRAM上读写全局变量。
这就意味着,前面这部分的代码,必须与位置无关。查看反编译dis文件:
里面有一行代码eb000018 bl 300000c4
,可是,此时SDRAM并未初始化,访问300000c4
肯定会出问题,这是怎么回事呢?
原来,这句代码其实并没有直接跳到300000c4
,而是跳到pc + offset
位置,即该条指令与位置无关,无论放到哪个位置执行,都能正确被执行。
我们可以修改链接脚本的运行地址为0x32000000
,这条指令的变成eb000018 bl 320000c4
,机器码并没有改变,恰好验证我们的猜测。
在dis文件中,
bl 0x3xxxxxxx
只是起方便查看的作用,而不是真正的跳转到这个地址上。
那怎么写位置无关的程序?
- 1、调用程序时,使用B/BL相对跳转指令。
- 2、重定位之前,不可使用绝对地址,比如:
不可访问全局变量、静态变量。
不可访问有初始值数组(初始化值存放到rodata中,使用绝对地址来访问) - 3、重定位之后,使用
ldr pc, =xxx
,跳转到Runtime Add,比如:
ldr pc, =main
所以现在,我们需要让CPU在SDRAM上执行,而不是原先的内存芯片上。这是我们只需要将相对跳转指令:
bl main // bl相对跳转,程序仍在NOR/sram执行
修改为绝对跳转指令:
ldr pc, =main // 绝对跳转,跳到SDRAM
编译烧写运行,发现程序运行速度明显变快。
六、C语言实现重定位和清除BSS段
start.S
.text
.global _start
_start:
/* 省略以下代码:
1、关闭看门狗
2、设置时钟
3、设置栈指针
*/
bl sdram_init
bl copy2sdram
bl clean_bss
bl uart0_init
bl main /*bl相对跳转,程序仍在NOR/sram执行*/
//ldr pc, =main /*绝对跳转,跳到SDRAM*/
halt:
b halt
init.c
void copy2sdram()
{
extern int _start, __bss_start;
unsigned int* src = (unsigned int*)0;
unsigned int* start = (unsigned int*)&_start;
unsigned int* end = (unsigned int*)&__bss_start;
while(start < end)
{
*start++ = *src++;
}
}
void clean_bss()
{
extern int __bss_start, _end;
unsigned int* start = (unsigned int*)&__bss_start;
unsigned int* end = (unsigned int*)&_end;
while(start < end)
{
*start++ = 0;
}
}
这里,我们在c语言中使调用了lds链接脚本和start.S启动文件中的变量。这些变量,被保存在了symbol table符号表中。
- 对于c文件:在编译时,symbol table里面存放了c变量的名字(name) 。在链接时,确定变量的地址。
- 对于lds文件:为了在C程序中使用lds中的值,借助了symbol table保存lds的变量的值,同样是在编译时,在symbol table里面存放了lds中变量的名字(name),在链接时确定变量的值(注意:不是地址)。
这些变量,在汇编代码中可以直接使用,而在c语言里,需要通过extern
关键字引入,然后取址获得。
main.c
#include "uart.h"
void delay(volatile int d)
{
while (d--);
}
char g_Char1 = 'A'; //定义一个全局变量
char g_temp1 = '-';
char g_temp2 = '-';
char g_Char2 = 'a';
char g_Char3 = '1'; //定义一个全局变量
//由于只copy 4字节,所以g_Char3无效
const char g_Char = 'B'; //定义固定的全局变量
int g_A = 0;
int g_B;
int main(void)
{
puts("\n\rg_A = ");
printHex(g_A);
puts("\n\r");
while(1)
{
putchar(g_Char1);
putchar(g_Char2);
putchar(g_Char3);
g_Char1++;
g_Char2++;
g_Char3++;
delay(1000000);
}
return 0;
}
编译烧写运行 ,一切正常。