使用到的工具
- VC6.0(观察寄存器变化)
- Excel(画堆栈图)
函数定义
函数定义的格式如下:
返回类型 函数名(参数列表)
{
功能
return;
}
例子:
int plus(int,x int,y)
{
return x+y;
}
int代表的是字节宽度,除int外,还有两个常使用的变量类型
变量类型 | 数据宽度 |
---|---|
int | 4个字节 |
short | 2个字节 |
char | 1个字节 |
画堆栈图
int plus(x,y)
{
return x+y;
}
void main() //程序入口
{
plus(1,2); //函数调用
return; //执行结束
}
就从上面这个程序来说,它究竟是怎么执行的呢?我们下个断点,追一下程序内存的变化,就像这样:
这里一定要记住两个寄存器的变化,一个是ESP栈顶寄存器,一个是EBP栈底寄存器,我们现在还没有运行,先记录一下栈顶:
接着看一下汇编指令,在这里可以看到,参数的传递有两个特征:第一是从右向左传递,第二是使用push指令在汇编中实现。
push 2
接着画图,这行指令执行完堆栈会发生什么变化呢?push 2,向堆栈中压入参数2,栈顶指针ESP的值-4,如图:
接着执行,push 1向堆栈压入参数1,esp的值-4。
push 1
接着往下走,这里push(1,2);对应这call,也就是说call指令就代表着函数调用,哪这个call指令执行以后要不要改堆栈?要,它会修改esp的的值-4,并将它下一行的地址压入栈顶。
接着F11跟进去看一下堆栈的变化,是不是与我们画的图返回的结果相同?
接着往下走,这里的这个jmp是VC6自己生成的,是它的特点,并不是直接跳到函数的指令那里,而是通过一个jmp来跳转,我们知道jmp是无条件跳转,不影响堆栈,所以跳就行了。
jmp plus(00101020)
接着往下看,这里它push了一个ebp,那么运行后堆栈的变化就是,向堆栈中压入ebp的值,栈顶指针esp的值-4,我们接着画一下。
push ebp
运行看一下结果是不是跟我们画的相同。
接着往下走,这里使用mov ebp,esp来提升堆栈,进行ebp寻址。
mov ebp,esp
在堆栈中应该是这样反应的。
我们运行一下看看结果:
接着往下走,这里esp的值要-40来提升堆栈,为这个函数的运行腾出空间,这里提升了40个堆栈,我们的堆栈一格是4个字节,这里需要进制转换,40转换为10进制是64,64÷4是16,也就是说我们要提升16个格,在堆栈中应该是这样显示:
sub esp,40h
单步一下看看结果:
接着往下看,这里push了三个寄存器,这其实可以理解为“备份”,因为程序后续运行可能会覆盖掉寄存器的值,但又会用到它原先的值,这样被覆盖了程序就出错了,我们画一下它的运行后的堆栈图。
push ebx
push esi
push edi
单步一下看看结果是不是跟我们画的一样。
缓冲区
在本程序中,从0012FF20到0012FEE4这一块内存,就是程序的缓冲区。
当前的函数在执行过程中,它需要用内存,那么它就会提升堆栈,自己给自己分配一块内存,这块内存,就是所谓的缓冲区。
接着往下走,可以看到这4行指令
lea edi,[ebp-40h]
mov ecx,10h
mov eax,0CCCCCCCCh
rep stos dword ptr [edi]
我们一行一行的分析,首先第一行,lea指令的意思就是,将源操作数给出的有效地址传送到指定的的寄存器中。
也就是说,这一行的意思就是,把ebp地址减去40的结果赋给edi,在堆栈中就是这样显示:
第二、三行的意思不必多说,把10赋给ecx,把CCCCCCCC赋给eax,第四行,stos就是把eax的值放到edi的位置,结合rep指令重复执行,这里重复执行多少次看的是ecx的值,这里重复10次换算过来刚好是16次。总结一下就是,把缓冲区的值,全部换成4个CC。
这里的CC可以理解为下的断点,程序一遇到CC它就会停止运行,这样就避免程序自己运行时的溢缓冲区出了。
看一下它在堆栈中的结果:
我们运行一下程序看看结果是否相同:
进行计算
接着往下看,这里的两行指令就是进行1+2的运算了
mov eax,dword ptr [ebp+8]
add eax,dword ptr [ebp+0Ch]
第一行指令:把ebp+8的值放到eax里
第二行指令:令eax与ebp+c的值相加,结果返回给eax
那么此时eax里边的值应该是3
看看堆栈图:
运行一下程序看看结果:
恢复堆栈
接着是三个出栈操作:
pop edi //把栈顶的值取出赋给edi。esp+4
pop esi //把栈顶的值取出赋给esi,esp+4
pop ebx //把栈顶的值取出赋给ebx,esp+4
这三个操作刚好对应了之前的压栈操作,这时候esp的值应该+c,我们看一下它在堆栈中的结果:
运行程序看看结果:
继续往下走,这里它把ebp的值赋给了esp
mov esp,ebp
这行代码执行完就意味着,esp=ebp,我们画一下堆栈图:
运行一下程序看看结果:
继续往下走,又是一个出栈操作,把当前栈顶的值赋给ebp,栈顶指针esp的值+4
pop ebp
我们画一下程序运行后的堆栈图:
运行一下程序看看结果是否相同:
程序结束
最开始运行前程序的操作是提升堆栈,现在程序即将执行完毕它就开始慢慢的恢复堆栈,这一波操作就是汇编中的堆栈平衡。
接着程序执行完毕,ret这一行指令的意思是:把ESP当前堆栈中的值赋给EIP,所以ret也等于pop eip
ret
画一下它在堆栈中的变化:
运行一下程序,所有的变化都跟我们分析的一摸一样:
最后的这一行指令是为了保持堆栈平衡,也就是函数执行前堆栈是什么样子,执行后堆栈就恢复到什么样子,堆栈平衡有内平栈与外平栈两种方法,这里就是采用的外平栈。
add esp,8 //esp的值+8,结果返回到esp中
画一下堆栈图:
运行一下程序,完全一致:
到这里,整个程序就执行完毕了,这里有两点变化:
- EAX的值发生了变化,储存了程序计算的结果。
- 堆栈中多了很多“垃圾”,这些如果是有价值的信息,是非常值得黑客去挖掘的。
好了,到这里我们就分析完毕了,剩下的指令就是程序刚开始main()函数生成的代码,这里我们就没必要再去跟了,因为我们已经分析了整个函数从传参、调用、执行、结束的全部过程了。
总结
- C语言中的参数传递:堆栈传参,从右到左。
- C语言中,返回值存储在寄存器EAX中。
- C语言中,参数传递用PUSH,函数调用用CALL。