序列化
简单来说就是将某个存储在内存中数据结构转换成可用于持久化保存的数据流或字符串。序列化是很多现代语言都会带有的一种对象操作,极大地方便了对象的存储与传输。常见的 Java、C#、Python 和 PHP 等语言都支持了序列化操作,但他们的序列化内容都各不相同,如 Java 和 Python 的序列化是产生二进制数据,而 PHP 则是产生可读字符串。
与序列化对应的是反序列化,即从一串数据中重新构建对象。需要注意的是,序列化和反序列化仅针对于数据成员,而非对象方法,其一是对象方法本质上就是可执行的代码,由于程序的运行环境不同,所以难以对可执行代码进行序列化;其次是对运行代码进行序列化容易造成注入漏洞。
PHP 的序列化结构
因为 PHP 是动态弱类型语言,所以我们在定义变量时不需要指定类型,但实际上在 PHP 解释器中,所有的数据仍然拥有类型属性,它们被分为七类:空值、布尔值、整数、浮点数、字符串、关系数组和对象。
空值即为 null
;布尔值为 true
和 false
;整数与 PHP 运行的系统位长相关,在 32 位系统下范围是 ~ ,在 64 位系统下范围是 ~ ,PHP 不支持无符号整数;浮点数为 IEEE 754 下的双精度浮点数,拥有 15 位有效数字,表示范围约 ~ ;字符串为 UTF-8 格式;关系数组的键类型可以为整数或字符串,值类型可以是任意类型;PHP 的对象类型实际上和关系数组很像,但它带有成员函数以及各种访问控制,还有继承、抽象等高级操作。
我们就这 7 种基本类型分别进行分析讲解:
空值
空值的序列化非常简单:
N;
一个大写字母 N
和一个英语分号就完事了。
布尔值
布尔值的序列化也很简单:
b:1;
b:0;
首先是 b:
开头,然后后面跟一个数字 0 或 1,分别表示 false
和 true
,最后带一个英语分号。
整数
整数也很简单:
i:19260817;
i:-114514;
首先是 i:
开头,然后输出整数,最后带一个英语分号。
浮点数
浮点数有多种情况:
d:1.23456;
d:1.0E+100;
d:INF;
d:-INF;
d:NAN;
首先是 d:
开头,然后分情况:
- 若浮点数可以用 17 位有效数字完全表示,那么直接输出小数形式,否则使用科学计数法
- 若浮点数为 INF、-INF、NAN,则直接输出
最后还是英语分号结尾。
字符串
字符串也很简单:
s:5:"apple";
s:3:"A
B";
首先是 s:
开头,然后输出字符串长度,再加一个英语分号,再输出由双引号括住的字符串,最后带一个英语分号。
需要注意的是,PHP 序列化不会转义字符串中的不可打印字符,这使得 PHP 序列化有一定的二进制成分,同时字符串中的双引号也不会被转义,所以程序不是通过双引号来识别字符串末尾,而是通过一开始输出的字符串长度。
关系数组
关系数组看起来比较乱,但实际上也是比较简单的:
a:2:{i:0;s:1:"A";i:1;s:1:"B";}
a:2:{s:1:"A";i:0;s:1:"B";i:1;}
首先是 a:
开头,然后输出数组的项数,再加一个英语分号。数组内容用花括号括住,不需要在尾部加英语分号,在花括号内部,按照任意顺序序列化每个数组项,先序列化键,再序列化值。所以你可以发现花括号里的项数永远是数组项数的两倍。
对象
对象的序列化比较复杂了:
O:4:"Test":2:{s:6:"test_a";i:114514;s:6:"test_b";a:0:{}}
首先我们需要知道几个知识点:
- PHP 不会对静态成员变量和常量进行序列化
- PHP 在序列化类的成员变量时,还会序列化基类的成员变量
- PHP 在序列化
public
成员时,不会带上$
前缀 - PHP 在序列化
protected
成员时,会将$
前缀修改为\0*\0
,其中\0
为 ASCII 码为 0 的字符 - PHP 在序列化
private
成员时,会将$
前缀修改为\0<class-name>\0
,其中\0
为 ASCII 码为 0 的字符,其中<class-name>
可能是该类本身的类名,
还可能是其基类的类名,取决于这个私有字段是属于该类本身还是属于基类
其次,对象的序列化就像是字符串序列化与关系数组序列化组合在一起,先是以 O:
开头,然后和字符串类型,输出类名长度和类名,然后又和关系数组类似,输出序列化类成员的数量及所有的序列化类成员。
扩展内容
如上的 7 种序列化类型是我们最常见的,其中除了关系数组和对象以外的其他 5 种统称为标量类型,关系数组和对象统称为复合类型。在日常使用中,你还可能看见以 r:
、R:
和 C:
开头的三种序列化,其中前两种为引用类型,后一种为自定义序列化类型。
引用类型序列化
如果把一个对象赋值给一个变量,那么这会触发对象的引用传递,这是一种节省内存空间的方法,此时这个变量就相当于一个指针。如果这个变量存在于对象中,而被赋值了对象本身,那么在序列化时,这种引用关系也需要被表达出来,由此产生了 r:
和 R:
这两种引用类型。
简单来说,r:
表示的是对象引用,你可以像操作一般变量一样给对象引用重新赋值,而不会影响其引用的类,也可以使用引用来调用引用类的成员。R:
表示的是指针引用,当你给指针引用重新赋值时,会导致其引用的类被重新赋值,效果就像操作指针指向的内存一样。
那么怎样知道我使用的是对象引用还是指针引用呢?一般对象引用是这样赋值的:
$a = new SomeClass();
$b = $a;
$b
就是一个对象引用,而如果让 $b
变成指针引用,则是这样赋值的:
$a = new SomeClass();
$b = &$a;
我们回到序列化中。假设我们现在有这样一串代码:
class Test
{
public $data;
}
$A = new Test();
$B = new Test();
$A->data = $A;
$B->data = &$B;
由我们上面得到的引用知识可知,$A
中存储了一个对象引用,$B
中存储了一个指针引用,它们俩分别序列化后是这样:
O:4:"Test":1:{s:4:"data";r:1;}
O:4:"Test":1:{s:4:"data";R:1;}
可以看见,引用序列化的格式和整数、布尔型等是很相似的,问题是后面的那个数字表示什么。这个数字代表所引用的对象第一次在序列化字符串中出现的位置。举个例子:
class Test
{
public $a;
public $b = true;
public $c = 114514;
public $d;
public $e;
public $f;
}
$A = new Test();
$A->d = $A;
$A->e = &$A;
$A->f = &$A->c;
序列化结果为:
O:4:"Test":6:{s:1:"a";N;s:1:"b";b:1;s:1:"c";i:114514;s:1:"d";r:1;s:1:"e";R:1;s:1:"f";R:4;}
我们按照对象来分割这个序列化字符串,需要注意的是,这里的对象含义是一个有意义的基本元素,比如对于关系数组和对象(数据结构意义)来说,键 + 值的组合才是有意义的。
O:4:"Test"
s:1:"a";N;
s:1:"b";b:1;
s:1:"c";i:114514;
s:1:"d";r:1;
s:1:"e";R:1;
s:1:"f";R:4;
分割的结果如上,我们从上往下,从 1 开始编号。由于 $A->d
被赋值为 $A
,这里是一个对象引用,所以是 r:
,然后引用的对象就是 Test
,从这个分割结果中看,其位置为 1,所以就是 r:1;
。以此类推,由于 $A->f
被赋值为 &$A->c
,这是一个指针引用,且对应引用的位置为 4,所以就是 R:4;
。
需要注意的是,如果引用指向了被序列化对象之外的另一个对象,那么这个引用会被当成一个新的对象进行序列化,而不会作为一个引用。
自定义序列化(PHP 8 以前)
自定义序列化是由实现了 Serializable
接口的类产生的 (PHP 8 之前),其一般形式如下:
C:<class-name-length>:"<class-name>":<data-length>:{<data>}
其中花括号前的内容与一般对象序列化相似,只不过花括号中的内容换成了用户自定义的序列化数据,如我们现在有如下类:
class Test implements Serializable
{
public $data = [];
public function serialize()
{
return json_encode($this->data);
}
public function unserialize($data)
{
$this->data = json_decode($data);
}
}
其序列化结果如下:
C:4:"Test":2:{[]}
需要注意的是,从 PHP 8 之后,你不再需要实现 Serializable
接口了,直接使用 __serialize()
和 __unserailize()
这两个魔术方法就可以完成自定义序列化,且序列化的格式也和一般对象一样了。
解析序列化字符串
有些时候我们不希望通过 PHP 来修改一个序列化的对象,如这个对象被序列化存储在数据库中,想要操作这个对象还需要写个 PHP 程序,拉取数据库,然后再修改,不如直接从数据库中把对象字符串 Dump 下来然后在其他程序里面操作(我说的就是某 T 姓博客系统)。
在了解了 PHP 序列化字符串的结构后,我们就可以实现一个解析器了。我们只需要实现 5 种标量类型与 2 种复合类型的解析,因为引用类型在实际中是在过于少见,且自定义序列化使用的接口已经过时。
我们先写一个类存储 PHP 的数据信息:
// PHP value type
struct php_value
{
// PHP value hash type
struct php_value_hash
{
size_t operator() (const php_value& tmp) const
{
if (tmp.type == PHP_T_INT)
{
return std::hash<int64_t>()(tmp.data.php_int);
}
else if (tmp.type == PHP_T_STR)
{
return std::hash<std::string>()(*tmp.data.php_str);
}
else
{
throw std::logic_error("Invalid array key type");
}
}
};
// Specific unordered map type
using umap_s_v = std::unordered_map<std::string, php_value>;
using umap_si_v = std::unordered_map<php_value, php_value, php_value_hash>;
// PHP type enumeration
using php_type = enum
{
PHP_T_NULL,
PHP_T_BOOL,
PHP_T_INT,
PHP_T_FLOAT,
PHP_T_STR,
PHP_T_ARRAY,
PHP_T_OBJECT
};
// PHP data union
using php_data = union
{
bool php_bool;
int64_t php_int;
double php_float;
std::string *php_str;
umap_si_v *php_array;
umap_s_v *php_object;
};
// Type field
php_type type;
// Data field
php_data data;
// Construct
php_value(): type(PHP_T_NULL) {}
php_value(const php_value& tmp)
{
type = tmp.type;
switch (tmp.type)
{
case PHP_T_NULL:
break;
case PHP_T_BOOL:
data.php_bool = tmp.data.php_bool;
break;
case PHP_T_INT:
data.php_int = tmp.data.php_int;
break;
case PHP_T_FLOAT:
data.php_float = tmp.data.php_float;
break;
case PHP_T_STR:
data.php_str = new std::string(*tmp.data.php_str);
break;
case PHP_T_ARRAY:
data.php_array = new umap_si_v(*tmp.data.php_array);
break;
case PHP_T_OBJECT:
data.php_object = new umap_s_v(*tmp.data.php_object);
break;
default:
throw std::logic_error("Invalid PHP type");
}
}
php_value(php_value&& tmp)
{
type = tmp.type;
switch (tmp.type)
{
case PHP_T_NULL:
break;
case PHP_T_BOOL:
data.php_bool = tmp.data.php_bool;
break;
case PHP_T_INT:
data.php_int = tmp.data.php_int;
break;
case PHP_T_FLOAT:
data.php_float = tmp.data.php_float;
break;
case PHP_T_STR:
data.php_str = tmp.data.php_str;
break;
case PHP_T_ARRAY:
data.php_array = tmp.data.php_array;
break;
case PHP_T_OBJECT:
data.php_object = tmp.data.php_object;
break;
default:
throw std::logic_error("Invalid PHP type");
}
tmp.type = PHP_T_NULL;
}
// Destruct
virtual ~php_value()
{
switch (type)
{
case PHP_T_NULL:
case PHP_T_BOOL:
case PHP_T_INT:
case PHP_T_FLOAT:
break;
case PHP_T_STR:
delete data.php_str;
break;
case PHP_T_ARRAY:
delete data.php_array;
break;
case PHP_T_OBJECT:
delete data.php_object;
break;
}
}
// Operator
bool operator==(const php_value& tmp) const
{
if (type != tmp.type)
{
return false;
}
switch (type)
{
case PHP_T_NULL:
return true;
case PHP_T_BOOL:
return data.php_bool == tmp.data.php_bool;
case PHP_T_INT:
return data.php_int == tmp.data.php_int;
case PHP_T_FLOAT:
return data.php_float == tmp.data.php_float;
case PHP_T_STR:
return *data.php_str == *tmp.data.php_str;
case PHP_T_ARRAY:
return *data.php_array == *tmp.data.php_array;
case PHP_T_OBJECT:
return *data.php_object == *tmp.data.php_object;
default:
return false;
}
}
// Print
void print(uint32_t tab = 0) const
{
for (uint32_t i = 0; i < tab; ++i)
{
std::cout.put(' ');
}
switch (type)
{
case PHP_T_NULL:
std::cout << "NULL";
break;
case PHP_T_BOOL:
std::cout << "bool(" << (data.php_bool ? "true" : "false") << ")";
break;
case PHP_T_INT:
std::cout << "int(" << data.php_int << ")";
break;
case PHP_T_FLOAT:
std::cout << "float(" << data.php_float << ")";
break;
case PHP_T_STR:
std::cout << "string(" << data.php_str->length() << ") \"" << (*data.php_str) << '"';
break;
case PHP_T_ARRAY:
std::cout << "array(" << data.php_array->size() << ") {\n";
for (const auto& i : *data.php_array)
{
for (uint32_t j = 0; j < tab + 2; ++j)
{
std::cout.put(' ');
}
std::cout.put('[');
if (i.first.type == PHP_T_INT)
{
std::cout << i.first.data.php_int;
}
else if (i.first.type == PHP_T_STR)
{
std::cout << '"' << (*i.first.data.php_str) << '"';
}
else
{
throw std::logic_error("Invalid array key type");
}
std::cout << "]=>\n";
i.second.print(tab + 2);
std::cout.put('\n');
}
for (uint32_t j = 0; j < tab; ++j)
{
std::cout.put(' ');
}
std::cout.put('}');
break;
case PHP_T_OBJECT:
std::cout << "object(" << (*data.php_object->at("$name").data.php_str) << ") (" << (data.php_object->size() - 1) << ") {\n";
for (const auto& i : *data.php_object)
{
if (i.first == "$name")
{
continue;
}
for (uint32_t j = 0; j < tab + 2; ++j)
{
std::cout.put(' ');
}
if (i.first[0] == '\0')
{
if (i.first[1] == '*')
{
if (i.first[2] == '\0')
{
std::cout << "[\"" << i.first.substr(3) << "\":protected]=>\n";
}
else
{
throw std::logic_error("Invalid PHP object access property");
}
}
else
{
size_t pos = i.first.find_first_of('\0', 1);
if (pos != std::string::npos)
{
std::cout << "[\"" << i.first.substr(pos + 1) << "\":\"" << i.first.substr(1, pos - 1) << "\":private]=>\n";
}
else
{
throw std::logic_error("Invalid PHP object access property");
}
}
}
else
{
std::cout << "[\"" << i.first << "\"]=>\n";
}
i.second.print(tab + 2);
std::cout.put('\n');
}
for (uint32_t j = 0; j < tab; ++j)
{
std::cout.put(' ');
}
std::cout.put('}');
break;
default:
throw std::logic_error("Invalid PHP type");
}
}
};
我们定义的 php_value
类除了存储 PHP 值类型,还可以使用 print()
格式化输出。
有了数据结构,那么我们就能编写解析器了。使用递归下降法进行语法解析:
// PHP serial parser
struct parser
{
// Eat
static void eat(std::ifstream& fin, char c, const std::string& err)
{
if (fin.peek() != c)
{
throw std::logic_error(err);
}
fin.get();
}
// Parse null
static php_value parse_null(std::ifstream& fin)
{
fin.get();
eat(fin, ';', "Invalid NULL");
return php_value();
}
// Parse bool
static php_value parse_bool(std::ifstream& fin)
{
fin.get();
eat(fin, ':', "Invalid BOOL");
char c = fin.get();
eat(fin, ';', "Invalid BOOL");
php_value rtn;
rtn.type = rtn.PHP_T_BOOL;
switch (c)
{
case '0':
rtn.data.php_bool = false;
break;
case '1':
rtn.data.php_bool = false;
break;
default:
throw std::logic_error("Invalid BOOL");
}
return rtn;
}
// Parse int
static php_value parse_int(std::ifstream& fin)
{
fin.get();
eat(fin, ':', "Invalid INT");
php_value rtn;
rtn.type = rtn.PHP_T_INT;
fin >> rtn.data.php_int;
eat(fin, ';', "Invalid INT");
return rtn;
}
// Parse float
static php_value parse_float(std::ifstream& fin)
{
fin.get();
eat(fin, ':', "Invalid FLOAT");
php_value rtn;
rtn.type = rtn.PHP_T_FLOAT;
fin >> rtn.data.php_float;
eat(fin, ';', "Invalid FLOAT");
return rtn;
}
// Parse string
static php_value parse_str(std::ifstream& fin)
{
fin.get();
eat(fin, ':', "Invalid STRING");
size_t len;
fin >> len;
eat(fin, ':', "Invalid STRING");
eat(fin, '"', "Invalid STRING");
php_value rtn;
rtn.type = rtn.PHP_T_STR;
rtn.data.php_str = new std::string();
for (size_t i = 0; i < len; ++i)
{
if (fin.eof())
{
throw std::logic_error("Invalid STRING");
}
*rtn.data.php_str += fin.get();
}
eat(fin, '"', "Invalid STRING");
eat(fin, ';', "Invalid STRING");
return rtn;
}
// Parse array
static php_value parse_array(std::ifstream& fin)
{
fin.get();
eat(fin, ':', "Invalid ARRAY");
size_t len;
fin >> len;
eat(fin, ':', "Invalid ARRAY");
eat(fin, '{', "Invalid ARRAY");
php_value rtn;
rtn.type = rtn.PHP_T_ARRAY;
rtn.data.php_array = new php_value::umap_si_v();
for (size_t i = 0; i < len; ++i)
{
php_value key = parse(fin);
if (key.type != key.PHP_T_INT && key.type != key.PHP_T_STR)
{
throw std::logic_error("Invalid ARRAY");
}
php_value val = parse(fin);
rtn.data.php_array->emplace(key, val);
}
eat(fin, '}', "Invalid ARRAY");
return rtn;
}
// Parse object
static php_value parse_object(std::ifstream& fin)
{
fin.get();
eat(fin, ':', "Invalid OBJECT");
size_t len;
fin >> len;
eat(fin, ':', "Invalid OBJECT");
eat(fin, '"', "Invalid OBJECT");
php_value rtn;
rtn.type = rtn.PHP_T_OBJECT;
rtn.data.php_object = new php_value::umap_s_v();
php_value tmpn;
tmpn.type = tmpn.PHP_T_STR;
tmpn.data.php_str = new std::string();
for (size_t i = 0; i < len; ++i)
{
if (fin.eof())
{
throw std::logic_error("Invalid OBJECT");
}
*tmpn.data.php_str += fin.get();
}
eat(fin, '"', "Invalid OBJECT");
rtn.data.php_object->emplace("$name", tmpn);
eat(fin, ':', "Invalid OBJECT");
fin >> len;
eat(fin, ':', "Invalid OBJECT");
eat(fin, '{', "Invalid OBJECT");
for (size_t i = 0; i < len; ++i)
{
php_value key = parse(fin);
if (key.type != key.PHP_T_STR)
{
throw std::logic_error("Invalid OBJECT");
}
php_value val = parse(fin);
rtn.data.php_object->emplace(*key.data.php_str, val);
}
eat(fin, '}', "Invalid OBJECT");
return rtn;
}
// Parse
static php_value parse(std::ifstream& fin)
{
switch (fin.peek())
{
case 'N':
return parse_null(fin);
case 'b':
return parse_bool(fin);
case 'i':
return parse_int(fin);
case 'd':
return parse_float(fin);
case 's':
return parse_str(fin);
case 'a':
return parse_array(fin);
case 'O':
return parse_object(fin);
default:
throw std::logic_error("Invalid type token");
}
}
};
由于 PHP 序列化字符串有二进制数据的成分,所以我们使用的读取手段当然是文件流。我们在主程序里把文件流建立的相关代码写一写:
int main(int argc, char* argv[])
{
if (argc != 2)
{
std::cerr << "Usage: serial <file-path>" << std::endl;
return 1;
}
std::ifstream fin(argv[1], std::ios::in | std::ios::binary);
if (!fin.is_open())
{
std::cerr << "Fail to open file: \"" << argv[1] << "\"" << std::endl;
return 1;
}
php_value root = parser::parse(fin);
root.print();
return 0;
}
这样,我们就能解析 PHP 序列化内容了。我们简单测试一个序列化字符串:
O:1:"A":3:{s:1:"a";a:3:{i:0;i:1;i:1;i:2;i:2;i:3;}s:1:"b";i:4;s:1:"c";O:1:"B":2:{s:1:"a";N;s:1:"b";s:6:"114514";}}
看起来非常的复杂,但是经过我们的程序解析,其结构就非常一目了然了:
object(A) (3) {
["c"]=>
object(B) (2) {
["b"]=>
string(6) "114514"
["a"]=>
NULL
}
["b"]=>
int(4)
["a"]=>
array(3) {
[2]=>
int(3)
[1]=>
int(2)
[0]=>
int(1)
}
}
我们的输出格式最大程度模拟了 PHP 自带的 var_dump()
格式,所以你可能看起来会感觉有一点熟悉。
当我们获得了序列化结构后,想要对它进行操作也就非常简单了,无非就是一些节点的属性修改与增删,我们就不在此演示了。