+ -
当前位置:首页 → 问答吧 → PHP源代码分析: BREAK/CONTINUE

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   发布时间: 2008-11-28

太强大了,这个程序不能停止吧

作者: badhot   发布时间: 2008-11-29

当然可以停止啊。

作者: Altair   发布时间: 2008-11-29