setjmp/longjmp非局部跳转函数分析
来源:互联网 发布:java base64encoder 编辑:程序博客网 时间:2024/05/22 17:25
之前就一直好奇setjmp()/longjmp()函数是怎么实现非局部跳转的,心中猜测应该是通过保存调用setjmp()函数处的栈上下文(stack context),之后通过函数longjmp()来恢复这个栈上下文来实现的,可是心中依然有疑惑,到底需要保存哪些东西呢,还有是怎么改变setjmp函数的返回值的呢。本文就通过实际程序调试以及glibc源码来一探究竟吧(本文针对i386平台)!
调制程序如下:
#include <stdio.h>#include <stdlib.h>#include <setjmp.h>jmp_buf env;int main(int argc, const char *argv[]){ if (0 == setjmp(env)) { printf("start to throw exception\n"); longjmp(env, 1); } else { /* exeception */ printf("in exception\n"); } exit(EXIT_SUCCESS);}
首先看下/usr/include/setjmp.h头文件中jmp_buf的定义:
#include <bits/setjmp.h> /* Get `__jmp_buf'. */#include <bits/sigset.h> /* Get `__sigset_t'. *//* Calling environment, plus possibly a saved signal mask. */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. */};__BEGIN_NAMESPACE_STD typedef struct __jmp_buf_tag jmp_buf[1];
可以看到类型jmp_buf被定义为结构体struct __jmp_buf_tag的一维数组,这样做至少有两个好处:
- 在声明jmp_buf时,可以把数据分配到堆栈上
- 作为参数传递时则作为一个指针
我们目前只关心__jmp_buf __jmpbuf,也就是注释中提到的calling environment,其他的我们在后文中提到sigsetjmp()/siglongjmp()时再提。在/usr/include/i386-linux-gnu/bits/setjmp.h中找到__jmp_buf的定义如下:
#ifndef _ASMtypedef int __jmp_buf[6];#endif
看来这里的整形数组就是保存栈上下文的地方了。现在开始gdb调试程序,先看main函数的反汇编代码如下:
astrol@astrol:~$ gdb setjmp -qReading symbols from /home/astrol/setjmp...done.(gdb) break mainBreakpoint 1 at 0x804844d: file setjmp.c, line 9.(gdb) runStarting program: /home/astrol/setjmpBreakpoint 1, main (argc=1, argv=0xbffff504) at setjmp.c:99 if (0 == setjmp(env)) {(gdb) disassembleDump of assembler code for function main: 0x08048444 <+0>: push %ebp 0x08048445 <+1>: mov %esp,%ebp 0x08048447 <+3>: and $0xfffffff0,%esp 0x0804844a <+6>: sub $0x10,%esp=> 0x0804844d <+9>: movl $0x804a040,(%esp) 0x08048454 <+16>: call 0x8048350 <_setjmp@plt> 0x08048459 <+21>: test %eax,%eax 0x0804845b <+23>: jne 0x804847d <main+57> 0x0804845d <+25>: movl $0x8048570,(%esp) 0x08048464 <+32>: call 0x8048360 <puts@plt> 0x08048469 <+37>: movl $0x2,0x4(%esp) 0x08048471 <+45>: movl $0x804a040,(%esp) 0x08048478 <+52>: call 0x8048340 <longjmp@plt> 0x0804847d <+57>: movl $0x8048589,(%esp) 0x08048484 <+64>: call 0x8048360 <puts@plt> 0x08048489 <+69>: movl $0x3,0x4(%esp) 0x08048491 <+77>: movl $0x804a040,(%esp) 0x08048498 <+84>: call 0x8048340 <longjmp@plt>End of assembler dump.(gdb)
最开始的三行是大家熟知的function prologue:将旧的函数帧指针ebp入栈,然后更新ebp为当前堆栈指针esp,接着使esp 16字节对齐。sub $0x10, %esp是在为main函数开辟栈空间,一般是用于局部变量以及后续的入出栈操作使用的。
(gdb) ptype envtype = struct __jmp_buf_tag { __jmp_buf __jmpbuf; int __mask_was_saved; __sigset_t __saved_mask;} [1](gdb) ptype __jmp_buftype = int [6](gdb) print $ebp$18 = (void *) 0xbffff468(gdb) print $esp$19 = (void *) 0xbffff44c(gdb) print env$20 = {{__jmpbuf = {0, 0, 0, 0, 0, 0}, __mask_was_saved = 0, __saved_mask = {__val = {0 <repeats 32 times>}}}}(gdb)
配合print &env的结果我们发现,程序将参数env的地址入栈,接着就是调用函数setjmp函数。注意在调用call指令的同时,程序也将下一条指令的地址入栈了,也就是test %eax,%eax的地址0x08048459,此时的函数帧情况如下(图中一个格子代表4个字节):
好,接下来就是重点了,让我们来看看函数setjmp都做了什么。
(gdb) disassembleDump of assembler code for function _setjmp:=> 0x00160bb0 <+0>: xor %eax,%eax 0x00160bb2 <+2>: mov 0x4(%esp),%edx 0x00160bb6 <+6>: mov %ebx,(%edx) 0x00160bb8 <+8>: mov %esi,0x4(%edx) 0x00160bbb <+11>: mov %edi,0x8(%edx) 0x00160bbe <+14>: lea 0x4(%esp),%ecx 0x00160bc2 <+18>: xor %gs:0x18,%ecx 0x00160bc9 <+25>: rol $0x9,%ecx 0x00160bcc <+28>: mov %ecx,0x10(%edx) 0x00160bcf <+31>: mov (%esp),%ecx 0x00160bd2 <+34>: xor %gs:0x18,%ecx 0x00160bd9 <+41>: rol $0x9,%ecx 0x00160bdc <+44>: mov %ecx,0x14(%edx) 0x00160bdf <+47>: mov %ebp,0xc(%edx) 0x00160be2 <+50>: mov %eax,0x18(%edx) 0x00160be5 <+53>: retEnd of assembler dump.
我们把glibc中的源码也贴出来看看(glibc-2.15):
glibc-2.15/sysdeps/i386/bsd-_setjmp.S:
/* This just does a tail-call to `__sigsetjmp (ARG, 0)'. We cannot do it in C because it must be a tail-call, so frame-unwinding in setjmp doesn't clobber the state restored by longjmp. */#include <sysdep.h>#include <jmpbuf-offsets.h>#include "bp-sym.h"#include "bp-asm.h"#define PARMSLINKAGE/* no space for saved regs */#define JMPBUFPARMS#define SIGMSKJMPBUF+PTR_SIZEENTRY (BP_SYM (_setjmp))ENTERxorl %eax, %eaxmovl JMPBUF(%esp), %edxCHECK_BOUNDS_BOTH_WIDE (%edx, JMPBUF(%esp), $(JB_SIZE+4)) /* Save registers. */movl %ebx, (JB_BX*4)(%edx)movl %esi, (JB_SI*4)(%edx)movl %edi, (JB_DI*4)(%edx)/* Save SP as it will be after we return. */leal JMPBUF(%esp), %ecx#ifdef PTR_MANGLEPTR_MANGLE (%ecx)#endif movl %ecx, (JB_SP*4)(%edx) /* Save PC we are returning to now. */movl PCOFF(%esp), %ecx#ifdef PTR_MANGLEPTR_MANGLE (%ecx)#endif movl %ecx, (JB_PC*4)(%edx)LEAVE/* Save caller's frame pointer. */movl %ebp, (JB_BP*4)(%edx) movl %eax, JB_SIZE(%edx) /* No signal mask set. */retEND (BP_SYM (_setjmp))libc_hidden_def (_setjmp)
glibc-2.15/sysdeps/i386/jmpbuf-offsets.h
#define JB_BX0#define JB_SI1#define JB_DI2#define JB_BP3#define JB_SP4#define JB_PC5#define JB_SIZE 24
从这里可以终于可以看出__jmp_buf整形数组存储的依次是:ebx,esi,edi,ebp,esp,eip
好,现在分析汇编代码。
一开始就将eax清零,这段汇编代码结束时,作为setjmp函数的返回值 -- 使用eax作为函数的返回值这种做法,是很常见的。
mov 0x4(%esp),%edx是将env的地址付给寄存器edx,即此时edx指向__jmpbuf首地址。如下图:
mov %ebx, (%edx)
mov %esi, 0x4(%edx)
mov %edi, 0x8(%edx)
这三句分别将当前程序的寄存器ebx,esi,edi中的值存入__jmpbuf数组,下标依次为JB_BX,JB_SI,JB_DI。
lea 0x4(%esp),%ecx
xor %gs:0x18,%ecx
rol $0x9,%ecx
mov %ecx,0x10(%edx)
这四句其实就是将当前程序的esp+4存入数组__jmpbuf下标JB_SP的位置,至于为什么不是直接存入,这里牵扯到glibc程序安全问题,我们后面再说,这里只要知道是存储数值esp+4就OK了。
mov (%esp),%ecx
xor %gs:0x18,%ecx
rol $0x9,%ecx
mov %ecx,0x14(%edx)
同样的道理,这四行代码是将eip存储到数组__jmpbuf下标JB_PC的位置。
mov %ebp,0xc(%edx)
这句将是寄存器ebp的值保存到数组__jmpbuf下标JB_BP位置。
mov %eax,0x18(%edx)就跟数组__jmpbuf无关了,而是之后的__mask_was_saved,这里将其赋值为0,表明不需要保存进程的信号屏蔽字。最后setjmp返回。
程序接着走,走到longjmp时首先进入的是glibc-2.15/setjmp/longjmp.c
/* Set the signal mask to the one specified in ENV, and jump to the position specified in ENV, causing the setjmp call there to return VAL, or 1 if VAL is 0. */void__libc_siglongjmp (sigjmp_buf env, int val){ /* Perform any cleanups needed by the frames being unwound. */ _longjmp_unwind (env, val); if (env[0].__mask_was_saved) /* Restore the saved signal mask. */ (void) __sigprocmask (SIG_SETMASK, &env[0].__saved_mask, (sigset_t *) NULL); /* Call the machine-dependent function to restore machine state. */ __longjmp (env[0].__jmpbuf, val ?: 1);}
可以看到如果env[0].__mask_was_saved非零的话,程序就会调用sigprocmask来恢复进程的信号屏蔽字。
同时我们也可以看到如果我们调用longjmp时第二个参数是0的话,那么程序会自动将其设置成1。
接着就真正进入longjmp的恢复栈上下文的代码了。
(gdb) disassembleDump of assembler code for function __longjmp:=> 0x00160c50 <+0>: mov 0x4(%esp),%eax 0x00160c54 <+4>: mov 0x14(%eax),%edx 0x00160c57 <+7>: mov 0x10(%eax),%ecx 0x00160c5a <+10>: ror $0x9,%edx 0x00160c5d <+13>: xor %gs:0x18,%edx 0x00160c64 <+20>: ror $0x9,%ecx 0x00160c67 <+23>: xor %gs:0x18,%ecx 0x00160c6e <+30>: mov (%eax),%ebx 0x00160c70 <+32>: mov 0x4(%eax),%esi 0x00160c73 <+35>: mov 0x8(%eax),%edi 0x00160c76 <+38>: mov 0xc(%eax),%ebp 0x00160c79 <+41>: mov 0x8(%esp),%eax 0x00160c7d <+45>: mov %ecx,%esp 0x00160c7f <+47>: jmp *%edxEnd of assembler dump.
glibc中的代码(glibc-2.15/sysdeps/i386/__longjmp.S):
#include <sysdep.h>#include <jmpbuf-offsets.h>#include <asm-syntax.h>.textENTRY (__longjmp)#ifdef PTR_DEMANGLEmovl 4(%esp), %eax/* User's jmp_buf in %eax. *//* Save the return address now. */movl (JB_PC*4)(%eax), %edx/* Get the stack pointer. */movl (JB_SP*4)(%eax), %ecxPTR_DEMANGLE (%edx)PTR_DEMANGLE (%ecx)cfi_def_cfa(%eax, 0)cfi_register(%eip, %edx)cfi_register(%esp, %ecx)cfi_offset(%ebx, JB_BX*4)cfi_offset(%esi, JB_SI*4)cfi_offset(%edi, JB_DI*4)cfi_offset(%ebp, JB_BP*4) /* Restore registers. */movl (JB_BX*4)(%eax), %ebxmovl (JB_SI*4)(%eax), %esimovl (JB_DI*4)(%eax), %edimovl (JB_BP*4)(%eax), %ebpcfi_restore(%ebx)cfi_restore(%esi)cfi_restore(%edi)cfi_restore(%ebp)movl 8(%esp), %eax/* Second argument is return value. */movl %ecx, %esp#elsemovl 4(%esp), %ecx/* User's jmp_buf in %ecx. */movl 8(%esp), %eax/* Second argument is return value. *//* Save the return address now. */movl (JB_PC*4)(%ecx), %edx /* Restore registers. */movl (JB_BX*4)(%ecx), %ebxmovl (JB_SI*4)(%ecx), %esimovl (JB_DI*4)(%ecx), %edimovl (JB_BP*4)(%ecx), %ebpmovl (JB_SP*4)(%ecx), %esp#endif/* Jump to saved PC. */ jmp *%edxEND (__longjmp)
mov 0x4(%esp), %eax 将数组__jmpbuf的地址复制为寄存器eax
mov 0x14(%eax), %edx
mov 0x10(%eax), %ecx
ror $0x9, %edx
xor %gs:0x18, %edx
ror $0x9, %ecx
xor %gs:0x18, %ecx
这六句就是把setjmp中经过特殊处理的eip和esp分别存储到寄存器edx和ecx中。
mov (%eax), %ebx
mov 0x4(%eax), %esi
mov 0x8(%eax), %edi
mov 0xc(%eax), %ebp
这四句分别在恢复寄存器ebx,esi,edi和ebp寄存器的值。
接下来的mov 0x8(%esp), %eax就解答了我文章一开始的疑问,就是通过这句来改变setjmp的返回值的,这里通过把压入栈的第二个参数复制给寄存器eax作为返回值。
倒数第二句mov %ecx, %esp是在恢复栈指针。
最后一句jmp *%edx就是调转到调用setjmp时压入到栈中的eip处继续执行指令。
好了,终于了解了setjmp()/longjmp()的实现细节了。
现在来了解下sigsetjmp()/siglongjmp()函数。为了了解为什么要有这两个函数,我们先来看一个有问题的程序。
#include <stdio.h>#include <stdlib.h>#include <unistd.h>#include <signal.h>#include <setjmp.h>jmp_buf env;static void sig_alrm(int signo){ printf("in SIGALRM signal handler.\n"); alarm(2); longjmp(env, 1);}int main(int argc, const char *argv[]){ if (signal(SIGALRM, sig_alrm) == SIG_ERR) { perror("signal error: "); exit(EXIT_FAILURE); } alarm(2); setjmp(env); for ( ;; ) pause(); exit(EXIT_SUCCESS);}
运行程序,我们会发现,尽管已经在信号处理程序sig_alrm中调用了alarm函数,但是该信号处理程序我们只调用了一次。是不是很奇怪?
默认情况下(不指定sa_flags为SA_NODEFER),信号处理时会自动阻塞正在被处理的信号。也就是说,和正在被处理同样的信号再次发生时,它会被阻塞到本次信号处理结束。在信号处理函数返回时,进程的信号屏蔽字会被恢复,即解除对当前信号的阻塞。在上面的程序中,并没有让信号处理函数正常返回,而是使用了longjmp()直接跳转,所以进程的信号屏蔽字在第一次收到信号后,就把信号设置为阻塞并且再也没有恢复,因而再也触发不了信号处理函数了,除非手动将进程的信号屏蔽字去除。
sigsetjmp()/siglongjmp()就解决了这个问题:sigsetjmp()会保存进程的当前屏蔽字(第二个参数非零的情况下),而siglongjmp()会恢复这个信号屏蔽字,所以不管你阻塞了什么信号,调用siglongjmp()时都会恢复。程序修改如下就可以了:
#include <stdio.h>#include <stdlib.h>#include <unistd.h>#include <signal.h>#include <setjmp.h>jmp_buf env;static volatile sig_atomic_t canjump;static void sig_alrm(int signo){ if (canjump == 0) return; printf("in SIGALRM signal handler.\n"); alarm(2); canjump = 0; siglongjmp(env, 1);}int main(int argc, const char *argv[]){ if (signal(SIGALRM, sig_alrm) == SIG_ERR) { perror("signal error: "); exit(EXIT_FAILURE); } alarm(2); sigsetjmp(env, 1); canjump = 1; for ( ;; ) pause(); exit(EXIT_SUCCESS);}
看看glibc源码是怎么做的(glibc-2.15/sysdeps/i386/setjmp.S):
#include <sysdep.h>#include <jmpbuf-offsets.h>#include <asm-syntax.h>#include "bp-sym.h"#include "bp-asm.h"#define PARMSLINKAGE/* no space for saved regs */#define JMPBUFPARMS#define SIGMSKJMPBUF+PTR_SIZEENTRY (BP_SYM (__sigsetjmp))ENTERmovl JMPBUF(%esp), %eaxCHECK_BOUNDS_BOTH_WIDE (%eax, JMPBUF(%esp), $JB_SIZE) /* Save registers. */movl %ebx, (JB_BX*4)(%eax)movl %esi, (JB_SI*4)(%eax)movl %edi, (JB_DI*4)(%eax)leal JMPBUF(%esp), %ecx/* Save SP as it will be after we return. */#ifdef PTR_MANGLEPTR_MANGLE (%ecx)#endif movl %ecx, (JB_SP*4)(%eax)movl PCOFF(%esp), %ecx/* Save PC we are returning to now. */#ifdef PTR_MANGLEPTR_MANGLE (%ecx)#endif movl %ecx, (JB_PC*4)(%eax)LEAVE /* pop frame pointer to prepare for tail-call. */movl %ebp, (JB_BP*4)(%eax) /* Save caller's frame pointer. */#if defined NOT_IN_libc && defined IS_IN_rtld/* In ld.so we never save the signal mask. */xorl %eax, %eaxret#else/* Make a tail call to __sigjmp_save; it takes the same args. */jmp __sigjmp_save#endifEND (BP_SYM (__sigsetjmp))
以下是调试汇编:
(gdb) disassembleDump of assembler code for function __sigsetjmp:=> 0x00160ad0 <+0>: mov 0x4(%esp),%eax # 将数组__jmpbuf的地址保存在eax 0x00160ad4 <+4>: mov %ebx,(%eax) # 恢复ebx 0x00160ad6 <+6>: mov %esi,0x4(%eax) # 恢复esi 0x00160ad9 <+9>: mov %edi,0x8(%eax) # 恢复edi 0x00160adc <+12>: lea 0x4(%esp),%ecx 0x00160ae0 <+16>: xor %gs:0x18,%ecx 0x00160ae7 <+23>: rol $0x9,%ecx 0x00160aea <+26>: mov %ecx,0x10(%eax) # 恢复esp 0x00160aed <+29>: mov (%esp),%ecx 0x00160af0 <+32>: xor %gs:0x18,%ecx 0x00160af7 <+39>: rol $0x9,%ecx 0x00160afa <+42>: mov %ecx,0x14(%eax) # 恢复eip 0x00160afd <+45>: mov %ebp,0xc(%eax) # 恢复ebp 0x00160b00 <+48>: jmp 0x160b10 <__sigjmp_save>End of assembler dump.
几乎和setjmp的源码是一样的,多了最后一句,调用了__sigjmp_save函数(glibc-2.15/setjmp/sigjmp.c):
#include <stddef.h>#include <setjmp.h>#include <signal.h>/* This function is called by the `sigsetjmp' macro before doing a `__setjmp' on ENV[0].__jmpbuf. Always return zero. */int__sigjmp_save (sigjmp_buf env, int savemask){ env[0].__mask_was_saved = (savemask && __sigprocmask (SIG_BLOCK, (sigset_t *) NULL, &env[0].__saved_mask) == 0); return 0;}
可以很清楚的看到,当调用sigsetjmp时,第二个参数非零时会保存进程的当前屏蔽字于__saved_mask中,并将__mask_was_saved赋值为1,供siglongjmp函数使用。
调用siglongjmp时调用的也是__libc_siglongjmp函数,只不过在该函数中会因为env[0].__mask_was_saved为1而调用sigprocmask恢复了进程的信号屏蔽字。
此外,还需要注意到,信号是异步的,即信号可以在任何一个时间点送达,因此它完全可能在sigsetjmp()或者setjmp()设置之前就到达,那么在这种情况下进入信号处理函数,并在里面调用了siglongjmp()或者longjmp() ,那么就会使用一个未初始化的jmpbuf(跳转缓冲)。为了防止这种情况,在程序中引入了一个监控变量canjump,如果canjump没有被设置时,信号处理函数只是简单的退出,而不会进行非局部跳转;也只有在sigsetjmp()或setjmp()设置完后,才会将该变量设为1。当然,另外一个办法是也可以在建立信号处理函数之前就调用sigsetjmp()或setjmp() --- 实际上,在一个复杂的程序里,很难保证说一定能让这两个步骤按此顺序去执行 --- 因此较为简单的方法还是像上面使用一个监控变量。
至此,我们就彻底了解了setjmp()/longjmp()和sigsetjmp()/siglongjmp()的实现细节了。
接下来简单谈一谈它们在使用上的注意点吧。
未完待续!
参考链接:http://web.eecs.utk.edu/~plank/plank/classes/cs360/360/notes/Setjmp/lecture.html
http://blog.codingnow.com/2010/05/setjmp.html
http://www.cnblogs.com/archimedes/p/c-exception-assert.html
http://www.cnblogs.com/lienhua34/archive/2012/04/22/2464859.html
http://wenku.baidu.com/link?url=XigAOFO54xdlZsK9ADfCYrjINucJVBr_U0oJAV5lkTluoyGHd2g3q_UVLLzZa8yQ94RKUwy1UnlS8n9s__bJR_Mq53YwK51rss8tnmdxuOy
http://yanx730.blog.163.com/blog/static/1194421200791423334964/
http://www.csl.mtu.edu/cs4411.ck/www/NOTES/non-local-goto/
http://blog.csdn.net/topasstem8/article/details/6004945
http://www.groad.net/bbs/forum.php?mod=viewthread&tid=919&highlight=setjmp
- setjmp/longjmp非局部跳转函数分析
- 非局部跳转函数 setjmp 和 longjmp
- 非局部跳转函数 setjmp 和 longjmp .
- setjmp longjmp 非局部跳转
- 非局部跳转语句---setjmp和longjmp函数
- setjmp与longjmp非局部跳转函数的使用
- C语言接口与实现【第四章】 setjmp/longjmp非局部跳转函数分析
- 【setjmp和longjmp 】 C语言的非局部跳转:setjmp和longjmp(跨函数长跳转)
- 浅析C语言的非局部跳转:setjmp和longjmp
- 使用setjmp()和longjmp()执行非局部跳转
- (C)非局部跳转语句(setjmp和longjmp)
- C语言的非局部跳转:setjmp和longjmp
- 浅析C语言的非局部跳转:setjmp和longjmp
- 浅析C语言的非局部跳转:setjmp和longjmp
- 浅析C语言的非局部跳转:setjmp和longjmp
- 非本地跳转函数setjmp,longjmp, sigsetjmp, siglongjmp
- 非局部跳转 setjmp
- setjmp和longjmp的"非本地跳转"
- adb获取IMIE号
- JDK 安装代替默认1.6(Mac)
- x86寄存器说明
- 简述二分图
- Android 技术博客汇总
- setjmp/longjmp非局部跳转函数分析
- 并查集入门
- oracle查询索引是否生效
- beta
- java 变量
- Openstack I版 结合 Ceph 分布式存储 部署安装(七)
- 《平凡的世界》 读后感
- mysql创建索引
- zoj 3861 Valid Pattern Lock(dfs)