[RISC-V] 链接脚本实战:从零构建内存布局与启动流程

张开发
2026/5/12 5:53:59 15 分钟阅读

分享文章

[RISC-V] 链接脚本实战:从零构建内存布局与启动流程
1. 理解链接脚本的核心作用第一次接触RISC-V开发时我对着编译生成的.elf文件发呆了很久——为什么代码能准确跳转到指定内存位置执行全局变量为何能正确初始化这些问题都指向一个关键角色链接脚本Linker Script。简单来说链接脚本就是嵌入式系统的城市规划图它告诉链接器代码应该放在Flash的哪个位置变量应该占用RAM的哪些区域堆栈空间如何划分特殊数据结构的存放规则以常见的CH32V103芯片为例它的内存布局是这样的MEMORY { FLASH (rx) : ORIGIN 0x00000000, LENGTH 64K RAM (xrw) : ORIGIN 0x20000000, LENGTH 20K }这表示我们有64KB只读可执行的Flash空间存放代码和20KB可读写执行的RAM空间存放数据。链接脚本的核心任务就是合理分配这两块土地资源。2. 从零构建内存布局2.1 定义MEMORY区域在项目根目录创建link.ld文件首先声明内存区域MEMORY { ITCM (rx) : ORIGIN 0x00000000, LENGTH 16K FLASH (rx) : ORIGIN 0x00004000, LENGTH 48K RAM (xrw) : ORIGIN 0x20000000, LENGTH 20K }这里做了更细致的划分ITCM16KB高速指令存储器主Flash剩余48KB空间RAM保持20KB不变注意ORIGIN地址必须参考芯片手册的Memory Map章节2.2 关键段(SECTION)布局SECTIONS { /* 中断向量表必须放在起始位置 */ .vectors : { *(.vectors) . ALIGN(4); } ITCM ATITCM /* 初始化代码 */ .init : { _sinit .; KEEP(*(SORT_NONE(.init))) _einit .; } FLASH ATFLASH /* 代码段 */ .text : { *(.text .text.*) *(.rodata .rodata.*) } FLASH ATFLASH }几个关键点.vectors段用ALIGN(4)保证4字节对齐KEEP确保.init段不被链接器优化掉*(.text .text.*)匹配所有.text开头的段3. 数据段的重定位魔法3.1 VMA与LMA的区分这是链接脚本最精妙的部分.data : { _data_vma .; /* RAM中的运行时地址 */ *(.data .data.*) _edata .; } RAM ATFLASH _data_lma LOADADDR(.data); /* Flash中的加载地址 */VMA (Virtual Memory Address): 程序运行时访问的地址RAMLMA (Load Memory Address): 存储在Flash中的物理地址3.2 启动时的数据搬运需要在启动文件中完成数据搬运la a0, _data_lma /* 源地址(Flash) */ la a1, _data_vma /* 目标地址(RAM) */ la a2, _edata /* 结束地址 */ 1: lw t0, (a0) sw t0, (a1) addi a0, a0, 4 addi a1, a1, 4 bltu a1, a2, 1b4. 堆栈与动态内存管理4.1 栈空间配置__stack_size 2K; .stack : { . ALIGN(16); _sstack .; . __stack_size; _estack .; } RAM在启动文件中初始化栈指针la sp, _estack4.2 堆空间定义.heap : { . ALIGN(4); _end .; /* 堆起始地址 */ . 0x800; /* 2KB堆空间 */ _heap_end .; /* 堆结束地址 */ } RAM配套实现_sbrk函数供malloc使用void *_sbrk(ptrdiff_t incr) { extern char _end[], _heap_end[]; static char *curbrk _end; if ((curbrk incr _heap_end)) return (void*)-1; void *oldbrk curbrk; curbrk incr; return oldbrk; }5. 高级技巧与调试5.1 使用PROVIDE定义全局符号.bss : { _sbss .; *(.bss .bss.*) _ebss .; PROVIDE(end .); } RAM这样在C代码中可以直接引用extern uint32_t _sbss, _ebss;5.2 调试信息保留.debug_info 0 : { *(.debug_info) } .debug_line 0 : { *(.debug_line) } .debug_abbrev 0 : { *(.debug_abbrev) }5.3 链接时检查ASSERT((SIZEOF(.stack) __stack_size), Error: Stack overflow);6. 完整示例GD32VF103链接脚本ENTRY(_start) MEMORY { FLASH (rx) : ORIGIN 0x08000000, LENGTH 128K RAM (xrw) : ORIGIN 0x20000000, LENGTH 32K } SECTIONS { .isr_vector : { . ALIGN(4); KEEP(*(.isr_vector)) . ALIGN(4); } FLASH .text : { . ALIGN(4); *(.text .text.*) *(.rodata .rodata.*) . ALIGN(4); } FLASH .data : { _data_vma .; *(.data .data.*) _edata .; } RAM ATFLASH .bss : { _sbss .; *(.bss .bss.*) _ebss .; } RAM .stack (NOLOAD) : { . ALIGN(8); _sstack .; . 4K; _estack .; } RAM /DISCARD/ : { *(.comment) *(.riscv.attributes) } }7. 常见问题排查链接失败地址溢出检查MEMORY区域长度是否足够使用-Wl,--print-memory-usage查看占用情况变量值异常确认.data段搬运代码正确执行检查VMA/LMA地址是否匹配栈溢出增大__stack_size使用-fstack-usage分析栈使用全局指针优化问题确认__global_pointer$正确定义必要时使用-mno-relax关闭优化在实际项目中我遇到过最棘手的问题是中断向量表位置错误导致无法进入中断。后来发现是.vectors段没有严格对齐到0x00000000地址。这个教训让我明白链接脚本的每个字节位置都值得认真对待。

更多文章