MOOC 个人学习笔记
# 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 用于访问注解
// 修改字节码文件 | |
public class MyClassVisitor extends ClassVisitor implements Opcodes { | |
private static String methodName; | |
public MyClassVisitor(ClassVisitor cv) { | |
super(ASM5, cv); | |
} | |
@Override | |
public void visit(int version, int access, String name, String signature, String superName, String[] interfaces) { | |
cv.visit(version, access, name, signature, superName, interfaces); | |
} | |
@Override | |
public MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) { | |
MethodVisitor mv = cv.visitMethod(access, name, desc, signature, exceptions); | |
// 忽略构造方法 | |
if (!name.equals("<init>") && mv != null) { | |
methodName = name; | |
mv = new MyMethodVisitor(mv); | |
} | |
return mv; | |
} | |
class MyMethodVisitor extends MethodVisitor implements Opcodes { | |
public MyMethodVisitor(MethodVisitor mv) { | |
super(Opcodes.ASM5, mv); | |
} | |
public void visitCode() { | |
super.visitCode(); | |
// 方法进入时打印信息 | |
// 拿到 java.lang.System 类的.out static 字段,并放入栈顶 | |
mv.visitFieldInsn(GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;"); | |
// 将待输出信息放入栈顶 | |
mv.visitLdcInsn("method " + methodName + " is starting"); | |
// 调用 println 方法 | |
mv.visitMethodInsn(INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V", false); | |
} | |
public void visitInsn(int opcode) { | |
// 判断 return 或抛出异常 | |
if ((opcode >= Opcodes.IRETURN && opcode <= Opcodes.RETURN) || opcode == Opcodes.ATHROW) { | |
mv.visitFieldInsn(GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;"); | |
mv.visitLdcInsn("method " + methodName + " is ending"); | |
mv.visitMethodInsn(INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V", false); | |
} | |
mv.visitInsn(opcode); | |
} | |
} | |
} |
// 生成字节码文件 | |
public class Generator { | |
public static void main(String[] args) throws Exception { | |
modifyGreeting(); | |
createAsmGreeting(); | |
} | |
public static void modifyGreeting() throws IOException { | |
// 读取 | |
ClassReader classReader = new ClassReader("com/test/Greeting"); | |
ClassWriter classWriter = new ClassWriter(ClassWriter.COMPUTE_MAXS); | |
// 处理 | |
ClassVisitor classVisitor = new MyClassVisitor(classWriter); | |
classReader.accept(classVisitor, ClassReader.SKIP_DEBUG); | |
byte[] data = classWriter.toByteArray(); | |
// 输出 | |
File f = new File("target/classes/com/test/Greeting.class"); | |
FileOutputStream fout = new FileOutputStream(f); | |
fout.write(data); | |
fout.close(); | |
System.out.println("Modify Greeting Class success!!!!!"); | |
} | |
public static void createAsmGreeting() throws Exception { | |
ClassWriter cw = new ClassWriter(0); | |
MethodVisitor mv; | |
// 主版本号设为 49,JDK5 之后都可以运行 | |
cw.visit(49, Opcodes.ACC_PUBLIC + Opcodes.ACC_SUPER, "com/test/AsmGreeting", null, "java/lang/Object", null); | |
cw.visitSource("AsmGreeting.java", null); | |
// 无参构造方法 | |
mv = cw.visitMethod(Opcodes.ACC_PUBLIC, "<init>", "()V", null, null); | |
mv.visitVarInsn(Opcodes.ALOAD, 0); // 将 this 放入栈顶 | |
// 调用 super () | |
mv.visitMethodInsn(Opcodes.INVOKESPECIAL, "java/lang/Object", "<init>", "()V", false); | |
mv.visitInsn(Opcodes.RETURN); | |
mv.visitMaxs(1, 1); | |
mv.visitEnd(); | |
//hello 方法,无参无返回值 | |
mv = cw.visitMethod(Opcodes.ACC_PUBLIC + Opcodes.ACC_STATIC, "hello", "()V", null, null); | |
mv.visitFieldInsn(Opcodes.GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;"); | |
mv.visitLdcInsn("Hello, this class is genrated by ASM"); | |
mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V", false); | |
mv.visitInsn(Opcodes.RETURN); | |
mv.visitMaxs(2, 1); | |
mv.visitEnd(); | |
cw.visitEnd(); | |
File f = new File("target/classes/com/test/AsmGreeting.class"); | |
FileOutputStream fout = new FileOutputStream(f); | |
fout.write(cw.toByteArray()); | |
fout.close(); | |
System.out.println("Create AsmGreeting success!!!!!"); | |
} | |
} |
# 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 *
- 不混淆指定匹配的内容,匹配到的内容所属类名依旧会被混淆
- -keepclasseswithmembers
- 与上面对应,这一项会让匹配到的内容所属类名也不被混淆
- -keep public class Proguard
- 不混淆指定类及指定其中的成员及方法
# ProGuard 注意事项
- 反射调用类或者方法,可能失败
- 对外接口的类和方法,不要混淆,否则调用失败
- 嵌套类混淆,导致调用失败
- native 的方法不要混淆
- 枚举类不要混淆
- ......