571
社区成员
发帖
与我相关
我的任务
分享RISC-V中常见寄存器作用
sp,ra,s0,a0,a5都是RISC-V汇编语言中的寄存器名。sp是栈指针寄存器,常储存栈顶地址。ra是返回地址寄存器,用来存储函数调用后的返回地址。s0是保存寄存器,用来保存函数调用前后不变的值,可以保存栈底。a0和a5是参数寄存器,用来传递函数调用的参数。
RISC-V中常见指令
加载指令:
ld和lw都是RISC-V指令集中的加载指令,用于从存储器中读取数据到寄存器中。它们的区别在于ld是加载双字(64位)数据,而lw是加载字(32位)数据。格式如下:
ld rd, offset(rs1) // rd = M[rs1 + offset]
lw rd, offset(rs1) // rd = M[rs1 + offset][31:0]
其中rd是目标寄存器,rs1是基址寄存器,offset是偏移量,M表示存储器1。
存储指令:
sd和sw都是RISC-V指令集中的存储指令,用于将寄存器中的数据写入到存储器中。它们的区别在于sd是存储双字(64位)数据,而sw是存储字(32位)数据。格式如下:
sd rs2, offset(rs1) // M[rs1 + offset] = rs2
sw rs2, offset(rs1) // M[rs1 + offset][31:0] = rs2[31:0]
其中rs2是源寄存器,rs1是基址寄存器,offset是偏移量,M表示存储器。
RISC-V目的数与操作数
如果你了解过RISC-V汇编,你会发现加载指令和存储指令的目的数和操作数似乎是相反的。
加载指令的目的数是寄存器,操作数是存储器。存储指令的目的数是存储器,操作数是寄存器。这是因为RISC-V通常将目的寄存器放在第一个位置,维护代码的一致性。
RISC-V伪指令
li伪指令
li指令是一种加载立即数的伪指令,它可以将一个常数加载到寄存器中。RISC-V中的li指令可能会被扩展为lui或addi或者两者都有,具体取决于立即数的大小和位数。
# 例子1:将立即数0x12345678加载到寄存器a5
li a5, 0x12345678 # 这条伪指令会被扩展为两条实际指令
lui a5, 0x12345 # 将高20位加载到a5
addi a5, a5, 0x678 # 将低12位加到a5
lui指令是加载上位立即数的指令,它可以将一个20位的立即数左移12位后存入目标寄存器,相当于将高20位赋值给寄存器,低12位补零12。addi指令是加上立即数的指令,它可以将一个12位的立即数与源寄存器中的值相加后存入目标寄存器3。如果要加载一个32位的立即数,可能需要用到lui和addi两条指令结合使用,先用lui加载高20位,再用addi加载低12位2。注意,在使用addi时,需要考虑立即数的符号和补码问题。
j指令是无条件跳转指令,它可以将一个26位的立即数左移2位后与当前程序计数器(PC)的高4位拼接,形成一个32位的地址,然后将该地址赋值给PC。jr指令是跳转寄存器指令,它可以将一个源寄存器中的值直接赋值给PC。j和jr都是用来改变程序执行流程的指令,但j需要提供一个立即数作为目标地址,而jr需要提供一个寄存器作为目标地址。
Call伪指令
RISC-V call伪指令是一种用于调用函数的简化指令,它实际上是两条真实的RISC-V指令的组合:auipc和jalr。
auipc和jalr是两条RISC-V指令,分别用于将程序计数器(PC)的高20位加到一个寄存器上,以及将PC设置为一个寄存器的值加上一个偏移量。它们通常一起使用,以实现跳转到任意地址的功能。例如,auipc t0, %pcrel_hi(label)会将PC的高20位加上label相对于当前指令的高位偏移量,存入t0寄存器;然后jalr t1, t0, %pcrel_lo(label)会将PC设置为t0寄存器的值加上label相对于当前指令的低位偏移量,并将原来的PC+4存入t1寄存器3。这样就完成了对label的跳转,并保存了返回地址。
auipc ra, getSum[31:12] # 将当前PC与getSum 地址高20位拼接后赋值给ra寄存器
jalr ra, ra, getSum[11:0] # 将当前PC+4赋值给ra寄存器,并将ra寄存器与getSum地址低12位相加后赋值给PC
C代码如下
int getSum(int num){
int sum = 0;
if(num < 0){
return 0;
}else{
while(num > 0){
sum += num;
num--;
}
}
return sum;
}
int main(){
return getSum(5);
}
编译为RISC-V汇编如下
.file "code.c"
.option pic
.text
.align 1
.globl getSum
.type getSum, @function
getSum:
addi sp,sp,-48 //开辟48byte的栈空间
sd s0,40(sp) //将s0的值写入栈顶指针sp+40的内存空间
addi s0,sp,48 //将栈顶指针sp+48,即栈底的地址赋给s0
mv a5,a0 //将a0的值赋给a5,a0寄存器保存的是传入参数
sw a5,-36(s0) //将a5 的值(32位)写入s0-36的空间
sw zero,-20(s0) //将0(32位)写入s0-20的地址空间
lw a5,-36(s0) //将s0 -36地址中的值(32位)赋给a5
sext.w a5,a5 //将a5扩展为64位
bgez a5,.L4 //比较a5与0的大小,若a5 >= 0 ,跳转到L4
li a5,0 //将立即数0加载到a5
j .L3 //无条件跳转到L3
.L5:
lw a4,-20(s0) //s0-20地址处的值加载到a4
lw a5,-36(s0) //s0-36地址处的值加载到a5
addw a5,a4,a5 //a4 + a5 的值(32)写回a5
sw a5,-20(s0) //a5值写入s0-20 注意这里保存的是sum值
lw a5,-36(s0) //s0-36值加载到a5 将num值重新写入a5
addiw a5,a5,-1 //a5值自减
sw a5,-36(s0) //a5的值写入s0-36 ,这里保存num
.L4: //注意这里没有跳转,顺序执行
lw a5,-36(s0) //s0-36 -> a5
sext.w a5,a5 //a5扩展为64位
bgtz a5,.L5 //这里验证while循环条件,a5 > 0 跳转到L5
lw a5,-20(s0) //s0 – 20 -> a5 , 即将sum值a5
.L3:
mv a0,a5 //a5 -> a0
ld s0,40(sp) //sp+40 ->s0,还记得吗,这里保存的是mian栈的栈底 addi sp,sp,48 //sp指向getSum栈底
jr ra //ra->pc,pc将会执行call的下一条语句
.size getSum, .-getSum
.align 1
.globl main
.type main, @function
main:
addi sp,sp,-16 //sp = sp – 16,开辟16byte空间
sd ra,8(sp) //ra中即返回地址写入sp+8
sd s0,0(sp) //s0中即上个栈底写入sp
addi s0,sp,16 //sp+16的写入s0,即将s0置为当前栈底
li a0,5 //将立即数5加载到a0,准备参数传递
call getSum //伪指令,保存下条指令到ra,并且pc转到getSum
mv a5,a0 //a0->a5
mv a0,a5 //a5->a0
ld ra,8(sp) //sp+8 -> ra ,返回地址值
ld s0,0(sp) //sp->s0,上个栈底
addi sp,sp,16 //sp + 16->sp, 栈顶指针指向栈底
jr ra //ra->pc
.size main, .-main
.ident "GCC: (Ubuntu 9.4.0-1ubuntu1~20.04) 9.4.0"
.section .note.GNU-stack,"",@progbits