调用函数时,函数栈的作用过程

    xiaoxiao2025-07-23  6

    1、什么是栈帧?

    栈帧也叫过程活动记录,是编译器用来实现函数调用过程的一种数据结构。C语言中,每个栈帧对应着一个未运行完的函数。从逻辑上讲,栈帧就是一个函数执行的环境:函数调用框架、函数参数、函数的局部变量、函数执行完后返回到哪里等等。栈是从高地址向低地址延伸的。每个函数的每次调用,都有它自己独立的一个栈帧,这个栈帧中维持着所需要的各种信息。寄存器ebp指向当前的栈帧的底部(高地址),寄存器esp指向当前的栈帧的顶部(低地址)。

    2、Add()函数的调用过程

    我们以Add()函数为例深入的研究一下函数的调用过程。 先看一段简单的代码:

    1 #include <stdio. h> 2 int Add(int x, int y) 3 { 4 int z = 0; 5 z = x + y; 6 return z; 7 } 8 int main() 9 { 10 int a = 10; 11 int b = 20; 12 int ret = Add(a, b) ; 13 printf("ret = %d\n", ret) ; 14 return 0; 15 }

    当讲程序调试的时候, 查看【调用堆栈】(按F10进入调试-窗口-调用堆栈,或按快捷键ctrl+alt+C) ,用VS2015调试 如下图:

    如果用版本更老的,或其他如VC6.0等编辑器则可以看到更多信息,VS2008调试如图:

    我们发现其实main函数在 __tmai nCRTStartup 函数中调用的,而 __tmai nCRTStartup 函数是在 mai nCRTStartup 被调用的。我们知道每一次函数调用都是一个过程。这个过程我们通常称之为: 函数的调用过程。这个过程要为函数开辟栈空间, 用于本次函数的调用中临时变量的保存、 现场保护。 这块栈空间我们称之为函数栈帧。 而栈帧的维护我们必须了解ebp和esp两个寄存器。 在函数调用的过程中这两个寄存器存放了维护这个栈的栈底和栈顶指针。比如:调用main函数, 我们为main函数分配栈帧空间, 那么栈帧维护如下:ebp存放了指向函数栈帧栈底的地址。esp存放了指向函数栈帧栈顶的地址。注意:ebp指向当前位于系统栈最上边一个栈帧的底部,而不是系统栈的底部。严格说来,“栈帧底部”和“栈底”是不同的概念;ESP所指的栈帧顶部和系统栈的顶部是同一个位置。

    1 . 从main函数的地方开始, 要展开main函数的调用就得为main函数创建栈帧, 那我们先来看main函数栈帧的创建。转到反汇编可以更清晰的看到过程:

    过程分析:

    a.首先mainCRTStartup(),__mainCRTStartup()函数的调用,调main()函数;

    b.将ebp压栈处理,保存指向栈底的ebp的地址(方便函数返回之后的现场恢复),此时esp指向新的栈顶位置;

    c.将esp的值赋给ebp,产生新的ebp;

    d.给esp减去一个16进制数0E4H(为main函数预开辟空间);

    e.push ebx、esi、edi;

    f.lea指令,加载有效地址;

    g.初始化预开辟的空间为0xcccccccc;

    h.创建变量a与b。

    2. 接下来是Add函数的调用。

    参数传递过程:

     过程分析:

    a.将b存入寄存器eax,再将将eax压栈;(传参过程,从左向右传递)

    b.将a存入寄存器ecx,再将将ecx压栈;

    c.call指令的调用,先要压栈call指令下一条指令的 地址,然后跳转(push+jmp)到Add()函数的地方(__cdecl调用约定)。 执行call指令的时候按F11 , 来到了这里。再按F11 就进入Add函数的执行代码处。Add函数栈帧的创建:

    过程分析:

    a.首先将main()函数ebp压栈处理,保存指向main()函数栈帧底部的ebp的地址(方便函数返回之后的现场恢复),此时esp指向新的栈顶位置;

    b.将esp的值赋给ebp,产生新的ebp,即Add()函数栈帧的ebp;

    c.给esp减去一个16进制数0E4H(为Add()函数预开辟空间);

    d.push ebx、esi、edi;

    e.lea指令,加载有效地址;

    f.初始化预开辟的空间为0xcccccccc;

    g.创建变量z;

    h.获取形参的a和b再相加,将结果存储到z中;

    i.将结果存储到eax寄存器,通过寄存器带回函数的返回值。 剩下的就是是函数返回部分:

    过程分析:

    a.pop3次,edi、esi、ebx依次出栈,esp 会向下移动;

    b.将ebp赋给esp,使esp指向ebp指向的地方

    c.ebp 出栈,将出栈的内容给ebp(即main()函数ebp),回到main()函数的栈帧;

    d.ret 指令,出栈一次,并将出栈的内容当做地址,并跳转到该地址处(pop+jmp)。

    注: 栈帧这部分内容在不同的编译器上实现存在差异, 但是思想都是一致的。

    栈帧的一般总结:

    1. 堆栈是C语言程序运行时必须的一个记录调用路径和参数的空间: ➢ 函数调用框架; ➢ 传递参数; ➢ 保存返回地址; ➢ 提供局部变量空间; ➢ 等等。 以x86体系结构为例2. 堆栈寄存器和堆栈操作  堆栈相关的寄存器 ➢ esp,堆栈指针(stack pointer) ➢ ebp,基址指针(base pointer) 堆栈操作 ➢ push 栈顶地址减少4个字节(32位) ➢ pop 栈顶地址增加4个字节 ❖ ebp在C语言中用作记录当前函数调用基址3. 利用堆栈实现函数调用和返回 ❖其他关键寄存器 ➢ cs : eip:总是指向下一条的指令地址 ● 顺序执行:总是指向地址连续的下一条指令 ● 跳转/分支:执行这样的指令的时候, cs : eip的值会根据程序需要被修改 ● call:将当前cs : eip的值压入栈顶, cs : eip指向被调用函数的入口地址 ● ret:从栈顶弹出原来保存在这里的cs : eip的值,放在cs : eip中 ● 发生中断时???4. 函数堆栈框架的形成

    ❖call xxx ➢执行call之前; ➢执行call时,cs:eip原来的值指向call下一条指令,该值被保存到栈顶,然后cs:eip的值指向xxx的入口地址 ❖进入xxx ➢第一条指令:pushl

    转载请注明原文地址: https://yun.8miu.com/read-139597.html
    最新回复(0)