当前位置: 首页 > news >正文

嵌入式预处理链接脚本lds和map文件

在嵌入式开发中,.lds.S 文件是一个 预处理后的链接脚本(Linker Script),它结合了 C 预处理器(Preprocessor) 的功能和链接脚本的语法。它的核心作用仍然是 定义内存布局和链接规则,但通过预处理器的特性(如宏定义、条件编译、文件包含等)使得链接脚本更加灵活和可配置。以下是详细解析:


1. .lds.S 文件的本质

  • 核心作用:与普通 .ld 文件相同,用于控制代码和数据的内存分配,定义符号地址等。
  • 特殊之处:文件扩展名 .S 表示这是一个 需要预处理的链接脚本(类似汇编文件 .S 需要预处理后再汇编)。
  • 处理流程
    1. 预处理阶段:通过 C 预处理器(如 cpp)处理 .lds.S 文件,展开宏、处理条件编译指令(#ifdef#define)等。
    2. 生成纯链接脚本:预处理后生成一个标准的 .ld 文件。
    3. 链接阶段:链接器(如 ld)使用生成的 .ld 文件完成内存分配。

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. 实际使用场景示例

场景:为不同芯片生成不同链接脚本
  1. 目录结构

    project/
    ├── linker/
    │   ├── stm32f4.lds.S   # STM32F4 的链接脚本模板
    │   └── stm32h7.lds.S   # STM32H7 的链接脚本模板
    ├── Makefile
    └── src/└── main.c
    
  2. 预处理生成最终链接脚本

    # Makefile 示例
    CHIP ?= stm32f4# 根据芯片选择模板
    LINKER_SCRIPT = linker/$(CHIP).lds.S# 预处理生成 .ld 文件
    %.ld: %.lds.S$(CC) -E -P -x c $< -o $@
    
  3. 编译时指定芯片型号

    # 编译 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 文件?
  • 方法
    1. 查看预处理后的 .ld 文件,确认宏展开是否符合预期。
    2. 结合 map 文件验证内存分配结果。

总结

  • .lds.S 文件
    本质是 增强版的链接脚本,通过预处理器实现动态配置,但不包含汇编代码功能。它是为了解决复杂内存布局的灵活性问题而设计的。

  • 与汇编文件的关系
    两者完全独立:

    • .S 文件实现代码逻辑(如启动代码、中断处理)。
    • .lds.S 文件控制代码和数据的内存布局。
  • 典型应用
    多硬件平台适配、条件内存分配、复杂项目配置。


以下是关于 .lds.S 文件语法规则的详细解析,涵盖其核心语法、预处理指令的使用以及实际编写技巧。通过示例和分类说明,帮助你快速掌握如何阅读、编写和修改这类文件。


一、.lds.S 文件的核心语法

.lds.S 文件本质是 链接脚本(Linker Script)C 预处理器 的结合,因此其语法包含两部分:

  1. 链接脚本语法:定义内存布局、段分配规则。
  2. 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。
为什么需要这样做?
  • 已初始化的全局变量(如 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())完成数据复制。
  • 复制逻辑
    1. 从 Flash 中读取 .data 段的初始值(加载地址 LMA)。
    2. 将初始值复制到 RAM 中 .data 段的运行时地址(虚拟地址 VMA)。
    3. 复制长度由 .data 段的大小决定(即 _edata - _sdata)。
(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)_sidataLOADADDR(.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 段的大小。
  • 关键依赖
    1. 链接脚本正确定义 _sdata_edata_sidata
    2. 启动代码正确实现复制逻辑。
  • 验证方法:结合 map 文件和调试器,确保数据地址和内容正确。

在链接脚本(包括 .ld.lds.S 文件)中,ENTRY 是一个 关键指令,用于显式指定程序的入口点(即程序执行的起始地址)。它的作用是告诉链接器:“程序从哪个符号(函数或地址)开始执行”。以下是关于 ENTRY 指令的详细解释和使用场景:


1. ENTRY 的基本语法

ENTRY(<符号名>)
  • 参数:符号名(如 boot_entryReset_Handler_start 等),必须是程序中已定义的全局符号。
  • 作用:指定程序执行的入口地址,该符号对应的代码会成为程序的第一条指令。

2. ENTRY 的核心作用

(1) 定义程序起点
  • 在嵌入式系统中,程序启动后,硬件会从 复位向量 中读取入口地址,跳转到该地址开始执行。
  • 若未在链接脚本中指定 ENTRY,链接器会尝试通过以下默认规则确定入口点:
    1. .text 段的起始地址。
    2. 符号 start_start 的地址(如果存在)。
    3. 地址 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,或未将其声明为全局符号(.globalextern)。
  • 解决:在代码中正确定义并导出符号:
    /* 汇编中定义 */
    .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 可以显著提升链接脚本的健壮性,避免因内存配置错误导致的隐蔽问题。

http://www.xdnf.cn/news/635491.html

相关文章:

  • ​​IIS文件上传漏洞绕过:深入解析与高效防御​
  • MySQL索引失效的12种场景及解决方案
  • 深入理解 Linux 的 set、env 和 printenv 命令
  • ZLG USBCANFD python UDS刷写脚本
  • Nature图形解析与绘制—热图的绘制及深入解析
  • React整合【ECharts】教程002:折线图的构建和基本设置
  • 初学Transformer架构和注意力机制
  • OpenCV 第7课 图像处理之平滑(二)
  • QML与C++交互2
  • 历年哈尔滨工业大学保研上机真题
  • uni-app学习笔记十二-vue3中组件传值(对象传值)
  • urdf文件和DH模型参数是一一对应的吗??
  • 在Windows平台基于VSCode准备GO的编译环境
  • Linux基本指令篇 —— whoami指令
  • JavaScript 中 console.log() 使用逗号和加号的区别
  • C++多态与虚函数详解:从入门到精通
  • 27. 自动化测试开发框架拓展之测试数据构造(一)
  • uniapp-商城-68-shop(1-商品列表,获取数据,utils、tofixed 、parseInt的使用)
  • 【b站计算机拓荒者】【2025】微信小程序开发教程 - chapter2 小程序核心
  • STM32八股【11】-----Linux Bootloader (U-Boot)
  • 嵌入式STM32学习—— 定时器中断(应用-利用定时器中断实现LED亮一秒灭一秒)
  • Q1:Go协程、Channel通道 被close后,读会带来什么问题?
  • ABP VNext + Webhook:订阅与异步回调
  • 6个跨境电商独立站平台
  • C++23 元编程工具新特性探索
  • 开发AR导航助手:ARKit+Unity+Mapbox全流程实战教程
  • 【Python 命名元祖】collections.namedtuple 学习指南
  • 反序列化之Wakeup方法绕过
  • Dify 本地源码安装教程
  • 【漫话机器学习系列】277.梯度裁剪(Gradient Clipping)