函数调用栈

转自  http://www.cnblogs.com/rain-lei/p/3622057.html

函数调用大家都不陌生,调用者向被调用者传递一些参数,然后执行被调用者的代码,最后被调用者向调用者返回结果,还有大家比较熟悉的一句话,就是函数调用是在栈上发生的,那么在计算机内部到底是如何实现的呢?

对于程序,编译器会对其分配一段内存,在逻辑上可以分为代码段,数据段,堆,栈

代码段:保存程序文本,指令指针EIP就是指向代码段,可读可执行不可写

数据段:保存初始化的全局变量和静态变量,可读可写不可执行

BSS:未初始化的全局变量和静态变量

堆(Heap):动态分配内存,向地址增大的方向增长,可读可写可执行

栈(Stack):存放局部变量,函数参数,当前状态,函数调用信息等,向地址减小的方向增长,非常非常重要,可读可写可执行

如图所示

寄存器

EAX:累加(Accumulator)寄存器,常用于函数返回值

EBX:基址(Base)寄存器,以它为基址访问内存

ECX:计数器(Counter)寄存器,常用作字符串和循环操作中的计数器

EDX:数据(Data)寄存器,常用于乘除法和I/O指针

ESI:源变址寄存器

DSI:目的变址寄存器

ESP:堆栈(Stack)指针寄存器,指向堆栈顶部

EBP:基址指针寄存器,指向当前堆栈底部

EIP:指令寄存器,指向下一条指令的地址

源代码

int print_out(int begin, int end)
{
 printf("%d ", begin++);
 int *p;
 p = (int*)(int(&begin) - 4);
 if(begin <= end)
  *p -= 5;
 return 1;
}

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

int pass(int a, int b, int c) {
 char buffer[4] = {0};
 int sum = 0;
 int *ret;
 ret = (int*)(buffer+28);
 //(*ret) += 0xA;
 sum = a + b + c;
 return sum;
}

int main()
{
 print_out(0, 2);
 printf("\n");
 int a = 1;
 int b = 2;
 int c;
 c = add(a, b);
 pass(a, b, c);
 int __sum;
 __asm
 {
  mov __sum, eax
 }
 printf("%d\n", __sum);
 system("pause");
}

函数初始化

  28: int main()
    29: {
011C1540 push ebp //压栈,保存ebp,注意push操作隐含esp-4
011C1541 mov ebp,esp //把esp的值传递给ebp,设置当前ebp
011C1543 sub esp,0F0h //给函数开辟空间,范围是(ebp, ebp-0xF0)
011C1549 push ebx
011C154A push esi
011C154B push edi
011C154C lea edi,[ebp-0F0h] //把edi赋值为ebp-0xF0
011C1552 mov ecx,3Ch //函数空间的dword数目,0xF0>>2 = 0x3C
011C1557 mov eax,0CCCCCCCCh
011C155C rep stos dword ptr es:[edi]
//rep指令的目的是重复其上面的指令.ECX的值是重复的次数.
//STOS指令的作用是将eax中的值拷贝到ES:EDI指向的地址,然后EDI+4

一般所用函数的开头都会有这段命令,完成了状态寄存器的保存,堆栈寄存器的保存,函数内存空间的初始化

函数调用

 30: print_out(0, 2);
013D155E push 2 //第二个实参压栈
013D1560 push 0 //第一个实参压栈
013D1562 call print_out (13D10FAh)//返回地址压栈,本例中是013D1567,然后调用print_out函数
013D1567 add esp,8  //两个实参出栈
//注意在call命令中,隐含操作是把下一条指令的地址压栈,也就是所谓的返回地址

除了VS可能增加一些安全性检查外,print_out的初始化与main函数的初始化完全相同

被调用函数返回

013D141C mov eax,1  //返回值传入eax中
013D1421 pop edi
013D1422 pop esi
013D1423 pop ebx //寄存器出栈
013D1424 add esp,0D0h //以下3条命令是调用VS的__RTC_CheckEsp,检查栈溢出
013D142A cmp ebp,esp
013D142C call @ILT+315(__RTC_CheckEsp) (13D1140h)
013D1431 mov esp,ebp //ebp的值传给esp,也就是恢复调用前esp的值
013D1433 pop ebp //弹出ebp,恢复ebp的值
013D1434 ret  //把返回地址写入EIP中,相当于pop EIP

call指令隐含操作push EIP,ret指令隐含操作 pop EIP,两条指令完全对应起来

写到这里我们就可以分析一下main函数调用print_out函数前后堆栈(Stack)发生了什么变化,下面用一系列图说明

接下来是返回过程,从上面的013D1431 行代码开始

   

 

  

print_out函数调用前后,main函数的栈帧完全一样,perfect!

下面我们来看看print_out函数到底做了什么事情

int *p;
p = (int*)(int(&begin) - 4);
if(begin <= end)
  *p -= 5;

根据上面调用print_out函数后的示意图,可以知道p实际上是指向了函数的返回地址addr,然后把addr-5,这又会发生什么?

再回头看一下反汇编的代码,

013D1560 push 0 //第一个实参压栈
013D1562 call print_out (13D10FAh)//返回地址压栈,本例中是013D1567,然后调用print_out函数
013D1567 add esp,8  //两个实参出栈

分析可知,返回地址addr的值是013D1567 ,addr-5为013D1562 ,把返回地址指向了call指令,结果是再次调用print_out函数,

从而print_out函数实现了打印从begin到end之间的所有数字,可以说是循环调用了print_out函数

对于add函数,主要是为了说明返回值存放于寄存器eax中。

另外,VS自身会提供一些安全检查

CheckStackVar安全检查http://blog.csdn.net/masefee/article/details/5630154,通过ecx和edx传递参数, 局部变量有数组时使用

__security_check_cookie返回地址检查, 数组长度大于等于5时使用

__RTC_CheckEsp程序栈检查,printf函数用使用

时间: 2024-07-28 23:30:01

函数调用栈的相关文章

C开发基础--函数调用栈

发现有一些问题几乎是所有的新人都会遇到,而且也常因为缺乏一些基本的知识而无从下手.函数调用栈的内容就是其中之一.于是花点时间把以前写的内容整理出来. 程序在运行期间,内存中有一块区域,用来实现程序的函数调用机制.这块区域是一块LIFO的数据结构区域,我们可以叫函数栈(调用栈).每个未退出的函数都会在函数栈中拥有一块数据区,我们叫函数的栈帧.函数的调用栈帧中,保存了相应的函数的一些重要信息:函数中使用的局部变量,函数的参数,另外还有一些维护函数栈所需要的数据,比如EBP指针,函数的返回地址.如下图

C语言函数调用栈(三)

6 调用栈实例分析 本节通过代码实例分析函数调用过程中栈帧的布局.形成和消亡. 6.1 栈帧的布局 示例代码如下: 1 //StackReg.c 2 #include <stdio.h> 3 4 //获取函数运行时寄存器%ebp和%esp的值 5 #define FETCH_SREG(_ebp, _esp) do{ 6 asm volatile( 7 "movl %%ebp, %0 \n" 8 "movl %%esp, %1 \n" 9 : "

[Win32]一个调试器的实现(十一)显示函数调用栈

[Win32]一个调试器的实现(十一)显示函数调用栈 作者:Zplutor 出处:http://www.cnblogs.com/zplutor/ 本文版权归作者和博客园共有,欢迎转载.但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文连接,否则保留追究法律责任的权利. 本文讲解如何在调试器中显示函数调用栈,如下图所示: 原理 首先我们来看一下显示调用栈所依据的原理.每个线程都有一个栈结构,用来记录函数的调用过程,这个栈是由高地址向低地址增长的,即栈底的地址比栈顶的地址大.ESP寄存器的

浅析函数调用栈

1. 预备知识: 函数调用大家都不陌生,调用者向被调用者传递一些参数,然后执行被调用者的代码,最后被调用者向调用者返回结果,还有大家比较熟悉的一句话,就是函数调用是在栈上发生的,那么在计算机内部到底是如何实现的呢? 对于程序,编译器会对其分配一段内存,在逻辑上可以分为代码段,数据段,堆,栈 代码段:保存程序文本,指令指针EIP就是指向代码段,可读可执行不可写 数据段:保存初始化的全局变量和静态变量,可读可写不可执行 BSS:未初始化的全局变量和静态变量 堆(Heap):动态分配内存,向地址增大的

Java函数调用栈

Java的函数调用栈就是Java虚拟机栈,它是线程私有的,与线程一同被创建,用于存储栈帧. 栈帧随着方法的调用而创建,随着方法的结束而销毁.可以说栈帧是方法的抽象. 于是,可以通过打印出Java虚拟机栈中的栈帧信息来了解函数调用过程.用于实现这个过程的Java代码如下: package methodcall; public class Methods { public void method1() { method2(); } public void method2() { method3();

在chrome开发者工具中观察函数调用栈、作用域链与闭包

在chrome开发者工具中观察函数调用栈.作用域链与闭包 在chrome的开发者工具中,通过断点调试,我们能够非常方便的一步一步的观察JavaScript的执行过程,直观感知函数调用栈,作用域链,变量对象,闭包,this等关键信息的变化.因此,断点调试对于快速定位代码错误,快速了解代码的执行过程有着非常重要的作用,这也是我们前端开发者必不可少的一个高级技能. 当然如果你对JavaScript的这些基础概念[执行上下文,变量对象,闭包,this等]了解还不够的话,想要透彻掌握断点调试可能会有一些困

函数调用栈、任务队列、事件轮询

最近接触nodeJs中,了解到了这三个概念.这三个概念在14年,阮一峰老师写了一篇博客,扑灵大神在此基础上完善了,. 对于小菜鸟,简直是神仙对话,不懂,不明白. 阮一峰: http://www.ruanyifeng.com/blog/2014/10/event-loop.html 扑灵:https://blog.csdn.net/lin_credible/article/details/40143961. 后续看到一篇文章,个人觉得写的比较好 https://www.cnblogs.com/li

PWN菜鸡入门之函数调用栈与栈溢出的联系

一.函数调用栈过程总结 Fig 1. 函数调用发生和结束时调用栈的变化 Fig 2. 将被调用函数的参数压入栈内 Fig 3. 将被调用函数的返回地址压入栈内 Fig 4. 将调用函数的基地址(ebp)压入栈内,并将当前栈顶地址传到 ebp 寄存器内 Fig 5. 将被调用函数的局部变量压入栈内 二.函数调用栈实例说明 首先我们来看看以下程序调用栈的过程: int sum(int a,int b) { int temp = 0; temp = a+b; return temp; } int ma

C函数调用 栈

这篇blog试图说明这么一个问题,当一个c函数被调用时,一个栈帧(stack frame)是如何被建立,又如何被消除的.这些细节跟操作系统平台及编译器的实现有关,下面的描述是针对运行在Linux的gcc编译器而言的.c语言的标准并没有描述实现的方式.所以,不同的编译器.不同的操作系统都可能有自己的建立栈帧的方式. 下面先看一个典型的栈帧: 上面个的这个图是一个典型的栈帧,图中,栈顶在上,地址空间往下增长. 在看看这个栈对应的函数代码: int foo(int arg1, int arg2, in

[转] 函数调用栈

http://kingj.iteye.com/blog/1555017 http://www.cnblogs.com/rain-lei/p/3622057.html 函数调用大家都不陌生,调用者向被调用者传递一些参数,然后执行被调用者的代码,最后被调用者向调用者返回结果,还有大家比较熟悉的一句话,就是函数调用是在栈上发生的,那么在计算机内部到底是如何实现的呢? 对于程序,编译器会对其分配一段内存,在逻辑上可以分为代码段,数据段,堆,栈 代码段:保存程序文本,指令指针EIP就是指向代码段,可读可执