一文搞懂系列——非局部跳转setjmp和longjmp使用及原理

一文搞懂系列——非局部跳转setjmp和longjmp使用及原理_第1张图片

背景介绍

在我们linux软件开发中似乎有一个不成文的规定:禁止使用goto跳转语句。并对它列举了几大”罪“。

  • 影响程序的可读性和可维护性。goto语句可以跳转到程序中的任意位置,这可能导致程序流程的混乱,使得其他程序员难以理解和维护。它被比喻为“程序中的泥潭”,一旦陷入,难以前行。
  • 增加调试难度:由于goto语句可以跳转至程序的任何点,这使得调试程序变得更加困难。程序员需要检查整个程序来确定goto可能到达的所有位置,增加了 debugging的复杂度。
  • 违反结构化编程原则:结构化编程提倡使用顺序、分支和循环结构来编写程序。这些结构清晰,易于理解和维护。goto语句打破了这种结构,使程序变得难以控制。
  • 与现代编程语言的设计趋势不符:随着编程语言的发展,现代语言如Python、Java、C#等都不再支持goto语句,这是为了鼓励程序员采用更结构化、更易于理解的编程方式。
  • 造成资源浪费在某些需要资源释放的场合,滥用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

使用

非局部跳转时实现逻辑如下:

  1. 使用setjmp保存当前的执行环境到jmp_buf,然后默认返回0。
  2. 程序执行,到某个地方调用longjmp,传入上面保存的jmp_buf,以及返回值。
  3. 此时执行点又回到调用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实际上用起来并不困难,我们进一步了解其原理。

原理

由案例可知,我们需要了解三方面:

  1. jmp_buf内部结构,它是如何达到保存当前执行环境的目的。
  2. setjmp原理。保存当前执行环境的哪些值。
  3. 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 
...

分析过程:

  1. lea 0x2bb81d(%rip),%rdi,将偏移地址0x2bb81d的变量保存在寄存器%rdi中。很明显这里指的是我们的全局变量env400b83+2bb81d=6bc3a0

一文搞懂系列——非局部跳转setjmp和longjmp使用及原理_第2张图片

  1. callq 40d9e0 <_setjmp>,进入_setjmp接口。其中xor %esi,%esi清空寄存器%esi
  2. jmpq 45a520 <__sigsetjmp>,进入__sigsetjmp接口。
    1. mov %rbx,(%rdi),此时,我们知道%rdi保存的是我们的全局变量env,即将寄存器%rbx保存至第一个元素。
    2. 将寄存器%rbp保存至第二个元素。
    mov    %rbp,%rax
    xor    %fs:0x30,%rax
    rol    $0x11,%rax
    mov    %rax,0x8(%rdi)
    
    1. %r12,0x10(%rdi),将寄存器%r12保存至第三个元素。
    2. %r13,0x18(%rdi),将寄存器%r13保存至第四个元素。
    3. %r14,0x20(%rdi),将寄存器%r14保存至第五个元素。
    4. %r15,0x28(%rdi),将寄存器%r15保存至第六个元素。
    5. 将寄存器%rsp+8保存至第七个元素。
    6. 将寄存器%rsp保存至第七个元素。
  3. 其中__sigjmp_save用于保存相关内容,在此不再赘述。

此时,全局变量env中保存了以下寄存器内容。

[%rbx,%rbp,%r12,%r13,%r14,%r15,%rsp+8,%rsp]

通过【程序员的自我修养11】栈与函数调用过程章节可知,函数的调用过程,其栈变化大致如下:

一文搞懂系列——非局部跳转setjmp和longjmp使用及原理_第3张图片

因此,理论上保存栈顶寄存器rsp和帧指针rbp即可。为什么还要保存其它寄存器呢?

  • rbx:由于它经常用作函数的参数和返回值,因此保存它的值可以确保函数调用的上下文信息被保留.
  • 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所在的函数栈被回收后,再进入该堆栈,可能会出现异常。

总结

本文向大家介绍了非局部跳转函数setjmplongjmp的使用方式,以及实现原理。即使它的存在,有很多不好的影响。但是在特定场景下还是能够体现它的价值。比如异常处理,实现C语言的异常捕获和协程。但是还是希望大家谨慎使用,按需设计。

若我的内容对您有所帮助,还请关注我的公众号。不定期分享干活,剖析案例,也可以一起讨论分享。
我的宗旨:
踩完您工作中的所有坑并分享给您,让你的工作无bug,人生尽是坦途

一文搞懂系列——非局部跳转setjmp和longjmp使用及原理_第4张图片

一文搞懂系列——非局部跳转setjmp和longjmp使用及原理_第5张图片

你可能感兴趣的:(一文搞懂系列,linux,网络,运维,setjmp,longjmp)