一、概述
1.1 什么是 LD 文件
LD 文件作为链接器脚本文件,在整个嵌入式开发流程中占据着举足轻重的地位。当编译器将我们编写的源代码转化为目标文件后,这些目标文件就如同零散的零件,而链接器的使命就是将这些零件以及所需的库文件巧妙地组合成一个完整且可执行的文件。在这个过程中,LD 文件就为链接器提供了详尽的操作指南。它会明确告知链接器如何对这些文件进行整合,怎样合理地分配内存空间,以及程序启动的入口点应设置在何处等核心信息。
对于 GD32 微控制器而言,其独特的硬件架构与内存布局,使得 LD 文件的编写合理性直接关乎硬件性能的充分发挥以及程序的高效、稳定运行。例如,GD32 所配备的闪存(FLASH)主要用于存储程序代码以及常量数据,而随机存取存储器(RAM)则承担着运行时变量存储与程序执行的重任。LD 文件能够精确地规划代码段、数据段等在这些不同内存区域中的具体存放位置,从而保障程序得以正确加载并顺利运行。
1.2 LD 文件的重要性
内存管理:借助 LD 文件,开发者能够依据项目的实际需求,对内存资源进行细致入微的规划。比如,清晰界定哪些代码和数据适宜存储在 FLASH 中,哪些应放置在 RAM 里,从而有效避免内存冲突与资源浪费的情况发生。这一点对于资源本就有限的嵌入式系统而言,尤为关键,它确保了每一个字节的内存都能被合理且高效地利用。
程序启动与执行:LD 文件中明确指定的程序入口点,是程序启动后执行的首个指令地址。准确无误地设置入口点,能够保障系统在复位后,有条不紊地进入初始化流程,进而使应用程序得以正常运转。同时,在程序执行的整个过程中,LD 文件还能确保各个模块的代码和数据能够精准无误地加载到内存中,维持程序的逻辑完整性与连贯性。
代码和数据组织:它有助于将不同功能的代码段和数据段进行合理的分组与安置。举例来说,将中断服务程序代码放置在特定的内存区域,这样当有中断事件发生时,系统能够迅速响应;将常量数据存放在只读的内存区域,防止因意外操作导致数据被修改。这种有序的组织方式,极大地提升了程序的可读性与可维护性,为后续的开发与调试工作提供了便利。
二、基本结构
2.1 语法基础
指令:LD 文件包含一系列预定义的指令,用于精准控制链接器的行为。在我们所分析的这个 LD 文件中,开篇便使用了ENTRY指令,即ENTRY(Reset_Handler) 。该指令的作用是指定程序的入口点,其中Reset_Handler是一个在程序中定义的符号,通常为一个函数名。这意味着程序启动后,将从Reset_Handler函数开始执行相关代码。
内存定义:通过MEMORY关键字来对目标系统中的内存区域进行定义。其语法格式为:
MEMORY
{
region_name (attributes) : ORIGIN = start_address, LENGTH = size
}
在给定的 LD 文件里,定义了两个主要的内存区域:
MEMORY
{
RAM (xrw) : ORIGIN = 0x20000000, LENGTH = 20K
FLASH (rx) : ORIGIN = 0x8000000, LENGTH = 128K
}
这里的RAM区域,起始地址为0x20000000,长度为20K,属性为xrw,即具备可执行、可读和可写的特性;FLASH区域,起始地址是0x8000000,长度为128K,属性为rx,表示可读且可执行。
段定义:利用SECTIONS关键字来定义程序中的各个段,并明确它们在内存中的具体位置。语法如下:
SECTIONS
{
.section_name :
{
input_files_pattern
symbol_assignments
} >memory_region
}
例如,在 LD 文件中对.text段的定义:
.text :
{
. = ALIGN(4);
*(.text) /*.text sections (code) */
*(.text*) /*.text* sections (code) */
*(.glue_7) /* glue arm to thumb code */
*(.glue_7t) /* glue thumb to arm code */
*(.eh_frame)
KEEP (*(.init))
KEEP (*(.fini))
. = ALIGN(4);
_etext =. ; /* define a global symbols at end of code */
} >FLASH
其中,.text是段的名称,用于存放程序的代码段。(.text)和(.text*)表示将所有输入文件中的.text段收集起来。(.glue_7)和(.glue_7t)用于处理 ARM 和 Thumb 代码之间的转换。KEEP ((.init))和KEEP ((.fini))用于保留特定的初始化和结束函数。. = ALIGN(4);表示对地址进行 4 字节对齐,以满足硬件的存储要求。最后的>FLASH表明该段将被放置到FLASH内存区域。
2.2 组成部分
文件头:此 LD 文件的文件头简洁明了,通过ENTRY(Reset_Handler)指令,将Reset_Handler函数指定为程序的入口点。在 GD32 系统复位后,处理器会立即跳转到这个指定的入口点,开始执行初始化代码,为后续程序的正常运行奠定基础。
内存区域定义:除了常见的RAM和FLASH区域定义外,该 LD 文件清晰地设置了各区域的属性、起始地址和长度。合理的内存区域定义为后续段的分配提供了明确的空间框架。例如,RAM区域较大的可写属性(xrw),适合用于存储运行时频繁读写的变量;而FLASH区域的只读属性(rx),保证了程序代码和常量数据的安全性与稳定性。
段定义和分配
.isr_vector 段:
.isr_vector :
{
. = ALIGN(4);
KEEP(*(.isr_vector)) /* Startup code */
. = ALIGN(4);
} >FLASH
该段用于存放中断向量表,它是程序响应中断的关键部分。通过ALIGN(4)进行 4 字节对齐,确保中断向量表的存储地址符合硬件要求。KEEP(*(.isr_vector))表示保留所有与中断向量表相关的代码。将其放置在FLASH中,利用了FLASH的非易失性特点,保证中断向量表在系统断电后也不会丢失。
.text 段:如前文所述,.text段收集了各种与程序代码相关的部分,包括普通的.text段、用于代码转换的部分以及特定的初始化和结束函数。通过一系列的ALIGN(4)操作,保证代码段的存储地址对齐,提高代码执行的效率。最后将其放置在FLASH中,因为程序代码通常需要长期存储且在运行时只读。
.rodata 段:
.rodata :
{
. = ALIGN(4);
*(.rodata) /*.rodata sections (constants, strings, etc.) */
*(.rodata*) /*.rodata* sections (constants, strings, etc.) */
. = ALIGN(4);
} >FLASH
此段用于存放常量数据,如字符串常量等。同样通过ALIGN(4)进行对齐,并放置在FLASH中,利用FLASH的只读属性防止常量数据被意外修改。
.data 段:
.data :
{
. = ALIGN(4);
_sdata =. ; /* create a global symbol at data start */
*(.data) /*.data sections */
*(.data*) /*.data* sections */
. = ALIGN(4);
_edata =. ; /* define a global symbol at data end */
} >RAM AT> FLASH
该段用于存储已初始化的全局变量和静态变量。>RAM AT> FLASH表示.data段最终存储在RAM区域,但它的初始值是从FLASH区域加载的。在程序启动时,需要将FLASH中存储的已初始化数据复制到RAM中,以便程序在运行时能够快速访问这些变量。通过定义_sdata和_edata符号,标记了数据段的起始和结束地址,方便在程序中进行相关操作。
.bss 段:
. = ALIGN(4);
.bss :
{
/* This is used by the startup in order to initialize the.bss secion */
_sbss =. ; /* define a global symbol at bss start */
__bss_start__ = _sbss;
*(.bss)
*(.bss*)
*(COMMON)
. = ALIGN(4);
_ebss =. ; /* define a global symbol at bss end */
__bss_end__ = _ebss;
} >RAM
.bss段用于存储未初始化的全局变量和静态变量。由于这些变量未初始化,在程序启动时只需要将该段的内存清零即可,不需要从FLASH中加载初始值。通过ALIGN(4)对齐,并定义_sbss和_ebss等符号,方便对.bss段进行管理和操作。
._user_heap_stack 段:
._user_heap_stack :
{
. = ALIGN(8);
PROVIDE ( end =. );
PROVIDE ( _end =. );
. =. + _Min_Heap_Size;
. =. + _Min_Stack_Size;
. = ALIGN(8);
} >RAM
该段用于定义用户堆和栈的区域。通过ALIGN(8)进行 8 字节对齐,以满足特定的硬件或编程规范要求。PROVIDE ( end =. )和PROVIDE ( _end =. )定义了相关符号用于标记该区域的结束地址。根据预先设定的_Min_Heap_Size和_Min_Stack_Size,确定了堆和栈的最小尺寸,并在内存中进行了相应的分配。
内存分配示意图
三、符号的定义和使用
符号的定义:在这个 LD 文件中,定义了众多符号,这些符号对于程序的正确运行和内存管理起着关键作用。
例如,_estack被定义为ORIGIN(RAM) + LENGTH(RAM),即RAM区域的最高地址,用于表示用户模式栈的最高地址。
_Min_Heap_Size被设置为0x200,表示所需堆的最小尺寸;_Min_Stack_Size被设置为0x400,表示所需栈的最小尺寸。这些符号的定义为后续内存分配和栈、堆的初始化提供了重要的参数。
此外,还定义了如_etext、_edata、_ebss等符号,分别用于标记代码段、已初始化数据段和未初始化数据段的结束地址。在.text段定义中,_etext =. ;在代码段结束时赋予_etext当前地址值,方便在程序中获取代码段的结束位置,可能用于计算代码段的大小或进行其他与代码段边界相关的操作。
在.data段中,_sdata和_edata分别标记了数据段的起始和结束地址;在.bss段中,_sbss和_ebss标记了未初始化数据段的起始和结束地址。这些符号为程序在初始化数据段和清零bss段时提供了准确的地址信息。
符号的使用:在 C 或 C++ 代码中,可以通过extern关键字来引用在 LD 文件中定义的符号。例如,如果在 C 代码中需要获取栈的大小,可以这样写:
extern unsigned int _Min_Stack_Size[];
void setup_stack(void)
{
// 使用_Min_Stack_Size来设置栈相关的操作
unsigned int stack_size = (unsigned int)(_Min_Stack_Size);
// 根据stack_size进行栈的初始化等操作
}
四、与启动文件的配合
启动文件的作用:在 GD32 的开发中,启动文件是一段汇编代码,在系统复位后率先执行。其主要功能包括:
硬件初始化:对系统时钟进行设置,确保系统以合适的频率运行;初始化中断向量表,使系统能够正确响应各类中断事件;配置栈指针,为程序的函数调用和局部变量存储提供空间。
初始化数据段和清零 BSS 段:将已初始化的数据从FLASH(rom)复制到RAM,保证程序运行时能够快速访问这些数据;将未初始化的数据段(.bss)清零,确保变量初始值为零,避免因未初始化变量导致的错误。
跳转到应用程序入口:完成一系列初始化操作后,启动文件会跳转到应用程序的main函数,将程序的执行权交给用户编写的代码,开始正式的应用程序运行。
与 LD 文件的协同工作
内存布局信息共享:LD 文件详细定义了内存的布局,包括各个段在内存中的位置和大小。启动文件在进行初始化操作时,需要这些信息来确保正确性。例如,在初始化数据段时,启动文件需要知道.data段在RAM中的起始地址(通过_sdata符号)和结束地址(通过_edata符号),以及在FLASH中的起始地址(通过LOADADDR(.data)获取),以便从FLASH中准确复制数据到RAM。同样,在清零.bss段时,需要知道.bss段在RAM中的起始地址(通过_sbss符号)和大小,这些信息都由 LD 文件提供。
程序入口点的确定:LD 文件通过ENTRY指令指定的程序入口点,通常是启动文件中的Reset_Handler函数。当系统复位后,处理器会依据这个指定的入口点,开始执行启动文件中的代码。启动文件在完成硬件初始化、数据段复制和bss段清零等操作后,会根据 LD 文件中定义的内存布局,将程序的执行流程准确无误地引导到应用程序的main函数。
栈的初始化:启动文件在初始化栈指针时,需要知道栈的大小和起始地址。在 LD 文件中定义的_estack(RAM区域的最高地址)和_Min_Stack_Size(所需栈的最小尺寸)为启动文件提供了关键信息。例如,假设栈从RAM的高端地址开始向下生长,启动文件可以通过以下汇编代码设置栈指针:
LDR SP, =_estack
SUB SP, SP, #_Min_Stack_Size
这里通过LDR指令将_estack的值加载到栈指针寄存器SP中,然后通过减法操作,根据_Min_Stack_Size的值确定栈的起始位置,完成栈指针的初始化。
五、优化和调整
存储器资源利用优化
根据实际的应用需求,合理调整 RAM 和 FLASH 的大小分配。如果程序中数据量较大,可能需要增加 RAM 的空间;如果代码较为复杂且庞大,可能需要扩展 FLASH 的容量。
同时,可以通过精细地划分段,将不常使用或只读的数据放置在 FLASH 中,而频繁读写的数据放在 RAM 中,以提高存储器的访问效率。
地址对齐优化
选择合适的地址对齐方式可以提高存储器访问速度。对于某些对性能要求较高的段,可以采用更严格的对齐方式,如 8 字节或 16 字节对齐。
但需要注意的是,过度的对齐可能会导致存储空间的浪费,因此需要在性能和资源利用之间进行平衡。
段的合并与分离
根据程序的特点和运行时的需求,可以将一些相关的段进行合并,以减少段切换带来的开销。
例如,如果有多个只读数据段,且它们在逻辑上紧密相关,可以考虑将它们合并为一个较大的只读数据段。
相反,如果某些段的访问模式或更新频率差异较大,可以将其分离,以避免不必要的影响。
与编译器选项的配合
编译器通常提供了一些选项,可以与 LD 文件的优化策略相互配合。
例如,编译器的优化级别选项可以影响代码生成的效率和大小,与 LD 文件中对代码段的布局和分配相结合,可以达到更好的整体性能。
————————————————
版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
原文链接:https://blog.csdn.net/pigliuxu/article/details/145146652
|