Garbage collector/tracing the stack/registers
Posted: Thu Aug 14, 2014 7:43 am
I'm trying to figure out how you would walk up the stack and registers in a garbage collector on JIT'ed code.
Here's one scenario I came up with: In a single threaded environment, every time I call a function pointer or perform a memory allocation, I have to make sure that any registers containing an object pointer is spilled to the stack.
I'll have to lay out my stack frame in a consistent way so my GC can scan it, something like this (growing down):
- Previous BP
- Number of GC pointers
- GC pointer 0
- GC pointer 1
...
- GC pointer n
- Shadow space for other variables
Then when my GC is invoked, it can walk up the stack frame by following the BP (EBP/RBP) pointers.
I do have to handle a special case. That is when we enter native code C. When I thunk from native->managed (JIT'ed) code I could put a little dummy frame where the "number of GC pointers" is 0xFFFF FFFF FFFF FFFF, and that alerts my GC to stop walking the stack because we're about to reach native code.
This fails if my JIT'ed environment is called recursively:
native block 1->managed block 1->native block 2->managed block 2
If the GC is invoked in managed block 2, it'll walk up the stack, reach native block 2, stop walking and we couldn't trace all references in managed block 1.
My solution would be to pass a "managed BP" in my managed->native thunks, and pass that "managed BP" back in my native->managed thunks, then in my stack frame the "previous BP" pointers can jump over the the native block. The thunks can either read/write this from memory (e.g. a thread->lastBP field somewhere) - but then I may be accessing arbitrary memory and breaking the CPU cache - or I can pass this as a parameter back to native code and the burden goes to the programmer to remember to pass this back into the VM when we call managed code again.
Now this wouldn't work in a multithreaded environment. Because if we stop all threads at some arbitrary point in time, we can't assume:
a) That all threads have all of their pointers spilled on the stack. Even if we defensively spill these registers on the stack between every assignment - there will be points of time - such as when a function call returns an object - that it exists purely in a register and not in the stack - unless I always have one register (e.g. r15) that is only ever used for returning and temporarily holding pointers, and my GC can check that when tracing a thread.
b) We will be in managed code. If we stop in native code, then all bets with scanning the registers and stacks are off. Unless again we are writing to thread->lastBP everytime we thunk into native code, and clearing it everytime we are entering managed code, and when we walk up the stack we start walking from either thread->lastBP if it's set (meaning we're in native code) or the RBP pointer (meaning we're in managed code.) But what if our native code is holding to a pointer to something?
But.. a) I've lost a register, b) I'm writing to memory (thread->lastBP) every time I jump between native and managed code and we still wouldn't find everything native code is holding a pointer to.
There is a lot of information online about garbage collectors but I've found very little information about how you would trace around registers and in multithreaded environment. Is there a better way to do this?
Here's one scenario I came up with: In a single threaded environment, every time I call a function pointer or perform a memory allocation, I have to make sure that any registers containing an object pointer is spilled to the stack.
I'll have to lay out my stack frame in a consistent way so my GC can scan it, something like this (growing down):
- Previous BP
- Number of GC pointers
- GC pointer 0
- GC pointer 1
...
- GC pointer n
- Shadow space for other variables
Then when my GC is invoked, it can walk up the stack frame by following the BP (EBP/RBP) pointers.
I do have to handle a special case. That is when we enter native code C. When I thunk from native->managed (JIT'ed) code I could put a little dummy frame where the "number of GC pointers" is 0xFFFF FFFF FFFF FFFF, and that alerts my GC to stop walking the stack because we're about to reach native code.
This fails if my JIT'ed environment is called recursively:
native block 1->managed block 1->native block 2->managed block 2
If the GC is invoked in managed block 2, it'll walk up the stack, reach native block 2, stop walking and we couldn't trace all references in managed block 1.
My solution would be to pass a "managed BP" in my managed->native thunks, and pass that "managed BP" back in my native->managed thunks, then in my stack frame the "previous BP" pointers can jump over the the native block. The thunks can either read/write this from memory (e.g. a thread->lastBP field somewhere) - but then I may be accessing arbitrary memory and breaking the CPU cache - or I can pass this as a parameter back to native code and the burden goes to the programmer to remember to pass this back into the VM when we call managed code again.
Now this wouldn't work in a multithreaded environment. Because if we stop all threads at some arbitrary point in time, we can't assume:
a) That all threads have all of their pointers spilled on the stack. Even if we defensively spill these registers on the stack between every assignment - there will be points of time - such as when a function call returns an object - that it exists purely in a register and not in the stack - unless I always have one register (e.g. r15) that is only ever used for returning and temporarily holding pointers, and my GC can check that when tracing a thread.
b) We will be in managed code. If we stop in native code, then all bets with scanning the registers and stacks are off. Unless again we are writing to thread->lastBP everytime we thunk into native code, and clearing it everytime we are entering managed code, and when we walk up the stack we start walking from either thread->lastBP if it's set (meaning we're in native code) or the RBP pointer (meaning we're in managed code.) But what if our native code is holding to a pointer to something?
But.. a) I've lost a register, b) I'm writing to memory (thread->lastBP) every time I jump between native and managed code and we still wouldn't find everything native code is holding a pointer to.
There is a lot of information online about garbage collectors but I've found very little information about how you would trace around registers and in multithreaded environment. Is there a better way to do this?