原文链接: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) { |
这里的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) |
03 |
static const int zend_vm_decode[] = { |
22 |
return zend_opcode_handlers[opcode * 25 |
23 |
+ zend_vm_decode[op->op1.op_type] * 5 |
24 |
+ zend_vm_decode[op->op2.op_type]]; |
27 |
ZEND_API void zend_vm_set_opcode_handler(zend_op* op) |
29 |
op->handler = zend_vm_get_opcode_handler(zend_user_opcodes[op->opcode], op); |
前面介绍了四种查找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() |
6 |
#define ZEND_VM_CONTINUE() return 0 |
在execute函数中,处理函数的执行是在一个while(1)循环作用范围中。如下:
09 |
if ((ret = EX(opline)->handler(execute_data TSRMLS_CC)) > 0) { |
12 |
EG(in_execution) = original_in_execution; |
15 |
op_array = EG(active_op_array); |
18 |
execute_data = EG(current_execute_data); |
前面说到每个中间代码在执行完后都会将中间代码的指针指向下一条指令,并且返回0。 当返回0时,while 循环中的if语句都不满足条件,从而使得中间代码可以继续执行下去。 正是这个while(1)的循环使得PHP内核中的opcode可以从第一条执行到最后一条, 当然这中间也有一些函数的跳转或类方法的执行等。
以上是一条中间代码的执行,那么对于函数的递归调用,PHP内核是如何处理的呢? 看如下一段PHP代码:
这是一个简单的递归调用函数实现,它递归调用了两次,这个递归调用是如何进行的呢? 我们知道函数的调用所在的中间代码最终是调用zend_do_fcall_common_helper_SPEC(Zend/zend_vm_execute.h)。 在此函数中有如下一段:
1 |
if (zend_execute == execute && !EG(exception)) { |
2 |
EX(call_opline) = opline; |
5 |
zend_execute(EG(active_op_array) TSRMLS_CC); |
前面提到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
|