mirror of
https://github.com/adambard/learnxinyminutes-docs.git
synced 2024-12-24 10:01:38 +00:00
335 lines
13 KiB
Markdown
335 lines
13 KiB
Markdown
---
|
||
language: "MIPS Assembly"
|
||
filename: MIPS-cn.asm
|
||
contributors:
|
||
- ["Stanley Lim", "https://github.com/Spiderpig86"]
|
||
translators:
|
||
- ["Liu Yihua", "https://github.com/yihuajack"]
|
||
lang: zh-cn
|
||
---
|
||
|
||
MIPS(Microprocessor without Interlocked Pipeline Stages)汇编语言是为了配合约翰·雷洛伊·亨尼西于1981年设计的 MIPS 微处理器范式而设计的,这些 RISC 处理器用于嵌入式系统,例如网关和路由器。
|
||
|
||
[阅读更多](https://en.wikipedia.org/wiki/MIPS_architecture)
|
||
|
||
```asm
|
||
# 注释用一个 '#' 表示
|
||
|
||
# 一行中 '#' 之后的所有文本都会被汇编器的词法分析器忽略
|
||
|
||
# 程序通常包含 .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"
|
||
|
||
```
|