cargo init
rustup override set nightly
main.rs
we start by importing the asm!
macro:#[repr(C)]
because we access the data the way we do in our assembly. Rust doesn't have a stable ABI, so there is no way for us to be sure that this will be represented in memory with rsp
as the first 8 bytes. C has a stable ABI and that's exactly what this attribute tells the compiler to use. Granted, our struct only has one field right now, but we will add more later.rsp
register (the address we set to new.rsp
will point to an address located on our own stack that leads to the function above). Got it?ret
keyword transfers program control to the return address located on top of the stack. Since we pushed our address to the rsp
register, the CPU will think that is the return address of the function it's currently running, so when we pass the ret
instruction it returns directly into our own stack.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 unsafeThreadContext
from which we will only read one field.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.0x00
offset (that means no offset at all in hex) from the memory location at {0}
to the rsp
register. Since the rsp
register stores a pointer to the next value on the stack, we effectively push the address we provide it on top of the current stack, overwriting what's already there.[{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 offset parameter here as we'll need it later on.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 second. AT&T
syntax, where reading it as "move a
tob
" 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.{0}
used like this in normal assembly code. 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 from 0, 1, 2…. We only have one input parameter here, which corresponds to {0}
. {}
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.[]
basically means: "get what's at this memory location", you can think of it as the same as dereferencing a pointer. To 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".ret
keyword instructs the CPU to pop a memory location off the top of 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.asm!
macro is our input
parameter. 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 like inout
and lateout
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.hello
is a pointer already (a function pointer) so we can cast it directly as an u64
since all pointers on 64 bits systems will be, well, 64 bit, and then we write this pointer to our new stack.let sb_aligned = (stack_bottom as usize &! 15) as *mut u8;
do?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.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 an u64 only to position 32 which is not what we want.rsp
(Stack Pointer) to the memory address of index 32 in our stack, we don't pass the value of the u64
stored at that location but an address to the first byte.cargo run
this code we get:hello
at any point, but it still was run. What happened is that we actually made the CPU jump over to our own stack and execute code there. We have taken the first step towards implementing a context switch.