c语言 - 编译器对 setjmp/longjmp 的特殊处理



在为什么 volatile 适用于 setjmp/longjmp 中,用户 greggo 评论道:

实际上,现代C编译器确实需要知道setjmp是一个特殊的。 的情况下,因为一般来说,有优化,其中的变化 由 setjmp 引起的流量可能会严重损坏事物,这些需要 避免。回到K&R时代,setjmp不需要特殊处理,并且 没有得到任何东西,所以关于当地人的警告适用。从那以后 警告已经存在并且(应该!)理解 - 当然, setjmp 的使用非常罕见 - 现代编译器没有动力 要竭尽全力解决"clobber"问题 - 它会 仍然在语言中。

是否有任何参考资料对此进行了详细说明,如果这是真的,是否可以安全地存在(其行为不比标准setjmp/longjmp更容易出错)setjmp/longjmp的定制实现(例如,也许我想保存一些额外的(线程本地)上下文)被命名为不同的东西?比如有没有告诉编译器"这个函数实际上是setjmp/longjmp"?

C 语言将 setjmp 定义为一个宏,并对它可能出现的上下文施加了严格的限制,而无需调用未定义的行为。它不是一个普通的函数:你不能获取它的地址,并期望通过生成的指针进行的调用表现为正确的setjmp调用。

特别是,通常由 setjmp 调用的汇编代码遵循与普通函数相同的调用约定是不正确的。Linux 和 Solaris 上的 SPARC 提供了一个反例:它的 setjmp 不会恢复所有保留调用的寄存器(vfork 也不会)。最近在 2018 年,GCC 感到惊讶(gcc 补丁线程,bugzilla 条目)。

但是,即使考虑到 setjmp 入口点遵循通常约定的"编译器友好"平台,仍然有必要将其识别为"返回两次"的函数。GCC 通过名称识别类似 setjmp 的函数(包括 vfork),并提供在自定义代码中注释此类函数的__attribute__((returns_twice))

这样做的原因是longjmp'ing回到setjmp可以将控制权从某个变量或临时变量似乎死了(编译器将其存储重用于不相关的东西)转移到它所在的位置(但它的存储现在被"破坏"了,哎呀)。

构建一个示例来演示这种情况是如何发生的有点棘手:被破坏的存储不能是一个寄存器,因为如果它是调用破坏的,它就不会在setjmp点使用,如果它是调用保存的longjmp会恢复它(SPARC例外除外)。因此,需要强制堆叠,而不使两个变量的地址以使它们的生命周期重叠的方式公开,防止重用堆栈插槽,并且不要使其中一个变量在 longjmp 之前超出范围。

运气好的话,我设法到达了以下测试用例,当使用-O2 -mtune-ctrl=^inter_unit_moves_from_vec编译时(在编译器资源管理器上查看):

//__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;
}

生成以下程序集:

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

-mtune-ctrl=^inter_unit_moves_from_vec标志导致 GCC 通过堆栈实现 SSE 到 gpr 的移动,并且两个移动使用相同的堆栈槽,因为据编译器所知,没有冲突(计算 'tmp' 会导致 noreturn 函数,因此不再需要用于计算 'ret' 的临时)。但是,如果my_setjmp my_longjmp在分支到标签 .L2 我们尝试从覆盖的插槽中读取"ret"的值。

GCC确实对setjmp做了一些特殊的处理,用名称与sigsetjmpvforkgetcontextsavectx匹配它。 (剥离前导_后)。 在比赛中,它将内部标志设置为ECF_RETURNS_TWICE。 我认为这相当于一个隐式__attribute__((returns_twice))(您可以将其用于自己的函数)。 glibc 标头不使用它,它们只依赖于名称匹配。 (这个答案的早期版本被愚弄了,认为它们根本不是特例。

longjmp不需要太多的特殊处理;它看起来就像任何其他__attribute__((noreturn))函数调用一样。 Glibc以这种方式声明longjmp,这应该在调用它之前对局部变量产生副作用,例如,避免在诸如int foo(){ if(x) return y; longjmp(jmpbuf); }


setjmp/longjmp不能保证比优化器的任何不透明函数(不可内联)看起来更多。 (但一个关键的区别是,当setjmp再次返回时,当一个本地变量可以回到范围时,不要为单独的局部变量重用堆栈空间,请参阅 @amonakov 的答案。

对非volatile局部变量的副作用可能在编译时重新排序。setjmp(或longjmp)逃逸分析是否可以显示没有全局变量可以有其地址。

在调用setjmp期间,仍然允许优化将局部变量保留在寄存器而不是内存中。 这意味着在setjmp之后、longjmp之前对非volatile变量的副作用可能会也可能不会在longjmp将调用保留的寄存器恢复到jmp_buf中的保存状态时回滚。

setjmp(3)的 Linux 手册页列出了规则:

编译器可以将变量优化为寄存器,并longjmp()可以恢复除 堆栈指针和程序计数器。 因此,值 调用 if 后未指定自动变量longjmp()它们符合以下所有标准:

  • 它们是使相应函数的本地setjmp()电话;
  • 它们的值在调用setjmp()longjmp();和
  • 它们未声明为volatile.

摘自格利布/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__));

有一堆C预处理器的东西来定义一个_版本(无信号setjmp)等等。

顺便说一句,有一个__builtin_setjmp.但它的工作方式有些不同:GCC 手册建议不要在用户代码中使用它,并且 ISO C setjmp/longjump 库函数不能根据它来定义。

首先,为什么volatile在链接帖子中工作的正确答案是"因为 C 标准明确这么说"。 我不认为引用的部分是正确的,因为 C 明确列出了许多与setjmp/longjmp相关的定义不佳的行为。相关部分可以在C17 7.13.2.1中找到:

截至调用longjmp函数时,所有可访问对象都具有值,抽象计算机的所有其他组件都具有状态,但包含相应setjmp宏调用的函数的本地自动存储持续时间对象的值是不确定的,这些对象没有volatile限定类型,并且在setjmp调用和longjmp调用之间发生了更改。

甚至 C90 的说法也或多或少与上述相同。因此,编译器,无论是否现代,都不需要"修复此问题"的原因是因为C从未要求他们这样做。在发布引用注释的示例中,第二次执行if ( foo != 5 )时,foo的值是不确定的(并且foo永远不会占用其地址),因此严格来说,该行只是调用未定义的行为,编译器可以从那里随心所欲地做 - 这是应用程序程序员创建的错误, 不是优化器。

通常,任何使用setjmp.h的应用程序程序员都会得到他们即将得到的东西。这是最糟糕的意大利面条编程形式。

最新更新