setjmp 和 longjmp
在 Unix/Linux 環境下 C 語言標準函數庫 setjmp.h 提供了兩個函數 setjmp() 和 longjmp() 用以實現非本地跳轉(non-local jump)。這兩個函數可以用來構建複雜的流程控制,比如異常處理,多任務調度等。
想要理解 setjmp() 和 longjmp() 的工作原理,必須透過現象看本質,理解計算機的底層工作機制。在計算機系統中,一個程序的狀態是完全由它的內存和寄存器值決定的。
內存信息包括:
- 代碼( code )
- 全局變量( global variable, Variable globale(fr),globale Variable(de) )
- 堆( heap, le tas(fr), der Haufen(de) )
- 棧( stack, la pile d'exécution(fr), Aufrufstapel(de) )
寄存器信息有:
- 棧指針( stack pointer, sp )
- 幀指針( frame pointer, fp )
- 程序計數器( program counter - pc )/指令指针( instruction pointer - ip )
setjmp() 的任務只不過就是將寄存器的信息保存起來,然後 longjmp() 可以之後將這些值恢復回來。這樣 longjmp() 就能幫助程序返回到調用了 setjmp() 的那個時刻的寄存器和內存狀態。
1 setjmp() 和 longjmp() 的定義和實現
1.1 setjmp() 的定義:
#include < setjmp.h > int setjmp(jmp_buf env);
這個函數將當前程序寄存器的狀態保存到 env 中。在 setjmp.h 中定義了 jmp_buf 如下:
typedef struct { unsigned long eax; unsigned long ebx; unsigned long ecx; unsigned long edx; unsigned long esi; unsigned long edi; unsigned long ebp; unsigned long esp; unsigned long eip; } jmp_buf[1];
這個詭異的類型聲明是定義了一個叫 jmp_buf 的類型,它是一個長度為 9 的 unsigned long 32 位整型數組類型1。這 9 個整型數值對應了 9 個處理器寄存器:
- 通用寄存器
eax, ebx, ecx, edx - 索引指針寄存器
esi, edi, ebp, esp - 以及程序計數器(program counter)又叫指令指針(instruction pointer)
eip
這些寄存器完全決定了一個程序的運行行為。
當你調用 setjmp() ,時,傳遞一個指向 env 的指針,讓程序把當前寄存器的值保存到 env 中,并返回 0。
1.2 longjmp() 的定義
longjmp(jmp_buf env, int val);
longjmp() 函數把 env 中的值恢復到 CPU 的寄存器中。
注意, longjmp() 函數調用是不會再返回了的。因為它讓程序恢復到上一次調用 setjmp() 時候的狀態,就好像那之後啥也沒發生過一樣。很有一種浮生若夢的感覺吧?
一旦調用了 longjmp() , eip 的值也恢復原樣了。唯一不同的是, setjmp() 的返回值被 longjmp() 修改成傳遞給 longjmp() 的參數 val 了。
不同的返回值可以用來判斷 setjmp() 是第一次被調用還是調用了 longjmp() 之後返回。
即非零的返回值意味著程序是從 longjmp() 返回。因此,需要注意,传递给 long_jmp() 的参数不可以是 0。當然好的庫實現在調用最後會判斷 val 是否為 0,如果是 0 會強制被設成 12。
2 setjmp() 和 longjmp() 應用舉例
我们来看看最简单的使用场景:
/* setjmp_01.c */ #include <stdio.h> #include <stdlib.h> #include <setjmp.h> void main() { jmp_buf env; int i; i = setjmp(env); printf("i = %d\n", i); if (i != 0) exit(0); longjmp(env, 2); printf("Printed or not?\n"); }
上面這段函數的運行結果如下:
kimim@Mars ~/code$ ./setjmp_01 i = 0 i = 2
第一次調用 sejmp() 的時候,這個函數返回 0 ,當我們調用了 longjmp() 之後, setjmp() 的返回值被改成了 2。緊接著判斷發現 if (i != 0) 成立,於是程序退出。最後一句話也不會被打印出來了。
3 寄存器內存分析
現在,我們知道 setjmp() 會把所有寄存器的值保存到 env 中,包括 sp 和 fp 。那麼當某個函數調用了 setjmp() 之後返回了,就意味著,保存在 env 中的寄存器的值都不再有效了。
因為 env 中保存了調用 setjmp() 的函數的 sp 和 fp ,如果這個函數返回退出了,程序棧的狀態和之前調用 setjmp() 時候是不一致的,這時候又從 env 中把 sp 和 fp 恢復回來,程序就會運行出錯,比如 setjmp_02.c 程序:
/* setjmp_02.c */ #include <stdio.h> #include <stdlib.h> #include <setjmp.h> void func_a(jmp_buf env) { int i = 0; char *s = "Kimim"; printf("Entering func_a\n"); i = setjmp(env); printf("Return from setjmp = %d\n", i); printf("s = %s\n", s); printf("Leaving func_a\n"); return; } int func_b(jmp_buf env) { int i = 1; int k = 2; printf("Entering func_b\n"); longjmp(env, i); printf("Leaving func_b\n"); } void main() { jmp_buf env; func_a(env); func_b(env); }
运行上面程序,输出结果如下:
kimim@Mars ~/code$ ./setjmp_02 Entering func_a Return from setjmp = 0 s = Kimim Leaving func_a Entering func_b Return from setjmp = 1 Segmentation fault (core dumped)
當我們調用了 longjmp() 之後,又一次進入了 func_a ,打印了 "Return from setjmp = 1" 之後,程序就出錯了(段錯誤 - segmentation fault)。
分析一下程序運行時候棧的狀態看看發生了什麽情況。當我們第一次調用 main 函數的時候,棧內存分佈如下:
|----------------| | | | | | | | | | | | | <--------- sp | env[0] | | env[1] | | env[2] | pc = main | env[3] | | .... | | env[8] | | .... | | 棧基址 | <--------- fp |--------------- |
然後, main 調用了 func_a 。CPU 首先把傳給 func_a 的參數壓入程序棧,然後調用 jsr ,把返回的 pc 值, main 的 fp 壓入棧。然後 fp ,=sp= 一起更改為 func_a() 留出一個空的棧:
|----------------|
| |
| | <--------- sp, fp
/------------- | main 里舊的 fp |
| | main 里舊的 pc |
| /--- | env 的指針 |
| \--> | env[0] |
| | env[1] |
| | env[2] | pc = func_a
| | env[3] |
| | .... |
| | env[8] |
| | .... |
\------------> | 棧基址 |
|--------------- |
func_a 首先給局部變量 i 和 s 分配空間:
|----------------|
| | <--------- sp
"Kimim"<---- | s = "Kimim" |
| i | <--------- fp
/------------- | main 里舊的 fp |
| | main 里舊的 pc |
| /--- | env 的指針 |
| \--> | env[0] |
| | env[1] |
| | env[2] | pc = a
| | env[3] |
| | .... |
| | env[8] |
| | .... |
\------------> | 棧基址 |
|--------------- |
然後 func_a 打印出:
Entering func_a
緊接著調用 setjmp() 把當前寄存器的狀態保存到 env 數組中。這些寄存器包括 sp , fp , pc 。接著打印出:
Return from setjmp = 0 s = Kimim Leaving func_a
然後函數返回到 main() 。這時候的程序棧恢復成之前的樣子,除了 env 中保存了調用 func_a 時候的機器狀態:
|----------------| | | | | | | | | | | | | <----------- sp | env[0] | | env[1] | | env[2] | pc = main | env[3] | | .... | | env[8] | | .... | | 棧基址 | <--------- fp |--------------- |
然後調用 func_b() ,這時候棧狀態如下:
|----------------|
| |
| | <--------- sp, fp
/------------- | main 里舊的 fp |
| | main 里舊的 pc |
| /--- | env 指針 |
| \--> | env[0] |
| | env[1] |
| | env[2] | pc = func_b
| | env[3] |
| | .... |
| | env[8] |
| | .... |
\------------> | 棧基址 |
|--------------- |
func_b() 首先給兩個局部變量分配空間,然後打印
Entering func_b
這時候程序棧狀態如下:
|----------------|
| | <--------- sp
| k = 2 |
| i = 1 | <--------- fp
/------------- | main 里舊的 fp |
| | main 里舊的 pc |
| /--- | env 指針 |
| \--> | env[0] |
| | env[1] |
| | env[2] | pc = func_b
| | env[3] |
| | .... |
| | env[8] |
| | .... |
\------------> | 棧基址 |
|--------------- |
接著調用 longjmp() 。寄存器的值恢復到在 func_a 中調用 setjmp() 時候的值, pc 的值也恢復到 func_a 。可是這個時候程序棧還是調用 func_a 時候的狀態:
|----------------|
| | <--------- sp
| k = 2 |
| i = 1 | <--------- fp
/------------- | main 里舊的 fp |
| | main 里舊的 pc |
| /--- | env 的指針 |
| \--> | env[0] |
| | env[1] |
| | env[2] | pc = a
| | env[3] |
| | .... |
| | env[8] |
| | .... |
\------------> | 棧基址 |
|--------------- |
此時, setjmp() 的返回值是 1,所以第一句打印出來為:
Return from setjmp = 1
接著 func_a 中的打印語句試圖在 k 的值 2 所指向的內存地址尋找一個字符串來打印:
printf("s = %s\n", s);
這就導致了段錯誤(segmentation fault),因為這個進程是不允許訪問該內存地址的。3
以上是使用 setjmp() 和 longjmp() 的時候最常遇到的 bug。所以切記: 千萬不要從調用 setjmp() 的函數中返回 。
本文主要翻譯自田納西大學 James S. Plank 的講稿 “CS360 Lecture notes – Setjmp”4,并做了不少擴展和修改。
Footnotes:
如果给 longjmp() 第二个参数传了 0,那么 setjmp() 的返回值默认为 1。
http://en.wikipedia.org/wiki/Segmentation_fault Different operating systems have different signal names to indicate that a segmentation fault has occurred. On Unix-like operating systems, a signal called SIGSEGV (abbreviated from segmentation violation) is sent to the offending process. On Microsoft Windows, the offending process receives a STATUSACCESSVIOLATION exception.