本文主要讲解代码重定位,以及清除bss段。
在开始本文内容之前,需要学习一下,链接脚本的编写。
参考文章:链接器介绍和链接脚本的编写
嵌入式Linux学习系列全部文章:嵌入式Linux学习—从裸机到应用教程大全
一、重定位引入
我们都知道S3C2440如下的内存模型
假如是Nand启动
上电后,S3C2440内部的Nand启动硬件会自动把Nand Flsh前4K复制到SRAM;
CPU从0地址运行SRAM;
如果程序大于4K,则前4K的代码需要把整个程序读出来放到SDRAM(即代码重定位)。
假如是Nor Flash启动
此时CPU认为的 0地址 在Nor Flash上面,片内内存SRAM的基地址就变成了0x40000000
由于Nor Flash特性:可以像内存一样读,但不能像内存直接写,因此需要把全局变量和静态变量重定位放到SDRAM里。
我们来看这样一段代码
文件名:sdram.c
#include "stdio.h"
char g_Char = 'A'; //定义一个全局变量
int sdram_init()
{
....../*略,sdram初始化*/
}
int main(void)
{
sdram_init();
while (1)
{
putchar(g_Char); /*输出g_Char*/
g_Char++;
delay(1000000);
}
return 0;
}
这段代码如果是Nand启动,就是正常的,如果是nor启动,就不正常,因为nor flash只能像sdram一样读,却不能正常写。
二、重定位
一个程序里面有
- .text 代码段
- .data 数据段
- rodata 只读数据段(const全局变量)
- bss段 (初始值为0,无初始值的全局变量)
- commen 注释
其中bss段和commen 注释不保存在bin文件中。
为了解决Nor Flash里面的变量不能写的问题,我们把变量所在的数据段放在SDRAM里面,看行不行。
修改Makefile 指定数据段为0x30000000 -Tdata 0x30000000:
arm-linux-ld -Ttext 0 -Tdata 0x30000000 start.o sdram.o -o sdram.elf
这样的话编译出来的bin文件 从0地址 到 0x30000000地址 文件大小有700多MB,代码段和数据段直接有间隔,称之为黑洞
解决黑洞有两个办法:
方法一:只重定位数据段
把数据段和代码段靠在一起;烧写在Nor Flash上面;运行时把全局变量复制到SDRAM,即0x3000000位置(重定位);
步骤:
修改Makefile,使用链接脚本sdram.lds指定。
arm-linux-ld -T sdram.lds start.o sdram.o -o sdram.elf
我们需要依次排列 代码段、只读数据段、数据段、.bss段、.common。
其中数据段放在0x700,但运行时在0x3000000:
SECTIONS {
.text 0 : { *(.text) }//所有文件的.text
. = ALIGN(4);
.rodata : { *(.rodata) } //只读数据段
. = ALIGN(4);
.data 0x30000000 : AT(0x700) { *(.data) } //放在0x700,但运行时在0x3000000
. = ALIGN(4);
.bss : { *(.bss) *(.COMMON) }//所有文件的bss段,所有文件的.COMMON段
}
重新编译后烧写bin文件,发现启动后显示乱码。
原因:
我们从0x30000000处获取g_Char,但在这之前,并没有在0x30000000处准备好数据(0x30000000处没有初始化)。
因此需要重定位数据段,将0x700的数据移动到0x30000000处,在start.S加入:
bl sdram_init
/* 重定位data段 */
mov r1, #0x700
ldr r0, [r1]
mov r1, #0x30000000
str r0, [r1]
bl main
上面的这种方法,只能复制0x700处的一位数据,不太通用,下面我们利用链接脚本中的符号,写一个更加通用的复制方法:
链接脚本修改如下:
SECTIONS {
.text 0 : { *(.text) }
. = ALIGN(4);
.rodata : { *(.rodata) }
. = ALIGN(4);
.data 0x30000000 : AT(0x700)
{
data_load_addr = LOADADDR(.data);
data_start = . ;//等于当前位置
*(.data) //等于数据段的大小
data_end = . ;//等于当前位置
}
. = ALIGN(4);
.bss : { *(.bss) *(.COMMON) }
}
data_load_addr = LOADADDR(.data);这句话的意思就是data_load_addr=0x700
data_start = . ;的意思就是data_start =0x30000000
data_end = . ;的意思是data_end =0x30000000+数据段大小
修改start.S
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 //r1加4
add r2, r2, #4 //r2加4
cmp r2, r3 //如果不等继续拷贝
ble cpy
bl main
方法二:重定位数据段和代码段
让文件直接从0x30000000开始,全局变量在0x3……;烧写Nor Flash上 0地址处;
运行会把整个代码段数据段(整个程序)从0地址复制到SDRAM的0x30000000(重定位);
先梳理下把整个程序复制到SDRAM需要哪些技术细节:
1. 把程序从Flash复制到运行地址,链接脚本中就要指定运行地址为SDRAM地址;
2. 编译链接生成的bin文件,需要在SDRAM地址上运行,但上电后却必须先在0地址运行,这就要求重定位之前的代码与位置无关(是位置无关码);
修改链接脚本:
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段
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
ldr pc, =main
halt:
b halt
将修改后的代码重新编译烧写在Nor Flash上,上电运行。
注意了,为什么用 ldr pc, =main来跳转至main?而不用bl main?
因为bl是相对跳转
B命令的本质是跳转到: pc + offset,PC的值是当前指令,offset是跳转目标位置到当前位置的距离
总结怎么写位置无关码
使用相对跳转命令 b或bl;
重定位之前,不可使用绝对地址,不可访问全局变量/静态变量,也不可访问有初始值的数组(因为初始值放在rodata里,使用绝对地址来访问);
重定位之后,使用ldr pc = xxx,跳转到runtime地址;
三、清除BSS段
bss段 (初始值为0,无初始值的全局变量)
bin文件/elf文件都不保存bss段 这些都是初始值为0 或者没有初始化的全局变量,不保存能缩小文件。
如果没有清除bss段操作,则初始值为0的全局变量的值不一定为0,所以需要程序去清除。
修改lds链接文件
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结束地址也是当前位置
修改start.s,清除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
现在的代码全局变量就是为0,如果初始值为0的全局变量很多,通过几行代码,就可以少几十个甚至上千个全局变量的存储空间。
四、完整代码
makefile
all:
arm-linux-gcc -c -o start.o start.S
arm-linux-gcc -c -o sdram.o sdram.c
arm-linux-gcc -c -o main.o main.c
arm-linux-ld -T sdram.lds start.o sdram.o main.o -o sdram.elf
arm-linux-objcopy -O binary -S sdram.elf sdram.bin
clean:
rm *.bin *.o *.elf
sdram.lds
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
.global _start
_start:
/* 关闭看门狗 */
ldr r0, =0x53000000
ldr r1, =0
str r1, [r0]
/* 设置MPLL, FCLK : HCLK : PCLK = 400m : 100m : 50m */
/* LOCKTIME(0x4C000000) = 0xFFFFFFFF */
ldr r0, =0x4C000000
ldr r1, =0xFFFFFFFF
str r1, [r0]
/* CLKDIVN(0x4C000014) = 0X5, tFCLK:tHCLK:tPCLK = 1:4:8 */
ldr r0, =0x4C000014
ldr r1, =0x5
str r1, [r0]
/* 设置CPU工作于异步模式 */
mrc p15,0,r0,c1,c0,0
orr r0,r0,#0xc0000000 //R1_nF:OR:R1_iA
mcr p15,0,r0,c1,c0,0
/* 设置MPLLCON(0x4C000004) = (92<<12)|(1<<4)|(1<<0)
* m = MDIV+8 = 92+8=100
* p = PDIV+2 = 1+2 = 3
* s = SDIV = 1
* FCLK = 2*m*Fin/(p*2^s) = 2*100*12/(3*2^1)=400M
*/
ldr r0, =0x4C000004
ldr r1, =(92<<12)|(1<<4)|(1<<0)
str r1, [r0]
/* 一旦设置PLL, 就会锁定lock time直到PLL输出稳定
* 然后CPU工作于新的频率FCLK
*/
ldr sp, =0x40000000+4096
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:
strb r3, [r1]
add r1, r1, #1
cmp r1, r2
bne clean
ldr pc, =main
halt:
b halt
sdram.c
#define BWSCON (*(volatile unsigned int *)0x48000000)
#define BANKCON6 (*(volatile unsigned int *)0x4800001C)
#define REFRESH (*(volatile unsigned int *)0x48000024)
#define BANKSIZE (*(volatile unsigned int *)0x48000028)
#define MRSRB6 (*(volatile unsigned int *)0x4800002C)
int sdram_init(void)
{
BWSCON = 0x22000000;
BANKCON6 = 0x18001;
REFRESH = 0x008404f5;
BANKSIZE = 0xb1;
MRSRB6 = 0x00000020;
}
main.c
#include <stdio.h>
#define GPFCON (*(volatile unsigned int *)0x56000050)
#define GPFDAT (*(volatile unsigned int *)0x56000054)
char g_char='A';
int g_int=0;
void delay(volatile int d)
{
while (d--);
}
int main(void)
{
int i=0;
while(i!=5)
{
if(g_char=='A')
{
GPFCON = 0x00000100;
GPFDAT = 0;
}
delay(200000);
if(g_int==0)
{
GPFDAT = 0xff;
}
delay(200000);
i++;
}
g_char='B';
while(1)
{
if(g_char=='B')
{
GPFDAT = 0;
}
}
return 0;
}
功能:验证全局变量g_char是否成功重定位,成功则灯亮
验证全局变量g_int是否成功被清0,成功则灯灭,
循环5次,验证g_char写入是否正常。
全部正常则:灯循环亮灭5次,然后长亮。