【转】浅析C语言的非局部跳转:setjmp和longjmp
轉自 http://www.cnblogs.com/lienhua34/archive/2012/04/22/2464859.html
C語言中有一個goto語句,其可以結合標號實現函數內部的任意跳轉(通常情況下,很多人都建議不要使用goto語句,因為采用goto語句后,代碼維護工作量加大)。另外,C語言標準中還提供一種非局部跳轉“no-local goto",其通過標準庫<setjmp.h>中的兩個標準函數setjmp和longjmp來實現。
C標準庫<setjmp.h>
下面是K&R的《C程序設計語言(第2版?.?新版)》第232頁給出的關于標準庫<setjmp.h>的說明。
8 非局部跳轉<setjmp.h>頭文件<setjmp.h>中的說明提供了一種避免通常的函數調用和返回順序的途徑,特別的,它允許立即從一個多層嵌套的函數調用中返回。8.1 setjmp#include <setjmp.h> int setjmp(jmp_buf env);setjmp()宏把當前狀態信息保存到env中,供以后longjmp()恢復狀態信息時使用。如果是直接調用setjmp(),那么返回值為0;如果是由于調用longjmp()而調用setjmp(),那么返回值非0。setjmp()只能在某些特定情況下調用,如在if語句、 switch語句及循環語句的條件測試部分以及一些簡單的關系表達式中。8.2 longjmp#include <setjmp.h> void longjmp(jmp_buf env, int val);longjmp()用于恢復由最近一次調用setjmp()時保存到env的狀態信息。當它執行完時,程序就象setjmp()剛剛執行完并返回非0值val那樣繼續執行。包含setjmp()宏調用的函數一定不能已經終止。所有可訪問的對象的值都與調用longjmp()時相同,唯一的例外是,那些調用setjmp()宏的函數中的非volatile自動變量如果在調用setjmp()后有了改變,那么就變成未定義的。jmp_buf是setjmp.h中定義的一個結構類型,其用于保存系統狀態信息。宏函數setjmp會將其所在的程序點的系統狀態信息保存到某個jmp_buf的結構變量env中,而調用函數longjmp會將宏函數setjmp保存在變量env中的系統狀態信息進行恢復,于是系統就會跳轉到setjmp()宏調用所在的程序點繼續進行。這樣setjmp/longjmp就實現了非局部跳轉的功能。
一個簡單的例子:
下面我們來看一個簡單的例子。
1 #include <stdio.h>2 #include <setjmp.h>3 4 jmp_buf jump_buffer;5 6 void func(void)7 {8 printf("Before calling longjmp\n");9 longjmp(jump_buffer, 1); 10 printf("After calling longjmp\n"); 11 } 12 void func1(void) 13 { 14 printf("Before calling func\n"); 15 func(); 16 printf("After calling func\n"); 17 } 18 int main() 19 { 20 if (setjmp(jump_buffer) == 0){ 21 printf("first calling set_jmp\n"); 22 func1(); 23 }else { 24 printf("second calling set_jmp\n"); 25 } 26 return 0; 27 }代碼的運行結果如下
lienhua34@lienhua34-laptop:~/program/test$ ./test first calling set_jmp Before calling func Before calling longjmp second calling set_jmp?通過上面這個簡單例子的運行結果可以看出。main函數運行的setjmp()宏調用,將當前程序點的系統狀態信息保存到全局變量jump_buffer中,然后返回結果0。于是,代碼打印出字符串"first calling set_jmp",然后調用函數func1()。在函數func1中,先打印字符串"Before calling func",然后去調用函數func()?,F在程序控制流轉到func函數中,函數func先打印字符串“Before calling longjmp",然后調用函數longjmp。這時候關鍵點到了!!!longjmp函數將main函數中setjmp()宏調用設置在全局變量jump_buffer中的系統狀態信息恢復到系統的相應寄存器中,導致程序的控制流跳轉到了main函數中setjmp()宏調用所在的程序點,此時相當于第二次進行setjmp()宏調用,并且此時的setjmp()宏調用的返回不再是0,而是傳遞給函數調用longjmp()的第二個參數1。于是程序控制流轉到main函數中if語句的else部分執行,打印字符串“second calling set_jmp“。最后,執行main函數中的語句“reture 0;”返回,程序運行結束退出。
?
從上面的運行過程,我們可以看出在longjmp()函數調用處的程序點嵌套在三層函數調用中:main, func1和func,但是longjmp()函數調用導致程序控制流跳過函數調用func和func1,直接回到main函數中setjmp()宏調用所在的程序點,然后執行main函數中后續的語句,從而忽略了函數func1和func中后續的語句部分。這就是非局部跳轉。
非局部跳轉的實現機制
C語言的運行控制模型,是一個基于棧結構的指令執行序列,表現出來就是call/return: call調用一個函數,然后return從一個函數返回。在這種運行控制模型中,每個函數調用都會對應著一個棧幀,其中保存了這個函數的參數、返回值地址、局部變量以及控制信息等內容。當調用一個函數時,系統會創建一個對應的棧幀壓入棧中,而從一個函數返回時,則系統會將該函數對應的棧幀從棧頂退出。正常的函數跳轉就是這樣從棧頂一個一個棧幀逐級地返回。
?
另外,系統內部有一些寄存器記錄著當前系統的狀態信息,其中包括當前棧頂位置、位于棧頂的棧幀位置以及其他一些系統信息(例如代碼段,數據段等等)。這些寄存器指示了當前程序運行點的系統狀態,可以稱為程序點。在宏函數setjmp中就是將這些系統寄存器的內容保存到jmp_buf類型變量env中,然后在函數longjmp中將函數setjmp保存在變量env中的系統狀態信息恢復,此時系統寄存器中指示的棧頂的棧幀就是調用宏函數setjmp時的棧頂的棧幀。于是,相當控制流跳過了中間的若干個函數調用對應的棧幀,到達setjmp所在那個函數的棧幀。這就是非局部跳轉的實現機制,其不同于上面所說的call/return跳轉機制。
?
正是因為這種實現機制,在上面的標準庫說明中提到:“包含setjmp()宏調用的函數一定不能終止”。如果該函數終止的話,該函數對應的棧幀也已經從系統棧中退出,于是setjmp()宏調用保存在env中的內容在longjmp函數恢復時,就不再是setjmp()宏調用所在程序點。此時,調用函數longjmp()就會出現不可預測的錯誤。?
非局部跳轉的運用
非局部跳轉通常被用于實現將程序控制流轉移到錯誤處理模塊中;或者是通過這種非正常的函數返回機制,返回到之前調用的函數中。
?
最近,在我的畢業設計,我也采用了這種非局部跳轉方式來實現錯誤處理機制。我的畢業設計是用C語言實現一個簡單的scheme解析器,在該求值器對某個表達式的求值過程中可能遇到某個錯誤,導致這個表達式無效。此時,需要跳轉到求值器的主循環開頭,重新讀取表達式,然后求值。于是,我的主循環框架就設計為:
while (1){if (setjmp(jump_buffer) == 0){/*讀取表達式求值表達式打印表達式的值*/}else {/* 進行錯誤處理,初始化求值環境 */} }其中,jump_buffer是一個jmp_buf類型的全局變量。循環開始時,if語句的條件判斷中,setjmp保存程序點信息到全局變量jump_buffer中,此時setjmp()宏調用返回值為0,然后開始讀取、求值表達式。當表達式求值遇到錯誤時,通過執行函數調用
longjmp(jump_buffer, 1);就可以跳轉到主循環的setjmp()宏調用所在程序點,而此時setjmp()宏調用的返回值為1,于是進入else部分進行錯誤處理,初始化求值環境。
轉載于:https://www.cnblogs.com/sshao/p/5445643.html
《新程序員》:云原生和全面數字化實踐50位技術專家共同創作,文字、視頻、音頻交互閱讀總結
以上是生活随笔為你收集整理的【转】浅析C语言的非局部跳转:setjmp和longjmp的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: redis 的使用 (sort set排
- 下一篇: HTML5全局属性和事件