目录
前言
对于堆栈图不够了解的话,可以先观看这篇文章《函数调用底层堆栈图》。此外,对于编译器最好使用VC++6。因为我当前使用的是win11操作系统,所以只能使用vs2010进行讲解。对于vs2010和VC++6对于局部变量在内存中的处理是不一样的,本篇文章以VC++6为标准。
代码准备
测试代码test01.cpp:
int pluso(int x, int y) // plus内嵌函数
{
return x+y;
}
int plus(int x, int y, int z) // plus调用pluso函数
{
int m = pluso(x,y);
return m+z;
}
int main() // 主函数,程序入口
{
__asm mov eax,eax; // 无用的汇编代码,用来设置断点
plus(1,2,3); // 调用plus,plus再调用pluso
return 0;
}
在__asm关键字处加上断点,然后运行,再使用ALT+8转到反汇编窗口即可(这里提示一下,每次编译还是尽量使用ctrl+alt+7因为这样是重新生成的。如果不重新生成,有的时候汇编代码会很乱)。
还是和以前一样,一行指令对应一个堆栈图。
堆栈图也是和以前一样,使用excel表。
绘制堆栈图
堆栈图初始状态:
先看plus调用函数的实现:
push 3:
push 2:
push 1:
这里最好做一下记号,之后平栈的时候好比较:
call plus (0AF110Eh):
接下来看函数内部实现:
push ebp:
mov ebp,esp:
sub esp,0CCh:
push ebx:
push esi:
push edi:
lea edi,[ebp-0CCh]:
该步骤是将提升堆栈后的那个地址存放在edi寄存器中,堆栈无变化
mov ecx,33h:
ecx中存放十六进制的33,堆栈无变化
mov eax,0CCCCCCCCh:
eax中存满断点0xCC,堆栈无变化
rep stos dword ptr es:[edi]:
该步骤是:重复ecx次将eax中的断点赋值给从edi中的值开始的地址。
上面几步执行后堆栈的变化:
接下来看函数中嵌套调用函数的底层原理:
mov eax,dword ptr [y]: 这里如果使用vc6的话,y会显示具体值。
这条指令是将y地址中的值取出来放在eax寄存器中。堆栈无变化。
我们知道x=1,y=2,z=3。在我们上面画的堆栈图里找到2。他的位置应该是ebp+C,因为从右往左传参,所以ebp+C如果是2,那么ebp+8应该是1,ebp+10就是3。自己可以用寄存器窗口中的ebp加上8或者C或者10,然后再去内存窗口中查看结果是否对的上。(注意这里的10是十六进制的)。
0075F8B0+8 = 0075F8B8:
我们继续。
push eax:
ecx,dword ptr [x]:
push ecx:
上面这两步就是把x(ebp+8)取出来然后push到堆栈中。
通过第一次调用plus函数的时候,我们发现函数的参数就是靠push传参的。所以这里两次push就是把x,y传给函数pluso。
堆栈变化:
call pluso (0AF10A0h):调用函数
pluso函数的代码:
push ebp:
mov ebp,esp:
sub esp,0C0h:
push ebx;push esi;push edi:
lea edi,[ebp-0c0h];
mov ecx,30h;
mov eax,0CCCCCCCCh;
rep stos dword ptr es:[edi]:
mov eax,dword ptr [x]:(这是vs2010显示的汇编)
如果是VC6,应该是这样的:
mov eax,dword ptr [ebp+8]:(拿出第一个参数)
当前ebp:
ebp+8 = 0075f7d0
查看0075f7d0:
所以这一步把"1"取出来了放在了eax里。
add eax,dword ptr [y]:
这一步是把y的值取出来与eax里的值相加再放入eax中。
pop edi;
pop esi;
pop ebx:
mov esp,ebp:
pop ebp:
ret:
平栈区操作:
add esp,8:外平栈
mov dword ptr [m],eax:
将x+y存放在eax中的值,赋值给m这个地址。m就是plus函数中定义的局部变量,局部变量一定是在缓存区中的,但是在哪里就不知道了,因为每个处理器的处理方式不一样。
上面这两行是将m中的值再取出来放到eax中,然后将上z的值,最终得到的结果存放在eax中,因为eax寄存器一般都用来存放返回值。
pop edi;
pop esi;
pop ebx; //这是将plus函数之前备份的寄存器出栈
add esp,0cch:
cmp ebp,esp;
call @ILT+305()(0AF1136h)
这两条指令是一起的。cmp指令会判断esp与ebp的值。
如果值相等,调用0AF1136h这个地址的函数就不会出错,程序继续执行
如果值不相等,函数就会报错。
mov esp,ebp:没什么用
pop ebp:
ret:
add esp,0ch:
xor eax,eax:将eax置零:
下面的指令就是主函数的出栈了,主函数也只不过是一共普通的函数,所以出栈也没什么太大的区别。
局部变量为什么一定要赋初始值?
使用vs的朋友可能不容易发现,因为变量的地址,在vs中都被变量名代替了。但是使用VC6的朋友可以发现:函数中的局部变量都是定义在缓冲区中的
这就导致一个什么样的问题呢?
仔细看看我们绘制的堆栈图,最后函数返回的时候,我们一直在将esp和ebp返回到使用函数前的地址。但是!!!我们并没有清空缓冲区啊。也就是说我们函数调用的时候,提升了堆栈、使用了堆栈,但是函数返回的时候并没有清空堆栈啊。如果再次调用其他的函数,我们再一次提升堆栈的时候,这个时候定义的局部变量,他的地址有可能会使用到以前使用过的地址,这样的话如果局部变量不赋初始值,很有可能导致使用的值是一个垃圾或者根本无法使用这个值。
那么全局变量为什么不需要赋初始值?
我们之前讲过全局变量拥有独一无二的地址,如果不重新编译,那么无论运行多少次他都是那个地址,所以不用担心。
总结
关于函数嵌套调用底层,其实就是重复函数调用的那几条指令,无非就是传参、调用、提升堆栈、操作、返回。主函数也是个普通的函数啊,所以主函数中调用了其他的函数,比如plus,这也是函数嵌套调用,只不过我们没有从主函数一开始就观察他的汇编代码,如果你去观察主函数的汇编代码,你会发现也就是那么几个动作,没什么太大的区别。
结语
还是感谢大家的观看!另外,如果将的有错,请大佬一定指出。如果有听不懂的地方,也请问我。谢谢大家!