在php中我们通过try catch 捕获异常, 通过 throw来抛出异常, 优秀文章推荐:异常处理
先来说说try catch编译完之后是什么内容,比如说下面的代码
编译后的OPCODE 和对应的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
怎么理解这个编译结果呢,首先我们知道每个函数或者php文件最终都会编译成一个opcode_array, 在_zend_op_array 结构体中有一个异常处理的数据结构 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开始的opcode位置
uint32_t catch_op; //第1个catch块的opcode位置
uint32_t finally_op; //finally开始的opcode位置
uint32_t finally_end;//finally结束的opcode位置
} zend_try_catch_element;
这个 try_catch_array 其实就定义了各个代码块的opcode位置,用于c语言层面的程序跳转,比如异常发生了就可以jmp到catch_op 或者 finally_op处执行。如果异常没在当前的opcode_array中被catch住,就会一层一层往上抛,如果都没有被catch住,那么在最外层的opcode_array中就会结束当前request的执行(注意:这个结束request并不代表进程退出,比如fpm sapi中代表request结束,进程不退出, 这个跟各个sapi的实现有关)
------------------------------------------------------------
以下内容来自:异常处理
异常处理的编译
异常捕获及处理的语法:
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是捕获到异常后的处理,可以定义多个,当try中抛出异常时会依次检查各个catch的异常类是否与抛出的匹配,如果匹配则有命中的那个catch块处理;finally为最后执行的代码,不管是否有异常抛出都会执行。
语法规则:
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; }
;
从语法规则可以看出,try-catch-finally最终编译为一个ZEND_AST_TRY
节点,包含三个子节点,分别是:try statement、catch list、finally statement,try statement、finally statement就是普通的ZEND_AST_STMT_LIST
节点,catch list包含多个ZEND_AST_CATCH
节点,每个节点有三个子节点:exception class、exception object及catch statement,最终生成的AST:
具体的编译过程如下:
- (1) 向所属zend_op_array注册一个zend_try_catch_element结构,所有try都会注册一个这样的结构,与循环结构注册的zend_brk_cont_element类似,当前zend_op_array所有定义的异常保存在zend_op_array->try_catch_array数组中,这个结构用来记录try、catch以及finally开始的位置,具体结构:
typedef struct _zend_try_catch_element {
uint32_t try_op; //try开始的opcode位置
uint32_t catch_op; //第1个catch块的opcode位置
uint32_t finally_op; //finally开始的opcode位置
uint32_t finally_end;//finally结束的opcode位置
} zend_try_catch_element;
(2) 编译try statement,编译完以后如果定义了catch块则编译一条
ZEND_JMP
,此opcode的作用时当无异常抛出时跳过所有catch跳到finally或整个异常之外的,因为catch块是在try statement之后编译的,所以具体的跳转值目前还无法确定;(3) 依次编译各个catch块,如果没有定义则跳过此步骤,每个catch编译时首先编译一条
ZEND_CATCH
,此opcode保存着此catch的exception class、exception object以及下一个catch块开始的位置,编译第1个catch时将此opcode的位置记录在zend_try_catch_element.catch_op上,接着编译catch statement,最后编译一条ZEND_JMP
(最后一个catch不需要),此opcode的作用与步骤(2)的相同;(4) 将步骤(2)、步骤(3)中
ZEND_JMP
跳转值设置为finally第1条opcode或异常定义之外的代码,如果没有定义finally则结束编译,否则编译finally块,首先编译一条ZEND_FAST_CALL
及ZEND_JMP
,接着编译finally statement,最后编译一条ZEND_FAST_RET
。
编译完以后的结构:
异常的抛出通过throw一个异常对象来实现,这个对象必须继承>自Exception类,抛出异常的语法:
throw exception_object;
throw的编译比较简单,最终只编译为一条opcode:ZEND_THROW
。
4.6.2 异常的抛出与捕获
上一小节我们介绍了exception结构在编译阶段的处理,接下来我们再介绍下运行时exception的处理过程,这个过程相对比较复杂,整体的讲其处理流程整体如下:
- (1) 检查抛出的是否是object,否则将导致error错误;
- (2) 将EG(exception)设置为抛出的异常对象,同时将当前stack(即:zend_execute_data)接下来要执行的opcode设置为
ZEND_HANDLE_EXCEPTION
; - (3) 执行
ZEND_HANDLE_EXCEPTION
,查找匹配的catch:- (3.1) 首先遍历当前zend_op_array下定义的所有异常捕获,即
zend_op_array->try_catch_array
数组,然后根据throw的位置、try开始的位置、catch开始的位置、finally开始的位置判断判断异常是否在try范围内,如果同时命中了多个try(即嵌套try的情况)则选择最后那个(也就是最里层的),遍历完以后如果命中了则进入步骤(3.2)处理,如果没有命中当前stack下任何try则进入步骤(4); - (3.2) 到这一步表示抛出的异常在当前zend_op_array下有try拦截(注意这里只是表示异常在try中抛出的,但是抛出的异常并一定能被catch),然后根据当前try块的
zend_try_catch_element
结构取出第一个catch的位置,将opcode设置为zend_try_catch_element.catch_op,跳到第一个catch块开始的位置执行,即:执行ZEND_CATCH
; - (3.3) 执行
ZEND_CATCH
,检查抛出的异常对象是否与当前catch的类型匹配,检查的过程为判断两个类是否存在父子关系,如果匹配则表示异常被成功捕获,将EG(exception)清空,如果没有则跳到下一个catch的位置重复步骤(3.3),如果到最后一个catch仍然没有命中则在这个catch的位置抛出一个异常(实际还是原来按个异常,只是将抛出的位置转移了当前catch的位置),然后回到步骤(3);
- (3.1) 首先遍历当前zend_op_array下定义的所有异常捕获,即
- (4) 当前zend_op_array没能成功捕获异常,需要继续往上抛:回到调用位置,将接下来要执行的opcode设置为
ZEND_HANDLE_EXCEPTION
,比如函数中抛出了一个异常没有在函数中捕获,则跳到调用的位置继续捕获,回到步骤(3);如果到最终主脚本也没有被捕获则将结束执行并导致error错误。
这个过程最复杂的地方在于异常匹配、传递的过程,主要为ZEND_HANDLE_EXCEPTION
、ZEND_CATCH
两条opcode之间的调用,当抛出一个异常时会终止后面opcode的执行,转向执行ZEND_HANDLE_EXCEPTION
,根据异常抛出的位置定位到最近的一个try的catch位置,如果这个catch没有匹配则跳到下一个catch块,然后再次执行ZEND_HANDLE_EXCEPTION
,如果到最后一个catch仍没有匹配则将异常抛出前位置EG(opline_before_exception)更新为最后一个catch的位置,再次执行ZEND_HANDLE_EXCEPTION
,由于异常抛出的位置已经更新了所以不会再匹配上次检查过的那个catch,这个过程实际就是不断递归执行ZEND_HANDLE_EXCEPTION
、ZEND_CATCH
;如果当前zend_op_array都无法捕获则将异常抛向上一个调用栈继续捕获,下面根据一个例子具体说明下:
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()中抛出了一个异常,首先在my_func()中抛出一个异常,然后在my_func()的zend_op_array中检查是不是能够捕获,发现没有,则回到调用的位置,再次检查,第1次匹配到catch(ErrorException $e)
,检查后发现并不匹配,然后跳到下一个catch块继续匹配,第2次匹配到catch(Exception $e)
,检查后发现命中,捕获成功。
上面的过程并没有提到finally的执行时机,首先要明确finally在哪些情况下会执行,命中catch的情况比较简单,即在catch statement执行完以后跳到finally执行,另外一种情况是如果一个异常在try中但没有命中任何catch那么其finally也是会被执行的,这种情况的finally实际是在步骤(3)中执行的,最后一个catch检查完以后会更新异常抛出位置:EG(opline_before_exception),然后会再次执行ZEND_HANDLE_EXCEPTION
,再次检查时就会发现没有命中任何catch但命中finally了(因为异常位置更新了),这时候就会将异常对象保存在finally块中,然后执行finally,执行完再将异常对象还原继续捕获