java编译器分析_Java反编译器的剖析
java編譯器分析
簡(jiǎn)單地說(shuō),反編譯器嘗試將源代碼轉(zhuǎn)換為目標(biāo)代碼。 但是有很多有趣的復(fù)雜性-Java源代碼是結(jié)構(gòu)化的; 字節(jié)碼當(dāng)然不是。 而且,轉(zhuǎn)換不是一對(duì)一的:兩個(gè)不同的Java程序可能會(huì)產(chǎn)生相同的字節(jié)碼。 我們需要應(yīng)用試探法以合理地近似原始來(lái)源。
(微小的)字節(jié)碼刷新器
為了了解反編譯器的工作原理,有必要了解字節(jié)碼的基礎(chǔ)知識(shí)。 如果您已經(jīng)熟悉字節(jié)碼,請(qǐng)隨時(shí)跳到下一部分。
JVM是基于堆棧的計(jì)算機(jī) (與基于寄存器的計(jì)算機(jī)相對(duì)),這意味著指令在評(píng)估堆棧上運(yùn)行。 可以從堆棧彈出操作數(shù),執(zhí)行各種操作,然后將結(jié)果推回堆棧以進(jìn)行進(jìn)一步評(píng)估。 請(qǐng)考慮以下方法:
public static int plus(int a, int b) {int c = a + b;return c; }注意:本文顯示的所有字節(jié)碼都是從javap輸出的,例如javap -c -p MyClass 。
public static int plus(int, int);Code:stack=2, locals=3, arguments=20: iload_0 // load ‘x’ from slot 0, push onto stack1: iload_1 // load ‘y’ from slot 1, push onto stack2: iadd // pop 2 integers, add them together, and push the result3: istore_2 // pop the result, store as ‘sum’ in slot 24: iload_2 // load ‘sum’ from slot 2, push onto stack5: ireturn // return the integer at the top of the stack(為清楚起見(jiàn)添加了注釋。)
方法的局部變量(包括該方法的參數(shù))存儲(chǔ)在JVM所謂的局部變量數(shù)組中 。 為了簡(jiǎn)潔起見(jiàn),我們將存儲(chǔ)在本地變量數(shù)組中位置#x的值(或引用)稱為“插槽#x”(請(qǐng)參閱JVM規(guī)范§3.6.1 )。
對(duì)于實(shí)例方法,插槽#0中的值始終是this指針。 然后從左到右依次是方法參數(shù),然后是方法中聲明的所有局部變量。 在上面的示例中,該方法是靜態(tài)的,因此沒(méi)有this指針。 插槽#0保留參數(shù)x ,插槽#1保留參數(shù)y 。 局部變量sum駐留在插槽#2中。
有趣的是,每種方法都具有最大堆棧大小和最大局部變量存儲(chǔ)量,這兩者都是在編譯時(shí)確定的。
從這里立即顯而易見(jiàn)的一件事是您最初可能不會(huì)想到的,那就是編譯器沒(méi)有嘗試優(yōu)化代碼。 實(shí)際上, javac幾乎從不發(fā)出優(yōu)化的字節(jié)碼。 這有很多好處,包括在大多數(shù)位置設(shè)置斷點(diǎn)的能力:如果我們要消除冗余的加載/存儲(chǔ)操作,我們將失去該能力。 因此,大部分繁重的工作都是由即時(shí)(JIT)編譯器在運(yùn)行時(shí)執(zhí)行的。
反編譯
因此,如何獲取基于棧的非結(jié)構(gòu)化字節(jié)碼并將其轉(zhuǎn)換回結(jié)構(gòu)化Java代碼? 第一步通常是擺脫操作數(shù)堆棧,我們可以通過(guò)將堆棧值映射到變量并插入適當(dāng)?shù)募虞d和存儲(chǔ)操作來(lái)做到這一點(diǎn)。
“堆棧變量”僅分配一次,并且消耗一次。 您可能會(huì)注意到,這將導(dǎo)致很多冗余變量-稍后再介紹! 反編譯器也可以將字節(jié)碼簡(jiǎn)化為更簡(jiǎn)單的指令集,但是在此我們不考慮。
我們將使用符號(hào)s0 (等)表示堆棧變量 ,使用v0表示原始字節(jié)碼中引用的真實(shí)局部變量(并存儲(chǔ)在插槽中)。
| 0 1個(gè) 2 3 4 5 | iload_0 iload_1 我加 istore_2 iload_2 我回來(lái) | s0 = v0 s1 = v1 s2 = s0 + s1 v2 = s2 s3 = v2 返回s3 | v2 = v0 + v1 返回v2 |
通過(guò)將標(biāo)識(shí)符分配給每個(gè)被推入或彈出的值,我們可以從字節(jié)碼到變量 ,例如, iadd彈出兩個(gè)操作數(shù)以進(jìn)行加和推結(jié)果。
然后,我們應(yīng)用一種稱為復(fù)制傳播的技術(shù)來(lái)消除一些冗余變量。 復(fù)制傳播是一種內(nèi)聯(lián)形式,其中只要對(duì)轉(zhuǎn)換有效,就可以簡(jiǎn)單地將對(duì)變量的引用替換為指定的值。
我們所說(shuō)的“有效”是什么意思? 好吧,這里有一些重要的限制。 考慮以下:
0: s0 = v1 1: v1 = s4 2: v2 = s0 <-- s0 cannot be replaced with v1在這里,如果我們用v0替換s0 ,則行為將發(fā)生變化,因?yàn)関0的值在分配s0之后但被消耗之前發(fā)生變化。 為了避免這些復(fù)雜性,我們僅將復(fù)制傳播用于僅分配一次的內(nèi)聯(lián)變量。
強(qiáng)制執(zhí)行的一種方式可能是追蹤所有門店非堆棧變量,即,我們知道, v1在#0分配V1 0,并且還V1 1在#2。 由于對(duì)v1有多個(gè)分配,因此我們無(wú)法執(zhí)行復(fù)制傳播。
但是,我們最初的示例沒(méi)有這種復(fù)雜性,最終我們得到了一個(gè)簡(jiǎn)潔明了的結(jié)果:
v2 = v0 + v1 return v2另外:恢復(fù)變量名
如果將變量簡(jiǎn)化為字節(jié)碼中的插槽引用,那么如何恢復(fù)原始變量名? 有可能我們做不到。 為了改善調(diào)試體驗(yàn),每種方法的字節(jié)碼可能包括一個(gè)稱為局部變量表的特殊部分。 對(duì)于原始源中的每個(gè)變量,都存在一個(gè)條目,用于指定名稱,插槽號(hào)和名稱所適用的字節(jié)碼范圍。 通過(guò)包含-v選項(xiàng),可以將該表(以及其他有用的元數(shù)據(jù))包含在javap反匯編中。 對(duì)于上面的plus()方法,該表如下所示:
Start Length Slot Name Signature 0 6 0 a I 0 6 1 b I 4 2 2 c I在這里,我們看到v2指的是原始變量' c ',其字節(jié)碼偏移量為#4-5。
對(duì)于已編譯而沒(méi)有局部變量表的類(或被混淆器剝離的類),我們必須生成自己的名稱。 有很多策略可以做到這一點(diǎn)。 一個(gè)聰明的實(shí)現(xiàn)可能會(huì)看一看如何將變量用于適當(dāng)名稱的提示。
堆棧分析
在前面的示例中,我們可以保證在任何給定點(diǎn)上哪個(gè)值位于堆棧的頂部,因此我們可以命名為s0 , s1等。
到目前為止,處理變量非常簡(jiǎn)單,因?yàn)槲覀冎惶剿髁藛蝹€(gè)代碼路徑的方法。 在現(xiàn)實(shí)世界的應(yīng)用程序中,大多數(shù)方法都不會(huì)那么適應(yīng)。 每次向方法添加循環(huán)或條件時(shí),都會(huì)增加調(diào)用者可能采用的路徑數(shù)量。 考慮我們先前示例的修改版本:
public static int plus(boolean t, int a, int b) {int c = t ? a : b;return c; }現(xiàn)在我們有了控制流程來(lái)使事情復(fù)雜化。 如果嘗試執(zhí)行與以前相同的任務(wù),則會(huì)遇到問(wèn)題。
| 0 1個(gè) 4 5 8 9 10 11 | iload_0 ifeq 8 iload_1 轉(zhuǎn)到9 iload_2 istore_3 iload_3 我回來(lái) | s0 = v0 如果(s0 == 0)轉(zhuǎn)到#8 s1 = v1 轉(zhuǎn)到#9 s2 = v2 v3 = {s1,s2} s4 = v3 返回s4 |
我們需要更加聰明地分配堆棧標(biāo)識(shí)符。 單獨(dú)考慮每條指令已不再足夠; 我們需要跟蹤堆棧在任何給定位置的外觀,因?yàn)槲覀兛赡軙?huì)采用多種路徑到達(dá)該位置。
當(dāng)我們檢查#9 ,我們看到istore_3彈出一個(gè)值,但是該值有兩個(gè)來(lái)源:它可能起源于#5或#8 。 堆棧頂部#9處的值可能是s1或s2 ,這取決于我們分別來(lái)自#5還是#8 。 因此,我們需要將它們視為相同的變量-我們將它們合并,并且對(duì)s1或s2所有引用都將成為對(duì)明確變量s{1,2}引用。 進(jìn)行“重新標(biāo)記”后,我們可以安全地執(zhí)行復(fù)制傳播。
| 0 1個(gè) 4 5 8 9 10 11 | s0 = v0 如果(s0 == 0)轉(zhuǎn)到#8 s {1,2} = v1 轉(zhuǎn)到#9 s {1,2} =:v2 v3 = s {1,2} s4 = v3 返回s4 | 如果(v0 == 0)轉(zhuǎn)到#8 s {1,2} = v1 轉(zhuǎn)到#9 s {1,2} = v2 v3 = s {1,2}返回v3 |
注意條件分支在#1 :如果s0的值為零,我們跳轉(zhuǎn)到else塊; else ,跳轉(zhuǎn)到else塊。 否則,我們將沿著當(dāng)前路徑繼續(xù)。 有趣的是,與原始來(lái)源相比,測(cè)試條件被否定了。
現(xiàn)在,我們已經(jīng)涵蓋了足夠的內(nèi)容,可以深入……
條件表達(dá)式
在這一點(diǎn)上,我們可以確定我們的代碼可以使用三元運(yùn)算符( ?: :)進(jìn)行建模:我們有一個(gè)條件,每個(gè)分支對(duì)同一堆棧變量s {1,2}具有單個(gè)賦值,此后兩個(gè)路徑會(huì)聚。
一旦確定了這種模式,就可以立即將三元數(shù)向上滾動(dòng)。
| 0 1個(gè) 4 5 8 9 10 11 | 如果(v0 == 0)轉(zhuǎn)到#8 s {1,2} = v1 轉(zhuǎn)到9 s {1,2} = v2 v3 = s {1,2}返回v3 | v3 = v0!= 0 v1:v2 返回v3 |
請(qǐng)注意,作為轉(zhuǎn)換的一部分,我們否定了#9的條件。 事實(shí)證明, javac否定條件的方式是相當(dāng)可預(yù)測(cè)的,因此,如果將條件翻轉(zhuǎn)回去,我們可以更緊密地匹配原始源。
除了–但是類型是什么?
在處理堆棧值時(shí),JVM使用的類型系統(tǒng)比Java源代碼更簡(jiǎn)單。 具體來(lái)說(shuō), boolean , char和short值使用與int值相同的指令進(jìn)行操作。 因此,比較v0 != 0可以解釋為:
v0 != false ? v1 : v2…要么:
v0 != 0 ? v1 : v2…甚至:
v0 != false ? v1 == true : v2 == true…等等!
但是,在這種情況下,我們很幸運(yùn)地知道v0的確切類型,因?yàn)樗诜椒枋龇?:
descriptor: (ZII)Iflags: ACC_PUBLIC, ACC_STATIC這告訴我們方法簽名的形式為:
public static int plus(boolean, int, int)我們還可以推斷v3應(yīng)該是一個(gè)int (而不是boolean ),因?yàn)樗挥米鞣祷刂?#xff0c;并且描述符告訴我們返回類型。 然后我們剩下:
v3 = v0 ? v1 : v2 return v3v0一句,如果v0是局部變量(而不是形式參數(shù)),那么我們可能不知道它表示boolean值而不是int 。 還記得我們前面提到的局部變量表,它告訴我們?cè)甲兞棵麊?#xff1f; 它還包含有關(guān)變量類型的信息 ,因此,如果將編譯器配置為發(fā)出調(diào)試信息,我們可以在該表中查找類型提示。 還有另一個(gè)類似的表,稱為L(zhǎng)ocalVariableTypeTable ,其中包含類似的信息。 主要區(qū)別在于LocalVariableTypeTable可能包含有關(guān)泛型類型的詳細(xì)信息,而LocalVariableTable無(wú)法。 值得注意的是,這些表是未經(jīng)驗(yàn)證的元數(shù)據(jù),因此它們不一定是可信賴的 。 一個(gè)特別狡猾的混淆器可能會(huì)選擇用謊言填充這些表,并且生成的字節(jié)碼仍然有效! 自行決定使用它們。
短路運(yùn)算符(
public static boolean fn(boolean a, boolean b, boolean c){return a || b && c; }怎么會(huì)更簡(jiǎn)單? 不幸的是,字節(jié)碼有點(diǎn)麻煩……
| 0 1個(gè) 4 5 8 9 12 13 16 17 | iload_0 ifne#12 iload_1 ifeq#16 iload_2 ifeq#16 iconst_1 轉(zhuǎn)到#17 iconst_0 我回來(lái) | s0 = v0 如果(s0!= 0)轉(zhuǎn)到#12 s1 = v1 如果(s1 == 0)轉(zhuǎn)到#16 s2 = v2 如果(s2 == 0)轉(zhuǎn)到#16 s3 = 1 轉(zhuǎn)到17 s4 = 0 返回s {3,4} | 如果(v0!= 0)轉(zhuǎn)到#12如果(v1 == 0)轉(zhuǎn)到#16 如果(v2 == 0)轉(zhuǎn)到#16 |
#17的ireturn指令可能會(huì)返回s3或s4 ,這取決于所采用的路徑。 我們?nèi)缟纤鰧?duì)它們進(jìn)行別名處理,然后執(zhí)行復(fù)制傳播以消除s0 , s1和s2 。
我們?cè)?1 , #5和#7處擁有三個(gè)連續(xù)的條件。 正如我們前面提到的,條件分支跳轉(zhuǎn)或落入下一條指令。
上面的字節(jié)碼包含遵循特定且非常有用的模式的條件分支序列:
| T1: if (c1) goto L1if (c2) goto L2 L1:...Becomesif (!c1 && c2) goto L2 L1:... | T1:if (c1) goto L2if (c2) goto L2 L1:...Becomesif (c1 || c2) goto L2 L1:... |
如果我們考慮上面的相鄰條件對(duì),則#1...#5不符合以下任何一種模式,但是#5...#9是條件析取( || ),因此我們應(yīng)用適當(dāng)?shù)淖儞Q:
1: if (v0 != 0) goto #125: if (v1 == 0 || v2 == 0) goto #16 12: s{3,4} = 1 13: goto #17 16: s{3,4} = 0 17: return s{3,4}請(qǐng)注意,我們應(yīng)用的每個(gè)轉(zhuǎn)換都可能創(chuàng)造機(jī)會(huì)執(zhí)行其他轉(zhuǎn)換。 在這種情況下,應(yīng)用|| transform重組了我們的條件,現(xiàn)在#1...#5符合&&模式! 因此,我們可以通過(guò)將這些行合并為單個(gè)條件分支來(lái)進(jìn)一步簡(jiǎn)化該方法:
1: if (v0 == 0 && (v1 == 0 || v2 == 0)) goto #16 12: s{3,4} = 1 13: goto #17 16: s{3,4} = 0 17: return s{3,4}這看起來(lái)很熟悉嗎? 它應(yīng)該:該字節(jié)碼現(xiàn)在符合我們前面介紹的三元( ?: :)運(yùn)算符模式。 我們可以將#1...#16簡(jiǎn)化為單個(gè)表達(dá)式,然后使用復(fù)制傳播將s{3,4}內(nèi)聯(lián)到#17的return語(yǔ)句中:
return (v0 == 0 && (v1 == 0 || v2 == 0)) ? 0 : 1;使用前面描述的方法描述符和局部變量類型表,我們可以推斷出將該表達(dá)式簡(jiǎn)化為的所有必要類型:
return (v0 == false && (v1 == false || v2 == false)) ? false : true;好吧,這當(dāng)然比我們最初的反編譯更簡(jiǎn)潔,但仍然很麻煩。 讓我們看看我們能做些什么。 我們可以先折疊x和!x比較,例如x == true和x == false 。 我們也可以通過(guò)減少x ? false : true來(lái)消除三元運(yùn)算符x ? false : true x ? false : true與簡(jiǎn)單表達(dá)式!x 。
return !(!v0 && (!v1 || !v2));更好,但是仍然很少。 如果您還記得高中離散數(shù)學(xué),那么可以看到De Morgan定理可以在這里應(yīng)用:
!(a || b) --> (!a) && (!b)!(a && b) --> (!a) || (!b)因此:
return ! ( !v0 && ( !v1 || !v2 ) )…成為:
return ( v0 || !(!v1 || !v2 ) )…最終:
return ( v0 || (v1 && v2) )歡呼!
處理方法調(diào)用
我們已經(jīng)了解了一種方法的外觀:在locals數(shù)組中將參數(shù)“到達(dá)”。 要調(diào)用方法,必須將參數(shù)壓入堆棧,對(duì)于例如方法,此參數(shù)必須緊跟this指針。 正如您所期望的那樣,以字節(jié)碼調(diào)用方法:
push arg_0push arg_1 invokevirtual METHODREF我們?cè)谏厦嬷付薸nvokevirtual ,這是用于調(diào)用大多數(shù)實(shí)例方法的指令。 JVM實(shí)際上有一些用于方法調(diào)用的指令,每個(gè)指令具有不同的語(yǔ)義:
對(duì)于反編譯器編寫器,重要的細(xì)節(jié)是該類的常量池包含有關(guān)任何所調(diào)用方法的詳細(xì)信息,包括其參數(shù)的數(shù)量和類型以及其返回類型。 在調(diào)用程序類中記錄此信息,可以使運(yùn)行時(shí)驗(yàn)證運(yùn)行時(shí)是否存在預(yù)期的方法,并且該方法符合預(yù)期的簽名。 如果目標(biāo)方法在第三方代碼中,并且其簽名發(fā)生了變化,則任何嘗試調(diào)用舊版本的代碼都將引發(fā)錯(cuò)誤(與產(chǎn)生未定義行為相反)。
回到上面的示例, invokevirtual操作碼的存在告訴我們目標(biāo)方法是實(shí)例方法 ,因此需要this指針作為隱式第一個(gè)參數(shù)。 常量池中的METHODREF告訴我們?cè)摲椒ň哂幸粋€(gè)形式參數(shù),因此我們知道除了目標(biāo)實(shí)例指針外,還需要從堆棧中彈出一個(gè)參數(shù)。 然后,我們可以將代碼重寫為:
arg_0.METHODREF(arg_1)當(dāng)然,字節(jié)碼并不總是那么友好。 不需要將堆棧參數(shù)整齊地推入堆棧,一個(gè)接一個(gè)地推。 例如,如果參數(shù)之一是三元表達(dá)式,則將存在需要獨(dú)立轉(zhuǎn)換的中間加載,存儲(chǔ)和分支指令。 混淆器可能會(huì)將方法重寫為特別復(fù)雜的指令序列。 一個(gè)好的反編譯器將需要足夠靈活,以處理超出本文范圍的許多有趣的極端情況。
不僅限于此……
到目前為止,我們僅限于分析單個(gè)代碼序列,首先是一系列簡(jiǎn)單的指令,然后應(yīng)用產(chǎn)生更熟悉的高級(jí)結(jié)構(gòu)的轉(zhuǎn)換。 如果您認(rèn)為這似乎過(guò)于簡(jiǎn)單,那么您是正確的。 Java是一種高度結(jié)構(gòu)化的語(yǔ)言,具有諸如范圍和塊之類的概念以及更復(fù)雜的控制流機(jī)制。 為了處理諸如if/else塊和循環(huán)之類的構(gòu)造,我們需要對(duì)代碼進(jìn)行更嚴(yán)格的分析,并特別注意可能采用的各種路徑。 這稱為控制流分析 。
我們首先將代碼分解為可以從頭到尾執(zhí)行的連續(xù)塊。 這些被稱為基本塊 ,我們通過(guò)沿著可能跳轉(zhuǎn)到另一個(gè)塊的地方以及可能成為跳轉(zhuǎn)目標(biāo)的任何指令拆分指令列表來(lái)構(gòu)造它們。
然后,我們通過(guò)在塊之間創(chuàng)建代表所有可能分支的邊來(lái)建立控制流程圖 (CFG)。 請(qǐng)注意,這些邊緣可能不是顯式分支; 包含可能引發(fā)異常的指令的塊將連接到它們各自的異常處理程序。 我們不會(huì)詳細(xì)介紹如何構(gòu)造CFG,但是需要一些高級(jí)知識(shí)才能理解我們?nèi)绾问褂眠@些圖來(lái)檢測(cè)諸如循環(huán)之類的代碼構(gòu)造。
控制流程圖的示例。
我們最感興趣的控制流程圖的方面是控制關(guān)系 :
- 如果到N所有路徑都通過(guò)D則稱節(jié)點(diǎn)D 支配另一個(gè)節(jié)點(diǎn)N 所有節(jié)點(diǎn)都占主導(dǎo)地位。 如果D和N是不同的節(jié)點(diǎn),則說(shuō)D 嚴(yán)格支配 N
- 如果D嚴(yán)格支配N并沒(méi)有嚴(yán)格的主宰任何其他節(jié)點(diǎn)是嚴(yán)格支配N ,然后D可立即主宰說(shuō)N 。
- 支配者樹(shù)是節(jié)點(diǎn)樹(shù),其中每個(gè)節(jié)點(diǎn)的子節(jié)點(diǎn)是其立即支配的節(jié)點(diǎn)。
- D的支配性邊界是所有節(jié)點(diǎn)N的集合,以使D支配N的直接前輩,但不嚴(yán)格支配N 換句話說(shuō),這是D的優(yōu)勢(shì)結(jié)束的節(jié)點(diǎn)集。
基本循環(huán)和控制流程
考慮以下Java方法:
public static void fn(int n) {for (int i = 0; i < n; ++i) {System.out.println(i);} }…及其拆卸:
0: iconst_01: istore_12: iload_13: iload_04: if_icmpge 207: getstatic #2 // System.out:PrintStream 10: iload_1 11: invokevirtual #3 // PrintStream.println:(I)V 14: iinc 1, 1 17: goto 2 20: return讓我們應(yīng)用上面討論的內(nèi)容,首先將其引入堆棧變量,然后執(zhí)行復(fù)制傳播,以將其轉(zhuǎn)換為更具可讀性的形式。
| 0 1個(gè) 2 3 4 7 10 11 14 17 20 | iconst_0 istore_1 iload_1 iload_0 if_icmpge 20 靜態(tài)#2 iload_1 invokevirtual#3 1號(hào),1號(hào) 轉(zhuǎn)到2 返回 | s0 = 0 v1 = s0 s2 = v1 s3 = v0 如果(s2> = s3)轉(zhuǎn)到20 s4 = System.out s5 = v1 s4.println(s5) v1 = v1 + 1 轉(zhuǎn)到2 返回 | 如果(v1> = v0)轉(zhuǎn)到20,則v1 = 0 System.out.println(v1) |
注意在#4處的條件分支和在#17處的goto如何創(chuàng)建邏輯循環(huán)。 通過(guò)查看控制流程圖,我們可以更容易地看到這一點(diǎn):
從圖中可以明顯看出,我們有一個(gè)整齊的循環(huán),其邊緣從goto到條件分支。 在這種情況下,條件分支稱為循環(huán)標(biāo)頭 ,可以將其定義為具有形成回路的后沿的支配器。 循環(huán)頭控制著循環(huán)體內(nèi)的所有節(jié)點(diǎn)。
我們可以通過(guò)尋找形成循環(huán)的后邊緣來(lái)確定條件是否為循環(huán)頭,但是我們?cè)撛趺醋瞿?#xff1f; 一個(gè)簡(jiǎn)單的解決方案是測(cè)試條件節(jié)點(diǎn)是否在其自己的優(yōu)勢(shì)邊界中。 一旦知道了循環(huán)頭,就必須找出要拉入循環(huán)主體的節(jié)點(diǎn)。 我們可以通過(guò)查找由標(biāo)頭控制的所有節(jié)點(diǎn)來(lái)做到這一點(diǎn)。 在偽代碼中,我們的算法如下所示:
findDominatedNodes(header)q := new Queue()r := new Set()q.enqueue(header)while (not q.empty())n := q.dequeue()if (header.dominates(n))r.add(n)for (s in n.successors())q.enqueue(n)return r一旦弄清了循環(huán)體,就可以將代碼轉(zhuǎn)換成循環(huán)。 請(qǐng)記住,我們的循環(huán)頭可能是一個(gè)條件跳出循環(huán),在這種情況下,我們需要否定的條件。
v1 = 0 while (v1 < v0) {System.out.println(v1)v1 = v1 + 1 } return瞧,我們有一個(gè)簡(jiǎn)單的前提條件循環(huán)! 大多數(shù)循環(huán)(包括while , for和for-each )都編譯為相同的基本模式,我們將其視為簡(jiǎn)單的while循環(huán)。 無(wú)法確定程序員最初編寫的是哪種循環(huán),但是for和for-each遵循我們可以尋找的非常具體的模式。 我們不會(huì)詳細(xì)介紹,但是如果您查看上面的while循環(huán),則可以看到原始的for循環(huán)的初始值設(shè)定項(xiàng)( v1 = 0 )在循環(huán)之前,并已插入其迭代器( v1 = v1 + 1 )。在循環(huán)主體的末尾。 我們將把它作為一種練習(xí),思考一下何時(shí)以及如何將while循環(huán)轉(zhuǎn)換for或for-each 。 考慮一下我們可能如何調(diào)整邏輯以檢測(cè)條件后 ( do/while )循環(huán)也很有趣。
我們可以應(yīng)用類似的技術(shù)來(lái)反編譯if/else語(yǔ)句。 字節(jié)碼模式非常簡(jiǎn)單:
begin:iftrue(!condition) goto #else// `if` block begins here...goto #endelse:// `else` block begins here...end:// end of `if/else`在這里,我們使用iftrue作為表示任何條件分支的偽指令:測(cè)試條件,如果條件通過(guò),則分支; 否則,請(qǐng)繼續(xù)。 我們知道if塊從條件之后的指令開(kāi)始, else塊從條件的跳轉(zhuǎn)目標(biāo)開(kāi)始。 查找那些塊的內(nèi)容就像查找由那些起始節(jié)點(diǎn)所控制的節(jié)點(diǎn)一樣簡(jiǎn)單,我們可以使用與上述相同的算法來(lái)完成。
現(xiàn)在,我們已經(jīng)介紹了基本的控制流機(jī)制,盡管還有其他一些機(jī)制(例如,異常處理程序和子例程),但是它們不在本文的介紹范圍之內(nèi)。
結(jié)語(yǔ)
編寫反編譯器絕非易事,而且經(jīng)驗(yàn)很容易轉(zhuǎn)化為一本書的材料價(jià)值,甚至可能是一系列書籍! 顯然,我們無(wú)法在單個(gè)博客文章中介紹所有內(nèi)容,并且如果我們嘗試過(guò),您可能不想閱讀它。 我們希望通過(guò)接觸最常見(jiàn)的結(jié)構(gòu)(邏輯運(yùn)算符,條件和基本控制流程),使您對(duì)反編譯器開(kāi)發(fā)領(lǐng)域有個(gè)有趣的了解。
Lee Benfield是CFR Java反編譯器的作者。
Mike Strobel是Procyon (一個(gè)Java反編譯器和元編程框架)的作者。
現(xiàn)在去寫你自己的! :)
翻譯自: https://www.javacodegeeks.com/2013/12/anatomy-of-a-java-decompiler.html
java編譯器分析
總結(jié)
以上是生活随笔為你收集整理的java编译器分析_Java反编译器的剖析的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。
- 上一篇: 在边缘,作为网关或在网格中构建控制平面以
- 下一篇: JDK 11上的JavaFX