MIPS Tutorial
Table of Contents
1. MIPS Tutorial
1.1. 指令格式
http://max.cs.kzoo.edu/cs230/Resources/MIPS/MachineXL/InstructionFormats.html
6bit | 5bit | 5bit | 5bit | 5bit | 6bit | |
R(egister) | op(0) | reg | reg | dest | shift | funct |
I(mmediate) | op | reg | reg | constant | constant | constant |
J(ump) | op | constant | constant | constant | constant | constant |
- 由于 reg 为 5 bit, 所以共支持 32 个寄存器
R 类指令的 op 为 0, 但使用后 6 bit 标识不同的 R 指令, 例如, funct 为 32时为 add, funct 为 34 时为 sub
R 指令主要用于仅仅操作寄存器的算法运行 (shift 类指令例外, 它们可以使用一个额外的 5bit shift)
- J 指令使用 op 标识不同的指令, 例如 op 2 是 J 指令的 j, op 3 是 J 指令的 jal
I 指令也使用 op 标识不同的指令, 例如 op 4 是 I 指令的 beq, op 5 是 I 指令的 bne
所有需要操作立即数的指令, 除了少数属于 jump 相关的指令 (j, jal.., 属于 J) 和 shift 相关的指令 (sll,sra…, 属于 R), 其它的都属于 I, 主要是包括:
- 需要使用 address 的指令, b label, b offset(reg), lw, sw
- 一些直接操作立即数算术指令,如 addi
内存寻址
内存寻址主要用在 branch 和 load/store, MIPS 只有一种寻址方式: offset($base_reg)
根据 offset 和 base_reg 的不同, 可以写成以下格式:
- label, 例如 b hello (即 base_reg 为 $0)
- const, 例如 lw $t1, 0x1234
- const(reg), 例如 b 2($t1)
- label+const(reg), 例如 lw $t1, hello+2($t2)
1.2. 伪指令与 $at
mips 的 assembler 提供了许多伪指令, 例如:
- 支持的指令数有限的情况下 (6 bit op 和 6 bit funct), 使用已有的指令完成新的功能, 方便使用.
- sne, set not equal, 实际上可用 xor 来实现
bge $t1, $t2, target => slt $at $t1 $t2; beqz $at target
在实现伪指令时, assembler 可以使用 $at 寄存器, 它是保留给 assembler 使用的
- 涉及 32bit 立即数的操作, 需要被分为多条指令, 因为这个立即数无法编码在一条指令中
la, 由于一条指令无法表示 32bit 的数, 所以操作被分成两部分被赋值
la $t1, 0x4000d0 => lui $t1, 0x40; addiu $t1, $t1, 0xd0
lw
lw $t1, 0x004000f0 => lui t1,0x40; lw t1,240(t1)
1.3. 寄存器
mips 共 32 个通用寄存器, 使用 $0…$31 表示, 同时也可以使用相应的别名, 表示约定的用法.
- $0 的值固定为零
- $t0..$t9, 用作临时变量
- $s0..$s7, 用作 callee saved register
- $a0..$a3, 函数调用时传递前四个参数
- $v0, $v1, 函数返回值
- $at, assembler 实现伪指令时使用
- $sp, $fp
- $gp, 引用 .sdata section 的变量时使用
- $ra, return address, ral 跳转时会把返回地址保存在 $ra 再跳转
1.4. 函数调用
1.4.1. Overview
fib: # 保存上下文 sub $sp, 32 sw $ra, 28($sp) sw $s1, 24($sp) sw $s2, 20($sp) # 使用 callee saved register # a0 保存着参数 move $s1, $a0 blt $s1, 2, fib_base sub $a0, $s1, 1 bal fib # v0 保存着返回值 move $s2, $v0 sub $a0, $s1, 2 bal fib add $v0, $s2 b fib_return fib_base: li $v0, 1 fib_return: # 恢复上下文 lw $ra, 28($sp) lw $s1, 24($sp) lw $s2, 20($sp) add $sp, 32 # 函数返回 jr $ra
1.4.2. a0, v0
- a0..a3 保存前四个参数
- v0,v1 保存返回值
1.4.3. ra
bal func 时, 返回地址被赋值给 ra, callee 通过 br $ra 返回
需要注意的是返回地址为 pc + 8 (而不是 pc + 4), 例如:
__start: bal hello addi $t1, 1 addi $t2, 2 hello: addi $t1, 1 ############## (gdb) disass Dump of assembler code for function _ftext: => 0x004000d0 <+0>: bal 0x4000e0 <hello> 0x004000d4 <+4>: nop 0x004000d8 <+8>: addi t1,t1,1 0x004000dc <+12>: addi t2,t2,2 End of assembler dump. (gdb) ni 6 addi $t1, 1 (gdb) p /x $ra $2 = 0x4000d8
$ra 跳转后赋值为 d8, 而非 d4, 同时代码 bal 之后被 assembler 插入一条 nop
这么做的原因是 delay slot.
1.4.4. fp, sp
fp 主要作用:
- 保存上一下 frame 的 sp (或者叫当前 frame 的基址)
- 做为基址访问当前 frame 的数据
- debugger 可以通过 fp 快速的进行 backtrack
对于 1/2, 若 stack frame 大小在编译时是确定的, 则不需要 fp, 直接使用 sp 也可以恢复上一个 frame 以及通过 sp 访问当前 frame 的数据, 如 Overview 中的例子所示
对于 3, 使用 DWARF 也可以代替 fp 的作用
所以必须使用 fp 的场合可能只有一种: 使用了 alloca 调用的代码, 在编译时无法确定 stack frame 的大小
1.4.5. callee saved register
s0..s7 是 callee saved register:
- caller 可以自由使用这个寄存器, 不需要担心被其它函数覆盖
- callee 需要负责保存和恢复它们
1.5. 汇编代码的基本结构
.text .global __start __start: la $gp, _gp la $a0, a bal hello move $t1, $v0 hello: sub $sp, 32 sw $ra, 28($sp) li $v0, 1 lw $ra, 28($sp) add $sp, 32 jr $ra .data a: .ascii "hello" b: .asciiz "hello" c: .word 0xf0f0f0f0 d: .byte 0xf0 .byte 0xf0 .byte 0xf0 .byte 0xf0 .sdata sa: .word 0x1234
1.6. global pointer
gp 用来指示 .sdata 的基址, assembler 针对 .sdata 中数据的访问会生成以 gp 为基址的指令, 例如:
__start: lw $t1, a .sdata a: .word 1 ################## (gdg) disass Dump of assembler code for function _ftext: => 0x004000f0 <+0>: lw t1,-32752(gp) 0x004000f4 <+4>: nop 0x004000f8 <+8>: nop 0x004000fc <+12>: nop
ld 会负责分配 .sdata 并用 _gp 这个符号指向它的基址.
(gdb) p &_gp $1 = (<data variable, no debug info> *) 0x418120
所以 asm 需要自己用 la $gp, _gp 来初始化 gp.
使用 gp 可以加快访存指令的速度, 因为普通的 lw 伪指令需要两条指令:
- lui $t1, <high>
- lw $t1, <low>($t1)
而使用 gp 只需要一条指令: lw $t1, <addr>($gp), 限制是只能访问 gp 为基址的 64K 地址
Backlinks
RISC-V Tutorial (RISC-V Tutorial > RISC-V Assembly > Register): - gp 是 global pointer
crt0.o (Bare Metal > crt > crt0.o): 1. 初始化 GP 2. 清空 BSS 3. 通过 atexit 注册 _libc_fini_array, 以便在程序结束时调用 finit_array 4. 通过 _libc_init_array 以调用 init_array 5. 调用 main 6. exit
1.7. Delay Slot
https://en.wikipedia.org/wiki/Delay_slot
assembler 可以通过调整指令的顺序避免生成一个无用的 nop:
__start: move $t1, $t2 move $t2, $t3 bal hello addi $t2, 2 hello: addi $t1, 1 #################### (gdb) disass => 0x004000d0 <+0>: move t1,t2 0x004000d4 <+4>: bal 0x4000e0 <hello> 0x004000d8 <+8>: move t2,t3 0x004000dc <+12>: addi t2,t2,2 End of assembler dump.
第二条 move 被放在 bal 之后做为 delay slot, 但执行起来与原来的顺序是一致的
ps. 实践中发现, 在编译时使用了 -g 参数后会禁止 assembler 的这个行为
函数函数时的 bal 需要 delay slot, 实际上, 凡是导致流水线不能持续的指令都需要 delay slot, 比如:
访问内存无法在一个CPU周期内执行完
# 原指令 lw $t1, __start move $t2, $t1 # 实际指令 lui t1,0x40 lw t1,240(t1) nop move t2,t1
mul 无法在一个CPU周期内执行完 (执行 mul 可能需要 3~5 个 CPU 周期)
# 原指令 mul $t1, $t2, $t3 move $t2, $t1 # 实际指令 multu t2,t3 mflo t1 nop nop move t2,t1
Backlinks
RISC-V Tutorial (RISC-V Tutorial > 其它 > RISC-V vs. MIPS): - riscv 没有 Delay Slot
1.8. 常用汇编指令
- li
- la
- move
- b
- j
- bal
- jr
- lw
- sw
- bgt
- sgt
- add
- sub
- mul
- div
- syscall