3

In Why volatile works for setjmp/longjmp, user greggo comments:

Actually modern C compilers do need to know that setjmp is a special case, since there are, in general, optimizations where the change of flow caused by setjmp could badly corrupt things, and these need to be avoided. Back in K&R days, setjmp did not need special handling, and didn't get any, and so the caveat about locals applied. Since that caveat is already there and (should be!) understood - and of course, setjmp use is pretty rare - there is no incentive for modern compilers to go to any extra lengths to fix the 'clobber' issue -- it would still be in the language.

Are there any references that elaborate on this and if this is true, can there safely exist (with behavior no more error-prone than that of standard setjmp/longjmp) custom-made implementations of setjmp/longjmp (e.g., maybe I'd like to save some extra (thread-local) context) that are named something different? Like is there anyway to tell compilers "this function is effectively setjmp/longjmp"?

Peter Cordes
  • 328,167
  • 45
  • 605
  • 847
Petr Skocik
  • 58,047
  • 6
  • 95
  • 142
  • 2
    There is `__attribute__((returns_twice))` which seems to be intended for this purpose, but I don't fully understand what it does. The description says "ensures that all registers are dead before calling such a function" which I don't get. Call-clobbered registers would have to be "dead" in any case, and there'd seem to be no benefit in saving call-preserved registers since your setjmp equivalent must save and restore them anyway. It's unclear if this attribute is supposed to be needed for correctness, or just for better warnings. – Nate Eldredge Jun 22 '22 at 13:17
  • @NateEldredge Nice find! I think it's meant to reduce problems with forbidden scenarios such as variables that are changed between setjmp and longjmp and are not marked volatile as they should be. If those are spilled rather than kept around in call-preserved registers, they won't be saved by setjmp and will keep their latest value after a longjmp. (https://gcc.godbolt.org/z/K3WsEhe5W) It's probably still too difficult for compilers to always eliminate the need for the volatile markings, though (https://stackoverflow.com/a/7969601/1084774). – Petr Skocik Jun 22 '22 at 15:03

3 Answers3

3

GCC does do a bit of special handling for setjmp, matching it by name along with sigsetjmp, vfork, getcontext, and savectx. (After stripping leading _). On a match it sets the internal flag ECF_RETURNS_TWICE. I think this is equivalent to an implicit __attribute__((returns_twice)) (which you can use for your own functions). The glibc headers don't use that, they just rely on the name matching. (An earlier version of this answer was fooled by that into thinking they weren't special-cased at all.)

longjmp doesn't need much special handling; it just looks like any other __attribute__((noreturn)) function call. Glibc declares longjmp that way, which should make side-effects on locals happen before a call to it, and for example avoids warnings about execution falling off the end of a non-void function in something like int foo(){ if(x) return y; longjmp(jmpbuf); }


setjmp / longjmp don't guarantee much more than what any opaque function (not inlinable) would look like for the optimizer. (But one key difference involves not reusing stack space for separate locals when one could come back into scope when setjmp returns again, see @amonakov's answer.)

Side effects on non-volatile locals might have been re-ordered at compile time wrt. setjmp (or longjmp) if escape analysis can show that no global variable could have their address.

Optimization is still allowed to keep locals in registers instead of memory during a call to setjmp. That means side-effects on non-volatile variables done after setjmp, before longjmp, might or might not get rolled back when longjmp restores the call-preserved registers to the saved state in the jmp_buf.

The Linux man page for setjmp(3) lays out the rules:

The compiler may optimize variables into registers, and longjmp() may restore the values of other registers in addition to the stack pointer and program counter. Consequently, the values of automatic variables are unspecified after a call to longjmp() if they meet all the following criteria:

  • they are local to the function that made the corresponding setjmp() call;
  • their values are changed between the calls to setjmp() and longjmp(); and
  • they are not declared as volatile.

From glibc's /usr/include/setjmp.h

// earlier CPP macros to define __THROWNL as __attribute__ ((__nothrow__)) in C++ mode

extern int setjmp (jmp_buf __env) __THROWNL;
extern void longjmp (struct __jmp_buf_tag __env[1], int __val)
     __THROWNL __attribute__ ((__noreturn__));
extern void siglongjmp (sigjmp_buf __env, int __val)
     __THROWNL __attribute__ ((__noreturn__));

There's a bunch of C preprocessor stuff to define a _ version (no-signal setjmp) and so on.

BTW, there is a __builtin_setjmp. But it works somewhat differently: the GCC manual recommends against using it in user code, and the ISO C setjmp/longjump library functions can't be defined in terms of it.

Peter Cordes
  • 328,167
  • 45
  • 605
  • 847
  • 2
    `__builin_setjmp` exists, it has different semantics to `setjmp` as documented in https://gcc.gnu.org/onlinedocs/gcc/Nonlocal-Gotos.html – amonakov Jun 28 '22 at 13:44
  • 2
    You're missing that optimizers need to be prevented from reusing storage of automatic variables *that are not modified after setjmp* for other automatic variables: https://gcc.godbolt.org/z/h6GGs5bbb – amonakov Jun 28 '22 at 14:39
  • @amonakov: Good points, especially the second one. Does GCC actually special-case `setjmp`? https://gcc.godbolt.org/z/saPPdYTjx is a more clearly dangerous example, using an actual `jmp_buf` and a `char tmp2[sizeof(jmp_buf)];`. With actual `setjmp`, GCC avoids using the same storage, but with `my_setjmp` it shares. How does GCC make that happen? Is it really special-casing the function name on its own, without headers doing anything? But the difference is present even with `-fno-builtin` https://gcc.godbolt.org/z/4zjY5P4o5 – Peter Cordes Jun 29 '22 at 00:28
  • @amonakov: I think your example has UB for any path of execution that could lead to my_setjmp returning twice. Your `tmp[]` is already out of scope when we reach `f()`, so there's no way a correct program could read it, to return to it as a jump buffer. Upon leaving scope, its storage *can* be reused. Updated my answer with it as a demo that GCC seems to be special-casing the name `_setjmp` somehow, but I don't think it's a correctness issue. – Peter Cordes Jun 29 '22 at 00:49
  • 3
    Yes, GCC recognizes a few functions that may return twice by name ([sig]setjmp, savectx, getcontext, vfork: https://gcc.gnu.org/git/?p=gcc.git;a=blob;f=gcc/calls.cc;h=f4e1299505ed542f34a6873c3537b881ed288c98;hb=HEAD#l578 ). Here's a better example where the clobbered variable does not go out of scope: https://gcc.godbolt.org/z/Y49snGGsG – amonakov Jun 29 '22 at 06:48
  • 1
    @amonakov: Thanks! Working on an update, but if you want to post your own answer, that'd be great. Seems I guessed wrong here and need to basically rewrite most of this. – Peter Cordes Jun 29 '22 at 06:58
  • I missed that in my last example ebx will be restored upon the "second return", so coming up with a proper demonstration proves tricky. – amonakov Jun 29 '22 at 10:05
  • ... okay, with a bit of luck I managed to produce a proper example: https://gcc.godbolt.org/z/cxvYTxoEv – amonakov Jun 29 '22 at 15:25
  • Posted my own answer now. – amonakov Jun 29 '22 at 19:55
  • "`setjmp` / `longjmp` don't guarantee much more less than what any opaque function (not inlinable) would look like for the optimizer." Is the "much more less" wording here in error? – ecm Jun 29 '22 at 21:15
  • @ecm: Yeah, just a stray word left over left over from editing "no more and no less than" into "not much more". Fixed, thanks. – Peter Cordes Jun 29 '22 at 21:54
3

The C language defines setjmp to be a macro and places strict limitations on context in which it may appear without invoking undefined behavior. It is not a normal function: you cannot take its address and expect a call via the resulting pointer to behave as a proper setjmp invocation.

In particular, it is not true in general that assembly code invoked by setjmp obeys the same calling conventions as normal functions. SPARC on Linux and Solaris provides a counterexample: its setjmp does not restore all call-preserved registers (nor does vfork). It took GCC by surprise as recently as 2018 (gcc-patches thread, bugzilla entry).

But even considering "compiler-friendly" platforms where setjmp entrypoint obeys the usual conventions, it is still necessary to recognize it as a function that "returns twice". GCC recognizes setjmp-like functions (including vfork) by name, and offers __attribute__((returns_twice)) for annotating such functions in custom code.

The reason for that is longjmp'ing back to setjmp can transfer control from a point where some variable or temporary appears dead (and the compiler reused its storage for something unrelated) back to where it was live (but its storage is "clobbered" now, oops).

Constructing an example that demonstrates how this happens is a bit tricky: the clobbered storage cannot be a register, because if it's call-clobbered it wouldn't be in use at the point of setjmp, and if it is call-saved longjmp would restore it (SPARC exception aside). So it needs to be forced to stack without making addresses of both variables exposed in a way that makes their lifetimes overlap, preventing reuse of stack slots, and without making one of them go out of scope before longjmp.

With a bit of luck I managed to arrive at the following testcase, which when compiled with -O2 -mtune-ctrl=^inter_unit_moves_from_vec (view on Compiler Explorer):

//__attribute__((returns_twice))
int my_setjmp(void);

__attribute__((noreturn))
void my_longjmp(int);

static inline
int float_as_int(float x)
{
    return (union{float f; int i;}){x}.i;
}

float f(void);

int g(void)
{
    int ret = float_as_int(f());

    if (__builtin_expect(my_setjmp(), 1)) {
        int tmp = float_as_int(f());
        my_longjmp(tmp);
    }
    return ret;
}

produces the following assembly:

g:
        sub     rsp, 24
        call    f
        movss   DWORD PTR [rsp+12], xmm0
        call    my_setjmp
        test    eax, eax
        je      .L2
        call    f
        movss   DWORD PTR [rsp+12], xmm0
        mov     edi, DWORD PTR [rsp+12]
        call    my_longjmp
.L2:
        mov     eax, DWORD PTR [rsp+12]
        add     rsp, 24
        ret

The -mtune-ctrl=^inter_unit_moves_from_vec flag causes GCC to implement SSE-to-gpr moves via stack, and both moves use the same stack slot, because as far as the compiler can tell, there's no conflict (computing 'tmp' leads to a noreturn function, so temporary used for computing 'ret' is no longer needed). However, if my_longjmp transfers control back to my_setjmp, after branching to label .L2 we try to read the value of 'ret' from the overwritten slot.

amonakov
  • 2,324
  • 11
  • 23
  • 1
    Non-volatile registers are "callee-saved" in [that confusing terminology](https://stackoverflow.com/a/56178078/224132). Or in more sane terminology, they're call-preserved. "caller-saved" registers are ones that a caller should expect to be clobbered by a function call (aka "call-clobbered"). I fixed your answer to say the right thing. – Peter Cordes Jun 29 '22 at 20:53
  • 1
    Also, nice work coming up with a test-case to demonstrate the need for `__attribute__((returns_twice))`. Yeah, this would return the result of the 2nd call to `f()` when it should return the first. – Peter Cordes Jun 29 '22 at 20:59
2

First of all, the correct answer to why volatile works in the linked posts is "because the C standard explicitly says so." I don't think the quoted part is true, because C explicitly lists a lot of poorly-defined behavior associated with setjmp/longjmp. The relevant part can be found in C17 7.13.2.1:

All accessible objects have values, and all other components of the abstract machine have state, as of the time the longjmp function was called, except that the values of objects of automatic storage duration that are local to the function containing the invocation of the corresponding setjmp macro that do not have volatile-qualified type and have been changed between the setjmp invocation and longjmp call are indeterminate.

Even C90 says more or less the same as the above. So the reason why compilers, modern or not, don't need to "fix this" is because C has never required them to. In the example where the quoted comment was posted, the second time that if ( foo != 5 ) is executed, the value of foo is indeterminate (and foo never has its address taken), so strictly speaking that line simply invokes undefined behavior and the compiler can do as it pleases from there - it's a bug created by the application programmer, not the optimizer.

Generally, any application programmer using setjmp.h will get what is coming to them. It is the worst possible form of spaghetti programming.

ecm
  • 2,583
  • 4
  • 21
  • 29
Lundin
  • 195,001
  • 40
  • 254
  • 396
  • 1
    setjmp/longjmp.is a primitive exception handling mechanism. There is nothing wrong with it really. It's a tool. You can build spaghetti code with it if you like spaghetti, but you don't have to. – n. m. could be an AI Jun 22 '22 at 08:35
  • @n.1.8e9-where's-my-sharem. There's some 10 to 20 different forms of poorly-defined behavior associated with `setjmp`/`longjmp`. I think that's some sort of record even for C features. Apart from spaghetti programming, the potential for severe bug creation when these functions are used is significant. Using them for exception handling is a terrible idea. – Lundin Jun 22 '22 at 08:49
  • 3
    I have worked with a huge multiplatform codebase which used setjmp/longjmp for exception handling. I don't remember problems associated with this feature. There were more than enough other problems, but not in this area. I guess these are my 2¢. – n. m. could be an AI Jun 22 '22 at 10:29