函数栈帧(用汇编来剖析)
2016-06-20 23:07
316 查看
好久没写博客了,学校开始实习,找了一个极破的地方,站公交将近两个小时,一天来回就要死了,说是实习,就是变着样的培训,一点实习的意思都没有,辣鸡。
这次讲解一下C++函数调用,学了这么久C语言,肯定听说过栈(数据结构啊,地址空间的栈啊之类的),函数调用就和栈密切相关。
因为地址空间内的栈是从高地址向低地址生长的,也就是说压栈顺序靠后的反而地址比较低,栈底的地址高于栈顶的地址,下面贴上一段测试代码
这段代码的运行结果,并没有执行main函数的第二个printf,而是跑到了bug函数中执行,这是因为我修改了函数栈帧中的返回地址部分
![](http://images2015.cnblogs.com/blog/928542/201606/928542-20160620221425256-1059170365.png)
本来是打算通过linux系统来看的,但是centos7的栈帧实现似乎有些不同,同样的代码在centos7上面跑不通。
下面是反汇编
因为[b]main函数本身真的是个函数!所以在执行我们编写的程序之前操作系统需要保存当前它运行的状态,就跟函数调用很类似
00A118E0 push ebp 这句话就是把操作系统的状态压栈
00A118E1 mov ebp,esp 然后把栈底指针挪到新的位置
00A118E3 sub esp,0D8h 扩展新的栈帧,你总不能让新的栈底和栈顶挨在一起吧?
过程图我会在讲到func函数的时候给出来,更容易理解,之后的push之类的就是为了保存现场和执行前准备
这部分就是调用printf的系统调用,因为库函数更多是对操作系统调用的再一次调用(封装?的说法也可以),因为我不是很懂这部分,也就不详细解释其中_printf的系统调用究竟怎么工作了
赋值阶段,这里给了双字,所以是dword 通过指针赋值~,ptr就是指针,mov dst src就是把后面的给前面的,就是dst=src这样的
重头戏来了,这就是这次要讲述的主要部分,函数调用时候的栈帧!令人惊讶的是传的实参是放在main函数栈帧中的。我们来结合func的汇编看一下
没错了这一部分就是保存main函数的状态了,至于它保存了哪些main函数的状态,通过哪些寄存器保存的这里就不详细说明了(使用push命令的一般都是保存状态用的),刚才说的在这里上图,按步骤阅读更佳
![](http://images2015.cnblogs.com/blog/928542/201606/928542-20160620224409772-656468703.png)
![](http://images2015.cnblogs.com/blog/928542/201606/928542-20160620224552365-1271676502.png)
这是func头两步的汇编指令
分别是把[b]返回main函数的地址就是push ebp啦,压栈!,然后把栈顶指针赋值给栈底指针,就把栈底挪过来了,这就是新的栈底了!!因为main栈帧已经告一段落了
![](http://images2015.cnblogs.com/blog/928542/201606/928542-20160620224841459-356610492.png)
这就是扩展函数栈帧的方式啦,将栈顶指针往后挪动一定的位置 00A11773 sub esp,0D8h ,这里挪动了D8(16进制),剩下的部分就是保存寄存器状态了,我就不讲了
简单来说,两个栈帧的大概情况就是这样的
![](http://images2015.cnblogs.com/blog/928542/201606/928542-20160620225435584-1111648801.png)
所以很简单,我们不必通过y=100这样的语句就可以对y进行赋值改下代码就好
![](http://images2015.cnblogs.com/blog/928542/201606/928542-20160620225559819-845850399.png)
别着急!还没结束!汇编解释来了!
没看到形参对不对?就两个实参,写完了不就改了么?不对哦~
我把代码改成这样看会变,这里并没有更改之前保存的寄存器里的东西,是取得了新的部分哦
这次讲解一下C++函数调用,学了这么久C语言,肯定听说过栈(数据结构啊,地址空间的栈啊之类的),函数调用就和栈密切相关。
因为地址空间内的栈是从高地址向低地址生长的,也就是说压栈顺序靠后的反而地址比较低,栈底的地址高于栈顶的地址,下面贴上一段测试代码
#include<stdio.h> #include<stdlib.h> void bug() { printf("haha I ma a bug!!"); exit(100); } int func(int x, int y) { int *p = &x; p--; *p = (int)bug; printf("x:%d,y:%d\n", x, y); int c = 0xcccc; return c; } int main() { printf("I am main\n"); int a = 0xaaaa; int b = 0xbbbb; func(a, b); printf("I should run here\n"); return 0; }
这段代码的运行结果,并没有执行main函数的第二个printf,而是跑到了bug函数中执行,这是因为我修改了函数栈帧中的返回地址部分
![](http://images2015.cnblogs.com/blog/928542/201606/928542-20160620221425256-1059170365.png)
本来是打算通过linux系统来看的,但是centos7的栈帧实现似乎有些不同,同样的代码在centos7上面跑不通。
下面是反汇编
int main() { 00A118E0 push ebp 00A118E1 mov ebp,esp 00A118E3 sub esp,0D8h 00A118E9 push ebx 00A118EA push esi 00A118EB push edi 00A118EC lea edi,[ebp-0D8h] 00A118F2 mov ecx,36h 00A118F7 mov eax,0CCCCCCCCh 00A118FC rep stos dword ptr es:[edi] printf("I am main\n"); 00A118FE push offset string "I am main\n" (0A16CF0h) 00A11903 call _printf (0A1132Ah) 00A11908 add esp,4 int a = 0xaaaa; 00A1190B mov dword ptr [a],0AAAAh int b = 0xbbbb; 00A11912 mov dword ptr ,0BBBBh func(a, b); 00A11919 mov eax,dword ptr [b] 00A1191C push eax 00A1191D mov ecx,dword ptr [a] 00A11920 push ecx 00A11921 call func (0A11366h) 00A11926 add esp,8 printf("I should run here\n"); 00A11929 push offset string "I should run here\n" (0A16CFCh) 00A1192E call _printf (0A1132Ah) 00A11933 add esp,4 return 0; 00A11936 xor eax,eax }
因为[b]main函数本身真的是个函数!所以在执行我们编写的程序之前操作系统需要保存当前它运行的状态,就跟函数调用很类似
00A118E0 push ebp 这句话就是把操作系统的状态压栈
00A118E1 mov ebp,esp 然后把栈底指针挪到新的位置
00A118E3 sub esp,0D8h 扩展新的栈帧,你总不能让新的栈底和栈顶挨在一起吧?
过程图我会在讲到func函数的时候给出来,更容易理解,之后的push之类的就是为了保存现场和执行前准备
printf("I am main\n"); 00A118FE push offset string "I am main\n" (0A16CF0h) 00A11903 call _printf (0A1132Ah) 00A11908 add esp,4
这部分就是调用printf的系统调用,因为库函数更多是对操作系统调用的再一次调用(封装?的说法也可以),因为我不是很懂这部分,也就不详细解释其中_printf的系统调用究竟怎么工作了
int a = 0xaaaa; 00A1190B mov dword ptr [a],0AAAAh int b = 0xbbbb; 00A11912 mov dword ptr ,0BBBBh
赋值阶段,这里给了双字,所以是dword 通过指针赋值~,ptr就是指针,mov dst src就是把后面的给前面的,就是dst=src这样的
func(a, b); 00A11919 mov eax,dword ptr [b] 00A1191C push eax 联合上一句的赋值语句构成参数压栈 y=b 00A1191D mov ecx,dword ptr [a] 00A11920 push ecx 联合上一句的赋值语句构成参数压栈 x=a 00A11921 call func (0A11366h) call函数调用,把fun函数的地址call一下 00A11926 add esp,8 push了这么多不得把栈顶指针挪一挪?
重头戏来了,这就是这次要讲述的主要部分,函数调用时候的栈帧!令人惊讶的是传的实参是放在main函数栈帧中的。我们来结合func的汇编看一下
int func(int x, int y) { 00A11770 push ebp 00A11771 mov ebp,esp 00A11773 sub esp,0D8h 00A11779 push ebx 00A1177A push esi 00A1177B push edi 00A1177C lea edi,[ebp-0D8h] 00A11782 mov ecx,36h 00A11787 mov eax,0CCCCCCCCh 00A1178C rep stos dword ptr es:[edi] int *p = &x; 00A1178E lea eax,[x] 00A11791 mov dword ptr [p],eax p--; 00A11794 mov eax,dword ptr [p] 00A11797 sub eax,4 00A1179A mov dword ptr [p],eax *p = (int)bug; 00A1179D mov eax,dword ptr [p] 00A117A0 mov dword ptr [eax],offset bug (0A1127Bh) printf("x:%d,y:%d\n", x, y); 00A117A6 mov eax,dword ptr [y] 00A117A9 push eax 00A117AA mov ecx,dword ptr [x] 00A117AD push ecx 00A117AE push offset string "x:%d,y:%d\n" (0A16B3Ch) 00A117B3 call _printf (0A1132Ah) 00A117B8 add esp,0Ch int c = 0xcccc; 00A117BB mov dword ptr [c],0CCCCh return c; 00A117C2 mov eax,dword ptr [c] }
int func(int x, int y) { 00A11770 push ebp 00A11771 mov ebp,esp 00A11773 sub esp,0D8h 00A11779 push ebx 00A1177A push esi 00A1177B push edi 00A1177C lea edi,[ebp-0D8h] 00A11782 mov ecx,36h 00A11787 mov eax,0CCCCCCCCh 00A1178C rep stos dword ptr es:[edi]
没错了这一部分就是保存main函数的状态了,至于它保存了哪些main函数的状态,通过哪些寄存器保存的这里就不详细说明了(使用push命令的一般都是保存状态用的),刚才说的在这里上图,按步骤阅读更佳
![](http://images2015.cnblogs.com/blog/928542/201606/928542-20160620224409772-656468703.png)
![](http://images2015.cnblogs.com/blog/928542/201606/928542-20160620224552365-1271676502.png)
这是func头两步的汇编指令
00A11770 push ebp 00A11771 mov ebp,esp
分别是把[b]返回main函数的地址就是push ebp啦,压栈!,然后把栈顶指针赋值给栈底指针,就把栈底挪过来了,这就是新的栈底了!!因为main栈帧已经告一段落了
![](http://images2015.cnblogs.com/blog/928542/201606/928542-20160620224841459-356610492.png)
这就是扩展函数栈帧的方式啦,将栈顶指针往后挪动一定的位置 00A11773 sub esp,0D8h ,这里挪动了D8(16进制),剩下的部分就是保存寄存器状态了,我就不讲了
简单来说,两个栈帧的大概情况就是这样的
![](http://images2015.cnblogs.com/blog/928542/201606/928542-20160620225435584-1111648801.png)
所以很简单,我们不必通过y=100这样的语句就可以对y进行赋值改下代码就好
int func(int x, int y) { int *p = &x; p++; *p = 100; printf("x:%d,y:%d\n", x, y); int c = 0xcccc; return c; }
![](http://images2015.cnblogs.com/blog/928542/201606/928542-20160620225559819-845850399.png)
别着急!还没结束!汇编解释来了!
int *p = &x; 0009178E lea eax,[x] 这就是取偏移地址,取得x对于当前ebp的偏移地址 00091791 mov dword ptr [p],eax 简单赋值 p--; 00091794 mov eax,dword ptr [p] 看他把寄存器来回赋值的,其实就是将把地址减个4 00091797 sub eax,4 0009179A mov dword ptr [p],eax *p = (int)bug; 0009179D mov eax,dword ptr [p] 把函数bug的地址传过来赋值 000917A0 mov dword ptr [eax],offset bug (09127Bh) offset也是取偏移的作用还是和lea有些不同的 printf("x:%d,y:%d\n", x, y); 000917A6 mov eax,dword ptr [y] 这就不说了是个系统调用,因为我也不是很懂 000917A9 push eax 000917AA mov ecx,dword ptr [x] 000917AD push ecx 000917AE push offset string "x:%d,y:%d\n" (096B3Ch) 000917B3 call _printf (09132Ah) 000917B8 add esp,0Ch int c = 0xcccc; 000917BB mov dword ptr [c],0CCCCh 创建的局部变量位置在ebp下面~看图! return c; 000917C2 mov eax,dword ptr [c]
没看到形参对不对?就两个实参,写完了不就改了么?不对哦~
x = 10; 000A178E mov dword ptr [x],0Ah y = 10; 000A1795 mov dword ptr [y],0Ah
我把代码改成这样看会变,这里并没有更改之前保存的寄存器里的东西,是取得了新的部分哦
dword ptr [x]这个已经不是之前的eax或者是ebx了~
相关文章推荐
- c语言学习笔记32
- muduo:Singleton类,单例模式
- 六轴传感器MPU6050
- 文件属性和管理
- solr5.5.0服务启动与停止
- JSON
- Wine虚拟技术及其使用
- RAM,ROM和Flash memory等存储器的比较
- android UI TextView setText显示服务器返回数据
- bzoj2054 疯狂的馒头
- HTML5多媒体audio和video(一)
- JSON clone 实现
- [BZOJ4537] [HNOI/AHOI2016] 最小公倍数 - 分块 - 并查集
- BZOJ_1614_ [Usaco2007_Jan]_Telephone_Lines_架设电话线_(二分+最短路_Dijkstra/Spfa)
- MySQL查询数据表的Auto_Increment(自增id)
- Scroller的使用,让View随心所欲的移动
- HTML表单标签
- 将应用交付服务引入到OpenStack-【中国IC微专栏】2016.6.16
- TabHost
- PHP 浏览器禁用cookie,解决session变量不能传值