为什么使用 setjmp/longjmp 未定义行为?
Why is this usage of setjmp/longjmp undefined behavior?
代码
#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
before std::longjmp
vs not printing 产生非常不同的结果,即使该行与控制流关系不大。
我现在明白了什么?
我理解std::longjmp
本质上是恢复寄存器并跳转到setjmp
宏保存的指令指针处。还有,functions are not optimized away,至少在编译的时候,有调用longjmp
的指令。
正在将 create_checkpoint
转换为宏 seems to solve the issue。但是我想知道有没有更好的方法来做到这一点?
来自https://en.cppreference.com/w/cpp/utility/program/longjmp
If the function that called setjmp has exited, the behavior is undefined (in other words, only long jumps up the call stack are allowed)
由于您没有遵守这条规则,您的程序有未定义的行为
填充 jmp_buff
的代码必须知道在传递给 longjmp
后堆栈的左侧应该有什么。如果 setjmp
被处理为编译器内部函数,它只能在 return int
的函数中使用,编译器可以安排一些事情,以便 longjmp
会导致调用 setjmp
到 "return twice",而不是将 setjmp
本身视为这样做。然而,在许多实现中,对 setjmp
的调用就像对函数的任何其他调用一样处理,编译器的知识仅限于原型。在这样的实现中,setjmp
无法安排 longjmp
return 到调用函数的调用者,而没有关于该函数的堆栈帧的信息。虽然处理 setjmp
调用的编译器会有所需的信息,但没有理由将其提供给 setjmp
,并且如果没有这样的编译器,setjmp
将无法获取信息支持。
顺便说一句,setjmp
更令人烦恼的是,虽然它有可能 return 一个值,但 setjmp
调用必须出现在非常狭窄的一组上下文中,none其中可以方便的捕获值returned。理论上可以说:
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
,这反过来又使任何成为可能任意使用 returned 值,但标准中未提供此类构造,编译器以可预测的方式处理有用的构造已不再流行,除非标准强制它们这样做。
代码
#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
before std::longjmp
vs not printing 产生非常不同的结果,即使该行与控制流关系不大。
我现在明白了什么?
我理解std::longjmp
本质上是恢复寄存器并跳转到setjmp
宏保存的指令指针处。还有,functions are not optimized away,至少在编译的时候,有调用longjmp
的指令。
正在将 create_checkpoint
转换为宏 seems to solve the issue。但是我想知道有没有更好的方法来做到这一点?
来自https://en.cppreference.com/w/cpp/utility/program/longjmp
If the function that called setjmp has exited, the behavior is undefined (in other words, only long jumps up the call stack are allowed)
由于您没有遵守这条规则,您的程序有未定义的行为
填充 jmp_buff
的代码必须知道在传递给 longjmp
后堆栈的左侧应该有什么。如果 setjmp
被处理为编译器内部函数,它只能在 return int
的函数中使用,编译器可以安排一些事情,以便 longjmp
会导致调用 setjmp
到 "return twice",而不是将 setjmp
本身视为这样做。然而,在许多实现中,对 setjmp
的调用就像对函数的任何其他调用一样处理,编译器的知识仅限于原型。在这样的实现中,setjmp
无法安排 longjmp
return 到调用函数的调用者,而没有关于该函数的堆栈帧的信息。虽然处理 setjmp
调用的编译器会有所需的信息,但没有理由将其提供给 setjmp
,并且如果没有这样的编译器,setjmp
将无法获取信息支持。
顺便说一句,setjmp
更令人烦恼的是,虽然它有可能 return 一个值,但 setjmp
调用必须出现在非常狭窄的一组上下文中,none其中可以方便的捕获值returned。理论上可以说:
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
,这反过来又使任何成为可能任意使用 returned 值,但标准中未提供此类构造,编译器以可预测的方式处理有用的构造已不再流行,除非标准强制它们这样做。