“PHP中的GC垃圾回收机制”
PHP中的GC垃圾回收机制
什么是GC
Gc
,全称Garbage collection
,即垃圾回收机制。
主要用于自动回收不再使用的内存,特别是用来管理引用计数机制下的内存回收。当 PHP 脚本执行时,垃圾回收会自动清理不再使用的对象和变量,以避免内存泄漏。
PHP 使用了 **引用计数 ** 和 **回收周期 ** 的结合来管理内存。
引用计数
为了理解这个引用计数的实现 我们需要了解一下zval 变量容器的工作原理
zval 容器
zval 容器是 PHP 内部用来存储变量信息的数据结构,除了存储变量的值外,它还包含了与该变量相关的额外信息,例如变量的类型、引用计数等。
在 PHP 中,所有的变量都会被封装在一个名为 zval
的容器中,zval
是一个结构体,包含以下几个关键部分:
类型:变量的类型(例如整数、字符串、数组、对象等)。值:变量存储的具体值。
引用计数 (refcount
):指向该 zval
容器的引用数量,表示有多少个变量或引用指向此 zval
。这是个bool值 用来标识这个变量是否属于引用集合
引用标志 (is_ref
):用于标识该变量是否为引用。PHP 引擎通过这个标志来区分普通变量和引用变量。
大概长这样
1 | typedef struct _zval_struct { |
比较重要的是is_ref
和 refcount
字段
is_ref
(引用标志):
is_ref
是一个布尔值,用于标识当前 zval
是否为引用。如果 is_ref
为 1
,则说明该 zval
是通过引用传递的;如果 is_ref
为 0
,则说明该 zval
是一个普通变量。
在 PHP 中,引用是通过 &
操作符实现的。例如,$b = &$a;
,此时 $b
是 $a
的引用,因此 b
的 zval
会将 is_ref
标志设置为 1
。
refcount
(引用计数):
refcount
是一个无符号整数,表示有多少个引用指向当前 zval
(指向zval
变量容器的变量个数)。也就是说,refcount
记录着有多少变量或引用使用了该 zval
。
每当有一个新的引用指向该 zval
时,refcount
会递增。当 refcount
减为零时,PHP 会释放这个 zval
所占用的内存。
1 |
|
来看这个例子
这里定义了一个变量$a
,生成了类型为String
和值为new string
的变量容器,而对于两个额外的字节,is_ref
和refcount
,我们这里可以看到是不存在引用的,所以is_ref
的值应该是false,而refcount
是表示变量个数的,那么这里就应该是1,接下来我们验证一下
(这里要看你的php有没有装Xdebug Xdebug:支持 — 量身定制的安装说明 这个非常好用 把你的phpinfo内容粘贴进去就有教程了)
下面我们添加 一个引用
1 |
|
在我们的思考中 每生成一个变量就有一个zval
记录其类型和值以及两个额外字节,那我们这里的话a的refcount
应该是1,is_ref
应该是true
结果这里是2 因为同一变量容器被变量a和变量b关联,当没必要时,php不会去复制已生成的变量容器。
所以这一个zval
容器存储了a
和b
两个变量,就使得refcount
的值为2.
知道这个容器是怎么生成的 那么他的销毁
变量容器在refcount
变成0时就被销毁。它这个值是如何减少的呢,当函数执行结束或者对变量调用了unset()函数,refcount
就会减1。
1 |
|
我这里和文章上出现的结果是不一样的
具体原因可能是版本差异
不过这个is_ref他反应的应该是变量曾经是否被引用过
所以这里是1应该也没问题
总之我们看到refcount数量变成了1才是比较关键的
GC在PHP 反序列化中的利用
GC
如果在PHP反序列化中生效,那它就会直接触发_destruct
方法
可以看一下这个demo示例
1 |
|
当使用 new
关键字创建一个类的实例时,构造函数 __construct
会被自动调用。
在代码中,$a = new test(1);
会调用 test
类的构造函数,输出 1__construct
。
析构函数 __destruct
在对象被销毁时自动调用。对象的销毁时机通常有两种情况:
脚本执行结束时,会自动销毁所有创建的对象,按对象创建的逆序调用析构函数。
当使用 unset
函数销毁对象时,会立即调用该对象的析构函数。
unset($a);
销毁了 $a
这个对象,所以会立即调用 $a
对应的 test
类实例的析构函数,输出 1__destruct()
。
另一种方法
核心是让对象在反序列化过程中提前变为 “无引用状态”(类似被赋值为 NULL),从而触发__destruct
析构函数
当对象的引用计数归零时(没有任何变量引用它),PHP 会立即回收该对象并调用__destruct
。“赋值为 NULL” 只是让引用计数归 0 的一种方式。
我们反序列化一个数组 第一个索引为对象 第二个元素使用与第一个元素相同的索引
比如
1 | // 序列化前的数组(逻辑上) |
当反序列化这个数组时,PHP 会按顺序解析:
先解析第一个元素:创建B
对象,存入索引0
(此时B
对象的引用计数 = 1)
再解析第二个元素:发现索引也是0
,于是用整数0
覆盖了原来的B
对象(数组中相同索引会被后出现的元素覆盖)
覆盖后,B
对象不再被任何变量引用(引用计数 = 0),触发垃圾回收,进而调用__destruct
原本B
对象被数组的索引0
引用(有一个引用)
当相同索引的元素被覆盖后,B
对象的所有引用都消失了(引用计数从 1 变为 0)
PHP 的垃圾回收器会实时检测到 “引用计数为 0” 的对象,将其标记为垃圾并立即回收
回收时必然会调用对象的__destruct
方法,无论后续是否有异常抛出
下面尝试一下下
1 |
|
我们发现在这里反序列化之后就触发异常了
所以按照正常的方法我们没法触发_destruct
我们再反序列化一个数组
1 |
|
把第二个索引改为0
当 B
类对象被整数 0
覆盖后,原本指向 B
类对象的引用就不存在了,B
类对象的引用计数变为 0。PHP 的垃圾回收机制会检测到引用计数为 0 的对象,将其视为垃圾进行回收。
成功看到flag
GC在phar反序列化中的利用
其实差不多都是一样的
不过在这里
phar文件中的i:0是不能随便修改的
改了会因为签名错误而导致文件出错 但是签名可以进行伪造
利用下面的脚本使签名正确
1 | import gzip |
路径记得修改一下
直接运行生成一个新的可用的phar文件
然后伪协议读取就可以了
基本的知识点就学完了
newstar2023第四周有道题目就是这个