原文链接:http://www.orlion.ga/941/

原文:http://www.nowamagic.net/librarys/veda/detail/1543

假如我们现在使用的是CLI模式,直接在SAPI/cli/php_cli.c文件中找到main函数, 默认情况下PHP的CLI模式的行为模式为PHP_MODE_STANDARD。 此行为模式中PHP内核会调用php_execute_script(&file_handle TSRMLS_CC);来执行PHP文件。 顺着这条执行的线路,可以看到一个PHP文件在经过词法分析,语法分析,编译后生成中间代码的过程:

1 EG(active_op_array) = zend_compile_file(file_handle, type TSRMLS_CC);

在销毁了文件所在的handler后,如果存在中间代码,则PHP虚拟机将通过以下代码执行中间代码:

1 zend_execute(EG(active_op_array) TSRMLS_CC);

如果你是使用VS查看源码的话,将光标移到zend_execute并直接按F12, 你会发现zend_execute的定义跳转到了一个指针函数的声明(Zend/zend_execute_API.c)。

1 ZEND_API void (*zend_execute)(zend_op_array *op_array TSRMLS_DC);

这是一个全局的函数指针,它的作用就是执行PHP代码文件解析完的转成的zend_op_array。 和zend_execute相同的还有一个zedn_execute_internal函数,它用来执行内部函数。 在PHP内核启动时(zend_startup)时,这个全局函数指针将会指向execute函数。 注意函数指针前面的修饰符ZEND_API,这是ZendAPI的一部分。 在zend_execute函数指针赋值时,还有PHP的中间代码编译函数zend_compile_file(文件形式)和zend_compile_string(字符串形式)。

1 zend_compile_file = compile_file;
2 zend_compile_string = compile_string;
3 zend_execute = execute;
4 zend_execute_internal = NULL;
5 zend_throw_exception_hook = NULL;

这几个全局的函数指针均只调用了系统默认实现的几个函数,比如compile_file和compile_string函数, 他们都是以全局函数指针存在,这种实现方式在PHP内核中比比皆是,其优势在于更低的耦合度,甚至可以定制这些函数。 比如在APC等opcode优化扩展中就是通过替换系统默认的zend_compile_file函数指针为自己的函数指针my_compile_file, 并且在my_compile_file中增加缓存等功能。

到这里我们找到了中间代码执行的最终函数:execute(Zend/zend_vm_execure.h)。 在这个函数中所有的中间代码的执行最终都会调用handler。这个handler是什么呢?

1 if ((ret = EX(opline)->handler(execute_data TSRMLS_CC)) > 0) {
2 }

这里的handler是一个函数指针,它指向执行该opcode时调用的处理函数。 此时我们需要看看handler函数指针是如何被设置的。 在前面我们有提到和execute一起设置的全局指针函数:zend_compile_string。 它的作用是编译字符串为中间代码。在Zend/zend_language_scanner.c文件中有compile_string函数的实现。 在此函数中,当解析完中间代码后,一般情况下,它会执行pass_two(Zend/zend_opcode.c)函数。 pass_two这个函数,从其命名上真有点看不出其意义是什么。 但是我们关注的是在函数内部,它遍历整个中间代码集合, 调用ZEND_VM_SET_OPCODE_HANDLER(opline);为每个中间代码设置处理函数。 ZEND_VM_SET_OPCODE_HANDLER是zend_vm_set_opcode_handler函数的接口宏, zend_vm_set_opcode_handler函数定义在Zend/zend_vm_execute.h文件。 其代码如下:

01 static opcode_handler_t zend_vm_get_opcode_handler(zend_uchar opcode, zend_op* op)
02 {
03         static const int zend_vm_decode[] = {
04             _UNUSED_CODE, /* 0              */
05             _CONST_CODE,  /* 1 = IS_CONST   */
06             _TMP_CODE,    /* 2 = IS_TMP_VAR */
07             _UNUSED_CODE, /* 3              */
08             _VAR_CODE,    /* 4 = IS_VAR     */
09             _UNUSED_CODE, /* 5              */
10             _UNUSED_CODE, /* 6              */
11             _UNUSED_CODE, /* 7              */
12             _UNUSED_CODE, /* 8 = IS_UNUSED  */
13             _UNUSED_CODE, /* 9              */
14             _UNUSED_CODE, /* 10             */
15             _UNUSED_CODE, /* 11             */
16             _UNUSED_CODE, /* 12             */
17             _UNUSED_CODE, /* 13             */
18             _UNUSED_CODE, /* 14             */
19             _UNUSED_CODE, /* 15             */
20             _CV_CODE      /* 16 = IS_CV     */
21         };
22         return zend_opcode_handlers[opcode * 25
23                 + zend_vm_decode[op->op1.op_type] * 5
24                 + zend_vm_decode[op->op2.op_type]];
25 }
26  
27 ZEND_API void zend_vm_set_opcode_handler(zend_op* op)
28 {
29     op->handler = zend_vm_get_opcode_handler(zend_user_opcodes[op->opcode], op);
30 }

前面介绍了四种查找opcode处理函数的方法, 而根据其本质实现查找也在其中,只是这种方法对于计算机来说比较容易识别,而对于自然人来说却不太友好。 比如一个简单的A + B的加法运算,如果你想用这种方法查找其中间代码的实现位置的话, 首先你需要知道中间代码的代表的值,然后知道第一个表达式和第二个表达式结果的类型所代表的值, 然后计算得到一个数值的结果,然后从数组zend_opcode_handlers找这个位置,位置所在的函数就是中间代码的函数。 这对阅读代码的速度没有好处,但是在开始阅读代码的时候根据代码的逻辑走这样一个流程却是大有好处。

回到正题。 handler所指向的方法基本都存在于Zend/zend_vm_execute.h文件文件。 知道了handler的由来,我们就知道每个opcode调用handler指针函数时最终调用的位置。

在opcode的处理函数执行完它的本职工作后,常规的opcode都会在函数的最后面添加一句:ZEND_VM_NEXT_OPCODE();。 这是一个宏,它的作用是将当前的opcode指针指向下一条opcode,并且返回0。如下代码:

1 #define ZEND_VM_NEXT_OPCODE() \
2 CHECK_SYMBOL_TABLES() \
3 EX(opline)++; \
4 ZEND_VM_CONTINUE()
5  
6 #define ZEND_VM_CONTINUE()   return 0

在execute函数中,处理函数的执行是在一个while(1)循环作用范围中。如下:

01 while (1) {
02         int ret;
03 #ifdef ZEND_WIN32
04         if (EG(timed_out)) {
05             zend_timeout(0);
06         }
07 #endif
08  
09         if ((ret = EX(opline)->handler(execute_data TSRMLS_CC)) > 0) {
10             switch (ret) {
11                 case 1:
12                     EG(in_execution) = original_in_execution;
13                     return;
14                 case 2:
15                     op_array = EG(active_op_array);
16                     goto zend_vm_enter;
17                 case 3:
18                     execute_data = EG(current_execute_data);
19                 default:
20                     break;
21             }
22         }
23  
24     }

前面说到每个中间代码在执行完后都会将中间代码的指针指向下一条指令,并且返回0。 当返回0时,while 循环中的if语句都不满足条件,从而使得中间代码可以继续执行下去。 正是这个while(1)的循环使得PHP内核中的opcode可以从第一条执行到最后一条, 当然这中间也有一些函数的跳转或类方法的执行等。

以上是一条中间代码的执行,那么对于函数的递归调用,PHP内核是如何处理的呢? 看如下一段PHP代码:

1 function t($c) {
2     echo $c"\n";
3     if ($c > 2) {
4             return ;
5     }
6     t($c + 1);
7 }
8 t(1);

这是一个简单的递归调用函数实现,它递归调用了两次,这个递归调用是如何进行的呢? 我们知道函数的调用所在的中间代码最终是调用zend_do_fcall_common_helper_SPEC(Zend/zend_vm_execute.h)。 在此函数中有如下一段:

1 if (zend_execute == execute && !EG(exception)) {
2     EX(call_opline) = opline;
3     ZEND_VM_ENTER();
4 else {
5     zend_execute(EG(active_op_array) TSRMLS_CC);
6 }

前面提到zend_execute API可能会被覆盖,这里就进行了简单的判断,如果扩展覆盖了opcode执行函数, 则进行特殊的逻辑处理。

上一段代码中的ZEND_VM_ENTER()定义在Zend/zend_vm_execute.h的开头,如下:

1 #define ZEND_VM_CONTINUE()   return 0
2 #define ZEND_VM_RETURN()     return 1
3 #define ZEND_VM_ENTER()      return 2

【转】中间代码opcode的执行的更多相关文章

  1. 存储opline的内存地址可以实时跟踪opcode的执行

    static intphp_handler(request_rec *r) { /* Initiliaze the context */ php_struct * volatile ctx; void ...

  2. opcode的执行

    原文链接:http://www.orlion.ga/1001/ 当.php文件被编译为opcode后,下一步的执行并非是把opcode编译为机器码而是类似于如下的方式执行: while (TRUE)  ...

  3. php-5.6.26源代码 - PHP文件汇编成opcode、执行

    文件 php-5.6.26/Zend/zend.c ZEND_API int zend_execute_scripts(int type TSRMLS_DC, zval **retval, int f ...

  4. php opcode

    opcode是计算机指令中的一部分,用于指定要执行的操作, 指令的格式和规范由处理器的指令规范指定. 除了指令本身以外通常还有指令所需要的操作数,可能有的指令不需要显式的操作数. 这些操作数可能是寄存 ...

  5. php内核探索 [转]

    PHP内核探索:从SAPI接口开始 PHP内核探索:一次请求的开始与结束 PHP内核探索:一次请求生命周期 PHP内核探索:单进程SAPI生命周期 PHP内核探索:多进程/线程的SAPI生命周期 PH ...

  6. PHP内核探索:哈希碰撞攻击是什么?

    最近哈希表碰撞攻击(Hashtable collisions as DOS attack)的话题不断被提起,各种语言纷纷中招.本文结合PHP内核源码,聊一聊这种攻击的原理及实现. 哈希表碰撞攻击的基本 ...

  7. PHP服务器脚本 PHP内核探索:新垃圾回收机制说明

    在5.2及更早版本的PHP中,没有专门的垃圾回收器GC(Garbage Collection),引擎在判断一个变量空间是否能够被释放的时候是依据这个变量的zval的refcount的值,如果refco ...

  8. 《PHP内核探索系列文章》系列分享专栏

    <PHP内核探索系列文章>已整理成PDF文档,点击可直接下载至本地查阅 简介 PHP内核探索系列文章收藏夹收藏有关PHP内核方面的知识的文章,对PHP高级进阶的朋友提供PHP内核方面的知识 ...

  9. PHP扩展编写、PHP扩展调试、VLD源码分析、基于嵌入式Embed SAPI实现opcode查看

    catalogue . 编译PHP源码 . 扩展结构.优缺点 . 使用PHP原生扩展框架wizard ext_skel编写扩展 . 编译安装VLD . Debug调试VLD . VLD源码分析 . 嵌 ...

随机推荐

  1. 数据库软件dbForge Studio for MySQL更新至v.6.1

    本文转自:慧都控件网 说到MariaDB,这个数据库算是MySQL的一个分支.现在非常的流行,很多地方都能看到它的身影.MariaDB作为一种新的数据库管理系统,在短时间内获得如此高的关注度.这也是D ...

  2. 小游戏runpig总结

    前几天写了一个JavaScript小游戏,大概是这样的 demo:strongfanfan.top/RunPig  源代码:www.github.com/strongfanfan/RunPig 画风简 ...

  3. VIM使用(二) 浏览内核源代码

    为了实现类似SourceInsight功能,通过VIM+Ctags+Cscope+Taglist+Source Explore +NERD Tree实现. 一, 安装插件 1)安装Ctags 和Csc ...

  4. 书TO BE READED

    Books Created Wednesday 10 August 2011 1.<你的降落伞是什么颜色> 在求职书里面,我觉得这本书是最接近于圣经的. 第一版出现大概还是上世纪70年代末 ...

  5. Java jstatd详解

    jstatd 命令全称: Virtual Machine jstat Daemon DESCRIPTION The  jstatd tool is an RMI server application ...

  6. Centos6.6下安装MariaDB步骤,利用yum进行安装 第二篇

    一.安装过程参考的相关文章: Centos 使用YUM安装MariaDB CentOS安装并设置MariaDB CentOS用yum安装.配置MariaDB MariaDB远程连接配置 ERROR 1 ...

  7. [后端人员耍前端系列]KnockoutJs篇:快速掌握KnockoutJs

    一.引言 之前这个系列文章已经介绍Bootstrap.由于最近项目中,前端是Asp.net MVC + KnockoutJs + Bootstrap来做的.所以我又重新开始写这个系列.今天就让我们来看 ...

  8. 在Linux上以服务的方式运行ASP.NET Core站点

    更新:用supervisor是更好的解决方法,详见 Linux下为 dotnet 创建守护进程 要在生成环境下在Linux服务器上跑ASP.NET Core站点,首先要解决的问题是以服务的方式运行AS ...

  9. 犀利的background-clip:text,实现K歌字幕效果

    今天学到了一个新的CSS3属性,更准确的说是属性值,那就是background-clip:text.利用此属性值可以制作出很神奇的效果.可惜只有chrome支持,不过今天可以先来玩玩这个属性. 先来介 ...

  10. FusionCharts简单教程(三)-----如何自定义图表上的工具提示

    最近有蛮多人总是问我这个FusionCharts制表的问题,帮助他们解决之后,在昨晚发现以前整理的笔记中有这个简单教程,而且以前也发表了几篇这个博文,所以就将其全部上传上来供别人参考.如有不正确之处望 ...