Java代碼編譯和反編譯的那些事兒
編程語言
在介紹編譯和反編譯之前,我們先來簡單介紹下編程語言(Programming Language)。編程語言(Programming Language)分為低級語言(Low-level Language)和高級語言(High-level Language)。
機器語言(Machine Language)和匯編語言(Assembly Language)屬于低級語言,直接用計算機指令編寫程序。
而C、C++、Java、Python等屬于高級語言,用語句(Statement)編寫程序,語句是計算機指令的抽象表示。
舉個例子,同樣一個語句用C語言、匯編語言和機器語言分別表示如下:
計算機只能對數(shù)字做運算,符號、聲音、圖像在計算機內(nèi)部都要用數(shù)字表示,指令也不例外,上表中的機器語言完全由十六進制數(shù)字組成。最早的程序員都是直接用機器語言編程,但是很麻煩,需要查大量的表格來確定每個數(shù)字表示什么意思,編寫出來的程序很不直觀,而且容易出錯,于是有了匯編語言,把機器語言中一組一組的數(shù)字用助記符(Mnemonic)表示,直接用這些助記符寫出匯編程序,然后讓匯編器(Assembler)去查表把助記符替換成數(shù)字,也就把匯編語言翻譯成了機器語言。
但是,匯編語言用起來同樣比較復(fù)雜,后面,就衍生出了Java、C、C++等高級語言。
什么是編譯
上面提到語言有兩種,一種低級語言,一種高級語言??梢赃@樣簡單的理解:低級語言是計算機認識的語言、高級語言是程序員認識的語言。
那么如何從高級語言轉(zhuǎn)換成低級語言呢?這個過程其實就是編譯。
從上面的例子還可以看出,C語言的語句和低級語言的指令之間不是簡單的一一對應(yīng)關(guān)系,一條a=b+1;語句要翻譯成三條匯編或機器指令,這個過程稱為編譯(Compile),由編譯器(Compiler)來完成,顯然編譯器的功能比匯編器要復(fù)雜得多。用C語言編寫的程序必須經(jīng)過編譯轉(zhuǎn)成機器指令才能被計算機執(zhí)行,編譯需要花一些時間,這是用高級語言編程的一個缺點,然而更多的是優(yōu)點。首先,用C語言編程更容易,寫出來的代碼更緊湊,可讀性更強,出了錯也更容易改正。
將便于人編寫、閱讀、維護的高級計算機語言所寫作的源代碼程序,翻譯為計算機能解讀、運行的低階機器語言的程序的過程就是編譯。負責(zé)這一過程的處理的工具叫做編譯器
現(xiàn)在我們知道了什么是編譯,也知道了什么是編譯器。不同的語言都有自己的編譯器,Java語言中負責(zé)編譯的編譯器是一個命令:javac
javac是收錄于JDK中的Java語言編譯器。該工具可以將后綴名為.java的源文件編譯為后綴名為.class的可以運行于Java虛擬機的字節(jié)碼。
當(dāng)我們寫完一個HelloWorld.java文件后,我們可以使用javac HelloWorld.java命令來生成HelloWorld.class文件,這個class類型的文件是JVM可以識別的文件。通常我們認為這個過程叫做Java語言的編譯。其實,class文件仍然不是機器能夠識別的語言,因為機器只能識別機器語言,還需要JVM再將這種class文件類型字節(jié)碼轉(zhuǎn)換成機器可以識別的機器語言。
什么是反編譯
反編譯的過程與編譯剛好相反,就是將已編譯好的編程語言還原到未編譯的狀態(tài),也就是找出程序語言的源代碼。就是將機器看得懂的語言轉(zhuǎn)換成程序員可以看得懂的語言。Java語言中的反編譯一般指將class文件轉(zhuǎn)換成java文件。
有了反編譯工具,我們可以做很多事情,最主要的功能就是有了反編譯工具,我們就能讀得懂Java編譯器生成的字節(jié)碼。如果你想問讀懂字節(jié)碼有啥用,那么我可以很負責(zé)任的告訴你,好處大大的。比如我的博文幾篇典型的原理性文章,都是通過反編譯工具得到反編譯后的代碼分析得到的。如深入理解多線程(一)——Synchronized的實現(xiàn)原理、深度分析Java的枚舉類型—-枚舉的線程安全性及序列化問題、Java中的Switch對整型、字符型、字符串型的具體實現(xiàn)細節(jié)、Java的類型擦除等。我最近在GitChat寫了一篇關(guān)于Java語法糖的文章,其中大部分內(nèi)容都用到反編譯工具來洞悉語法糖背后的原理。
Java反編譯工具
本文主要介紹3個Java的反編譯工具:javap、jad和cfr
javap
javap是jdk自帶的一個工具,可以對代碼反編譯,也可以查看java編譯器生成的字節(jié)碼。javap和其他兩個反編譯工具最大的區(qū)別是他生成的文件并不是java文件,也不像其他兩個工具生成代碼那樣更容易理解。拿一段簡單的代碼舉例,如我們想分析Java 7中的switch是如何支持String的,我們先有以下可以編譯通過的源代碼:
public class switchDemoString { public static void main(String[] args) { String str = "world"; switch (str) { case "hello": System.out.println("hello"); break; case "world": System.out.println("world"); break; default: break; } } }
執(zhí)行以下兩個命令:
javac switchDemoString.java javap -c switchDemoString.class
生成代碼如下:
public class com.hollis.suguar.switchDemoString { public com.hollis.suguar.switchDemoString(); Code: 0: aload_0 1: invokespecial #1 // Method java/lang/Object."<init>":()V 4: return public static void main(java.lang.String[]); Code: 0: ldc #2 // String world 2: astore_1 3: aload_1 4: astore_2 5: iconst_m1 6: istore_3 7: aload_2 8: invokevirtual #3 // Method java/lang/String.hashCode:()I 11: lookupswitch { // 2 99162322: 36 113318802: 50 default: 61 } 36: aload_2 37: ldc #4 // String hello 39: invokevirtual #5 // Method java/lang/String.equals:(Ljava/lang/Object;)Z 42: ifeq 61 45: iconst_0 46: istore_3 47: goto 61 50: aload_2 51: ldc #2 // String world 53: invokevirtual #5 // Method java/lang/String.equals:(Ljava/lang/Object;)Z 56: ifeq 61 59: iconst_1 60: istore_3 61: iload_3 62: lookupswitch { // 2 0: 88 1: 99 default: 110 } 88: getstatic #6 // Field java/lang/System.out:Ljava/io/PrintStream; 91: ldc #4 // String hello 93: invokevirtual #7 // Method java/io/PrintStream.println:(Ljava/lang/String;)V 96: goto 110 99: getstatic #6 // Field java/lang/System.out:Ljava/io/PrintStream; 102: ldc #2 // String world 104: invokevirtual #7 // Method java/io/PrintStream.println:(Ljava/lang/String;)V 107: goto 110 110: return }
我個人的理解,javap并沒有將字節(jié)碼反編譯成java文件,而是生成了一種我們可以看得懂字節(jié)碼。其實javap生成的文件仍然是字節(jié)碼,只是程序員可以稍微看得懂一些。如果你對字節(jié)碼有所掌握,還是可以看得懂以上的代碼的。其實就是把String轉(zhuǎn)成hashcode,然后進行比較。
個人認為,一般情況下我們會用到j(luò)avap命令的時候不多,一般只有在真的需要看字節(jié)碼的時候才會用到。但是字節(jié)碼中間暴露的東西是最全的,你肯定有機會用到,比如我在分析synchronized的原理的時候就有是用到j(luò)avap。通過javap生成的字節(jié)碼,我發(fā)現(xiàn)synchronized底層依賴了ACC_SYNCHRONIZED標(biāo)記和monitorenter、monitorexit兩個指令來實現(xiàn)同步。
jad
jad是一個比較不錯的反編譯工具,只要下載一個執(zhí)行工具,就可以實現(xiàn)對class文件的反編譯了。還是上面的源代碼,使用jad反編譯后內(nèi)容如下:
命令:jad switchDemoString.class
public class switchDemoString { public switchDemoString() { } public static void main(String args[]) { String str = "world"; String s; switch((s = str).hashCode()) { default: break; case 99162322: if(s.equals("hello")) System.out.println("hello"); break; case 113318802: if(s.equals("world")) System.out.println("world"); break; } } }
看,這個代碼你肯定看的懂,因為這不就是標(biāo)準的java的源代碼么。這個就很清楚的可以看到原來字符串的switch是通過equals()和hashCode()方法來實現(xiàn)的。
但是,jad已經(jīng)很久不更新了,在對Java7生成的字節(jié)碼進行反編譯時,偶爾會出現(xiàn)不支持的問題,在對Java 8的lambda表達式反編譯時就徹底失敗。
CFR
jad很好用,但是無奈的是很久沒更新了,所以只能用一款新的工具替代他,CFR是一個不錯的選擇,相比jad來說,他的語法可能會稍微復(fù)雜一些,但是好在他可以work。
如,我們使用cfr對剛剛的代碼進行反編譯。執(zhí)行一下命令:
java -jar cfr_0_125.jar switchDemoString.class --decodestringswitch false
得到以下代碼:
public class switchDemoString { public static void main(String[] arrstring) { String string; String string2 = string = "world"; int n = -1; switch (string2.hashCode()) { case 99162322: { if (!string2.equals("hello")) break; n = 0; break; } case 113318802: { if (!string2.equals("world")) break; n = 1; } } switch (n) { case 0: { System.out.println("hello"); break; } case 1: { System.out.println("world"); break; } } } }
通過這段代碼也能得到字符串的switch是通過equals()和hashCode()方法來實現(xiàn)的結(jié)論。
相比Jad來說,CFR有很多參數(shù),還是剛剛的代碼,如果我們使用以下命令,輸出結(jié)果就會不同:
java -jar cfr_0_125.jar switchDemoString.class public class switchDemoString { public static void main(String[] arrstring) { String string; switch (string = "world") { case "hello": { System.out.println("hello"); break; } case "world": { System.out.println("world"); break; } } } }
所以--decodestringswitch表示對于switch支持string的細節(jié)進行解碼。類似的還有--decodeenumswitch、--decodefinally、--decodelambdas等。在我的關(guān)于語法糖的文章中,我使用--decodelambdas對lambda表達式警進行了反編譯。 源碼:
public static void main(String... args) { List<String> strList = ImmutableList.of("Hollis", "公眾號:Hollis", "博客:www.hollischuang.com"); strList.forEach( s -> { System.out.println(s); } ); }
java -jar cfr_0_125.jar lambdaDemo.class --decodelambdas false反編譯后代碼:
public static /* varargs */ void main(String ... args) { ImmutableList strList = ImmutableList.of((Object)"Hollis", (Object)"\u516c\u4f17\u53f7\uff1aHollis", (Object)"\u535a\u5ba2\uff1awww.hollischuang.com"); strList.forEach((Consumer<String>)LambdaMetafactory.metafactory(null, null, null, (Ljava/lang/Object;)V, lambda$main$0(java.lang.String ), (Ljava/lang/String;)V)()); } private static /* synthetic */ void lambda$main$0(String s) { System.out.println(s); }
CFR還有很多其他參數(shù),均用于不同場景,讀者可以使用java -jar cfr_0_125.jar --help進行了解。這里不逐一介紹了。
如何防止反編譯
由于我們有工具可以對Class文件進行反編譯,所以,對開發(fā)人員來說,如何保護Java程序就變成了一個非常重要的挑戰(zhàn)。但是,魔高一尺、道高一丈。當(dāng)然有對應(yīng)的技術(shù)可以應(yīng)對反編譯咯。但是,這里還是要說明一點,和網(wǎng)絡(luò)安全的防護一樣,無論做出多少努力,其實都只是提高攻擊者的成本而已。無法徹底防治。
典型的應(yīng)對策略有以下幾種:
- 隔離Java程序
- 讓用戶接觸不到你的Class文件
- 對Class文件進行加密
- 提到破解難度
- 代碼混淆
- 將代碼轉(zhuǎn)換成功能上等價,但是難于閱讀和理解的形式
總結(jié)
以上就是這篇文章的全部內(nèi)容了,希望本文的內(nèi)容對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,謝謝大家對腳本之家的支持。
相關(guān)文章
java 域?qū)ο蠊蚕頂?shù)據(jù)的實現(xiàn)
本文主要介紹了java 域?qū)ο蠊蚕頂?shù)據(jù)的實現(xiàn),文中通過示例代碼介紹的非常詳細,對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2023-03-03Java Base64算法實際應(yīng)用之郵件發(fā)送實例分析
這篇文章主要介紹了Java Base64算法實際應(yīng)用之郵件發(fā)送,結(jié)合實例形式分析了java字符編碼與郵件發(fā)送相關(guān)操作技巧,需要的朋友可以參考下2019-09-09mybatis-plus(insertBatchSomeColumn批量添加方式)
這篇文章主要介紹了mybatis-plus(insertBatchSomeColumn批量添加方式),具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2023-03-03SkyWalking?自定義插件(Spring?RabbitMQ)具體分析過程
這篇文章主要介紹了SkyWalking?自定義插件(Spring?RabbitMQ)具體分析過程,本文通過實例代碼給大家介紹的非常詳細,對大家的學(xué)習(xí)或工作具有一定的參考借鑒價值,需要的朋友可以參考下2022-02-02Spring Boot中實現(xiàn)定時任務(wù)應(yīng)用實踐
定時任務(wù)一般是項目中都需要用到的,可以用于定時處理一些特殊的任務(wù)。下面這篇文章主要給大家介紹了關(guān)于Spring Boot中實現(xiàn)定時任務(wù)應(yīng)用實踐的相關(guān)資料,文中通過示例代碼介紹的非常詳細,需要的朋友可以參考下。2018-05-05解析SpringSecurity自定義登錄驗證成功與失敗的結(jié)果處理問題
這篇文章主要介紹了SpringSecurity系列之自定義登錄驗證成功與失敗的結(jié)果處理問題,本文通過實例給大家講解的非常詳細,具有一定的參考借鑒價值,需要的朋友可以參考下2019-11-11IntelliJ IDEA 部署 Web 項目,看這一篇夠了!
這篇文章主要介紹了IntelliJ IDEA 部署 Web 項目的圖文教程,本文通過圖文并茂的形式給大家介紹的非常詳細,對大家的學(xué)習(xí)或工作具有一定的參考借鑒價值,需要的朋友可以參考下2020-05-05Java JVM原理與調(diào)優(yōu)_動力節(jié)點Java學(xué)院整理
JVM是Java Virtual Machine(Java虛擬機)的縮寫,JVM是一種用于計算設(shè)備的規(guī)范,它是一個虛構(gòu)出來的計算機,是通過在實際的計算機上仿真模擬各種計算機功能來實現(xiàn)的。下面通過本文給大家介紹jvm原理與調(diào)優(yōu)相關(guān)知識,感興趣的朋友一起學(xué)習(xí)吧2017-04-04