C 语言 setjump() 和 longjump()

2025年1月7日 | 阅读 6 分钟

setjmp()longjmp() 函数用于在 C 程序中执行非局部跳转。它允许您从程序中的任何位置跳转回先前设置的跳转点。

setjmp() 函数将当前执行环境或上下文保存到 jmp_buf 变量中。它存储堆栈指针、寄存器等信息。首次调用时返回 0。

longjmp() 函数恢复由先前 setjmp() 调用保存的执行上下文。之后,它返回,因此执行将从 setjmp() 调用处恢复。然而,这一次,setjmp() 将返回一个非零值,表示 longjmp() 被调用,而不是直接调用 setjmp() 返回。

它允许您跳转到调用堆栈中先前保存的点。这对于错误处理场景非常有用,当您想从深度嵌套的函数调用中干净地回退时。

例如,setjmp() 可以在程序的顶层保存环境。然后,在某个嵌套函数深处,如果发生错误,longjmp() 可以跳回到调用 setjmp() 的顶层,而无需从每个嵌套函数层返回。

基本语法

以下是 setjmp() 和 longjmp() 的基本语法

setjmp()

  • 'env': 它是指向 jmp_buf 的指针,用于保存环境上下文。
  • 返回值: 首次调用时返回 0,由 longjmp() 返回时返回非零值。

longjmp()

  • 'env': 由先前 setjmp() 调用保存的 jmp_buf
  • 'val': 从 setjmp() 返回的非零值。
  • 无返回值 (从不返回)

setjmp() 的工作原理

以下是 setjmp() 在 C 程序中设置跳转点的工作原理说明

设置跳转点

setjmp() 函数将当前执行状态/上下文保存到 'jmp_buf env' 参数指定的环境缓冲区中。此保存的状态包括:

  • 堆栈指针
  • 寄存器值
  • 程序计数器 - 返回点

它保存了 'jmp_buf env',代表一个可以稍后使用 'longjmp()' 返回的跳转点或检查点。

首次调用 'setjmp()'

当首次调用 'setjmp()' 时,它会保存上述环境并返回 0。之后,执行将从 'setjmp()' 调用后正常继续。因此,第一次调用只是设置了稍后可以跳转回的检查点。

后续调用

如果稍后调用 'longjmp()' 进行跳转,当环境恢复时,'setjmp()' 将被调用第二次。之后,'setjmp()' 将返回一个非零值。这个非零返回值是作为 'val' 参数传递给 'longjmp()' 的。

总而言之,第一次调用保存环境并返回 0。由 'longjmp()' 引起的后续调用返回 'longjmp(env, val)' 中的非零值,以指示发生了跳转。代码可以通过检查 'setjmp()' 返回 0 与否来判断是首次调用还是跳转返回。

setjmp()/longjmp() 实现非局部跳转,允许通过跳转到已保存点来处理错误/恢复。

longjmp() 的用法

以下是 longjmp() 在 C 程序中执行非局部跳转的用法说明

执行非局部跳转

'longjmp()' 函数恢复之前通过调用 'setjmp()' 保存的执行上下文。它有效地将执行跳转到 'setjmp()' 被调用的点。

然而,与第一次返回 0 不同,在 'longjmp()' 跳转回来后,'setjmp()' 将返回一个非零值,从而使程序能够检测到发生了跳转。

longjmp() 参数

'longjmp()' 接受两个参数

  1. 'jmp_buf env' 是由先前 'setjmp()' 调用保存的环境缓冲区。它包含要跳转回的上下文。
  2. 'int val': 这个 int 值将在 longjmp() 跳转回来后从 'setjmp()' 返回。通常,使用非零错误代码来指示错误。

这种非局部跳转能力为复杂情况下的错误处理和恢复提供了一种非常灵活的方式。

使用 setjmp() 和 longjmp() 进行错误处理

setjmp()/longjmp() 的一个常见用例是处理深度嵌套函数调用中的错误。

例如

它允许 nestFunc1() 在发生问题时通过非局部跳转立即返回到错误处理块。

最佳实践

  • 检查 setjmp() 的 错误返回值 以区分首次调用和 longjmp() 跳转返回。
  • 在 longjmp() 之前释放已分配的内存,以避免内存泄漏。
  • 声明 env 变量为 volatile,因为否则 longjmp() 调用可能导致意外行为。
  • 不要从信号处理程序中调用 longjmp()。

陷阱

  • 由于突然跳转,内存分配等资源可能无法正确释放。
  • 堆栈展开不正确,有时会导致意外行为。

总而言之,setjmp()/longjmp() 提供了一种非局部返回的方法,但必须小心处理,以避免在处理错误时产生副作用或泄漏。

范围和限制

以下是关于正确使用 setjmp() 和 longjmp() 的范围和限制的一些关键点

恰当的用途

  • 在复杂、深度嵌套的函数流中进行错误处理和恢复。
  • 在灾难性错误后恢复环境。
  • 程序控制流需要非局部跳转。
  • 在执行上下文之间切换。

不推荐的用途

  • 作为通用的程序流程控制机制 (谨慎使用)。
  • Setjmp/longjmp 会阻止某些编译器优化。
  • 代替 return 或 break 退出循环。
  • 在信号处理程序或多线程代码中使用。
  • 在无法维持内存分配/释放纪律的情况下。

局限性

  • 跨调用帧的异常流通常需要语言支持。
  • 堆栈展开不会自动发生。
  • 资源/内存泄漏更有可能发生。
  • 跨非局部跳转的调试可能会更困难。
  • 在某些情况下行为依赖于实现。

示例和代码片段

以下是一些实际示例和代码片段,说明了 setjmp() 和 longjmp() 在不同上下文中的用法

示例:1

输出

Start of main()
Executing foo()
Error handled in foo()
End of main()

示例 2:跨函数的非局部跳转

输出

Start of main()
Inside foo()
Inside bar()
Jumped back to main() from longjmp()
End of main()

示例 3:资源清理

输出

Start of main()
Performing a risky operation...
Performing cleanup...

这些示例展示了 setjmp() 和 longjmp() 在不同上下文中的错误处理、非局部跳转和资源清理。它们提供了对这些函数如何在实际场景中应用的实际理解。

安全担忧

  • 如果未能在跳转前清除,内存中的敏感数据可能会暴露。
  • 通过任意跳转可以违反控制流的假设。
  • 堆栈保护标志可能被绕过,导致缓冲区溢出。
  • 跳转后文件描述符或资源可能被泄漏。
  • 跳转缓冲区成为黑客攻击的目标。

推荐

  • 在跳转前将内存区域中的敏感数据清零。
  • 跳转后检查状态和约束以确保有效性。
  • 将 setjmp/longjmp 代码包装在具有有限权限的安全上下文中。
  • 使用堆栈 cookie 和保护标志来防止一些缓冲区问题。
  • 仅跳转到经过验证的目的地,不受用户影响。
  • 在跳转前通过析构函数正确释放资源。
  • 在高安全性要求的代码中禁用 setjmp/longjmp 的使用。