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为例:
函数的调用约定,对寄存器也有影响,以x86的cdecl(这是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:整型值
-----------------------
参数1:jmp_buf
-----------------------
调用者下一条指令的地址 <-- esp
-----------------------low
在第14行执行后,执行流程就跳到setjmp的返回处,并且因为eax被设置成了val,所以返回值也变成val。
后记
我们大概明白setjmp/longjmp的作用是保存执行环境,并且恢复执行环境。那么这个特性除了模拟异常处理,还可以用来实现什么功能呢?后面也许会有相关的文章,敬请期待。