PHP内核中与异常处理有关的结构体

本文基于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 #0 INIT_FCALL<1> 96 "test"
L8 #1 SEND_VAL 10000 1
L8 #2 DO_FCALL
L8 #3 JMP J6
L9 #4 CATCH<9> "TypeError" $e
L10 #5 ECHO "handle error\n"
L12 #6 RETURN<-1> 1

[User Function test (2 ops)]
L3-5 test() /root/codeDir/phpCode/test/test.php - 0x7f06cee66000 + 2 ops
L3 #0 RECV 1 $arr
L5 #1 RETURN<-1> null

执行结果如下:

1
2
[root@97043d024896 test]# php test.php
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)是一个包含3opline的数组。并且,把3oplineopcode都设置为了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()) {
/* no need to rethrow the exception */
return;
}
EG(opline_before_exception) = EG(current_execute_data)->opline;
EG(current_execute_data)->opline = EG(exception_op);
}

我们发现,这里设置了EG(current_execute_data)->oplineEG(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,保留当前异常的信息,例如messagefilelineno等。

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_RECVDO_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 #0 INIT_FCALL<1> 96 "test"
L8 #1 SEND_VAL 10000 1
L8 #2 DO_FCALL
L8 #3 JMP J6
L9 #4 CATCH<9> "TypeError" $e
L10 #5 ECHO "handle error\n"
L11 #6 FAST_CALL J8 ~0
L11 #7 JMP J10
L12 #8 ECHO "finally\n"
L12 #9 FAST_RET ~0
L14 #10 RETURN<-1> 1

那么zend_try_catch_element里面的内容如下:

1
2
3
4
try_op -> 0;
catch_op -> 4;
finally_op -> 8;
finally_end -> 9;

如果try后面没有跟catch,那么,catch_op0;如果try后面没有跟finally,那么finally_op0

所以,try_op表示try里面的第一条opline的索引;catch_op表示ZEND_CATCH这条opline的索引;finally_op表示finally里面的第一条opline的索引;finally_end表示ZEND_FAST_RET这条opline的索引。

我们发现,try_opfinally_op都是表示它们里面的opline的位置,而catch_op却是表示catch这条opline本身的位置。这是因为我们不能给tryfinally传参,但是可以给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_TRYZEND_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_catch2

有了上面的基础之后,那么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;

// 省略其他代码

/* Find the innermost try/catch/finally the exception was thrown in */
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) {
/* further blocks will not be relevant... */
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的目的。