ホームページ  >  記事  >  バックエンド開発  >  PHP の基礎となるカーネル ソース コード内の変数の分析 (3)

PHP の基礎となるカーネル ソース コード内の変数の分析 (3)

藏色散人
藏色散人転載
2021-06-10 14:22:362062ブラウズ

今回は「PHPの基盤となるカーネルソースコードの変数解析(3)」を紹介します。一定の参考値があるので、困っている友達が参考になれば幸いです。

おすすめ関連記事: 「PHP の基礎となるカーネル ソース コードの変数の分析 (1)」 「PHP の基礎となるカーネル ソース コードの変数の分析 (2) 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 構造体はコピーオンライト機能を実現できます

typedef struct _zend_refcounted_h {
uint32_t         refcount;//引用数
union {
uint32_t type_info;   //字符串所属的变量类别
} u;
} zend_refcounted_h;

copy-on -write テクノロジーは Redis と Linux カーネルで広く使用されています

たとえば、Redis は現在のサーバー プロセスの子プロセスを作成する必要があり、ほとんどのオペレーティング システムはコピー オン ライト (コピー オン ライト) を使用します。 ).on-write) を使用して子プロセスの使用効率を最適化するため、子プロセスの存在中にサーバーは負荷係数のしきい値を増加させ、それによって存在中のハッシュ テーブル拡張操作や不要なメモリ書き込み操作を回避します。子プロセスのメモリをできるだけ節約します。

PHP 7 は、代入操作中にメモリを節約するためにコピーオンライトも使用します。文字列が代入されるとき、データのコピーを直接コピーするのではなく、zend_string 構造体の _zend_refcounted_h にデータをコピーします。 refcount は 1 として計算され、文字列が破棄されると、zend_string 構造体の _zend_refcounted_h の refcount は -1 として計算されます。

Chen Lei 著「PHP の基礎となるソース コードの設計と実装」という本を読んだことがある方は、私のバージョンが PHP7.4 であるため、この本が少し異なることに気づくかもしれません。この本のバージョンは、ローカルにインストールしたものですが、おそらく統合メモリ管理のためだと思います。

zend_string 構造体の gc.u.flags フィールド、gc.u.flags には、カテゴリごとに 1 つずつ、合計 8 ビットがあり、繰り返しラベルを付けることができます。理論上は、最大 8 種類のラベルを付けることができます。印刷することができます。現在の PHP 7 ソース コードには、主に次のカテゴリが含まれます。 1) 一時的な通常の文字列の場合、フラグ フィールドは 0 として識別されます。 2) PHP コードでリテラル、識別子などを格納するために使用される内部文字列の場合、フラグ フィールドはIS_STR_PERSISTENT |IS_STR_INTERNED として識別されます。 3) PHP の既知の文字列の場合、フラグ フィールドは 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 の 2 つのタイプしかありません。同時に、最新の PHP8 バージョンのコードも調べましたが、同じでした。

ソース コードをよりよく理解するために、以前のバージョンのコードも載せておきます。 PHP

GDB (GNU シンボリック デバッガー) は単なるデバッグ ツールです。これは、一般公衆利用許諾書 (GPL) によって保護されているフリー ソフトウェアです。すべてのデバッガと同様に、GDB を使用すると、必要な場所でプログラムを停止するなど、プログラムをデバッグできます。その際、変数、レジスタ、メモリ、スタックを表示できます。さらに、変数やメモリ値を変更することもできます。 GDB は、複数の言語をデバッグできる非常に強力なデバッガーです。ここでは C と C のデバッグについてのみ説明し、他の言語については説明しません。もう 1 つの注意点は、統合環境である VC とは異なり、GDB はデバッガーであることです。 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 が関数であるとは言っていません。echo と echo の主な違いについてのインタビューの質問があります。 () と var_dump() in php?)

このエコー 実際、ブレークポイントを設定するのは ZEND_ECHO_SPEC_CV_HANDLER

ZEND_ECHO_SPEC_CV_HANDLER は実際にはマクロです。図

に示すように、字句解析と構文解析が実行されます。

PHP の基礎となるカーネル ソース コード内の変数の分析 (3)

我们设置这个断点的意义是为了让程序在拼接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 &#39;\006&#39;, type_flags = 0 &#39;\000&#39;, 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 の基礎となるカーネル ソース コード内の変数の分析 (3)の詳細内容です。詳細については、PHP 中国語 Web サイトの他の関連記事を参照してください。

声明:
この記事はzhihu.comで複製されています。侵害がある場合は、admin@php.cn までご連絡ください。