s3c2440裸机编程-代码重定位和清bss

1 引入代码重定位#

s3c2440的cpu默认是从0地址开始取指令执行,当从nor启动时,0地址对应nor, nor可以像内存一样读,但不能像内存一样写,前面s3c2440裸机编程-内存控制器 讲过,写入norflash要进行按照spec进行命令表写入。因此我们能够从nor上取指令执行。

当nand启动的时候,我们nand中的前4K指令会变自动加载到sram中去,这时的0地址对应sram。那么我们的程序如果大于4K,要从nand启动,sram只拷贝了nand中的前4K代码,那么如何解决这个问题呢?

就需要重定位代码到dram(ddr)中去,dram的容量较大,又可以直接被cpu访问。

1.1 程序地址空间#

我们知道,程序包含:

1
2
3
4
5
代码段(.text)
数据段(.data):存放初始值不为0的全局变量/静态变量
rodata段(.rodata):const修饰的全局变量或静态变量
bss段(.bss):存放初始值为0或者未初始化的全局变量/静态变量
commen段(.commen):注释

下面展开一个实验引入为什么要代码重定位。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#include "s3c2440_soc.h"
#include "uart.h"
#include "init.h"
int g_Char = 'A'; //.data
int g_CharB = 'B'; //.data
int g_CharC = 'C'; //.data
int g_CharD = 'D'; //.data
const int g_roval = 'C'; //.rodata
int g_A = 0; //bss
int g_B; //bss
int main(void) {
uart0_init();
while (1) {
putchar(g_Char);
g_Char++; /* nor启动时, 此代码无效,由于nor启动,nor上不可写 */
delay(1000000);
}
return 0;
}

我们将上面的代码编译出的裸机程序分别烧录到nand和nor flash,看看结果:

1
2
1. 烧录到nor flash, 程序一直输出`AAAA`。
2. 烧录到nand flash,程序无任何输出。

我们发现nor启动时, 对全局变量g_char++无效, nand启动程序无任何输出。我们对程序进行反汇编处理:

可以看到.text段是从0地址开始的,证明cpu的确从0地址取指令进行译码、执行。
当从nor启动时,0地址对应nor;当从nand启动时,0地址对应sram,所以无论从nand还是从nor启动cpu都能取指令执行。

再进一步分析反汇编:

可以看到.data段的起始地址是0x8474(即g_Char变量的地址为0x8474)。

1
2
1. 当把程序烧录进nor,  .data段在nor上的某一段区域, 由于nor能像内存一样读,但不能像内存一样直接写,因此对'g_Char'修改无效。
2. 当把程序烧录进nand, .data段在nand的某一区域,nand启动时硬件会自动把nand上的前4K数据copy到SRAM,然后cpu从sram取指令执行。但是.data段的起始地址0x8474>0x1000,超过了4K, cpu没法把.data段也copy到SRAM,所以当访问'g_Char'时,发生了异常(abt数据访问终止,这个异常后面有在下一节“异常与中断”里面专门讲解),因此程序卡死。

再仔细看看反汇编,发现.rodata段和.text段是连续的,但是.rodata段和.data段中间有一段”空洞”。用图形表示更形象,bin文件的内容分布如下所示:


那么我们怎么去掉空洞,让.data段了紧接着.rodata段呢?

1
2
1. 链接脚本(后面1.2有专门讲)
2. 直接在编译的时候用 "-Tdata 0x800",这样指定.data段基地址为0x800,这样nand启动时.data就能自动copy到SRAM了。

我们现在使用-Tdata 0x800编译出裸机程序,对应反汇编如下:

这时我们烧录程序到nand,从nand启动,发现能输出ABCDEFG了,这就对了,因为.data段数据从nand自动拷贝到了sram。

有人说为什么不吧.data段指向到dram呢,这样无论时nor启动还是nand启动不就都能对全局变量写了?
当然这个没错,我做了这个尝试,编译时用-Tdata 0x30000000, 发现编译出来的bin文件有800多M,为什么有这么大呢?由于我们指定.data段存放在0x30000000(sdram的基地址),这时bin文件的内部结构如下所示:

这么大的bin文件根本无法烧录。通过上面的例子,现在总结下为什么要代码重定位:

1
2
1.nand启动,前4K代码被自动copy到sram,当程序大于4K的时候需要重定位代码到sdram。
2.nor启动, 全局变量在nor上,不能像内存一样直接写该全局变量,那么也需要重定位到sdram。

1.2 链接脚本#

1.2.1 重定位data段#

我们发现arm-linux-ld -Ttext 0 -Tdata 0x30000000这种方式编译出来的bin文件有800多M,这肯定是不行的。可以通过AT参数指定.data段在编译时的存放位置,我们发现这样指定太不方便了,而且不好确定要放在bin文件的哪个位置。这里就要引入链接脚本,它可以帮我们解决这个不必要的麻烦。

1.2.1.1 链接脚本格式#

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
SECTIONS {
. = 0x00000000; //表示当前地址为0
. = ALIGN(4); //设置当前位置让4字节对齐
.text :
{
cpu/arm920t/start.o (.text)
board/lyb2440/boot_init.o (.text)
*(.text)
} //表示.text段从0x4开始存放,其中可以手动调整代码段的位置,
//比如让start.o,boot_init.o中的函数放在最前面,然后存放剩余的代码段

. = ALIGN(4); //设置当前位置让4字节对齐
.rodata : { *(.rodata) } //从该位置开始存放所有的.rodata段

. = ALIGN(4); //设置当前位置让4字节对齐
.data : 0x30000000 : AT(0x800) { *(.data) } //从该位置开始存放所有的.data段 设置运行

__bss_start = .; //设置.bss段的起始位置
.bss : { *(.bss) } //从该位置开始存放所有的.bss段
_end = .;//设置.bss段的结束位置(也就是整个链接脚本的结束为止)
}

这是从uboot中裁剪过来的链接脚本,注释已经链接脚本的结构讲解的差不多了。这里.data段指定了程序的运行(链接)地址为sdram的base_addr(0x30000000),通过AT指定加载(在bin文件的存放)地址0x800

1.2.1.2 重定位data段例子#

对于nor启动时,我们可以直接从nor上取指令执行,所以可以只进行数据段的重定位(数据段需要写入),我们编写链接脚本sdram.lds如下所示:

1
2
3
4
5
6
SECTIONS {
.text 0 : { *(.text) }//所有文件的.text
.rodata : { *(.rodata) } //只读数据段
.data 0x30000000 : AT(0x800) { *(.data) } //放在0x800,但运行时在0x3000000
.bss : { *(.bss) *(.COMMON) }//所有文件的bss段,所有文件的.COMMON段
}

将程序烧录进nor flash,程序运行时会将.data拷贝到0x0x30000000也就是sdram中去。只重定位数据段的过程用下图更直观:

因此就可以对g_char进行写入了。Makefile如下:

1
2
3
4
5
6
7
8
9
10
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 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
arm-linux-objcopy -O binary -S sdram.elf sdram.bin
arm-linux-objdump -D sdram.elf > sdram.dis

修改start.s进行.data段的重定位。我们需要将以0x800为.data段基地址的整个数据段copy到0x30000000处:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
.text
.global _start
_start:
/* 关闭看门狗 */
/* 初始化时钟 */
/* 设置栈 */
/*初始化sdram*/
...
/* 重定位data段,把加载地址0x800(bin文件中在nor中)的数据段的内容重定位到sdram的baseaddr */
mov r1, #0x800
ldr r0, [r1]
mov r1, #0x30000000
str r0, [r1]

bl main
halt:
b halt

用几行简单的数据加载存储指令即可实现数据段的重定位,这里是用的相对跳转指令bl main,因为还没有重定位整个完整的代码,所以不能用ldr绝对跳转。前面的初始化时钟、sdram我就不写了,参考s3c2440裸机编程-时钟体系 , s3c2440裸机编程-内存控制器

缺点:
这里只是人为的对.data段写死了,那么当我有多个全局变量时,还要计算重定位的次数,而且我们也不知道有多少个全局变量,所以这重定位方式有缺陷。那么我们对这种重定位.data断的方法做一个改进,将链接脚本修改如下:

1
2
3
4
5
6
7
8
9
10
11
12
SECTIONS{
.text 0 : { *(.text) }
.rodata : { *(.rodata) }
.data 0x30000000 : AT(0x800)
{
data_load_addr = LOADADDR(.data); /* data段在bin文件中的地址, 加载地址 */
data_start = . ; /* data段在重定位地址, 运行时的地址 */
*(.data)
data_end = . ; /* data段结束地址 */
}
.bss : { *(.bss) *(.COMMON) }
}

链接脚本用一个变量data_load_addr指定了加载地址(data段在bin文件中的地址,即0x800),用变量data_start指定了运行地址(即为0x30000000),那么用data_end - data_start就是我们数据段的总长度。

对start.s重定位过程做出如下修改:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/* 重定位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] /*r2存入data_load_addr 0x400, 然后,r2,r1依次自加*/
add r1, r1, #1
add r2, r2, #1
cmp r2, r3
ble cpy

bl main
halt:
b halt

这里start.s中用到了链接脚本中的label地址。

1.2.2 重定位整个程序段#

由于我们的程序可能会大于SRAM或者nor的容量,那么就必须连代码段也一起进行重定位,这种重定位方式更好,在实际应用中也是用的这种方式去做的重定位。

1
2
3
4
5
6
7
8
9
10
11
12
13
SECTIONS {
. = 0x30000000;
. = ALIGN(4);
.text : { *(.text) }
. = ALIGN(4);
.rodata : { *(.rodata) }
. = ALIGN(4);
.data : { *(.data) }
. = ALIGN(4);
__bss_start = .;
.bss : { *(.bss) *(.COMMON) }
_end = .;
}

我们将代码段的地址设置为0x3000_0004,然后紧接着放.rodata段,然后再紧接着放.data段。这样我们的bin文件就不再有“空洞”了。再来看重定位代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
.text
.global _start
_start:
...
/* 重定位text, rodata, data段整个程序 */
mov r1, #0
ldr r2, =_start /* 第1条指令运行时的地址,也就是.text段的runtime addr,在这里是0x3000_0004*/
ldr r3, =__bss_start /* bss段的起始地址,也就是整个程序的结束地址 */
cpy:
ldrb r4, [r1]
strb r4, [r2]
add r1, r1, #1
add r2, r2, #1
cmp r2, r3
ble cpy

bl main
halt:
b halt

整个bin文件程序的长度(.text + .rodata + .data)为__bss_start - _start,那么我们是把bin文件从存储介质的0地址copy到程序的运行地址0x3000_0004,这样我们访问.data段时就是访问sdram中重定位后的数据段了。

1.2.3 重定位代码优化#

1.2.3.1 strb/ldrb替换成str/ldr#

前面重定位时,我们使用的是ldrb命令从的Nor Flash读取1字节数据,再用strb命令将1字节数据写到SDRAM里面。
我们开发板的Nor Flash是16位,SDRAM是32位。 假设现在需要复制16byte数据。

不同的读写指令 cpu读取nor的次数 cpu写入sdram的次数
ldrb、strb 16 16
ldr、str 8 4

可以看出我们更换读写指令后读写次数变少了,提升了cpu的访问效率。修改如下:

1
2
3
4
5
6
7
8
9
...
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
...

1.2.3.2 改成c代码重定位#

1
2
3
4
5
6
7
8
9
10
11
12
13
14
SECTIONS {
. = 0x30000000;
__code_start = .;
. = ALIGN(4);
.text : { *(.text) }
. = ALIGN(4);
.rodata : { *(.rodata) }
. = ALIGN(4);
.data : { *(.data) }
. = ALIGN(4);
__bss_start = .;
.bss : { *(.bss) *(.COMMON) }
_end = .;
}
1
2
3
4
5
6
7
8
9
10
11
12
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++; //从0地址依次copy到__code_start(代码段的运行地址)
}
}

在start.s中设置栈指针sp后,即可执行bl copy2sdram进行重定位代码。如何设置栈指针参考s3c2440裸机编程-时钟体系 有实现,重复代码我就不贴上来了。

2 清除bss#

2.1 bss段介绍#

bss段是什么?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
#include "s3c2440_soc.h"
#include "uart.h"

char g_Char = 'A'; //.data
char g_Char3 = 'a';
const char g_Char2 = 'B'; //.rodata
int g_A = 0; //bss
int g_B; //bss

int main(void) {
uart0_init();

puts("\n\rg_A = ");
printHex(g_A);
puts("\n\r");

while (1) {
putchar(g_Char);
g_Char++; /* nor启动时, 此代码无效 ,重定位到sdram的baseaddr后有效*/
putchar(g_Char3);
g_Char3++;
delay(1000000);
}
return 0;
}

把程序烧进去,然后打印g_A,但是发现g_A这个值并不是0,而是一个随机值。我们学习linux时知道全局变量g_A, g_B输出肯定是0,裸机输出不是0,为什么呢?

1
2
原因:程序执行汇编过程做完了重定位后把代码copy到了sdram上,然后sdram上紧接着的地址就是.bss的基地址了,这时候bss段的这块内存没有经过任何处理,所以是随机的。
那么我们重定位完代码后需要进行清除sdram上.bss段的数据,因为我们知道bss是未初始化和初始值为0的全局变量。

2.2 清bss#

1
2
3
4
5
6
7
8
9
10
11
12
13
SECTIONS {
. = 0x30000000;
. = ALIGN(4);
.text :{ *(.text) }
. = ALIGN(4);
.rodata : { *(.rodata) }
. = ALIGN(4);
.data : { *(.data) }
. = ALIGN(4);
__bss_start = .;
.bss : { *(.bss) *(.COMMON) }
_end = .;
}

清除bss段的代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
/* 清除BSS段 */
ldr r1, =__bss_start
ldr r2, =_end
mov r3, #0
clean:
strb r3, [r1]
add r1, r1, #1
cmp r1, r2
ble clean

bl main
halt:
b halt

我们把程序再烧进去,然后打印g_A,但是发现g_A的值是0了。本质上就是对重定位后的bss段数据清0。

2.3 清bss优化#

2.3.1 strb/ldrb替换成str/ldr#

1
2
3
4
5
6
7
8
9
10
11
    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

2.3.2 改成c代码清bss#

1
2
3
4
5
6
7
8
9
10
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语言代码中获取的是链接脚本中的变量的值,所以这里的用C语言改进重定位还是清bss都是要加取址符。

2.3.3 每个段地址4字节对齐#

前面为了加快重定位和清bss的速度,用到了ldr,str这样以4字节为单位进行读写,但是还可能导致一个问题,假设现在链接脚本没有进行用ALIGN(4)让不同的段以4字节对齐,那么就会出现访问错乱的情况。举个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#include "s3c2440_soc.h"
#include "uart.h"
#include "init.h"
char g_Char = 'A'; //.data
char g_Char3 = 'a';
const char g_Char2 = 'B'; //.rodata
int g_A = 0; //bss
int g_B; //bss

int main(void) {
uart0_init();

puts("\n\rg_A = ");
printHex(g_A);
puts("\n\r");
putchar(g_Char);
return 0;
}

将链接脚本中.data段和.bss之间的**ALIGN(4)**去掉。那么我们会发现程序执行的时候输出的g_A=0,为什么呢,我们明明初始化g_A=‘A’呀?

打开反汇编分析:

我们的.bss段紧接着.data段后面,可知在对bss段进行清除的时候,由于我们是以4字节为单位操作的,所以我们清除g_A的时候,连带g_Char,g_Char的值也一起清除了。

所以data段和数据段之间添加ALIGN(4)。修改后就会发现bss段的地址以0x30000248开始了,如下图:

3 位置无关码#

3.1 绝对跳转与相对跳转指令#

3.1.1 相对跳转#

使用b, bl跳转指令。

bl sdram_test指令进行分析,查看反汇编, 代码段的链接地址为0x3000,0000。


这里的bl 3000036c不是跳转到3000036c,这个时候sdram并未初始化,那么这个物理地址是无法访问的.

为了验证,我们做另一个实验,修改连接脚本sdram.lds, 链接地址改为0x3000,0800,编译查看反汇编:

可以看到现在变成了bl 300003ec,但两个的机器码e1a0c00d都是一样的,机器码一样,执行的内容肯定都是一样的。 因此这里并不是跳转到显示的地址,而是跳转到: pc + offset,这个由链接器决定。

假设程序从0x30000000执行,当前指令地址:0x3000005c ,那么就是跳到0x3000036c;如果程序从0运行,当前指令地址:0x5c 跳到:0x000003ec。
因此:跳转到某个地址并不是由bl指令所决定,而是由当前pc值和offset偏移量决定。反汇编显示这个值只是为了方便读代码。

结论: 反汇编文件里, B或BL 某个值,只是起到方便查看的作用,并不是真的跳转。

3.1.2 绝对跳转#

1
2
//bl main  /*bl相对跳转,程序仍在NOR/sram执行*/
ldr pc, =main/*绝对跳转,跳到SDRAM*/

3.1.3 相对跳转与决定跳转比较#

怎么写位置无关码?

1
2
3
4
使用相对跳转命令 b或bl;
重定位之前,不可使用绝对地址(因为你sdram还没初始化,没有重定位代码过去,跳转过去不就死机了),也不可访问有初始值的数组(因为初始值放在rodata里,使用绝对地址来访问);
重定位之后,使用ldr pc = xxx,跳转到/runtime地址;
写位置无关码,其实就是不使用绝对地址

因此,前面的重定位和清bss例子,程序使用bl命令相对跳转,程序仍在NOR/sram执行,要想让main函数在SDRAM执行,需要修改代码:

1
2
//bl main  /*bl相对跳转,程序仍在NOR/sram执行*/
ldr pc, =main/*绝对跳转,跳到SDRAM*/