1 9 組譯器與連結器 (上) - ianchen0119/AwesomeCS GitHub Wiki
本文目標
- 理解 RISC-V 基礎指令集
- 假指令 (pseudo instruction)、擴展指令集
- 組譯器與組合語言
組譯器 (Assembler) 能夠將組合語言轉換為目標代碼(機器語言或接近於機器語言的程式碼)。
在 Unix 系統中,組譯器的輸入為 .s
後綴的檔案,像是: foo.s
。
此外,組譯器還有一項非常重要的功能: 它可以將擴展指令轉為基礎指令。
在談擴展指令集之前,我們先來了解一下指令集的定義。 指令集顧名思義代表著指令的集合, RISC-V 有多個基本指令集:
- RV32I
- RV32E
- RV64I
- RV128I
以 RV32I 指令集來看,該指令集一共有 47 個 32 位地址的指令集並且支持 32 個通用整數寄存器。
從上圖可以了解不同 Type 的指令如何利用 32 位元的空間,以 I-Type 來看:
- Bit 20 - Bit 31 用來表示立即數
- Bit 15 - Bit 19 表示 rs1 (Input 暫存器)
- Bit 12 - Bit 14 表示 funct (在確定指令屬於哪一個 Type 後,觀察 Funct 來判斷該指令是哪一個操作)
- Bit 7 - Bit 11 表示 rd (目標暫存器)
- Bit 0 - Bit 6 為 opcode (用於辨別 Type)
以 addi
指令為例,該指令會將 rs1 存放的數值加上立即數後將結果存放到 rd 中。I-Type 指令的 opcode 統一為 0010011
,而 addi
的 funct3 為 000
。
再以 xori
為例,該指令會將 rs1 存放的數值與立即數進行按位元的 xor 操作,完成後再將結果存放到 rd 中。而 xori
的 funct3 會是 100
。
談完基礎指令集後,再回到擴展指令集上面。 擴展指令集的誕生與人類賦予計算機的任務逐漸加重有很大的關係,像是在現代應用中,我們利用電腦處理音頻、影像的處理。縱使現代處理器具有極強的運算能力,但基礎指令一次也僅能處理一個數據,在處理 RGB 或是座標問題時就需要拆成多項指令才能完成任務。因此,勢必需要增加特殊指令處理這些問題,這些新增的指令便成 了擴展指令集。 由於 RISC-V 是屬於精減指令架構 (RISC) ,所以就連常見的乘除法操作也會透過擴展指令實作。
好奇計算機怎麼做加減乘除嗎? 可以參考先前的透過數位邏輯電路學習 Bitwise 操作一文唷!
在 RISC-V 中,有以下常見的指令集:
擴展指令集 | 指令數 | 描述 |
---|---|---|
M | 8 | 整數乘法與除法指令 |
A | 11 | 儲存器原子操作指令以及 Load-Reserved/Store-Conditional 指令 |
F | 26 | 單精度(32 bits)浮點數指令 |
D | 26 | 雙精度(64 bits)浮點數指令 |
C | 46 | 壓縮指令,指令長度為 16 bits |
以上 IMAFD 指令集組合又被稱為通用組合,在英文中以 G (General)
表示,所以 RV32G
等於 RV32IMAFD
。
要解析擴展指令有兩種直觀的做法:
- 設計相關硬體使 CPU 能夠對指令進行硬處理
- 利用多個指令構成新的指令 (假指令)
在 RISC 架構的處理器中,多數應屬於後者,這個結果也呼應到本文一開始所提到組譯器主要的功能。
在 RISC-V 中,我們編寫組合語言時會在開頭使用組譯指示符 (assemble directives)
:
- .text
- .align
- .globl
- .section
- ...
在實際檔案中會長這樣:
.text
.align 2
.globl main
main:
addi sp,sp,-16
sw ra,12(sp)
# ...
ret
# ...
其中, ret
指令並不是基礎指令,而是假指令的一種。
根據上圖,可以知道 ret
指令是由基礎指令 jalr x0, 0(x1)
擴展而成。
本文並沒有介紹全部的假指令,更多的資訊還是需要讀者自行去翻閱 RISC-V 規格書。
再以 C 語言程式碼為例:
#include <stdio.h>
int main()
{
printf("Hello, %s\n", "world");
return 0;
}
我們可以將 C 程式碼使用 RISC-V 的 C 語言編譯器進行編譯得到組譯檔案。 結果如下:
.text
.align 2
.globl main
main:
addi sp,sp,-16
sw ra,12(sp)
lui a0,%hi(string1)
addi a0,a0,%lo(string1)
lui a0,%hi(string2)
addi a0,a0,%lo(string2)
call printf
lw ra,12(sp)
addi sp,sp,16
li a0,0
ret
.secton rodata
.balign 4
string1:
.string "Hello, %s!\n"
string2:
.string "world"
-
第 7 - 8 行:
lui
指令可以將 unsigned 20-bit放到 rd暫存器的最高 20-bit,並將剩餘的 12-bit補 0 ,而%hi(string1)
則是用來取string1
的前 20 位地址值(一共 32 位)。
我們將組合語言丟到組譯器編譯後就能得到 RISC-V 的機器語言了。
00000000 <main>:
0: ff010113 addi sp,sp,-16
4: 00112623 sw ra,12(sp)
8: 00000537 lui a0,0x0
c: 00050513 mv a0,a0
10: 000005b7 lui a1,0x0
14: 00058593 mv a1,a1
18: 00000097 auipc ra,0x0
1c: 000080e7 jalr ra
20: 00c12083 lw ra,12(sp)
24: 01010113 addi sp,sp,16
28: 00000513 li a0,0
2c: 00008067 ret
組譯器的介紹在今天告一段落,下一篇會接著介紹連接器。 看完本篇與下一篇後就能了解 C 語言從編譯到執行到底精過了哪些階段,是不是很有趣呢?
- 偽指令
- Jim's Dev Blog
- RISC-V 手冊