報(bào)告,書(shū)里有個(gè)BUG!

你好呀,我是 Kirito。今天分享一篇我的好友 Why 近期寫(xiě)的一篇原創(chuàng)文章。
最近在看《深入理解 JVM 虛擬機(jī)》(第三版)的時(shí)候發(fā)現(xiàn)一個(gè)有意思的 BUG。
給大家匯報(bào)一下。

這段話位于第三版的 326 頁(yè),屬于書(shū)中的第八章虛擬機(jī)字節(jié)碼執(zhí)行引擎這一部分的內(nèi)容。
整個(gè)第八章主要分析了虛擬機(jī)在執(zhí)行代碼時(shí),如何找到正確的方法、如何執(zhí)行方法內(nèi)的字節(jié)碼,以及執(zhí)行代碼時(shí)涉及的內(nèi)存結(jié)構(gòu)。
而其中的 8.4 小節(jié)是這樣的:

其實(shí)還有個(gè) 8.4.5 小節(jié),由于排版問(wèn)題,我不好拍下來(lái)。
而出 Bug 的地方,就是對(duì)應(yīng)書(shū)中的 8.4.5 小節(jié),標(biāo)題是:
實(shí)戰(zhàn):掌控方法分派規(guī)則
接下來(lái),我們就看看到底是哪里出 Bug 了。
另外,需要提前說(shuō)明的是,我沒(méi)有做背景知識(shí)的鋪墊,默認(rèn)你是了解關(guān)于 Java 虛擬機(jī)層面對(duì)于動(dòng)態(tài)類型語(yǔ)言的支持的。
其實(shí)說(shuō)白了就是那幾個(gè)指令:
invokestatic invokespecial invokevirtual invokeinterface invokedynameic

同時(shí)也了解 MethodHandle 類中下面幾個(gè)方法和上述幾個(gè)指令的關(guān)系的:
findStatic findSpecial findVirtual
不知道也沒(méi)關(guān)系,就看一樂(lè)呵。面試不考,放心。

啥 BUG
先直接給大家上個(gè)代碼,也是書(shū)上的示例代碼,你思考一下,能不能實(shí)現(xiàn)這個(gè)需求:

絕大部分人的第一反應(yīng)就是 super 關(guān)鍵字。
但是可惜的是 super 調(diào)用的是父類的 thinking 方法,而當(dāng)前類 son 的父類是 Father 類。
再接著想,可能有的同學(xué)能想到操作字節(jié)碼,比如用 ASM、Javassist 等字節(jié)碼操作工具,去搞一些騷操作。
這個(gè)思路是可以的,但是屬于作弊行為。
題目是要求在字節(jié)碼之上的 Java 層面解決。
有的同學(xué)還能想到反射。
誒,想到反射的同學(xué)很不錯(cuò),可以給自己鼓個(gè)掌。

先公布答案,為了你方便運(yùn)行,我直接把整個(gè)代碼放這里,你粘過(guò)去就能跑:
public class MethodHandleTest {
class GrandFather{
void thinking(){
System.out.println("i am grandfather");
}
}
class Father extends GrandFather{
void thinking(){
System.out.println("i am father");
}
}
class Son extends Father {
void thinking() {
try {
MethodType mt = MethodType.methodType(void.class);
MethodHandle mh = lookup().findSpecial(GrandFather.class,
"thinking", mt, getClass());
mh.invoke(this);
} catch (Throwable e) {
}
}
}
public static void main(String[] args) {
(new MethodHandleTest().new Son()).thinking();
}
}
上面這個(gè)答案就是來(lái)自書(shū)中的答案。
但是當(dāng)你粘出來(lái)運(yùn)行的時(shí)候,有趣的事情發(fā)生了:

什么情況,為什么書(shū)上的運(yùn)行結(jié)果是這樣的?

誒,這就是 BUG 的體現(xiàn)了。

為啥是這樣的?
同樣的程序,在第三版里面是這樣描述的:

很明顯了,在 JDK 7 Update 9 之前的運(yùn)行結(jié)果是這樣的,說(shuō)明后續(xù)更的時(shí)候修復(fù)了什么問(wèn)題。
如果你的運(yùn)行結(jié)果還是 i am grandfather,那么兄弟,你的 JDK 版本該升級(jí)一下了。
那么到底修復(fù)了什么問(wèn)題呢?
我在知乎上找到了關(guān)于這個(gè)問(wèn)題的R大的回答:
https://www.zhihu.com/question/40427344

首先這個(gè)神一樣的男人,直接就說(shuō)書(shū)上的結(jié)論是錯(cuò)誤的。
他說(shuō):因?yàn)?MethodHandle 用于模擬 invokespecial 時(shí),必須遵守跟 Java 字節(jié)碼里的 invokespecial 指令相同的限制,只能調(diào)用到傳給 findSpecial() 方法的最后一個(gè)參數(shù)(“specialCaller”)的直接父類的版本。
啥意思,直接就是看著頭大。
不慌,根據(jù)我們深厚的語(yǔ)文功底,大家都知道,重點(diǎn)在后半句:
只能調(diào)用到傳給 findSpecial() 方法的最后一個(gè)參數(shù)(“specialCaller”)的直接父類的版本。
那么最后一個(gè)參數(shù)是什么?
它的直接父類又是什么?
來(lái),我給你 Debug 一下:

通過(guò)截圖我們知道最后一個(gè)參數(shù)其實(shí)就是當(dāng)前類,即 son。
它的直接父類又是什么?
在周大大書(shū)里的例子里,類之間的基礎(chǔ)關(guān)系是這樣的:
Son->Father->GrandFather
所以 son 的直接父類,就是 father 類:

從這里可以清楚的看到,這里的 method 其實(shí)是 father 類的 thinking 方法。
同時(shí),R大還說(shuō)了:
findSpecial()還特別限制如果Lookup發(fā)現(xiàn)傳入的最后一個(gè)參數(shù)(“specialCaller”)跟當(dāng)前類不一致的話默認(rèn)會(huì)馬上拋異常
來(lái),試驗(yàn)一把嘛。

當(dāng)我們把最后一個(gè)參數(shù)傳 Father.class,再次運(yùn)行發(fā)現(xiàn)拋出了異常。
最后,R大也指出,曾經(jīng)有這樣的 bug 存在,所以也有可能是存在示例代碼中的結(jié)果的:
可能是因?yàn)閒indSpecial()得到的MethodHandle的具體語(yǔ)義在JSR 292的設(shè)計(jì)過(guò)程中有被調(diào)整過(guò)。有一段時(shí)間findSpecial()得到的MethodHandle確實(shí)可以超越invokespecial的限制去調(diào)用到任意版本的虛方法,但這種行為很快就被認(rèn)為是bug而修正了。
所以,周大大在第三版中也更新了這部分的內(nèi)容:

我也去看了 JDK 8 關(guān)于 findSpecial 方法的規(guī)范說(shuō)明 :
https://docs.oracle.com/javase/8/docs/api/java/lang/invoke/MethodHandles.Lookup.html#findSpecial-java.lang.Class-java.lang.String-java.lang.invoke.MethodType-java.lang.Class
其中有這樣的一句話:
The function MethodHandles.lookup is caller sensitive so that there can be a secure foundation for lookups. Nearly all other methods in the JSR 292 API rely on lookup objects to check access requests.
簡(jiǎn)單翻譯一下就是這樣的。
MethodHandles.lookup這個(gè)函數(shù)對(duì)調(diào)用者是敏感的,這樣就可以有一個(gè)安全的查找基礎(chǔ)。JSR 292 API 中的幾乎所有其他方法都依賴查找對(duì)象來(lái)檢查訪問(wèn)請(qǐng)求。
調(diào)用者敏感,我是這樣理解的:不同調(diào)用者,訪問(wèn)權(quán)限不同,其結(jié)果也不同。
比如在書(shū)中的例中,在 Son 類中調(diào)用 MethodHandles.lookup,Son 是調(diào)用者,因?yàn)檎{(diào)用者是敏感,所以只能訪問(wèn)到 Father 類的 thinking。
另外,文檔中提到的 JSR 292 也和 R 大的回答呼應(yīng)上了。
我對(duì)比了一下 JDK 7 和 8 之間描述的差異:

發(fā)現(xiàn) JDK 8 的描述多了整整一個(gè) Caller sensitive methods 小節(jié)。
翻譯過(guò)來(lái)就是“這是一個(gè)調(diào)用者敏感的方法”。
這一小節(jié)里面的這一句話,就是我剛剛說(shuō)的那句。

能突破嗎?
知道問(wèn)題被修復(fù)了,那么問(wèn)題又來(lái)了。

這個(gè)需求還能實(shí)現(xiàn)嗎?
現(xiàn)在這個(gè)需求按照前面的思路走不通的原因,是因?yàn)檫@個(gè)地方的校驗(yàn)繞不過(guò)去:
java.lang.invoke.MethodHandles.Lookup#checkSpecialCaller

那我們繞過(guò)這個(gè)限制就好了。
這個(gè)方法看起來(lái)也不復(fù)雜,而且有這樣的一個(gè)判斷,如果成立則直接返回,不做校驗(yàn):

allowedModes,這個(gè)值如果我們可以設(shè)置為 “TRUSTED”,那么就能直接返回,從而避開(kāi)下面的這些校驗(yàn)。
怎么繞開(kāi)呢?
直接上代碼:
class Son extends Father {
void thinking() {
try {
MethodType mt = MethodType.methodType(void.class);
Field lookupImpl = MethodHandles.Lookup.class.getDeclaredField("IMPL_LOOKUP");
lookupImpl.setAccessible(true);
MethodHandle mh = ((MethodHandles.Lookup) lookupImpl.get(null)).findSpecial(GrandFather.class, "thinking", mt, GrandFather.class);
mh.invoke(this);
} catch (Throwable e) {
e.printStackTrace();
}
}
}
來(lái)看運(yùn)行結(jié)果:

這個(gè)方案也是周大大書(shū)上寫(xiě)的方案:

結(jié)合著這個(gè)看,基本上就能看懂了:

不得不說(shuō),反射真的是太“流氓”了。
好了,本文就這些內(nèi)容了。
那你看完了,我問(wèn)你一個(gè)問(wèn)題:
你覺(jué)得你知道了這個(gè)點(diǎn),有什么卵用嗎?
是的,沒(méi)有。
那么恭喜你,又在我這里學(xué)到了一個(gè)沒(méi)有任何卵用的知識(shí)點(diǎn)。
如果一定要說(shuō)有用的地方,那么就是看書(shū)的時(shí)候別只看,得動(dòng)手。
比如本文的例子,如果不動(dòng)手,你自己大概率是不會(huì)踩到這個(gè)“彩蛋”的。
