571
社区成员
经过对x86和ARM6两种经典指令集架构的学习,对指令集架构的框架有了一点基础的了解,遂做了一点调研和学习,关于RISC-V。
RISC-V是一个基于精简指令集(RISC)原则的开源指令集架构(ISA),被设计成可以支持丰富的定制化和特殊化。最基础的指令是RV32I,即32位的指令,同时还存在其他不同的拓展,如RV64I(64位基础指令扩展)、RV32M(乘法指令扩展)、RV32F(单精度浮点扩展)、RV32D(双精度浮点扩展)等。本文的分享集中于RV32I。
文章内容分为两个主要部分:寄存器和指令集。
RV32I 基础指令集总共定义了 32 个 32 位的通用寄存器,这一点与x86和ARM64有所不同。
它们分别被标记为x0 ~ x31。其中零号寄存器 x0 是只读寄存器,其值永远为零。在用汇编语言编写时,这 32 个寄存器的名称也根据其在调用约定中的职能而被重新命名。
其中19个寄存器被划分为两组:x5-x7、x28-x31为临时寄存器,在过程调用中不被被调用者保存;x8-x9、x18-x27为保存寄存器,在过程调用中被保存。同时设置x10-x17参数寄存器,用于传递参数或返回值,x1一个返回地址寄存器,用于返回到起始点。
除32个通用寄存器外,RSIC-V还定义一个PC寄存器,该寄存器指向当前正在执行的指令的内存地址。
与我们熟悉的x86和ARM64不同,RISC-V指令被设计为定长32位,且只有6种类型,简化了指令解码。
指令在存储器中必须在边界(4字节为单位)对齐。当发生一个条件分支或者无条件转移时,如果目标地址没有对齐,将产生一个指令地址不对齐异常。
具体六种指令格式如下:
R类型:寄存器-寄存器操作;
I类型:短立即数和访存load操作;
S类型:访存store操作;
B类型:条件跳转操作;
U类型:长立即数;
J类型:无条件跳转。
其中R、I、S和U类型是四种基础指令,而B和J类型都只包含一条指令,具体指令格式如下:
RISC-V汇编指令分类主要有以下几种:
算术运算指令
逻辑运算指令
位移运算指令
内存读写指令
分支与跳转指令
如下图所示:
发生溢出时会自动截断高位,将低32位写入寄存器中。
左移在右边补0;逻辑右移在最高位添0,算数右移在最高位添加符号位。
有符号数:
无符号数:
#include <stdio.h>
void swap(int *a, int *b){
int c = *a;
*a = *b;
*b = c;
}
int main()
{
int a= 101;
int b= 202;
swap(&a, &b);
printf("a = %d\n", a);
printf("b = %d\n", b);
return 0;
}
void swap(int *a, int *b){
103e: 7179 addi sp,sp,-48 // 继续给栈开辟48字节的空间
1040: f406 sd ra,40(sp) // 将ra保存至栈中
1042: f022 sd s0,32(sp) // 将s0保存至栈中
1044: 1800 addi s0,sp,48 // s0此时只想栈的高地址
1046: fea43423 sd a0,-24(s0) // 将a0寄存器的地址,到s0-16 ~ s0-24中
104a: feb43023 sd a1,-32(s0) // 将a1寄存器的地址,保存到s0-24 ~ s0-32中
int c = *a;
104e: fe843503 ld a0,-24(s0) // 从s0-16 ~ s0-24中读取八个字节到a0中,该地址为a0传入的地址,地址中存储的值为101
1052: 4108 lw a0,0(a0) // 将a0地址中的值加载到a0寄存器中,此时a0为101
1054: fca42e23 sw a0,-36(s0) // 将a0的值加载到s0-32 ~ s0-36中,此处存入的为一个值
*a = *b;
1058: fe043503 ld a0,-32(s0) // 将s0-24 ~ s0-32的地址读取到a0中
105c: 4108 lw a0,0(a0) // 将a0的值加载给a0,此时a0为202
105e: fe843583 ld a1,-24(s0) // 将s0-16 ~ s0-24的八字节地址给a1
1062: c188 sw a0,0(a1) // 将a0的值加载到a1的地址中,此时a1的地址,即s0-16 ~ s0-24中的值为202
*b = c;
1064: fdc42503 lw a0,-36(s0) // 将s0-32 ~ s0-36存的值加载给a0,此处的值为101
1068: fe043583 ld a1,-32(s0) // 将s0-24 ~ s0-32的八字节地址读取到a1中
106c: c188 sw a0,0(a1) // 将a0的值读取到a1中,原来s0-
0000000000001076 <main>:
int main()
{
1076: 7139 addi sp,sp,-64 // 给栈开辟64字节的,此处sp为栈指针
1078: fc06 sd ra,56(sp) // 将返回地址ra保存到栈中
107a: f822 sd s0,48(sp) // 将s0(fp)帧指针保存到栈中
107c: 0080 addi s0,sp,64 // sp指向的是栈底,s0此时的值为sp+64的值,也就是基地址
000000000000107e <.LBB1_3>:
107e: 00001517 auipc a0,0x1 // 将0x1取31位到12位,然后左移12位+pc的地址,结果写入到a0寄存器
1082: 19a53503 ld a0,410(a0) # 2218 <__stack_chk_guard@LIBC> // 将a0偏移410字节的值给a0,此步骤是为了防止堆栈溢出添加的检测保护
1086: 610c ld a1,0(a0) // 将a0的八字节赋给a1
1088: feb43423 sd a1,-24(s0) // 将a1寄存器的值保存到栈中,保存至s0-16 ~ s0-24的位置
108c: 4581 li a1,0 // 将a1寄存器赋值为0,此处的作用为初始化一个寄存器为0
108e: fcb42e23 sw a1,-36(s0) // 将a1寄存器的值保存到栈中表示的地址(s0-32 ~ s0-36的位置)
1092: 06500593 li a1,101 // 将a1寄存器赋值为101
int a= 101;
1096: feb42223 sw a1,-28(s0) // 由于int在类型为四个字节,此时将101存储到s0-24 ~ s0-28所表示的地址中
109a: 0ca00593 li a1,202 // 还是将a0寄存器进行操作,赋值为202
int b= 202;
109e: feb42023 sw a1,-32(s0) // 将202存储到s0-28 ~ s0-32的地址中
10a2: fe440593 addi a1,s0,-28 // 将s0-28的地址给到a1
10a6: fe040613 addi a2,s0,-32 // 将s0-32中地址给到a2
swap(&a, &b);
10aa: fca43823 sd a0,-48(s0) // 将a0的地址保存到s0-40 ~ s0-48中
00000000000010d2 <.LBB1_5>:
10e4: 060080e7 jalr 96(ra) # 1140 <printf@plt>
10e8: fd043583 ld a1,-48(s0) // 将s0-40 ~ s0-48的地址赋值给a1,之前存入的是a0的地址
10ec: 6190 ld a2,0(a1) // 将a1的地址复制给a2
10ee: fe843683 ld a3,-24(s0) // 将s0-16 ~ s0-24的八字节赋给a3,也就是之前存入的a0的地址
10f2: 00d61963 bne a2,a3,1104 <.LBB1_2> //判断此处的栈是否溢出了
00000000000010fa <.LBB1_1>:
10fa: 4501 li a0,0 // 将a0寄存器赋值为0
return 0;
10fc: 7442 ld s0,48(sp) // 恢复s0的值
10fe: 70e2 ld ra,56(sp) // 恢复ra的值
1100: 6121 addi sp,sp,64 // 恢复sp的值
1102: 8082 ret // 整个函数退出
根据上述分析,可以对RSIC-V函数调用栈的框架有一点了解和认识:
参考:https://zhuanlan.zhihu.com/p/496767749
https://www.jianshu.com/p/5babd0d13dfc
学号:438