源代碼下載: MIPS-cn.asm
MIPS(Microprocessor without Interlocked Pipeline Stages)彙編語言是為了配合約翰·雷洛伊·亨尼西於1981年設計的 MIPS 微處理器範式而設計的,這些 RISC 處理器用於嵌入式系統,例如網關和路由器。
閲讀更多
# 註釋用一個 '#' 表示
# 一行中 '#' 之後的所有文本都會被彙編器的詞法分析器忽略
# 程序通常包含 .data 和 .text 部分
.data # 數據存儲在內存中(在RAM中分配)
# 類似於高級語言中的變量
# 聲明遵循( 標籤: .類型 值 )的聲明形式
hello_world: .asciiz "Hello World\n" # 聲明一個 null 結束的字符串
num1: .word 42 # 整數被視為字
# (32位值)
arr1: .word 1, 2, 3, 4, 5 # 字數組
arr2: .byte 'a', 'b' # 字符數組(每個1字節)
buffer: .space 60 # 在 RAM 中分配空間
# (不清除為0)
# 數據類型的大小
_byte: .byte 'a' # 1字節
_halfword: .half 53 # 2字節
_word: .word 3 # 4字節
_float: .float 3.14 # 4字節
_double: .double 7.0 # 8字節
.align 2 # 數據的內存對齊
# 其中數字(應是2的冪)表示幾字節對齊
# .align 2 表示字對齊(因為 2^2 = 4 字節)
.text # 這部分包括指令和程序邏輯
.globl _main # 聲明一個全局指令標籤
# 其他文件都可以訪問
_main: # MIPS 程序按順序執行指令
# 這條標籤下的代碼將首先執行
# 打印 "hello world"
la $a0, hello_world # 加載存儲在內存中的字符串地址
li $v0, 4 # 加載 syscall 的值
# (數字代表要執行哪個 syscall)
syscall # 使用給定的參數($a0)執行指定的 syscall
# 寄存器(用於在程序執行期間保存數據)
# $t0 - $t9 # 臨時寄存器,用於過程內部的中間計算
# (過程調用時不保存)
# $s0 - $s7 # 保留寄存器(被保留的寄存器,過程調用時保存)
# 通常保存在棧中
# $a0 - $a3 # 參數寄存器,用於傳遞過程的參數
# $v0 - $v1 # 返回寄存器,用於向調用過程返回值
# 存取指令
la $t0, label # 將內存中由 label 指定的值的地址複製到寄存器 $t0 中
lw $t0, label # 從內存中複製一個字
lw $t1, 4($s0) # 從寄存器中存儲的地址複製一個字
# 偏移量為4字節(地址 + 4)
lb $t2, label # 把一個字節複製到寄存器 $t2 的低階部分
lb $t2, 0($s0) # 從 $s0 的源地址複製一個字節
# 偏移量為0
# 同理也適用於 'lh' (取半字)
sw $t0, label # 將字存儲到由 label 映射的內存地址中
sw $t0, 8($s0) # 將字存儲到 $s0 指定的地址中
# 偏移量為8字節
# 同理也適用於 'sb' (存字)和 'sh' (存半字)。'sa'不存在
### 數學 ###
_math:
# 記住要將值加載到寄存器中
lw $t0, num # 從數據部分
li $t0, 5 # 或者從一個立即數(常數)
li $t1, 6
add $t2, $t0, $t1 # $t2 = $t0 + $t1
sub $t2, $t0, $t1 # $t2 = $t0 - $t1
mul $t2, $t0, $t1 # $t2 = $t0 * $t1
div $t2, $t0, $t1 # $t2 = $t0 / $t1
# (MARS 的某些版本可能不支持)
div $t0, $t1 # 執行 $t0 / $t1。
# 用 'mflo' 得商,用 'mfhi' 得餘數
# 移位
sll $t0, $t0, 2 # 按位左移立即數(常數值)2
sllv $t0, $t1, $t2 # 根據一個寄存器中的變量值左移相應位
srl $t0, $t0, 5 # 按位右移
# (不保留符號,用0符號擴展)
srlv $t0, $t1, $t2 # 根據一個寄存器中的變量值右移相應位
sra $t0, $t0, 7 # 按算術位右移(保留符號)
srav $t0, $t1, $t2 # 根據一個寄存器中的變量值右移相應算數位
# 按位運算符
and $t0, $t1, $t2 # 按位與
andi $t0, $t1, 0xFFF # 用立即數按位與
or $t0, $t1, $t2 # 按位或
ori $t0, $t1, 0xFFF # 用立即數按位或
xor $t0, $t1, $t2 # 按位異或
xori $t0, $t1, 0xFFF # 用立即數按位異或
nor $t0, $t1, $t2 # 按位或非
## 分支 ##
_branching:
# 分支指令的基本格式通常遵循 <指令> <寄存器1> <寄存器2> <標籤>
# 如果給定的條件求值為真,則跳轉到標籤
# 有時向後編寫條件邏輯更容易,如下面的簡單的 if 語句示例所示
beq $t0, $t1, reg_eq # 如果 $t0 == $t1,則跳轉到 reg_eq
# 否則執行下一行
bne $t0, $t1, reg_neq # 當 $t0 != $t1 時跳轉
b branch_target # 非條件分支,總會執行
beqz $t0, req_eq_zero # 當 $t0 == 0 時跳轉
bnez $t0, req_neq_zero # 當 $t0 != 0 時跳轉
bgt $t0, $t1, t0_gt_t1 # 當 $t0 > $t1 時跳轉
bge $t0, $t1, t0_gte_t1 # 當 $t0 >= $t1 時跳轉
bgtz $t0, t0_gt0 # 當 $t0 > 0 時跳轉
blt $t0, $t1, t0_gt_t1 # 當 $t0 < $t1 時跳轉
ble $t0, $t1, t0_gte_t1 # 當 $t0 <= $t1 時跳轉
bltz $t0, t0_lt0 # 當 $t0 < 0 時跳轉
slt $s0, $t0, $t1 # 當 $t0 < $t1 時結果為 $s0 (1為真)
# 簡單的 if 語句
# if (i == j)
# f = g + h;
# f = f - i;
# 讓 $s0 = f, $s1 = g, $s2 = h, $s3 = i, $s4 = j
bne $s3, $s4, L1 # if (i !=j)
add $s0, $s1, $s2 # f = g + h
L1:
sub $s0, $s0, $s3 # f = f - i
# 下面是一個求3個數的最大值的例子
# 從 Java 到 MIPS 邏輯的直接翻譯:
# if (a > b)
# if (a > c)
# max = a;
# else
# max = c;
# else
# max = b;
# else
# max = c;
# 讓 $s0 = a, $s1 = b, $s2 = c, $v0 = 返回寄存器
ble $s0, $s1, a_LTE_b # 如果 (a <= b) 跳轉到 (a_LTE_b)
ble $s0, $s2, max_C # 如果 (a > b && a <= c) 跳轉到 (max_C)
move $v0, $s0 # 否則 [a > b && a > c] max = a
j done # 跳轉到程序結束
a_LTE_b: # 當 a <= b 時的標籤
ble $s1, $s2, max_C # 如果 (a <= b && b <= c) 跳轉到 (max_C)
move $v0, $s1 # 如果 (a <= b && b > c) max = b
j done # 跳轉到 done
max_C:
move $v0, $s2 # max = c
done: # 程序結束
## 循環 ##
_loops:
# 循環的基本結構是一個退出條件和一個繼續執行的跳轉指令
li $t0, 0
while:
bgt $t0, 10, end_while # 當 $t0 小於 10,不停迭代
addi $t0, $t0, 1 # 累加值
j while # 跳轉回循環開始
end_while:
# 二維矩陣遍歷
# 假設 $a0 存儲整數 3 × 3 矩陣的地址
li $t0, 0 # 計數器 i
li $t1, 0 # 計數器 j
matrix_row:
bgt $t0, 3, matrix_row_end
matrix_col:
bgt $t1, 3, matrix_col_end
# 執行一些東西
addi $t1, $t1, 1 # 累加列計數器
matrix_col_end:
# 執行一些東西
addi $t0, $t0, 1
matrix_row_end:
## 函數 ##
_functions:
# 函數是可調用的過程,可以接受參數並返回所有用標籤表示的值,如前所示
main: # 程序以 main 函數開始
jal return_1 # jal 會把當前程序計數器(PC)存儲在 $ra
# 並跳轉到 return_1
# 如果我們想傳入參數呢?
# 首先,我們必須將形參傳遞給參數寄存器
li $a0, 1
li $a1, 2
jal sum # 現在我們可以調用函數了
# 遞歸怎麼樣?
# 這需要更多的工作
# 由於 jal 會自動覆蓋每次調用,我們需要確保在 $ra 中保存並恢復之前的程序計數器
li $a0, 3
jal fact
li $v0, 10
syscall
# 這個函數返回1
return_1:
li $v0, 1 # 將值取到返回寄存器 $v0 中
jr $ra # 跳轉回原先的程序計數器繼續執行
# 有2個參數的函數
sum:
add $v0, $a0, $a1
jr $ra # 返回
# 求階乘的遞歸函數
fact:
addi $sp, $sp, -8 # 在棧中分配空間
sw $s0, ($sp) # 存儲保存當前數字的寄存器
sw $ra, 4($sp) # 存儲先前的程序計數器
li $v0, 1 # 初始化返回值
beq $a0, 0, fact_done # 如果參數為0則完成
# 否則繼續遞歸
move $s0, $a0 # 複製 $a0 到 $s0
sub $a0, $a0, 1
jal fact
mul $v0, $s0, $v0 # 做乘法
fact_done:
lw $s0, ($sp)
lw $ra, ($sp) # 恢復程序計數器
addi $sp, $sp, 8
jr $ra
## 宏 ##
_macros:
# 宏可以實現用單個標籤替換重複的代碼塊,這可以增強程序的可讀性
# 它們絕不是函數的替代品
# 它們必須在使用之前聲明
# 用於打印換行符的宏(這可以被多次重用)
.macro println()
la $a0, newline # 存儲在這裏的新行字符串
li $v0, 4
syscall
.end_macro
println() # 彙編器會在運行前複製此代碼塊
# 參數可以通過宏傳入。
# 它們由 '%' 符號表示,可以選擇起任意名字
.macro print_int(%num)
li $v0, 1
lw $a0, %num
syscall
.end_macro
li $t0, 1
print_int($t0)
# 我們也可以給宏傳遞立即數
.macro immediates(%a, %b)
add $t0, %a, %b
.end_macro
immediates(3, 5)
# 以及標籤
.macro print(%string)
la $a0, %string
li $v0, 4
syscall
.end_macro
print(hello_world)
## 數組 ##
.data
list: .word 3, 0, 1, 2, 6 # 這是一個字數組
char_arr: .asciiz "hello" # 這是一個字符數組
buffer: .space 128 # 在內存中分配塊,不會自動清除
# 這些內存塊彼此對齊
.text
la $s0, list # 取 list 的地址
li $t0, 0 # 計數器
li $t1, 5 # list 的長度
loop:
bgt $t0, $t1, end_loop
lw $a0, ($s0)
li $v0, 1
syscall # 打印數字
addi $s0, $s0, 4 # 字的大小為4字節
addi $t0, $t0, 1 # 累加
j loop
end_loop:
## INCLUDE ##
# 使用 include 語句可以將外部文件導入到程序中
# (它只是將文件中的代碼放入 include 語句的位置)
.include "somefile.asm"
有建議?或者發現什麼錯誤?在 Github 上開一個 issue,或者發起 pull request!
原著 Stanley Lim,並由 0 個好心人修改。
© 2022 Stanley Lim
Translated by: Liu Yihua
本作品採用 CC BY-SA 3.0 協議進行許可。