Java程式碼的編譯與反編譯那些事兒
程式語言
在介紹編譯和反編譯之前,我們先來簡單介紹下程式語言(Programming Language)。程式語言(Programming Language)分為低階語言(Low-level Language)和高階語言(High-level Language)。
機器語言(Machine Language)和組合語言(Assembly Language)屬於低階語言,直接用計算機指令編寫程式。
而C、C++、Java、Python等屬於高階語言,用語句(Statement)編寫程式,語句是計算機指令的抽象表示。
舉個例子,同樣一個語句用C語言、組合語言和機器語言分別表示如下:
計算機只能對數字做運算,符號、聲音、影象在計算機內部都要用數字表示,指令也不例外,上表中的機器語言完全由十六進位制數字組成。最早的程式設計師都是直接用機器語言程式設計,但是很麻煩,需要查大量的表格來確定每個數字表示什麼意思,編寫出來的程式很不直觀,而且容易出錯,於是有了組合語言,把機器語言中一組一組的數字用助記符(Mnemonic)表示,直接用這些助記符寫出彙編程式,然後讓彙編器(Assembler)去查表把助記符替換成數字,也就把組合語言翻譯成了機器語言。
但是,組合語言用起來同樣比較複雜,後面,就衍生出了Java、C、C++等高階語言。
什麼是編譯
上面提到語言有兩種,一種低階語言,一種高階語言。可以這樣簡單的理解:低階語言是計算機認識的語言、高階語言是程式設計師認識的語言。
那麼如何從高階語言轉換成低階語言呢?這個過程其實就是編譯。
從上面的例子還可以看出,C語言的語句和低階語言的指令之間不是簡單的一一對應關係,一條a=b+1
;語句要翻譯成三條彙編或機器指令,這個過程稱為編譯(Compile),由編譯器(Compiler)來完成,顯然編譯器的功能比彙編器要複雜得多。用C語言編寫的程式必須經過編譯轉成機器指令才能被計算機執行,編譯需要花一些時間,這是用高階語言程式設計的一個缺點,然而更多的是優點。首先,用C語言程式設計更容易,寫出來的程式碼更緊湊,可讀性更強,出了錯也更容易改正。
將便於人編寫、閱讀、維護的高階計算機語言所寫作的原始碼程式,翻譯為計算機能解讀、執行的低階機器語言的程式的過程就是編譯。負責這一過程的處理的工具叫做編譯器
現在我們知道了什麼是編譯,也知道了什麼是編譯器。不同的語言都有自己的編譯器,Java語言中負責編譯的編譯器是一個命令:javac
javac是收錄於JDK中的Java語言編譯器。該工具可以將字尾名為.java的原始檔編譯為字尾名為.class的可以運行於Java虛擬機器的位元組碼。
當我們寫完一個HelloWorld.java
檔案後,我們可以使用javac HelloWorld.java
命令來生成HelloWorld.class
檔案,這個class
class
檔案仍然不是機器能夠識別的語言,因為機器只能識別機器語言,還需要JVM再將這種class
檔案型別位元組碼轉換成機器可以識別的機器語言。
什麼是反編譯
反編譯的過程與編譯剛好相反,就是將已編譯好的程式語言還原到未編譯的狀態,也就是找出程式語言的原始碼。就是將機器看得懂的語言轉換成程式設計師可以看得懂的語言。Java語言中的反編譯一般指將class
檔案轉換成java
檔案。
有了反編譯工具,我們可以做很多事情,最主要的功能就是有了反編譯工具,我們就能讀得懂Java編譯器生成的位元組碼。如果你想問讀懂位元組碼有啥用,那麼我可以很負責任的告訴你,好處大大的。比如我的博文幾篇典型的原理性文章,都是通過反編譯工具得到反編譯後的程式碼分析得到的。如深入理解多執行緒(一)——Synchronized的實現原理、深度分析Java的列舉型別—-列舉的執行緒安全性及序列化問題、Java中的Switch對整型、字元型、字串型的具體實現細節、Java的型別擦除等。我最近在GitChat寫了一篇關於Java語法糖的文章,其中大部分內容都用到反編譯工具來洞悉語法糖背後的原理。
Java反編譯工具
本文主要介紹3個Java的反編譯工具:javap、jad和cfr
javap
javap
是jdk自帶的一個工具,可以對程式碼反編譯,也可以檢視java編譯器生成的位元組碼。javap
和其他兩個反編譯工具最大的區別是他生成的檔案並不是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;
}
}
}
執行以下兩個命令:
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
並沒有將位元組碼反編譯成java
檔案,而是生成了一種我們可以看得懂位元組碼。其實javap生成的檔案仍然是位元組碼,只是程式設計師可以稍微看得懂一些。如果你對位元組碼有所掌握,還是可以看得懂以上的程式碼的。其實就是把String轉成hashcode,然後進行比較。
個人認為,一般情況下我們會用到javap
命令的時候不多,一般只有在真的需要看位元組碼的時候才會用到。但是位元組碼中間暴露的東西是最全的,你肯定有機會用到,比如我在分析synchronized
的原理的時候就有是用到javap
。通過javap
生成的位元組碼,我發現synchronized
底層依賴了ACC_SYNCHRONIZED
標記和monitorenter
、monitorexit
兩個指令來實現同步。
jad
jad是一個比較不錯的反編譯工具,只要下載一個執行工具,就可以實現對class
檔案的反編譯了。還是上面的原始碼,使用jad反編譯後內容如下:
命令: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;
}
}
}
看,這個程式碼你肯定看的懂,因為這不就是標準的java的原始碼麼。這個就很清楚的可以看到原來字串的switch是通過equals()
和hashCode()
方法來實現的。
但是,jad已經很久不更新了,在對Java7生成的位元組碼進行反編譯時,偶爾會出現不支援的問題,在對Java 8的lambda表示式反編譯時就徹底失敗。
CFR
jad很好用,但是無奈的是很久沒更新了,所以只能用一款新的工具替代他,CFR是一個不錯的選擇,相比jad來說,他的語法可能會稍微複雜一些,但是好在他可以work。
如,我們使用cfr對剛剛的程式碼進行反編譯。執行一下命令:
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()
方法來實現的結論。
相比Jad來說,CFR有很多引數,還是剛剛的程式碼,如果我們使用以下命令,輸出結果就會不同:
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的細節進行解碼。類似的還有--decodeenumswitch
、--decodefinally
、--decodelambdas
等。在我的關於語法糖的文章中,我使用--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還有很多其他引數,均用於不同場景,讀者可以使用java -jar cfr_0_125.jar --help
進行了解。這裡不逐一介紹了。
如何防止反編譯
由於我們有工具可以對Class
檔案進行反編譯,所以,對開發人員來說,如何保護Java程式就變成了一個非常重要的挑戰。但是,魔高一尺、道高一丈。當然有對應的技術可以應對反編譯咯。但是,這裡還是要說明一點,和網路安全的防護一樣,無論做出多少努力,其實都只是提高攻擊者的成本而已。無法徹底防治。
典型的應對策略有以下幾種:
- 隔離Java程式
- 讓使用者接觸不到你的Class檔案
- 對Class檔案進行加密
- 提到破解難度
- 程式碼混淆
- 將程式碼轉換成功能上等價,但是難於閱讀和理解的形式
歡迎關注我的公眾號,一起交流:
相關推薦
Java程式碼加密與反編譯(一):利用混淆器工具proGuard對jar包加密
Java 程式碼編譯後生成的 .class 中包含有原始碼中的所有資訊(不包括註釋),尤其是在其中儲存有除錯資訊的時候。所以一個按照正常方式編譯的 Java .class 檔案可以非常輕易地被反編譯。通常情況下,反編譯可以利用現有的工具jd-gui.exe或者jad.e
Java程式碼的編譯與反編譯那些事兒
程式語言 在介紹編譯和反編譯之前,我們先來簡單介紹下程式語言(Programming Language)。程式語言(Programming Language)分為低階語言(Low-level Language)和高階語言(High-level Language)。 機器語言(Machine Language)
程式碼混淆與反編譯
程式碼混淆 程式碼混淆就是程式碼加密,讓別人看不到自己的程式碼,當然這只是相對的,並不能完全的加密,大公司都會有單獨的加密方式,防止被竊取,這裡講的混淆只是灰常灰常簡單的一
Linux系統移植——裝置樹檔案編譯與反編譯
裝置樹檔案編譯與反編譯 一、裝置樹編譯 有兩種方式 1、將裝置樹檔案拷貝到核心原始碼的arch/*(處理器平臺)/boot/dts/*(廠家)/目錄下, 執行make dtbs 2、dtc -I dts -O dtb *.dts > my.dtb 二、裝置
編譯與反編譯GNU Linux語言檔案方法
/********************************************************************* * Author : Samson * Date : 11/21/2014 * Test platform: *
android混淆程式碼與反編譯
android studio已經提供了預設的混淆程式碼,我們要做的是, 1、在build.gradle中新增 buildTypes { release { m
【深入Java虛擬機】之七:Javac編譯與JIT編譯
p s ots 基本 關鍵字 目前 關註 script 和數 語言 轉載請註明出處:http://blog.csdn.net/ns_code/article/details/18009455 編譯過程 不論是物理機還是虛擬機,大部分的程序代碼從開始編譯到最終轉化
使用 apktool 工具對 Android APK 進行反編譯與回編譯
原文:https://testerhome.com/topics/12075?locale=zh-TW keytool -genkey -keystore ~/bm.keystore -alias bm -keyalg RSA -validity 10000 jarsigner -v
裝置樹編譯與反彙編
轉載地址:https://blog.csdn.net/fight_onlyfor_you/article/details/74059029 1.編譯最新的核心 第一步 tar -xvf .........解壓核心 第二步 mak
luac 格式分析與反編譯
前言 測試某遊戲時,遊戲載入的是luac指令碼: 檔案格式 - 010editor官方bt只能識別luac52版本 opcode對照表 - 這個遊戲luac的opcode對照表也被重新排序,unluac需要找到lua vm的opcode對照表,才能反編譯。
在Java專案中如何反編譯class檔案及批量反編譯?
前言: 反編譯是一個對目標檔案可執行程式進行逆向分析,從而得到原始碼的過程。尤其是像Java這樣的執行在虛擬機器上的程式語言,更容易進行反編譯得到原始碼。今天介紹幾款反編譯的工具,以及如何更快的批量反編譯。 一、介紹 市面上免費的工具
android apk反編譯(反編譯—改程式碼—再編譯—簽名)
1.工具(請到網站搜尋並自行下載): ①apktool(反編譯:能得到圖片資源與佈局檔案等) ②dex2jar(反編譯:能得到activity等java程式碼) ③jd-gui(檢視dex2jar得到的java檔案) ④手機簽名工具
簽名-程式碼混淆Progurard-反編譯
簽名 路徑/檔名+.jks 簽名檔案通過 build generate signed apk 記住簽名庫的密碼 和key 的密碼 程式碼混淆 混淆程式碼 可以 減小APK體積,將類名簡單化,在反編
Java學習筆記 (八) 反編譯 switch 觀察其實現細節
看了H大的部落格之後,發現還有反編譯程式碼看實現細節的操作,學習一波,吸收營養。文末放部落格連結地址。 switch對整型支援的實現細節 原始碼,如下: public class SwitchTest{ public static void ma
【深入Java虛擬機器】之七:Javac編譯與JIT編譯
編譯過程 不論是物理機還是虛擬機器,大部分的程式程式碼從開始編譯到最終轉化成物理機的目的碼或虛擬機器能執行的指令集之前,都會按照如下圖所示的各個步驟進行: 其中綠色的模組可
Java程式碼到底是如何編譯成機器指令的。
在《Java程式碼的編譯與反編譯》中,有過關於Java語言的編譯和反編譯的介紹。我們可以通過javac命令將Java程式的原始碼編譯成Java位元組碼,即我們常說的class檔案。這是我們通常意義上理解的編譯。 但是,位元組碼並不是機器語言,要想讓機器能夠執行,還需要把
藉助apktool.jar工具,使用python程式碼簡化批量反編譯apk安裝包的簡單實現
工作需要,要對批量的apk(渠道包)安裝包進行反編譯,用來抽檢渠道包的相關渠道資訊是否正確,以前都是使用apktool一個一個的手動反編譯,然後檢視結果,覺得很是繁瑣;初學Python,也萌生了這樣一個想法,暫時還是很簡單的實現; 環境準備: 1、已配置了Python環境;
Android APK XML解析與反編譯方法
APK中的XML為何不能直接開啟,是否只是簡單的二進位制檔案,難道被加密了?為什麼AXMLPrinter2反編譯的時候竟然報錯了,如何解決? java.lang.ArrayIndexOutOfBoundsException: 128 at android.c
java javac編譯與JIT編譯
Javac的工作流程: 原始碼——詞法分析器——Token流——語法分析器——語法樹——語義分析器——註解語法樹——程式碼生成器——位元組碼 1)詞法分析 讀取原始碼,一個位元組一個位元組地讀進來,找到這些位元組中哪些是定義的語法關鍵詞,如Java中的if、else、for、while等關鍵詞,要識別哪些
Java序列化與反序列化
setname [] 進制 方式 gets 創建 保存 ati 取數據 Java序列化與反序列化是什麽?為什麽需要序列化與反序列化?如何實現Java序列化與反序列化?本文圍繞這些問題進行了探討。 1.Java序列化與反序列化 Java序列化是指把Java對象轉換為字節序