进行反汇编尝试-基于简单的C程序
使用Linux系统进行汇编,在Linux中使用vim编写程序,学过C语言的应该都对下面这段程序不陌生,这段程序定义了三十函数,函数名分别为g,f与main,在g函数中我们输入局部变量x的值,返回的是局部变量x+6,在f函数,返回的是g函数得到的值,在main函数中返回的是f函数的值(这个f 函数中局部变量已经指定为17)加上1。
int g(int x){
return x+6;
}
int f(int x){
return g(x);
}
int main(){
return f(17)+1;
}
接下来要进行编译操作,使用如下指令即可,在这里 -S指的是编译.c文件使其变为.s(汇编)文件,最后加上 -m32是因为我的Linux系统是64位,要编译其为32位的汇编程序。
gcc -S com_test.c -o com_test.s -m32
在Linix下查看生成的.s文件,可以cat com_test.s来查看,得到的结果如下:
.file "com_test.c"
.text
.globl g
.type g, @function
g:
.LFB0:
.cfi_startproc
pushl %ebp
.cfi_def_cfa_offset 8
.cfi_offset 5, -8
movl %esp, %ebp
.cfi_def_cfa_register 5
call __x86.get_pc_thunk.ax
addl $_GLOBAL_OFFSET_TABLE_, %eax
movl 8(%ebp), %eax
addl $6, %eax
popl %ebp
.cfi_restore 5
.cfi_def_cfa 4, 4
ret
.cfi_endproc
.LFE0:
.size g, .-g
.globl f
.type f, @function
f:
.LFB1:
.cfi_startproc
pushl %ebp
.cfi_def_cfa_offset 8
.cfi_offset 5, -8
movl %esp, %ebp
.cfi_def_cfa_register 5
call __x86.get_pc_thunk.ax
addl $_GLOBAL_OFFSET_TABLE_, %eax
pushl 8(%ebp)
call g
addl $4, %esp
leave
.cfi_restore 5
.cfi_def_cfa 4, 4
ret
.cfi_endproc
.LFE1:
.size f, .-f
.globl main
.type main, @function
main:
.LFB2:
.cfi_startproc
pushl %ebp
.cfi_def_cfa_offset 8
.cfi_offset 5, -8
movl %esp, %ebp
.cfi_def_cfa_register 5
call __x86.get_pc_thunk.ax
addl $_GLOBAL_OFFSET_TABLE_, %eax
pushl $17
call f
addl $4, %esp
addl $1, %eax
leave
.cfi_restore 5
.cfi_def_cfa 4, 4
ret
.cfi_endproc
.LFE2:
.size main, .-main
.section .text.__x86.get_pc_thunk.ax,"axG",@progbits,__x86.get_pc_thunk.ax,comdat
.globl __x86.get_pc_thunk.ax
.hidden __x86.get_pc_thunk.ax
.type __x86.get_pc_thunk.ax, @function
__x86.get_pc_thunk.ax:
.LFB3:
.cfi_startproc
movl (%esp), %eax
ret
.cfi_endproc
.LFE3:
.ident "GCC: (Debian 10.3.0-9) 10.3.0"
.section .note.GNU-stack,"",@progbits
实在是让人眼花缭乱,其实对我们真正有用的只是汇编指令,所以我们要删除最前面带有".“的语句,使用如下命令,其中sed命令是可以利用脚本来处理文本文件,这里 -i是实际修改文件,[.]是找到文件中所有带”."的语句,d是删除操作。
sed -i '/[.]/d' com_test.s
执行语句后再次查看.s文件,发现简略了很多,已经剩下了对我们有用的信息,其实带有
G
L
O
B
A
L
O
F
F
S
E
T
T
A
B
L
E
变
量
的
行
也
可
以
省
略
,
_GLOBAL_OFFSET_TABLE_变量的行也可以省略,
G?LOBALO?FFSETT?ABLE变?量的行也可以省略,_GLOBAL_OFFSET_TABLE_是指全局变量偏移表。
g:
pushl %ebp
movl %esp, %ebp
addl $_GLOBAL_OFFSET_TABLE_, %eax
movl 8(%ebp), %eax
addl $6, %eax
popl %ebp
ret
f:
pushl %ebp
movl %esp, %ebp
addl $_GLOBAL_OFFSET_TABLE_, %eax
pushl 8(%ebp)
call g
addl $4, %esp
leave
ret
main:
pushl %ebp
movl %esp, %ebp
addl $_GLOBAL_OFFSET_TABLE_, %eax
pushl $17
call f
addl $4, %esp
addl $1, %eax
leave
ret
movl (%esp), %eax
ret
汇编基础知识
有如上的汇编代码还是很懵逼,因为看不懂啊,本人才疏学浅,为了看懂这一段汇编代码所以必须去恶补这方面的知识,如果说的不对,欢迎大佬们指正。(以下说的都是x86架构下 CPU中寄存器,均为32bit,所以这也是我们之前汇编使汇编代码为32bit的原因)
X86 CPU早期只有八个寄存器
eax:是累加器,是加法与乘法的缺省(我理解为系统自动完成而不需人工干预)寄存器
ebx:基址寄存器,存放基地址
ecx:计数器
edx:放除法操作得到的余数
ebp:基址指针寄存器,含有一个指针,其中这个指针指向系统栈最上面的栈帧的栈底(帧指针)
esp:栈顶指针寄存处,存在一个指针,其中这个指针指向系统栈最上面的栈帧的栈顶,esp所在位置是内存的低地址位置,压入的数据增多会使esp的值减少,在32bit平台中,esp每次减少4-指的是int型,因为int型是4个字节(栈指针)
esi:源索引寄存器
edi:目标索引寄存器
接下来是一些操作的讲解
以下均为AT&T格式,与intel格式有些许不同
l指的是双字(相当于intel中的dword)
pushl是指压栈
movl是指将第一个操作数(寄存器的内容...)复制到第二个操作数(寄存器的内容...)
addl是指加法指令,将操作数加到累加器中并存到累加器eax中
call是调用指令,这时,程序就会去找调用的函数标签,并为该函数建立一个新的帧
leave是将栈指针指向帧指针,然后pop备份的原帧指针到%EBP,在32位汇编中相当于如下命令:
movl %ebp,%esp
popl %ebp (回到ebp的旧值)
popl是出栈
ret是执行call之前的地址
对之前的汇编代码进行分析
要理解汇编代码就要对内存模型有一定理解,学过数据结构应该都知道栈吧,栈是LIFO的结构,也就是后进先出的结构。 首先介绍下堆(heap):程序运行时,OS会自动分配一段内存,这段内存里存储着数据与程序,需要注意的是程序运行完后这段内存不会自己消失,也就是需要自己去释放内存,Java中的垃圾回收机制就是干的这个事。 栈帧(stack):是函数运行时临时占用的区域,至于帧,我的理解是就是一个一个函数体,因为是临时占用的所以运行结束后会自动回收。 详见下图(画的很丑,请见谅): 这是一段内存结构,最下面是低地址,最上面是高地址,初始esp(栈指针),ebp(帧指针)指向最上面也就是最高的地址。因为我们的函数有三个函数体(我认为可以看成三个帧,这三个帧构成一个堆栈结构-如果说的不对,请大佬们指正),分别g,f,与main但是学过C语言的都知道,最先执行的是main函数,汇编也一样,首先是main入栈,main调用了f函数,所以f函数入栈,f函数调用了g函数,所以g函数入栈。每个帧里面是这个函数体的变量等值,比如局部变量等。
需要注意的是,由于栈是从高地址开始的,所以入栈其实相当于是esp减而不是加,也就是从高地址到低地址。出栈则是相反。
接下来对整体开始分析(以下均为上面为代码,下面为分析的格式): 一开始ebp与esp均在最高位置
pushl %ebp
压入ebp,ebp的值(%ebp)是0,同时esp的值被修改,也就是esp的位置减去1(或者说四个字节)
movl %esp, %ebp
相当于esp的值赋值给ebp,相当于ebp的值也变了,变为与esp的值一样
pushl $17
即将17压入,esp会向下减1(或者说四字节),ebp不变
call f
在这里开始调用f函数,在这里需要压入的是eip(24),也就是这个call f下一句的地址这个24是行数(addl $4, %esp)
pushl %ebp
进入f函数体,执行f函数的第一句又将f的ebp压入栈中,esp减1
movl %esp, %ebp
这里esp,ebp又到了一个位置,相当于ebp向下减去2
pushl 8(%ebp)
这是变址寻址,相当于ebp向上移动2(8相当于2个字节)即退出f函数体
call g
调用g函数,eip(15)压入栈,esp又-1
pushl %ebp
这里进入了g函数,压入%ebp,%ebp的值为0,esp又向下移1
movl %esp, %ebp
将esp内容复制给ebp,相当于esp与ebp又到了同一个位置
movl 8(%ebp), %eax
还记得eax是累加器吧,这里相当于将ebp的位置加上2即向上移动2。
addl $6, %eax
这一段没啥说的,就是加法,之前eax的值为8,现在加了6值变为14。
popl %ebp
这一段需要特别注意,因为 比较难理解,对于ebp相当于回到了之前的ebp,esp会减1。
ret
esp的位置会指到之前的eip(15)那个位置(相当于回到了f),eip会回到15(行)的位置
addl $4, %esp
又是加法指令,使esp的值变为4(向上移动1)
leave(相当于有两条指令)
这里相当于撤销函数堆栈即esp和ebp的位置一样了都到了上一个%esp的位置(movl %ebp %esp)。然后ebp回到之前的%ebp位置,esp再加个1即向上移动一个位置(popl %ebp)。
ret
esp移回到eip(24)的位置,然后eip指向24(addl $4, %esp)
addl $4, %esp
esp的值加上1,相当于指针往上移1个位置
addl $1, %eax
eax的值加上1
leave
这里相当于撤销函数堆栈即esp和ebp的位置一样了都到了上一个%esp的位置(movl %ebp %esp)。然后ebp回到之前的%ebp位置,esp再加个1即向上移动一个位置(popl %ebp)。
ret
返回
movl (%esp), %eax
将esp的值赋值给eax
ret
这样形容也许很枯燥,接下来会用图的样式展示一下esp与ebp是如何移动的(图很丑,请见谅)
终于结束了。。。。眼睛都看花了,用OD或者IDA这些工具分析不行吗?
计算机如何工作
对于这个问题我想用之前做的一个笔记来做总结: 这就是冯诺依曼模型机(SISD-单指令流单数据流)的工作方式。
|