前言
https://p4d0rn.gitbook.io/java/rasp/asm/asm0
本来看的这篇文章但是有些高深,一开始看不懂只能通过上一篇的文章过渡一下,这篇文章会主要介绍ASM是什么以及功能。
ASM 是什么
ASM是一个操作Java字节码的类库
一个.java
文件经过Java编译器(javac
)编译之后会生成一个.class
文件,.class
文件中存储的是字节码(ByteCode)数据,ASM所操作的对象即字节码(ByteCode)
字节码文件是具有一定格式的文件,ASM首先将字节码拆分为多个部分(decompose),再对某一部分的信息进行修改(modify),最后将多个部分重新组织为一个新的字节码(recompose)。
既然ASM是操作字节码的工具,而字节码的生成又和Java版本有关,因此需要注意ASM版本和Java版本的兼容,尽量选择较高的ASM版本
ASM能做什么
- generation: 创建新的字节码
- transformation: 对已有字节码进行变换
- analysis: 对已有字节码进行分析
ASM Structure
ASM主要分成而部分:
Core API
asm.jar
最重要的三个类:
- ClassReader: 读取字节码文件,并拆分为不同的部分(decompose)
- ClassVisitor: 对字节码中某一部分进行修改(modify)
- ClassWriter: 将各个部分重组为完整的字节码文件(recompose)
对已有类进行修改,需要这三个类的参与。
对应的还有Fieldvisitor、FieldWriter和MethodVisitor、MethodWriter
根据上一篇文章的介绍,这里很容易看明白
Class Generation
对于字节码的generation,主要是ClassVisitor和ClassWriter的参与,ClassReader将在transform部分介绍。
ClassVisitor
ClassVisitor
是一个抽象类,不能直接new,它有两个常用的子类分别是ClassWriter(Core API)和ClassNode(Tree API)
public class ClassWriter extends ClassVisitor
public class ClassNode extends ClassVisitor
ClassVisitor定义两个字段api和cv
public abstract class ClassVisitor {
/**
* The ASM API version implemented by this visitor. The value of this field must be one of Opcodes
*/
protected final int api;
/** The class visitor to which this visitor must delegate method calls. May be null. */
protected ClassVisitor cv;
public ClassVisitor(final int api, final ClassVisitor classVisitor) {
this.api = api;
this.cv = classVisitor;
}
}
api
指定当前使用的ASM版本,取值为Opcodes.ASM4 ~ Opcodes.ASM9
,这里的版本是ASM9(这里不就是int类型,上篇文章用的5)cv
也是一个ClassVisitor,用来连接多个ClassVisitor
四个关键的方法
ClassVisitor`中定义了很多`visitXxx`方法,这里使用了访问者模式。主要看四个方法:`visit()`、`visitField()`、`visitMethod()`、`visitEnd()
(上篇也基本用过,但是没太懂定义)
在字节码中,描述符是对类型的简单化描述
- 对于字段,描述符即字段本身的类型
- 对于方法,描述符为方法接受的参数类型和返回值的类型
Java 类型 | ClassFile 描述符 |
---|---|
boolean |
Z (Z 表示 Zero,零表示 false ,非零表示 true ) |
byte |
B |
char |
C |
double |
D |
float |
F |
int |
I |
long |
J |
short |
S |
void |
V |
non-array reference |
L<InternalName>; |
array reference |
[ |
String |
Ljava/lang/String; |
Object |
Ljava/lang/Object; |
void |
V |
另外,对于visitMethod
的第二个参数name
,若我们要生成构造方法,这里固定为<init>
;若要生成静态代码块,name
为<clinit>
,signature
为()V
ublic MethodVisitor visitMethod(int access, String name, String descriptor, String signature, String[] exceptions) {
这四个方法的内部都是调用的cv.visitXxx()
并且都是有顺序的,这里只举例列出我们需要用到的
- []表示最多调用一次
- *表示调用0次或多次
-
()和 表示可以任选一个方法,不分先后顺序
visit
(
visitField |
visitMethod
)*
visitEnd
就是开始时 visit方法会调用一次,剩下的字段和方法根据 java内容多少调用,最后调用一次visitEnd
ClassWriter
这个方法顾名思义,就是写文件的类(既然写文件一定会搭配toByteArray)
/*
A ClassVisitor that generates a corresponding ClassFile structure, as defined in the Java Virtual Machine Specification (JVMS). It can be used alone, to generate a Java class "from scratch", or with one or more ClassReader and adapter ClassVisitor to generate a modified class from one or more existing Java classes.
*/
public class ClassWriter extends ClassVisitor{
public static final int COMPUTE_MAXS = 1;
public static final int COMPUTE_FRAMES = 2;
只列出重要的内容,上篇文章由于我们的stack值的错误,导致编译错误。
当时修改就是用的COMPUTE_MAXS说是能自动匹配,
COMPUTE_MAXS
COMPUTE_MAXS:用于自动计算方法的最大堆栈大小和最大局部变量数的标志
MethodVisitor.visitFrame 方法的调用,并从方法字节码重新计算堆栈映射帧。
COMPUTE_FRAMES`包含了`COMPUTE_MAXS`的功能,不仅计算`max stack size`和`max local variables`,还计算`stack map frames`,所以一般都使用`COMPUTE_FRAMES
注意:虽然这个字段能够自动计算,但代码中仍要调用visitMaxs
,否则会报错
使用ClassWriter
创建一个字节码文件,分为三步:
- 创建
ClassWriter
对象 - 调用
ClassWriter#visitXxx
方法 - 调用
ClassWriter#toByteArray
方法
例子
package sample;
public class HelloWorld {
public static final int NUM = 17;
public String name;
public HelloWorld() {
}
public int hi(Object a) {
return 6;
}
}
package demo;
import com.sun.org.apache.xml.internal.security.utils.JavaUtils;
import org.objectweb.asm.ClassWriter;
import org.objectweb.asm.FieldVisitor;
import org.objectweb.asm.MethodVisitor;
import static org.objectweb.asm.Opcodes.*;
public class Test {
public static void main(String[] args) {
JavaUtils.writeBytesToFilename("HelloWorld.class",dump());
}
public static byte[] dump(){
ClassWriter cw=new ClassWriter(ClassWriter.COMPUTE_FRAMES);
cw.visit(V1_8, // version Java8
ACC_PUBLIC, // access flag
"sample/HelloWorld", // name - Internal Name
null, // signature
"java/lang/Object", // superName
null); // interfaces 指定的是那个类
//接下来指定静态变量 NUM
{// public static final int NUM = 17;
FieldVisitor fv1 = cw.visitField(ACC_PUBLIC + ACC_FINAL + ACC_STATIC, // access flag
"NUM", // name
"I", // descriptor
null, // signature
17); // value , could be null
fv1.visitEnd();
}
{// public String name;
FieldVisitor fv2 = cw.visitField(ACC_PUBLIC, // access flag
"name", // name
"Ljava/lang/String", // descriptor
null, // signature
null); // value , could be null
fv2.visitEnd();
}
//public HelloWorld() { 准备构建无参构造方法
// }
{
MethodVisitor mv1 = cw.visitMethod(ACC_PUBLIC, "<init>", "()V", null, null);
mv1.visitEnd();
}
// public int hi(Object a) {
// return 6;
// } 难度在于有return 返回值
{
MethodVisitor mv2 = cw.visitMethod(ACC_PUBLIC, "hi", "(Ljava/lang/Object;)I", null, null);
//上面第三个,Ljava/lang/Object; I 没看懂, int(Object a) 这部分没实现, 强制理解为 I 就是 类型 a形参确实也不重要
mv2.visitLdcInsn(6);
mv2.visitInsn(IRETURN);
mv2.visitEnd();
}
cw.visitEnd();
return cw.toByteArray();
}
}
这里别的文章推荐IDEA插件 ASM Bytecode Viewer生成(暂时还没了解)
FieldVisitor
ClassVisitor#visitField
返回了一个FieldVisitor
对象,和ClassVistor
类很像
// A visitor to visit a Java field.
public abstract class FieldVisitor {//是一个抽象类
protected final int api;
protected FieldVisitor fv;
public FieldVisitor(final int api) {
this(api, null);
}
public void visitEnd()
}
FieldVisitor
调用visitXxx
也遵循一定的调用顺序
FieldVisitor调用visitXxx也遵循一定的调用顺序
FieldWriter
ClassVisitor#visitField
的实现就是通过FieldWriter
(父类为FieldVisitor
)来实现的
// ClassWriter#visitField
@Override
public final FieldVisitor visitField(
final int access,
final String name,
final String descriptor,
final String signature,
final Object value) {
FieldWriter fieldWriter =
new FieldWriter(symbolTable, access, name, descriptor, signature, value);
if (firstField == null) {
firstField = fieldWriter;
} else {
lastField.fv = fieldWriter;
}
return lastField = fieldWriter;
}
在toByteArray
方法中也用到了FieldWriter
的两个方法computeFieldInfoSize()
和putFieldInfo()
MethodVisitor
ClassVisitor#visitMethod
返回了一个MethodVisitor
对象,和ClassVistor
类很像。
// A visitor to visit a Java method
public abstract class MethodVisitor {
protected final int api;
protected MethodVisitor mv;
public MethodVisitor(final int api) {
this(api, null);
}
}
(visitParameter)*
[visitAnnotationDefault]
(visitAnnotation | visitAnnotableParameterCount | visitParameterAnnotation | visitTypeAnnotation | visitAttribute)*
[
visitCode
(
visitFrame |
visitXxxInsn |
visitLabel |
visitInsnAnnotation |
visitTryCatchBlock |
visitTryCatchAnnotation |
visitLocalVariable |
visitLocalVariableAnnotation |
visitLineNumber
)*
visitMaxs
]
visitEnd
visitCode()
和visitMaxs()
之间的方法主要负责当前方法的方法体中opcode内容
MethodWriter
ClassVisitor#visitMethod
的实现就是通过MethodWriter
(父类为MethodVisitor
)来实现的,这时候初始化了方法头的信息
@Override
public final MethodVisitor visitMethod(
final int access,
final String name,
final String descriptor,
final String signature,
final String[] exceptions) {
MethodWriter methodWriter =
new MethodWriter(symbolTable, access, name, descriptor, signature, exceptions, compute);
if (firstMethod == null) {
firstMethod = methodWriter;
} else {
lastMethod.mv = methodWriter;
}
return lastMethod = methodWriter;
}
发现了规律,这几个方法都有一个 Visitor和Writer一对,猜测一个是访问触发事件,一个是写入对应的东西
例子
package sample;
public class HellWorld {
static {
System.out.println("static block");
}
public HellWorld() {
super();
}
}
这个例子的难点就是静态代码块?无参构造调用父类?
public static byte[] dump() throws Exception {
ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_FRAMES);
cw.visit(V1_8,
ACC_PUBLIC,
"example/HellWorld",
null,
"java/lang/Object",
null);
{
// public HellWorld() {
// super();
// }
MethodVisitor mv1 = cw.visitMethod(ACC_PUBLIC,
"<init>",
"()V",
null,
null);//这里声明了一个无参构造方法
mv1.visitCode();
mv1.visitVarInsn(ALOAD, 0); // push locals[0] to operand stack
mv1.visitMethodInsn(INVOKESPECIAL, // call method of super class
"java/lang/Object",//触发父类object的无参构造
"<init>", "()V",
false);
mv1.visitInsn(RETURN);
mv1.visitMaxs(1, 1);//压栈操作
mv1.visitEnd();
}
//static {
// System.out.println("static block");
// }
{
MethodVisitor mv2 = cw.visitMethod(ACC_STATIC, "<clinit>", "()V", null, null);
//一个静态方法
mv2.visitCode();
mv2.visitFieldInsn(GETSTATIC, // get static field
"java/lang/System", "out", "Ljava/io/PrintStream;");
mv2.visitLdcInsn("static block");
mv2.visitMethodInsn(INVOKEVIRTUAL, // call method of instance
"java/io/PrintStream", "println", "(Ljava/lang/String;)V", false);
mv2.visitInsn(RETURN);
mv2.visitMaxs(2, 0);
mv2.visitEnd();
}
cw.visitEnd();
return cw.toByteArray();
}
}
//就是这个 mv2.visitMaxs(2, 0);不太难看懂
Challenge2
假设有一个dog类
package demo;
public class Dog {
public String name;
public int age;
public Dog(String name, int age) {
this.name = name;
this.age = age;
}
@Override
public String toString() {
return "Dog{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}
}
生成如下类:
package sample;
import demo.Dog;
public class HellWorld {
public static Dog test() {
return new Dog("taco", 8);
}
}
{
MethodVisitor mv2 = cw.visitMethod(ACC_PUBLIC | ACC_STATIC, "test", "()Ldemo/Dog;", null, null);
mv2.visitCode();//访问方法的字节码
mv2.visitTypeInsn(NEW, "demo/Dog");//使用mv2.visitTypeInsn指令创建了一个类型指令,它在堆上创建了一个新的对象。该指令将新对象的引用推送到操作数栈上。
mv2.visitInsn(DUP);//指令复制了刚刚创建的对象的引用,以备后续使用
mv2.visitLdcInsn("taco");//将字符串常量"taco"推送到操作数栈上
mv2.visitIntInsn(BIPUSH, 8);//指令将字节型整数常量8推送到操作数栈上。
mv2.visitMethodInsn(INVOKESPECIAL, "demo/Dog", "<init>", "(Ljava/lang/String;I)V", false);
//指令调用了对象的构造方法,初始化了demo.Dog类的一个实例。构造方法的签名为(Ljava/lang/String;I)V,表示接受一个java.lang.String和一个int类型的参数。
mv2.visitInsn(ARETURN);
//指令从方法中返回刚刚创建的demo.Dog对象。该指令将对象的引用从操作数栈中弹出,并将其作为方法的返回值。
mv2.visitMaxs(4, 2);//方法指定操作数栈和局部变量表的大小。
mv2.visitEnd();//方法结束对方法字节码的访问。
}
challenge3
package sample;
public class HellWorld {
public static void test(int a, int b) {
int val = Math.max(a, b);
System.out.println(val);
}
}
可以借用 javap -verbose HellWorld,反向操作一波,对应起来很简单
MethodVisitor mv2 = cw.visitMethod(ACC_PUBLIC | ACC_STATIC, "test", "(II)V", null, null);
mv2.visitCode();
mv2.visitVarInsn(ILOAD, 0);
mv2.visitVarInsn(ILOAD, 1);
mv2.visitMethodInsn(INVOKESTATIC, "java/lang/Math", "max", "(II)I", false);
mv2.visitVarInsn(ISTORE, 2);
mv2.visitFieldInsn(GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;");
mv2.visitVarInsn(ILOAD, 2);
mv2.visitMethodInsn(INVOKEVIRTUAL, "java/io/PrintStream", "println", "(I)V", false);
mv2.visitInsn(RETURN);
mv2.visitMaxs(2, 3);
mv2.visitEnd();
Label
Java程序中有三种基本控制结构:顺序、选择和循环,但转化为字节码后,只存在顺序和跳转两种指令
这时候就要用到Label类了
public class Test {
public boolean cmp(int flag) {
if (flag > 0) {
return true;
} else {
return false;
}
}
}
ifile
表示当栈顶int类型数值小于等于0时跳转(判断条件为假),跳转的位置即后面跟的数字6,6这里表示相对偏移量。
如果判断条件为假,即栈顶int类型数值小于等于0,则跳转到6,iconst_0
表示把0这个常量压入operand stack,最后ireturn
返回栈顶的int数据;如果判断条件为真,即栈顶int类型数值大于0,则顺序执行4、5,返回1
public class Label {
// The offset of this label in the bytecode of its method
int bytecodeOffset;
}
label类的bytecodeOffset即对应上面的相对偏移量
通过调用 MethodVisitor#visitLabel(label)来标记跳转目标
Best Practice
IF
package example;
public class HellWorld {
public void test(boolean flag) {
if (flag) {
System.out.println("value is true");
} else {
System.out.println("value is false");
}
}
}
ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_FRAMES);
cw.visit(V1_8,
ACC_PUBLIC,
"example/HellWorld",
null,
"java/lang/Object",
null);
{
MethodVisitor mv2 = cw.visitMethod(ACC_PUBLIC | ACC_STATIC, "test", "()Ldemo/Dog;", null, null);
mv2.visitCode();//访问方法的字节码
mv2.visitTypeInsn(NEW, "demo/Dog");//使用mv2.visitTypeInsn指令创建了一个类型指令,它在堆上创建了一个新的对象。该指令将新对象的引用推送到操作数栈上。
mv2.visitInsn(DUP);//指令复制了刚刚创建的对象的引用,以备后续使用
mv2.visitLdcInsn("taco");//将字符串常量"taco"推送到操作数栈上
mv2.visitIntInsn(BIPUSH, 8);//指令将字节型整数常量8推送到操作数栈上。
mv2.visitMethodInsn(INVOKESPECIAL, "demo/Dog", "<init>", "(Ljava/lang/String;I)V", false);
//指令调用了对象的构造方法,初始化了demo.Dog类的一个实例。构造方法的签名为(Ljava/lang/String;I)V,表示接受一个java.lang.String和一个int类型的参数。
mv2.visitInsn(ARETURN);
//指令从方法中返回刚刚创建的demo.Dog对象。该指令将对象的引用从操作数栈中弹出,并将其作为方法的返回值。
mv2.visitMaxs(4, 2);//方法指定操作数栈和局部变量表的大小。
mv2.visitEnd();//方法结束对方法字节码的访问。
}
剩下的回来用到了再补上
本作品采用CC BY-NC-ND 4.0进行许可。转载,请注明原作者 Azeril 及本文源链接。