C/C++ 中的全局调用堆栈基础

Base of Global Call Stack in C/C++

我读到每个函数调用都会导致 在全局调用堆栈中压入一个堆栈帧,并且一旦函数调用完成,调用堆栈就会弹出,并且控件传递给我们从弹出的堆栈帧中获得的地址。如果一个被调用的函数调用另一个函数,它将把另一个 return 地址压入同一个调用堆栈的顶部,依此类推,信息按照程序指示堆叠和拆栈。

我想知道在 C 或 C++ 程序中 全局调用堆栈 的基础是什么?

我在互联网上进行了一些搜索,但 none 的来源明确提到了它。是不是我们程序启动时调用栈是空的,只有调用一次函数,调用栈用法才开始? main() 函数必须 return 的地址是否被隐式推送为我们的调用堆栈的基础并且是我们调用堆栈中的堆栈帧?我希望 main() 在我们的调用堆栈中也有一个堆栈帧,因为我们总是在我们的 main() 函数的末尾 returning 一些东西并且需要一些地址到 return到。 OR 这取决于 compiler/OS 并且根据 实现 ?

而不同

如果有人有一些关于此的信息链接或可以提供有关进入它的过程的详细信息,那将会很有帮助。

我不确定是否有一个通用的答案,因为堆栈的实现可能因架构而异。例如,堆栈可能增长(即堆栈位置指针值在压入堆栈时增加)或向下增长。

退出main()通常是通过调用一个操作函数来指示程序希望终止(使用指定的return代码),所以我不希望return main() 的地址出现在堆栈上,但这可能因操作系统甚至编译器而异。

我不确定你为什么需要知道这个,因为这通常是你留给系统的事情。

main() 由处理为可执行文件等设置环境的 libc 代码调用。因此在调用 main() 时,堆栈已经至少有一个由调用者创建的帧。

首先,没有"global call stack"这样的东西。每个线程都有一个堆栈,主线程的堆栈通常看起来与后来产生的任何线程的线程完全不同。大多数情况下,这些 "stacks" 中的每一个都只是当前声明使用的任意内存段,从任意合适的内存池中进行子分配。

并且由于编译器优化,许多函数调用通常甚至不会在堆栈上结束。这意味着不一定有可区分的堆栈框架。您只能保证可以引用放在堆栈上的变量,但不能保证编译器必须保留您未明确引用的任何内容。

甚至不能保证调用堆栈的内存布局必须组织在可区分的框架中。函数指针永远不能保证是堆栈帧的一部分,只是恰好是数据和函数指针可能共存于地址 space 的架构中的一个实现细节。 (因为有些体系结构要求 return 地址存储在与调用堆栈中使用的数据不同的地址 space 中。)

除此之外,是的,还有在 main() 函数之外执行的代码。特别是全局 static 变量的初始值设定项、设置 运行 时间环境的代码(env、调用参数、stdin/stdout)等

例如当链接到 libc 时,__libc_start_main 会在初始化完成后调用您的 main 函数。并在 main 函数 returns.

时清理

__libc_start_main 大约是 "stack" 开始使用的点,就您在程序中可以看到的而言。但这实际上不是真的,已经在内核 space 中执行了一些加载程序代码,用于为您的进程保留内存以在最初运行(包括未来堆栈的内存),将寄存器和内存初始化为明确定义的值等等

就在实际上 "starting" 您的进程退出内核模式之后,指向未来堆栈的任意指针以及程序的第一条指令被加载到相应的处理器寄存器中。实际上,这就是 __libc_start_main(或任何其他初始化函数,取决于您的 运行 时间)开始 运行ning,并且您可见的堆栈开始建立的地方。

现在回到内核通常涉及到一个中断,它也不遵循堆栈,而是直接访问处理器寄存器以简单地交换相应处理器寄存器的内容。 (例如,如果您从内核调用一个函数,函数调用中的调用堆栈所需的内存不是从您的堆栈中分配的,而是从您甚至无法访问的堆栈中分配的。)

无论哪种方式,在调用 main() 之前发生的一切,以及每当您进入系统调用时,都依赖于实现,并且不保证任何特定的可观察行为。就纯 C / C++ 运行 时间而言,处理处理器寄存器并因此交替程序流也远远超出定义的行为。

我见过的每个系统,当调用 main() 时都会设置堆栈。它必须是或只是在 main 中声明一个变量会失败。一旦创建线程或进程,就会设置堆栈。因此,任何执行线程都有一个堆栈。此外,在我所知道的每种汇编语言中,寄存器或固定内存位置用于指示堆栈指针的当前值,因此堆栈的概念始终存在(堆栈指针可能是错误的,但堆栈操作始终存在,因为它们是内置于每一种主流汇编语言中)。