setjmp是怎么工作的

一个例子

C语言没有C++Java的异常机制,但可以通过setjmp/longjmp实现类似的效果:

下面是一个简单的例子:

#include <stdio.h>

#include <stdlib.h>

#include <setjmp.h>

 

jmp_buf env;

 

int my_func(int a, int b) {

    if (b == 0) {

        printf("do not allow division by 0\n");

        longjmp(env, 1);

    }

    return a / b;

}

 

int main(int argc, char const *argv[]) {

    int res = setjmp(env);

    if (res == 0) {

        printf("return from setjmp\n");

        my_func(10, 0);

    } else {

        printf("return from longjmp: %d\n", res);

    }

    return 0;

}

输出结果为:

return from setjmp

do not allow division by 0

return from longjmp: 1

这个执行流程有点违背常规的逻辑,看起来有点像跨函数的goto语句,我们不禁要问它是怎么做到的?本质上就是怎么保存setjmp处的上下文环境,以及如何在longjmp处恢复它?

背景知识

在了解setjmp的内部机理前,需要先知道一点背景知识。

首先什么是执行环境,简单地说,就是CPU中的一些寄存器,这些寄存器保存了程序执行的必要信息,以x86为例:

函数的调用约定,对寄存器也有影响,以x86cdecl(这是C语言函数的调用约定)为例:

关于调用约定更详细的信息,你需要阅读维基百科x86 calling conventions,维基百科真是一个伟大的网站:)

最后再说一下汇编中call指令的含义,它被分解成两条语句:把下一条指令的地址压栈,然后跳到函数的入口地址;函数调用完之后,从栈中把指令地址恢复,这样就能正常的返回到函数调用的下一条指令处。

以上背景知识以x86为例,x64类似,至于其他的指令集如ARM,本人没有接触过,不过相信原理上是差不多的。

setjmp的实现

要实现执行环境的保存,只需要按上面的背景知识,把相关的寄存器保存到jmp_buf中,这就是setjmp要做的事情;然后在longjmp里,从jmp_buf恢复寄存器的值,恢复之后,执行点就回到setjmp返回的地方。

以下的源代码取自musl libc

setjmp的代码是这样的:

setjmp:

1   mov    4(%esp), %eax    // eax = jmp_buf

2   mov    %ebx, (%eax)     // jmp_buf[0] = ebx

3   mov    %esi, 4(%eax)    // jmp_buf[1] = esi

4   mov    %edi, 8(%eax)    // jmp_buf[2] = edi

5   mov    %ebp, 12(%eax)   // jmp_buf[3] = ebp

6   lea    4(%esp), %ecx

7   mov    %ecx, 16(%eax)   // jmp_buf[4] = esp+4

8   mov    (%esp), %ecx    

9   mov    %ecx, 20(%eax)   // jmp_buf[5] = *esp

0   xor    %eax, %eax       // eax = 0

10  ret                     // return eax

-----------------------high

  参数:jmp_buf

-----------------------

  调用者下一条指令的地址   <-- esp

-----------------------low

-----------------------high

  jmp_buf参数           <-- esp

-----------------------low

setjmp完成后,jmp_buf保存了这些信息:

[ebx, esi, edi, ebp, esp, eip]

longjmp的实现

现在看看longjmp怎么处理:

longjmp:

1     mov  4(%esp),%edx // edx = jmp_buf

2     mov  8(%esp),%eax // eax = val

3     test    %eax,%eax // val == 0?

4     jnz 1f

5     inc     %eax      //  eax++

6  1:

7     mov   (%edx),%ebx // ebx = jmp_buf[0]

8     mov  4(%edx),%esi // esi = jmp_buf[1]

9     mov  8(%edx),%edi // edi = jmp_buf[2]

10    mov 12(%edx),%ebp // ebp = jmp_buf[3]

11    mov 16(%edx),%ecx // ecx = jmp_buf[4]

12    mov     %ecx,%esp // esp = ecx

13    mov 20(%edx),%ecx // ecx = jmp_buf[5]

14    jmp *%ecx         // eip = ecx

longjmp需要传递两个参数,所以调用longjmp时的栈信息是这样的:

-----------------------high

  参数2:整型值

-----------------------

  参数1jmp_buf

-----------------------

  调用者下一条指令的地址   <-- esp

-----------------------low

在第14行执行后,执行流程就跳到setjmp的返回处,并且因为eax被设置成了val,所以返回值也变成val

后记

我们大概明白setjmp/longjmp的作用是保存执行环境,并且恢复执行环境。那么这个特性除了模拟异常处理,还可以用来实现什么功能呢?后面也许会有相关的文章,敬请期待。