热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

【走进php内核】之中断及跳转(break,continue,goto)

中断及跳转PHP中的中断及跳转语句主要有break、continue、goto,这几种语句的实现基础都是跳转。break与continuebreak用于结束当前

中断及跳转

PHP中的中断及跳转语句主要有break、continue、goto,这几种语句的实现基础都是跳转。

break与continue

break用于结束当前for、foreach、while、do-while 或者 switch 结构的执行;continue用于跳过本次循环中剩余代码,进行下一轮循环。break、continue是非常相像的,它们都可以接受一个可选数字参数来决定跳过的循环层数,两者的不同点在于break是跳到循环结束的位置,而continue是跳到循环判断条件的位置,本质在于跳转位置的不同。

break、continue的实现稍微有些复杂,下面具体介绍下其编译过程。

上一节我们已经介绍过循环语句的编译,其中在各种循环编译过程中有两个特殊操作:zend_begin_loop()、zend_end_loop(),分别在循环编译前以及编译后调用,这两步操作就是为break、continue服务的。

在每层循环编译时都会创建一个zend_brk_cont_element的结构:

typedef struct _zend_brk_cont_element {int start;int cont;int brk;int parent;
} zend_brk_cont_element;

cont记录的是当前循环判断条件opcode起始位置,brk记录的是当前循环结束的位置,parent记录的是父层循环zend_brk_cont_element结构的存储位置,也就是说多层嵌套循环会生成一个zend_brk_cont_element的链表,每层循环编译结束时更新自己的zend_brk_cont_element结构,所以break、continue的处理过程实际就是根据跳出的层级索引到那一层的zend_brk_cont_element结构,然后得到它的cont、brk进行相应的opcode跳转。

各循环的zend_brk_cont_element结构保存在zend_op_array->brk_cont_array数组中,编译各循环时依次申请一个zend_brk_cont_element,zend_op_array->last_brk_cont记录此数组第一个可用位置,每申请一个元素last_brk_cont就相应的增加1,然后将数组扩容,parent记录的就是父层循环结构在该数组中的存储位置。

zend_brk_cont_element *get_next_brk_cont_element(zend_op_array *op_array)
{op_array->last_brk_cont++;op_array->brk_cont_array = erealloc(op_array->brk_cont_array, sizeof(zend_brk_cont_element)*op_array->last_brk_cont);return &op_array->brk_cont_array[op_array->last_brk_cont-1];
}

示例:

$i = 0;
while(1){while(1){if($i > 10){break 2;}++$i}
}

循环编译完以后对应的内存结构:

介绍完编译循环结构时为break、continue做的准备,接下来我们具体分析下break、continue的编译。

有了前面的准备,break、continue的编译过程就比较简单了,主要就是各生成一条临时opcode:ZEND_BRK、ZEND_CONT,这条opcode记录着两个重要信息:

  • op1: 记录着当前循环zend_brk_cont_element结构的存储位置(在循环编译过程中CG(context).current_brk_cont记录着当前循环zend_brk_cont_element的位置)
  • op2: 记录着要跳出循环的层级,如果break/continue没有加数字,则默认为1

void zend_compile_break_continue(zend_ast *ast)
{zend_ast *depth_ast = ast->child[0];zend_op *opline;int depth;if (depth_ast) {zval *depth_zv;...depth = Z_LVAL_P(depth_zv);} else {depth = 1;}...//生成opcodeopline = zend_emit_op(NULL, ast->kind == ZEND_AST_BREAK ? ZEND_BRK : ZEND_CONT, NULL, NULL);opline->op1.num = CG(context).current_brk_cont; //break、continue所在循环层opline->op2.num = depth; //要跳出的层数
}

zend_compile_break_continue()到这一步完成整个break、continue的编译还没有完成,因为CG(active_op_array)->brk_cont_array这个数组只是编译期间使用的一个临时结构,break、continue编译生成的opcode:ZEND_BRK、ZEND_CONT并不是运行时直接执行的,这条opcode在整个脚本编译完成后、执行前被优化为 ZEND_JMP ,这个操作在pass_two()中完成,关于这个过程在《AST->zend_op_array》一节曾经介绍过。

ZEND_API zend_op_array *compile_file(zend_file_handle *file_handle, int type)
{//语法解析zendparse();//AST->opcodeszend_compile_top_stmt(CG(ast));pass_two(op_array);...
}

ZEND_API int pass_two(zend_op_array *op_array)
{...opline = op_array->opcodes;end = opline + op_array->last;while (opline opcode) {...case ZEND_BRK:case ZEND_CONT:{//计算跳转位置uint32_t jmp_target = zend_get_brk_cont_target(op_array, opline);...//将opcode修改为ZEND_JMPopline->opcode = ZEND_JMP;opline->op1.opline_num = jmp_target;opline->op2.num = 0;//将绝对跳转opcode位置修改为相对当前opcode的位置ZEND_PASS_TWO_UPDATE_JMP_TARGET(op_array, opline, opline->op1);}break;...}}op_array->fn_flags |= ZEND_ACC_DONE_PASS_TWO;return 0;
}

从上面的过程可以看出,如果opcode为:ZEND_BRK或ZEND_CONT则统一设置opcode为ZEND_JMP,新opcode的op1记录的是break、continue跳到opcode的位置,这个值根据编译期间的zend_brk_cont_element计算得到,首先从op1、op2取出break、continue所在循环的zend_brk_cont_element结构以及要跳过的层级,然后根据zend_brk_cont_element.parent及层级数找到具体要跳出层的zend_brk_cont_element结构,从这个结构中获得那层循环判断条件及循环结束的opcode的位置。

static uint32_t zend_get_brk_cont_target(const zend_op_array *op_array, const zend_op *opline) {int nest_levels = opline->op2.num; //跳出的层级:break n;int array_offset = opline->op1.num;//break、continue所属循环zend_brk_cont_element的存储下标zend_brk_cont_element *jmp_to;do {//从break/continue所在循环层开始jmp_to = &op_array->brk_cont_array[array_offset];if (nest_levels > 1) {//如果还没到要跳出的层数则接着跳到上层array_offset = jmp_to->parent;}} while (--nest_levels > 0);return opline->opcode == ZEND_BRK ? jmp_to->brk : jmp_to->cont;
}

上面那个例子最终执行前的opcode如下图:

执行时直接跳到对应的opcode位置即可。

Note:

在多层循环中break、continue直接根据层级数字跳转很不方便,这点PHP可以借鉴Golang的语法:break/continue + LABEL,支持按标签break、continue,根据上一节及本节介绍的内容这一个实现起来并不复杂,有兴趣的可以思考下如何实现。


goto

goto 操作符可以用来跳转到程序中的另一位置。该目标位置可以用目标名称加上冒号来标记,而跳转指令是 goto 之后接上目标位置的标记。PHP 中的 goto 有一定限制,目标位置只能位于同一个文件和作用域,也就是说无法跳出一个函数或类方法,也无法跳入到另一个函数,可以跳出循环但无法跳入循环(可以在同一层循环中跳转),多层循环中通常会用goto代替多层break。

goto语法:

goto LABEL;LABEL:statement;

goto与label需要组合使用,其实现与break、continue类似,最终也是被优化为ZEND_JMP,首先看下定义一个label时都有哪些操作:

statement:...| T_STRING ':' { $$ = zend_ast_create(ZEND_AST_LABEL, $1); }
;

label的编译过程非常简单,与循环结构的编译类似,编译时会把label插入CG(context).labels哈希表中,key就是label名称,value是一个zend_label结构:

typedef struct _zend_label {int brk_cont; //当前label所在循环uint32_t opline_num; //下一条opcode位置
} zend_label;

brk_cont用于记录当前label所在的循环,这个值就是上面介绍的每个循环在zend_op_array->brk_cont_array数组中的位置;opline_num比较容易理解,就是label下面第一条opcode的位置。到这里你应该能猜得到goto的工作过程了,首先根据label名称在CG(context).labels查找到跳转label的zend_label结构,然后jmp到zend_label.opline_num的位置,brk_cont的作用是用来判断是不是goto到了另一层循环中去。label具体的编译过程:

void zend_compile_label(zend_ast *ast)
{ zend_string *label = zend_ast_get_str(ast->child[0]);zend_label dest;//编译时会将label插入CG(context).labels哈希表if (!CG(context).labels) {ALLOC_HASHTABLE(CG(context).labels);zend_hash_init(CG(context).labels, 8, NULL, label_ptr_dtor, 0);} //设置label信息:当前所在循环、下一条opcode编号dest.brk_cont = CG(context).current_brk_cont;dest.opline_num = get_next_op_number(CG(active_op_array));if (!zend_hash_add_mem(CG(context).labels, label, &dest, sizeof(zend_label))) {zend_error_noreturn(E_COMPILE_ERROR, "Label '%s' already defined", ZSTR_VAL(label));}
}

goto的编译过程:

void zend_compile_goto(zend_ast *ast)
{zend_ast *label_ast = ast->child[0];znode label_node;zend_op *opline;uint32_t opnum_start = get_next_op_number(CG(active_op_array));zend_compile_expr(&label_node, label_ast);//如果当前在一个循环内则有的情况下是不能简单跳出循环的zend_handle_loops_and_finally();//编译一条临时opcode:ZEND_GOTOopline = zend_emit_op(NULL, ZEND_GOTO, NULL, &label_node);opline->op1.num = get_next_op_number(CG(active_op_array)) - opnum_start - 1;opline->extended_value = CG(context).current_brk_cont;
}

goto初步被编译为ZEND_GOTO,其中label名称保存在op2,extended_value记录的是goto所在循环,如果没有在循环中这个值就等于-1,op1比较特殊,从上面编译的过程分析,它的值等于goto之间的opcode数,goto只编译了一条ZEND_GOTO哪来的其他opcode呢?这种情况就是goto在一个循环中,上一节介绍的循环结构中有一个比较特殊:foreach,它在遍历前会新生成一个zval用于遍历,这个zval是在循环结束时才被释放,假如foreach循环体中执行了goto,直接像普通跳转一样跳到了别的位置,那么这个zval就无法释放了,所以这种情况下在goto跳转前需要先执行这些收尾的opcode,这些opcode就是上面zend_handle_loops_and_finally()编译的,具体的细节这里不再展开,有兴趣的可以仔细研究下foreach编译时zend_begin_loop()的特殊处理。

后面的处理就与break、continue一样了,在pass_two()中ZEND_GOTO被重置为ZEND_JMP,具体的处理过程在zend_resolve_goto_label(),比较简单,不再赘述。


推荐阅读
  • 先看官方文档TheJavaTutorialshavebeenwrittenforJDK8.Examplesandpracticesdescribedinthispagedontta ... [详细]
  • 本文介绍了C#中数据集DataSet对象的使用及相关方法详解,包括DataSet对象的概述、与数据关系对象的互联、Rows集合和Columns集合的组成,以及DataSet对象常用的方法之一——Merge方法的使用。通过本文的阅读,读者可以了解到DataSet对象在C#中的重要性和使用方法。 ... [详细]
  • 本文介绍了iOS数据库Sqlite的SQL语句分类和常见约束关键字。SQL语句分为DDL、DML和DQL三种类型,其中DDL语句用于定义、删除和修改数据表,关键字包括create、drop和alter。常见约束关键字包括if not exists、if exists、primary key、autoincrement、not null和default。此外,还介绍了常见的数据库数据类型,包括integer、text和real。 ... [详细]
  • Java中包装类的设计原因以及操作方法
    本文主要介绍了Java中设计包装类的原因以及操作方法。在Java中,除了对象类型,还有八大基本类型,为了将基本类型转换成对象,Java引入了包装类。文章通过介绍包装类的定义和实现,解答了为什么需要包装类的问题,并提供了简单易用的操作方法。通过本文的学习,读者可以更好地理解和应用Java中的包装类。 ... [详细]
  • JDK源码学习之HashTable(附带面试题)的学习笔记
    本文介绍了JDK源码学习之HashTable(附带面试题)的学习笔记,包括HashTable的定义、数据类型、与HashMap的关系和区别。文章提供了干货,并附带了其他相关主题的学习笔记。 ... [详细]
  • 海马s5近光灯能否直接更换为H7?
    本文主要介绍了海马s5车型的近光灯是否可以直接更换为H7灯泡,并提供了完整的教程下载地址。此外,还详细讲解了DSP功能函数中的数据拷贝、数据填充和浮点数转换为定点数的相关内容。 ... [详细]
  • Week04面向对象设计与继承学习总结及作业要求
    本文总结了Week04面向对象设计与继承的重要知识点,包括对象、类、封装性、静态属性、静态方法、重载、继承和多态等。同时,还介绍了私有构造函数在类外部无法被调用、static不能访问非静态属性以及该类实例可以共享类里的static属性等内容。此外,还提到了作业要求,包括讲述一个在网上商城购物或在班级博客进行学习的故事,并使用Markdown的加粗标记和语句块标记标注关键名词和动词。最后,还提到了参考资料中关于UML类图如何绘制的范例。 ... [详细]
  • C# 7.0 新特性:基于Tuple的“多”返回值方法
    本文介绍了C# 7.0中基于Tuple的“多”返回值方法的使用。通过对C# 6.0及更早版本的做法进行回顾,提出了问题:如何使一个方法可返回多个返回值。然后详细介绍了C# 7.0中使用Tuple的写法,并给出了示例代码。最后,总结了该新特性的优点。 ... [详细]
  • 1,关于死锁的理解死锁,我们可以简单的理解为是两个线程同时使用同一资源,两个线程又得不到相应的资源而造成永无相互等待的情况。 2,模拟死锁背景介绍:我们创建一个朋友 ... [详细]
  • PHP中的单例模式与静态变量的区别及使用方法
    本文介绍了PHP中的单例模式与静态变量的区别及使用方法。在PHP中,静态变量的存活周期仅仅是每次PHP的会话周期,与Java、C++不同。静态变量在PHP中的作用域仅限于当前文件内,在函数或类中可以传递变量。本文还通过示例代码解释了静态变量在函数和类中的使用方法,并说明了静态变量的生命周期与结构体的生命周期相关联。同时,本文还介绍了静态变量在类中的使用方法,并通过示例代码展示了如何在类中使用静态变量。 ... [详细]
  • 后台获取视图对应的字符串
    1.帮助类后台获取视图对应的字符串publicclassViewHelper{将View输出为字符串(注:不会执行对应的ac ... [详细]
  • 猜字母游戏
    猜字母游戏猜字母游戏——设计数据结构猜字母游戏——设计程序结构猜字母游戏——实现字母生成方法猜字母游戏——实现字母检测方法猜字母游戏——实现主方法1猜字母游戏——设计数据结构1.1 ... [详细]
  • 前景:当UI一个查询条件为多项选择,或录入多个条件的时候,比如查询所有名称里面包含以下动态条件,需要模糊查询里面每一项时比如是这样一个数组条件:newstring[]{兴业银行, ... [详细]
  • Iamtryingtocreateanarrayofstructinstanceslikethis:我试图创建一个这样的struct实例数组:letinstallers: ... [详细]
  • 本文介绍了一种图的存储和遍历方法——链式前向星法,该方法在存储带边权的图时时间效率比vector略高且节省空间。然而,链式前向星法存图的最大问题是对一个点的出边进行排序去重不容易,但在平行边无所谓的情况下选择这个方法是非常明智的。文章还提及了图中搜索树的父子关系一般不是很重要,同时给出了相应的代码示例。 ... [详细]
author-avatar
加勒比小洁_149
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有