首页 > 其他分享 >探究C语言函数栈帧的创建和销毁

探究C语言函数栈帧的创建和销毁

时间:2024-06-20 17:58:04浏览次数:22  
标签:函数 C语言 探究 指令 ebp 寄存器 main 栈帧

引言

在C语言程序中,每当一个函数被调用时,系统都会在栈上为该函数分配一块内存空间,这块内存空间就被称为栈帧。

栈帧中包含了函数执行所需的所有信息,如局部变量、参数、返回地址等。栈帧的创建和销毁是函数调用的核心部分,它们确保了函数能够正确地执行和返回。

本文将在VS2013环境下,通过实践操作,对比较简单的C语言程序进行调试的基础上,对C语言函数栈帧的创建和销毁过程进行详细的论述,并探讨函数中局部变量的创建、参数的传递、形参的引用以及返回值等过程。

一、概念

我们在写c语言程序时,通常会把独立的功能封装为一个个函数,所以C语言程序是以函数为基本单位的。而函数的传参、调用和返回值等问题都和函数栈帧有关。

1.栈

栈是一个线性数据结构,遵循先进后出的规则,可将数据从栈顶压入,也可将数据从栈顶弹出。在windows操作系统中,栈是由高地址向低地址使用的。

2.函数栈帧

每一个函数调用,都要在栈区上开辟一块空间,这块空间就是函数栈帧。

这块空间用来存放:

(1)函数参数和返回值;

(2)局部变量;

(3)保存上下文信息。

3.寄存器

(1)eax:通用寄存器,保留临时数据,常用于返回值;

(2)ebx:通用寄存器,保留临时数据;

(3)ebp:栈底寄存器;

(4)esp:栈顶寄存器;

(5)eip:指令寄存器,保存当前指令的下一条指令的地址。

ebp和esp是维护函数栈帧的两个寄存器,哪个函数被调用,它们就指向哪个函数的栈帧空间进行维护,来标识哪个函数正在被使用,为对这个函数的操作提供支持。

4.汇编命令

(1)mov:数据转移指令;

(2)push:数据入栈;

(3)pop:数据弹出至指定位置;

(4)sub:减法命令;

(5)add:加法命令;

(6)call:函数调用,压入返回地址,转入目标函数;

(7)jump:通过修改eip,转入目标函数,进行调用;

(8)ret:恢复返回地址。

5.使用的C程序

二、main函数的运行

1.main函数被调用过程

main函数并不是最初被程序调用的函数,main函数也是通过被其他函数调用而被使用的。调用main函数的具体过程,是由编译器决定的。

main函数由__tmainCRTstartup函数调用,而__tmainCRTstartup函数又由mainCRTstartup函数调用。

在栈区内存中,空间是由高地址向低地址使用的,所以程序开始运行后,mainCRTstartup函数先在较高地址处创建栈帧,然后调用__tmainCRTstartup函数并创建栈帧,最后再调用main函数并为其创建相应的栈帧空间。

2.main函数栈帧的创建

main函数在运行时也会在栈区内存上开辟一块空间,这个空间由ebp和esp两个寄存器来维护。ebp指向栈底的较高地址,esp指向栈顶的较低地址,两个寄存器记录的地址之间就是内存划分给main函数,供main函数使用调配的空间。

可以经过反编译,得到main函数栈帧创建的汇编代码:

为main函数创建栈帧的时候,ebp和esp正在维护__tmainCRTstartup函数创建的栈帧。

为main函数创建栈帧的过程如下:

(1)00C21410  push    ebp

push指令进行压栈,把ebp中的地址压入栈顶,此时维护栈顶的指针esp值减小4,把刚刚压入的元素纳入__tmainCRTstartup函数的栈帧空间;

(2)00C21411  mov    ebp,esp

mov指令将栈底指针移动到栈顶;

(3)00C21423  sub    esp,0E4h

sub指令让esp中的值减去一个数,来到更低地址的位置,此时esp和ebp两个寄存器就离开了原先__tmainCRTstartup函数的栈帧空间,指向了一块新的栈区空间,这块空间就是为main函数预申请的栈帧空间;

(4)00C21419  push    ebx

     00C2141A  push    esi

     00C2141B  push    edi

push指令将ebx、esi、edi的值压入栈;

(5)00C2141C  lea    edi,[ebp+FFFFFF1Ch]

lea指令把ebp-0E4h加载进edi中,这其实就是压入ebx、esi、edi三个元素前esp的地址;

(6)00C21422  mov    ecx,39h

     00C21427  mov    eax,0CCCCCCCCh

mov指令将39h、0CCCCCCCCh两个值分别放入ecx和eax两个寄存器当中;

(7)00C2142C  rep stos    dword ptr es:[edi]

rep stos指令,在这里是将从edi中的地址开始,向下39h次,每次改变dword(4个字节)的空间,全部改为eax的值,这个操作把为main函数开辟的栈帧空间中的值全部初始化为cccccccc(每4个字节)。

3.main函数中局部变量的创建

(1)00C2142E  mov    dword ptr [ebp-8],0Ah

     00C21435  mov    dword ptr [ebp-14h],14h

     00C2143C  mov    dword ptr [ebp-20h],0

mov指令将值10放入ebp-8的位置,创建了整型变量a;将值20放入ebp-14h的位置,创建了整型变量b;将值0放入ebp-20h的位置,创建了整型变量c.

注意到整型变量a创建在ebp-8的位置上,而随后的整型变量b创建在ebp-14h(ebp-20)的位置上,之间相隔8个字节,共2个整型的空间,这就是平时我们会观察到,前后紧邻创建的两个变量,在内存空间上却并不是紧邻的根本原因。

在代码中因为越界或其他因素访问到没有被初值初始化的内存空间,打印出随机值时,经常出现烫烫烫烫的字样,就是因为函数栈帧中初始存放的值是cccccccc(4个字节),这些值在打印的时候被译为烫烫烫烫。

4.main函数中为Add函数传参

(1)00C21443  mov    eax,dword ptr [ebp-14h]

     00C21446  push    eax

     00C21447  mov    ecx,dword ptr [ebp-8]

     00C2144A  push    ecx

mov和push指令在这里把ebp-14h中的值20,也就是整型变量b的值,放入到了寄存器eax中,然后压入栈顶,把栈顶指针减少;之后又把ebp-8中的值10,也就是整型变量a中的值放入了寄存器ecx中,然后压入栈顶,把栈顶指针减少。这两步操作其实就是在传参,把main函数中实参部分,整型变量a和整型变量b的值,传递给Add函数中的形参整型变量x和整型变量y,也就是说,这里的寄存器eax和ecx就相当于放着整型变量x和整型变量y。

另外从这个传参过程中也可以很清楚的看到,函数参数的传递是从右向左进行的的,先传递的整型变量b,再传递的整型变量a。

5.创建Add函数的函数栈帧

call指令将call指令的下一条指令的地址压入栈中,自己通过jmp指令来找到Add函数。这里就是在开始调用Add函数,同时把下一条指令的地址记录下来,方便调用完成后返回到应当执行的位置,从而使得main函数完成对Add函数的调用后,接下来的代码能准确运行。

6.Add函数中对形参的引用

为Add函数创建好栈帧,并且创建好整型变量z变量后,开始执行z=x+y对应的汇编代码。

可以看到在这之前是没有创建出变量x和y的,这里调用x和y的时候,使用的是ebp+8和ebp+0Ch的值。从上面可以知道,这两个地址中的值其实就是上面传参时压入main函数栈帧的,存放在寄存器eax和ecx中的值。

所以说,平时我们传递参数的时候,如果采用传值调用,那么因为在被调函数中实际访问和操作的是寄存器eax和ecx中的值,而与传值过去的变量无关,所以在被调函数中对形参的操作不影响原函数中变量的值。

而如果采用传址调用,那么寄存器eax和ecx中存放的就是原函数中变量的地址,程序通过这个地址,就找到原函数中的变量,从而使得被调函数中的操作,改变原函数中变量的值。

在Add函数还未调用的时候,参数就已经先传递过去了,是在main函数的堆栈中,然后才是对Add函数栈帧的创建与初始化。

所以有一句非常形象且正确的话:形参是实参的一份临时拷贝。

7.Add函数返回值,程序返回main函数

返回值的时候,mov指令把ebp-8地址处的值,也就是整型变量z的值,放入寄存器eax当中,这里需要注意的是,寄存器是不会随函数的销毁而销毁的。

edi、esi、ebx三个元素也是Add函数栈帧的一部分,pop指令把edi、esi、ebx三个元素从栈顶依次弹出。

mov指令把寄存器ebp中的值赋给寄存器esp。

pop指令把ebp处的元素弹出并且赋给ebp,这里存放的值就是调用Add的函数,也就是main函数原先的栈底地址。所以经过这次pop,栈顶指针和栈底指针,又指向了main函数的栈顶和栈底,返回对main函数的维护。

ret指令返回的时候,其实相当于从栈顶指针处pop掉一个元素,而这个元素就是上面call指令压入的地址,是call指令下一条指令的地址。所以通过ret这个指令,就使内存上返回到了原先调用Add时的状态,并且走到了调用完成后的下一步。

此时传递的形参还在,之后执行的add指令使esp增加8个字节,弹出了传递的两个形参。

mov指令将eax中的值放入ebp-20h中,也就是变量c中,到这里完成了Add函数返回值的操作。

我们知道在一个函数中创建的局部变量,在函数销毁的时候也会同步销毁,而返回值的时候函数已经执行完毕被销毁掉了,那么用函数中的变量返回一个值,是怎么做到的呢?就是通过不随函数销毁而销毁的寄存器完成的,先把局部变量的值放入寄存器,通过寄存器将值返回,就避免了返回失败的问题。

标签:函数,C语言,探究,指令,ebp,寄存器,main,栈帧
From: https://blog.csdn.net/jk23728/article/details/139811839

相关文章

  • C语言基础入门 -1
    一.计算机中单位bit -比特位(最小单位);byte-字节kb-千字节mb-兆字节gb-千兆字节tb-太字节1字节=8比特位;其他单位之间换算为1024;二.数据类型与所占字节char//字符型   1字节short//短整型   2字节int  //整型   4字节long//长整型  ......
  • C语言----自定义类型:联合和枚举
     1.联合体联合体的特点像结构体一样,联合体也是一个或者多个成员构成的,这些成员可以是不同的类型联合体的关键字:union结构体的关键字:struct枚举的关键字:enum但是编译器只为最⼤的成员分配⾜够的内存空间。联合体的特点是所有成员共⽤同⼀块内存空间。所以联合体也叫:共......
  • 【C语言】初识C语言 - 数组
    一、一维数组的创建和初始化1.1一维数组的创建数组的定义:是一种线性数据结构,用于存储相同数据类型的元素的集合。数组中的元素在内存中是连续存储的,并且通过索引来访问。#include<stdio.h>intmain(){ intarr[1]={2}; //数组数据类型为:int //数组名为:arr //......
  • C语言期末复习笔记
    目录一,基础介绍。二,标识符起名规范。三,数据类型。四,变量。五,运算符和表达式1,加减乘除​编辑  /为整除,%为余数,*为乘号2,关系运算符3,逻辑运算符4,运算符优先级5,前自增,后自增6,三目运算符。7,符合运算符。六,控制语句。1,if判断2,多重判断。3,for循环4,while循环5,d......
  • C语言程序设计-11 结构体与共用体
    11.1定义一个结构的一般形式11.2结构类型变量的说明1.先定义结构,再说明结构变量。2.在定义结构类型的同时说明结构变量。3.直接说明结构变量。11.3结构变量成员的表示方法11.4结构变量的赋值结构变量的赋值就是给各成员赋值。可用输入语句或赋值语句来完成。......
  • C语言程序设计-10 指针
    指针是C语言中广泛使用的一种数据类型。运用指针编程是C语言最主要的风格之一。利用指针变量可以表示各种数据结构;能很方便地使用数组和字符串;并能象汇编语言一样处理内存地址,从而编出精练而高效的程序。指针极大地丰富了C语言的功能。学习指针是学习C语言中最重要的一环,能......
  • C语言程序设计-8 函 数
    8.1概述在前面已经介绍过,C源程序是由函数组成的。虽然在前面各章的程序中大都只有一个主函数main(),但实用程序往往由多个函数组成。函数是C源程序的基本模块,通过对函数模块的调用实现特定的功能。C语言中的函数相当于其它高级语言的子程序。C语言不仅提供了极为丰富的库函......
  • linux下C语言如何操作文件(四)
    structstring_segment*parse_path(constchar*file_path)函数,解析路径,将一个linux的路径分成两部分路径和文件名。返回一个字符串分段接头体指针,里边有三个字符串。第0个是原始的file_path,第二个是已经拆分好的路径,第三个是文件名部分。代码如下:/***解析linux下的路径......
  • C语言犯错集锦
    .h和.c文件全局变量定义和声明报错:error#10056:symbol"_bChannel"redefined:firstdefinedin"./src/Source/schedule.obj";redefinedin"./src/Source/main.obj"//错误:仅在.h中定义并初始化//正确://全局变量:需要在.h中用extern声明,在.c中定义并初始化,如果是......
  • C语言 计算平闰年,某个月有多少天
    #include<stdio.h>intisLeapYear(intyear){   if((year%4==0&&year%100!=0)||(year%400==0))       return1; //闰年返回1   return0; //平年返回0}intgetDaysInMonth(intyear,intmonth){   intdays;   sw......