Linux裸机开发5——代码段与代码重定向
声明:以下知识与图片全部来自韦东山裸机开发视频
一、重定向的引入
1、Nor与Nand的启动特性
1)Nor启动
1、Nor可以像内存一样读,不可写
2、Nor启动时SRAM的起始地址为:0x400,0000
3、程序中含有需要改变的变量,需要重定向到SDRAM中进行修改
2)Nand启动
1、Nand可读可写
2、Nand启动时SRAM的起始地址为0
3、Nand启动时先将前4k的代码读出并复制,再放入内存(SRAM)中,如果大于4k则将整个程序放入SDRAM中
2、代码段的概念
由反汇编文件可以看到,代码分成许多段,如下图:
3、重定向
实际上就是将一个段从原地址进行内容复制到另一个地址
二、链接脚本的引入
1、对数据段进行重定向使用链接脚本
1、由反汇编文件可得,数据段位于0x800
2、要使Nor启动的时候,可以访问全局变量,就要将数据段重定向到SDRAM——SDRAM在2440的首地址为0x300,0000
3、但是,如果直接将数据段复制到0x300,0000时,中间有大空隙,导致.bin文件有800多M
4、因此,采用链接脚本,将数据段拼在代码段的后面(紧跟着),再将其烧录到Nor Flash中,再使用重定位,即将数据段拷贝到SDRAM中,这样就可以访问
2、链接脚本的解析
1)链接脚本的语法
相关链接:
2)利用链接脚本进行重定向的原理:
加载地址:段在重定向之前在bin中的地址
运行地址:段在重定位之后的地址,即运行时的地址
程序运行时,应位于runtime address(运行地址)
1、因此重定向实际上是将段从原地址复制到另一个地址(通常为SDRAM)中并运行,改变了数据段的运行地址**
2、将数据段拷贝到SDRAM之后,实际上Nor Flash的数据是不会产生变化的,只是运行地址变成了SDRAM
3)ELF文件与BIN文件
ELF文件:
BIN文件:
3、编写代码
1)指定重定向前数据段的起始地址
由重定向前的代码测试可得,数据段的起始地址为:0x900
SECTIONS {
.text 0 : { *(.text) }
.rodata : { *(.rodata) }
.data 0x30000000 : AT(0x900)/*表示加载地址为0x8454,运行地址为0x3000000,即SDRAM的起始地址*/
{
data_load_addr = LOADADDR(.data);
data_start = . ; /*获取重定向后数据段起始地址为data_start*/
*(.data)
data_end = . ; /*获取重定向后数据段结束地址为data_start*/
}
.bss : { *(.bss) *(.COMMON) } /*紧接着的是bss与common段*/
}
事实上,经过后面的视频学习可得:
重定位之前的代码,代码跳转到的实际位置与bin文件中显示的位置无关
2)Makefile文件加入的命令
arm-linux-ld
一般使用-T File (连接脚本),arm内存资源相对充足,可以在连接脚本里,让代码的text段,data段,bss段连在一起放到一个整块空间内,在脚本内也方便修改。最终会生成elf文件
例子:
arm-linux-ld -T sdram.lds start.o led.o uart.o init.o main.o -o sdram.elf
下面将几个重要的命令介绍一下:
-T File --script FILE指定一个链接器脚本文件,指示编译器按照脚本进行链接;
-Tbss ADDRESS bss段的链接地址;
-Ttext ADDRESS 代码段链接地址;
-Tdata ADDRESS 数据段链接地址;
-r --relocateable 产生可重定向的输出,比如,产生一个输出文件它可再次作为‘ld'的输入,这经常被叫做“部分链接”,当我们需要将几个小的.o文件链接成为一个.o文件的时候,需要使用此选项。
-e 指定程序的入口标号;
-l LibName 指定要链接的库;
-L Directory 增加库文件的搜索路径;
-o FILE 设置输出文件名;
-O 输出文件最优;
-a 指定arch体系;
3)使用链接脚本进行链接的代码
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 sram_start.o sram_start.S
arm-linux-ld -T sram.lds sram_start.o led.o uart.o init.o main.o -o sram.elf
arm-linux-objcopy -O binary -S sram.elf sram.bin
arm-linux-objdump -D sram.elf > sram.dis
clean:
rm *.bin *.o *.elf *.dis
sram_start.S
.text
.global _start
_start:
//关闭看门狗
mov r0, #0x53000000
mov r1, #0
str r1, [r0]
/* 先设置时钟 FCLK:HCLK:PCLK = 400:100:50 */
//先设置lock time
ldr r0, =0x4C000000
ldr r1, =0xFFFFFFFF
str r1, [r0]
//设置HCLK= FCLK/4 与PCLK = FCLK/8
ldr r1, =0x4C000014
ldr r0, =0x5
str r0, [r1]
//设置PLL之前先设置异步模式
mrc p15,0,r0,c1,c0,0
orr r0,r0,#0xc0000000 //R1_nF:OR:R1_iA
mcr p15,0,r0,c1,c0,0
//设置PLL
ldr r0, =0x4C000004
ldr r1, =(92<<12)|(1<<4)|(1<<0)
str r1, [r0]
//根据nand与nor的特性自动辨认nor启动与nand启动
//nor flash可读不可写
mov r1, #0x0 //r1 = 0
ldr r0, [r1] //将0地址的数值保存在r0中
str r1, [r1] //向0地址中写入0
ldr r2, [r1] //取出0地址的值
cmp r1, r2 //比较r1与r2的值,相等则为nand,否则为nor
ldr sp, = 4096+0x40000000
moveq sp, #4096 //r1 = r2,nand启动
streq r0, [r1] //恢复0地址原来的值
//调用SDRAM初始化函数
bl sdram_init
//获取重定向相关地址
ldr r0, = data_load_addr //加载地址,重定向前数据段的起始地址
ldr r1, = data_start //数据段重定向之后的起始地址
ldr r2, = data_end //数据段重定向之后的终止地址
copy:
//执行重定向
ldrb r3, [r0] //将加载地址中的值存入r3
strb r3, [r1] //将r3的值赋予运行地址中的值
add r0, r0, #1 //加载地址加1
add r1, r1, #1 //运行地址加1
cmp r1, r2 //比较当前运行地址与终止地址
bne copy //如果此时运行地址不等于终止地址,则还未结束,继续进行拷贝(重定向)
/*调用main函数*/
bl main
/*死循环*/
halt:
b halt
sram.lds
SECTIONS {
.text 0 : { *(.text) }
.rodata : { *(.rodata) }
.data 0x30000000 : AT(0x800)
{
data_load_addr = LOADADDR(.data);
data_start = . ;
*(.data)
data_end = . ;
}
.bss : { *(.bss) *(.COMMON) }
}
三、链接脚本的改进
1、引入字节对齐
因为原来使用的是:
ldrb 与strb,也就是一个一个字节去操作,效率是很低的
硬件(2 字节的Nand flash )例如是16字节时,
当使用单字节读取16个字节时,其余高24字节会被屏蔽掉不传入Nand flash中,
程序需要执行16次,硬件需要执行16/2 = 8次
而使用 ldr / str ——4字节读取指令时
稿24字节不会被硬件屏蔽掉,
程序只需执行 16/4 = 4,硬件需要执行16/2 = 8次
/*以下指令表示4字节对齐*/ . = ALIGN(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 = . ;
}
2、清除bss段
一般来说 bss段是不需要写入bin文件中,因此会对其进行清除
汇编文件
//获取bss段相关地址
ldr r0, = bss_start //bss段的起始地址
ldr r1, = bss_end //bss段的终止地址
clearb:
//清除bss段
mov r2, #0 //初始化一个变量为0,用于之后的清零操作
strb r2, [r0] //将起始地址保存的值清零
add r0, r0, #1 //起始地址加1
cmp r0, r1 //比较当前运行地址与终止地址
bne clearb //如果此时运行地址不等于终止地址,则还未结束,继续进行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 = . ;
}
四、位置无关码
1、重定向之前的代码与位置无关
1、重定向之前的代码与位置无关,由位置无关码构成
2、实际上重定向之前的pc跳转:b bl
并不是跳转到反汇编文件中的地址,而是跳转到pc + 偏移,偏移是由编译器决定的
2、如何写位置无关码
1、调用程序时使用B/BL相对跳转指令
2、重定位之前, 不可使用绝对地址,比如:
不可访问全局变量/静态变量
不可访问有初始值的数组(因为初始值放在rodata里,使用绝对地址来访问)
3、 重定位之后, 使用绝对跳转命令跳到运行地址,比如:
ldr pc, =main
3、C代码中如何使用链接脚本中定义的变量
C代码中如何使用链接脚本中定义的变量
http://www.100ask.org/bbs/forum.php?mod=viewthread&tid=16231&highlight=%C1%B4%BD%D3%BD%C5%B1%BE参考文章:https://sourceware.org/ml/binutils/2007-07/msg00154.html
1)C函数怎么使用lds文件中的变量abc
1、在C函数中声明改变量为 extern 类型, 比如:
extern int abc;
2、使用时, 要使用 & 取址, 比如:
int *p = &abc; // p的值即为lds文件中abc的值
2)lds文件保存变量的实质
lds文件使用system table对里面所定义的变量进行保存
1、C程序不保存lds文件中的变量
2、借助system table保存lds的变量
4、将所有段重定向之后运行代码
1)用C语言编写重定向与清除bss段代码
//用于重定向的代码
void copy_by_c(void)
{
extern int __data_start;
extern int __bss_start;
volatile unsigned int * start = (volatile unsigned int *)(&__data_start);
volatile unsigned int * end = (volatile unsigned int *)(&__bss_start);
volatile unsigned int * src = (volatile unsigned int *)0;
while (start <= end)
{
*start++ = *src++;
}
}
//用于清除bss段的代码
void clear_by_c(void)
{
extern int __bss_start;
extern int _end;
volatile unsigned int * start = (volatile unsigned int *)(&__bss_start);
volatile unsigned int * end = (volatile unsigned int *)(&_end);
while (start <= end)
{
*start++ = 0;
}
}
2)改进后的lds文件
SECTIONS
{
. = 0x30000000;
__data_start = . ;
. = ALIGN(4);
.text :
{
*(.text)
}
. = ALIGN(4);
.rodata : { *(.rodata) }
. = ALIGN(4);
.data : { *(.data) }
. = ALIGN(4);
__bss_start = .;
.bss : { *(.bss) *(.COMMON) }
_end = .;
}
3)改进后的汇编文件
//调用SDRAM初始化函数
bl sdram_init
//执行重定向
bl copy_by_c
//执行bss段的清除
bl clear_by_c
/*调用main函数*/
// bl main //相对跳转
ldr pc, = main //绝对跳转,重定向之后要使用绝对跳转,否则跳转不到真正的地址