fzdwx

fzdwx

Hello , https://github.com/fzdwx

RISC-V 簡介

Resource#

  1. RISC-V Green Card
  2. RISC-V Call convertion
  3. P&H(RISC-V)

在匯編語言中沒有變量這個概念,匯編語言通常操作的是寄存器. 算術指令的操作數必須取自寄存器,內建於硬體的特殊位置 (CPU 內?).

{{< block type="tip" >}}

寄存器(Register)是中央處理器內用來暫存指令、數據地址電腦存儲器. 寄存器的存儲容量有限,讀寫速度非常快。在計算機體系結構裡,寄存器存儲在已知時間點所作計算的中間結果,通過快速地訪問數據來加速計算機程序的執行.

{{< /block >}}

RISC-V Card#

RISC-V 操作數#

  • 如果寄存器的大小是 64 位 則稱為雙字,32 位 則是單字.
  • x0 被硬連接到 0
    • add x3, x4, x0 => x3 = x4 (x0 is hard-wired to value 0)

匯編指令#

存儲操作數#

{{< block type="tip" >}}

將數據從內存複製到寄存器的數據傳輸指令稱為 載入指令 (load). 在 RISC-V 中指令是 ld, 表示取雙字.

{{< /block >}}

一個從數組中取值的 C 程序,寫出匯編代碼#

g = h + A[8];

A 是一個 100 個雙字組成的數組,g, h 分別存儲在 x20, x21 中,數組起始地址或基址位於 x22 中.

ld x9, 8(x22) // x9 = A[8]
add x21, x20, x9; // x21 = x20 + x9

存放基址的寄存器 (x22) 被稱為基址寄存器, 數據傳輸指令中的 8 稱為偏移量.

{{<block type="tip" title="大端與小端編址">}}
計算機分為兩種,一種使用最左邊或 “大端” 字節的地址作為雙字地址,另一種使用最右端或 “小端” 字節的地址作為雙字地址.

RISC-V 使用小端。由於僅在以雙字形式和 8 個單獨字節訪問相同數據時,字節順序才有影響,因此大多情況不需要關係 “大小端”.
{{< /block >}}

所以為了上面的代碼獲得正確的字節地址加到 x22 這個寄存器的偏移量為 64(8x8).

與載入指令相反的指令通常被稱為存儲指令 (store), 從寄存器複製數據到內存。指令是sd, 表示存儲雙字.

{{< block type="tip">}}
在一些體系結構中,字的起始地址必須是 4 的倍數,雙字的起始地址必須是 8 的倍數。該要求成為對齊限制
{{< /block >}}

RISC-V 和 Intel x86 沒有對齊限制,但 MIPS 有這個限制.

使用 load 和 store 編譯生成指令#

A[12] = h + A[8];

h 存放在 x21 中,A 的基址存放在 x22 中.

ld x9, 64(x22)  // x9 = A[8]
add x9, x21, x9 // x9 = h + A[8]
sd x9, 96(x22)  // A[12] = x9

將字符串複製程序編譯為匯編#

void strcpy(char x[],char y[]){
	size_t i;
	i = 0;
	while((x[i] = y[i]) != '\0'){
		i += 1;
	}
}

x, y 的基址存放在 x10 和 x11 中,i 存放在 x19 中.

strcpy:
	addi sp, sp, -8  // 調整棧指針,以存放一個item(x19)
	sd x19, 0(sp)    // x19 入棧
	add x19, x0, x0  // x19 = 0 + 0
L1: add x5, x19, x11 // x5 = x19 + x11 => address of y[i] in x5
	lbu x6, 0(x5)    // temp: x6 = y[i]
	add x7, x19, x10 // x5 = x19 + x11 => address of x[i] in x7
	sd  x6, 0(x7)    // x[i] = y[i]
	beq x6, x0, L2   // if x6 ==0 then go to L2
	addi x19, x19, 1 // i = i  + 1
	jal x0, L1       // go to L1
L2: ld x19, 0(sp)    // 恢復 x19 以及棧指針
	addi sp, sp, 8 
	jalr x0, 0(x1)

一段循環代碼編譯為匯編#

int A[20];
int sum = 0;
for (int  i = 0; i < 20; i++){
	sum += A[i];
}

RISC-V 匯編(32 bit)

	add x9, x8, x0     # x9 = &A[0]
	add x10, x0, x0    # sum
	add x11, x0, x0    # i
	addi x13,x0, 20    # 20
Loop:
	bge x11, x13, Done # if x11 > x13 go to Done(end loop)
	lw x12, 0(x9)      # x12 = A[i]
	add x10, x10, x12  # sum
	addi x9, x9, 4     # x9 = &A[i+1]
	addi x11, x11, 1   # i++
	j Loop
Done:

邏輯操作#

  • and andi
    • and x5, x6, x9 => x5 = x6 & x9
    • addi x5, x6, 3 => x5 = x6 & 3
  • sll ssli , 左移 (擴大)
    • slli x11, x23, 2 => x11 = x23 << 2
    • 0000 0010 => 2
    • 0000 1000 => 8
  • srl srli , 右移(縮小)
    • srli x23, x11, 2 = > x23 = x11 >> 2
    • 0000 1000 => 8
    • 0000 0010 => 2
  • sra srai, 算數右移
    • 1111 1111 1111 1111 1111 1111 1110 0111 = -25
    • srai x10, x10, 4
    • 1111 1111 1111 1111 1111 1111 1111 1110 = -2

Helpful RISC-V Assmebler Features#

  1. a0 - a7 是參數寄存器 (x10 - x17, 用於函數調用.
  2. zero 代表 x0
  3. mv rd, rs = addi rd, rs, 0
  4. li rd, 13 = addi rd, x0, 13
  5. nop = addi x0, x0
  6. la a1 Label 將 Label 的 地址 加載到 a1
  7. a0 - a7 (x10 - x17): 8 個寄存器用於參數傳遞以及兩個返回值 (a0 - a1)
  8. ra(x1): 一個返回 address 的寄存器,用於返回原點(調用的位置)
  9. s0 - s1 (x8 - x9) and s2 - s11 (s18 - x27): 保存的寄存器

RISC-V 函數調用的轉換#

  1. 寄存器比內存快,所以使用它們
  2. jal rd, Label 跳轉和鏈接
    1. jal x1, 100
  3. jalr rd, rs, imm 跳轉和鏈接寄存器
    1. jalr x1, 100(x5)
  4. jal Label => jal ra, Label 調用函數
  5. jalr s1 當 s1 是方法指針時,這就是一個函數調用

一段函數調用轉換為匯編#

...
sum(a,b);
...

int sum(int x, int y){
	return x + y;
}
1000 mv a0, s0              # x = a
1004 mv a1, s1              # y= b
1008 addi ra, zero, 1016    # 1016 is sum function
1012 j                      # jump to sum
1016 ... 
...
2000 sum: add a0, a0, a1
2004 jr ra

1008 ~ 1012 可以使用 jal sum 來替代、

調用函數的基本步驟#

  1. 把需要的參數放到方法可以訪問的地方(寄存器)
  2. 轉移控制權給函數,使用 (jal)
    1. 保持地址,並跳轉到函數的地址
  3. 獲取函數執行所需的 (local) 存儲資源
  4. 執行預期的函數
  5. 將返回值放在調用代碼可以訪問的地方,並恢復我們使用到的寄存器,釋放本地存儲
  6. 將控制器返回給主處理器(ret), 使用存儲在寄存器中的地址,返回到調用它的地方

方法調用示例#

int leaf(int g, int h, int i, int j){
	int f;
	f = (g + h) - (i + j);
	return f;
}
  1. g,h,i,j in a0,a1,a2,a3
  2. f in s0
  3. temp is s1
leaf:
	# prologue start
	addi sp, sp, -8   # 騰出 8byte 來存放的2個整數
	sw s1, 4(sp)      # 保存 s1, s0 到 sp 中
	sw s0, 0(sp)
	# prologue end
	add s0, a0, a1    # f = g + h
	add s1, a2, a3    # temp = i + j
	sub a0, s0, s1    # a0 = (g + h) - (i + j) 

	# epilogue
	lw s0, 0(sp)      # 恢復 s1, s0
	lw s1, 4(sp)    
	addi sp, sp, 8 

	jr ra

sp#

{{< block type="tip" >}}

sp 是棧指針,從內存空間 的最頂部開始向下增長,在 RISC-V 中使用 x2 這個寄存器.

  1. push 是減少 sp 的指針地址
  2. pop 是增加

{{< /block >}}

每個函數都有一組存放在棧上的數據,它們是棧幀(stack frame ), 棧幀通常包含:

  1. 返回地址
  2. 參數
  3. 使用的局部變量的空間

嵌套函數調用#

int sumSquare(int x,int y){
	return mult(x,x) + y;
}

在 ra 中有一個 sumSquare 想要跳回的值,但是這個值會被調用 mult 覆蓋.

  1. caller: 調用函數的人
  2. calle: 被調用的函數
  3. 當被調用者從執行中返回時,調用者需要知道哪些寄存器可能發生了變化,哪些寄存器被保證是不變的.
  4. 寄存器規定: 即哪些寄存器在程序調用 (jal) 後將被取消緩存,哪些可以被改變.
    1. 即有一些寄存器是易失的 (temp), 一些是要保存的(調用者需要恢復它們原來的值).
    2. 這優化了每次進入棧幀的寄存器的數量
  5. 分類:
    1. 跨函數調用保留:
      1. sp, gp, tp
      2. s0 - s11 (s0 is also fp)
    2. 不保留:
      1. 參數寄存器以及返回寄存器: a0 - a7, ra
      2. temp 寄存器: t0 - t6

上面代碼的 RISC-V

x in a1, y in a1

sumSquare:
	addi sp, sp, -8
	sw ra, 4(sp)             // save return address to sp
	sw a1, 0(sp)             // save s1 to y
	mv a1, a0                // y = x => mult(x,x)
	jal mult                 // call mult
	lw a1, 0(sp)             // get y from sp
	add a0, a0, a1           // mult() + y
	lw ra, 4(sp)             // get return address from sp
	addi sp, sp, 8
	jr ra

RISC-V 寄存器名稱#

RISC-V 方法調用套路#

matmul:  
    # 壓棧,騰出空間保存我們要使用的幾個 s 寄存器
    addi sp, sp, -36  
    sw ra, 0(sp)  
    sw s0, 4(sp)  
    sw s1, 8(sp)  
    sw s2, 12(sp)  
    sw s3, 16(sp)  
    sw s4, 20(sp)  
    sw s5, 24(sp)  
    sw s6, 28(sp)  
    sw s7, 32(sp)  
body:
    # xxx xxx

end:  
    # 恢復寄存器的值  
    lw ra, 0(sp)  
    lw s0, 4(sp)  
    lw s1, 8(sp)  
    lw s2, 12(sp)  
    lw s3, 16(sp)  
    lw s4, 20(sp)  
    lw s5, 24(sp)  
    lw s6, 28(sp)  
    lw s7, 32(sp)  
    addi sp, sp, 36  
    ret  

RISC-V 指令二進制的表示#

R 格式佈局#

用於算術和邏輯運算的指令

  1. opcode,funct3, funct7 : 將告訴我們是否要執行加,減,左移,異或等操作.
    1. R-format 的 opcode 固定為 0110011
  2. 一個 add 操作: add x18 x19 x10 => x18 = x19 + x10
  3. 0000000 01010 10011 000 10010 0110011
  4. rs2 = x19, rs1 = x10, rd = x18

I 格式佈局#

處理立即數,比如addi rd rs1, imm => addi a0 a0 1

  1. imm 的範圍是 -2084 ~ 2047

RISC-V Loads#

load 指令也是 I 類型的.

S 格式佈局#

載入中......
此文章數據所有權由區塊鏈加密技術和智能合約保障僅歸創作者所有。