深入理解嵌入式系统中的链接脚本
在嵌入式系统开发过程中,链接脚本(Linker Script)扮演着至关重要的角色。它不仅控制着程序的链接过程,还决定了代码和数据在内存中的布局。本文将全面解析嵌入式系统中的链接脚本,涵盖其基本概念、构成部分、语法规则、实际应用及注意事项,帮助开发者深入理解并有效应用链接脚本,以优化系统性能和资源利用。
一、链接脚本的基本概念
链接脚本是一个文本文件,通常以 .ld 或 .lds 为文件后缀,用于指导链接器(如 ld)如何将多个目标文件(object files)和库文件(libraries)组合成一个最终的可执行文件。对于嵌入式系统而言,链接脚本尤为重要,因为这些系统通常具有有限且特定的内存资源,需要精确控制程序在内存中的位置和布局。
1.1 链接脚本的作用
- 内存布局控制:定义程序的各个段(如代码段、数据段、堆栈段)在内存中的具体位置,确保程序在目标设备的内存中正确运行。
- 符号定义:通过链接脚本可以定义全局符号,供程序在运行时引用,如入口点和特定内存区域的标识符。
- 优化性能:合理的内存分配可以提高程序的执行效率,减少延迟,优化资源使用。
- 提高安全性:将重要的代码和数据放置在受保护的内存区域,防止未经授权的访问和修改。
1.2 为什么嵌入式系统需要链接脚本
嵌入式系统通常具有以下特点:
- 内存资源有限,如 Flash 和 RAM 的容量固定且受限。
- 需要映射特定的硬件寄存器到特定的内存地址。
- 对代码和数据的存储位置有严格的控制要求,以满足性能和安全的需求。
链接脚本通过精确控制内存布局,帮助开发者有效管理这些资源,确保系统的稳定性和高效性。
二、链接脚本的组成部分
2.1 MEMORY 块
MEMORY 块用于定义目标设备的内存区域,每个区域包括名称、起始地址(ORIGIN)和长度(LENGTH)。内存区域还可以指定访问属性,如只读(r)、可写(w)和可执行(x)。
MEMORY
{
FLASH (rx) : ORIGIN = 0x08000000, LENGTH = 256K
RAM (rwx) : ORIGIN = 0x20000000, LENGTH = 64K
}
FLASH:定义了只读存储器(如 Flash),起始地址为0x08000000,大小为 256KB。RAM:定义了可读写执行的内存区域(如 SRAM),起始地址为0x20000000,大小为 64KB。
2.2 SECTIONS 块
SECTIONS 块定义了程序的各个段(如 .text、.data、.bss)如何映射到内存区域中。每个输出段可以包含一个或多个输入段,并且可以指定它们在内存中的位置。
SECTIONS
{
.isr_vector :
{
KEEP(*(.isr_vector))
} > FLASH
.text :
{
*(.text)
*(.rodata)
} > FLASH
.data :
{
_sdata = .;
*(.data)
_edata = .;
} > RAM AT > FLASH
.bss :
{
_sbss = .;
*(.bss)
_ebss = .;
} > RAM
}
.isr_vector:包含中断向量表,并放置在 Flash 中,使用KEEP保留该段,即使未被引用。.text:存放代码和只读数据,位于 Flash 中。.data:存放已初始化的全局和静态变量,加载地址在 Flash 中,运行地址在 RAM 中。.bss:存放未初始化的全局和静态变量,仅在 RAM 中分配空间。
2.3 ENTRY 命令
ENTRY 命令用于指定程序的入口点,即程序开始执行的地址。通常用于指定启动代码的入口函数。
ENTRY(Reset_Handler)
在上述例子中,Reset_Handler 是程序的入口点,通常由启动代码定义。
2.4 符号定义与操作
链接脚本允许定义符号,用于标记特定的地址或值,供程序在运行时引用。
_sdata = .;
_edata = .;
_sbss = .;
_ebss = .;
_sdata和_edata分别标记.data段的起始和结束地址。_sbss和_ebss分别标记.bss段的起始和结束地址。
三、链接脚本的语法规则
3.1 基本语法
链接脚本的语法类似于 C 语言,包含多种命令和语句,用于定义内存布局和段分配。以下是一些基本语法规则:
- 每个命令以分号
;结束。 - 注释可以使用
/* ... */或//。 - 关键字和符号区分大小写。
3.2 常用命令
MEMORY:定义内存区域。SECTIONS:定义程序段的布局和内存映射。ENTRY:定义程序的入口点。INCLUDE:包含其他链接脚本文件。PROVIDE:定义符号,只有在符号未被定义时生效。
3.3 特殊符号与表达式
.:位置计数器,表示当前内存地址。ALIGN(expression):对齐当前位置到指定边界。ORIGIN和LENGTH:分别表示内存区域的起始地址和长度。
.text :
{
*(.text)
} > FLASH
.data :
{
_sdata = .;
*(.data)
_edata = .;
} > RAM AT > FLASH
.bss :
{
_sbss = .;
*(.bss)
_ebss = .;
} > RAM
四、链接脚本的实际应用
4.1 内存布局控制
通过链接脚本,可以将程序的各个段精确地放置到目标设备的内存区域。例如,将代码段放置在 Flash 中,初始化数据段放置在 RAM 中,而未初始化数据段也在 RAM 中。这种布局确保了程序在启动时能够正确地从 Flash 复制数据到 RAM,并在运行过程中高效地访问这些数据。
ENTRY(Reset_Handler)
MEMORY
{
FLASH (rx) : ORIGIN = 0x08000000, LENGTH = 256K
RAM (rwx) : ORIGIN = 0x20000000, LENGTH = 64K
}
SECTIONS
{
.isr_vector :
{
KEEP(*(.isr_vector))
} > FLASH
.text :
{
*(.text)
*(.rodata)
} > FLASH
.data :
{
_sdata = .;
*(.data)
_edata = .;
} > RAM AT > FLASH
.bss :
{
_sbss = .;
*(.bss)
_ebss = .;
} > RAM
}
4.2 启动代码配置
嵌入式系统通常需要启动代码(Startup Code)来初始化硬件和内存。链接脚本在此过程中起到了关键作用,通过定义入口点和段分配,使启动代码能够正确复制初始化数据并清零未初始化数据段。
extern uint32_t _sdata, _edata, _sbss, _ebss;
extern uint32_t etext;
void init_data_and_bss(void)
{
uint32_t *src = &etext
uint32_t *dest = &_sdata;
// 初始化 .data 段
while (dest < &_edata)
{
*dest++ = *src++;
}
// 清零 .bss 段
dest = &_sbss;
while (dest < &_ebss)
{
*dest++ = 0;
}
}
4.3 多段内存支持
对于复杂的嵌入式系统,可能需要定义多个内存区域和程序段。例如,可以将高性能计算的相关数据放在特定的高速 RAM(如 CCMRAM)中,或将堆和栈放置在不同的内存区域以优化性能和资源管理。
MEMORY
{
FLASH (rx) : ORIGIN = 0x08000000, LENGTH = 256K
SRAM1 (rwx): ORIGIN = 0x20000000, LENGTH = 64K
SRAM2 (rwx): ORIGIN = 0x20010000, LENGTH = 64K
}
SECTIONS
{
.text :
{
*(.text)
} > FLASH
.rodata :
{
*(.rodata)
} > FLASH
.data :
{
*(.data)
} > SRAM1 AT > FLASH
.bss :
{
*(.bss)
} > SRAM2
}
4.4 调试与监控支持
链接脚本中定义的符号(如 _sheap 和 _eheap)可以用于监控堆的使用情况,通过调试工具(如 GDB)可以方便地查看这些符号的值,帮助开发者进行调试和性能分析。
五、链接脚本的使用方法
5.1 创建链接脚本
链接脚本是一个普通的文本文件,可以使用任何文本编辑器创建。文件名通常以 .ld 结尾,例如 linkerscript.ld。
5.2 在构建过程中使用链接脚本
使用 GCC 编译器时,可以通过 -T 选项指定链接脚本。例如:
gcc -T linkerscript.ld -o output.elf main.o
5.3 在 C 代码中引用链接脚本定义的符号
链接脚本中定义的符号可以在 C 代码中通过 extern 关键字引用,用于初始化数据段和清零未初始化数据段。
extern uint32_t _sdata, _edata, _sbss, _ebss;
extern uint32_t etext;
void init_data_and_bss(void)
{
uint32_t *src = &etext
uint32_t *dest = &_sdata;
// 初始化 .data 段
while (dest < &_edata)
{
*dest++ = *src++;
}
// 清零 .bss 段
dest = &_sbss;
while (dest < &_ebss)
{
*dest++ = 0;
}
}
六、链接脚本的实际案例
6.1 完整的嵌入式链接脚本示例
以下是一个完整的嵌入式链接脚本示例,展示了如何定义内存区域、入口点以及各个程序段的布局:
ENTRY(Reset_Handler)
MEMORY
{
FLASH (rx) : ORIGIN = 0x08000000, LENGTH = 256K
RAM (rwx): ORIGIN = 0x20000000, LENGTH = 64K
}
SECTIONS
{
.isr_vector :
{
KEEP(*(.isr_vector))
} > FLASH
.text :
{
*(.text)
} > FLASH
.rodata :
{
*(.rodata)
} > FLASH
.data :
{
_sdata = .;
*(.data)
_edata = .;
} > RAM AT > FLASH
.bss :
{
_sbss = .;
*(.bss)
_ebss = .;
} > RAM
}
-
ENTRY(Reset_Handler):指定程序的入口点为Reset_Handler。 -
.isr_vector:中断向量表,放置在 Flash 中,并使用KEEP保留该段。 -
.text和.rodata:代码和只读数据都放置在 Flash 中。 -
.data:已初始化的数据段,加载地址在 Flash 中,运行时位于 RAM 中。 -
.bss:未初始化的数据段,仅在 RAM 中分配空间。
6.2 使用多段内存的链接脚本示例
对于需要多段内存支持的复杂系统,可以定义多个内存区域并将程序段分配到不同的区域中:
MEMORY
{
FLASH (rx) : ORIGIN = 0x08000000, LENGTH = 256K
SRAM1 (rwx): ORIGIN = 0x20000000, LENGTH = 64K
SRAM2 (rwx): ORIGIN = 0x20010000, LENGTH = 64K
}
SECTIONS
{
.text :
{
*(.text)
} > FLASH
.rodata :
{
*(.rodata)
} > FLASH
.data :
{
*(.data)
} > SRAM1 AT > FLASH
.bss :
{
*(.bss)
} > SRAM2
}
SRAM1:用于存放初始化数据段.data。SRAM2:用于存放未初始化数据段.bss。
七、链接脚本的注意事项
7.1 内存对齐
确保各段的起始地址满足硬件的对齐要求,如 4 字节或 8 字节对齐,这对于某些处理器架构至关重要,以避免访问异常或性能下降。
.data :
{
_sdata = .;
*(.data)
_edata = .;
} > RAM AT > FLASH
.bss :
{
_sbss = .;
*(.bss)
_ebss = .;
} > RAM ALIGN(4)
7.2 符号冲突
避免在链接脚本中定义的符号与程序代码中的符号名称冲突,否则可能导致链接错误。使用前缀或命名约定可以有效避免此类问题。
7.3 内存溢出防范
确保每个段的大小不超过其所在内存区域的长度,否则会导致内存溢出。这可以通过合理规划内存区域和段分配,以及在构建过程中检查链接器的输出日志来实现。
7.4 硬件相关性
链接脚本通常与特定的硬件架构密切相关,在移植程序到不同硬件平台时,需要相应地修改链接脚本以匹配新的内存布局和资源。
7.5 调试工具支持
使用调试工具(如 GDB)时,链接脚本中定义的符号和段布局可以帮助开发者更方便地进行断点设置、内存查看和性能分析。确保链接脚本与调试信息兼容,以便调试工具能够正确解析程序的内存布局。
八、总结
链接脚本是嵌入式系统开发中的关键工具,通过定义内存布局和段分配,确保程序能够在目标设备上正确、高效且安全地运行。理解链接脚本的基本概念、语法规则及其实际应用,能够帮助开发者优化内存使用、提高系统性能,并有效应对嵌入式系统中的各种资源限制和硬件要求。
对于嵌入式开发者来说,熟练掌握链接脚本的编写和使用,不仅是提高开发效率的必要手段,也是保证系统稳定性和性能的重要保障。通过本文的详细讲解,希望您能够深入理解并灵活应用嵌入式系统中的链接脚本,提升项目的整体质量和可靠性。
Last updated January 3, 2025