-
-
[原创]软件调试基础--02函数调用与堆栈平衡
-
发表于:
2016-1-21 14:27
7078
-
本节用比较简单的例子,来讲述函数的调用过程和这个过程中栈内存的变化。关于各种函数调用约定之间的区别,请自行百度or Google。本文只讲解两种常见的调用约定:_cdecl和_stdcall。
1._cdecl调用约定
代码段:
执行到此处,右键转到反汇编查看add函数调用所翻译成的汇编指令,如下图:
可见,调用add之前,先将参数二压栈,再将参数一压栈,然后才是call指令。此时记录一下寄存器的值,如下图:
注意ESP的值为0031FB54,这是调用add之前的栈顶指针值。单步执行到call指令处,如下图:
再观察此时寄存器的值,如下图:
**此时,ESP的值为0031FB4C,0031FB54-0031FB4C=8*这正是两次压栈进去的参数所占用的栈空间。而且可以观察出,栈的增*长*方向是由大地址方向向小地址方向增长的。然后F11,如下图:***
Call指令,实际上跳转到了一个jmp指令所在的地址,执行jmp后,才真正进入add函数。仔细观察这个add函数上面是一系列函数的地址,那这个位置是不是有什么说法呢,我暂时给他起个名字,叫函数地址表吧,也就是说,一个可执行模块中的所有函数的地址,都会汇集在某个内存位置,形成一张表,然后call指令实际上就是跳转到这个表中的一项上,而这一项的jmp,才真正跳进被调用函数内部。
继续,F10之后,进入被调用函数add,如下图:
再次观察此时的寄存器值,如下图:
0031FB4C-0031FB48=4,这个过程栈定指针又减少了4,那这4个字节用来干什么了呢?我们看看这4个字节里的内容是什么,如下图:
00d114a7这是一个什么东西呢?稍后我们再说,暂且记下。先解释一下这个add被翻译成的反汇编含义。如下
int add(int a, int b)
{
00D11BE0 push ebp //1将ebp寄存器压入栈中
00D11BE1 mov ebp,esp //2将esp赋值给ebp
00D11BE3 sub esp,0CCh //3从栈中分配CC大小的空间
00D11BE9 push ebx //4将ebx压栈
00D11BEA push esi //5将esi压栈
00D11BEB push edi //6将edi压栈
00D11BEC lea edi,[ebp-0CCh] //7取出3中分配的CC大小栈空间的首地址
00D11BF2 mov ecx,33h //8将ecx赋值为33
00D11BF7 mov eax,0CCCCCCCCh //9将eax赋值为CCCCCCCC
00D11BFC rep stos dword ptr es:[edi] //10重复执行ecx次将eax冲内容写入edi指向位置
int c = a+b;
00D11BFE mov eax,dword ptr [a]
00D11C01 add eax,dword ptr
00D11C04 mov dword ptr [c],eax
return c;
00D11C07 mov eax,dword ptr [c] //11将函数的返回值内容放入eax寄存器
}
00D11C0A pop edi //12从栈顶弹出4字节内容,放入edi
00D11C0B pop esi //13从栈顶弹出4字节内容,放入esi
00D11C0C pop ebx //14从栈顶弹出4字节内容,放入ebx
00D11C0D mov esp,ebp //15用ebp的值恢复刚刚进入函数时esp的值
00D11C0F pop ebp //16从栈中将ebp弹出
00D11C10 ret //17函数返回
继续执行到add函数逻辑完成处,如下图:
观察此时的寄存器值,如下图:
0031FB48-0031FA6C=DC=CC(函数开始处分配)+10(函数开始时压栈的ebp、ebx、esi、edi),如此,add调用过程中,栈的每一个增长都找到了理由。下面看add逻辑执行完后继续做的事情,如下:
四次pop弹出之前四次push的寄存器,但进入add函数后栈还分配过一次CC大小的栈空间,也应该销毁啊。对,正是这个mov esp,ebp干的这件事。为了证实这件事,我们执行到mov esp,ebp这条指令处,按照我们的分析,ebp-esp应该恰好是CC才对,这样把ebp赋值给esp后,才能实现esp-CC的效果,看下图:
哈哈,0031FB44-0031FA78=CC,果然如此。其实这行mov esp,ebp指令,也可以直接替换成sub esp,0CCh嘛,一样的效果。add马上就要执行完毕了,执行到ret处,我们再来观察一下情况,如下图:
哈哈,ESP=0031FB48,回头看看我们刚刚进入add函数的时候ESP是多少呢?也是0031FB48,这便是所谓的堆栈平衡了!但是,到底为什么要堆栈平衡呢?聪明的你应该知道,我们还没有真正执行完add函数,还差最后一步,那就是ret指令!Ret指令如何知道下一步应该执行的指令所在位置呢?还记得我们刚刚记录的栈中曾经出现过一个数字不,它的值是00d114a7,这个值便是ret应该返回的位置,那它放在了什么位置上呢?哈哈,你不会想不到吧,之前我们观看过一次内存地址上的内容,不就是0031FB48处么?这个位置存着00d114a7呢!也就是说ret实际上就是拿当前esp指向的栈顶处,拿4个字节内容,当成ret返回后应该执行的下一条指令所在位置。试想一下,如果add执行完了,esp的值已经不是0031FB48,会是什么后果?可能跳转到一个未知的地址去执行了吧,如果刚好那个地址不可访问,还好,程序会崩溃掉。如果是其他人精心设计的一个值,刚好ret之后,栈顶的内容就是他设计的值,而这个值处是一些破坏性代码,那后果将不可预料,这便是栈溢出攻击的原理。
好了,我们应该继续执行,看看ret之后 又干了什么,如图:
这行代码的位置00D114A7正是我们前面压栈的地址。证明前面的猜想是对的。add esp,8这行指令,是恢复上面push 5和push 4所造成的栈开销,因为push 5和push 4所使用的栈是main函数的,而add是由main函数调用的。这样我们成add为被调用函数,main为调用者函数。_cdecl调用约定规定,栈(函数参数所占用的栈空间)由调用者自行清理,也就是在main里清理。至此,add函数被调用的全过程,就完事了。
而另一种调用约定_stdcall,与_cdecl调用约定的区别,就在于_stdcall为被调用函数自己清理堆栈。
我们再来看看调用者函数中的反汇编:
此时,add调用完后,不再有add esp,8指令了,那这8个字节是在哪里清理的呢?我们看看此时add函数的完整反汇编吧。如下图:
哈哈,正是这个ret 8,实现了add函数参数所在栈空间的释放。
总结:本节讲述函数调用过程,对后面的软件调试具有非常重要的意义,而且,作为一个开发人员,函数调用过程,如果不能烂熟于心,何以显示出我们的专业?初学者一定要多多尝试此节内容。这次使用的是VS2008观察,下次将在windbg中进行观察。
[招生]科锐逆向工程师培训(2024年11月15日实地,远程教学同时开班, 第51期)