目录
title: 链接脚本与重定位
tags: ARM
date: 2018-10-12 19:25:53
---
学习视频 韦东山
ldr
才能跳出去SECTIONS {
...
secname start BLOCK(align) (NOLOAD) : AT ( ldadr )
{ contents } >region :phdr =fill
...
}
start.o
或者这样start.o *(.text)
注意 一般需要4字节对齐,如果代码段和数据段的重定位使用4字节读写
最简单的链接脚本
SECTIONS {
. = 0x33f80000;
.text : { *(.text) }
. = ALIGN(4);
.rodata : {*(.rodata*)}
. = ALIGN(4);
.data : { *(.data) }
. = ALIGN(4);
__bss_start = .;
.bss : { *(.bss) *(COMMON) }
__bss_end = .;
}
对于全局变量来说,如果初始化了不为0的值,那么该全局变量则被保存在data段,如果初始化的值为0,那么将其保存在bss段,如果没有初始化,则将其保存在common段,等到链接时再将其放入到BSS段。关于第三点不同编译器行为会不同,有的编译器会把没有初始化的全局变量直接放到BSS段。
elf文件
1 链接得到elf文件,含有地址信息(load addr)
2 使用加载器(裸机程序是JTAG/应用程序的加载器本身也是个App去加载)
3 运行程序
4 如果loadaddr != runtimeaddr程序本身要重定位
核心程序运行时应该位于 runtimeaddr(reloate addr)或者链接地址
bin文件
1 elf生成bin文件
2 硬件机制启动,此时没有加载器
3 如果bin文件所在位置 不等于runtimeaddr ,程序本身实现重定位
bin文件/elf文件都不保存bss段 这些都是初始值为0 或者没有初始化的全局变量,程序运行时把bss段对应的空间清零
https://sourceware.org/ml/binutils/2007-07/msg00154.html 链接脚本的符号表
使用伪汇编指令ldr r1, =__bss_start
存放到一块区域,再去读取
.global _bss_start
_bss_start:
.word __bss_start
ldr r1, _bss_start //读取内存,这里是读取label所在内存的数据
//c中这么引用lable
extern ulong _bss_start;
C中获取链接脚本的值,链接的时候会根据lds与C所需要的lds中的值,产生一张内存表,内存表的标号也就是lds中的符号名(__bss_start).=========这里是重点======
extern int __bss_start;
int val =&__bss_start; //!< 获得__bss_start的值
int *p=&__bss_start; //!< p指向这个内存单元
------------------------------------------------------------------
下面是一个一个的符号表 ,都是 name + 地址的格式
-----
name: g_i //!<正常变量,存的是变量的地址
Addr:xxx
-----
name: g_j
Addr:xxx
------
name:__bss_start //!< lds中,存的就是值了
Addr:xxx
------
常规变量比如我们定义了int g_i
,那么就有一个int
大小的内存分配出来,地址也就是&g_i
,对于lds中的变量,我们也就要是需要先取址,再取值,也就是*(&__bss_start)
g_i
的操作是:1.寻找符号表中的g_i
,获得其内存地址,然后对内存操作__bss_start
他的值(地址值)就是所需要的值.总结:
全局变量
全局变量在放在链接地址指定的位置,所以其链接地址必须是可写的.
局部变量
虽然局部变量是在栈中的,但是局部变量的数组他的初始值是从链接地址中取出来的.例如:
参考 JTAG调试中nand调试章节
void memsetup()
{
unsigned long const mem_cfg_val[]={ 0x22011110, //BWSCON
0x00000700, //BANKCON0
0x00000700, //BANKCON1
0x00000700, //BANKCON2
0x00000700, //BANKCON3
0x00000700, //BANKCON4
0x00000700, //BANKCON5
0x00018005, //BANKCON6
0x00018005, //BANKCON7
0x008C07A3, //REFRESH
0x000000B1, //BANKSIZE
0x00000030, //MRSRB6
0x00000030, //MRSRB7
};
}
//mem_cfg_val是在栈,是位置无关的,但是他的初始值是去在链接地址取的,也就是0x3000000后面
// ip=300005bc
30000050: e1a0400c mov r4, ip
30000054: e8b4000f ldmia r4!, {r0, r1, r2, r3}
//从r4中指向的内存给r0~r3
//也就是说从 300005bc读取一些数据,但是这个时候300005bc(sdram)并没有被初始化且进行代码搬运,所以这里的数据肯定有问题
//这里的其实就是那个局部数组的值 mem_cfg_val
300005bc <.rodata>:
300005bc: 22011110 andcs r1, r1, #4 ; 0x4
300005c0: 00000700 andeq r0, r0, r0, lsl #14
300005c4: 00000700 andeq r0, r0, r0, lsl #14
300005c8: 00000700 andeq r0, r0, r0, lsl #14
300005cc: 00000700 andeq r0, r0, r0, lsl #14
300005d0: 00000700 andeq r0, r0, r0, lsl #14
300005d4: 00000700 andeq r0, r0, r0, lsl #14
300005d8: 00018005 andeq r8, r1, r5
300005dc: 00018005 andeq r8, r1, r5
300005e0: 008c07a3 addeq r0, ip, r3, lsr #15
300005e4: 000000b1 streqh r0, [r0], -r1
300005e8: 00000030 andeq r0, r0, r0, lsr r0
300005ec: 00000030 andeq r0, r0, r0, lsr r0
Disassembly of section .comment:
arm-linux-ld -Ttext 0 -Tdata 0x30000000
// 数据段如下
Disassembly of section .data:
30000000 <__data_start>:
30000000: Address 0x30000000 is out of bounds.
这个时候会发现,代码段从0开始,数据段从0x3000,0000开始,bin文件生成有0x3000,0000+1大小(1个全局变量),也就是产生了代码黑洞.代码段和数据段有大量的空白.
在1中会产生巨大的代码空白,所以需要解决这个问题,也就是数据段和代码段不能有大的空白,必须紧靠着.
将数据段与代码段在一起,全部烧写到Nor的0地址上
运行时将全局变量复制到sdram上,做数据段的重定位
这里的Makefile需要使用链接脚本了,这里的关键就是实现了数据放置在A,但是实际运行的时候是在B
arm-linux-ld -T sdram.lds start.o led.o uart.o init.o main.o -o sdram.elf
// 重点 data 0x30000000 : AT(0x700) { *(.data) } //放在0x700,但运行时在0x3000000
//lds
SECTIONS {
.text 0 : { *(.text) }//所有文件的.text
.rodata : { *(.rodata) } //只读数据段
.data 0x30000000 : AT(0x800) { *(.data) } //放在0x700,但运行时在0x3000000
.bss : { *(.bss) *(.COMMON) }//所有文件的bss段,所有文件的.COMMON段
}
需要手动初始化全局变量的值,也就是说先提前复制存储地址的值到运行地址,也就是将0x800的值复制0x3000,0000
// c语言调用全局变量
char g_Char = ‘A‘;
putchar(g_Char);
// 汇编,从484获取到链接的地址,也就是说从这个地方0x3000,0000取数据
450: e59f302c ldr r3, [pc, #44] ; 484 <.text+0x484>
454: e5d33000 ldrb r3, [r3]
458: e1a00003 mov r0, r3
45c: ebffff6f bl 220 <putchar>
...
484: 30000000 andcc r0, r0, r0
手动复制数据段,注意需要先初始化sdram
/* 重定位data段 */
mov r1, #0x800
ldr r0, [r1] //读取0x800 的内容到r0
mov r1, #0x30000000
str r0, [r1] //将r0存到0x3000,0000
上述方式是看了反汇编,知道代码就一个全局变量,这在具体应用肯定是不行的,需要从链接脚本获得数据段的大小.修改链接脚本如下,其中可以通过data_load_addr
获得其存储地址
SECTIONS {
.text 0 : { *(.text) }
.rodata : { *(.rodata) }
.data 0x30000000 : AT(0x700)
{
data_load_addr = LOADADDR(.data);
data_start = . ;//等于当前位置
*(.data) //等于数据段的大小
data_end = . ;//等于当前位置
}
.bss : { *(.bss) *(.COMMON) }
}
代码中自动复制相关的重定位的数据段
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
{
. = 0x30000000;
. = ALIGN(4);
.text :
{
*(.text)
}
. = ALIGN(4);
.rodata : { *(.rodata) }
. = ALIGN(4);
.data : { *(.data) }
. = ALIGN(4);
__bss_start = .;
.bss : { *(.bss) *(.COMMON) }
_end = .;
}
代码搬运和清除bss如下
/* 重定位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 //这里代码还是在nor上的,为什么能运行?因为代码在nor其实也没有什么关系
ldr pc, =main/*绝对跳转,跳到SDRAM*/
halt:
b halt
在反汇编中的B或者Bl跳转指令,其并不是跳转的相应的地址
3000005c: eb000106 bl 30000478 <sdram_init>
30000060: e3a01000 mov r1, #0 ; 0x0
30000064: e59f204c ldr r2, [pc, #76] ; 300000b8 <.text+0xb8>
30000068: e59f304c ldr r3, [pc, #76] ; 300000bc <.text+0xbc>
当我们修改链接脚本的时候,修改了链接地址,机器码也是不变的.实际上的跳转其实是相对pc跳转.由链接器决定.
假设程序从0x30000000执行,当前指令地址:0x3000005c ,那么就是跳到0x30000478;如果程序从0运行,当前指令地址:0x5c 调到:0x00000478
跳转到某个地址并不是由bl指令所决定,而是由当前pc值决定。反汇编显示这个值只是为了方便读代码。
重点: 反汇编文件里, B或BL 某个值,只是起到方便查看的作用,并不是真的跳转。
初始值为0的全局变量是存放在bss段的,需要自己写代码清0
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 //这里代码还是在nor上的,为什么能运行?因为代码在nor其实也没有什么关系
halt:
b halt
上述代码是字节读写的,cpu是32位的,sdram是32位宽的,可以直接使用32位访问,这里需要对链接脚本进行4字节对齐,否则在clean清bss段的时候,bss的起始地址不是4字节对齐的,那么他向4取整,也就是
ldr r1, =bss_start 这里地址如果不是4取整,那么比如0x3000,0002
clean:
strb r3, [r1] //这里是将0 写入 0x3000,0002 ,但是这是str会向4去整,也就是破坏bss段上面的
//2个字节
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 = .;
}
改进代码搬运为4字节访问
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 //这里代码还是在nor上的,为什么能运行?因为代码在nor其实也没有什么关系
回头看 获得连接脚本的值 上面的章节
使用汇编给c传递参数,也就是先使用汇编ldr r1,=__bss_start
获得链接脚本的参数
/* 重定位text, rodata, data段整个程序 */
mov r0, #0
ldr r1, =_start /* 第1条指令运行时的地址 */
ldr r2, =__bss_start /* bss段的起始地址 */
sub r2, r2, r1 /*长度*/
bl copy2sdram /* src, dest, len */
/* 清除BSS段 */
ldr r0, =__bss_start
ldr r1, =_end
bl clean_bss /* start, end */
c直接从链接脚本取值
///lds
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;
}
}
写位置无关码,其实就是不使用绝对地址,判断有没有使用绝对地址,除了前面的几个规则,最根本的办法看反汇编。
原文:https://www.cnblogs.com/zongzi10010/p/10023561.html