Options
The last thing we need to introduce to get a minimal understanding of Rust’s inline assembly for now is the options keyword. After the input and output parameters, you’ll often see something like options(att_syntax), which specifies that the assembly is written with the AT&T syntax instead of the Intel syntax. Other options include pure, nostack, and several others.
I’ll refer you to the documentation for you to read about them since they’re explained in detail there:
https://doc.rust-lang.org/nightly/reference/inline-assembly.html#options
Inline assembly is quite complex, so we’ll take this step by step and introduce more details on how it works along the way through our examples.
Running our example
The last bit we need is the main function to run our example. I’ll present the whole function and we’ll walk through it step by step:
fn main() {
let mut ctx = ThreadContext::default();
let mut stack = vec![0_u8; SSIZE as usize];
unsafe {
let stack_bottom = stack.as_mut_ptr().offset(SSIZE);
let sb_aligned = (stack_bottom as usize & !15) as *mut u8;
std::ptr::write(sb_aligned.offset(-16) as *mut u64, hello as u64);
ctx.rsp = sb_aligned.offset(-16) as u64;
gt_switch(&mut ctx);
}
}
So, in this function, we’re actually creating our new stack. hello is a pointer already (a function pointer), so we can cast it directly as an u64 since all pointers on 64-bit systems will be, well, 64-bit. Then, we write this pointer to our new stack.
Note
We’ll talk more about the stack in the next segment, but one thing we need to know now is that the stack grows downwards. If our 48-byte stack starts at index 0 and ends on index 47, index 32 will be the first index of a 16-byte offset from the start/base of our stack.
Make note that we write the pointer to an offset of 16 bytes from the base of our stack.
What does the line let sb_aligned = (stack_bottom as usize &! 15) as *mut u8; do?
When we ask for memory like we do when creating a Vec<u8>, there is no guarantee that the memory we get is 16-byte-aligned when we get it. This line of code essentially rounds our memory address down to the nearest 16-byte-aligned address. If it’s already 16 byte-aligned, it does nothing. This way, we know that we end up at a 16-byte-aligned address if we simply subtract 16 from the base of our stack.
We cast the address to hello as a pointer to a u64 instead of a pointer to a u8. We want to write to position “32, 33, 34, 35, 36, 37, 38, 39”, which is the 8-byte space we need to store our u64. If we don’t do this cast, we try to write a u64 only to position 32, which is not what we want.
When we run the example by writing cargo run in our terminal, we get:
Finished dev [unoptimized + debuginfo] target(s) in 0.58s
Running `target\debug\a-stack-swap`
I LOVE WAKING UP ON A NEW STACK!