3. RISC-V汇编与C语言编程
在 RISC-V SoC 系统软件开发中,C 语言负责业务逻辑与驱动主体,汇编负责启动、上下文切换、CSR 访问等“贴近硬件”的动作。两者之间的桥梁由三部分构成:汇编语法与内联汇编、ABI 调用约定、链接脚本与启动流程。理解这三者,才能解释“为什么 C 函数能调用汇编入口”“为什么中断返回后寄存器还是对的”“为什么变量被放到了错误的地址”。
一、RISC-V 汇编语法要点
RISC-V 汇编采用“操作码 + 目标寄存器 + 源寄存器”的固定顺序,寄存器以 x0–x31 编号,同时有 ABI 别名(如 sp、ra、a0)。伪指令(li、la、mv、call)由汇编器展开为真实指令,编写时优先使用伪指令可提升可读性。常见段指令包括 .text、.data、.bss、.section、.globl、.align。
.section .text
.globl _start
_start:
la t0, _stack_top # 取栈顶地址
mv sp, t0 # 初始化栈指针
call main # 调用 C 入口
j . # main 返回后原地循环
.section .bss
.align 4
stack:
.space 4096
_stack_top:
注意:la 依赖链接时的重定位,若链接脚本未定义符号或地址超出范围,会在链接阶段报错,而不是运行时才暴露。
二、内联汇编:把 CSR 与特权指令写进 C
访问 mstatus、mie、mepc 等 CSR,或执行 fence、wfi、sfence.vma 时,C 编译器没有对应语法,必须使用内联汇编。GCC 的基本形式为 asm volatile("指令" : 输出 : 输入 : 破坏列表)。其中 volatile 防止被优化删除,破坏列表(clobber)必须如实声明被修改的寄存器,否则编译器可能复用这些寄存器导致数据错乱。
static inline unsigned long read_mstatus(void)
{
unsigned long v;
asm volatile("csrr %0, mstatus" : "=r"(v));
return v;
}
static inline void write_mie(unsigned long v)
{
asm volatile("csrw mie, %0" : : "r"(v) : "memory");
}
static inline void enable_irq(void)
{
asm volatile("csrsi mstatus, 8" : : : "memory");
}
"memory",避免编译器把内存访问重排到 CSR 操作之前。三、ABI 调用约定:寄存器分工与栈帧
RISC-V 的整数调用约定(ILP32/LP64)规定:a0–a7 传参并返回结果,t0–t6 为调用者保存(caller-saved),s0–s11 为被调用者保存(callee-saved),ra 保存返回地址,sp 必须 16 字节对齐。函数序言保存 ra 与被使用的 s 寄存器,尾声恢复并 ret。
| 寄存器 | ABI 名 | 角色 | 谁负责保存 |
|---|---|---|---|
| x0 | zero | 常量 0 | — |
| x1 | ra | 返回地址 | 被调用者 |
| x2 | sp | 栈指针 | 被调用者 |
| x10–x17 | a0–a7 | 参数/返回值 | 调用者 |
| x5–x7, x28–x31 | t0–t6 | 临时寄存器 | 调用者 |
| x8–x9, x18–x27 | s0–s11 | 长期变量 | 被调用者 |
中断/异常处理函数若用 C 编写,必须加 __attribute__((interrupt)) 或由汇编包装保存全部 caller-saved 寄存器,否则被中断的 C 代码恢复后会读到被破坏的 t 寄存器。
四、链接脚本与启动流程
链接脚本(.ld)决定各段在地址空间中的位置,是“代码从哪开始执行”的最终裁决者。典型布局:.text 放在 ROM/Flash 起始处,.data 的加载地址在 ROM、运行地址在 RAM,.bss 仅占 RAM 不占镜像。启动代码需完成:设置栈指针、清零 .bss、从 ROM 拷贝 .data 到 RAM、设置 mtvec、跳转 main。
MEMORY {
ROM (rx) : ORIGIN = 0x80000000, LENGTH = 256K
RAM (rwx) : ORIGIN = 0x80040000, LENGTH = 64K
}
SECTIONS {
.text : { *(.text*) } > ROM
.rodata : { *(.rodata*) } > ROM
.data : { *(.data*) } > RAM AT> ROM
.bss : { *(.bss*) *(COMMON) } > RAM
}
五、典型误区与复盘
- 栈未对齐:RISC-V 要求
sp16 字节对齐,若启动代码把栈顶设为非对齐地址,调用含浮点或向量指令的函数会触发异常。 - 内联汇编漏写破坏列表:编译器认为某寄存器未被修改,继续使用旧值,表现为“偶发数据错误”,极难定位。
- .data 未拷贝:全局变量初值在 ROM 中,若启动代码跳过拷贝,变量读到的是随机值或 0。
- 链接脚本段顺序与对齐错误:
.text未按 4 字节对齐会导致取指异常;.data的AT>加载地址写错会导致拷贝源错误。
riscv64-unknown-elf-objdump -d 反汇编 _start,并用 readelf -S 核对各段地址,比盲目改代码更高效。综上,汇编语法决定“怎么写”,内联汇编决定“怎么在 C 中碰硬件”,ABI 决定“函数之间怎么交接”,链接脚本与启动流程决定“代码最终落在哪、从哪跑”。四者环环相扣,任何一环出错都会在启动阶段以异常、跑飞或数据错误的形式暴露。