記錄學習編譯與反編譯知識,並且使用cfr反編譯工具,深入了解java常用語法糖
一.編程語言
二.編譯
1.編譯過程
2.JIT hotspot
三.反編譯
四:如何防止反編譯
五.反編譯實踐
1.switch
2.String "+"
3.lambda
4.枚舉
5.自動拆裝箱
6.try-with-resource
7.條件編譯
8.foreach
9.變長參數
一.編程語言
編程語言一般分為低級編程語言與高級編程語言.
(1 低級語言:直接使用計算機指令編寫程序,匯編語言與機器語言就屬於這一種.
(2 高級語言 :使用語句(statement)編寫程序,語句是計算機指令的抽象表示.
簡單的理解:低級語言是計算機認識的語言,高級語言是程序員認識的語言.
而將高級語言轉換為低級語言的過程稱為編譯.
二.編譯
將便於人編寫、閱讀、維護的高級計算機語言所寫作的源代碼程序,翻譯為計算機能解讀、運行的低階機器語言的程序的過程就是編譯。負責這一過程的處理的工具叫做編譯器.
java中的編譯器:javac是jdk中的JAVA語言編譯器,使用javac命令可以將以.java結尾的源文件編譯成以.class結尾的能夠由jvm識別的字節碼.
但是如果要想由計算機來執行程序,jvm必須再將字節碼轉換成機器碼,也被稱為深層次的編譯.
1.編譯過程
根據完成任務不同,可以將編譯器的組成部分划分為前端(Front End)與后端(Back End)。
(1)前端編譯主要指與源語言有關但與目標機無關的部分,包括詞法分析、語法分析、語義分析與中間代碼生成,即將.java編譯成.class 的過程。
詞法分析
詞法分析階段是編譯過程的第一個階段。即從左到右一個字符一個字符地讀入源程序,將字符序列轉換為標記(token)序列的過程。標記是一個字符串,是構成源代碼的最小單位。在這個過程中,還會對標記進行分類。
詞法分析器通常不會關心標記之間的關系(屬於語法分析的范疇),舉例來說:詞法分析器能夠將括號識別為標記,但並不保證括號是否匹配。
語法分析
語法分析的任務是在詞法分析的基礎上將單詞序列組合成各類語法短語,如“程序”,“語句”,“表達式”等等.語法分析程序判斷源程序在結構上是否正確.源程序的結構由上下文無關文法描述。
語義分析
語義分析是編譯過程的一個邏輯階段, 語義分析的任務是對結構上正確的源程序進行上下文有關性質的審查,進行類型審查。語義分析是審查源程序有無語義錯誤,為代碼生成階段收集類型信息。
語義分析的一個重要部分就是類型檢查。比如很多語言要求數組下標必須為整數,如果使用浮點數作為下標,編譯器就必須報錯。再比如,很多語言允許某些類型轉換,稱為自動類型轉換。
中間代碼生成
在源程序的語法分析和語義分析完成之后,很多編譯器生成一個明確的低級的或類機器語言的中間表示。該中間表示有兩個重要的性質: 1.易於生成; 2.能夠輕松地翻譯為目標機器上的語言。
(2)后端編譯主要指與目標機有關的部分,包括代碼優化和目標代碼生成等,即 將.class 文件翻譯成機器碼的過程 。
主要由jvm解釋器來進行執行,通過解釋java字節碼,將其翻譯成對應的機器碼指令,逐條讀入,逐條解釋翻譯.但在效率上會比可執行的二進制字節碼程序慢許多.為了解決效率問題,java特別引入JIT技術.
(3)
編譯解釋:
當程序需要迅速啟動和執行的時候,解釋器可以首先發揮作用,省去編譯的時間,立即執行。在程序運行后,隨着時間的推移,編譯器逐漸發揮作用,把越來越多的代碼編譯成本地代碼之后,可以獲取更高的執行效率。當程序運行環境中內存資源限制較大(如部分嵌入式系統中),可以使用解釋器執行節約內存,反之可以使用編譯執行來提升效率。此外,如果編譯后出現“罕見陷阱”,可以通過逆優化退回到解釋執行。
2.JIT hotspot
時間開銷:一般所說的JIT比解釋快,說的是上述提到的"執行編譯后的代碼比解釋器解釋執行快".實際上,編譯過程比直接解釋執行多了一步編譯的步驟.因此對於執行一次的代碼,解釋執行總是比編譯快的多.因此只有對多次執行的代碼進行即時編譯,才有正面收益.
空間開銷:編譯后的中間代碼比較字節碼的大小,膨脹比時常達到10x.從空間開銷來講,只有對多次執行的代碼,才不會因為編譯所有代碼而造成"代碼爆炸".
原理:當jvm發現某個方法或代碼塊運行十分頻繁,便認為這段代碼是 "熱點代碼".JIT便將熱點代碼進行優化后,直接編譯成本地機器相關的機器碼緩存起來.
HotSpot虛擬機中內置了兩個JIT編譯器:Client Complier和Server Complier,分別用在客戶端和服務端,目前主流的HotSpot虛擬機中默認是采用解釋器與其中一個編譯器直接配合的方式工作,即查看java版本時看到 虛擬機標注的 mixed mode。
熱點檢測
目前主要的熱點代碼識別方式是熱點探測(Hot Spot Detection),有以下兩種:
(1)基於采樣的方式探測(Sample Based Hot Spot Detection) :周期性檢測各個線程的棧頂,發現某個方法經常出現在棧頂,就認為是熱點方法。好處就是簡單,缺點就是無法精確確認一個方法的熱度。容易受線程阻塞或別的原因干擾熱點探測。
(2)基於計數器的熱點探測(Counter Based Hot Spot Detection)。采用這種方法的虛擬機會為每個方法,甚至是代碼塊建立計數器,統計方法的執行次數,某個方法超過閥值就認為是熱點方法,觸發JIT編譯。
(3)在HotSpot虛擬機中使用的是第二種——基於計數器的熱點探測方法,因此它為每個方法准備了兩個計數器:方法調用計數器和回邊計數器,分別用於方法與循環代碼塊.
方法計數器:記錄一個方法被調用次數的計數器。
- 默認閾值,在client模式下為1500,server下是10000,可通過"-XX:CompilerThreshold"參數調節
- 每次調用方法,判斷是否存在本地機器碼.若存在,直接執行本地機器碼.若不存在,則將方法計數器+1,並判斷"方法計數器和回邊計數器之和"是否超過閾值.是則執行棧上替換的相應步驟.
- 熱度是會衰減的.一段時間內沒有被調用,計數器減半,一般發生在GC的時候.
回邊計數器:記錄方法中的for或者while的運行次數的計數器;執行到"}"進行計數.
- 默認閾值:Client下13995,Server下10700
- 調用邏輯與方法計數器相似,但沒有熱度衰減.
即時編譯:
一旦判定代碼段是熱點代碼,則解釋器將發送一次請求編譯器,進行編譯,在編譯成功之前解釋器仍舊運行着。等編譯完成后,直接將pc寄存器中方法的調用地址進行替換,替換為編譯后的方法地址。
這一過程就是 棧上替換---OSR.
三.反編譯
即將已編譯的編程語言還原到未編譯的狀態.再java中,就是將.class文件 轉換成.java文件.
反編譯工具:javap,jad,cfr,具體可以查看參考資料
1.javap:javap是jdk自帶的一個工具,可以對代碼反編譯,也可以查看java編譯器生成的字節碼。但並沒把.class文件反編譯成源文件,而是一種相對易於理解的字節碼文件,在一定程度上,與匯編語言類似.
2.jad:一種比較不錯的反編譯工具,支持用戶界面操作.但已經很久沒有更新,在對java7的字節碼操作時偶爾會出現不支持的情況.在對java8的lambda表達式反編譯時徹底失敗.
3.crf:這種反編譯工具的語法相對復雜很多,具備許多可選的參數設置.例如是否開啟對一些java語法糖的細節解析.
四:如何防止反編譯
- 隔離Java程序
- 讓用戶接觸不到你的Class文件
- 對Class文件進行加密
- 提到破解難度
- 代碼混淆
- 將代碼轉換成功能上等價,但是難於閱讀和理解的形式
五.反編譯實踐
一直對java的語法糖比較感興趣,這邊會使用cfr反編譯工具,對java常見的一些語法糖代碼進行反編譯,希望能對其有更深的認識.
1.switch
Java 7中,switch的參數可以是String類型了,這是一個很方便的改進。到目前為止switch支持這樣幾種數據類型:byte short int char String 。
(1)String
String s = "hello";
switch (s){ case "hello": System.out.println("hello"); break; case "world": System.out.println("world");
break; }

底層依舊是通過int實現選擇,通過對hashcode進行比較,為避免hashcode碰撞,加一層equals的判斷.根據結果的int值,執行原先封裝在case下的語句.
(2)byte short char
如果在switch中使用上述的三種類型,反編譯的結果都會顯示底層依然使用int類型來實現case的選擇.byte/short轉換為int類型,char根據ASCII碼轉化成對應的int值.
byte b = 1; switch (b) { case 1: System.out.println(1); break; case 2: System.out.println(2); break; } short t = 3; switch (t) { case 3: System.out.println(3); break; case 4: System.out.println(4); break; } char c = 'a'; switch (c) { case 'a': System.out.println('a'); break; case 'b': System.out.println('b'); break; } }

(3)enum
Test test = Test.TEST1;
switch (test) { case TEST1: System.out.println("test1"); break; case TEST2: System.out.println("test2"); break; }

實際上利用枚舉的ordinal屬性在編譯期內構建了一個局部變量數組,在switch比較時,根據ordinal()獲取的值作為數組的下標,獲取一個代表着該對象在enum類中位置的int值,從而實現switch..case分支選擇.1,2代表着枚舉實例聲明的順序
所以雖然是switch支持了上述的多種類型,但實際上,switch只支持int類型
2.String "+"
public void StringAdd(){ String s = "hello"; System.out.println( " world" + " hh" + "asdasdad"); System.out.println(s + " world" + " hh" + "asdasdad"); System.out.println(s + " world" + " hh" + "asdasdad" + s); }

只有對string類型變量使用"+"進行字符串拼接時,才會去創建一個StringBuilder對象使用append方法拼接字符串.如果直接使用字符串字面量連拼接時,會在編譯期內將字符串直接編譯成一個結果字符串.
3.lambda
源代碼如下:
public void test() { List<Integer> list = new ArrayList<>(16); list.add(1); list.add(2); list.add(3); list.forEach(i -> { System.out.println(i); }); list.forEach(i -> { System.out.println(i + num); }); }
編譯結果如下:
實質就是編譯器會根據lambda表達式生成一個私有的 命名格式為lambda$方法名$序號的函數,根據是否調用類成員變量決定是否為靜態函數.
但是創建之后如何調用呢?
實際上由上面這段代碼為lambda表達式創建了內部類,在所創建的內部類中的方法實際調用了生成的lambda函數.
限於經驗有限,講的不是很直觀,而且更深入的一些分析也不是很理解.可以點擊第一個參考資料,對lambda有更直觀的認識.
4.枚舉
實質上創建的枚舉類繼承自Enum類,並且是final類型.在其中聲明的實例是public static final類型,並且在聲明一個存放實例的數組,在靜態代碼塊中將創建的實例放入.此外創建了兩個方法對數組進行獲取數組或獲取實例的操作.
public enum EnumTest { TEST1(1),TEST2(2); int i = 0; EnumTest(int i){ this.i = i; } }
5.自動拆裝箱
裝箱:實際調用包裝器的valueOf()方法;拆箱:實際使用包裝器的xxxValue()方法.
Integer integer = 10; int i = integer; 編譯結果如下: Integer integer = Integer.valueOf((int)10); int i = integer.intValue();
6.try-with-resource
自1.7后,運行通過在try中創建流來達到自動關閉流的作用.
File file = new File("out.xlsx"); try(BufferedReader br = new BufferedReader(new FileReader(file))){ String line; while ((line = br.readLine()) != null) { System.out.println(line); } } catch (IOException e) { // handle exception } 編譯結果如下: File file = new File((String)"out.xlsx"); try { BufferedReader br = new BufferedReader((Reader)new FileReader((File)file)); Throwable throwable = null; try { String line; while ((line = br.readLine()) != null) { System.out.println((String)line); } } catch (Throwable line) { throwable = line; throw line; } finally { if (br != null) { if (throwable != null) { try { br.close(); } catch (Throwable line) { throwable.addSuppressed((Throwable)line); } } else { br.close(); } } } } catch (IOException br) { // empty catch block }
實際上,會安全的判斷流是否為空,如不為空則嘗試關閉流,並且安全處理異常.
在1.9以后,允許在try之外創建流的對象,只需在try()聲明流的變量,就可以保證安全的關閉流.但前提是變量必須是final修飾或者等同於final的.即成員變量必須是final,或者局部變量(等同於final)
File file = new File("out.xlsx"); BufferedReader br = new BufferedReader(new FileReader(file));; try(br){ String line; while ((line = br.readLine()) != null) { System.out.println(line); } } catch (IOException e) { // handle exception }
7.條件編譯
1 public static void main(String[] args) { 2 if (true) { 3 System.out.println("true"); 4 } else { 5 System.out.println("false"); 6 } 7 } 8 編譯結果如下: 9 public static void main(String[] args) { 10 boolean flag = true; 11 System.out.println((String)"true"); 12 }
Java語法的條件編譯,是通過判斷條件為常量的if語句實現的。其原理也是Java語言的語法糖。根據if判斷條件的真假,編譯器直接把分支為false的代碼塊消除。通過該方式實現的條件編譯,必須在方法體內實現,而無法在正整個Java類的結構或者類的屬性上進行條件編譯.
8.foreach
實際上對數組就是使用 普通的for循環,而對於集合則使用迭代器來進行遍歷
public static void main(String[] args) { int[] ints = {1,2,3,4}; List<Integer> list = Arrays.asList(1,2,3,4); for (int i : ints){ System.out.println(i); } for (int i :list) { System.out.println(i); } }
編譯結果如下:
public static void main(String[] args) { int[] ints = new int[]{1, 2, 3, 4}; List<Integer> list = Arrays.asList(Integer.valueOf((int)1), Integer.valueOf((int)2), Integer.valueOf((int)3), Integer.valueOf((int)4)); Object object = ints; int n = object.length; for (int i = 0; i < n; ++i) { int i2 = object[i]; System.out.println((int)i2); } object = list.iterator(); while (object.hasNext()) { int i = ((Integer)object.next()).intValue(); System.out.println((int)i); }
}
9.變長參數
public static void main(String[] args) { get(1,2,3,4,5); } public static void get(int... var) { for(int i : var){ System.out.println(i); } } 反編譯結果: public static void main(String[] args) { VarLength.get((int[])new int[]{1, 2, 3, 4, 5}); } public static /* varargs */ void get(int ... var) { int[] arrn = var; int n = arrn.length; for (int i = 0; i < n; ++i) { int i2 = arrn[i]; System.out.println((int)i2); } }
在方法參數聲明的地方,實際聲明的是一個數組參數.而在傳入參數時,會新建一個與參數個數等長的數組,並將所有參數放入后傳給方法.
參考資料:
