Guard, skip, and switch functions – Creating Our Own Fibers-1

There are a few functions we’ve referred to that are really important for our Runtime to actually work. Fortunately, all but one of them are extremely simple to understand. We’ll start with the guard function:
fn guard() {
    unsafe {
        let rt_ptr = RUNTIME as *mut Runtime;
        (*rt_ptr).t_return();
    };
}

The guard function is called when the function that we passed in, f, has returned. When f returns, it means our task is finished, so we de-reference our Runtime and call t_return(). We could have made a function that does some additional work when a thread is finished, but right now, our t_return() function does all we need. It marks our thread as Available (if it’s not our base thread) and yields so we can resume work on a different thread.

Next is our skip function:
#[naked]
unsafe extern “C” fn skip() {
    asm!(“ret”, options(noreturn))
}

There is not much happening in the skip function. We use the #[naked] attribute so that this function essentially compiles down to just ret instruction. ret will just pop off the next value from the stack and jump to whatever instructions that address points to. In our case, this is the guard function.

Next up is a small helper function named yield_thread:
pub fn yield_thread() {
    unsafe {
        let rt_ptr = RUNTIME as *mut Runtime;
        (*rt_ptr).t_yield();
    };
}

This helper function lets us call t_yield on our Runtime from an arbitrary place in our code without needing any references to it. This function is very unsafe, and it’s one of the places where we make big shortcuts to make our example slightly simpler to understand. If we call this and our Runtime is not initialized yet or the runtime is dropped, it will result in undefined behavior. However, making this safer is not a priority for us just to get our example up and running.

We are very close to the finish line; just one more function to go. The last bit we need is our switch function, and you already know the most important parts of it already. Let’s see how it looks and explain how it differs from our first stack swap function:
#[naked]
#[no_mangle]
unsafe extern “C” fn switch() {
    asm!(
        “mov [rdi + 0x00], rsp”,
        “mov [rdi + 0x08], r15”,
        “mov [rdi + 0x10], r14”,
        “mov [rdi + 0x18], r13”,
        “mov [rdi + 0x20], r12”,
        “mov [rdi + 0x28], rbx”,
        “mov [rdi + 0x30], rbp”,
        “mov rsp, [rsi + 0x00]”,
        “mov r15, [rsi + 0x08]”,
        “mov r14, [rsi + 0x10]”,
        “mov r13, [rsi + 0x18]”,
        “mov r12, [rsi + 0x20]”,
        “mov rbx, [rsi + 0x28]”,
        “mov rbp, [rsi + 0x30]”,
        “ret”, options(noreturn)
    );
}

So, this is our full stack switch function. You probably remember from our first example that this is just a bit more elaborate. We first read out the values of all the registers we need and then set all the register values to the register values we saved when we suspended execution on the new thread.

This is essentially all we need to do to save and resume the execution.

Here we see the #[naked] attribute used again. Usually, every function has a prologue and an epilogue and we don’t want that here since this is all assembly and we want to handle everything ourselves. If we don’t include this, we will fail to switch back to our stack the second time.

You can also see us using the offset we introduced earlier in practice:
0x00[rdi] # 0
0x08[rdi] # 8
0x10[rdi] # 16
0x18[rdi] # 24

Leave a Reply

Your email address will not be published. Required fields are marked *

Related Post