Stack Memory
We need to talk about memory in order to understand some important Rust concepts, but we're going to hand-wave away a lot of details. After all, this workshop is an introduction to Rust, not to operating systems and hardware!
The Stack and the Heap
Practically all processes organize their giant memory array into two regions: the "stack" and the "heap." The stack is specifically used to store data for function arguments and return values.
So let's imagine that we have 4 gigabytes of memory, and it's subdivided into 1 gigabyte for the stack and 3 gigabytes for the heap. (The heap is almost always much larger than the stack.)
let stack_bytes: [u8; 1_000_000_000];
let heap_bytes: [u8; 3_000_000_000];
It's technically possible to resize these at runtime, but in practice it's very rarely done.
Adding arguments to the Stack
When we call a function, the arguments get written to stack memory. Consider this function:
fn print_number(num: i8) {
println!("Number: {}", num);
}
fn add_numbers(num1: i8, num2: i8) {
print_number(num1 + num2);
}
add_numbers(5, 42);
When we run that last line of code, here's what happens:
- Our program sets
stack_bytes[0] = 5
andstack_bytes[1] = 42
- Our program also increments a special
stack_length
global variable twice, so it's at 2 now. - The
add_numbers
function starts running. It looks atstack_bytes[stack_length - 2]
for its first argument, andstack_bytes[stack_length - 1]
for its second argument. - It adds the two numbers and passes them to
print_number
. Passing arguments toprint_number
means writing the number tostack_bytes[stack_length]
and then incrementingstack_length
. - Now when
print_number
runs, it knows that its one argument is instack_bytes[stack_length - 1]
, just likeadd_numbers
did. - When
print_number
returns, it decrementsstack_length
, meaning the bytes it was using for its argument are now effectively available for future function calls to use. - When
add_number
returns, it decrementsstack_length
twice, meaning it is now back down to 0 and the entire stack is free for future function calls.
Normally, this is how things go - as we nest function calls, stack_length
increases, and as they return, it decreases. If we have too many nested function
calls, stack_length
might exceed the total number of bytes available in the
stack. This is called a Stack Overflow, and it normally crashes the program.
Returning values on the Stack
Returning values on the stack works similarly to passing arguments.
fn add_numbers(num1: i8, num2: i8) -> i8 {
return num1 + num2;
}
let answer = add_numbers(5, 42);
Here, the same operations happen as before for passing arguments. However,
before writing the arguments into stack_bytes
, the caller increments
stack_length
an extra time up front, to leave a buffer for the return value.
This means that the arguments will be written one byte later than before, and
there will be a gap of unused memory right before them.
After add_numbers
finishes running, it will decrement stack_length
twice as
usual (to free the stack space taken up by its arguments).
Now stack_bytes[stack_length - 1]
will refer to that gap of unused memory we
created ahead of time. add_numbers
will write the return value into that
slot before exiting the function.
Now when the caller resumes running, it knows that the return value from
add_numbers
can be found in stack_bytes[stack_length - 1]
. It also knows
that it needs to decrement stack_length
one more time before it exits, to make
sure it frees up that stack memory for other functions.