Hey guys,
In today's "Zneak's time-wasting adventures", I decided I should try to implement coroutines (I think that's how I should call them). I expect to have to use assembler, and probably some C if I want to make this actually useful for anything.
Bear in mind that this is for educational purposes. Using an already built coroutine library is too easy (and really no fun).
You guys know setjmp
and longjmp
? They allow you to unwind the stack up to a predefined location, and resumes execution from there. However, it can't rewind to "later" on the stack. Only come back earlier.
jmpbuf_t checkpoint;
int retval = setjmp(&checkpoint); // returns 0 the first time
/* lots of stuff, lots of calls, ... We're not even in the same frame anymore! */
longjmp(checkpoint, 0xcafebabe); // execution resumes where setjmp is, and now it returns 0xcafebabe instead of 0
What I'd like is a way to run, without threading, two functions on different stacks. (Obviously, only one runs at a time. No threading, I said.) These two functions must be able to resume the other's execution (and halt their own). Somewhat like if they were longjmp
ing to the other. Once it returns to the other function, it must resume where it left (that is, during or after the call that gave control to the other function), a bit like how longjmp
returns to setjmp
.
This is how I thought it:
- Function
A
creates and zeroes a parallel stack (allocates memory and all that). - Function
A
pushes all its registers to the current stack. - Function
A
sets the stack pointer and the base pointer to that new location, and pushes a mysterious data structure indicating where to jump back and where to set the instruction pointer back. - Function
A
zeroes most of its registers and sets the instruction pointer to the beginning of functionB
.
That's for the initialization. Now, the following situation will indefinitely loop:
- Function
B
works on that stack, does whatever work it needs to. - Function
B
comes to a point where it needs to interrupt and giveA
control again. - Function
B
pushes all of its registers to its stack, takes the mysterious data structureA
gave it at the very beginning, and sets the stack pointer and the instruction pointer to whereA
told it to. In the process, it hands backA
a new, modified data structure that tells where to resumeB
. - Function
A
wakes up, popping back all the registers it pushed to its stack, and does work until it comes to a point where it needs to interrupt and giveB
control again.
All this sounds good to me. However, there is a number of things I'm not exactly at ease with.
- Apparently, on good ol' x86, there was this
pusha
instruction that would send all registers to the stack. However, processor architectures evolve, and now with x86_64 we've got a lot more general-purpose registers, and likely several SSE registers. I couldn't find any evidence thatpusha
does push them. There are about 40 public registers in a mordern x86 CPU. Do I have to do all thepush
es myself? Moreover, there is nopush
for SSE registers (though there's bound to be an equivalent—I'm new to this whole "x86 assembler" thing). Is changing the instruction pointer as easy as saying it? Can I do, like,It's justmov rip, rax
(Intel syntax)? Also, getting the value from it must be somewhat special as it constantly changes. If I do likemov rax, rip
(Intel syntax again), willrip
be positioned on themov
instruction, to the instruction after it, or somewhere between?jmp foo
. Dummy.- I've mentioned a mysterious data structure a few times. Up to now I've assumed it needs to contain at least three things: the base pointer, the stack pointer and the instruction pointer. Is there anything else?
- Did I forget anything?
- While I'd really like to understand how things work, I'm pretty sure there are a handful of libraries that do just that. Do you know any? Is there any POSIX- or BSD-defined standard way to do it, like
pthread
for threads?
Thanks for reading my question textwall.