-
-
[原创]函数的栈帧结构
-
发表于: 2024-8-7 09:39 2230
-
函数调用是一个压栈/出栈的过程,在此过程中每个函数在栈中都有自己的一片内存区域,称之为”栈帧“。
在VC++6.0上,可以通过调试查看栈帧的运行过程。
之所以用6.0老版本,是因为它的内存地址是固定的,便于观察。
准备一段C代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | int func( int x, int y, int z, int m) { return x + y + z + m; } int main( int argc, char * argv[]) { int sum = 0; for ( int i = 0; i < 10; i++) { sum += i; } func(4, 5, 6, sum); return 0; } |
按F10调试暂停至main函数,打开内存窗口跳转至esp处,也就是栈指针寄存器当前指向的地方:
从esp起,第一个双字是main函数执行完毕后的返回地址(可以记一下,待会儿验证),第二个双字是参数argc,第三个参数是argv。
可以跳转到argv保存的地址处(0x00380fd0,小端序)查看一下保存了什么:
这里保存了一个地址,地址指向的就是下一个四字,这里保存的是argv[0],也就是当前可执行文件全路径。
所以,在进入main函数时,栈内存布局如下:
接下来打开反汇编窗口
前6个指令,将ebp压栈,再将当前esp的值传送给ebp,于是现在ebp作为main函数的栈底;接下来将esp减0x48,相当于在栈中开辟了72字节内存,这72字节将会用来保存main的局部变量;接下来依次压入ebx,esi,edi。
于是,现在的栈帧变成这样:
接下来的指令:
让edi指向刚刚开辟的0x48个字节,向里面填充0xcccccccc,于是这四条指令执行完毕后,这片内存区域是这样子:
在GBK编码中,0xCCCC是汉字“烫”,初学C语言时,经常在黑窗口看到“烫烫烫……”便是这个原因了。
接下来给局部变量sum和i赋值:
这里可以看到sum和i都是通过栈底ebp来定位的,这就是之前要把esp的值传送给ebp的原因,栈底是固定的,方便定位,而栈顶esp是动态变化的。
局部变量初始化完成后,这两个内存位置已置0:
接下来运行至调用func处:
先将内存地址ebp-4(变量sum)处的值传送给edx,再依次压入edx,6,5,4四个参数,然后执行call指令调用func函数,此时会将func的返回地址,也就是call指令的下一条指令地址0x004010b1压入栈:
此时的栈帧结构:
接下来是func函数:
还是熟悉的配方,再次压入ebp,将当前esp传送给ebp作为栈底,在栈中开辟0x40字节空间并初始化为0xcccccccc,于是func的栈帧结构变为:
如图,每个函数在栈中都如同胶片一样一帧一帧,故名为“栈帧”。
执行完func的代码,准备返回:
依次弹出edi,esi,ebx,再将ebp的值传送给esp,再弹出前一个ebp的值,于是栈帧结构恢复为这样:
此时esp指向了返回地址,然后执行ret指令,代码跳转回0x004010b1处执行:
因为func是C约定__cdecl,这里需要由调用者清理参数空间,所以将esp加0x10字节,然后栈帧结构变为这样:
接下来是main函数的返回过程:
先用xor指令将eax清0作为返回值,后面还是熟悉的味道,依次弹出edi,esi,ebx,再将esp加0x48字节,恢复上一个ebp,执行ret指令跳转回0x00401209处执行:
后面就是C运行时库CRT0.c中的内容了。
[招生]科锐逆向工程师培训(2024年11月15日实地,远程教学同时开班, 第51期)
赞赏
- [原创]从底层视角看面向对象 5133
- [原创]C语言的文件与缓冲区 4113
- [原创]CC1利用链分析 4407
- [原创]VC++6调试状态下的堆结构 3498
- [原创]URLDNS反序列化利用链 5139