zoukankan      html  css  js  c++  java
  • C语言的函数调用过程(栈帧的创建与销毁)

    从汇编的角度解析函数调用过程

    看看下面这个简单函数的调用过程:

     1 int Add(int x,int y)
     2 {
     3     int sum = 0;
     4     sum = x + y;
     5     return sum;
     6 }
     7 
     8 int main ()
     9 {
    10     int a = 10;
    11     int b = 12;
    12     int ret = 0;
    13     ret = Add(a,b);
    14     return 0;
    15 }

    今天主要用汇编代码去讲述这个过程,首先介绍几个寄存器和简单的汇编指令的意思。 
    先看几个函数调用过程涉及到的寄存器: 
    (1)esp:栈指针寄存器(extended stack pointer),其内存放着一个指针,该指针永远指向系统栈最上面一个栈帧的栈顶。 
    (2)ebp:基址指针寄存器(extended base pointer),其内存放着一个指针,该指针永远指向系统栈最上面一个栈帧的底部。 
    (3)eax 是”累加器”(accumulator), 它是很多加法乘法指令的缺省寄存器。 
    (4)ebx 是”基地址”(base)寄存器, 在内存寻址时存放基地址。 
    (5)ecx 是计数器(counter), 是重复(REP)前缀指令和LOOP指令的内定计数器。 
    (6)edx 则总是被用来放整数除法产生的余数。 
    (7)esi/edi分别叫做”源/目标索引寄存器”(source/destination index),因为在很多字符串操作指令中, DS:ESI指向源串,而ES:EDI指向目标串. 
    在32位平台上,ESP每次减少4字节。 
    再看几条简单的汇编指令: 
    mov :数据传送指令,也是最基本的编程指令,用于将一个数据从源地址传送到目标地址(寄存器间的数据传送本质上也是一样的) 
    sub:减法指令 
    lea:取偏移地址 
    push:实现压入操作的指令是PUSH指令 
    pop:实现弹出操作的指令 
    call:用于保存当前指令的下一条指令并跳转到目标函数。 
    这些指令当然能看懂最好,可以让你很深刻的理解函数调用过程,不能看懂就只能通过我的描述去理解了。 
    进行分析之前,先来了解下内存地址空间的分布: 

    栈空间是向低地址增长的,主要是用来保存函数栈帧。 栈空间的大小很有限,仅有区区几MB大小 
    汇编代码实现: 
    main函数汇编代码:

    int main ()
    {
    011B26E0  push        ebp  
    011B26E1  mov         ebp,esp 
    011B26E3  sub         esp,0E4h 
    011B26E9  push        ebx  
    011B26EA  push        esi  
    011B26EB  push        edi  
    011B26EC  lea         edi,[ebp-0E4h] 
    011B26F2  mov         ecx,39h 
    011B26F7  mov         eax,0CCCCCCCCh 
    011B26FC  rep stos    dword ptr es:[edi] 
        int a = 10;
    011B26FE  mov         dword ptr [a],0Ah 
        int b = 12;
    011B2705  mov         dword ptr [b],0Ch 
        int ret = 0;
    011B270C  mov         dword ptr [ret],0 
        ret = Add(a,b);
    011B2713  mov         eax,dword ptr [b] 
    011B2716  push        eax  
    011B2717  mov         ecx,dword ptr [a] 
    011B271A  push        ecx  
    011B271B  call        @ILT+640(_Add) (11B1285h) 
    011B2720  add         esp,8 
    011B2723  mov         dword ptr [ret],eax 
        return 0;
    011B2726  xor         eax,eax 
    }
    011B2728  pop         edi  
    011B2729  pop         esi  
    011B272A  pop         ebx  
    011B272B  add         esp,0E4h 
    011B2731  cmp         ebp,esp 
    011B2733  call        @ILT+450(__RTC_CheckEsp) (11B11C7h) 
    011B2738  mov         esp,ebp 
    011B273A  pop         ebp  
    011B273B  ret            

    Add函数汇编代码:

    int Add(int x,int y)
    {
    011B26A0  push        ebp  
    011B26A1  mov         ebp,esp 
    011B26A3  sub         esp,0CCh 
    011B26A9  push        ebx  
    011B26AA  push        esi  
    011B26AB  push        edi  
    011B26AC  lea         edi,[ebp-0CCh] 
    011B26B2  mov         ecx,33h 
    011B26B7  mov         eax,0CCCCCCCCh 
    011B26BC  rep stos    dword ptr es:[edi] 
        int sum = 0;
    011B26BE  mov         dword ptr [sum],0 
        sum = x + y;
    011B26C5  mov         eax,dword ptr [x] 
    011B26C8  add         eax,dword ptr [y] 
    011B26CB  mov         dword ptr [sum],eax 
        return sum;
    011B26CE  mov         eax,dword ptr [sum] 
    }
    011B26D1  pop         edi  
    011B26D2  pop         esi  
    011B26D3  pop         ebx  
    011B26D4  mov         esp,ebp 
    011B26D6  pop         ebp  
    011B26D7  ret              

    下面图中详细描述了调用过程地址变化(此处所有地址是取自32位windows系统vs编辑器下的调试过程。): 

    过程描述: 
    1、参数拷贝(参数实例化)。 
    2、保存当前指令的下一条指令,并跳转到被调函数。 
    这些操作均在main函数中进行。

    接下来是调用Add函数并执行的一些操作,包括: 
    1、移动ebp、esp形成新的栈帧结构。 
    2、压栈(push)形成临时变量并执行相关操作。 
    3、return一个值。 
    这些操作在Add函数中进行。

    被调函数完成相关操作后需返回到原函数中执行下一条指令,操作如下: 
    1、出栈(pop)。 
    2、回复main函数的栈帧结构。(pop ) 
    3、返回main函数 
    这些操作也在Add函数中进行。 至此,在main函数中调用Add函数的整个过程已经完成。 
    总结起来整个过程就三步: 
    1)根据调用的函数名找到函数入口; 
    2)在栈中审请调用函数中的参数及函数体内定义的变量的内存空间 
    3)函数执行完后,释放函数在栈中的审请的参数和变量的空间,最后返回值(如果有的话) 
    如果你学了微机原理,你会想到cpu中断处理过程,是的,函数调用过程和中断处理过程一模一样。

    函数调用约定: 
    这里再补充一下各种调用规定的基本内容。 
    _stdcall调用约定

    所有参数按照从右到左压入堆栈,由被调用的子程序清理堆栈

    _cdecl调用约定(The C default calling convention,C调用规定)

    参数也是从右到左压入堆栈,但由调用者清理堆栈。

    _fastcall调用约定

    顾名思义,_fastcall的目的主要是为了更快的调用函数。它主要依靠寄存器传递参数,剩下的参数依然按照从右到左的顺序压入堆栈,并由被调用的子程序清理堆栈。

    本篇博文是按调用约定__stdcall 调用函数。

    csdn博客地址:http://blog.csdn.net/qq_38646470

  • 相关阅读:
    Class类与Java反射《java从入门到精通》第十六章
    maven配置,Java环境变量配置,电脑系统重装之后需要环境配置(大概每年都要一次重装系统)
    Docker部分--尚硅谷2020微服务分布式电商项目《谷粒商城》(没给学习文档,在这做笔记)
    centOS7下载安装(阿里云镜像下载,速度10M/s),(好用免费的工具VirtualBox,FinalShell比xshell更舒服)
    从程序员到项目经理:为什么要当项目经理
    状态图(Statechart Diagram)
    ServiceLoader实现原理
    Java8 lambda表达式10个示例
    Java8函数之旅(四) --四大函数接口
    Java8特性详解 lambda表达式 Stream
  • 原文地址:https://www.cnblogs.com/zhonglongbo/p/8392026.html
Copyright © 2011-2022 走看看