您好,欢迎访问一九零五行业门户网

分析PHP底层内核源码之变量 (三)

本篇文章给大家介绍《分析php底层内核源码之变量 (三)》。有一定的参考价值,有需要的朋友可以参考一下,希望对大家有所帮助。
相关文章推荐:《解析php底层内核源码之变量 (一)》《分析php底层内核源码之变量 (二) zend_string》
上文通读了zend_string的 结构体 的源码。
struct _zend_string {zend_refcounted_h gc; //占用8个字节 用于gc的计数和字符串类型的记录zend_ulong h; // 占用8个字节 用于记录 字符串的哈希值size_t len; //占用8个字节 字符串的长度char val[1]; //占用1个字节 字符串的值存储位置};
其中 len 变量 使得 zend_string 具备了 二进制安全 的特性
gc 也就是zend_refcounted_h 结构体的加持 可以实现 写时复制 (写时拷贝 copy-on-write) 的功能
typedef struct _zend_refcounted_h {uint32_t refcount;//引用数union {uint32_t type_info; //字符串所属的变量类别} u;} zend_refcounted_h;
copy-on-write 技术在redis 和linux内核里广泛应用
比如 redis需要创建当前服务器进程的子进程,而大多数操作系统都采用写时复制(copy-on-write)来优化子进程的使用效率,所以在子进程存在期间,服务器会提高负载因子的阈值,从而避免在子进程存在期间进行哈希表扩展操作,避免不必要的内存写入操作,最大限度地节约内存。
php 7也采用了写时复制从而在进行赋值操作时比较节省内存,当字符串在赋值时并不直接拷贝一份数据,而是把zend_string结构体里的 _zend_refcounted_h中的 refcount 做+1 运算,字符串销毁时再把zend_string结构体里的 _zend_refcounted_h中的 refcount 做-1 运算。
如果您看过 陈雷大佬写的 《php底层源码设计与实现》 一书 可以会发现 稍微不一样 因为 我的版本是php7.4 书中版本 与我本地安装的不同 ,猜测可能是为了统一进行内存管理。
zend_string结构体里面的gc.u.flags字段,gc.u.flags总共有8位,每个类别占一位,可以重复打标签,理论上最多打8种标签。目前php 7源码主要涉及以下几种:1)对于临时的普通字符串,flags字段被标识为0。2)对于内部字符串,用于存储php代码中的字面量、标识符等,flags字段被标识成is_str_persistent |is_str_interned。3)对于php已知字符串,flags字段会被标识成is_str_persistent|is_str_interned|is_str_permanent。
--------摘自 《php底层源码设计与实现》
在 php7.4源码底层会给 变量进行分类 方便内存的管理 其依赖于 zend_zval结构体里的u1.v.type_flags字段
struct _zval_struct { 197 zend_value value; //变量 198 union { 199 struct { 200 zend_endian_lohi_3( 201 zend_uchar type, //变量类型 202 zend_uchar type_flags,//可以用于变量的分类 203 union { 204 uint16_t extra; /* not further specified */ 205 } u) 206 } v; 207 uint32_t type_info;//变量类型 208 } u1; 209 u2; 222 };
在555行有如下代码
/* zval.u1.v.type_flags */#define is_type_refcounted(1<<0) //refcounted 可以计数的#define is_type_collectable(1<<1) // type_collectable可收集的#if 1/* this optimized version assumes that we have a single "type_flag" *//* is_type_collectable may be used only with is_type_refcounted *//*优化后的版本假设我们有一个单一的"type_flag" *//* is_type_collectable只能与is_type_refcounted一起使用*/# define z_type_info_refcounted(t)(((t) & z_type_flags_mask) != 0)#else# define z_type_info_refcounted(t)(((t) & (is_type_refcounted << z_type_flags_shift)) != 0)#endif
所以php7.4版本中 zval.u1.v.type_flags 只有两种类型 0或者 1 同时我也看了下最新的php8版本代码 也是如此
为了更好的深入了解源码 也将 前面两节内容穿起来 我们安装gdb 来调试下php
gdb(gnu symbolic debugger)简单地说就是一个调试工具。它是一个受通用公共许可证即gpl保护的自由软件。像所有的调试器一样,gdb可以让你调试一个程序,包括让程序在你希望的地方停下,此时你可以查看变量、寄存器、内存及堆栈。更进一步你可以修改变量及内存值。gdb是一个功能很强大的调试器,它可以调试多种语言。在此我们仅涉及 c 和 c++ 的调试,而不包括其它语言。还有一点要说明的是,gdb是一个调试器,而不像 vc 是一个集成环境。你可以使用一些前端工具如xxgdb、ddd等。他们都有图形化界面,因此使用更方便,但它们仅是gdb的一层外壳。因此,你仍应熟悉gdb命令。事实上,当你使用这些图形化界面时间较长时,你才会发现熟悉gdb命令的重要性。
-----摘自oschina
[root@a3d3f47671d9 /]# php -vphp 7.4.15 (cli) (built: feb 21 2021 09:07:07) ( nts )copyright (c) the php groupzend engine v3.4.0, copyright (c) zend technologies[root@a3d3f47671d9 /]# gbv bash: gbv: command not found[root@a3d3f47671d9 /]# gdbbash: gdb: command not found[root@a3d3f47671d9 /]# yum install gdb
.........
新建一个 php 文件
[root@a3d3f47671d9 cui]# vim php7-4-test-zval.php php7-4-test-zval.php  buffers <?php $a="abcdefg"; echo $a; $b=88; echo $b; $c = $a; echo $c; echo $a; $c ="abc"; echo $c; echo $a;
用 gdb 运行 php
[root@a3d3f47671d9 cui]# gdb phpgnu gdb (gdb) red hat enterprise linux 8.2-12.el8copyright (c) 2018 free software foundation, inc.license gplv3+: gnu gpl version 3 or later <http://gnu.org/licenses/gpl.html>this is free software: you are free to change and redistribute it.there is no warranty, to the extent permitted by law.type "show copying" and "show warranty" for details.this gdb was configured as "x86_64-redhat-linux-gnu".type "show configuration" for configuration details.for bug reporting instructions, please see:<http://www.gnu.org/software/gdb/bugs/>.find the gdb manual and other documentation resources online at: <http://www.gnu.org/software/gdb/documentation/>.for help, type "help".type "apropos word" to search for commands related to "word"...reading symbols from php...done.(gdb) b zend_echo_spec_cv_handler # b 命令意思是打断点breakpoint 1 at 0x6dfe80: file /cui/php-7.4.15/zend/zend_vm_execute.h, line 36987.(gdb) r php7-4-test-zval.phpstarting program: /usr/local/bin/php php7-4-test-zval.phpwarning: error disabling address space randomization: operation not permittedmissing separate debuginfos, use: yum debuginfo-install glibc-2.28-127.el8.x86_64warning: loadable section ".note.gnu.property" outside of elf segmentswarning: loadable section ".note.gnu.property" outside of elf segmentswarning: loadable section ".note.gnu.property" outside of elf segments[thread debugging using libthread_db enabled]using host libthread_db library "/lib64/libthread_db.so.1".warning: loadable section ".note.gnu.property" outside of elf segmentswarning: loadable section ".note.gnu.property" outside of elf segmentsbreakpoint 1, zend_echo_spec_cv_handler () at /cui/php-7.4.15/zend/zend_vm_execute.h:3698736987save_opline();missing separate debuginfos, use: yum debuginfo-install libxcrypt-4.1.1-4.el8.x86_64 libxml2-2.9.7-8.el8.x86_64 sqlite-libs-3.26.0-11.el8.x86_64 xz-libs-5.2.4-3.el8.x86_64 zlib-1.2.11-16.el8_2.x86_64
可以看到 我的报错了 因为我是在docker里跑的 centos镜像 查了一些资料解决方法如下
编辑 /etc/yum.repos.d/centos-debuginfo.repo 文件
修改enable=1
然后 yum install yum-utils
然后 dnf install glibc-langpack-en
yum debuginfo-install libxcrypt-4.1.1-4.el8.x86_64 libxml2-2.9.7-8.el8.x86_64 sqlite-libs-3.26.0-11.el8.x86_64 xz-libs-5.2.4-3.el8.x86_64 zlib-1.2.11-16.el8_2.x86_64
yum debuginfo-install glibc-2.28-127.el8.x86_64
让我们再次运行一下 gdb
[root@a3d3f47671d9 cui]# vim php7-4-test-zval.php[root@a3d3f47671d9 cui]# gdb phpgnu gdb (gdb) red hat enterprise linux 8.2-12.el8copyright (c) 2018 free software foundation, inc.license gplv3+: gnu gpl version 3 or later <http://gnu.org/licenses/gpl.html>this is free software: you are free to change and redistribute it.there is no warranty, to the extent permitted by law.type "show copying" and "show warranty" for details.this gdb was configured as "x86_64-redhat-linux-gnu".type "show configuration" for configuration details.for bug reporting instructions, please see:<http://www.gnu.org/software/gdb/bugs/>.find the gdb manual and other documentation resources online at: <http://www.gnu.org/software/gdb/documentation/>.for help, type "help".type "apropos word" to search for commands related to "word"...reading symbols from php...done.(gdb)
在gdb模式 命令b 可以设置断点 你可以理解为php的 xdebug
还记得我们的 php7-4-test-zval.php 文件内容吗
<?php $a="abcdefg"; echo $a; $b=88; echo $b; $c = $a; echo $c; echo $a; $c ="abc"; echo $c; echo $a;
这个echo 语言结构 是为了我们调试使用 这里是个小技巧
(ps 我这里说的语言结构 可没说echo是函数 有一道面试题 php 中 echo()和var_dump()的主要区别?)
这个echo 其实是为了我们设置 断点zend_echo_spec_cv_handler
zend_echo_spec_cv_handler其实是个宏 以后在词法解析 语法分析 execute时候会详细展开讲解 如图
我们设置这个断点的意义是为了让程序在拼接echo 的时候暂停代码 以便我们分析
(gdb) b zend_echo_spec_cv_handlerbreakpoint 1 at 0x6dfe80: file /cui/php-7.4.15/zend/zend_vm_execute.h, line 36987.
在gdb中 使用 r 运行文件
(gdb) r php7-4-test-zval.php starting program: /usr/local/bin/php php7-4-test-zval.phpwarning: error disabling address space randomization: operation not permitted[thread debugging using libthread_db enabled]using host libthread_db library "/lib64/libthread_db.so.1".breakpoint 1, zend_echo_spec_cv_handler () at /cui/php-7.4.15/zend/zend_vm_execute.h:3698736987save_opline();
在gdb中 用 n 可以执行下一步操作
(gdb) n36988z = ex_var(opline->op1.var);
这里我们暂且忽略继续往下走
zend_echo_spec_cv_handler的完整代码如下(我贴出来只是想告诉你代码里有这行代码 让你知道为什么往下走,你现阶段不需要理解代码,慢慢来 )
static zend_opcode_handler_ret zend_fastcall zend_echo_spec_cv_handler(zend_opcode_handler_args){use_oplinezval *z;save_opline();//****************走到了此处**************z = ex_var(opline->op1.var);if (z_type_p(z) == is_string) {zend_string *str = z_str_p(z);if (zstr_len(str) != 0) {zend_write(zstr_val(str), zstr_len(str));}} else {zend_string *str = zval_get_string_func(z);if (zstr_len(str) != 0) {zend_write(zstr_val(str), zstr_len(str));} else if (is_cv == is_cv && unexpected(z_type_p(z) == is_undef)) {zval_undefined_op1();}zend_string_release_ex(str, 0);}zend_vm_next_opcode_check_exception();}(gdb) n441return pz->u1.v.type;(gdb) n36991zend_string *str = z_str_p(z);
这里到了关键位置 变量z出现了
gdb中 用p 查看变量
(gdb) p z$1 = (zval *) 0x7f4235a13070
这是一个 zval 结构体的指针地址
(gdb) p *z$2 = { value = {lval = 139922344256128, dval = 6.9130823382525114e-310, counted = 0x7f4235a02280, str = 0x7f4235a02280, arr = 0x7f4235a02280, obj = 0x7f4235a02280, res = 0x7f4235a02280, ref = 0x7f4235a02280, ast = 0x7f4235a02280, zv = 0x7f4235a02280, ptr = 0x7f4235a02280, ce = 0x7f4235a02280, func = 0x7f4235a02280, ww = {w1 = 899687040, w2 = 32578}}, u1 = {v = {type = 6 '\006', type_flags = 0 '\000', u = {extra = 0}}, type_info = 6}, u2 = {next = 0, cache_slot = 0, opline_num = 0, lineno = 0, num_args = 0, fe_pos = 0, fe_iter_idx = 0, access_flags = 0, property_guard = 0, constant_flags = 0, extra = 0}}
看到这里应该很熟悉了 这就是源码里的 结构体 格式
再次复习下 zval
struct _zval_struct { zend_value value; //变量 union { struct { zend_endian_lohi_3( zend_uchar type, //变量类型 zend_uchar type_flags,//可以用于变量的分类 union { uint16_t extra; /* not further specified */ } u) } v; uint32_t type_info;//变量类型 } u1; u2; };
gdb中变量$2 中 u1.v.type=6 我们拿出第二节的 类型定义源码部分对比下
/* regular data types */#define is_undef0#define is_null1#define is_false2#define is_true3#define is_long4#define is_double5#define is_string6#define is_array7#define is_object8#define is_resource9#define is_reference10.....//其实有20种 剩下的不是常用类型 代码就不全部粘出来了u1.v.type=6 类型是 is_string
再看下 zval种 value 对应的 zend_value联合体中的代码
ypedef union _zend_value {zend_long lval;/* long value */double dval;/* double value */zend_refcounted *counted;zend_string *str;zend_array *arr;zend_object *obj;zend_resource *res;zend_reference *ref;zend_ast_ref *ast;zval *zv;void *ptr;zend_class_entry *ce;zend_function *func;struct {uint32_t w1;uint32_t w2;} ww;} zend_value;
还记得联合体的特性吗 ? 所有值公用一个内存空间
上面的gdb中变量$2 的v.type=6 所以 在value中 值被str占用了 同时str 前面有个*
*星号 在c语言里代表指针 指向另外一个值的地址 所以指向 zend_string结构体
关于c语言指针您可以参考 菜鸟学院-指针
所以 接下来我们可以通过获取value中的str来获取 查看值
(gdb) p *z.value .str $4 = {gc = {refcount = 1, u = {type_info = 70}}, h = 9223601495925209889, len = 7, val = "a"}
对比下 zend_string 源码
struct _zend_string {zend_refcounted_h gc;//引用计数zend_ulong h; /* hash value */size_t len;//字符串长度char val[1];};
* 你可能有疑问 val为啥 是val=“a” 我们不是定义$a="abcdefg"; 吗 ? 还记得柔性数组吗?:)
接下来继续往下走
gdb中 用c 来执行到下一个断点处
(gdb) ccontinuing.breakpoint 1, zend_echo_spec_cv_handler () at /cui/php-7.4.15/zend/zend_vm_execute.h:3698736987save_opline();(gdb) n36988z = ex_var(opline->op1.var);(gdb) n441return pz->u1.v.type;(gdb) n36997zend_string *str = zval_get_string_func(z);(gdb) p *z$6 = { value = {lval = 88, dval = 4.3477776834029696e-322, counted = 0x58, str = 0x58, arr = 0x58, obj = 0x58, res = 0x58, ref = 0x58, ast = 0x58, zv = 0x58, ptr = 0x58, ce = 0x58, func = 0x58, ww = {w1 = 88, w2 = 0}}, u1 = {v = {type = 4 '\004', type_flags = 0 '\000', u = {extra = 0}}, type_info = 4}, u2 = {next = 0, cache_slot = 0, opline_num = 0, lineno = 0, num_args = 0, fe_pos = 0, fe_iter_idx = 0, access_flags = 0, property_guard = 0, constant_flags = 0, extra = 0}}
u1.v.type=4 对应的是is_long 代表整型 所以 在value中 值被lval占用了
可以看到值就是88 (lval不是指针 无需再跟进去查看了)
至此 我们用gdb 结合之前所看的核心源码 亲自实战了 php的zval
下一节我们继续 进行写时复制 的gdb跟踪
看完此文 希望你务必也用gdb调试下 深度体会zval的巧妙之处
感谢陈雷前辈的《php7源码底层设计与实现》
▏本文经原作者php崔雪峰同意,发布在,原文地址:https://zhuanlan.zhihu.com/p/353173325
以上就是分析php底层内核源码之变量 (三)的详细内容。
其它类似信息

推荐信息