使用 RISC-V 彙編器進行程式設計需要什麼

最後更新: 08/10/2025
作者: 艾薩克
  • 包括 RV32I:暫存器、ABI 和帶有 ecall 的流控制。
  • 與木星一起練習和練習:否定、因素、鏈、遞歸。
  • 掌握交叉工具鏈, 腳本 使用 objdump 進行連結和調試。

RISC-V 彙編器:要求和入門

如果您對彙編程式感到好奇,並認為 RISC-V 是可行的方法,那麼您來對地方了。 在 RISC-V 上開始使用 ASM 比看起來更實惠 如果你了解這些工具, 程序設計 以及一些關鍵的建築規則。

在下面的幾行中,我結合了幾個來源的最佳內容:使用木星型模擬器的實踐, RV32I 基本曲目的約定、循環和遞歸範例、系統調用,甚至查看 VHDL 中的 RISC-V CPU 設計(帶有 ALU、記憶體控制和狀態機),以及對交叉工具鍊和連結腳本的回顧。

什麼是 RISC-V 彙編程式?它與機器語言有何不同?

雖然兩者都與 硬件, 機器語言是純二進位(1 和 0) CPU 直接解釋,而彙編器使用助記符和 符號 比組譯程式更易讀,然後轉換為二進位。

RISC-V 定義了一個具有非常乾淨的基礎指令集的開放 ISA。 RV32I(32 位元)設定檔包含 39 個使用者指令 具有卓越的正交性,將記憶體存取與純計算分開,並在 GCC/LLVM 中提供出色的支援。

記錄、協定和入口點

在 RV32I 中,您有 32個通用暫存器(x0–x31) 32 位元;x0 始終讀取為 0,且無法寫入。諸如 a0–a7(參數)、t0–t6(臨時變數)或 s0–s11(已儲存)之類的別名對於遵循 ABI 也很有用。

根據環境或模擬器,程式可能會從特定標籤啟動。 在 Jupiter 中,程式從全域 __start 標籤開始。,您必須將其宣告為可見(例如,使用 .globl)以標記入口點。

標籤以冒號結尾,每行只能放一條指令,註解可以用#或;開頭,所以組譯器會忽略它們。

工具和模擬器:Jupiter 和基本工作流程

為了順利進行練習,您必須 Jupiter 模擬器/彙編器,一種受 SPIM/MARS/VENUS 啟發的圖形工具,可在單一環境中方便編輯、組裝和執行。

在 Jupiter 中,您可以在編輯器標籤中建立、編輯和刪除檔案。 儲存後,按F3組裝並運行 逐條指令地調試流程,使用暫存器和記憶體視圖來了解機器的狀態。

程式必須以對環境的呼叫結束: 使用代碼 10 退出呼叫設定 a0 (退出)。在 RISC-V 中,ecall 相當於系統呼叫或對環境/系統的陷阱。

程式的最小結構和系統調用

學術範例中的典型結構定義了一個起點,完成了工作,並以 ecall 結束。 ecall 參數通常在 a0–a2 中傳輸 以及 a7 中的服務選擇器,取決於環境。

在一個 Linux 例如,RISC-V,您可以使用寫入系統呼叫進行列印,並使用適當的程式碼退出。 對於寫入,a0(fd)、a1(緩衝區)、a2(長度)和a7與服務編號一起使用。最後,將 a0 設定為回傳代碼,將 a7 設定為退出號。

# Ejemplo mínimo (Linux RISC-V) para escribir y salir
.global _start
_start:
  addi a0, x0, 1        # fd = 1 (stdout)
  la   a1, msg          # a1 = &msg
  addi a2, x0, 12       # a2 = longitud
  addi a7, x0, 64       # write
  ecall

  addi a0, x0, 0        # return code
  addi a7, x0, 93       # exit
  ecall

.data
msg: .ascii "Hola mundo\n"

如果你在 Linux 之外工作,例如 擁有自己的物聯網服務的教育模擬器,根據環境文件更改ecall號碼和暫存器。

  取得 Windows 8.1 ISO 檔案 [USB 和 DVD 設定]

幫助您熟悉條件、循環和記憶的初步練習

典型的熱身是偵測整數是否為負數。 如果為正數則傳回 0,如果為負數則回傳 1。;使用 RV32I,透過與 0 進行比較並在小於時進行設置,可以透過一條經過深思熟慮的指令解決該問題。

另一個非常有用的練習是列出一個數字的因數: 從 1 到 n 遍歷,列印除數,並傳回除數的數量您將練習條件分支、除法(或重複減法)以及加法和比較循環。

使用字串會強制您管理記憶體: 存取記憶體中字串的每個字符,並就地將小寫字母轉換為大寫字母 如果它們在 ASCII 範圍內。完成後,返回字串的原始位址。

循環、函數與遞歸:階乘、斐波那契和漢諾塔

設計循環時,請考慮三個區塊:條件、主體和步驟。 使用 beq/bne/bge 和無條件跳轉 jal/j while/for 構建 沒有神秘感,依靠附加和比較。

.text
.globl __start
__start:
  li t0, 0        # i
  li t1, 10       # max
cond:
  bge t0, t1, end # si i >= max, salta
  # cuerpo: usar a0/a1 segun servicio IO del entorno
  addi t0, t0, 1  # i++
  j cond
end:
  li a0, 10
  ecall

在函數呼叫中,尊重 ABI: 如果您要連續調用更多調用,請儲存,如果修改了 s0–s11,則保留它們,並使用 sp 以字的倍數移動的堆疊。

階乘是經典的遞歸: 基本情況 n==0 回傳 1;否則,呼叫 factorial(n-1) 並乘以 n。在呼叫之前,保護堆疊上保存的 ra 和暫存器,並在返回時恢復它們。

factorial:
  beq a0, x0, base
  addi sp, sp, -8
  sw   ra, 4(sp)
  sw   s0, 0(sp)
  mv   s0, a0
  addi a0, a0, -1
  jal  factorial
  mul  a0, a0, s0
  lw   s0, 0(sp)
  lw   ra, 4(sp)
  addi sp, sp, 8
  jr   ra
base:
  li a0, 1
  jr ra

斐波那契數列對於練習 兩次調用的遞歸 作為一個高效的迭代版本,帶有累加器變數。如果你想要流程和參數控制的挑戰,可以將解決方案翻譯成組合語言 河內塔 有四個參數:磁碟、來源、目標和輔助塔;它遵循呼叫順序並顯示每個動作。

記憶體存取、陣列和字串操作

在 RISC-V 中,記憶體存取是透過載入/儲存完成的: lw/sw 表示字,lh/sh 表示半字,lb/sb 表示字節,電荷中有符號或無符號的變體(lb vs lbu、lh vs lhu)。

  Android 上的 Project Zomboid:您可以在手機上玩嗎?

要遍歷整數數組,每個索引使用 4 個位元組偏移量;對於文字字串, 逐字節前進,直到找到終止符 如果約定要求的話(例如,\0)。記得保存基底位址,並根據需要使用 addi/auipc/la 處理指標。

從頭開始設計 RV32I CPU:進階概述

如果你想深入研究矽片,一個教育計畫可以建造一個 RV32I CPU 採用 VHDL 語言,可在 FPGA 中綜合 中低階。包含程式 ROM、資料 RAM 以及用於點亮 LED 的簡單 GPIO。

核心實作了基本指令集(沒有 M/A/C 擴充或 CSR), 使用 32 位元位址匯流排 並允許在適當的情況下進行8/16/32位元符號擴展記憶體存取。此設計明確地將暫存器、ALU、記憶體控制器和狀態機分開。

ALU、移位和「延遲載入」的概念

ALU 是透過以下操作組合來描述的,例如 加法、減法、異或、或、與、比較(有符號和無符號) 以及邏輯/算術移位。

為了在 FPGA 中保存 LUT,實現了多個移位 由狀態機控制的迭代 1 位移位:您消耗了幾個週期,但減少了邏輯資源。

在同步電路中,時脈邊緣會改變。 「延遲載入」的概念讓人想起多工器選擇的內容會影響下一個週期的暫存器,這是設計獲取-解碼-執行狀態機時的關鍵方面。

記憶體控制器和映射:ROM、RAM 和 GPIO

內存塊將 ROM 和 RAM 整合到連續的空間中, 簡化處理器介面控制器接收 AddressIn(32 位元)、DataIn、寬度(位元組/半/字)、符號擴展訊號、WE(讀取/寫入)和 Start 來啟動交易。

手術結束後, ReadyOut 設定為 1,如果已讀取,DataOut 包含資料(請求時進行符號擴充)。如果已寫入,則資料保留在 RAM 中。

實用地圖範例: ROM 從 0x0000 到 0x0FFF,位於 0x1000 的 GPIO 位元組(引腳的位元 0)和 RAM 從 0x1001 到 0x1FFF透過這個,您可以透過寫入和切換輸出位元來製作閃光燈。

暫存器、多工器和狀態機

CPU 定義了 32 個通用暫存器,這些暫存器在 VHDL 中以陣列形式實例化, 解碼器 從 ALU 中選擇寫入目的地並保留其餘部分。

多工器控制 ALU 輸入(操作數和操作), 發送到記憶體控制器的訊號 (寬度、位址、啟動控制和讀取/寫入)和特殊暫存器:PC、IR 和用於迭代移位的輔助計數器。

狀態機開始於 重置, 取得 PC 指向的指令 (4 位元組讀取),它在準備就緒時被載入到 IR 中並傳遞到執行節點:ALU(1 個週期內一條指令,移位除外)、載入/儲存、分支和跳轉,以及 ebreak 等特殊指令。

  如何在 Windows 10 中設定藍牙

跨工具鏈、連結和調試

若要產生 RV32I 二進位文件,請使用 跨 GCC(目標 riscv32-none-elf)。您編譯 C/C++/ASM 原始程式碼,連結定義記憶體對映的腳本,並將輸出轉換為 ROM/FPGA 期望的形式。

一個簡單的鉤子腳本可以放置 ROM 中的 .text 從 0x0000 開始 和 .data 從 0x1004 開始儲存在 RAM 中(如果 0x1000–0x1003 被 GPIO 暫存器佔用)。啟動例程可以“裸露”,並將 RAM 末端的堆疊指標(例如 0x1FFC) 在呼叫 main 之前。

/* Mapa simple
 * ROM: 0x00000000 - 0x00000FFF
 * GPIO: 0x00001000 - 0x00001003
 * RAM: 0x00001004 - 0x00001FFF
 */
SECTIONS {
  . = 0x00000000;
  .text : { *(.startup) *(.text) *(.text.*) *(.rodata*) }
  . = 0x00001004;
  .data : { *(.data) *(.data.*) }
}

使用 riscv32-none-elf-objdump 你可以 反彙編 ELF 並檢查地址;例如,你會看到 開機 在 0x00000000 處,使用 lui/addi/jal 等指令並轉換到主程式。對於 VHDL 仿真,GHDL 會產生可用 GtkWave 開啟的軌跡。

經過模擬驗證後,將設計轉移到 FPGA(Quartus 或其他工具鏈)。 如果 RAM 被推斷為內部區塊,且程式碼清晰 RTL,即使在老舊的設備上,您也應該能夠毫無意外地進行合成。

實用提醒和初期常見錯誤

別忘了 x0 始終為零;寫入它沒有任何效果,讀取它則回傳 0。在進行加法、比較和註冊表清理時,可以利用這一點。

當你實現功能時, 儲存您修改的 ra 和 sN 記錄,並透過對 sp 進行字對齊的加/減操作來管理堆疊。返回時,它以相反的順序恢復,並使用 jr ra 進行跳轉。

在 Jupiter 等模擬器中,檢查 __start 是全域的,你用 ecall 結束它 正確(a0=10 退出)。如果出現問題,請檢查標籤、全域性並重新編譯(F3)。

在與 IO 的練習中, 尊重環境協議:哪些暫存器承載參數、服務編號、預期的是位址或立即值。請使用模擬器或作業系統文件。

有了清晰的 ISA 基礎(RV32I、寄存器和 ABI)、像 Jupiter 這樣舒適的模擬器以及不斷增加的示例(負數、因子、大寫、循環、階乘、斐波那契和漢諾塔),RISC-V 彙編器不再是一堵牆,而是成為了解 CPU 如何思考的豐富地形。 如果您敢於深入研究 VHDL,您將會看到 ALU、記憶體和控制是如何組合在一起的。:從指令取得和延遲載入到記憶體介面以及帶有 ROM、RAM 和 GPIO 的地圖,可讓您使用自己的處理器閃爍 LED。

最佳程式設計程式
相關文章:
7 個最佳編程程序