在我们linux软件开发中似乎有一个不成文的规定:禁止使用goto
跳转语句。并对它列举了几大”罪“。
goto
语句也仅仅是局部内的跳转,而setjmp/longjmp
是非局部跳转。理应其负面影响是有过之而无不及。因此,我之前对它的态度是嗤之以鼻。但是随着最近几次工作中接触,决定还是深入了解一下。
valgrind
在android平台交叉编译异常,会提示以下错误。其实际与setjmp/longjmp
有关。m_gdbserver/m_gdbserver.c:772:10: error: __builtin_longjmp is not supported for the current target
VG_MINIMAL_LONGJMP(tst->sched_jmpbuf);
^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
../include/pub_tool_libcsetjmp.h:142:35: note: expanded from macro 'VG_MINIMAL_LONGJMP'
#define VG_MINIMAL_LONGJMP(_env) __builtin_longjmp((_env),1)
setjmp/longjmp
。非局部跳转时实现逻辑如下:
setjmp
保存当前的执行环境到jmp_buf
,然后默认返回0。longjmp
,传入上面保存的jmp_buf
,以及返回值。setjmp
的返回处,且返回值变为了longjmp
设置的值。示例:
//main.c
#include
#include
#include
jmp_buf env;
int main(int argc, char const *argv[]) {
int res = setjmp(env);
if (res == 0) {
printf("return from setjmp\n");
longjmp(env, 1);
} else {
printf("return from longjmp: %d\n", res);
}
return 0;
}
编译&运行:
yihua@ubuntu:~/test/setjmp$ gcc main.c -o main
yihua@ubuntu:~/test/setjmp$ ./main
return from setjmp
return from longjmp: 1
yihua@ubuntu:~/test/setjmp$
如上示例,setjmp/longjmp
实际上用起来并不困难,我们进一步了解其原理。
由案例可知,我们需要了解三方面:
jmp_buf
内部结构,它是如何达到保存当前执行环境的目的。setjmp
原理。保存当前执行环境的哪些值。longjmp
原理。如何实现环境的恢复。以下分析,以x86-64环境为例。
jmp_buf 类型:
通过查看/usr/include/setjmp.h
和/usr/include/x86_64-linux-gnu/bits/setjmp.h
,得知jmp_buf
定义如下:
# if __WORDSIZE == 64
typedef long int __jmp_buf[8];
# elif defined __x86_64__
__extension__ typedef long long int __jmp_buf[8];
# else
typedef int __jmp_buf[6];
# endif
struct __jmp_buf_tag
{
/* NOTE: The machine-dependent definitions of `__sigsetjmp'
assume that a `jmp_buf' begins with a `__jmp_buf' and that
`__mask_was_saved' follows it. Do not move these members
or add others before it. */
__jmp_buf __jmpbuf; /* Calling environment. */
int __mask_was_saved; /* Saved the signal mask? */
__sigset_t __saved_mask; /* Saved signal mask. */
};
typedef struct __jmp_buf_tag jmp_buf[1];
分析:
一、typedef struct __jmp_buf_tag jmp_buf[1];
为什么定义为数组?
这其实就是利用了一个C语言中的数组特性。如示例中,全局变量jmp_buf env;
,因为jmp_buf
是一个数组,那么变量env
就是一个数组,且长度为1。通过调用setjmp
设置环境变量时,变量名的传入就会退化成指针传入,setjmp
内部就可以达到修改env
值的目的。否则应该传入&env
。
二、__jmp_buf_tag
分析
由定义可知,__jmp_buf_tag
中有三个变量成员;
__jmpbuf
用于保存寄存器值。用于后续上下文恢复。__mask_was_saved
和__saved_mask
。在多线程环境中,可以帮助恢复调用时的工作信号屏蔽字。本文不再详谈。而__jmpbuf
在x86-64环境下,被定义为long long int
的长度为8的数组。其中long long int
一般表示为64bit。即__jmpbuf
保存8个寄存器的值,用于恢复longjmp
的恢复。
三、setjmp
实现的过程
我们可以修改示例的编译方式,再反汇编,查看setjmp
的实现过程。
yihua@ubuntu:~/test/setjmp$ gcc main.c -o main -static -lc
yihua@ubuntu:~/test/setjmp$ objdump -d main > asm
yihua@ubuntu:~/test/setjmp$
注:该方式会导致main
体积较大
分析asm中相关内容,我们只关注main、setjmp、longjmp等接口,如下:
...
0000000000400b6d <main>:
400b6d: 55 push %rbp
400b6e: 48 89 e5 mov %rsp,%rbp
400b71: 48 83 ec 20 sub $0x20,%rsp
400b75: 89 7d ec mov %edi,-0x14(%rbp)
400b78: 48 89 75 e0 mov %rsi,-0x20(%rbp)
400b7c: 48 8d 3d 1d b8 2b 00 lea 0x2bb81d(%rip),%rdi # 6bc3a0 <env>
400b83: e8 58 ce 00 00 callq 40d9e0 <_setjmp>
400b88: 89 45 fc mov %eax,-0x4(%rbp)
400b8b: 83 7d fc 00 cmpl $0x0,-0x4(%rbp)
400b8f: 75 1d jne 400bae <main+0x41>
400b91: 48 8d 3d 0c 17 09 00 lea 0x9170c(%rip),%rdi # 4922a4 <_IO_stdin_used+0x4>
400b98: e8 83 f7 00 00 callq 410320 <_IO_puts>
400b9d: be 01 00 00 00 mov $0x1,%esi
400ba2: 48 8d 3d f7 b7 2b 00 lea 0x2bb7f7(%rip),%rdi # 6bc3a0 <env>
400ba9: e8 42 ce 00 00 callq 40d9f0 <__libc_longjmp>
400bae: 8b 45 fc mov -0x4(%rbp),%eax
400bb1: 89 c6 mov %eax,%esi
400bb3: 48 8d 3d fd 16 09 00 lea 0x916fd(%rip),%rdi # 4922b7 <_IO_stdin_used+0x17>
400bba: b8 00 00 00 00 mov $0x0,%eax
400bbf: e8 dc ea 00 00 callq 40f6a0 <_IO_printf>
400bc4: b8 00 00 00 00 mov $0x0,%eax
400bc9: c9 leaveq
400bca: c3 retq
400bcb: 0f 1f 44 00 00 nopl 0x0(%rax,%rax,1)
...
000000000040d9e0 <_setjmp>:
40d9e0: 31 f6 xor %esi,%esi
40d9e2: e9 39 cb 04 00 jmpq 45a520 <__sigsetjmp>
40d9e7: 66 0f 1f 84 00 00 00 nopw 0x0(%rax,%rax,1)
40d9ee: 00 00
...
000000000045a520 <__sigsetjmp>:
45a520: 48 89 1f mov %rbx,(%rdi)
45a523: 48 89 e8 mov %rbp,%rax
45a526: 64 48 33 04 25 30 00 xor %fs:0x30,%rax
45a52d: 00 00
45a52f: 48 c1 c0 11 rol $0x11,%rax
45a533: 48 89 47 08 mov %rax,0x8(%rdi)
45a537: 4c 89 67 10 mov %r12,0x10(%rdi)
45a53b: 4c 89 6f 18 mov %r13,0x18(%rdi)
45a53f: 4c 89 77 20 mov %r14,0x20(%rdi)
45a543: 4c 89 7f 28 mov %r15,0x28(%rdi)
45a547: 48 8d 54 24 08 lea 0x8(%rsp),%rdx
45a54c: 64 48 33 14 25 30 00 xor %fs:0x30,%rdx
45a553: 00 00
45a555: 48 c1 c2 11 rol $0x11,%rdx
45a559: 48 89 57 30 mov %rdx,0x30(%rdi)
45a55d: 48 8b 04 24 mov (%rsp),%rax
45a561: 90 nop
45a562: 64 48 33 04 25 30 00 xor %fs:0x30,%rax
45a569: 00 00
45a56b: 48 c1 c0 11 rol $0x11,%rax
45a56f: 48 89 47 38 mov %rax,0x38(%rdi)
45a573: e9 08 00 00 00 jmpq 45a580 <__sigjmp_save>
45a578: 0f 1f 84 00 00 00 00 nopl 0x0(%rax,%rax,1)
45a57f: 00
...
分析过程:
lea 0x2bb81d(%rip),%rdi
,将偏移地址0x2bb81d的变量保存在寄存器%rdi
中。很明显这里指的是我们的全局变量env
,400b83+2bb81d=6bc3a0
。callq 40d9e0 <_setjmp>
,进入_setjmp
接口。其中xor %esi,%esi
清空寄存器%esi
。jmpq 45a520 <__sigsetjmp>
,进入__sigsetjmp
接口。
mov %rbx,(%rdi)
,此时,我们知道%rdi
保存的是我们的全局变量env
,即将寄存器%rbx
保存至第一个元素。%rbp
保存至第二个元素。mov %rbp,%rax
xor %fs:0x30,%rax
rol $0x11,%rax
mov %rax,0x8(%rdi)
%r12,0x10(%rdi)
,将寄存器%r12
保存至第三个元素。%r13,0x18(%rdi)
,将寄存器%r13
保存至第四个元素。%r14,0x20(%rdi)
,将寄存器%r14
保存至第五个元素。%r15,0x28(%rdi)
,将寄存器%r15
保存至第六个元素。%rsp+8
保存至第七个元素。%rsp
保存至第七个元素。__sigjmp_save
用于保存相关内容,在此不再赘述。此时,全局变量env
中保存了以下寄存器内容。
[%rbx,%rbp,%r12,%r13,%r14,%r15,%rsp+8,%rsp]
通过【程序员的自我修养11】栈与函数调用过程章节可知,函数的调用过程,其栈变化大致如下:
因此,理论上保存栈顶寄存器rsp
和帧指针rbp
即可。为什么还要保存其它寄存器呢?
r12、r13、r14、r15
:这些寄存器被用作临时工作寄存器,它们的值可能会在函数执行期间发生变化。了解setjmp
的原理后,那么longjmp
实质上就是恢复的过程,有兴趣的可以自行分析:
000000000040da30 <__longjmp>:
40da30: 4c 8b 47 30 mov 0x30(%rdi),%r8
40da34: 4c 8b 4f 08 mov 0x8(%rdi),%r9
40da38: 48 8b 57 38 mov 0x38(%rdi),%rdx
40da3c: 49 c1 c8 11 ror $0x11,%r8
40da40: 64 4c 33 04 25 30 00 xor %fs:0x30,%r8
40da47: 00 00
40da49: 49 c1 c9 11 ror $0x11,%r9
40da4d: 64 4c 33 0c 25 30 00 xor %fs:0x30,%r9
40da54: 00 00
40da56: 48 c1 ca 11 ror $0x11,%rdx
40da5a: 64 48 33 14 25 30 00 xor %fs:0x30,%rdx
40da61: 00 00
40da63: 90 nop
40da64: 48 8b 1f mov (%rdi),%rbx
40da67: 4c 8b 67 10 mov 0x10(%rdi),%r12
40da6b: 4c 8b 6f 18 mov 0x18(%rdi),%r13
40da6f: 4c 8b 77 20 mov 0x20(%rdi),%r14
40da73: 4c 8b 7f 28 mov 0x28(%rdi),%r15
40da77: 89 f0 mov %esi,%eax
40da79: 4c 89 c4 mov %r8,%rsp
40da7c: 4c 89 cd mov %r9,%rbp
40da7f: 90 nop
40da80: ff e2 jmpq *%rdx
40da82: 66 2e 0f 1f 84 00 00 nopw %cs:0x0(%rax,%rax,1)
40da89: 00 00 00
40da8c: 0f 1f 40 00 nopl 0x0(%rax)
一、setjmp 宏与 longjmp 函数组合使用时,它们必须有严格的先后执行顺序。
即必须先调用 setjmp 来初始化 jmp_buf 结构体变量 env 之后,才能够调用 longjmp 函数来恢复到先前被保存的堆栈环境(即程序执行点)。如果在 setjmp 调用之前执行 longjmp 函数,那么将导致程序的执行流变得不可预测,很容易导致程序崩溃而退出。
这是理所当然的,若没有进行setjmp
初始化jmp_buf
,那么其中的数组大概率是非法地址,恢复现场时,也会达到一个随机的地址。触发段错误。
二、longjmp函数必须在setjmp的作用域之内。
在一个函数中使用 setjmp 来初始化一个全局变量(jmp_buf buf)buf 之后,只要这个函数没有被返回,那么在其他任何地方都可以通过 longjmp 调用来跳转到 setjmp 的下一条语句执行。也就是说,setjmp 将发生调用处的局部堆栈环境保存在一个 jmp_buf 结构体变量 env 中,只要主调函数中对应的内存未曾释放,在调用 longjmp 的时候就可以根据已保存的 jmp_buf 参数恢复到 setjmp 的地方执行。
可以简单理解为,若setjmp所在的函数栈被回收后,再进入该堆栈,可能会出现异常。
本文向大家介绍了非局部跳转函数setjmp
和longjmp
的使用方式,以及实现原理。即使它的存在,有很多不好的影响。但是在特定场景下还是能够体现它的价值。比如异常处理,实现C语言的异常捕获和协程。但是还是希望大家谨慎使用,按需设计。
若我的内容对您有所帮助,还请关注我的公众号。不定期分享干活,剖析案例,也可以一起讨论分享。
我的宗旨:
踩完您工作中的所有坑并分享给您,让你的工作无bug,人生尽是坦途