1. ARM嵌入式开发中的链接脚本基础
在嵌入式系统开发中,链接脚本(Linker Script)扮演着至关重要的角色。它就像一位精明的城市规划师,决定着我们程序代码和数据在内存中的布局方式。不同于桌面应用开发,嵌入式系统往往面临严格的内存限制和特殊的硬件约束,这使得链接脚本成为嵌入式开发者必须掌握的技能。
1.1 链接脚本的作用与重要性
链接脚本的核心作用是控制链接器(Linker)如何将输入的目标文件(.o文件)中的各个段(Section)合并、组织到最终的可执行文件中。在ARM嵌入式开发中,这种控制尤为重要,原因有三:
- 内存资源有限:嵌入式设备通常只有几十KB到几MB的内存,需要精确控制每个段的位置和大小
- 非统一内存架构:许多ARM芯片采用哈佛架构,代码存储器(Flash)和数据存储器(RAM)物理分离
- 启动过程特殊:需要手动初始化.data段(从Flash复制到RAM)和清零.bss段
一个典型的ARM嵌入式项目中,链接脚本需要处理的主要段包括:
- .text:存放程序代码
- .rodata:只读数据
- .data:已初始化的全局/静态变量
- .bss:未初始化的全局/静态变量(运行时清零)
- .heap:动态内存分配区域
- .stack:函数调用栈空间
1.2 ELF文件格式基础
要理解链接脚本,必须先了解ELF(Executable and Linkable Format)文件格式的基本结构。ELF文件由以下几部分组成:
- ELF Header:描述文件的基本属性(目标架构、入口地址等)
- Program Headers(段头表):描述运行时如何加载程序(对操作系统重要)
- Section Headers(节头表):描述文件中各个节的信息(对链接器重要)
- 实际节数据:包含代码、数据等实际内容
在链接过程中,链接器主要关注的是节(Section),而在程序加载执行时,加载器关注的是段(Segment)。一个段可以包含多个节,这种映射关系正是由链接脚本中的PHDRS命令控制的。
提示:在嵌入式开发中,我们经常需要查看ELF文件的结构。可以使用
arm-none-eabi-objdump -h查看节信息,-p查看程序头信息。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. ARM工具链中的链接脚本语法详解
2.1 基本命令结构
ARM工具链使用的链接器(通常是armlink或ld.lld)支持丰富的链接脚本语法。一个完整的链接脚本通常包含以下几个部分:
ld复制/* 注释风格与C语言相同 */
ENTRY(Reset_Handler) /* 指定程序入口点 */
MEMORY {
/* 定义内存区域 */
FLASH (rx) : ORIGIN = 0x08000000, LENGTH = 512K
RAM (rwx) : ORIGIN = 0x20000000, LENGTH = 128K
}
SECTIONS {
/* 定义输出段的布局 */
.text : {
*(.vectors) /* 中断向量表 */
*(.text*) /* 所有.text开头的节 */
} > FLASH
/* 更多段定义... */
}
2.2 ENTRY命令:设置程序入口点
ENTRY命令用于指定程序的入口点,也就是芯片复位后执行的第一条指令所在的符号。在ARM Cortex-M系列中,这通常是中断向量表中的Reset_Handler。
ld复制ENTRY(Reset_Handler)
重要说明:
- 入口点符号必须在某个目标文件中定义
- 可以通过链接器选项
-e覆盖脚本中的ENTRY设置 - 如果没有指定ENTRY,链接器会尝试使用符号
_start作为默认入口
在Cortex-M中,向量表的第一个字是初始栈指针值,第二个字就是Reset_Handler的地址,因此ENTRY的设置必须与向量表一致。
2.3 MEMORY命令:定义物理内存布局
MEMORY命令定义了目标设备上的物理内存区域及其属性,这是嵌入式链接脚本区别于通用系统链接脚本的关键所在。
ld复制MEMORY {
FLASH (rx) : ORIGIN = 0x08000000, LENGTH = 512K
RAM (rwx) : ORIGIN = 0x20000000, LENGTH = 128K
CCRAM (rw) : ORIGIN = 0x10000000, LENGTH = 64K /* 核心耦合内存 */
}
每个内存区域可以指定以下属性:
r:可读w:可写x:可执行a:可分配!:否定后面的属性(如!w表示不可写)
内存区域的定义直接影响后续SECTIONS命令中段的分配。在实际项目中,我们需要根据芯片的数据手册准确填写这些地址和大小。
2.4 SECTIONS命令:控制段布局
SECTIONS命令是链接脚本的核心,它定义了输入节如何组合成输出段,以及这些段在内存中的位置。
基本语法结构:
ld复制SECTIONS {
.section_name [address] [(type)] : [AT(lma)]
{
section_commands
} >region [:phdr] [=fillexp]
}
关键概念:
- VMA(Virtual Memory Address):段运行时所在的虚拟地址
- LMA(Load Memory Address):段在加载时所在的物理地址
- >region:指定段应该放置在哪个内存区域
- AT(lma):显式指定LMA地址(常用于将.data段初始值存放在Flash中)
一个典型的.text段定义示例:
ld复制.text : {
/* 确保向量表位于最前面 */
KEEP(*(.vectors))
/* 所有.text节 *
