php 反序列化格式基础

 

PHP 序列化(serialize)格式详解

转载于:php 序列化(serialize)格式详解 - 達達尼亞 - 博客园

PHP 对不同类型的数据用不同的字母进行表示:

对普通字符串的序列化分为 2 种。**一种是 non-escaped 字符串,使用 s 标识;另一种是 escaped 字符串,使用大写 S 标识。

  • a - array
  • b - boolean
  • d - double
  • i - integer
  • o - common object
  • r - reference
  • s - non-escaped binary string
  • S - escaped binary string
  • C - custom object 自定义对象
  • O - class
  • N - null
  • R - pointer reference 指针引用
  • U - unicode string

NULL 和标量类型的序列化

NULL 的序列化

在 PHP 中,NULL 被序列化为:

N;

boolean 型数据的序列化

boolean 型数据被序列化为:

b:;

其中 为 0 或 1,当 boolean 型数据为 false 时, 为 0,否则为 1。

integer 型数据的序列化

integer 型数据(整数)被序列化为:

i:<number>;

其中 <number> 为一个整型数,范围为:-2147483648 到 2147483647。数字前可以有正负号,如果被序列化的数字超过这个范围,则会被序列化为浮点数类型而不是整型。如果序列化后的数字超过这个范围 (PHP 本身序列化时不会发生这个问题),则反序列化时,将不会返回期望的数值。

double 型数据的序列化

double 型数据(浮点数)被序列化为:

d:<number>;

其中 <number> 为一个浮点数,其范围与 PHP 中浮点数的范围一样。可以表示成整数形式、浮点数形式和科学技术法形式。如果序列化无穷大数,则 <number> 为 INF,如果序列化负无穷大,则 <number> 为 -INF。序列化后的数字范围超过 PHP 能表示的最大值,则反序列化时返回无穷大(INF),如果序列化后的数字范围超过 PHP 所能表示的最小精度,则反序列化时返回 0。当浮点数为非数时,被序列化为 NAN,NAN 反序列化时返回 0。但其它语言可以将 NAN 反序列化为相应语言所支持的 NaN 表示。

string 型数据的序列化

string 型数据(字符串)被序列化为:

s:<length>:"<value>";

其中 <length><value> 的长度,<length> 是非负整数,数字前可以带有正号(+)。<value> 为字符串值,这里的每个字符都是单字节字符,其范围与 ASCII 码的 0 - 255 的字符相对应。每个字符都表示原字符含义,没有转义字符,<value> 两边的引号(”“)是必须的,但不计算在 <length> 当中。这里的 <value> 相当于一个字节流,而 <length> 是这个字节流的字节个数。

escaped binary string 方式

这是相对与上面那种 non-escaped binary string 方式来说的:

S:<length>:”";

其中 <length> 是源字符串的长度,而非 <value> 的长度。<length> 是非负整数,数字前可以带有正号(+)。<value> 为经过转义之后的字符串。

它的转义编码很简单,对于 ASCII 码小于 128 的字符(但不包括 \),按照单个字节写入(与 s 标识的相同),对于 128~255 的字符和 \ 字符,则将其 ASCII 码值转化为 16 进制编码的字符串,以 \ 作为开头,后面两个字节分别是这个字符的 16 进制编码,顺序按照由高位到低位排列,也就是第 8-5 位所对应的16进制数字字符(abcdef 这几个字母是小写)作为第一个字节,第 4-1 位作为第二个字节。依次编码下来,得到的就是 <value> 的内容了。

简单复合类型的序列化

PHP 中的复合类型有数组(array)和对象(object)两种.

数组的序列化

数组(array)通常被序列化为:

a:\<n>:{<key 1><value 1><key 2><value 2>...\<key n>\<value n>}

其中 <n> 表示数组元素的个数,<key 1>、<key 2>……<key n> 表示数组下标,<value 1>、<value 2>……<value n> 表示与下标相对应的数组元素的值。

下标的类型只能是整型或者字符串型(包括后面那种 Unicode 字符串型),序列化后的格式跟整型和字符串型数据序列化后的格式相同。

数组元素值可以是任意类型,其序列化后的格式与其所对应的类型序列化后的格式相同。

对象的序列化

对象(object)通常被序列化为:

O:<length>:"<class name>":<n>:{<field name 1><field value 1><field name 2><field value 2>...<field name n><field value n>}

其中 <length> 表示对象的类名 <class name> 的字符串长度。<n> 表示对象中的字段1个数。这些字段包括在对象所在类及其祖先类中用 var、public、protected 和 private 声明的字段,但是不包括 static 和 const 声明的静态字段。也就是说只有实例(instance)字段。

<filed name 1>、<filed name 2>……<filed name n> 表示每个字段的字段名,而 <filed value 1>、<filed value 2>……<filed value n> 则表示与字段名所对应的字段值。

字段名是字符串型,序列化后格式与字符串型数据序列化后的格式相同。

字段值可以是任意类型,其序列化后的格式与其所对应的类型序列化后的格式相同。

但字段名的序列化与它们声明的可见性是有关的,下面重点讨论一下关于字段名的序列化。

对象字段名的序列化

var 和 public 声明的字段都是公共字段,因此它们的字段名的序列化格式是相同的。公共字段的字段名按照声明时的字段名进行序列化,但序列化后的字段名中不包括声明时的变量前缀符号 $。

protected 声明的字段为保护字段,在所声明的类和该类的子类中可见,但在该类的对象实例中不可见。因此保护字段的字段名在序列化时,字段名前面会加上

\0*\0

的前缀。这里的 \0 表示 ASCII 码为 0 的字符,而不是 \0 组合。

private 声明的字段为私有字段,只在所声明的类中可见,在该类的子类和该类的对象实例中均不可见。因此私有字段的字段名在序列化时,字段名前面会加上

\0<declared class name>\0

的前缀。这里 <declared class name> 表示的是声明该私有字段的类的类名,而不是被序列化的对象的类名。因为声明该私有字段的类不一定是被序列化的对象的类,而有可能是它的祖先类。

字段名被作为字符串序列化时,字符串值中包括根据其可见性所加的前缀。字符串长度也包括所加前缀的长度。其中 \0 字符也是计算长度的。


注: 在 PHP 手册中,字段被称为属性,而实际上,在 PHP 5 中引入的用 __set、__get 来定义的对象成员更适合叫做属性。因为用 __set、__get 来定义的对象成员与其它语言中的属性的行为是一致,而 PHP 手册中所说的属性实际上在其他语言中(例如:C#)中被称为字段,为了避免混淆,这里也称为字段,而不是属性。

嵌套复合类型的序列化

上一章讨论了简单的复合类型的序列化,大家会发现对于简单的数组和对象其实也很容易。但是如果遇到自己包含自己或者 A 包含 B,B 又包含 A 这类的对象或数组时,PHP 又该如何序列化这种对象和数组呢?本章我们就来讨论这种情况下的序列化形式。

对象引用和指针引用

在 PHP 中,标量类型数据是值传递的,而复合类型数据(对象和数组)是引用传递的。但是复合类型数据的引用传递和用 & 符号明确指定的引用传递是有区别的,前者的引用传递是对象引用,而后者是指针引用。

在解释对象引用和指针引用之前,先让我们看几个例子。

<?php
echo "<pre>";
class SampleClass {
   var $value;
}
$a = new SampleClass();
$a->value = $a;
 
$b = new SampleClass();
$b->value = &$b;
 
echo serialize($a);
echo "\n";
echo serialize($b);
echo "\n";
echo "</pre>";
?>

这个例子的输出结果是这样的:

O:11:"SampleClass":1:{s:5:"value";r:1;}
O:11:"SampleClass":1:{s:5:"value";R:1;}

大家会发现,这里变量 $a 的 value 字段的值被序列化成了 r:1,而 $b 的 value 字段的值被序列化成了 R:1。

但是对象引用和指针引用到底有什么区别呢?

大家可以看下面这个例子:

echo "<pre>";
class SampleClass {
   var $value;
}
$a = new SampleClass();
$a->value = $a;
 
$b = new SampleClass();
$b->value = &$b;
 
$a->value = 1;
$b->value = 1;
 
var_dump($a);
var_dump($b);
echo "</pre>";

大家会发现,运行结果也许出乎你的预料:

object(SampleClass)#1 (1) {
 ["value"]=>
 int(1)
}
int(1)

改变 $a->value 的值仅仅是改变了 $a->value 的值,而改变 $b->value 的值却改变了 $b 本身,这就是对象引用和指针引用的区别。

不过很不幸的是,PHP 对数组的序列化犯了一个错误,虽然数组本身在传递时也是对象引用传递,但是在序列化时,PHP 似乎忘记了这一点,看下面的例子:

echo "<pre>";
$a = array();
$a[1] = 1;
$a["value"] = $a;
 
echo $a["value"]["value"][1];
echo "\n";
$a = unserialize(serialize($a));
echo $a["value"]["value"][1];
echo "</pre>";

结果是:

1

大家会发现,将原数组序列化再反序列化后,数组结构变了。原本 $a\["value"]\["value"][1] 中的值 1,在反序列化之后丢失了。

原因是什么呢?让我们输出序列化之后的结果来看一看:

$a = array();
$a[1] = 1;
$a["value"] = $a;

echo serialize($a);

结果是:

a:2:{i:1;i:1;s:5:"value";a:2:{i:1;i:1;s:5:"value";N;}}

原来,序列化之后,$a["value"]["value"] 变成了 NULL,而不是一个对象引用。

也就是说,PHP 只对对象在序列化时才会生成对象引用标示(r)。对所有的标量类型和数组(也包括 NULL)序列化时都不会生成对象引用。但是如果明确使用了 & 符号作的引用,在序列化时,会被序列化为指针引用标示(R)。

引用标示后的数字

在上面的例子中大家可能已经看到了,对象引用(r)和指针引用(R)的格式为:

r:<number>;
R:<number>;

大家一定很奇怪后面这个 <number> 是什么吧?本节我们就来详细讨论这个问题。

这个 <number> 简单的说,就是所引用的对象在序列化串中第一次出现的位置,但是这个位置不是指字符的位置,而是指对象(这里的对象是泛指所有类型的量,而不仅限于对象类型)的位置。

我想大家可能还不是很明白,那么我来举例说明一下:

class ClassA {
   var $int;
   var $str;
   var $bool;
   var $obj;
   var $pr;
}

$a = new ClassA();
$a->int = 1;
$a->str = "Hello";
$a->bool = false;
$a->obj = $a;
$a->pr = &$a->str;

echo serialize($a);

这个例子的结果是:

O:6:"ClassA":5:{s:3:"int";i:1;s:3:"str";s:5:"Hello";s:4:"bool";b:0;s:3:"obj";r:1;s:2:"pr";R:3;}

在这个例子中,首先序列化的对象是 ClassA 的一个对象,那么给它编号为 1,接下来要序列化的是这个对象的几个成员,第一个被序列化的成员是 int 字段,那它的编号就为 2,接下来被序列化的成员是 str,那它的编号就是 3,依此类推,到了 obj 成员时,它发现该成员已经被序列化了,并且编号为 1,因此它被序列化时,就被序列化成了 r:1; ,在接下来被序列化的是 pr 成员,它发现该成员实际上是指向 str 成员的一个引用,而 str 成员的编号为 3,因此,pr 就被序列化为 R:3; 了。

PHP 是如何来编号被序列化的对象的呢?实际上,PHP 在序列化时,首先建立一个空表,然后每个被序列化的对象在被序列化之前,都需要先计算该对象的 Hash 值,然后判断该 Hash 值是否已经出现在该表中了,如果没有出现,就把该 Hash 值添加到这个表的最后,返回添加成功。如果出现了,则返回添加失败,但是在返回失败前先判断该对象是否是一个引用(用 & 符号定义的引用),如果不是则也把 Hash 值添加到表后(尽管返回的是添加失败)。如果返回失败,则同时返回上一次出现的位置。

在添加 Hash 值到表中之后,如果添加失败,则判断添加的是一个引用还是一个对象,如果是引用,则返回 R 标示,如果是对象,则返回 r 标示。因为失败时,会同时返回上一次出现的位置,因此,R 和 r 标示后面的数字,就是这个位置。

对象引用的反序列化

PHP 在反序列化处理对象引用时很有意思,如果反序列化的字符串不是 PHP 的 serialize() 本身生成的,而是人为构造或者用其它语言生成的,即使对象引用指向的不是一个对象,它也能正确地按照对象引用所指向的数据进行反序列化。例如:

echo "<pre>";
class StrClass {
   var $a;
   var $b;
}
 
$a = unserialize('O:8:"StrClass":2:{s:1:"a";s:5:"Hello";s:1:"b";r:2;}');
 
var_dump($a);
echo "</pre>";

运行结果:

object(StrClass)#1 (2) {
 ["a"]=>
 string(5) "Hello"
 ["b"]=>
 string(5) "Hello"
}

大家会发现,上面的例子反序列化后,$a->b 的值与 $a->a 的值是一样的,尽管 $a->a 不是一个对象,而是一个字符串。因此如果大家用其它语言来实现序列化的话,不一定非要把 string 作为标量类型来处理,即使按照对象引用来序列化拥有相同字符串内容的复合类型,用 PHP 同样可以正确的反序列化。这样可以更节省序列化后的内容所占用的空间。

自定义对象序列化

PHP 4 中自定义对象序列化

PHP 4 中提供了 __sleep 和 __wakeup 这两个方法来自定义对象的序列化。不过这两个函数并不改变对象序列化的格式,影响的仅仅是被序列化字段的个数。

PHP 5 中自定义对象序列化

PHP 5 中增加了接口(interface)功能。PHP 5 本身提供了一个 Serializable 接口,如果用户在自己定义的类中实现了这个接口,那么在该类的对象序列化时,就会被按照用户实现的方式去进行序列化,并且序列化后的标示不再是 O,而改为 C。C 标示的格式如下:

C:<name length>:"<class name>":<data length>:{<data>}

其中 <name length> 表示类名 <class name> 的长度,<data length> 表示自定义序列化数据 <data> 的长度,而自定义的序列化数据 <data> 是完全的用户自己定义的格式,与 PHP 序列化格式可以完全无关,这部分数据由用户自己实现的序列化和反序列化接口方法来管理。

Serializable 接口中定义了 2 个方法,serialize()unserialize($data),这两个方法不会被直接调用,而是在调用 PHP 序列化函数时,被自动调用。其中 serialize 函数没有参数,它的返回值就是 <data> 的内容。而 unserialize($data) 有一个参数 $data,这个参数的值就是 <data> 的内容。这样大家应该就明白了,实际上接口中 serialize 方法就是让用户来自己序列化对象中的内容,序列化后的内容格式,PHP 并不关心,PHP 只负责把它充填到 中,等到反序列化时,PHP 只负责取出这部分内容,然后传给用户实现的 unserialize($data) 接口方法,让用户自己去反序列化这部分内容。

下面举个简单的例子,来说明 Serializable 接口的使用:

class MyClass implements Serializable
{
   public $member;
 
   function MyClass()
   {
     $this->member = 'member value';
   }
 
   public function serialize()
   {
     return wddx_serialize_value($this->member);
   }
 
   public function unserialize($data)
   {
     $this->member = wddx_deserialize($data);
   }
}
$a = new MyClass();
echo serialize($a);
echo "\n";
print_r(unserialize(serialize($a)));

输出结果为(浏览器中的源代码):

C:7:"MyClass":90:{<wddxPacket version='1.0'><header/><data><string>member value</string></data></wddxPacket>}
MyClass Object
(
   [member] => member value
)

因此如果想用其它语言来实现 PHP 序列化中的 C 标示的话,也需要提供一种这样的机制,让用户自定义类时,能够自己在反序列化时处理 内容,否则,这些内容就无法被反序列化了。

Unicode 字符串的序列化

PHP Unicode 字符串的序列化格式如下:

U:<length>:"<unicode string>";

这里 <length> 是指原 Unicode String 的长度,而不是 <unicode string> 的长度,因为 <unicode string> 是经过编码以后的字节流了。

但是还有一点要注意,<length> 尽管是原 Unicode String 的长度,但是也不是只它的字节数,当然也不完全是指它的字符数,确切的说是之它的字符单位数。因为 Unicode String 中采用的是 UTF16 编码,这种编码方式使用 16 位来表示一个字符的,但是并不是所有的都是可以用 16 位表示的,因此有些字符需要两个 16 位来表示一个字符。因此,在 UTF16 编码中,16 位字符算作一个字符单位,一个实际的字符可能就是一个字符单位,也有可能由两个字符单位组成。因此, Unicode String 中字符数并不总是等于字符单位数,而这里的 <length> 指的就是字符单位数,而不是字符数。

又是怎样被编码的呢?实际上,它的编码也很简单,对于编码小于 128 的字符(但不包括 \),按照单个字节写入,对于大于 128 的字符和 \ 字符,则转化为 16 进制编码的字符串,以 \ 作为开头,后面四个字节分别是这个字符单位的 16 进制编码,顺序按照由高位到低位排列,也就是第 16-13 位所对应的16进制数字字符(abcdef 这几个字母是小写)作为第一个字节,第 12-9 位作为第二个字节,第 8-5 位作为第三个字节,最后的第 4-1 位作为第四个字节。依次编码下来,得到的就是 的内容了。

php 反序列化魔术方法

方法列表

起点
__destruct() //对象被销毁时触发
__wakeup() //unserialize() 会检查是否存在一个 __wakeup() 方法。如果存在,则会先调用 __wakeup 方法,预先准备对象需要的资源。
__toString() //__toString() 方法用于一个类被当成字符串时应怎样回应
   

中间跳板
__call() //在对象上下文中调用不可访问的方法时触发
__callStatic() //在静态上下文中调用不可访问的方法时触发
__get() //用于从不可访问的属性读取数据
__set() //用于将数据写入不可访问的属性
__isset() //在不可访问的属性上调用isset()或empty()触发
__unset() //在不可访问的属性上使用unset()时触发
__invoke() //当脚本尝试将对象调用为函数时触发
__toString() //

终点
__call: 调用不可访问或不存在的方法时被调用
call_user_funccall_user_func_array 等代码执行点
    
其他
__construct()//创建对象时触发0
__sleep() //对象被序列化之前触发

__toString 方法触发条件

这个 __toString 触发的条件比较多,也因为这个原因容易被忽略,常见的触发条件有下面几种

  • echo ($obj) / print($obj) 打印时会触发
  • 反序列化对象与字符串连接时
  • 反序列化对象参与格式化字符串时
  • 反序列化对象与字符串进行==比较时(PHP进行==比较的时候会转换参数类型)
  • 反序列化对象参与格式化SQL语句,绑定参数时
  • 反序列化对象在经过php字符串函数,如 strlen()、addslashes()时
  • 在in_array()方法中,第一个参数是反序列化对象,第二个参数的数组中有 toString返回的字符串的时候 toString 会被调用
  • 反序列化的对象作为 class_exists() 的参数的时候
  • 作为 file_exists() 函数时也会触发

参考链接