网站开发规范有哪些,安装wordpress时出现空白,企业为什么要做网络营销推广,aso安卓优化目录 1.函数栈帧是什么?
2. 理解函数栈帧能解决什么问题
3、函数栈帧的创建和销毁具体过程
3.1 什么是栈
3.2 认识相关寄存器和汇编指令
3.3函数栈帧的创建和销毁
3.3.1 预备知识
3.3.2 函数的调用堆栈
3.3.3 准备环境
3.3.4 转到反汇编
3.3.5 函数栈帧的创建
3.3…目录 1.函数栈帧是什么?
2. 理解函数栈帧能解决什么问题
3、函数栈帧的创建和销毁具体过程
3.1 什么是栈
3.2 认识相关寄存器和汇编指令
3.3函数栈帧的创建和销毁
3.3.1 预备知识
3.3.2 函数的调用堆栈
3.3.3 准备环境
3.3.4 转到反汇编
3.3.5 函数栈帧的创建
3.3.6 函数栈帧的销毁
相关概念知识(辅助理解):
1.栈Stack
2. esp 和 ebp 的作用
3. 寄存器
1.通用寄存器General-Purpose Registers
2. 段寄存器Segment Registers
3. 控制寄存器Control Registers
4. 关键寄存器详解 1.函数栈帧是什么?
在C语言中书写代码时 我们通常会把一个独立的功能用函数来实现 不同的函数用来实现不同的功能, 所以C程序是以函数为基本单位的。 那函数是如何被调用的 函数的返回值又是如何待会的 函数的形参和实参是如何传递的 这些问题都和函数栈帧有关系。
函数栈帧Stack Frame 是函数运行时在内存栈Stack中占用的一个独立空间用来存储该函数运行所需的所有临时数据。 独立空间所存放的数据包括: 函数参数和函数返回的地址(函数返回值) 旧的基指针(保存调用者Caller的栈帧基址EBP/RBP) 局部变量和临时数据 2. 理解函数栈帧能解决什么问题
只要理解了函数栈帧的创建和销毁就能大概弄懂一下的问题 局部变量是如何创建的为什么局部变量不初始化内容是随机的函数调用时参数时如何传递的传参的顺序是怎样的函数的形参和实参分别是怎样实例化的函数的返回值是如何带会的 3、函数栈帧的创建和销毁具体过程
3.1 什么是栈
栈Stack是现代计算机程序的核心基础之一几乎所有程序都依赖它运行。
简单来说栈就像一个严格遵守后来先出规则的容器数据像叠盘子一样被压入push栈顶取出时也只能从最上面弹出pop。
在计算机中栈是一块特殊的内存区域由CPU通过栈指针寄存器如x86架构的ESP/RSP自动管理随着数据压入栈顶指针向低地址移动栈向下增长弹出时则向高地址回退。
正是这个精巧的设计使得函数调用、局部变量存储、参数传递等关键功能得以实现可以说没有栈就没有现代编程语言中的函数概念。
3.2 认识相关寄存器和汇编指令
相关寄存器: 1.eax通用寄存器保留临时数据常用于返回值 2.ebx通用寄存器保留临时数据 3.ebp栈底寄存器 4.esp栈顶寄存器 5.eip指令寄存器保存当前指令的下一条指令的地址 汇编指令: 1.call:保存下一条指令地址返回地址到栈顶并跳转到目标函数 2.ret:从栈顶弹出返回地址跳转回调用位置继续执行。 3.push:将数据压入栈顶栈指针下移栈向低地址增长。 4.pop:从栈顶弹出数据栈指针上移。 5.enter:建立新栈帧保存旧帧指针分配局部变量空间。 6.leave:撤销当前栈帧恢复旧帧指针和栈指针。 7.mov (ebp/esp):直接操作帧指针ebp或栈指针esp用于调整栈结构。 8.sub/add (esp):动态调整栈空间如分配/释放局部变量。 3.3函数栈帧的创建和销毁
3.3.1 预备知识
首先我们达成一些预备知识才能有效的帮助我们理解函数栈帧的创建和销毁。
1.每一次函数调用都要为本次函数调用开辟空间就是函数栈帧的空间。
2.这块空间的维护是使用了2个寄存器 esp 和 ebp ebp 记录的是栈底的地址 esp 记录的是栈顶的地址。
3. 函数栈帧的创建和销毁过程在不同的编译器上实现的方法大同小异。
如图: 3.3.2 函数的调用堆栈
演示代码
#define _CRT_SECURE_NO_WARNINGS
#includestdio.h
int Mystrlen(char* arr)
{if (*arr ! \0)return 1Mystrlen(arr1);elsereturn 0;
}
int main()
{char arr1[10] abcdedg;int len Mystrlen(arr1);printf(%d, len);return 0;
}
这段代码如果我们在VS2022编译器上调试调试进入Mystrlen函数后我们就可以观察到函数的调用堆栈右击勾选【显示外部代码】如下图 打开方法: 通过菜单栏打开: 启动调试按 F5 或点击 调试 ) 开始调试在调试状态下,点击菜单栏的 调试 (Debug)选择 窗口 (Windows) 调用堆栈 (Call Stack)。 快捷键Ctrl Alt C默认。 函数调用堆栈是反馈函数调用逻辑的那我们可以清晰的观察到 main 函数调用之前是由 invoke_main 函数来调用main函数。在 invoke_main 函数之前的函数调用我们就暂时不考虑了。那我们可以确定 invoke_main 函数应该会有自己的栈帧 main 函数和 Add 函数也会维护自己的栈帧每个函数栈帧都有自己的 ebp 和 esp 来维护栈帧空间。那接下来我们从main函数的栈帧创建开始讲解
3.3.3 准备环境
为了让我们研究函数栈帧的过程足够清晰不要太多干扰我们可以关闭下面的选项让汇编代码中排除一些编译器附加的代码 3.3.4 转到反汇编
调试到main函数开始执行的第一行右击鼠标转到反汇编。 注VS编译器每次调试都会为程序重新分配内存课件中的反汇编代码是一次调试代码过程中数据每次调试略有差异。
int main()
{
//函数栈帧的创建
00007FF79DB71900 push rbp
00007FF79DB71902 push rdi
00007FF79DB71903 sub rsp,148h
00007FF79DB7190A lea rbp,[rsp20h]
00007FF79DB7190F lea rdi,[rsp20h]
00007FF79DB71914 mov ecx,2Ah
00007FF79DB71919 mov eax,0CCCCCCCCh
00007FF79DB7191E rep stos dword ptr [rdi]
00007FF79DB71920 mov rax,qword ptr [__security_cookie (07FF79DB7D000h)]
00007FF79DB71927 xor rax,rbp
00007FF79DB7192A mov qword ptr [rbp118h],rax
00007FF79DB71931 lea rcx,[__167BB7BA_源c (07FF79DB82008h)]
00007FF79DB71938 call __CheckForDebuggerJustMyCode (07FF79DB71370h)
00007FF79DB7193D nop
//main函数中的核心代码char arr1[10] abcdedg;
00007FF79DB7193E mov rax,qword ptr [string abcdedg (07FF79DB7AC70h)]
00007FF79DB71945 mov qword ptr [arr1],rax
00007FF79DB71949 lea rax,[rbp60h]
00007FF79DB7194D mov rdi,rax
00007FF79DB71950 xor eax,eax
00007FF79DB71952 mov ecx,2
00007FF79DB71957 rep stos byte ptr [rdi] int len Mystrlen(arr1);
00007FF79DB71959 lea rcx,[arr1]
00007FF79DB7195D call Mystrlen (07FF79DB713DEh)
00007FF79DB71962 mov dword ptr [len],eax printf(%d, len);
00007FF79DB71968 mov edx,dword ptr [len]
00007FF79DB7196E lea rcx,[string %d (07FF79DB7ACB4h)]
00007FF79DB71975 call printf (07FF79DB7119Ah)
00007FF79DB7197A nop return 0;
00007FF79DB7197B xor eax,eax
}
3.3.5 函数栈帧的创建
这里我看到 main 函数转化来的汇编代码如上所示。接下来我们就一行行拆解汇编代码
00007FF79DB71900 push rbp
//将调用者如invoke_main的栈基址rbp压栈保存esp自动-8x64下指针占8字节
//此时rsp指向栈顶保存了旧的rbp值00007FF79DB71902 push rdi
//保存rdi寄存器的值到栈中x64调用约定中rdi可能被调用者修改esp再-800007FF79DB71903 sub rsp,148h
//给main函数分配栈空间rsp减去0x148字节328字节
//现在rsp指向main函数栈帧的顶部与后续的rbp构成栈帧范围00007FF79DB7190A lea rbp,[rsp20h]
//设置main函数的栈基址rbp rsp 0x20
//这样rbp到rsp之间保留0x20字节可能用于调试或局部变量00007FF79DB7190F lea rdi,[rsp20h]
//将rdi指向栈初始化区域的起始地址rbp的位置准备填充0xCC00007FF79DB71914 mov ecx,2Ah
//设置循环次数ecx 0x2A42次每次处理4字节共初始化42*4168字节00007FF79DB71919 mov eax,0CCCCCCCCh
//用调试模式填充值0xCCCCCCCC初始化栈空间未初始化内存的标记00007FF79DB7191E rep stos dword ptr [rdi]
//从rdi指向的地址开始重复填充eax的值(0xCCCCCCCC)到内存共ecx次
//相当于初始化[rbp-0x20]到[rbp0xA8]的范围168字节00007FF79DB71920 mov rax,qword ptr [__security_cookie (07FF79DB7D000h)]
// 从全局变量加载安全cookie栈溢出保护值到rax00007FF79DB71927 xor rax,rbp
//将安全cookie与当前栈基址rbp异或生成唯一校验值00007FF79DB7192A mov qword ptr [rbp118h],rax
//将校验值存入栈中[rbp0x118]的位置函数返回时会验证是否被篡改00007FF79DB71931 lea rcx,[__167BB7BA_源c (07FF79DB82008h)]
//加载调试信息符号地址到rcx用于Just My Code调试功能00007FF79DB71938 call __CheckForDebuggerJustMyCode (07FF79DB71370h)
//调用VS调试器检查函数确认是否在调试模式下运行00007FF79DB7193D nop
//空指令用于对齐或预留调试断点位置
上面的这段代码,等价于下面的伪代码:
void main() {// 1. 保存调用者的栈基址和寄存器push(rbp); // 保存invoke_main的rbppush(rdi); // 保存可能被修改的rdi// 2. 分配栈空间x64下更大rsp - 0x148; // 分配328字节空间// 3. 设置新的栈基址跳过预留区域rbp rsp 0x20; // rbp指向有效栈帧起始处// 4. 初始化栈空间填充0xCCrdi rbp; // 初始化起始地址ecx 42; // 循环次数42次×4字节168字节eax 0xCCCCCCCC;memset(rdi, eax, ecx * 4); // 填充168字节// 5. 栈溢出保护x64特有rax __security_cookie; // 加载安全cookierax ^ rbp; // 与栈基址异或加密*(rbp 0x118) rax; // 存储校验值// 6. 调试检查VS特有if (IsDebuggerPresent()) { // 检查调试器__CheckForDebuggerJustMyCode(); // 调试钩子}
}
小知识 : 烫烫烫烫烫烫烫烫烫烫烫烫 出现 “烫烫烫……” 的原因是在 Windows 下未初始化的栈内存可能会被初始化为 0xCC 而 0xCC 对应的字符在当前字符编码下显示为 “烫” 。
接下来我们再分析main函数中的核心代码:
1. 初始化字符数组 arr1[10] abcdedg;
00007FF79DB7193E mov rax, qword ptr [string abcdedg (07FF79DB7AC70h)]
//从全局数据段地址 07FF79DB7AC70h加载字符串 abcdedg 的前 8 字节到 rax。
//由于 abcdedg 是 7 字节含 \0rax 会包含a,b,c,d,e,d,g,\000007FF79DB71945 mov qword ptr [arr1], rax
//将 rax 的值即字符串的前 8 字节存储到 arr1 的起始地址[arr1]。
//此时 arr1 的前 8 字节已填充为 abcdedg\0。00007FF79DB71949 lea rax, [rbp60h]
//计算 arr1 的剩余部分地址rbp60h
//即 arr1[8] 的位置因为 arr1 是 char[10]前 8 字节已填充剩余 2 字节。00007FF79DB7194D mov rdi, rax
//将目标地址 rbp60h 存入 rdistos 指令的目的寄存器。00007FF79DB71950 xor eax, eax
//清零 eax即 al 0\0 字符。00007FF79DB71952 mov ecx, 2
//设置循环次数 ecx 2剩余 2 字节需要填充 \0。00007FF79DB71957 rep stos byte ptr [rdi]
//从 rdi 指向的地址开始重复填充 al0到内存共 ecx 次2 次。
//相当于 arr1[8] \0; arr1[9] \0;确保数组完全以 \0 结尾。
2. 调用 Mystrlen(arr1) 计算字符串长度
00007FF79DB71959 lea rcx, [arr1]
//将 arr1 的地址加载到 rcxx64 调用约定第一个参数用 rcx 传递。00007FF79DB7195D call Mystrlen (07FF79DB713DEh)
//调用 Mystrlen 函数返回值存储在 eax 中。00007FF79DB71962 mov dword ptr [len], eax
//将返回值字符串长度存入局部变量 len。
3. 调用 printf 打印长度
00007FF79DB71968 mov edx, dword ptr [len]
//将 len 的值7存入 edxx64 调用约定第二个参数用 edx 传递。00007FF79DB7196E lea rcx, [string %d (07FF79DB7ACB4h)]
//加载格式字符串 %d 的地址到 rcx第一个参数。00007FF79DB71975 call printf (07FF79DB7119Ah)
//调用 printf输出 7。00007FF79DB7197A nop
//空指令对齐或占位。
4. 返回 0
00007FF79DB7197B xor eax, eax
//将 eax 清零return 0; 的常见优化写法。
3.3.6 函数栈帧的销毁
当函数调用要结束返回的时候前面创建的函数栈帧也开始销毁。那具体是怎么销毁的呢我们看一下反汇编代码。
00007FF773182288 lea rsp, [rbp0C8h]
//将 rsp 直接设置为 rbp 0C8h相当于回收整个函数的栈空间esp ebp 分配的大小
//此时 rsp 指向调用者栈帧的栈顶函数调用前的 rsp 值 00007FF77318228F pop rdi
//从栈顶弹出一个值存放到 rdi 中恢复调用者的 rdi 寄存器rsp 8x64 下指针占 8 字节 00007FF773182290 pop rbp
//从栈顶弹出一个值存放到 rbp 中此时栈顶的值就是调用者的 rbp恢复调用者的栈基址rsp 8 00007FF773182291 ret
//ret 指令的执行
//1. 从栈顶弹出一个值此时栈顶的值就是 call 指令下一条指令的地址rsp 8
//2. 跳转到该地址继续执行调用者的代码
这样之后就会跳转到main函数内继续执行代码
本章结束 以上就是函数栈帧创建和销毁
以下是一些概念知识 需要的可自行阅读 相关概念知识(辅助理解):
1.栈Stack 栈是一种后进先出LIFO的数据结构在内存中从高地址向低地址增长。在函数调用时栈用于 存储函数参数由调用者压栈保存返回地址call指令自动压入保存调用者的ebp被调函数保存分配局部变量存储临时数据如运算中间结果 2. esp 和 ebp 的作用 寄存器全称作用espExtended Stack Pointer始终指向栈的当前顶部最低可用地址随push/pop动态变化ebpExtended Base Pointer指向当前函数栈帧的基地址用于定位局部变量和参数 esp 的特点 动态变化每次push、pop、sub esp, N分配空间或add esp, N释放空间都会改变。在函数调用时esp会调整以容纳新的栈帧。 ebp 的特点 在函数执行期间固定作为局部变量和参数的基准。通过[ebp offset]访问参数[ebp - offset]访问局部变量。 3. 寄存器 寄存器Registers是CPU内部的高速存储单元用于临时存放数据、地址和控制信息。在函数调用和栈帧管理中关键的寄存器包括 通用寄存器、段寄存器 和 控制寄存器 1.通用寄存器General-Purpose Registers 这些寄存器可用于计算、寻址和数据传输主要分为 寄存器名称主要用途eaxAccumulator存放函数返回值、算术运算ebxBase数据存储较少用于计算ecxCounter循环计数如rep指令edxData辅助eax如乘法/除法的高位结果esiSource Index字符串/数组操作的源指针ediDestination Index字符串/数组操作的目标指针espStack Pointer指向栈顶动态变化ebpBase Pointer指向当前栈帧基址固定 2. 段寄存器Segment Registers 用于内存分段现代操作系统已较少使用 寄存器名称用途csCode Segment代码段基址dsData Segment数据段基址ssStack Segment栈段基址esp/ebp默认在此段es, fs, gsExtra Segments附加数据段 3. 控制寄存器Control Registers 寄存器名称用途eipInstruction Pointer指向下一条要执行的指令不可直接修改eflagsFlags存储状态标志如零标志ZF、进位标志CF 4. 关键寄存器详解 1espStack Pointer 作用始终指向栈的当前顶部即最后入栈的数据地址。变化规则 push 时esp 减小栈向低地址增长。pop 时esp 增大。函数调用时esp 会动态调整以分配/释放栈空间。 2ebpBase Pointer 作用指向当前函数栈帧的基地址用于 定位局部变量[ebp - offset]。访问函数参数[ebp offset]。 特点 在函数执行期间固定不变除非手动修改。通过 mov ebp, esp 在函数开头建立栈帧。 本博客借鉴于:函数栈帧的创建与销毁超详解-CSDN博客