Tip
As we end the program in an endless loop, you’ll have to exit by pressing Ctrl + C.
OK, so what happened? We didn’t call the function hello at any point, but it still executed.
What happened is that we actually made the CPU jump over to our own stack, and since it thinks it returns from a function, it will read the address to hello and start executing the instructions it points to. We have taken the first step toward implementing a context switch.
In the next sections, we will talk about the stack in a bit more detail before we implement our fibers. It will be easier now that we have covered so much of the basics.
The stack
A stack is nothing more than a piece of contiguous memory.
This is important to know. A computer only has memory, it doesn’t have a special stack memory and a heap memory; it’s all part of the same memory.
The difference is how this memory is accessed and used. The stack supports simple push/pop instructions on a contiguous part of memory, that’s what makes it fast to use. The heap memory is allocated by a memory allocator on demand and can be scattered around in different locations.
We’ll not go through the differences between the stack and the heap here since there are numerous articles explaining them in detail, including a chapter in The Rust Programming Language at https://doc.rust-lang.org/stable/book/ch04-01-what-is-ownership.html#the-stack-and-the-heap.
What does the stack look like?
Let’s start with a simplified view of the stack. A 64-bit CPU will read 8 bytes at a time. Even though the natural way for us to see a stack is a long line of u8 as shown in Figure 5.2, the CPU will treat it more like a long line of u64 instead since it won’t be able to read less than 8 bytes when it makes a load or a store.
Figure 5.3 – The stack
When we pass a pointer, we need to make sure we pass in a pointer to either address 0016, 0008, or 0000 in the example.
The stack grows downwards, so we start at the top and work our way down.
When we set the stack pointer in a 16-byte aligned stack, we need to make sure to put our stack pointer to an address that is a multiple of 16. In the example, the only address that satisfies this requirement is 0008 (remember the stack starts on the top).
If we add the following lines of code to our example in the last chapter just before we do the switch in our main function, we can effectively print out our stack and have a look at it:
ch05/b-show-stack
for i in 0..SSIZE {
println!(“mem: {}, val: {}”,
sb_aligned.offset(-i as isize) as usize,
*sb_aligned.offset(-i as isize))
}
The output we get is as follows:
mem: 2643866716720, val: 0
mem: 2643866716719, val: 0
mem: 2643866716718, val: 0
mem: 2643866716717, val: 0
mem: 2643866716716, val: 0
mem: 2643866716715, val: 0
mem: 2643866716714, val: 0
mem: 2643866716713, val: 0
mem: 2643866716712, val: 0
mem: 2643866716711, val: 0
mem: 2643866716710, val: 0
mem: 2643866716709, val: 127
mem: 2643866716708, val: 247
mem: 2643866716707, val: 172
mem: 2643866716706, val: 15
mem: 2643866716705, val: 29
mem: 2643866716704, val: 240
mem: 2643866716703, val: 0
mem: 2643866716702, val: 0
mem: 2643866716701, val: 0
mem: 2643866716700, val: 0
mem: 2643866716699, val: 0
…
mem: 2643866716675, val: 0
mem: 2643866716674, val: 0
mem: 2643866716673, val: 0
I LOVE WAKING UP ON A NEW STACK!
I’ve printed out the memory addresses as u64 here, so it’s easier to parse if you’re not very familiar with hex.
The first thing to note is that this is just a contiguous piece of memory, starting at address 2643866716673 and ending at 2643866716720.
The addresses 2643866716704 to 2643866716712 are of special interest to us. The first address is the address of our stack pointer, the value we write to the rsp register of the CPU. The range represents the values we wrote to the stack before we made the switch.