報告,書里有個BUG!

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

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

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

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

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

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

先公布答案,為了你方便運行,我直接把整個代碼放這里,你粘過去就能跑:
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();
}
}
上面這個答案就是來自書中的答案。
但是當(dāng)你粘出來運行的時候,有趣的事情發(fā)生了:

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

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

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

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

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

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

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

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

我也去看了 JDK 8 關(guān)于 findSpecial 方法的規(guī)范說明 :
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.
簡單翻譯一下就是這樣的。
MethodHandles.lookup這個函數(shù)對調(diào)用者是敏感的,這樣就可以有一個安全的查找基礎(chǔ)。JSR 292 API 中的幾乎所有其他方法都依賴查找對象來檢查訪問請求。
調(diào)用者敏感,我是這樣理解的:不同調(diào)用者,訪問權(quán)限不同,其結(jié)果也不同。
比如在書中的例中,在 Son 類中調(diào)用 MethodHandles.lookup,Son 是調(diào)用者,因為調(diào)用者是敏感,所以只能訪問到 Father 類的 thinking。
另外,文檔中提到的 JSR 292 也和 R 大的回答呼應(yīng)上了。
我對比了一下 JDK 7 和 8 之間描述的差異:

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

能突破嗎?
知道問題被修復(fù)了,那么問題又來了。

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

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

allowedModes,這個值如果我們可以設(shè)置為 “TRUSTED”,那么就能直接返回,從而避開下面的這些校驗。
怎么繞開呢?
直接上代碼:
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();
}
}
}
來看運行結(jié)果:

這個方案也是周大大書上寫的方案:

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

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