1.字节码概述
- class文件:字节码(bytecode)文件
- class文件是Java“一次编译,到处运行”的基础
- class文件具备平台无关性,由JVM执行
- 每个class文件包含了一个类或接口或模块的定义
- class文件是一个二进制文件,由JVM定义class文件的规范
- 任何满足这种规范的class文件都会被JVM加载运行
- class文件可以由其他语言编译生成,甚至不用程序语言直接生成
- JDK版本不同,所编译出.class文件略有不同
2.字节码文件构成
class文件构成
- 采用类似于C语言结构体的结构来表示数据
- 包括两种数据类型
- 定长数据:无符号数,u1, u2, u4 (分别代表1个字节、2个字节、4个字节的无符号数)
- 不定长数据:由多个无符号数组成,通常在数据的前面给出其长度
魔数
- 前4个字节为魔数,十六进制表示为0xCAFEBABE,标识该文件为class文件
版本号
- 第5、6字节表示次版本号
- 7、8字节表示主版本号
- 主版本号与JDK版本有映射关系
常量池
- 常量池主要存放两大类常量
- 字面量
- 如文本字符串、final的常量值等
- 符号引用
- 类和接口的全限定名
- 字段的名称和描述符
- 方法的名称和描述符
访问标志
- 常量池结束之后的两个字节,描述该Class是类还是接口,以及是否被public、abstract、final等修饰符修饰
类索引、父类索引与接口索引集合
- 类索引
- 访问标志后的两个字节,描述的是当前类的全限定名
- 这两个字节保存的值为常量池中的索引值,根据索引值就能在常量池中找到这个类的全限定名
- 父类索引
-当前类名后的两个字节,描述父类的全限定名,同上,保存的也是常量池中的索引值 - 接口索引集合
- 父类名称后为两字节的接口计数器,描述了该类或父类实现的接口数量
- 紧接着的n个字节是所有接口名称的字符串常量的索引值
字段表
- 字段表用于描述类和接口中声明的变量,包含类级别的变量以及实例变量,但是不包含方法内部声明的局部变量
- 字段表也分为两部分,第一部分为两个字节,描述字段个数;第二部分是每个字段的详细信息fields_info
- 方法部分属性
- Code,源代码对应的JVM指令操作码
- LineNumberTable,行号表,将Code区的操作码和源代码中的行号对应
方法表
- 字段表结束后为方法表,方法表也是由两部分组成,第一部分为两个字节描述方法的个数
- 第二部分为每个方法的详细信息。方法的详细信息较为复杂,包括方法的访问标志、方法名、方法的描述符以及方法的属性
附加属性
- 字节码的最后一部分,该项存放了在该文件中类或接口所定义属性的基本信息
- 属性信息相对灵活,编译器可自由写入属性信息,JVM会忽略不认识的属性信息
3.字节码指令分类
字节码分类
- 加载和存储指令
- 用于将数据在栈帧中的局部变量表和操作数栈之间来回传输
- 将一个局部变量加载到操作栈:iload、lload、fload、dload、aload等
- 将一个数值从操作数栈存储到局部变量表:istore、lstore、fstore、dstore、astore等
- 将一个常量加载到操作数栈:bipush、sipush、ldc、ldc_w、ldc2_w、aconst_null、iconst_m1等
- 运算指令
- iadd、isub、imul、idiv等
- 类型转换指令
- i2b、i2l、i2s等
- 对象/数组创建与访问指令
- new、newarray、getfield等
- 操作数栈管理指令
- pop、dup等
- 控制转移指令
- Ifeq、goto等
- 方法调用和返回指令
- invokevirtual、ireturn等
- 异常处理指令
- athrow
- 同步控制指令
- monitorenter、monitorexit
字节码指令简介
- JVM指令由操作码和零至多个操作数组成
- 操作码(OpCode,代表着某种特定操作含义的数字)
- 操作数(Operand,操作所需参数)
- JVM的指令集是基于栈而不是寄存器
- 字节码指令控制的是JVM操作数栈
4.字节码操作ASM
- 字节码操作:指令层次较为复杂
- 很多第三方字节码工具包
- ASM是生成、转换、分析class文件的工具包
- https://asm.ow2.io/
- 体积小、轻量、快速、文档完善,被众多开源项目采用
- Groovy/Kotlin Compiler
- Gradle
- Jacoco //代码覆盖率统计
- Mockito // Java Test Mock框架
ASM API
- Core API
- 类比解析XML文件中的SAX方式
- 不需要读取类的整个结构,使用流式的方法来处理字节码文件
- 非常节约内存,但是编程难度较大
- 出于性能考虑,一般情况下编程都使用Core API
- Tree API
- 类比解析XML文件中的DOM方式,把整个类的结构读取到内存中
- 消耗内存多,但是编程比较简单
- 通过各种Node类来映射字节码的各个区域
ASM API 核心类
- 核心类
- ClassReader 用于读取已经编译好的.class文件
- ClassWriter 用于重新构建编译后的类
- 如修改类名、属性以及方法,也可以生成新的类的字节码文件
- Visitor类
- CoreAPI根据字节码从上到下依次处理
- 对于字节码文件中不同的区域有不同的Visitor
- MethodVisitor 用于访问类方法
- FieldVisitor 访问类变量
- AnnotationVisitor 用于访问注解
1 | //修改字节码文件 |
1 | //生成字节码文件 |
5.字节码增强
- 字节码操作:通常在字节码使用之前完成
- 源码、编译、(字节码操作)、运行
- 字节码增强:运行时对字节码进行修改/调换
- Java ClassLoader类加载器
- Java Instrument
Java Instrument
- JDK 5 引入,java.lang.instrument包
- 对程序的替换,都是通过代理程序(javaagent)进行
- premain:支持在main函数之前,对类的字节码进行修改/替换
- agentmain:支持在程序运行过程中,对字节码进行替换
Java 运行前代理
- 在main函数运行之前,修改/替换某类的字节码
- 启动Java程序时,给java.exe增加一个参数javaagent:someone.jar
- 在someone.jar的清单文件(manifest)指定了Premain-Class:SomeAgent
- SomeAgent类中,有一个premain方法,此方法先于main运行
- premain方法有一个Instrumentation的形参,可以调用addTransformer方法,增加一个ClassTransformer转换类
- 自定义一个ClassTransformer类 ,重写tranform方法,修改/替换字节码
Java 运行时代理
- 在main函数运行时,修改某类的字节码
- Test调用Greeting类工作
- 编写AttachToTest类,对Test进程附加一个agent(jar)
- 在jar中,利用Instrument对Greeting类进行retransformClasses,重新加载
- 对进程附加agent,是JVMTI的技术
- JVM Tool Interface
类替换的注意事项
- 可以修改方法体和常量池
- 不可以增加、修改成员变量/方法定义
- 不可以修改继承关系
- 未来版本还会增加限制条件
6.字节码混淆
Java字节码的弱点
- Java 字节码文件机制
- .java文件是程序源码,是程序员智慧劳动的结晶,需要保护
- 字节码文件是程序运行的主体,遵守JVM的规范,且被分发使用
- 为了各种需要,产生出很多反编译工具,从字节码恢复源码
- javap, JDK自带的,可以解析出(可阅读的)字节码
- Jad, https://varaneckas.com/jad/, 历史最老
- JD(Java Decompiler, http://java-decompiler.github.io/)
- Procyon, https://bitbucket.org/mstrobel/procyon/src/default/
- Luyten, GUI版本,https://github.com/deathmarine/Luyten
- CRF, http://www.benf.org/other/cfr/
Java字节码的保护
- 字节码保护
- 字节码加密
- 对字节码进行加密,不再遵循JVM制定的规范
- JVM加载之前,对字节码解密后,再加载
- 字节码加密
- 字节码混淆
- 被混淆的代码依然遵循JVM制定的规范
- 变量命名和程序流程上进行等效替换,使得程序的可读性变差
- 代码难以被理解和重用,达到保护代码的效果
ProGuard
- 最著名的Java字节码混淆器
- https://www.guardsquare.com/en/products/proguard
- 除了混淆,也具有代码压缩、优化、预检等功能
- 可以命令行运行,也可以集成到Eclipse等IDE中使用
- 不仅可以处理Java代码,也可以处理Android的代码
- ProGuard核心配置文件
- ignorewarnings 跳过警告
- -verbose 显示所有日志
- -injars 需要转化的对象地址
- -outjars 输出的地址
- -libraryjars <java.home>/lib/rt.jar 用到的支持库
- -printmapping proguard.map 指定自定义的混淆用的映射文件
- -dontshrink 不压缩,保持项目原有类和方法,仅对命名及内容进行混淆
- -keepclassmembers public class * {void set*(*** );*** name;}
- 不混淆指定匹配的内容,匹配到的内容所属类名依旧会被混淆
- -keepclasseswithmembers
- 与上面对应,这一项会让匹配到的内容所属类名也不被混淆
- -keep public class Proguard { void main(java.lang.String[]);}
- 不混淆指定类及指定其中的成员及方法
ProGuard注意事项
- 反射调用类或者方法,可能失败
- 对外接口的类和方法,不要混淆,否则调用失败
- 嵌套类混淆,导致调用失败
- native的方法不要混淆
- 枚举类不要混淆
- …