이 글에서는 "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;
쓰기 시 복사 기술 Redis 및 Linux 커널에서 널리 사용됩니다
예를 들어 Redis는 현재 서버 프로세스의 하위 프로세스를 생성해야 하는데 대부분의 운영 체제는 하위 프로세스의 사용 효율성을 최적화하기 위해 Copy-On-Write를 사용하므로 하위 프로세스가 존재하는 동안 서버가 증가합니다. 자식 프로세스가 존재하는 동안 해시 테이블 확장 작업을 방지하고 불필요한 메모리 쓰기 작업을 방지하며 메모리 절약을 극대화하기 위한 로드 팩터 임계값입니다.
PHP 7은 또한 할당 작업 중 메모리를 절약하기 위해 쓰기 중 복사를 사용합니다. 문자열이 할당되면 데이터 복사본을 직접 복사하지 않지만 zend_string 구조의 _zend_refcounted_h에 있는 참조 개수에 +1을 추가합니다. , 문자열이 소멸되면 zend_string 구조의 _zend_refcounted_h에 있는 refcount가 -1로 연산됩니다.
Chen Lei가 쓴 "PHP 기본 소스 코드 설계 및 구현"이라는 책을 읽어보셨다면 제 버전이 PHP7.4이기 때문에 책에 있는 버전이 제가 설치한 버전과 다르기 때문에 약간 다르다는 것을 아실 수 있습니다. 로컬로 메모리 관리를 수행하는 것이 아닐까 싶습니다.
zend_string 구조의 gc.u.flags 필드인 gc.u.flags는 각 카테고리마다 하나씩 총 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 구조의 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(GNU 기호)를 디버깅합니다. 디버거)는 단순히 디버깅 도구입니다. GPL(General Public License)의 보호를 받는 무료 소프트웨어입니다. 모든 디버거와 마찬가지로 GDB를 사용하면 원하는 곳에서 프로그램을 중지하는 것을 포함하여 프로그램을 디버그할 수 있으며 이때 변수, 레지스터, 메모리 및 스택을 볼 수 있습니다. 또한 변수와 메모리 값을 수정할 수도 있습니다. GDB는 여러 언어를 디버깅할 수 있는 매우 강력한 디버거입니다. 여기서는 다른 언어가 아닌 C 및 C++ 디버깅만 다룹니다. 주목해야 할 또 다른 점은 통합 환경인 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 파일을 편집하고
Modify 활성화=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. -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가 함수라고 말하지 않았습니다. PHP에서 echo()와 var_dump()의 주요 차이점에 대한 인터뷰 질문이 있습니까? 구문 분석은 그림과 같이 실행 시 자세히 설명됩니다
我们设置这个断点的意义是为了让程序在拼接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의 기본 커널 소스 코드에서 변수 분석(3)의 상세 내용입니다. 자세한 내용은 PHP 중국어 웹사이트의 기타 관련 기사를 참조하세요!