Hacker Newsnew | past | comments | ask | show | jobs | submitlogin

Having done this for a dozen of experiments/toys I fully agree with most of the post, would be nice if the the addition of must_tail attribute could be reliable across the big 3 compilers, but it's not something that can be relied on (luckily Clang seems to be fairly reliable on Windows these days).

2 additional points,

1: The article mentions DWARF, even without it you can use #line directives to give line-numbers in your generated code (and this goes a very long way when debugging), the other part is local variables and their contents.

For variables one can get a good distance by using a C++ subset(a subset that doesn't affect compile time, so avoid any std:: namespaced includes) instead and f.ex. "root/gc/smart" ptr's,etc (depending on language semantics), since the variables will show up in a debugger when you have your #line directives (so "sane" name mangling of output variables is needed).

2: The real sore point of C as a backend is GC, the best GC's are intertwined with the regular stack-frame so normal stack-walking routines also gives everything needed for accuracte GC (required for any moving GC designs, even if more naive generation collectors are possible without it).

Now if you want accurate somewhat fast portable stack-scanning the most sane way currently is to maintain a shadow-stack, where you pass prev-frame ptrs in calls and the prev-frame ptr is a ptr to the end of a flat array that is pre-pended by a magic ptr and the previous prev-frame ptr (forming a linked list with the cost of a few writes, one extra argument with no cleanup cost).

Sadly, the performant linked shadow-stack will obfuscate all your pointers for debugging since they need to be clumped into one array instead of multiple named variables (and restricts you from on-stack complex objects).

Hopefully, one can use the new C++ reflection support for shadow-stacks without breaking compile times, but that's another story.





> Having done this for a dozen of experiments/toys I fully agree with most of the post, would be nice if the the addition of must_tail attribute could be reliable across the big 3 compilers, but it's not something that can be relied on (luckily Clang seems to be fairly reliable on Windows these days).

This may be a stupid question, but if the function must tail, that's just a jump, no? Why not use goto?


Works for self recursion, but not interprocedural (Probably why Wingolog mentions the need to keep track in Scheme code).

Great point, thank you, I knew I was forgetting about something!

Related to shadow stacks, I've had trouble convincing the C optimizer that no one else is aliasing my heap-allocated helper stacks. Supposedly there ought to be a way to tell it using restrict annotations, but those are quite fiddly: only work for function parameters, and can be dusmissed for many reasons. Does anyone know of a compiler that successfully used restrict pointers in their generated code? I'd love to be pointed towards something that works.

Note that declaring no aliasing is probably unsafe for concurrent or moving garbage collectors, as then the C compiler can conveniently "forget" to either store or load values to the shadow stack at some points...

(though it is fine if GC can only happen inside a function call and the call takes the shadow stack as an argument)


Concurrent GC's isn't a mess I've dealt with (majority single-threaded languages), moving should be ok if all heap accesses are in single statements through the shadow stack and a pointer to the shadow-stack is always passed on to called functions (Thus the compiler shouldn't be allowed to retain anything, I could be wrong on some slight C standard detail here though).

> ... [pointers] need to be clumped into one array ...

You could put each stack frame into a struct, and have the first field be a pointer to a const static stack-map data structure or function that enumerates the pointers within the frame.

BTW, the passed pointer to this struct could also be used to implement access to the calling function's variables, for when you have nested functions and closures.


Good point, definetly better for debugging, iirc the reason for the array was that the shadow-stack array was part of my calling convention.

The GC-heap arguments to called functions with moveable pointers in the GC was indirected through the passed GC-chain ptr, ie you passed a pointer to the N:th element in the caller's shadow-stack that was then the 1:st heap based argument to the callee.

The benefit of that was the callee didn't need to copy over it's incoming arguments to a new shadow-stack (thus increasing a fixed costs of more arguments) but could just link the previous array and IFF there was any gc-action then pointers "visible" to both the caller(albeit expired) and callee would be correctly moved around.

I guess a "virtual" type as a head and members being an aliasing union could work well enough.

   struct { StackVTab vtab; union { struct { TypeA *localA; TypeB *localB; ... }; void *rawPtrs[LOCALCOUNT];  }; } heapmembers;

As for closures that's trickier, locals visible to a nested function/closure needs to be moved to on-heap objects unless you do more or less recursive lifetime analysis (but that stuff quickly becomes expensive and/or indeterminate).



Guidelines | FAQ | Lists | API | Security | Legal | Apply to YC | Contact

Search: