本文目標
組譯器 (Assembler) 能夠將組合語言轉換為目標代碼(機器語言或接近於機器語言的程式碼)。
在 Unix 系統中,組譯器的輸入為 .s
後綴的檔案,像是: foo.s
。
此外,組譯器還有一項非常重要的功能: 它可以將擴展指令轉為基礎指令。
在談擴展指令集之前,我們先來了解一下指令集的定義。
指令集顧名思義代表著指令的集合, RISC-V 有多個基本指令集:
以 RV32I 指令集來看,該指令集一共有 47 個 32 位地址的指令集並且支持 32 個通用整數寄存器。
從上圖可以了解不同 Type 的指令如何利用 32 位元的空間,以 I-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
。
要解析擴展指令有兩種直觀的做法:
在 RISC 架構的處理器中,多數應屬於後者,這個結果也呼應到本文一開始所提到組譯器主要的功能。
在 RISC-V 中,我們編寫組合語言時會在開頭使用組譯指示符 (assemble directives)
:
在實際檔案中會長這樣:
.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 語言從編譯到執行到底精過了哪些階段,是不是很有趣呢?