setjmp/longjmp in C

Table of Contents

Example Analysis

下面代码是setjmp以及longjmp的使用实例,以及其反汇编出来的结果,具体api的使用细节请参考man手册

#include <stdio.h>
#include <setjmp.h>

static jmp_buf env;

double divide(double a,double b)
{
    const double delta = 0.00000000001;
    if(!((-delta < b) && (b < delta)))
    {
        return  a/b ;
    }
    else
    {
        longjmp(env, 1);
        return 0;
    }
}

int main( )
{
    int ret;

    ret = setjmp(env);

    if (!ret) {
        printf("5/0=%lf\n", divide(5,0));
    } else  if (ret == 1) {
        printf("ERR\n");
    }
    return 0;
}

对应的汇编代码

Dump of assembler code for function divide:
/var/folders/jh/0ybww9_12tv8l_jbrc8kwhxw0000gn/T/ox-bash-wtGI3WvdO/test.c:
7   {
8       const double delta = 0.00000000001;
9       if(!((-delta < b) && (b < delta)))
   0x0000000000400674 <+0>: 00 00 00 90 adrp    x0, 0x400000
   0x0000000000400678 <+4>: 02 bc 43 fd ldr d2, [x0, #1912]
   0x000000000040067c <+8>: 30 20 62 1e fcmpe   d1, d2
   0x0000000000400680 <+12>:    6c 00 00 54 b.gt    0x40068c <divide+24>

10      {
11          return  a/b ;
   0x0000000000400684 <+16>:    00 18 61 1e fdiv    d0, d0, d1

16          return 0;
17      }
18  }
   0x0000000000400688 <+20>:    c0 03 5f d6 ret

9       if(!((-delta < b) && (b < delta)))
   0x000000000040068c <+24>:    00 00 00 90 adrp    x0, 0x400000
   0x0000000000400690 <+28>:    02 c0 43 fd ldr d2, [x0, #1920]
   0x0000000000400694 <+32>:    30 20 62 1e fcmpe   d1, d2
   0x0000000000400698 <+36>:    44 00 00 54 b.mi    0x4006a0 <divide+44>  // b.first
   0x000000000040069c <+40>:    fa ff ff 17 b   0x400684 <divide+16>

12      }
13      else
14      {
15          longjmp(env, 1);
   0x00000000004006a0 <+44>:    fd 7b bf a9 stp x29, x30, [sp, #-16]!
   0x00000000004006a4 <+48>:    80 00 00 b0 adrp    x0, 0x411000 <_setjmp@got.plt>
   0x00000000004006a8 <+52>:    21 00 80 52 mov w1, #0x1                    // #1

7   {
   0x00000000004006ac <+56>:    fd 03 00 91 mov x29, sp

12      }
13      else
14      {
15          longjmp(env, 1);
   0x00000000004006b0 <+60>:    00 20 01 91 add x0, x0, #0x48
   0x00000000004006b4 <+64>:    9b ff ff 97 bl  0x400520 <longjmp@plt>
End of assembler dump.
Dump of assembler code for function main:
/var/folders/jh/0ybww9_12tv8l_jbrc8kwhxw0000gn/T/ox-bash-wtGI3WvdO/test.c:
21  {
22      int ret;
23  
24      ret = setjmp(env);
   0x0000000000400530 <+0>: fd 7b bf a9 stp x29, x30, [sp, #-16]!
   0x0000000000400534 <+4>: 80 00 00 b0 adrp    x0, 0x411000 <_setjmp@got.plt>
   0x0000000000400538 <+8>: 00 20 01 91 add x0, x0, #0x48

21  {
   0x000000000040053c <+12>:    fd 03 00 91 mov x29, sp

22      int ret;
23  
24      ret = setjmp(env);
   0x0000000000400540 <+16>:    e4 ff ff 97 bl  0x4004d0 <_setjmp@plt>

25  
26      if (!ret) {
   0x0000000000400544 <+20>:    a0 00 00 35 cbnz    w0, 0x400558 <main+40>

15          longjmp(env, 1);
   0x0000000000400548 <+24>:    80 00 00 b0 adrp    x0, 0x411000 <_setjmp@got.plt>
   0x000000000040054c <+28>:    21 00 80 52 mov w1, #0x1                    // #1
   0x0000000000400550 <+32>:    00 20 01 91 add x0, x0, #0x48
   0x0000000000400554 <+36>:    f3 ff ff 97 bl  0x400520 <longjmp@plt>

27          printf("5/0=%lf\n", divide(5,0));
28      } else  if (ret == 1) {
   0x0000000000400558 <+40>:    1f 04 00 71 cmp w0, #0x1
   0x000000000040055c <+44>:    81 00 00 54 b.ne    0x40056c <main+60>  // b.any

29          printf("ERR\n");
   0x0000000000400560 <+48>:    00 00 00 90 adrp    x0, 0x400000
   0x0000000000400564 <+52>:    00 c0 1d 91 add x0, x0, #0x770
   0x0000000000400568 <+56>:    ea ff ff 97 bl  0x400510 <puts@plt>

30      }
31      return 0;
   0x000000000040056c <+60>:    00 00 80 52 mov w0, #0x0                    // #0
   0x0000000000400570 <+64>:    fd 7b c1 a8 ldp x29, x30, [sp], #16
   0x0000000000400574 <+68>:    c0 03 5f d6 ret
End of assembler dump.

从上面的汇编代码可以看到,setjmp/longjmp只是普通的函数调用,调用到c库里边的_setjmp/longjmp函数,并没有和编译器有太大的联系.

libc implement of setjmp/longjmp

setjmp

/* Keep traditional entry points in with sigsetjmp(). */
ENTRY (setjmp)
    mov x1, #1
    b   1f
END (setjmp)

ENTRY (_setjmp)
    mov x1, #0
    b   1f
END (_setjmp)
libc_hidden_def (_setjmp)

ENTRY (__sigsetjmp)
    DELOUSE (0)

1:
    stp x19, x20, [x0, #JB_X19<<3]
    stp x21, x22, [x0, #JB_X21<<3]
    stp x23, x24, [x0, #JB_X23<<3]
    stp x25, x26, [x0, #JB_X25<<3]
    stp x27, x28, [x0, #JB_X27<<3]

#ifdef PTR_MANGLE
    PTR_MANGLE (4, 30, 3, 2)
    stp x29,  x4, [x0, #JB_X29<<3]
#else
    stp x29, x30, [x0, #JB_X29<<3]
#endif
    /* setjmp probe takes 3 arguments, address of jump buffer
       first argument (8@x0), return value second argument (-4@x1),
       and target address (8@x30), respectively.  */
    LIBC_PROBE (setjmp, 3, 8@x0, -4@x1, 8@x30)
    stp  d8,  d9, [x0, #JB_D8<<3]
    stp d10, d11, [x0, #JB_D10<<3]
    stp d12, d13, [x0, #JB_D12<<3]
    stp d14, d15, [x0, #JB_D14<<3]
#ifdef PTR_MANGLE
    mov x4, sp
    PTR_MANGLE (5, 4, 3, 2)
    str x5, [x0, #JB_SP<<3]
#else
    mov x2,  sp
    str x2,  [x0, #JB_SP<<3]
#endif
#if IS_IN (rtld)
    /* In ld.so we never save the signal mask */
    mov w0, #0
    RET
#else
    b   C_SYMBOL_NAME(__sigjmp_save)
#endif
END (__sigsetjmp)
hidden_def (__sigsetjmp)

从setjmp的实现中可以看到,setjmp只是简单的保存了当前的寄存器信息,包括lr,以及其他需要保存的寄存器,__sigjmp_save还会保存当前的signal mask信息

longjmp

https://github.com/lattera/glibc/blob/895ef79e04a953cac1493863bcae29ad85657ee1/sysdeps/aarch64/__longjmp.S#L25

ENTRY (__longjmp)
    cfi_def_cfa(x0, 0)
    cfi_offset(x19, JB_X19<<3)
    cfi_offset(x20, JB_X20<<3)
    cfi_offset(x21, JB_X21<<3)
    cfi_offset(x22, JB_X22<<3)
    cfi_offset(x23, JB_X23<<3)
    cfi_offset(x24, JB_X24<<3)
    cfi_offset(x25, JB_X25<<3)
    cfi_offset(x26, JB_X26<<3)
    cfi_offset(x27, JB_X27<<3)
    cfi_offset(x28, JB_X28<<3)
    cfi_offset(x29, JB_X29<<3)
    cfi_offset(x30, JB_LR<<3)

    cfi_offset( d8, JB_D8<<3)
    cfi_offset( d9, JB_D9<<3)
    cfi_offset(d10, JB_D10<<3)
    cfi_offset(d11, JB_D11<<3)
    cfi_offset(d12, JB_D12<<3)
    cfi_offset(d13, JB_D13<<3)
    cfi_offset(d14, JB_D14<<3)
    cfi_offset(d15, JB_D15<<3)

    DELOUSE (0)

    ldp x19, x20, [x0, #JB_X19<<3]
    ldp x21, x22, [x0, #JB_X21<<3]
    ldp x23, x24, [x0, #JB_X23<<3]
    ldp x25, x26, [x0, #JB_X25<<3]
    ldp x27, x28, [x0, #JB_X27<<3]
#ifdef PTR_DEMANGLE
    ldp x29,  x4, [x0, #JB_X29<<3]
    PTR_DEMANGLE (30, 4, 3, 2)
#else
    ldp x29, x30, [x0, #JB_X29<<3]
#endif
    /* longjmp probe takes 3 arguments, address of jump buffer as
       first argument (8@x0), return value as second argument (-4@x1),
       and target address (8@x30), respectively.  */
    LIBC_PROBE (longjmp, 3, 8@x0, -4@x1, 8@x30)
    ldp  d8,  d9, [x0, #JB_D8<<3]
    ldp d10, d11, [x0, #JB_D10<<3]
    ldp d12, d13, [x0, #JB_D12<<3]
    ldp d14, d15, [x0, #JB_D14<<3]

        /* Originally this was implemented with a series of
       .cfi_restore() directives.
           The theory was that cfi_restore should revert to previous
           frame value is the same as the current value.  In practice
           this doesn't work, even after cfi_restore() gdb continues
           to try to recover a previous frame value offset from x0,
           which gets stuffed after a few more instructions.  The
           cfi_same_value() mechanism appears to work fine.  */

    cfi_same_value(x19)
    cfi_same_value(x20)
    cfi_same_value(x21)
    cfi_same_value(x22)
    cfi_same_value(x23)
    cfi_same_value(x24)
    cfi_same_value(x25)
    cfi_same_value(x26)
    cfi_same_value(x27)
    cfi_same_value(x28)
    cfi_same_value(x29)
    cfi_same_value(x30)
    cfi_same_value(d8)
    cfi_same_value(d9)
    cfi_same_value(d10)
    cfi_same_value(d11)
    cfi_same_value(d12)
    cfi_same_value(d13)
    cfi_same_value(d14)
    cfi_same_value(d15)
#ifdef PTR_DEMANGLE
    ldr x4, [x0, #JB_SP<<3]
    PTR_DEMANGLE (5, 4, 3, 2)
#else
    ldr x5, [x0, #JB_SP<<3]
#endif
    mov sp, x5

    /* longjmp_target probe takes 3 arguments, address of jump buffer
       as first argument (8@x0), return value as second argument (-4@x1),
       and target address (8@x30), respectively.  */
    LIBC_PROBE (longjmp_target, 3, 8@x0, -4@x1, 8@x30)
    cmp x1, #0
    mov x0, #1
    csel    x0, x1, x0, ne
    /* Use br instead of ret because ret is guaranteed to mispredict */
    br  x30
END (__longjmp)

从longjmp的实现中,longjmp会还原之前的寄存器,并且重新返回之前保存的lr指针位置

risk of setjmp/longjmp usage

由于setjmp/longjmp的实现,以及编译器的优化,会导致某些情况下出现局部变量的值被修改的情况

  • 由于编译器优化,不同的变量可能使用同一个寄存器/栈空间来实现,因次带来问题
  • 由于编译器优化,代码顺序被调整而导致问题

使用时局部变量需要使用volatile关键字来修饰,防止编译器优化
详情请参考标准文档 http://www.open-std.org/jtc1/sc22/wg14/www/docs/n1494.pdf 7.13.1.1 The setjmp macro

  • the entire controlling expression of a selection or iteration statement;
  • one operand of a relational or equality operator with the other operand an integer constant expression, with the resulting expression being the entire controlling expression of a selection or iteration statement;
  • the operand of a unary ! operator with the resulting expression being the entire controlling expression of a selection or iteration statement; or
  • the entire expression of an expression statement (possibly cast to void).

详情可以参考:
https://rules.sonarsource.com/c/RSPEC-982
https://gcc.gnu.org/bugzilla/show_bug.cgi?id=56982

Contact me via :)
虚怀乃若谷,水深则流缓。