很多人都想看PHP的源码,但是由于缺乏正确的方法,浅尝辄止,止步于想法。今天我们就浅谈一下(我研究也不深,只是把我所知道的与大家探讨一下)。
第一步:下载源码:https://github.com/php/php-src
第二步:编译:
如何编译PHP请查阅官方文档
编译的时候请添加–debug=1的参数,这样才能更好的调试代码,不加的话,很多地方打断点无效,应该是被编译器优化掉了。
第三步:安装编辑器:
推荐clion,真正的高手都是用vim写c代码,查看c代码。我等不属于那一类人,还是老老实实用编辑器,个人看了好多个编辑器,感觉clion是最好的。
不过用clion存在一个问题,就是它吧cmake作为代码结构的解析工具,所以没有CMakeLists.txt的项目,它是没法去解析的,也就是找不到代码追踪关系,只能硬看。不过可以自己添加一个CMakeLists.txt。
大家可以看一下我整理的CMakeLists.txthttps://github.com/he1016060110/php-src/blob/learn/CMakeLists.txt
第四步:熟悉代码调试工具lldb和gdb
mac下的gdb不好用,用lldb
如果在linux下调试代码,请使用gdb
使用教程如下:https://linuxtools-rst.readthedocs.io/zh_CN/latest/tool/gdb.html
php为了方便gdb调试,专门给gdb写了一些便捷的方法,执行gdb的时候source一下就行。
文件是:https://github.com/he1016060110/php-src/blob/master/.gdbinit
第五步:了解代码结构和调试
php7的代码编译执行是根据下面的流程进行的,先用flex和bison将PHP代码进行词法和语法分析,得到一颗抽象语法树,然后再编译语法树得到opcode,然后在顺序执行opcode
![4f617504d0ef915613b1eed98d1c3ca3.png](https://i-blog.csdnimg.cn/blog_migrate/50255e0b238c42cae4c0613c1585c84e.jpeg)
接下来我们一步步调试PHP代码,看看每一个步骤的代码该如果去调试,为了简单,我们使用php cli模式来调试(单进程好调试,fpm至少得有两个进程,一个master一个worker)
介绍一下代码的目录结构:
Zend:zend 引擎最核心的代码:比如编译、数组,对象的实现
main:一些最基础的关于协议之类的代码:比如fastcgi的接口
ext:扩展的类或者方法,比如pdo,pcre等等,里面有个目录非常特殊,standard,许多很多核心函数也保存在里面。
我们先来尝个鲜
比如var_dump,我们编辑一个最简单的文件a.php
<?php
$a = 1;
var_dump($a);
![07421bbb8b107fad89bfb201bc9a5095.png](https://i-blog.csdnimg.cn/blog_migrate/cdfea901c92abf83134e257acd5e6371.png)
上面我们在zif_var_dump打了一个断点,执行到这个方法的时候,程序中断了,至于为什么是zif_前缀我们后面讲。我们找到var_dump的代码了,接着一步步调试即可。
![8c9460f8001574096f18436f1b0d7fd1.png](https://i-blog.csdnimg.cn/blog_migrate/99a9bc824d8b8b4950289510083221ad.jpeg)
gdb 执行p args[0]
![3b14bbd54db26f271a9c9863376f9f4e.png](https://i-blog.csdnimg.cn/blog_migrate/de6262ce495d3b3b23fd4f65500edfec.png)
得到zval的type为4,type为4的zval是什么呢,我么去看看zend_types.h,type为4的是long类型,属于int
![377cbb1a32feae08a16d30c9ce282000.png](https://i-blog.csdnimg.cn/blog_migrate/f65591e72f550909a3dca5c0433954e2.jpeg)
我们再看zval的结构
![632f102d4f38be82a45144cf70bd7ad9.png](https://i-blog.csdnimg.cn/blog_migrate/b1e82b8f9e078a642c50a25376d81ba2.jpeg)
type为long类型,我们就读取value元素的lval,于是在gdb里面执行:
p args[0].value.lval
![19a42844d38f3dd9ab694276ed550524.png](https://i-blog.csdnimg.cn/blog_migrate/e853c8cc387746a23511e6181c087fec.png)
得到要var_dump的对象$a的值为1
接下来我们来解释为什么是zif前缀了
![9fc670015b6888034ac8f7a19be2f913.png](https://i-blog.csdnimg.cn/blog_migrate/01f5a910dd916f561c1aa73d595e7df1.png)
![536da5527b974b68031bed5aaa97b518.png](https://i-blog.csdnimg.cn/blog_migrate/ae4bd289709a37e998d90671bd0dd555.png)
根据宏定义可以得知:
PHP_FUNCTION(var_dump)宏展开为:zif_var_dump
那么调试对象的方法是什么呢,比如PDOStatement 类的fetchAll方法
根据宏定义,我们可以知道这个方法在c语言里面为:
zim_PDOStatement_fetchAll,在这个方法打断点就行了
![8b8d4ee2a6d9b802bf5e5e8d2645c83f.png](https://i-blog.csdnimg.cn/blog_migrate/77aef6bb532a177ef17f1b3b040aaa1d.jpeg)
查看语法抽象树的底层结构
我们就使用刚刚的a.php来了解大概如果得到抽象语法树。
先看下语法树大概是个什么样的东西(画的难看见谅)
![a327b7b4faac455cb3ca5611dd6b105e.png](https://i-blog.csdnimg.cn/blog_migrate/b946fbd13ee6acda9e6f3e8ce92b091b.jpeg)
语法树叶子节点的种类在zend_ast.h里面定义的,具体内容如下
enum _zend_ast_kind {
/* special nodes */
ZEND_AST_ZVAL = 1 << ZEND_AST_SPECIAL_SHIFT,
ZEND_AST_ZNODE,
/* declaration nodes */
ZEND_AST_FUNC_DECL,
ZEND_AST_CLOSURE,
ZEND_AST_METHOD,
ZEND_AST_CLASS,
/* list nodes */
ZEND_AST_ARG_LIST = 1 << ZEND_AST_IS_LIST_SHIFT,
ZEND_AST_ARRAY,
ZEND_AST_ENCAPS_LIST,
ZEND_AST_EXPR_LIST,
ZEND_AST_STMT_LIST,
ZEND_AST_IF,
ZEND_AST_SWITCH_LIST,
ZEND_AST_CATCH_LIST,
ZEND_AST_PARAM_LIST,
ZEND_AST_CLOSURE_USES,
ZEND_AST_PROP_DECL,
ZEND_AST_CONST_DECL,
ZEND_AST_CLASS_CONST_DECL,
ZEND_AST_NAME_LIST,
ZEND_AST_TRAIT_ADAPTATIONS,
ZEND_AST_USE,
/* 0 child nodes */
ZEND_AST_MAGIC_CONST = 0 << ZEND_AST_NUM_CHILDREN_SHIFT,
ZEND_AST_TYPE,
/* 1 child node */
ZEND_AST_VAR = 1 << ZEND_AST_NUM_CHILDREN_SHIFT,
ZEND_AST_CONST,
ZEND_AST_UNPACK,
ZEND_AST_UNARY_PLUS,
ZEND_AST_UNARY_MINUS,
ZEND_AST_CAST,
ZEND_AST_EMPTY,
ZEND_AST_ISSET,
ZEND_AST_SILENCE,
ZEND_AST_SHELL_EXEC,
ZEND_AST_CLONE,
ZEND_AST_EXIT,
ZEND_AST_PRINT,
ZEND_AST_INCLUDE_OR_EVAL,
ZEND_AST_UNARY_OP,
ZEND_AST_PRE_INC,
ZEND_AST_PRE_DEC,
ZEND_AST_POST_INC,
ZEND_AST_POST_DEC,
ZEND_AST_YIELD_FROM,
ZEND_AST_GLOBAL,
ZEND_AST_UNSET,
ZEND_AST_RETURN,
ZEND_AST_LABEL,
ZEND_AST_REF,
ZEND_AST_HALT_COMPILER,
ZEND_AST_ECHO,
ZEND_AST_THROW,
ZEND_AST_GOTO,
ZEND_AST_BREAK,
ZEND_AST_CONTINUE,
/* 2 child nodes */
ZEND_AST_DIM = 2 << ZEND_AST_NUM_CHILDREN_SHIFT,
ZEND_AST_PROP,
ZEND_AST_STATIC_PROP,
ZEND_AST_CALL,
ZEND_AST_CLASS_CONST,
ZEND_AST_ASSIGN,
ZEND_AST_ASSIGN_REF,
ZEND_AST_ASSIGN_OP,
ZEND_AST_BINARY_OP,
ZEND_AST_GREATER,
ZEND_AST_GREATER_EQUAL,
ZEND_AST_AND,
ZEND_AST_OR,
ZEND_AST_ARRAY_ELEM,
ZEND_AST_NEW,
ZEND_AST_INSTANCEOF,
ZEND_AST_YIELD,
ZEND_AST_COALESCE,
ZEND_AST_STATIC,
ZEND_AST_WHILE,
ZEND_AST_DO_WHILE,
ZEND_AST_IF_ELEM,
ZEND_AST_SWITCH,
ZEND_AST_SWITCH_CASE,
ZEND_AST_DECLARE,
ZEND_AST_USE_TRAIT,
ZEND_AST_TRAIT_PRECEDENCE,
ZEND_AST_METHOD_REFERENCE,
ZEND_AST_NAMESPACE,
ZEND_AST_USE_ELEM,
ZEND_AST_TRAIT_ALIAS,
ZEND_AST_GROUP_USE,
/* 3 child nodes */
ZEND_AST_METHOD_CALL = 3 << ZEND_AST_NUM_CHILDREN_SHIFT,
ZEND_AST_STATIC_CALL,
ZEND_AST_CONDITIONAL,
ZEND_AST_TRY,
ZEND_AST_CATCH,
ZEND_AST_PARAM,
ZEND_AST_PROP_ELEM,
ZEND_AST_CONST_ELEM,
/* 4 child nodes */
ZEND_AST_FOR = 4 << ZEND_AST_NUM_CHILDREN_SHIFT,
ZEND_AST_FOREACH,
};
接下来我们开始分析a.php解析后得到的语法树
首先我们在terminal执行gdb php
然后执行set args a.php
b zend_compile
然后一直输入n,直到zend_compile_top_stmt的前一句
接下来执行以下语句
p (char *)((zend_ast_zval)(((zend_ast_list*)compiler_globals.ast).child[0].child[0].child[0])).val.value.str.val
得到a
p ((zend_ast_zval *)(((zend_ast_list)compiler_globals.ast).child[0].child[1])).val.value.lval
得到1
p (char *)((zend_ast_zval)(((zend_ast_list*)compiler_globals.ast).child[1].child[0])).val.value.str.val
得到:var_dump
p (char *)((zend_ast_zval)(((zend_ast_list)(((zend_ast_list)compiler_globals.ast).child[1].child[1])).child[0].child[0])).val.value.str.val
得到a
至于每一个child是什么,我们自己一个个去调试下,对应着代码就直到了。
查看opcode
查看php的安装目录,有一个phpdbg的文件,我们用它,可以很轻松的得到一个PHP文件有哪些opcode。
![40bbbb1a2edbf0f4012c0e8105259aa5.png](https://i-blog.csdnimg.cn/blog_migrate/79909ab86979e778e498686fd375943c.jpeg)