概述
介绍了MIPS
架构下的寄存器,指令集,函数调用约定即相关架构知识,最后辅以一个简单的程序进行探究
寄存器
通用寄存器
在MIPS
下,无论是32位还是64位其通用寄存器均为32个,如下图所示
图源参考文章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架构知识
- 与
x86
不同,MIPS
指令长度固定4字节 - 栈从高地址向低地址生长
- 叶子函数:函数内部没有再调用其他函数
- 非叶子函数:函数内部调用了其他的函数
- 流水线效应:在分析
MIPS
汇编代码时会发现,其跳转到函数或者分支跳转语句的下一条都是nop
(如下图),这是因为MIPS
采用了高度的流水线,其中最重要的是跳转指令导致的分支延迟效应。在分支跳转语句后面那条语句叫做分支延迟槽,当跳转语句刚执行的一瞬间,跳转到的地址刚填充好(填充到程序计数器),还没有执行程序计数器中存放的指令,分支延迟槽的指令已经被执行了,这就是流水线效应(几条指令被同时执行,只是处于不同的阶段,MIPS
不像其他架构那样存在流水线阻塞),为了避免出现问题,因此在分支跳转语句的下一条指令通常是nop
指令或者其他有用的指令。(这个好像CSAPP一书里有提到,后面再去精读一下那一章) - 缓存刷新机制:
MIPS CPUs
有两个独立的cache
:指令cache
和数据cache
。 指令和数据分别在两个不同的缓存中。当缓存满了,会触发flush
, 将数据写回到主内存。攻击者的攻击payload
通常会被应用当做数据来处理,存储在数据缓存中。当payload
触发漏洞, 劫持程序执行流程的时候,会去执行内存中的shellcode
.如果数据缓存没有触发flush
的话,shellcode
依然存储在缓存中,而没有写入主内存。这会导致程序执行了本该存储shellcode
的地址处随机的代码,导致不可预知的后果。(通常执行sleep(1)
刷新)
函数调用约定
寄存器中预留了四个寄存器$a0~$a3
来进行参数传递,多余的则通过栈来传参,但其实还是会为前四个参数预留栈空间,在被调用者使用前四个参数时,会统一的将参数存放在栈上
在函数A
调用叶子函数B
时,会直接将返回地址存放于$ra
寄存器中
在函数A
调用非叶子函数B
时,会将其返回地址存放于$ra
寄存器中,然后在函数B
内部将$ra
的值存放于栈上,在当函数B
调用函数C
时,将函数B
返回地址存放在$ra
寄存器中,等函数C
和函数B
执行完毕后又将存储在栈上的返回地址读到$ra
寄存器中通过je $ra
指令回到A
函数
调试分析
不同编译选项
1 | #32位小端序: |
然后通过qemu-mipsel -L /usr/mipsel-linux-gnu/ ./test_mipsel_32
来运行
运行时需要通过-L
选项来指定运行库
参考文章2还提供了一键启动gdb的脚本
1 | !/bin/bash |
使用的时候按需更改即可