使用riscv-none-embed-gcc时如何将main内局部变量分配至数据内存

预期实现效果:系统存在两块独立内存,分别为程序内存与数据内存,访问数据时需从数据内存中读取对应值。
测试所用C代码如下:
#define XRAM0 0x4000 // REG I/O int b=0x35; void main () { int a=0x55; // 希望GCC编译器将0x55初始值写入mem_data区域 int *address; address = XRAM0; address[0]=a; // 但GCC始终从DBus读取a变量(该区域未初始化,不存在0x55值) address[1]=b; }
代码包含全局变量b与局部变量a,当前使用riscv-none-embed-gcc编译后,始终从DBus读取a变量,但对应区域未完成初始化,无法读取到0x55值,目标是让编译器将a的初始值0x55预先写入mem_data区域。
本次编译通过ld链接脚本配置内存地址映射,所用编译命令如下:
riscv-none-embed-gcc -lc -nostdlib -O3 -march=rv32im -T test.ld TEST.c -o TEST.o -lg -lgloss -mcmodel=medlow riscv-none-embed-objcopy -O verilog "TEST.o" "TEST.vn" riscv-none-embed-objdump --disassemble-all -S -d TEST.o > TEST.lst
第一版链接脚本与编译结果
第一版链接脚本已配置程序内存与数据内存起始地址,指定数据段存放至数据内存,内容如下:
OUTPUT_ARCH( "riscv" ) MEMORY { mem_text (rx) : ORIGIN = 0x00000000, LENGTH = 0x1000000 // 程序内存起始地址 mem_data (rw) : ORIGIN = 0x20000000, LENGTH = 0x1000000 // 数据内存起始地址 } SECTIONS { . = 0x00000000; .text : { *(.text) } > mem_text // 代码段放入程序内存 . = 0x20000000; .data : { *(.sdata) } > mem_data // 数据段放入数据内存 }
编译后全局变量b已存放至mem_data区域,但main函数内的局部变量a未存入该区域,反汇编结果如下:
TEST.o: file format elf32-littleriscv Disassembly of section .text.startup: 00000000 <main>: 0: 000047b7 lui a5,0x4 4: 05500713 li a4,85 8: 00e7a023 sw a4,0(a5) # 4000 <main+0x4000> c: 20000737 lui a4,0x20000 10: 00072703 lw a4,0(a4) # 20000000 <b> 14: 00e7a223 sw a4,4(a5) 18: 00008067 ret Disassembly of section .data: 20000000 <b>: 20000000: 0035 c.nop 13 // b变量位于mem_data区域,a变量不在该区域
转换为机器码后结果一致,仅b变量位于mem_data区域,a变量未存入:
@00000000 B7 47 00 00 13 07 50 05 23 A0 E7 00 37 07 00 20 03 27 07 00 23 A2 E7 00 67 80 00 00 @20000000 35 00 00 00 // b变量位于mem_data区域,a变量不在该区域
第二版链接脚本与编译结果
修改链接脚本后机器码生成结果无变化,修改后的链接脚本如下:
OUTPUT_ARCH( "riscv" ) MEMORY { mem_text (rx) : ORIGIN = 0x00000000, LENGTH = 0x1000000 mem_data (rw) : ORIGIN = 0x20000000, LENGTH = 0x1000000 } SECTIONS { . = 0x00000000; .text : { *(.text) } > mem_text . = 0x20000000; .data : { *(.sdata) } > mem_data at > mem_text }
修改后反汇编结果如下:
TEST.o: file format elf32-littleriscv Disassembly of section .text.startup: 00000000 <main>: 0: 000047b7 lui a5,0x4 4: 05500713 li a4,85 8: 00e7a023 sw a4,0(a5) # 4000 <main+0x4000> c: 20000737 lui a4,0x20000 10: 00072703 lw a4,0(a4) # 20000000 <b> 14: 00e7a223 sw a4,4(a5) 18: 00008067 ret Disassembly of section .data: 20000000 <b>: 20000000: 0035 c.nop 13
对应生成的机器码如下:
@00000000 B7 47 00 00 13 07 50 05 23 A0 E7 00 37 07 00 20 03 27 07 00 23 A2 E7 00 67 80 00 00 @0000001C 35 00 00 00
改链接脚本无效的核心原因非常明确:开启-O3优化的情况下,普通的非static、非volatile局部变量不会被分配内存空间,编译器会直接把它的常量值硬编码成指令里的立即数。
看反汇编代码里的li a4,85,85就是0x55,编译器已经把a的值直接嵌到指令里了,从头到尾没给a分配数据段或者栈上的内存地址,这种情况下不管怎么改链接脚本,都不可能把不存在的内存变量放到mem_data区域。
另外链接脚本本身也有配置缺陷:.data段只匹配了.sdata段,普通.data段、.bss段、栈空间都没有正确映射到mem_data,就算变量被分配到数据段,也可能因为匹配规则遗漏没有放到目标内存区域。
根据需求选以下任意一种方案即可:
- 方案1:把局部变量a改为静态存储周期。要么把a定义成全局变量,要么给局部变量a加
static修饰,这样a会被编译器分配到.data段,只要链接脚本正确配置.data段到mem_data,0x55的初始值就会被预存到数据内存。修改后的代码示例:
#define XRAM0 0x4000 // REG I/O int b=0x35; static int a = 0x55; // static修饰后进入.data段 void main () { int *address; address = XRAM0; address[0] = a; address[1] = b; }
- 方案2:用volatile修饰a同时配置栈空间到mem_data。给a加
volatile关键字可以强制编译器禁止常量折叠优化,必须给a分配内存地址。注意这种方式a默认分配在栈上,需要同时在链接脚本里把栈指针指向mem_data地址范围,否则a还是会落在非目标内存区域。示例代码:
#define XRAM0 0x4000 // REG I/O int b=0x35; void main () { volatile int a = 0x55; // 强制分配内存,禁止优化为立即数 int *address; address = XRAM0; address[0] = a; address[1] = b; }
补全链接脚本里.data段的匹配规则,修正后的.data段配置参考:
.data : { *(.sdata) *(.sdata.*) *(.data) *(.data.*) . = ALIGN(4); } > mem_data
如果硬件是哈佛架构,程序内存和数据内存完全独立、地址总线分开,还需要补充一段启动代码:上电后把存储在程序内存里的.data段初始值拷贝到mem_data对应的地址,否则就算链接脚本配置正确,数据内存上电时是随机值,也读不到预存的0x55和0x35初始值。第二版链接脚本里写的> mem_data at > mem_text就是把.data的加载地址放在程序内存、运行地址放在数据内存,这种配置必须搭配上述拷贝启动代码才能正常工作。
内容的提问来源于stack exchange,提问作者kostya

