链接脚本基础语法
目录
前言
ELF文件布局
链接脚本语法
段定义标准格式
地址计数器 .
地址计数器的动态特性
赋值 vs 引用
符号定义
通配符规则
COMMON块
COMMON 块的产生与处理
示例脚本
前言
由于嵌入式系统内存资源珍贵,链接脚本可指定代码段(.text )、只读数据段(.rodata) 、数据段(.data )、未初始化数据段(.bss )等在内存中的存储位置以实现内存布局管控;
链接脚本将多个目标文件(.o )中相同类型的段(如
.text
代码段、.data
已初始化数据段、.bss
未初始化数据段 )合并,构建统一可执行文件;链接脚本可以控制程序入口地址,定义初始化数据在启动时从 Flash 到 RAM 的搬移规则
ELF文件布局
代码段(.text): 存储可执行的机器指令数据段(.data): 存储已初始化的全局变量和静态变量只读数据段(.rodata): 存储不可修改的数据,比如字符串常量 const修饰的全局变量 常量BSS 段(.bss): 记录未初始化的全局变量和静态变量,不占用文件空间
链接脚本语法
SECTIONS {// 定义各个输出段及其内存布局
}
SECTIONS
是链接脚本的关键字,表示开始定义各个段的内存布局;- 大括号
{}
内包含所有段的配置信息;
段定义标准格式
段名 [地址] : { 段内容 }
- 段名:比如
.text
、.data、.rodata、.bss
等,标识段的类型;- [地址]:可选参数,指定具体段的起始地址(如
0x80000000
);- 冒号(
:
):必须存在,用于分隔 "段名 + 地址" 和 "段内容",是语法结构的核心标识;- 段内容:用大括号
{}
包裹,定义哪些输入段需要合并到当前段中(如*(.text*)
);
/* 示例脚本 */
SECTIONS {.text 0x08000000 : { *(.text*)}
}
以点开头的段名被定义为系统保留段,以点开头的段名由编译器和链接器 "预留",链接器会默认将以点开头的段名视为标准段,并应用特定的处理逻辑,如果省略点,可能导致链接器无法正确识别段的类型,用户自定义的段通常不以点开头(如
my_section
),从而避免与系统段名冲突;
my_section : { *(.my_section*) } // 用户自定义段名
地址计数器 .
. = 0x08000000; /* 设置当前地址计数器为 0x08000000 */
.text : {*(.text*) /* 所有 .text 输入段从这里开始放置 */
}
/* 链接器自动增加 . 的值到 .text 段末尾 */
地址计数器 . 的本质为变量,其值表示下一个将要被分配的地址(即下一个空闲地址),而非当前已使用的地址,地址计数器 . 始终指向尚未分配的内存位置;地址计数器决定当前正在描述的输出段会被放置到哪个具体地址;
地址计数器的动态特性
地址计数器 . 的值在链接脚本执行过程中会自动变化:
隐式增长: 当你将一个输入段(比如 .
text
, .data
)放入输出段时,链接器会自动将.
的值增加该输入段的大小,指向该段之后的下一个可用地址;显式设置: 可以通过赋值语句直接设置
.
的值(例如. = 0x08000000;
),强制将后续内容定位到特定地址;
赋值 vs 引用
. = expression; /* 赋值操作,直接设置 . 的值 */symbol = .; /* 定义符号 symbol 并将其值设置为当前 . 的值, 引用不会改变 . 本身 */
符号定义
symbol = expression;
symbol
:自定义的全局符号名称(通常以_
或__
开头避免冲突);
expression
:基于地址计数器.
、内存区域或其它符号的表达式;
@ 链接脚本文件
.data 0x80004000 : {__data_start = .; @ 记录数据段起始地址*(.data*) @ 合并所有.data*段( 非语句,无分号)__data_end = .; @ 记录数据段结束地址
}
链接脚本中定义的符号为强符号(Strong Symbol),不会被其他目标文件中的同名弱符号(Weak Symbol)覆盖;
链接脚本中定义的符号(__data_start 、__data_end)具有全局属性,可在C/C++代码中用extern声明或在汇编文件中直接使用;
@ ARM汇编文件
ldr r0, =__data_start @ 将链接脚本符号作为立即数加载
汇编器处理机制1. 符号引用: 汇编器遇到 __data_start 时,会将其视为未解析的符号;2. 生成重定位信息: 在目标文件中记录该符号需要链接器解析;3. 链接器解析: 链接阶段,链接器将 __start_start 替换为实际地址值;
链接脚本中定义的符号本身不携带类型信息,在 C/C++ 和汇编文件中,开发者可以通过选择适当的类型声明来安全地使用这些符号;
extern uint8_t __data_start;
extern void* __data_start;
通配符规则
星号
*
:匹配任意字符序列
- 作用:代替 “任意长度的任意字符”,用于匹配不确定的段名前缀、后缀或中间部分。
- 示例:
*(.text)
:匹配所有输入文件中名称以.text
开头的段(如.text
、.text.init
、.text.fini
等子段 ),常用于合并所有代码段。*.o(.data)
:匹配所有文件名以.o
结尾的目标文件中的.data
段,精准筛选特定文件的段
注意:
- “动” 则加分号:凡是改变地址(
. = ...
)或定义符号(__symbol = ...
)的 "动作",末尾必须加分号;- “静” 则无分号:单纯 “选择段”(
*(.text*)
)或 “引用内容” 的表达式,无需分号;
COMMON块
.bss : {__bss_start = .;*(.bss*) @ 合并目标文件中的 .bss 段(已分配空间的未初始化变量)*(COMMON) @ 合并目标文件中的 COMMON 块(未分配空间的未初始化变量)__bss_end = .;
}
早期 C 语言编译器中,对于 未初始化的全局变量,编译器不会为其分配固定大小的内存空间,而是将其标记为一个 "未定义的符号",并记录其类型和期望的大小,此类符号在目标文件(.o文件)中会被组织成 COMMON 块;
COMMON 块的产生与处理
1. 示例程序
// file1.c
int global_var; // 未初始化全局变量,生成 COMMON 块
// file2.c
int global_var; // 同名未初始化全局变量,另一个 COMMON 块
2. 编译后的目标文件
file1.o
和file2.o
中各有一个COMMON
块,只记录global_var
为int
类型(4 字节);- 每个目标文件中的
COMMON
块不占用实际空间,仅记录符号信息
3. 链接时的处理
- 链接器遇到
*(COMMON)
时,将两个目标文件的COMMON
块合并,为global_var
分配 4 字节空间,放入 .bss 段;- 最终 .bss 段中只有一个
global_var
变量,避免重复定义错误;
注意:
- 如果省略
*(COMMON)
,可能导致未初始化变量无法被正确处理,所以链接脚本中必须写*(COMMON)
;
- 为确保完整收集不同编译器生成的未初始化变量,避免因遗漏导致的链接错误或运行时内存错误,因此在链接脚本的
.bss
段中必须同时合并.bss*
和COMMON
块;
示例脚本
SECTIONS {.text 0x80000000 : {*(.text*)}.rodata : {*(.rodata*)}.data 0x80004000 : {__data_start = .;*(.data*)__data_end = .;}.bss : {__bss_start = .;*(.bss*)*(COMMON)__bss_end = .;}.stack 0x80200000 : {. = . + 0x40000; @ 分配256KB栈空间__stack_top = .; @ 栈顶地址}
}