为什么这个编译器在涉及到setjmp时输出错误?

Why is this compiler output wrong when setjmp is involved?

给出这个 C 代码

#include <stdio.h>
#include <setjmp.h>
void foo(int x) {
  jmp_buf env;
  if (setjmp(env) == 0) {
    printf("%d\n", 23);
    longjmp(env, 1);
  } else {
    printf("%d\n", x);
  }
}

结果应该是它打印 23 然后是 x 并且应该都定义明确。

但是假设编译器不知道 setjmp/longjmp 是特殊函数,它会生成以下代码:

;function foo
;r0 : int x

foo:
    sub sp, sp, #sizeof(jmp_buf) ; reserve space for env
    push r0         ; save x for later
    add r0, sp, #4  ; load address of env
    call setjmp
    pop r1          ; restore SP, move x to r1 <<== corrupt after jongjmp
    cmp r0, #0      ; if (setjmp(env) == 0)
    bne 1f
    lea r0, "%d\n"  ; printf("%d\n", 23)
    mov r1, #23
    call printf
    mov r0, sp  ; load address of env
    mov r1, #1
    call longjmp
    b 2f
1:
    lea r0, "%d\n"      ; printf("%\dn", x), x already in r1
    call printf
2:
    add sp, sizeof(jmp_buf)
    ret

这将按预期打印 23,但随后会打印 longjmp 调用的重新运行地址,即 1 标签的地址。

变量 x 仅临时存储在堆栈中以在 setjmp 函数调用中保留它(r0 是参数寄存器,由调用者保存)。我认为这对编译器来说是一件完全有效的事情。但是因为 setjmp returns 两次这破坏了变量,而 C 标准说它不应该。

Why is this compiler output wrong when setjmp is involved?

因为编译器不符合标准。因为生成的机器码执行时产生的副作用不符合C标准中对抽象机的规范。您的代码是一个简单的示例,因为 printf 会产生副作用。编译器(唯一)的工作通常是将句子从一种语言翻译成另一种语言,以便副作用保持不变。如果它做不到,那么它就坏了。

C11 7.13.2.1p3 说:

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.

x 具有自动存储持续时间并且是函数中的局部变量并且不是易失性的,但它在调用 setjmplongjmp 之间没有被修改。因此,如果 x 在调用 longjmp 之前有一些值,它在调用 longjmp.

之后必须具有相同的值

生成不符合本规范的汇编代码的编译器是not-conformingC编译器,或者更严厉地说,它根本不是C编译器。

有些编译器和架构不支持 setjmp/longjmp 调用,但是,没关系。此类编译器明确声明它们不支持 setjmp,例如它们可能缺少 setjmp.h header,因此无法使用它。

setjmp 是一个宏,而不是一个函数,这是标准的认可,即在某些实现中它可能需要普通函数不可用的特性。

对于可以使用标准调用语义通过函数实现的实现,该标准明确允许宏简单地扩展为同名函数。但是,如果应用程序试图绕过宏,无论是使用#undef 还是使用 (setjmp)(jmpbuf),它都会导致未定义的行为。这与普通的标准库函数相反,后者也可以作为宏和函数来实现,但可以使用上述技术访问以避免宏扩展。

此外,setjmp 被指定为宏这一事实意味着 &setbuf 也是未定义的行为。事实上,该标准只允许在两种情况下调用 setbuf

  1. 作为一个完整的表达式语句,可能会显式转换为 void

  2. if或循环语句的条件中,且仅当条件为

    • setjmp 调用自身

    • setjmp 调用作为参数的运算符 !

    • setjmp 调用与整数常量之间的比较。

换句话说,setjmp调用的值不能被保存或参与运算,调用上下文周围的序列点内不能进行其他计算。

因此,标准为实现 setjmp 提供了很大的自由度。