为什么 setjmp/longjmp 的这种用法是未定义的行为?



代码

#include <csetjmp>
template <typename Callable>
void create_checkpoint(std::jmp_buf buf, Callable&& callable)
{
if (setjmp(buf) != 0)
{
callable();
}
}
#include <iostream>
struct announcer {
int id;
announcer(int id):
id{id}
{
std::cout << "created announcer with id " << id << 'n';
}
~announcer() {
std::cout << "destructing announcer with id " << id << 'n'; 
}
};
void oopsie(std::jmp_buf buf, bool shouldJump)
{
if (shouldJump)
{
// std::cout << "performing jump...n";
std::longjmp(buf, 1);
}
}
void test1() 
{
std::jmp_buf buf;
announcer a1{1};
create_checkpoint(buf, []() {throw std::exception();});
oopsie(buf, true);
}
void test2()
{
std::jmp_buf buf;
announcer a1{1};
create_checkpoint(buf, []() {throw std::exception();});
oopsie(buf, false);

announcer a2{2};
create_checkpoint(buf, []() {throw std::exception();});
oopsie(buf, true);
}
int main()
{
try 
{
test1();
}
catch (...)
{}
try 
{
test2();
}
catch (...)
{}
}

上下文

我必须调用一些通过longjmp报告错误的 C 库。为了提供强大的异常保证,我想创建一个与std::lock_guard非常相似的函数,例如,我只是编写create_checkpoint(buf, handler)并继续调用 C 库函数,直到我分配更多资源(如果我理解正确,不会调用在setjmp行下创建的对象的析构函数(。

问题

为什么在这种情况下调用未定义的行为以及如何修复它?

我是如何发现这是未定义的行为的?

std::longjmp之前打印消息到std::cout与不打印会产生非常不同的结果,即使该行与控制流关系不大。

我现在明白了什么?

我知道std::longjmp本质上恢复寄存器并跳转到宏保存setjmp指令指针。此外,函数没有被优化掉,至少在编译过程中,有一个指令来调用longjmp

create_checkpoint转换为宏似乎可以解决问题。然而,我想知道有没有更好的方法来做到这一点?

来自 https://en.cppreference.com/w/cpp/utility/program/longjmp

如果调用 setjmp 的函数已退出,则行为未定义(换句话说,只允许在调用堆栈中向上跳远(

由于您不遵循此规则,因此您的程序具有未定义的行为

填充jmp_buff的代码必须知道在传递给longjmp后堆栈上应该在左侧的内容。 如果setjmp被处理为只能在返回int的函数中使用的编译器内部函数,编译器可以安排事情,以便longjmp会导致调用setjmp的函数"返回两次",而不是将setjmp本身视为这样做。 但是,在许多实现中,对setjmp的调用的处理方式就像对编译器的知识仅限于原型的任何其他函数调用一样。 在这样的实现中,如果没有有关调用函数堆栈帧的信息,setjmp将无法安排让longjmp返回到调用函数的调用方。 虽然处理setjmp调用的编译器将拥有所需的信息,但它没有理由将其提供给setjmp,如果没有这样的编译器支持,setjmp将无法获取信息。

顺便说一句,setjmp更令人烦恼的是,虽然它可以返回一个值,但setjmp调用必须出现在一组非常狭窄的上下文中,这些上下文都不能方便地捕获返回的值。 理论上可以说:

int setJmpValue;
switch(setjmp(...))
{
case 0: setJmpValue=0; break;
case 1: setJmpValue=1; break;
...
case INT_MAX-1: setJmpValue = INT_MAX-1; break;
case INT_MAX  : setJmpValue = INT_MAX  ; break;
}

但这会很烦人,无法导出到函数。

我认为允许i = setJmp(...);i是静态或自动持续时间的int应该没有任何困难,这反过来又会使返回值的任何任意使用成为可能,但标准中没有提供这样的结构,并且编译器不再流行可预测地处理有用的结构,除非标准迫使他们这样做。

最新更新