Linux 编程之非局部跳转:longjmp siglongjmp
Posted 拭心
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Linux 编程之非局部跳转:longjmp siglongjmp相关的知识,希望对你有一定的参考价值。
在同一个函数中,我们可以使用 goto 修改程序的执行逻辑。如果在多个函数中,想要修改函数的执行顺序(从一个函数,返回到之前函数的某个预定义逻辑),怎么办呢?
答案是使用 longjmp 或 siglongjmp。
本文主要内容:
文章目录
longjmp 的作用及使用
https://man7.org/linux/man-pages/man3/setjmp.3.html
longjmp 需要和 setjmp 成对使用。
#include <setjmp.h>
typedef long jmp_buf[_JBLEN];
int setjmp(jmp_buf __env) __returns_twice;
__noreturn void longjmp(jmp_buf __env, int __value);
setjmp 的作用类似问道里的阴阳八卦令,保存当前“坐标”,稍后可以飞回这里。
- 即保存当前函数的一些运行环境,比如栈帧、指令指针、寄存器值、signal mask(信号阻塞用的掩码)等。
- 参数 jmp_buf 非常关键,后面 longjmp 究竟跳转到哪里,取决于参数里 jmp_buf 创建的点
- 正常调用会返回 0,longjmp 执行后,会返回 longjmp 传递的参数
写一个简单的例子:
static jmp_buf jmp_buffer;
static int global_value = -1;
void call_long_jmp()
LOG("call_long_jmp");
longjmp(jmp_buffer, 111);
void do_something()
LOG("do_something");
call_long_jmp();
/**
* setjmp 和 longjmp 可以修改执行顺序,跳转到 setjmp 的栈帧继续执行
*/
void test_set_jump()
int age = 2021 - 1993;
global_value = 1024;
int local_value = -1;
LOG("exec some logic, my age: %d ", age);
int ret = setjmp(jmp_buffer);
//注意这里打印的全局变量和局部变量的值
LOG("setjmp ret: %d, global_value: %d, local_value: %d", ret, global_value, local_value);
if (ret)
LOG("back to yesterday!");
return;
//这里初始化了局部变量
local_value = 512;
do_something();
核心函数是 test_set_jump,在这个函数中:
- 先执行了一些逻辑
- 然后调用 setjmp 函数,它会给参数 jmp_buffer 保存当前函数的“environment”,同时返回值 ret 是 0
- 由于 ret ==0 ,所以执行了 do_something,继而执行了 call_long_jmp,在这个函数里,我们调用了
longjmp(jmp_buffer, 111)
,参数是在 test_set_jump 里设置的,因此接下来会跳回到test_set_jump
函数的 setjmp 函数调用处,同时返回值 ret 变为 111 - 于是接下来执行了和之前截然不同的逻辑
运行结果:
E/zsx_linux: exec some logic, my age: 28
E/zsx_linux: setjmp ret: 0, global_value: 1024, local_value: -1
E/zsx_linux: do_something
E/zsx_linux: call_long_jmp
E/zsx_linux: setjmp ret: 111, global_value: 1024, local_value: 512 //第二次执行时,局部变量是之前初始化过的值,说明栈帧不是新创建的
E/zsx_linux: back to yesterday!
可以看到:在执行 longjmp 跳回 test_set_jump 后,的确是包含上一次函数执行时的所有栈帧数据,包括局部变量的值也存在。
需要注意的是:setjmp 的参数 jmp_buf 不是一直有效的,当调用 setjmp 的函数返回后,jmp_buf 就会无效。
siglongjmp 的作用及使用
https://man7.org/linux/man-pages/man3/sigsetjmp.3p.html
siglongjmp 需要和 sigsetjmp 成对使用。
#include <setjmp.h>
typedef long sigjmp_buf[_JBLEN + 1];
int sigsetjmp(sigjmp_buf __env, int __save_signal_mask);
__noreturn void siglongjmp(sigjmp_buf __env, int __value);
sigsetjmp siglongjmp 的作用和 setjmp longjmp 很类似,但从名字就可以看出来,它们还额外有一些针对信号的功能。
在《Unix 环境高级编程》中提到,之所以要有 sigsetjmp siglongjmp,是为了解决 setjmp longjmp 无法恢复信号阻塞的问题。
Under FreeBSD 5.2.1 and Mac OS X 10.3, setjmp and longjmp save and restore the signal mask. Linux 2.4.22 and Solaris 9, however, do not do this. FreeBSD and Mac OS X provide the functions _setjmp and _longjmp, which do not save and restore the signal mask.
To allow either form of behavior, POSIX.1 does not specify the effect of setjmp and longjmp on signal masks. Instead, two new functions, sigsetjmp and siglongjmp, are defined by POSIX.1. These two functions should always be used when branching from a signal handler.
什么意思呢?
在 Unix 系统中,一个信号触发执行信号处理函数时,这个信号默认会被阻塞,以防止在信号处理函数执行时,这个信号再次来到,导致信号处理被中断。
longjmp 提供了从信号处理返回代码逻辑的方式,对于这个被阻塞的信号,期望是可以在返回后恢复的。书上说在部分系统(Linux 2.4.22 and Solaris 9)上,longjmp 没有恢复。因此在 POSIX.1 中,sigsetjmp siglongjmp 诞生了。书中建议我们:在信号处理函数中进行非局部跳转时,一定要使用带 sig 的这俩。
如果 sigsetjmp 的第二个参数 __save_signal_mask 不为 0 ,说明要保存当前线程的信号掩码(即要阻塞的信号集),后面跳转回来时,也会恢复。
举个例子:
void _crash();
void do_something()
LOG("");
LOG("do_something crazy !!!");
_crash();
void my_siglongjmp_sigaction(int signo, struct siginfo* info, void* context)
pid_t tid = gettid();
LOG("my_jmp_sigaction called ! signo: %d, can_jump: %d, tid: %d", signo, can_jump, tid);
sigset_t old_set;
int ret = sigprocmask(SIG_BLOCK, nullptr, &old_set);
LOG("check process mask, SIGPROF ismember: %d", sigismember(&old_set, SIGPROF));
LOG("check process mask, SIGSEGV ismember: %d", sigismember(&old_set, SIGSEGV));
if (can_jump == 1)
LOG("my_jmp_sigaction [longjmp] called !");
siglongjmp(jmp_buffer, 123);
void _crash()
LOG(" ");
LOG("Oh... crash begin >>>");
// abort(); //SIGABRT 6
raise(SIGSEGV); //SIGSEGV 11
LOG("_crash end >>>");
/**
* 测试 sigsetjmp & siglongjmp
*/
void test_sig_set_jump()
LOG(" ");
LOG("test_sig_set_jump >>>");
pid_t tid = gettid();
sigaction_test(my_siglongjmp_sigaction);
int age = 2021 - 1993;
global_value = 1024;
int local_value = -1;
LOG("\\n\\n exec some logic, my age: %d , tid: %d ", age, tid);
int ret = sigsetjmp(jmp_buffer, 1);
LOG(" ");
LOG("setjmp ret: %d, global_value: %d, local_value: %d", ret, global_value, local_value);
if (ret)
//说明是从异常处理调回来的,兜底逻辑
can_jump = false;
LOG(" ");
LOG("back to business logic! check sigprocmask");
sigset_t old_set;
ret = sigprocmask(SIG_BLOCK, nullptr, &old_set);
LOG("check process mask, SIGPROF ismember: %d", sigismember(&old_set, SIGPROF));
LOG("check process mask, SIGSEGV ismember: %d", sigismember(&old_set, SIGSEGV));
sigset_t pending_set;
if (sigpending(&pending_set))
LOG("sigpending failed");
LOG("sigpending ret: %d, pending_set: %ld", ret, sizeof(pending_set.sig) / sizeof(long));
LOG("continue logic");
return;
//有风险的逻辑
can_jump = true;
local_value = 512;
do_something();
运行结果:
两者的区别
- 普通的业务逻辑跳转,两者没有区别。
- 在信号处理函数中恢复之前的逻辑,应该使用 sigsetjmp 和 siglongjmp。
我们可以通过这种方式兜底:在容易崩溃的逻辑前保存环境,然后在信号处理函数中恢复执行另外的逻辑。
需要注意的是:如果在异常后继续执行发生异常的逻辑,可能会导致意想不到的后果,最好不要这么操作!
android 中没有区别
上面提到的区别,是在部分 Linux 版本中。经过测试和查看源码,确认:在 Android 系统中,两者没有区别!
http://aospxref.com/android-12.0.0_r3/xref/bionic/libc/arch-arm64/bionic/setjmp.S
可以看到,在 bionic 实现中,setjmp 也是调用的 sigsetjmp,传的第二个参数是 1。
longjmp 看起来也是 siglongjmp 的一个别名,看下这个 ALIAS_SYMBOL 宏展开是什么:
.globl
和 .equ
指令是什么意思?
从 https://ftp.gnu.org/old-gnu/Manuals/gas-2.9.1/html_chapter/as_7.html#SEC93 看到:
- .globl alias: 让 alias 符号对链接器可见,这样其他依赖的动态库也能访问
- .equ alias, original: (和
.set alias, original
一样)设置 alias 符号的值为 original
因此可以看到,在 Android 中,longjmp 其实和 siglongjmp 没有区别。
以上是关于Linux 编程之非局部跳转:longjmp siglongjmp的主要内容,如果未能解决你的问题,请参考以下文章