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, 主要是包括:

    1. 需要使用 address 的指令, b label, b offset(reg), lw, sw
    2. 一些直接操作立即数算术指令,如 addi
  • 内存寻址

    内存寻址主要用在 branch 和 load/store, MIPS 只有一种寻址方式: offset($base_reg)

    根据 offset 和 base_reg 的不同, 可以写成以下格式:

    1. label, 例如 b hello (即 base_reg 为 $0)
    2. const, 例如 lw $t1, 0x1234
    3. const(reg), 例如 b 2($t1)
    4. label+const(reg), 例如 lw $t1, hello+2($t2)

1.2. 伪指令与 $at

mips 的 assembler 提供了许多伪指令, 例如:

  1. 支持的指令数有限的情况下 (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 使用的

  2. 涉及 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 主要作用:

  1. 保存上一下 frame 的 sp (或者叫当前 frame 的基址)
  2. 做为基址访问当前 frame 的数据
  3. 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 伪指令需要两条指令:

  1. lui $t1, <high>
  2. 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

Author: [email protected]
Date: 2020-10-12 Mon 00:00
Last updated: 2023-04-26 Wed 18:47

知识共享许可协议