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
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