本文基于PHP8.0.1
测试脚本如下:
1 2 3 4 5 6 7 8 9 10 11 <?php function test (array $arr ) {} try { test(10000 ); } catch (\TypeError $e) { echo "handle error\n" ; }
对应的opcodes
如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 [Stack in /root/codeDir/phpCode/test /test.php (7 ops)] L1-12 {main}() /root/codeDir/phpCode/test /test.php - 0x7f61ca65e3c0 + 7 ops L8 L8 L8 L8 L9 L10 L12 [User Function test (2 ops)] L3-5 test () /root/codeDir/phpCode/test /test.php - 0x7f06cee66000 + 2 ops L3 L5
执行结果如下:
1 2 [root@97043d024896 test ] handle error
我们来梳理一下流程:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 graph-easy <<< " graph { flow: down; } [zend_startup] -> [zend_init_exception_op] -> [ZEND_RECV_SPEC_UNUSED_HANDLER] -> [zend_verify_recv_arg_type_helper_SPEC] -> [zend_throw_error] -> [zend_throw_exception] -> [zend_throw_exception_internal] -> [ZEND_HANDLE_EXCEPTION_SPEC_HANDLER] " +---------------------------------------+ | zend_startup | +---------------------------------------+ | | v +---------------------------------------+ | zend_init_exception_op | +---------------------------------------+ | | v +---------------------------------------+ | ZEND_RECV_SPEC_UNUSED_HANDLER | +---------------------------------------+ | | v +---------------------------------------+ | zend_verify_recv_arg_type_helper_SPEC | +---------------------------------------+ | | v +---------------------------------------+ | zend_throw_error | +---------------------------------------+ | | v +---------------------------------------+ | zend_throw_exception | +---------------------------------------+ | | v +---------------------------------------+ | zend_throw_exception_internal | +---------------------------------------+ | | v +---------------------------------------+ | ZEND_HANDLE_EXCEPTION_SPEC_HANDLER | +---------------------------------------+
首先,在zend_startup
阶段,初始化PHP
的异常处理opline
:
1 2 3 4 5 6 7 8 9 10 static void zend_init_exception_op (void ) { memset (EG(exception_op), 0 , sizeof (EG(exception_op))); EG(exception_op)[0 ].opcode = ZEND_HANDLE_EXCEPTION; ZEND_VM_SET_OPCODE_HANDLER(EG(exception_op)); EG(exception_op)[1 ].opcode = ZEND_HANDLE_EXCEPTION; ZEND_VM_SET_OPCODE_HANDLER(EG(exception_op)+1 ); EG(exception_op)[2 ].opcode = ZEND_HANDLE_EXCEPTION; ZEND_VM_SET_OPCODE_HANDLER(EG(exception_op)+2 ); }
这里,我们可以看到,EG(exception_op)
是一个包含3
条opline
的数组。并且,把3
条opline
的opcode
都设置为了ZEND_HANDLE_EXCEPTION
。
接着,php
解释器开始执行我们的测试脚本。当test
函数接收参数的时候,调用zend_verify_recv_arg_type_helper_SPEC
发现参数不对:
1 2 3 4 5 6 7 8 9 10 11 static zend_never_inline ZEND_COLD ZEND_OPCODE_HANDLER_RET ZEND_FASTCALL zend_verify_recv_arg_type_helper_SPEC (zval *op_1 ZEND_OPCODE_HANDLER_ARGS_DC) { USE_OPLINE SAVE_OPLINE(); if (UNEXPECTED(!zend_verify_recv_arg_type(EX(func), opline->op1.num, op_1, CACHE_ADDR(opline->extended_value)))) { HANDLE_EXCEPTION(); } ZEND_VM_NEXT_OPCODE(); }
就会调用zend_throw_error
来抛出一个error
级别的异常。
最终,调用zend_throw_exception_internal
函数:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 ZEND_API ZEND_COLD void zend_throw_exception_internal (zend_object *exception) { if (zend_throw_exception_hook) { zend_throw_exception_hook(exception); } if (is_handle_exception_set()) { return ; } EG(opline_before_exception) = EG(current_execute_data)->opline; EG(current_execute_data)->opline = EG(exception_op); }
我们发现,这里设置了EG(current_execute_data)->opline
为EG(exception_op)
。
接着,就会执行zend_verify_recv_arg_type_helper_SPEC
里面的HANDLE_EXCEPTION()
:
1 #define HANDLE_EXCEPTION() ZEND_ASSERT(EG(exception)); LOAD_OPLINE(); ZEND_VM_CONTINUE()
所以,下一条opline
就变成了去执行ZEND_HANDLE_EXCEPTION
对应的ZEND_HANDLE_EXCEPTION_SPEC_HANDLER
了。而这个handler
就是用来处理异常的,例如查找当前作用域上是否有对异常抛出点进行try ... catch ... finally
。
这个handler
我们不去细讲,我们主要讲一讲和异常处理有关的结构体,搞明白了结构体里面各各属性的作用,就知道这个handler
做了些什么事情了。
与异常有关的数据结构 zend_object *zend_executor_globals::exception
,保留当前异常的信息,例如message
、file
、lineno
等。
zend_op *zend_executor_globals::opline_before_exception
,用来回溯异常抛出时候的opline
。例如,我们有如下异常回溯关系:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 graph-easy <<< " graph { flow: up; } [ZEND_RECV] -> [DO_FCALL] " +-----------+ | DO_FCALL | +-----------+ ^ | | +-----------+ | ZEND_RECV | +-----------+
那么,opline_before_exception
依次是ZEND_RECV
和DO_FCALL
对应的opline
。
那么,有如下计算:
1 uint32_t throw_op_num = EG(opline_before_exception) - EX(func)->op_array.opcodes
throw_op_num
则是抛异常的opline
的索引。
1 2 3 4 5 6 typedef struct _zend_try_catch_element { uint32_t try_op; uint32_t catch_op; uint32_t finally_op; uint32_t finally_end; } zend_try_catch_element;
这几个字段什么意思呢?假设我们有如下代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 <?php function test (array $arr ) {} try { test(10000 ); } catch (\TypeError $e) { echo "handle error\n" ; } finally { echo "finally\n" ; }
main
函数对应的opcodes
如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 [Stack in /Users/codinghuang/.phpbrew/build/php-8.0.1/test.php (11 ops)] L1-14 {main}() /Users/codinghuang/.phpbrew/build/php-8.0.1/test.php - 0x108a066f0 + 11 ops L8 L8 L8 L8 L9 L10 L11 L11 L12 L12 L14
那么zend_try_catch_element
里面的内容如下:
1 2 3 4 try_op -> 0 ; catch_op -> 4 ; finally_op -> 8 ; finally_end -> 9 ;
如果try
后面没有跟catch
,那么,catch_op
为0
;如果try
后面没有跟finally
,那么finally_op
为0
。
所以,try_op
表示try
里面的第一条opline
的索引;catch_op
表示ZEND_CATCH
这条opline
的索引;finally_op
表示finally
里面的第一条opline
的索引;finally_end
表示ZEND_FAST_RET
这条opline
的索引。
我们发现,try_op
和finally_op
都是表示它们里面的opline
的位置,而catch_op
却是表示catch
这条opline
本身的位置。这是因为我们不能给try
和finally
传参,但是可以给catch
传参。例如,不能这么写:
1 2 3 4 5 6 7 try (something) { test(10000 ); } catch (\TypeError $e) { echo "handle error\n" ; } finally (something) { echo "finally\n" ; }
所以,我们发现,这里只有catch
生成了ZEND_CATCH
,但是却没有ZEND_TRY
和ZEND_FINALLY
这样的opline
。
zend_op_array::last_try_catch
表示当前作用域有几组try ... catch ... finally
。例如:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 <?php function test (array $arr ) {} try { throw new Exception ("Error Processing Request" , 1 ); } catch (\Exception $e) { echo "handle error\n" ; } try { test(10000 ); } catch (\TypeError $e) { echo "handle error\n" ; } finally { echo "finally\n" ; }
因为main
作用域有两组try ... catch
,所以zend_op_array::last_try_catch
是2
。
有了上面的基础之后,那么ZEND_HANDLE_EXCEPTION_SPEC_HANDLER
里面查找zend_try_catch_element
的流程就好理解了:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 const zend_op *throw_op = EG(opline_before_exception);uint32_t throw_op_num = throw_op - EX(func)->op_array.opcodes;int i, current_try_catch_offset = -1 ;for (i = 0 ; i < EX(func)->op_array.last_try_catch; i++) { zend_try_catch_element *try_catch = &EX(func)->op_array.try_catch_array[i]; if (try_catch->try_op > throw_op_num) { break ; } if (throw_op_num < try_catch->catch_op || throw_op_num < try_catch->finally_end) { current_try_catch_offset = i; } }
这段代码就是通过异常抛出的opline
来找到对应的zend_try_catch_element
。
因此,异常处理的核心就是,通过改变EG(current_execute_data)->opline
,达到执行ZEND_HANDLE_EXCEPTION_SPEC_HANDLER
的目的。