2 ms·
Not spilling registers into the stack will show up quickly. You have to instigate a GC torture test as part of your testing, combined with a way of detecting u
by kazinator 3y ago
Not spilling registers into the stack will show up quickly.
You have to instigate a GC torture test as part of your testing, combined with a way of detecting use-after-free. (An obvious way is to reset the type of freed objects, so any use of such an object runs into type mismatches.)
A GC torture tests causes the collection routine to be called often, like after every allocation. If an object is allocated, and now held only in an unspilled register, and then another object is allocated; that first object will be wrongly freed. This is what we want: reproduce the problem, and then hopefully detect it.
If you're garbage collecting underneath a compiler that doesn't know about garbage collection, you have to worry about not only values held in registers not being spilled, but values that are optimized away entirely, which your garbage collector has to see.
For instance:
obj foo_create(obj param)
{
struct foo_object *foo = malloc(sizeof *fo);
foo->bar_attribute = param;
return create_heap_object(foo_type, foo);
}
The problem is that param has no next use after it is assigned to bar_attribute; as far as the optimizer is concerned, it is now a dead variable. Whatever register it occupied can be reused for something else.
Let's assume our garbage collector is conservative only w.r.t. stack scanning. Heap objects are traced precisely. We are not scanning every malloc object in existence, in other words, and so, the bar_attribute is not visible to the garbage collector.
Once we call create_heap_object(foo_type, foo), the foo_object structure becomes hooked up to a garbage-collected heap object, and then it is visible to the garbage collector. The foo_type has garbage collector hooks for traversing the "struct foo_object".
Problem is, create_heap_object calls into the garbage collection module to allocate a new heap object, and that can trigger GC. When GC runs, it can wrongly collect the param object.
This can happen if the function has a caller which itself has let go of the param object, like:
obj caller(obj cparam)
{
obj tparam = transform_somehow(cparam);
obj foo = foo_create(tparam);
return foo;
}
Again, tparam has no next use, and so it can cease to exist in this stack frame the moment that foo_create is called. I.e. the only reference to tparam in the entire program is the activation frame of foo_create. Scanning the stack of caller will not find it.
- matheusmoreira 3y agoI assumed that param would still be reachable because foo_object is reachable. If this is not the case, is it because struct foo_object differs from the obj type? Perhaps that causes the garbage collector to fail to find the pointers. Will the code be correct if I ensure that every value on the stack can be recognized by the garbage collector? If not, then I suppose the way to avoid those issues is to tell the compiler the variable is used somehow. There's the volatile keyword and also lots of compiler attributes I can use. I'm finding it pretty difficult to determine which variables need to be marked this way though. What is the proper discipline? How did you solve the problem? I compile with strict aliasing disabled because of problems like these... Maybe I need to disable more optimizations.