跳到主要内容位置

ARM架构-段与重定位

1.段和重定位#

led.imx = 头部 + led.binled.stm32 = 头部 + led.bin

  • 头部里含有位置信息(addr):固件要把 led.bin 复制到哪里去
    • 链接程序时,指定了链接地址,一般来说头部信息的 addr 就等于链接地址
    • 如果,偏要修改头部信息的 addr,让它不等于链接地址,会发生什么是?
  • 头部里含有长度信息(len):led.bin 多大

SOC 上电运行时,会执行片上 ROM(bootrom)的程序,会根据头部信息把.bin 读到内存(RAM)上的 addr 地址。

这并不是理所当然的,例如在 F103 里,就没有这个过程,需要在代码里把程序读到内存。

1.1 有哪些段#

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

.text - 代码段:函数代码

.data - 可读可写数据段:已初始化的全局变量(包括全局静态变量)和局部静态变量,static

.rodata - 只读数据段:全局常量(const)、字符串常量

.bss - 未初始化的全局变量(包括全局静态变量)和局部静态变量,static

1.2 重定位的相关概念#

1)链接地址和运行(加载)地址。#

  • 运行地址,顾名思义就是程序运行的时候的地址,也就是你用工具将代码下载到 RAM 的那个地址,也叫加载地址。
  • 链接地址,由链接脚本指定的地址。为什么需要链接脚本指定地址呢?你想一下,在 c 语言编程中,当我们需要调用一个 A 函数的时候,编译器是怎么找到这个 A 函数?编译器肯定是知道它被放在哪里才可以找到它。那就是链接脚本的作用,链接脚本其实在程序被执行之前都已经指定 A 函数一个地址编号,以后所有的函数调用我们都会去这个地址编号那里寻找 A 函数。有点类似于 c 语言的指针变量。

2)链接地址跟运行(加载)地址不同的情况下会出现什么情况?#

答:以上面举的函数 A 为例,当链接地址跟运行地址不同的时候,假如链接地址是 0x1000,运行地址(加载地址)是 0x0000,

链接脚本指定函数 A 将来是要存放到(基地址+偏移量)=0x1000+0x0001=0x1001 地址的,但是程序在下载的时候却把这个程序下载到 0x0000,所以函数 A 的地址实际上是存放在(基地址+偏移量)=0x0000+0x0001=0x0001 这个地址的。

当程序运行到一行位置有关码例如:ldr PC, A ,编译器首先就会按照链接脚本指定的 A 的那个地址 0x1001 寻找 A 函数,但是因为加载地址跟链接地址不同的原因,实际上 A 函数已经被放到了 0x0001,所以执行就会出错。

所以,当这两个地址不同的时候,执行一段位置有关码的时候就会发生不可预估的错误。

3)位置有关码与位置无关码。#

  • 位置有关码,就是这句代码的执行正确与否还需要取决于当前的地址,也就是说跟地址已经绑定了的,例如:ldr PC, _main,就是 PC 指针必须跳转到_main(函数名就是一个地址)这个地址去,代码执行成功与否就相当于受到了这个地址的约束,假如这个地址的内容不存放_main 这个函数,就会出错了。
  • 位置无关码,就是这句代码在哪里运行都可以的,跟所处的地址无关,跟位置有关码相反。

4)为什么会出现链接地址跟运行(加载)地址不同的情况?#

答:当一块芯片启动的时候,依靠内部的 SRAM,可以运行一小段代码,而因为 DDR 还没初始化,注定了开始的运行地址是在内部 SRAM 中的。当我们需要运行一个操作系统,那么点的内存怎么够运行呢?所以这时候就需要初始化 DDR 才可,而因为我们知道这代码将来都是在 DDR 上面运行的,所以链接脚本指定的链接地址肯定是 DDR 上面的地址,所以这就出现了链接地址跟运行地址不同的情况了。

5)为什么需要重定位?#

答:就是链接地址跟运行(加载)地址不同,在这个情况下我们可以有两种方案: ① 全部使用位置无关码。 ② 进行重定位让这两个地址相同。 我们知道,如果是一个小代码,使用 ① 时可以的,但是一个大的代码文件很难保证全部都使用位置无关码的,这也是不现实的,所以必须使用重定位解决这个问题。

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.链接脚本的使用与分析#

3.1 重定位的实质#

实质就是移动数据,也就是复制。

数据复制的三要素是源、目的、长度。

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

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

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

  • 内存很大,几十 M、几百 M,甚至几 G
  • 可能没有 XIP 设备(XIP: eXecute In Place,原地执行)
    • 没有类似 STM32F103 上的 Flash,代码无法在存储芯片上直接运行

基于这些特点,在 MPU 板子上

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

3.2 链接脚本示例#

SECTIONS {
. = 0xC0200000; /* 对于STM32MP157设置链接地址为0xC0200000, 对于IMX6ULL设为0x80200000 */
// . 表示当前地址
. = ALIGN(4); // 向4字节取整
.text :
{
*(.text)
}
// . 会随着段增长
. = ALIGN(4);
.rodata : { *(.rodata) }
. = ALIGN(4);
.data : { *(.data) }
. = ALIGN(4);
__bss_start = .; // bss段的起始地址
.bss : { *(.bss) *(.COMMON) }
__bss_end = .; // bss段的结束地址,通过起始地址和结束地址就可以对bss段清0了
}

3.3 链接脚本语法#

一个链接脚本由一个 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

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

  • 示例 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) }
}

4.数据段重定位#

4.1 怎么确定源?#

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

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

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

4.2 怎么确定目的地址?#

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

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

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

4.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.4 编写#

以数据段为例,写一个重定位代码。

先在链接脚本里,记录一下 rodata 的起始地址,拿__bss_start - __rodata_start就是数据段的大小

SECTIONS {
. = 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:
/* 设置sp */
ldr sp, =(0x80000000+0x100000)
/* rodata/data重定位 */
ldr r0, =__rodata_start
ldr r2, =_start /* B:link addr */
adr r3, _start /* A:load addr: 取出当前_start地址 */
sub r2, r2, r3 /* 上图:B-A */
sub r1, r0, r2 /* 源 */
ldr r3, =__bss_start
sub r2, r3, r0
bl memcpy /* r0: 目的, r1: 源, r2:长度 */
/* 调用main函数 */
bl main

然后需要实现一下 memcpy

void memcpy(void *dest, void *src, unsigned int len)
{
unsigned char *pcDest = dest;
unsigned char *pcSrc = src;
while (len--)
{
*pcDest = *pcSrc;
pcSrc++;
pcDest++;
}
}

5.清除 BSS 段#

BSS 段不需要复制过去,只需要将对应的地址空间清 0 即可。

具体而言:程序里的全局变量,如果它的初始值为 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.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 怎么清除 BSS 段#

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

实现 memset

void memset(void *dest, unsigned char val, unsigned int len)
{
unsigned char *pcDest = dest;
while (len--)
{
*pcDest = val;
pcDest++;
}
}

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 代码段在哪?多大?#

这要看链接脚本,对于 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.3 编写#

.text
.global _start
_start:
/* 设置sp */
ldr sp, =(0x80000000+0x100000)
/* text/rodata/data重定位 */
ldr r0, =_start
adr r1, _start /* 源 */
ldr r3, =__bss_start
sub r2, r3, r0
bl memcpy /* r0: 目的, r1: 源, r2:长度 */
/* clear bss */
ldr r0, =__bss_start
mov r1, #0
ldr r2, =__bss_end
sub r2, r2, r0
bl memset /* r0: dest, r1: val(0), r2: len */
/* 调用main函数 */
//bl main // 相对跳转指令:位置无关
ldr pc, =main // 绝对跳转指令:跳转到链接地址的main函数去

为什么这里之前没有重定位,代码可以正常运行?#

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

  • 只使用相对跳转指令: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 {
. = 0x80200000;
. = ALIGN(4);
__text_start = .;
.text :
{
*(.text)
}
. = ALIGN(4);
__rodata_start = .;
.rodata : { *(.rodata) }
. = ALIGN(4);
.data : { *(.data) }
. = ALIGN(4);
__bss_start = .;
.bss : { *(.bss) *(.COMMON) }
__bss_end = .;
}

1)汇编代码#

ldr r0, =__text_start
ldr r1, =__bss_start
ldr r2, =__bss_end

2)C 语言#

  • 方法 1 声明为外部变量,使用时需要使用取址符:
extern unsigned int __text_start;
extern unsigned int __bss_start;
extern unsigned int __bss_end;
unsigned int text_data_len, bss_len;
text_data_len = (unsigned int)&__bss_start - (unsigned int)&__text_start;
bss_len = (unsigned int)&__bss_end - (unsigned int)&__bss_start;
memcpy(__text_start, loadaddr, text_data_len);
memset(&__bss_start, 0, bss_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数组名本身就表示地址

7.3 编写程序#

start.S

.text
.global _start
_start:
/* 设置sp */
ldr sp, =(0x80000000+0x100000)
adr r0, _start
bl SystemInit
/* 调用main函数 */
//bl main
ldr pc, =main

Init.c

#include "string.h"
#if 0
void SystemInit(void *loadaddr)
{
extern char __text_start;
extern char __bss_start;
extern char __bss_end;
unsigned int len;
/* text/rodata/data重定位 */
len = &__bss_start - &__text_start;
memcpy(&__text_start, loadaddr, len); /* 目的, 源, 长度 */
/* clear bss */
len = &__bss_end - &__bss_start;
memset(&__bss_start, 0, len); /* dest, val(0), len */
}
#else
void SystemInit(void *loadaddr)
{
extern char __text_start[];
extern char __bss_start[];
extern char __bss_end[];
unsigned int len;
/* text/rodata/data重定位 */
len = __bss_start - __text_start;
memcpy(__text_start, loadaddr, len); /* 目的, 源, 长度 */
/* clear bss */
len = __bss_end - __bss_start;
memset(__bss_start, 0, len); /* dest, val(0), len */
}
#endif