### 寄存器 RISCV中有x0~x31 共32个寄存器,其中x0寄存器恒为0 ### 算术指令 #### 加法和减法指令 ```assembly add x1,x2,x3 #x1=x2+x3 sub x3,x4,x5 #x3=x2-x5 ``` 那么怎么实现稍微复杂的`a=b+c+d-e`语句呢? 我们可以把过程分成3步 1. a=b+c 2. a=a+d 3. a=a-e 用汇编表示如下(假设a,b,c,d,e分别用寄存器x10,x1,x2,x3,4存储) ```assembly add x10,x1,x2 add x10,x10,x3 sub x10,x10,x4 ``` 带括号的`f=(g+h)-(i+j)`该如何计算?其过程也是相同,先分为以下几步 1. temp1=g+h 2. temp2=i+j 3. f=temp1-temp2 其汇编如下:(假设变量f,g,h,i,j分别对应寄存器x19,x20,x21,x22,x23) ```assembly add x5,x20,x21 add x6,x22,x23 sub x19,x5,x6 ``` 如果有立即数(常量)该怎么办,如`a=b+10`,这时我们就要用到立即数指令`addi`,仅在最后面添加了符号i(immediate,立即数)。但是RISCV里面并没有减立即数的指令,因为这个指令可以通过加立即数来实现 ```assembly addi x3,x4,10 #x3=x4+10 addi x3,x4,-10 #x3=x4-10 ``` 因为x0恒为0,所以它可以为其它操作提供0值,如: ```assembly add x3,x4,x0 #x3=x4 ``` 因为x0恒为0,所以任何对x0的写操作都是无效的,如`add x0,x3,x4`就是条无效指令 ### 访存指令 8位为一个字节,寻址是按照字节来寻找的。一个字(word)4个字节,32位。 #### 大端存储与小段存储 大端与小端是指字节在存储器中存储顺序的两种方式。 大端是将一个字数据的最低位字节存储在最高字节地址上。 小端是将一个字数据的最低位字节存储在最低字节地址上 #### 寄存器与存储器的差异 32个32位的寄存器组只有128个字节的容量 DRAM存储器的容量至少数GB 寄存器的访问速度是DRAM的100倍到500倍 #### 把数据从内存加载到寄存器 用的是`lw`(load word,一个word是32位)指令。以下C代码用RISCV汇编表示为 ```c int A[100]; g = h+A[3]; ``` 假设变量A,g,h分别对应寄存器x15,x10,x12 ```assembly lw x10,12(x15) add x11,x12,x10 ``` 上面出现了一个之前没有的写法`12(x15)`,其中`(x15)`表示为取寄存器x15中地址的数据,假设`x15=100`那么`(x15)`就表示内存中物理地址为100处的数据,而`12`就表示为偏移量,组合起来就是`12(x15)`是取内存中物理地址为112处的数据。(一个Int占4个字节,A[3]就要偏移A[0] 12个字节,所以这个`lw`和下面的`sw`的偏移量都必须是4的倍数,因为一个word就是4字节) #### 把寄存器中的值放到内存 用的是`sw`(store word)指令。如: ```c int A[100]; A[10] = h + A[3] ``` 假设变量A,h对应寄存器为x15,x12 ```assembly lw x10,12(x15)add x10,x12,x10sw x10,40(x15) #这里x10的放到40(x15)处 ``` RISCV中还提供了`lb`(load byte),`sb`(store byte)指令,它们只读(存)一个字节,但是存到寄存器中需要进行符号扩展,即把最高位填充到其它位上,如`lb x10,3(x11)`(这里的偏移量就不要求是4的倍数了),假设`3(x11)`里面的数据是`1001 0001`(16进制为`0x91`),因为最高位是1,所以放到寄存器x10中的值为`0xffffff91` RISCV还提供了 `lbu`(load unsigned byte,加载无符号),这个指令是全0填充的。但是没有`sbu`指令,因为写数据时都是按字节为单位写的,所以不存在符号扩展(位填充)的情况。 ### 条件判断分支转移指令 #### 条件分支转移 类似于高级语言中的if语句。如`beq`(branch equal) ```assembly beq register1,register2,L1 ``` 如果register1的值和register2的值相等,则跳转到L1地址位置执行。否则就继续执行之后的指令。 类似的还有 * `bne`(branch not equal),该指令在两个值不相等时跳转 * `blt`(branch less than) 小于时跳转。 * `bltu`(branch less than unsigned)也是小于时跳转,不过它会将两个数当作无符号数进行比较 * `bge`(branch greater than or equal) 大于等于时跳转。(把两个比较的值换下位置就可以实现小于等于跳转了) * `bgeu` 无符号的大于等于时跳转 #### 无条件分支转移 可以直接进行跳转而不需要附加条件判断,如`j`(jump)指令可以直接跳转到label标识的指令目标地址 如以下C代码 ```c if (i == j){ f = g + h;} ``` 假设变量i,j,f,g,h对应的寄存器为x13,x14,x10,x11,x12 ```assembly bne x13,x14,Exit #如果x13,x14不相等,则跳转到Exit处,相等则继续执行下面的add语句add x10,x11,x12Exit: ``` 更复杂一点的if-else语句 ```c if(i==j){ f = g+h;}else{ f = g-h;} ``` 假设变量i,j,f,g,h对应的寄存器为x13,x14,x10,x11,x12 ```assembly ben x13,x14,Elseadd x10,x11,x12j ExitElse: sub x10,x11,x12Exit: ``` for循环的案例 ```c int A[20];int sum = 0;for(int i=0;i<20;i++){ sum+=A[i];} ``` ```assembly add x9,x8,0 #给寄存器x9赋数组A[0]的地址add x10,x0,x0 #x10赋变量sum的初始值0add x11,x0,x0# x11赋i的初始值0addi x13 x0,20 #x13赋循环次数值20Loop:bge x11,x13,Donelw x12,0(x9) #x12 = A[i]add x10,x10,x12 #sum+=A[i]addi x9,x9,4 # x9=A[i+1]addi x11,x11,1 #i++j LoopDone: ``` ### 逻辑运算指令 | 逻辑操作 | C | Java | RISC-V 指令 | | -------- | ---- | ---- | ------------------------ | | 按位与 | & | & | and | | 按位或 | \| | \| | or | | 异或 | ^ | ^ | xor | | 逻辑左移 | << | << | sll(shift left logical) | | 逻辑右移 | >> | >> | srl(shift right logical) | 这些逻辑运算的指令都有立即数的版本,`andi,ori,xori,slli,srli` 如 ```assembly and x5,x6,x7 #x5 = x6 & x7andi x5,x6,3 #x5 = x6 & 3 ``` `addi`一般用于掩码操作,如一个数(32位)与`0x0000 00ff`进行与操作,则可以得到该数的低8位;同理与`0xff00 0000`进行与操作可以得到高8位。 RISC-V中没有逻辑非的指令,这时因为异或立即数`0xffff ffff`即可得到取反的结果 #### 算术移位 算术右移`sra`(shift right arithmetic),也有立即数版本`srai`。右移后高位用符号位填充,假设x10中的数为 ``` 1111 1111 1111 1111 1111 1111 1110 0111 #10进制的-25 ``` 执行指令`srai x10,x10,4`即算术右移4位后得到 ``` 1111 1111 1111 1111 1111 1111 1111 1110 #10进制的-2 ``` 算数右移右移n位后得到的结果不等同于原数被$2^n$相除,因为右移后会把非0的数丢失(像上面右移4位就把低4位的0111移没了,如果丢失的全是0,那结果就是除$2^n$) 例:假设x10的值是0x34ff ```assembly slli x12,x10,0x10 #x12 = x10 << 16 = 0x34ff 0000srli x12,x12,0x08 #x12 = x12 >> 8 = 0x0034 ff00and x12,x12,x10 #x12 = x12 & x10 = 0x0000 3400 ``` ### 程序段与函数调用的实现方法 a0 ~ a7对应寄存器x10 ~ x17常用于传递参数;zero对应寄存器x0 RISC-V中还有一些伪指令,用来简化一些常用的操作,如: ```assembly mv rd,rs # addi rd,rs,0 即把rs赋值给rdli rd,13 # addi rd,x0,13 即把立即数13赋给rd ``` #### 函数调用的步骤 1. 发生函数调用时,在执行函数功能前,先将这次调用中需要用到的参数保存,方便取用 2. 将控制权移交给这次调用的功能函数 3. 根据情况为函数申请一定的本地存储空间,以满足函数执行过程中需要的存储需求; 4. 执行该函数的功能操作 5. 在函数执行完成后,将得到的结果数据存放好,便于主进程来获取,同时还原函数执行过程中使用到的 寄存器值、释放分配给函数的本地存储空间 6. 将控制权返还给原进程 一些常用的寄存器别名 * `a0 - a7` :编号`x10-x17`的寄存器,用来向调用的函数传递参数,a0和a1寄存器常用于传递返回值; * `ra`,即`x1`寄存器,用来保存返回时的返回地址值; * `s0-s11`,对应编号`x8-x9`和`x18-x27`的寄存器用来作为保存寄存器,保存原进程中的关键数据 避免在函数调用过程中被破坏。 ```c ... #其它语句sum(a,b); #调用了一个sum函数... #其它语句 int sum(int x,int y){ return x+y;} ``` ```assembly 1000 mv a0,s0 #x=a1004 mv a1,s1 #y=b1008 addi ra,zero,1016 #ra=1016,即函数返回后的下一条指令的地址1012 j sum #跳到sum段1016 ......2000 sum:add a0,a0,a12004 jr ra #jump register ra ``` 上面的1000,1004都是指令地址,因为指令都是4字节的,所以都是4的倍数。 这里用的是`jr`(jump register)跳转指令,而不是之前的`j`指令。这时因为函数调用可能发生在程序段的**多个位置**,每次函数调用时返回地址都会随发生调用时地址的不同而不同 需要在函数调用前**记录下返回地址保存在ra寄存器中**返回时再利用**jr指令**返回到ra寄存器中保存的地址值保证多次调用的灵活性。 我们还可以用`jal`(jump and link)指令 ```assembly #之前的指令1008 addi ra,zero,1016 #ra=10161012 j sum #跳到sum函数#用jal1008 jal sum #ra=1012,然后跳到sum函数 ``` **jal指令意为“跳转并链接”** 通过jal指令,可以形成指向调用点的地址或链接,从而使函数能返回正确的地址。 跳转则会使PC跳转指向被调用函数的地址,并且将链接得到的下一指令的地址作为返回地址,保存在ra寄存器中。 **jr指令** 能跳转到寄存器值所对应的地址空间,我们使用这条指令跳转到ra寄存器保存的地址值以实现函数的返回。 在汇编用法中,**有用ret指令来指代jr ra的操作。** 像上面的`2004 jr ra`就被替换成了`2004 ret` ### 栈的使用 在函数调用前,很可能一些寄存器保存着当前要使用的数据,如果对这些数据不加以保存,而直接进行函数调用,那么函数调用后这些数据就会丢失,从而引发错误。那么我们该如何保存这些寄存器原有的值?并在函数调用后还原这些值?这就需要在**函数调用前开辟一个能保存寄存器原值的空间,在函数返回时重装这些原值,最后释放这部分占用空间。** 理想的做法是用栈来操作。栈也是存储系统的一部分,因此需要一个指向它的寄存器来保存它的基地址。sp(stack point)寄存器(也是x2寄存器的别名),就是RISC-V中的栈指针寄存器。 我们通常沿着高地址向低地址值的方向来扩展栈空间,通过递减sp值不断压入( push)数据,通过递增sp值来弹出(pop)数据。 ```c int leaf(int g,int h,int i,int j){ int f; f=(g+h)-(i+j); return f;} ``` 上面代码中有4个参数g,h,i,j对应寄存器a0 ~ a3,还有局部变量f(用寄存器s0),还需要一个临时变量来存放临时结果(用寄存器s1),那么对应的汇编如下: ```assembly addi sp,sp,-8 #因为要保存s0,s1两个寄存器的值,所以开辟2个字节的栈sw s1,4(sp) #把s1的值放到栈中sw s0,0(sp) #把s0的值放到栈中add s0,a0,a1 # f = g+hadd s1,a2,a3 # s1 = i+jsub a0,s0,s1 # (g+h)-(i+j)lw s0,0(sp) #还原s0lw s1,4(sp) #还原s1addi sp,sp,8 #把开辟的2个字节的空间释放jr ra #return ``` #### 递归函数调用 被调用的函数内部又调用了其它函数时,是否因为新的函数调用而破坏原调用中的参数寄存器值a0-a 7以及ra中的返回地址值。 下面的一个函数sumSquare中又调用了另一个函数mult ```c int sumSquare(int x,int y){ return mult(x,x)+y;} ``` 故需要在调用mult函数前,将存放在ra寄存器中的sumSquare函数的返回地址值先保存起来 一些概念: * 在函数调用关系中,我们通常将发起**调用的函数**称为**调用函数caller**,将**被调用的函数**称为**被调用函数callee**。 * 当**被调用函数**执行结束后返回时,**调用函数**需要知道在这次函数调用中哪些寄存器的值可能被改过了、哪些寄存器的值需要保证不变; * 寄存器使用规范:规定函数调用后哪些寄存器能被修改哪些不能被修改的普遍认同准则 * **函数调用时保留的寄存器**:被调用函数一般不会使用这些寄存器,即便使用也会提前保存好原值。**可以信任:**sp(stack point,栈寄存器)、gp(global point,全局寄存器)、tp(thread point,线程寄存器)寄存器和s0-s11寄存器; * **函数调用时不保存的寄存器**:有可能被被调用函数使用更改,需要调用函数在调用前对自己用到的寄存器值进行保存。 * 寄存器包括:参数与返回值寄存器a0-a7返回地址寄存器ra、临时寄存器t0-t6。 | Register | ABI Name | Description | Saver | | -------- | -------- | ----------------------------------- | ------ | | x0 | zero | Hard- wired | zero | | x1 | ra | Return address | Caller | | x2 | sp | Stack pointer | Callee | | x3 | gp | Global pointer | -- | | x4 | tp | Thread pointer | 一 | | x5 | t0 | Temporary / alternate link register | Caller | | x6-7 | t1- 2 | Temporaries | Caller | | x8 | s0/fp | Saved register/ frame pointer | Callee | | x9 | s1 | Saved register | Callee | | x10-11 | a0-1 | Function arguments/return values | Caller | | x12-17 | a2-7 | Function arguments | Caller | | x18-27 | s2-11 | Saved registers | Callee | | x28- 31 | t3- 6 | Temporaries | Caller | 上面的sumSquare用汇编如下 ```assembly sumSquare:addi sp,sp,-8 #开辟空间sw ra,4(sp)#保存返回地址sw a1,0(sp)#保存ymv al,a0# mult(x,x)jal mult #call multlw a1,0(sp) #复原yadd a0,a0,a1#mult() +ylw ra,4(sp)#复原返回地址addi sp,sp,8#释放空间jr ramulti:... ``` #### 内存分配 * **静态区**中保存的是在程序中只声明一次的全局变量。这部分存储空间只有在程序执行完毕后才会被释放 * **堆区**是程序员使用malloc函数申请的一些动态存储空间。保存一些程序中的动态变量; * **栈区**是程序中发生函数调用时用来保存寄存器值存储空间。 ## RISC-V指令类型 * R型:用于寄存器与寄存器算术运算 * I型:用于短立即数和访存load操作 * S型:写存储器的指令 * B型:用于条件跳转 * U型:用于长立即数操作 * J型:用于无条件跳转 ### R型指令 **用于寄存器与寄存器算术运算**。如`add` 指令划分: | funct7 | rs2 | rs1 | funct3 | rd | opcode | | ------ | ---- | ---- | ------ | ---- | ------ | | 7位 | 5位 | 5位 | 3位 | 5位 | 7位 | 一条指令共7+5+5+3+5+7=32位 funct7:功能码7 rs2(Source Register):源寄存器2 rs1:源寄存器1 funct3:功能码3 rd(Destination Register):目的寄存器 opcode:操作码 对所有的R型指令来说,操作码都是`0110011`。功能码7和功能码3与操作码组合使用,描述了操作类型。 例:加法指令`add x18,x19,x10`的机器码表示如下 功能码7:0000000,功能码3:000。源寄存器有x10和x19,所以$rs2=(10)_{10}=(01010)_2,rs1=(19)_{10}=(10011)_2$。目的寄存器是x18,所以$rd=(18)_{10}=(10010)_2$。 而R型指令的操作码固定为0110011,所以其机器码为 | funct7 | rs2 | rs1 | funct3 | rd | opcode | | ------- | ----- | ----- | ------ | ----- | ------- | | 0000000 | 01010 | 10011 | 000 | 10010 | 0110011 | 以下是其它R型指令的机器码,可以看到仅funct7和funct3不同 | funct7 | rs2 | rs1 | funct3 | rd | opcode | 指令 | | ------- | ---- | ---- | ------ | ---- | ------- | ---- | | 0000000 | rs2 | rs1 | 000 | rd | 0110011 | add | | 0100000 | rs2 | rs 1 | 000 | rd | 0110011 | sub | | 0000000 | rs2. | rs 1 | 001 | rd | 0110011 | sll | | 0000000 | rs 2 | rs1 | 010 | rd | 0110011 | slt | | 0000000 | rs 2 | rs 1 | 011 | rd | 0110011 | sltu | | 0000000 | rs 2 | rs1 | 100 | rd | 0110011 | xor | | 0000000 | rs 2 | rs1 | 101 | rd | 0110011 | srl | | 0100000 | rs2. | rs1 | 101 | rd | 0110011 | sra | | 0000000 | rs2 | rs1 | 110 | rd | 0110011 | or | | 0000000 | rs2 | rs1 | 111 | rd | 0110011 | and | ### I型指令 它把R型指令中funct7和rs2合并成了一个12位的有符号数Imm,可以表示[-2048,+2047],之后会有如何处理超过这个范围的方法。 | Imm[11:0] | rs1 | funct3 | rd | opcode | | --------- | ---- | ------ | ---- | ------ | | 12位 | 5位 | 3位 | 5位 | 7位 | 例:`addi x15,x1,-50` I型指令操作码为:0010011;add的功能码3为000;源寄存器有x1;目的寄存器有x15;-50的二进制位补码为1111001110 | imm | rs1 | funct3 | rd | opcode | | ------------ | ----- | ------ | ----- | ------- | | 111111001110 | 00001 | 000 | 01111 | 0010011 | 其它的I型指令 | imm | rs1 | funct3 | rd | opcode | 指令 | | ----------- | ---- | ------ | ---- | ------- | ----- | | imm[11:0] | rs1 | 000 | rd | 0010011 | addi | | imm [11 :0] | rs1 | 010 | rd | 0010011 | slti | | imm[11 :0] | rs1 | 011 | rd | 0010011 | sltiu | | imm[11:0] | rs 1 | 100 | rd | 0010011 | xori | | imm[11 :0] | rs1 | 110 | rd | 0010011 | ori | | imm[11:0] | rs1 | 111 | rd | 0010011 | andi | 除此之外还有几个格式与上面不同的 | 0000000 | shamt | rs1 | 001 | rd | 0010011 | slli | | ------- | ----- | ---- | ---- | ---- | ------- | ---- | | 0000000 | shamt | rs1 | 101 | rd | 0010011 | srli | | 0100000 | shamt | rs1 | 101 | rd | 0010011 | srai | 这是因为移位操作只能移0~31位,用5位表示就够了,此外逻辑右移和算术右移用高位的第二位是0和1来区分(`0000000`和`0100000`) #### I型指令的访存操作 | offset[11:0] | base | width | dest | opcode | | ------------ | ---- | ----- | ---- | ------ | | 12位 | 5位 | 3位 | 5位 | 7位 | base+offset得到目标地址,然后取出目的地址的数放到dest寄存器中 例:`lw x14,8(x2)` | offset[11:0] | base | width | dest | opcode | | -------------- | ----- | ----- | ----- | ------- | | 0000 0000 1000 | 00010 | 010 | 01110 | 0000011 | 其它的访存指令如下: | offset[11:0] | base | width | dest | opcode | 指令 | | ------------ | ---- | ----- | ---- | ------- | ---- | | imm[11: 0] | rs1 | 000 | rd | 0000011 | lb | | imm[11:0] | rs1 | 010 | rd | 0000011 | lh | | imm[11:0] | rs1 | 011 | rd | 0000011 | lw | | imm[11:0] | rs1 | 100 | rd, | 0000011 | lbu | | imm[11:0] | rs1 | 110 | rd | 0000011 | lhu | ### S型指令 这里的偏移量一共12位,拆分为高7位和低5位,放到不同的位置 | Imm[11:5] | rs2 | rs1 | funct3 | imm[4:0] | opcode | | ------------ | ---- | ---- | ------ | ----------- | ------ | | 7位 | 5位 | 5位 | 3位 | 5位 | 7位 | | offset[11:5] | src | base | width | offset[4:0] | store | 例:`sw x14,8(x2)` | Imm[11:0] | rs2 | rs1 | funct3 | imm[4:0] | opcode | | --------- | ----- | ----- | ------ | -------- | ------- | | 0000 000 | 01110 | 00010 | 010 | 01000 | 0100011 | 其它的指令如下: | Imm[11:5] | rs2 | rs1 | funct3 | imm[4:0] | opcode | 指令 | | --------- | ---- | ---- | ------ | -------- | ------- | ---- | | Imm[11:5] | rs2 | rs1 | 000 | imm[4:0] | 0100011 | sb | | Imm[11:5] | rs2 | rs1 | 001 | imm[4:0] | 0100011 | sh | | Imm[11:5] | rs2 | rs1 | 010 | imm[4:0] | 0100011 | sw |