iT邦幫忙

2018 iT 邦幫忙鐵人賽
DAY 12
1

前情提要


昨日初步觀察 RISC-V 的指令集架構的一些特徵,如暫存器編排、指令結構設計哲學與主要的指令型態等。今天,我們則要深入介紹 RISC-V 的指令。當一個作者說要逐條介紹 x86 指令的時候,諸位讀者可以預期他接下來就要花個十年半月在單單介紹指令這件事情上面了;但是 RISC-V,為了極簡就是美的哲學,本文就將基本指令集講完吧!順便也讓我們印證一下昨日簡單條列的指令型態部份。

想像中,指令集介紹這種事情簡直像是,以一本字典為讀書會主題而分享的行為;但是筆者必須澄清,其實原本的規格手冊中,介紹了許多設計理念、哲學、定性的初步分析與比較等等,是很有參考價值的文件。但是筆者也知道我們生啃原文文件總是吃力,幸好中國有勇敢的網友果敢地分享知識給大家,有前一個 2.1 版本的規格書翻譯,這裡也附給大家參考。

筆者這裡必須食言,IMA 的支援有點超過負荷,這裡修正 rvgc 的實作份量為整數基本指令集,日後再求擴充

整數基本指令集:I


算術邏輯指令

這個部份的指令全部都是 R 型態的,包含以下 10 個指令:

  • add, sub(加減)。E.g. rd = rs1 + rs2
  • and, or, xor(且、或、互斥或)。E.g. rd = rs1 ^ rs2
  • sll, srl, sra(左移、邏輯右移(單純右移)、算術右移(有號延伸))。E.g. rd = rs1 << (rs2 % 32)
  • slt, sltu(set less than,有號整數與無號整數版本)。 E.g. rd = (rs1 < rs2)

以下就以昨日也出現過的指令位址圖為骨架講解指令的內容。

| 31      25 | 24   20 | 19   15 | 14      12 | 11  07 | 06      00 |
+------------+---------+---------+------------+--------+------------+
|   funct7   |   rs2   |   rs1   |   funct3   |   rd   |   opcode   |
+------------+---------+---------+------------+--------+------------+
    0000000       add                000                   0110011
    0100000       sub                000                   0110011
    0000000       sll                001                   0110011
    0000000       slt                010                   0110011
    0000000       sltu               011                   0110011 
    0000000       xor                100                   0110011 
    0000000       srl                101                   0110011
    0100000       sra                101                   0110011
    0000000       or                 110                   0110011 
    0000000       and                111                   0110011 

    0000000       addw               000                   0111011
    0100000       subw               000                   0111011
    0000000       sllw               001                   0111011
    0000000       srlw               101                   0111011
    0100000       sraw               101                   0111011 (64-bit 擴充*)

在這裡,funct3 有 8 種分配方法、funct7 有 2 種。其中,加和減共用一組 funct3 的值、邏輯右移和算術右移也共用一組,所以總共產生十種指令的編排方式。

*:在 64-bit 的暫存器操作中,有時候我們會希望能夠將一個有號 32-bit 整數延伸到整個 64-bit 空間。如果都是 32-bit 的暫存器操作,那麼 2 的補數規則確保了有號、無號整數的算術一致性,但對於延伸到 64-bit 時,就會有上半部全零和全一的差別了。64-bit 擴充操作只適用在加減法以及挪移(shift)指令。

這個階段有 15 個指令:add, sub, and, or, xor, sll, srl, sra, slt, sltu, addw, subw, sllw, srlw, sraw

讀取與儲存記憶體

讀取指令全部都是 I-type,取 rd = rs1[imm] 的意思,然後依照組語指令決定讀取的空間大小,以及正負號擴充與否,總共有 7 個指令

| 31                20 | 19   15 | 14      12 | 11  07 | 06      00 |
+----------------------+---------+------------+--------+------------+
|   immediate[11:0]    |   rs1   |   funct3   |   rd   |   opcode   |
+----------------------+---------+------------+--------+------------+
           lb, load byte              000                  0000011
           lbu,load byte unsigned     100                  0000011
           lh, load half              001                  0000011
           lhu,load half unsigned     101                  0000011
           lw, load word              010                  0000011
           lwu,load word unsigned     110                  0000011
           ld, load dword             011                  0000011

分別按照 1、2、4、8 bytes 還有有號無號的分別,優雅地透過 funct3 處理了分歧。

儲存指令則全部都是 S-type,取 rs1[imm] = rs2 的意思。設計時為了讓 rs1 記憶體保持基準位址的結果,就是不得不把整數部份拆掉,改成佔用原本 rd 的部份:

| 31      25 | 24   20 | 19   15 | 14      12 | 11  07 | 06      00 |
+------------+---------+---------+------------+--------+------------+
|  imm[11:5] |   rs2   |   rs1   |   funct3   | i[4:0] |   opcode   |
+------------+---------+---------+------------+--------+------------+ 
           sb, save byte              000                  0100011
           sh, save half              001                  0100011
           sw, save word              010                  0100011
           sd, save dword             011                  0100011

儲存指令不像讀取必須顧慮有號無號的問題,就直接寫入特定的內容到記憶體去了。

這個階段有 11 個指令:lb, lh, lw, ld, lbu, lhu, lwu, sb, sh, sw, sd

邏輯算術指令:一個運算元是 12-bit 整數

也就是 rd = rs1 <某種運算> imm 的計算指令。

| 31                20 | 19   15 | 14      12 | 11  07 | 06      00 |
+----------------------+---------+------------+--------+------------+
|   immediate[11:0]    |   rs1   |   funct3   |   rd   |   opcode   |
+----------------------+---------+------------+--------+------------+
           addi                       000                  0010011
           slti                       010                  0010011
           sltiu                      011                  0010011
           xori                       100                  0010011
           ori                        110                  0010011
           andi                       111                  0010011
           addiw                      000                  0011011

移動指令系列的三個比較特殊,它們稍微特化了數值的區域,使得他們的格式類似這樣:

| 31     26 | 25    20 | 19   15 | 14      12 | 11  07 | 06      00 |
+-----------+----------+---------+------------+--------+------------+
|  funct7   |  shamt   |   rs1   |   funct3   |   rd   |   opcode   |
+-----------+----------+---------+------------+--------+------------+
   000000     slli                    001                  0010011
   000000     srli                    101                  0010011
   010000     srai                    101                  0010011
   000000     slliw                   001                  0011011
   000000     srliw                   101                  0011011
   010000     sraiw                   101                  0011011

其中,字尾有 w 的指令作用在 32-bit 內容,且產生的結果是有號的擴充、第 25 個 bit 的內容一定是 0。

這個階段有 12 個指令:addi, slti, sltiu, xori, ori, andi, slli, srli, srai, slliw, srliw, sraiw

條件指令

條件指令用的格式是 S-type 的擴充,或稱為 B-type,型態如下

| 31      25 | 24   20 | 19   15 | 14      12 | 11  07 | 06      00 |
+------------+---------+---------+------------+--------+------------+
| i[12|10:5] |   rs2   |   rs1   |   funct3   |[4:1|11]|   opcode   |
+------------+---------+---------+------------+--------+------------+ 
           beq, branch on equal       000                  1100011
           bne, branch not equal      001                  1100011
           blt, branch less than      100                  1100011
           bgt, branch grater than    101                  1100011
           bltu, branch less than     110                  1100011
           bgtu, branch greater than  111                  1100011

以 blt 和 bltu 指令為例,當 rs1 < rs2 時,指令中內嵌得很複雜的數字會被加到 pc 暫存器中。平常使用者不能任意修改 pc 的值,而只能透過條件指令與跳躍指令來做流程控制。至於為什麼整數的編碼這麼奇怪?筆者猜測是因為,由於可執行的位址一定要對齊 2 bytes(最短的壓縮指令也有兩個位元組),所以這裡雖然只有 12-bit,但若能省略最低位bit,就可以多擴充一倍的可到達空間,也就是 +/- 4K 的記憶體範圍。

筆者猜測這裡看起來很複雜的 bit 位址分割是因為調整了整數的意義。一般來講,這個 12-bit 的空間可以支援 0~4095 或是 -2048~2047,但是這裡牽涉到指令的跳躍,所以可以多拿一個 bit 來用(讓真正的 bit 0 永遠是 0,也就是永遠是偶數的意思)。然後,因為 bit 31 事關正負號擴充,所以還是必須放置嵌入整數的最高位(imm[12]),但是剩下的線路與 S-type 指令重複使用,所以是 bit 30~25,11~8 的部份線路,然後最後一個 bit 07 已經不需要給真正的 imm[0] 使用,所以就用來放置還沒有去處的 imm[11]。型態上還是類似 S-type,因此被視為是 S-type 的變體。

這裡的指令意義是,如果 rs1 和 rs2 的比較(等於、不等於、小於、大於等於)成立,就跳到 pc + imm 的位址去。

這個階段有 6 個指令:beq, bne, blt, bgt, bltu, bgtu

非條件跳躍

RISC-V 的非條件跳躍有兩種模式,一種是與 pc 相對差距在 -1MiB(1048576)~+1MiB(1048575) 之內的 jal 指令(可支配 20 bit 整數,代表 imm[20:1]),另外一種是相對差距在 -2048~2047 的 jalr 指令(可支配 12 bit 整數,和讀寫記憶體相同)。jal 指令的調整類似我們剛看過的條件跳躍指令,但是因為嵌入整數的長度不同,所以使用的是修改自 U-type 的 J-type 指令:

| 31                                       12 | 11  07 | 06      00 |
+---------------------------------------------+--------+------------+
|        imm[ 20 | 10:1 | 11 | 19:12]         |   rd   |   1101111  |
+---------------------------------------------+--------+------------+

之所以弄成這樣一副怪德性,應該也可以套用剛才的分析方法:因為最高位 bit 的意義對於判斷整數來說非常重要,所以 imm[20] 還是放置在最前面,然後為了盡量利用原本 I-type 的 11:0 的部份,就是接下來的 imm[10:1] 緊接在後;後面補上 imm[11] 應該是因為這個模式可以和條件跳躍部份的線路重複使用;最後 imm[19:12] 就沒有必要再耍什麼花樣了。jal 指令的行為是將 pc 加上 imm (下一個指令就會是已經跳過去的位址)之前,先將 pc+4 的指令位址存在 rd 中。如果 rd 是 ra,也就是回傳位址的暫存器的話,那麼這個指令就相當於是呼叫,因為在被跳躍位址處回傳時應該可以取得這個回傳位址並跳回;若不是將 rd 設為 ra 暫存器,則就只是一個直接的跳躍。

jalr 指令則是標準的 I-type 指令,意義大致與 jal 類同,只是可支援的範圍只有以 rs1 暫存器為基礎的 -2048~2047。

| 31                20 | 19   15 | 14      12 | 11  07 | 06      00 |
+----------------------+---------+------------+--------+------------+
|   immediate[11:0]    |   rs1   |    000     |   rd   |   1100111  |
+----------------------+---------+------------+--------+------------+

這個意義是將 rd 設為pc 的值設為原本的 pc+4,然後將 pc 的值設為 rs1+imm 的結果。這個乍看之下不知道可以作什麼,事實上搭配下一段的指令介紹與使用,讀者們就可以了解為什麼這麼限制重重的跳躍模式仍然可以讓程式流程去到(與 pc 相距 32-bit 的)任何地方。

這個階段有 2 個指令:jal, jalr

設置暫存器的高位位元指令

這是昨日介紹 U-type 時也稍有著墨的 lui 與 auipc 指令。且讓筆者引用昨日所言:

這個指令需要動到 rd 暫存器中高達 20-bit 的內容。有兩個主要的指令使用這個格式,它們是 lui 指令,代表將 rd 暫存器的 [31:12] 取代為指令中的整數;另一個是 auipc,將 rd 取代為 pc 暫存器加上 imm 整數部份,常用於函數呼叫。

| 31                                       12 | 11  07 | 06      00 |
+---------------------------------------------+--------+------------+
|             immediate[31:12]                |   rd   |   opcode   |
+---------------------------------------------+--------+------------+
    lui, load upper immediate                              0110111
    auipc, add upper immediate to pc                       0010111

所以,與之前小節的指令結合的話,就可以產生以下的幾種使用方法:

  • 取得全域變數的位址:auipc/addi
  • 讀/寫全域變數:auipc/ld 或是 auipc/sd
  • 呼叫 pc 範圍內 32-bit 的函數:auipc/jalr

為什麼 lui 的狀況比較少見?因為產出 lui 搭配的定址內容之後,很容易成為位址相依的程式碼,實務上使用的量遠遠不及 auipc。但是筆者也注意到 linux kernel 之中一律使用 lui/addi 組合來定位 __vdso_rt_sigreturn,也許背後還有什麼理由是值得的探討的,但因為不在本系列最感興趣的範圍,因此這裡尚不深究

這個階段有 2 個指令:lui, auipc

系統暫存器與指令

這個部份筆者打算只介紹一個指令,那就是系統呼叫使用的 ecall 指令,與其說屬於 I-type,不如說是硬編碼

| 31                20 | 19   15 | 14      12 | 11  07 | 06      00 |
+----------------------+---------+------------+--------+------------+
|     000000000000     |  00000  |    000     |  00000 |   1110011  |
+----------------------+---------+------------+--------+------------+

這個指令會導致系統呼叫的發生,而 Linux 使用的系統呼叫暫存器是 a7,我們之後也許有機會進行相關的範例!

到這裡為止,我們總共介紹了 49 個指令!筆者快不行了...

小結


今日我們介紹 RISC-V 的 I 指令集合,雖然略去了系統相關的部份,也就是這個系列中打算實作在 rvgc 函式庫中的份量,很抱歉稍微有點枯燥,但作為一個參考應該還可以算是可以接受的吧!年關將近,也是要繼續鐵著心發鐵人文,各位讀者,我們明日再見!


上一篇
第十一日:RISC-V 指令集架構介紹
下一篇
第十三日:實作 rvgc 函式庫之一
系列文
與妖精共舞:在 RISC-V 架構上使用 GO 語言實作 binutils 工具包30

尚未有邦友留言

立即登入留言