During execution of a program, memory is used to store code, constants and data as well as manage function calls. A process’ memory is divided into several segments (see Figure 1):
The Text segment stores compiled code.
The .rodata segment stores constants (i.e.: read-only data).
The .data segment stores initialized static variables.
The .bss segment stores uninitialized static variables.
The Stack stores function calls and local variables.
The Heap stores dynamic memory allocation.
Figure 1:Process memory
7.1.1Text (Code)¶
In the Text segment is stored the whole machine code generated by the compiler. This memory zone is set as read-only by the Operating System: trying to write in this zone would trigger a segmentation fault.
7.1.2.rodata¶
The .rodata segment is where are stored constants. As for the Text segment, this memory zone is set as read-only by the Operating System: trying to write in this zone would trigger a segmentation fault.
Declared constants are usually stored inside the .rodata segment, though they may be inlined by the compiler for optimization:
const PI: f32 = 3.14159;
PI3.14159Literal strings are stored inside the .rodata segment:
static GREETING: &str = "Hello, world!";
GREETING"Hello, world!"7.1.3.data¶
The .data segment stores initialized static variables: static variables that have initialization values at compile time.
Example with an integer:
static ZIPCODE: u32 = 92340;
ZIPCODE92340Example with a structure initialized using a function:
#[derive(Debug)]
#[allow(dead_code)]
struct Point {
x: i32,
y: i32,
}
const fn new_point(x: i32, y: i32) -> Point {
Point {x, y}
}
static ORIGIN: Point = new_point(0, 0);
ORIGINPoint { x: 0, y: 0 }7.1.4.bss¶
The .bss segment (Block Started by Symbol) is where are stored uninitialized static variables: static variables that have no known initialization values at compile time. However, the compiler, knowing the sizes of those objects, allocates the needed size inside the generated program’s file. At runtime, the Operating System provisions the necessary memory zone, and initialize it to zeros. Later on, during program’s execution, these data will eventually receive values.
In Rust, zero initialized variables are stored in the .bss segment:
static mut N: usize = 0;
static mut BUFFER: [u8; 16] = [0; 16];
#[allow(static_mut_refs)]
fn fill_and_print() {
unsafe {
// Initialize
N = 10;
for i in 0..N {
BUFFER[i] = 5;
}
// Print
println!("N={N}");
println!("BUFFER={BUFFER:?}");
}
}
fill_and_print();N=10
BUFFER=[5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 0, 0, 0, 0, 0, 0]
7.1.5Stack¶
The stack is a region of memory that stores local variables created by each function (including the main function). It operates in a last-in, first-out (LIFO) manner, meaning that the most recently added function call is the first one to be removed when it completes.
Each time a function is called, a new stack frame is created on top of the stack to hold (see Figure 2):
The return address (where to go back in the code after the function finishes).
The received arguments.
The local variables.
When the function returns, its stack frame is popped off the stack, and execution is resumed at the return address.
In the following example, functions foo_a(), foo_b() and foo_c() are
pushed on the stack, as well as their variables/parameters n, m and
o:
fn foo_c(o: i32) -> i32 {
o * 3 + 8
}
fn foo_b(m: i32) -> i32 {
foo_c(m / 2)
}
fn foo_a(n: i32) -> i32 {
foo_b(n + 5) * 2
}
foo_a(10)58Figure 2:Memory stack
7.1.5.1Stack overflow¶
If a program uses too much stack space, through too many nested function calls and/or heavy usage of local variables, it can lead to a stack overflow error, causing the program to crash, as show in Figure 3.
Figure 3:Stack overflow
In the following example we push a huge array on the stack that would generate an overflow at runtime (not executed here to avoid crashing the kernel):
let array = [123; 1000_000_000];
array.len()7.1.5.2Call stack¶
The call stack is a conceptual representation of the active stack frames at any given point during program execution. It shows the sequence of function calls that led to the current point in the program, with the most recent call at the top (see Figure 4).
Figure 4:Call stack
7.1.6Heap¶
The heap (see Figure 5) is a region of memory used for dynamic memory allocation, where objects are stored and managed during the program’s execution. Unlike the stack, which has a strict LIFO structure, the heap allows for flexible memory allocation and deallocation. Objects in the heap can be created and destroyed in any order, and their lifetimes are not directly tied to function calls.
Figure 5:Memory heap
The memory heap grows according to the needs of the program. Each time more memory is needed, the program sends a request to the operating system. The theoretical maximum size of the heap is determined by the operating system, architedture (32-bit vs 64-bit) and configuration, but is always larger than the available physical memory, thanks to virtual memory techniques. This is however aperformance bottleneck, as accessing data in RAM is much faster than accessing data in virtual memory (on disk). Therefore, it is important to manage memory usage efficiently to avoid excessive swapping between RAM and disk.
The following example makes usage of heap objects:
let mut books: Vec<String> = Vec::new();
books.push(String::from("Le Petit Prince"));
books.push(String::from("Le avventure di Pinocchio. Storia di un burattino"));
books["Le Petit Prince", "Le avventure di Pinocchio. Storia di un burattino"]