目录
一、段的概念重定位引入
- S3C2440的CPU可以直接给SDRAM发送命令、给Nor Flash发送命令、给4K的片上SRAM发送命令,但是不能直接给Nand Flsh发送命令(有Nand Flash控制器)
- nor启动时,CPU认为0地址在Nor Flash上;nand启动时,硬件会自动把Nand Flsh前4K复制到SRAM,片内内存SRAM的基地址就变成了0x40000000,Nand启动时片内内存SRAM的基地址基地址是0
- 对于nor启动,可以像内存一样读,但不能像内存直接写,因此需要把全局变量和静态变量重定位放到SDRAM里;对于nand启动,如果程序大于4K,前4K的代码需要把整个程序读出来放到SDRAM(即代码重定位)
#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;
}
- 编译后查看sdram.dis文件 发现data数据段放在了0x00008474这个地址导致 程序太大
- 修改makefile,将数据段放在0x700的地方,十进制为2048
arm-linux-ld -Ttext 0 -Tdata 0x30000000 start.o led.o uart.o init.o main.o -o sdram.elf //这时我们的bin文件就变为2049
- 由于存在全局变量,nand启动正常运行显示ABCDE…,nor启动不能正常运行显示AAA…
- 程序里面有.text 代码段(指令)、.data 数据段(有初值的全局变量)、rodata只读数据段(const全局变量)、bss段 (初始值为0、无初始值的全局变量)commen 注释
- 其中bss段和commen 注释不保存在bin文件中
二、链接脚本的引入与测试
- 为了解决Nor Flash里面的变量不能写的问题,把变量所在的数据段放在SDRAM里面
- 修改Makefile
arm-linux-ld -Ttext 0 -Tdata 0x30000000 start.o led.o uart.o init.o main.o -o sdram.elf
- 编译出来的bin文件 从0地址 到 0x30000000地址 文件大小有700多MB,代码段和数据段直接有间隔,称之为黑洞
- 解决两个方法:重定位数据段,或者数据段和代码段都重定位
- 修改Makefile使用链接脚本sdram.lds指定
#arm-linux-ld -Ttext 0 -Tdata 0x30000000 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
- 链接脚本的语法:
SECTIONS {
...
secname start BLOCK(align) (NOLOAD) : AT ( ldadr )
{ contents } >region :phdr =fill
...
}
- 第一种方法(第二种方法在位置无关码段落解释)
bl sdram_init //先初始化sdram保证0x30000000可用
/* 重定位data段 */
mov r1, #0x700
ldr r0, [r1]
mov r1, #0x30000000
str r0, [r1]
bl main
链接脚本
SECTIONS {
.text 0 : { *(.text) }//所有文件的.text
.rodata : { *(.rodata) } //只读数据段
.data 0x30000000 : AT(0x700) { *(.data) } //放在0x700,但运行时在0x3000000
.bss : { *(.bss) *(.COMMON) }//所有文件的bss段,所有文件的.COMMON段
}
若有多个全局变量:修改start.S和链接脚本
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] //从r1读到r4
strb r4, [r2] //r4存放到r2
add r1, r1, #1 //r1+1
add r2, r2, #1 //r2+1
cmp r2, r3 //r2 r3比较
bne cpy //如果不等则继续拷贝
bl main
SECTIONS {
.text 0 : { *(.text) }
.rodata : { *(.rodata) }
.data 0x30000000 : AT(0x700)
{
data_load_addr = LOADADDR(.data);
data_start = . ;//等于当前位置
*(.data) //等于数据段的大小
data_end = . ;//等于当前位置
}
.bss : { *(.bss) *(.COMMON) }
}
三、链接脚本的解析与清除bss段
- 链接脚本的解析
secname :段名
start :起始地址:运行时的地址(runtime addr);重定位地址(relocate addr)
AT ( ldadr ) :可有可无(load addr:加载地址) 不写时LoadAddr = runtime addr
{ contents } 的内容:
start.o //内容为start.o文件
*(.text)所有的代码段文件
start.o *(.text)文件
- elf文件格式
1 链接得到elf文件,含有地址信息(load addr)
2 使用加载器
:: 2.1 对于裸板是JTAG调试工具
:: 2.2 对于APP,加载器也是APP 把elf文件解析读入内存的加载地址
3 运行程序
4 如果loadaddr != runtimeaddr程序本身要重定位
核心程序运行时应该位于 runtimeaddr(reloate addr)或者链接地址
- 清除bss段
/* 0xABCDEF12 */
void printHex(unsigned int val)
{
int i;
unsigned char arr[8];
/* 先取出每一位的值 */
for (i = 0; i < 8; i++)
{
arr[i] = val & 0xf;
val >>= 4; /* arr[0] = 2, arr[1] = 1, arr[2] = 0xF */
}
/* 打印 */
puts("0x");
for (i = 7; i >=0; i--)
{
if (arr[i] >= 0 && arr[i] <= 9)
putchar(arr[i] + '0');
else if(arr[i] >= 0xA && arr[i] <= 0xF)
putchar(arr[i] - 0xA + 'A');
}
}
#include "s3c2440_soc.h"
#include "uart.h"
#include "init.h"
char g_Char = 'A';
char g_Char3 = 'a';
const char g_Char2 = '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)
{
#if 0
puts("\n\rg_Char = ");
printHex(g_Char);
puts("\n\r");
puts("\n\rg_Char3 = ");
printHex(g_Char3);
puts("\n\r");
#endif
putchar(g_Char);
g_Char++;
putchar(g_Char3);
g_Char3++;
delay(1000000);
}
return 0;
}
-
上述代码,若没有清理bss段 g_A为莫名奇妙的值并不等于0,所以需要清理bss段
-
修改链接脚本和start.s
SECTIONS {
.text 0 : { *(.text) }
.rodata : { *(.rodata) }
.data 0x30000000 : AT(0x700)
{
data_load_addr = LOADADDR(.data);
data_start = . ;
*(.data)
data_end = . ;
}
bss_start = .; //bss开始地址是当前位置
.bss : { *(.bss) *(.COMMON) }
bss_end = .; //bss结束地址也是当前位置
}
/* 清除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
bl main
halt:
四、改进
- JZ2440上的Nor Flash是16位,SDRAM是32位
- 采用ldrb命令每次只能加载1byte,因此CPU需要发出16次命令,内存控制器每次收到命令后,访问硬件Nor Flash,因此需要访问硬件16次; 同理,访问SDRAM时,CPU需要执行strb 16次,内存控制器每次收到命令后,访问硬件SDRAM,也要16次,这样总共访问32次。
- 使用ldr从Nor Flash中读,ldr命令每次加载4字节数据,因此CPU只需执行4次,但由于Nor Flash是16位的,内存控制器每次收到CPU命令后,需要拆分成两次访问,因此需要访问硬件8次; 使用str写SDRAM,CPU只需执行4次,内存控制器每次收到命令后,直接硬件访问32位的SDRAM,因此这里只需要4次,这样总共访问只需要12次。在整个操作中,花费时间最长的就是硬件访问,改进后代码,减少了硬件访问的次数,极大的提高了效率。
- 修改start.S和链接脚本:
cpy:
ldr r4, [r1]
str r4, [r2]
add r1, r1, #4 //r1加4
add r2, r2, #4 //r2加4
cmp r2, r3 //如果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 //如果r1 =< r2则继续拷贝
ble clean
bl main
SECTIONS {
.text 0 : { *(.text) }
.rodata : { *(.rodata) }
.data 0x30000000 : AT(0x700)
{
data_load_addr = LOADADDR(.data);
. = ALIGN(4);
data_start = . ;
*(.data)
data_end = . ;
}
. = ALIGN(4);//让当前地址向4对齐
bss_start = .;
.bss : { *(.bss) *(.COMMON) }
bss_end = .;
}
- 链接脚本如果没有ALIGN(4)向4取整,编译烧写,发现启动后没有输出字符。修改主程序,尝试以整数格式输出字符,发现输出的数从0开始,全局变量被破坏了,解决的方法是:修改链接脚本让bss段,使用ALIGN(4)向4取整
- Uboot是裸机的集大成者,可以参考uboot链接脚本也是类似的
五、位置无关码
- 在重定位之前的代码与位置无关即位置无关码
- 第二种方法重定位整个代码
参考Uboot修改链接脚本:
SECTIONS
{
. = 0x30000000;
. = ALIGN(4);
.text :
{
*(.text)
}
. = ALIGN(4);
.rodata : { *(.rodata) }
. = ALIGN(4);
.data : { *(.data) }
. = ALIGN(4);
__bss_start = .;
.bss : { *(.bss) *(.COMMON) }
_end = .;
}
- 这个链接脚本,称为一体式链接脚本,对比前面的分体式链接脚本区别在于代码段和数据段的存放位置是否是分开的
- 例如现在的一体式链接脚本的代码段后面依次就是只读数据段、数据段、bss段,都是连续在一起的
- 分体式链接脚本则是代码段、只读数据段,中间相关很远之后才是数据段、bss段
- 更多的采用一体式链接脚本,原因如下:
1. 分体式链接脚本适合单片机,单片机自带有flash,不需要再将代码复制到内存占用空间。而我们的嵌入式系统内存非常大,没必要节省这点空间,并且有些嵌入式系统没有Nor Flash等可以直接运行代码的Flash,就需要从Nand Flash或者SD卡复制整个代码到内存;
2. JTAG等调试器一般只支持一体式链接脚本;
- 修改start.S
/* 重定位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
- 查看反汇编,其中 3000005c: eb000106 bl 30000478 <sdram_init>
这里的bl 30000478不是跳转到30000478,这个时候sdram并未初始化;跳转到某个地址并不是由bl指令所决定,而是由当前pc值决定,跳转到: pc + offset,这个由链接器决定。
跳转到某个地址并不是由bl指令所决定,而是由当前pc值决定。反汇编显示这个值只是为了方便读代码。
- 怎么写位置无关码
使用相对跳转命令 b或bl;
重定位之前,不可使用绝对地址,不可访问全局变量/静态变量,也不可访问有初始值的数组(因为初始值放在rodata里,使用绝对地址来访问)
重定位之后,使用ldr pc = xxx,跳转到/runtime地址
六、C实现定位清除bss利用lds的变量
- 修改start.S,利用r1 r2 r3来传参
/* 重定位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
-
C函数
void copy2sdram(volatile unsigned int *src, volatile unsigned int *dest, unsigned int len) /* src, dest, len */
{
unsigned int i = 0;
while (i < len)
{
*dest++ = *src++;
i += 4;
}
}
void clean_bss(volatile unsigned int *start, volatile unsigned int *end) /* start, end */
{
while (start <= end)
{
*start++ = 0;
}
}
- 利用lds脚本变量来传参
修改start.S
bl sdram_init
//bl sdram_init2 /* 用到有初始值的数组, 不是位置无关码 */
/* 重定位text, rodata, data段整个程序 */
bl copy2sdram
/* 清除BSS段 */
bl clean_bss
修改链接脚本
SECTIONS
{
. = 0x30000000;
__code_start = .; //定义__code_start地址位当前地址
. = ALIGN(4);
.text :
{
*(.text)
}
. = ALIGN(4);
.rodata : { *(.rodata) }
. = ALIGN(4);
.data : { *(.data) }
. = ALIGN(4);
__bss_start = .;
.bss : { *(.bss) *(.COMMON) }
_end = .;
}
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;
}
}
- C函数怎么使用lds文件总的变量abc
在C函数中声明改变量为extern外部变量类型,比如:extern int abc;
- 使用时,要取址,比如:
int *p = &abc;//p的只即为lds文件中abc的值
- 如果我们C程序只用到几个变量,完全没必要全部存储lds里面的所有变量,C程序是不保存lds中的变量的
- 对于万一要用到的变量,编译程序时,有一个symbol table符号表来保存lds常量
如何使用symbol table符号表
- 对于常规变量g_i,得到里面的值,使用&g_i得到addr
- 为了保持代码的一致,对于lds中的a1,使用&a1得到里面的值addr
结论:
- C程序中不保存lds文件中的变量,lds再大也不影响
- 借助symbol table保存lds的变量,使用时加上”&”得到它的值,链接脚本的变量要在C程序中声明为外部变量,任何类型都可以
想更详细了解参考韦东山老师https://blog.csdn.net/thisway_diy/article/details/79397066?spm=1001.2014.3001.5502