注:本文所述的编译与反编译知识点绝大部分基于Java语言。
编译
什么是编译
利用编译程序从源语言编写的源程序产生目标程序的过程。编译就是把高级语言变成计算机可识别的2进制语言。
编译程序把一个源程序翻译成目标程序的工作过程分为五个阶段:词法分析;语法分析;语义检查和中间代码生成;代码优化;目标代码生成。主要是进行词法分析和语法分析,又称为源程序分析,分析过程中发现有语法错误,给出提示信息。
编译方式
编译方式,即编译分类,一种分类是静态编译和动态编译。
静态编译:Static Compilation,事前编译(ahead-of-time compilation,AOT)。在编译时把所有的模块都编译进目标程序(如exe)里去,当启动这个目标程序时所有模块都加载进来。
静态编译就是编译器在编译可执行文件时,将可执行文件需要调用的对应动态链接库(.so或.lib)中的部分提取出来,链接到可执行文件中去,使可执行文件在运行时不依赖于动态链接库。其优缺点与动态编译的可执行文件正好互补。
动态编译:Dynamic Compilation,在运行时进行编译。在编译时,所需的模块没有都编译进去,一般情况下可把那些模块都编译成dll,这样启动程序(初始化)时这些模块不会被加载,而是在运行时,用到哪个模块就调用哪个模块。动态编译的可执行文件需要附带一个动态链接库。在执行时,需要调用其对应动态链接库中的命令。
优点:
- 缩小执行文件本身体积;
- 加快编译速度,节省系统资源。
缺点:
- 是哪怕是很简单的程序,只用到链接库中的一两条命令,也需要附带一个相对庞大的链接库;
- 如果其他计算机上没有安装对应的运行库,则用动态编译的可执行文件就不能运行。
编译层次
在JVM中,字节码(Bytecode)的编译层次是指JVM如何将字节码优化为机器代码的不同阶段。这一过程由JIT(Just-In-Time)编译器负责,主要分为两种编译器:C1(Client Compiler)和C2(Server Compiler)。此外,Profiling(性能监控)是整个编译优化流程的重要组成部分。
Java的编译层次是分阶段优化字节码的一种策略。为了在性能和启动时间之间取得平衡,HotSpot JVM定义以下编译层次:
- 字节码解释执行:初始阶段,字节码由解释器逐条解释执行,适合短生命周期代码。性能较低,但启动速度快;
- C1编译:主要用于客户端应用程序(如GUI),关注启动时间和占用内存。编译过程中,C1会对代码进行轻量级优化。可选择是否进行Profiling,帮助JVM收集运行时性能数据;
- C2编译:主要用于服务器端应用程序(如Web服务),关注吞吐量和长期性能。执行更复杂的优化,包括循环展开、方法内联等。需要依赖Profiling数据,以便在热点代码上进行深度优化;
- 分层编译:Tiered Compilation,HotSpot JVM从JDK7开始的默认模式,结合C1和C2的优势,初始阶段由解释器和C1提供快速启动,后续阶段由C1收集Profiling数据,提供给C2用于深度优化。
层次 | 执行方式 | 特点 |
---|---|---|
解释执行 | 逐条解释字节码 | 启动快,性能低 |
C1编译 | 轻量编译,适合短生命周期代码 | 可选择带Profiling,优化简单,提升启动性能 |
C2编译 | 高级优化,适合长期运行代码 | 依赖Profiling数据,优化复杂,提升长期性能 |
Tiered Compilation | C1和C2协同工作 | 启动快,长期性能高 |
Profiling是指JVM在代码运行时,收集有关代码执行行为的数据,以指导后续优化。C1和C2编译器可以带或不带Profiling数据工作:
C1编译
- 带Profiling:JVM启用
-XX:+TieredCompilation
时,C1会同时进行编译和Profiling。收集方法调用频率、分支跳转路径、循环执行次数等数据,供C2编译优化使用。 - 不带Profiling:C1直接生成简单的本地代码,不做复杂优化,适合对启动时间敏感的场景。
C2编译
- 带Profiling:基于C1收集的Profiling数据,C2进行高层次优化,生成性能极高的机器代码。Profiling数据指导C2如何优化,例如方法内联、循环展开。
- 不带Profiling:C2编译器也可以单独工作,但由于缺乏运行时数据,优化效果不如带Profiling的方式。
参数设置:
-XX:+TieredCompilation
:默认,启用分层编译;-XX:-TieredCompilation
:禁用分层编译,仅使用C2;-Xint
:完全禁用JIT,仅解释执行
Java编译
Java程序代码需要编译后才能在虚拟机中运行,编译涉及到:编译原理、语言规范、虚拟机规范、本地机器码优化等。
Java编译时间
指虚拟机的JIT编译器编译热点代码的时间。Java源代码编译出的class文件中存储的是字节码,虚拟机通过解释方式执行字节码文件,比起C、C++的本地二进制代码速度慢很多,JVM内置两个运行时编译器,如果一个Java方法,被调用次数达到一定程度,就会被判定为热代码,交给JIT编译器编译为本地代码,提高运行速度。C、C++是静态编译,而Java大多是动态编译。
Java有三种编译方式:前端编译、JIT编译(即时编译,just-in-time compile)、AOT编译(静态提前编译)
JIT编译狭义来说是当某段代码即将第一次被执行时进行编译,因而叫即时编译。JIT编译是动态编译的一种特例。JIT编译一词后来被泛化,时常与动态编译等价;但要注意广义与狭义的JIT编译所指的区别。
自适应动态编译(adaptive dynamic compilation)也是一种动态编译,但它通常执行的时机比JIT编译迟,先让程序以某种形式先运行起来,收集一些信息之后再做动态编译。这样的编译可以更加优化。
前端编译
把Java源码文件编译成Class文件的过程;也即把满足Java语言规范的程序转化为满足JVM规范所要求格式的文件。
优点:
- 这阶段的优化是指程序编码方面的;
- 许多Java语法新特性(泛型、内部类等语法糖),是靠前端编译器实现的,而不是依赖虚拟机;
- 编译后的Class文件可以直接给JVM解释器解释执行,省去编译时间,加快启动速度;
缺点:
- 对代码运行效率几乎没有任何优化措施;
- 解释执行效率较低;
- 前端编译器:Oracle javac、Eclipse JDT中的增量式编译器(ECJ)等;
JIT编译
后端编译,JVM内置,在运行时把Class字节码文件编译成本地机器码的过程。
JIT优化技术:
- 内联:可避免方法跳跃;
- 垃圾代码:死代码,当某些对象存在于字节码中且不被使用时,编译器可以决定从机器代码中删除它们;
- 循环优化:编译器可以组织并优化循环执行顺序或对尾递归优化成for循环等,以此来优化CPU所执行的代码;
- 用实现方法替换接口方法:当给定接口的一个方法有且仅由一个对象实现时,编译器可以决定直接使用实现的方法,以避免在运行时绑定真正实现的方法所引起的开销。
优点:
- 通过在运行时收集监控信息,把热点代码(Hot Spot Code)编译成与本地平台相关的机器码,并进行各种层次的优化;
- 可以大大提高执行效率;
缺点:
- 收集监控信息影响程序运行;
- 编译过程占用程序运行时间(如使得启动速度变慢);
- 编译机器码占用内存;
- JIT编译器:HotSpot虚拟机的C1、C2编译器等;
JIT编译速度及编译结果的优劣,衡量JVM性能的重要指标;所以对程序运行性能优化集中到这个阶段;可在这个阶段进行JVM调优。
AOT编译
程序运行前,直接把Java源码文件编译成本地机器码的过程;
优点:
- 编译不占用运行时间,可以做一些较耗时的优化,并可加快程序启动;
- 把编译的本地机器码保存磁盘,不占用内存,并可多次使用;
缺点:
- 因为Java语言的动态性(如反射)带来额外的复杂性,影响静态编译代码的质量;
- 一般静态编译不如JIT编译的质量,这种方式用得比较少;
- AOT编译器:JAOTC、GCJ、Excelsior JET、ART(Android Runtime)等;
组合
目前Java体系中主要还是采用前端编译+JIT编译的方式,如HotSpot虚拟机,其运作过程大体如下:
- 首先通过前端编译把符合Java语言规范的程序代码转化为满足JVM规范所要求Class格式;
- 然后程序启动时Class格式文件发挥作用,解释执行,省去编译时间,加快启动速度;
- 针对Class解释执行效率低的问题,在运行中收集性能监控信息,得知热点代码;
- JIT逐渐发挥作用,把越来越多的热点代码编译优化成本地代码,提高执行效率;
编译工具
- javac
- JitWatch
查看class文件
javac编译.java
文件到.class
文件:javac Hello.java
javap查看class文件:javap -c Hello.class
反编译
反编译,即计算机软件反向工程(Reverse Engineering),计算机软件还原工程,是指通过对他人软件的目标程序(可执行程序)进行逆向分析、研究工作,以推导出他人的软件产品所使用的思路、原理、结构、算法、处理过程、运行方法等设计要素,某些特定情况下可能推导出源代码。反编译作为自己开发软件时的参考,或者直接用于自己的软件产品中。
class文件打破C或者C++等语言所遵循的传统,使用这些传统语言写的程序通常首先被编译,然后被连接成单独的、专门支持特定硬件平台和操作系统的二进制文件。通常情况下,一个平台上的二进制可执行文件不能在其他平台上工作。
Java class文件是可运行在任何支持JVM的硬件平台和操作系统上的二进制文件。
Java最突出的跨平台优势使得它不能被编译成本地代码,而要以中间代码的形式运行在VM环境中,这使得Java的反编译要比别的高级语言容易实现,且反编译的代码经过优化后几乎可与源代码相媲美。
Java反编译,就是通过*.class
文件得到其.java
文件。
什么时候需要反编译
1、只有一个类的class文件(或者是jar、war包),但是看不懂class文件或者说看起来效率非常低下,此时可考虑反编译。
2、想知道Java一些语法糖实现细节,可借助反编译。
反编译工具
又名反编译器,解码器,将目标程序码反转成源代码。两者之间的转换不是一一对应的:两段完全不同的Java程序也可能生成完全相同的字节码,有时需要一些试探才能更加接近源码。
工具:
- javap:JDK自带。
- Jad
- JODE
- Java Decompiler
- Fernflower decompiler:IDEA使用的反编译器。
JAD官网,一个命令行工具,没有图形界面,上述的这些工具大多是在JAD内核的基础之上加一个图形界面(比如jd-GUI)。JAD是使用MS Visual C++开发的,运行速度非常快,可处理很复杂的Java编译文件。JAD提供很多参数用于灵活应付多种加密手段,使得反编译得到代码更加优化和易读:
-d
:用于指定输出文件的目录;-s
:输出文件扩展名(默认为: .jad),通常都会把输出文件扩展名直接指定为.java,以方便修改的重新编译;-8
:将Unicode字符转换为ANSI字符串,如果输出字符串是中文的话一定要加上这个参数才能正确显示。
命令:Jad –d c:\\java –s .java -8 test.class
将当前目录下的test.class
反编译为test.java
并保存在c:\\java
目录里,其中的提示输出为中文,而不是Unicode代码。
JODE,官网,即Java Optimize and Decompile Environment,纯Java开发,众多Java反编译软件的核心引擎,能应付一些常见的加密手段,如混淆技术等。很古老,很久没有更新,源码拖管于SourceForge。
字节码
字节码文件,即.class
文件。Java一次编译到处运行,得益于JVM对字节码文件的解析。不论该字节码文件来自何方,由哪种编译器编译,甚至是手写字节码文件,只要符合JVM规范,JVM就能执行该文件。
public class Demo {
private static final Integer NUM = 1;
public static void main(String[] args) {
}
}
习惯使用IDE,此时如果想要看字节码文件,不能使用IDE查看class文件,IDE会自动将class文件反编译成和源文件非常类似的文件:
public class Demo {
private static final Integer NUM = 1;
public Demo() {
}
public static void main(String[] args) {
}
}
上面即为使用IDEA自带的Fernflower decompiler反编译之后得到的文件,增加一个默认的构造方法。
想要查看字节码文件需要使用文本编辑器,比如sublime text。
看到的是这样的效果:
cafe babe 0000 0034 001f 0a00 0500 160a
0017 0018 0900 0400 1907 001a 0700 1b01
0003 4e55 4d01 0013 4c6a 6176 612f 6c61
一堆16进制的字节。
Java字节码的总览图,一共含有10部分。
指令集
在JVM的字节码指令集中,invokevirtual、invokestatic、invokeinterface、invokespecial和invokedynamic是用于调用方法的不同指令。代表不同类型的方法调用方式,主要用于实现多态、静态方法调用、接口方法调用等特性:
- invokevirtual:实例方法,依赖虚方法表(vtable)来实现方法的动态分派;
- invokestatic:静态方法,不依赖于对象,静态绑定,即编译期确定;
- invokeinterface:接口方法,在运行时会通过接口方法表(itable)查找具体方法,比invokevirtual更灵活,支持接口的多实现
- invokespecial:调用一些需要特殊处理的实例方法,如构造方法
<init>
,私有方法,父类方法super.method()
, - invokedynamic:动态调用方法,在运行时由引导方法(Bootstrap Method)确定具体的调用逻辑。主要用于实现动态语言特性(如Lambda表达式和动态代理),在运行时动态解析并绑定目标方法。
指令 | 用途 | 绑定类型 | 是否支持多态 |
---|---|---|---|
invokevirtual | 调用实例方法 | 动态绑定 | 支持(运行时确定方法) |
invokestatic | 调用静态方法 | 静态绑定 | 不支持 |
invokeinterface | 调用接口方法 | 动态绑定 | 支持(运行时查找itable) |
invokespecial | 调用私有、父类方法或构造器 | 静态绑定 | 不支持 |
invokedynamic | 动态调用 | 动态绑定 | 支持(灵活性最高) |
invokevirtual和invokeinterface
反汇编
比反编译更底层的过程。