本次的分享,我们将以一段简单的C语言程序为例,使用mips-linux的gcc编译器对其进行编译后,使用ida对编译后的程序进行反汇编,并通过对照C语言代码的方式理解MIPS汇编指令。
首先我们来看这段C代码(test.c):
#include <stdio.h>
#include <string.h>
void B() {
printf(".....\n");
}
void A(char *content) {
char buf[32];
strcpy(buf, content);
printf("buf: %s\n", buf);
B();
}
void main(int argc, char **argv) {
A(argv[1]);
}
在代码中,main函数直接调用A函数,并将第一个命令行参数作为函数A的参数传递过去,在函数A中,使用strcpy函数将参数保存到局部变量buf中,然后是使用printf函数打印buf变量。最后函数A调用了函数B,在函数B中只是简单的调用了printf函数打印了一串字符串常量。
我们使用mips-linux-gnu-gcc命令将源码编译,得到名为test的可执行文件:
$ mips-linux-gnu-gcc -static test.c -o test
编译后,使用qemu-mips命令执行这个test程序:
$ cp /usr/bin/qemu-mips-static .
$ ./qemu-mips-static ./test AAAAAAAA
buf: AAAAAAAA
.....
可以看到,我们编译的程序是可以正常运行的。
然后我们使用IDA保持一路默认打开这个test程序:
在IDA窗口左侧的Function window窗口中,我们可以看到,IDA识别出test中包含了main函数、A函数、B函数。
双击这个main函数,就可以开始进入汇编世界了:
在main函数的起始处,有4个变量定义,分别是var_8、var_4、arg_0、arg_4。为了更便于理解,我在文稿中绘制了当前main函数的栈帧图示:
main函数实际上是由__libc_start_main函数调用起来的,所以在main函数被执行后,main函数首先要为自己分配函数栈空间,对应的汇编指令就是0x00400970地址处的addiu
s
p
,
−
0
x
20
指
令
。
a
d
d
是
加
法
,
i
代
表
立
即
数
,
u
代
表
无
符
号
数
。
而
sp, -0x20指令。add是加法,i代表立即数,u代表无符号数。而
sp,−0x20指令。add是加法,i代表立即数,u代表无符号数。而sp就是栈顶指针,通过将栈顶指针向下移动0x20个字节,这0x20个字节就是main函数的函数栈空间,对应图中的绿色部分。
为了保证在main函数返回后,程序流程继续回到__lib_start_main函数中继续执行,需要将__lib_start_main函数中调用main函数完成后的下一条指令的地址保存到栈空间,也就是将RA寄存器的值存储到main函数的函数栈中,对应的汇编指令为:sw
r
a
,
0
x
20
+
v
a
r
4
(
ra, 0x20+var_4(
ra,0x20+var4(sp),sw指令是将ra寄存器的值存储到$sp+0x20-4地址处,这个地址就是变量var_4所在的内存。
类似的,汇编代码中也将fp寄存器做了同样的处理,关于这个fp寄存器,为了不与栈顶指针sp混淆,我们目前可以不必关心。
紧接着在汇编代码的0x00400980地址处,分别使用sw指令将a0、a1寄存器的值存储到了__lib_start_main函数的参数调用空间中,也就是上面图片中的arg_0、arg_4。a0和a1分别代表了main函数的两个参数,也就是argc和argv。其中arg_4代表了前面使用命令行执行test程序时传递的命令函参数,是一个字符串数组。
紧接着,使用lw指令将arg_4参数取出,并复制给v0寄存器。并在v0寄存器指向的main函数命令行参数数组中取出argv[1],也就是“AAAAAAAA”,然后将这个“AAAAAAAA”再次复制给v0寄存器,并最终赋值给a0寄存器:
上一篇的分享中,我们提到过,在MIPS架构程序做函数调用时,会将前4个参数分别使用a0~a3寄存器进行保存,所以,代码分析到这里,我们已经能够发现,为了调用函数A,汇编代码为此准备好了第一个参数,也就是a0寄存器,它对应了字符串“AAAAAAAA”。
然后在main函数中,使用了jal指令将程序流程跳转到A函数执行:
这里我们应该知道的一个关键点是,使用jal指令进行函数跳转时,会隐式的将调用函数A完成后的下一条指令的地址保存到RA寄存器中。所以RA寄存器在这里被覆盖了,所以,在main函数的末尾处应该从main函数的中空间中将RA寄存器的值进行恢复。
我们假设函数A已经执行完成,在函数A的尾部会通过RA寄存器,将程序的执行流程跳转回main函数,也就是main函数中的jal指令后面的地址处:
然后,main函数的结尾处几行汇编代码会将
s
p
、
sp、
sp、ra、$fp寄存器从main函数的栈空间中进行值恢复,然后使用jr指令,将程序的执行流程返回到__libc_start_main函数中执行。注意,这里使用的jr指令并不会改变RA寄存器的值,jal指令才会改变RA寄存器的值。
我们通过今天分享,通过将我们自己编写的C代码编译为mips-linux架构的程序,并使用IDA对其进行逆向分析,初探了MIPS汇编代码。对于繁杂的汇编代码,看似要记住的指令有很多,但它们不过是一些英文单词的缩写组合而成的。比如add代表加法、i代表Immediat,就是立即数、u代表unsigned,就是无符号、lw中的l代表load,就是加载的意思,w代表word,就是4个字节的意思、jal指令中的j代表jump,就是跳转的意思、a代表address,就是跳转到某个地址的意思、l代表long,就是跳转的地址距离jal这条指令很远的意思(这个l代表long纯属乱猜,只是为了方便自己记忆)。
希望本节的分析能够为你打开进入MIPS汇编世界的大门。下次分享中,我们会继续通过分析main函数调用的A函数的反汇编代码,以此来增强我们对汇编语言的理解。
最后希望本次的分享能够为你带来帮助,谢谢大家。