函数堆栈调用过程

从内存的角度详细的分析C语言中的函数调用过程:

首先写一个测试用的代码:

#include <stdio.h>

int add(int x, int y)
{
	int z = 0;
	z = x + y;
	return z;
}

int main()
{
	int a = 1, b = 2;
	int c = 0;

	c = add(a, b);

	return 0;
}

这是一个简单的的求和函数。

其次,让我们确定一下,程序是从哪里开始运行的:

调试程序,按一下F10(博主用的VS2013),

进入main函数:

然后进调试--->窗口--->调用堆栈(用来显示函数的调用关系)。

发现正在调用main这个函数,但现在我想知道是谁在调用main函数,F10一路走到return 0,接着换F11(逐语句调试),然后会发现,main函数返回后,我们来到了这里:

再看看此时的调用堆栈:

直接来看,现在运行的函数是__tmainCRTStartup(),这个函数又被mainCRTStratup()调用,而我们刚刚是从main()函数返回来的,所以,main()函数是由__tmainCRTStartup()这个函数调用的。

了解了main()函数是被谁调用后,我们可以进一步分析这其中的细节了!

现在重新F10进入调试,到这一步:

进入main()函数后还没有执行任何一条语句,我们 右击-->转到反汇编:

看到了汇编语言的代码,图中的ebp和esp是什么东西呢?我们知道,调用函数的时候操作系统要给这个函数分配一段内存空间,之前又说了main()函数是由—__tCRTStartup()函数调用的,所以请看:

mainCRTStratup()函数调用__tmainCRTStra()函数的时候就会从栈上为__tmainCRTStra()分配类似图中这么一块空间,把这块空间叫做栈帧。我们知道栈是由高地址向低地址扩展的。其中ebp叫做栈底指针,esp叫做栈顶指针(当然也有其它叫法)。ebp,esp本身是一个寄存器,其中存放了地址时,我们就称之为指针!

现在再来看汇编程序:

按一下F10执行第一条语句,箭头指向下一条语句,变成这样:

(和我们在外边的调试是一样的)这句 push ebp 就是将ebp中的值进行压栈,而此时ebp存放的是系统分给__tmainCRTStartup()函数的空间的起始地址。因为我们现在要调用main()函数了,所以当然要先把__tmainCRTStartup()函数的运行状态保存下来,这样main()函数才能返回的时候才能找得到!push是在栈顶进行的,所以,push之后,esp要向上移动:

刚刚说了,栈是由高地址向低地址扩展的,所以这个push操作应该是对esp进行一个减操作,具体见了多少,可以在内存里查一查:

先看一下push之前esp的的值:

esp的当前值为0x00ABFA30,代表它指向0x00ABFA30这个地址代表的内存。

再看一下push之后esp的值发生了什么变化:

变成了0x00ABFA2C,差了4个字节,就是放进去的地址的大小。

然后继续执行下一条语句: mov         ebp,esp

即把esp的值赋给ebp,这样,ebp也就指向了现在esp的位置,如下图:

接着又执行语句:sub         esp,0E4h

即将esp的值减去E4h,所以esp向上移动了E4h个位置(相当于申请了这么大的一块空间),新申请的这块空间就给main()用了。如下:

接下来紧接着三条push语句将后面要用到的寄存器中原来的值存储起来,等我们借用完寄存器后再给人家pop回去,不管它,这里esp再向上移动三次。

(ps:图片太大,所以只截了当前要用到的)

紧接着的四条语句共同完成一个任务,就是将图中最大长方形区域初始化为0CCCCCCCh(你经常看到的:烫烫烫烫......)

第一句:lea         edi,[ebp-0E4h]  
就是将ebp减去E4h的值赋给edi,这个E4h是不是很眼熟呢?它就是我们上一步分配给main()的空间的大小,即edi指向了3次push之前的esp的位置;

第二句:mov         ecx,39h 
把39h放在ecx中(充当了计数器)

第三句:mov         eax,0CCCCCCCCh

把要初始化的数据写入eax

最后一句:rep stos    dword ptr es:[edi]

循环的从低地址(ebp-0E4h)向高地址(ebp)写0CCCCCCCCh,循环了39h次!

我们在执行之前转到内存中看一下:

先查找ebp:

(我往下拖了一点,左下角的光标处的地址就是ebp当前值0z00ABFA2C)

四条语句执行后:

相应的位置已经被初始化为0CCCCCCCh,其它部分是乱码(此时ebp值为0x00ABFA2C,它之上的一段空间是分配给main()的)

程序继续往下执行:

mov         dword ptr [ebp-8],1  在ebp-8h的位置放一个1,
mov         dword ptr [ebp-14h], 2 在ebp-14h的位置放一个2

即分别创建了a,b两个变量,如图:

接着创建c:

此时我们的内存分配变成了这样:

然后到了这里,调用add()准备工作:

mov         eax,dword ptr [ebp-14h]    是把ebp-14h位置的值放入eax(此时ebp-14h的值是我们的变量b的值),然后:push        eax , 即eax压栈;

同理,mov         ecx,dword ptr [ebp-8]   把ebp-8位置的值放入ecx,然后ecx压栈。如下(传递形参给x和y):

程序到这:

在汇编里我们用call调用一个函数(_add是一个标号,它代表了一个地址,是add()函数的首地址),而call在执行的同时,会把它下一条指令的地址(就是图中的00D1450)push到main()的栈桢中去,以便add()执行完后返回的时候还可以找到程序当初执行到了哪里,然后接着执行。

为了证明这一点,我们先查看一下esp所指向内存的值:

然后F11跟进去到这里:

再查看esp所指内存:

可以看到esp的位置发生了改变,此时内存中的值 50 14 0d 00 是不是很像刚刚的call语句下一条指令地址呢?对它就是00 0d 14 50 的小端字节序,这里不再解释小端字节序,只需理解它是内存中字节存储的一种方式,有兴趣的可以查看:http://blog.csdn.net/qq_33724710/article/details/51056542

栈桢分配图变成了这样:

接着F11执行刚刚的jmp语句:

历尽千辛万苦终于进入add()!现在贴出来的这几句代码就和我们刚刚进入main()函数的语句大同小异了。

push        ebp  //ebp压栈
  mov         ebp,esp  //ebp指向esp所指
  sub         esp,0CCh  //esp - 0CCh, 开辟了新的栈桢
  push        ebx  //3个push,照旧不管它
  push        esi  
  push        edi  
  lea         edi,[ebp-0CCh]  //初始化烫烫烫烫......  
  mov         ecx,33h  
  mov         eax,0CCCCCCCCh  
  rep stos    dword ptr es:[edi]

然后到这里:

给ebp-8处放了个0,就是创建z啦!

再接着到这里:

eax,dword ptr [ebp+8]  //注意是加了8,取出的是我们之前传递进来的形参值1,放到eax
add         eax,dword ptr [ebp+0Ch]  //取epb+0Ch,取出的是我们之前传递进来的形参值2,加到eax
dword ptr [ebp-8],eax  //再把求和后的值eax赋给epb - 8的位置,就是z喽!

程序执行到这,准备返回main()了:

因为z是个临时变量,出了add()就会销毁,要返回z的值,就要把它的值放进寄存器:

mov         eax,dword ptr [ebp-8]  //epb-8找到的就是z,赋给eax
 pop         edi  //连续三个pop,之前连续三个push我们没管它,现在仍然不管它
 pop         esi  
 pop         ebx  
3次pop后,esp高地址处移动了3个单位:

虽然esp上边的空间还在,但是已经不属于当前的栈桢了,相当于释放掉了!

然后:

mov         esp,ebp  //esp指向当前ebp
  pop         ebp  //main()起始地址赋给给ebp,esp往高地址处移动一次

所以变成这样:

最后执行ret,程序回到这里:


看见了没,ret指令自动取出了call的下一条语句地址(ret自动执行了pop,esp又往高地址处移动了一次)赋给了PC(PC总是指向下一条要执行的语句)。

接着的add         esp,8  使esp继续往高地址方向移动,并跳过1,2两个参数,如下:

mov         dword ptr [ebp-20h],eax  //还记得eax吗,当初我们把求和的结果,即 z 的值赋给了它,ebp-20h依然是当初的c

现在,我们要的结果已经赋给 c 了!

xor         eax,eax  //eax没用了,异或eax,清零

pop         edi  //又是连续3个pop
  pop         esi  
  pop         ebx  
  add         esp,0E4h  //oE4h,当出开辟的main()栈桢的大小,现在释放掉
  cmp         ebp,esp  //不管它
  call        000D113B  //不管它
  mov         esp,ebp  //释放main()栈桢
  pop         ebp  //ebp指向__tmainCRTStartup()起始地址,esp下移
  ret  //返回到__tmainCRTStartup()

__tmainCRTStartup()和mainCRTStart()里边的过程就不在分析了!

时间: 2024-10-11 08:06:10

函数堆栈调用过程的相关文章

函数递归调用过程中的调用堆栈的情况

为了加深对函数递归调用过程中的理解,本Demo程序特意在VS2008 C#控制台程序实现了阶乘的计算功能,用于观察函数递归调用过程中的调用堆栈的情况. 源码如下: using System; using System.Collections.Generic; using System.Linq; using System.Text; namespace RecursiveTset { class Program { //阶乘的定义:n!=n*(n-1)!,特别的,1!=1:0!=1 //阶乘的实

函数的调用过程(栈帧)

1.什么是栈帧? 栈帧也叫过程活动记录,是编译器用来实现函数调用过程的一种数据结构.C语言中,每个栈帧对应着一个未运行完的函数.从逻辑上讲,栈帧就是一个函数执行的环境:函数调用框架.函数参数.函数的局部变量.函数执行完后返回到哪里等等.栈是从高地址向低地址延伸的.每个函数的每次调用,都有它自己独立的一个栈帧,这个栈帧中维持着所需要的各种信息.寄存器ebp指向当前的栈帧的底部(高地址),寄存器esp指向当前的栈帧的顶部(低地址). 2.Add()函数的调用过程 我们以Add()函数为例深入的研究一

C函数的调用过程 &nbsp; 栈帧

C语言中,每个栈帧对应着一个未运行完的函数.栈帧中保存了该函数的返回地址和局部变量. 首先,栈是从高地址向低地址延伸的.寄存器ebp指向当前的栈帧的底部(高地址),寄存器esp指向当前的栈帧的顶部(低地址). 先来看一个代码 #include <stdio.h> void fun() {     int tmp = 10;     int*p = (int*)(*(&tmp + 1));     *(p - 1) = 20; } int main() {     int a = 0;

使用IDA PRO+OllyDbg+PEview 追踪windows API 动态链接库函数的调用过程

http://bbs.pediy.com/showthread.php?p=1354999 标 题: [原创]使用IDA PRO+OllyDbg+PEview 追踪windows API 动态链接库函数的调用过程.作 者: shayi时 间: 2015-02-12,05:19:54链 接: http://bbs.pediy.com/showthread.php?t=197829 使用IDA PRO+OllyDbg+PEview 追踪windows API 动态链接库函数的调用过程. (本文同步更

函数的调用过程——栈帧。

今天我们来看一下函数的调用过程与栈帧. 我们通过一段简单的代码和图示来介绍这个过程: #include<stdio.h> int add(int x,int y) { int z = x + y; return z; } int main() { int a = 0xaaaaaaaa; int b = 0xbbbbbbbb; int c = add(a, b); printf("run here!%d\n", c); return 0; } 将这个过程用图示表示出来: 在c

[Android Pro] 深入理解函数的调用过程——栈帧

cp :http://blog.csdn.net/x_perseverance/article/details/78897637 每一个函数被调用时,都会为函数开辟一块空间,这块空间就称为栈帧. 首先,我们了解一下不同种类的寄存器: (1)eax,ebx,ecx,edx :通用寄存器 (2)ebp:存放指向函数栈帧栈底的地址 (3)esp:存放指向函数栈帧栈顶的地址 (4)eip:程序计数器——保存程序当前正在执行指令的下一个指令的地址 接着我们以下面这段代码为例,深入到函数的调用过程中去: #

手动跟踪函数的调用过程【转】

转自:https://blog.csdn.net/ccjjnn19890720/article/details/6871036/ 今天是10月13号,不知道为什么日子过的如此的快,大概是假期的原因吧.在十一国庆以后,上了3天课又放假了...感觉研究生的生活越来越没有学生样子啦...老师在很久以前就安排了一个任务给我,叫我完成在arm板子上的视频显示,做过了前期的JPEG的显示,觉得这个问题本身不是很大.大概是自己对这种事情了解的太少,当真正的去接触的时候就觉得难度很大. 视频本身是有一帧一帧的数

从内存角度看C函数的调用过程

从内存的角度详细的分析C语言中的函数调用过程: 首先写一个测试用的代码: #include <stdio.h> int add(int x, int y) { int z = 0; z = x + y; return z; } int main() { int a = 0, b = 0; int c = 0; c = add(a, b); return 0; } 这是一个简单的的求和函数. 其次,让我们确定一下,程序是从哪里开始运行的: 调试程序,按一下F10(博主用的VS2013), 进入m

堆栈桢的生成原理(调试器是如何生成堆栈的调用过程的)

一直比较好奇,调试器是如何生成堆栈的调用过程的,比如如下代码: int add(int a, int b){    return a + b;} int main(){    int c = add(1, 2);    system("pause"); return 0;} 调用Add时的堆栈截图如下: 调试器究竟是如何生成这个堆栈过程的呢? 我最初的理解调试器是根据EBP来生成该堆栈的,原理如下:可以看到按照上面的原理, 每次EBP里存放的都是当前函数的堆栈桢基址,所以我们只要一直递