共通 - 程序和二进制文件
目标程序说明
上一节的目标程序可以分成三个部分:
- 计算
1+2+3+...+100的结果:t0是循环计数器,t1保存上限100,t2保存累加结果。beq t0, t1, next用于判断是否已经累加到 100。 - 将结果存入
0x80000100处:lui t0, 0x80000得到0x8000_0000,随后sw t2, 0x100(t0)将t2中保存的计算结果写到0x8000_0100。 - 在串口输出
done!:输出每个字符前,程序先用lb读取 UART 状态字节,再用andi ... , 0x20保留 bit 5。如果结果为零,说明串口发送端不可用,继续轮询;否则用sb写入数据寄存器,发送一个字符。这与实验 4 中“先读状态,再写数据”的状态机流程相对应。
为什么要在代码中打 5 遍几乎相同的代码来输出'done!'?
如果使用类似于实验 1 中的方式,将写串口封装为函数,则会向程序中加入两条相对较难实现的指令jal和jalr。为了让实验 5 更加简单,我们选择不在实验 5 中要求大家实现这两条指令。
获得程序
在本实验中,程序需要预先装载至 SRAM 当中。CPU 读取的应该是指令的二进制编码,而不是汇编源文件中的代码,因此需要先生成目标程序对应的二进制文件。
在提供的 rv-2025.zip 中(简介#代码示例),有 asmcode 这个文件夹。在这个文件夹中提供了两份样例汇编以及一个 makefile 文件用于生成之后需要用到的二进制文件。
其中 makefile 文件如下:
BUILD_DIR = .
ARM_SOURCES = $(wildcard *.s)
TARGETS = $(addprefix $(BUILD_DIR)/,$(notdir $(ARM_SOURCES:.s=.bin)))
all: $(TARGETS)
%.elf: %.s
riscv64-unknown-elf-gcc $< -march=rv32i -mabi=ilp32 -static -fno-pic -fno-builtin -nostdlib -nostdinc -g -o $@ -Wl,-Ttext,0x0
%.bin: %.elf
riscv64-unknown-elf-objcopy -j .text -O binary -S $< $@
可以看到,这个 makefile 默认对文件夹下的每一个.s 文件都会生成对应的.bin 文件。
关于生成 elf 这条命令中的参数,在RISC-V 汇编语言程序的调试一节中已经做了详细说明,在此不多赘述。
在生成 elf 之后,通过工具链中的 objcopy 工具,将 elf 文件中的.text 段提取了出来,并以二进制的方式写入到了.bin 文件中。
将上一节中的汇编放入该文件夹,保存为扩展名为 s 的汇编文件(如 sum.s)。在 实验环境 中,进入该文件夹执行 make 指令,我们就获得了可用于实验的二进制文件了(使用 sum.s 会获得 sum.bin)。
当你需要获得 ELF 文件进行反汇编时,可以单独生成它。例如,在同一文件夹中执行:
make sum.elf
riscv64-unknown-elf-objdump -d sum.elf
反汇编结果可以帮助你把仿真中的指令编码与汇编语句对应起来。
链接地址与装载地址
上面的命令将 .text 链接在 0x0,而实验将 .bin 装入 BaseRAM 起始地址 0x8000_0000,CPU 复位后也从这个地址取指。.bin 本身不包含装载地址信息,需要由装载工具指定位置。
目标程序中的 beq 使用相对于当前指令地址的偏移,因此将整段程序平移到 BaseRAM 后,分支仍能跳到正确位置。查看反汇编时,指令地址 0x0 对应实际运行地址 0x8000_0000,后续地址依此类推。
三个文件的关系如下:
| 文件 | 内容 | 用途 |
|---|---|---|
sum.s |
人可以阅读和编辑的汇编代码 | 编写目标程序 |
sum.elf |
机器码以及段信息、符号等 | 反汇编,检查指令编码和跳转位置 |
sum.bin |
从 ELF 的 .text 段提取的原始字节 |
装入 SRAM,供 CPU 取指 |
总结
本节说明了目标程序的三个主要功能,以及从汇编源文件生成二进制文件的过程。
学完这节内容我需要做什么?
生成目标程序的二进制文件,理解求和、保存结果和轮询串口的流程。