【嵌入式原理系列-第六篇】从Flash到RAM:MCU ld脚本全解析

发布于:2025-08-31 ⋅ 阅读:(21) ⋅ 点赞:(0)

目录

引言

一.ld文件的作用

1.1 静态存储布局控制

1.2 动态运行布局控制

二.ld语法

2.1 文件结构概览

2.2 SECTIONS块内容详解

2.2.1 输入文件(.o)指定

2.2.2 当前地址指针

2.2.3 KEEP

2.2.4 对齐

2.2.5 NOLOAD

2.2.6 OUTPUT_FORMAT和OUTPUT_ARCH

2.2.7 AT

2.2.8 ENTRY


引言

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 中定义或在输入文件中存在。

想了解更多嵌入式技术知识,请点击阅读我的其他文章

烟花的文章链接集合-CSDN博客

如果你觉得内容对您有帮助,别忘了点赞、收藏和分享支持下哦


网站公告

今日签到

点亮在社区的每一天
去签到