C/C++堆栈指引
来源:互联网 发布:郎咸平 离婚 知乎 编辑:程序博客网 时间:2024/04/30 13:46
转自:http://blog.sina.com.cn/s/blog_44c488680100rg11.html
Binhua Liu
Stack
Heap
局部变量存储在 stack中
debug 时,查看stack 可以知道函数的调用顺序
函数调用时传递函数,事实上是把参数压入堆栈
本文的编译环境是 VisualC/C++
本文讨论的stack,是指程序员为每个线程分配的默认 stack.而不是自己定义的stack
本文讨论的平台intelx86
1基本的知识和概念
地址是不断减小的。
执行push时,esp-4,数据拷贝到esp指向的地址。
执行pop时,esp指向的数据拷贝到 内存地址/寄存器 中,esp+4。
已将分配好空间了,而不是随着函数的执行而不断创建和销毁的);在函数退出时,整个函数帧将被销毁
。
所以引入这个概念,是因为一个函数帧的建立和清理,有些工作是由Caller完成的,有些则是由Callee完
成的。
2堆栈是如何工作的
int foo1(intm,int n)
{intp=m*n;return p;}
int foo(int a,int b)
{int c=a+1;intd=b+1; int e=foo1(c,d);return e;}
intmain()
{intresult=foo(3,4);return 0;}
用代码跟踪堆栈的建立,使用,销毁。
3堆栈的建立
应的堆栈帧已将存在在堆栈中了。
1)参数入栈
当foo函数被调用,首先,caller(main函数)把foo函数的两个参数:a=3,b=4压入堆栈。
参数入栈的顺序是由函数的调用约定(CallingConvention)决定。一般,参数都是从右往左入栈的,因此
,b=4先入栈,a=3后入栈。
2)返回入栈地址
我们知道,当函数结束时,代码要返回到上一层函数继续执行,那么,函数如何知道该返回到哪个函数的
什么位置执行呢?函数被调用时,会自动把下一条指令的地址压入堆栈,函数结束时,从堆栈读取这个地
址,就可以跳转到该指令执行了。
如果当前"callfoo"指令的地址是0x00171482,由于call指令占5个字节,那么下一个指令的地址为
0x00171487,0x00171487将被压入堆栈:
3)代码跳转到被调用函数执行
返回地址入栈后,代码跳转到被调用函数foo中执行。
到目前为止,堆栈帧的前一部分,是由caller构建的;而在此之后,堆栈的其他部分是由callee来构建。
4)ebp指针入栈
在foo函数中,首先将ebp寄存器的值压入堆栈。因为此时EBP寄存器的值还是用于main函数的,用来访问
main函数的参数和局部变量的,因此需要将它暂存在堆栈中,在foo函数退出时恢复。同时,给EBP赋于新
值。
EBP+4的地址就是函数返回值的地址,EBP+8就是函数的第一个参数的地址(第一个参数地址并不一定是
EBP+8,后文中将讲到)。因此,通过EBP很容易查找函数是被谁调用的或者访问函数的参数(或局部变量
)。
5)为局部变量分配地址
个值,直接为所有的局部变量分配空间,比如在foo函数中有ESP=ESP-0x00E4,(根据烛秋兄在其他编译
环境上的测试,也可能使用push命令分配地址,本质上并没有差别,特此说明)
奇怪的是,在debug模式下,编译器为局部变量分配的空间远远大于实际所需,而且局部变量之间的地址
不是连续的(据我观察,总是间隔8个字节)
我还不知道编译器为什么这么设计,或许是为了在堆栈中插入调试数据
6)通用寄存器入栈
用寄存器是EBX,ESI,EDI,将它们压入堆栈。
至此,一个完整的堆栈帧建立起来了。
4堆栈特性的分析
上一节中,一个完整的堆栈帧已经建立起来,现在函数可以开始正式执行代码了。本节我们对堆栈的特性
进行分析,有助于了解函数与堆栈帧的依赖关系。
不论函数在什么时候被谁调用,它对应的堆栈帧的结构也是一定的。
函数中调用foo1函数,foo1函数的堆栈帧将在foo函数的堆栈帧下方建立。
3)函数用EBP寄存器来访问参数和局部变量。我们知道,参数的地址总是比EBP的值高,而局部变量的地
址总是比EBP的值低。而在特定的堆栈帧中,每个参数或局部变量相对于EBP的地址偏移总是固定的。因此
函数对参数和局部变量的的访问是通过EBP加上某个偏移量来访问的。比如,在foo函数中,EBP+8为第一
个参数的地址,EBP-8为第一个局部变量的地址。
4)如果仔细思考,我们很容易发现EBP寄存器还有一个非常重要的特性,
了一个链表!这个特性有什么用呢,我们知道EBP+4地址存储了函数的返回地址,通过该地址我们可以知
道当前函数的上一级函数(通过在符号文件中查找距该函数返回地址最近的函数地址,该函数即当前函数
的上一级函数),以此类推,我们就可以知道当前线程整个的函数调用顺序。事实上,调试器正是这么做
的,这也就是为什么调试时我们查看函数调用顺序时总是说“查看堆栈”了。
5返回值是如何传递的
栈帧建立起后,函数的代码真正地开始执行,它会操作堆栈中的参数,操作堆栈中的局部变量,甚至在堆
(Heap)上创建对象,balabala….,终于函数完成了它的工作,有些函数需要将结果返回给它的上一层
函数,这是怎么做的呢?
的,因此caller需要从callee的函数声明就可以知道应该从什么地方取得返回值。同样的,callee不能随
便把返回值放在某个寄存器或者内存中而指望Caller能够正确地获得的,它应该根据函数的声明,按照“
约定”把返回值放在正确的”地方“。下面我们来讲解这个“约定”:
是字节、字、双字、布尔型、指针等类型,都通过EAX寄存器返回。
储高位4字节,EAX存储低位4字节。例如返回值类型为__int64或者8字节的结构体通过EAX和EDX返回。
细讲解:
6堆栈帧的销毁
栈帧的清理顺序和堆栈建立的顺序刚好相反:(堆栈帧的销毁过程就不一一画图说明了)
相同)。
用的调用约定(callingconvention )来决定的。下面的小节我们就来讲解函数的调用约定。
函数的调用约定(callingconvention)
7函数的调用约定(calling convention)
退出时,又是由谁(Caller还是Callee)来清理堆栈中的参数。有2个办法可以指定函数使用的调用约定
:
1)在函数定义时加上修饰符来指定,如
1void__thiscall mymethod();
2{
3 ...
4}
Project|ProjectProperty|Configuration Properties|C/C++|Advanced|CallingConvention,选择调
用约定(注意:这种做法对类成员函数无效)。
caller清理堆栈中的参数。这种调用约定的特点是支持可变数量的参数,比如printf方法。由于callee不
知道caller到底将多少参数压入堆栈,因此callee就没有办法自己清理堆栈,所以只有函数退出之后,由
caller清理堆栈,因为caller总是知道自己传入了多少参数。
时由callee自己清理堆栈中的参数。由于参数是由callee自己清理的,所以__stdcall不支持可变数量的
参数。
this指针通过ECX寄存器传递,函数退出时由callee清理堆栈中的参数,x86构架下this指针通过ECX寄存
器传递。同样不支持可变数量的参数。如果显式地把类成员函数声明为使用__cdecl或者__stdcall,那么
,将采用__cdecl或者__stdcall的规则来压栈和出栈,而this指针将作为函数的第一个参数最后压入堆栈
,而不是使用ECX寄存器来传递了。
8反编译代码的跟踪(不熟悉汇编可跳过)
中对堆栈的描述:
ain函数中 intresult=foo(3,4); 的反汇编:
008A147E push 4 //b=4 压入堆栈
008A1480 push 3 //a=3压入堆栈,到达图2的状态
008A1482 call foo (8A10F5h) //函数返回值入栈,转入foo中执行,到达图3的状态
008A1487 add esp,8 //foo返回,由于采用__cdecl,由Caller清理参数
008A148A mov dword ptr [result],eax//返回值保存在EAX中,把EAX赋予result变量
下面是foo函数代码正式执行前和执行后的反汇编代码
008A13F0 push ebp //把ebp压入堆栈
008A13F1 mov ebp,esp //ebp指向先前的ebp,到达图4的状态
008A13F3 sub esp,0E4h //为局部变量分配0E4字节的空间,到达图5的状态
008A13F9 push ebx //压入EBX
008A13FA push esi //压入ESI
008A13FB push edi //压入EDI,到达图7的状态
008A13FC lea edi,[ebp-0E4h] //以下4行把局部变量区初始化为每个字节都等于cch
008A1402 mov ecx,39h
008A1407 mov eax,0CCCCCCCCh
008A140C rep stos dword ptr es:[edi]
...... //省略代码执行N行
......
008A1436 pop edi //恢复EDI
008A1437 pop esi //恢复ESI
008A1438 pop ebx //恢复EBX
008A1439 add esp,0E4h //回收局部变量地址空间
008A143F cmp ebp,esp //以下3行为Runtime Checking,检查ESP和EBP是否一致
008A1441 call @ILT+330(__RTC_CheckEsp)(8A114Fh)
008A1446 mov esp,ebp
008A1448 pop ebp //恢复EBP
008A1449 ret //弹出函数返回地址,跳转到函数返回地址执行
- [C/C++]堆栈指引
- C/C++堆栈指引
- C/C++堆栈指引
- C/C++堆栈指引
- C/C++堆栈指引
- C/C++堆栈指引
- C/C++堆栈指引
- C/C++堆栈指引
- C/C++堆栈指引
- C/C++堆栈指引
- C/C++堆栈指引
- 浅谈C/C++堆栈指引
- 浅谈C/C++堆栈指引——C/C++堆栈
- C/C++堆栈指引(作者:Binhua Liu)
- C/C++堆栈指引(X86)
- C/C++堆栈指引(函数调用框架)
- 浅谈C/C++堆栈指引——C/C++堆栈很强大
- 浅谈C/C++堆栈指引——C/C++堆栈很强大
- Sicily 1825 Nickname
- java学习起步
- 修复IIS和ASP.Net授权
- Sicily 2015 A New Year Gift
- 学习OpenGLProgrammingGuide7thEdition有感-OpenGl中的全局及局部坐标系统理解
- C/C++堆栈指引
- 12-1-27进步一小点
- JAVA 打豆豆辅助工具
- shell 变量 & 环境变量 (bash)
- PE结构导出表详细解析
- 基于python的中文词频分析
- [CopySilent]纪念本菜当年第一个Win32程序
- Vim+cscope+ctags+tags阅读源代码
- 清除VS2008中最近的项目与最近的文件