嵌入式预处理链接脚本lds和map文件
在嵌入式开发中,.lds.S
文件是一个 预处理后的链接脚本(Linker Script),它结合了 C 预处理器(Preprocessor) 的功能和链接脚本的语法。它的核心作用仍然是 定义内存布局和链接规则,但通过预处理器的特性(如宏定义、条件编译、文件包含等)使得链接脚本更加灵活和可配置。以下是详细解析:
1. .lds.S
文件的本质
- 核心作用:与普通
.ld
文件相同,用于控制代码和数据的内存分配,定义符号地址等。 - 特殊之处:文件扩展名
.S
表示这是一个 需要预处理的链接脚本(类似汇编文件.S
需要预处理后再汇编)。 - 处理流程:
- 预处理阶段:通过 C 预处理器(如
cpp
)处理.lds.S
文件,展开宏、处理条件编译指令(#ifdef
、#define
)等。 - 生成纯链接脚本:预处理后生成一个标准的
.ld
文件。 - 链接阶段:链接器(如
ld
)使用生成的.ld
文件完成内存分配。
- 预处理阶段:通过 C 预处理器(如
2. .lds.S
文件的典型内容
以下是一个简化示例,展示 .lds.S
文件如何利用预处理器的特性:
/* 使用 C 预处理器定义宏 */
#define FLASH_BASE 0x08000000
#define FLASH_SIZE 512K
#define RAM_BASE 0x20000000
#define RAM_SIZE 128K#ifdef USE_EXTERNAL_RAM#define RAM2_BASE 0xD0000000#define RAM2_SIZE 1M
#endifMEMORY {FLASH (rx) : ORIGIN = FLASH_BASE, LENGTH = FLASH_SIZERAM (rwx) : ORIGIN = RAM_BASE, LENGTH = RAM_SIZE#ifdef USE_EXTERNAL_RAMEXTRAM (rwx) : ORIGIN = RAM2_BASE, LENGTH = RAM2_SIZE#endif
}SECTIONS {.text : {*(.text*)} > FLASH/* 条件编译:仅在启用外部 RAM 时分配特定段 */#ifdef USE_EXTERNAL_RAM.external_data : {*(.external_data*)} > EXTRAM#endif
}
3. 为什么需要 .lds.S
文件?
(1) 动态配置内存布局
- 场景:同一份代码需适配不同硬件版本(如芯片内置 RAM 大小不同)。
- 解决方案:通过预处理器宏(如
#ifdef
)动态选择内存区域定义。#ifdef CHIP_V2#define RAM_SIZE 256K #else#define RAM_SIZE 128K #endif
(2) 代码复用
- 场景:多个项目共享相似的链接脚本逻辑,但细节不同(如不同厂商的芯片)。
- 解决方案:使用
#include
包含公共部分,差异化部分通过宏定义。#include "common_memory_layout.ld" #define CUSTOM_HEAP_SIZE 0x2000
(3) 简化复杂条件
- 场景:根据编译选项(如调试模式)调整内存分配。
- 解决方案:通过预处理器启用或禁用特定段。
#ifdef DEBUG.debug_logs : {*(.debug_logs*)} > RAM #endif
4. .lds.S
文件与汇编文件(.S)的区别
特性 | .lds.S(预处理链接脚本) | .S(汇编文件) |
---|---|---|
文件类型 | 链接脚本(经过预处理) | 汇编代码(经过预处理) |
处理工具 | C 预处理器 → 链接器 | C 预处理器 → 汇编器 → 链接器 |
核心内容 | 内存区域定义、段分配规则 | 汇编指令(如 MOV , B )、硬件操作 |
作用阶段 | 链接阶段 | 编译阶段(生成机器码) |
典型指令 | MEMORY , SECTIONS , #include | .section , .global , MOV |
5. 实际使用场景示例
场景:为不同芯片生成不同链接脚本
-
目录结构:
project/ ├── linker/ │ ├── stm32f4.lds.S # STM32F4 的链接脚本模板 │ └── stm32h7.lds.S # STM32H7 的链接脚本模板 ├── Makefile └── src/└── main.c
-
预处理生成最终链接脚本:
# Makefile 示例 CHIP ?= stm32f4# 根据芯片选择模板 LINKER_SCRIPT = linker/$(CHIP).lds.S# 预处理生成 .ld 文件 %.ld: %.lds.S$(CC) -E -P -x c $< -o $@
-
编译时指定芯片型号:
# 编译 STM32F4 版本 make CHIP=stm32f4# 编译 STM32H7 版本 make CHIP=stm32h7
6. 常见问题
Q1:.lds.S
文件需要手动预处理吗?
- 答案:通常由构建系统(如 Makefile、CMake)自动处理。例如,在 Makefile 中使用
gcc -E
预处理生成.ld
文件。
Q2:能否在 .lds.S
中混合汇编代码?
- 答案:不能。
.lds.S
本质仍是链接脚本,预处理后生成的是纯链接脚本(.ld
),不含汇编指令。
Q3:如何调试 .lds.S
文件?
- 方法:
- 查看预处理后的
.ld
文件,确认宏展开是否符合预期。 - 结合
map
文件验证内存分配结果。
- 查看预处理后的
总结
-
.lds.S
文件:
本质是 增强版的链接脚本,通过预处理器实现动态配置,但不包含汇编代码功能。它是为了解决复杂内存布局的灵活性问题而设计的。 -
与汇编文件的关系:
两者完全独立:.S
文件实现代码逻辑(如启动代码、中断处理)。.lds.S
文件控制代码和数据的内存布局。
-
典型应用:
多硬件平台适配、条件内存分配、复杂项目配置。
以下是关于 .lds.S
文件语法规则的详细解析,涵盖其核心语法、预处理指令的使用以及实际编写技巧。通过示例和分类说明,帮助你快速掌握如何阅读、编写和修改这类文件。
一、.lds.S
文件的核心语法
.lds.S
文件本质是 链接脚本(Linker Script) 与 C 预处理器 的结合,因此其语法包含两部分:
- 链接脚本语法:定义内存布局、段分配规则。
- C 预处理器语法:通过
#define
,#include
,#ifdef
等指令实现动态配置。
二、链接脚本核心语法详解
1. 内存区域定义(MEMORY
)
- 作用:定义物理内存的地址范围和属性。
- 语法:
MEMORY {<名称> (<属性>) : ORIGIN = <起始地址>, LENGTH = <长度> }
- 属性说明:
r
:可读(Readable)w
:可写(Writable)x
:可执行(Executable)a
:可分配(Allocatable)l
:已初始化(Initialized)!
:取反(如!w
表示不可写)
- 示例:
MEMORY {FLASH (rx) : ORIGIN = 0x08000000, LENGTH = 512K /* Flash 用于存储代码 */RAM (rwx) : ORIGIN = 0x20000000, LENGTH = 128K /* RAM 用于运行时数据 */ }
2. 段分配规则(SECTIONS
)
- 作用:将输入文件(
.o
)中的段分配到输出文件(.elf
/.bin
)的指定内存区域。 - 语法:
SECTIONS {<段名> [<地址约束>] : {<输入段匹配规则>} [> <内存区域>] [AT> <加载地址>] }
- 关键指令:
*(.text*)
:匹配所有以.text
开头的段(如.text
,.text.*
)。KEEP(*(.isr_vector))
:防止未使用的段被链接器丢弃。. = ALIGN(4);
:将当前位置对齐到 4 字节边界。PROVIDE(<符号> = <表达式>);
:定义符号(避免重复定义冲突)。
- 示例:
SECTIONS {.isr_vector : {KEEP(*(.isr_vector)) /* 保留中断向量表 */} > FLASH.text : {*(.text*) /* 所有代码段 */*(.rodata*) /* 只读数据段 */} > FLASH.data : {_sdata = .; /* 记录数据段起始地址 */*(.data*)_edata = .; /* 记录数据段结束地址 */} > RAM AT > FLASH /* 运行时在 RAM,存储时在 FLASH */.bss : {_sbss = .;*(.bss*)_ebss = .;} > RAM }
3. 符号定义与引用
- 作用:定义全局符号,供程序或启动代码使用。
- 语法:
<符号> = <表达式>;
- 示例:
_estack = ORIGIN(RAM) + LENGTH(RAM); /* 定义堆栈顶为 RAM 末尾 */
三、C 预处理器语法详解
1. 宏定义(#define
)
- 作用:定义常量或表达式,简化重复配置。
- 示例:
#define FLASH_BASE 0x08000000 #define FLASH_SIZE 512K
2. 条件编译(#ifdef
, #if
, #endif
)
- 作用:根据条件动态包含或排除代码块。
- 示例:
#ifdef DEBUG.debug_logs : {*(.debug_logs*)} > RAM #endif
3. 文件包含(#include
)
- 作用:复用其他链接脚本片段。
- 示例:
#include "common_memory.ld"
4. 宏展开与拼接
- 作用:动态生成符号或段名。
- 示例:
#define REGION(name, base, size) \name (rwx) : ORIGIN = base, LENGTH = sizeMEMORY {REGION(FLASH, 0x08000000, 512K)REGION(RAM, 0x20000000, 128K) }
四、实际编写技巧与示例
1. 多芯片适配
通过宏定义区分不同芯片的内存配置:
#ifdef CHIP_STM32F4#define FLASH_BASE 0x08000000#define FLASH_SIZE 512K#define RAM_BASE 0x20000000#define RAM_SIZE 128K
#elif defined(CHIP_STM32H7)#define FLASH_BASE 0x08000000#define FLASH_SIZE 2M#define RAM_BASE 0x24000000#define RAM_SIZE 512K
#endifMEMORY {FLASH (rx) : ORIGIN = FLASH_BASE, LENGTH = FLASH_SIZERAM (rwx) : ORIGIN = RAM_BASE, LENGTH = RAM_SIZE
}
2. 动态调整堆和栈大小
#define HEAP_SIZE 0x2000
#define STACK_SIZE 0x1000SECTIONS {.heap : {. = ALIGN(8);_sheap = .;. += HEAP_SIZE;_eheap = .;} > RAM.stack : {. = ALIGN(8);_estack = .;. += STACK_SIZE;} > RAM
}
3. 处理外部内存
#ifdef USE_EXTERNAL_SRAMMEMORY {EXTRAM (rwx) : ORIGIN = 0x60000000, LENGTH = 1M}SECTIONS {.external_data : {*(.external_data*)} > EXTRAM}
#endif
五、调试与验证
1. 预处理后生成 .ld
文件
在终端中手动预处理 .lds.S
文件(以 GCC 为例):
gcc -E -P -x c -I. your_linker.lds.S -o output.ld
-E
:运行预处理器。-P
:禁止生成行标记(#line
指令)。-x c
:强制按 C 语言处理文件(即使扩展名不是.c
)。
2. 分析 map
文件
编译后生成的 map
文件会显示:
- 各段的起始地址和大小。
- 符号的最终地址。
- 内存区域的使用情况。
3. 常见错误排查
- 未定义的符号:检查链接脚本中是否正确定义符号(如
_estack
)。 - 段未分配:确认链接脚本中是否将段分配到内存区域。
- 内存溢出:通过
map
文件检查各段结束地址是否超出内存区域大小。
六、完整示例:.lds.S
文件模板
#include "chip_config.h" /* 包含芯片配置宏(如 CHIP_STM32F4) *//* 定义内存基址和大小 */
#ifdef CHIP_STM32F4#define FLASH_BASE 0x08000000#define FLASH_SIZE 512K#define RAM_BASE 0x20000000#define RAM_SIZE 128K
#elif defined(CHIP_STM32H7)#define FLASH_BASE 0x08000000#define FLASH_SIZE 2M#define RAM_BASE 0x24000000#define RAM_SIZE 512K
#endif/* 定义堆和栈大小 */
#define HEAP_SIZE 0x2000
#define STACK_SIZE 0x1000MEMORY {FLASH (rx) : ORIGIN = FLASH_BASE, LENGTH = FLASH_SIZERAM (rwx) : ORIGIN = RAM_BASE, LENGTH = RAM_SIZE
}SECTIONS {/* 中断向量表必须位于 Flash 起始位置 */.isr_vector : {KEEP(*(.isr_vector))} > FLASH/* 代码段和只读数据 */.text : {*(.text*)*(.rodata*)} > FLASH/* 初始化数据(从 Flash 加载到 RAM) */.data : {_sdata = .;*(.data*)_edata = .;} > RAM AT > FLASH/* 未初始化数据 */.bss : {_sbss = .;*(.bss*)_ebss = .;} > RAM/* 堆和栈 */.heap : {. = ALIGN(8);_sheap = .;. += HEAP_SIZE;_eheap = .;} > RAM.stack : {. = ALIGN(8);_estack = .;. += STACK_SIZE;} > RAM/* 调试信息(仅在 DEBUG 模式下保留) */#ifdef DEBUG.debug_logs : {*(.debug_logs*)} > RAM#endif
}
七、总结
.lds.S
文件 = 链接脚本 + 预处理器。- 核心能力:通过宏和条件编译实现动态内存布局。
- 调试关键:预处理后检查生成的
.ld
文件,结合map
文件验证内存分配。 - 进阶技巧:利用预处理器实现复杂逻辑(如多级宏、文件包含)。
掌握这些规则后,你可以灵活地根据项目需求定制内存布局,适配不同硬件平台或编译配置。
好的,我来详细解释你提供的链接脚本中的 SECTIONS
部分,尤其是 = .;
这种语法的含义和作用。这段代码是典型的链接脚本段分配规则,用于控制程序的内存布局。以下是逐行解析:
1. .isr_vector
段:中断向量表
.isr_vector : {KEEP(*(.isr_vector)) /* 强制保留中断向量表 */
} > FLASH
- 作用:将输入文件(如
.o
文件)中的.isr_vector
段合并到输出文件的.isr_vector
段,并强制保留(即使未引用)。 KEEP
:防止链接器优化时丢弃未显式使用的段(中断向量表可能不会被代码直接引用,但必须保留)。> FLASH
:将该段分配到FLASH
内存区域(即代码存储器)。
2. .text
段:代码和只读数据
.text : {*(.text*) /* 所有以 .text 开头的段(如函数代码) */*(.rodata*) /* 所有以 .rodata 开头的段(如常量字符串) */
} > FLASH
- 作用:将代码(
.text*
)和只读数据(.rodata*
)合并到.text
段,并分配到FLASH
。 *(.text*)
:通配符匹配所有输入文件的.text
段(如main.o(.text)
、lib.o(.text)
)。> FLASH
:代码段存储在 Flash 中(不可写,但可执行)。
3. .data
段:已初始化的全局/静态变量
.data : {_sdata = .; /* 记录 .data 段的起始地址 */*(.data*) /* 所有以 .data 开头的段 */_edata = .; /* 记录 .data 段的结束地址 */
} > RAM AT > FLASH /* 运行时在 RAM,存储时在 FLASH */
关键点解析
-
_sdata = .;
和_edata = .;
:.
是 定位计数器(Location Counter),表示当前段的地址位置。_sdata
和_edata
是符号,分别记录.data
段的起始和结束地址。- 这些符号会在程序启动时被用来从 Flash 复制数据到 RAM(通过启动代码)。
-
> RAM AT > FLASH
:- 运行时地址(VMA):
.data
段在运行时位于 RAM(程序直接访问的地址)。 - 加载地址(LMA):
.data
段的初始值存储在 Flash 中,上电后需由启动代码将其复制到 RAM。
- 运行时地址(VMA):
为什么需要这样做?
- 已初始化的全局变量(如
int x = 42;
)的初始值必须存储在 Flash(非易失存储器),但运行时需要可写,因此需复制到 RAM。
4. .bss
段:未初始化的全局/静态变量
.bss : {_sbss = .; /* 记录 .bss 段的起始地址 */*(.bss*) /* 所有以 .bss 开头的段 */_ebss = .; /* 记录 .bss 段的结束地址 */
} > RAM /* 运行时在 RAM */
- 作用:将未初始化的全局变量(如
int y;
)合并到.bss
段,分配到 RAM。 _sbss
和_ebss
:记录.bss
段的地址范围,启动代码会将其清零(未初始化的变量默认值为 0)。> RAM
:.bss
段仅存在于 RAM(无需存储初始值到 Flash)。
关于 = .;
的深入解释
-
.
的含义:.
是链接脚本中的 当前地址计数器,表示当前段的位置。- 在段定义过程中,
.
会自动递增以反映段的大小。
-
_sdata = .;
的作用:- 在
.data
段开始时,将当前地址(即.data
段的起始地址)赋值给符号_sdata
。 - 类似地,
_edata = .;
将.data
段结束后的地址赋值给_edata
。
- 在
-
符号的用途:
- 这些符号(如
_sdata
、_edata
、_sbss
、_ebss
)会被启动代码引用,用于初始化数据段和清零 BSS 段。 - 例如,在启动文件(
.S
)中,通过以下代码复制.data
段:/* 从 Flash 的 LMA 复制到 RAM 的 VMA */ ldr r0, =_sdata /* RAM 目标地址(VMA) */ ldr r1, =_edata ldr r2, =_sidata /* Flash 中的初始值地址(LMA) */ copy_loop:cmp r0, r1beq copy_doneldr r3, [r2], #4str r3, [r0], #4b copy_loop copy_done:
- 这些符号(如
内存布局示意图
FLASH (存储) RAM (运行时)
+----------------+ +----------------+
| .isr_vector | | |
| .text | | |
| .rodata | | |
| .data (初始值) | ---复制-->| .data |
| | | .bss (清零后) |
+----------------+ +----------------+
常见问题解答
1. 为什么 .data
段需要 AT > FLASH
?
- 因为已初始化变量的初始值必须存储在非易失存储器(Flash)中,但运行时需要可写,因此需在启动时从 Flash 复制到 RAM。
2. _sdata
和 _edata
是如何被程序使用的?
- 这些符号会被编译器和启动代码引用,用于定位数据段的位置,完成初始化操作。例如:
extern uint32_t _sdata, _edata, _sidata; // _sidata 是 Flash 中存储 .data 初始值的地址(需在链接脚本中定义)
3. 如果省略 KEEP
会怎样?
- 如果中断向量表未被其他代码显式引用,链接器可能将其优化删除,导致程序无法正确响应中断。
4. 如何验证符号地址是否正确?
- 查看生成的
map
文件,搜索_sdata
、_edata
等符号,确认它们的地址是否符合预期。
总结
.
的作用:动态记录当前段的地址位置,用于符号赋值。- 符号赋值(如
_sdata = .;
):为启动代码提供关键地址信息,完成数据初始化和内存清零。 > RAM AT > FLASH
:分离运行地址和加载地址,解决非易失存储与可写内存的矛盾。
理解这些细节后,你可以根据具体芯片的内存布局,灵活调整链接脚本,确保程序正确运行。
是的,你的理解完全正确!.data
段的内容会完整地从 Flash 复制到 RAM 中,且复制的数据量严格等于 .data
段的大小。这是嵌入式系统启动过程中的关键步骤之一。以下是详细的流程解析:
1. 数据复制的机制
(1) .data
段的本质
- 作用:存储已初始化的全局变量和静态变量(例如
int x = 42;
)。 - 特性:
- 初始值必须存储在 Flash(非易失存储器)中,因为 RAM 是易失的,断电后数据会丢失。
- 运行时必须位于 RAM 中,因为这些变量需要被程序修改(RAM 可写,Flash 不可写)。
(2) 复制的触发者
- 启动代码(Startup Code):通常由汇编文件(如
startup_xxx.S
)或 C 语言编写的初始化函数(如SystemInit()
)完成数据复制。 - 复制逻辑:
- 从 Flash 中读取
.data
段的初始值(加载地址 LMA)。 - 将初始值复制到 RAM 中
.data
段的运行时地址(虚拟地址 VMA)。 - 复制长度由
.data
段的大小决定(即_edata - _sdata
)。
- 从 Flash 中读取
(3) 复制的关键符号
_sdata
和_edata
:
定义在链接脚本中,分别表示.data
段在 RAM 中的 起始地址 和 结束地址。.data : {_sdata = .; /* RAM 中的起始地址(VMA) */*(.data*)_edata = .; /* RAM 中的结束地址(VMA) */ } > RAM AT > FLASH /* LMA 在 Flash */
_sidata
:
通常需要额外定义.data
段在 Flash 中的初始值地址(LMA),例如:_sidata = LOADADDR(.data); /* Flash 中 .data 段的初始值地址 */
2. 复制过程详解
步骤 1:确定复制的源地址、目标地址和长度
参数 | 符号 | 计算方式 |
---|---|---|
目标地址(RAM) | _sdata | 直接来自链接脚本定义 |
源地址(Flash) | _sidata | LOADADDR(.data) |
数据长度 | _edata - _sdata | 结束地址 - 起始地址 |
步骤 2:启动代码中的实际复制操作(以汇编为例)
/* 从 Flash 复制 .data 段到 RAM */
ldr r0, =_sdata /* RAM 目标地址(VMA) */
ldr r1, =_edata
ldr r2, =_sidata /* Flash 源地址(LMA) */copy_data_loop:cmp r0, r1 /* 检查是否复制完成 */beq copy_data_doneldr r3, [r2], #4 /* 从 Flash 读取 4 字节 */str r3, [r0], #4 /* 写入 RAM */b copy_data_loop /* 循环 */copy_data_done:
步骤 3:验证复制长度
- 复制的数据量是
.data
段的实际大小,即_edata - _sdata
。 - 如果
.data
段为空(无初始化变量),_edata == _sdata
,则不会执行复制。
3. 实际示例
场景:假设有以下全局变量
// 已初始化的全局变量(属于 .data 段)
int g_value = 0x1234;
const char g_message[] = "Hello"; // 注意:const 变量可能属于 .rodata
链接脚本生成的符号
_sdata = 0x20000000
(RAM 起始地址)_edata = 0x20000008
(假设int
占 4 字节,字符串占 5 字节 + 对齐)_sidata = 0x08001000
(Flash 中存储初始值的位置)
复制的数据内容
- 从 Flash 地址
0x08001000
开始,复制0x08
字节(即0x20000008 - 0x20000000
)到 RAM 地址0x20000000
。 - 复制的数据为
0x1234
和字符串"Hello"
的二进制表示。
4. 特殊情况处理
情况 1:.data
段为空
- 若没有已初始化的全局变量,
.data
段大小为 0,_sdata == _edata
。 - 启动代码会跳过复制操作,无额外开销。
情况 2:未正确复制
- 表现:全局变量的初始值不正确(例如
int x = 42;
实际为随机值)。 - 原因:
- 链接脚本未正确定义
_sdata
/_edata
。 - 启动代码未实现数据复制逻辑。
- 内存溢出导致数据被覆盖。
- 链接脚本未正确定义
情况 3:数据段跨多个内存区域
- 若
.data
段需要分布在不同的 RAM 区域(如内部 RAM 和外部 SDRAM),需在链接脚本中拆分段:.data : {*(.data_fast*) /* 分配到内部 RAM */*(.data_slow*) /* 分配到外部 SDRAM */ } > RAM AT > FLASH
5. 调试技巧
方法 1:检查 map
文件
- 确认
_sdata
、_edata
、_sidata
的地址是否符合预期。.data 0x20000000 0x10_sdata 0x20000000*(.data*)_edata 0x20000010 LOADADDR(.data) 0x08001000
方法 2:使用调试器查看内存
- 在启动代码执行后,检查 RAM 地址
_sdata
处的内容是否与 Flash 地址_sidata
处一致。# 通过 OpenOCD 或 J-Link 读取内存 mdw 0x20000000 4 # 查看 RAM 中的数据 mdw 0x08001000 4 # 查看 Flash 中的初始值
方法 3:添加调试输出
- 在启动代码中打印复制信息:
printf("Copying .data: %d bytes from 0x%08x to 0x%08x\n", (uint32_t)(&_edata - &_sdata), (uint32_t)&_sidata, (uint32_t)&_sdata);
总结
.data
段的内容会完整复制到 RAM,且复制的数据量严格等于.data
段的大小。- 关键依赖:
- 链接脚本正确定义
_sdata
、_edata
、_sidata
。 - 启动代码正确实现复制逻辑。
- 链接脚本正确定义
- 验证方法:结合
map
文件和调试器,确保数据地址和内容正确。
在链接脚本(包括 .ld
或 .lds.S
文件)中,ENTRY
是一个 关键指令,用于显式指定程序的入口点(即程序执行的起始地址)。它的作用是告诉链接器:“程序从哪个符号(函数或地址)开始执行”。以下是关于 ENTRY
指令的详细解释和使用场景:
1. ENTRY
的基本语法
ENTRY(<符号名>)
- 参数:符号名(如
boot_entry
、Reset_Handler
、_start
等),必须是程序中已定义的全局符号。 - 作用:指定程序执行的入口地址,该符号对应的代码会成为程序的第一条指令。
2. ENTRY
的核心作用
(1) 定义程序起点
- 在嵌入式系统中,程序启动后,硬件会从 复位向量 中读取入口地址,跳转到该地址开始执行。
- 若未在链接脚本中指定
ENTRY
,链接器会尝试通过以下默认规则确定入口点:.text
段的起始地址。- 符号
start
或_start
的地址(如果存在)。 - 地址
0
(如果前两者均未定义)。
(2) 确保关键代码不被优化
- 通过
ENTRY
显式指定入口点,链接器会强制保留该符号对应的代码(即使未被其他代码显式调用),避免被优化删除。
3. 实际使用场景
场景 1:标准嵌入式启动流程
- 入口符号:通常为
Reset_Handler
(定义在启动文件.S
中)。 - 链接脚本:
ENTRY(Reset_Handler) /* 指定入口为复位处理函数 */MEMORY { ... } SECTIONS {.isr_vector : { ... } > FLASH.text : {*(.text*)KEEP(*(.init)) /* 保留入口代码 */} > FLASH }
- 启动文件(.S):
.section .isr_vector .word _estack .word Reset_Handler /* 中断向量表指向入口 */.text .global Reset_Handler Reset_Handler: /* 入口点 */MOV sp, #0x20001000BL main
场景 2:自定义引导加载程序(Bootloader)
- 入口符号:
boot_entry
(自定义的引导代码)。 - 链接脚本:
ENTRY(boot_entry) /* 程序从 boot_entry 开始执行 */MEMORY {BOOT_FLASH (rx) : ORIGIN = 0x08000000, LENGTH = 16KAPP_FLASH (rx) : ORIGIN = 0x08004000, LENGTH = 240KRAM (rwx) : ORIGIN = 0x20000000, LENGTH = 64K }SECTIONS {.boot : {KEEP(*(.boot_entry)) /* 强制保留引导代码 */} > BOOT_FLASH/* 其他段... */ }
- 代码中定义入口符号:
// boot.c void boot_entry(void) {// 初始化硬件,验证应用程序,跳转到应用程序jump_to_app(); }
4. 验证 ENTRY
是否正确
(1) 查看生成的 map
文件
在 map
文件的 入口点(Entry Point) 部分,确认符号地址是否正确:
Entry point address: 0x08000100
Entry point: Reset_Handler
(2) 反汇编可执行文件
使用 objdump
或 IDE 的反汇编工具,查看程序开头是否为入口符号的代码:
arm-none-eabi-objdump -D your_program.elf | less
输出示例:
08000100 <Reset_Handler>:8000100: mov sp, #0x200010008000104: bl 8000200 <main>
(3) 调试器验证
在调试器中加载程序,检查 PC(程序计数器)的初始值是否指向入口符号地址。
5. 常见问题
问题 1:链接时报错“未定义符号 boot_entry
”
- 原因:代码中未定义
boot_entry
,或未将其声明为全局符号(.global
或extern
)。 - 解决:在代码中正确定义并导出符号:
/* 汇编中定义 */ .global boot_entry boot_entry:/* 代码 */
/* C 语言中定义 */ void boot_entry(void) __attribute__((naked, section(".boot_entry"))); void boot_entry(void) {/* 代码 */ }
问题 2:程序未从入口点启动
- 原因:中断向量表未正确指向入口点(需在
.isr_vector
段中显式指定)。 - 解决:确保中断向量表的第一个条目是入口地址:
.section .isr_vector .word _estack .word Reset_Handler /* 第一个异常向量是复位处理函数 */
6. 总结
关键点 | 说明 |
---|---|
ENTRY 的作用 | 定义程序执行的入口地址,确保关键代码不被优化。 |
典型入口符号 | Reset_Handler (标准启动)、boot_entry (自定义引导程序)、_start 。 |
入口符号实现 | 需在代码中定义为全局符号,通常位于启动文件或引导模块。 |
验证方法 | 通过 map 文件、反汇编工具、调试器确认入口地址正确。 |
与中断向量表的关系 | 入口点需与中断向量表中的复位向量地址一致。 |
理解 ENTRY
的用法,可以确保程序从正确的位置启动,尤其在多阶段引导、自定义启动流程等场景中至关重要。
在链接脚本(包括 .ld
或 .lds.S
文件)中,ASSERT
是一个 断言指令,用于在链接阶段检查特定条件是否满足。如果条件不成立,链接过程将终止并报错,避免生成无效的可执行文件。以下是 ASSERT
的详细用法、实际场景和注意事项:
一、ASSERT
的语法与作用
语法
ASSERT(<条件表达式>, <错误信息>)
- 条件表达式:必须为真(非零),否则触发错误。
- 错误信息:字符串,描述断言失败的原因(可选,但建议提供)。
作用
- 验证链接规则:确保内存分配、符号地址、段大小等符合预期。
- 防止隐蔽错误:例如内存溢出、地址未对齐等难以调试的问题。
二、典型使用场景
1. 检查内存区域是否溢出
验证某个段的大小不超过其分配的内存区域容量:
MEMORY {FLASH (rx) : ORIGIN = 0x08000000, LENGTH = 512KRAM (rwx) : ORIGIN = 0x20000000, LENGTH = 128K
}SECTIONS {.text : { *(.text*) } > FLASH.data : { *(.data*) } > RAM AT > FLASH/* 检查 .text 段是否超出 FLASH 容量 */ASSERT(LENGTH(FLASH) >= SIZEOF(.text), "Error: .text section overflow in FLASH!")
}
2. 验证符号地址对齐
确保关键数据结构的地址满足对齐要求(如 DMA 传输需要 4 字节对齐):
.bss : {. = ALIGN(4); /* 强制 4 字节对齐 */_sbss = .;*(.bss*)_ebss = .;ASSERT((_ebss - _sbss) % 4 == 0, ".bss section size must be 4-byte aligned!")
} > RAM
3. 确保中断向量表位于正确位置
检查中断向量表是否位于 Flash 起始地址:
.isr_vector : {KEEP(*(.isr_vector))
} > FLASH/* 确保中断向量表起始地址为 FLASH 的起始地址 */
ASSERT(ORIGIN(FLASH) == ADDR(.isr_vector), "Interrupt vector table must start at FLASH base address!")
4. 防止堆栈冲突
检查堆(Heap)和栈(Stack)之间是否有足够的间隙:
.heap : {_sheap = .;. += HEAP_SIZE;_eheap = .;
} > RAM.stack : {_estack = .;. += STACK_SIZE;
} > RAM/* 确保堆和栈不重叠 */
ASSERT(_eheap <= _estack, "Heap and Stack overlap!")
三、ASSERT
的注意事项
1. 直接使用性
- 可以直接使用:
ASSERT
是 GNU 链接器(ld
)的标准功能,无需额外配置。 - 兼容性:主流的嵌入式工具链(如 ARM GCC、RISC-V GCC)均支持。
2. 条件表达式
- 可以是 算术表达式、符号比较 或 链接脚本函数(如
SIZEOF
,ADDR
,ALIGN
)。 - 示例:
ASSERT( _ebss - _sbss > 0x100, "BSS section is too small!" ) ASSERT( (ADDR(.data) & 0x3) == 0, ".data section is not 4-byte aligned!" )
3. 错误信息
- 错误信息是可选参数,但强烈建议提供,便于快速定位问题。
- 示例:
ASSERT( LENGTH(RAM) >= (SIZEOF(.data) + SIZEOF(.bss)), "RAM overflow!" )
4. 预处理器的交互
- 在
.lds.S
文件中,ASSERT
可以与预处理器宏结合,实现动态检查:#define REQUIRED_FLASH_SIZE 0x80000 /* 512 KB */MEMORY {FLASH (rx) : ORIGIN = 0x08000000, LENGTH = REQUIRED_FLASH_SIZE }/* 动态检查 Flash 容量是否足够 */ ASSERT(LENGTH(FLASH) >= REQUIRED_FLASH_SIZE, "Flash size is insufficient!")
四、调试技巧
1. 查看断言失败信息
若断言失败,链接器会输出错误信息并终止:
ld: your_script.ld:XX: error: assertion failed: Flash size is insufficient!
2. 结合 map
文件分析
生成 map
文件,检查各段地址和大小是否符合预期:
arm-none-eabi-ld -T your_script.ld -Map=program.map -o program.elf
3. 条件表达式验证
手动计算断言中的表达式值,确认逻辑正确:
# 示例:计算 .text 段大小是否超出 Flash 容量
size_text=$(arm-none-eabi-size -A program.elf | grep .text | awk '{print $2}')
flash_size=0x80000 # 512 KB
if [ $size_text -gt $flash_size ]; thenecho "Error: .text section overflow!"
fi
五、总结
要点 | 说明 |
---|---|
ASSERT 的作用 | 在链接阶段验证条件,防止生成无效的可执行文件。 |
典型场景 | 内存溢出检查、地址对齐验证、关键段位置确认。 |
直接使用性 | 是,GNU 链接器原生支持。 |
错误信息 | 建议提供清晰的错误描述,便于快速定位问题。 |
与预处理器的结合 | 可通过宏实现动态条件检查,增强灵活性。 |
合理使用 ASSERT
可以显著提升链接脚本的健壮性,避免因内存配置错误导致的隐蔽问题。