Java——编译与反编译
**
一、基礎知識
**
1.1 編程語言
在介紹編譯和反編譯之前,我們先來簡單介紹下編程語言(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++等高級語言。
1.2 什么是編譯
上面提到語言有兩種,一種低級語言,一種高級語言。可以這樣簡單的理解:低級語言是計算機認識的語言、高級語言是程序員認識的語言。
那么如何從高級語言轉換成低級語言呢?這個過程其實就是編譯。
從上面的例子還可以看出,C語言的語句和低級語言的指令之間不是簡單的一一對應關系,一條a=b+1;語句要翻譯成三條匯編或機器指令,這個過程稱為編譯(Compile),由編譯器(Compiler)來完成,顯然編譯器的功能比匯編器要復雜得多。用C語言編寫的程序必須經過編譯轉成機器指令才能被計算機執行,編譯需要花一些時間,這是用高級語言編程的一個缺點,然而更多的是優點。首先,用C語言編程更容易,寫出來的代碼更緊湊,可讀性更強,出了錯也更容易改正。
將便于人編寫、閱讀、維護的高級計算機語言所寫作的源代碼程序,翻譯為計算機能解讀、運行的低階機器語言的程序的過程就是編譯。負責這一過程的處理的工具叫做編譯器
現在我們知道了什么是編譯,也知道了什么是編譯器。不同的語言都有自己的編譯器,Java語言中負責編譯的編譯器是一個命令:javac
javac是收錄于JDK中的Java語言編譯器。該工具可以將后綴名為.java的源文件編譯為后綴名為.class的可以運行于Java虛擬機的字節碼。
當我們寫完一個HelloWorld.java文件后,我們可以使用javac HelloWorld.java命令來生成HelloWorld.class文件,這個class類型的文件是JVM可以識別的文件。通常我們認為這個過程叫做Java語言的編譯。其實,class文件仍然不是機器能夠識別的語言,因為機器只能識別機器語言,還需要JVM再將這種class文件類型字節碼轉換成機器可以識別的機器語言。
1.3 什么是反編譯
反編譯的過程與編譯剛好相反,就是將已編譯好的編程語言還原到未編譯的狀態,也就是找出程序語言的源代碼。就是將機器看得懂的語言轉換成程序員可以看得懂的語言。Java語言中的反編譯一般指將class文件轉換成java文件。
有了反編譯工具,我們可以做很多事情,最主要的功能就是有了反編譯工具,我們就能讀得懂Java編譯器生成的字節碼。如果你想問讀懂字節碼有啥用,那么我可以很負責任的告訴你,好處大大的。比如我的博文幾篇典型的原理性文章,都是通過反編譯工具得到反編譯后的代碼分析得到的。如深入理解多線程(一)——Synchronized的實現原理、深度分析Java的枚舉類型—-枚舉的線程安全性及序列化問題、Java中的Switch對整型、字符型、字符串型的具體實現細節、Java的類型擦除等。我最近在GitChat寫了一篇關于Java語法糖的文章,其中大部分內容都用到反編譯工具來洞悉語法糖背后的原理。
**
二、Java的編譯與反編譯
**
2.1 Java 編譯
Java的編譯有些不同,因為Java的特性是一次編寫,到處運行。做到這種效果的主要依據就是JVM。Java的編譯是分為兩個階段的,首先,利用JDK自帶的編譯器,將源代碼經過詞法分析,語法分析直至語義分析,然后就會產生一個class文件。這段過程稱之為前端編譯,此時產生的class文件還無法被計算機識別執行,只能算是整個編譯過程中產生的一個中間產物。
然后JVM將讀取到的二進制文件進行深度編譯,將其編譯成與具體平臺相關的指令代碼,這個過程是后端編譯,它主要依賴于JVM。前端編譯是與操作系統平臺無關的,最終生成的class文件是可以在各個JVM平臺進行深度編譯;而后端編譯就需要跟具體操作系統平臺相關了,因為JVM有不同平臺的版本,可以將這種統一格式的class文件進一步深度編譯,將其轉換成與具體平臺相關的指令代碼。
對于編譯器,Java內置的有javac工具,此外很多IDE工具也內置了編譯工具,但是這些都是前端編譯器,主要功能就是把【.java】文件編程成【.class】文件。
2.1.1 詞法分析器
這個階段是將源程序文件從左到右一個字符一個字符地讀入,將字符序列轉換為標記(token)序列的過程。這里的標記是一個字符串,是構成源代碼的最小單位,該過程中,詞法分析器還會對標記進行分類。
詞法分析器通常不會關心標記之間的關系(分析關系主要在語法分析階段)。如:源程序中會有很多括號,但是詞法分析器只是將其識別為標記,但是它并不關心這些括號是否能正確匹配(即:如果只有“{”,而沒有“}”,詞法分析中不會發現問題)。
2.1.2 語法分析器
它是在詞法分析的基礎上,將單詞序列組合成各種短語,如“程序”、“語句”、“表達式”等等。語法分析能夠判斷源程序在結構上是否正確。
2.1.3 語義分析
該階段是程序編譯的一個邏輯階段。它是對結構正確的源程序進行上下文有關性質的審查。它主要是審查源程序是否含有語義錯誤,同時也為代碼的生成階段收集類型信息。
語義分析中的一個很重要的部分就是類型審查,比如很多語言要求數組下標必須為整數,如果使用浮點數作為下標,編譯器就會報錯;再比如很多程序允許某些類型之間能夠進行自動轉換等等。
經歷過上面的過程之后,就開始生成中間代碼,中間代碼具有兩個很重要的性質:易于生成、能夠輕松翻譯成目標機器上的語言。著名的解語法糖操作就是在javac中完成的。
2.1.4 后端編譯
Java在經歷過前端編譯之后,如果需要執行編譯后的class文件,需要借助于JVM,JVM會將class文件中的內容逐條翻譯成機器指令,這個解釋的過程就是JVM的解釋器(Interpreter)的功勞。很明顯,這個解釋是比較浪費時間的,為了提高這種解釋的效率,Java引入了JIT技術。
雖然引入了JIT,Java仍然使用解釋器進行代碼解釋,但是在解釋的過程中,隨著代碼的不斷執行,會識別出代碼中執行比較頻繁的代碼段,這段代碼就會被標記為“熱點代碼”。JIT就會將這段熱點代碼編譯后的機器代碼進行優化后,緩存起來,下次再執行到這段代碼,直接跳過編譯過程,使用緩存的機器碼。
HotSpot虛擬機中內置了兩種JIT編譯器:Client Compiler和Server Compiler。目前主流的方式就是采用其中一種編譯器與解釋器配合工作的方式。那為什么不將其全部編譯成熱點代碼呢?主要是出于資源最大化利用的考慮:在程序中,不可能所有代碼都是熱點代碼,真正頻繁執行的代碼只是占據很少一部分,如果將那些只執行了一遍就再也不執行的代碼也進行緩存,完全就是在浪費緩存資源。另外在將代碼轉換成熱點代碼過程中是需要經過一個編譯過程的,如果這種只執行一次的代碼也要編譯,其實也是浪費時間。
熱點檢測
要想觸發JIT編譯,就必須滿足熱點代碼的檢測,目前主要有兩種熱點代碼探測的方式:
HotSpot虛擬機采用的就是上面第二種探測方式,準備了兩個計數器:方法計數器和回邊計數器。它們分別對應方法調用次數統計和代碼循環執行次數統計。
編譯優化
其實JIT除了具有緩存的功能,還會對代碼做各種優化,例如:逃逸分析、鎖消除、鎖膨脹、方法內聯、空值檢查消除、類型檢測消除、公共子表達式消除等等。可以搜索相關概念介紹,了解其原理,這里暫時不再贅述。
2.2 Java 反編譯
2.2.1 Java反編譯工具
常見的Java的反編譯工具:javap、jad、jd-gui和cfr
其中,jd-gui提供了UI界面,使用起來很方便
2.2.2 如何防止反編譯
由于我們有工具可以對Class文件進行反編譯,所以,對開發人員來說,如何保護Java程序就變成了一個非常重要的挑戰。但是,魔高一尺、道高一丈。當然有對應的技術可以應對反編譯咯。但是,這里還是要說明一點,和網絡安全的防護一樣,無論做出多少努力,其實都只是提高攻擊者的成本而已。無法徹底防治。
典型的應對策略有以下幾種:
總結
以上是生活随笔為你收集整理的Java——编译与反编译的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 吃苹果可以减肥吗
- 下一篇: 设计模式——创建型模型