【问题标题】:Why is this usage of setjmp/longjmp undefined behavior?为什么 setjmp/longjmp 的这种用法是未定义的行为?
【发布时间】:2020-01-11 14:59:56
【问题描述】:

代码

#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 行下创建的对象的析构函数)。

问题

为什么在这种情况下会调用未定义的行为,我该如何解决?

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

Printing message to std::cout 之前的std::longjmpnot printing 产生非常不同的结果,即使该行与控制流几乎没有关系。

我现在明白了什么?

我知道std::longjmp本质上是恢复寄存器并跳转到setjmp宏保存的指令指针。另外,functions are not optimized away,至少在编译期间,有一条指令可以调用longjmp

create_checkpoint 转换为宏seems to solve the issue。但我想知道有没有更好的方法来做到这一点?

【问题讨论】:

  • 不鼓励在现代 C++ 中使用任何 setjmp/longjmp。事实上,我认为任何使用都表明所讨论的代码应该永远合并到主代码库中。
  • longjmp 绕过 C++ 堆栈展开,因此不调用析构函数。您可以通过不使用longjmp 来修复它,或者仔细捕捉每个靠近调用它的代码部分并将其转换为异常或适当地处理它。另请注意,setjmp/longjmp 不会在 setjmp 位置恢复非易失性变量,因此它们不值得在处理程序中正确考虑。
  • 我应该提一下,我维护了一个非常大的程序,它广泛使用了setjmp/longjmp。我强烈反对在 C++ 程序中使用它们,尽我所能。它们相当于goto 的核。
  • @Eljay,不幸的是,在其中一种情况下,这似乎是不可避免的(libjpeg)。我们不知道整个调用树是否被正确覆盖,所以我认为有一个保护函数会很好。宏似乎可以解决问题,但我不确定它是否是正确的前进方向。
  • 我认为这是个好问题。这是 C++ 继承自 C 的东西,但如果在 C++ 中使用会引起很多麻烦。

标签: c++ undefined-behavior setjmp


【解决方案1】:

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

如果调用 setjmp 的函数已经退出,则行为未定义(换句话说,只允许长跳转调用堆栈)

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

【讨论】:

  • 我明白了。所以只能创建宏,不能创建函数?是否有可能以某种方式内联该函数?另外,我应该删除问题吗?现在好像没什么用了。
  • 是的,宏可能可以完成这项工作。不需要删除问题,以后可能会有人有类似的问题
【解决方案2】:

填充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,这反过来又可以任意使用返回值,但是标准中没有提供这样的构造,并且编译器不再流行以可预测的方式处理有用的构造,除非标准强制他们这样做。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2011-12-21
    • 2021-10-30
    • 2019-12-26
    • 2011-05-05
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多