Memory Regions - Stack
#memory | #lowlevel | #cacheThink of the stack memory like a stack of plates at a buffet. You add new plates (data) on top, and when done, you remove them in the same order.
Key characteristics
- Memory is allocated and deallocated in a last in, first out (lifo) manner;
- Very fast access because of its simple allocation and deallocation automatic pattern;
- Stack size is limited by CPU & OS. Different OSes have different default stack sizes;
- Used for function call stack frames, arguments, local variables, and return values;
- Data in the stack is automatically cleaned up once the function scope ends;
Limitations
- Risk of throwing segmentation faults or stack overflow error if used size excceds the limit;
- If a function calls itself recursively and endlessly, it may throw with a stack overflow;
- Should not be used to store very large objects;
- No garbage collection because the cleaning operation is based on scopes;
Example 1
#include <stdio.h>
#define true 1
void functionA(int sum) {
int a = 10; // 2. a is stored in stack memory
int b = 20; // 3. b is stored in stack memory
if (sum) {
printf("a + b = %d\n", a + b);
} else {
printf("a = %d\n", a);
}
}
int main() {
functionA(true); // 1. Stack frame for functionA is created
return 0; // 4. Stack frame is removed with all its related data after functionA exits
}
How to prove?
Talking is cheap! If you're on a x86_64 system and you want to prove and check the stack frame, you can use a tool like the gdb to disassemble the actual program.
Info
If you're on Windows, you can install gdb in several ways — for example, via scoop:
scoop install gdb
I'm currently running gdb on WSL2. Just keep in mind that different OS and gdb versions may produce slightly different outputs.
wsl --version
WSL version: 2.4.12.0
Kernel version: 5.15.167.4-1
WSLg version: 1.0.65
MSRDC version: 1.2.5716
Direct3D version: 1.611.1-81528511
DXCore version: 10.0.26100.1-240331-1435.ge-release
Windows version: 10.0.26100.3775
gdb --version
GNU gdb (Ubuntu 12.1-0ubuntu1~22.04.2) 12.1
Copyright (C) 2022 Free Software Foundation, Inc.
License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law.
Compile the program with the debug symbols -g
and no optimizations -O0
;
gcc -g -O0 stack.c -o stack
Open the program, mine is called stack, using the gdb to read the debug symbols generated by the compiler;
gdb stack
For help, type "help".
Type "apropos word" to search for commands related to "word"...
Reading symbols from stack...
Now, let us set two breakpoints: one at the line 17
of the main function and another at the line 8
of the functionA function. To do this, type the following commands:
(gdb) break 17
Breakpoint 1 at 0x11ba: file stack.c, line 17.
(gdb) break 8
Breakpoint 2 at 0x1166: file stack.c, line 8.
Run the program with the command run
. The program will stop at the first breakpoint, which is at line 8
of the functionA function.
(gdb) run
Starting program: /home/celsojr/repos/gdb-blog/stack
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/lib/x86_64-linux-gnu/libthread_db.so.1".
Breakpoint 2, functionA (sum=1) at stack.c:8
warning: Source file is more recent than executable.
8 if (sum) {
The next command we're gonna use is the backtrace
command, which will show us the current stack frame and the stack trace. The output will look like this:
(gdb) backtrace
#0 functionA (sum=1) at stack.c:8
#1 0x00005555555551ba in main () at stack.c:16
In order for us to know on which stack frame we are, we can use the frame
command. Not very useful in this case, but it is a good practice to know how to use it in case you are debugging a more complex program with multiple stack frames.
(gdb) frame
#0 functionA (sum=1) at stack.c:8
8 if (sum) {
Next, we can use the info locals
command to see the local variables in the current stack frame. This is useful to understand the current state of the program and the values of the variables at this point in time.
(gdb) info locals
a = 10
b = 20
Remember that the stack is a fixed region of memory that holds the stack frames for function calls. Each stack frame contains information about the function call, including local variables, the return address, and the parameters passed to the function. The content of the stack frames changes dynamically as the program executes, depending on which functions are active.
We can use the info args
command to see the arguments passed to the current function. For example, it shows that the sum
argument was passed with a value of 1
. This helps us understand how the function was called and what values were provided.
(gdb) info args
sum = 1
The info frame
command displays detailed information about the current stack frame, including memory addresses and saved registers. It's a more advanced command that provides insight into the inner workings of a function call.
(gdb) info frame
Stack level 0, frame at 0x7fffffffdca0:
rip = 0x555555555166 in functionA (stack.c:8); saved rip = 0x5555555551ba
called by frame at 0x7fffffffdcb0
source language c.
Arglist at 0x7fffffffdc90, args: sum=1
Locals at 0x7fffffffdc90, Previous frame's sp is 0x7fffffffdca0
Saved registers:
rbp at 0x7fffffffdc90, rip at 0x7fffffffdc98
This is the time to continue the program execution. You can use the continue
command to continue the program execution until the next breakpoint or until the program exits.
(gdb) continue
Continuing.
a + b = 30
Breakpoint 1, main () at stack.c:17
17 return 0;
But, as we've also defined a breakpoint at line 17
of the main function, the program will stop at this line. You can use the backtrace
command again to see the current stack frame and the stack trace. Notice that the stack frame has changed, and now we are in the main function.
(gdb) backtrace
#0 main () at stack.c:17
At this point, we can use the info locals
command again to see that the local variables in the main function are empty because all the local variables stored in the previous stack frame, which is the functionA function, are gone. All the functionA function stack frame data is gone because they were removed from the stack when the function exited.
(gdb) info locals
No locals.
(gdb) info args
No arguments.
Congrats! You just learned how to use the gdb to check the stack frame and the stack trace. This is a powerful tool that can help you understand how your program is executing and what values are being passed around. The gdb is a powerful tool that can help you debug your programs and understand how they work at a low-level.
Bonus tip!
If you want to see the stack frame in a more visual way, you can use the gdb with the gdb-dashboard plugin. This plugin provides a nice interface to see the stack frames, local variables, and other useful information. You can install it by doing the following:
wget https://git.io/.gdbinit -O ~/.gdbinit
Example 2
Deep recursion that can quickly overflow the stack
If a program exceeds the stack size, it crashes with a segmentation fault (SIGSEGV) on Linux/macOS or a stack overflow exception on Windows. This is a common problem in programming, especially when using recursion. A stack overflow occurs when a program uses more stack space than is available, causing the program to crash.
// WARNING: Bad code - Infinity loop
void recurse() { recurse(); }
int main() { recurse(); }