On x86-64 Linux, the stack is given 8MB by default. Browse Ciro Santilli's answer about the memory layout of x86 Linux here: Where is the stack memory allocated from for a Linux process?.
For example, you could have something like the following:
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
For the unavailable section see Peter Cordes's answer here: Why does QEMU return the wrong addresses when filling the higher half of the PML4?.
In itself, the loader doesn't have to read the executable for the stack size. The stack size is not commonly stored in an ELF file. The OS simply assumes a default stack size is enough for most programs.
You seem to misunderstand what it means to allocate stack space. The stack is allocated during compilation. It is allocated by simple means of subtracting RSP of the space required for the function. When a process enters a function (including main) it will:
Push RBP on the stack;
Put RSP in RBP;
Subtract RSP of the allocated stack space for the function.
Step 3 clears the way for the function to work within its allocated stack space. After those 3 steps, the stack is accessed by using a relative negative offset from RBP. I have a recently deleted answer which specifically corresponds to the question so I'll copy its text here:
The local variables are allocated on the stack. Memory is allocated for variables/objects you initialize with new at runtime using a system call. Local variables are accessed using a negative relative offset from RBP and global variables are accessed using a relative offset from RIP (by default).
I had to study a bit of how that works because I've been in the process of writing an x86-64 OS and I had to understand this stuff in order to continue my development.
Now it is quite confusing for a beginner so let's look at a concrete example of what this means. Create a main.cpp file and place the following into it:
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;
}
Compile with the following:
g++ --entry main -static -ffreestanding -nostdlib main.cpp -omain.elf
Here we set the entry to be the main function with --entry main
we ask the code to be all included in the executable with -static
and we ask to remove the standard library from the code with -nostdlib
. This is to simplify the output of objdump -d main.elf
(disassembly of the executable) which is the following:
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
Here we see the main
function and the func
function stripped of any unnecessary overhead to simplify the example. When we enter a function in C++, the code will push RBP on the stack, put RSP in RBP then decrement RSP of the allocated stack space for the function. This allocated stack space is always known directly at compile time because the space used by statically allocated variables is always known during compilation.
Afterwards, everything is either a relative offset from RIP (for accessing global variables) or a negative relative offset from RBP (for accessing local variables). In particular, the line movl $0x4,-0x4(%rbp)
accesses the local variable called local_variable
and places 4 into it. Then the line movl $0x5,0x2fc3(%rip)
accesses the global variable called global_variable
and makes it become 5.
When you allocate a variable with new, the compiler cannot know the size of the allocation at compile time because it is a dynamically allocated variable. The memory allocation will thus be compiled to putting the arguments in some registers and then using the syscall
assembly instruction to get some memory.
Most of that is dynamically linked. It means that the standard library is not included in the executable but is instead linked with the executable by the dynamic linker at launch time of the executable. The functions of the standard library are defined in a library (libstdc++). This library is a shared object and contains all the symbols of the different C++ standard functions (including new).
When you call new from C++, the symbol of the function to call for allocating memory dynamically will be kept in the final executable. The address of that function (where to call to get to that function) will be determined before runtime (at launch time) by the dynamic loader. Since libstdc++ is a relocatable shared object, the position of the function can be anywhere. The dynamic loader will determine that using algorithms.