Java反序列化原理篇01-原生序列化流程分析

0x00 前言

写这篇文章的初衷,是我做JavaDeserializeLabs的Lab6,需要分析原生反序列化的流程,便想着从序列化开始分析。

0x01 demo

先创建一个Person类,作为序列化的对象:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import java.io.Serializable;

public class Person implements Serializable {
public String name;
private int age;
private transient String hobby = "Handsome";

public Person(){}

public Person(String name, int age) {
this.name = name;
this.age = age;
}
}

主类Main:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;

public class Main {
public static void main(String[] args) throws Exception{
Person person = new Person("Pax", 666);
byte[] serialize = serialize(person);
Person deserialize = (Person) deserialize(serialize);
}

public static <T> byte[] serialize(T o) throws Exception {
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
ObjectOutputStream out = new ObjectOutputStream(byteArrayOutputStream);
out.writeObject(o);
return byteArrayOutputStream.toByteArray();
}

public static <T> T deserialize(byte[] ser) throws Exception {
ObjectInputStream in = new ObjectInputStream(new ByteArrayInputStream(ser));
return (T) in.readObject();
}
}

0x02 创建对象输出流

断点打在如下位置:

image-20250410233549899

跟进到ObjectOutputStream的有参构造方法:

image-20250410234119255

首先就是设置对象输出流的协议版本。

image-20250410233822492

这里扩展一下Data Block模式:

在JDK 1.2中,有必要修改和JDK 1.1不兼容的字节流格式;为了处理这种情况,向前兼容性是必须的,一个兼容标记将会写入到字节流中,这个兼容标记是类似PROTOCOL_VERSION的格式,ObjectOutputStream中的useProtocolVersion方法会接收一个参数以表示写入的可序列化字节流的协议版本。

使用的字节流协议版本如下:

  • ObjectStreamConstants.PROTOCOL_VERSION_1:表示最初序列化字节流的格式;
  • ObjectStreamConstants.PROTOCOL_VERSION_2:表示新的外部字节流格式,基础类型的数据将会使用数据块【Data-Block】的模式写入字节流,它以标记TC_ENDBLOCKDATA结束

数据块的边界是标准化的,使用数据块模式写入字节流的基础类型的数据通常不能超过1024字节长度,这种变化的好处是固定以及规范化序列化数据格式,有利于其向前和向后的兼容性。

JDK1.2默认使用PROTOCOL_VERSION_2JDK1.1默认使用PROTOCOL_VERSION_1JDK 1.1.7版本以及以上的版本可读取以上的两种版本,而JDK 1.1.7之前的版本只能读取PROTOCOL_VERSION_1版本;

然后调用verifySubclass方法处理缓存信息,验证过程确保该类或子类的实例可以安全地被构造:

image-20250410234052018

往下是几个属性的赋值:

image-20250410234417793

属性名 类型 作用
bout ObjectInputStream.BlockDataOutputStream 类似“容器”,程序将序列化的字节内容一步步存放到bout里
handles HandleTable 一个哈希表,表示从对象到引用的映射
subs HandleTable 一个哈希表,表示从对象到“替换对象”的一个映射关系
enableOverride boolean 决定在序列化Java对象时选用writeObjectOverride方法还是writeObjec方法

赋值以后,调用了writeStreamHeader方法,向bout中写入魔术头以及版本号:

image-20250410235110251

往下是一个分支,extendedDebugInfo属性在Java序列化中用于提供更详细的调试信息,非本文重点,略。

成功初始化ObjectOutputStream,进入writeObject方法。

0x03 writeObject方法

跟进writeObject方法:

image-20250410235632552

enableOverride=false,调用writeObject0方法:

整个writeObject0方法的代码比较长,就不贴图了,直接从第一行开始。

首先代码先关闭输出流的Data Block模式,并且将原始模式赋值给变量oldMode

1
boolean oldMode = bout.setBlockDataMode(false);

往下走,依次判断obj(Person类实例)是否存在替换对象和引用,以及是否属于一些特定类:

image-20250411000722061

当然都不符合。接着走,根据obj的类型创建了desc对象:

image-20250411000858225

后面经常用到desc对象:

image-20250411001043101

跳过两个if分支,关注下一个if分支。因为传入对象实现了Serializable接口,调用writeOrdinaryObject法将数据写入字节流:

image-20250411001155795

先调用desc.checkSerialize(),该方法检查当前对象是否是一个可序列化对象,如果不是便会终止本次序列化并抛出newInvalidClassException()错误:

image-20250411001348326

image-20250411001514178

接着走,如果传入对象是一个可序列化对象,bout写入TC_OBJECT标记(表示开始)。接着调用writeClassDesc方法写入当前对象所属类的类描述信息:

image-20250411001736722

writeClassDesc方法主要用于判断当前的类描述符使用什么方式写入,如果传入的类描述信息是一个null引用,那么会调用writeNull方法,如果没有使用unshared方式,并且可以在handles对象池中找到传入的对象信息,那么调用writeHandle,如果传入的类是一个动态代理类,那么调用writeProxyDesc方法,如果上面三个条件都不满足,那么调用writeNonProxyDesc方法

跟进writeNonProxyDesc方法。首先bout写入TC_CLASSDESC标记(表示新类的描述信息);

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
private void writeNonProxyDesc(ObjectStreamClass desc, boolean unshared)
throws IOException
{
bout.writeByte(TC_CLASSDESC);
handles.assign(unshared ? null : desc);

if (protocol == PROTOCOL_VERSION_1) {
// do not invoke class descriptor write hook with old protocol
desc.writeNonProxy(this);
} else {
writeClassDescriptor(desc);
}

Class<?> cl = desc.forClass();
bout.setBlockDataMode(true);
if (cl != null && isCustomSubclass()) {
ReflectUtil.checkPackageAccess(cl);
}
annotateClass(cl);
bout.setBlockDataMode(false);
bout.writeByte(TC_ENDBLOCKDATA);

writeClassDesc(desc.getSuperDesc(), false);
}

进入writeClassDescriptor(desc),会调用writeNonProxy方法,跟进:

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
void writeNonProxy(ObjectOutputStream out) throws IOException {
out.writeUTF(name);
out.writeLong(getSerialVersionUID());

byte flags = 0;
if (externalizable) {
flags |= ObjectStreamConstants.SC_EXTERNALIZABLE;
int protocol = out.getProtocolVersion();
if (protocol != ObjectStreamConstants.PROTOCOL_VERSION_1) {
flags |= ObjectStreamConstants.SC_BLOCK_DATA;
}
} else if (serializable) {
flags |= ObjectStreamConstants.SC_SERIALIZABLE;
}
if (hasWriteObjectData) {
flags |= ObjectStreamConstants.SC_WRITE_METHOD;
}
if (isEnum) {
flags |= ObjectStreamConstants.SC_ENUM;
}
out.writeByte(flags);

out.writeShort(fields.length);
for (int i = 0; i < fields.length; i++) {
ObjectStreamField f = fields[i];
out.writeByte(f.getTypeCode());
out.writeUTF(f.getName());
if (!f.isPrimitive()) {
out.writeTypeString(f.getTypeString());
}
}
}
  1. 先调用writeUTF方法写入类名到字节流,这里的类名是类全名,带了包名的那种(out.writeUTF(name);)
  2. 再调用writeLong方法写入serialVersionUID的值到字节流(out.writeLong(getSerialVersionUID());)
  3. 然后开始写入当前类中成员属性的数量信息到字节流(out.writeShort(fields.length);)
  4. 最后如下图所示,会写入每一个字段的信息,这里的字段信息包含三部分内容:TypeCode、fieldName、fieldType

回到writeNonProxyDesc方法。往bout里写入TC_ENDBLOCKDATA标记(表示一个数据块的结束);

image-20250411002709701

到这里,writeNonProxy和writeClassDescriptor流程结束,同样,也导致writeClassDesc流程结束,并且回到writeOrdinaryObject方法。

首先根据unshared模式来判断是否将desc所表示的类元数据信息插入到handles对象的映射表中;然后,判断传入对象是否实现了外部化并且是动态代理类,并因此调用不同的方法写入对象信息:

image-20250411082829759

跟进writeSerialData方法,该方法主要向obj对象写入数据信息,比如字段值和相关引用等,写入的时候会从顶级父类从上至下递归执行。

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
private void writeSerialData(Object obj, ObjectStreamClass desc)
throws IOException
{
ObjectStreamClass.ClassDataSlot[] slots = desc.getClassDataLayout();
for (int i = 0; i < slots.length; i++) {
ObjectStreamClass slotDesc = slots[i].desc;
if (slotDesc.hasWriteObjectMethod()) {
PutFieldImpl oldPut = curPut;
curPut = null;
SerialCallbackContext oldContext = curContext;

if (extendedDebugInfo) {
debugInfoStack.push(
"custom writeObject data (class \"" +
slotDesc.getName() + "\")");
}
try {
curContext = new SerialCallbackContext(obj, slotDesc);
bout.setBlockDataMode(true);
slotDesc.invokeWriteObject(obj, this);
bout.setBlockDataMode(false);
bout.writeByte(TC_ENDBLOCKDATA);
} finally {
curContext.setUsed();
curContext = oldContext;
if (extendedDebugInfo) {
debugInfoStack.pop();
}
}

curPut = oldPut;
} else {
defaultWriteFields(obj, slotDesc);
}
}
}

在序列化传入对象之前,先从类描述信息中获取ClassDataSlot信息,在得到继承结构后,开始遍历:

先判断传入对象的类是否重写了writeObject方法,有则调用该重写方法,然后bout写入TC_ENDBLOCKDATA标记。

如果没有重写writeObject方法,则直接调用defaultWriteFields方法,该方法负责读取 obj 对象中的字段数据(desc),并且将字段数据写入到字节流中。

image-20250411084125436

还是先用checkDefaultSerialize()检查一遍当前对象是否可序列化,如果该对象不可序列化,抛出newInvalidClassException异常。

先获取该对象中所有基础类型字段的值:

image-20250411084533280

跟进到getPrimFieldValues方法:

image-20250411084643343

按照Person类的字段,首先是int类型的age:

image-20250411084921179

然后是String类型name,不是基础类型,直接结束流程。

再获取非基础类型的字段的值:

image-20250411085150708

跟进到getObjFieldValues方法,流程类似:

image-20250411085225566

在Java序列化中,符号[和L分别代表:

  • [:表示数组类型。例如,[I表示一个整数数组,[Ljava/lang/String;表示一个字符串对象数组。
  • L:表示一个对象类型。L后面跟着对象的完全限定名,例如,Ljava/lang/String;表示一个字符串对象。

然后再进入一次writeObject0方法,在这个方法里写入对象类型的字段的值,最终完成序列化操作。其实还是一个嵌套的过程。

image-20250411085611066

到这里,序列化流程也就结束了。

囿于能力有限,笔者也是草草分析。

0x04 参考

序列化流程分析总结