在 C 语言开发中,链接脚本(Linker Script)用于控制可执行文件或库的内存布局,尤其在嵌入式系统和底层开发中非常重要。
一、链接脚本的主要作用
1. 内存布局控制
指定代码段(.text)、数据段(.data)、BSS段(.bss)的存放位置
定义内存区域(RAM、ROM、Flash等)的起始地址和大小
2. 符号定义与管理
定义全局符号(如堆栈起始地址)
控制符号的可见性和导出
3. 特殊需求处理
启动代码的特定放置
中断向量表的精确定位
自定义段的创建和布局
二、基本语法结构
一个示例的链接配置文件如下:
换行复制代码
/* 最简单的链接脚本示例 */
MEMORY
{
ROM (rx) : ORIGIN = 0x08000000, LENGTH = 512K
RAM (rwx) : ORIGIN = 0x20000000, LENGTH = 128K
}
SECTIONS
{
/* 代码段 */
.text :
{
*(.vectors) /* 中断向量表优先 */
*(.text*) /* 所有代码 */
*(.rodata*) /* 只读数据 */
} > ROM
/* 初始化数据 */
.data : AT(ADDR(.text) + SIZEOF(.text))
{
_sdata = .; /* 数据段起始地址 */
*(.data*)
_edata = .; /* 数据段结束地址 */
} > RAM
/* 未初始化数据 */
.bss :
{
_sbss = .;
*(.bss*)
*(COMMON)
_ebss = .;
} > RAM
/* 堆栈定义 */
_estack = ORIGIN(RAM) + LENGTH(RAM);
}1. 注释
换行复制代码
/* 多行注释 */
// 单行注释(某些链接器支持)2. 命令格式
换行复制代码
命令名 参数 {
子命令;
符号 = 表达式;
}二、核心关键字
1. MEMORY - 定义内存区域
换行复制代码
MEMORY
{
名称 (属性) : ORIGIN = 起始地址, LENGTH = 长度
}属性说明:
r- 可读w- 可写x- 可执行a- 可分配i- 初始化l- 与i相同!- 反转属性
示例:
换行复制代码
MEMORY
{
ROM (rx) : ORIGIN = 0x0000, LENGTH = 256K
RAM (rwx) : ORIGIN = 0x4000, LENGTH = 64K
EEPROM (rw) : ORIGIN = 0xF000, LENGTH = 4K
}2. SECTIONS - 定义输出段
换行复制代码
SECTIONS
{
输出段名 [地址] : [AT(加载地址)]
{
内容描述
} [>区域] [AT>区域]
}3. 输出段内容描述符
输入段选择器
换行复制代码
*(.text) /* 所有文件的.text段 */
file.o(.data) /* 特定文件的.data段 */
*(.text.*) /* 匹配所有.text.开头的段 */特殊命令
换行复制代码
KEEP(*(.init)) /* 强制保留,即使未引用 */
PROVIDE(symbol = .) /* 定义符号,不重复定义 */
SORT(CONSTRUCTORS) /* 按名称排序 */三、位置计数器 (.)
基本用法
换行复制代码
. = 0x1000; /* 设置当前位置 */
. = ALIGN(4); /* 4字节对齐 */
. += 0x100; /* 当前位置向后移动256字节 */常用表达式
换行复制代码
. = DEFINED(symbol) ? symbol : 0x1000; /* 条件设置 */
. = ( . + 3 ) & ~3; /* 4字节对齐(手动) */四、符号定义
1. 赋值操作符
换行复制代码
symbol = expression; /* 简单赋值 */
symbol .= expression; /* 相对赋值 */
symbol += expression; /* 加法赋值 */
symbol -= expression; /* 减法赋值 */
symbol *= expression; /* 乘法赋值 */
symbol /= expression; /* 除法赋值 */
symbol <<= expression; /* 左移赋值 */
symbol >>= expression; /* 右移赋值 */
symbol |= expression; /* 或赋值 */
0symbol &= expression; /* 与赋值 */ 2. 特殊符号
换行复制代码
/* 预定义符号 */
__executable_start /* 程序起始地址 */
etext, _etext, etext /* .text段结束地址 */
edata, _edata /* .data段结束地址 */
end, _end /* BSS段结束地址 */
/* 自定义符号 */
_stack_start = .; /* 栈起始地址 */
_stack_size = 0x400; /* 栈大小 */
_heap_end = ORIGIN(RAM) + LENGTH(RAM); /* 堆结束地址 */ 五、函数和运算符
1. 地址相关函数
换行复制代码
ADDR(section) /* 获取段的运行时地址 */
LOADADDR(section) /* 获取段的加载地址 */
SIZEOF(section) /* 获取段的大小 */
ALIGN(exp) /* 对齐到exp的倍数 */
ALIGNOF(section) /* 获取段的对齐要求 */2. 符号相关函数
换行复制代码
DEFINED(symbol) /* 检查符号是否已定义 */
MAX(exp1, exp2) /* 取最大值 */
MIN(exp1, exp2) /* 取最小值 */3. 运算符优先级
1. () /* 括号 */ 2. ! ~ - + /* 一元运算符 */ 3. * / % /* 乘除 */ 4. + - /* 加减 */ 5. >> << /* 移位 */ 6. == != > < >= <= /* 关系运算 */ 7. & | ^ /* 位运算 */ 8. && || /* 逻辑运算 */ 9. ?: /* 条件运算 */ 10. = += -= *= /= /* 赋值 */
六、段属性控制
1. 段类型
换行复制代码
.text : { *(.text) } TYPE=PT_LOAD /* 可加载段 */
.rodata : { *(.rodata) } TYPE=PT_LOAD2. 段标志
换行复制代码
.data : { *(.data) } :data /* 使用内存区域属性 */七、条件语句
IF-ELSE 语句
换行复制代码
SECTIONS
{
.text : {
*(.text)
. = ALIGN(4);
IF (DEFINED(__USE_SOFT_FLOAT__)) {
*(.text.softfloat)
} ELSE {
*(.text.hardfloat)
}
} > ROM
} 八、包含指令
INCLUDE 指令
换行复制代码
/* 包含其他链接脚本 */
INCLUDE memory.ld
INCLUDE sections.ld
/* 条件包含 */
IF (MCU == "STM32F103")
INCLUDE stm32f103.ld
ELSE
INCLUDE generic.ld
ENDIF九、输出格式控制
OUTPUT_FORMAT
换行复制代码
OUTPUT_FORMAT("elf32-littlearm", "elf32-bigarm", "elf32-littlearm")
OUTPUT_ARCH(arm)
ENTRY(Reset_Handler) /* 指定入口点 */十、完整示例分解
换行复制代码
/* 1. 输出格式设置 */
OUTPUT_FORMAT("elf32-littlearm")
OUTPUT_ARCH(arm)
ENTRY(Reset_Handler)
/* 2. 内存区域定义 */
MEMORY
{
FLASH (rx) : ORIGIN = 0x08000000, LENGTH = 512K
RAM (rwx) : ORIGIN = 0x20000000, LENGTH = 128K
CCMRAM (rw) : ORIGIN = 0x10000000, LENGTH = 64K
}
/* 3. 段定义 */
SECTIONS
{
/* 中断向量表 - 必须放在起始位置 */
.isr_vector :
{
. = ALIGN(4);
KEEP(*(.isr_vector))
. = ALIGN(4);
} > FLASH
/* 代码段 */
.text :
{
*(.text) /* .text段(代码) */
*(.text.*) /* .text.*段 */
*(.glue_7) /* ARM/Thumb胶水代码 */
*(.glue_7t) /* Thumb/ARM胶水代码 */
*(.eh_frame) /* 异常处理框架 */
/* 构造函数 */
KEEP(*(.init))
KEEP(*(.fini))
. = ALIGN(4);
_etext = .; /* 代码段结束地址 */
} > FLASH
/* 只读数据 */
.rodata :
{
*(.rodata)
*(.rodata.*)
. = ALIGN(4);
} > FLASH
/* ARM特定段 */
.ARM.extab :
{
*(.ARM.extab*)
*(.gnu.linkonce.armextab.*)
} > FLASH
.ARM.exidx :
{
__exidx_start = .;
*(.ARM.exidx*)
*(.gnu.linkonce.armexidx.*)
__exidx_end = .;
} > FLASH
/* 预初始化数组 */
.preinit_array :
{
PROVIDE_HIDDEN(__preinit_array_start = .);
KEEP(*(.preinit_array*))
PROVIDE_HIDDEN(__preinit_array_end = .);
} > FLASH
/* 初始化数组 */
.init_array :
{
PROVIDE_HIDDEN(__init_array_start = .);
KEEP(*(SORT(.init_array.*)))
KEEP(*(.init_array*))
PROVIDE_HIDDEN(__init_array_end = .);
} > FLASH
/* 结束数组 */
.fini_array :
{
PROVIDE_HIDDEN(__fini_array_start = .);
KEEP(*(SORT(.fini_array.*)))
KEEP(*(.fini_array*))
PROVIDE_HIDDEN(__fini_array_end = .);
} > FLASH
/* 初始化数据(从Flash加载到RAM) */
_sidata = LOADADDR(.data);
.data :
{
. = ALIGN(4);
_sdata = .;
*(.data)
*(.data.*)
. = ALIGN(4);
_edata = .;
} > RAM AT > FLASH
/* 未初始化数据 */
.bss :
{
. = ALIGN(4);
_sbss = .;
*(.bss)
*(.bss.*)
*(COMMON)
. = ALIGN(4);
_ebss = .;
} > RAM
/* 用户堆栈 */
._user_heap_stack :
{
. = ALIGN(8);
PROVIDE(end = .);
PROVIDE(_end = .);
. = . + _Min_Heap_Size;
. = . + _Min_Stack_Size;
. = ALIGN(8);
} > RAM
/* 移除调试信息 */
/DISCARD/ :
{
libc.a(*)
libm.a(*)
libgcc.a(*)
}
/* 自定义段 */
.my_section :
{
__my_section_start = .;
KEEP(*(.my_section))
__my_section_end = .;
} > CCMRAM
/* 绝对符号定义 */
.abs_section :
{
__abs_start = ABSOLUTE(.);
*(.abs_section)
__abs_end = ABSOLUTE(.);
} > FLASH
}
/* 4. 全局符号定义 */
PROVIDE(_estack = ORIGIN(RAM) + LENGTH(RAM));
PROVIDE(_Min_Heap_Size = 0x200);
PROVIDE(_Min_Stack_Size = 0x400);
十一、调试技巧
1. 查看链接映射
换行复制代码
arm-none-eabi-ld -T script.ld -Map=output.map ...
//riscv-rtos
riscv32-unknown-elf-objdump -h RTOSDemo.elf | less
or
riscv32-unknown-elf-nm -n RTOSDemo.elf | less2. 常见错误处理
换行复制代码
/* 确保关键段不被优化 */
KEEP(*(.vector_table))
KEEP(*(.isr_vector))
/* 处理未使用段 */
/DISCARD/ : { *(.note.GNU-stack) }3. 版本兼容性
换行复制代码
/* GNU LD版本检查 */
__LD_VERSION__ = 0x23800; /* 2.56.0 */
/* 条件编译 */
VERSION {
CURRENT {
local: *;
};
}