Vit*_*ira 5 c operating-system low-level stack-memory
当我们运行代码时,编译器在编译后“检测”必要的堆栈内存量?这样,每个程序都有自己的堆栈内存“块”。
或者每个程序的堆栈内存是由操作系统定义的?
谁定义了每个运行的应用程序的堆栈内存量?
或者我们没有这个,每个程序都可以根据需要使用所有堆栈内存?
在 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)时,它将:
将 RBP 压入堆栈;
将RSP放入RBP中;
减去为函数分配的堆栈空间的 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++是可重定位共享对象,因此函数的位置可以在任何地方。动态加载器将使用算法来确定。