前言 想研究一下 PHP 的 unserialize 流程的想法是从网鼎杯的一道 PHP 反序列化题开始的,当时发现本地 public 序列化的字段可以在远程反序列化成 protected,就觉得很神奇,于是计划阅读源码跟进一下。
本文主要侧重类的非静态成员变量。
测试用类 1 2 3 4 5 6 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 函数:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 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))); } } ... 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 之后的结果:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 #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 #define IS_CONSTANT_AST 11 #define IS_INDIRECT 13 #define IS_PTR 14 #define IS_ALIAS_PTR 15 #define _IS_ERROR 15 #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 结构体:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 struct _zend_class_entry { char type; zend_string *name; 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
成员变量写入内存 继续看代码,将非静态成员变量写入用户类的关键代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 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); 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 的结构如下:
1 2 3 4 5 6 7 8 9 typedef struct _zend_property_info { uint32_t offset; uint32_t flags; zend_string *name; zend_string *doc_comment; zend_class_entry *ce; zend_type type; } zend_property_info;
flags 修饰符等标识,其他数据都好理解,而 offset 用来表示内存中对象的成员变量距离头部的偏移:
1 #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 中,哈希表的结构如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 typedef struct _Bucket { zval val; zend_ulong h; zend_string *key; } 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,不知道为什么是倒着来复制的,子类的放在后面:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 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 的处理如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 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 不存在就加上去,因为父类放在前面,所以偏移不需要改变:
1 2 3 4 5 6 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:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 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); 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:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 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 字节表示类型等数据:
1 2 3 4 5 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:
1 2 3 4 5 6 7 8 9 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 结构体:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 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 结构体的指针数组,可以对照上面的存放地址来看:
1 2 3 pwndbg> x/20xg ce->properties_info_table 0x7ffff3e03368: 0x00007ffff3e032c8 0x00007ffff3e032f0 0x7ffff3e03378: 0x00007ffff3e03318 0x00007ffff3e03340
class 管理 在将一个 class 的相关数据都写入内存之后,自然需要有一个统一的管理,不然 new 的时候都不知道去哪里找这个类。PHP 底层用来管理类的就是一个哈希表 class_table,可以根据类名来查找类。class_table 存放 class 的结构大致如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 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 结构体 对象结构体如下:
1 2 3 4 5 6 7 8 struct _zend_object { zend_refcounted_h gc; uint32_t handle; zend_class_entry *ce; const zend_object_handlers *handlers; HashTable *properties; zval properties_table[1 ]; };
比类的结构体简单很多,跟成员变量相关的是 properties 和 properties_table 两个成员。
根据类名查找类 PHP 底层有一个函数 zend_lookup_class_ex,可以根据类名查找类:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 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 函数中:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 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 初始化成员变量:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 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 函数:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 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 里面。这是因为私有属性无法被继承,但是可以被父类的方法访问,所以需要保留下来。
反序列化 反序列化数据结构体 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 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 函数:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 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' ) { } 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' ) { } *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