10分鐘教你如何hack掉Java編譯器

導讀

如標題所述,咱們如何才能hack掉java編譯器,也就是javac呢?爲了找到這個套路,咱們須要從通常的編譯流程,javac的編譯流程,插入式註解處理器提及,最後經過一個例子演示如何在編譯期間篡改代碼,而且介紹業界常見的應用場景。讀完該篇文章,你能夠了解到:

1.編譯器通常編譯流程html

2.javac的編譯流程是怎樣的前端

3.如何hack掉Java編譯器java

4.運行時DI和編譯期DI的區別node


1. 程序編譯執行流程
一、程序編譯執行流程

1.一、通常執行流程

通常狀況下,一個程序從編譯到執行,有如下這些階段:

1.二、編譯案例

以下,以龍書中的例子爲例,一個語句的編譯流程:
符號表: 是一種用於數據結構,源程序中的每一個標識符都和它的聲明或使用信息綁定在一塊兒,好比其數據類型、做用域以及內存地址。
在編譯程序工做過程當中,會不斷收集、記錄和使用源程序中一些語法符號的類型和特徵等相關信息,這些信息通常以表格形式存儲於系統中,如常數表、變量表、數組名錶、過程名錶、標號表等,這些統稱爲符號表。

2. Java程序編譯類型
二、Java程序編譯類型
而在Java中,有幾種編譯模式,若是用的是前端編譯+後端編譯,則把以上流程進行劃分,經常使用的組合是: javac前端編譯器+JIT後端編譯器:
而在執行過程當中,會進行混合模式執行: 部分函數會解釋執行,部分會編譯執行。

2.一、Java程序編譯執行過程

以下圖,爲Java代碼從編譯到執行的過程:
  • 在前端編譯時,把Java源文件編譯爲Class文件;git

  • 在解釋執行時,會收集運行數據,根據熱點代碼進行JIT編譯優化,生成本地機器碼,加快程序的執行。github

更多關於類加載器,系統初始化,以及加載Class文件到JVM的過程,參考以前發佈的兩篇文章:
3. javac
三、javac

3.一、javac中的主要類

3.二、javac主要處理流程

主要處理流程入口: JavaCompiler.compile()
compile2()方法中的默認編譯策略:
梳理一下以上的代碼流程,得出如下編譯流程圖:
  1. initProcessAnnotations(processors)後端

    1. 準備過程:初始化插入式註解處理器api

  2. ParseparseFiles(sourceFileObjects) 解析步驟,讀取一系列的Java源文件,把解析的Token序列結果映射到AST-Nodes(抽象語法樹各個節點):

    1. 詞法分析將字符流轉換爲標記(Token)集合(符號流);

    2. 語法分析根據token序列構造抽象語法樹,後續操做都創建在抽象語法樹上,語法分析相關類:Parser

  3. EnterenterTrees方法負責填充符號表,編譯器將在其做用域範圍內找到全部定義的符號,主要包含如下兩個階段:

    1. 第一階段:註冊全部類到其相應的做用域範圍,在這一步編譯器爲每一個類符號記錄一個MemberEnter對象,該對象將用於第二階段;

    2. 第二階段:使用上面的MemberEnter對象繼續完善類符號相關信息。主要包括:肯定類的參數,超類和接口。

  4. AnnotateprocessAnnotations():

    1. 註解處理器的執行過程。若是存在註解處理器,而且請求了註解處理,則將處理在指定的編譯單元中找到的全部註解。JSR 269定義了用於編寫此類插件的接口,後面會有詳細介紹。

  5. delegateCompiler.compile2()分析及字節碼生成

    1. Attribute語義分析過程,標註檢查,主要包括諸如變量使用前是否已被聲明、變量與賦值之間的數據類型是否可以匹配等;同時會進行常量摺疊(int a = 1+2 摺疊爲 int a =3);

    2. Flow語義分析過程,數據及控制流分析。這一步是對程序上下文邏輯更進一步的驗證,能夠檢查出諸如程序局部變量在使用前是否有賦值、方法的每條路徑是否都有返回值、是否全部的受檢驗異常都被正確處理了等問題。

      1. final類型的局部變量就是經過在這一步分析來保證不被從新賦值的;由於局部變量不像類變量,在Class文件中有CONSTANT_Fieldref_info符號引用,記錄了訪問標誌。

    3. Desugar解除語法糖(inner classes, class literals, assertions, foreach loops),重寫AST;

    4. Generate生成字節碼,同時會進行少許代碼添加和轉換工做。如:

      1. 添加實例構造器<init>()方法和類構造器<clinit>()方法;

      2. 把字符串相加操做替換爲StringBuffer或者StringBuilder(JDK 1.5+);



4. 註解處理器

四、註解處理器

咱們上一節講解了javac的主要處理流程,其中在解析成抽象語法樹以後,有一個 處理註解 流程,這個流程是經過提供一組 插入式註解處理器 的標準API(Java規範提案 JSR 269: Pluggable Annotation Processing API )在編譯期間對註解進行處理。咱們能夠把它看作是一組 編譯器的插件 ,在插件中能夠讀取,修改和添加抽象語法樹中的任意元素。

JSR269是從Java6開始提供;
在Java5 以前註解處理器還沒有成熟,註解處理器的API並非JDK標準,而是經過獨立的apt工具(Annotation Processor Tool,分發於 com.sun.mirror 包下)來編寫自定義處理器。
若是 插入式註解處理器 在處理註解期間修改了AST(抽象語法樹),編譯器將回到解析與填充符號表的過程從新處理,直到全部插入式註解處理器都沒有在修改AST爲止,每一次循環成爲一個 Round ,以下圖:
咱們也能夠本身實現JSR 269的API,自定義一個插入式註解處理器,爲javac自定義編譯行爲。

4.一、註解處理器與反射的區別

咱們能夠經過反射獲取註解,可是這隻能在運行時經過反射獲取註解,運行效率比較低; 另外反射沒法作到在編譯階段進行代碼檢查;
Java 6開始,可使用JSR 269的API編寫註解處理器。 JSR 269能夠在javac 編譯期 利用註解進行檢查和改寫語法樹的能力,與反射的 運行期 干預不一樣,大大提升了執行效率。

4.二、如何實現一個註解處理器

自定義註解處理器的接口
註解處理器實現了 javax.annotation.processing.Processor 接口,遵循給定的協定。 爲了方便實現,同時提供了 javax.annotation.processing.AbstractProcessor 類實現具備自定義處理器通用功能的抽象實現。 如下是該接口的關鍵須要實現的方法,註釋處理期間,Java編譯器將調用這兩個方法:
        
1
2
3
4
5
6
7
8
9
10
11
        
/**
*第一個方法被調用一次以初始化插件
*/
public synchronized void init(ProcessingEnvironment processingEnv)

/**
* 在每次註釋循環中被調用,在全部回合完成後再被調用一次
* @return 這些annotations註解是否由此 Processor 處理,返回ture表示該註解已經被處理, 不會再有後續其餘處理器處理進行處理; 返回false表示仍可被其餘後續處理器處理
*/
public boolean process(Set<? extends TypeElement> annotations,
RoundEnvironment roundEnv)
自定義註解處理器使用到的註解
  • javax.annotation.processing.SupportedAnnotationTypes用於註冊處理器支持的註解。有效值是註釋類型的標準名稱,容許使用通配符。

  • javax.annotation.processing.SupportedSourceVersion用於註冊處理器支持的源代碼版本。

  • javax.annotation.processing.SupportedOptions此註釋用於註冊容許經過命令行傳遞的自定義選項。


下面是一個註解處理器的例子,該例子源於:
http://scg.unibe.ch/archive/projects/Erni08b.pdf
這個例子主要是把如下格式的斷言:
        
1
        
assert cond : detail;
在編譯階段替換爲異常:
        
1
        
if (!cond) throw new AssertionError(detail);

4.2.一、寫一個註解

        
1
2
        
public @interface ForceAssertions {
}

4.2.二、寫一個註解處理器

注意,本例基於Java8,因爲該例子中使用到了sun.tools包中的類,該包中的類非Java平臺標準類,不一樣Java版本類方法有所不一樣,若是是Java6,參考源例子中的代碼。
        
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
        
/**
* 注意,此例使用到了sun.tools中的類,可能會致使不穩定.
* 開發者不該該調用sun包,Oracle一直在提醒開發者,調用sun.*包裏面的方法是危險的。
* sun包並不包含在Java平臺的標準中,它與操做系統相關,
* 在不一樣的操做系統如Solaris,Windows,Linux,Mac等中的實現也各不相同,而且可能隨着JDK版本而變化。詳細說明:
* http://www.oracle.com/technetwork/java/faq-sun-packages-142232.html
*
* Created by arthinking on 30/1/2020.
*/
@SupportedAnnotationTypes("com.itzhai.annotation.process.demo.ForceAssertions")
@SupportedSourceVersion(SourceVersion.RELEASE_8)
public class ForceAssertionsProcessor extends AbstractProcessor {

// 計數器用於向用戶報告已應用的替換次數

private int tally;

// Trees JSR269的工具類,鏈接程序元素和樹節點的橋樑。

// 例如,給定一個method元素,咱們能夠得到其關聯的AST樹節點
private Trees trees;

// TreeMaker 編譯器的內部組件,用於建立樹節點的工廠

private TreeMaker make;

// Name.Table 編譯器的一個內部組件, Name是內部編譯器字符串的抽象。

// 出於效率緣由,Javac使用存儲在公共大型緩衝區中的哈希字符串。
private Names names;

@Override

public synchronized void init(ProcessingEnvironment env) {
super.init(env);
trees = Trees.instance(env);
// 咱們使用處理環境來處理必要的編譯器組件。在編譯器內,對編譯器的每次調用都使用單個處理環境(或context上下文,內部稱爲上下文)。
// 把JSR269的ProcessingEnvironment轉換爲實際的編譯器類型JavacProcessingEnvironment,以便可以調用更多的內部方法
JavacProcessingEnvironment javacProcessingEnvironment = (JavacProcessingEnvironment)env;
// 使用context上下文來確保每一個編譯器調用都存在每一個編譯器組件的單個副本。
Context context = javacProcessingEnvironment.getContext();
// 在編譯器中,咱們僅使用 Component.instance(context) 來獲取對該階段的引用
make = TreeMaker.instance(context);
names = Names.instance(context);
// tally 計數器用於向用戶報告已應用的替換次數。
tally = 0;
}

@Override

public boolean process(Set<? extends TypeElement> annotations,
RoundEnvironment roundEnv) {
if (!roundEnv.processingOver()) {
// 遍歷全部的程序元素,而且重寫每一個類的AST
Set<? extends Element> elements = roundEnv.getRootElements();
for (Element each : elements) {
if (each.getKind() == ElementKind.CLASS) {
// 把JSR269的 Tree 轉換爲實際的JCTree類型,以即可以訪問全部的AST元素。
JCTree tree = (JCTree) trees.getTree(each);
// 經過對TreeTranslator進行子類化來完成樹翻譯,
// TreeTranslator自己是TreeVisitor的子類。
// 這些類都不是JSR269的一部分,而是Java編譯器內部的類。
TreeTranslator visitor = new Inliner();
tree.accept(visitor);
}
}
} else {
// 輸出處理的斷言語句的數量
processingEnv.getMessager().printMessage(
Diagnostic.Kind.NOTE, tally + " assertions inlined.");
}
return false;
}

/**

* Inliner類實現了AST的重寫
*/
private class Inliner extends TreeTranslator {

/**

* 爲了改變assert語句,咱們這裏重寫了 visitAssert(JCAssert tree) 方法
* @param tree
*/
@Override
public void visitAssert(JCAssert tree) {
// 必須調用超類方法,以確保將轉換也應用於節點的子代。
super.visitAssert(tree);
// 改寫邏輯在makeIfThrowException這個方法中,結果賦值給 TreeTranslator.result
result = makeIfThrowException(tree);
tally++;
}

/**

* 具體的assert語句轉換邏輯:
* assert cond : detail;
* 轉換爲:
* if (!cond) throw new AssertionError(detail);
*
* 該方法將一個斷言語句做爲參數,並返回一個if語句。
* 這是一個有效的返回值,由於兩個樹節點都是語句,所以與Java語法等效。
*
* @param node
* @return
*/
private JCStatement makeIfThrowException(JCAssert node) {
// make: if (!(condition) throw new AssertionError(detail);
// 獲取斷言的 detail
List<JCExpression> args = node.getDetail() == null
? List.<JCExpression>nil()
: List.of(node.detail);
// 建立了一個AST節點,該節點建立了「AssertionError」的新實例。
JCExpression expr = make.NewClass(
null,
null,
// 使用Name.Table獲取編譯器內部字符串表示形式
make.Ident(names.fromString("AssertionError")),
args,
null);
// 返回一個if語句
return make.If(
// 倒置 assert的條件
make.Unary(JCTree.Tag.NOT, node.cond),
// 建立一個 throw 表達式
make.Throw(expr),
null);
}
}
}

4.2.三、經過SPI註冊你的註解處理器

項目目錄以下:
注意,紅框部分的目錄要保持一致。
javax.annotation.processing.Processor 文件中填寫註解處理器,一行一個,本例子中該文件的內容爲:
        
1
        
com.itzhai.annotation.process.demo.ForceAssertionsProcessor

4.2.四、打包而且使用你的lib包

這裏以maven打包爲例,您須要使用以下的maven插件:
        
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
        
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>2.3.2</version>
<configuration>
<!-- 設置爲true以打印有關編譯器相關的日誌 -->
<verbose>true</verbose>
<!-- 容許在單獨的進程中運行編譯器。若是爲false,則使用內置編譯器;若是爲true,則使用可執行文件。
要使compilerVersion標籤生效,須要將fork設爲true,用於明確表示編譯版本配置可用
-->
<fork>true</fork>
<!-- 指定插件將使用的編譯器的版本 -->
<compilerVersion>1.8</compilerVersion>
<!-- 源代碼使用的JDK版本 -->
<source>1.8</source>
<!--<executable>${JAVA_HOME}/bin/javac</executable>-->
<!-- 須要生成的目標class文件的編譯版本 -->
<target>1.8</target>
<!-- 須要生成的目標class文件的編譯版本 -->
<encoding>utf-8</encoding>
<!--
重點!
https://stackoverflow.com/questions/38926255/maven-annotation-processing-processor-not-found
默認的,編譯器會找到Processor配置,而且執行註解處理器,但此時註解處理器還沒編譯好,因此會報錯,爲了不這種錯誤,須要作一下參數配置:
-->
<proc>none</proc>
<!-- 這個選項用來傳遞編譯器自身不包含可是卻支持的參數選項 -->
<compilerArguments>
<!-- 重點!自定義註解處理器使用到了 com.sun.tools 包中的類,因此這裏要確保引用 tools.jar-->
<classpath>${JAVA_HOME}/lib/tools.jar</classpath>
</compilerArguments>
</configuration>
</plugin>
注意以上標明 重點! 的地方,不能配錯了,不然可能致使打包失敗。
而後經過Maven打包成jar包,這樣就能夠在其餘項目中引入jar包,在代碼編譯的時候編譯器會自動查找到該註解處理器,對須要處理的類進行處理了。

4.2.五、使用案例

咱們在一個新的項目中引入上面打的註解處理器jar包:
        
1
2
3
4
5
6
7
8
        
<dependencies>
<dependency>
<groupId>com.itzhai</groupId>
<artifactId>annotation-process</artifactId>
<version>0.0.1-SNAPSHOT</version>
<scope>compile</scope>
</dependency>
</dependencies>
編寫以下代碼進行測試:
        
1
2
3
4
5
6
7
8
9
10
11
12
        
public class ForceAssertExample {

/**

* java -ea com.itzhai.annotation.process.demo.ForceAssertExample
* @param args
*/
public static void main(String[] args) {
String str = null;
assert str != null : "Must not be null";
}

}

直接編譯發現assert並無被替換掉,能夠經過 javap -v 查看對應的反彙編代碼:
緣由是少了註解處理器對應的註解 @ForceAssertions ,咱們把它加到類上面,從新編譯,發現assert已經被替換掉了:
該例子完整代碼: https://github.com/arthinking/pluggable-annotation-processor

4.三、註解處理器其餘相關應用

4.3.一、Lombok

使用 Lombok ,能夠消除POJO中冗長的get, set, hashCode, equals, 構造參數等代碼,這也是經過註解處理器來實現的。 Lombok 基於JSR 269,而且hack了javac和jdt以便可以訪問和修改類的抽象語法樹的內部實現。
如何編寫一個相似Lombok的 @Builder 功能更,能夠參考此文:
https://www.cnblogs.com/throwable/p/9139908.html

4.3.二、Dagger

Dagger 是一種快速,輕量級的依賴注入框架,該框架可用於Java和Android,該框架在編譯時注入以得到更高的行能。 Dagger是第一個實現標準 javax.inject 註解的DI框架(JSR 330)。 其底層也是經過註解處理器實現的,其核心處理類是 ComponentProcessor ,繼承了Google Auto提供的抽象註解處理框架的 BasicAnnotationProcessor 實現的。
依賴注入 控制反轉 原理的具體應用,不一樣的框架以不一樣的方式實現依賴注入,這裏咱們對比如下兩類:
  • 運行時依賴注入,一般基於反射,更易於使用,可是會致使運行時更慢,Spring就是運行時的DI框架;

  • 編譯時生成具體的代碼,這意味着全部繁重的操做都是在編譯期間執行的,編譯時DI增長了複雜性,可是一般執行的更快,Dagger就是編譯時依賴注入

4.3.三、Checker

Checker是一個經過向Java語言中添加可插入類型系統來增長Java類型系統的框架。
在定義類類型限定符以及語義和編譯器插件(註解處理器)以後,開發人員能夠在其程序中編寫類型限定符,並使用該插件檢測或者防止錯誤,例如空指針異常,SQL注入,併發錯誤等等。
下面是一個使用例子,咱們使用 @NonNull 註解代表ref必須引用到非空的對象:
        
1
2
3
4
5
6
        
import org.checkerframework.checker.nullness.qual.*;
public class Example {
void sample() {
@NonNull Object ref = null;
}
}
若是咱們執行Checker:
        
1
        
javac -processor org.checkerframework.checker.nullness.NullnessChecker Example.java
會發現提示以下錯誤:
        
1
2
3
4
5
6
        
Example.java:4: incompatible types.
found : @Nullable
required: @NonNull Object
@NonNull Object ref = null;
^
1 error
更多Checker的註解: Checker Framework Manual.

References

1. What is JIT in Java?
https://www.edureka.co/blog/just-in-time-compiler/
2. Compilation and Execution of a Java Program
https://www.geeksforgeeks.org/compilation-execution-java-program/
3. Javac編譯器詳解
https://www.cnblogs.com/blogtech/p/10000162.html
4. Compiler Theory(編譯原理)、詞法/語法/AST/中間代碼優化在Webshell檢測上的應用
https://www.cnblogs.com/LittleHann/p/4754446.html
5. 《The Dragon Book》
6. The Hacker’s Guide to Javac
http://scg.unibe.ch/archive/projects/Erni08b.pdf
7. 十分鐘搞懂Lombok使用與原理
https://www.jianshu.com/p/63038c7c515a
8. Java奇技淫巧-插件化註解處理API(Pluggable Annotation Processing API)
https://www.cnblogs.com/throwable/p/9139908.html
9. JSR 269: Pluggable Annotation Processing API
https://www.jcp.org/en/jsr/detail?id=269
10. Gwt and JSR 269’s Pluggable Annotation Processing API
https://www.slideshare.net/ltearno/gwt-and-jsr-269s-pluggable-annotation-processing-api
11. Code Generation using Annotation Processors in the Java language – part 2: Annotation Processors
https://deors.wordpress.com/2011/10/08/annotation-processors/
12. Introduction to Dagger 2
https://www.baeldung.com/dagger-2
13. Java Annotation: Dependency Injection and Beyond
https://objectcomputing.com/resources/publications/sett/java-annotation-dependency-injection-beyond

JVM系列文章


Java運行時數據區域是如何工做的


Java對象歷險記與垃圾回收機制


Class文件16進制數字背後的祕密


一篇圖文完全弄懂Class文件是如何被加載進JVM的


一篇圖文完全弄懂類加載器與雙親委派機制


深刻思考:JVM是如何進行方法調用的

·END·
 點擊「閱讀原文」查看個人博客更多文章


Java架構雜談
Java後端技術架構 · 技術專題 · 經驗分享
blog: itzhai.com

若有收穫,點個「在看」喔~

本文分享自微信公衆號 - Java架構雜談(itread)。
若有侵權,請聯繫 support@oschina.cn 刪除。
本文參與「OSC源創計劃」,歡迎正在閱讀的你也加入,一塊兒分享。

相關文章
相關標籤/搜索