MIPS汇编
寄存器
学习汇编语言建议先从寄存器的种类开始,大概了解每个汇编指令集有哪些寄存器,有助于我们学习汇编语言。
MIPS
寄存器:32个通用寄存器、32个32位单精度浮点寄存器、两个用于乘法和除法的特殊寄存器LO
和HI
- 下面是
MIPS
32个通用寄存器
寄存器编号 | 寄存器名 | 寄存器用途 |
---|---|---|
0 | zero | 值永远为0 |
1 | $at | 汇编保留寄存器(与宏指令相关不可用作其他用途) |
2-3 | $v0-$v1(value) | 存储表达式或者函数的返回值 |
4-7 | $a0-$a3 | 存储子程序的前4个参数,在子程序调用过程中释放 |
8-15 | $t0-$t7 | 临时变量,调用时不保存 |
16-23 | $s0-$s7 | 静态变量,调用时保存 |
24-25 | $t8-$t9 | 临时变量,同$t0-$t7的延续 |
26-27 | $k0-$k1 | 中断函数返回值,不可做其他用途 |
28 | $gp | 全局指针 |
29 | $sp | 栈指针,指向栈顶(低地址) |
30 | $fp | 帧指针(相当于x86—64下的rbp指针)指向栈底(高地址) |
31 | $ra | 返回地址 |
- 这边个人认为比较重要的寄存器就是:
$v0-$v1
、$a0-$a3
、$sp
、$ra
汇编指令
- 汇编指令大致都可以分成一下5种,之后先逐类介绍,然后再汇总起来
运算指令
- 运算指令最多三个操作数
- 操作数只能是寄存器或者立即数,不可以是地址
算数运算
- 算数运算有
add
指令、sub
指令、mul
指令、div
指令这四大类,分别就是加减乘除 算数运算指令有如下,有比较多的是实现功能一样但是操作数不一样:
add
、addi
、addu
、addiu
、dadd
、daddi
、daddu
、daddiu
sub
、subu
、dsub
、dsubu
mul
、mult
、multu
、dmul
、dmultu
、madd
、msub
div
、divu
、ddiv
、ddivu
- 注:sub、add指令中带有
i
后缀的第三个操作数都是立即数,带有u
结尾不会检查结果是否溢出,带d
前缀的是对64位寄存器操作 - 注:mul、div指令带有u后缀的表示无符号乘法、除法
add指令示例:
1 | add $s1,$s2,$s3 # $s1 = $s2+$s3,三个寄存器操作数 |
- sub指令示例:
1 | sub $s1,$s2,$s3 # $s1 = $s2 - $s3,三个寄存器操作数,注意sub没有subi指令,直接用addi $s1,$s2,-10即可 |
- mul指令示例:
1 | mul $s1,$s2,$s3 # $s1 = $s2 * $s3 |
- div指令:
1 | div $t1,$t2 # 执行无符号除法, $t1/$t2 |
逻辑运算
- 逻辑运算就差不多是与、或、非、位移运算
- 可以分为
and
指令、or
指令、xor
指令、nor
指令、sll
指令、srl
指令、sra
指令 - 这里注意:mips架构下的汇编指令并没有算数左移指令
- 具体操作指令如下
and
、andi
or
、ori
、xor
、xori
、nor
、nori
sll
、srl
、sra
- and指令示例:
1 | and $s1,$s2,$s3 # s1 = s2 & s3 三个都是寄存器操作数 |
- or系列指令示例:
1 | # or进行或运算 |
- 位移运算指令:
1 | sll $t0, $t1, 2 # 将 $t1 左移 2 位,结果存储在 $t0 中 $t0 = $t1<<2 |
条件分支与跳转指令
- 条件分支与跳转指令就三大类指令
branch
跳转分支指令、jump
跳转指令、set
条件设置指令 具体指令如下:
beq
、bne
、blt
、bgt
、ble
、bge
j
、jal
、jr
、jalr
seq
、sne
、slt
、sgt
、sle
、sge
bgezal
、bltzal
branch指令示例:
1 | beq $s1,$s2,0x40000 # 如果($s1 == $s2) 跳转到0x40000这个地址 |
- junm指令示例:
1 | j 0x40000 # 无条件跳转到0x40000 |
- seq指令示例:
1 | seq $t0, $t1, $t2 # 如果 $t1 == $t2,$t0 = 1;否则 $t0 = 0 |
- 复合指令:
1 | bgezal $t0, target # 如果 $t0 >= 0,跳转到 target,并将返回地址保存到 $ra |
存储访问指令
- 如果要访问内存只能使用
load(取)
和store(存)
指令 - 指令有如下几个:
lw
、lb
、sw
、sb
、li
、la
,注意:w代表word,b代表byte 这里也顺便说一下
move
指令load指令:
1 | lw $s1,100($s2) # $s1 = Memory[$s2+100],从内存中取一个字 |
- store指令:
1 | sw $s1,100($s2) # Memory[$s2+100] = $s1,存一个字 |
- li、la指令:
1 | # li指令是将立即数给寄存器 |
- move指令:
1 | # move指令和li指令区别 |
其他指令
- 剩下的命令比较不常用,有的也确实不重要,但是有几个是比较重要的
pref
、syscall
(重要)、eret
、break
、teq
、tne
、mf
、mt
、clz
、clo
、ins
、ext
- 不写具体例子了
- 注意:mips并没有pop和push指令,但是可以通过其他指令组合实现pop和push
1 | # 实现 push 指令 |
函数调用约定
- 这里我先搭建了一个mips的交叉编译环境,然后使用c语言编写了简单的代码,使用IDA反汇编之后看到了函数调用的具体操作
1 |
|
- 这里先做个简单描述:
- 在调用之前,如果调用之后还要用到
$t0~$t9
主调函数就会先将其压栈,做备份 - 调用时首先会使用
li
指令将参数传递给寄存器$a0-$a3中,如果超出4个参数,超出的参数会压入栈中 - 然后再使用
jal
调用子函数,$ra存放着返回地址 - 被调用的子函数开头会先改变栈帧,使栈帧指向更低的地址,$sp和$fp指针都会改变
- 改变栈帧后,先对
$s0~$s7
压栈做副本(必要时),再对$a0~$a3
压栈做副本(必要时) - 中间过程实现函数的功能
- 实现完函数功能后就会先将返回值给
$v0~$v1
,然后再使用jr $ra
指令返回
- 在调用之前,如果调用之后还要用到
- 接下来给出这个mips架构程序的elf文件,并给出使用IDA反汇编后的程序
1 | # Attributes: bp-based frame fpd=0x30 |
- func函数的汇编
1 | # Attributes: bp-based frame fpd=0x20 |
- func1函数汇编
1 | # Attributes: bp-based frame fpd=0x18 |
函数调用指令
- 函数调用指令在
x86
架构下是call
指令,而MIPS
架构下就是jal
指令 jal
指令上面有介绍,有一个隐含操作
1 | # jal隐含了 |
- 在上方的示例代码主要聚焦到main函数中的调用func函数中的汇编过程
1 | # 调用子函数的过程 |
参数的传递
- 参数的传递先用寄存器传递参数,当传递的参数个数大于寄存器个数时,超出部分就会使用栈传递参数
返回地址的保存
- 一般返回地址都保存在
$ra
寄存器时,当要递归嵌套时,或者子函数要调用另一个子函数的时候就需要将$ra
寄存器储存的返回地址压入栈中做副本,返回地址就在$fp
寄存器所指的栈中高一个地址
返回值的保存
- 返回值保存在寄存器
$v0-$v1
中
示例程序
- 示例程序要求看懂,这边有几个实例程序必须掌握。可以使用在线编译器将c语言和MIPS汇编对照着看
- 接下来还可以在Linux下搭建MIPS交叉编译的环境,为Linux的异架构pwn搭建环境(本篇文章就不做介绍了)
本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 iyheart的博客!
评论