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

输出:
root@ubuntu:~/vm_disk_dpdk/study/apue# ./a.out      
Main: first time return from setjmp, ret = 0
Enter 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:
通过如下命令,得到预处理后的文件,以便取得相应的结构:
root@ubuntu:~/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 <_setjmp@plt> //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.S
ENTRY (__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.

posted on 2015-12-24 00:02  marvin.li  阅读(2334)  评论(0编辑  收藏  举报

导航