目录
参考链接:https://blog.csdn.net/Sanjay_Wu/article/details/88723448
资料来源:韦东山嵌入式linux
段的概念_重定位的引入
2440访问内存有以下几种方式
1.由内存控制器直接访问SDRAM
2.由内存控制器直接访问NorFlash,此时SRAM的首地址为0x40000000,程序从Nor中运行,以4字节的方式
3.内存控制器直接访问SRAM
4.内存控制器通过NandFlash控制器访问NandFlash,此时SRAM的首地址为0,首先将前4K的程序拷贝到SRAM运行
重定位的概念
1、程序烧写在Nand Flash上的情况
(1)Nand Flash程序的启动流程:当我程序烧写在Nand Flash的时候,CPU是无法从Nand Flash中取代码执行的,开机上电的时候Nand启动硬件会自动把Nand Flash前4K复制到CPU的内部SRAM里面,然后CPU从内部SRAM的0地址开心执行代码。
(2)存在问题:如果当我们的程序大于4K的时候,内部SRAM就不够存放Nand Flash的程序了,因为SRAM的大小只有4K。
(3)解决程序大于4K的方法:在程序设计时,设计程序的前4K代码实现将整个程序读出来放到外部的SDRAM。
2、程序烧写在Nor Flash上的情况
(1)Nor Flash程序的启动流程:Nor Flash的基地址为0,片内RAM地址为0x4000 0000;CPU读出Nor上第1个指令(前4字节),执行;CPU继续读出其它指令执行。
(2)存在问题:Nor Flash可以像内存直接读,但是不能直接写。当程序中含有需要写的全局变量或静态变量时,假如是在Nand Flash可以正常操作,如果是在Nor Flash,修改是无效。
(3)解决写无效的方法:我们把全局变量和静态变量这些数据段或者整个程序重定位放到外部SDRAM上,在SDRAM上修改全局变量和静态变量的值。
段的概念
一个程序包含多个段,分别是代码段,数据段(初值不为0的全局变量),rodata段(const全局变量),bss段(初值为0,或者无初值的全局变量),commen段(注释)。其中bss段与commen段不保存在bin中。
示例:Nor启动和Nand下修改全局变量的值:
main:
#include "s3c2440_soc.h"
#include "uart.h"
#include "init.h"
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++; /* nor启动时, 此代码无效 */
delay(1000000);
}
return 0;
}
makefile:
all:
arm-linux-gcc -c -o led.o led.c
arm-linux-gcc -c -o uart.o uart.c
arm-linux-gcc -c -o init.o init.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 led.o uart.o init.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
arm-linux-ld -Ttext 0 -Tdata 0x800 start.o led.o uart.o init.o main.o -o sdram.elf :将代码段放在0地址,数据段放在0x800地址
生成bin文件:
bin文件大小2049,对应0x801,所以bin文件中,地址0x800只包含了char g_Char = 'A';如图:
可以发现,bss段,comment段是不保存在bin中的(int g_A = 0;int g_B;),而rodata段紧随代码段保存(const char g_Char2 = 'B';),数据段(char g_Char = 'A';)保存在了makefile指定的0x800
运行结果:
当运行nand启动时,程序可以输出ABCDE... ,当运行Nor启动时,程序输出AAAAAA
原因:
Nor启动时,SRAM的地址是0x40000000,程序直接从nor中运行,nor中0x800存放g_char,Nor可以读,但是不能简单的写,所以g_char++并没有把'A'+1,则输出全是AAA
Nand启动时,程序从Nard中将4K程序拷贝到SRAM中运行,SRAM的地址为0,这时g_char存放在SRAM的0x800中,可以直接修改。
链接脚本的引入与简单测试
如何解决呢?是否可以让数据段放在SDRAM中,代码段放在Nor中呢?答案是否定的,如果这样做了,可以发现,在bin文件中,代码段与数据段之间有巨大的空间浪费,且bin文件达到800M,这样是不行的。所以,这里引入重定位。
1.变量的重定位
将bin文件(包括代码段,数据段)烧写到Nor中,运行时将数据段拷贝到SDRAM中,也就是地址0x3000000中。
2.代码重定位
将bin文件(包含代码段,数据段)烧写到Nor中,运行时,将代码段和数据段都复制到SDRAM中。
变量的重定位
实现方法
1.修改makefile,添加脚本:
all:
arm-linux-gcc -c -o led.o led.c
arm-linux-gcc -c -o uart.o uart.c
arm-linux-gcc -c -o init.o init.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 led.o uart.o init.o main.o -o sdram.elf
#添加链脚本
arm-linux-ld -T sdram.lds start.o led.o uart.o init.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
新建脚本
SECTIONS {
.text 0 : { *(.text) }
.rodata : { *(.rodata) }
.data 0x30000000 : AT(0x800) { *(.data) }
.bss : { *(.bss) *(.COMMON) }
}
首先代码段text,从0地址开始,然后是rodata段,然后是data段,data段从0x800重定位到0x30000000,再然后是bss段与commen段
main函数开始前,初始化SDRAM,在启动文件Start.S中添加重定位:
bl sdram_init
mov r1,#0x800
ldr r0,[r1] //读取0x800的值到r0
mov r1,#0x30000000
str r0,[r1] //将r0的值写入0x30000000
但是这样的重定位并不智能,需要手写地址,过于麻烦,所以可以自动重定位地址:
修改sdram.lds
SECTIONS {
.text 0 : { *(.text) }
.rodata : { *(.rodata) }
.data 0x30000000 : AT(0x800)
{
data_load_addr = LOADADDR(.data); #0x800
data_start = . ; #0x30000000
*(.data)
data_end = . ;
}
.bss : { *(.bss) *(.COMMON) }
}
data_load_addr表示bin文件中数据加载地址
data_start表示重定位地址
data_end表示段结束地址
修改Start.s
在sdram_init与main中添加:
/* 重定位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 /*地址+1*/
add r2, r2, #1
cmp r2, r3 /*比较r2与r3*/
bne cpy /*如果不相等,跳到cpy*/
链接脚本的解析
脚本格式:
SECTIONS {
...
secname start BLOCK(align)(NOLOAD) : AT (ldadr)
{ contents } >region:phdr =fill
...
}
secname:段名:
start:起始地址:运行时地址runtime addr,重定位地址 relocate addr
AT(ldadr) Load Addr:加载地址 不写时,LoadAddr = runtime addr 如果没有加AT 它的的加载地址就等于链接时的起始地址。
使用脚本时,通过makefile:
arm-linux-ld -T sdram.lds start.o led.o uart.o init.o main.o -o sdram.elf
elf格式程序含有地址信息。再将elf格式的文件生成bin文件 bin文件里面不含有地址信息。
arm-linux-objcopy -O binary -- sdram.elf sdram.bin
对于elf格式的程序,
1.链接得到elf文件,含有地址信息(LoadAddr)
2.使用加载器将elf程序读入内存(读到LoadAddr)调试工具。对于APP 加载器也是APP。
3.运行程序
4.如果LoadAddr != RuntimeAddr 程序本身要重定位。
核心:程序运行时,应为与runtimeAddr
而对于裸板程序,
1.blf->bin
2.硬件启动机制
3.如果bin文件所在位置!=runtimeAddr,程序本身实现重定位
SECTIONS {
.text 0 : { *(.text) }
.rodata : { *(.rodata) }
.data 0x30000000 : AT(0x800)
{
data_load_addr = LOADADDR(.data); #0x800
data_start = . ; #0x30000000
*(.data)
data_end = . ;
}
.bss : { *(.bss) *(.COMMON) }
}
上面代码中,data段loadAddr=0x800,RuntimeAddr=0x30000000,.bss段不在bin文件中,runtimeAdde跟随data,在0x3xxxxxxx。bss段中包含未定义的全局变量或者初始值为0的全局变量,在程序运行时,需要把bss段对应的空间位置清0.如果未清0,bss段的变量会输出乱码,如图:
修改sdram.lds 添加bss代码段的标志
SECTIONS {
.text 0 : { *(.text) }
.rodata : { *(.rodata) }
.data 0x30000000 : AT(0x800)
{
data_load_addr = LOADADDR(.data);
data_start = . ;
*(.data)
data_end = . ;
}
bss_start = .;
.bss : { *(.bss) *(.COMMON) }
bss_end = .;
}
在启动文件中添加清0代码
/*清除BSS段*/
ldr r1,=bss_start
ldr r2,=bss_end
mov r3,#0
clean:
strb r3,[r1]
add r1,r1,#1
cmp r1,r2
bne clean
运行结果:
总结:bss段可能包含数百上千个全局变量,如果全部放入bin,会占据很大空间,所以,只需要在程序运行前,将bss段放置在runtimeAddr中,便可省区大量的空间。
脚本的改进
上述脚本中,对数据的复制cpy都是以字节的单位操作,可以改为以字,4字节的单位操作
start.s中:
cpy:
ldr r4, [r1]
str r4, [r2]
add r1, r1, #4
add r2, r2, #4
cmp r2, r3
ble cpy //ble小于等于
/*清除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
同时,在脚本文件中进行地址的4字节对齐:
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 = .;
}
代码重定位
实现方法
操作步骤:
1.链接脚本指定RuntimeAdd为SDRAM地址
2.重定位之前的代码为与运行位置无关,为位置无关码
修改链接脚本,代码runtimeAddr为0x30000000
SECTIONS
{
. = 0x30000000;
. = ALIGN(4);
.text :
{
*(.text)
}
. = ALIGN(4);
.rodata : { *(.rodata) }
. = ALIGN(4);
.data : { *(.data) }
. = ALIGN(4);
__bss_start = .;
.bss : { *(.bss) *(.COMMON) }
_end = .;
}
修改启动文件Start.s中的重定位相关代码:
/* 重定位text data rodata段 */
mov r1, #0 /*从0地址开始复制代码段 */
ldr r2, =_start /* 启动地址,地址在Start.s中确认 */
ldr r3, =__bss_start /* data段结束地址 */
cpy:
ldr r4, [r1]
str r4, [r2]
add r1, r1, #4
add r2, r2, #4
cmp r2, r3
ble cpy //ble小于等于
/*清除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
ldr pc, =main
将代码Nor中0地址的代码复制到0x30000000,同时在重定位后使用ldr pc, =main 将pc重定位到0x30000000
注:在对应的反汇编里,重定位前,程序跳转使用的是相对跳转
3000005c: eb000105 bl 30000478 <sdram_init>
30000478并不是指一定跳转到30000478地址,这是一个相对跳转,程序运行开始于30000000,而sdram_init存放于离程序运行开始478的位置,这个偏移量由系统算得。
而重定位后,需要使用绝对跳转指令将pc指向当前代码需要运行的runtimeAdd:
ldr pc, =main
总结:怎么写位置无关的程序:
使用位置无关码! 不使用绝对地址! 最根本的办法是看反汇编
a. 调用程序时使用B/BL相对跳转指令
b. 重定位之前, 不可使用绝对地址,比如:
不可访问全局变量/静态变量
不可访问有初始值的数组(因为初始值放在rodata里,使用绝对地址来访问)
c. 重定位之后, 使用绝对跳转命令跳到Runtime Addr,比如:
ldr pc, =main
C语言实现
a. 在C函数中声明改变量为extern类型, 比如:
extern int abc;
b. 使用时, 要取址, 比如:
int *p = &abc; // p的值即为lds文件中abc的值
在C程序中获得需要复制的代码段地址。
首先在链接脚本中添加地址变量:
SECTIONS
{
. = 0x30000000;
__code_start = .;
. = ALIGN(4);
.text :
{
*(.text)
}
. = ALIGN(4);
.rodata : { *(.rodata) }
. = ALIGN(4);
.data : { *(.data) }
. = ALIGN(4);
__bss_start = .;
.bss : { *(.bss) *(.COMMON) }
_end = .;
}
在启动文件中添加C程序函数:
bl sdram_init
//bl sdram_init2 /* 用到有初始值的数组, 不是位置无关码 */
/* 重定位text, rodata, data段整个程序 */
bl copy2sdram
/* 清除BSS段 */
bl clean_bss
//bl main /* 使用BL命令相对跳转, 程序仍然在NOR/sram执行 */
ldr pc, =main /* 绝对跳转, 跳到SDRAM */
在sdram_int.c文件中实现函数:
void copy2sdram(void)
{
/* 要从lds文件中获得 __code_start, __bss_start
* 然后从0地址把数据复制到__code_start
*/
extern int __code_start, __bss_start;
volatile unsigned int *dest = (volatile unsigned int *)&__code_start;
volatile unsigned int *end = (volatile unsigned int *)&__bss_start;
volatile unsigned int *src = (volatile unsigned int *)0;
while (dest < end)
{
*dest++ = *src++;
}
}
void clean_bss(void)
{
/* 要从lds文件中获得 __bss_start, _end
*/
extern int _end, __bss_start;
volatile unsigned int *start = (volatile unsigned int *)&__bss_start;
volatile unsigned int *end = (volatile unsigned int *)&_end;
while (start <= end)
{
*start++ = 0;
}
}