PHP源代码分析: BREAK/CONTINUE
时间:2008-11-28
来源:互联网
PHP源代码分析:OPCODE选解系列:BRK/CNT
作者:Altair (http://www.phpinternals.com/) 转发请注明出处。
在PHP的循环控制中,有两个改变能循环流程的语句,分别是break, contine。今天我们研究一下它们的实现。
首先我们看一个例子:[code]
<?php
echo "Break/Continue OPCODE Study Sample ";
for ($i = 1; $i < 5; $i ++) {
for ($j = 1; $j < 5; $j ++) {
for ($k = 1; $k < 5; $k ++) {
for ($l = 1; $l < 5; $l ++) {
echo "i = $i, j = $j , k = $k, l = $l ";
if ($i + $j > 5) {
echo "break loop ... ";
break 2; // 第11行
}
} /* for $l */
if ($i + $j + $k > 10) break; // 第14行
} /* for $k */
} /* for $j */
} /* for $i */
?>[/code]我们知道,第11行break 2表示从当前的循环嵌套层次(第四层for $l)退出两层到for $j层。第14行的break表示从当前的循环嵌套层次(第三层for $k)退出一层到for $j层。那么,这样的机制在Zend Engine中究竟是怎么实现的呢?
首先我们还是使用OPDUMP将这段程序编译成OPCODE:[code]
start:-1 brk:51 cont:4 parent:-1
start:-1 brk:50 cont:10 parent:0
start:-1 brk:49 cont:16 parent:1
start:-1 brk:42 cont:22 parent:2
1: <?php
2: echo "Break/Continue OPCODE Study Sample ";
0 ECHO 'Break/Continue OPCODE Study Sample'
3:
4: for ($i = 1; $i < 5; $i ++) {
1 ASSIGN !0, 1
2 IS_SMALLER !0, 5 =>RES[~1]
3 JMPZNZ ~1, ->51 [extval:7]
4 POST_INC !0 =>RES[~2]
5 FREE ~2
6 JMP ->2
5: for ($j = 1; $j < 5; $j ++) {
7 ASSIGN !1, 1
8 IS_SMALLER !1, 5 =>RES[~4]
9 JMPZNZ ~4, ->50 [extval:13]
10 POST_INC !1 =>RES[~5]
11 FREE ~5
12 JMP ->8
6: for ($k = 1; $k < 5; $k ++) {
13 ASSIGN !2, 1
14 IS_SMALLER !2, 5 =>RES[~7]
15 JMPZNZ ~7, ->49 [extval:19]
16 POST_INC !2 =>RES[~8]
17 FREE ~8
18 JMP ->14
7: for ($l = 1; $l < 5; $l ++) {
19 ASSIGN !3, 1
20 IS_SMALLER !3, 5 =>RES[~10]
21 JMPZNZ ~10, ->42 [extval:25]
22 POST_INC !3 =>RES[~11]
23 FREE ~11
24 JMP ->20
8: echo "i = $i, j = $j , k = $k, l = $l ";
25 ADD_STRING 'i = ' =>RES[~12]
26 ADD_VAR ~12, !0 =>RES[~12]
27 ADD_STRING ~12, ', j = ' =>RES[~12]
28 ADD_VAR ~12, !1 =>RES[~12]
29 ADD_STRING ~12, ' , k = ' =>RES[~12]
30 ADD_VAR ~12, !2 =>RES[~12]
31 ADD_STRING ~12, ', l = ' =>RES[~12]
32 ADD_VAR ~12, !3 =>RES[~12]
33 ADD_CHAR ~12, 10 =>RES[~12]
34 ECHO ~12
9: if ($i + $j > 5) {
35 ADD !0, !1 =>RES[~13]
36 IS_SMALLER 5, ~13 =>RES[~14]
37 JMPZ ~14, ->41
10: echo "break loop ... ";
38 ECHO 'break loop ...'
11: break 2;
39 BRK 3, 2
12: }
40* JMP ->41
13: }
41 JMP ->22
14: if ($i + $j + $k > 10) break;
42 ADD !0, !1 =>RES[~15]
43 ADD ~15, !2 =>RES[~16]
44 IS_SMALLER 10, ~16 =>RES[~17]
45 JMPZ ~17, ->48
46 BRK 2, 1
47* JMP ->48
15: }
48 JMP ->16
16: }
49 JMP ->10
17: }
50 JMP ->4
18: ?>
51 RETURN 1
[/code]我们可以看到第11行与第14行 break编译后的OPCODE是分别是 break 3, 2以及break 2, 1。
现在再看Zend Engine对BRK指令的处理:(zend_vm_def.h)[实际上,zend_vm_def.h最后会由php_gen_vm.php脚本生成zend_vm_execute.h,我们只研究zend_vm_def.h中的代码是因为它的逻辑更清晰。它与zend_vm_execute.h的不同之处在于zend_vm_def.h中封装了对不同形式的变量的存取逻辑,zend_vm_execute.h是实际可以编译的代码,而zend_vm_def.h中的代码只是一种伪码][code]
ZEND_VM_HANDLER(50, ZEND_BRK, ANY, CONST|TMP|VAR|CV)
{
zend_op *opline = EX(opline);
zend_free_op free_op2;
zend_brk_cont_element *el;
el = zend_brk_cont(GET_OP2_ZVAL_PTR(BP_VAR_R), opline->op1.u.opline_num,
EX(op_array), EX(Ts) TSRMLS_CC);
FREE_OP2();
ZEND_VM_JMP(EX(op_array)->opcodes + el->brk);
}
[/code]这段代码比较简单,先由zend_brk_cont函数根据ZEND_BRK两个操作数op1, op2计算出一个zend_brk_cont_element类型的变量,该变量确定出跳转后的新的opcode的下标。看来最关键的还在于zend_brk_cont函数的实现以及zend_brk_cont_element的结构。我们先来看zend_brk_cont函数的实现:[code]
(下面的代码来自zend_execute.c)
static inline zend_brk_cont_element* zend_brk_cont(const zval *nest_levels_zval, int array_offset, const zend_op_array *op_array, const temp_variable *Ts TSRMLS_DC)
{
zval tmp;
int nest_levels, original_nest_levels;
zend_brk_cont_element *jmp_to;
if (nest_levels_zval->type != IS_LONG) {
tmp = *nest_levels_zval;
zval_copy_ctor(&tmp);
convert_to_long(&tmp);
nest_levels = tmp.value.lval;
} else {
nest_levels = nest_levels_zval->value.lval;
}
original_nest_levels = nest_levels;
do {
if (array_offset==-1) {
zend_error_noreturn(E_ERROR, "Cannot break/continue %d level%s", original_nest_levels, (original_nest_levels == 1) ? "" : "s");
}
jmp_to = &op_array->brk_cont_array[array_offset];
if (nest_levels>1) {
... /* 明显跟跳转目标无关的代码 */
}
array_offset = jmp_to->parent;
} while (--nest_levels > 0);
return jmp_to;
}
[/code]在这个函数里,我们又有了新发现,这就是brk_cont_array数组。它的定义在zend_compile.h中:[code]
struct _zend_op_array {
...
zend_brk_cont_element *brk_cont_array;
int last_brk_cont;
int current_brk_cont;
...
}
[/code]zend_brk_cont_element的定义同样也在zend_compile.h中:[code]
typedef struct _zend_brk_cont_element {
int start;
int cont;
int brk;
int parent;
} zend_brk_cont_element;
[/code]我们现在再研究一下上面的OPDUMP输出结果最开始的四行,这实际上就是brk_cont_array内容的转储,将它与opcode对照,我们明显可以发现:
这个数组共有四个元素(每个元素都是zend_brk_cont_element结构)表示当前的四个嵌套循环层次,并且按嵌套层次的顺序从外到内排列。每个元素的parent指向上一级嵌套层次的下标,如果parent值为-1,则表示该层次是最外层的循环嵌套层次。brk的值表示从当前嵌套层次break后的继续执行的opcode下标。
现在再来看zend_brk_cont函数的实现就比较好理解了。根据ZEND_BRK指令的处理代码:
el = zend_brk_cont(GET_OP2_ZVAL_PTR(BP_VAR_R), opline->op1.u.opline_num,
EX(op_array), EX(Ts) TSRMLS_CC);
我们知道了在zend_brk_cont函数的参数中,nest_levels_zval表示需要退出的嵌套层次,array_offset表示当前的循环层次。注意,这里的循环嵌套层次都是从0开始计数的。EX(op_array)表示当前正在执行的OPCODE序列。(EX(Ts)表示的是当前函数/OPCODE序列的临时变量数组,它并不影响我们对zend_brk_cont函数的执行机制的理解)。
zend_brk_cont函数中最关键的是[code]
do {
...
jmp_to = &op_array->brk_cont_array[array_offset];
...
array_offset = jmp_to->parent;
} while (--nest_levels > 0)
[/code]这个循环从当前嵌套层次array_offset开始,循环nest_levels次,找到该层次对应的opcode代码。
通过上面的说明,现在我们对Zend Engine中BREAK语句的执行机制已经有了比较清楚的了解了。实际上CONTINUE语句的执行机制与BREAK完全一致,不同的是它使用brk_cont_array数组中的cont字段。
现在还有最后一个问题:如果一段程序中有多个非嵌套的循环又该怎么处理呢?例如下面的程序,brk_cont_array数组将会是怎样的呢?[code]
<?php
for ($i = 1; $i < 10; $i ++) {
for ($j = 1; $j < 10; $j ++) {
if ($j > 5) break;
}
}
for ($k = 1; $k < 10; $k ++) {
for ($l = 1; $l < 10; $l ++) {
if ($l > 5) break;
}
}
[/code]下面给出OPDUMP的编译结果,请细细体会:[code]
start:-1 brk:18 cont:3 parent:-1
start:-1 brk:17 cont:9 parent:0
start:-1 brk:36 cont:21 parent:-1
start:-1 brk:35 cont:27 parent:2
1: <?php
2: for ($i = 1; $i < 10; $i ++) {
0 ASSIGN !0, 1
1 IS_SMALLER !0, 10 =>RES[~1]
2 JMPZNZ ~1, ->18 [extval:6]
3 POST_INC !0 =>RES[~2]
4 FREE ~2
5 JMP ->1
3: for ($j = 1; $j < 10; $j ++) {
6 ASSIGN !1, 1
7 IS_SMALLER !1, 10 =>RES[~4]
8 JMPZNZ ~4, ->17 [extval:12]
9 POST_INC !1 =>RES[~5]
10 FREE ~5
11 JMP ->7
4: if ($j > 5) break;
12 IS_SMALLER 5, !1 =>RES[~6]
13 JMPZ ~6, ->16
14 BRK 1, 1
15* JMP ->16
5: }
16 JMP ->9
6: }
17 JMP ->3
7:
8: for ($k = 1; $k < 10; $k ++) {
18 ASSIGN !2, 1
19 IS_SMALLER !2, 10 =>RES[~8]
20 JMPZNZ ~8, ->36 [extval:24]
21 POST_INC !2 =>RES[~9]
22 FREE ~9
23 JMP ->19
9: for ($l = 1; $l < 10; $l ++) {
24 ASSIGN !3, 1
25 IS_SMALLER !3, 10 =>RES[~11]
26 JMPZNZ ~11, ->35 [extval:30]
27 POST_INC !3 =>RES[~12]
28 FREE ~12
29 JMP ->25
10: if ($l > 5) break;
30 IS_SMALLER 5, !3 =>RES[~13]
31 JMPZ ~13, ->34
32 BRK 3, 1
33* JMP ->34
11: }
34 JMP ->27
12: }
35 JMP ->21
36 RETURN 1
[/code]
作者:Altair (http://www.phpinternals.com/) 转发请注明出处。
在PHP的循环控制中,有两个改变能循环流程的语句,分别是break, contine。今天我们研究一下它们的实现。
首先我们看一个例子:[code]
<?php
echo "Break/Continue OPCODE Study Sample ";
for ($i = 1; $i < 5; $i ++) {
for ($j = 1; $j < 5; $j ++) {
for ($k = 1; $k < 5; $k ++) {
for ($l = 1; $l < 5; $l ++) {
echo "i = $i, j = $j , k = $k, l = $l ";
if ($i + $j > 5) {
echo "break loop ... ";
break 2; // 第11行
}
} /* for $l */
if ($i + $j + $k > 10) break; // 第14行
} /* for $k */
} /* for $j */
} /* for $i */
?>[/code]我们知道,第11行break 2表示从当前的循环嵌套层次(第四层for $l)退出两层到for $j层。第14行的break表示从当前的循环嵌套层次(第三层for $k)退出一层到for $j层。那么,这样的机制在Zend Engine中究竟是怎么实现的呢?
首先我们还是使用OPDUMP将这段程序编译成OPCODE:[code]
start:-1 brk:51 cont:4 parent:-1
start:-1 brk:50 cont:10 parent:0
start:-1 brk:49 cont:16 parent:1
start:-1 brk:42 cont:22 parent:2
1: <?php
2: echo "Break/Continue OPCODE Study Sample ";
0 ECHO 'Break/Continue OPCODE Study Sample'
3:
4: for ($i = 1; $i < 5; $i ++) {
1 ASSIGN !0, 1
2 IS_SMALLER !0, 5 =>RES[~1]
3 JMPZNZ ~1, ->51 [extval:7]
4 POST_INC !0 =>RES[~2]
5 FREE ~2
6 JMP ->2
5: for ($j = 1; $j < 5; $j ++) {
7 ASSIGN !1, 1
8 IS_SMALLER !1, 5 =>RES[~4]
9 JMPZNZ ~4, ->50 [extval:13]
10 POST_INC !1 =>RES[~5]
11 FREE ~5
12 JMP ->8
6: for ($k = 1; $k < 5; $k ++) {
13 ASSIGN !2, 1
14 IS_SMALLER !2, 5 =>RES[~7]
15 JMPZNZ ~7, ->49 [extval:19]
16 POST_INC !2 =>RES[~8]
17 FREE ~8
18 JMP ->14
7: for ($l = 1; $l < 5; $l ++) {
19 ASSIGN !3, 1
20 IS_SMALLER !3, 5 =>RES[~10]
21 JMPZNZ ~10, ->42 [extval:25]
22 POST_INC !3 =>RES[~11]
23 FREE ~11
24 JMP ->20
8: echo "i = $i, j = $j , k = $k, l = $l ";
25 ADD_STRING 'i = ' =>RES[~12]
26 ADD_VAR ~12, !0 =>RES[~12]
27 ADD_STRING ~12, ', j = ' =>RES[~12]
28 ADD_VAR ~12, !1 =>RES[~12]
29 ADD_STRING ~12, ' , k = ' =>RES[~12]
30 ADD_VAR ~12, !2 =>RES[~12]
31 ADD_STRING ~12, ', l = ' =>RES[~12]
32 ADD_VAR ~12, !3 =>RES[~12]
33 ADD_CHAR ~12, 10 =>RES[~12]
34 ECHO ~12
9: if ($i + $j > 5) {
35 ADD !0, !1 =>RES[~13]
36 IS_SMALLER 5, ~13 =>RES[~14]
37 JMPZ ~14, ->41
10: echo "break loop ... ";
38 ECHO 'break loop ...'
11: break 2;
39 BRK 3, 2
12: }
40* JMP ->41
13: }
41 JMP ->22
14: if ($i + $j + $k > 10) break;
42 ADD !0, !1 =>RES[~15]
43 ADD ~15, !2 =>RES[~16]
44 IS_SMALLER 10, ~16 =>RES[~17]
45 JMPZ ~17, ->48
46 BRK 2, 1
47* JMP ->48
15: }
48 JMP ->16
16: }
49 JMP ->10
17: }
50 JMP ->4
18: ?>
51 RETURN 1
[/code]我们可以看到第11行与第14行 break编译后的OPCODE是分别是 break 3, 2以及break 2, 1。
现在再看Zend Engine对BRK指令的处理:(zend_vm_def.h)[实际上,zend_vm_def.h最后会由php_gen_vm.php脚本生成zend_vm_execute.h,我们只研究zend_vm_def.h中的代码是因为它的逻辑更清晰。它与zend_vm_execute.h的不同之处在于zend_vm_def.h中封装了对不同形式的变量的存取逻辑,zend_vm_execute.h是实际可以编译的代码,而zend_vm_def.h中的代码只是一种伪码][code]
ZEND_VM_HANDLER(50, ZEND_BRK, ANY, CONST|TMP|VAR|CV)
{
zend_op *opline = EX(opline);
zend_free_op free_op2;
zend_brk_cont_element *el;
el = zend_brk_cont(GET_OP2_ZVAL_PTR(BP_VAR_R), opline->op1.u.opline_num,
EX(op_array), EX(Ts) TSRMLS_CC);
FREE_OP2();
ZEND_VM_JMP(EX(op_array)->opcodes + el->brk);
}
[/code]这段代码比较简单,先由zend_brk_cont函数根据ZEND_BRK两个操作数op1, op2计算出一个zend_brk_cont_element类型的变量,该变量确定出跳转后的新的opcode的下标。看来最关键的还在于zend_brk_cont函数的实现以及zend_brk_cont_element的结构。我们先来看zend_brk_cont函数的实现:[code]
(下面的代码来自zend_execute.c)
static inline zend_brk_cont_element* zend_brk_cont(const zval *nest_levels_zval, int array_offset, const zend_op_array *op_array, const temp_variable *Ts TSRMLS_DC)
{
zval tmp;
int nest_levels, original_nest_levels;
zend_brk_cont_element *jmp_to;
if (nest_levels_zval->type != IS_LONG) {
tmp = *nest_levels_zval;
zval_copy_ctor(&tmp);
convert_to_long(&tmp);
nest_levels = tmp.value.lval;
} else {
nest_levels = nest_levels_zval->value.lval;
}
original_nest_levels = nest_levels;
do {
if (array_offset==-1) {
zend_error_noreturn(E_ERROR, "Cannot break/continue %d level%s", original_nest_levels, (original_nest_levels == 1) ? "" : "s");
}
jmp_to = &op_array->brk_cont_array[array_offset];
if (nest_levels>1) {
... /* 明显跟跳转目标无关的代码 */
}
array_offset = jmp_to->parent;
} while (--nest_levels > 0);
return jmp_to;
}
[/code]在这个函数里,我们又有了新发现,这就是brk_cont_array数组。它的定义在zend_compile.h中:[code]
struct _zend_op_array {
...
zend_brk_cont_element *brk_cont_array;
int last_brk_cont;
int current_brk_cont;
...
}
[/code]zend_brk_cont_element的定义同样也在zend_compile.h中:[code]
typedef struct _zend_brk_cont_element {
int start;
int cont;
int brk;
int parent;
} zend_brk_cont_element;
[/code]我们现在再研究一下上面的OPDUMP输出结果最开始的四行,这实际上就是brk_cont_array内容的转储,将它与opcode对照,我们明显可以发现:
这个数组共有四个元素(每个元素都是zend_brk_cont_element结构)表示当前的四个嵌套循环层次,并且按嵌套层次的顺序从外到内排列。每个元素的parent指向上一级嵌套层次的下标,如果parent值为-1,则表示该层次是最外层的循环嵌套层次。brk的值表示从当前嵌套层次break后的继续执行的opcode下标。
现在再来看zend_brk_cont函数的实现就比较好理解了。根据ZEND_BRK指令的处理代码:
el = zend_brk_cont(GET_OP2_ZVAL_PTR(BP_VAR_R), opline->op1.u.opline_num,
EX(op_array), EX(Ts) TSRMLS_CC);
我们知道了在zend_brk_cont函数的参数中,nest_levels_zval表示需要退出的嵌套层次,array_offset表示当前的循环层次。注意,这里的循环嵌套层次都是从0开始计数的。EX(op_array)表示当前正在执行的OPCODE序列。(EX(Ts)表示的是当前函数/OPCODE序列的临时变量数组,它并不影响我们对zend_brk_cont函数的执行机制的理解)。
zend_brk_cont函数中最关键的是[code]
do {
...
jmp_to = &op_array->brk_cont_array[array_offset];
...
array_offset = jmp_to->parent;
} while (--nest_levels > 0)
[/code]这个循环从当前嵌套层次array_offset开始,循环nest_levels次,找到该层次对应的opcode代码。
通过上面的说明,现在我们对Zend Engine中BREAK语句的执行机制已经有了比较清楚的了解了。实际上CONTINUE语句的执行机制与BREAK完全一致,不同的是它使用brk_cont_array数组中的cont字段。
现在还有最后一个问题:如果一段程序中有多个非嵌套的循环又该怎么处理呢?例如下面的程序,brk_cont_array数组将会是怎样的呢?[code]
<?php
for ($i = 1; $i < 10; $i ++) {
for ($j = 1; $j < 10; $j ++) {
if ($j > 5) break;
}
}
for ($k = 1; $k < 10; $k ++) {
for ($l = 1; $l < 10; $l ++) {
if ($l > 5) break;
}
}
[/code]下面给出OPDUMP的编译结果,请细细体会:[code]
start:-1 brk:18 cont:3 parent:-1
start:-1 brk:17 cont:9 parent:0
start:-1 brk:36 cont:21 parent:-1
start:-1 brk:35 cont:27 parent:2
1: <?php
2: for ($i = 1; $i < 10; $i ++) {
0 ASSIGN !0, 1
1 IS_SMALLER !0, 10 =>RES[~1]
2 JMPZNZ ~1, ->18 [extval:6]
3 POST_INC !0 =>RES[~2]
4 FREE ~2
5 JMP ->1
3: for ($j = 1; $j < 10; $j ++) {
6 ASSIGN !1, 1
7 IS_SMALLER !1, 10 =>RES[~4]
8 JMPZNZ ~4, ->17 [extval:12]
9 POST_INC !1 =>RES[~5]
10 FREE ~5
11 JMP ->7
4: if ($j > 5) break;
12 IS_SMALLER 5, !1 =>RES[~6]
13 JMPZ ~6, ->16
14 BRK 1, 1
15* JMP ->16
5: }
16 JMP ->9
6: }
17 JMP ->3
7:
8: for ($k = 1; $k < 10; $k ++) {
18 ASSIGN !2, 1
19 IS_SMALLER !2, 10 =>RES[~8]
20 JMPZNZ ~8, ->36 [extval:24]
21 POST_INC !2 =>RES[~9]
22 FREE ~9
23 JMP ->19
9: for ($l = 1; $l < 10; $l ++) {
24 ASSIGN !3, 1
25 IS_SMALLER !3, 10 =>RES[~11]
26 JMPZNZ ~11, ->35 [extval:30]
27 POST_INC !3 =>RES[~12]
28 FREE ~12
29 JMP ->25
10: if ($l > 5) break;
30 IS_SMALLER 5, !3 =>RES[~13]
31 JMPZ ~13, ->34
32 BRK 3, 1
33* JMP ->34
11: }
34 JMP ->27
12: }
35 JMP ->21
36 RETURN 1
[/code]
作者: Altair 发布时间: 2008-11-28
太强大了,这个程序不能停止吧
作者: badhot 发布时间: 2008-11-29
当然可以停止啊。
作者: Altair 发布时间: 2008-11-29
相关阅读 更多
热门阅读
-
office 2019专业增强版最新2021版激活秘钥/序列号/激活码推荐 附激活工具
阅读:74
-
如何安装mysql8.0
阅读:31
-
Word快速设置标题样式步骤详解
阅读:28
-
20+道必知必会的Vue面试题(附答案解析)
阅读:37
-
HTML如何制作表单
阅读:22
-
百词斩可以改天数吗?当然可以,4个步骤轻松修改天数!
阅读:31
-
ET文件格式和XLS格式文件之间如何转化?
阅读:24
-
react和vue的区别及优缺点是什么
阅读:121
-
支付宝人脸识别如何关闭?
阅读:21
-
腾讯微云怎么修改照片或视频备份路径?
阅读:28