setjmp & longjmp实现分析

如何使用setjmp & longjmp,就不再细说了,请参考APUE 7.10.

本文解释如下知识点:
1、简单介绍X86_64的寄存器
2、setjmp & longjmp是怎么实现的。
3、为什么能从setjmp处多次返回。
4、从setjmp返回时,那些数据是无效了,如何避免。

本文没有画出函数调用栈桢的图,如果对汇编不是很熟悉的话,最好边看边画^_^,会事关功倍。

下文是通过反汇编如下代码来分析的

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

jmp_buf my_jum_buf;
void fun_fun()
{
    printf("Enter fun_fun ...\n");
    longjmp(my_jum_buf, 8);//如下代码不会被执行
    printf("fun_fun::can‘t see");
}

void fun()
{
    fun_fun();
}

int main()
{
    int ret;
    if(ret = setjmp(my_jum_buf))
    {
        printf("Main: return after calling longjmp, ret = %d.\n", ret);
    }
    else
    {
        printf("Main: first time return from setjmp, ret = %d\n", ret);
        fun();
    }
    return 0;
}

输出:[email protected]:~/vm_disk_dpdk/study/apue# ./a.out      Main: first time return from setjmp, ret = 0Enter fun_fun ...Main: return after calling longjmp, ret = 8.

 
1、简单介绍X86_64的寄存器
X86-64通用寄存器相对于x86_32,新增加了%r8到%r15,原x86_32的8个(原32位寄存器名中的e改为r),一共16个寄存器。X86-64有16个64位寄存器,分别是:%rax,%rbx,%rcx,%rdx,%esi,%edi,%rbp,%rsp,%r8,%r9,%r10,%r11,%r12,%r13,%r14,%r15。
其中:
    %rax 作为函数返回值使用。
    %rsp 栈指针寄存器,指向栈顶
    %rdi,%rsi,%rdx,%rcx,%r8,%r9 用作函数参数,依次对应第1参数,第2参数......
    %rbx,%rbp,%r12,%r13,%14,%15 用作数据存储,遵循被调用者使用规则,即子函数使用之前要备份,以防他被修改
    %r10,%r11 用作数据存储,遵循调用者使用规则,简单说就是使用之前要先保存原值

补充:(https://msdn.microsoft.com/zh-cn/library/6t169e9c.aspx):
寄存器 RAX、RCX、RDX、R8、R9、R10、R11 被视为易失的,并且必须在函数调用时视为已销毁(除非通过全程序优化等分析被认定为安全的)。
寄存器 RBX、RBP、RDI、RSI、RSP、R12、R13、R14 和 R15 被视为非易失的,必须由使用它们的函数进行保存和还原。

这是x86_64的寄存器使用约定,如果要从子函数中正确返回,子函数只需要,也是仅需要保存与还原非易失的寄存器即可,rdi、rsi是做为参数使用的。

要保证执行完longjmp时,能从setjmp返回处继续正确执行,只需要如下两项正确即可:
1、程序执行流正确:能恢复PC的值为调用setjmp指令的下一条指令地址即可。而在调用setjmp时,CPU硬件会自动将PC(EIP)的值压入栈中,通过备份此栈中数据即可。
2、数据寄存器恢复到原样:对于做为数据存储的寄存器,只用关注非易失的寄存器,即子函数使用之前要备份的寄存器(RBX、RBP、RSP、R12、R13、R14 和 R15 ),在setjmp中备份即可。
即,只需要在setjmp中备份:RBX、RBP、RSP、R12、R13、R14 、 R15 及PC即可。

2、setjmp & longjmp的实现 -- 数据结构jmp_buf:
通过如下命令,得到预处理后的文件,以便取得相应的结构:
[email protected]:~/vm_disk_dpdk/study/apue# gcc -E setjmp.c > setjmp.i

typedef long int __jmp_buf[8];  //8个寄存器(RBX、RBP、RSP、R12、R13、R14 、 R15 及PC)的值,就保存在此变量中,
typedef int __sig_atomic_t;

typedef struct
{
    unsigned long int __val[(1024 / (8 * sizeof (unsigned long int)))];
} __sigset_t;

struct __jmp_buf_tag
{
    __jmp_buf __jmpbuf;
    int __mask_was_saved;
    __sigset_t __saved_mask;
};
typedef struct __jmp_buf_tag jmp_buf[1];

3、setjmp & longjmp的实现 -- setjmp:
反汇编分析:
(gdb) disassemble main
Dump of assembler code for function main:
   0x000000000040063a <+0>:     push   %rbp
   0x000000000040063b <+1>:     mov    %rsp,%rbp
=> 0x000000000040063e <+4>:     sub    $0x10,%rsp                           //在此时的esp即为调用setjmp之前的值,因此setjmp要备份此esp
   0x0000000000400642 <+8>:     mov    $0x601080,%edi                   //0x601080为全局变量my_jum_buf的地址,存入edi寄存器中。
   0x0000000000400647 <+13>:    callq  0x4004f0 <[email protected]> //call指令隐含了push pc,
   0x000000000040064c <+18>:    mov    %eax,-0x4(%rbp)
   0x000000000040064f <+21>:    cmpl   $0x0,-0x4(%rbp)
   0x0000000000400653 <+25>:    je     0x40066b <main+49>
   .............................................
   
(gdb) disassemble _setjmp
Dump of assembler code for function _setjmp:
   0x00007ffff7a4bb20 <+0>:     xor    %esi,%esi
   0x00007ffff7a4bb22 <+2>:     jmpq   0x7ffff7a4ba80 <__sigsetjmp> //jmp指令没有栈操作
End of assembler dump.
(gdb) disassemble __sigsetjmp
Dump of assembler code for function __sigsetjmp:
   0x00007ffff7a4ba80 <+0>:     mov    %rbx,(%rdi)              //将rbx存入my_jum_buf.__jmp_buf[0]
   0x00007ffff7a4ba83 <+3>:     mov    %rbp,%rax               //如下2行是对rbp寄存器加密,以防hancker,在longjum时会有逆操作
   0x00007ffff7a4ba86 <+6>:     xor    %fs:0x30,%rax         //关于加密,请参考:http://hmarco.org/bugs/CVE-2013-4788.html
   0x00007ffff7a4ba8f <+15>:    rol    $0x11,%rax
   0x00007ffff7a4ba93 <+19>:    mov    %rax,0x8(%rdi)    //将rbp存入my_jum_buf.__jmp_buf[1],因为是64位系统,所以是加8
   0x00007ffff7a4ba97 <+23>:    mov    %r12,0x10(%rdi)
   0x00007ffff7a4ba9b <+27>:    mov    %r13,0x18(%rdi)
   0x00007ffff7a4ba9f <+31>:    mov    %r14,0x20(%rdi)
   0x00007ffff7a4baa3 <+35>:    mov    %r15,0x28(%rdi)
   0x00007ffff7a4baa7 <+39>:    lea    0x8(%rsp),%rdx    //此时,rsp加8所对应的地址为main中凋用setjmp时的rsp,因为
   0x00007ffff7a4baac <+44>:    xor    %fs:0x30,%rdx     // 在main中调用setjmp时,硬件对PC执行了入栈操作,所以此处
   0x00007ffff7a4bab5 <+53>:    rol    $0x11,%rdx            //要减8。然后对rsp进行加密是并保存到my_jum_buf.__jmp_buf[6]
   0x00007ffff7a4bab9 <+57>:    mov    %rdx,0x30(%rdi)
   0x00007ffff7a4babd <+61>:    mov    (%rsp),%rax  //此时,rsp指向的是调用setjmp函数时,由硬件入栈的PC值
   0x00007ffff7a4baac <+44>:    xor    %fs:0x30,%rdx     //对PC进行加密是并保存到my_jum_buf.__jmp_buf[7]
   0x00007ffff7a4bac1 <+65>:    xor    %fs:0x30,%rax
   0x00007ffff7a4baca <+74>:    rol    $0x11,%rax
   0x00007ffff7a4bace <+78>:    mov    %rax,0x38(%rdi)
   0x00007ffff7a4bad2 <+82>:    jmpq   0x7ffff7a4bae0 <__sigjmp_save>
End of assembler dump.
(gdb) disassemble __sigjmp_save
Dump of assembler code for function __sigjmp_save:
   0x00007ffff7df1c70 <+0>:     movl   $0x0,0x40(%rdi)
   0x00007ffff7df1c77 <+7>:     xor    %eax,%eax    //置eax为0,即首次调用后,返回值是0
   0x00007ffff7df1c79 <+9>:     retq                        //setjmp函数返回,隐含:mov(%esp), $PC
End of assembler dump.
(gdb)

在这时,已经将8个寄存器的值保存到了jmpbuf中了。

对应glibc源码:

//sysdeps\x86_64\jmpbuf-offsets.h#define JB_RBX    0#define JB_RBP    1#define JB_R12    2#define JB_R13    3#define JB_R14    4#define JB_R15    5#define JB_RSP    6#define JB_PC    7#define JB_SIZE (8*8)

//sysdeps\x86_64\setjmp.SENTRY (__sigsetjmp)
    /* Save registers.  */
    movq %rbx, (JB_RBX*8)(%rdi)
#ifdef PTR_MANGLE
    movq %rbp, %rax
    PTR_MANGLE (%rax)
    movq %rax, (JB_RBP*8)(%rdi)
#else
    movq %rbp, (JB_RBP*8)(%rdi)
#endif
    movq %r12, (JB_R12*8)(%rdi)
    movq %r13, (JB_R13*8)(%rdi)
    movq %r14, (JB_R14*8)(%rdi)
    movq %r15, (JB_R15*8)(%rdi)
    leaq 8(%rsp), %rdx    /* Save SP as it will be after we return.  */
#ifdef PTR_MANGLE
    PTR_MANGLE (%rdx)
#endif
    movq %rdx, (JB_RSP*8)(%rdi)
    movq (%rsp), %rax    /* Save PC we are returning to now.  */
#ifdef PTR_MANGLE
    PTR_MANGLE (%rax)
#endif
    movq %rax, (JB_PC*8)(%rdi)

#if defined NOT_IN_libc && defined IS_IN_rtld
    /* In ld.so we never save the signal mask.  */
    xorl %eax, %eax
    retq
#else
    /* Make a tail call to __sigjmp_save; it takes the same args.  */
# ifdef    PIC
    jmp C_SYMBOL_NAME (BP_SYM (__sigjmp_save))@PLT
# else
    jmp BP_SYM (__sigjmp_save)
# endif

4、setjmp & longjmp的实现 -- longjmp:

调用longjmp的第一个参数jmpbuf地址,存放在edi中。第二个参数存放在esi中,此参数将做了setjmp的返回值。
将jmpbuf中备份的数据,恢复相应寄存器。之前在setjmp加密的,需要解密后再恢复。对于PC寄存器,因为PC不支持赋值操作,通过jmp指令实现PC的加载。
(gdb) disassemble longjmp
Dump of assembler code for function __libc_siglongjmp:
   .........................
   0x00007ffff7a4bb54 <+36>:    callq  0x7ffff7a4bb70 <__longjmp>
   .........................
End of assembler dump.
(gdb) disassemble __longjmp
Dump of assembler code for function __longjmp:
   0x00007ffff7a4bb70 <+0>:     mov    0x30(%rdi),%r8 //除了PC的值不能直接赋值外,其它7个寄存器
   0x00007ffff7a4bb74 <+4>:     mov    0x8(%rdi),%r9
   0x00007ffff7a4bb78 <+8>:     mov    0x38(%rdi),%rdx  //PC的值存入rdx,后面将通过jmp指令实现PC寄存器的加载
   0x00007ffff7a4bb7c <+12>:    ror    $0x11,%r8
   0x00007ffff7a4bb80 <+16>:    xor    %fs:0x30,%r8
   0x00007ffff7a4bb89 <+25>:    ror    $0x11,%r9
   0x00007ffff7a4bb8d <+29>:    xor    %fs:0x30,%r9
   0x00007ffff7a4bb96 <+38>:    ror    $0x11,%rdx
   0x00007ffff7a4bb9a <+42>:    xor    %fs:0x30,%rdx
   0x00007ffff7a4bba3 <+51>:    mov    (%rdi),%rbx
   0x00007ffff7a4bba6 <+54>:    mov    0x10(%rdi),%r12
   0x00007ffff7a4bbaa <+58>:    mov    0x18(%rdi),%r13
   0x00007ffff7a4bbae <+62>:    mov    0x20(%rdi),%r14
   0x00007ffff7a4bbb2 <+66>:    mov    0x28(%rdi),%r15
   0x00007ffff7a4bbb6 <+70>:    mov    %esi,%eax
   0x00007ffff7a4bbb8 <+72>:    mov    %r8,%rsp
   0x00007ffff7a4bbbb <+75>:    mov    %r9,%rbp
   0x00007ffff7a4bbbe <+78>:    jmpq   *%rdx
End of assembler dump.
(gdb)

5、为什么能从setjmp处多次返回,且返回值可自行设置

通过上面的分析,就容易知道了,由于在setjmp时,保存了执行现场,主要就是PC与栈指针。而在longjmp时,会恢复这些寄存器,所以,一恢复,就接着从setjmp指令的下一条指令执行了。

在longjmp时,将第二个参数(存放在esi寄存器中),直接赋值給eax,而函数的返回值就是在EAX中的,这是x86 CPU的约定。

而setjmp返回后的第一条指令,即是判断返回值。

6、从setjmp返回时,那些数据是无效了,如何避免

从上面的分析可知,非备份的寄存器,其数据是不可知的,可能被污染。

如果是从内存中取出的数据,则是OK的。如果有编译优先,当读变量,不一定是从内存取,有可能是从寄存器读的,此时,需要在变量前加关键字:volatile,保证每次都从内存取,而不是寄存器。

附:

Note that the optimizations don‘t affect the global, static, and volatile variables; their values after the longjmp are the last values that they assumed. The setjmp(3) manual page on one system states that variables stored in memory will have values as of the time of the longjmp, whereas variables in the CPU and floating-point registers are restored to their values when setjmp was called. This is indeed what we see when we run the program in Figure 7.13. Without optimization, all five variables are stored in memory (the register hint is ignored for regival). When we enable optimization, both autoval and regival go into registers, even though the former wasn‘t declared register, and the volatile variable stays in memory. The thing to realize with this example is that you must use the volatile attribute if you‘re writing portable code that uses nonlocal jumps. Anything else can change from one system to the next.

时间: 2024-10-16 08:22:44

setjmp & longjmp实现分析的相关文章

函数 setjmp, longjmp, sigsetjmp, siglongjmp

一,相关函数接口 1,setjmp,longjmp,sigsetjmp,siglongjmp   #include <setjmp.h> int setjmp(jmp_buf env); int sigsetjmp(sigjmp_buf env, int savesigs);    //savesigs非0时,在env中保存进程当前信号屏蔽字. void longjmp(jmp_buf env, int val); void siglongjmp(sigjmp_buf env, int val

setjmp/longjmp

1.setjmp/longjmp属于传统的错误处理 2.setjmp/longjmp是对goto语句的补充,goto只能实现局部跳转,setjmp/longjmp可以实现全局跳转 3.setjmp/longjmp与goto都有缺陷 传统错误处理的3种方式是: 1.设置全局错误状态,标准c中使用errno与perror函数来支持 2.使用标准c库中的信号 3.使用setjmp/longjmp setjmp/longjmp

Linux setjmp longjmp

/********************************************************************* * Linux setjmp longjmp * 说明: * 最近在看cmockery源代码的时候发现setjmp和longjmp函数,于是查了 * 相关的内容,发现真是个好东西,可以完成函数之间的直接跳转. * * 2016-5-7 深圳 南山平山村 曾剑锋 ************************************************

c setjmp longjmp

http://coolshell.cn/?s=setjmp http://www.cnblogs.com/hazir/p/c_setjmp_longjmp.html 1 double divide(double to, double by, jmp_buf env) 2 { 3 if(by == 0) 4 if (env) 5 longjmp(env, 1); 6 else 7 return 0; 8 else 9 return to/by; 10 } 11 12 void f() 13 { 1

setjmp()/longjmp()的使用方法

setjmp和longjmp.为了让你实现复杂的流控制,程序在系统里面运行完全依靠内存(代码段,全局段,堆存储器,栈存储器)和寄存器的内容(栈指针,基地址,计数器),setjmp保存当前的寄存器里面的内容,longjmp是恢复这些内容.longjmp返回setjmp程序当前的状态 先看一个例子: #include <csetjmp> #include <cstdio> #include <windows.h> int main() { jmp_buf env; int

C语言中setjmp与longjmp学习笔记

一.基础介绍 ?? ?头文件:#include<setjmp.h> ?? ?原型:??int?setjmp(jmp_buf envbuf) ?? ?宏函数setjmp()在缓冲区envbuf中保存系统堆栈里的内容,供longjmp()以后使用.首次调用setjmp()宏时,返回值为0,然而longjmp()把一个变原传递给setjmp(),该值(恒不为0)就是调用longjmp()后出现的setjmp()的值. void longjmp(jmp_buf envbuf,int status);

setjmp和longjmp

此文是学习 C专家编程 中的笔记. setjmp和longjmp是C语言所独有的,它们部分弥补了C语言有限的转移能力. 函数说明(来自wiki百科): int setjmp(jmp_buf env) 建立本地的jmp_buf缓冲区并且初始化,用于将来跳转回此处.这个子程序保存程序的调用环境于env参数所指的缓冲区,env将被longjmp使用.如果是从setjmp直接调用返回,setjmp返回值为0.如果是从longjmp恢复的程序调用环境返回,setjmp返回非零值. void longjmp

【C语言天天练(五)】setjmp和longjmp

setjmp和longjmp组合可以实现跳转,与goto语句有相似的地方.但有以下不同: 1.用longjmp只能跳回到曾经到过的地方.在执行setjmp的地方仍留有一个过程活动记录.从这个角度看,longjmp更像是"从何处来"而不是"往何处去".longjmp接收一个额外的整型参数并返回它的值,这可以知道是由longjmp转移到这里的还是从上一条语句执行后自然而然来到这里的. 2.goto语句不能跳出C语言当前的函数,而longjmp可以跳的更远,可以跳出函数,

C语言中利用setjmp和longjmp做异常处理

错误处理是任何语言都需要解决的问题,只有不能保证100%的正确运行,就需要有处理错误的机制.异常处理就是其中的一种错误处理方式. 1 过程活动记录(Active Record) C语言中每当有一个函数调用时,就会在堆栈(Stack)上准备一个被称为AR的结构,抛开具体编译器实现细节的不同,这个AR基本结构如下所示. 每当遇到一次函数调用的语句,C编译器都会产生出汇编代码来在堆栈上分配这个AR.例如下面的C代码: void a(int i) { if(i==0){ i = 1; } else {