[英]ASM skips Classes if COMPUTE_FRAMES is set in ClassWriter
[英]ClassWriter COMPUTE_FRAMES in ASM
我一直試圖通過玩轉 ASM 中的跳轉來了解堆棧映射框架在 Java 中的工作原理。 我創建了一個簡單的方法來嘗試一些東西:(用 Krakatau 拆卸):
L0: ldc 'hello'
L2: astore_1
L3: getstatic Field java/lang/System out Ljava/io/PrintStream;
L6: new java/lang/StringBuilder
L9: dup
L10: invokespecial Method java/lang/StringBuilder <init> ()V
L13: ldc 'concat1'
L15: invokevirtual Method java/lang/StringBuilder append (Ljava/lang/String;)Ljava/lang/StringBuilder;
L18: aload_1
L19: invokevirtual Method java/lang/StringBuilder append (Ljava/lang/String;)Ljava/lang/StringBuilder;
L22: invokevirtual Method java/lang/StringBuilder toString ()Ljava/lang/String;
L25: invokevirtual Method java/io/PrintStream println (Ljava/lang/String;)V
L28: getstatic Field java/lang/System out Ljava/io/PrintStream;
L31: new java/lang/StringBuilder
L34: dup
L35: invokespecial Method java/lang/StringBuilder <init> ()V
L38: ldc 'concat2'
L40: invokevirtual Method java/lang/StringBuilder append (Ljava/lang/String;)Ljava/lang/StringBuilder;
L43: aload_1
L44: invokevirtual Method java/lang/StringBuilder append (Ljava/lang/String;)Ljava/lang/StringBuilder;
L47: invokevirtual Method java/lang/StringBuilder toString ()Ljava/lang/String;
L50: invokevirtual Method java/io/PrintStream println (Ljava/lang/String;)V
L53: return
它所做的只是創建一個StringBuilder
來連接一些帶有變量的字符串。
由於 L35 處的 invokespecial 調用與 L10 處的 invokespecial 調用具有完全相同的堆棧,因此我決定添加一個ICONST_1; IFEQ L10
ICONST_1; IFEQ L10
序列就在帶有 ASM 的 L35 之前。
當我拆解時(再次用 Krakatau),我發現結果很奇怪。 ASM 已計算出 L10 處的堆棧幀為:
.stack full
locals Object [Ljava/lang/String; Object java/lang/String
stack Object java/io/PrintStream Top Top
.end stack
代替
stack Object java/io/PrintStream Object java/lang/StringBuilder Object java/lang/StringBuilder
正如我所料。
此外,這個類也不會通過驗證,因為不能在Top
上調用StringBuilder#<init>
。 根據ASM手冊, Top
指的是一個未初始化的值,但在代碼中似乎沒有未初始化,無論是跳轉位置還是之前的代碼。 我不明白跳躍有什么問題。
我插入的跳轉是否有問題,以某種方式使類無法計算幀? 這可能是 ASM 的 ClassWriter 的錯誤嗎?
未初始化的實例是特殊的。 考慮到,當您dup
引用時,您已經在堆棧上有兩個對同一實例的引用,您可能會執行更多堆棧操作或將引用轉移到局部變量,然后從那里將其復制到其他變量或再次推送. 盡管如此,在您以任何方式使用它之前,引用的目標應該被初始化一次。 為了驗證這一點,必須跟蹤對象的身份,以便在對同一對象執行invokespecial <init>
時,所有這些對同一對象的引用將從未初始化變為已初始化。
Java 編程語言並沒有使用所有的可能性,但是對於像這樣的合法代碼
new Foo(new Foo(new Foo(), new Foo(b? new Foo(a): new Foo(b, c)))
它不應該了,履帶哪些Foo
實例已初始化,並且沒有,當分支成立。
因此,每個未初始化的實例堆棧幀條目都與創建它的new
指令相關聯。 所有條目在傳輸或復制時都保留引用(可以像記住new
指令的字節碼偏移一樣輕松處理)。 只有在invokespecial <init>
調用了invokespecial <init>
之后,指向同一條new
指令的所有引用才轉向聲明類的普通實例,並且隨后可以與其他類型兼容的條目合並。
這意味着像您試圖實現的分支是不可能的。 相同類型但由不同new
指令創建的兩個未初始化實例條目是不兼容的。 並且不兼容的類型被合並到一個Top
條目,這基本上是一個不可用的條目。 它甚至可能是正確的代碼,如果您不嘗試在分支目標上使用該條目,那么 ASM 在將它們合並到Top
時不會做任何錯誤而不會抱怨。
請注意,這也意味着任何可能導致堆棧幀具有多個由同一條new
指令創建的未初始化實例的循環都是不允許的。
new java/lang/StringBuilder
不會創建有效的StringBuilder
而是一個統一的對象,該對象在堆棧映射幀中與TOP
一起捐贈。 這個值在對象構造過程中加入跳轉指令時使用,例如:
new Foo(a ? b : c);
這被翻譯成幾個 goto 語句。
在對象上調用構造函數時,該對象首先被視為StringBuilder
,即invokespecial Method java/lang/StringBuilder <init> ()V
。 JVM 不支持在不同位置初始化此對象,因為驗證器只能查看TOP
類型,該類型不能反映所需類型的實際陰影,即未初始化的StringBuilder
。 您可能會爭辯說 JVM 應該支持這一點,但這將需要更大的數組來包含堆棧映射幀以反映類型和初始化狀態,這可能無法證明這種能力甚至不被 Java 語言使用。
為了說明這一點,請考慮以下情況:
new Foo
dup
.stack full
locals
stack Top Top
.end stack
invokespecial Bar <init> ()V
如果 JVM 允許對TOP
類型進行未經檢查的初始化,這將是有效的,但顯然不應允許您在Foo
上調用Bar
構造函數。
聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.