目录
2.2.6 OUTPUT_FORMAT和OUTPUT_ARCH
引言
MCU中一般有三种不同类型的存储空间
- Flash(Code):用于存储我们的代码
- Flash(Data):用于永久存储数据,掉电也不丢失,但是有写入次数的限制(一般10w次左右)
- Ram:Ram的用途多种多样,特点是读取写入次数不受限且速度比Flash快得多,我们的代码运行是内存开销(堆/栈),包括全局变量的空间分配都是使用的它。
Flash(Data)主要是数据存储,不参与镜像文件的生成。但是Flash(Code)和Ram不一样,它两在生成镜像文件时是需要进行特殊分配管控的。思考下面几个简单问题:
1)程序复位后从哪个位置开始执行?
当然你可以默认从Flash(Code)的逻辑地址"0"开始执行。但是如果Flash(Code)不在0怎么办?像这样:
存储空间名 | 存储地址 | 说明 |
---|---|---|
Ram | 0x8000~0x8FFF | Ram空间 |
Flash(Code) | 0x2000~0x7FFF | Code空间 |
芯片预留 | 0x0000~0x1FFF | 不允许程序使用 |
所以这个假设从地址"0"开始执行其实是不靠谱的。
2)Flash(Code)到Ram的映射怎么进行的?
即我们在代码中写了如下代码:
int g_MyTestTemp;
这个变量最后运行时是在Ram中的,那么它在Ram的哪个地址?
3)堆和栈的地址和大小?
C语言规定malloc动态内存基于堆,局部变量和函数运行时内存开销基于栈。堆和栈都是使用Ram,那么他们在Ram中的地址是?
从上面可以看出,如果没有一个管理机制,基于C语言的语法(其他语言也类似)我们的程序是跑不起来的。所以我们需要ld文件(ld一般由芯片原厂提供最基础的版本。工程师根据项目具体需求对基础ld文件进行修改和定制)。
一.ld文件的作用
ld文件的核心作用是代码静态存储和动态运行时,在存储器中的布局。具体来说,它负责:
1.1 静态存储布局控制
我们的代码存在Flash(code)里,它包含逻辑和数据,C语言场景下。一般在Flash中上述段的排布顺序如下所述:
- 中断向量表:中断向量表一般放在首地址
- 代码段:一般称为".text",存储我们的代码逻辑
- 只读常量段:一般称为".rodata",存储带const标识的全局或者静态变量,注意const局部变量不在此区域。
- 带初值的全局或静态变量:由于只是用Flash(code)来保存数据,通常没有独立段名,但会指定其起始地址。
- 其他特殊段:如OTA段,用户自定义数据等
1.2 动态运行布局控制
- 堆:一般称为".heap",需要指定其在Ram的起始和结束地址
- 栈:一般称为".stack",需要指定其在Ram的起始和结束地址
- 带初值的全局或静态变量:一般称为".data",需要指定其在Ram的起始和结束地址。根据".data"的段大小,以及其在Flash(code)的起始地址 ,就可以将启动文件中将其初值复制到Ram中的".data"段。
- 不带初值的全局/静态变量:一般称为".bss",需要指定其在Ram的起始和结束地址。一般在启动文件中将其初值赋值为0。
注意:
对于".rodata"段的内容,运行时如果需要使用,则CPU直接从Flash(code)中读取,不会将其迁移到Ram。
二.ld语法
2.1 文件结构概览
ld文件大致由两部分组成:
- MEMORY块:定义芯片存储区域段
- SECTIONS块:定义各段在存储器中的位置和排列
一个典型ld文件格式如下:
MEMORY
{
存储区域名1 : ORIGIN = 起始地址, LENGTH = 长度
存储区域名2 : ORIGIN = 起始地址, LENGTH = 长度
}SECTIONS
{
.输出段名1 :
{
内容
} > 存储区域名1
.输出段名2 :
{
内容
} > 存储区域名2
}
语法说明
- 关键字:MEMORY、SECTIONS、ORIGIN、LENGTH是关键字,名称大小写不可更改。
- 符号:示例中标记红色的符号全部不可省略(包括分号、逗号、冒号、大括号等)
- 存储区域名:逻辑名称,可自定义(数字、字母、下划线)
- 输出段名:逻辑名称,可自定义(数字、字母、下划线),输出段名可以理解成存储区域名的别名,将MEMORY和SECTION关联起来。
- 起始地址:存储区域在物理存储器中的实际地址
- 长度:存储区域的大小
2.2 SECTIONS块内容详解
在定义了MEMORY之后,SECTIONS命令是LD脚本的灵魂,它指导链接器如何将输入文件(编译中间文件,一般为".o")中的段映射到输出文件(最终烧录文件,如".hex")的内存位置。
2.2.1 输入文件(.o)指定
ld文件需要将多个.o文件进行合并,那么这涉及到如何指定是哪个.o,ld文件提供语法如下:
输入文件名(.输入段名)
上面表示在特定的输入文件中查找特定的输入段名。一般实际工程中多个输入文件的输入段名是需要合并的,因此格式变成如下:
*(.输入段名)
这样还不够,一般默认输入段名前缀一致的段都应该合并。即:
- *(.bss)
- *(.bssmy)
- *(.bss1)
所以最终格式如下:
*(.输入段名*)
整个语法表示的含义是遍历所有输入文件,找到符合命名规则的段,第二个*号表示整合所有前缀一致的段。
注意:
输出段名可以完全用户自定义,但是输入段名有一部分是固定不可更改的。
输出段名的内容描述中,输入段名可以与输出段名重名。如:
SECTIONS
{
.输出段名 :
{
*(.输入段名1*)
*(.输入段名2*)
} > 存储区域名
}SECTIONS
{
.mydata :
{
*(.mydata*)
*(.data*)
} > Ram
}
2.2.2 当前地址指针
ld文件中用"."表示指向当前地址的指针,有了当前地址获取方式我们就可以进行地址的设置、修改、获取。
设置/修改当前地址:
. = 0x0000; /* 设置当前地址为0,注意这里是有分号的 */
获取当前地址:
_text_start = .; /* .text段的开始地址 */
*(.text*)
_text_end = .; /* .text段的结束地址 */
注意:
- _text_start和_text_end是ld中的符号,类似于C语言中的变量,符号定义不需要类型声明。可以对齐进行赋值操作,在C语言代码中也可以直接使用此符号。
- _text_end的地址是编译器自动计算出来的,相当于*(.text*)会将所有输入文件中符合要求的内容统计出来并输出到输出文件中,同时当前指针移动到*(.text*)的末尾。因此_text_end获取的当前指针就是*(.text*)的结束地址。
2.2.3 KEEP
如果输入文件指定段中的符号没有被引用(即某个.mydata段中定义了变量,但是变量没有被任何区域使用),编译的时候默认会被舍去,如果不想被优化,则使用如下语法:
KEEP(*(.段名*)) /* KEEP后必须跟圆括号 */
2.2.4 对齐
对齐一般指的是将当前地址指针对齐到4的倍数,方法有很多种,列举以下两种:
1)自动对齐
. = ALIGN(4); /* 将当前地址指针下移,对齐到4的倍数*/
2)手动对齐
当前地址指针对齐示例:
- (. + 3): 代表当前地址+3
- & (-4):-4 的二进制补码为 0xFFFFFFFC,与上-4即代表将低两位置零,保证地址是 4 的倍数
. = (. + 3) & (-4)
示例如下:
当前地址 . |
加 3 后 | & (-4) 后 | 解释 |
---|---|---|---|
0x1000 | 0x1003 | 0x1000 | 已对齐,不动 |
0x1001 | 0x1004 | 0x1004 | 向上对齐到下一个 4 的倍数 |
0x1002 | 0x1005 | 0x1004 | 向上对齐到下一个 4 的倍数 |
0x1003 | 0x1006 | 0x1004 | 向上对齐到下一个 4 的倍数 |
2.2.5 NOLOAD
该属性标识一个段不需要被加载到最终的可执行镜像(输出文件)中。它通常用于:
- 定义一段在运行时由程序完全管理、无需初始数据的内存(例如高级电源管理中的.noinit段)。
- 纯粹为了在内存地图中保留地址空间,防止其他段占用。
SECTIONS
{
.输出段名 (NOLOAD) :
{
} > 存储区域名
}
注意:
该段数据的持久化(如掉电复位后保持)依赖于硬件特性,(NOLOAD)本身并不保证这一点,它只是一个给链接器的指令。”
2.2.6 OUTPUT_FORMAT和OUTPUT_ARCH
定义输出文件格式和架构信息,通常放在ld文件开头。
OUTPUT_FORMAT("elf32-littlearm", "elf32-littlearm", "elf32-littlearm")
OUTPUT_ARCH(arm)
2.2.7 AT
前面我们讲过".data"初始值在Flash(code),在启动文件中会迁移到Ram区域,后续都在Ram中使用。这一流程是如何指定的呢?请看下面示例:
MEMORY
{
Flash: ORIGIN = 起始地址, LENGTH = 长度
Ram : ORIGIN = 起始地址, LENGTH = 长度
}SECTIONS
{.text :
{
_text_end = .; /* 一般.data的初始值默认放在text末尾。_text_end符号仅用于启动代码计算.data初始值拷贝地址,可不写入SECTIONS,只是一个惯例 */
} > Flash/* .data必须在.text后定义 */
.data:
{} > Ram AT> Flash /* 将.data的初始值放入text末尾 */
}
2.2.8 ENTRY
ENTRY 用于指定程序复位后执行的入口地址,也就是 MCU 上电或复位后 CPU 第一次执行的指令所在位置。通常指向启动文件(startup.s)中的 _start 或初始化函数。示例如下:
ENTRY(符号名)
- 符号名:必须是已在 LD 文件或输入文件中定义的符号,如 _start。
- 行为:如果不指定 ENTRY,链接器通常会将程序的第一个段(如 .text)起始地址作为入口,但这不一定符合 MCU 启动要求。ENTRY 指令逻辑上与 MEMORY 和 SECTIONS独立,但引用的符号必须已在 SECTIONS 中定义或在输入文件中存在。
想了解更多嵌入式技术知识,请点击阅读我的其他文章
如果你觉得内容对您有帮助,别忘了点赞、收藏和分享支持下哦