注:以下内容学习于韦东山老师arm裸机第一期视频教程
一.段的概念和重定位的引入
1.1 重定位的引入
2440框架图如下
CPU发出的地址可以直接到达SDRAM,SRAM,NOR但是无法直接到达NAND
因此我们的程序可以直接放在NOR,SDRAM直接运行,假设我们把程序烧录到NAND中,CPU无法直接从NAND取地址运行.
1.1.1 但是我们仍然可以设置为NAND启动,NAND启动时(SRAM的地址对应0,因此NAND启动时NOR无法访问):
前4K会被复制到SRAM,然后CPU从0地址运行,就对应SRAM
如果bin文件超过4K怎么办?
前面的4K需要将整个代码读出来放到SDRAM(这就是重定位)
1.1.2 NOR启动时,0地址对应NOR上面,SRAM的地址对应0x40000000
NOR可以向内存一样的读,但是不能像内存一样的写=>需要发出特定的指令才可以写.
mov r0, #0
ldr r1, [r0] /* 可以读 */
str r1, [r0] /* 无效 */
因此引入问题,当程序中含有需要写的变量(局部变量在栈中,栈指向SRAM,读写没有问题)
但是全局变量,静态变量是包含在bin文件中烧到NOR中,直接写修改变量无效
因此,我们需要将这些全局变量,静态变量重定位放在SDRAM中
示例码如下:
#include "my_printf.h"
#include "uart.h"
#include "sdram.h"
#include "SetTacc.h"
char g_cA = 'A';
int main()
{
char c;
Uart0Init();
SdramInit();
puts("");
while (1)
{
putchar(g_cA);
g_cA++; /* nor启动时代码无效 */
}
return 0;
}
分别烧写到NOR和NAND,烧些到NAND会打印出ABCD,NOR启动只会打印出AAA
NAND启动需要重定位-> 将全部代码重定位到SDRAM中
NOR启动需要重定位-> 将全局变量,静态变量重定位到SRAM中
1.2 段的概念
程序至少包含代码段和数据段,数据段中存放(全局变量)
在main.c中定义下面几个变量
char g_cA = 'A';
const g_cB = 'B';
int g_iA = 0;
int g_iB;
编译后查看反汇编文件,如下图
因此可以看出程序还包含bss段,rodata段,最后的common段表示注释段
总结,代码中的段:
代码段 .text
数据段 .data
只读数据段 .rodata
/* bss段和注释段不保存在bin文件中 */
bss段 .bss,未初始化或初始化为0的全局变量
注释段 .common
二.链接脚本的引入与简单测试
2.1 修改Makefile使得全局变量存放到SDRAM,0x3000000中
修改Makefile
objs = uart.o main.o start.o SetTacc.o my_printf.o lib1funcs.o sdram.o
A = test
all:$(objs)
arm-linux-ld -Ttext -Tdata 0x30000000 0 $^ -o $(A).elf
arm-linux-objcopy -O binary -S $(A).elf $(A).bin
arm-linux-objdump -D $(A).elf > $(A).dis
%.o:%.c
arm-linux-gcc -c -o $@ $<
%.o:%.S
arm-linux-gcc -c -o $@ $<
clean:
rm *.o *.elf *.bin
但是这么编译出来的bin文件有800多M,这是因为代码段和数据段之间有一个很大的间隔.
2.2 两种解决办法:
2.2.1
a.bin文件中,让全局变量和代码段在一起,链接在0地址
b.烧写bin文件在NOR的0地址
c.运行时前面的代码将全局变量复制到0x30000000处
2.2.2
a.让代码段的链接地址在0x30000000开始,全局变量在紧接着后面开始
b.烧写bin文件在NOR的0地址
c.运行时前面的代码将代码段和全局变量全部复制到0x30000000
2.3 链接脚本
修改Makefile,在链接时指定链接脚本
objs = uart.o main.o start.o SetTacc.o my_printf.o lib1funcs.o sdram.o
A = test
all:$(objs)
#arm-linux-ld -Ttext 0 -Tdata 0x800 $^ -o $(A).elf
arm-linux-ld -T sdram.lds $^ -o $(A).elf
arm-linux-objcopy -O binary -S $(A).elf $(A).bin
arm-linux-objdump -D $(A).elf > $(A).dis
%.o:%.c
arm-linux-gcc -c -o $@ $<
%.o:%.S
arm-linux-gcc -c -o $@ $<
clean:
rm *.o *.elf *.bin
/* 链接脚本如下 */
SECTIONS {
.text 0 : { *(.text) }
.rodata : { *(.rodata) }
.data 0x30000000 : AT(0x1000) { *(.data) }
.bss : { *(.bss) *(.COMMON) }
}
这样代码段会放在0地址,在0x800的地方放了全局变量,但是main函数中访问全局变量时是以0x30000000的地址来访问的
我们并没有设置0x30000000的内存数值是A,我们的代码中缺少了重定位.
修改代码Start.S,在执行main函数之前需要进行重定位data段,在前面还要初始化sdram
/* 重定位了1个字节,0x1000是我们看反汇编确定的地址,并不通用 */
mov r1, #0x1000
ldr r0, [r1]
mov r1, #0x30000000
str r0, [r1]
/* 我们需要得到通用的重定位的办法 */
修改sdram.lds如下
SECTIONS {
.text 0 : { *(.text) }
.rodata : { *(.rodata) }
.data 0x30000000 : AT(0x1000)
{
data_load_addr = LOADADDR(.data)
data_start = .;
*(.data)
data_end = .;
}
.bss : { *(.bss) *(.COMMON) }
}
修改Start.S重定位代码
ldr r1, =data_load_addr /* data段在bin文件中的地址,加载地址 */
ldr r2, =data_start /* 重定位地址,运行时的地址 */
ldr r3, =data_end /* data段的结束地址 */
str r0, [r1]
cpy:
ldrb r4, [r1]
strb r4, [r2]
add r1, r1, #1
add r2, r2, #1
cmp r2, r3
bne cpy
三.链接脚本的解析
链接脚本的格式
SECTIONS {
secname(段的名字,可以随便写) start->(起始地址,运行时的地址,重定位后的地址) AT(ldadr)->(可以写,可以不写,加载地址,如果不写加载地址等于重定位地址)
{contents}
}
contents,内容:
格式:a. start.o(指定整个文件)
b. *(.text)
c. start.o *(.text) (start.o放在最前面,然后是 剩下所有文件的text段)
对于elf格式文件
1.链接得到elf格式的文件,里面含有地址信息(例如加载地址)
2.使用加载器(对于裸板就是JTAG调试工具,对于应用程序加载器本身是一个应用程序)把elf文件解析,读入内存(读到加载地址)
3.运行程序
4.如果loadaddr不等于加载地址,程序本身需要重定位代码
以上面的例子为例,data段的运行地址在0x30000000,在取值是就回去0x300000000取值,但是指定了数据段在0x1000,因此需要将数据段拷贝到0x30000000去
核心: 程序运行时的地址应位于运行时地址(重定位后的地址)(链接地址)
对于bin文件
1.elf->bin
2.硬件机制启动
3.如果bin文件所在位置不等于运行时地址程序本身实现重定位
解析链接脚本
SECTIONS {
.text 0 : { *(.text) } /* 加载地址等于运行地址,所有文件的代码段排在前面,按照Makefile中文件的顺序来排,也可以在链接脚本中指定谁派在前面 */
.rodata : { *(.rodata) } /* 所有文件的只读数据段 */
.data 0x30000000 : AT(0x1000) /* 加载地址等于0x1000,运行地址等于0x30000000,会导致data段在bin文件中处于0x10000位置 */
{ /* 我们需要将数据段复制到0x30000000的位置,由前面的代码段来拷贝 */
data_load_addr = LOADADDR(.data)
data_start = .;
*(.data)
data_end = .;
}
.bss : { *(.bss) *(.COMMON) } /* bss段紧接着data段排放,放在0x3xxxxxxx,bin文件中不存放bss段 */
} /* 程序运行时把bss段的数据清0 */
首先将被设置为0的全局变量的值打印出来,发现数值是一个乱码,因此需要把bss段的数据清0,修改start.S与链接脚本
SECTIONS {
.text 0 : { *(.text) }
.rodata : { *(.rodata) }
.data 0x30000000 : AT(0x1000)
{
data_load_addr = LOADADDR(.data)
data_start = .;
*(.data)
data_end = .;
}
.bss_start = .;
.bss : { *(.bss) *(.COMMON) }
.bss_end = .;
}
/* Start.S清除bss段 */
ldr r1, = bss_start
ldr r2, = bss_end
mov r2, #0
clean:
strb r2, [r1]
add r1, r1, #1
cmp r1, r2
bne clean
四.拷贝代码与链接脚本的改进
4.1 拷贝代码的拷贝(将数据段拷贝代SDRAM)
ldr r1, =data_load_addr /* data段在bin文件中的地址,加载地址 */
ldr r2, =data_start /* 重定位地址,运行时的地址 */
ldr r3, =data_end /* data段的结束地址 */
str r0, [r1]
cpy:
ldrb r4, [r1] /* 每次读取一个字节,但是我们的SDRAM是32位的,NOR是16位的,这样做效率会很低 */
strb r4, [r2]
add r1, r1, #1
add r2, r2, #1
cmp r2, r3
bne cpy
在上面拷贝的时候,我们每次从源地址读取一个字节,写入一个字节,但是我们的SDRAM是32位的,NOR是16位的,这样做效率很低
ldrb从NOR中得到数据,strb来写SDRAM,假设复制16字节,ldrb需要执行16次访问NOR16次,strb执行16次,访问SDRAM16次,共32次
当CPU要读一个字节的时候,将地址发给内存控制器,内存控制器读SDRAM会读出4个字节,从中挑出需要的1个字节返回给CPU
当CPU要写一个字节的时候,将地址和数据发送给内存控制器,内存控制器将32位的数据发送给SDRAM,同时会向SDRAM发送数据屏蔽信号(三条)DQM,会屏蔽掉不需要写的三个字节
改进方法:使用ldr 从NOR中读,使用str 写入SDRAM中
ldr从NOR中读,假设复制16字节,执行4次,访问8次,内存控制器会访问两次NOR,因为一次只能够得到两个字节
str写SDRAM,执行4次,访问硬件4次,一次可以写入4个字节
修改代码如下
ldr r1, =data_load_addr /* data段在bin文件中的地址,加载地址 */
ldr r2, =data_start /* 重定位地址,运行时的地址 */
ldr r3, =data_end /* data段的结束地址 */
str r0, [r1]
cpy:
ldr r4, [r1] /* 每次读取一个字节,但是我们的SDRAM是32位的,NOR是16位的,这样做效率会很低 */
str r4, [r2]
add r1, r1, #4
add r2, r2, #4
cmp r2, r3
ble cpy
/* Start.S清除bss段 */
ldr r1, = bss_start
ldr r2, = bss_end /* 查看反汇编,r1 = 0x30000002 r2 = 0x3000000c */
mov r2, #0
clean:
str r2, [r1] /* 清0的时候以4字节清0,但是0x30000002并不是4字节对齐 */
/* 会将0存放到0x30000000处,str 0, [0x30000000],会破坏别的数据 */
add r1, r1, #4
cmp r1, r2
ble clean
4.2 链接脚本的改进
修改链接脚本使bss段向4取整
SECTIONS {
.text 0 : { *(.text) }
.rodata : { *(.rodata) }
.data 0x30000000 : AT(0x1000)
{
data_load_addr = LOADADDR(.data)
. = ALIGN(4); /* 向4取整 */
data_start = .;
*(.data)
data_end = .;
}
. = ALIGN(4); /* 向4取整 */
.bss_start = .;
.bss : { *(.bss) *(.COMMON) }
.bss_end = .;
}
五.代码重定位与位置无关码
5.1 将整个程序的代码段和数据段重定位
5.1.1 在链接脚本中指定runtime addr指定为SDRAM的地址
5.1.2 前面的代码需要将整个代码拷贝到SDRAM中
5.1.3 程序应该位于0x30000000地址,但是刚开始位于0地址,仍然可以运行,因此重定位之前的代码与位置无关,即用位置无关码写成
5.1.4 修改链接脚本如下:
SECTIONS {
. = 0x30000000;
. = ALIGN(4);
.text :
{
*(.text)
}
. = ALIGN(4);
.rodata :
{
*(.rodata)
}
. = ALIGN(4);
.data :
{
*(.data)
}
. = ALIGN(4);
bss_start = .;
.bss :
{
*(.bss) *(.COMMON)
}
bss_end = .;
}
一般都是使用上面这种代码段和数据段放在一起的链接脚本
5.1.5 修改Start.S对数据段的重定位为对代码段,数据段,rodata段整个程序重定位
/* 重定位text, data, rodata段 */
mov r1, #0
ldr r2, =_start
ldr r3, =bss_start
cpy:
ldr r4, [r1]
str r4, [r2]
add r1, r1, #4
add r2, r2, #4
cmp r2, r3
ble cpy
5.2 分析启动代码
5.2.1 在重定位代码之前,需要调用sdram_init函数来初始化sdram,对应的反汇编如下图
bl 30000c40 <SdramInit>
这一句的意思并不是调到0x30000c40,这时候sdram没有初始化,跳过去执行肯定GG
修改链接脚本中的. = 0x30000000修改为 .=0x32000000
再次编译查看反汇编文件如下图
两次跳转的机器完全一致,这并不是调到0x30000c40地址,而是跳到当前PC值+offset(链接器算出来的)
具体跳到哪里由当前PC值决定
假设程序从0x30000000执行,当前指令的地址320001ec,那么程序就会跳到0x30000c40执行
如果程序从0执行,当前指令的地址是0x1ec,那么程序就会跳到0xc40
如果程序从0x32000000执行,当前指令的地址是0x320001ec,那么程序就会跳到0x32000c40
注意:
5.2.1 反汇编文件里的b/bl xxxx只是方便查看的作用,不是调到这个地址
5.2.2 跳到哪里由PC值+offset决定
5.2.2 注意到在调用main函数时使用bl main,查看反汇编
30000230: ebffffc2 bl 30000140 <main>
由于是bl跳转指令,程序从0开始执行,因此会跳到0x230地址,但是之前已经将代码拷贝到SDRAM中去了,拷贝到0x30000230
因此这时不对的,我们想让程序调到SDRAM的话必须使用绝对跳转如下
ldr PC, =main
反汇编如下
30000230: e59ff01c ldr pc, [pc, #28] ; 30000254 <.text+0x254>
5.3 怎么写位置无关码-> 不使用绝对地址,看反汇编有没有用到绝对地址
5.3.1 使用相对跳转命令(b/bl)
5.3.2 重定位之前不可使用绝对地址,不可以访问全局变量,静态变量
5.3.3 重定位之后,使用ldr pc, =xxx来跳转,跳转到runtime addr
5.3.4 不可访问有初始值数组,因为初始值会存放在rodata或者data中,使用绝对地址来访问具体见下面的例子
5.4 sdram_init函数的写法
5.4.1 在sdram_init函数中,直接对寄存器进行赋值,没有访问任何全局变量,静态变量
5.4.2 修改sdram_init函数如下:
void SdramInit()
{
unsigned int arr[] = {
0x22000000,
0x00000700,
0x00000700,
0x00000700,
0x00000700,
0x00000700,
0x00000700,
0x18001,
0x18001,
0x8404f5,
0xb1,
0x20,
0x20,
};
volatile unsigned int *p = (volatile unsigned int*)0x48000000;
int i;
for (i = 0; i < 13; i++)
{
*p = array[i];
p++;
}
}
这个函数编译烧写后并没有任何反应,表示这个函数并不是位置无关的
反汇编代码如下
30000c40 <SdramInit>:
30000c40: e1a0c00d mov ip, sp
30000c44: e92dd800 stmdb sp!, {fp, ip, lr, pc}
30000c48: e24cb004 sub fp, ip, #4 ; 0x4
30000c4c: e24dd03c sub sp, sp, #60 ; 0x3c
30000c50: e59f3088 ldr r3, [pc, #136] ; 30000ce0 <.text+0xce0>
/* 读30000ce0的值,依赖于PC值,如果是0地址运行就是赌0xce0处的值 */
/* 读到30000eb4, r3 = 0x30000eb4 */
30000c54: e24be040 sub lr, fp, #64 ; 0x40
30000c58: e1a0c003 mov ip, r3
/* ip = r3 = 0x30000eb4 */
30000c5c: e8bc000f ldmia ip!, {r0, r1, r2, r3}
/* 读0x30000eb4的数据,加载到r0,r1,r2,r3上去,但是sdram没有初始化,程序会死掉 */
/* 0x30000eb4存放了寄存器的初始值 */
/* 初始值保存在rodata里面,用初始值来初始化数组,而数组保存在栈里面 */
30000c60: e8ae000f stmia lr!, {r0, r1, r2, r3}
30000c64: e8bc000f ldmia ip!, {r0, r1, r2, r3}
30000c68: e8ae000f stmia lr!, {r0, r1, r2, r3}
30000c6c: e8bc000f ldmia ip!, {r0, r1, r2, r3}
30000c70: e8ae000f stmia lr!, {r0, r1, r2, r3}
30000c74: e59c3000 ldr r3, [ip]
30000c78: e58e3000 str r3, [lr]
30000c7c: e3a03312 mov r3, #1207959552 ; 0x48000000
30000c80: e50b3044 str r3, [fp, #-68]
30000c84: e3a03000 mov r3, #0 ; 0x0
30000c88: e50b3048 str r3, [fp, #-72]
30000c8c: e51b3048 ldr r3, [fp, #-72]
30000c90: e353000c cmp r3, #12 ; 0xc
30000c94: ca00000f bgt 30000cd8 <SdramInit+0x98>
30000c98: e51b1044 ldr r1, [fp, #-68]
30000c9c: e51b3048 ldr r3, [fp, #-72]
30000ca0: e3e02033 mvn r2, #51 ; 0x33
30000ca4: e1a03103 mov r3, r3, lsl #2
30000ca8: e24b000c sub r0, fp, #12 ; 0xc
30000cac: e0833000 add r3, r3, r0
30000cb0: e0833002 add r3, r3, r2
30000cb4: e5933000 ldr r3, [r3]
30000cb8: e5813000 str r3, [r1]
30000cbc: e51b3044 ldr r3, [fp, #-68]
30000cc0: e2833004 add r3, r3, #4 ; 0x4
30000cc4: e50b3044 str r3, [fp, #-68]
30000cc8: e51b3048 ldr r3, [fp, #-72]
30000ccc: e2833001 add r3, r3, #1 ; 0x1
30000cd0: e50b3048 str r3, [fp, #-72]
30000cd4: eaffffec b 30000c8c <SdramInit+0x4c>
30000cd8: e24bd00c sub sp, fp, #12 ; 0xc
30000cdc: e89da800 ldmia sp, {fp, sp, pc}
30000ce0: 30000eb4 strcch r0, [r0], -r4
六.重定位_清除BSS段的C函数实现
6.1 汇编传递参数
mov r0, #0
ldr r1, =_start
ldr r2, =bss_start
sub r2, r2, r1
bl Copy2Sdram
void Copy2Sdram(volatile unsigned int *src, volatile unsigned int *dest, unsigned int len)
{
unsigned int i = 0;
while (i < len)
{
*dest++ = *src++;
i += 4;
}
}
ldr r0, =bss_start
ldr r1, =bss_end
bl CleanBss
void CleanBss(volatile unsigned int *start, volatile unsigned int *end)
{
while (start < end)
{
*start++ = 0;
}
}
6.2 C语言直接获得地址参数
void Copy2Sdram(void)
{
extern unsigned int code_addr, bss_start;
volatile unsigned int *dest = (volatile unsigned int *)&code_addr;
volatile unsigned int *src = (volatile unsigned int *)0;
volatile unsigned int *end = (volatile unsigned int *)&bss_start;
while (dest < end)
{
*dest++ = *src++;
}
}
void CleanBss(void)
{
extern unsigned int bss_start, bss_end;
volatile unsigned int *start = (volatile unsigned int *)&bss_start;
volatile unsigned int *end = (volatile unsigned int *)&bss_end;
while (start < end)
{
*start++ = 0;
}
}
C函数如何使用lds文件中的变量abc(汇编文件中可以直接使用)?
a.在C函数中生命该变量未extern类型,比如extern int abc;
b.使用时,要取址,比如
int *p = &abc; //p的值即为lds文件中abc的值
6.3 C函数中使用链接脚本变量需要生命,汇编文件中可以直接使用的原因:
C函数中声明某个变量,必须声明,例如int g_i,那么程序必然有4字节保存这个变量
假设lds文件中有100W个,a1,a2....等变量,C程序完全没有必要存储这些变量
编译程序时有一个symbol tabel(符号表),保存着变量的名字和地址
对于链接脚本的变量也使用符号表,保存着lds变量(准确来说是常量,里面的值是固定的)的名字和值(注意,不是地址)
对于普通变量,使用&g_i 得到addr,为了保持代码的一致,对于lds中的a1,使用&a1得到里面的值
符号表里面的值在链接时确定,符号表不会存放到程序中去
结论:
6.3.1 C程序中不保存lds文件中的变量
6.3.2 借助符号表来保存lds中常量的值,使用时加上"&"得到它的值