10分钟教你如何hack掉Java编译器
Posted Java架构杂谈
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了10分钟教你如何hack掉Java编译器相关的知识,希望对你有一定的参考价值。
导读
如标题所述,我们如何才能hack掉java编译器,也就是javac呢?为了找到这个套路,我们需要从一般的编译流程,javac的编译流程,插入式注解处理器说起,最后通过一个例子演示如何在编译期间篡改代码,并且介绍业界常见的应用场景。读完该篇文章,你可以了解到:
1.编译器一般编译流程
2.javac的编译流程是怎样的
3.如何hack掉Java编译器
4.运行时DI和编译器DI的区别
1、程序编译执行流程
1.1、一般执行流程
一般情况下,一个程序从编译到执行,有以下这些阶段:
1.2、编译案例
如下,以龙书中的例子为例,一个语句的编译流程:
在编译程序工作过程中,会不断收集、记录和使用源程序中一些语法符号的类型和特征等相关信息,这些信息一般以表格形式存储于系统中,如常数表、变量表、数组名表、过程名表、标号表等,这些统称为符号表。
2、Java程序编译类型
而在Java中,有几种编译模式,如果用的是前端编译+后端编译,则把以上流程进行划分,常用的组合是:javac前端编译器+JIT后端编译器:
而在执行过程中,会进行混合模式执行:部分函数会解释执行,部分会编译执行。
2.1、Java程序编译执行过程
如下图,为Java代码从编译到执行的过程:
在前端编译时,把Java源文件编译为Class文件;
在解释执行时,会收集运行数据,根据热点代码进行JIT编译优化,生成本地机器码,加快程序的执行。
更多关于类加载器,系统初始化,以及加载Class文件到JVM的过程,参考之前发布的两篇文章:
关于类加载器以及系统启动执行流程:
具体的加载Class文件到JVM的流程:
3.1、javac中的主要类
3.2、javac主要处理流程
主要处理流程入口:JavaCompiler.compile()
compile2()方法中的默认编译策略:
梳理一下以上的代码流程,如下图所示:
initProcessAnnotations(processors)
:
准备过程:初始化插入式注解处理器
Parse
:parseFiles(sourceFileObjects) 解析步骤,读取一系列的Java源文件,把解析的Token序列结果映射到AST-Nodes(抽象语法树各个节点):词法分析
:将字符流转换为标记(Token
)集合(符号流);语法分析
:根据token序列构造抽象语法树,后续操作都建立在语法树上,语法分析相关类:Parser
;Enter
:enterTrees 填充符号表,编译器将在其作用域范围内找到所有定义的符号,主要包含以下两个阶段:第一阶段:注册所有类到其相应的作用域范围,在这一步编译器为每个类符号记录一个MemberEnter对象,该对象将用于第二阶段;
第二阶段:使用上面的MemberEnter对象继续完善类符号相关信息。主要包括:确定类的参数,超类和接口。
Annotate
:processAnnotations():注解处理器的执行过程。如果存在注解处理器,并且请求了注解处理,则将处理在指定的编译单元中找到的所有注解。JSR 269定义了用于编写此类插件的接口,后面会有详细介绍。
delegateCompiler.compile2()
:分析及字节码生成添加实例构造器
<init>()
方法和类构造器<clinit>()
方法;把字符串相加操作替换为StringBuffer或者StringBuilder(JDK 1.5+);
-
final类型的局部变量就是通过在这一步分析来保证不被重新赋值的;因为局部变量不像类变量,在Class文件中有CONSTANT_Fieldref_info符号引用,记录了访问标志。
Attribute
:语义分析过程,标注检查,主要包括诸如变量使用前是否已被声明、变量与赋值之间的数据类型是否能够匹配等;同时会进行常量折叠(int a = 1+2 折叠为 int a =3);Flow
:语义分析过程,数据及控制流分析。这一步是对程序上下文逻辑更进一步的验证,可以检查出诸如程序局部变量在使用前是否有赋值、方法的每条路径是否都有返回值、是否所有的受检验异常都被正确处理了等问题。Desugar
:解除语法糖(inner classes, class literals, assertions, foreach loops),重写AST;Generage
:生成字节码,同时会进行少量代码添加和转换工作。如:
4、注解处理器
处理注解
流程,这个流程是通过提供一组
插入式注解处理器
的标准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.1、注解处理器与反射的区别
我们可以通过反射获取注解,但是这只能在运行时通过反射获取注解,运行效率比较低;另外反射无法做到在编译阶段进行代码检查;
Java 6开始,可以使用JSR 269的API编写注解处理器。JSR 269可以在javac编译期
利用注解进行检查和改写语法树的能力,与反射的运行期
干预不同,大大提高了执行效率。
4.2、如何实现一个注解处理器
自定义注解处理器的接口
注解处理器实现了javax.annotation.processing.Processor
接口,遵循给定的协定。为了方便实现,同时提供了javax.annotation.processing.AbstractProcessor
类实现具有自定义处理器通用功能的抽象实现。以下是该接口的关键需要实现的方法,注释处理期间,Java编译器将调用这两个方法:
1 |
/** |
自定义注解处理器使用到的注解
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、写一个注解
1 |
public @interface ForceAssertions { |
4.2.2、写一个注解处理器
注意,本例基于Java8,由于该例子中使用到了sun.tools包中的类,该包中的类非Java平台标准类,不同Java版本类方法有所不同,如果是Java6,参考源例子中的代码。
1 |
/** |
4.2.3、通过SPI注册你的注解处理器
项目目录如下:
注意,红框部分的目录要保持一致。
javax.annotation.processing.Processor
文件中填写注解处理器,一行一个,本例子中该文件的内容为:
1 |
com.itzhai.annotation.process.demo.ForceAssertionsProcessor |
4.2.4、打包并且使用你的lib包
这里以maven打包为例,您需要使用如下的maven插件:
1 |
<plugin> |
注意以上标明重点!
的地方,不能配错了,否则可能导致打包失败。
然后通过Maven打包成jar包,这样就可以在其他项目中引入jar包,在代码编译的时候编译器会自动查找到该注解处理器,对需要处理的类进行处理了。
4.2.5、使用案例
我们在一个新的项目中引入上面打的注解处理器jar包:
1 |
<dependencies> |
编写如下代码进行测试:
1 |
public class ForceAssertExample { |
直接编译发现assert并没有被替换掉,可以通过javap -v
查看对应的反汇编代码:
原因是少了注解处理器对应的注解@ForceAssertions
,我们把它加到类上面,重新编译,发现assert已经被替换掉了:
该例子完整代码:https://github.com/arthinking/pluggable-annotation-processor
4.3、注解处理器其他相关应用
4.3.1、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.2、Dagger
Dagger
是一种快速,轻量级的依赖注入框架,该框架可用于Java和android,该框架在编译时注入以获得更高的行能。Dagger是第一个实现标准javax.inject
注解的DI框架(JSR 330)。其底层也是通过注解处理器实现的,期核心处理类是ComponentProcessor
,继承了Google Auto提供的抽象注解处理框架的BasicAnnotationProcessor
实现的。
依赖注入
是控制反转
原理的具体应用,不同的框架以不同的方式实现依赖注入,这里我们对比以下两类:
运行时
依赖注入
,通常基于反射,更易于使用,但是会导致运行时更慢,Spring就是运行时的DI框架;编译时生成具体的代码,这意味着所有繁重的操作都是在编译期间执行的,编译时DI增加了复杂性,但是通常执行的更快,Dagger就是编译时
依赖注入
。
4.3.3、Checker
Checker是一个通过向Java语言中添加可插入类型系统来增加Java类型系统的框架。
在定义类类型限定符以及语义和编译器插件(注解处理器)之后,开发人员可以在其程序中编写类型限定符,并使用该插件检测或者防止错误,例如空指针异常,SQL注入,并发错误等等。
下面是一个使用例子,我们使用@NonNull
注解表明ref必须引用到非空的对象:
1 |
import org.checkerframework.checker.nullness.qual.*; |
如果我们执行Checker:
1 |
javac -processor org.checkerframework.checker.nullness.NullnessChecker Example.java |
会发现提示如下错误:
1 |
Example.java:4: incompatible types. |
更多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
·END·
点击「阅读原文」查看我的博客更多文章
以上是关于10分钟教你如何hack掉Java编译器的主要内容,如果未能解决你的问题,请参考以下文章
[教你做小游戏] 《五子棋》怎么判断输赢?你能5分钟交出代码吗?
SpringBoot 如何优雅读取配置文件?10分钟教你搞定