第一次从事物联网安全相关的研究,发现很多知识需要从头学起。在努力学习之余,想发个系列文章,把学过的知识和走过的弯路都发出来,一来是督促自己,二来是希望和大牛们多学习。希望大家不吝赐教哟!
目前的物联网设备多是MIPS架构,那么就从MIPS汇编语言学起吧!
1 基础概念
MIPS(Microprocessor without Interlocked Piped Stages architecture),是一种采取精简指令集(RISC)的处理架构,由MIPS科技公司开发并授权,广泛应用在许多电子产品、网络设备、个人娱乐装置与商业装置上。最早的MPS架构是32位,最新的版本已经变成64位。
MIPS结构的基本特点是:包含大量的寄存器、指令数和字符、可视的管道延时间隙,这些特性使得MIPS架构能够提供最高的每平方毫米性能和当今SoC设计中最低的能耗。
我们平时听到很多CISC、RISC指令集,那么他们之间有什么关联呢?
- CISC(Complex Instruction Set Computer)复杂指令计算机。Intel的Pentium、AMD的K5、K6为该类型。
- RISC(Reduced Instruction Set Computer)精简指令集计算机。Acorn公司的ARM、IBM的PowerPC、MIPS公司的MIPS为该类型。
2 寄存器
2.1 通用寄存器
在MIPS体系结构中有32个通用寄存器(每个寄存器大小根据芯片型号不同而不同,在一般情况下,32位处理器中每个寄存器的大小是32位,即4字节),在汇编程序中可以用编号$0~$31表示,也可以用寄存器的名字表示,如$sp、$t1、$ra等。堆栈是从内存的高地址方向向低地址方向增长的。
编号 | 名称 | 说明 | 备注 |
---|---|---|---|
$0 | $zero | 和0值作比较运算是常用该寄存器 | |
$1 | $at | 汇编保留保留寄存器,不可做其他用途 | Assembler Temporary |
$2~$3 | $v0~$v1 | 存储函数的返回值。如果返回值为1字节,则只有$v0有效 | Values |
$4~$7 | $a0~$a3 | 作为函数调用时传递的前4个参数,不够时才使用堆栈传递 | Arguments |
$8~$15 | $t0~$t7 | 临时寄存器,无需保证函数调用的恢复 | Temporaries |
$16~$23 | $s0~$s7 | 函数调用后这些寄存器需要被恢复 | Saved Values |
$24~$25 | $t8~$t9 | 临时寄存器的拓展补充 | Temporaries |
$26~$27 | $k0~$k1 | 系统保留寄存器,请勿使用 | Kernel reserved |
$28 | $gp | 全局指针,静态数据访问时需要保证在gp指定的64kb范围 | Global Pointer |
$29 | $sp | 堆栈指针 | Stack Pointer |
$30 | $s8/$fp | Saved value / Frame Pointer | |
$31 | $ra | 存放函数调用返回的地址 | Return Address |
2.2 特殊寄存器
MIPS32架构中定义了3个特殊寄存器,分别是PC、HI、和LO。这几个用的比较少,暂时先不介绍啦。
3 字节序
数据在存储器中是按照字节存放的,处理器也是按照字节访问存储器中的指令或数据的。如果要读出一个WORD(4bytes),那么有两种结果:
- 大端模式(Big-Endian),也称MSB(Most Significant Byte),低地址存放高字节。
- 小端模式(Little-Endian),也称LSB(Least Significant Byte),低地址存放低字节。
例如0x12345678在两种模式下的存储情况如下:
低地址 高地址 大端 12 34 56 78 小端 78 56 34 12
4 MIPS指令集
4.1 MIPS指令特点
MIPS指令有一些特别的地方:
- MIPS固定4字节指令长度;
- 内存中的数据访问(load/store)必须严格对其(至少4字节对齐);
- 跳转指令只有26位目标地址,加上2位对齐位,可寻址28位的空间,即256MB;
- 条件分支指令只有16位跳转地址,加上2位对齐位,可寻址18位的空间,即256KB;
- 流水线效应。MIPS采用了高度的流水线,其中最重要的就是分支延迟效应。在分支跳转语句后面那条语句叫分支延迟槽。实际上,在程序执行到分支语句时,当他刚把要跳转的地址填充好(填充到代码计数器里),还没有完成本条指令时,分支语句后面的那个指令就已经执行了,其原因就是流水线效应——几条指令同时执行,只是处于不同的阶段。
4.2 指令格式
MIPS指令长度为32位,其中指令位均为6位,其余的26位可以分为R型、I型、J型共3种类型。
R型 Opcode(6) Rd(5) Rs(5) Rt(5) Shamt(5) Funct(6)
I型 Opcode(6) Rd(5) Rs(5) Immediate(16)
J型 Opcode(6) Address(26)
各字段含义如下:
- Opcode:指令基本操作,成为操作码;
- Rs:第一个源操作数寄存器;
- Rt:第二个源操作数寄存器;
- Rd:存放操作结果的目的操作数;
- Shamt:位移量;
- Funct:函数,这个字段选择Opcode操作的某个特定变体。
4.3 常用指令
在下文语法表述中寄存器前面使用”$”符号进行标注,“imm”表示立即数,“MEM[]”表示RAM中的一段内存,”offset“表示偏移量。
4.3.1 LOAD/STORE指令
- la(Load Address):用于将一个地址或标签存入一个寄存器
语法:
la $Rd, Label
示例:
la $t0, val_1 //复制val_1表示的地址到$t0寄存器中,其中val_1是一个Label
- li(Load Immediate):用于将一个立即数存入一个寄存器
语法:
li $Rd, imm
示例:
la $t0, 40 //将寄存器$t1赋值为40
- lui(Load Upper halfword Immediate):读取一个16位立即数放入寄存器的高16位,低16位补0。如果加载一个32位立即数(DWORD)则需要lui和addi两条指令配合完成。因为作为32位定长指令没有足够的空间存储32位立即数,只能用16位代替。
lui $Rd, imm
示例:
/* byte 1字节 8位 word 2字节 16位 dword 4字节 32位 以小端格式,0x42为例 */ lui $a1, 0x42 //将0x42放入$a1的高16位
- lw(Load Word):用于从一个指定的地址加载一个word类型的值到寄存器中
语法:
lw $Rt, offset($Rs)
示例:
lw $s0, 0($sp) //取堆栈地址偏移0内存word长度的值到$s0中,$s0 = MEM[$sp+0]
- sw(Load Word):用于将源寄存器中的值存入指定的地址
语法:
sw $Rt, offset($Rs)
示例:
sw $a0, 0($sp) //将$a0寄存器中的一个word大小的值存入堆栈,且$sp自动抬栈
- move:用于寄存器之间值的传递
语法:
move $Rt, $Rs
示例:
move $t5, $t1 //$t5 = $t1
4.3.2 算数运算指令
MIP汇编指令的特点如下:
- 算数运算指令的所有操作数都是寄存器,不能直接使用RAM地址或间接寻址
- 操作数的大小都为word(4 Byte)
add $t0, $t1, $t2 //$t0 = $t1 + $t2,带符号数相加 sub $t0, $t1, $t2 //$t0 = $t1 - $t2,带符号数相减 addi $t0, $t1, 5 //$t0 = $t1 + 5 addu $t0, $t1, $t2 //$t0 = $t1 + $t2,无符号数相加 subu $t0, $t1, $t2 //$t0 = $t1 - $t2,无符号数相减 mult $t3, $t4 //(Hi, Lo) = $t3 * $t4 div $t5, $t6 //$Lo = $t5 / $t6 $Lo为商的整数部分, $Hi为商的余数部分 mfhi $t0 //$t0 = $Hi mflo $t1 //$t1 = $Lo
4.3.3 类比较指令
在MIPS寄存器中没有标志寄存器,但是再MIPS指令中有一种指令——SLT系列指令,可以通过比较设置某个寄存器后与分支跳转指令联合使用。
- slt(Set on Less Than):有符号比较
slt $Rd, $Rs, $Rt //if ($Rs < $Rt):$Rd=1 else $Rd=0
- slti(Set on Less Than Immediate):有符号比较
slti $Rd, $Rs, imm //if ($Rs < imm):$Rd=1 else $Rd=0
- sltu(Set on Less Than Unsigned):无符号比较
sltu $Rd, $Rs, $Rt //if ($Rs < $Rt):$Rd=1 else $Rd=0
- sltiu(Set on Less Than Immediate Unsigned):有符号比较
slt $Rd, $Rs, $Rt //if ($Rs < $Rt):$Rd=1 else $Rd=0
4.3.4 系统调用指令
SYSCALL可以产生一个软中断,从而实现系统调用。
语法:
//系统调用号存放在$v0中 //参数存放在$a0~$a3中(如果参数过多,会有另一套机制来处理) //系统调用的返回值通常放在$v0中 //如果系统调用出错,则会在$a3中返回一个错误号 syscall
4.3.5 分支跳转指令
在MIPS中,分支跳转指令本身可通过比较两个寄存器中的值来决定是否跳转。要想实现与立即数比较的跳转,可以结合类跳转指令实现。
b target //goto target beq $Rs, $Rt, target //if ($Rs == $Rt): goto target blt $Rs, $Rt, target //if ($Rs < $Rt): goto target ble $Rs, $Rt, target //if ($Rs <= $Rt): goto target bgt $Rs, $Rt, target //if ($Rs > $Rt): goto target bge $Rs, $Rt, target //if ($Rs >= $Rt): goto target bne $Rs, $Rt, target //if ($Rs != $Rt): goto target
4.4.6 跳转指令
//直接跳转到target标签处 j target //跳转到$Rs寄存器指向的地址处 //常用于子函数返回 //如果子函数内又调用了其他子函数,那么$Rs的值应该被保存到堆栈中 jr $Rs //跳转到target标签处,并保存返回地址到$ra //常用于子函数的调用 //1、复制当前PC值(即子函数的返回地址)到$ra寄存器中 //2、程序跳转到子程序标签target处 jal target
5 MIPS函数调用
5.1 基本栈帧结构
完整的栈帧结构具有5个部分:
Low Address Top of Stack frame +----------------------------------+ | Local Data Storage Section | +----------------------------------+ | Pad | +----------------------------------+ | Return Address Section | +----------------------------------+ | Saved Registers Section | +----------------------------------+ | Argument Section | +----------------------------------+ High Address Bottom of Stack frmae
- 参数段(Argument Section)。参数段旨在函数调用子函数(非叶子函数)时出现。
- 编译器在生成这个段的时候保证了它的大小足够所有的参数存放。
- 参数段最小4*4 byte 大小。
- 参数寄存器$a0~$a3用于传递子函数的前4个参数。这几个参数将不会复制到栈帧空间中去(注意:栈帧空间仍将为其预留等额空间),额外的参数将复制进入参数段(偏移为16,20,24等等)。
- 参数端在栈帧的低地址端(栈顶位置)。
- 寄存器保存段(Saved Registers Section)。当函数需要使用到$s0~$s7中的寄存器时出现。
- 寄存器保存端将为每个需要保存的寄存器准备4 byte大小的的空间。
- 函数的开场白(Prologue)会在函数入口处将每一个需要保存的寄存器的值保存到寄存器保存段中,然后在返回前将其恢复到对应的寄存器中。
- 如果函数本身没有对寄存器的内容进行改变,则不需要进行保存/恢复这个过程。
- 寄存器保存段在参数段后面(栈底方向)。
- 返回地址段(Return Address Section)。当函数调用了其他子函数时出现。
- 返回地址段只有1字节大小,即用1字节来保存返回地址。
- 返回地址寄存器$ra将在函数入口处被复制到返回地址段,再函数返回前恢复到$ra中。
- 返回地址段在寄存器保存段后面(栈底方向)。
- 填充段(Pad)。当参数段+寄存器保存段+返回地址段的大小不是8的倍数时出现。
- 填充段只有1字节大小(要么就没有)。
- 填充段不存数据。
- 填充段在返回地址段后面(栈底方向)。
- 局部数据存储段(Local Data Storage Section)。当函数使用了局部参数或必须保存寄存器参数时出现。
- 局部数据存储段的大小是由子程序对于局部数据存储的需求决定的,它总是8的倍数。
- 局部数据存储段的数据是函数私有的,是被调用函数和函数的子函数都无法访问的数据。
局部数据存储段在填充段后面(栈底方向),是栈帧的最后一部分
5.2 简单叶子函数
简单叶子函数指的是不调用任何子函数,不访问任何堆栈上的内存空间的函数。
考虑如下C语言代码:
int g(int x, int y){ return (x + y); }
用汇编语言显示其调用过程如下:
g: add $v0, $a0, $a1 #result of args jr $ra #return
5.3 带数据访问的叶子函数
带数据访问的叶子函数指的是访问栈空间但是不再调用子函数的函数。
考虑如下C语言代码:
int g(int x, int y){ int a[32]; #(...calculate using x, y, a) return a[0]; }
用汇编语言显示其调用过程如下(假设计算过程中改变了$s0, $s1, $s3的值):
#start of prologue addiu $sp, $sp, (-128) #push stack frame sw $s0, 0($sp) #save value of $s0 sw $s1, 4($sp) #save value of $s1 sw $s3, 8($sp) #save value of $s3 #end of prologue #calculate using $a0, $a1, and a array is stored at address 16($sp) to 140($sp) ... lw $v0, 0($sp) #result is a[0] #start of epilogue lw $s0, 0($sp) #restore value of $s0 lw $s1, 4($sp) #restore value of $s1 lw $s3, 8($sp) #restore value of $s3 addiu $sp, $sp, 128 #pop stack frame #end of epilogue jr $ra #return
栈帧结构如下:
Low Address Top of Stack frame +-----------------------------------+ | $s0 | 0($sp) +-----------------------------------+ | $s1 | 4($sp) +-----------------------------------+ | $s3 | 8($sp) +-----------------------------------+ | empty (4 byte) | 12($sp) +-----------------------------------+ | a[0] | 16($sp) +-----------------------------------+ | a[1] | 20($sp) +-----------------------------------+ | ... | +-----------------------------------+ | a[30] | 136($sp) +-----------------------------------+ | a[31] | 144($sp) +-----------------------------------+ High Address Bottom of Stack frmae
注意:如果函数在计算过程中并没有改变$s0,$s1,$s3的值,那么编译器就不会对其进行保存操作。
5.4 非叶子函数
非叶子函数指的是将会调用子函数的函数,这样的函数栈帧空间将具有所有栈帧节区属性。
考虑如下C语言代码:
int g(int x, int y){ int a[32]; #(...calculate using x, y, a) a[1] = f(y, x, a[2], a[3], a[4]); a[0] = f(x, y, a[1], a[2], a[3]); return a[0]; }
用汇编语言显示其调用过程如下:
#start of prologue addiu $sp, $sp, (-168) #push stack frame sw $ra, 32($sp) #save the return address sw $s0, 20($sp) #save value of $s0 sw $s1, 24($sp) #save value of $s1 sw $s3, 28($sp) #save value of $s3 #end of prologue #start of body #calculate using $a0, $a1, and a array is stored at address 40($sp) to 164($sp) ... #save $a0, and $a1 in caller's stack frame sw $a0, 168($sp) #save $a0 (variable x) sw $a1, 172($sp) #save $a1 (variable y) #first call to function f lw $a0, 172($sp) #arg0 is variable y lw $a1, 168($sp) #arg1 is variable x lw $a2, 48($sp) #arg2 is a[2] lw $a3, 52($sp) #arg3 is a[3] lw $t0, 56($sp) #arg4 is a[4] sw $t0, 16($sp) #BUT it is passed on the stack! jal f #call f sw $v0, 44($sp) #store value of f into a[1] #second call to function f lw $a0, 168($sp) #arg0 is variable x lw $a1, 172($sp) #arg1 is variable y lw $a2, 44($sp) #arg2 is a[1] lw $a3, 48($sp) #arg3 is a[2] lw $t0, 52($sp) #arg4 is a[3] sw $t0, 16($sp) #BUT it is passed on the stack jal f #call f sw $v0, 40($sp) #store value of f into a[0] #load return value of g into $v0 lw $v0, 40($sp) #result is a[0] #end of body #start of epilogue lw $s0, 20($sp) #restore value of $s0 lw $s1, 24($sp) #restore value of $s1 lw $s3, 28($sp) #restore value of $s3 lw $ra, 32($sp) #restore the return address addiu $sp, $sp, 168 #pop stack frame #end of epilogue jr $ra #return
栈帧结构如下:
Low Address Top of Stack frame +-----------------------------------+ | (arg 0) | 0($sp) +-----------------------------------+ | (arg 1) | 4($sp) +-----------------------------------+ | (arg 2) | 8($sp) +-----------------------------------+ | (arg 3) | 12($sp) +-----------------------------------+ | arg 4 | 16($sp) +-----------------------------------+ | $s0 | 20($sp) +-----------------------------------+ | $s1 | 24($sp) +-----------------------------------+ | $s3 | 28($sp) +-----------------------------------+ | $ra | 32($sp) +-----------------------------------+ | empty (4 byte) | 36($sp) +-----------------------------------+ | a[0] | 40($sp) +-----------------------------------+ | a[1] | 44($sp) +-----------------------------------+ | ... | +-----------------------------------+ | a[30] | 160($sp) +-----------------------------------+ | a[31] | 164($sp) +-----------------------------------+ High Address Bottom of Stack frmae
好啦,这次就写到这里,如果有什么疏漏欢迎大家批评,我会尽快修正的!
发表评论
您还未登录,请先登录。
登录