運行時異常處理程序是如何實現的?

異常被公認不是「語法糖」


我曾經看過一篇博客,對我理解C++的異常這方面帶來了很深遠的影響,所以我當時就直接把這篇文章記錄在我的書籤裡面了。

如果題主能看到我這個回答的話,你也可以如我當初一樣,跟著這篇博客一路做下來,你會獲益無窮。C++ exception handling internals


JIT腳本引擎:關於X86上Windows 32位操作系統異常處理(SEH)的處理過程 - λ-calculus(驚愕到手了歐耶,GetBlogPostIds.aspx) - C++博客

JIT腳本引擎:使用彙編實現__try和__catch - λ-calculus(驚愕到手了歐耶,GetBlogPostIds.aspx) - C++博客

JIT腳本引擎:關於自己的異常處理函數在Release下失效的解決辦法 - λ-calculus(驚愕到手了歐耶,GetBlogPostIds.aspx) - C++博客

大四的時候自己看intel的開發人員手冊和各種古人寫的Windows NT系列開發文章,自己弄了個簡單的編譯器,把代碼編譯成二進位寫進函數指針里(可以自己編寫導入語句,然後跟宿主VC++程序裡面的函數和結構體直接交互),順便就搞了一下try-catch。題主把裡面的東西都看了,就明白了,特別是第一篇。

其他答案裡面凡是說setjmp/longjmp的你都不要聽,在catch和finally(主要指析構函數)有可能拋異常的情況下,異常鏈表需要被訪問兩遍,這是setjmp/longjmp搞不定的。


異常絕對不是語法糖,異常是一種特殊的 Continuation,是程序語言設計中非常核心的東西。


異常不是語法糖,異常牽扯到異常處理塊的搜索,棧回滾以及最後finally塊的執行,如果finally里拋異常了還會有更複雜的東西

以上是Java

C++的話更加複雜,異常還要保證應該析構的對象一定要析構

異常不是if else,如果你一定想用C語言糊一個出來,可以參考《C語言介面與實現》這本書


如果題主一定要用語法糖的方式理解try-catch,可以參考Rust裡面try!和Result。

簡單說就是把所有函數的返回值變成(return-type | error-type),然後把所有函數調用變成一個if is-error then return error else do-real-thing

當然真正支持try-catch的編譯器會用優化的方式,比如說一個try-catch鏈中當前exception只會有一個(如果多於一個就包成一個),所以可以專門開個地方給它,每thread一個就行,這樣一來可以把stack省出來給真正的代碼,而且可以兼容其它語言的abi


try-catch塊至少在Java里不是語法糖。

Java中,try-catch塊會被編碼在方法的異常表內。每個異常表項都會對應著異常處理器,並且存儲著這個異常處理器所處理的位元組碼的開始索引和結束索引。JVM運行時,在遇到異常時,會搜索對應的異常處理器進行處理。這顯然不只是一個語法糖了。

對於下面的這個簡單的類

public class T {
public static void main(String[] args) {
try {
System.out.println("Hello world!");
} catch(Throwable t) {
System.out.println("error!");
}
}
}

我們可以看到,它最後生成的位元組碼中

public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=2, args_size=1
0: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
3: ldc #3 // String Hello world!
5: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
8: goto 20
11: astore_1
12: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
15: ldc #6 // String error!
17: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
20: return
Exception table:
from to target type
0 8 11 Class java/lang/Throwable
LineNumberTable:
line 4: 0
line 7: 8
line 5: 11
line 6: 12
line 8: 20
StackMapTable: number_of_entries = 2
frame_type = 75 /* same_locals_1_stack_item */
stack = [ class java/lang/Throwable ]
frame_type = 8 /* same */

注意main方法的Exception table,我們的try-catch塊就是依賴他實現的。如果在我們try-catch塊中拋出了一個異常,那麼虛擬機就會開始檢查,拋出異常的代碼是不是在某個異常表項的處理範圍內,並且這個異常是這個異常處理器處理的異常類型的子類型。如果符合條件,那麼他就會跳轉到target指向的異常處理部分的位元組碼上進行處理。


你需要LLVM的 exception文檔


C++異常機制的實現方式和開銷分析

比較詳細的實現分析,樓主可以參考下.


Idris 中 ST 裡面的異常是這麼做的……

module Control.ST.Exception

import Control.ST
import Control.Catchable
import Control.IOExcept

public export
interface Exception (m : Type -&> Type) errorType | m where
throw : errorType -&> STrans m a ctxt (const ctxt)

catch : STrans m a in_res (const out_res) -&>
(errorType -&> STrans m a out_res (const out_res)) -&>
STrans m a in_res (const out_res)

export
Exception (Either errorType) errorType where
throw err = lift (Left err)
catch prog f = do res &<- runAs prog case res of Left err =&> f err
Right ok =&> pure ok

export
Exception Maybe () where
throw err = lift Nothing
catch prog f = do res &<- runAs prog case res of Nothing =&> f ()
Just ok =&> pure ok

export
Exception (IOExcept errorType) errorType where
throw err = lift (ioe_fail err)
catch prog f = do io_res &<- runAs prog res &<- lift (catch (do r &<- io_res pure (Right r)) (err =&> pure (Left err)))
either (err =&> f err) (ok =&> pure ok) res


無責任回答:

設函數A1拋出了異常,那麼編譯器的代碼會跳轉到A 的「守護者」(我發明的名字)E1。

E1以「觀察者」的角度觀察A1,把A的執行流程反過來掃描一遍,遇到數據則析構,遇到callee save的register則出棧,遇到函數入口則查找A1的調用者A2的守護者E2, E2則繼續前述流程....直到遇到Ex觀察到Ax有catch行為,則檢查能否catch;如果catch匹配成功,則執行參數拷貝,把異常拷貝給catch代碼,並且跳轉到catch代碼則執行。

E的觀察行為實際上是在編譯A的時候就直接生成好了,並不會臨時的去反彙編觀察A,,。需要析構的數據和出棧的數據也在編譯期知道了他們和堆棧指針之間的偏移量。

Ex的所有可能流程是棵樹,樹的根對應Ax的入口,Ax某點拋出異常則從Ex對應的點往根回溯。到根了再找A(x-1)調用Ax處對應E(x-1)的某個位置,,,

Ax和Ex的地址映射應該是運行時才能決定的。這樣Ax上的堆棧無需存儲任何Ex的東東。

-----------------------------------------------------------------------------------------------------------------------------------

唉唉,以上都是我胡說八道的,只是我心目中自己設想的理想的異常處理流程(這是我的學習方法:先自己山寨設計一把,再去讀具體的實現文檔,這樣能夠更充分的理解別人設計中的意圖和痛點)。實際的實現方法千奇百怪,GCC實際就有DWARF和longjmp等標準,win上面還 有SEH。。


異常不算語法糖吧。如果用C來可以用setjmp/longjmp方式實現。實現方式大致如下:

#include &
#include &
#include &
#include &


#include &

typedef struct TryStack_s TryStack;

struct TryStack_s {
TryStack *bottom;
jmp_buf jbuf;
void *exception;
};

static pthread_once_t try_once = PTHREAD_ONCE_INIT;
static pthread_key_t try_key;

static void
try_key_init (void)
{
pthread_key_create(try_key, NULL);
}

static TryStack*
try_stack_get (void)
{
return (TryStack*)pthread_getspecific(try_key);
}

static void
try_stack_push (TryStack *ts)
{
pthread_once(try_once, try_key_init);

ts-&>bottom = try_stack_get();
ts-&>exception = NULL;
pthread_setspecific(try_key, ts);
}

static void
try_stack_pop (TryStack *ts)
{
pthread_setspecific(try_key, ts-&>bottom);
}

static void
throw (void *e)
{
TryStack *ts = try_stack_get();

assert(ts);

ts-&>exception = e;

longjmp(ts-&>jbuf, 1);
}

#define TRY
{
TryStack ts;
try_stack_push(ts);
if (setjmp(ts.jbuf) == 0) {

#define CATCH(e)
} else if (ts.exception == (void*)(size_t)e) {
ts.exception = NULL;

#define FINALLY
}

#define END
try_stack_pop(ts);
if (ts.exception) {
TryStack *bottom = ts.bottom;
assert(bottom);
bottom-&>exception = ts.exception;
longjmp(bottom-&>jbuf, 1);
}
}

static void
test (char *str)
{
if (!strcmp(str, "1"))
throw((void*)(size_t)1);
else if (!strcmp(str, "2"))
throw((void*)(size_t)2);
}

int
main (int argc, char **argv)
{
TRY
if (argc &> 1)
test(argv[1]);
CATCH(1)
printf("catch exception 1
");
CATCH(2)
printf("catch exception 2
");
FINALLY
printf("finally
");
END

return 0;
}


異常顯然不是語法糖,而是在原本的函數調用-返回控制流之外,又多加了一個throw-catch控制流。


cps

圖片來自Programming Language Concepts 這裡得講義


推薦閱讀:

為什麼大多數程序主函數都return 0; 不return 1; ?
為什麼使用gcc編譯代碼後局部數組變數的初始值消失了?
怎麼來學習c++?
學習完 C++ Primer 能做什麼項目練手或者看什麼好的開源項目源碼?
關於C++宏定義的一個疑問?

TAG:編程語言 | Java | C | CC |