MIPS基础

概述

介绍了MIPS架构下的寄存器,指令集,函数调用约定即相关架构知识,最后辅以一个简单的程序进行探究

寄存器

通用寄存器

MIPS下,无论是32位还是64位其通用寄存器均为32个,如下图所示

0

图源参考文章1

特殊寄存器

名称 功能
$sr 状态寄存器(stste register),反应cpu的状态以及控制cpu
$hi,$lo 乘除结果高位和低位寄存器.乘法运算时,分别储存高低32位数据,除法运算时,$hi储存余数,$lo储存商
$pc 程序计数器(program counter),类似于x86的eip,指向当前执行的指令
$f0~$31 浮点寄存器(floating-point register),用于储存浮点数据

指令集

只归纳常见指令,后面遇到不会的再去google吧

算数运算指令

指令 功能 格式
add 有符号整数加法(将结果储存在$t0) add $t0,$t1,$t2
addi 有符号整数加法(立即数) addi,$t0,$t1,imm
addu 无符号整数加法
addiu 无符号整数加法(立即数)
sub 有符号整数减法
mult 有符号乘法
div 有符号除法

逻辑运算指令

指令 功能 格式
and 按位与 and $t0,$t1,$t2
andi 按位与(立即数) and $t0,$t1,imm
or 按位或
ori 按位或(立即数)
xor 按位异或
nor 按位取反或

移位指令

指令 功能 格式
sll 左移 sll $t0,$t1,shamt
srl 逻辑右移
sra 算数右移

数据传输指令

指令 功能 格式
lw 加载字(从指定地址加载一个字到寄存器) lw $t0,offset($t1)
sw 储存字(将寄存器的值写入指定地址)
lb 加载字节
sb 储存字节
li 将一个立即数存入通用寄存器中 li $t0,imm
lui 将一个16位的立即数左移16位后存入寄存器中
la 将一个地址或标签存入寄存器
move 用于寄存器间传值(将$t1赋值给$0) move $t0,$t1

条件分支指令

指令 功能 格式
beq 等于则跳转 beq $t0,$t1,label1
bne 不等于则跳转
bgtz 大于零则跳转 bgtz $t0,label1
blez 小于等于零则跳转

跳转指令

指令 功能 格式
j 无条件跳转 b label1
jal 跳转并链接(将$PC的值赋值给$ra再跳转) jal label1
jr 寄存器跳转 jr $t0

特殊指令

指令 功能
syscall 执行系统调用
nop 空指令

MIPS32架构知识

  1. x86不同,MIPS指令长度固定4字节
  2. 栈从高地址向低地址生长
  3. 叶子函数:函数内部没有再调用其他函数
  4. 非叶子函数:函数内部调用了其他的函数
  5. 流水线效应:在分析 MIPS 汇编代码时会发现,其跳转到函数或者分支跳转语句的下一条都是 nop (如下图),这是因为 MIPS 采用了高度的流水线,其中最重要的是跳转指令导致的分支延迟效应。在分支跳转语句后面那条语句叫做分支延迟槽,当跳转语句刚执行的一瞬间,跳转到的地址刚填充好(填充到程序计数器),还没有执行程序计数器中存放的指令,分支延迟槽的指令已经被执行了,这就是流水线效应(几条指令被同时执行,只是处于不同的阶段, MIPS 不像其他架构那样存在流水线阻塞),为了避免出现问题,因此在分支跳转语句的下一条指令通常是 nop 指令或者其他有用的指令。(这个好像CSAPP一书里有提到,后面再去精读一下那一章)
  6. 缓存刷新机制:MIPS CPUs有两个独立的 cache : 指令cache数据cache 。 指令和数据分别在两个不同的缓存中。当缓存满了,会触发 flush , 将数据写回到主内存。攻击者的攻击payload 通常会被应用当做数据来处理,存储在数据缓存中。当 payload 触发漏洞, 劫持程序执行流程的时候,会去执行内存中的 shellcode .如果数据缓存没有触发 flush 的话,shellcode 依然存储在缓存中,而没有写入主内存。这会导致程序执行了本该存储 shellcode 的地址处随机的代码,导致不可预知的后果。(通常执行 sleep(1) 刷新)

函数调用约定

1

寄存器中预留了四个寄存器$a0~$a3来进行参数传递,多余的则通过栈来传参,但其实还是会为前四个参数预留栈空间,在被调用者使用前四个参数时,会统一的将参数存放在栈上

在函数A调用叶子函数B时,会直接将返回地址存放于$ra寄存器中

在函数A调用非叶子函数B时,会将其返回地址存放于$ra寄存器中,然后在函数B内部将$ra的值存放于栈上,在当函数B调用函数C时,将函数B返回地址存放在$ra寄存器中,等函数C和函数B执行完毕后又将存储在栈上的返回地址读到$ra寄存器中通过je $ra指令回到A函数

2

调试分析

不同编译选项

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#32位小端序:
mipsel-linux-gnu-gcc -g test.c -o test_mipsel_32
#使用小端 MIPS 架构的编译器生成 32 位小端格式的可执行文件。

#32位大端序:
mips-linux-gnu-gcc -g test.c -o test_mips_32
#使用大端 MIPS 架构的编译器生成 32 位大端格式的可执行文件。

#64位小端序:
mips64el-linux-gnuabi64-gcc -g test.c -o test_mipsel_64
#使用小端 MIPS 64 位架构的编译器生成小端 64 位的可执行文件。

#64位大端序:
mips64-linux-gnuabi64-gcc -g test.c -o test_mips_64
#使用大端 MIPS 64 位架构的编译器生成大端 64 位的可执行文件。

然后通过qemu-mipsel -L /usr/mipsel-linux-gnu/ ./test_mipsel_32来运行

运行时需要通过-L选项来指定运行库

参考文章2还提供了一键启动gdb的脚本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
#!/bin/bash

# Ensure the script exits on error
set -e

# Define variables for file name, GDB port, and architecture
target_file="test_mipsel_32"
gdb_port="1234"

# Check if the target file exists
if [ ! -f "$target_file" ]; then
echo "Error: Target file '$target_file' does not exist."
exit 1
fi

# Step 1: Start QEMU with GDB server enabled
echo "Starting QEMU with GDB server on port $gdb_port..."
qemu-mipsel -L /usr/mipsel-linux-gnu/ -g $gdb_port "$target_file" &
qemu_pid=$!

# Wait for QEMU to be ready
sleep 2

echo "QEMU started with PID $qemu_pid."

# Step 2: Start GDB and connect to QEMU GDB server
mips_gdb="gdb-multiarch"

# Check if GDB is installed
if ! command -v $mips_gdb &> /dev/null; then
echo "Error: GDB ($mips_gdb) not found. Please install it and try again."
exit 1
fi

echo "Starting GDB and connecting to QEMU..."
$mips_gdb -ex "target remote :$gdb_port" "$target_file"

# Cleanup after GDB session ends
echo "Stopping QEMU..."
kill $qemu_pid

# End of script

使用的时候按需更改即可

参考文章

IOT从入门到入土

mips架构初探(一)

IOT安全入门学习–MIPS汇编基础