前言
想研究一下 PHP 的 unserialize 流程的想法是从网鼎杯的一道 PHP 反序列化题开始的,当时发现本地 public 序列化的字段可以在远程反序列化成 protected,就觉得很神奇,于是计划阅读源码跟进一下。
本文主要侧重类的非静态成员变量。
测试用类
class Twings {
public $a = "Aluvion";
protected $b = 1;
private $c = array("PHP", "Java", 7);
private String $d;
}
class
class 的编译及成员变量类型检测
php 代码的词法分析略,这部分太长了,直接到解析 AST,获取成员变量数据进行检测的部分,在 zend_compile.c 的 zend_compile_prop_decl 函数:
void zend_compile_prop_decl(zend_ast *ast, zend_ast *type_ast, uint32_t flags) /* {{{ */
{
...
if (ce->ce_flags & ZEND_ACC_INTERFACE) {
zend_error_noreturn(E_COMPILE_ERROR, "Interfaces may not include member variables");
}
if (flags & ZEND_ACC_ABSTRACT) {
zend_error_noreturn(E_COMPILE_ERROR, "Properties cannot be declared abstract");
}
for (i = 0; i < children; ++i) {
...
if (type_ast) {
type = zend_compile_typename(type_ast, 0);
if (ZEND_TYPE_CODE(type) == IS_VOID || ZEND_TYPE_CODE(type) == IS_CALLABLE) {
zend_error_noreturn(E_COMPILE_ERROR,
"Property %s::$%s cannot have type %s",
ZSTR_VAL(ce->name),
ZSTR_VAL(name),
zend_get_type_by_const(ZEND_TYPE_CODE(type)));
}
}
/* Doc comment has been appended as last element in ZEND_AST_PROP_ELEM ast */
...
if (flags & ZEND_ACC_FINAL) {
...
}
if (zend_hash_exists(&ce->properties_info, name)) {
...
}
if (value_ast) {
zend_const_expr_to_zval(&value_zv, value_ast);
if (ZEND_TYPE_IS_SET(type) && !Z_CONSTANT(value_zv)) {
if (Z_TYPE(value_zv) == IS_NULL) {
if (!ZEND_TYPE_ALLOW_NULL(type)) {
...
}
} else if (ZEND_TYPE_IS_CLASS(type)) {
...
} else if (ZEND_TYPE_CODE(type) == IS_ARRAY || ZEND_TYPE_CODE(type) == IS_ITERABLE) {
if (Z_TYPE(value_zv) != IS_ARRAY) {
...
}
} else if (ZEND_TYPE_CODE(type) == IS_DOUBLE) {
if (Z_TYPE(value_zv) != IS_DOUBLE && Z_TYPE(value_zv) != IS_LONG) {
...
}
convert_to_double(&value_zv);
} else if (!ZEND_SAME_FAKE_TYPE(ZEND_TYPE_CODE(type), Z_TYPE(value_zv))) {
...
}
}
} else if (!ZEND_TYPE_IS_SET(type)) {
ZVAL_NULL(&value_zv);
} else {
ZVAL_UNDEF(&value_zv);
}
zend_declare_typed_property(ce, name, &value_zv, flags, doc_comment, type);
}
}
flags 指成员变量的修饰符(如 public、protected、private),不合法的修饰符(如 final)会在这里检测并抛出错误。
type 是从 type_ast 里面解析出来的指成员变量的类型,这里的类型是在 PHP 代码里面规定好的类型,没有规定类型的成员变量的 type 为 0,同样也有不合法类型的检测(如 void)。顺便在这里贴一个 PHP 底层类型的定义,这里的 type 是内部定义好的代表类型的数字 x4 之后的结果:
/* regular data types */
#define IS_UNDEF 0
#define IS_NULL 1
#define IS_FALSE 2
#define IS_TRUE 3
#define IS_LONG 4
#define IS_DOUBLE 5
#define IS_STRING 6
#define IS_ARRAY 7
#define IS_OBJECT 8
#define IS_RESOURCE 9
#define IS_REFERENCE 10
/* constant expressions */
#define IS_CONSTANT_AST 11
/* internal types */
#define IS_INDIRECT 13
#define IS_PTR 14
#define IS_ALIAS_PTR 15
#define _IS_ERROR 15
/* fake types used only for type hinting (Z_TYPE(zv) can not use them) */
#define _IS_BOOL 16
#define IS_CALLABLE 17
#define IS_ITERABLE 18
#define IS_VOID 19
#define _IS_NUMBER 20
成员变量重复定义的检测是通过查询类里面的哈希表 properties_info 实现的,这个哈希表在后面创建的时候再说。
value_zv 是从 value_ast 里面解析出来的成员变量的默认值。这里会有更详细的变量类型的检测,具体什么样的是不合法的类型可以自行阅读源码,宏定义配合报错信息很容易理解。这里提一下两个宏:ZEND_TYPE_IS_SET 和 ZEND_TYPE_CODE,ZEND_TYPE_IS_SET 虽然叫 is,其实是个 > 3 的比较,在规定的类型里面除了未定义其他类型都会返回 true,而 ZEND_TYPE_CODE 是个将 type / 4 转换成内部定义数字的宏。
完成检测之后,会调用 zend_declare_typed_property 函数写入内存。
class 结构体
class 在 PHP 内存中是一个 _zend_class_entry 结构体:
struct _zend_class_entry {
char type;
zend_string *name;
/* class_entry or string depending on ZEND_ACC_LINKED */
union {
zend_class_entry *parent;
zend_string *parent_name;
};
int refcount;
uint32_t ce_flags;
int default_properties_count;
int default_static_members_count;
zval *default_properties_table;
zval *default_static_members_table;
ZEND_MAP_PTR_DEF(zval *, static_members_table);
HashTable function_table;
HashTable properties_info;
HashTable constants_table;
struct _zend_property_info **properties_info_table;
zend_function *constructor;
zend_function *destructor;
zend_function *clone;
zend_function *__get;
zend_function *__set;
zend_function *__unset;
zend_function *__isset;
zend_function *__call;
zend_function *__callstatic;
zend_function *__tostring;
zend_function *__debugInfo;
zend_function *serialize_func;
zend_function *unserialize_func;
...
};
本类和父类的各种信息、各种魔术方法和静态成员变量暂且不提,可以看到跟非静态成员变量相关的数据主要是这几个:
- default_properties_count
- default_properties_table
- properties_info
- properties_info_table
成员变量写入内存
继续看代码,将非静态成员变量写入用户类的关键代码如下:
ZEND_API int zend_declare_typed_property(zend_class_entry *ce, zend_string *name, zval *property, int access_type, zend_string *doc_comment, zend_type type) /* {{{ */
{
zend_property_info *property_info, *property_info_ptr;
...
property_info = zend_arena_alloc(&CG(arena), sizeof(zend_property_info));
...
if (!(access_type & ZEND_ACC_PPP_MASK)) {
access_type |= ZEND_ACC_PUBLIC;
}
if (access_type & ZEND_ACC_STATIC) {
...
} else {
zval *property_default_ptr;
if ((property_info_ptr = zend_hash_find_ptr(&ce->properties_info, name)) != NULL &&
(property_info_ptr->flags & ZEND_ACC_STATIC) == 0) {
...
} else {
property_info->offset = OBJ_PROP_TO_OFFSET(ce->default_properties_count);
ce->default_properties_count++;
ce->default_properties_table = perealloc(ce->default_properties_table, sizeof(zval) * ce->default_properties_count, ce->type == ZEND_INTERNAL_CLASS);
/* For user classes this is handled during linking */
if (ce->type == ZEND_INTERNAL_CLASS) {
...
}
}
property_default_ptr = &ce->default_properties_table[OBJ_PROP_TO_NUM(property_info->offset)];
ZVAL_COPY_VALUE(property_default_ptr, property);
Z_PROP_FLAG_P(property_default_ptr) = Z_ISUNDEF_P(property) ? IS_PROP_UNINIT : 0;
}
if (ce->type & ZEND_INTERNAL_CLASS) {
...
}
if (access_type & ZEND_ACC_PUBLIC) {
property_info->name = zend_string_copy(name);
} else if (access_type & ZEND_ACC_PRIVATE) {
property_info->name = zend_mangle_property_name(ZSTR_VAL(ce->name), ZSTR_LEN(ce->name), ZSTR_VAL(name), ZSTR_LEN(name), is_persistent_class(ce));
} else {
ZEND_ASSERT(access_type & ZEND_ACC_PROTECTED);
property_info->name = zend_mangle_property_name("*", 1, ZSTR_VAL(name), ZSTR_LEN(name), is_persistent_class(ce));
}
property_info->name = zend_new_interned_string(property_info->name);
property_info->flags = access_type;
property_info->doc_comment = doc_comment;
property_info->ce = ce;
property_info->type = type;
zend_hash_update_ptr(&ce->properties_info, name, property_info);
return SUCCESS;
}
不符合 public、protected、private 的变量修饰符会变成 public。
然后开始将非静态成员变量的数据写入内存,首先需要一个计算非静态成员变量数目的变量,也就是 default_properties_count,然后是计算 offset。
先说一下非静态成员变量的存放方式,非静态成员变量的值存放在 default_properties_table,其它信息则是先存放在 property_info,最后会更新到哈希表 properties_info 里面。property_info 的结构如下:
typedef struct _zend_property_info {
uint32_t offset; /* property offset for object properties or
property index for static properties */
uint32_t flags;
zend_string *name;
zend_string *doc_comment;
zend_class_entry *ce;
zend_type type;
} zend_property_info;
flags 修饰符等标识,其他数据都好理解,而 offset 用来表示内存中对象的成员变量距离头部的偏移:
#define OBJ_PROP_TO_OFFSET(num) ((uint32_t)(XtOffsetOf(zend_object, properties_table) + sizeof(zval) * (num)))
所以 offset 就是 0x28 + 0x10 * default_properties_count,有什么用途以后再说。
然后将 property(即代表成员变量值的 zval,包括类型和值两个数据)复制到 default_properties_table 的相应位置上,default_properties_table 实质上就是一个 zval 数组。
接下里则是将成员变量的相关信息存放进 property_info 里面,首先是按照修饰符修改成员变量名,public 不改,protected 加上 \x00*\x00,private 加上 \x00类名\x00,就编程了我们在序列化字符串中看到的样子。然后则是将修饰符、类指针、规定的变量类型都放入 property_info,最后调用 zend_hash_update_ptr 将其更新到哈希表 properties_info 中,哈希表的结构如下:
typedef struct _Bucket {
zval val;
zend_ulong h; /* hash value (or numeric index) */
zend_string *key; /* string key or NULL for numerics */
} Bucket;
typedef struct _zend_array HashTable;
struct _zend_array {
zend_refcounted_h gc;
union {
struct {
ZEND_ENDIAN_LOHI_4(
zend_uchar flags,
zend_uchar _unused,
zend_uchar nIteratorsCount,
zend_uchar _unused2)
} v;
uint32_t flags;
} u;
uint32_t nTableMask;
Bucket *arData;
uint32_t nNumUsed;
uint32_t nNumOfElements;
uint32_t nTableSize;
uint32_t nInternalPointer;
zend_long nNextFreeElement;
dtor_func_t pDestructor;
};
数据存放在 arData 中,数量则用 nNumUsed 记录。如 property_info 就是作为一个 ptr 类型的数据存放在 val 中,key 则是根据修饰符修改前的变量名,h 是根据 key 生成的一个哈希。
至此,类非静态成员变量的处理及相关的 default_properties_count、default_properties_table、properties_info 等结构体成员的构成都大致清楚了,非静态成员变量相关的数据已经基本齐全,剩下的 properties_info_table 其实跟 properties_info 的数据存在重复,具体可以看 zend_build_properties_info_table 函数,它其实就是存放在 properties_info 里的 property_info。
他们的区别就在于 properties_info 存放了根据修饰符修改前的变量名,所以可以通过偏移和查找 key 来访问,而 properties_info_table 只可以通过偏移来访问。但是 properties_info_table 也有一个优点,那就是结构比较简单,可以直接查找该变量的规定类型(如 zend_get_typed_property_info_for_slot 函数)。
顺带一提,在继承的情况下,在分析编译完字类之后,要将父类的数据继承到字类,即按照父类对字类做一些修改,修改的逻辑在 zend_do_inheritance_ex 函数中。如果重写了父类的成员变量,修改后字类的 default_properties_count、default_properties_table 和 properties_info 属性都会改变,前两者加上父类的相应数据,复制到一个新的 zval 数组里再赋值回 default_properties_table,不知道为什么是倒着来复制的,子类的放在后面:
if (parent_ce->default_properties_count) {
if (ce->default_properties_count) {
zval *table = pemalloc(sizeof(zval) * (ce->default_properties_count + parent_ce->default_properties_count), ce->type == ZEND_INTERNAL_CLASS);
src = ce->default_properties_table + ce->default_properties_count;
end = table + parent_ce->default_properties_count;
dst = end + ce->default_properties_count;
ce->default_properties_table = table;
do {
dst--;
src--;
ZVAL_COPY_VALUE_PROP(dst, src);
} while (dst != end);
pefree(src, ce->type == ZEND_INTERNAL_CLASS);
end = ce->default_properties_table;
}
...
src = parent_ce->default_properties_table + parent_ce->default_properties_count;
...
do {
dst--;
src--;
ZVAL_COPY_PROP(dst, src);
if (Z_OPT_TYPE_P(dst) == IS_CONSTANT_AST) {
ce->ce_flags &= ~ZEND_ACC_CONSTANTS_UPDATED;
}
continue;
} while (dst != end);
ce->default_properties_count += parent_ce->default_properties_count;
}
properties_info 的处理如下:
ZEND_HASH_FOREACH_PTR(&ce->properties_info, property_info) {
if (property_info->ce == ce) {
if (property_info->flags & ZEND_ACC_STATIC) {
property_info->offset += parent_ce->default_static_members_count;
} else {
property_info->offset += parent_ce->default_properties_count * sizeof(zval);
}
}
} ZEND_HASH_FOREACH_END();
if (zend_hash_num_elements(&parent_ce->properties_info)) {
zend_hash_extend(&ce->properties_info,
zend_hash_num_elements(&ce->properties_info) +
zend_hash_num_elements(&parent_ce->properties_info), 0);
ZEND_HASH_FOREACH_STR_KEY_PTR(&parent_ce->properties_info, key, property_info) {
do_inherit_property(property_info, key, ce);
} ZEND_HASH_FOREACH_END();
}
主要有两个部分,首先是遍历字类的 properties_info 然后修改里面 propertie_info 的偏移,因为前面复制 default_properties_table 的时候将父类的变量放在前面,所以需要加上相应的偏移。然后就是遍历父类的 properties_info,如果 key 不存在就加上去,因为父类放在前面,所以偏移不需要改变:
if (UNEXPECTED(ce->type & ZEND_INTERNAL_CLASS)) {
child_info = zend_duplicate_property_info_internal(parent_info);
} else {
child_info = parent_info;
}
_zend_hash_append_ptr(&ce->properties_info, key, child_info);
而如果字类中存在这个 key,即重写了父类中的成员变量,而且父类中这个变量的修饰符不是 private:
if (UNEXPECTED((child_info->flags & ZEND_ACC_PPP_MASK) > (parent_info->flags & ZEND_ACC_PPP_MASK))) {
zend_error_noreturn(...);
} else if ((child_info->flags & ZEND_ACC_STATIC) == 0) {
int parent_num = OBJ_PROP_TO_NUM(parent_info->offset);
int child_num = OBJ_PROP_TO_NUM(child_info->offset);
/* Don't keep default properties in GC (they may be freed by opcache) */
zval_ptr_dtor_nogc(&(ce->default_properties_table[parent_num]));
ce->default_properties_table[parent_num] = ce->default_properties_table[child_num];
ZVAL_UNDEF(&ce->default_properties_table[child_num]);
child_info->offset = parent_info->offset;
}
if (UNEXPECTED(ZEND_TYPE_IS_SET(parent_info->type))) {
inheritance_status status = property_types_compatible(parent_info, child_info);
if (status == INHERITANCE_ERROR) {
emit_incompatible_property_error(child_info, parent_info);
}
if (status == INHERITANCE_UNRESOLVED) {
add_property_compatibility_obligation(ce, child_info, parent_info);
}
} else if (UNEXPECTED(ZEND_TYPE_IS_SET(child_info->type) && !ZEND_TYPE_IS_SET(parent_info->type))) {
zend_error_noreturn(...);
}
修饰符等级不能变大,如 protected 不能变成 private。
修改 default_properties_table,将字类的重复变量覆盖到前面的父类上面,修改字类该变量的偏移,然后尝试清空字类里面的多余数据。
最后检测父类该变量是否有规定类型,若有规定类型则检测父类和字类重复变量的类型是否能共存,具体判断就不说了。
如果是 private 类型就不会进行改动,但是会给字类的这个变量加上一个 ZEND_ACC_CHANGED 的标识,后面实例化的时候会有不同的处理。
class 在内存中的存放
为了方便理解,最好一边调试一边看内存,class:
pwndbg> p *ce
$5 = {
default_properties_count = 4,
default_properties_table = 0x7ffff3e6c240,
properties_info = {
gc = {
refcount = 1,
u = {
type_info = 23
}
},
u = {
v = {
flags = 16 '\020',
_unused = 0 '\000',
nIteratorsCount = 0 '\000',
_unused2 = 0 '\000'
},
flags = 16
},
nTableMask = 4294967280,
arData = 0x7ffff3e5c7c0,
nNumUsed = 4,
nNumOfElements = 4,
nTableSize = 8,
nInternalPointer = 0,
nNextFreeElement = 0,
pDestructor = 0x0
},
properties_info_table = 0x7ffff3e03368,
}
default_properties_count,非静态成员变量数目。
default_properties_table,zval 结构体数组,前 8 字节表示值,后 8 字节表示类型等数据:
pwndbg> x/20xg ce->default_properties_table
0x7ffff3e6c240: 0x00007ffff3e6b5a0 0x0000000000000006
0x7ffff3e6c250: 0x0000000000000001 0x0000000000000004
0x7ffff3e6c260: 0x00007ffff3e562a0 0x0000000000000307
0x7ffff3e6c270: 0x00007ffff3e562a0 0x0000000100000000
properties_info,长度为 4 的哈希表,arData 为 Bucket 数组,其中后 16 字节中的前 8 字节为 hash,后 8 字节为 zend_string 类型的 key:
pwndbg> x/20xg ce->properties_info->arData
0x7ffff3e5c7c0: 0x00007ffff3e032c8 0xffffffff0000000e
0x7ffff3e5c7d0: 0x800000000002b606 0x00000000016437d0
0x7ffff3e5c7e0: 0x00007ffff3e032f0 0xffffffff0000000e
0x7ffff3e5c7f0: 0x800000000002b607 0x0000000001643800
0x7ffff3e5c800: 0x00007ffff3e03318 0xffffffff0000000e
0x7ffff3e5c810: 0x800000000002b608 0x0000000001643830
0x7ffff3e5c820: 0x00007ffff3e03340 0xffffffff0000000e
0x7ffff3e5c830: 0x800000000002b609 0x0000000001643860
前 16 字节为 ptr 类型的 zval,指向一个 zend_property_info 结构体:
pwndbg> x/20xg (zend_property_info*)ce->properties_info->arData->val->value->ptr
0x7ffff3e032c8: 0x0000000100000028 0x00000000016437d0
0x7ffff3e032d8: 0x0000000000000000 0x00007ffff3e03100
0x7ffff3e032e8: 0x0000000000000000 0x0000000200000038
0x7ffff3e032f8: 0x00007ffff3e6b640 0x0000000000000000
0x7ffff3e03308: 0x00007ffff3e03100 0x0000000000000000
0x7ffff3e03318: 0x0000000400000048 0x00007ffff3e01aa0
0x7ffff3e03328: 0x0000000000000000 0x00007ffff3e03100
0x7ffff3e03338: 0x0000000000000000 0x0000000400000058
0x7ffff3e03348: 0x00007ffff3e01ac8 0x0000000000000000
0x7ffff3e03358: 0x00007ffff3e03100 0x0000000000000018
pwndbg> p *(zend_property_info*)ce->properties_info->arData->val->value->ptr
$17 = {
offset = 40,
flags = 1,
name = 0x16437d0,
doc_comment = 0x0,
ce = 0x7ffff3e03100,
type = 0
}
properties_info_table,指向 properties_info 内 zend_property_info 结构体的指针数组,可以对照上面的存放地址来看:
pwndbg> x/20xg ce->properties_info_table
0x7ffff3e03368: 0x00007ffff3e032c8 0x00007ffff3e032f0
0x7ffff3e03378: 0x00007ffff3e03318 0x00007ffff3e03340
class 管理
在将一个 class 的相关数据都写入内存之后,自然需要有一个统一的管理,不然 new 的时候都不知道去哪里找这个类。PHP 底层用来管理类的就是一个哈希表 class_table,可以根据类名来查找类。class_table 存放 class 的结构大致如下:
pwndbg> p *(Bucket*)(CG(class_table)->arData+158)
$38 = {
val = {
value = {
ptr = 0x7ffff3e03100,
},
u1 = {
v = {
type = 14 '\016',
type_flags = 0 '\000',
u = {
extra = 0
}
},
type_info = 14
},
},
h = 9223378990928643873,
key = 0x7ffff3e6b520
}
pwndbg> x/10s (*(Bucket*)(CG(class_table)->arData+158))->key->val
0x7ffff3e6b538: "twings"
object
object 结构体
对象结构体如下:
struct _zend_object {
zend_refcounted_h gc;
uint32_t handle; // TODO: may be removed ???
zend_class_entry *ce;
const zend_object_handlers *handlers;
HashTable *properties;
zval properties_table[1];
};
比类的结构体简单很多,跟成员变量相关的是 properties 和 properties_table 两个成员。
根据类名查找类
PHP 底层有一个函数 zend_lookup_class_ex,可以根据类名查找类:
ZEND_API zend_class_entry *zend_lookup_class_ex(zend_string *name, zend_string *key, uint32_t flags) /* {{{ */
{
zend_class_entry *ce = NULL;
zval args[1], *zv;
zval local_retval;
zend_string *lc_name;
zend_fcall_info fcall_info;
zend_fcall_info_cache fcall_cache;
zend_class_entry *orig_fake_scope;
if (key) {
lc_name = key;
} else {
if (name == NULL || !ZSTR_LEN(name)) {
return NULL;
}
if (ZSTR_VAL(name)[0] == '\\') {
lc_name = zend_string_alloc(ZSTR_LEN(name) - 1, 0);
zend_str_tolower_copy(ZSTR_VAL(lc_name), ZSTR_VAL(name) + 1, ZSTR_LEN(name) - 1);
} else {
lc_name = zend_string_tolower(name);
}
}
zv = zend_hash_find(EG(class_table), lc_name);
if (zv) {
if (!key) {
zend_string_release_ex(lc_name, 0);
}
ce = (zend_class_entry*)Z_PTR_P(zv);
...
return ce;
}
...
}
可以看到命名空间 \ 处理,类名小写处理以及查询哈希表的操作,源码里的很多标识(zend_ACC_*)都可以在 zend_compile.h 里面找到定义,结合注释和标识名可以更好理解。这个函数后面还有一段查询失败后调用 autoload 来动态加载类的代码,这里就不提了。
根据类新建对象
获取到类后,就要根据类来新建对象,处理代码在 _object_and_properties_init 函数中:
static zend_always_inline int _object_and_properties_init(zval *arg, zend_class_entry *class_type, HashTable *properties) /* {{{ */
{
if (UNEXPECTED(class_type->ce_flags & (ZEND_ACC_INTERFACE|ZEND_ACC_TRAIT|ZEND_ACC_IMPLICIT_ABSTRACT_CLASS|ZEND_ACC_EXPLICIT_ABSTRACT_CLASS))) {
if (class_type->ce_flags & ZEND_ACC_INTERFACE) {
zend_throw_error(NULL, "Cannot instantiate interface %s", ZSTR_VAL(class_type->name));
} else if (class_type->ce_flags & ZEND_ACC_TRAIT) {
zend_throw_error(NULL, "Cannot instantiate trait %s", ZSTR_VAL(class_type->name));
} else {
zend_throw_error(NULL, "Cannot instantiate abstract class %s", ZSTR_VAL(class_type->name));
}
ZVAL_NULL(arg);
Z_OBJ_P(arg) = NULL;
return FAILURE;
}
if (UNEXPECTED(!(class_type->ce_flags & ZEND_ACC_CONSTANTS_UPDATED))) {
if (UNEXPECTED(zend_update_class_constants(class_type) != SUCCESS)) {
ZVAL_NULL(arg);
Z_OBJ_P(arg) = NULL;
return FAILURE;
}
}
if (class_type->create_object == NULL) {
zend_object *obj = zend_objects_new(class_type);
ZVAL_OBJ(arg, obj);
if (properties) {
object_properties_init_ex(obj, properties);
} else {
_object_properties_init(obj, class_type);
}
} else {
ZVAL_OBJ(arg, class_type->create_object(class_type));
}
return SUCCESS;
}
新建一个空白对象后,会调用 _object_properties_init 初始化成员变量:
static zend_always_inline void _object_properties_init(zend_object *object, zend_class_entry *class_type) /* {{{ */
{
if (class_type->default_properties_count) {
zval *src = class_type->default_properties_table;
zval *dst = object->properties_table;
zval *end = src + class_type->default_properties_count;
if (UNEXPECTED(class_type->type == ZEND_INTERNAL_CLASS)) {
do {
ZVAL_COPY_OR_DUP_PROP(dst, src);
src++;
dst++;
} while (src != end);
} else {
do {
ZVAL_COPY_PROP(dst, src);
src++;
dst++;
} while (src != end);
}
}
}
简单来说就是遍历类的 default_properties_table,然后将其中数据复制到对象的 properties_table 中。这个时候,对象里面只有变量的值,用于根据 key 查询的 properties 哈希表还是空的,而哈希表的初始化需要调用 Z_OBJPROP_P,其实就是 object_handlers 的 zend_std_get_properties 函数:
ZEND_API HashTable *zend_std_get_properties(zval *object) /* {{{ */
{
zend_object *zobj;
zobj = Z_OBJ_P(object);
if (!zobj->properties) {
rebuild_object_properties(zobj);
}
return zobj->properties;
}
...
ZEND_API void rebuild_object_properties(zend_object *zobj) /* {{{ */
{
if (!zobj->properties) {
zend_property_info *prop_info;
zend_class_entry *ce = zobj->ce;
uint32_t flags = 0;
zobj->properties = zend_new_array(ce->default_properties_count);
if (ce->default_properties_count) {
zend_hash_real_init_mixed(zobj->properties);
ZEND_HASH_FOREACH_PTR(&ce->properties_info, prop_info) {
if (!(prop_info->flags & ZEND_ACC_STATIC)) {
flags |= prop_info->flags;
if (UNEXPECTED(Z_TYPE_P(OBJ_PROP(zobj, prop_info->offset)) == IS_UNDEF)) {
HT_FLAGS(zobj->properties) |= HASH_FLAG_HAS_EMPTY_IND;
}
_zend_hash_append_ind(zobj->properties, prop_info->name,
OBJ_PROP(zobj, prop_info->offset));
}
} ZEND_HASH_FOREACH_END();
if (flags & ZEND_ACC_CHANGED) {
while (ce->parent && ce->parent->default_properties_count) {
ce = ce->parent;
ZEND_HASH_FOREACH_PTR(&ce->properties_info, prop_info) {
if (prop_info->ce == ce &&
!(prop_info->flags & ZEND_ACC_STATIC) &&
(prop_info->flags & ZEND_ACC_PRIVATE)) {
zval zv;
if (UNEXPECTED(Z_TYPE_P(OBJ_PROP(zobj, prop_info->offset)) == IS_UNDEF)) {
HT_FLAGS(zobj->properties) |= HASH_FLAG_HAS_EMPTY_IND;
}
ZVAL_INDIRECT(&zv, OBJ_PROP(zobj, prop_info->offset));
zend_hash_add(zobj->properties, prop_info->name, &zv);
}
} ZEND_HASH_FOREACH_END();
}
}
}
}
}
遍历类的 properties_info,从里面取出该成员变量的变量名、标识以及在对象中存储的偏移,再根据偏移获取 properties_table 中的 zval 数据存入 properties 中,需要注意的是这里的 key 是根据修饰符修改后的 key,所以 properties 存储的其实就是 properties_table 的数据,里面没有父类的重复变量。
就像上文所说,在继承的情况下,如果重写了父类的私有变量,就会有一个 ZEND_ACC_CHANGED 的标识,在遍历完子类的 properties_info 之后,还会往上遍历父类的 properties_info,将父类的重复变量也放进 properties 里面。这是因为私有属性无法被继承,但是可以被父类的方法访问,所以需要保留下来。
反序列化
反序列化数据结构体
typedef struct {
zend_long used_slots;
void *next;
zval *data[VAR_ENTRIES_MAX];
} var_entries;
typedef struct {
zend_long used_slots;
void *next;
zval data[VAR_DTOR_ENTRIES_MAX];
} var_dtor_entries;
struct php_unserialize_data {
var_entries *last;
var_dtor_entries *first_dtor;
var_dtor_entries *last_dtor;
HashTable *allowed_classes;
HashTable *ref_props;
zend_long cur_depth;
zend_long max_depth;
var_entries entries;
};
cur_depth、max_depth 很好理解,就是反序列化的深度和最大深度。
allowed_classes 也很好理解,允许反序列化的类。
last、entries,构成了一个类似栈链的结构,last 指向最后一个栈,entries 是第一个栈,存储反序列化过程数据。比如反序列化一个 Object,反序列化完前面 O 的部分后会将这个部分放进栈里,再去反序列化第一个 key。
first_dtor、last_dtor,同样是一个栈链的结构,存储反序列化中的临时变量。
ref_props,用来处理 PHP 的变量引用的类型问题。
反序列化流程
以 Object 为例。
PHP_FUNCTION(unserialize)
根据 options 配置反序列化参数,包括 max_depth 和 allowed_classes 等,然后调用 php_var_unserialize。
php_var_unserialize
调用 php_var_unserialize_internal 开始反序列化。
php_var_unserialize_internal
把 rval push 进栈里,rval 是一个存储反序列化过程数据的全局变量,然后开始词法分析。
词法分析过程很好理解,在分析出类名之后,调用 zend_lookup_class 尝试获取类,如果获取不到则尝试调用 unserialize_callback_func 动态加载类,加载完后会再次调用 zend_lookup_class 获取类。
获取到内存中的类后,会判断该类是否定义了 __unserialize 函数,定义了该函数的反序列化过程与一般的类不太一样,具体过程可以看这里,会将类成员作为一个数组然后调用函数。然后调用 object_init_ex 实例化一个新的对象,进入 object_common 方法。
object_common
如果定义了 __unserialize 函数,php 会调用 process_nested_data 方法将里面的数据反序列化为一个数组,然后跟对象一起放入栈中,在最后 php_var_unserialize_destroy 的时候调用。
如果没有定义,就会判断是否定义了 __wakeup,流程跟上面的一样。然后调用 Z_OBJPROP_P 建立对象的 properties 哈希表,同样调用 process_nested_data 反序列化对象内数据。
process_nested_data
首先是反序列化深度的检测,然后就开始根据对象/数组内部数据的数量逐个反序列化。哈希表的更新都很好懂,如果该变量已经有默认值,就将它更新为空,而在向哈希表里添加数据的时候,properties_table 因为哈希表没有对应的指针,所以不会添加。这里重点说一下 string_key 里面是怎么将处理成员变量名的。
首先会再次调用 php_var_unserialize_internal,解析出 key,然后关注 zend_unmangle_property_name_ex 函数:
ZEND_API int zend_unmangle_property_name_ex(const zend_string *name, const char **class_name, const char **prop_name, size_t *prop_len) /* {{{ */
{
size_t class_name_len;
size_t anonclass_src_len;
*class_name = NULL;
if (!ZSTR_LEN(name) || ZSTR_VAL(name)[0] != '\0') {
*prop_name = ZSTR_VAL(name);
if (prop_len) {
*prop_len = ZSTR_LEN(name);
}
return SUCCESS;
}
if (ZSTR_LEN(name) < 3 || ZSTR_VAL(name)[1] == '\0') {
// FAILURE;
}
class_name_len = zend_strnlen(ZSTR_VAL(name) + 1, ZSTR_LEN(name) - 2);
if (class_name_len >= ZSTR_LEN(name) - 2 || ZSTR_VAL(name)[class_name_len + 1] != '\0') {
// FAILURE;
}
*class_name = ZSTR_VAL(name) + 1;
anonclass_src_len = zend_strnlen(*class_name + class_name_len + 1, ZSTR_LEN(name) - class_name_len - 2);
if (class_name_len + anonclass_src_len + 2 != ZSTR_LEN(name)) {
class_name_len += anonclass_src_len + 1;
}
*prop_name = ZSTR_VAL(name) + class_name_len + 2;
if (prop_len) {
*prop_len = ZSTR_LEN(name) - class_name_len - 2;
}
return SUCCESS;
}
这里会对词法解析出来的 key 进行拆解,如果是 public,即 key 不是 \x00 就会直接返回;然后开始拆解 key,从里面把类名扒出来,再将拆完剩下的最初的变量名放到 prop_name 里面,可以直接去类的哈希表 properties_info 里面查找这个变量在服务端的修饰符,这样变量修饰符就不会被序列化数据影响了。
后面就没有什么好说的了,之后会再调用 php_var_unserialize_internal,分析出变量值。反序列化结束后,会调用 php_var_unserialize_destroy,清理栈内数据,调用反序列化魔术方法。
整个反序列化过程其实就是个递归的过程。
Orz