在php中我們通過(guò)try catch 捕獲異常, 通過(guò) throw來(lái)拋出異常, 優(yōu)秀文章推薦:異常處理
先來(lái)說(shuō)說(shuō)try catch編譯完之后是什么內(nèi)容,比如說(shuō)下面的代碼
<?php
echo "1111111\n";
try {
$data_1 = "222222";
throw new Exception("ddddddddddd");
} catch (\Exception $e) {
echo "22222222\n";
} finally {
echo "xxxxxxxxx\n";
}
echo "222222\n";
編譯后的OPCODE 和對(duì)應(yīng)的handler如下, (不知道如何看opcode的參考這篇php源碼-如何查看opcode源碼
)
opcode: 40 ZEND_ECHO , index:1401 ZEND_ECHO_SPEC_CONST_HANDLER
opcode: 38 ZEND_ASSIGN , index:1366 ZEND_ASSIGN_SPEC_CV_CONST_RETVAL_UNUSED_HANDLER
opcode: 68 ZEND_NEW , index:1672 ZEND_NEW_SPEC_CONST_HANDLER
opcode: 116 ZEND_SEND_VAL_EX , index:2488 ZEND_SEND_VAL_EX_SPEC_CONST_QUICK_HANDLER
opcode: 60 ZEND_DO_FCALL , index:1634 ZEND_DO_FCALL_SPEC_RETVAL_UNUSED_HANDLER
opcode: 108 ZEND_THROW , index:2369 ZEND_THROW_SPEC_VAR_HANDLER
opcode: 42 ZEND_JMP , index:1407 ZEND_JMP_SPEC_HANDLER
opcode: 107 ZEND_CATCH , index:2346 ZEND_CATCH_SPEC_CONST_CV_HANDLER
opcode: 40 ZEND_ECHO , index:1401 ZEND_ECHO_SPEC_CONST_HANDLER
opcode: 162 ZEND_FAST_CALL , index:3141 ZEND_FAST_CALL_SPEC_HANDLER
opcode: 42 ZEND_JMP , index:1407 ZEND_JMP_SPEC_HANDLER
opcode: 40 ZEND_ECHO , index:1401 ZEND_ECHO_SPEC_CONST_HANDLER
opcode: 163 ZEND_FAST_RET , index:3142 ZEND_FAST_RET_SPEC_HANDLER
opcode: 40 ZEND_ECHO , index:1401 ZEND_ECHO_SPEC_CONST_HANDLER
opcode: 62 ZEND_RETURN , index:1641 ZEND_RETURN_SPEC_CONST_HANDLER
怎么理解這個(gè)編譯結(jié)果呢,首先我們知道每個(gè)函數(shù)或者php文件最終都會(huì)編譯成一個(gè)opcode_array, 在_zend_op_array 結(jié)構(gòu)體中有一個(gè)異常處理的數(shù)據(jù)結(jié)構(gòu) try_catch_array
struct _zend_op_array {
//...
zend_try_catch_element *try_catch_array;
};
typedef struct _zend_try_catch_element {
uint32_t try_op; //try開(kāi)始的opcode位置
uint32_t catch_op; //第1個(gè)catch塊的opcode位置
uint32_t finally_op; //finally開(kāi)始的opcode位置
uint32_t finally_end;//finally結(jié)束的opcode位置
} zend_try_catch_element;
這個(gè) try_catch_array 其實(shí)就定義了各個(gè)代碼塊的opcode位置,用于c語(yǔ)言層面的程序跳轉(zhuǎn),比如異常發(fā)生了就可以jmp到catch_op 或者 finally_op處執(zhí)行。如果異常沒(méi)在當(dāng)前的opcode_array中被catch住,就會(huì)一層一層往上拋,如果都沒(méi)有被catch住,那么在最外層的opcode_array中就會(huì)結(jié)束當(dāng)前request的執(zhí)行(注意:這個(gè)結(jié)束request并不代表進(jìn)程退出,比如fpm sapi中代表request結(jié)束,進(jìn)程不退出, 這個(gè)跟各個(gè)sapi的實(shí)現(xiàn)有關(guān))
------------------------------------------------------------
以下內(nèi)容來(lái)自:異常處理
異常處理的編譯
異常捕獲及處理的語(yǔ)法:
try{
try statement;
}catch(exception_class_1 $e){
catch statement 1;
}catch(exception_class_2 $e){
catch statement 2;
}finally{
finally statement;
}
try表示要捕獲try statement中可能拋出的異常;catch是捕獲到異常后的處理,可以定義多個(gè),當(dāng)try中拋出異常時(shí)會(huì)依次檢查各個(gè)catch的異常類是否與拋出的匹配,如果匹配則有命中的那個(gè)catch塊處理;finally為最后執(zhí)行的代碼,不管是否有異常拋出都會(huì)執(zhí)行。
語(yǔ)法規(guī)則:
statement:
...
| T_TRY '{' inner_statement_list '}' catch_list finally_statement
{ $$ = zend_ast_create(ZEND_AST_TRY, $3, $5, $6); }
...
;
catch_list:
/* empty */
{ $$ = zend_ast_create_list(0, ZEND_AST_CATCH_LIST); }
| catch_list T_CATCH '(' name T_VARIABLE ')' '{' inner_statement_list '}'
{ $$ = zend_ast_list_add($1, zend_ast_create(ZEND_AST_CATCH, $4, $5, $8)); }
;
finally_statement:
/* empty */ { $$ = NULL; }
| T_FINALLY '{' inner_statement_list '}' { $$ = $3; }
;
從語(yǔ)法規(guī)則可以看出,try-catch-finally最終編譯為一個(gè)ZEND_AST_TRY節(jié)點(diǎn),包含三個(gè)子節(jié)點(diǎn),分別是:try statement、catch list、finally statement,try statement、finally statement就是普通的ZEND_AST_STMT_LIST節(jié)點(diǎn),catch list包含多個(gè)ZEND_AST_CATCH節(jié)點(diǎn),每個(gè)節(jié)點(diǎn)有三個(gè)子節(jié)點(diǎn):exception class、exception object及catch statement,最終生成的AST:

具體的編譯過(guò)程如下:
- (1) 向所屬zend_op_array注冊(cè)一個(gè)zend_try_catch_element結(jié)構(gòu),所有try都會(huì)注冊(cè)一個(gè)這樣的結(jié)構(gòu),與循環(huán)結(jié)構(gòu)注冊(cè)的zend_brk_cont_element類似,當(dāng)前zend_op_array所有定義的異常保存在zend_op_array->try_catch_array數(shù)組中,這個(gè)結(jié)構(gòu)用來(lái)記錄try、catch以及finally開(kāi)始的位置,具體結(jié)構(gòu):
typedef struct _zend_try_catch_element {
uint32_t try_op; //try開(kāi)始的opcode位置
uint32_t catch_op; //第1個(gè)catch塊的opcode位置
uint32_t finally_op; //finally開(kāi)始的opcode位置
uint32_t finally_end;//finally結(jié)束的opcode位置
} zend_try_catch_element;
(2) 編譯try statement,編譯完以后如果定義了catch塊則編譯一條
ZEND_JMP,此opcode的作用時(shí)當(dāng)無(wú)異常拋出時(shí)跳過(guò)所有catch跳到finally或整個(gè)異常之外的,因?yàn)閏atch塊是在try statement之后編譯的,所以具體的跳轉(zhuǎn)值目前還無(wú)法確定;(3) 依次編譯各個(gè)catch塊,如果沒(méi)有定義則跳過(guò)此步驟,每個(gè)catch編譯時(shí)首先編譯一條
ZEND_CATCH,此opcode保存著此catch的exception class、exception object以及下一個(gè)catch塊開(kāi)始的位置,編譯第1個(gè)catch時(shí)將此opcode的位置記錄在zend_try_catch_element.catch_op上,接著編譯catch statement,最后編譯一條ZEND_JMP(最后一個(gè)catch不需要),此opcode的作用與步驟(2)的相同;(4) 將步驟(2)、步驟(3)中
ZEND_JMP跳轉(zhuǎn)值設(shè)置為finally第1條opcode或異常定義之外的代碼,如果沒(méi)有定義finally則結(jié)束編譯,否則編譯finally塊,首先編譯一條ZEND_FAST_CALL及ZEND_JMP,接著編譯finally statement,最后編譯一條ZEND_FAST_RET。
編譯完以后的結(jié)構(gòu):

異常的拋出通過(guò)throw一個(gè)異常對(duì)象來(lái)實(shí)現(xiàn),這個(gè)對(duì)象必須繼承>自Exception類,拋出異常的語(yǔ)法:
throw exception_object;
throw的編譯比較簡(jiǎn)單,最終只編譯為一條opcode:ZEND_THROW。
4.6.2 異常的拋出與捕獲
上一小節(jié)我們介紹了exception結(jié)構(gòu)在編譯階段的處理,接下來(lái)我們?cè)俳榻B下運(yùn)行時(shí)exception的處理過(guò)程,這個(gè)過(guò)程相對(duì)比較復(fù)雜,整體的講其處理流程整體如下:
- (1) 檢查拋出的是否是object,否則將導(dǎo)致error錯(cuò)誤;
-
(2) 將EG(exception)設(shè)置為拋出的異常對(duì)象,同時(shí)將當(dāng)前stack(即:zend_execute_data)接下來(lái)要執(zhí)行的opcode設(shè)置為
ZEND_HANDLE_EXCEPTION; -
(3) 執(zhí)行
ZEND_HANDLE_EXCEPTION,查找匹配的catch:-
(3.1) 首先遍歷當(dāng)前zend_op_array下定義的所有異常捕獲,即
zend_op_array->try_catch_array數(shù)組,然后根據(jù)throw的位置、try開(kāi)始的位置、catch開(kāi)始的位置、finally開(kāi)始的位置判斷判斷異常是否在try范圍內(nèi),如果同時(shí)命中了多個(gè)try(即嵌套try的情況)則選擇最后那個(gè)(也就是最里層的),遍歷完以后如果命中了則進(jìn)入步驟(3.2)處理,如果沒(méi)有命中當(dāng)前stack下任何try則進(jìn)入步驟(4); -
(3.2) 到這一步表示拋出的異常在當(dāng)前zend_op_array下有try攔截(注意這里只是表示異常在try中拋出的,但是拋出的異常并一定能被catch),然后根據(jù)當(dāng)前try塊的
zend_try_catch_element結(jié)構(gòu)取出第一個(gè)catch的位置,將opcode設(shè)置為zend_try_catch_element.catch_op,跳到第一個(gè)catch塊開(kāi)始的位置執(zhí)行,即:執(zhí)行ZEND_CATCH; -
(3.3) 執(zhí)行
ZEND_CATCH,檢查拋出的異常對(duì)象是否與當(dāng)前catch的類型匹配,檢查的過(guò)程為判斷兩個(gè)類是否存在父子關(guān)系,如果匹配則表示異常被成功捕獲,將EG(exception)清空,如果沒(méi)有則跳到下一個(gè)catch的位置重復(fù)步驟(3.3),如果到最后一個(gè)catch仍然沒(méi)有命中則在這個(gè)catch的位置拋出一個(gè)異常(實(shí)際還是原來(lái)按個(gè)異常,只是將拋出的位置轉(zhuǎn)移了當(dāng)前catch的位置),然后回到步驟(3);
-
(3.1) 首先遍歷當(dāng)前zend_op_array下定義的所有異常捕獲,即
-
(4) 當(dāng)前zend_op_array沒(méi)能成功捕獲異常,需要繼續(xù)往上拋:回到調(diào)用位置,將接下來(lái)要執(zhí)行的opcode設(shè)置為
ZEND_HANDLE_EXCEPTION,比如函數(shù)中拋出了一個(gè)異常沒(méi)有在函數(shù)中捕獲,則跳到調(diào)用的位置繼續(xù)捕獲,回到步驟(3);如果到最終主腳本也沒(méi)有被捕獲則將結(jié)束執(zhí)行并導(dǎo)致error錯(cuò)誤。

這個(gè)過(guò)程最復(fù)雜的地方在于異常匹配、傳遞的過(guò)程,主要為ZEND_HANDLE_EXCEPTION、ZEND_CATCH兩條opcode之間的調(diào)用,當(dāng)拋出一個(gè)異常時(shí)會(huì)終止后面opcode的執(zhí)行,轉(zhuǎn)向執(zhí)行ZEND_HANDLE_EXCEPTION,根據(jù)異常拋出的位置定位到最近的一個(gè)try的catch位置,如果這個(gè)catch沒(méi)有匹配則跳到下一個(gè)catch塊,然后再次執(zhí)行ZEND_HANDLE_EXCEPTION,如果到最后一個(gè)catch仍沒(méi)有匹配則將異常拋出前位置EG(opline_before_exception)更新為最后一個(gè)catch的位置,再次執(zhí)行ZEND_HANDLE_EXCEPTION,由于異常拋出的位置已經(jīng)更新了所以不會(huì)再匹配上次檢查過(guò)的那個(gè)catch,這個(gè)過(guò)程實(shí)際就是不斷遞歸執(zhí)行ZEND_HANDLE_EXCEPTION、ZEND_CATCH;如果當(dāng)前zend_op_array都無(wú)法捕獲則將異常拋向上一個(gè)調(diào)用棧繼續(xù)捕獲,下面根據(jù)一個(gè)例子具體說(shuō)明下:
function my_func(){
//...
throw new Exception("This is a exception from my_func()");
}
try{
my_func();
}catch(ErrorException $e){
echo "ErrorException";
}catch(Exception $e){
echo "Exception";
}
my_func()中拋出了一個(gè)異常,首先在my_func()中拋出一個(gè)異常,然后在my_func()的zend_op_array中檢查是不是能夠捕獲,發(fā)現(xiàn)沒(méi)有,則回到調(diào)用的位置,再次檢查,第1次匹配到catch(ErrorException $e),檢查后發(fā)現(xiàn)并不匹配,然后跳到下一個(gè)catch塊繼續(xù)匹配,第2次匹配到catch(Exception $e),檢查后發(fā)現(xiàn)命中,捕獲成功。

上面的過(guò)程并沒(méi)有提到finally的執(zhí)行時(shí)機(jī),首先要明確finally在哪些情況下會(huì)執(zhí)行,命中catch的情況比較簡(jiǎn)單,即在catch statement執(zhí)行完以后跳到finally執(zhí)行,另外一種情況是如果一個(gè)異常在try中但沒(méi)有命中任何catch那么其finally也是會(huì)被執(zhí)行的,這種情況的finally實(shí)際是在步驟(3)中執(zhí)行的,最后一個(gè)catch檢查完以后會(huì)更新異常拋出位置:EG(opline_before_exception),然后會(huì)再次執(zhí)行ZEND_HANDLE_EXCEPTION,再次檢查時(shí)就會(huì)發(fā)現(xiàn)沒(méi)有命中任何catch但命中finally了(因?yàn)楫惓N恢酶铝?,這時(shí)候就會(huì)將異常對(duì)象保存在finally塊中,然后執(zhí)行finally,執(zhí)行完再將異常對(duì)象還原繼續(xù)捕獲