嵌入式预处理链接脚本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
可以显著提升链接脚本的健壮性,避免因内存配置错误导致的隐蔽问题。
相关文章:
嵌入式预处理链接脚本lds和map文件
在嵌入式开发中,.lds.S 文件是一个 预处理后的链接脚本(Linker Script),它结合了 C 预处理器(Preprocessor) 的功能和链接脚本的语法。它的核心作用仍然是 定义内存布局和链接规则,但通过预处理…...
9. Spring AI 各版本的详细功能与发布时间整理
目录 一、旧版本(Legacy) 0.8.1(2024年3月) 二、里程碑版本(Milestone) 1.0.0-M1(2024年5月30日) 1.0.0-M2(2024年7月) 1.0.0-M3(2024年10月8日) 1.0.0-M4(2024年12月) 1.0.0-M5(2025年1月9日) 1.0.0-M6(2025年3月) 1.0.0-M7(2025年4月14日) 1.…...

《Android 应用开发基础教程》——第十四章:Android 多线程编程与异步任务机制(Handler、AsyncTask、线程池等)
目录 第十四章:Android 多线程编程与异步任务机制(Handler、AsyncTask、线程池等) 🔸 14.1 为什么需要多线程? 🔸 14.2 Handler Thread 模型 ✦ 使用 Handler 与 Thread 进行线程通信 ✦ 简要说明&am…...
Apache 高级配置实战:从连接保持到日志分析的完整指南
Apache 高级配置实战:从连接保持到日志分析的完整指南 前言 最近在深入学习 Apache 服务器配置时,发现很多朋友对 Apache 的高级功能还不够了解。作为一个在运维路上摸爬滚打的技术人,我想把这些实用的配置技巧分享给大家。今天这篇文章会带…...
开源 OIDC(OpenID Connect)身份提供方(IdP)、iam选型
文章目录 开源 OIDC(OpenID Connect)身份提供方(IdP)、iam选型主流开源 OIDC(OpenID Connect)身份提供方(IdP)zitadeldexory开源 OIDC(OpenID Connect)身份提供方(IdP)、iam选型 主流开源 OIDC(OpenID Connect)身份提供方(IdP) 当前主流的**开源 OIDC(OpenI…...
Android OkHttp控制链:深入理解网络请求的流程管理
OkHttp作为Android和Java平台上广泛使用的HTTP客户端,其核心设计之一就是"控制链"(Chain)机制。本文将深入探讨OkHttp控制链的工作原理、实现细节以及如何利用这一机制进行高级定制。 一、什么是OkHttp控制链 OkHttp控制链是一种责任链模式的实现&#…...

【JVM 01-引言入门篇】
JVM 引言篇01 笔记记录 1. 什么是JVM?2. 学习JVM有什么用?3. 常见的JVM4. 学习路线 学习资料来源-b站黑马 1. 什么是JVM? 定义:Java虚拟机(Java Virtual Machine 简称JVM)是运行所有Java程序的抽象计算机&a…...

Pandas数据规整
(1)层次化索引 1.创建带层次化索引的df 第一种,直接创建 import pandas as pd import numpy as npdata pd.Series(np.random.randn(9),index [[a, a, a, b, b, c, c, d, d],[1, 2, 3, 1, 3, 1, 2, 2, 3]]) print(data) # a 1 -0.6416…...

ThreadLocal线程本地变量在dubbo服务使用时候遇到的一个坑
我昨天遇到一个问题,就是我springboot项目里面有一个提供代办服务审核的dubbo接口,这个接口给房源项目调用,但是碰到一个问题就是,房源项目每天凌晨5点会查询满足条件过期的数据,然后调用我这边的代办审核dubbo接口&am…...
pga 作用
Oracle pga的作用 PGA 内存结构与功能解释: PGA ├── 1. Private SQL Area ├── 2. Session Memory ├── 3. SQL Work Areas │ ├── Sort Area │ ├── Hash Area │ ├── Bitmap Merge Area │ └── Bitmap Create Area └── 4. Stack S…...
setup.py Pip wheel
. ├── my_package │ ├── __init__.py │ └── my_file.py └── setup.pymy_file.py def my_func():print("Hello World")setup.py from setuptools import setup, find_packages import datetimesetup(namemy_package, # 记得改version0.1.1,packag…...
GO 语言进阶之 时间处理和Json 处理
更多个人笔记见: github个人笔记仓库 gitee 个人笔记仓库 个人学习,学习过程中还会不断补充~ (后续会更新在github上) 文章目录 时间处理基本例子 Json处理基础案例 时间处理 时间格式化必须使用:2006-01-…...
对WireShark 中的UDP抓包数据进行解析
对WireShark 中的UDP抓包数据进行解析 本文尝试对 WireShark 中抓包的 UDP 数据进行解析。 但是在尝试对 TCP 中的 FTP 数据进行解析的时候,发现除了从端口号进行区分之外, 没有什么好的方式来进行处理。 import numpy as np import matplotlib.pyplot …...
Flannel后端为UDP模式下,分析数据包的发送方式(二)
发往 10.244.2.5 的数据包最终会经过物理网卡 enp0s3,尽管路由表直接指定通过 flannel.1 发出。以下以 Markdown 格式详细解释为什么会经过 enp0s3,结合 Kubernetes 和 Flannel UDP 模式的背景。 问题分析 在 Kubernetes 环境中,使用 Flanne…...

从 0 到 1:Spring Boot 与 Spring AI 深度实战(基于深度求索 DeepSeek)
在人工智能技术与企业级开发深度融合的今天,传统软件开发模式与 AI 工程化开发的差异日益显著。作为 Spring 生态体系中专注于 AI 工程化的核心框架,Spring AI通过标准化集成方案大幅降低 AI 应用开发门槛。本文将以国产大模型代表 ** 深度求索ÿ…...

upload-labs通关笔记-第20关 文件上传之杠点绕过
系列目录 upload-labs通关笔记-第1关 文件上传之前端绕过(3种渗透方法) upload-labs通关笔记-第2关 文件上传之MIME绕过-CSDN博客 upload-labs通关笔记-第3关 文件上传之黑名单绕过-CSDN博客 upload-labs通关笔记-第4关 文件上传之.htacess绕过-CSDN…...

Vscode +Keil Assistant编译报错处理
Vscode Keil Assistant编译报错处理 1.报错图片内容 所在位置 行:1 字符: 25 chcp.com 65001 -Command & c:\Users\92170.vscode\extensions\cl.keil-a … ~ 不允许使用与号(&)。& 运算符是为将来使用而保留的;请用双引号将与号引起来(“&”)&…...
记录python在excel中添加一列新的列
思路是,先将需要添加为新的列存储到一个暂时的列表中,然后用到以下函数来存储 data_.loc[:, "新列的名字"] save_list_ 上面的save_list_就是暂时存储了信息的列表了。 以下是我的代码,供以后快速回忆。 schools_data {"98…...
WebRTC:实时通信的未来之路
WebRTC:实时通信的未来之路 目录 WebRTC:实时通信的未来之路一、背景介绍二、使用方式三、前途展望 一、背景介绍 随着互联网的飞速发展,实时音视频通信需求日益增长。传统的音视频通信多依赖于专有协议和插件(如Flash、ActiveX等…...
探索产品经理的MVP:从概念到实践
在产品开发的世界里,MVP(Minimum Viable Product,最小可行产品)是一个至关重要的概念。它不仅帮助团队快速验证假设,还能降低失败风险,为后续的产品迭代奠定坚实的基础。本文将深入探讨MVP的概念、重要性及…...
用python实现中国象棋
一.象棋规则 象棋是二人对弈的棋类游戏,棋盘由 9 条竖线和 10 条横线交叉构成,中间 “河界” 分楚汉,两端 “九宫” 各 9 个交叉点。棋子分红黑,各 16 枚,含 7 兵种。 1.棋子走法 1.1 红方棋子 帅:1 个…...
GO 语言基础3 struct 结构体
更多个人笔记见: github个人笔记仓库 gitee 个人笔记仓库 个人学习,学习过程中还会不断补充~ (后续会更新在github上) 文章目录 strcut结构体基本例子传入数值和指针的区别初始化方法汇总结构体特点结构体方法定义基于…...

VSCode C/C++ 开发环境完整配置及一些扩展用途(自用)update:2025/3/31
这里主要记录了一些与配置相关的内容。由于网上教程众多,部分解决方法并不能完全契合我遇到的问题,因此我选择以自己偏好的方式,对 VSCode 进行完整的配置,并记录在使用过程中遇到的问题及解决方案。后续内容也会持续更新和完善。…...
iOS 上线前的性能与稳定性检查流程实录:开发者的“最后一公里”(含 KeyMob 应用经验)
一个 iOS 项目写完功能、跑完测试,离上线只差一步了——但很多问题恰恰就在“这最后一公里”暴露:某些设备发热严重,部分流程偶发卡顿,某些崩溃只有长时间运行后才出现。 今天我分享的是我在多个 iOS 项目上线前实际执行过的性能…...

Docker系列(二):开机自启动与基础配置、镜像加速器优化与疑难排查指南
引言 docker 的快速部署与高效运行依赖于两大核心环节:基础环境搭建与镜像生态优化。本期博文从零开始,系统讲解 docker 服务的管理配置与镜像加速实践。第一部分聚焦 docker 服务的安装、权限控制与自启动设置,确保环境稳定可用;…...

a16z:AI带来了全新的9种开发软件的模式
非常有启发的9条新兴模式,推荐给已经上手 vibeCoding 的读者们。 开发者正在将 AI 从简单的工具转变为构建软件的新基础。许多核心概念,如版本控制、模板、文档,甚至用户的定义,都在被重新思考。代理(Agent)…...
20.迭代器模式:思考与解读
原文地址:迭代器模式:思考与解读 更多内容请关注:深入思考与解读设计模式 引言 在软件开发中,尤其是在处理集合数据时,你是否曾经遇到过这样的问题:你需要遍历一个集合(如数组、列表、集合等)…...
Java 学习笔记:注解、泛型与 IO 流
目录 课程目标 Java 注解(Annotation) 1. 概念与作用 2. 自定义注解示例 3. JDK 内置注解 4.注释 Java 泛型(Generics) 1. 基本语法 2. 通配符与上下限 3. 常见应用场景 Java IO 流 1. 流的分类1.File文件类 2. 字节流与字符流 3. 经典示例:文件拷贝 总结与…...

在 Excel 使用macro————仙盟创梦IDE
Dim filePath As StringDim fileContent As StringDim lines() As StringDim dataArray() As StringDim lineCount As LongDim maxCols As LongDim i As Long, j As Long 文件路径filePath "" 检查文件是否存在If Dir(filePath) "" ThenMsgBox "文件…...
【MySQL】08.视图
视图就是一个由查询到的内容定义的虚拟表。它和真实的表一样,视图包含一系列带有名称的列和行数据。视图的数据变化会影响到基表,基表的数据变化也会影响到视图。 1. 基本使用 mysql> select * from user; -------------------- | id | age | name …...