We’ll use the body of our gt_switch function as a starting point by going through everything step by step.
If you haven’t used inline assembly before, this might look foreign, but we’ll use an extended version of the example later to switch contexts, so we need to understand what’s going on.
unsafe is a keyword that indicates that Rust cannot enforce the safety guarantees in the function we write. Since we are manipulating the CPU directly, this is most definitely unsafe. The function will also take a pointer to an instance of our ThreadContext from which we will only read one field:
unsafe gt_switch(new: *const ThreadContext)
The next line is the asm! macro in the Rust standard library. It will check our syntax and provide an error message if it encounters something that doesn’t look like valid Intel (by default) assembly syntax.
asm!(
The first thing the macro takes as input is the assembly template:
“mov rsp, [{0} + 0x00]”,
This is a simple instruction that moves the value stored at 0x00 offset (that means no offset at all in hex) from the memory location at {0} to the rsp register. Since the rsp register usually stores a pointer to the most recently pushed value on the stack, we effectively push the address to hello on top of the current stack so that the CPU will return to that address instead of resuming where it left off in the previous stack frame.
Note
Note that we don’t need to write [{0} + 0x00] when we don’t want an offset from the memory location. Writing mov rsp, [{0}] would be perfectly fine. However, I chose to introduce how we do an offset here as we’ll need it later on when we want to access more fields in our ThreadContext struct.
Note that the Intel syntax is a little backward. You might be tempted to think mov a, b means “move what’s at a to b”, but the Intel dialect usually dictates that the destination register is first and the source is second.
To make this confusing, this is the opposite of what’s typically the case with the AT&T syntax, where reading it as “move a to b” is the correct thing to do. This is one of the fundamental differences between the two dialects, and it’s useful to be aware of.
You will not see {0} used like this in normal assembly. This is part of the assembly template and is a placeholder for the value passed as the first parameter to the macro. You’ll notice that this closely matches how string templates are formatted in Rust using println! or the like. The parameters are numbered in ascending order starting from 0. We only have one input parameter here, which corresponds to {0}.
You don’t really have to index your parameters like this; writing {} in the correct order would suffice (as you would do using the println! macro). However, using an index improves readability and I would strongly recommend doing it that way.
The [] basically means “get what’s at this memory location”, you can think of it as the same as dereferencing a pointer.
Let’s try to sum up what we do here with words:
Move what’s at the + 0x00 offset from the memory location that {compiler_chosen_general_purpose_register} points to to the rsp register.
The next line is the ret keyword, which instructs the CPU to pop a memory location off the stack and then makes an unconditional jump to that location. In effect, we have hijacked our CPU and made it return to our stack.
Next up is the first non-assembly argument to the asm! macro is our input parameter:
in(reg) new,
When we write in(reg), we let the compiler decide on a general-purpose register to store the value of new. out(reg) means that the register is an output, so if we write out(reg) new, we need new to be mut so we can write a value to it. You’ll also find other versions such as inout and lateout.