- 包括 RV32I:暫存器、ABI 和帶有 ecall 的流控制。
- 與木星一起練習和練習:否定、因素、鏈、遞歸。
- 掌握交叉工具鏈, 腳本 使用 objdump 進行連結和調試。
如果您對彙編程式感到好奇,並認為 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號碼和暫存器。
幫助您熟悉條件、循環和記憶的初步練習
典型的熱身是偵測整數是否為負數。 如果為正數則傳回 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)。
要遍歷整數數組,每個索引使用 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 等特殊指令。
跨工具鏈、連結和調試
若要產生 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。
對字節世界和一般技術充滿熱情的作家。我喜歡透過寫作分享我的知識,這就是我在這個部落格中要做的,向您展示有關小工具、軟體、硬體、技術趨勢等的所有最有趣的事情。我的目標是幫助您以簡單有趣的方式暢遊數位世界。