Press "Enter" to skip to content

深入浅出PHP(Exploring PHP)

一直以来,横观国内的PHP现状,很少有专门介绍PHP内部机制的书。呵呵,我会随时记录下研究的心得,有机会的时候,汇总成书。:)
今天这篇,我内心是想打算做为一个导论:
PHP是一个被广泛应用的脚本语言,因为它的成功,所以很多时候,我们应用PHP的时候是更不不需要考虑底层到底是怎么实现的。我相信大多数的PHP程序 员是不会去考虑这一点的。从我接触PHP开始,到今天也就是3年,这三年里,前俩年我一直都是在"用"PHP,每次写出来一段脚本,我就会想“恩,不用担 心,PHP解释器会知道我想做什么的”,直到去年来到雅虎,接受了一个工作,是做一个PHP的Extension,从这个时候开始,我就好奇于新接触的一 大堆的新鲜事物,zend, TSRM, zval, hashtable, op_array…
于是我到处查阅资料,每次获得一篇好的文章,或者一段好的文字我就会如获珍宝,打印保存起来,细细研读。我发现,国内关于PHP内部的资料真是少的可怜, 不知道是因为懂得的人多但是不愿意分享,还是懂得的人本来就少,所以,这条路,我走的很辛苦。于是,就会有了这篇文章。
在这篇文章中,我会从整个PHP的执行期入手,大致的介绍下各个阶段,词法分析,语法分析,op code等等,以后的文章我会再详细介绍每个阶(当然,如果你急不可耐的想知道详细,呵呵,那么可以直接联系我)。
从最初我们编写的PHP脚本->到最后脚本被执行->得到执行结果,这个过程,其实可以分为如下几个阶段(鄙视:CSDN不能上图):
首先,Zend Engine(ZE),调用词法分析器(Lex生成的,源文件在 Zend/zend_language_sanner.l), 将我们要执行的PHP源文件,去掉空格 ,注释,分割成一个一个的token。
然后,ZE会将得到的token forward给语法分析器(yacc生成, 源文件在 Zend/zend_language_parser.y),生成一个一个的op code,opcode一般会以op array的形式存在,它是PHP执行的中间语言。
最后,ZE调用zend_executor来执行op array,输出结果。


图1 处理流程

ZE是一个虚拟机,正是由于它的存在,所以才能使得我们写PHP脚本,完全不需要考虑所在的操作系统类型是什么。ZE是一个CISC(复杂指令处理器), 它支持150条指令(具体指令在 Zend/zend_vm_opcodes.h),包括从最简单的ZEND_ECHO(echo)到复杂的 ZEND_INCLUDE_OR_EVAL(include,require),所有我们编写的PHP都会最终被处理为这150条指令(op code)的序列,从而最终被执行。
那有什么办法可以看到我们的PHP脚本,最终被“翻译”成什么样的呢? 也就是说,op code张的什么样子呢? 呵呵,达到这个,我们需要重新编译PHP,修改它的compile_file和zend_execute函数。不过,在PECL中已经有这样的模块,可以 让我们直接使用了,那就是由 Derick Rethans开发的VLD (Vulcan Logic Dissassembler)模块。你只要下载这个模块,并把他载入PHP中,就可以通过简单的设置,来得到脚本翻译的结果了。具体关于这个模块的使用说 明-雅虎一下,你就知道^_^。
接下来,让我们尝试用VLD来查看一段简单的PHP脚本的中间语言。
原始代码:

<?php
$i = "This is a string";
//I am comments
echo $i. that has been echoed to screen;
?>

采用VLD得到的op codes:

filename:/home/Desktop/vldOutOne.php
function name: (null)
number of ops: 7
line #  op                 fetch       ext  operands
-------------------------------------------------------------------------------------------------------------------------------
2 0 FETCH_W local $0, 'i'
1 ASSIGN $0, 'This+is+a+string'
4 2 FETCH_R local $2, 'i'
3 CONCAT ~3, $2,
'+that+has+been+echoed+to+screen'
4 ECHO ~3
6 5 RETURN 1
6 ZEND_HANDLE_EXCEPTION

我们可以看到,源文件中的注释,在op code中,已经没有了,所以不用担心注释太多会影响你的脚本执行时间(实际上,它是会影响ZE的词法处理阶段的用时而已)。
现在我们来一条一条的分析这段op codes,每一条op code 又叫做一条op_line,都由如下7个部分,在zend_compile.h中,我们可以看到如下定义:

struct _zend_op {
opcode_handler_t handler;
znode result;
znode op1;
znode op2;
ulong extended_value;
uint lineno;
zend_uchar opcode;
};

其中,opcode字段指明了这操作类型,handler指明了处理器,然后有俩个操作数,和一个操作结果。

  1. FETCH_W, 是以写的方式获取一个变量,此处是获取变量名”i”的变量于$0(*zval)。
  2. 将字符串”this+is+a+string”赋值(ASSIGN)给$0
  3. 字符串连接
  4. 显示

可以看出,这个很类似于很多同学大学学习编译原理时候的三元式,不同的是,这些中间代码会被Zend VM(Zend虚拟机)直接执行。

真正负责执行的函数是,zend_execute, 查看zend_execute.h:

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

可以看出, zend_execute接受zend_op_array*作为参数。

 struct _zend_op_array {
    /* Common elements */
    zend_uchar type;
    char *function_name;
    zend_class_entry *scope;
    zend_uint fn_flags;
    union _zend_function *prototype;
    zend_uint num_args;
    zend_uint required_num_args;
    zend_arg_info *arg_info;
    zend_bool pass_rest_by_reference;
    unsigned char return_reference;
    /* END of common elements */
    zend_uint *refcount;
    zend_op *opcodes;
    zend_uint last, size;
    zend_compiled_variable *vars;
    int last_var, size_var;
    zend_uint T;
    zend_brk_cont_element *brk_cont_array;
    zend_uint last_brk_cont;
    zend_uint current_brk_cont;
    zend_try_catch_element *try_catch_array;
    int last_try_catch;
    /* static variables support */
    HashTable *static_variables;
    zend_op *start_op;
    int backpatch_count;
    zend_bool done_pass_two;
    zend_bool uses_this;
    char *filename;
    zend_uint line_start;
    zend_uint line_end;
    char *doc_comment;
    zend_uint doc_comment_len;
    void *reserved[ZEND_MAX_RESERVED_RESOURCES];
};

可以看到,zend_op_array的结构和zend_function的结构很像(参看我的其他文章), 对于在全局作用域的代码,就是不包含在任何function内的op_array,它的function_name为NULL。结构中的opcodes保存了属于这个op_array的op code数组,zend_execute会从start_op开始,逐条解释执行传入的每条op code, 从而实现我们PHP脚本想要的结果。

下一次,我将介绍PHP变量的灵魂 - zval, 你将会看到PHP是如何实现它的变量传递,类型戏法,等等。

38 Comments

  1. newbee
    newbee November 23, 2017

    现在是2017年11月23日 17:23:44,鸟哥的书还没出不单止,连博客也停更了。。

  2. shyandsy
    shyandsy August 30, 2015

    看到这篇文章时候已经173条指令了

  3. […] opcode是php的中间语言.(要理解php的执行机制可以戳鸟哥的深入理解php) 而如何查看opcode?那就是vld(Vulcan Logic Dumper)这款php扩展了。默认的php是没有提供这一扩展的,本文所要描述的是如何安装vld。我的环境是archlinux,别的环境类似(路径会有差异) […]

  4. xiaobao
    xiaobao August 26, 2010

    上面那张图对吗,鸟哥?
    require或者函数是编译的时候一并编译了,还是等到执行的时候,再回头去编译?好像应该是前者吧。

  5. google
    google July 29, 2010

    翻山越岭终于找到这篇好文章

  6. mahone
    mahone July 23, 2010

    不是计算机专业,没学过编译原理和汇编,看不懂文章……可悲……

  7. mahone
    mahone July 23, 2010

    非常可惜啊,不是计算机专业,编译原理和汇编都没学过……悲剧啊……看不懂文章

  8. 人海孤鸿
    人海孤鸿 May 28, 2010

  9. phper
    phper May 15, 2010

    博主的文章写得太好了!偶像,我要长期关注

  10. xiaokai
    xiaokai February 1, 2010

    很是深奥, 但是总一天我会弄懂的..

  11. zhifu
    zhifu October 9, 2009

    强人,我也做了快一年的PHPER,最近在写项目,总是遇到很棘手的问题,导致最后学得去研究底层,现在才发现,要想把技术做好,真得研究底层。
    还是很感谢,楼主有这样的精力去研究,并开源给我们,小弟真的十分感谢呀

  12. 不是什么区
    不是什么区 May 30, 2009

    不错,既然有现成了我就懒得写了,不介意我转走了吧?

  13. dyh1919
    dyh1919 May 6, 2009

    编译原理没学好,看不懂

  14. guangtianxia
    guangtianxia December 7, 2008

    支持这种精神。

  15. smilesoul
    smilesoul November 20, 2008

    老大啊 你是我的偶像啊 你怎么学这么深 佩服佩服!
    能不能指点下怎么学写一个php的c扩展
    需要具备学习哪些知识,能不能谈谈经验

  16. highjade
    highjade October 13, 2008

    学习~

  17. sue
    sue September 8, 2008

    传说中的“右手诗”在哪里啊。。

  18. glemir
    glemir August 25, 2008

    冰之河说:“要坚持,俗话说:一个人,吟一首诗并不难;难的是,吟一辈子诗”
    xinquan的目标是要吟一辈子“唐诗”。

  19. liexusong
    liexusong August 24, 2008

    你出PHP内核的书的话,我一定买!!

  20. Leftwater
    Leftwater August 23, 2008

    决定长期阅读学习博主文章~

  21. lffly
    lffly August 18, 2008

    写的不错。订阅了你的博客

  22. yongtao.pang
    yongtao.pang August 13, 2008

    看zval去啦,有人劈荆斩棘真好

  23. 雪候鸟
    雪候鸟 August 12, 2008

    恩,有功夫我就写点,呵呵;)

  24. 冰的河
    冰的河 August 11, 2008

    不错,尤其是右手代码,左手诗,很赞。要坚持,俗话说:一个人,吟一首诗并不难;难的是,吟一辈子诗。

  25. xinquan
    xinquan August 11, 2008

    en, not bad … continue …

Leave a Reply

Your email address will not be published. Required fields are marked *