`
Michaelmatrix
  • 浏览: 206905 次
  • 来自: 北京
文章分类
社区版块
存档分类
最新评论

C/C++函数调用时的堆栈变化

 
阅读更多

深入学习的引起:csdn论坛某诡异问题
转抄如下:

  1. class Test
  2. {
  3. public :
  4. int i;
  5. int j;
  6. };
  7. int crashme(Test*t)
  8. {
  9. //??在这里干点什么能让下面的代码崩溃
  10. }
  11. int main( int argc, char **argv)
  12. {
  13. Test*t=new Test;
  14. crashme(t);
  15. t->i=4;//需要在此处崩溃
  16. return 0;
  17. }

某高手的回复:

  1. int crashme(Test*t)
  2. {
  3. /*我的注:破坏了t所指向的内容,让传进来的实参指向0*/
  4. *(int *)(*(( int *)&t-2)-4)=0;
  5. }


实验下来确实可行!
可以看出这样的答复需要提出者对函数调用过程中堆栈变化很好的领悟和丰富的经验,钦佩之余也希望乘此机会深入的了解一下在函数调用过程中堆栈是如何变化的。
相对深入地了解了一下,知识记录如下,对于上诉问题最后再给以分析:

%%%%%%%%%%%%%%%%%%%%%%分界线%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%

基础知识(此处内容很大部分转自http://www.fmddlmyy.cn/text12.html ,感谢):
1.什么是堆栈
编译器一般使用堆栈实现函数调用。堆栈是存储器的一个区域,嵌入式环境有时需要程序员自己定义一个数组作为堆栈。Windows为每个线程自动维护一个堆栈,堆栈的大小可以设置。编译器使用堆栈来堆放每个函数的参数、局部变量等信息。
函数调用经常是嵌套的,在同一时刻,堆栈中会有多个函数的信息,每个函数占用一个连续的区域。一个函数占用的区域被称作帧(frame)。
编译器是从高地址开始使用堆栈。
在多线程(任务)环境,CPU的堆栈指针指向的存储器区域就是当前使用的堆栈。切换线程的一个重要工作,就是将堆栈指针设为当前线程的堆栈栈顶地址。
不同CPU,不同编译器的堆栈布局、函数调用方法都可能不同,但堆栈的基本概念是一样的。

1.1堆栈相关寄存器:


esp:堆栈指针(stack pointer),指向系统栈最上面一个栈帧的栈顶
ebp: 基址指针(base pointer),指向系统栈最上面一个栈帧的底部
cs:eip:指令寄存器(extended instruction pointer),指向下一条等待执行的指令地址
注:ebp在C语言中用作记录当前函数调用基址。

1.2堆栈操作


push: 以字节为单位将数据(对于32位系统可以是4个字节)压入栈,从高到低按字节依次将数据存入ESP-1、ESP-2、ESP-3、ESP-4的地址单元。
pop: 过程与PUSH相反。
call: 用来调用一个函数或过程,此时,下一条指令地址会被压入堆栈,以备返回时能恢复执行下条指令。
ret: 从一个函数或过程返回,之前call保存的下条指令地址会从栈内弹出到EIP寄存器中,程序转到CALL之前下条指令处执行 。
注:
call指令的两个作用
①将下一条指令的地址A保存在栈顶
②设置eip指向被调用程序代码开始处

1.3函数堆栈框架的形成(C语言中)


①执行call XXX之前
cs : eip原来的值指向call下一条指令,该值被保存到栈顶
然后cs : eip的值指向xxx的入口地址
②进入 XXX
第一条指令: pushl %ebp //意为保存调用者的栈帧地址
第二条指令: movl %esp, %ebp //初始化XXX的栈帧地址
然后函数体中的常规操作,可能会压栈、出栈
③退出XXX
movl %ebp,%esp
popl %ebp
ret

1.4堆栈其他作用(之后会有所描述)
①参数的传递

函数调用时,调用者依次把参数压栈,然后调用函数,函数被调用以后,在堆栈中取得数据,并进行计算。函数计算结束以后,或者调用者、或者函数本身修改堆栈,使堆栈恢复原装。

在参数传递中,有两个很重要的问题必须得到明确说明:

当参数个数多于一个时,按照什么顺序把参数压入堆栈
函数调用后,由谁来把堆栈恢复原装
这两个问题,在高级语言中,通过函数调用约定来解决,详细参看本文下文“2.函数调用约定”。


②局部变量的使用

2.函数调用约定
函数调用约定包括传递参数的顺序,谁负责清理参数占用的堆栈等,如下面这个主要的函数约定表显示的 :

函数调用约定 参数传递顺序 谁负责清理参数占用的堆栈
__pascal 从左到右 调用者
__stdcall 从右到左 被调函数
__cdecl 从右到左 调用者

调用函数的代码和被调函数必须采用相同的函数的调用约定,程序才能正常运行。在Windows上,__cdecl是C/C++程序的缺省函数调用约定。
在有的cpu上,编译器会用寄存器传递参数,函数使用的堆栈由被调函数分配和释放。这种调用约定在行为上和__cdecl有一个共同点:实参和形参数目不符不会导致堆栈错误。
不过,即使用寄存器传递参数,编译器在进入函数时,还是会将寄存器里的参数存入堆栈指定位置。参数和局部变量一样应该在堆栈中有一席之地。参数可以被理解为由调用函数指定初值的局部变量。

3 例子:__cdecl和__stdcall
不同的CPU,不同的编译器,堆栈的布局可能是不同的。本文以x86,VC++的编译器为例。
VC++编译器的已经不再支持__pascal, __fortran, __syscall等函数调用约定。目前只支持__cdecl和__stdcall。
采用__cdecl或__stdcall调用方式的程序,在刚进入子函数时,堆栈内容是一样的。esp指向的栈顶是返回地址。这是被call指令压入堆栈的。下面是参数,左边参数在上,右边参数在下(指高地址,先入栈)。
如前表所示,__cdecl和__stdcall的区别是:__cdecl是调用者清理参数占用的堆栈,__stdcall是被调函数清理参数占用的堆栈。
由于__stdcall的被调函数在编译时就必须知道传入参数的准确数目(被调函数要清理堆栈),所以不能支持变参数函数,例如printf。而且如果调用者使用了不正确的参数数目,会导致堆栈错误。


通过查看汇编代码,__cdecl函数调用在call语句后会有一个堆栈调整语句,例如:


a = 0x1234;
b = 0x5678;
c = add(a, b);


对应x86汇编:


mov dword ptr [ebp-4],1234h
mov dword ptr [ebp-8],5678h
mov eax,dword ptr [ebp-8]
push eax
mov ecx,dword ptr [ebp-4]
push ecx
call 0040100a
add esp,8
mov dword ptr [ebp-0Ch],eax

__stdcall的函数调用则不需要调整堆栈:


call 00401005
mov dword ptr [ebp-0Ch],eax

函数


int __cdecl add(int a, int b)
{
return a+b;
}


产生以下汇编代码(Debug版本):


push ebp
mov ebp,esp
sub esp,40h
push ebx
push esi
push edi
lea edi,[ebp-40h]
mov ecx,10h
mov eax,0CCCCCCCCh
rep stos dword ptr [edi]
mov eax,dword ptr [ebp+8]
add eax,dword ptr [ebp+0Ch]
pop edi
pop esi
pop ebx
mov esp,ebp
pop ebp
ret // 跳转到esp所指地址,并将esp+4,使esp指向进入函数时的第一个参数


再查看__stdcall函数的实现,会发现与__cdecl函数只有最后一行不同:


ret 8 // 执行ret并清理参数占用的堆栈


对于调试版本,VC++编译器在“直接调用地址”时会增加检查esp的代码,例如:


ta = (TAdd)add; // TAdd定义:typedef int (__cdecl *TAdd)(int a, int b);
c = ta(a, b);


产生以下汇编代码:


mov [ebp-10h],0040100a
mov esi,esp
mov ecx,dword ptr [ebp-8]
push ecx
mov edx,dword ptr [ebp-4]
push edx
call dword ptr [ebp-10h]
add esp,8
cmp esi,esp
call __chkesp (004011e0)
mov dword ptr [ebp-0Ch],eax


__chkesp 代码如下。如果esp不等于函数调用前保存的值,就会转到错误处理代码。


004011E0 jne __chkesp+3 (004011e3)
004011E2 ret
004011E3 ;错误处理代码


__chkesp的错误处理会弹出对话框,报告函数调用造成esp值不正确。 Release版本的汇编代码要简洁得多。也不会增加 __chkesp。如果发生esp错误,程序会继续运行,直到“遇到问题需要关闭”。


4 补充说明

函数调用约定只是“调用函数的代码”和被调用函数之间的关系。
假设函数A是__stdcall,函数B调用函数A。你必须通过函数声明告诉编译器,函数A是__stdcall。编译器自然会产生正确的调用代码。
如果函数A是__stdcall。但在引用函数A的地方,你却告诉编译器,函数A是__cdecl方式,编译器产生__cdecl方式的代码,与函数A的调用约定不一致,就会发生错误。
以delphi调用VC函数为例,delphi的函数缺省采用__pascal约定,VC的函数缺省采用__cdecl约定。我们一般将VC的函数设为__stdcall,例如:


int __stdcall add(int a, int b);


在delphi中将这个函数也声明为__stdcall,就可以调用了:


function add(a: Integer; b: Integer): Integer;
stdcall; external 'a.dll';


因为考虑到可能被其它语言的程序调用,不少API采用__stdcall的调用约定。

%%%%%%%%%%%%%%%%%%%%%%分界线结束%%%%%%%%%%%%%%%%%%%%%%%%%%%%%

再回到最初的问题,我们可以有一个解题思路,那就是:

1.由于本题中情况的简单性,我们可以直接形参推算到实参的位置:

引用原解答如下:

int crashme(Test* t)
{
//*****
*(int*)( *( (int*)&t - 2 ) - 4 ) = 0;
}

有已有的知识我们可以知道运行到“//*****”时栈中的情况如下:

低地址 ……

主调函数的ebp值

返回的eip

形参t(指针,实参t的值拷贝,同样指向记录对象地址)

主调函数中实参t(指针,记录对象地址)

(主调函数ebp记录的地址就在此处)

高地址 ……

推算过程如下:

&t为形参参数地址,此地址正是处在栈中
(int*)&t - 1为返回的eip的地址
(int*)&t - 2为存储主调函数ebp的地址
(*((int*)&t - 2)为主调函数ebp的值
(*((int*)&t - 2) - 4)为主调函数中t的地址
*(int*) (*((int*)&t - 2) - 4) = 0;让主调函数中的t为0 ,即指针t设为NULL。

在这里会发现高手有一个问题没有考虑,那就是,new Test这个在堆中申请的空间没有释放无法再访问到,造成了内存泄露。当然这个不是这个问题的关键所在^~^。

有一个疑问依然存在的是:

在crashme中只写下delete t为什么依然能够在函数调用完毕后访问到t所指向的对象?值得进一步去了解一下。

分享到:
评论

相关推荐

    C++高效获取函数调用堆栈

    C++ 获取函数调用堆栈的 高效实现代码

    剖析C++函数调用约定

    Visual C/C++的编译器提供了几种函数调用约定,了解这些函数调用约定的含义及它们之间的区别可以帮助我们更好地调试程序。在这篇文章里,我就和大家共同探讨一些关于函数调用约定的内容。 Visual C/C++的编译器支持...

    C/C++函数调用的几种方式总结

    函数调用时,调用者依次把参数压栈,然后调用函数,函数被调用以后,在堆栈中取得数据,并进行计算。函数计算结束以后,或者调用者、或者函数本身修改堆栈,使堆栈恢复原装。 在参数传递中,有两个重要的问题必须要...

    函数调用与堆栈

    自动生存期:局部变量和函数形参一般都具有自动生存期,它们的内存空间在程序执行到定义它们的复合语句(包括函数体)时才分配,当定义它们的复合语句执行结束时内存被收回。 动态生存期:具有动态生存期的变量的生存...

    Win32环境下函数调用的堆栈之研究

    Win32环境下函数调用的堆栈之研究 由于阅读《Q版缓冲区溢出教程》的需要理解和掌握栈的相关知识,故而使用VC 6.0工具来研究win32环境下函数调用时具体的栈操作。 阅读本文建议先看结论,大概了解相关概念,再看第4...

    堆栈、栈帧与函数调用过程分析

    【应聘笔记系列】堆栈、栈帧与函数调用过程分析,C-C++堆栈指引

    通过EBP EIP来找函数调用堆栈

    通过EBP EIP来找函数调用堆栈 通过EBP EIP来找函数调用堆栈 通过EBP EIP来找函数调用堆栈 通过EBP EIP来找函数调用堆栈

    C/C++程序员面试指南.杨国祥(带详细书签).pdf

    C、C++语言是IT行业的主流编程语言,也是很多程序员必备的软件基本功,是软件开发行业招聘考查的重点。本书以流行的面试题讲解为主要内容,介绍了C、C++语言基本概念,包括保留字、字符串、指针和引用、结构体、...

    C/C++笔试题(附答案,华为面试题系列)

    答:函数模板的实例化是由编译程序在处理函数调用时自动完成的,而类模板的实例化 必须由程序员在程序中显式地指定。 16一般数据库若出现日志满了,会出现什么情况,是否还能使用? 答:只能执行查询等读操作,不能...

    C++函数调用传参与返回值深度分析

    也许你从书上了解到了C++的函数参数和返回值类型有类对象,引用,指针。 但是却不知道在内存中到底是怎么回事。本文档从内存堆栈分别揭示了这6种情况下到底在这个过程中发生了什么事情。

    基于c/c++的MFC的多线程

     在MFC程序中创建一个线程,宜调用AfxBeginThread函数。该函数因参数不同而具有两种重载版本,分别对应工作者线程和用户接口(UI)线程。  工作者线程 CWinThread *AfxBeginThread(  AFX_THREADPROC ...

    eclipse 开发c/c++

    C 和 C++ 语言都是世界上最流行且使用最普遍的编程语言, 因此 Eclipse 平台(Eclipse Platform)提供对 C/C++ 开发的支持一点都不足为奇。 因为 Eclipse 平台只是用于开发者工具的一个框架,它不直接支持 C/C++;它...

    函数调用约定与函数名称修饰规则

    这些现象通常是出现在C和C++的代码混合使用的情况下或在C++程序中使用第三方的库的情况下(不是用C++语言开发的),其实这都是函数调用约定(Calling Convention)和函数名修饰(Decorated Name)规则惹的祸。函数...

    YC++编译器网页浏览器

    在C/C++函数中执行printf后的输出文本可自动插入到HTML中。<br/>15. 用户以前编写的C/C++程序,稍加修改便可嵌入到web页面中。<br/>16. 自动检测堆栈是否溢出,资源是否泄漏。<br/>17. 可先用HTML、javascript及DOM...

    个人总结--函数堆栈调用

    自己总结了一点C,C++的资料,主要讲空类中的默认函数, 以及函数调用时栈的调用关系.

    C-C++堆栈指引

    【应聘笔记系列】堆栈、栈帧与函数调用过程分析,C-C++堆栈指引

    C++析构函数调用时间及分配对象堆与栈区别demo

    描述了C++析构函数调用时间及分配对象堆与栈区别,一个理清C++析构函数和默认系统析构函数,C++堆栈分配的原则。

    追踪谁调用了函数

    追踪谁调用了函数 堆栈追踪 StackWalk 追踪谁调用了函数 堆栈追踪 StackWalk

    利用堆栈回溯、addr2line和Graphviz生成运行时函数调用图

    现在的软件源代码动则千万行,初学者常常感到迷惘,如果能自动生成关键函数的调用关系图,则思路可以清晰许多。如下面这幅图展示了WebKit网页渲染的部分函数执行过程,比单纯地看代码直观多了。 ...

    c/c++ 学习总结 初学者必备

    (3) 在模块内,一个被声明为静态的函数只可被这一模块内的其它函数调用。那就是,这个函数被限制在声明它的模块的本地范围内使用。 15、引用与指针有什么区别? 答: (1) 引用必须被初始化,指针不必。 (2) 引用...

Global site tag (gtag.js) - Google Analytics