RISC-V组件讲座讲稿

2020-10-17 23:31:17

RISC-V组件与任何其他组件类似,与MIPS组件相似。就像任何程序集一样,我们有一个指令列表,它会逐渐使我们更接近我们的解决方案。

我们将使用riscv-g++编译器并将C++文件与汇编文件链接起来。您将编写程序集文件,而C++文件有助于简化实验。

部件文件以.S(大写S)结尾。编译器包括编译、汇编和链接的所有阶段,但是当我们传递一个大写为S的文件时,编译器将直接跳到汇编阶段。不过,大写字母S允许我们使用预处理器,而小写字母s将跳过所有这些。

RISC-V包含32个整数寄存器和32个浮点寄存器。通过ABI名称,我们保留了其中一些用于特定目的的登记簿。例如,所有以t开头表示临时的寄存器都可以用于任何目的。所有以a for参数开头的寄存器都用于传递给函数的参数。对于保存的寄存器,所有以s开头的寄存器(sp除外)都是跨函数调用保留的寄存器。

RISC-V包含整数和逻辑指令以及一些内存指令。RISC-V是加载/存储体系结构,因此整数指令操作数必须是寄存器。

从存储器地址(sp+8)加载(取消引用)到寄存器t0。Lb=加载字节,lh=加载半字,lw=加载字,ld=加载双字。

将寄存器t0存储(取消引用)到内存地址(sp+8)。Sb=存储字节,sh=存储半字,SW=存储字,SD=存储双字。

将t0的值与t1的值相加,并将和存储到a0中。

将t0的值乘以t1的值,并将乘积存储在a0中。

将T3(分母)的值除以S3(分子)的值,并将商存储到寄存器A1中。

将T3(分母)的值除以S3(分子)的值,并将余数存储到寄存器A1中。

对操作数T3和S3执行逻辑与,并将结果存储到寄存器A3中。

对操作数T3和S3执行逻辑或,并将结果存储到寄存器A3中。

对操作数T3和S3执行逻辑异或,并将结果存储到寄存器A3中。

由于RISC-V是精简指令集,因此许多可以使用另一条指令完成的指令被省略。例如,neg a0,a1(二进制补码)指令不存在。然而,这相当于suba0,0,a1。换句话说,0-A1与-A1相同。

汇编器提供几条伪指令,这些伪指令扩展成实指令。例如,上面的neg是一条伪指令。每当汇编器读取此指令时,它都会自动将其展开为SUB指令。下面是所有伪指令及其功能的列表。

浮点指令以诸如fld、fsw的f作为前缀,分别用于浮点加载双字和浮点存储字。浮点指令有两种类型:(1)单精度和(2)双精度。您可以通过添加后缀来选择所需的数据大小,后缀可以是.s(表示单精度)或.d(表示双精度)。

#加载双精度值flw ft0,0(Sp)#ft0现在包含我们从内存加载的所有内容+0flw ft1,4(Sp)#ft1现在包含我们从内存加载的任何内容+4fadd.s ft2,ft0,ft1#ft2现在是ft0+ft1。

注意,在上面的代码中,我们使用fadd.s指令告诉RISC-V处理器将两个单精度值(ft0和ft1)相加,并将其作为单精度值存储到ft2中。

我们可以使用指令fcvt.d.s(从单精度转换为双精度)或fcvt.s.d(从双精度转换为单精度)在双精度和单精度之间进行转换。

分支指令是跳转到代码的不同部分的一种方式。如果我们没有分支指令,CPU将只能执行一条接一条指令。有了跳跃和分支,我们可以转到任何指令,即使是无序的!

分支指令是函数调用和条件在汇编中实现的方式。分支指的是条件跳转指令,例如用于分支的beq、bne、bgt、bge、blt、ble-if等于、不等于、大于、大于或等于、小于和小于或等于。

分支指令有三个参数:要比较的两个操作数(寄存器),如果比较结果为真,则是要执行的指令的内存标签。如果分支条件为FALSE,则忽略分支指令,并且CPU转到下面的下一条指令。

#t0=0li t0,0li t2,10loop_head:bge t0,t2,loop_end#此处重复代码addi t0,t0,1j loop_headloop_end:

请注意,我使用了与条件相反的";视图。在for循环中,只要条件成立,我们就执行循环体。在集会上,我采取了相反的做法。我的意思是,如果t0大于或等于t2(>;=与<;相反),则跳出循环并完成。

堆栈用于本地内存存储。堆栈从底部(高内存)到顶部(低内存)增长,并且堆栈的底部有一个专用寄存器,称为sp,用于堆栈指针。

无论何时使用保存的寄存器,或者如果我们希望在函数调用中保留临时寄存器,都必须将其保存在堆栈上。要从堆栈中分配,我们需要减去。我们补充说,要解除分配。请注意,我们没有清理堆栈。这就是为什么C++中未初始化的变量会被认为是垃圾,因为堆栈上剩下的任何东西都还在那里。

堆栈必须对齐到8,这意味着我们必须始终从堆栈中减去8的倍数,然后再将8的倍数加到堆栈中。

上面的代码将返回地址保存在堆栈上,调用printf,然后当printf返回时,我们将返回地址的旧值加载回堆栈,然后通过添加8来解除分配。

编译器的工作是将.cpp文件转换成汇编文件,汇编器将汇编文件汇编成机器代码作为目标文件。然后,链接器将所有目标文件链接到可执行文件或库中。

我们知道我们的C++代码归结为汇编,所以无论我们在C++中能做什么,我们也可以在汇编中做。我已经在上面展示了一些关于如何编写for循环的示例,但是让我们来看看其他的C++构造。

函数只是第一条指令的内存标签。应用程序二进制接口(ABI)指定哪些寄存器获取哪些参数,以及如何来回返回内容。然而,所有函数都有一个前导码和一个尾部,前者实质上是为本地存储设置堆栈帧,后者通常需要加载保存的寄存器和返回地址,并在返回前移动堆栈指针。

My_function:#序言addi sp,sp,-32 sd ra,0(Sp)sd a0,8(Sp)s0,16(Sp)s1,24(Sp)#epilogue ld ra,0(Sp)ld a0,8(Sp)ld s0,16(Sp)ld s1,24(Sp)addi sp,sp,32 ret。

此代码显示,我们首先从堆栈中分配32个字节,这是4个寄存器的大小。您可以看到,我首先从堆栈中减去所有必要的空间,存储值,运行代码,然后执行结束语。这是向存储和加载指令添加偏移量的主要目的。

另一件需要注意的事情是,我正在存储所有调用者保存的寄存器。再一次,我们必须考虑销毁所有调用者保存的寄存器。包括所有临时寄存器、参数寄存器和返回地址寄存器。我确实保存了上面一些保存的寄存器,但是请记住,如果我们使用保存的寄存器,我们需要在返回之前将它们的原始值放回其中。

我们要一个开场白和一个结束语。当我们调用其他函数时,我们希望我们的堆栈是成帧的。在编程语言课程中,您将听说堆栈框架。因此,我们为自己分配函数所需的所有空间,然后存储到其中。

Bne t0,0,1f#如果t0==0j 2f 1:bne t1,0,1f#代码转到这里:#如果t1==0j 2f1:#代码转到这里如果t0!=0和t1!=02:#转储点在这里。

上面的汇编代码模仿了下面的C++代码。If(!t0){//如果t0==0}则代码转到这里}否则如果(!t1){//代码转到这里如果t1==0}否则{//代码转到这里如果t0!=0并且t1!=0}//转储点在这里。

如果您不记得,标签1f的意思是转到给定位置前面的数字标签1。这与1b相反,后者向后查找给定位置的数字标签1。

Printf要求第一个参数是c样式的、以NULL结尾的字符串,我们可以使用.asciz汇编指令创建该字符串。下面的代码给出了一个如何使用printf的示例。

.section.rodataPrompt:.asciz";t0=%ld和t1=%ld\n";.section.textmyfunc:addi sp,sp,-8 sd ra,0(Sp)la a0,提示mv a1,t0 mv a2,t1调用printf ld ra,0(Sp)addi sp,sp,8 ret。

上面的代码显示,我们将第一个参数放入了a0中的printf,这是我们想要输出的字符串。然后我们要输出t0和t1的值,因此需要将它们分别移到其他参数寄存器a1和a2中。

任何时候看到函数调用时,都应该考虑保存返回地址寄存器,就像我上面所做的那样。我可能不是从使用堆栈开始的,但是每次我键入";call";时,我的手指都会自动地期望开始键入一些内容来保存RA(返回地址)寄存器。另外,记住在你回来之前一定要取消分配!

我们有8个参数寄存器a0到a7。这些将是传递给函数的8个非浮点参数。这包括指针,其中AX将包含内存地址,或按值传递,其中AX将包含实际值。仅对于浮点值,您将使用Fa0到Fa7。

ABI进一步指出,我们必须通过a0返回整数值,或者通过Fa0返回浮点值。

如果你有一个结合了整数和浮点数的函数,你可以使用前面没有取的任何数字。例如,考虑以下原型。

此函数要求int a在寄存器a0中,int*b在a1中具有b指向的内存地址,在fa0中具有浮点数c的值。因为我们返回一个浮点数,所以在执行ret指令之前必须将结果放入Fa0。

请注意,我们使用的是a0、a1、...、a7。这适用于所有大小,字节、字、双字等。请记住,我们通过选择指令来解析数据大小。对于FLOAT和DOUBLE,我们选择direction.s和direction.d。例如,fadd.s fa0、ft0、ft1添加单精度值,而fadd.d fa0、ft0、ft1添加双精度值。