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 個通用整數寄存器。

RV32I

從上圖可以了解不同 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 語言從編譯到執行到底精過了哪些階段,是不是很有趣呢?

Reference

⚠️ **GitHub.com Fallback** ⚠️