1.段的概念_重定位的引入
1.1. 问题的引入
led.imx = 头部 + led.bin
或 led.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.链接脚本使用与分析
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 */