菜鸟的程序编程理解
文章目录
- 一、局部函数如何调用-- 及-- 调用前后栈上动态分配过程
- 1. 调用 `add` 函数前的准备(参数传递和返回地址保存)
- 2. `add` 函数栈帧创建
- 3. `add` 函数执行
- 4. `add` 函数栈帧销毁
- 5.备注(栈帧和基址指针的理解)
一、局部函数如何调用-- 及-- 调用前后栈上动态分配过程
下面详细说明从 add
函数栈帧创建到销毁过程中,栈上依次入栈的参数及相关信息,结合之前给出的示例代码:
#include <stdio.h>
// 被调函数
int add(int a, int b) {
int result = a + b;
return result;
}
// 主调函数
int main() {
int x = 3;
int y = 5;
int sum = add(x, y);
printf("The sum is: %d\n", sum);
return 0;
}
1. 调用 add
函数前的准备(参数传递和返回地址保存)
在 main
函数调用 add
函数时,首先要进行参数传递和返回地址的保存,这些操作会使数据入栈,具体如下:
- 参数入栈:按照从右到左的顺序将参数压入栈中。在这个例子中,先将参数
b
(也就是y
的值 5)压入栈,接着将参数a
(即x
的值 3)压入栈。这是因为在C语言的函数调用中,通常采用从右到左的参数传递顺序,这样可以方便实现可变参数函数。 - 返回地址入栈:把
main
函数中调用add
函数之后要执行的下一条指令的地址压入栈。这是为了让add
函数执行完毕后能够准确返回到main
函数的正确位置继续执行。
2. add
函数栈帧创建
当参数和返回地址都入栈后,就开始为 add
函数创建栈帧,栈帧里包含以下入栈内容:
- 旧的基址指针(Base Pointer,BP):
main
函数栈帧的基址指针(通常是ebp
寄存器的值)会被压入栈。这是为了在add
函数执行结束后,能够恢复main
函数的栈帧。基址指针指向当前栈帧的底部,它可以帮助函数访问栈上的参数和局部变量。 - 新的基址指针设置:将当前栈指针(通常是
esp
寄存器的值)赋给基址指针寄存器(ebp
),这样就确定了add
函数栈帧的底部。 - 局部变量入栈:为
add
函数的局部变量分配栈空间。在这个例子中,add
函数有一个局部变量result
,会在栈上为其分配空间。
3. add
函数执行
在 add
函数执行期间,栈上的数据基本保持不变,只是利用栈上的参数和局部变量进行计算。add
函数从栈上获取参数 a
和 b
的值,计算它们的和,并将结果存储在局部变量 result
中。
4. add
函数栈帧销毁
当 add
函数执行完毕准备返回时,会进行栈帧销毁操作,涉及数据出栈,不过从入栈角度来看,在销毁前没有新的数据入栈,而是按照相反的顺序将之前入栈的数据依次出栈:
- 恢复旧的基址指针:从栈中弹出之前保存的
main
函数的基址指针值,恢复到ebp
寄存器,这样就回到了main
函数的栈帧底部。 - 释放局部变量空间:通过调整栈指针(增加
esp
的值)来释放add
函数局部变量所占用的栈空间。 - 返回地址出栈:从栈中弹出之前保存的返回地址,将控制权交还给
main
函数,让main
函数从该地址处继续执行。 - 参数出栈:可以由调用者(
main
函数)或者被调用者(add
函数)来清理栈上的参数,这取决于具体的调用约定。在常见的C调用约定(如__cdecl
)中,由调用者负责清理栈上的参数,也就是在main
函数中调整栈指针来移除之前压入的参数a
和b
。
综上所述,从 add
函数栈帧创建到销毁,栈上依次入栈的内容为:参数 b
、参数 a
、返回地址、旧的基址指针、局部变量 result
。
5.备注(栈帧和基址指针的理解)
5.1基址指针
基址指针(Base Pointer)是计算机系统中用于辅助管理栈帧的数据指针,在 x86 架构中,通常用 ebp
(Extended Base Pointer)寄存器来表示。下面详细介绍它的作用和用途:
定义与存储位置
在 x86 架构的计算机中,ebp
寄存器是一个 32 位的通用寄存器,在 64 位系统里对应为 rbp
寄存器。它在栈帧管理中起到关键作用,一般存储着当前栈帧的基地址,也就是栈帧的起始位置。
具体作用
1. 定位栈帧
在函数调用过程中,每个函数都会在栈上分配一块独立的内存区域,即栈帧。基址指针指向当前函数栈帧的底部,这样可以清晰界定当前函数栈帧的范围,把不同函数的栈帧区分开来。当函数调用嵌套时,不同函数的栈帧依次排列在栈上,基址指针能够帮助操作系统和程序准确找到当前正在执行的函数的栈帧。
2. 访问局部变量
函数的局部变量通常存储在栈帧中,基址指针可以作为访问这些局部变量的参考点。通过相对于基址指针的偏移量,程序可以准确地找到栈帧内的各个局部变量。例如,若某个局部变量位于基址指针偏移量为 -4 字节的位置,程序就可以通过 ebp - 4
来访问该变量。
3. 访问函数参数
在函数调用时,参数会被压入栈中。基址指针同样能作为访问这些参数的参考。参数通常位于基址指针上方(偏移量为正)的位置,程序可以依据偏移量来访问传入的参数。比如,第一个参数可能位于基址指针偏移量为 +8 字节的位置,程序就能通过 ebp + 8
来获取该参数的值。
4. 恢复调用函数的栈帧
当函数执行完毕需要返回时,需要恢复调用函数的栈帧。基址指针在此时发挥重要作用,因为它保存着调用函数栈帧的基地址。通过恢复基址指针的值,程序可以准确回到调用函数的栈帧,继续执行后续代码。
示例说明
以下是一段简单的 C 语言代码示例:
#include <stdio.h>
int add(int a, int b) {
int result = a + b;
return result;
}
int main() {
int x = 3;
int y = 5;
int sum = add(x, y);
printf("The sum is: %d\n", sum);
return 0;
}
在这个示例中,当 main
函数调用 add
函数时:
add
函数会保存main
函数的基址指针(ebp
)到栈中。- 为
add
函数设置新的基址指针,指向add
函数栈帧的底部。 add
函数通过相对于新基址指针的偏移量来访问参数a
和b
以及局部变量result
。- 当
add
函数执行完毕返回时,会恢复main
函数的基址指针,从而回到main
函数的栈帧继续执行。
综上所述,基址指针在函数调用和栈帧管理过程中是不可或缺的,它为程序准确访问栈上的数据和恢复调用上下文提供了重要支持。
5.2 栈帧
可以这么理解,基址指针(在 x86 架构里通常是 ebp
寄存器,64 位系统中是 rbp
)指向的是当前函数栈帧的起始位置(底部),下面详细解释:
栈帧的概念
在程序运行时,每当调用一个函数,系统会在栈上为该函数分配一块连续的内存区域,这个区域就叫做栈帧。每个栈帧包含了函数执行所需的各种信息,如函数的参数、局部变量、返回地址以及旧的基址指针等。
基址指针与栈帧起始位置
- 指向当前栈帧底部:基址指针在函数调用时被设置为指向当前函数栈帧的起始位置(底部)。在函数执行过程中,它就像一个固定的参考点,程序可以通过相对于基址指针的偏移量来准确访问栈帧内的各种数据。
- 辅助数据访问:借助基址指针,函数能够方便地找到自己的参数和局部变量。例如,参数通常存放在基址指针上方(地址值更大)的栈内存区域,通过基址指针加上特定的偏移量就能访问;而局部变量一般在基址指针下方(地址值更小),通过基址指针减去相应偏移量来访问。
示例说明
结合之前的 add
函数和 main
函数示例:
#include <stdio.h>
int add(int a, int b) {
int result = a + b;
return result;
}
int main() {
int x = 3;
int y = 5;
int sum = add(x, y);
printf("The sum is: %d\n", sum);
return 0;
}
main
函数:在main
函数执行时,基址指针指向main
函数栈帧的底部。这个栈帧包含了main
函数的局部变量x
、y
和sum
等。- 调用
add
函数:当main
函数调用add
函数时,会进行以下操作:- 把
main
函数的基址指针(ebp
)的值压入栈中保存。 - 将当前栈指针(
esp
)的值赋给基址指针寄存器(ebp
),此时ebp
就指向了add
函数栈帧的起始位置(底部)。 - 为
add
函数的局部变量(如result
)在栈上分配空间。
- 把
add
函数执行:在add
函数执行期间,通过相对于ebp
的偏移量来访问参数a
、b
和局部变量result
。add
函数返回:add
函数执行完毕后,会从栈中弹出之前保存的main
函数的基址指针值,恢复到ebp
寄存器,这样ebp
又重新指向main
函数栈帧的底部。
所以,基址指针在不同函数调用时,会指向当前正在执行的函数栈帧的起始位置,以此来辅助函数访问栈上的数据和管理栈帧。