原文链接: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. 使用CSS中的meta实现web定时刷新或跳转的方法

    这篇文章主要介绍了使用CSS中的meta实现web定时刷新或跳转的方法,比使用JavaScript脚本实现起来更加简单一些,需要的朋友可以参考下 meta源信息功能之页面定时跳转与刷新 几乎所有的网页 ...

  2. 第五章GPIO接口

    5.1 GPIO硬件介绍 可以不通过他们输出高低电平或者通过它们读入应交的状态 S3C2410有117个I/O端口,分为A~H共8组:GPA.GPB....GPH S3C2440有130个I/O端口, ...

  3. jackson报错 无法解析,但是json一切正常

    因为类里面缺少无参构造(被有参构造盖掉了)

  4. (转)Android开发出来的APP在手机的安装路径是?

    一.安装路径在哪? Android应用安装涉及到如下几个目录: system/app系统自带的应用程序,无法删除.data/app用户程序安装的目录,有删除权限.安装时把apk文件复制到此目录.dat ...

  5. mono 开发

    引用 segmentfault.com/a/1190000002449629 配置 ASP.NET Linux( CentOS 6.5 ) 运行环境 MONO + Jexus me15000 179 ...

  6. 《Memcache学习总结》[PDF]发布

    <Memcache学习总结>[PDF]发布 百度网盘共享: http://pan.baidu.com/s/1mgvayQO  版本号: V1.2 最后跟新: 2015-04-01 讨论组: ...

  7. Linux环境变量配置

    /etc/profile:此文件为系统的每个用户设置环境信息,当用户第一次登录时,该文件被执行.并从/etc/profile.d目录的配置文件中搜集shell的设置./etc/bashrc:为每一个运 ...

  8. 《利用Python进行数据分析》第7章学习笔记

    数据规整化:清理.转换.合并.重塑 合并数据集 pandas.merge pandas.concat combine_first 数据库风格的DataFrame合并 索引上的合并 join()实例方法 ...

  9. 让Entity Framework启动不再效验__MigrationHistory表

    Entity Framework中DbContext首次加载OnModelCreating会检查__MigrationHistory表,作为使用Code Frist编程模式,而实际先有数据库时,这种检 ...

  10. Aoite 系列(03) - 一起来 Redis 吧!

    Aoite 是一个适于任何 .Net Framework 4.0+ 项目的快速开发整体解决方案.Aoite.Data 适用于市面上大多数的数据库提供程序,通过统一封装,可以在日常开发中简单便捷的操作数 ...