2026/4/18 7:17:57
网站建设
项目流程
网站服务器放在哪里好,网页图片格式有哪些,用手机制作网站,中企动力科技股份有限公司沈阳分公司实验2#xff1a;函数调用栈帧机制#xff08;重点难点#xff09;
函数调用的核心是“栈帧的创建与销毁”#xff0c;栈帧是函数运行的独立内存空间#xff0c;用于存储局部变量、参数、返回地址等。本实验通过多参数函数调用#xff0c;拆解栈帧结构与参数传递规则。
#…实验2函数调用栈帧机制重点难点函数调用的核心是“栈帧的创建与销毁”栈帧是函数运行的独立内存空间用于存储局部变量、参数、返回地址等。本实验通过多参数函数调用拆解栈帧结构与参数传递规则。#include stdio.h// 多参数函数计算a b * c - d int calc(int a, int b, int c, int d){int tempb * c;// 局部变量 int resulta temp - d;returnresult;}intmain(){int x5, y3, z4, w6;int rescalc(x, y, z, w);printf(calc(%d, %d, %d, %d) %d\n, x, y, z, w, res);return0;}0:000uf main basic_functions_stack!main[C:\Users\wanni\Desktop\Asm64\basic_functions_stack.c 10]:1000007ff78a8972a0 4883ec38 sub rsp,38h1300007ff78a8972a4 c74424280b000000 mov dword ptr[rsp28h],0Bh1300007ff78a8972ac 488d0d6ddc0700 lea rcx,[basic_functions_stack!string(00007ff78a914f20)]1300007ff78a8972b3 ba05000000 mov edx,51300007ff78a8972b8 c744242006000000 mov dword ptr[rsp20h],61300007ff78a8972c0 41b904000000 mov r9d,41300007ff78a8972c6 41b803000000 mov r8d,31300007ff78a8972cc e865b7ffff call basic_functions_stack!ILT6705(printf)(00007ff78a892a36)1400007ff78a8972d1 33c0 xor eax,eax1500007ff78a8972d3 4883c438addrsp,38h1500007ff78a8972d7 c3 ret一、什么是栈帧Stack Frame 栈帧是每个函数在被调用时在栈上分配的一块私有内存区域用于 存放 函数参数部分或全部 存放 局部变量 保存 返回地址caller 的下一条指令地址 保存 被调用者需保护的寄存器callee-saved registers 栈帧由 调用者caller 和 被调用者callee 共同协作建立和清理。二、x64 调用约定的关键规则Windows在 Windows x64 平台上微软采用如下调用约定三、分析你的 main 函数反汇编 asm basic_functions_stack!main[C:\Users\wanni\Desktop\Asm64\basic_functions_stack.c 10]:1000007ff78a8972a0 4883ec38 sub rsp,38h;分配 0x3856字节栈空间1300007ff78a8972a4 c74424280b000000 mov dword ptr[rsp28h],0Bh;第5个参数实际是局部变量或额外参数1300007ff78a8972ac 488d0d6ddc0700 lea rcx,[string];第1个参数字符串地址 → RCX1300007ff78a8972b3 ba05000000 mov edx,5;第2个参数5 → EDX(RDX)1300007ff78a8972b8 c744242006000000 mov dword ptr[rsp20h],6;放到 rsp20h1300007ff78a8972c0 41b904000000 mov r9d,4;第4个参数 → R9D1300007ff78a8972c6 41b803000000 mov r8d,3;第3个参数 → R8D1300007ff78a8972cc e865b7ffff callprintf;调用printf1400007ff78a8972d1 33c0 xor eax,eax;return0;1500007ff78a8972d3 4883c438addrsp,38h;恢复栈指针销毁栈帧1500007ff78a8972d7 c3 ret;返回 对应的 C 代码推测 intmain(){printf(...,3,4,5,6,11);// 共6个参数return0;}注意printf 是变参函数其参数传递仍遵循 x64 规则。 四、栈帧结构详解以 main 为例 执行 sub rsp, 38h 后栈布局如下从高地址 → 低地址 High Address │ ├──[RSP 20h]← Shadow SpaceforR9(d6)├──[RSP 18h]← Shadow SpaceforR8(c4)├──[RSP 10h]← Shadow SpaceforRDX(b3)├──[RSP 08h]← Shadow SpaceforRCX(a5)├──[RSP 00h]← Return Address 由 CALL 自动压入 │ ├──[RSP - 08h]← 局部变量 temp编译器分配 ├──[RSP - 10h]← padding / 其他局部 │ Low Address ← RSP当前栈顶 注意Shadow Space 虽由caller分配但 callee 可自由使用例如 spill 寄存器值。RSP 0x38 →caller的栈帧返回地址等 RSP 0x30 → Shadow SpaceforR9 第4个参数影子区 RSP 0x28 → Shadow SpaceforR8 可能存放第5/6个参数如 0xB11 RSP 0x20 → Shadow SpaceforRDX → 这里存了6RSP 0x18 → Shadow SpaceforRCX RSP 0x10 → 局部变量 / 对齐填充 RSP 0x08 →... RSP 0x00 → 当前栈顶16字节对齐 Shadow Space0x00 0x1F共32字节必须由 caller即 main分配供 calleeprintf使用。 额外参数第5、6个由于 x64 只用寄存器传前4个第5个开始放栈上。这里[rsp20h]6→ 第5个参数[rsp28h]0Bh(11)→ 第6个参数 所以printf实际收到 RCX字符串地址 RDX5R83R94[rsp20h]6[rsp28h]11注意参数顺序在 C 中是从左到右但第5个参数在栈上是 从右到左压入不过由于是caller直接写内存这里直接按偏移赋值。 结合 Intel 手册解释函数调用机制 ✅1. CALL 指令的行为Intel SDM Vol. 2A, Section3.2 Operation(64-bit mode, near call): text RSP ← RSP −8Memory[RSP]← RIP(address of next instruction after CALL)RIP ← target address(calc)在 main 中执行 call calc 时 CPU 自动将 返回地址即printf前那条指令的地址压入栈顶 RSP 减8 程序跳转到 calc 入口。 这就是栈帧中“返回地址”字段的来源——由硬件自动完成无需软件干预。 ✅2. RET 指令的行为Intel SDM Vol. 2A Operation: text RIP ← Memory[RSP]RSP ← RSP 8在 calc 结尾执行 ret 从当前 RSP 处读取返回地址 RSP 加8恢复到 call 之前的值 控制流跳回 main。 这完成了栈帧的“逻辑销毁”——返回地址被消费控制权交还。 ✅3. 栈指针RSP的操作与栈帧分配 calc 开头sub rsp, 18h 分配24字节栈空间用于 局部变量 temp4 字节 可能的 padding为16字节对齐 callee 保存寄存器空间本例未用 结尾add rsp, 18h 手动释放栈帧x64 中 callee 负责清理自己的局部空间 注意CALL/RET 只管理 返回地址局部变量空间必须由软件显式分配/释放。 这体现了栈帧硬件管理的返回地址 软件管理的局部存储 ✅4. 栈对齐要求Intel SDM Vol.1, Section3.4.114.4 “The stack pointer should be aligned on a16-byte boundary prior to calling a function.” 在 main 调用 calc 前 假设进入 main 时 RSP %168因 CRT 启动代码已对齐 sub rsp, 28h40 字节→40%168所以 RSP 变为(8-8)%160 实际需考虑 CALL 压栈的影响。 更准确地说 调用者main必须确保在执行 call 指令前RSP %168因为 call 会压入8字节使被调用者入口处 RSP %160这样 calc 内部才能安全使用 movaps、pxor 等要求16字节对齐的 SIMD 指令即使本例未用 ⚠️ 若违反对齐某些 SSE 指令会触发#GPGeneral Protection Fault✅5. 控制流转移机制 call calc改变控制流跳转到新函数 ret恢复控制流回到调用点 整个过程通过 栈上的返回地址链 实现嵌套调用如 main → calc → printf Intel 手册强调这种机制支持任意深度的函数嵌套和递归因为每次 call 都压入独立返回地址。 五、栈帧的“创建”与“销毁” 创建 sub rsp, 38h分配栈空间含 shadow space 局部变量 对齐 初始化参数寄存器 栈上 销毁addrsp, 38h释放栈空间恢复 RSP ret弹出返回地址跳回调用者此处是 CRT 启动代码 这体现了“谁分配谁释放”原则main 自己分配的栈空间自己回收。 六、实验意义总结 本实验通过一个多参数printf调用清晰展示了 x64 参数传递机制前4个用寄存器后续用栈 Shadow Space 的强制存在即使不用也必须预留 栈帧的生命周期函数入口分配出口释放 栈对齐的重要性确保 SIMD 指令和系统调用正常工作 调试器如何揭示底层行为通过 uf main 反汇编看到高级语言背后的机器逻辑。 ✅ 结论 函数调用的本质确实是 栈帧的创建与销毁。每一次调用都是一次“上下文隔离”的过程而栈帧就是这个隔离容器。理解它就理解了程序运行时的内存组织核心机制。二、ABI 是栈帧结构的“宪法” ✅ ABIApplication Binary Interface 是连接编译器、链接器、操作系统和 CPU 的桥梁。它明确规定1. 参数如何传递 Windows x64 ABIMicrosoft 整型/指针RCX, RDX, R8, R9 浮点XMM0–XMM3 第5个参数从右到左压栈由caller分配空间 强制32字节 shadow space即使函数只用1个参数 System V ABILinux/macOS 整型RDI, RSI, RDX, RCX, R8, R9 无 shadow space 第7个参数才上栈 同一段 C 代码在 Windows 和 Linux 下汇编完全不同2. 寄存器分类→ 这决定了哪些寄存器需要在栈帧中保存。3. 栈对齐要求 调用时刻RSP %168因为 CALL 会 -8使被调用者入口处 RSP %160 违反 → 某些 SSE 指令崩溃如 movaps4. 返回值传递 整型/指针RAX 大结构体caller 分配内存传隐藏指针作为第一个参数RCX 三、编译器ABI 的忠实执行者或优化者 编译器读取 ABI 规范生成符合要求的代码但也会进行优化 示例你的 calc 函数在不同场景下的实现 场景1Debug 模式保留栈帧 asm 编辑 calc: push rbp mov rbp, rsp sub rsp, 20h;为 temp 和对齐分配空间 mov dword ptr[rbp-4], edx;tempb imul eax, edx, r8d mov dword ptr[rbp-4], eaxaddeax, ecx sub eax, r9d mov rsp, rbp pop rbp ret → 完整栈帧便于调试。 场景2Release 模式优化掉栈帧 asm 编辑 calc: imul eax, edx, r8d;b * caddeax, ecx; a sub eax, r9d;- d ret;无栈操作 → 没有 sub rsp没有局部变量存储因为寄存器足够。 即便如此ABI 仍被遵守参数仍在正确寄存器返回值在 RAX栈对齐依然满足。 四、Intel 手册提供“舞台”不规定“剧本” Intel 手册确保 CALL 一定会压返回地址 RSP 是隐式操作数 内存访问支持任意偏移如[rsp20h] 对齐不当会触发异常 但它不会说 “你应该把第一个参数放 RCX” “必须预留32字节 shadow space” “局部变量要放在[rbp-4]” 这些全是 ABI 的“剧本”。五、完整流程main 调用 calc 的栈帧生命周期✅ 整个过程是硬件、ABI、编译器精密配合的结果。六、总结为什么必须三者结合| 仅看 Intel 手册 | ❌ 不知道参数放哪、栈怎么布局 || 仅看 ABI 文档 | ❌ 不知道 CALL 如何压栈、RSP 如何变化 || 仅看编译器输出 | ❌ 不理解为何这样生成、能否跨平台 | 只有三者结合才能真正掌握函数调用时 CPU 做了什么操作系统 要求我们怎么做编译器 实际上怎么做的而这正是逆向工程、性能调优、系统编程、安全分析的根基。分析递归或异常如 SEH对栈帧的影响func4 函数的C语言等价代码 c // func4的C语言版本 int func4(int a, int b, int c){//aedi,besi,cedx int tc - b;// tedx - esi int signt31;// 取符号位(t0?-1:0)t(t sign)1;// t(t sign)/2int midt b;// mid(c - b)/2 b(b c)/2if(mida){if(mida){return0;}else{// midareturn2* func4(a, mid 1, c)1;}}else{// midareturn2* func4(a, b, mid -1);}}逐行分析汇编 text 400fce: sub$0x8,%rsp;栈空间 400fd2: mov %edx,%eax;eaxc 400fd4: sub %esi,%eax;eaxc - b 400fd6: mov %eax,%ecx;ecxc - b 400fd8: shr$0x1f,%ecx;ecx(c-b)31(符号位)400fdb:add%ecx,%eax;eax(c-b) sign 400fdd: sar %eax;eax((c-b) sign)/2400fdf: lea(%rax,%rsi,1),%ecx;ecx(c-b)/2 b(bc)/2mid;比较 mid 和 a 400fe2:cmp%edi,%ecx;比较 mid 和 a 400fe4: jle 400ff2;如果 mida, 跳转;mida 的情况 400fe6: lea -0x1(%rcx),%edx;cmid -1400fe9: callq 400fcefunc4;递归调用 400fee:add%eax,%eax;结果 *2400ff0: jmp401007;返回;mida 的情况 400ff2: mov$0x0,%eax;eax0400ff7:cmp%edi,%ecx;再次比较 mid 和 a 400ff9: jge401007;如果 mida, 返回0;mida 的情况 400ffb: lea 0x1(%rcx),%esi;bmid 1400ffe: callq 400fcefunc4;递归调用401003: lea 0x1(%rax,%rax,1),%eax;eax2*result 1401007:add$0x8,%rsp;恢复栈 40100b: retq;返回func4 是一个典型的递归函数实现的是二分查找的变种并返回一个与路径相关的整数值类似“决策树编码”。它完美展示了 递归对栈帧的动态影响。 我们将结合 ✅ Intel 手册硬件CALL/RET 如何操作栈 ✅ System V ABILinux x64 软件约定参数传递、寄存器使用 ✅ GCC 编译器行为如何为递归函数分配栈帧 来深入分析 递归调用对栈帧的影响。 注本例无 SEH因为 SEH 是 Windows 特有Linux 使用 DWARF 异常处理但 func4 无异常故聚焦递归 一、func4 的功能简析辅助理解栈行为 c 编辑 int func4(int a, int b, int c){int mid(b c)/2;// 向下取整通过符号位修正if(mida)return0;elseif(mida)return2* func4(a, mid1, c)1;elsereturn2* func4(a, b, mid-1);}这是一个 尾递归不是普通递归调用后还要做 *2 或 *21 每次递归缩小[b, c]区间 最深递归深度 ≈ log₂(c - b)二、单次调用的栈帧结构非递归视角 汇编开头/结尾 asm 编辑 sub$0x8, %rsp;分配8字节...add$0x8, %rsp;释放8字节 retq 为什么只分配8字节 函数很小局部变量 t, sign, mid 全用寄存器%eax, %ecx 唯一需要栈的原因对齐要求 System V ABI 要求函数入口处 %rsp %168因为 call 会压入8字节使被调用者入口处 RSP %160但 GCC 发现内部要调用其他函数call func4所以必须保持16字节对齐 → sub$0x8使 RSP %168满足下一次 call 前的要求 ✅ 所以这8字节 不是用于局部变量而是用于栈对齐 三、递归调用时的栈帧累积核心 假设调用链 text 编辑 func4(5,0,14)└─ func4(5,8,14)└─ func4(5,8,10)└─ func4(5,8,8)→mid85→ func4(5,8,7)→ base case? 每次 call func4 发生时栈内存布局递归深度3时 text 编辑 High Address │ ├──[RSP10h]→ 返回地址第3层 → 第2层 ├──[RSP08h]→ 对齐空间第3层 ├──[RSP00h]→ ← 当前 RSP第3层入口 │ ├──[RSP-08h]→ 返回地址第2层 → 第1层 ├──[RSP-10h]→ 对齐空间第2层 │ ├──[RSP-18h]→ 返回地址第1层 → caller ├──[RSP-20h]→ 对齐空间第1层 │ Low Address 每层递归消耗16字节栈空间8B 返回地址 8B 对齐 四、Intel 手册如何支撑递归1. CALL 指令Vol. 2A “Pushes thereturnaddress onto the stack and transfers control.” 每次递归调用都压入独立的返回地址 形成 返回地址链确保 ret 能逐层返回2. RET 指令 “Pops thereturnaddress and jumps to it.” 从当前栈顶弹出地址跳转 自动恢复上一层的 RIP3. 栈指针连续性 RSP 始终指向当前函数的栈顶 递归深度增加 → RSP 不断减小向低地址增长 ✅ 递归的本质利用栈的 LIFO 特性保存多层上下文 五、ABI 与编译器在递归中的角色 System V ABI 规定 参数通过 %rdi, %rsi, %rdx 传递 → 每次递归调用前重新设置 调用者即 func4 自己负责清理参数但参数在寄存器无需清理 必须保持栈16字节对齐 → 所以每层 sub$0x8GCC 行为 没有优化成尾递归因为 *2 和 1 在调用后执行 每层独立栈帧即使无局部变量也保留对齐空间 若开启 -O2可能内联或优化但本例保留递归结构 六、潜在风险栈溢出Stack Overflow 每层递归消耗 16 字节 默认栈大小Linux通常为 8MB 最大递归深度 ≈ 8MB / 16B ≈524,288层 但在 func4 中 区间[b, c]每次至少缩小1最大深度 ≈ c - b最坏线性但实际是二分 → 深度 ≈ log₂(15)≈45 层 ✅ 所以安全。但若写成 c 编辑 int bad(int n){returnbad(n-1);}// 无限递归 → 很快触发 Segmentation Fault栈溢出 栈溢出不是由 CPU 检测而是由 OS 的 栈保护页guard page 触发 SIGSEGV 七、与异常处理如 SEH的对比说明 虽然 func4 没有异常但可简要对比八、总结递归对栈帧的核心影响 栈帧动态累积每层递归创建独立栈帧含返回地址 对齐空间 硬件自动管理返回地址CALL/RET 构建调用链 ABI 强制对齐即使无局部变量也需 sub rsp, N 满足16字节对齐 编译器保留结构因非尾递归无法优化为循环 深度决定栈消耗二分递归深度浅安全线性递归易栈溢出 ✅ 最终结论 func4 是理解 递归与栈帧关系 的绝佳例子——它展示了 每一次递归调用都是在栈上叠加一个新的上下文盒子 而每一次返回都是将这个盒子优雅地拆除。 这正是冯·诺依曼架构“用栈实现递归”的精妙体现。