函数栈帧的创建与销毁

我是目录

  • 环境
  • 理解栈帧
  • 函数栈帧图
  • 预备知识
    • 寄存器
    • MOV 指令
    • SUB 指令
    • PUSH 指令
    • POP 指令
    • LEA 指令
    • CALL 指令
    • REP STOS 指令
  • 一个简单的C程序
    • 栈帧创建
    • 栈帧销毁
  • 如何传参
    • 数值参数
    • 变量参数
  • 如何返回值
    • 数值返回
    • 变量返回

环境

集成环境:VS2022 x86

编辑语言:C

汇编语言:MASM

理解栈帧

为何函数创建需要栈帧?

我知道函数的代码段地址,直接调用就好啦,为什么还需要给他创建一个栈帧来管理呢?

  • 管理函数的局部变量和参数:每个函数都有自己独立的局部变量和参数,它们的生命周期仅在该函数执行期间有效。栈帧为每个函数分配一块内存空间,以便保存这些局部变量和参数。这样当函数调用结束后,栈帧会从栈中释放,局部变量的内存也会随之回收,不会影响到其他函数。
  • 保存返回地址:在函数调用过程中,需要记录调用者的返回地址,以便在函数执行完毕后能够返回到正确的位置继续执行。而栈帧中通常包含了调用函数的返回地址,确保调用关系能够正确恢复

以上两点是主要原因

为何要学习栈帧的创建与销毁?

大家都说,函数栈帧对我们而言是透明的,那为什么我们还要学习他的创建与销毁呢?

  • 掌握底层编程和逆向工程:在低级语言编程(如汇编、C语言)或逆向工程中,栈帧知识尤为重要。在汇编语言中,栈帧的创建和管理是编程者的直接责任。而逆向工程和安全领域的很多操作都基于栈帧的概念,比如分析函数调用关系、漏洞利用中的缓冲区溢出等。
  • 优化程序性能:栈帧分配和释放开销较小,但在高频函数调用中,栈帧的频繁创建与销毁会影响性能。理解栈帧结构可以帮助我们写出更高效的代码,尤其是在需要手动优化的场景中。比如在一些嵌入式系统中,栈空间有限,优化栈帧的使用能避免不必要的内存开销。

函数栈帧图

在这里插入图片描述

main函数的栈帧由esp、ebp负责管理。当调用函数时,被调用的函数会被压入栈帧(从高地址往低地址走)

预备知识

由于函数栈帧的创建与汇编代码紧密结合,所以必须要有一点汇编基础(一点就够啦),才能彻底看懂栈帧的创建与销毁

寄存器

通用寄存器:eax,ebx,ecx,edx,ebp,esp…。这些寄存器中可以存放数值(如:1、2、0aH、01001010B…)、也可存放地址(如:12345678H…)

其中ebp,esp是今天的主角:他们分别用来管理栈底&栈顶地址,用来维护函数栈帧

MOV 指令

MOV 操作数1,操作数2

将操作数2的值赋给操作数1

SUB 指令

SUB 操作数1,操作数2

将操作数1的值减去操作数2

PUSH 指令

PUSH 操作数

将操作数的值压入栈顶,并且将当前栈顶指针esp的值-=操作数的占用空间(类型所占空间)

POP 指令

POP 操作数

将栈顶指针esp所指向的地址按照类型取值赋给操作数,然后esp的值+=操作数的占用空间(类型所占空间)(如:操作数是word类型,便取出esp所指向的两个字节赋给操作数)

LEA 指令

LEA 操作数1,操作数2

将操作数2的地址(而不是值)放入操作数1中(如:lea eax,buffer,让eax存放buffer的地址)

CALL 指令

CALL 操作数

调转到指定地址

REP STOS 指令

该指令结合了重复操作前缀 (REP) 和 存储字符串操作 (STOS),实现对一段内存的快速初始化。

一个简单的C程序

栈帧创建

我们将从一个最简单的C程序开始,学习栈帧的创建与销毁。

在这个程序中,没有形参、返回值

#include<stdio.h>
void func()
{printf("i am func\n");
}
int main()
{func();return 0;
}

反汇编代码如下(只需关注func函数的创建、销毁过程):

//main
int main()
{
//...func();
002A18F2  call        _func (02A1104h)  
//...
}//func
void func()
{
002A1870  push        ebp  
002A1871  mov         ebp,esp  
002A1873  sub         esp,0C0h  
002A1879  push        ebx  
002A187A  push        esi  
002A187B  push        edi  
002A187C  mov         edi,ebp  
002A187E  xor         ecx,ecx  
002A1880  mov         eax,0CCCCCCCCh  
002A1885  rep stos    dword ptr es:[edi]  
002A1887  mov         ecx,offset _74B3F4D1_test@c (02AC008h)  
002A188C  call        @__CheckForDebuggerJustMyCode@4 (02A132Fh)  
002A1891  nop  printf("i am func\n");
002A1892  push        offset string "i am func\n" (02A7B30h)  
002A1897  call        _printf (02A10D2h)  
002A189C  add         esp,4  	
}
  1. **002A18F2 call _func (02A1104h) **

    main 函数中调用 func,执行 call 跳转指令,跳转到 func 函数的地址。这时开始准备创建新的栈帧。

  2. 002A1870 push ebp

    将当前的 EBP 寄存器值压入栈中。这一步的作用是在将来函数返回时能够恢复调用者的栈帧(即 main 函数的栈帧)。同时,ESP 会减少 4 字节(ESP = ESP - 4),指向栈顶的低 4 字节。需要注意的是,栈是从高地址向低地址生长的,因此栈顶的地址小于栈底。

    registerbeforeafter
    esp0x00cff81c0x00cff818
    ebp0x00cff8ec0x00cff8ec

    在这里插入图片描述

  3. 002A1871 mov ebp,esp
    002A1873 sub esp,0C0h

    将当前的 ESP 值赋给 EBP,使得 EBP 成为当前栈帧的基指针。此时,EBP 指向该函数栈帧的底部。

    ESP 向下移动 0C0h(192 字节),为该函数的局部变量和其他数据分配栈空间。此时,EBPESP 之间的空间就构成了该函数的栈帧,栈帧的大小是 192 字节。

    在这里插入图片描述

  4. 002A1879 push ebx
    002A187A push esi
    002A187B push edi

    这三条指令用于将 EBXESIEDI 寄存器的值压入栈中。根据 x86 调用约定,非易失性寄存器(如 EBXESIEDI)在函数调用后必须保持不变。如果函数需要修改这些寄存器的值,它必须在使用前将其保存,并在函数结束时恢复。因此,这三条指令将这些寄存器的值保存到栈中,确保它们在函数结束时能够恢复。注意:每次 push 操作时,ESP 的值也会随之变化。

    在这里插入图片描述

  5. 002A187C mov edi,ebp

    将当前栈帧的基指针(EBP)的值存储到 EDI 寄存器中。这样做的目的是为了在访问局部变量时减少对 EBP 的多次访问。通过 EDI 寄存器和偏移量,可以直接定位局部变量的地址。

  6. 002A187E xor ecx,ecx

    通过 xor 操作将 ECX 的值置为 0。xor 是异或操作,ECX 和自身异或的结果是 0,类似于对 ECX 寄存器进行清零。

  7. 002A1880 mov eax,0CCCCCCCCh

    将值 0xCCCCCCCC 移动到 EAX 寄存器中。这个值通常用于在调试过程中标记未初始化的内存区域。

  8. 002A1885 rep stos dword ptr es:[edi]

    使用 REP STOS 指令将 EAX 的值(即 0xCCCCCCCC)以 dword(4 字节)大小填充到 EDI 寄存器指向的地址。由于 ECX 为 0,循环不会执行,因此这一步实际上并没有进行任何内存填充。

    这一点说明,虽然这条指令本应填充内存区域,但由于 ECX = 0,它实际上不会执行任何填充操作。代码可能认为这段内存不需要填充,因此直接跳过了该操作,从而节省了资源。

    在这里插入图片描述

    可以确认,栈帧初始化部分并没有进行实际的内存填充,因此栈帧中的内存部分并未被初始化为 0xCCCCCCCC

到此为止,函数的栈帧已经完成初始化。接下来的步骤涉及其他操作,这些操作与栈帧的创建关系不大,属于函数执行过程中的其他额外步骤。

栈帧销毁

反汇编代码如下:

002A189F  pop         edi  
002A18A0  pop         esi  
002A18A1  pop         ebx  
002A18A2  add         esp,0C0h  
002A18A8  cmp         ebp,esp  
002A18AA  call        __RTC_CheckEsp (02A1253h)  
002A18AF  mov         esp,ebp  
002A18B1  pop         ebp  
002A18B2  ret  
  1. 002A189F pop edi
    002A18A0 pop esi
    002A18A1 pop ebx

    这三条指令将栈顶的 EDIESIEBX 寄存器值从栈中弹出,恢复它们在函数调用前的值。每次执行 POP 指令时,栈指针 ESP 都会增加 4 字节(ESP = ESP + 4),因此总共会增加 12 字节,使 ESP 恢复到这些寄存器值被保存之前的位置。

    在这里插入图片描述

  2. 002A18A2 add esp,0C0h
    002A18A8 cmp ebp,esp
    002A18AA call __RTC_CheckEsp (02A1253h)

    这一指令将 ESP 的值增加 0C0h(192 字节),恢复栈指针到之前为局部变量分配栈空间的位置。这步操作是为了清理当前函数栈帧的空间,确保栈指针正确指向调用函数(main)的栈顶。

    通过 CMP 指令比较 EBPESP 的值。EBP 保存了函数栈帧的底部地址,而 ESP 保存了栈顶的地址。如果栈帧没有被修改,EBPESP 的值应当相同。如果两者不相等,说明栈帧遭到了非法修改或者栈溢出,可能会导致数据损坏。

    如果 EBPESP 的值不同,就会执行 call 跳转到 Microsoft 运行时检查函数 __RTC_CheckEsp。该函数用于检测栈是否被非法修改或栈是否越界,通常用于调试时捕捉栈相关的错误。

    在这里插入图片描述

  3. 002A18AF mov esp,ebp
    002A18B1 pop ebp
    002A18B2 ret

    无论是否发生栈溢出或非法修改,执行完运行时检查后,将 ESP 恢复到 EBP 的位置。这样做是为了保证栈指针在函数返回时处于正确的位置,从而避免栈损坏。

    恢复 EBP 寄存器的值,即恢复原来保存的 main 函数的栈帧基指针。通过这一步,栈指针恢复到 main 函数调用 func 前的状态。

    最后,RET 指令将控制权返回给调用者,即返回到 main 函数的后续代码。这也标志着当前函数(func)的执行结束,栈帧被清理。

    在这里插入图片描述

如何传参

数值参数

让我们对代码进行一些小小的修改,以便验证如何使用栈帧进行传参的

void func(int a)
{printf("i am func %d\n",a);
}int main()
{func(1);return 0;
}

反汇编代码如下:

int main()
{
//...func(1);
005318F2  push        1  
005318F4  call        _func (0531104h)  
//... 
}void func(int a)
{
00531870  push        ebp  
00531871  mov         ebp,esp  
00531873  sub         esp,0C0h  
00531879  push        ebx  
0053187A  push        esi  
0053187B  push        edi  
0053187C  mov         edi,ebp  
0053187E  xor         ecx,ecx  
00531880  mov         eax,0CCCCCCCCh  
00531885  rep stos    dword ptr es:[edi]  
00531887  mov         ecx,offset _74B3F4D1_test@c (053C008h)  
0053188C  call        @__CheckForDebuggerJustMyCode@4 (053132Fh)  
00531891  nop  printf("i am func %d\n",a);
//...
}
  1. 005318F4 call _func (0531104h)

    call 指令之前,通常会有一条 push 指令将函数参数(例如 1)压入栈中。这是因为在 x86 架构中,函数的参数通常是通过栈传递的。栈的操作是从高地址到低地址依次压入参数。

    在这里插入图片描述

    在这里插入图片描述

  2. 中间过程与前面创建的一样

  3. 00531887 mov ecx,offset _74B3F4D1_test@c (053C008h)

    之前将 1 压入栈中后,指令 mov ecx, offset _74B3F4D1_test@c (053C008h)1 的值传递到 ecx 寄存器。这是 x86 调用约定中的一种常见做法,尤其是在使用快速调用约定(如 fastcall)时,函数的第一个参数通常会通过 ecx 寄存器传递。

    在这里插入图片描述

变量参数

  1. 单个变量传递

    int main()
    {int a=10;func(a);return 0;
    }
    
    009618FD  mov         eax,dword ptr [a]  
    00961900  push        eax  
    00961901  call        _func (0961104h)  
    

    这段代码将实参 a 的值加载到 eax 寄存器中,然后将其压入栈中,最终调用 func 函数。对于单个形参的函数,传递实参的过程非常简单,就是将实参的值拷贝到 eax 中,然后将其压入栈中。

    对于传值的方式,所有的传值都会涉及到将原始的值拷贝一次,然后传递给调用函数。这就是为什么在函数内部修改值,函数外部不会有任何效果,因为传递的是值的副本。

  2. 多个变量传递

    	func(a,b,c,d,e,f);
    01014700  mov         eax,dword ptr [f]  
    01014703  push        eax  
    01014704  mov         ecx,dword ptr [e]  
    01014707  push        ecx  
    01014708  mov         edx,dword ptr [d]  
    0101470B  push        edx  
    0101470C  mov         eax,dword ptr [c]  
    0101470F  push        eax  
    01014710  mov         ecx,dword ptr [b]  
    01014713  push        ecx  
    01014714  mov         edx,dword ptr [a]  
    01014717  push        edx  
    01014718  call        _func (01011104h)  
    

    在这段代码中,我们看到传递了多个参数。实参依然是从右向左依次压入栈中的,确保栈顶是最右边的参数(f),而栈底是最左边的参数(a)。

    形参和实参的对应关系:

    对于只有单个形参的函数,栈中只有一个参数,形参对应的地址可以直接访问。然而,对于多形参函数,如何确保每个形参与其对应的实参正确匹配呢?

    1. 参数的压栈顺序:
      根据调用约定(如 cdecl),栈中最先压入的就是最右侧的参数(即函数调用中的最后一个参数),最后压入的是最左侧的参数(即函数声明中的第一个形参)。这种顺序保证了形参与实参的正确对应。
    2. 栈帧结构:
      栈帧是由 ebp 指针标识的,它提供了一个固定的基准点。每个参数都会根据栈帧的布局,通过一个固定的偏移量来访问。具体来说,ebp 指向栈帧的底部,紧接着是函数的返回地址和保存的上一层 ebp,然后才是函数的各个形参。

    通过栈帧的结构,编译器能够准确地通过相对于 ebp 的偏移量来访问每个参数。例如,在调用 func(a, b, c, d, e, f) 时:

    • f 会存储在栈的最顶端,ebp+12
    • e 存储在 ebp+16
    • d 存储在 ebp+20
    • c 存储在 ebp+24
    • b 存储在 ebp+28
    • a 存储在 ebp+32

    这样,通过栈帧指针 ebp 和相应的偏移量,编译器可以确保形参和实参的正确对应。

  3. 指针传递

    指针也是变量,所以和变量的方法一样:压栈、压栈、压栈!!不过由于其是指针,所以形参声明的类型也是指针,这样在被调用的函数中就拥有访问变量地址以实现永久修改变量值的能力了

如何返回值

数值返回

int func()
{printf("i am func\n");return 1;
}int main()
{func();return 0;
}
	return 1;
0081189F  mov         eax,1

将要返回的值放入eax寄存器中,然后在外面就可以通过访问该寄存器访问到返回值

变量返回

  1. 变量返回

    int func()
    {printf("i am func\n");int a=10;return a;
    }int main()
    {func();return 0;
    }
    
    	return a;
    00C418AA  mov         eax,dword ptr [a] 
    

    将要返回的变量的值放入eax中,然后在外面就可以通过访问该寄存器访问到返回值

  2. 指针返回

    int* func()
    {printf("i am func\n");int* a = malloc(sizeof(int));*a = 10;return a;
    }int main()
    {//int a=0, b=0, c=0,d=0,e=0,f=0;func();return 0;
    }
    
    	return a;
    008418B9  mov         eax,dword ptr [a] 
    

    和变量返回一样,不过由于声明的是指针,所以编译器和你都知道该通过解引用的方式访问。然后在外面就可以对eax中存储的地址进行访问,进而访问到返回值了

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.xdnf.cn/news/17313.html

如若内容造成侵权/违法违规/事实不符,请联系一条长河网进行投诉反馈,一经查实,立即删除!

相关文章

服务端高并发分布式结构进阶之路

序言 在技术求知的旅途中&#xff0c;鉴于多数读者缺乏在中大型系统实践中的亲身体验&#xff0c;难以从宏观角度把握某些概念&#xff0c;因此&#xff0c;本文特选取“电子商务应用”作为实例&#xff0c;详细阐述从百级至千万级并发场景下服务端架构的逐步演变历程。同时&am…

Linux:版本控制器git和调试工具cgdb

✨✨所属专栏&#xff1a;Linux✨✨ ✨✨作者主页&#xff1a;嶔某✨✨ 版本控制器 为了能够更⽅便我们管理这些不同版本的⽂件&#xff0c;便有了版本控制器。所谓的版本控制器&#xff0c;就是能让你了解到⼀个⽂件的历史&#xff0c;以及它的发展过程的系统。通俗的讲就是⼀…

【0x001C】HCI_Write_Page_Scan_Activity详解

目录 一、命令概述 二、命令格式和参数说明 2.1. HCI_Write_Page_Scan_Activity命令格式 2.2. Page_Scan_Interval 2.3. Page_Scan_Window 三、响应事件及参数说明 3.1. HCI_Command_Complete事件 3.2. Status 3.3. 示例 四、命令执行流程 4.1. 命令发起阶段(主机端…

云原生之运维监控实践-使用Prometheus与Grafana实现对Nginx和Nacos服务的监测

背景 如果你要为应用程序构建规范或用户故事&#xff0c;那么务必先把应用程序每个组件的监控指标考虑进来&#xff0c;千万不要等到项目结束或部署之前再做这件事情。——《Prometheus监控实战》 去年写了一篇在Docker环境下部署若依微服务ruoyi-cloud项目的文章&#xff0c;当…

突破工业管理新高度:AI多模态引擎赋能设备维护管理

结合AI技术&#xff0c;可以帮助企业提升设备维护效率和管理复杂信息的能力。以下是一个详细流程和思路&#xff1a; 1. 项目背景概述 在高端制造业领域&#xff0c;如飞机、轮船、光刻机等设备的操作手册及零件图纸涉及大量的零配件信息和操作维护流程。传统方式难以高效管理…

C++重写和重定义和重载

重写 概念&#xff1a; 重写发生在类的继承体系中&#xff0c;是指在派生类中重新定义基类中已声明为虚函数&#xff08;使用 virtual 关键字修饰&#xff09;的函数。其目的是让派生类根据自身的需求对基类的虚函数提供不同的具体实现&#xff0c;从而实现运行时多态。 规则及…

centos7在使用yum源安装依赖时报错

1.在centos7中使用yum命令时候报错如下类似信息&#xff1a; Loading mirror speeds from cached hostfile Could not retrieve mirrorlist http://mirrorlist.centos.org/?release7&archx86_64&repoos&infrastock error was 14: curl#6 - "Could not resol…

小版本大不同 | Navicat 17 新增 TiDB 功能

近日&#xff0c;Navicat 17 迎来了小版本更新。此次版本新增了对 PingCap 公司的 TiDB 开源分布式关系型数据库的支持&#xff0c;进一步拓展了 Navicat 的兼容边界。即日起&#xff0c;Navicat 17 所有用户可免费升级至最新版本&#xff0c;通过 Navicat 工具实现 TiDB 数据库…

python 编程 在 Matplotlib 中 默认预定的所有颜色,可以使用多种方法来指定颜色,包括预定义的颜色名称、十六进制颜色代码、

在 Matplotlib 中&#xff0c;可以使用多种方法来指定颜色&#xff0c;包括预定义的颜色名称、十六进制颜色代码、RGB 元组等。如果你想要一个比较深的颜色&#xff0c;你可以选择一些预定义的深色名称&#xff0c;或者使用较低的亮度值来定义自己的颜色。 以下是一些预定义的…

【基于Java Springboot敬老院管理系统

一、作品包含 源码数据库设计文档万字全套环境和工具资源部署教程 二、项目技术 前端技术&#xff1a;Html、Css、Js、Vue、Element-ui 数据库&#xff1a;MySQL 后端技术&#xff1a;Java、Spring Boot、MyBatis 三、运行环境 开发工具&#xff1a;IDEA/eclipse 数据库…

JRebel插件,全教程

JRebel是一套JavaEE开发工具。相信大家都用过&#xff0c;但是频繁的需要激活&#xff0c;已经让java开发者烦不胜烦。 本篇文章来给大家解决这个烦恼。当然没有用过的同行&#xff0c;我也跟大家介绍一下: 简单来说&#xff0c;Jrebel 可快速实现热部署&#xff0c;在本地开发…

PPPoE技术详解

一 &#xff0c; 背景 随着运营商对宽带接入技术要求的不断提高&#xff0c;以xDSL&#xff0c;CableModem和以太网为主的几种宽带接入技术在用户管理和计费等方面的不足开始显露&#xff0c;已无法满足运营商的需求。 在众多的技术中&#xff0c;以太网接入方式经济实惠&…

[JAVA]MyBatis环境配置介绍

什么是MyBatis环境配置&#xff1f; MyBatis是基于JDBC对数据库进行操作&#xff0c;在我们进行数据操作时&#xff0c;我们需要告诉MyBatis我们连接哪个数据库&#xff0c;ip地址&#xff0c;数据库名称&#xff0c;用户名密码等。以此来进行环境配置。 首先&#xff0c;MyB…

Javascirpt时区——脱坑指南

最近业务反馈了一个约课功能的问题&#xff0c;澳大利亚的用户反馈&#xff0c;无法进行选课。排查之后发现是时区不对引起的&#xff0c;由于时区的偏差已经超过时间&#xff0c;导致无法选课。 这里对js中处理时区的问题做一些总结。 时区 时区&#xff08;Time Zone&#xf…

不用来回切换,一个界面管理多个微信

你是不是也有多个微信号需要管理&#xff1f; 是不是也觉得频繁切换账号很麻烦&#xff1f; 是不是也想提升多账号管理的效率&#xff1f; 在工作中&#xff0c;好的辅助工具&#xff0c;能让我们的效率加倍增长&#xff01; 今天&#xff0c; 就给大家分享一个多微管理工具…

每日OJ题_牛客_AB32【模板】哈夫曼编码_C++_Java

目录 牛客_AB32【模板】哈夫曼编码 题目解析 C代码 Java代码 牛客_AB32【模板】哈夫曼编码 【模板】哈夫曼编码_牛客题霸_牛客网 描述&#xff1a; 给出一个有n种字符组成的字符串&#xff0c;其中第ii种字符出现的次数为ai​。请你对该字符串应用哈夫曼编码&#xff0c;…

UDP协议

​ UDP协议 前置知识一、应用层的进程为什么要bind端口号二、如何确定网络中的一个进程三、进程 服务 协议 端口之间的关系四、常见的协议对应的端口五、一些命令六、一个进程能不能绑定多个端口号&#xff0c;一个端口号能不能被多个进程绑定七、对任何一个协议报文的认识 UD…

KkFileView4.1.0部署文档--linux

先看下官方文档&#xff1a;kkFileView - 在线文件预览 环境要求中的JDK8如果没有的&#xff0c;需先安装JDK8&#xff0c;这里不做展示。 第二个office相关环境要求在linux中会自动下载安装&#xff0c;不用管。 1、下载地址 Linux 或 MacOS 版&#xff1a; https://kkfil…

[论文笔记]An LLM Compiler for Parallel Function Calling

引言 今天带来一篇优化函数调用的论文笔记——An LLM Compiler for Parallel Function Calling。 为了简单&#xff0c;下文中以翻译的口吻记录&#xff0c;比如替换"作者"为"我们"。 当前的函数(工具)调用方法通常需要对每个函数进行顺序推理和操作&…

基于JAVA的资源检索系统(源码+定制+开发)

博主介绍&#xff1a; ✌我是阿龙&#xff0c;一名专注于Java技术领域的程序员&#xff0c;全网拥有10W粉丝。作为CSDN特邀作者、博客专家、新星计划导师&#xff0c;我在计算机毕业设计开发方面积累了丰富的经验。同时&#xff0c;我也是掘金、华为云、阿里云、InfoQ等平台…