longjmp是如何工作的?

7

我需要了解longjmp函数的工作原理;我知道它的功能,但我需要知道它是如何实现的。

我尝试在gdb中反汇编代码,但我无法理解其中的一些步骤。代码如下:

0xb7ead420 <siglongjmp+0>:      push   %ebp
0xb7ead421 <siglongjmp+1>:      mov    %esp,%ebp
0xb7ead423 <siglongjmp+3>:      sub    $0x18,%esp
0xb7ead426 <siglongjmp+6>:      mov    %ebx,-0xc(%ebp)
0xb7ead429 <siglongjmp+9>:      call   0xb7e9828f <_Unwind_Find_FDE@plt+119>
0xb7ead42e <siglongjmp+14>:     add    $0x12bbc6,%ebx
0xb7ead434 <siglongjmp+20>:     mov    %esi,-0x8(%ebp)
0xb7ead437 <siglongjmp+23>:     mov    0xc(%ebp),%esi
0xb7ead43a <siglongjmp+26>:     mov    %edi,-0x4(%ebp)
0xb7ead43d <siglongjmp+29>:     mov    0x8(%ebp),%edi
0xb7ead440 <siglongjmp+32>:     mov    %esi,0x4(%esp)
0xb7ead444 <siglongjmp+36>:     mov    %edi,(%esp)
0xb7ead447 <siglongjmp+39>:     call   0xb7ead4d0
0xb7ead44c <siglongjmp+44>:     mov    0x18(%edi),%eax
0xb7ead44f <siglongjmp+47>:     test   %eax,%eax
0xb7ead451 <siglongjmp+49>:     jne    0xb7ead470 <siglongjmp+80>
0xb7ead453 <siglongjmp+51>:     test   %esi,%esi
0xb7ead455 <siglongjmp+53>:     mov    $0x1,%eax
0xb7ead45a <siglongjmp+58>:     cmove  %eax,%esi
0xb7ead45d <siglongjmp+61>:     mov    %esi,0x4(%esp)
0xb7ead461 <siglongjmp+65>:     mov    %edi,(%esp)
0xb7ead464 <siglongjmp+68>:     call   0xb7ead490
0xb7ead469 <siglongjmp+73>:     lea    0x0(%esi,%eiz,1),%esi
0xb7ead470 <siglongjmp+80>:     lea    0x1c(%edi),%eax
0xb7ead473 <siglongjmp+83>:     movl   $0x0,0x8(%esp)
0xb7ead47b <siglongjmp+91>:     mov    %eax,0x4(%esp)
0xb7ead47f <siglongjmp+95>:     movl   $0x2,(%esp)
0xb7ead486 <siglongjmp+102>:    call   0xb7ead890 <sigprocmask>
0xb7ead48b <siglongjmp+107>:    jmp    0xb7ead453 <siglongjmp+51>

有人能否简要地解释一下这段代码,或者指示我在系统中可以找到源代码的位置?

2
你应该查看 longjmp 的源代码,而不是 siglongjmp。后者很可能是编译的 C 代码,用于恢复信号掩码,然后调用或跳转到实际的 longjmp 汇编代码。 - R.. GitHub STOP HELPING ICE
6个回答

6

这里是标准i386 ABI下的longjmp代码,没有任何与C++、异常、清理函数、信号掩码等交互的疯狂扩展:

    mov 4(%esp),%edx
    mov 8(%esp),%eax
    test %eax,%eax
    jnz 1f
    inc %eax
1:
    mov (%edx),%ebx
    mov 4(%edx),%esi
    mov 8(%edx),%edi
    mov 12(%edx),%ebp
    mov 16(%edx),%ecx
    mov %ecx,%esp
    mov 20(%edx),%ecx
    jmp *%ecx

@R:指的是哪个i386代码?那个与Unix V6中使用的实现非常相似,存在各种问题,例如不清理。gcc的i386代码表现得更好。请参见http://glibc.sourcearchive.com/documentation/2.7-18lenny7/setjmp_2longjmp_8c_source.html上的源代码。 - wallyk
8
这不是gcc的代码,而是glibc的代码。在C语言中没有“cleanup”的概念。longjmp函数被规定为一个纯跳转。如果你跳过需要清理的任何内容,那么你的程序最好情况下会有资源泄漏,最坏情况下会引起未定义的行为。这就是C语言。如果您不喜欢它,请不要使用longjmp。不要坚持要求根据其他语言更改longjmp以满足您的要求。 - R.. GitHub STOP HELPING ICE

4
大多数情况下,它会恢复寄存器和堆栈,就像对应的setjmp()调用时一样。还需要进行一些额外的清理工作(修复信号处理和解除挂起的堆栈处理程序),以及返回一个不同的值作为setjmp的表面返回值,但恢复状态是操作的本质。
为了使其正常工作,堆栈不能低于调用setjmp的点。longjmp是一种残暴的方式,可以忘记从它下面到调用堆栈中相同级别的所有内容(或函数调用嵌套序列),主要是通过将堆栈指针设置为调用setjmp时的同一帧来实现。
为了使其正常工作,longjmp()会调用所有中间函数的退出处理程序,以便它们可以删除变量和执行其他通常在函数返回时完成的清理工作。将堆栈重置到较浅的点会释放所有auto变量,但如果其中一个是FILE *,则还需要关闭文件并释放i/o缓冲区。

谢谢您的回答。这就是它的功能...但我需要详细了解它是如何实现的...我的意思是,它不仅仅是将保存的寄存器复制到实际寄存器中,而是以一种不同的方式实现的。 - Aslan986
6
C语言中的longjmp函数不会调用不存在于C语言中的“中间函数”的“退出处理程序”。这个答案错误地暗示了在这些所谓的“中间函数”中分配的资源将被释放,这在C语言中是完全不可能的。 - R.. GitHub STOP HELPING ICE
3
如果跳过任何析构函数等,它们的行为将变得未定义。 - R.. GitHub STOP HELPING ICE
3
在标准行为中,longjmp 不包括取消堆栈帧。这是一种有害扩展,会使本应是 O(1) 的操作变成 O(n)。只有在平台将其调用堆栈实现为复杂结构而不是简单地增加和减少堆栈指针时,才需要取消堆栈帧。 - R.. GitHub STOP HELPING ICE
3
另外,我认为调用析构函数可能会限制 longjmp() 的使用价值,例如,如果您将其用作贫穷版本的 setcontext() 的构建块(如果这样做是可能的话,我还没有仔细考虑过)。 - ninjalj
显示剩余3条评论

3

我认为你需要查看 过程激活记录调用栈 以及 Setjmp.h 中的 jmp_buf 结构。

引自《专家 C 程序设计:深度 C 语言秘籍》:

setjmp函数保存程序计数器和当前栈顶指针的副本。如果您喜欢,这将保存一些初始值。然后,longjmp函数恢复这些值,有效地转移控制并将状态重置回保存时的位置。它被称为“解开堆栈”,因为您要从堆栈中展开激活记录,直到达到保存的记录。

在此处还可以查看第153页。

堆栈帧将高度依赖于机器和可执行文件,但是其思想是相同的。


除了它不会逐帧展开堆栈,它只是将堆栈指针设置为保存的值。它不会像异常处理机制那样在退出时运行析构函数。(因此,如果这是一个问题,请不要在C++中使用它。) - Peter Cordes

1
在 Windows X64 MASM 中。
.code

my_jmp_buf STRUCT

        _Frame QWORD ?;
        _Rbx QWORD ?;
        _Rsp QWORD ?;
        _Rbp QWORD ?;
        _Rsi QWORD ?;
        _Rdi QWORD ?;
        _R12 QWORD ?;
        _R13 QWORD ?;
        _R14 QWORD ?;
        _R15 QWORD ?;
        _Rip QWORD ?;
        _MxCsr DWORD ?;
        _FpCsr WORD ?;
        _Spare WORD ?;
        _Xmm6 XMMWORD ?;
        _Xmm7 XMMWORD ?;
        _Xmm8 XMMWORD ?;
        _Xmm9 XMMWORD ?;
        _Xmm10 XMMWORD ?;
        _Xmm11 XMMWORD ?;
        _Xmm12 XMMWORD ?;
        _Xmm13 XMMWORD ?;
        _Xmm14 XMMWORD ?;
        _Xmm15 XMMWORD ?;

my_jmp_buf ENDS


;extern "C" int my_setjmp(jmp_buf env);
public my_setjmp

my_setjmp PROC

    mov rax, [rsp] ;save ip 
    mov (my_jmp_buf ptr[rcx])._Rip, rax

    lea rax, [rsp + 8] ;save sp before call this function
    mov (my_jmp_buf ptr[rcx])._Rsp, rax
    mov (my_jmp_buf ptr[rcx])._Frame, rax

    ;save gprs
    mov (my_jmp_buf ptr[rcx])._Rbx,rbx  
    mov (my_jmp_buf ptr[rcx])._Rbp,rbp  
    mov (my_jmp_buf ptr[rcx])._Rsi,rsi  
    mov (my_jmp_buf ptr[rcx])._Rdi,rdi  
    mov (my_jmp_buf ptr[rcx])._R12,r12  
    mov (my_jmp_buf ptr[rcx])._R13,r13  
    mov (my_jmp_buf ptr[rcx])._R14,r14  
    mov (my_jmp_buf ptr[rcx])._R15,r15  

    ;save fp and xmm
    stmxcsr     (my_jmp_buf ptr[rcx])._MxCsr
    fnstcw      (my_jmp_buf ptr[rcx])._FpCsr
    movdqa      (my_jmp_buf ptr[rcx])._Xmm6,xmm6  
    movdqa      (my_jmp_buf ptr[rcx])._Xmm7,xmm7  
    movdqa      (my_jmp_buf ptr[rcx])._Xmm8,xmm8  
    movdqa      (my_jmp_buf ptr[rcx])._Xmm9,xmm9  
    movdqa      (my_jmp_buf ptr[rcx])._Xmm10,xmm10
    movdqa      (my_jmp_buf ptr[rcx])._Xmm11,xmm11
    movdqa      (my_jmp_buf ptr[rcx])._Xmm12,xmm12
    movdqa      (my_jmp_buf ptr[rcx])._Xmm13,xmm13
    movdqa      (my_jmp_buf ptr[rcx])._Xmm14,xmm14
    movdqa      (my_jmp_buf ptr[rcx])._Xmm15,xmm15

    xor         rax,rax  
    ret  

my_setjmp ENDP


;extern "C" void my_longjmp(jmp_buf env,  int value);

public my_longjmp

my_longjmp PROC

    ;restore fp and xmm
    movdqa      xmm15,(my_jmp_buf ptr[rcx])._Xmm15
    movdqa      xmm14,(my_jmp_buf ptr[rcx])._Xmm14
    movdqa      xmm13,(my_jmp_buf ptr[rcx])._Xmm13
    movdqa      xmm12,(my_jmp_buf ptr[rcx])._Xmm12
    movdqa      xmm11,(my_jmp_buf ptr[rcx])._Xmm11
    movdqa      xmm10,(my_jmp_buf ptr[rcx])._Xmm10
    movdqa      xmm9,(my_jmp_buf ptr[rcx])._Xmm9
    movdqa      xmm8,(my_jmp_buf ptr[rcx])._Xmm8
    movdqa      xmm7,(my_jmp_buf ptr[rcx])._Xmm7
    movdqa      xmm6,(my_jmp_buf ptr[rcx])._Xmm6

    fldcw      (my_jmp_buf ptr[rcx])._FpCsr
    ldmxcsr    (my_jmp_buf ptr[rcx])._MxCsr


    ;restore gprs
    mov r15, (my_jmp_buf ptr[rcx])._R15 
    mov r14, (my_jmp_buf ptr[rcx])._R14
    mov r13, (my_jmp_buf ptr[rcx])._R13  
    mov r12, (my_jmp_buf ptr[rcx])._R12  
    mov rdi, (my_jmp_buf ptr[rcx])._Rdi  
    mov rsi, (my_jmp_buf ptr[rcx])._Rsi  
    mov rbp, (my_jmp_buf ptr[rcx])._Rbp  
    mov rbx, (my_jmp_buf ptr[rcx])._Rbx



    ;retore sp
    mov rsp, (my_jmp_buf ptr[rcx])._Rsp

    ;restore ip
    mov rcx, (my_jmp_buf ptr[rcx])._Rip; must be the last instruction as rcx modified

    ;return value
    mov rax, rdx 

   jmp rcx
my_longjmp ENDP

END

1
一些解释“为什么”的文本会很有用:这些是需要恢复的保留调用寄存器,以使setjmp在从该函数的子级调用longjmp时看起来像第二次返回。 (我认为您不需要保存/恢复MXCSR或FP状态寄存器;这取决于您希望在setjmp和longjmp之间进行舍入模式更改的语义) - Peter Cordes

0

这里是我为一个小型clib子集编写和测试的setmp和longjmp版本(使用Visual Studio 2008编写和测试)。汇编代码存储在单独的.asm文件中。

.586
.MODEL FLAT, C  ; Flat memory model, C calling conventions.
;.STACK         ; Not required for this example.
;.DATA          ; Not required for this example.
.code


; Simple version of setjmp (x86-32 bit).
;
; Saves ebp, ebx, edi, esi, esp and eip in that order.
; 
setjmp_t proc
    push ebp
    mov ebp, esp
    push edi

    mov edi, [ebp+8]    ; Pointer to jmpbuf struct.

    mov eax, [ebp]      ; Save ebp, note we are saving the stored version on the stack.
    mov [edi], eax

    mov [edi+4], ebx    ; Save ebx

    mov eax, [ebp-4]
    mov [edi+8], eax    ; Save edi, note we are saving the stored verion on the stack.

    mov [edi+12], esi   ; Save esi 

    mov eax, ebp;
    add eax, 8
    mov [edi+16], eax   ; Save sp, note saving sp pointing to last item on stack just before call to setjmp.

    mov eax, [ebp+4]
    mov [edi+20], eax   ; Save return address (will be used as jump address in longjmp().

    xor eax, eax        ; return 0;

    pop edi
    pop ebp
    ret
setjmp_t endp


; Simple version of longjmp (x86-32 bit).
;
; Restores ebp, ebx, edi, esi, esp and eip.
; 
longjmp_t proc
    mov edi, [esp+4]    ; Pointer to jmpbuf struct.
    mov eax, [esp+8]    ; Get return value (value passed to longjmp).

    mov ebp, [edi]      ; Restore ebp.
    mov ebx, [edi+4]    ; Restore ebx.
    mov esi, [edi+12]   ; Restore esi.
    mov esp, [edi+16]   ; Restore stack pointer.
    mov ecx, [edi+20]   ; Original return address to setjmp. 

    mov edi, [edi+8]    ; Restore edi, note, done last as we were using edi up to this point.
    jmp ecx             ; Wing and a prayer...
longjmp_t endp

end

一个用于测试的 C 代码片段:

extern "C" int setjmp_t( int *p);
extern "C" int longjmp_t( int *p, int n);
jmp_buf test2_buff;

void DoTest2()
{
    int x;

    x = setjmp_t( test2_buff);
    printf( "setjmp_t return - %d\n", x);

    switch (x)
    {
        case 0:
            printf( "About to do long jump...\n");
            longjmp_t( test2_buff, 99);
            break;
        default:
            printf( "Here becauuse of long jump...\n");
            break;
    }

    printf( "Test2 passed!\n");
}

请注意,我在缓冲区中使用了“setjmp.h”的声明,但如果您愿意,可以使用一个整数数组(最少6个整数)。

你可以在函数中使用 EAX、ECX 和 EDX 寄存器,这样会比只使用 EAX 和需要保存的寄存器(如 EDI)更简单。 - Peter Cordes

0

你需要向 setjmp() 函数传递一个缓冲区参数。它会将当前的寄存器信息等存储到该缓冲区中。而 longjmp() 函数的调用则会从缓冲区中恢复这些值。此外,wallyk 所说的也是如此。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接