Java 反序列化之 readObject 分析

前言

近期上班有點忙,沒有太多空閒時間能學新東西

剛好前陣子蠻常遇到 java 反序列化,就用下班後的零碎時間稍微小跟了一下 readObject() 底層流程

雖然都是萬年老梗內容,但還是順手筆記一下追 code 的過程

大家都很熟 readObject 用法,但應該很少人實際去追過底層 (?)

(同時也順便更新一下很久沒放技術文的 Blog XD)


序列化/反序列化

  • 序列化: 把物件轉成Bytes sequences
  • 反序列化: 把Bytes sequences還原成物件

這樣做的目的,可以方便我們將物件狀態保存起來,或是用於網路傳輸中(常見於分散式架構),向不同台機器傳遞物件狀態

序列化機制在 Java 中應用非常廣泛,例如常見的 RMI、JMX、EJB 等都以此為基礎

Java 的反序列化跟 PHP 或其他語言的反序列化機制一樣,若反序列化的內容為使用者可控,將有機會導致安全問題


漏洞歷史

Java 反序列漏洞最為人知的就是 2015 年 FoxGlove Security 提出的 Apache Commons Collections 反序列化漏洞

因為 Common Collections 是一個被廣泛使用的第三方套件包

所以當時造成的影響範圍非常大,包括 WebSphere, JBoss, Jenkins, WebLogic 等都受到此漏洞影響

具體可以參考原文: https://foxglovesecurity.com/2015/11/06/what-do-weblogic-websphere-jboss-jenkins-opennms-and-your-application-have-in-common-this-vulnerability/

也就是說只要找到一個反序列化的入口點,再滿足 classpath 中有低版本 common collections 套件,就能直接走這條 gadget chain 達到 RCE

神器 ysoserial 就佛心整理了各版本 Common collections 和其它套件的 gadget chain,可以直接拿來爽爽打


readObject

在 PHP 裡面,我們可以透過 unserialize(input) 去對 input 做反序列化

而在 Java 中,通常會透過 ObjectInputStream.readObject() 作為反序列化的起始點

並且物件必須實作 java.io.Serializable 才能被序列化

直接看例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.Serializable;
public class Kaibro implements Serializable {
public String gg;
public Kaibro() {
gg = "meow";
}
private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
in.defaultReadObject();
System.out.println("QQ");
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
public class main {
public static void main(String args[]) throws Exception {
Kaibro kb = new Kaibro();
ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("/tmp/ser"));
out.writeObject(kb);
ObjectInputStream ois = new ObjectInputStream(new FileInputStream("/tmp/ser"));
Kaibro tmp = (Kaibro)ois.readObject();
System.out.println(tmp.gg);
}
}

可以看到我們透過 ObjectOutputStream.writeObject()kb 物件序列化存放到 /tmp/ser

之後透過 ObjectOutputStream.readObject()/tmp/ser 讀出來做反序列化

並且可以注意到 Kaibro class 中也有一個同名的 readObject() 方法

這個方法的作用是,讓開發者可以自定義物件反序列化還原的邏輯

HashMap 為例,它為了保持反序列化後,物件的狀態能夠一致,所以重寫了 readObject 方法來處理反序列化

而如果覆寫的 readObject 方法中有其他方法可以讓我們繼續利用的話,就有機會串下一個 gadget,最後形成一條完整的 gadget chain

例如 ysoserial 中 URLDNS 這條 gadget chain 就利用到 HashMap readObject 中的 putVal(), hash() 等方法達到發送 DNS 請求的效果


看到這裡,應該有的人會有疑問:

ObjectInputStream.readObject()之後,到底發生什麼事,又為何最後會呼叫到我們重寫的Kaibro.readObject()

後面就讓我們來跟一下 JDK 原始碼,看一下背後到底做了啥事情


分析

下面的內容,會以 JDK 8 來當作分析的目標

而在分析之前,我們先用SerializationDumper這個工具看一下前面例子造出來的序列化內容的結構:

開頭兩個 Bytesac ed 標示這是一個 Java 序列化 Stream

後面的兩個 Bytes 00 05 則是版本號

1
2
3
4
5
$ cat /tmp/ser | xxd
00000000: aced 0005 7372 0006 4b61 6962 726f e9d6 ....sr..Kaibro..
00000010: ae3b 5461 820d 0200 014c 0002 6767 7400 .;Ta.....L..ggt.
00000020: 124c 6a61 7661 2f6c 616e 672f 5374 7269 .Ljava/lang/Stri
00000030: 6e67 3b78 7074 0004 6d65 6f77 ng;xpt..meow
1
2
$ cat /tmp/ser | base64
rO0ABXNyAAZLYWlicm/p1q47VGGCDQIAAUwAAmdndAASTGphdmEvbGFuZy9TdHJpbmc7eHB0AARtZW93

所以當我們在測試 Java Web 應用時,只要看到 ac ed 00 05 ... 或是 rO0AB...(Base64) 等特徵

就可以猜測它 87% 是序列化 Stream,可以嘗試做進一步的反序列化利用


接下來直接從 ObjectInputStream.readObject() 下手,跟進 Source code:

1
2
3
4
public final Object readObject()
throws IOException, ClassNotFoundException {
return readObject(Object.class);
}

這裡直接回傳 readObject(Object.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
private final Object readObject(Class<?> type)
throws IOException, ClassNotFoundException
{
if (enableOverride) {
return readObjectOverride();
}
if (! (type == Object.class || type == String.class))
throw new AssertionError("internal error");
// if nested read, passHandle contains handle of enclosing object
int outerHandle = passHandle;
try {
Object obj = readObject0(type, false);
handles.markDependency(outerHandle, passHandle);
ClassNotFoundException ex = handles.lookupException(passHandle);
if (ex != null) {
throw ex;
}
if (depth == 0) {
vlist.doCallbacks();
}
return obj;
} finally {
passHandle = outerHandle;
if (closed && depth == 0) {
clear();
}
}
}

開頭有一個 if 判斷式,其中的 enableOverride 來自 ObjectInputStream 的 constructor:

1
2
3
4
5
public ObjectInputStream(InputStream in) throws IOException {
...
enableOverride = false;
...
}

只要是由帶參數的 constructor 建立的 ObjectInputStream 實例,這個變數值預設就是 false

當 constructor 沒有參數時,才會將 enavleOverride 設成 true:

1
2
3
4
5
protected ObjectInputStream() throws IOException, SecurityException {
...
enableOverride = true;
...
}

而條件成立後的readObjectOverride()實際上也只是個空函數,沒有任何作用

接著繼續看:

1
Object obj = readObject0(type, false);

跟進去 readObject0():

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
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
/**
* Underlying readObject implementation.
* @param type a type expected to be deserialized; non-null
* @param unshared true if the object can not be a reference to a shared object, otherwise false
*/
private Object readObject0(Class<?> type, boolean unshared) throws IOException {
boolean oldMode = bin.getBlockDataMode();
if (oldMode) {
int remain = bin.currentBlockRemaining();
if (remain > 0) {
throw new OptionalDataException(remain);
} else if (defaultDataEnd) {
/*
* Fix for 4360508: stream is currently at the end of a field
* value block written via default serialization; since there
* is no terminating TC_ENDBLOCKDATA tag, simulate
* end-of-custom-data behavior explicitly.
*/
throw new OptionalDataException(true);
}
bin.setBlockDataMode(false);
}
byte tc;
while ((tc = bin.peekByte()) == TC_RESET) {
bin.readByte();
handleReset();
}
depth++;
totalObjectRefs++;
try {
switch (tc) {
case TC_NULL:
return readNull();
case TC_REFERENCE:
// check the type of the existing object
return type.cast(readHandle(unshared));
case TC_CLASS:
if (type == String.class) {
throw new ClassCastException("Cannot cast a class to java.lang.String");
}
return readClass(unshared);
case TC_CLASSDESC:
case TC_PROXYCLASSDESC:
if (type == String.class) {
throw new ClassCastException("Cannot cast a class to java.lang.String");
}
return readClassDesc(unshared);
case TC_STRING:
case TC_LONGSTRING:
return checkResolve(readString(unshared));
case TC_ARRAY:
if (type == String.class) {
throw new ClassCastException("Cannot cast an array to java.lang.String");
}
return checkResolve(readArray(unshared));
case TC_ENUM:
if (type == String.class) {
throw new ClassCastException("Cannot cast an enum to java.lang.String");
}
return checkResolve(readEnum(unshared));
case TC_OBJECT:
if (type == String.class) {
throw new ClassCastException("Cannot cast an object to java.lang.String");
}
return checkResolve(readOrdinaryObject(unshared));
case TC_EXCEPTION:
if (type == String.class) {
throw new ClassCastException("Cannot cast an exception to java.lang.String");
}
IOException ex = readFatalException();
throw new WriteAbortedException("writing aborted", ex);
case TC_BLOCKDATA:
case TC_BLOCKDATALONG:
if (oldMode) {
bin.setBlockDataMode(true);
bin.peek(); // force header read
throw new OptionalDataException(
bin.currentBlockRemaining());
} else {
throw new StreamCorruptedException(
"unexpected block data");
}
case TC_ENDBLOCKDATA:
if (oldMode) {
throw new OptionalDataException(true);
} else {
throw new StreamCorruptedException(
"unexpected end of block data");
}
default:
throw new StreamCorruptedException(
String.format("invalid type code: %02X", tc));
}
} finally {
depth--;
bin.setBlockDataMode(oldMode);
}
}

到這裡才真正開始處理序列化Stream中的內容

開頭的bin變數一樣由 constructor 做初始化,其實可以把它想成是一個序列化 Stream 的讀取器

1
2
3
4
5
6
7
8
9
/** filter stream for handling block data conversion */
private final BlockDataInputStream bin;
public ObjectInputStream(InputStream in) throws IOException {
...
bin = new BlockDataInputStream(in);
...
bin.setBlockDataMode(true);
}

BlockDataInputStreamObjectInputStream底層的資料讀取類別,用來完成對序列化Stream的讀取

其分為兩種讀取模式: Default mode 和 Block mode

從 code 裡可以看到,如果是 Block mode,會檢查當前 block 是否有剩餘的 bytes,都沒有就轉 Default mode

接著 tc = bin.peekByte() 會去呼叫 PeekInputStream.peek()

這個 PeekInputStream 類別背後是繼承 InputStream 類別,最後呼叫的是 InputStream.read()

所以其實這邊的 tc 就是從序列化 Stream 中讀一個 Byte 出來

以我們前面Kaibro class那個例子來說,根據 SerializationDumper 的結果,可以知道 tc 會走到 TC_OBJECT 這個分支

1
2
3
4
5
case TC_OBJECT:
if (type == String.class) {
throw new ClassCastException("Cannot cast an object to java.lang.String");
}
return checkResolve(readOrdinaryObject(unshared));

常數 TC_OBJECT 對應的整數是 0x73 (可參考src),代表讀進來的是個 object

繼續跟進 readOrdinaryObject(unshared):

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
/**
* Reads and returns "ordinary" (i.e., not a String, Class,
* ObjectStreamClass, array, or enum constant) object, or null if object's
* class is unresolvable (in which case a ClassNotFoundException will be
* associated with object's handle). Sets passHandle to object's assigned
* handle.
*/
private Object readOrdinaryObject(boolean unshared)
throws IOException
{
if (bin.readByte() != TC_OBJECT) {
throw new InternalError();
}
ObjectStreamClass desc = readClassDesc(false);
desc.checkDeserialize();
Class<?> cl = desc.forClass();
if (cl == String.class || cl == Class.class
|| cl == ObjectStreamClass.class) {
throw new InvalidClassException("invalid class descriptor");
}
Object obj;
try {
obj = desc.isInstantiable() ? desc.newInstance() : null;
} catch (Exception ex) {
throw (IOException) new InvalidClassException(
desc.forClass().getName(),
"unable to create instance").initCause(ex);
}
passHandle = handles.assign(unshared ? unsharedMarker : obj);
ClassNotFoundException resolveEx = desc.getResolveException();
if (resolveEx != null) {
handles.markException(passHandle, resolveEx);
}
if (desc.isExternalizable()) {
readExternalData((Externalizable) obj, desc);
} else {
readSerialData(obj, desc);
}
handles.finish(passHandle);
if (obj != null &&
handles.lookupException(passHandle) == null &&
desc.hasReadResolveMethod())
{
Object rep = desc.invokeReadResolve(obj);
if (unshared && rep.getClass().isArray()) {
rep = cloneArray(rep);
}
if (rep != obj) {
// Filter the replacement object
if (rep != null) {
if (rep.getClass().isArray()) {
filterCheck(rep.getClass(), Array.getLength(rep));
} else {
filterCheck(rep.getClass(), -1);
}
}
handles.setObject(passHandle, obj = rep);
}
}
return obj;
}

一開頭就直接呼叫 readClassDesc(false)

繼續跟進去:

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
/**
* Reads in and returns (possibly null) class descriptor. Sets passHandle
* to class descriptor's assigned handle. If class descriptor cannot be
* resolved to a class in the local VM, a ClassNotFoundException is
* associated with the class descriptor's handle.
*/
private ObjectStreamClass readClassDesc(boolean unshared)
throws IOException
{
byte tc = bin.peekByte();
ObjectStreamClass descriptor;
switch (tc) {
case TC_NULL:
descriptor = (ObjectStreamClass) readNull();
break;
case TC_REFERENCE:
descriptor = (ObjectStreamClass) readHandle(unshared);
break;
case TC_PROXYCLASSDESC:
descriptor = readProxyDesc(unshared);
break;
case TC_CLASSDESC:
descriptor = readNonProxyDesc(unshared);
break;
default:
throw new StreamCorruptedException(
String.format("invalid type code: %02X", tc));
}
if (descriptor != null) {
validateDescriptor(descriptor);
}
return descriptor;
}

這邊的程式邏輯就跟方法名描述的一樣,會嘗試從序列化 Stream 中,構造出 class descriptor

以我們這邊的例子來說,第一個 Byte 讀到的會是 TC_CLASSDESC (0x72),代表 Class Descriptor,就是一種用來描述類別的結構,包含類別名字、成員類型等資訊

所以接下來會呼叫 descriptor = readNonProxyDesc(unshared) 來讀出這個 class descriptor

一樣繼續跟進去:

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
/**
* Reads in and returns class descriptor for a class that is not a dynamic
* proxy class. Sets passHandle to class descriptor's assigned handle. If
* class descriptor cannot be resolved to a class in the local VM, a
* ClassNotFoundException is associated with the descriptor's handle.
*/
private ObjectStreamClass readNonProxyDesc(boolean unshared)
throws IOException
{
if (bin.readByte() != TC_CLASSDESC) {
throw new InternalError();
}
ObjectStreamClass desc = new ObjectStreamClass();
int descHandle = handles.assign(unshared ? unsharedMarker : desc);
passHandle = NULL_HANDLE;
ObjectStreamClass readDesc = null;
try {
readDesc = readClassDescriptor();
} catch (ClassNotFoundException ex) {
throw (IOException) new InvalidClassException(
"failed to read class descriptor").initCause(ex);
}
Class<?> cl = null;
ClassNotFoundException resolveEx = null;
bin.setBlockDataMode(true);
final boolean checksRequired = isCustomSubclass();
try {
if ((cl = resolveClass(readDesc)) == null) {
resolveEx = new ClassNotFoundException("null class");
} else if (checksRequired) {
ReflectUtil.checkPackageAccess(cl);
}
} catch (ClassNotFoundException ex) {
resolveEx = ex;
}
// Call filterCheck on the class before reading anything else
filterCheck(cl, -1);
skipCustomData();
try {
totalObjectRefs++;
depth++;
desc.initNonProxy(readDesc, cl, resolveEx, readClassDesc(false));
} finally {
depth--;
}
handles.finish(descHandle);
passHandle = descHandle;
return desc;
}

這裡會先初始化一個 ObjectStreamClass 物件 desc,他代表的就是序列化 class descriptor

接著後面呼叫 readClassDescriptor(),它一樣會去初始化一個 ObjectStreamClass 物件

然後對這個物件呼叫 readNonProxy(this) 方法

跟進 readNonProxy():

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
/**
* Reads non-proxy class descriptor information from given input stream.
* The resulting class descriptor is not fully functional; it can only be
* used as input to the ObjectInputStream.resolveClass() and
* ObjectStreamClass.initNonProxy() methods.
*/
void readNonProxy(ObjectInputStream in)
throws IOException, ClassNotFoundException
{
name = in.readUTF();
suid = Long.valueOf(in.readLong());
isProxy = false;
byte flags = in.readByte();
hasWriteObjectData =
((flags & ObjectStreamConstants.SC_WRITE_METHOD) != 0);
hasBlockExternalData =
((flags & ObjectStreamConstants.SC_BLOCK_DATA) != 0);
externalizable =
((flags & ObjectStreamConstants.SC_EXTERNALIZABLE) != 0);
boolean sflag =
((flags & ObjectStreamConstants.SC_SERIALIZABLE) != 0);
if (externalizable && sflag) {
throw new InvalidClassException(
name, "serializable and externalizable flags conflict");
}
serializable = externalizable || sflag;
isEnum = ((flags & ObjectStreamConstants.SC_ENUM) != 0);
if (isEnum && suid.longValue() != 0L) {
throw new InvalidClassException(name,
"enum descriptor has non-zero serialVersionUID: " + suid);
}
int numFields = in.readShort();
if (isEnum && numFields != 0) {
throw new InvalidClassException(name,
"enum descriptor has non-zero field count: " + numFields);
}
fields = (numFields > 0) ?
new ObjectStreamField[numFields] : NO_FIELDS;
for (int i = 0; i < numFields; i++) {
char tcode = (char) in.readByte();
String fname = in.readUTF();
String signature = ((tcode == 'L') || (tcode == '[')) ?
in.readTypeString() : new String(new char[] { tcode });
try {
fields[i] = new ObjectStreamField(fname, signature, false);
} catch (RuntimeException e) {
throw (IOException) new InvalidClassException(name,
"invalid descriptor for field " + fname).initCause(e);
}
}
computeFieldOffsets();
}

小追一下 readUTF() 這部分的 code:

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
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
public String readUTF() throws IOException {
return readUTFBody(readUnsignedShort());
}
// ObjectInputStream.readUnsignedShort()
public int readUnsignedShort() throws IOException {
return bin.readUnsignedShort();
}
// BlockDataInputStream.readUnsignedShort()
public int readUnsignedShort() throws IOException {
if (!blkmode) {
pos = 0;
in.readFully(buf, 0, 2);
} else if (end - pos < 2) {
return din.readUnsignedShort();
}
int v = Bits.getShort(buf, pos) & 0xFFFF;
pos += 2;
return v;
}
public void readFully(byte[] b, int off, int len) throws IOException {
readFully(b, off, len, false);
}
public void readFully(byte[] b, int off, int len, boolean copy)
throws IOException
{
while (len > 0) {
int n = read(b, off, len, copy);
if (n < 0) {
throw new EOFException();
}
off += n;
len -= n;
}
}
public int read(byte[] buf, int off, int len) throws IOException {
if (buf == null) {
throw new NullPointerException();
}
int endoff = off + len;
if (off < 0 || len < 0 || endoff > buf.length || endoff < 0) {
throw new IndexOutOfBoundsException();
}
return bin.read(buf, off, len, false);
}
private String readUTFBody(long utflen) throws IOException {
StringBuilder sbuf;
if (utflen > 0 && utflen < Integer.MAX_VALUE) {
// a reasonable initial capacity based on the UTF length
int initialCapacity = Math.min((int)utflen, 0xFFFF);
sbuf = new StringBuilder(initialCapacity);
} else {
sbuf = new StringBuilder();
}
if (!blkmode) {
end = pos = 0;
}
while (utflen > 0) {
int avail = end - pos;
if (avail >= 3 || (long) avail == utflen) {
utflen -= readUTFSpan(sbuf, utflen);
} else {
if (blkmode) {
// near block boundary, read one byte at a time
utflen -= readUTFChar(sbuf, utflen);
} else {
// shift and refill buffer manually
if (avail > 0) {
System.arraycopy(buf, pos, buf, 0, avail);
}
pos = 0;
end = (int) Math.min(MAX_BLOCK_SIZE, utflen);
in.readFully(buf, avail, end - avail);
}
}
}
return sbuf.toString();
}

這幾個方法基本上都是從序列化 Stream 讀資料的細部操作

所以 name = in.readUTF() 就是 Stream 中讀出這個 class descriptor 表示的 class 名字

下一行 suid = Long.valueOf(in.readLong()) 就是讀出大家熟知的 serialVersionUID

大家都知道 serialVersionUID 是用在反序列化流程中,驗證版本是否一致的重要欄位

只要 serialVersionUID 不同,反序列化過程就會拋出異常


這裡就花點篇幅稍微小補充一下,serialVersionUID 的生成方式:

1
2
3
4
5
6
7
/**
* Writes non-proxy class descriptor information to given output stream.
*/
void writeNonProxy(ObjectOutputStream out) throws IOException {
out.writeUTF(name);
out.writeLong(getSerialVersionUID());
...

這個方法在序列化過程中會被呼叫,其中 getSerialVersionUID() 會嘗試取得 suid 的值:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
/**
* Return the serialVersionUID for this class. The serialVersionUID
* defines a set of classes all with the same name that have evolved from a
* common root class and agree to be serialized and deserialized using a
* common format. NonSerializable classes have a serialVersionUID of 0L.
*
* @return the SUID of the class described by this descriptor
*/
public long getSerialVersionUID() {
// REMIND: synchronize instead of relying on volatile?
if (suid == null) {
suid = AccessController.doPrivileged(
new PrivilegedAction<Long>() {
public Long run() {
return computeDefaultSUID(cl);
}
}
);
}
return suid.longValue();
}

suid 值是 null,就進入 computeDefaultSUID(cl) 計算

計算 suid 時,會透過創立的 DataOutputStream,將一些資訊寫入其包裝的 ByteArrayOutputStream 中:

1
2
ByteArrayOutputStream bout = new ByteArrayOutputStream();
DataOutputStream dout = new DataOutputStream(bout);

寫入類別名字:

1
dout.writeUTF(cl.getName());

寫入 modifier:

1
2
3
4
5
6
7
8
9
10
int classMods = cl.getModifiers() &
(Modifier.PUBLIC | Modifier.FINAL |
Modifier.INTERFACE | Modifier.ABSTRACT);
Method[] methods = cl.getDeclaredMethods();
if ((classMods & Modifier.INTERFACE) != 0) {
classMods = (methods.length > 0) ?
(classMods | Modifier.ABSTRACT) :
(classMods & ~Modifier.ABSTRACT);
}
dout.writeInt(classMods);

照 interface name 排序之後寫入:

1
2
3
4
5
6
7
8
9
10
11
if (!cl.isArray()) {
Class<?>[] interfaces = cl.getInterfaces();
String[] ifaceNames = new String[interfaces.length];
for (int i = 0; i < interfaces.length; i++) {
ifaceNames[i] = interfaces[i].getName();
}
Arrays.sort(ifaceNames);
for (int i = 0; i < ifaceNames.length; i++) {
dout.writeUTF(ifaceNames[i]);
}
}

根據 field name 排序,然後把 name, modifier, signature 寫入:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
Field[] fields = cl.getDeclaredFields();
MemberSignature[] fieldSigs = new MemberSignature[fields.length];
for (int i = 0; i < fields.length; i++) {
fieldSigs[i] = new MemberSignature(fields[i]);
}
Arrays.sort(fieldSigs, new Comparator<MemberSignature>() {
public int compare(MemberSignature ms1, MemberSignature ms2) {
return ms1.name.compareTo(ms2.name);
}
});
for (int i = 0; i < fieldSigs.length; i++) {
MemberSignature sig = fieldSigs[i];
int mods = sig.member.getModifiers() &
(Modifier.PUBLIC | Modifier.PRIVATE | Modifier.PROTECTED |
Modifier.STATIC | Modifier.FINAL | Modifier.VOLATILE |
Modifier.TRANSIENT);
if (((mods & Modifier.PRIVATE) == 0) ||
((mods & (Modifier.STATIC | Modifier.TRANSIENT)) == 0))
{
dout.writeUTF(sig.name);
dout.writeInt(mods);
dout.writeUTF(sig.signature);
}
}

這邊可以注意到,如果 modifier 是 PRIVATE 或是 STATICTRANSIENT 就不寫入

所以在 java 序列化時,只要變數前加上 transient 關鍵字,就不會對這個變數做序列化

繼續往下看

當存在 Static Initializer 時,會將這段寫入:

1
2
3
4
5
if (hasStaticInitializer(cl)) {
dout.writeUTF("<clinit>");
dout.writeInt(Modifier.STATIC);
dout.writeUTF("()V");
}

(註: Static Initializer 的功能在於初始化類別,當類被載入至 JVM 時,會執行寫在 Static Block 裡的程式碼)

根據 signature 排序,然後將非 private 的 constuctor 寫入:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
Constructor<?>[] cons = cl.getDeclaredConstructors();
MemberSignature[] consSigs = new MemberSignature[cons.length];
for (int i = 0; i < cons.length; i++) {
consSigs[i] = new MemberSignature(cons[i]);
}
Arrays.sort(consSigs, new Comparator<MemberSignature>() {
public int compare(MemberSignature ms1, MemberSignature ms2) {
return ms1.signature.compareTo(ms2.signature);
}
});
for (int i = 0; i < consSigs.length; i++) {
MemberSignature sig = consSigs[i];
int mods = sig.member.getModifiers() &
(Modifier.PUBLIC | Modifier.PRIVATE | Modifier.PROTECTED |
Modifier.STATIC | Modifier.FINAL |
Modifier.SYNCHRONIZED | Modifier.NATIVE |
Modifier.ABSTRACT | Modifier.STRICT);
if ((mods & Modifier.PRIVATE) == 0) {
dout.writeUTF("<init>");
dout.writeInt(mods);
dout.writeUTF(sig.signature.replace('/', '.'));
}
}

照 method name 和 signature 排序,然後寫入非 private method 的 name, modifier, signature:

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
MemberSignature[] methSigs = new MemberSignature[methods.length];
for (int i = 0; i < methods.length; i++) {
methSigs[i] = new MemberSignature(methods[i]);
}
Arrays.sort(methSigs, new Comparator<MemberSignature>() {
public int compare(MemberSignature ms1, MemberSignature ms2) {
int comp = ms1.name.compareTo(ms2.name);
if (comp == 0) {
comp = ms1.signature.compareTo(ms2.signature);
}
return comp;
}
});
for (int i = 0; i < methSigs.length; i++) {
MemberSignature sig = methSigs[i];
int mods = sig.member.getModifiers() &
(Modifier.PUBLIC | Modifier.PRIVATE | Modifier.PROTECTED |
Modifier.STATIC | Modifier.FINAL |
Modifier.SYNCHRONIZED | Modifier.NATIVE |
Modifier.ABSTRACT | Modifier.STRICT);
if ((mods & Modifier.PRIVATE) == 0) {
dout.writeUTF(sig.name);
dout.writeInt(mods);
dout.writeUTF(sig.signature.replace('/', '.'));
}
}
dout.flush();

最後把 bout 拿去做 SHA1,取前 8 個 Bytes 當作 suid 回傳

1
2
3
4
5
6
7
MessageDigest md = MessageDigest.getInstance("SHA");
byte[] hashBytes = md.digest(bout.toByteArray());
long hash = 0;
for (int i = Math.min(hashBytes.length, 8) - 1; i >= 0; i--) {
hash = (hash << 8) | (hashBytes[i] & 0xFF);
}
return hash;

所以我們現在知道,並不是所有類別更改都會影響到 suid


好了,扯遠了,繼續回來看 readNonProxy()

所以 readNonProxy() 初始化完類別名字、suid 之後,readClassDescriptor()就會把這個初始化的 class descriptor 回傳回去

接著回到 readNonProxyDesc():

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
private ObjectStreamClass readNonProxyDesc(boolean unshared)
throws IOException
{
...
Class<?> cl = null;
ClassNotFoundException resolveEx = null;
bin.setBlockDataMode(true);
final boolean checksRequired = isCustomSubclass();
try {
if ((cl = resolveClass(readDesc)) == null) {
resolveEx = new ClassNotFoundException("null class");
} else if (checksRequired) {
ReflectUtil.checkPackageAccess(cl);
}
} catch (ClassNotFoundException ex) {
resolveEx = ex;
}
// Call filterCheck on the class before reading anything else
filterCheck(cl, -1);
...

剛剛初始化完的 class descriptor readDesc 被丟進 resovleClass()

resolveClass() 做的事情很單純,透過反射,取得並回傳當前 descriptor 描述的類別物件,也就是對應到我們這個例子的 Kaibro

反射機制:
Java 是個靜態語言,不像 PHP 有那麼多靈活的動態特性,但透過反射機制,可以大幅提升 Java 的動態性
核心概念是,它運行時才動態載入或調用、訪問方法和屬性,不需事先定義目標是誰
例如,你的程式沒有 import 某個類別,可以透過反射來動態載入: Class<?> cls = Class.forName("java.lang.Runtime");

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
protected Class<?> resolveClass(ObjectStreamClass desc)
throws IOException, ClassNotFoundException
{
String name = desc.getName();
try {
return Class.forName(name, false, latestUserDefinedLoader());
} catch (ClassNotFoundException ex) {
Class<?> cl = primClasses.get(name);
if (cl != null) {
return cl;
} else {
throw ex;
}
}
}

接著呼叫 filterCheck(cl, -1),這裡的 cl 就是我們剛才 reovleClass 的類別物件

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
/**
* Invoke the serialization filter if non-null.
* If the filter rejects or an exception is thrown, throws InvalidClassException.
*
* @param clazz the class; may be null
* @param arrayLength the array length requested; use {@code -1} if not creating an array
* @throws InvalidClassException if it rejected by the filter or
* a {@link RuntimeException} is thrown
*/
private void filterCheck(Class<?> clazz, int arrayLength)
throws InvalidClassException {
if (serialFilter != null) {
RuntimeException ex = null;
ObjectInputFilter.Status status;
// Info about the stream is not available if overridden by subclass, return 0
long bytesRead = (bin == null) ? 0 : bin.getBytesRead();
try {
status = serialFilter.checkInput(new FilterValues(clazz, arrayLength,
totalObjectRefs, depth, bytesRead));
} catch (RuntimeException e) {
// Preventive interception of an exception to log
status = ObjectInputFilter.Status.REJECTED;
ex = e;
}
if (status == null ||
status == ObjectInputFilter.Status.REJECTED) {
// Debug logging of filter checks that fail
if (Logging.infoLogger != null) {
Logging.infoLogger.info(
"ObjectInputFilter {0}: {1}, array length: {2}, nRefs: {3}, depth: {4}, bytes: {5}, ex: {6}",
status, clazz, arrayLength, totalObjectRefs, depth, bytesRead,
Objects.toString(ex, "n/a"));
}
InvalidClassException ice = new InvalidClassException("filter status: " + status);
ice.initCause(ex);
throw ice;
} else {
// Trace logging for those that succeed
if (Logging.traceLogger != null) {
Logging.traceLogger.finer(
"ObjectInputFilter {0}: {1}, array length: {2}, nRefs: {3}, depth: {4}, bytes: {5}, ex: {6}",
status, clazz, arrayLength, totalObjectRefs, depth, bytesRead,
Objects.toString(ex, "n/a"));
}
}
}
}

這裡可以看到 serialFilter 是在 ObjectInputStream 初始化時取得的

serialFilter 存在時,filtercheck 會去做檢查、過濾,如果沒通過就直接拋出 Exception

serialFilter = ObjectInputFilter.Config.getSerialFilter();

這個其實就是大名鼎鼎的 JEP290 防禦機制


繼續回來看 readNonProxyDesc() 後半部分:

1
2
3
4
5
6
7
8
9
10
11
private ObjectStreamClass readNonProxyDesc(boolean unshared)
throws IOException
{
...
desc.initNonProxy(readDesc, cl, resolveEx, readClassDesc(false));
handles.finish(descHandle);
passHandle = descHandle;
return desc;
}

這裡我們跟進去看 initNonProxy():

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
/**
* Initializes class descriptor representing a non-proxy class.
*/
void initNonProxy(ObjectStreamClass model,
Class<?> cl,
ClassNotFoundException resolveEx,
ObjectStreamClass superDesc)
throws InvalidClassException
{
this.cl = cl;
this.resolveEx = resolveEx;
this.superDesc = superDesc;
name = model.name;
suid = Long.valueOf(model.getSerialVersionUID());
isProxy = false;
isEnum = model.isEnum;
serializable = model.serializable;
externalizable = model.externalizable;
hasBlockExternalData = model.hasBlockExternalData;
hasWriteObjectData = model.hasWriteObjectData;
fields = model.fields;
primDataSize = model.primDataSize;
numObjFields = model.numObjFields;
if (cl != null) {
localDesc = lookup(cl, true);
...
cons = localDesc.cons;
writeObjectMethod = localDesc.writeObjectMethod;
readObjectMethod = localDesc.readObjectMethod;
readObjectNoDataMethod = localDesc.readObjectNoDataMethod;
writeReplaceMethod = localDesc.writeReplaceMethod;
readResolveMethod = localDesc.readResolveMethod;
if (deserializeEx == null) {
deserializeEx = localDesc.deserializeEx;
}
}
fieldRefl = getReflector(fields, localDesc);
// reassign to matched fields so as to reflect local unshared settings
fields = fieldRefl.getFields();
}

這個方法做了很多初始化操作

包括前面講的 suid 檢查、計算等,在這個方法中都有處理到

這裡要稍微注意,參數 model 是我們剛剛從序列化 Stream 中,讀出來的 readDesc,而目前 initNonProxy 這個方法是由我們前面剛建立的 desc 呼叫的

這個方法會使用 readDesc (反序列化還原出來的) 屬性來初始化 desc,所以必須先檢查 readDesc 正確性

為了檢查 readDesc 正確性,它會判斷跟本地直接 new 出來的物件 localDesc 的 suid, class name 等內容是否相同,若不同則拋出 Exception

其中 localDesc = lookup(cl, true) 是根據 class,返回對應的 class descriptor:

1
2
3
4
5
6
7
8
9
10
11
12
13
static ObjectStreamClass lookup(Class<?> cl, boolean all) {
...
if (entry == null) {
try {
entry = new ObjectStreamClass(cl);
} catch (Throwable th) {
entry = th;
}
...
}
if (entry instanceof ObjectStreamClass) {
return (ObjectStreamClass) entry;
...

可以看到它建立了一個新的 ObjectStreamClass 物件

來看一下 ObjectStreamClass 的 constructor:

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
/**
* Creates local class descriptor representing given class.
*/
private ObjectStreamClass(final Class<?> cl) {
this.cl = cl;
name = cl.getName();
isProxy = Proxy.isProxyClass(cl);
isEnum = Enum.class.isAssignableFrom(cl);
serializable = Serializable.class.isAssignableFrom(cl);
externalizable = Externalizable.class.isAssignableFrom(cl);
Class<?> superCl = cl.getSuperclass();
superDesc = (superCl != null) ? lookup(superCl, false) : null;
localDesc = this;
if (serializable) {
AccessController.doPrivileged(new PrivilegedAction<Void>() {
public Void run() {
if (isEnum) {
suid = Long.valueOf(0);
fields = NO_FIELDS;
return null;
}
if (cl.isArray()) {
fields = NO_FIELDS;
return null;
}
suid = getDeclaredSUID(cl);
try {
fields = getSerialFields(cl);
computeFieldOffsets();
} catch (InvalidClassException e) {
serializeEx = deserializeEx =
new ExceptionInfo(e.classname, e.getMessage());
fields = NO_FIELDS;
}
if (externalizable) {
cons = getExternalizableConstructor(cl);
} else {
cons = getSerializableConstructor(cl);
writeObjectMethod = getPrivateMethod(cl, "writeObject",
new Class<?>[] { ObjectOutputStream.class },
Void.TYPE);
readObjectMethod = getPrivateMethod(cl, "readObject",
new Class<?>[] { ObjectInputStream.class },
Void.TYPE);
readObjectNoDataMethod = getPrivateMethod(
cl, "readObjectNoData", null, Void.TYPE);
hasWriteObjectData = (writeObjectMethod != null);
}
writeReplaceMethod = getInheritableMethod(
cl, "writeReplace", null, Object.class);
readResolveMethod = getInheritableMethod(
cl, "readResolve", null, Object.class);
return null;
}
});
...

這裡的 conscl 對應的 constructor

而後面的 writeObjectMethod, readObjectMethod, readObjectNoDataMethod 都是透過 getPrivateMethod() 反射取得的方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
/**
* Returns non-static private method with given signature defined by given
* class, or null if none found. Access checks are disabled on the
* returned method (if any).
*/
private static Method getPrivateMethod(Class<?> cl, String name,
Class<?>[] argTypes,
Class<?> returnType)
{
try {
Method meth = cl.getDeclaredMethod(name, argTypes);
meth.setAccessible(true);
int mods = meth.getModifiers();
return ((meth.getReturnType() == returnType) &&
((mods & Modifier.STATIC) == 0) &&
((mods & Modifier.PRIVATE) != 0)) ? meth : null;
} catch (NoSuchMethodException ex) {
return null;
}
}

然後回到剛剛的initNonProxy():

1
2
3
4
5
6
7
8
localDesc = lookup(cl, true);
...
cons = localDesc.cons;
writeObjectMethod = localDesc.writeObjectMethod;
readObjectMethod = localDesc.readObjectMethod;
readObjectNoDataMethod = localDesc.readObjectNoDataMethod;
writeReplaceMethod = localDesc.writeReplaceMethod;
...

我們前面建立的 ObjectStreamClass 物件,就是這裡的 localDesc

它把 localDesc 中的 Constructor, writeObjectMethod, readObjectNoDataMethod, writeReplaceMethod 都賦值到當前物件屬性上

也就是再更前面的 readNonProxyDesc() 中的 desc 物件

所以目前 desc 物件已經初始化完成,裡頭有我們剛剛反射出來的 Constuctor, readObjectNoDataMethod 等屬性

接著就把這個物件返回給 readClassDesc()descriptor

之後過一個 validator 檢查:

1
2
3
if (descriptor != null) {
validateDescriptor(descriptor);
}

檢查通過之後,就 return 回最開頭的 readOrdinaryObject():

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
private Object readOrdinaryObject(boolean unshared)
throws IOException
{
...
ObjectStreamClass desc = readClassDesc(false); // 返回的 descriptor
...
Object obj;
try {
obj = desc.isInstantiable() ? desc.newInstance() : null;
} catch (Exception ex) {
throw (IOException) new InvalidClassException(
desc.forClass().getName(),
"unable to create instance").initCause(ex);
}
...
if (desc.isExternalizable()) {
readExternalData((Externalizable) obj, desc);
} else {
readSerialData(obj, desc);
}

可以看到這裡呼叫 desc.newInstance() 做實例化,其實背後就是透過我們剛才得到的 Constructor 去生成物件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
Object newInstance()
throws InstantiationException, InvocationTargetException,
UnsupportedOperationException
{
if (cons != null) {
try {
return cons.newInstance();
} catch (IllegalAccessException ex) {
// should not occur, as access checks have been suppressed
throw new InternalError(ex);
}
} else {
throw new UnsupportedOperationException();
}
}

接著,當 desc 不是 Externalizable 時會呼叫 readSerialData(obj, desc)

繼續跟下去:

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
/**
* Reads (or attempts to skip, if obj is null or is tagged with a
* ClassNotFoundException) instance data for each serializable class of
* object in stream, from superclass to subclass. Expects that passHandle
* is set to obj's handle before this method is called.
*/
private void readSerialData(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 (slots[i].hasData) {
if (obj == null || handles.lookupException(passHandle) != null) {
defaultReadFields(null, slotDesc); // skip field values
} else if (slotDesc.hasReadObjectMethod()) {
ThreadDeath t = null;
boolean reset = false;
SerialCallbackContext oldContext = curContext;
if (oldContext != null)
oldContext.check();
try {
curContext = new SerialCallbackContext(obj, slotDesc);
bin.setBlockDataMode(true);
slotDesc.invokeReadObject(obj, this);
} catch (ClassNotFoundException ex) {
/*
* In most cases, the handle table has already
* propagated a CNFException to passHandle at this
* point; this mark call is included to address cases
* where the custom readObject method has cons'ed and
* thrown a new CNFException of its own.
*/
handles.markException(passHandle, ex);
} finally {
curContext.setUsed();
curContext = oldContext;
}
/*
* defaultDataEnd may have been set indirectly by custom
* readObject() method when calling defaultReadObject() or
* readFields(); clear it to restore normal read behavior.
*/
defaultDataEnd = false;
} else {
defaultReadFields(obj, slotDesc);
}
...

如果我們有自己重寫 readObject,則呼叫 slotDesc.invokeReadObject(obj, this)
若沒有,則呼叫 defaultReadFields 填充數據

invokeReadObject() 實際上就是去呼叫我們重寫的 readObject:

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
/**
* Invokes the readObject method of the represented serializable class.
* Throws UnsupportedOperationException if this class descriptor is not
* associated with a class, or if the class is externalizable,
* non-serializable or does not define readObject.
*/
void invokeReadObject(Object obj, ObjectInputStream in)
throws ClassNotFoundException, IOException,
UnsupportedOperationException
{
if (readObjectMethod != null) {
try {
readObjectMethod.invoke(obj, new Object[]{ in });
} catch (InvocationTargetException ex) {
Throwable th = ex.getTargetException();
if (th instanceof ClassNotFoundException) {
throw (ClassNotFoundException) th;
} else if (th instanceof IOException) {
throw (IOException) th;
} else {
throwMiscException(th);
}
} catch (IllegalAccessException ex) {
// should not occur, as access checks have been suppressed
throw new InternalError(ex);
}
} else {
throw new UnsupportedOperationException();
}
}

接著可以看到 readObjectMethod.invoke(obj, new Object[]{ in })

這裡的 readObjectMethod 就是我們前面透過反射設定的 readObject 方法,也就是 Kaibro.readObject

所以到目前為止,終於追到我們一開始的目標了!

ObjectInputStream.readObject() 一路追到這裡我們自己重寫的 Kaibro.readObject()

打完收工!


最後再小補充一下,一般我們在重寫的 readObject() 中,會去呼叫 ObjectInputStream.defaultReadObject()

它的作用是會去讀出 non-static 和 non-transient 的 field 出來

例如 Kaibro 這個例子裡,我在 readObject() 中,第一行呼叫了 in.defaultReadObject()

追一下這個方法:

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
/**
* Read the non-static and non-transient fields of the current class from
* this stream. This may only be called from the readObject method of the
* class being deserialized. It will throw the NotActiveException if it is
* called otherwise.
*
* @throws ClassNotFoundException if the class of a serialized object
* could not be found.
* @throws IOException if an I/O error occurs.
* @throws NotActiveException if the stream is not currently reading
* objects.
*/
public void defaultReadObject()
throws IOException, ClassNotFoundException
{
SerialCallbackContext ctx = curContext;
if (ctx == null) {
throw new NotActiveException("not in call to readObject");
}
Object curObj = ctx.getObj();
ObjectStreamClass curDesc = ctx.getDesc();
bin.setBlockDataMode(false);
defaultReadFields(curObj, curDesc);
bin.setBlockDataMode(true);
if (!curDesc.hasWriteObjectData()) {
/*
* Fix for 4360508: since stream does not contain terminating
* TC_ENDBLOCKDATA tag, set flag so that reading code elsewhere
* knows to simulate end-of-custom-data behavior.
*/
defaultDataEnd = true;
}
ClassNotFoundException ex = handles.lookupException(passHandle);
if (ex != null) {
throw ex;
}
}

可以看到實際上這個方法,背後其實也會呼叫 defaultReadFields(curObj, curDesc) 去填充物件的 field

所以如果我們把 defaultReadObject() 拔掉,那我們物件的 field 就沒辦法正常還原

一樣以我們的 Kaibro class 為例,如果把 in.defaultReadObject() 拿掉

最後反序列化時,System.out.println(tmp.gg) 的結果就會是 null


總結

這篇文章中,我們是用實作 SerializableKaibro class 當作例子去追

並未深入去追使用 Externalizable 的例子

但其實流程都大同小異,有興趣的讀者可以自己追一下

Externalizable:
該接口 extends Serializable 接口,並新增兩種方法: writeExternal 和 readExternal
這兩個方法會在序列化和反序列化過程中被調用


由於這篇是為了追自定義 readObject 的呼叫時機

所以未對 Java 序列化格式與讀取方式做細部分析

對這方面有興趣的讀者可以去看 Java Serialization Protocol 的 spec:
https://docs.oracle.com/javase/8/docs/platform/serialization/spec/protocol.html


最後,簡化一下整篇的執行流程:

  • ObjectInputSteram.readObject()
    • readObject0()
      • readOrdinaryObject()
        • desc = readClassDesc(false)
          • descriptor = readNonProxyDesc(unshared)
            • readDesc = readClassDescriptor()
            • cl = resolveClass(readDesc)
            • filterCheck(cl, -1)
            • desc.initNonProxy(readDesc, cl, resolveEx, readClassDesc(false))
              • 各種初始化、檢查 suid 等
            • return desc
          • return descriptor
        • obj = desc.isInstantiable() ? desc.newInstance() : null
        • readSerialData(obj, desc)
          • slotDesc.invokeReadObject(obj, this)
            • readObjectMethod.invoke(obj, new Object[]{ in })


因為這篇是用空閒時間隨意寫的,如果有哪邊寫錯或寫不清楚,歡迎留言指教!