Interestingly, the compiler gives me this warning:
I'll answer this first because the rest can't be done properly with this in the way. The compiler can generate prolog/epilog code for a function:
SwitchContext:
push ebp ; save the previous frame pointer
mov ebp, esp ; setup the frame pointer
sub esp, 8 ; space for local variables
... ; function code
mov esp, ebp ; restore the stack pointer
pop ebp ; restore the frame pointer
ret
Now it should be obvious what's the warning about. The compiler uses ebp for its own purposes and warns you if you change it in inline asm. This compiler generated code can be a problem when you want to implement context switching because it affects the stack layout. You can implement SwitchContext even if such code is present but creating the stack for a new thread will be a problem because you don't know what stack layout SwitchContext expects.
A possible solution is to use __declspec(naked) (which prevents such code from being generated) and __fastcall (which causes the first 2 arguments of the function to be passed in registers ecx and edx).
I'm a bit rusty on my x86 assembly, but what I don't understand is that the code is moving oldStack into eax, then a few lines later, esp is moved into eax. Is my translation of the original code even correct?
Nope. In the original version there were some parathesis which you ignored. Something like mov %esp, (%eax) converts to mov [eax], esp.
Here's an example that uses __declspec(naked) and __fastcall):
__declspec(naked) void __fastcall SwitchContext(VirtualThread* pOld, VirtualThread* pNew)
{
// N.B. the following code assumes that NativeStack is at offset 4 in VirtualThread. If that's not true
// then the appropiate offset needs to be used when storing/loading the stack.
__asm
{
// Save registers
push ebp
push ebx
push esi
push edi
// Save old stack
mov [ecx+4], esp // store to pOld->NativeStack
// Load new stack
mov esp, [edx+4] // load from pNew->NativeStack
// Restore registers
pop edi
pop esi
pop ebx
pop ebp
ret
}
}
So what I'm missing right now is a way to set up these stacks for the initial run
To create a new thread you have to allocate memory for its native stack and setup the stack exactly the same as in SwitchContext does. Once the thread stack is properly initialized you can simply call SwitchContext to start the new thread.
In addition, my assumption here is that my VirtualThread class will not only hold the "virtual stack", but also this native stack that will be used to do context switching for each thread.
Yes, if you create thread you'll need to also allocate the native stacks yourself.
void InitializeContext(VirtualThread* pNew, void *startFunction)
{
int stackSize = 4096;
char *stack = reinterpret_cast<char *>(malloc(stackSize)); // allocate some space, malloc is just an example
stack += stackSize; // go to the top of the stack because the stack grows down
__asm
{
mov ecx, esp // save current stack pointer
mov esp, stack
mov eax, startFunction
push eax // push the start function address as the return address (of SwitchContext)
// Save registers
xor eax, eax // let's always start a thread with zeroed registers
push eax
push eax
push eax
push eax
mov stack, esp
mov esp, ecx // restore the stack pointer
}
pNew->NativeStack = stack;
}