0

0

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

藏色散人

藏色散人

发布时间:2021-06-10 14:22:36

|

2271人浏览过

|

来源于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内核里广泛应用

立即学习PHP免费学习笔记(深入)”;

比如 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 -v
PHP 7.4.15 (cli) (built: Feb 21 2021 09:07:07) ( NTS )
Copyright (c) The PHP Group
Zend Engine v3.4.0, Copyright (c) Zend Technologies
[root@a3d3f47671d9 /]# gbv    
bash: gbv: command not found
[root@a3d3f47671d9 /]# gdb
bash: 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 php
GNU gdb (GDB) Red Hat Enterprise Linux 8.2-12.el8
Copyright (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.php
Starting program: /usr/local/bin/php php7-4-test-zval.php
warning: Error disabling address space randomization: Operation not permitted
Missing separate debuginfos, use: yum debuginfo-install glibc-2.28-127.el8.x86_64
warning: Loadable section ".note.gnu.property" outside of ELF segments
warning: Loadable section ".note.gnu.property" outside of ELF segments
warning: 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 segments
warning: Loadable section ".note.gnu.property" outside of ELF segments
Breakpoint 1, ZEND_ECHO_SPEC_CV_HANDLER () at /cui/php-7.4.15/Zend/zend_vm_execute.h:36987
36987SAVE_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 php
GNU gdb (GDB) Red Hat Enterprise Linux 8.2-12.el8
Copyright (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()的主要区别?)

人民网AIGC-X
人民网AIGC-X

国内科研机构联合推出的AI生成内容检测工具

下载

这个echo 其实是为了我们设置 断点ZEND_ECHO_SPEC_CV_HANDLER

ZEND_ECHO_SPEC_CV_HANDLER其实是个宏 以后在词法解析 语法分析 execute时候会详细展开讲解 如图

a7ed81460e4b6ab1c4f94fd8cee16d7.png

我们设置这个断点的意义是为了让程序在拼接echo 的时候暂停代码 以便我们分析

(gdb) b ZEND_ECHO_SPEC_CV_HANDLER
Breakpoint 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.php
warning: 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:36987
36987SAVE_OPLINE();

在gdb中 用 n 可以执行下一步操作

(gdb) n
36988z = 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_OPLINE
zval *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) n
441return pz->u1.v.type;
(gdb) n
36991zend_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) c
Continuing.
Breakpoint 1, ZEND_ECHO_SPEC_CV_HANDLER () at /cui/php-7.4.15/Zend/zend_vm_execute.h:36987
36987SAVE_OPLINE();
(gdb) n
36988z = EX_VAR(opline->op1.var);
(gdb) n
441return pz->u1.v.type;
(gdb) n
36997zend_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崔雪峰同意,发布在php中文网,原文地址:https://zhuanlan.zhihu.com/p/353173325

相关文章

PHP速学教程(入门到精通)
PHP速学教程(入门到精通)

PHP怎么学习?PHP怎么入门?PHP在哪学?PHP怎么学才快?不用担心,这里为大家提供了PHP速学教程(入门到精通),有需要的小伙伴保存下载就能学习啦!

下载

本站声明:本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系admin@php.cn

热门AI工具

更多
DeepSeek
DeepSeek

幻方量化公司旗下的开源大模型平台

豆包大模型
豆包大模型

字节跳动自主研发的一系列大型语言模型

通义千问
通义千问

阿里巴巴推出的全能AI助手

腾讯元宝
腾讯元宝

腾讯混元平台推出的AI助手

文心一言
文心一言

文心一言是百度开发的AI聊天机器人,通过对话可以生成各种形式的内容。

讯飞写作
讯飞写作

基于讯飞星火大模型的AI写作工具,可以快速生成新闻稿件、品宣文案、工作总结、心得体会等各种文文稿

即梦AI
即梦AI

一站式AI创作平台,免费AI图片和视频生成。

ChatGPT
ChatGPT

最最强大的AI聊天机器人程序,ChatGPT不单是聊天机器人,还能进行撰写邮件、视频脚本、文案、翻译、代码等任务。

相关专题

更多
C语言变量命名
C语言变量命名

c语言变量名规则是:1、变量名以英文字母开头;2、变量名中的字母是区分大小写的;3、变量名不能是关键字;4、变量名中不能包含空格、标点符号和类型说明符。php中文网还提供c语言变量的相关下载、相关课程等内容,供大家免费下载使用。

410

2023.06.20

c语言入门自学零基础
c语言入门自学零基础

C语言是当代人学习及生活中的必备基础知识,应用十分广泛,本专题为大家c语言入门自学零基础的相关文章,以及相关课程,感兴趣的朋友千万不要错过了。

637

2023.07.25

c语言运算符的优先级顺序
c语言运算符的优先级顺序

c语言运算符的优先级顺序是括号运算符 > 一元运算符 > 算术运算符 > 移位运算符 > 关系运算符 > 位运算符 > 逻辑运算符 > 赋值运算符 > 逗号运算符。本专题为大家提供c语言运算符相关的各种文章、以及下载和课程。

362

2023.08.02

c语言数据结构
c语言数据结构

数据结构是指将数据按照一定的方式组织和存储的方法。它是计算机科学中的重要概念,用来描述和解决实际问题中的数据组织和处理问题。数据结构可以分为线性结构和非线性结构。线性结构包括数组、链表、堆栈和队列等,而非线性结构包括树和图等。php中文网给大家带来了相关的教程以及文章,欢迎大家前来学习阅读。

263

2023.08.09

c语言random函数用法
c语言random函数用法

c语言random函数用法:1、random.random,随机生成(0,1)之间的浮点数;2、random.randint,随机生成在范围之内的整数,两个参数分别表示上限和下限;3、random.randrange,在指定范围内,按指定基数递增的集合中获得一个随机数;4、random.choice,从序列中随机抽选一个数;5、random.shuffle,随机排序。

629

2023.09.05

c语言const用法
c语言const用法

const是关键字,可以用于声明常量、函数参数中的const修饰符、const修饰函数返回值、const修饰指针。详细介绍:1、声明常量,const关键字可用于声明常量,常量的值在程序运行期间不可修改,常量可以是基本数据类型,如整数、浮点数、字符等,也可是自定义的数据类型;2、函数参数中的const修饰符,const关键字可用于函数的参数中,表示该参数在函数内部不可修改等等。

562

2023.09.20

c语言get函数的用法
c语言get函数的用法

get函数是一个用于从输入流中获取字符的函数。可以从键盘、文件或其他输入设备中读取字符,并将其存储在指定的变量中。本文介绍了get函数的用法以及一些相关的注意事项。希望这篇文章能够帮助你更好地理解和使用get函数 。

669

2023.09.20

c数组初始化的方法
c数组初始化的方法

c语言数组初始化的方法有直接赋值法、不完全初始化法、省略数组长度法和二维数组初始化法。详细介绍:1、直接赋值法,这种方法可以直接将数组的值进行初始化;2、不完全初始化法,。这种方法可以在一定程度上节省内存空间;3、省略数组长度法,这种方法可以让编译器自动计算数组的长度;4、二维数组初始化法等等。

618

2023.09.22

C# ASP.NET Core微服务架构与API网关实践
C# ASP.NET Core微服务架构与API网关实践

本专题围绕 C# 在现代后端架构中的微服务实践展开,系统讲解基于 ASP.NET Core 构建可扩展服务体系的核心方法。内容涵盖服务拆分策略、RESTful API 设计、服务间通信、API 网关统一入口管理以及服务治理机制。通过真实项目案例,帮助开发者掌握构建高可用微服务系统的关键技术,提高系统的可扩展性与维护效率。

76

2026.03.11

热门下载

更多
网站特效
/
网站源码
/
网站素材
/
前端模板

精品课程

更多
相关推荐
/
热门推荐
/
最新课程
SciPy 教程
SciPy 教程

共10课时 | 1.9万人学习

php-src源码分析探索
php-src源码分析探索

共6课时 | 0.5万人学习

进程与SOCKET
进程与SOCKET

共6课时 | 0.4万人学习

关于我们 免责申明 举报中心 意见反馈 讲师合作 广告合作 最新更新
php中文网:公益在线php培训,帮助PHP学习者快速成长!
关注服务号 技术交流群
PHP中文网订阅号
每天精选资源文章推送

Copyright 2014-2026 https://www.php.cn/ All Rights Reserved | php.cn | 湘ICP备2023035733号