GCC内LD链接脚本语法
一、 声明式配置 DSL(领域特定语言)
不需要真正掌握,只需要会用
无逻辑:没有 if/for/函数(极少例外,如
SORT)无状态:不保存变量,不改变程序行为
纯映射:只描述“输入段 → 输出段 → 物理地址”的映射关系
非图灵完备:不能写算法,不能做计算(简单算术除外)
下面的代码块展示的是LD文件的常见功能块
/* 1. 入口点定义(程序从哪开始跑) */
ENTRY(Reset_Handler)
/* 2. 内存区域定义(芯片物理资源) */
MEMORY { ... }
/* 3. 段定义(内容放哪) */
SECTIONS { ... }
/* 4. 符号定义(导出关键地址给C代码) */
PROVIDE(symbol = value);
/* 5. 高级特性(覆盖段/断言等) */
ASSERT(...);
二、 LD文件的作用
规定程序的运行入口:通过
ENTRY()明确告诉 CPU 上电复位后,第一行代码从哪里开始执行(如Reset_Handler)。规划内存的物理版图:通过
MEMORY命令告诉链接器芯片上有哪些存储块(Flash、SRAM、DTCM等),以及它们的起始地址和大小。定义段的分配规则:通过
SECTIONS命令,将输入文件中的段(如机器码.text、常量.rodata、变量.data)精确映射到具体的内存区域中。导出关键地址给启动代码:通过定义特殊符号(如
_sdata,_edata,_sbss),在内存中打下“界桩”,供汇编启动代码在 C 语言main()函数执行前,精准地完成内存初始化和数据拷贝。
三、 基本语法
(1)MEMORY 块 - 定义物理内存区域(逻辑分区)
MEMORY
{
名称 (属性) : ORIGIN = 起始地址, LENGTH = 大小
}
例如:
MEMORY
{
ITCMRAM (xrw) : ORIGIN = 0x00000000, LENGTH = 64K # 指令紧耦合RAM,64KB,可执行
DTCMRAM (xrw) : ORIGIN = 0x20000000, LENGTH = 128K # 数据紧耦合RAM,128KB
FLASH (rx) : ORIGIN = 0x08000000, LENGTH = 2048K # Flash存储器,2MB,只读可执行
RAM_D1 (xrw) : ORIGIN = 0x24000000, LENGTH = 512K # D1域RAM,512KB
RAM_D2 (xrw) : ORIGIN = 0x30000000, LENGTH = 288K # D2域RAM,288KB
RAM_D3 (xrw) : ORIGIN = 0x38000000, LENGTH = 64K # D3域RAM,64KB
BACKUP (xrw) : ORIGIN = 0x38800000, LENGTH = 4K # 备份RAM,4KB
}
其中属性说明:
r = read (可读)
w = write (可写)
x = execute (可执行)
a = allocatable (可分配)
i = initialized (已初始化)
l = load (同i)
! = 反转属性
(2)SECTIONS 块-定义段的放置规则
SECTIONS
{
段名称 :
{
内容规则
} >目标内存区域 AT>加载地址
}
例如:
SECTIONS
{
/* 段名 : {内容} >输出区域 */
.text :
{
/* 特殊符号 */
. = ALIGN(8); /* 当前位置对齐到8字节 */
_stext = .; /* 定义段开始符号 */
/* 输入段 */
*(.text) /* 所有文件的.text段 */
*(.text*) /* 所有以.text开头的段 */
*(.glue_7) /* ARM特定的粘合代码 */
*(.glue_7t)
KEEP(*(.init)) /* 保持段不被垃圾回收 */
KEEP(*(.fini))
. = ALIGN(4);
_etext = .; /* 定义段结束符号 */
} >ROM
}
1.通配符规则
SECTIONS
{
.text :
{
/* 文件通配符 */
main.o(.text) /* 特定文件的.text段 */
*(.text) /* 所有文件的.text段 */
lib*.a:(.text) /* 所有lib开头的库文件 */
/* 段名通配符 */
*(.text.*) /* 如 .text.startup, .text.exit */
*(.text.foo) /* 特定的.text.foo段 */
/* 排除规则 */
*(EXCLUDE_FILE(boot.o debug.o) .text) /* 排除特定文件 */
/* 排序规则 */
*(SORT(.text.*)) /* 按名称排序 */
*(SORT_BY_NAME(.text.*))
*(SORT_BY_ALIGNMENT(.text.*))
} >ROM
}
*(.text)
│
└── 表示"所有输入文件"
*(.text*)
│
└── 表示"所有以.text开头的段"
*(.rodata)
│ │
│ └── 段名:.rodata段
└──── 文件:所有输入文件
# 含义:将所有输入文件中的.rodata段放到这里
*(.rodata*)
│ │ │
│ │ └── 通配符:匹配.rodata开头的所有段
│ └──────── 段名前缀:.rodata
└──────────── 文件:所有输入文件
含义:将所有输入文件中以.rodata开头的段都放到这里
匹配:.rodata、.rodata.str1.4、.rodata.constants等
SECTIONS
{
.text :
{
/* 1. 特定文件的特定段 */
main.o(.text) /* 只要main.o文件的.text段 */
/* 2. 所有文件的特定段 */
*(.text) /* 所有文件的.text段 */
/* 3. 所有文件的匹配段 */
*(.text*) /* 所有文件中以.text开头的段 */
/* 匹配:.text、.text.startup、.text.exit等 */
/* 4. 多个段名 */
*(.text .text.*) /* .text段和所有.text.开头的段 */
/* 5. 特定库文件 */
libxxx.a:(.text) /* libxxx.a库中的.text段 */
/* 6. 库文件通配符 */
lib*.a:(.text) /* 所有lib开头的库文件的.text段 */
/* 7. 排除特定文件 */
*(EXCLUDE_FILE(boot.o debug.o) .text) /* 除了boot.o和debug.o外的所有.text段 */
/* 8. 特定文件的所有段 */
startup.o(*) /* startup.o的所有段 */
} >ROM
}
2. VMA和LMA(重要概念)
SECTIONS
{
/* VMA = Virtual Memory Address (运行地址) */
/* LMA = Load Memory Address (加载/存储地址) */
/* 方式1: AT关键字 */
.data :
{
*(.data)
} >RAM AT>ROM /* 运行在RAM,存储在ROM */
/* 方式2: AT()函数 */
.data : AT(0x08010000)
{
*(.data)
} >RAM
/* 方式3: 使用变量 */
.data : AT(_sidata)
{
_sdata = .; /* RAM中的起始地址 */
*(.data)
_edata = .; /* RAM中的结束地址 */
} >RAM
}
(3)特殊符号和内置函数
1. 位置计数器
SECTIONS
{
.text :
{
. = 0x1000; /* 设置当前地址 */
*(.text)
. = . + 0x100; /* 预留256字节空间 */
*(.rodata)
. = ALIGN(0x10); /* 对齐到16字节 */
} >ROM
}
2.内置函数
SECTIONS
{
.text :
{
/* 对齐函数 */
. = ALIGN(4); /* 对齐到4字节 */
. = ALIGN(0x100); /* 对齐到256字节 */
/* 地址函数 */
_data_lma = LOADADDR(.data); /* 获取.data段的LMA */
_text_size = SIZEOF(.text); /* 获取.text段大小 */
_ram_start = ORIGIN(RAM); /* 获取RAM起始地址 */
_ram_size = LENGTH(RAM); /* 获取RAM大小 */
/* 数学函数 */
_value = MAX(0x1000, 0x2000); /* 最大值 */
_value = MIN(0x1000, 0x2000); /* 最小值 */
_value = ABSOLUTE(-100); /* 绝对值 */
} >ROM
}
3.对齐
四、一个示例
下文是作者写的一个综合的 LD 文件示例
该LD文件来自于作者的一个项目,需要强制隔离推理段,以留出空间实现.
该示例不仅涵盖了基础的段隔离,还深入实践了VMA(运行地址)与LMA(加载地址)的分离配置。
这份脚本展示了一个包含 Bootloader + App (A/B分区 OTA) + AI 推理 (TensorFlow Lite Micro) + TLS (线程局部存储) 的复杂系统架构。通过阅读这个示例,你可以直观地看到:
Flash 分区规划:如何划分 Bootloader、Metadata、App A/B 区以及 AI 权重区。
内存隔离:如何利用 CCMRAM 存放 AI 推理数组,以及如何在特定 RAM 中保留 Boot Magic 标志位。
复杂段映射:如何处理
.data、.ccmram的 LMA 与 VMA 映射,以及多线程环境下的.tdata和.tbss段布局。
ENTRY(Reset_Handler)
MEMORY
{
RAM (xrw) : ORIGIN = 0x20000000, LENGTH = 127K
RESERVED_RAM (rw) : ORIGIN = 0x2001FC00, LENGTH = 1K
CCMRAM (xrw) : ORIGIN = 0x10000000, LENGTH = 64K /* 推理数组 */
BOOTLOADER (rx) : ORIGIN = 0x08000000, LENGTH = 32K /*BOOTLOADER引导区*/
METADATA (rx) : ORIGIN = 0x08008000, LENGTH = 16K /*OTA结构体*/
ERROR_MAIN (rx) : ORIGIN = 0x0800C000, LENGTH = 16K /*AB区均失效的备用运行*/
WEIGHTS (rx) : ORIGIN = 0x08010000, LENGTH = 64K /*权重文件*/
APP_A (rx) : ORIGIN = 0x08020000, LENGTH = 128K /*APP A区*/
APP_B (rx) : ORIGIN = 0x08040000, LENGTH = 128K /*APP B区*/
TFLM_FRAME (rx) : ORIGIN = 0x08060000, LENGTH = 128K /*AI推理*/
}
_estack = ORIGIN(RAM) + LENGTH(RAM); /*栈顶地址*/
_sstack = _estack - _Min_Stack_Size; /*栈底地址*/
_Min_Heap_Size = 0x200; /*最小堆大小*/
_Min_Stack_Size = 0x1000 ; /*最小栈大小*/
/*设计思路:函数跳转BL与数据访问LDR之间的障碍,经过查询BL 指令跳转范围是 ±16MB。*/
/*而 LDR指令的范围是 LDR R0, [PC, #imm],偏移量只有 ±4KB,因此会死机。避免“长跳转”惩罚*/
SECTIONS
{
.tflm_text 0x08060000 : AT(0x08060000) /*VMA == LMA。*/
{
_tflm_text_start = .;
. = ALIGN(4);
*libtflmicro.a:(.text .text.* .glue_7 .glue_7t)/*ARM 特有的“蹦床代码”,每个函数都是一个独立段*/
. = ALIGN(4);
_tflm_text_end = .;
} >TFLM_FRAME
/*链接器会自动把这段的起始地址,紧接在上一段(.tflm_text)的结束地址后面。*/
.tflm_rodata :
{
_tflm_rodata_start = .;
. = ALIGN(4);
*libtflmicro.a:(.rodata .rodata.* .constdata)
. = ALIGN(4);
_tflm_rodata_end = .;
} >TFLM_FRAME
.isr_vector :
{
KEEP(*(.isr_vector)) /*禁止优化*/
} >APP_A
.text :/*代码段*/
{
. = ALIGN(4);
*(EXCLUDE_FILE(*libtflmicro.a) .text)
*(EXCLUDE_FILE(*libtflmicro.a) .text*)
*(.glue_7)
*(.glue_7t)
*(.eh_frame)
KEEP (*(.init))
KEEP (*(.fini))
. = ALIGN(4);
_etext = .;
} >APP_A
/*这个是c++专用的全局对象的构造函数。*/
.init_array :
{
. = ALIGN(4);
PROVIDE(__init_array_start = .);
KEEP(*(.init_array))
KEEP(*(SORT(.init_array.*)))
. = ALIGN(4);
PROVIDE(__init_array_end = .);
} >APP_A
/*C++ 全局析构函数*/
.fini_array :
{
. = ALIGN(4);
KEEP(*(.fini_array))
KEEP(*(SORT(.fini_array.*)))
. = ALIGN(4);
} >APP_A
.rodata :/*只读数据段 const*/
{
. = ALIGN(4);
*(EXCLUDE_FILE(*libtflmicro.a) .rodata)
*(EXCLUDE_FILE(*libtflmicro.a) .rodata*)
. = ALIGN(4);
} >APP_A
.data : /*数据段 已初始化的变量*/
{
. = ALIGN(4);
_sdata = .;
*(.data)
*(.data*)
*(.RamFunc)
*(.RamFunc*)
. = ALIGN(4);
_edata = .;
} >RAM AT> APP_A /*AT就是指定加载地址也就是FLASH,真正要读写变量,要去RAM里找*/
_sidata = LOADADDR(.data);/*给启动文件指路,flash的起始点*/
.ccmram (NOLOAD) :
{
. = ALIGN(4);
_sccmram = .;
*(.ccmram)
*(.ccmram*)
. = ALIGN(4);
_eccmram = .;
} >CCMRAM
.bss : /*未初始化段 清零变量*/
{
. = ALIGN(4);
_sbss = .;
__bss_start__ = _sbss;
*(.bss)
*(.bss*)
*(COMMON)
. = ALIGN(4);
_ebss = .;
__bss_end__ = _ebss;
} >RAM
._user_heap_stack (NOLOAD) :/*NOLOAD 关键字这表示这个段不需要被加载。它不占用 Flash 空间,也不包含任何初始数据。它只是告诉链接器:“在 RAM 里给我划这么一块地盘,大小是 _Min_Heap_Size + _Min_Stack_Size,但别往里面写东西。”*/
{
. = ALIGN(8);
PROVIDE ( end = . );
PROVIDE ( _end = . );
. = . + _Min_Heap_Size;
. = . + _Min_Stack_Size;
. = ALIGN(8);
} >RAM
.Tensorflow_weights :
{
. = ALIGN(4);
KEEP(*(.Tensorflow_weights))
. = ALIGN(4);
} >WEIGHTS
.boot_magic (NOLOAD) :
{
. = ALIGN(4);
KEEP(*(.boot_magic))
. = ALIGN(4);
} >RESERVED_RAM
/DISCARD/ :
{
libc.a:* ( * )
libm.a:* ( * )
libgcc.a:* ( * )
}
}五、其他
KEEP(*(.init)),在现代编译器优化等级较高(如
-Os,-O2)且开启了垃圾回收(GC sections,--gc-sections)时,如果没有任何代码引用某个函数,链接器会把它删掉。赋值上的问题:
普通赋值
_start = .:强制定义符号。如果 C 代码里也定义了全局变量叫_start,链接器会报重定义错误 (Multiple Definition)。PROVIDE 赋值
PROVIDE(_start = .):弱定义。意思是“如果 C 代码里没定义这个符号,就用我这个地址;如果 C 代码里定义了,听 C 代码的,我不报错”。
对齐:
段内对齐:本文的对齐统指为段内对齐,即. = ALIGN(32); ,当遇到该语句时它会在段内部往后推(填充 0x00),直到当前地址是 32 的倍数
段间对齐:.data ALIGN(16) :链接器会先算出
.text结束后的地址,然后整体把.data段推到0x1010。段与段之间干干净净,没有浪费空间。
KEEP(*(SORT(.init_array.*))),它决定了 C++ 全局构造函数的执行顺序。
SORT(.init_array.*)会按段名字符串排序,确保先定义的构造函数先执行。如果去掉SORT,构造函数的调用顺序将是未定义的,可能导致依赖全局对象的代码在对象未初始化时就运行。示例项目中,本质上是将多个部分程序的编译都统一为一个LD文件,如果在大型项目中,可以使用INCLUDE sections.ld 这是工业级项目的标准做法。
在C语言代码中,可以使用attribute((section(".Tensorflow_weights"))) const uint8_t model_weights[] = { ... };把相关内容放进段中。
六、参考用书
《Linkers and Loaders》 (John R. Levine)
《Linker Scripts》 (GNU Binutils Documentation)
受限于个人水平,文中难免有理解不到位或笔误之处。恳请各位大佬不吝赐教,欢迎邮件交流:sharkey_z@163.com
