ARM架构与编程(基于I.MX6ULL): 代码重定位(八)

1.段的概念_重定位的引入

1.1. 问题的引入

  • led.imx = 头部 + led.binled.stm32 = 头部 + led.bin
    • 头部里含有位置信息(addr):固件要把led.bin复制到哪里去
      • 链接程序时,指定了链接地址,一般来说头部信息的addr就等于链接地址
      • 如果,偏要修改头部信息的addr,让它不等于链接地址,会发生什么是?
    • 头部里含有长度信息(len):led.bin多大
  • 在串口程序中添加全局变量,把它打印出来,看看会发生什么事。

1.2. 段的概念

1.2.1 程序直接烧写在ROM上

代码段、只读数据段、可读可写的数据段、BSS段。

char g_Char = 'A';           // 可读可写,不能放在ROM上,应该放在RAM里
const char g_Char2 = 'B';    // 只读变量,可以放在ROM上
int g_A = 0;   // 初始值为0,干嘛浪费空间保存在ROM上?没必要
int g_B;       // 没有初始化,干嘛浪费空间保存在ROM上?没必要

所以,程序分为这几个段:

  • 代码段(RO-CODE):就是程序本身,不会被修改
  • 可读可写的数据段(RW-DATA):有初始值的全局变量、静态变量,需要从ROM上复制到内存
  • 只读的数据段(RO-DATA):可以放在ROM上,不需要复制到内存
  • BSS段或ZI段:
    • 初始值为0的全局变量或静态变量,没必要放在ROM上,使用之前清零就可以
    • 未初始化的全局变量或静态变量,没必要放在ROM上,使用之前清零就可以
  • 局部变量:保存在栈中,运行时生成
  • 堆:一块空闲空间,使用malloc函数来管理它,malloc函数可以自己写

1.2.2 片内固件功能强大,理解段的概念麻烦一点

看视频

1.3. 重定位

保存在ROM上的全局变量的值,在使用前要复制到内存,这就是数据段重定位。

想把代码移动到其他位置,这就是代码重定位。

2.重定位要做的事

2.1. 程序中含有什么?

  • 代码段:如果它不在链接地址上,就需要重定位
  • 只读数据段:如果它不在链接地址上,就需要重定位
  • 可读可写的数据段:如果它不在链接地址上,就需要重定位
  • BSS段:不需要重定位,因为程序里根本不保存BSS段,使用前把BSS段对应的空间清零即可

2.2. 谁来做重定位?

  • 程序本身:它把自己复制到链接地址去
  • 一开始,程序可能并不位于它的链接地址上,为什么它可以执行重定位的操作?
    • 因为重定位的代码是使用“位置无关码”写的
  • 什么叫位置无关码:这段代码扔在任何位置都可以运行,跟它所在的位置无关
  • 怎么写出位置无关码:
    • 跳转:使用相对跳转指令,不能使用绝对跳转指令
      • 只能使用branch指令(比如bl main),不能给PC直接复制,比如ldr pc, =main
    • 不要访问全局变量、静态变量
    • 不使用字符串

2.3. 怎么做重定位和清除BSS段?

  • 核心:复制
  • 复制的三要素:源、目的、长度
    • 怎么知道代码段/数据段保存在哪?(加载地址)
    • 怎么知道代码段/数据段要被复制到哪?(链接地址)
    • 怎么知道代码段/数据段的长度?
  • 怎么知道BSS段的地址范围:起始地址、长度?
  • 这一切
    • 在keil中使用散列文件(Scatter File)来描述
    • 在GCC中使用链接脚本(Link Script)来描述

2.4. 加载地址和链接地址的区别

程序运行时,应该位于它的链接地址处,因为:

  • 使用函数地址时用的是"函数的链接地址",所以代码段应该位于链接地址处
  • 去访问全局变量、静态变量时,用的是"变量的链接地址",所以数据段应该位于链接地址处

但是: 程序一开始时可能并没有位于它的"链接地址":

  • 比如对于STM32F103,程序被烧录器烧写在Flash上,这个地址称为"加载地址"
  • 比如对于IMX6ULL/STM32MP157,片内ROM根据头部信息把程序读入内存,这个地址称为“加载地址”

加载地址 != 链接地址时,就需要重定位。

3.链接脚本使用与分析

参考手册:Using LD, the GNU linker

3.1. 重定位的实质: 移动数据

把代码段、只读数据段、数据段,移动到它的链接地址处。
也就是复制
数据复制的三要素:源、目的、长度。

  • 数据保存在哪里?加载地址

  • 数据要复制到哪里?链接地址

  • 长度

这3要素怎么得到?
在GCC中,使用链接脚本来描述。
在keil中,跟链接脚本对应的是散列文件,散列的意思就是"分散排列",在STM32F103这类资源紧缺的单片机芯片中:

  • 代码段保存在Flash上,直接在Flash上运行(当然也可以重定位到内存里)
  • 数据段保存在Flash上,使用前被复制到内存里

但是,在资源丰富的MPU板子上:

  • 内存很大,几十M、几百M,甚至几G

  • 可能没有XIP设备(XIP: eXecute In Place,原地执行)

    • 没有类似STM32F103上的Flash,代码无法在存储芯片上直接运行
  • 基于这些特点,在MPU板子上

    • 代码段、数据段、BSS段等等,运行时没有必要分开存放
    • 重定位时,把整个程序(包括代码段、数据段等),一起复制到它的链接地址去

3.2. 链接脚本示例

3.2.1 链接脚本示例

SECTIONS {
    . = 0xC0200000;   /* 对于STM32MP157设置链接地址为0xC0200000, 对于IMX6ULL设为0x80200000 */

    . = ALIGN(4);
    .text      :
    {
      *(.text)
    }

    . = ALIGN(4);
    .rodata : { *(.rodata) }

    . = ALIGN(4);
    .data : { *(.data) }

    . = ALIGN(4);
    __bss_start = .;
    .bss : { *(.bss) *(.COMMON) }
    __bss_end = .;
}

3.2.2 链接脚本语法

3.2.2.1 完整的语法

一个链接脚本由一个SECTIONS组成。
一个SECTIONS里面,含有一个或多个section。

SECTIONS {
...
secname start BLOCK(align) (NOLOAD) : AT ( ldadr )
  { contents } >region :phdr =fill
...
}

section是链接脚本的核心,它的语法如下:

secname start BLOCK(align) (NOLOAD) : AT ( ldadr )
  { contents } >region :phdr =fill
3.2.2.2 几个例子

实际上不需要那么复制,不需要把语法里各项都写完。

  • 示例1
SECTIONS { 
  .text : { *(.text) }            /* secname为".text",里面是所有文件的".text"段 */ 
  .data : { *(.data) }            /* secname为".data",里面是所有文件的".data"段 */
  .bss :  { *(.bss)  *(.COMMON) } /* secname为".bss",里面是所有文件的".bss"段和".COMMON"段 */
} 
  • 示例2
    还可以按文件指定
SECTIONS {
  outputa 0x10000 :     /* secname为"outputa",链接地址为0x10000 */ 
    {
    first.o                 /* 把first.o整个文件放在前面 */
    second.o (.text)        /* 接下来是second.o的".text"段 */
    }
  outputb :             /* secname为"outputb",链接地址紧随outputa */ 
    {
    second.o (.data)        /* second.o的".data"段 */
    }
  outputc :             /* secname为"outputc",链接地址紧随outputb */  
    {
    *(.bss)                 /* 所有文件的".bss"段 */
    *(.COMMON)              /* 所有文件的".COMMON"段 */
    }
}
  • 示例3
SECTIONS { 
  .text 0x10000 : AT (0)       /* secname为".text",链接地址是0x10000,加载地址是0 */ 
  { *(.text) }  
  .data 0x20000 : AT (0x1000)  /* secname为".data",链接地址是0x20000,加载地址是0x1000 */
  { *(.data) } 
  .bss :                       /* secname为".bss",链接地址紧随.data段,加载地址紧随.data段 */
  { *(.bss)  *(.COMMON) } 
} 

3.3. 怎么获得各个段的信息

数据复制3要素:源、目的、长度。
怎么知道某个段的加载地址、链接地址、长度?

3.3.1 怎么确定源?

可以用ADR伪指令获得当前代码的地址,对于这样的代码:

.text
.global  _start
_start: 
    ......
    adr r0, _start

adr是伪指令,它最终要转换为真实的指令。它怎么获得_start代码的当前所处地址呢?
实际上,adr r0, _start指令的本质是r0 = pc - offset,offset是在链接时就确定了。

3.3.2 怎么确定目的地址?

也就是怎么确定链接地址?可以用LDR伪指令。
对于这样的代码:

.text
.global  _start
_start: 
    ......
    ldr r0, =_start

ldr是伪指令,它最终要转换为真实的指令。它怎么获得_start的链接地址呢?
_start的链接地址在链接时,由链接脚本确定。

3.3.3 如何获得更详细的信息

在链接脚本里可以定义各类符号,在代码里读取这些符号的值。
比如对于下面的链接脚本,可以使用__bss_start__bss_end得到BSS段的起始、结束地址:

    __bss_start = .;
    .bss : { *(.bss) *(.COMMON) }
    __bss_end = .;

上述代码里,有一个".“,它被称为"Location Counter”,表示当前地址:可读可写。
它表示的是链接地址。

. = 0xABC;       /* 设置当前地址为0xABC */ 
_abc_addr = . ;  /* 设置_abc_addr等于当前地址 */
. = . + 0x100;   /* 当前地址增加0x100 */
. = ALIGN(4);    /* 当前地址向4对齐 */

注意:"Location Counter"只能增大,不能较小。

3.4. 编写程序重定位数据段

3.4.1 修改链接脚本

我们故意只重定位数据段,在后面的课程再来重定位代码段并引入更多知识。
数据段要被复制到哪去?需要在链接脚本里确定一下:增加了__data_start

SECTIONS {
    . = 0xC0200000;   /* 对于STM32MP157设置链接地址为0xC0200000, 对于IMX6ULL设为0x80200000 */

    . = ALIGN(4);
    .text      :
    {
      *(.text)
    }

    . = ALIGN(4);
    __data_start = .;
    .rodata : { *(.rodata) }

    . = ALIGN(4);
    .data : { *(.data) }

    . = ALIGN(4);
    __bss_start = .;
    .bss : { *(.bss) *(.COMMON) }
    __bss_end = .;
}

3.4.2 编写程序

修改start.S:

ldr r0, =__data_start   /* 目的: 链接地址 */

/* 计算data段的当前地址:
 * _start的链接地址 - _start的当前地址 = __data_start的链接地址 - data段的当前地址
 * data段的当前地址 = __data_start的链接地址 - (_start的链接地址 - _start的当前地址)
 */
adr r1, _start
ldr r2, =_start
sub r2, r2, r1
sub r1, r0, r2

/* 计算data段的长度 */
ldr r2, =__bss_start
ldr r3, =__data_start
sub r2, r2, r3

bl memcpy    /* 需要3个参数: dest, src, len */

4.数据段重定位

参考手册:Using LD, the GNU linker

4.1. 怎么获得各个段的信息

数据复制3要素:源、目的、长度。
怎么知道某个段的加载地址、链接地址、长度?

4.1.1 怎么确定源?

可以用ADR伪指令获得当前代码的地址,对于这样的代码:

.text
.global  _start
_start: 
    ......
    adr r0, _start

adr是伪指令,它最终要转换为真实的指令。它怎么获得_start代码的当前所处地址呢?
实际上,adr r0, _start指令的本质是r0 = pc - offset,offset是在链接时就确定了。

4.1.2 怎么确定目的地址?

也就是怎么确定链接地址?可以用LDR伪指令。
对于这样的代码:

.text
.global  _start
_start: 
    ......
    ldr r0, =_start

ldr是伪指令,它最终要转换为真实的指令。它怎么获得_start的链接地址呢?
_start的链接地址在链接时,由链接脚本确定。

4.1.3 如何获得更详细的信息

在链接脚本里可以定义各类符号,在代码里读取这些符号的值。
比如对于下面的链接脚本,可以使用__bss_start__bss_end得到BSS段的起始、结束地址:

    __bss_start = .;
    .bss : { *(.bss) *(.COMMON) }
    __bss_end = .;

上述代码里,有一个".“,它被称为"Location Counter”,表示当前地址:可读可写。
它表示的是链接地址。

. = 0xABC;       /* 设置当前地址为0xABC */ 
_abc_addr = . ;  /* 设置_abc_addr等于当前地址 */
. = . + 0x100;   /* 当前地址增加0x100 */
. = ALIGN(4);    /* 当前地址向4对齐 */

注意:"Location Counter"只能增大,不能较小。

4.2. 编写程序重定位数据段

4.2.1 修改链接脚本

我们故意只重定位数据段,在后面的课程再来重定位代码段并引入更多知识。
数据段要被复制到哪去?需要在链接脚本里确定一下:增加了__data_start

SECTIONS {
    . = 0xC0200000;   /* 对于STM32MP157设置链接地址为0xC0200000, 对于IMX6ULL设为0x80200000 */

    . = ALIGN(4);
    .text      :
    {
      *(.text)
    }

    . = ALIGN(4);
    __rodata_start = .;
    .rodata : { *(.rodata) }

    . = ALIGN(4);
    .data : { *(.data) }

    . = ALIGN(4);
    __bss_start = .;
    .bss : { *(.bss) *(.COMMON) }
    __bss_end = .;
}

4.2.2 编写程序

修改start.S:

ldr r0, =__data_start   /* 目的: 链接地址 */

/* 计算data段的当前地址:
 * _start的链接地址 - _start的当前地址 = __rodata_start的链接地址 - rodata段的当前地址
 * data段的当前地址 = __rodata_start的链接地址 - (_start的链接地址 - _start的当前地址)
 */
ldr r0, =__rodata_start

ldr r2, =_start  /* link addr */
adr r3, _start   /* load addr */
sub r2, r2, r3
sub r1, r0, r2   /* 源 */

ldr r3, =__bss_start
sub r2, r3, r0

bl memcpy    /* r0: 目的, r1: 源, r2:长度 */

5.清除BSS段

5.1. C语言中的BSS段

程序里的全局变量,如果它的初始值为0,或者没有设置初始值,这些变量被放在BSS段里。

char g_Char = 'A';
const char g_Char2 = 'B';
int g_A = 0;  // 放在BSS段
int g_B;      // 放在BSS段

BSS段并不会放入bin文件中,否则也太浪费空间了。
在使用BSS段里的变量之前,把BSS段所占据的内存清零就可以了。

5.2. 清除BSS段

5.2.1 BSS段在哪?多大?

在链接脚本中,BSS段如下描述:

SECTIONS {
    . = 0xC0200000;   /* 对于STM32MP157设置链接地址为0xC0200000, 对于IMX6ULL设为0x80200000 */

    . = ALIGN(4);
    .text      :
    {
      *(.text)
    }

    . = ALIGN(4);
    __rodata_start = .;
    .rodata : { *(.rodata) }

    . = ALIGN(4);
    .data : { *(.data) }

    . = ALIGN(4);
    __bss_start = .;
    .bss : { *(.bss) *(.COMMON) }
    __bss_end = .;
}

BSS段的起始地址、结束地址,使用__bss_start__bss_end来获得,它们是链接地址。

5.2.2 怎么清除BSS段

ldr r0, =__bss_start   /* 目的 */
mov r1, #0             /* 值 */
ldr r2, =__bss_end     
sub r2, r2, r1         /* 长度 */
bl memset              /* r0: 目的, r1: 值, r2: 长度 */

6.代码段重定位

6.1. 代码段不重定位的后果

谁执行了数据段的重定位?
谁清除了BSS段?
都是程序自己做的,也就是代码段那些指令实现的。
代码段并没有位于它的链接地址上,并没有重定位,为什么它也可以执行?

因为重定位之前的代码是使用位置无关码写的,后面再说。

如果代码段没有重定位,则不能使用链接地址来调用函数:

  • 汇编中

    ldr  pc, =main   ; 这样调用函数时,用到main函数的链接地址,如果代码段没有重定位,则跳转失败
    
  • C语言中

    void (*funcptr)(const char *s, unsigned int val);
    funcptr = put_s_hex;
    funcptr("hello, test function ptr", 123);
    

6.2. 代码段重定位

6.2.1 代码段在哪?多大?

这要看链接脚本,对于MPU的程序,代码段、数据段一般是紧挨着排列的。
所以重定位时,干脆把代码段、数据段一起重定位。

  • 链接脚本
SECTIONS {
    . = 0xC0200000;   /* 对于STM32MP157设置链接地址为0xC0200000, 对于IMX6ULL设为0x80200000 */

    . = ALIGN(4);
    .text      :
    {
      *(.text)
    }

    . = ALIGN(4);
    __rodata_start = .;
    .rodata : { *(.rodata) }

    . = ALIGN(4);
    .data : { *(.data) }

    . = ALIGN(4);
    __bss_start = .;
    .bss : { *(.bss) *(.COMMON) }
    __bss_end = .;
}

对于这样的代码:

.text
.global  _start
_start: 				
  • 确定目的

    ldr r0, =_start
    
  • 确定源

    adr  r1, _start
    
  • 确定长度

    ldr r3, =__bss_start
    sub r2, r3, r0
    

6.2.2 怎么重定位

ldr r0, =_start
adr  r1, _start
ldr r3, =__bss_start
sub r2, r3, r0
bl memcpy

6.3 为什么重定位之前的代码也可以正常运行?

因为重定位之前的代码是使用位置无关码写的:

  • 只使用相对跳转指令:b、bl

  • 不只用绝对跳转指令:

    ldr pc, =main
    
  • 不访问全局变量、静态变量、字符串、数组

  • 重定位完后,使用绝对跳转指令跳转到XXX函数的链接地址去

    bl main         // bl相对跳转,程序仍在原来的区域运行
    
    ldr pc, =main   // 绝对跳转,跳到链接地址去运行
    
    ldr r0, =main   // 更规范的写法,支持指令集切换
    blx r0
    

7.重定位的纯C函数实现

7.1. 怎么得到链接脚本里的值

对于这样的链接脚本,怎么得到其中的__bss_start __bss_end:

SECTIONS {
    . = 0xC0200000;   /* 对于STM32MP157设置链接地址为0xC0200000, 对于IMX6ULL设为0x80200000 */

    . = ALIGN(4);
    .text      :
    {
      *(.text)
    }

    . = ALIGN(4);
    __rodata_start = .;
    .rodata : { *(.rodata) }

    . = ALIGN(4);
    .data : { *(.data) }

    . = ALIGN(4);
    __bss_start = .;
    .bss : { *(.bss) *(.COMMON) }
    __bss_end = .;
}

7.1.1 汇编代码

ldr  r0, =__bss_start
ldr  r1, =__bss_end

7.1.2 C语言

  • 方法1
    声明为外部变量,使用时需要使用取址符:
extern unsigned int __bss_start;
extern unsigned int __bss_end;
unsigned int len;
len = (unsigned int)&__bss_end - (unsigned int)&__bss_start;
memset(&__bss_start, 0, len);
  • 方法2
    声明为外部数组,使用时不需要使用取址符:

    extern char __bss_start[];
    extern char __bss_end[];
    unsigned int len;
    len = __bss_end - __bss_start;
    memset(__bss_start, 0, len);
    

7.2. 怎么理解上述代码

对于这样的C变量:

int g_a;

编译的时候会有一个符号表(symbol table),如下:

NameAddress
g_axxxxxxxx

对于链接脚本中的各类Symbol,有2中声明方式:

extern unsigned int __bss_start;     // 声明为一般变量
extern char __bss_start[];           // 声明为数组

不管是哪种方式,它们都会保存在符号表里,比如:

NameAddress
g_axxxxxxxx
__bss_startyyyyyyyy
  • 对于int g_a变量
    • 使用&g_a得到符号表里的地址。
  • 对于extern unsigned int __bss_start变量
    • 要得到符号表中的地址,也是使用&__bss_start
  • 对于extern char __bss_start[]变量
    • 要得到符号表中的地址,直接使用__bss_start[],不需要加&
    • 为什么?`__bss_start本身就表示地址啊
  • 3
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值