非本地跳轉(unlocal jump)是與本地跳轉相對應的一個概念。
本地跳轉主要指的是類似于goto語句的一系列應用,當設置了標志之后,可以跳到所在函數內部的標號上。然而,本地跳轉不能將控制權轉移到所在程序的任意地點,不能跨越函數,因此也就有了非本地跳轉。
C語言里面提供了setjmp和longjmp函數來進行跨越函數之間的控制權的跳轉,從而稱之為非本地跳轉。
#include <setjmp.h>
int setjmp(jmp_buf env);
該函數主要用來保存當前執行狀態,作為后續跳轉的目標。調用時,當前狀態會被存放在env
指向的結構中,env
將被 long_jmp 操作作為參數,以返回調用點,跳轉的結果看起來就好像剛從setjmp返回一樣。
需要注意的是,第一次調用setjmp的時候返回值為0;而從long_jmp操作返回時,返回值是非0的,數值與longjmp傳入的參數value有關。通過判斷setjmp的返回值,就可以判斷當前執行狀態。
#include<setjmp.h> void long_jmp(jmp_buf env, int value);
該函數用來恢復env
中保存的執行狀態,另一參數value
用來傳遞返回值給跳轉目標。如果value
值為0,則跳轉后返回setjmp處的值為1;否則,返回setjmp處的值為value
。
因此,整個非本地跳轉的執行過程是:首先,在程序中調用setjmp進行當前運行棧環境的保存,接著在程序的其它地方調用longjmp進行跳轉,跳轉回的位置就是該setjmp的位置。
這其中需要注意的是:jmp_buf類型的變量env,因為該變量保存的是當前執行位置的運行棧環境;因此如果需要還能跳轉到這個位置,那么longjmp必須能調用到這個env變量,因此這個變量一般為全局的。
接下來,簡單的介紹一下運行棧的概念。 ?
運行棧
簡單的來說,程序運行的時候如果一個函數Fa內部調用了另一個函數Fb,那么當Fb執行完了之后,控制權如何返回給Fa,并繼續執行Fa后面的語句呢?這就使用到了運行棧。
簡要的說,運行棧里按照函數調用的順序將一個個函數壓入棧中,當一個函數執行結束之后,這個函數的棧幀(stack frame)就會從運行棧中pop出來,并將控制權(PC)轉向調用函數(callee),控制權的記錄就在每個函數所對應的的棧幀中。
因此,如果我們有程序段如下:
void Fa() {...Fb();... } void Fb() {...Fc();... } void Fc() {...//do something here }void main() {...Fa(); }
那么,當調用到函數Fc時程序的運行棧大致如下圖所示:
當執行完Fc之后就會將Fc的棧幀pop出來,如下圖所示:
非本地跳轉模擬異常處理機制
當使用非本地跳轉時,就不需要一層層的解開程序調用棧,而是直接將控制流轉移到對應的位置。
在最初,還沒有實現異常處理機制的時候,有的時候會用非本地跳轉來模擬異常處理機制,大致思路如下:
#define myTry if(setjmp(env) == 0){#define myCatch(err) }else if(err != NULL){ \//TO DO SOMETHING HERE
\ }#define myThrow(err) longjmp(env, err.ToInteger())
在myTry處設置setjmp,并保存當前的程序運行棧到env中,當程序中某個函數throw出一個異常時,就使用longjmp進行跳轉。此時,程序又回到了setjmp處,但是因為返回值已經被設置為err.ToInteger(),因此setjmp的返回值肯定不是0,于是程序就進入到了else if分支,即myCatch語句塊中。
但是,使用非本地跳轉來模擬異常處理機制時會產生一定的問題,那就是在語句塊中定義的局部變量所占的內存并不會被正常的釋放,導致內存泄漏。
因為,非本地跳轉在發生跳轉時是直接將程序的控制流轉移過去,而不是進行正常的棧退解(stack unwinding)操作,因此并不能識別出其中的變量并進行析構,不過現在的一些編譯器已經實現了相應的功能,因編譯器而異。