非本地跳轉之setjmp與longjmp


非本地跳轉(unlocal jump)是與本地跳轉相對應的一個概念。

本地跳轉主要指的是類似於goto語句的一系列應用,當設置了標志之后,可以跳到所在函數內部的標號上。然而,本地跳轉不能將控制權轉移到所在程序的任意地點,不能跨越函數,因此也就有了非本地跳轉

C語言里面提供了setjmplongjmp函數來進行跨越函數之間的控制權的跳轉,從而稱之為非本地跳轉。

#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)操作,因此並不能識別出其中的變量並進行析構,不過現在的一些編譯器已經實現了相應的功能,因編譯器而異。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM