嵌入式开发必备:手把手教你编写.lds链接脚本(附内存对齐技巧)
如果你在嵌入式领域摸爬滚打了一段时间,大概率已经不止一次被链接脚本(.lds文件)搞得头疼过。这东西平时不显山露水,但一旦出了问题,往往就是那种最难排查的“玄学”问题:程序莫名其妙跑飞、变量地址错乱、甚至代码直接无法启动。很多开发者习惯性地从网上找个现成的脚本改改就用,直到项目遇到内存紧张、需要精细控制布局,或者要搞一些高级功能(比如自定义初始化顺序、多段内存分配)时,才猛然发现,自己对链接脚本的理解还停留在“知道有这么个东西”的层面。
链接脚本远不止是告诉链接器“代码放哪里,数据放哪里”那么简单。它本质上是你与硬件内存布局、编译器输出之间的一份契约。尤其在资源受限的裸机开发、RTOS移植、Bootloader编写等场景下,一份精心设计的链接脚本,往往是系统稳定、高效运行的基础。它能帮你解决内存碎片化、优化启动速度、实现固件升级、甚至构建模块化的软件架构。今天,我们就抛开那些枯燥的语法手册,从工程实践的角度,深入聊聊如何编写一份既健壮又高效的链接脚本,并重点剖析那些容易被忽略但至关重要的内存对齐技巧。
1. 理解链接脚本:不只是“地址映射表”
在深入语法之前,我们必须先建立正确的认知:链接脚本(Linker Script)是GNU链接器(ld)的“配置文件”,它精确控制了从多个目标文件(.o)生成最终可执行文件(.elf/.bin)的整个过程。这个过程的核心是段(Section)的合并与放置。
1.1 程序在内存中的“解剖图”
一个典型的嵌入式C程序,编译后会生成几个核心的段:
- .text段:存放所有可执行的机器指令代码。这是只读的。
- .rodata段:存放只读数据,比如字符串常量、
const修饰的全局变量。 - .data段:存放已初始化且初值非零的全局变量和静态变量。这部分数据在程序加载时需要从非易失性存储器(如Flash)拷贝到RAM中。
- .bss段:存放未初始化或初始化为零的全局变量和静态变量。在程序启动时,需要将这片内存区域清零。
- .stack段:栈空间,用于函数调用、局部变量等。
- .heap段:堆空间,用于动态内存分配(如
malloc)。
链接脚本的任务,就是告诉链接器:“请把来自所有输入文件(.o)的.text段收集起来,拼成一个大的.text段,放到内存地址0x80000000开始的地方;把所有的.data段拼起来,放到0x20000000开始的地方,但记得它的加载地址(LMA)要设置在Flash里……”
1.2 一个最简链接脚本的深度拆解
让我们从一个最简单的、用于ARM Cortex-M内核的链接脚本开始,逐行分析其含义和潜在陷阱。
/* 1. 指定入口点:告诉CPU从哪里开始执行 */
ENTRY(Reset_Handler)
/* 2. 定义内存区域:描述硬件上的物理内存布局 */
MEMORY
{
FLASH (rx) : ORIGIN = 0x08000000, LENGTH = 512K
RAM (xrw) : ORIGIN = 0x20000000, LENGTH = 128K
}
/* 3. SECTIONS命令:核心,定义输出文件的段布局 */
SECTIONS
{
/* 3.1 初始化栈顶指针(通常由启动代码使用) */
/* 这是一个符号赋值,不是段定义 */
_estack = ORIGIN(RAM) + LENGTH(RAM);
/* 3.2 .isr_vector段:中断向量表,必须放在最前面 */
.isr_vector :
{
. = ALIGN(4);
KEEP(*(.isr_vector)) /* KEEP确保即使未被引用也不会被优化掉 */
. = ALIGN(4);
} >FLASH /* 将此段放置在FLASH内存区域 */
/* 3.3 .text段:程序代码 */
.text :
{
. = ALIGN(4);
*(.text) /* 所有输入文件的.text段 */
*(.text*) /* 匹配所有以.text开头的段,如.text.function_name */
*(.glue_7) /* 某些编译器生成的ARM/Thumb交互代码 */
*(.glue_7t)
*(.eh_frame)
KEEP (*(.init))
KEEP (*(.fini))
. = ALIGN(4);
_etext = .; /* 定义一个符号,记录.text段的结束地址 */
} >FLASH
/* 3.4 .rodata段:只读数据 */
.rodata :
{
. = ALIGN(4);
*(.rodata)
*(.rodata*)
. = ALIGN(4);
} >FLASH
/* 关键概念:LMA与VMA的分离 */
/* .data段的内容在Flash中(LMA),但运行时在RAM中(VMA) */
/* 启动代码需要将.data段从Flash拷贝到RAM */
_sidata = LOADADDR(.data); /* 获取.data段在Flash中的加载地址(LMA) */
.data : AT ( _sidata ) /* AT指定加载地址为_sidata (在Flash中) */
{
. = ALIGN(4);
_sdata = .; /* 在RAM中的起始地址(VMA) */
*(.data)
*(.data*)
. = ALIGN(4);
_edata = .; /* 在RAM中的结束地址(VMA) */
} >RAM /* 运行时地址(VMA)在RAM中 */
/* 3.6 .bss段:未初始化数据,启动代码需要将其清零 */
.bss :
{
. = ALIGN(4);
_sbss = .;
*(.bss)
*(.bss*)
*(COMMON) /* COMMON段存放未初始化的全局变量 */
. = ALIGN(4);
_ebss = .;
} >RAM
/* 3.7 用户堆栈设置(可选,更常见的做法是在启动文件定义) */
/* ._user_heap_stack :
{
. = ALIGN(8);
PROVIDE ( e

6806

被折叠的 条评论
为什么被折叠?



