4 ms·
This is a very neat hack, but it breaks in the presence of: - Inlining: it will work but the defer will be executed at the end of the caller function, which ma
by comex 6y ago
This is a very neat hack, but it breaks in the presence of:
- Inlining: it will work but the defer will be executed at the end of the caller function, which may not be what you expected.
- Tail call optimization: same issue. (You mention in a comment that you use a dummy asm statement to prevent the call to `__defer` itself from being tail-call optimized, but there's still an issue if one defer-using function tail-calls another defer-using function.)
- Function outlining aka hot/cold splitting (currently implemented in LLVM): arbitrary chunks of a function can be split out into their own functions; if one of those does a defer, the cleanup might be run too early, considerably more dangerous than too late.
- Various CFI (control-flow integrity) implementations that are specifically designed to prevent the return address from being overwritten (by exploits).
- Interprocedural register allocation (-fipa-ra in GCC) if __defer gets inlined or analyzed via link-time optimization: The compiler can make assumptions that functions won't modify certain registers that the ABI would normally allow them to modify; this will be violated if it unexpectedly jumps to __defer. This is fixable by marking __defer as __attribute__((noipa)) or reimplementing in assembly.
- Targeting WebAssembly or BPF or other high-level machines that don't support overwriting return addresses.
- Compilers that don't support inline assembly (MSVC).
EDIT: - Targeting ARM if the compiler happens to stash the return address in an unexpected location. You can't fix this by writing to LR like you suggested; the return address needs to be in LR when you execute the ret instruction, but the compiler doesn't need to keep it there for the whole function, and usually won't. Instead, it will usually save it to the stack frame, and load it back before returning, potentially using LR for completely unrelated purposes in between. So usually you can modify the return address by using __builtin_frame_address just like on x86. But that's an implementation detail; it could decide to keep a copy in another register, and move that to LR when returning. Not sure if any compilers actually do that, though I think I might have seen something like that on PowerPC.
Your approach is also relatively slow, since the cleanup code can't be inlined.
(If you're going to use a GNU extension for inline asm, why not just use the GNU extension __attribute__((cleanup))? It's block-scoped, but it doesn't disable any optimization passes or anything since the compiler knows about it, it's portable, and it doesn't have the problems I mentioned.)
- jart 6y agoThank you for the thorough response. Inlining, tailcall, hot/cold: None of these are issues. They don't change the fact that memory passed to gc() will be freed. Worst case scenario is the can gets kicked down the road, which is relatively easy to predict. See https://gist.github.com/jart/5aba7fc72c7b6781dadd5949c289a0b6 https://gist.github.com/jart/5aba7fc72c7b6781dadd5949c289a0b... So long as you're not using this technique to unlock mutexes, you'll be fine. Developers who are required to use CFI need to reach out to their policymakers for authorization to modify return addresses before using the gc() macro. Folks required to use MSVC can use the existence of the gc() macro as compelling evidence for their bosses on the benefits of switching to GCC or Clang. I'll take you on your word on IPA. I've added a comment to the Gist making sure folks who use it are aware. Thanks for the awesome info on ARM. That's good to know. Also I believe the code is fast. I like the gc() macro because it can be used in expressions. I find __attribute__((__cleanup__)) unpleasant since it has strong opinions about how variables and cleanup functions need to be declared.
- avianes 6y ago> So long as you're not using this technique to unlock mutexes, you'll be fine. This does not sound good. In addition, it is worth mentioning that your hack will probably break the return stack buffer of the x86 superscalar processors. While a "defer" implementation would have neither of these two flaws. No doubt it's a pretty neat hack, but I would be sceptical about using it extensively in place of a "defer".
- comex 6y ago> Inlining, tailcall, hot/cold: None of these are issues. They don't change the fact that memory passed to gc() will be freed. Worst case scenario is the can gets kicked down the road, which is relatively easy to predict. [..] So long as you're not using this technique to unlock mutexes, you'll be fine. That's true for inlining and tailcall, but not hot/cold, since it can make cleanups execute earlier than expected rather than later. This happens if: (1) some chunk of the function is extracted into a separate function; (2) the chunk contains a call to defer; and (3) some code which is not in the extracted chunk, but executes after it, expects the cleanup to not have run yet. See this example: https://gcc.godbolt.org/z/1ro1z6 https://gcc.godbolt.org/z/1ro1z6 To be fair, Clang does not enable this optimization by default, and it might get replaced by a different implementation of hot-cold splitting [1] which happens to not suffer from this issue (because it operates later in the pipeline, essentially splitting blocks at the assembly level rather than the IR level). On the other hand, GCC does enable a form of hot-cold splitting by default, via -fpartial-inlining, but it's limited to splitting out suffixes of the function (i.e. regions starting somewhere in the function and including everything in the function that can be executed from then on), rather than arbitrary regions. Therefore, it can't run into the problematic case where non-extracted code runs after extracted code. Still, this is just an implementation limitation that could be lifted in the future. > I like the gc() macro because it can be used in expressions. I find __attribute__((__cleanup__)) unpleasant since it has strong opinions about how variables and cleanup functions need to be declared. Fair enough; I do agree on that point. (I wish GCC had a way to either use attribute cleanup with C99 compound literals, or somehow declare variables that live within statement expressions as having their lifetime extended to a surrounding block… Maybe what I really want is a better macro system. Or the native `defer` feature proposed here, but I doubt that will ever happen.) [1] https://lists.llvm.org/pipermail/llvm-dev/2020-August/144012.html https://lists.llvm.org/pipermail/llvm-dev/2020-August/144012...