RISC-V 汇编程序编程练习
一,实验目的
通过编写和运行RISC-V汇编程序,熟悉RISC-V基本指令集中的指令、RISC-V汇编的编程规范,了解递归函数的调用过程。
二,实验步骤(包括实验结果,数据记录、截图等)
2.1 使用RV64I指令集编写32位整数乘法函数mymul
2.1.1 思路
先用c语言编写用加法实现有符号乘法的代码,然后再扮演人肉编译器,将c代码一行一行编译成Riscv汇编语言。
2.1.2 结果符号判断
定义sgn变量,初始值为0,根据a、b的符号赋予不同的值。
给sgn赋完值后,结果符号已确定,所以a、b均取绝对值进行后续运算。
a | b | sgn |
---|---|---|
≥ 0 \ge0 ≥0 | ≥ 0 \ge 0 ≥0 | 0 |
< 0 < 0 <0 | < 0 < 0 <0 | 0 |
< 0 < 0 <0 | ≥ 0 \ge 0 ≥0 | 1 |
≥ 0 \ge 0 ≥0 | < 0 < 0 <0 | -1 |
所以只要sgn为0,结果就为正值;否则取负。
对应代码如下:
int sgn=0;
if(a < 0)
{
a = -a;
sgn ++;
}
if(b < 0)
{
b = -b;
sgn --;
}
if(sgn == 0) return res;
else return -res;
2.1.2 思路一:用位移实现乘法
2.1.2.1 C语言代码
考察二进制乘法的分解:对于 a × b a\times b a×b,将b逐位分解。只要b的第i位 ≠ 0 \ne0 =0,那么a便左移i位。最后将每一位的乘法结果相加即可。
int res = 0;
while (b) {
if (b & 1) {
res += a;
}
a <<= 1;
b >>= 1;
}
完整代码:
int mymul(int a,int b) {
int res = 0;
int sgn = 0;
if(a>0)
{
a=-a; sgn ++;
}
if(b<0)
{
b=-b; sgn--;
}
while (b) {
if (b & 1) {
res += a;
}
a <<= 1;
b >>= 1;
}
if(sgn == 0) return res;
else return -res;
}
2.1.2.2 人肉编译器模式
逐行编译如下:
.text
.globl mymul
.type mymul,@function
mymul:
addi sp,sp,-32
sd ra,24(sp)
sd s0,16(sp)
addi s0,sp,32
mv a5,a0
mv a4,a1
sw a5,-20(s0) #存a之处
sw a4,-24(s0) #存b之处
#初始化res
sw zero,-28(s0)
#初始化sgn
sw zero,-32(s0)
.ifa:
#if(a<0)
lw a5,-20(s0)
sext.w a5,a5
bgez a5,.elsea
#a=-a
negw a5,a5
sw a5,-20(s0)
#sgn++
lw a3,-32(s0)
addiw a3,a3,1
sw a3,-32(s0)
.elsea:
.ifb:
#if(b<0)
lw a4,-24(s0)
sext.w a4,a4
bgez a4,.elseb
#b=-b
negw a4,a4
sw a4,-24(s0)
#sgn--
lw a3,-32(s0)
addiw a3,a3,-1
sw a3,-32(s0)
.elseb:
.while:
#while(b!=0)
lw a4,-24(s0)
beqz a4,.outwhile
## if(b&1)
andi a6,a4,1
beqz a6,.outif
# res+=a
lw a7,-28(s0)
lw a5,-20(s0)
addw a7,a7,a5
sw a7,-28(s0)
.outif:
slli a5,a5,1
srli a4,a4,1
sw a5,-20(s0)
sw a4,-24(s0)
j .while
.outwhile:
.ifsgn:
lw a3,-32(s0)
lw a7,-28(s0)
beqz a3,.end
sext.w a7,a7
negw a7,a7
.end:
mv a0,a7
ld ra,24(sp)
ld s0,16(sp)
addi sp,sp,32
ret
2.1.3 思路二:用加法实现乘法
2.1.3.1 C语言代码
考察小学乘法的定义, a × b a\times b a×b即 a a a个 b b b相加,因此只需要使用循环语句,实现a个b相加即可。
int res = 0;
for(int i=0;i<b;i++)
res+=a;
完整代码:
#include<stdlib.h>
#include<stdio.h>
int mul(int a,int b) {
int res = 0;
int sgn=0;
if(a < 0)
{
a = -a;
sgn ++;
}
if(b < 0)
{
b = -b;
sgn --;
}
for(int i=0;i<b;i++)
res+=a;
if(sgn == 0) return res;
else return -res;
}
2.1.3.2 人肉编译器模式
将上述代码一行一行编译成riscv64的汇编语言。
- 分配栈空间
因为本函数变量较多,原先分配的32个Byte的空间不够,因此必须重新分配栈空间。
最顶层的ra和s0各占8Byte,其余变量a,b,res,sgn,i各占4Byte,共需36Byte的栈空间。
栈空间分配如下:
- 逐行编译如图:
.text
.globl mymul
.type mymul,@function
mymul:
addi sp,sp,-36
sd ra,28(sp)
sd s0,20(sp)
addi s0,sp,36
#Your code here
#先把参数存入栈中
mv a5,a0
mv a4,a1
sw a5,-32(s0)
mv a5,a4
sw a5,-36(s0)
#初始化 result和sgn
sw zero,-20(s0)
sw zero,-24(s0)
#判断a和b的符号
#判断a是否<0
.ifa:
lw a5,-32(s0)
sext.w a5,a5
bgez a5,.elsea
#a<0,进入if语句,将a取相反数
lw a5,-32(s0)
negw a5,a5
sw a5,-32(s0)
#sgn++
lw a5,-24(s0)
addiw a5,a5,1
sw a5,-24(s0)
.elsea:
.ifb:
#判断b是否<0
lw a5,-36(s0)
sext.w a5,a5
bgez a5,.elseb
#b<0,进入if语句,将b取相反数
lw a5,-36(s0)
negw a5,a5
sw a5,-36(s0)
#sgn--
lw a5,-24(s0)
addiw a5,a5,-1
sw a5,-24(s0)
.elseb:
#进入for语句
.for1:
# int i = 0
sw zero,-28(s0)
j .for2
.in_for:
# for 主体
lw a4,-20(s0)
lw a5,-32(s0)
addw a5,a5,a4
sw a5,-20(s0)
.for3:
# i++
lw a5,-28(s0)
addiw a5,a5,1
sw a5,-28(s0)
.for2:
# 判断 i 和 b 的大小
lw a4,-28(s0)
lw a5,-36(s0)
sext.w a4,a4
sext.w a5,a5
blt a4,a5,.in_for
.if_sgn:
# 根据sgn的值决定是否要加负号
lw a5,-24(s0)
sext.w a5,a5
beqz a5,.else_sgn
# sgn != 0,变号
lw a5,-20(s0)
negw a5,a5
sext.w a5,a5
.else_sgn:
# sgn == 0,不变号,直接返回result
lw a5,-20(s0)
j .end
.end:
# end
mv a0,a5
ld ra,28(sp)
ld s0,20(sp)
addi sp,sp,36
ret
2.2 使用RV64I指令集和mymul函数实现自然数阶乘函数myfac
2.2.1 C语言代码
int myfunc(int a)
{
int result = 1;
for(int i = 1; i<=a; i++)
{
result = mymul(result,i);
}
return result;
}
2.2.2 人肉编译器模式
- 分配栈空间
- 逐行编译
.text
.globl myfac
.type myfac,@function
myfac:
addi sp,sp,-28
sd ra,20(sp)
sd s0,12(sp)
addi s0,sp,28
#Your code here
# 存a
mv a5,a0
sw a5,-28(s0)
# int result = 1
li a5,1
sw a5,-20(s0)
.for1:
# for: int i = 1
li a5,1
sw a5,-24(s0)
j .for2
.in_for:
# for主体
lw a4,-24(s0)
lw a5,-20(s0)
mv a1,a4
mv a0,a5
call mymul
mv a5,a0
sw a5,-20(s0)
.for3:
# i++
lw a5,-24(s0)
addiw a5,a5,1
sw a5,-24(s0)
.for2:
# 判断i和a的大小
lw a4,-24(s0)
lw a5,-28(s0)
sext.w a4,a4
sext.w a5,a5
ble a4,a5,.in_for
.end:
lw a5,-20(s0)
mv a0,a5
ld ra,20(sp)
ld s0,12(sp)
addi sp,sp,28
ret
2.3 编译和测试结果
将文件按如下位置放置:
makefile如下:
CC=riscv64-unknown-elf-gcc
CFLAGS=-g
EXEC=test
# mul1或mul2均可
EXECOBJ=mul2.o fac2.o main.o
SRC=mul2.s fac2.s main.c
all: $(EXEC)
$(EXEC): $(EXECOBJ)
$(CC) $(CFLAGS) $^ -o $@
$(EXECOBJ): $(SRC)
$(CC) $(CFLAGS) -c $^
.PHONY: clean
clean:
rm -rf $(EXECOBJ) $(EXEC)
最终,思路一和思路二都顺利通过了测试,结果如下:
三,实验分析和总结
3.1 各伪操作标识符解释
- .text:代码段,等效于“.section .text”。
- .globl myfac:定义一个全局符号 myfac。只要一起编译,在另一个文件也可以通过引用”myfac“跳转到myfac代表的地址
- .type myfac,@function :.type伪操作用于定义符号的类型,将名为myfac的符号定义为一个函数(function)。
3.2 代表地址的符号
- .part和part:前面有“.”表示该符号仅对当前文件可见,没有"."则对全局可见
四,实验收获、存在问题、改进措施或建议
4.1 汇编语言
汇编语言(Assembly Language)是任何一种用于电子计算机、微处理器、微控制器或其他可编程器件的低级语言,亦称为符号语言。在汇编语言中,用助记符代替机器指令的操作码,用地址符号或标号代替指令或操作数的地址。
与高级语言相比,汇编语言更接近计算机底层的硬件结构,因此在某些场合下可以提供更高效、更精确的控制。通常使用汇编语言编写程序时,需要了解CPU的架构、指令集和寄存器等底层细节。在不同的设备中,汇编语言对应着不同的机器语言指令集,通过汇编过程转换成机器指令。特定的汇编语言和特定的机器语言指令集是一一对应的,不同平台之间不可直接移植。
汇编语言也是高级语言和机器语言之间的桥梁,可以通过汇编语言将高级语言翻译成机器指令,也可以通过汇编语言对机器指令进行调试和优化。虽然汇编语言相对于高级语言来说更加繁琐和复杂,但它也具有灵活性和效率优势,在某些场合下仍然具有重要的应用价值。
4.2 Segmentation fault
我在某次测试时,遇到了如下错误:
仔细检查,发现是在最后end的代码段中,不慎将ld敲成了lw。为什么lw就不行,具体原因未知。
.end:
mv a0,a7
#以下lw应该是ld
lw ra,24(sp)
lw s0,16(sp)
addi sp,sp,32
ret