已经过了填坑的黄金时期
涉及到的架构太多了,这里只是部分,当然遇到这里没有的架构参考下面的方法基本也就会了。
很多开源项目需要交叉编译到特定架构上,因此需要安装对应的交叉编译工具链。
sudo apt install gcc-arm-linux-gnueabi g++-arm-linux-gnueabi -y
sudo apt install gcc-aarch64-linux-gnu g++-aarch64-linux-gnu -y
sudo apt install gcc-mips64el-linux-gnuabi64 g++-mips64el-linux-gnuabi64 -y
sudo apt install gcc-mips-linux-gnu g++-mips-linux-gnu -y
sudo apt install gcc-mipsel-linux-gnu g++-mipsel-linux-gnu -y
sudo apt install gcc-mips64-linux-gnuabi64 g++-mips64-linux-gnuabi64 -y
sudo apt install gcc-powerpc-linux-gnu g++-powerpc-linux-gnu -y
sudo apt install gcc-powerpc64-linux-gnu g++-powerpc64-linux-gnu -y
sudo apt install gcc-riscv64-linux-gnu g++-riscv64-linux-gnu -y
sudo apt install gcc-alpha-linux-gnu g++-alpha-linux-gnu -y
sudo apt install gcc-s390x-linux-gnu g++-s390x-linux-gnu -y
sudo apt install gcc-sparc64-linux-gnu g++-sparc64-linux-gnu -y
以 arm
为例,我们有 C 编译器 arm-linux-gnueabi-gcc
和 C++ 编译器 arm-linux-gnueabi-g++
,使用方法和 gcc
与 g++
相同。而工具链位于 /usr/arm-linux-gnueabi
。
安装 gdb-multiarch
。
sudo apt install gdb gdb-multiarch
gdbserver
或者 qemu-user
的 -g
参数):target remote ip地址:端口号
gdb -p 进程pid
交叉编译多个架构的 gdbserver
静态版本。注意编译的 gdb
版本最好和本机 gdb
版本相同。
curl https://ftp.gnu.org/gnu/gdb/gdb-9.2.tar.gz --output gdb-9.2.tar.gz
tar -zxvf ./gdb-9.2.tar.gz
cd gdb-9.2
# arm-32-little gdbserver
mkdir build_arm_static && cd build_arm_static
CC="arm-linux-gnueabi-gcc" CXX="arm-linux-gnueabi-g++" ../gdb/gdbserver/configure --target=arm-linux-gnueabi --host="arm-linux-gnueabi"
make LDFLAGS=-static -j $(nproc) && cd ..
# mips-32-little gdbserver
mkdir build_mipsel_static && cd build_mipsel_static
CC="mipsel-linux-gnu-gcc" CXX="mipsel-linux-gnu-g++" ../gdb/gdbserver/configure --target=mipsel-linux-gnu --host="mipsel-linux-gnu"
make LDFLAGS=-static -j $(nproc) && cd ..
# mips-32-big gdbserver
mkdir build_mips_static && cd build_mips_static
CC="mips-linux-gnu-gcc" CXX="mips-linux-gnu-g++" ../gdb/gdbserver/configure --target=mips-linux-gnu --host="mips-linux-gnu"
make LDFLAGS=-static -j $(nproc) && cd ..
# mips64-64-little gdbserver
mkdir build_mips64el_static && cd build_mips64el_static
CC="mips64el-linux-gnuabi64-gcc" CXX="mips64el-linux-gnuabi64-g++" ../gdb/gdbserver/configure --target=mips64el-linux-gnuabi64 --host="mips64el-linux-gnuabi64"
make LDFLAGS=-static -j $(nproc) && cd ..
# mips64-64-big gdbserver
mkdir build_mips64_static && cd build_mips64_static
CC="mips64-linux-gnuabi64-gcc" CXX="mips64-linux-gnuabi64-g++" ../gdb/gdbserver/configure --target=mips64-linux-gnuabi64 --host="mips64-linux-gnuabi64"
make LDFLAGS=-static -j $(nproc) && cd ..
# powerpc-32-big gdbserver
mkdir build_ppc_static && cd build_ppc_static
CC="powerpc-linux-gnu-gcc" CXX="powerpc-linux-gnu-g++" ../gdb/gdbserver/configure --target=powerpc-linux-gnu --host="powerpc-linux-gnu"
make LDFLAGS=-static -j $(nproc) && cd ..
# aarch64-64-little gdbserver
mkdir build_aarch64_static && cd build_aarch64_static
CC="aarch64-linux-gnu-gcc" CXX="aarch64-linux-gnu-g++" ../gdb/gdbserver/configure --target=aarch64-linux-gnu --host="aarch64-linux-gnu"
make LDFLAGS=-static -j $(nproc) && cd ..
CC="mipsel-linux-gnu-gcc"
:指定 C 编译器为 mipsel-linux-gnu-gcc
。这告诉配置脚本在编译期间使用指定的 C 编译器。CXX="mipsel-linux-gnu-g++"
:指定 C++ 编译器为 mipsel-linux-gnu-g++
。这告诉配置脚本在编译期间使用指定的 C++ 编译器。../gdb/gdbserver/configure
:运行 configure
脚本,它是一个用于自动配置软件的常见脚本。--target=mipsel-linux-gnu
:指定目标平台为 mipsel-linux-gnu
。这告诉配置脚本生成针对 mipsel-linux-gnu
平台的交叉编译配置。--host="mipsel-linux-gnu"
:指定主机平台为 mipsel-linux-gnu
。这告诉配置脚本生成适用于 mipsel-linux-gnu
主机平台的编译配置。有些环境恶劣到这种静态编译 gdbserver
都跑不起来可以考虑尝试能不能将 qemu
虚拟机联网,然后 apt
安装 gdb-multiarch
顺带将 gdbserver
下载下来。
gdbserver
附加进程命令:gdbserver :端口号 --attach 进程pid
qemu 具体有如下分类:
qemu-user
:提供用户态的简单仿真。适用于对一些相对简单程序的进行仿真。
qemu-user-static
:静态编译的 qemu-user
,由于有些依赖特定动态库的固件程序需要通过 chroot
将根目录切换到解压出的文件系统根目录然后再用 qemu-user
仿真程序,如果使用动态链接的 qemu-user
仿真会因根目录切换而无法找到 qemu-user
本身所依赖的动态库而导致仿真失败,因此需要使用 qemu-user-static
。qemu-system
:提供完整的系统级仿真。有些固件的运行需要一整套复杂的环境,单纯使用 qemu-user
仿真比较麻烦并且容易出问题,因此一个比较简单的方法是把整个 IOT 设备的文件系统扔到对应架构的一个完整虚拟机中然后 chroot
切换到 IOT 设备的文件系统的根目录进行仿真,此时需要使用 qemu-system
运行整个虚拟机。qemu-utils
:qemu 的一些配套工具,比如制作磁盘镜像的 qemu-img
。在 ubuntu 20.04 中 qemu 各部分需要分别安装。
sudo apt install qemu -y
sudo apt install qemu-user qemu-user-static -y
sudo apt install qemu-system -y
sudo apt install qemu-utils -y
Linux 内核有一个名为 Miscellaneous Binary Format(binfmt_misc
)的机制,可以通过要打开文件的特性来选择到底使用哪个程序来打开。这种机制可以通过文件的扩展名和文件开始位置的特殊的字节(Magic Byte)来判断应该如何打开文件。安装好 qemu-user
后会在 /proc/sys/fs/binfmt_misc/
路径下添加 binfmt_misc
相应的配置文件。因此我们直接运行异架构程序 Linux 会选择正确的 qemu-user-static
程序运行。
➜ ~ cat /proc/sys/fs/binfmt_misc/qemu-arm
enabled
interpreter /usr/bin/qemu-arm-static
flags: OCF
offset 0
magic 7f454c4601010100000000000000000002002800
mask ffffffffffffff00fffffffffffffffffeffffff
然而对于动态链接的程序,qemu 可以正常加载程序,但是动态库却会默认使用本机的动态库导致程序崩溃,因此需要 -L
参数指定 ld
的前缀径:
qemu-arm -L /usr/arm-linux-gnueabi 程序路径
另外如果将交叉编译工具链添加到对应的 qemu-binfmt
,则 qemu-user
在运行程序时能加载正确的动态链接库,不需要指定路径。
sudo mkdir /etc/qemu-binfmt
sudo ln -s /usr/arm-linux-gnueabi /etc/qemu-binfmt/arm
sudo ln -s /usr/mipsel-linux-gnu/ /etc/qemu-binfmt/mipsel
sudo ln -s /usr/aarch64-linux-gnu /etc/qemu-binfmt/aarch64
sudo ln -s /usr/powerpc-linux-gnu /etc/qemu-binfmt/ppc
sudo ln -s /usr/mips64-linux-gnuabi64 /etc/qemu-binfmt/mips64
sudo ln -s /usr/mips64el-linux-gnuabi64 /etc/qemu-binfmt/mips64el
sudo ln -s /usr/mips-linux-gnu /etc/qemu-binfmt/mips
另外 qemu-user
还有如下常用功能:
-g 端口号
:开启 gdb
远程调试端口并等待 gdb
连接。-E var=value
:设置环境变量。-strace
:打印系统调用日志。qemu-system
仿真需要提供系统内核和文件系统,我们可以在这个网站下载所需结构的内核和文件系统。
qemu-system
启动命令如下,具体启动参数还要参考镜像对应的 README
作相应的调整。
qemu-system-arm \
-M versatilepb \
-kernel vmlinuz-3.2.0-4-versatile \
-hda debian_wheezy_armel_standard.qcow2 \
-initrd initrd.img-3.2.0-4-versatile \
-append "root=/dev/sda1 console=tty0" \
-net nic \
-net tap,ifname=tap0,script=no,downscript=no \
-nographic
-M malta
:主板类型,参考内核镜像后缀。-kernel vmlinux-3.2.0-4-4kc-malta
:内核镜像。-hda debian_wheezy_mipsel_standard.qcow2
:虚拟磁盘镜像。-initrd initrd.img-3.2.0-4-versatile
:一个临时的文件系统映像,用于在Linux系统引导过程中提供必要的文件和工具,它通常用于初始化和准备真正的根文件系统之前。-append root=/dev/sda1 console=tty0
: 内核启动参数。-net nic
:添加一个网络接口卡(NIC)到模拟器中,以实现网络功能。-net tap,ifname=tap0,script=no,downscript=no
:添加一个 TAP 设备,并将其与模拟器中的网络接口卡关联起来,用于与主机的网络进行通信。-nographic
:禁用图形界面输出,只使用命令行界面进行连接和操作。为了能够与 qemu 虚拟机通信,需要再 qemu 虚拟机启动先在外部主机创建并配置网卡 tap0
。
sudo apt install uml-utilities
if ! ip link show tap0 &>/dev/null; then
tunctl -t tap0 -u $(whoami)
fi
ifconfig tap0 192.168.2.1/24
qemu 虚拟机启动后需要再虚拟机中分配 ip 。
ifconfig eth0 192.168.2.2/24
之后就可以通过 scp
向虚拟机传文件或者 ssh
登录虚拟机。
sudo apt install sshpass
sshpass -p root scp squashfs-root.tar.xz [email protected]:~
sshpass -p root ssh [email protected]
一个分析和解压固件的工具。
sudo apt install binwalk -y
binwalk
使用 -e
参数可以解压固件。注意新版的 binwalk
为了安全会将符号链接到具体设备的链接修改为链接到 /dev/null
,因此需要添加 --preserve-symlinks
参数。
binwalk 固件名称
binwalk -e 固件名称
binwalk -Me 固件名称
另外需要安装 sasquatch
用于对非标准的 SquashFS
格式的文件镜像进行解压。
git clone https://github.com/devttys0/sasquatch.git
cd sasquatch
wget https://github.com/devttys0/sasquatch/pull/47.patch
patch -p1 < 47.patch
sudo ./build.sh
Firmadyne 是一个自动化的固件识别与仿真的框架,FirmAE 是 Firmadyne 的改进版。
firmware-analysis-toolkit 是在 Firmadyne 的基础上开发的固件仿真框架。可以采用如下方法安装 firmware-analysis-toolkit 。
git clone https://github.com/attify/firmware-analysis-toolkit.git
cd firmware-analysis-toolkit
sudo ./setup.sh
之后配置 fat.config
文件,将 sudo_password
改成正确的值,firmadyne_path
不需要改。
仿真的方式是 fat.py + 固件的路径名
$ ./fat.py <firmware file>
在我的环境下使用 python3 fat.py
命令运行会出现固件解压失败的问题;
不使用绝对路径也会出错,需要注意一下。
运行后 FAT 能正常识别出网卡信息说明能够仿真成功,之后按回车键就可以把系统跑起来。
一个存放复现 IOT 漏洞的 docker 环境的项目,由于其中内置的 docker 比较老外加 docker 本身的限制导致调试比较困难,但是项目本身搭建环境的思想还是值得学习的。
由于 ida 对 arm 汇编进行了“美化”,因此我们通过 ida 看到的不是原生的 arm 汇编。这里我们借助 catstione 和 keystone-engine 库定义了几个辅助函数方便查看原生汇编。
from keystone import *
from capstone import *
import idc
asmer =Ks(KS_ARCH_ARM,KS_MODE_ARM)
disasmer=Cs(CS_ARCH_ARM,CS_MODE_ARM)
def disasm(machine_code, addr=0):
l = ""
for i in disasmer.disasm(machine_code, addr):
l += "{:8s} {};\n".format(i.mnemonic, i.op_str)
return l.strip('\n')
def asm(asm_code, addr=0):
l = b''
for i in asmer.asm(asm_code, addr)[0]:
l += bytes([i])
return l
def get_disasm(addr):
return disasm(idc.get_bytes(addr, idc.next_head(addr) - addr), addr)
def get_asm(addr):
return asm(get_disasm(addr), addr)
def get_code(start, end):
cur = start
code = ""
while cur < end:
code += "0x%08x %s\n"%(cur, get_disasm(cur))
cur = idc.next_head(cur)
return code
寄存器名称 | 寄存器描述 |
---|---|
R0 | 函数的第1个参数, 以及保存函数返回的结果 |
R1 - R3 | 保存函数的第2~4个参数 |
R4 – R8 | 通用寄存器,其中R7在系统调用时存储调用号 |
R9 | 平台相关 |
R10 | 通用寄存器, 可用于保存局部变量 |
R11/FP | 栈帧指针, 用于记录栈帧 |
R12 | 过程间调用, 保存函数及其调用的子函数之间的立即数 |
R13/SP | 栈指针, 指向栈顶 |
R14/LR | 链接寄存器, 用于保存子函数调用的返回地址 |
R15/PC | 程序计数器, 保存当前执行指令地址+8 |
CSPR | 当前程序状态寄存器 |
在 pwndbg
项目的 pwndbg/lib/regs.py
中可以修改寄存器显示。
arm = RegisterSet(
retaddr=("lr",),
flags={"cpsr": arm_cpsr_flags},
gpr=("r0", "r1", "r2", "r3", "r4", "r5", "r6", "r7", "r8", "r9", "r10", "r11", "r12", "lr"),
args=("r0", "r1", "r2", "r3"),
retval="r0",
)
指令 | 描述 | 指令 | 描述 |
---|---|---|---|
MOV | 移动数据 | EOR | 按位异或 |
MVN | 移动并取反 | LDR | 加载 |
ADD | 加 | STR | 存储 |
SUB | 减 | LDM | 加载多个 |
MUL | 乘 | STM | 存储多个 |
LSL | 逻辑左移 | PUSH | 入栈 |
LSR | 逻辑右移 | POP | 出栈 |
ASR | 算术右移 | B | 跳转 |
ROR | 循环右移 | BL | 跳转并将返回地址保存到LR寄存器 |
CMP | 比较 | BX | 跳转并切换Arm/Thumb模式 |
AND | 按位与 | BLX | 跳转,保存返回地址,并切换ARM/Thubmb模式 |
ORR | 按位或 | SWI/WVC | 系统调用 |
特殊情况:
在 IDA 调试中,无论是断点还是单步调试都是依靠 #UND 0x10
触发断点实现,且 IDA 调试器 无法分辨出改指令是否是自己设置的,在读取数据时会按实际情况返回数据。
该指令在 Arm 模式和 Thumb 模式的硬编码不同:
F0 01 F0 E7
10 DE
MOV PC, R0
:相当于 PC = R0
MOV R0, PC
R0 = PC + 8
R0 = PC + 4
LDR R0, [PC,#x]
MOV R0, PC
相同,即根据处于 Arm 或 Thumb 模式加上相应的偏移N,bit[31]
Z,bit[30]
C,bit[29]
V,bit[28]
T,bit[5]
T = 0
表示 Arm 模式T = 1
表示 Thumb 模式。注意:指令是否影响标志位取决于是否是否加 S 后缀(大多数情况,具体看指令硬编码第 20 位是否置 1),比如 MOV
不影响标志位但 MOVS
影响标志位。
cond 标志的值及其对应含义如下表所示:
cond 标志位位于指令硬编码的高 4 比特,在执行时根据标志寄存器决定该指令是否执行。
MOV 不访问内存,因此操作数只能是寄存器或立即数。
由于 ARM 汇编指令长度的影响,对立即数范围有严格的限制。允许 MOV 的立即数有如下几类:
MOV R0, R1,LSL#4
,该指令等同于 LSL R0, R1,#4
。MOV R0, R1,LSR R2
,该指令等同于 LSR R0, R1,R2
。ADD
:加
ADR
:PC 与操作数相加结果放入结果寄存器中ADRL
:伪指令,与 ADR
相似,不过通过类似 MOV + MOVT
的方式使得寻址范围更大CMN
:加,只影响标志寄存器SUB
:减
CMP
:减,只影响标志寄存器RSB
:反减AND
:与
TST
:与,只影响标志寄存器BIC
:第二个操作数与第三个操作数的反码逻辑与结果放在第一个寄存器中ORR
:或EOR
:异或
TEQ
:异或,只影响标志寄存器LSL
:逻辑左移LSR
:逻辑右移ASR
:算术右移ADD R0, R1, R2,LSL #4
。寄存器 ← 内存
,例:ldr r0, [pc, #8]
,ldr r3, [r5], #4
。寄存器 → 内存
,例:str r3, [r4]
。LDR R0, [R1]
LDR R0, [R1,#4]
LDR R0, [R1,#4]!
[R1 + 4]
赋值给 R0 ,然后将 R1 的值设为 R1 + 4
。LDR R0, [R1],#4
[R1]
赋值给 R0 ,然后将 R1 的值设为 R1 + 4
。注意:外偏移和内偏移不能同时存在。
根据附加附加行为性质可知:
PUSH R0
相当于 STR R0, [SP,#-4]!
POP R0
相当于 LDR R0, [SP],#4
LDM\STM+后缀 寄存器(!), {寄存器组}
{R0-R4}
;也可指定具体寄存器,比如 {R0,R2,R3}
。但是读写操作是按照寄存器下标的顺序依次操作寄存器组中的寄存器(编号小的在低地址),因为指令对应硬编码无法体现出寄存器组的顺序。根据不同后缀类型的性质可以确定如下用法:
指令编码的立即数为目标地址与 PC 寄存器的差值除 4 。由于涉及读 PC 寄存器,因此根据当前模式要加上相应的偏移。跳转范围为 PC 值加上正负 32M 的偏移。
下一条指令的地址 | T 标志位
下一条指令的地址 | T 标志位
下一条指令的地址 | T 标志位
BX reg
和 MOV PC, reg
的区别:BX 可以切换模式,MOV 不能切换模式。LDR PC, [R0]
:可以做模式切换,常用于 PLT 表中调用 GOT 表中对应的函数地址(最低位表示模式)。LDMFD SP!, {R11,PC}
:同样可以做模式切换,常与 STMFD SP!, {R11,PC}
一起用于函数调用时保存栈帧和返回地址。IT 指令的 mask 编码为从高到低,T 为 0,E 为 1 ,最后填一个 1 表示结束。比如 ITTEE EQ
的 mask 的二进制形式为 0111
。
函数示例:
0x00010570 push {fp, lr}; ; 保存 FP 和 LR 寄存器,其中 FP 寄存器在栈顶。
0x00010574 add fp, sp, #4; ; FP 寄存器指向保存返回地址的位置。
0x00010578 sub sp, sp, #0x20; ; 抬升栈顶,开辟栈空间。
0x0001057C sub r3, fp, #0x24; ; R3 寄存器指向局部变量 char s[0x24] 开头,也就是栈顶。
0x00010580 mov r2, #0x20; ; memset 参数3
0x00010584 mov r1, #0; ; memset 参数2
0x00010588 mov r0, r3; ; memset 参数1
0x0001058C bl #0x10410; ; 调用 memset@plt
0x00010590 ldr r0, [pc, #0x40]; ; 由于是 ARM 模式,因此是取 0x00010590 + 8 + 0x40 = 0x000105D8 地址处的值。考虑到 ARM 的访存能力,编译器会为每个函数创建一个地址表来记录全局变量的地址。
0x00010594 bl #0x103d4; ; 调用 puts@plt
0x00010598 ldr r0, [pc, #0x3c];
0x0001059C bl #0x103d4;
0x000105A0 ldr r0, [pc, #0x38];
0x000105A4 bl #0x103d4;
0x000105A8 ldr r0, [pc, #0x34];
0x000105AC bl #0x103bc;
0x000105B0 sub r3, fp, #0x24; ; R3 寄存器指向局部变量 char s[0x24] 开头。
0x000105B4 mov r2, #0x38; ; read 第 3 个参数
0x000105B8 mov r1, r3; ; read 第 2 个参数
0x000105BC mov r0, #0; ; read 第 1 个参数
0x000105C0 bl #0x103c8; ; 调用 read@plt
0x000105C4 ldr r0, [pc, #0x1c];
0x000105C8 bl #0x103d4;
0x000105CC mov r0, r0;
0x000105D0 sub sp, fp, #4; ; SP = FP - 4 ,即指向了栈上保存 FP 寄存器的值的位置。
0x000105D4 pop {fp, pc}; ; 恢复 FP 和 PC 寄存器。
.text:000105D8 B0 06 01 00 off_105D8 DCD aForMyFirstTric ; DATA XREF: pwnme+20↑r
.text:000105D8 ; "For my first trick, I will attempt to f"...
.text:000105DC 10 07 01 00 off_105DC DCD aWhatCouldPossi ; DATA XREF: pwnme+28↑r
.text:000105DC ; "What could possibly go wrong?"
.text:000105E0 30 07 01 00 off_105E0 DCD aYouThereMayIHa ; DATA XREF: pwnme+30↑r
.text:000105E0 ; "You there, may I have your input please"...
.text:000105E4 90 07 01 00 off_105E4 DCD format ; DATA XREF: pwnme+38↑r
.text:000105E4 ; "> "
.text:000105E8 94 07 01 00 off_105E8 DCD aThankYou ; DATA XREF: pwnme+54↑r
.text:000105E8 ; "Thank you!"
对应反编译代码如下:
int pwnme()
{
char s[36]; // [sp+0h] [bp-24h] BYREF
memset(s, 0, 0x20u);
puts("For my first trick, I will attempt to fit 56 bytes of user input into 32 bytes of stack buffer!");
puts("What could possibly go wrong?");
puts("You there, may I have your input please? And don't worry about null bytes, we're using read()!\n");
printf("> ");
read(0, s, 0x38u);
return puts("Thank you!");
}
MIPS 有 32 个通用寄存器 (General purpose registers),以美元符号 ($) 表示。可以表示为 $0~$31,也可以用寄存器名称表示如,$sp、 $t9 、$fp 等等。
寄存器 | 名称 | 用途 |
---|---|---|
$0 | $zero | 常量0 |
$1 | $at | 保留给汇编器 |
$2-$3 | $v0-$v1 | 函数调用返回值 |
$4-$7 | $a0-$a3 | 函数调用参数 |
$8-$15 | $t0-$t7 | 暂时使用,不需要保存恢复 |
$16-$23 | $s0-$s7 | 使用需要保存和恢复 |
$24-$25 | $t8-$t9 | 暂时使用,不需要保存和恢复,$t9 通常与调用函数有关 |
$28 | $gp | 全局指针,用来充当寄存器间接寻址时的基址寄存器 |
$29 | $sp | 堆栈指针 |
$30 | $fp | 栈帧指针 |
$31 | $ra | 返回地址 |
pwndbg 中的 S8
寄存器对应的是 $fp
,而 pwndbg 中的 FP
寄存器貌似是虚拟出来用来表示栈底的,大概就是始终指向每次进一个函数时的栈顶位置。
修改一下 pwndbg/lib/regs.py
中寄存器的定义,让 $ra
寄存器也能显示出来。
# http://logos.cs.uic.edu/366/notes/mips%20quick%20tutorial.htm
# r0 => zero
# r1 => temporary
# r2-r3 => values
# r4-r7 => arguments
# r8-r15 => temporary
# r16-r23 => saved values
# r24-r25 => temporary
# r26-r27 => interrupt/trap handler
# r28 => global pointer
# r29 => stack pointer
# r30 => frame pointer
# r31 => return address
mips = RegisterSet(
frame="fp",
retaddr=("ra",),
gpr=(
"v0",
"v1",
"a0",
"a1",
"a2",
"a3",
"t0",
"t1",
"t2",
"t3",
"t4",
"t5",
"t6",
"t7",
"t8",
"t9",
"s0",
"s1",
"s2",
"s3",
"s4",
"s5",
"s6",
"s7",
"s8",
"gp",
"ra",
),
args=("a0", "a1", "a2", "a3"),
retval="v0",
)
MIPS32 架构还定义了 3 个特殊寄存器,分别是 $PC(程序计数器)、$HI (乘除结果高位寄存器)和 $LO (乘除结果低位寄存器)。在进行乘运算的时候,$HI 和 $LO 保存乘法的结果,其中 $HI 保存高 32 位,$LO 保存低 32 位。而在除法运算中,$HI 保存余数,$LO 保存商。
MIPS 的运算指令有 ADD ,SUB,MULT,DIV,AND,OR,XOR 等,运算指令有如下特征:
add $d, $s, $t
表示 $d = $s + $t
。add $d, $s, $t
。addi $t, $s, imm
。addu $d, $s, $t
。addiu $t, $s, imm
。另外移位操作 SL(左移),SR(右移)后面必须跟后缀描述操作类型等,以 SR 为例有如下形式:
sra $d, $t, h
。srl $d, $t, h
。srlv $d, $t, $s
。与 X86 和 ARM 架构不同,MIPS 没有状态寄存器,条件判断有专门的指令,并将判断结果存放到指定的寄存器中,这与高级语言比较相似。条件判断指令的形式为 SLT+描述操作数类型的后缀。
以 SLT(Set on less than)为例,语法为 slt $d, $s, $t
,表示如果 $s < $t 则将 $d 置 0 ,否则置 1 。
跳转指令的操作码长度为 6 比特,因此后面的 26 比特的操作数描述了跳转的地址。因为 MIPS 的指令长度为 4 字节,即每条指令的低 2 位始终为 0,也就是说 26 比特的操作数描述了28 比特长度的地址。因此 MIPS 的地址计算方式为:跳转地址 = (当前指令的起始地址 & 0xF0000000)|(操作数 << 2)
。
比如下面这段汇编:
.text:00400728 register_tm_clones:
...
.text:00400824 CA 01 10 08 j register_tm_clones
跳转指令 j 对应的操作数为 0x081001CA & 0x3FFFFFF
,因此跳转地址为 ((0x081001CA & 0x3FFFFFF) << 2)|(0x00400824 & 0xF0000000) = 0x00400728
。
从上面的描述可知,立即数的跳转方式只能跳转 256M 范围的地址区域,要想跳转 32 位的地址范围需要使用寄存器存储跳转地址。
跳转指令有如下几种:
j target
。jal target
。jr $s
。条件跳转指会根据条件判断是否跳转。对于 3 个操作数的条件跳转指令,计算机会根据前两个操作数是否满足条件来决定是否跳转,跳转的偏移由第 3 个操作数决定,跳转的计算方式是:跳转地址 = 下一条指令的起始地址 + (第3个操作数 << 2)
,其中第 3 个操作数为指令的低 16 比特。
例如下面这段汇编指令:
.text:00400700 07 00 44 10 beq $v0, $a0, locret_400720
.text:00400704 42 00 1C 3C lui $gp, 0x42 # 'B'
...
.text:00400720 locret_400720:
第 3 个操作数为 0x00404410 & 0xFFFF
而下一条指令起始地址为 0x00400704
,因此如果满足 $v0 = $a0,则跳转地址为 0x00400704 + (0x00404410 & 0xFFFF) = 0x00400720
。
条件跳转指令有如下几种:
beq $s, $t, offset
。bne $s, $t, offset
。bgez $s, offset
。bgezal $s, offset
。bgtz $s, offset
。blez $s, offset
。bltz $s, offset
。bltzal $s, offset
。MIPS 作为一种 load-store 架构,意味着当我们要访问内存必须要通过加载和存储指令才能访问。所有其他指令(add,sub,mul,div 等等)必须从寄存器获取其操作数并将其结果存储在寄存器中。也就是说除了专门读写内存的指令,其他指令的操作数只能是立即数或寄存器。
MIPS 的访存指令有 l(LOAD)和 s(STORE)两类,二者的数据流向分别是寄存器到内存和内存到寄存器。访存指令后面会跟一个后缀描述存取的数据长度,例如 b(Byte),w(Word)。
以 lb(Load byte)指令为例,语法为 lb $t, offset($s)
,表示的是读取内存中地址为 $s + offset 的 1 字节数据到 $t 寄存中。
除了访存外,l 指令还有其他用法,比如 LUI(Load upper immediate),语法为 lui $t, imm
,效果是 $t = imm << 16
。
mfhi $d
。mflo $d
。mov $a0, $s2
jalr strchr
mov $a0, $s0
在执行第 2 行的跳转分支时,第 3 行的 move 指令已经执行完了。故 strchr 函数参数 $a0 来自于第 3 行的 $s0,而不是第 1 行的 $s2 。在函数开头先开辟栈帧然后保存 $fp
和 $ra
,ida 在分析栈的时候按照保存上一个 $fp
的位置为基址进行分析。
.text:004008F4 addiu $sp, -0x40
.text:004008F8 sw $ra, 0x38+ret_addr($sp)
.text:004008FC sw $fp, 0x38+last_fp($sp)
.text:00400900 move $fp, $sp
在函数结束的时候恢复 $fp
和 $ra
然后跳转至 $ra
返回。
.text:004009E8 move $sp, $fp
.text:004009EC lw $ra, 0x38+ret_addr($sp)
.text:004009F0 lw $fp, 0x38+last_fp($sp)
.text:004009F4 addiu $sp, 0x40
.text:004009F8 jr $ra
由于 MIPS 没有 call 指令,也就是说无法根据内存中存储的函数地址跳转到函数上,而是需要将内存中存储的函数地址 lw
到寄存器中,然后再通过寄存器跳转到函数上。这一点与 plt 中的功能极为相似,因此 mips 架构的程序是直接从 got 表中取函数地址然后跳转到函数执行的,如果开启了延迟绑定且函数对应 got 表未修复则会跳转至 plt 表来完成修复 got 表和调用函数的工作。