每个程序分配固定的堆栈大小?谁定义了每个运行的应用程序的堆栈内存量?

Vit*_*ira 5 c operating-system low-level stack-memory

当我们运行代码时,编译器在编译后“检测”必要的堆栈内存量?这样,每个程序都有自己的堆栈内存“块”。

或者每个程序的堆栈内存是由操作系统定义的?

谁定义了每个运行的应用程序的堆栈内存量?

或者我们没有这个,每个程序都可以根据需要使用所有堆栈内存?

use*_*123 5

在 x86-64 Linux 上,堆栈默认为 8MB。在这里浏览 Ciro Santilli 关于 x86 Linux 内存布局的答案:Where is the stack memory allocate from for a Linux process?

例如,您可以有如下内容:

Content                       Virtual address
_______________________________________________________________________

----------------------------- 0xFFFF_FFFF_FFFF_FFFF
Kernel
----------------------------- 0xFFFF_8000_0000_0000
Unavailable due to the canonical address requirement (PML4 or PML5 determines size of hole; smaller with 5 level paging)
----------------------------- 0x0000_8000_0000_0000
Stack grows downward from the top here
v v v v v v v v v
Maximum stack size is here
----------------------------
Process
----------------------------- 0x400000
Run Code Online (Sandbox Code Playgroud)

对于不可用的部分,请参阅 Peter Cordes 的回答:Why does QEMU return the error paths while filling the upper half of the PML4?

就其本身而言,加载程序不必读取可执行文件的堆栈大小。堆栈大小通常不存储在 ELF 文件中。操作系统简单地假设默认堆栈大小足以满足大多数程序的需要。

您似乎误解了分配堆栈空间的含义。堆栈是在编译期间分配的。它是通过减去函数所需空间的 RSP 的简单方法来分配的。当进程进入函数(包括 main)时,它将:

  1. 将 RBP 压入堆栈;

  2. 将RSP放入RBP中;

  3. 减去为函数分配的堆栈空间的 RSP。

步骤 3 为函数在其分配的堆栈空间内工作扫清了道路。在这 3 个步骤之后,通过使用 RBP 的相对负偏移量来访问堆栈。我最近删除了一个与该问题具体对应的答案,因此我将在此处复制其文本:

局部变量在堆栈上分配。内存是为您在运行时使用系统调用用 new 初始化的变量/对象分配的。使用 RBP 的负相对偏移量来访问局部变量,使用 RIP 的相对偏移量来访问全局变量(默认情况下)。

我必须研究一下它的工作原理,因为我一直在编写 x86-64 操作系统,并且我必须了解这些内容才能继续我的开发。

对于初学者来说这很令人困惑,所以让我们看一个具体的例子来说明这意味着什么。创建一个 main.cpp 文件并将以下内容放入其中:

int global_variable = 3;

void func(){
    int local_variable = 10;
    global_variable = 10;
    local_variable++;
}

int main(){
    int local_variable = 4;
    global_variable = 5;
    local_variable += 4;
    func();
    return 0;
}
Run Code Online (Sandbox Code Playgroud)

使用以下内容进行编译:

g++ --entry main -static -ffreestanding -nostdlib main.cpp -omain.elf
Run Code Online (Sandbox Code Playgroud)

这里我们将条目设置为主函数,--entry main我们要求代码全部包含在可执行文件中-static,我们要求从代码中删除标准库-nostdlib。这是为了简化(可执行文件的反汇编)的输出,objdump -d main.elf如下所示:

user@user-System-Product-Name:~$ objdump -d main.elf

main.elf:     file format elf64-x86-64


Disassembly of section .text:

0000000000401000 <_Z4funcv>:
  401000:   f3 0f 1e fa             endbr64 
  401004:   55                      push   %rbp
  401005:   48 89 e5                mov    %rsp,%rbp
  401008:   c7 45 fc 0a 00 00 00    movl   $0xa,-0x4(%rbp)
  40100f:   c7 05 e7 2f 00 00 0a    movl   $0xa,0x2fe7(%rip)        # 404000 <global_variable>
  401016:   00 00 00 
  401019:   83 45 fc 01             addl   $0x1,-0x4(%rbp)
  40101d:   90                      nop
  40101e:   5d                      pop    %rbp
  40101f:   c3                      retq   

0000000000401020 <main>:
  401020:   f3 0f 1e fa             endbr64 
  401024:   55                      push   %rbp
  401025:   48 89 e5                mov    %rsp,%rbp
  401028:   48 83 ec 10             sub    $0x10,%rsp
  40102c:   c7 45 fc 04 00 00 00    movl   $0x4,-0x4(%rbp)
  401033:   c7 05 c3 2f 00 00 05    movl   $0x5,0x2fc3(%rip)        # 404000 <global_variable>
  40103a:   00 00 00 
  40103d:   83 45 fc 04             addl   $0x4,-0x4(%rbp)
  401041:   e8 ba ff ff ff          callq  401000 <_Z4funcv>
  401046:   b8 00 00 00 00          mov    $0x0,%eax
  40104b:   c9                      leaveq 
  40104c:   c3                      retq
Run Code Online (Sandbox Code Playgroud)

在这里,我们看到该main函数和该func函数删除了任何不必要的开销以简化示例。当我们在C++中进入一个函数时,代码会将RBP压入堆栈,将RSP放入RBP中,然后减少为该函数分配的堆栈空间的RSP。该分配的堆栈空间始终在编译时直接已知,因为静态分配的变量使用的空间在编译期间始终已知。

之后,一切要么是 RIP 的相对偏移(用于访问全局变量),要么是 RBP 的负相对偏移(用于访问局部变量)。特别是,该行movl $0x4,-0x4(%rbp)访问局部变量调用local_variable并将 4 放入其中。然后该行movl $0x5,0x2fc3(%rip)访问全局变量调用global_variable并使其变为5。

当你用new分配一个变量时,编译器在编译时无法知道分配的大小,因为它是动态分配的变量。因此,内存分配将被编译为将参数放入某些寄存器中,然后使用syscall汇编指令来获取一些内存。

其中大部分是动态链接的。这意味着标准库不包含在可执行文件中,而是在可执行文件启动时通过动态链接器与可执行文件链接。标准库的函数在库(libstdc++)中定义。该库是一个共享对象,包含不同 C++ 标准函数(包括 new)的所有符号。

当您从 C++ 调用 new 时,调用动态分配内存的函数的符号将保留在最终的可执行文件中。该函数的地址(调用该函数的位置)将在运行时(启动时)之前由动态加载器确定。由于libstdc++是可重定位共享对象,因此函数的位置可以在任何地方。动态加载器将使用算法来确定。