寄存器

  • 学习汇编语言建议先从寄存器的种类开始,大概了解每个汇编指令集有哪些寄存器,有助于我们学习汇编语言。

  • MIPS寄存器:32个通用寄存器、32个32位单精度浮点寄存器、两个用于乘法和除法的特殊寄存器LOHI

  • 下面是MIPS32个通用寄存器

寄存器编号 寄存器名 寄存器用途
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种,之后先逐类介绍,然后再汇总起来

image-20241009143006192

运算指令

  • 运算指令最多三个操作数
  • 操作数只能是寄存器或者立即数,不可以是地址

算数运算

  • 算数运算有add指令、sub指令、mul指令、div指令这四大类,分别就是加减乘除

  • 算数运算指令有如下,有比较多的是实现功能一样但是操作数不一样:

    • addaddiadduaddiudadddaddidaddudaddiu
    • subsubudsubdsubu
    • mulmultmultudmuldmultumaddmsub
    • divdivuddivddivu
    • 注:sub、add指令中带有i后缀的第三个操作数都是立即数,带有u结尾不会检查结果是否溢出,带d前缀的是对64位寄存器操作
    • 注:mul、div指令带有u后缀的表示无符号乘法、除法
  • add指令示例:

1
2
add   $s1,$s2,$s3  # $s1 = $s2+$s3,三个寄存器操作数
addi $s1,$s2,10 # $s1 = $s2+10,二个寄存器操作数,一个立即数
  • sub指令示例:
1
sub  $s1,$s2,$s3   # $s1 = $s2 - $s3,三个寄存器操作数,注意sub没有subi指令,直接用addi $s1,$s2,-10即可
  • mul指令示例:
1
2
3
4
5
6
7
8
9
10
11
12
mul  $s1,$s2,$s3    # $s1 = $s2 * $s3
madd $t0, $t1, $t2 # $t2 = $t2 + ($t0 * $t1)

#mult是有符号乘法,结果的低32位存储在LO寄存器,高32位存储在HI寄存器
mult $t1, $t2 # HI = $t1 * $t2 的高 32 位,LO = $t1 * $t2 的低 32 位
mflo $t0 # 将低 32 位结果存入 $t0,mflo这个指令相当于$t0=lo
mfhi $t3 # 将高 32 位结果存入 $t3,mfhi这个指令相当于$t3=hi

#multu是无符号乘法,将俩个源寄存器进行无符号相乘,结果存放在LO、HI寄存器中
multu $t1, $t2 # HI = $t1 * $t2 的高 32 位(无符号),LO = $t1 * $t2 的低 32 位(无符号)
mflo $t0 # 将低 32 位结果存入 $t0
mfhi $t3 # 将高 32 位结果存入 $t3
  • div指令:
1
2
3
4
5
6
7
div  $t1,$t2       # 执行无符号除法,  $t1/$t2
mflo $t0 # 从LO寄存器获取商,存储到$t0
mfhi $t3 # 从HI寄存器获取余数,存储到$t3

divu $t1, $t2 # 执行无符号除法 $t1/$t2
mflo $t0 # 从 LO 寄存器获取商,存储到 $t0
mfhi $t3 # 从 HI 寄存器获取余数,存储到 $t3

逻辑运算

  • 逻辑运算就差不多是与、或、非、位移运算
  • 可以分为and指令、or指令、xor指令、nor指令、sll指令、srl指令、sra指令
  • 这里注意:mips架构下的汇编指令并没有算数左移指令
  • 具体操作指令如下
    • andandi
    • ororixorxorinornori
    • sllsrlsra
  • and指令示例:
1
2
and $s1,$s2,$s3  # s1 = s2 & s3  三个都是寄存器操作数
and $s1,$s2,10 # s1 = s2 & 10 二个寄存器操作数,一个立即数
  • or系列指令示例:
1
2
3
4
5
6
7
8
9
10
11
# or进行或运算
or $s1,$s2,$s3 # s1 = s2 | s3
ori $s1,$s2,10 # s1 = s2 | 10

# xor进行异或运算
xor $s1,$s2,$s3 # s1 = s2 ^ s3
xori $s1,$s2,10 # s1 = s2 ^ 10

# nor进行或非操作,先或后非
nor $s1,$s2,$s3 # s1 = ~(s2 | s3)
nori $s1,$s2,10 # s1 = ~(s2 | 10)
  • 位移运算指令:
1
2
3
sll $t0, $t1, 2  # 将 $t1 左移 2 位,结果存储在 $t0 中 $t0 = $t1<<2
srl $t0, $t1, 2 # 将 $t1 右移 2 位,结果存储在 $t0 中 $t0 = $t1>>2
sra $t0, $t1, 2 # 将 $t1 算术右移 2 位,结果存储在 $t0 中 $t0 = $t1>>2

条件分支与跳转指令

  • 条件分支与跳转指令就三大类指令branch跳转分支指令、jump跳转指令、set条件设置指令

  • 具体指令如下:

    • beqbnebltbgtblebge
    • jjaljrjalr
    • seqsnesltsgtslesge
    • bgezalbltzal
  • branch指令示例:

1
2
3
4
5
6
beq $s1,$s2,0x40000  # 如果($s1 == $s2) 跳转到0x40000这个地址
bne $s1,$s2,0x40000 # 如果($s1 != $s2) 跳转到0x40000这个地址
blt $s1,$s2,0x40000 # 如果($s1 < $s2) 跳转到0x40000这个地址 blt branch less than
bgt $s1,$s2,0x40000 # 如果($s1 > $s2) 跳转到0x40000这个地址 bgt greater
ble $s1,$s2,0x40000 # 如果($s1 <= $s2) 跳转到0x40000这个地址 ble less or equal
bge $s1,$s2,0x40000 # 如果($s1 >= $s2) 跳转到0x40000这个地址 bge greater or equal
  • junm指令示例:
1
2
3
4
5
6
7
j    0x40000   # 无条件跳转到0x40000
jal 0x40000 # 无条件跳转并链接0x40000,在跳转前会保存下一个指令的地址,jal Jump and Link
# jal隐含了
# $ra = PC + 4 保存返回地址
# PC = target_address 跳转到目标地址
jr $ra # ra寄存器存储返回地址,无条件跳转到ra寄存器所存储的地址 jr Jump Register
jalr $ra # ra寄存器存储返回地址,无条件跳转到ra寄存器所存储的地址,跳转前记录下一个指令的返回值
  • seq指令示例:
1
2
3
4
seq $t0, $t1, $t2  # 如果 $t1 == $t2,$t0 = 1;否则 $t0 = 0
sne $t0, $t1, $t2 # 如果 $t1 != $t2,$t0 = 1;否则 $t0 = 0
slt $t0, $t1, $t2 # 如果 $t1 < $t2,$t0 = 1;否则 $t0 = 0
剩下的逻辑与branch指令一样
  • 复合指令:
1
2
bgezal $t0, target  # 如果 $t0 >= 0,跳转到 target,并将返回地址保存到 $ra
bltzal $t0, target # 如果 $t0 < 0,跳转到 target,并将返回地址保存到 $ra

存储访问指令

  • 如果要访问内存只能使用load(取)store(存)指令

  • 指令有如下几个:lwlbswsblila,注意:w代表word,b代表byte

  • 这里也顺便说一下move指令

  • load指令:

1
2
lw $s1,100($s2)    # $s1 = Memory[$s2+100],从内存中取一个字
lb $s1,100($s2) # $s1 = Memory[$s2+100],从内存中取一个字节
  • store指令:
1
2
3
sw $s1,100($s2)    # Memory[$s2+100] = $s1,存一个字
sw $s1, 0($s2) # Memory[$s2] = $s1,存一个字
sb $s1,100($s2)
  • li、la指令:
1
2
3
4
5
# li指令是将立即数给寄存器
li $s1,10 # $s1=10

# la指令是将一个标签的地址给寄存器,相当于x86的lea指令
la $s1,label # $s1=label
  • move指令:
1
2
3
4
# move指令和li指令区别
# li指令是将立即数给寄存器
# move指令是将一个寄存器的值复制给另一个寄存器
move $t0, $t1 # 将 $t1 中的值复制到 $t0, $t1=$t0

其他指令

  • 剩下的命令比较不常用,有的也确实不重要,但是有几个是比较重要的
  • prefsyscall(重要)、eretbreakteqtnemfmtclzcloinsext
  • 不写具体例子了
  • 注意:mips并没有pop和push指令,但是可以通过其他指令组合实现pop和push
1
2
3
4
5
6
7
8
9
# 实现 push 指令
push:
addi $sp, $sp, -4 # 为新元素分配空间
sw $t0, 0($sp) # 将 $t0 中的值存储到栈顶

# 实现 pop 指令
pop:
lw $t0, 0($sp) # 从栈顶加载值到 $t0
addi $sp, $sp, 4 # 移动栈指针,恢复栈顶

函数调用约定

  • 这里我先搭建了一个mips的交叉编译环境,然后使用c语言编写了简单的代码,使用IDA反汇编之后看到了函数调用的具体操作
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#include<stdio.h>
#include<unistd.h>
void func(int a,int b,int c);
void func1(char a,char b,char c);
void func(int a,int b,int c){
int d = a+b+c;
printf("%d",d);
func1('a','b','c');
}
void func1(char a,char b,char c){
printf("%c,%c,%c",a,b,c);
}
int main(){
char buffer[0x10];
printf("iyheart\n");
read(0,buffer,0x10);
printf("%s",buffer);
func(1,2,3);
return 0;
}
// mipsel-linux-gnu-gcc -g test1.c -o test3
  • 这里先做个简单描述:
    • 在调用之前,如果调用之后还要用到$t0~$t9主调函数就会先将其压栈,做备份
    • 调用时首先会使用li指令将参数传递给寄存器$a0-$a3中,如果超出4个参数,超出的参数会压入栈中
    • 然后再使用jal调用子函数,$ra存放着返回地址
    • 被调用的子函数开头会先改变栈帧,使栈帧指向更低的地址,$sp和$fp指针都会改变
    • 改变栈帧后,先对$s0~$s7压栈做副本(必要时),再对$a0~$a3压栈做副本(必要时)
    • 中间过程实现函数的功能
    • 实现完函数功能后就会先将返回值给$v0~$v1,然后再使用jr $ra指令返回
  • 接下来给出这个mips架构程序的elf文件,并给出使用IDA反汇编后的程序
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
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
 # Attributes: bp-based frame fpd=0x30

# int __fastcall main(int argc, const char **argv, const char **envp)
.globl main
main:

var_20= -0x20
buffer= -0x14
var_4= -4
var_s0= 0
var_s4= 4

addiu $sp, -0x38
sw $ra, 0x30+var_s4($sp)
sw $fp, 0x30+var_s0($sp)
move $fp, $sp
li $gp, (_GLOBAL_OFFSET_TABLE_+0x7FF0)
sw $gp, 0x30+var_20($sp)
la $v0, __stack_chk_guard
lw $v0, (__stack_chk_guard - 0x411084)($v0)
sw $v0, 0x30+var_4($fp)
lui $v0, 0x40 # '@'
addiu $a0, $v0, (aIyheart - 0x400000) # "iyheart"
la $v0, puts
move $t9, $v0
jalr $t9 # puts
nop
lw $gp, 0x30+var_20($fp)
addiu $v0, $fp, 0x30+buffer
li $a2, 0x10 # nbytes
move $a1, $v0 # buf
move $a0, $zero # fd
la $v0, read
move $t9, $v0
jalr $t9 # read
nop
lw $gp, 0x30+var_20($fp)
addiu $v0, $fp, 0x30+buffer
move $a1, $v0
lui $v0, 0x40 # '@'
addiu $a0, $v0, (aS - 0x400000) # "%s"
la $v0, printf
move $t9, $v0
jalr $t9 # printf
nop
lw $gp, 0x30+var_20($fp)
li $a2, 3 # c
li $a1, 2 # b
li $a0, 1 # a
jal func
nop
lw $gp, 0x30+var_20($fp)
move $v0, $zero
move $a0, $v0
la $v0, __stack_chk_guard
lw $v1, 0x30+var_4($fp)
lw $v0, (__stack_chk_guard - 0x411084)($v0)
beq $v1, $v0, loc_4009A0
nop
loc_4009A0:
move $v0, $a0
move $sp, $fp
lw $ra, 0x30+var_s4($sp)
lw $fp, 0x30+var_s0($sp)
addiu $sp, 0x38
jr $ra
nop
# End of function main
  • func函数的汇编
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
43
44
45
46
47
48
49
50
51
52
 # Attributes: bp-based frame fpd=0x20

# void __cdecl func(int a, int b, int c)
.globl func
func:

var_10= -0x10
d= -4
var_s0= 0
var_s4= 4
a= 8
b= 0xC
c= 0x10

addiu $sp, -0x28
sw $ra, 0x20+var_s4($sp)
sw $fp, 0x20+var_s0($sp)
move $fp, $sp
li $gp, (_GLOBAL_OFFSET_TABLE_+0x7FF0)
sw $gp, 0x20+var_10($sp)
sw $a0, 0x20+a($fp)
sw $a1, 0x20+b($fp)
sw $a2, 0x20+c($fp)
lw $v1, 0x20+a($fp)
lw $v0, 0x20+b($fp)
addu $v0, $v1, $v0
lw $v1, 0x20+c($fp)
addu $v0, $v1, $v0
sw $v0, 0x20+d($fp)
lw $a1, 0x20+d($fp)
lui $v0, 0x40 # '@'
addiu $a0, $v0, (unk_400AE0 - 0x400000) # format
la $v0, printf
move $t9, $v0
jalr $t9 # printf
nop
lw $gp, 0x20+var_10($fp)
li $a2, 0x63 # 'c' # c
li $a1, 0x62 # 'b' # b
li $a0, 0x61 # 'a' # a
jal func1
nop
lw $gp, 0x20+var_10($fp)
nop
move $sp, $fp
lw $ra, 0x20+var_s4($sp)
lw $fp, 0x20+var_s0($sp)
addiu $sp, 0x28
jr $ra
nop
# End of function func

  • func1函数汇编
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
43
44
45
46
47
48
 # Attributes: bp-based frame fpd=0x18

# void __cdecl func1(char a, char b, char c)
.globl func1
func1:

var_8= -8
var_s0= 0
var_s4= 4
a= 8
b= 0xC
c= 0x10

addiu $sp, -0x20
sw $ra, 0x18+var_s4($sp)
sw $fp, 0x18+var_s0($sp)
move $fp, $sp
li $gp, (_GLOBAL_OFFSET_TABLE_+0x7FF0)
sw $gp, 0x18+var_8($sp)
move $v0, $a0
move $a0, $a1
move $v1, $a2
sb $v0, 0x18+a($fp)
move $v0, $a0
sb $v0, 0x18+b($fp)
move $v0, $v1
sb $v0, 0x18+c($fp)
lb $v0, 0x18+a($fp)
lb $v1, 0x18+b($fp)
lb $a0, 0x18+c($fp)
move $a3, $a0
move $a2, $v1
move $a1, $v0
lui $v0, 0x40 # '@'
addiu $a0, $v0, (aCCC - 0x400000) # "%c,%c,%c"
la $v0, printf
move $t9, $v0
jalr $t9 # printf
nop
lw $gp, 0x18+var_8($fp)
nop
move $sp, $fp
lw $ra, 0x18+var_s4($sp)
lw $fp, 0x18+var_s0($sp)
addiu $sp, 0x20
jr $ra
nop
# End of function func1

函数调用指令

  • 函数调用指令在x86架构下是call指令,而MIPS架构下就是jal指令
  • jal指令上面有介绍,有一个隐含操作
1
2
3
4
# jal隐含了
jal target_address
$ra = PC + 4 #保存返回地址
PC = target_address #跳转到目标地址
  • 在上方的示例代码主要聚焦到main函数中的调用func函数中的汇编过程
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
# 调用子函数的过程
lw $gp, 0x30+var_20($fp)
li $a2, 3 # c
li $a1, 2 # b
li $a0, 1 # a
jal func
nop

# 子函数开头的准备
addiu $sp, -0x28
sw $ra, 0x20+var_s4($sp)
sw $fp, 0x20+var_s0($sp)
move $fp, $sp
li $gp, (_GLOBAL_OFFSET_TABLE_+0x7FF0)
sw $gp, 0x20+var_10($sp)
sw $a0, 0x20+a($fp)
sw $a1, 0x20+b($fp)
sw $a2, 0x20+c($fp)

# 子函数func函数返回到main函数的准备
nop
lw $gp, 0x20+var_10($fp)
nop
move $sp, $fp
lw $ra, 0x20+var_s4($sp)
lw $fp, 0x20+var_s0($sp)
addiu $sp, 0x28
jr $ra
nop

参数的传递

  • 参数的传递先用寄存器传递参数,当传递的参数个数大于寄存器个数时,超出部分就会使用栈传递参数

返回地址的保存

  • 一般返回地址都保存在$ra寄存器时,当要递归嵌套时,或者子函数要调用另一个子函数的时候就需要将$ra寄存器储存的返回地址压入栈中做副本,返回地址就在$fp寄存器所指的栈中高一个地址

返回值的保存

  • 返回值保存在寄存器$v0-$v1

示例程序

  • 示例程序要求看懂,这边有几个实例程序必须掌握。可以使用在线编译器将c语言和MIPS汇编对照着看
  • 接下来还可以在Linux下搭建MIPS交叉编译的环境,为Linux的异架构pwn搭建环境(本篇文章就不做介绍了)