01环境与工具链
这次实验从 Linux 用户程序开始。宿主开发环境是 x86_64 / WSL2 Debian,编译目标为 RISC-V 64 位 ELF。先检查两套交叉编译器与 QEMU,再分别验证 C 程序和手写汇编程序。
| 工具 | 版本 | 用途 |
|---|---|---|
riscv64-linux-gnu-gcc | 14.2.0 | Linux 目标交叉编译 |
riscv64-unknown-elf-gcc | 14.2.0 | 裸机目标工具链 |
qemu-riscv64 | 10.0.13 | 用户模式运行 |
qemu-system-riscv64 | 10.0.13 | 系统模式;本次检查版本 |
riscv64-linux-gnu-objdump | — | ELF 反汇编观察 |
02C → ELF → 运行
先用一个只输出字符串的 C 程序,验证交叉编译与用户模式运行这条链路。
#include <stdio.h>
int main(void) {
printf("Subete Anata no Sei Desu.\n");
return 0;
}riscv64-linux-gnu-gcc -static hello.c -o hello
file hello
qemu-riscv64 hellofile hello 显示目标文件为静态链接的 RISC-V 64 位 ELF。通过 qemu-riscv64 运行后,程序输出 Subete Anata no Sei Desu.。
03观察 main 的反汇编
使用 riscv64-linux-gnu-objdump -d 观察生成的 ELF。下面是实际程序中 main 的反汇编摘录。
# Excerpt from the actual C ELF, as printed by objdump; no personal fields.
000000000001047a <main>:
1047a: 1141 addi sp,sp,-16
1047c: e406 sd ra,8(sp)
1047e: e022 sd s0,0(sp)
10480: 0800 addi s0,sp,16
10482: 00040517 auipc a0,0x40
10486: 52650513 addi a0,a0,1318 # 509a8
1048a: 043000ef jal 10ccc <_IO_puts>
1048e: 4781 li a5,0
10490: 853e mv a0,a5
10492: 60a2 ld ra,8(sp)
10494: 6402 ld s0,0(sp)
10496: 0141 addi sp,sp,16
10498: 8082 ret- 函数序言:保存
ra与s0,建立 16 字节栈帧。 - 参数与调用:
auipc配合addi计算字符串地址,jal调用_IO_puts;a0用于参数与返回值。 - 函数返回:恢复寄存器与栈指针,再通过
ret返回。 - 指令长度:反汇编中同时出现 2 字节与 4 字节编码,反映 RVC 压缩指令扩展的使用。
04直接编写汇编
绕过 C 源码,直接编写 RISC-V 汇编,再依次通过 as 与 ld 完成汇编和链接。这段程序使用 Linux 系统调用输出相同的字符串。
.section .rodata
message:
.ascii "Subete Anata no Sei Desu.\n"
message_end:
.equ message_len, message_end - message
.section .text
.globl _start
_start:
# write(1, message, message_len)
li a0, 1
la a1, message
li a2, message_len
li a7, 64
ecall
# exit(0)
li a0, 0
li a7, 93
ecalla7 = 64选择 Linuxwrite。a0 = 1指向标准输出,a1保存缓冲区地址,a2保存长度,由ecall发起调用。a7 = 93、a0 = 0发起exit(0)。
实际运行输出与 C 程序相同,退出状态为 0。
05问题与排查
stdio.h 缺失:选择匹配目标的工具链
最初使用现有的 riscv64-unknown-elf-gcc 编译包含 stdio.h 的程序,报错 stdio.h: No such file or directory。本次要运行的是 Linux 用户程序,切换到 riscv64-linux-gnu-gcc 后完成编译。
动态加载器路径:为 QEMU 指定目标 sysroot
去掉 -static 后,生成的动态 ELF 指定了 RISC-V 加载器 /lib/ld-linux-riscv64-lp64d.so.1。直接运行时,QEMU 提示找不到这个加载器。
改用 qemu-riscv64 -L /usr/riscv64-linux-gnu 指定目标 sysroot 后,同一个 ELF 正常运行。对应报错与成功输出保留在下方的真实记录中。
长度符号的位置:在使用前定义 .equ
汇编时曾把长度符号的定义放在 li a2,message_len 之后,遇到 illegal operands。将字符串起止符号及 .equ 移至使用位置之前后,汇编成功;上方的公开源码保留了调整后的顺序。
06运行记录与结果
# Real experiment output excerpt; personal fields omitted.
# On host: x86_64 WSL2 Debian; target: RISC-V Linux ELF.
riscv64-linux-gnu-gcc version 14.2.0
qemu-riscv64 version 10.0.13
ELF 64-bit LSB executable, UCB RISC-V, RVC, ... statically linked
$ qemu-riscv64 hello
Subete Anata no Sei Desu.
$ qemu-riscv64 hello-asm
Subete Anata no Sei Desu.
$ echo $?
0
# Dynamic ELF path issue (same file works after specifying target sysroot):
$ qemu-riscv64 /tmp/hello-dynamic
qemu-riscv64: Could not open '/lib/ld-linux-riscv64-lp64d.so.1': No such file or directory
$ qemu-riscv64 -L /usr/riscv64-linux-gnu /tmp/hello-dynamic
Subete Anata no Sei Desu.