c# 十六进制转为字节_从ASM入门字节码增强冰岩分享

本文介绍了ASM库在Java字节码增强中的应用,通过实例展示了如何将十六进制转换为字节码。内容涵盖了ASM的基础使用、设计流程,以及ClassReader、ClassVisitor和ClassWriter的角色。同时,详细解析了Class文件结构和操作码,阐述了ASM如何通过访问者模式实现字节码的动态修改。
摘要由CSDN通过智能技术生成

ASM入门字节码增强

2e40ffcfd30e725b65eb7d3beee64965.png

ASM作为Java字节码层次的处理框架,能够直接对字节码进行操作,使用ASM能够轻松完成代码注入等字节码增强的相关操作。

基础使用

这里使用一个基础调用展示ASM的使用

val cr = ClassReader(inputStream)val cw = ClassWriter(cr, 0)val cv = MyClassVisitor(Opcodes.ASM6, cw)cr.accept(cv, ClassReader.EXPAND_FRAMES)inputStream.close()val byteArray = cw.toByteArray()

在这里用到了ClassReader,ClassWriter以及一个自定义的ClassVIsitor,首先通过ClassReader读入输入流,之后定义ClassWriter,初始化ClassVisitor,再通过accept开启整个流程,最后通过ClassWriter获取修改过后的输出流。

自定义ClassVisitor如下,这里在Activity的onCreate方法中插入了方法System.out.println(System.currentTimeMills());

class MyClassVisitor(api: Int, cv: ClassVisitor) : ClassVisitor(api, cv) {    override fun visitMethod(access: Int, name: String?, desc: String?,                             signature: String?, exceptions: Array<out String>?): MethodVisitor? {        val mv = super.visitMethod(access, name, desc, signature, exceptions)        if (name == "onCreate") {            mv.visitMaxs(0,0)            return BingyanMethodVisitor(api, mv)        }        return mv    }    private inner class BingyanMethodVisitor(api: Int, mv: MethodVisitor) : MethodVisitor(api, mv) {        override fun visitCode() {            super.visitCode()            mv.visitFieldInsn(GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;");            mv.visitMethodInsn(INVOKESTATIC, "java/lang/System", "currentTimeMillis", "()J", false);            mv.visitMethodInsn(INVOKEVIRTUAL, "java/io/PrintStream", "println", "(J)V", false);        }    }}

基础准备(字节码)

Class文件结构

ASM作为操作字节码的工具,那么首先会介绍相关字节码的简单结构。用一张表格来表示class文件的结构,class文件中分成了以下十个部分:

字节码结构
名称大小描述
魔数4字节0xcafebabe
版本号4字节次版本号+主版本号
常量池2字节+N计数器+数据区
访问标志2字节类or接口,以及修饰符
当前类索引2字节当前类全限定名(常量池索引)
父类索引2字节父类的全限定名(常量池索引)
接口索引2字节+N计数器+常量池索引
字段表2字节+N计数器+类和接口中声明的变量字段的详细信息
方法表2字节+N计数器+方法信息
附加属性类或接口所定义属性的基本信息

1、魔数

前四个字节是魔数,固定值为:0xCAFEBABE,用来标识一个文件是class文件。

2、版本号

四个字节标示版本号,其中两个字节为次版本号,后两个字节为主版本号。例如 “00 00 00 34” 转化为16进制后分别为 0 和 52。对应版本1.8.0。

3、常量池

  • 计数器

在常量池中前两位是计数器,用来标示在当前class文件中有多少个常量。将16进制转化成10进制之后-1。

  • 数据区

由指定数量的cp_info结构组成,每一个cp_info有两部分或三部分组成。储存字面量与符号引用,字面量即代码中定义的文本字符串等,而符号引用包括类和接口的全限定名,字段的名称和描述符号,方法的名称和描述符。

CONSTANT_Utf8_infotag (1 byte)length (2 byte)
1字符串长度长度为length的字符串
CONSTANT_Integer_infotag (1 byte)4 byte
3高位在前存储的int值
CONSTANT_Float_infotag (1 byte)4 byte
4高位在前存储的float值
CONSTANT_Long_infotag (1 byte)8 byte
5高位在前存储的long值
CONSTANT_Double_infotag (1 byte)8 byte
6高位在前存储的double值
CONSTANT_Class_infotag (1 byte)index (2 byte)
7全限定名常量项索引
CONSTANT_String_infotag (1 byte)index (2 byte)
8字符串字面量索引
CONSTANT_Fieldref_infotag (1 byte)index (2 byte)index (2 byte)
9声明字段的类或接口描述符索引字段描述符索引
CONSTANT_Methodref_infotag (1 byte)index (2 byte)index (2 byte)
10声明方法的类描述符索引名称及类描述符索引
CONSTANT_InterfaceMethodref_infotag (1 byte)index (2 byte)index (2 byte)
11声明方法的接口描述符索引名称及类描述符索引
CONSTANT_NameAndType_infotag (1 byte)index (2 byte)index (2 byte)
12字段和方法名称常量索引字段和方法名称描述符常量索引
CONSTANT_MethodHandle_infotag (1 byte)reference_kind (1 byte)reference_kind (2 byte)
151~9 方法句柄类型常量池索引
CONSTANT_MethodType_infotag (1 byte)descripto_index (2 byte)
16常量池索引,方法描述符
CONSTANT_InvokeDyanmic_infotag (1 byte)(2 byte)(2 byte)
18

4、访问标志

两个字节标示是类还是接口,以及pulic,abstract,final等。

5、类名

两个字节,当前类的全限定名在常量池中的索引

6、父类名称

两个字节,父类的全限定名在常量池中的索引

7、接口信息

  • 计数器:两个字节,描述接口数量

  • n个字节,接口名称的字符串常量的索引

8、字段表

描述类和接口中声明的变量

2 bytes
计数器
2 bytes2 bytes2 bytes2 bytesn bytes
权限修饰符字段索引描述符索引属性个数属性列表
2 bytes2 bytes2 bytes2 bytesn bytes
权限修饰符字段索引描述符索引属性个数属性列表
2 bytes2 bytes2 bytes2 bytesn bytes
权限修饰符字段索引描述符索引属性个数属性列表

9、方法表

描述方法

2 bytes
计数器
2 bytes2 bytes2 bytes2 bytesn bytes
权限修饰符方法名索引描述符索引属性个数属性列表
2 bytes2 bytes2 bytes2 bytesn bytes
权限修饰符方法名索引描述符索引属性个数属性列表
2 bytes2 bytes2 bytes2 bytesn bytes
权限修饰符方法名索引描述符索引属性个数属性列表

方法的属性由三部分组成

  • Code:JVM对应的操作指令码

  • LineNumberTable:对应java文件中的行号

  • LocalVariableTable:本地变量表

10、附加属性表

文件中类或接口所定义属性的基本信息。

操作码

java文件编译后会生成让jvm执行的十六进制操作码,这里不列举具体操作码对应的十六进制表示和含义,仅用一个例子做为说明:

public final void add(int, int);    descriptor: (II)V    flags: ACC_PUBLIC, ACC_FINAL    Code:      stack=2, locals=5, args_size=3         0: iload_1    //变量1入栈         1: iload_2    //变量2入栈         2: iadd       //相加         3: istore_3   //将值存入变量3         4: iconst_0   //this入栈         5: istore        4    //         7: getstatic     #35    //调用索引35,这里是System.out        10: iload_3    //变量3        11: invokevirtual #46    //调用System.out的索引为46的方法,参数为变量3        14: return      LineNumberTable:        line 39: 0        line 40: 4        line 41: 14      LocalVariableTable:        Start  Length  Slot  Name   Signature            4      11     3     c   I            0      15     0  this   Lcom/example/flint/MainActivity;            0      15     1     a   I            0      15     2     b   I

ASM设计流程

通过ASM的调用可以看出ASM整体流程,ClassReader用来读入字节码流并调用accept开启整个流程,作为典型的访问者设计模式去进一步调用ClassVisitor中的相应的方法。在ClassVisitor中则进一步分成了annotationvisitor,fieldvisitor,methodvisitor。而ClassWriter是ClassVisitor的子类,在调用ClassVisitor的相应的方法时,同时也会调用ClassWriter中的方法,在ClassWriter中,保存了对输入流进行修改过后的字节流,在整体流程结束后,通过toByteArray获取修改过后的输入流。

这里用简单的流程图说明:

c8a230dbe8dc44ed5b5237cfc5feca86.png

ASM类图

307370e26402b6513393340792f5bea1.png

ClassReader

ClassReader可以看出作用就是用来读取一个class文件,并且调用accept加载ClassVisitor并开始整个流程。

public ClassReader(final byte[] b, final int off, final int len) {    this.b = b;    // checks the class version    if (readShort(off + 6) > Opcodes.V9) {        throw new IllegalArgumentException();    }    // parses the constant pool    items = new int[readUnsignedShort(off + 8)];    int n = items.length;    strings = new String[n];    int max = 0;    int index = off + 10;    for (int i = 1; i < n; ++i) {        items[i] = index + 1;        int size;        switch (b[index]) {        case ClassWriter.FIELD:        case ClassWriter.METH:        ......        case ClassWriter.HANDLE:            size = 4;            break;        default:            size = 3;            break;        }        index += size;    }    maxStringLength = max;    // the class header information starts just after the constant pool    header = index;}

在ClassReader的构造方法中,首先检查版本号,之后创建items数组用来存储输入流(即字节码)中的信息,在这里通过iteam保存每一个类型的index。

// visits the class declarationclassVisitor.visit(readInt(items[1] - 7), access, name, signature,        superClass, interfaces);// visits the source and debug infoif ((flags & SKIP_DEBUG) == 0        && (sourceFile != null || sourceDebug != null)) {    classVisitor.visitSource(sourceFile, sourceDebug);}// visits the module info and associated attributesif (module != 0) {    readModule(classVisitor, context, module,            moduleMainClass, packages);}// visits the outer classif (enclosingOwner != null) {    classVisitor.visitOuterClass(enclosingOwner, enclosingName,            enclosingDesc);}// visits the class annotations and type annotationsif (anns != 0) {    for (int i = readUnsignedShort(anns), v = anns + 2; i > 0; --i) {        v = readAnnotationValues(v + 2, c, true,                classVisitor.visitAnnotation(readUTF8(v, c), true));    }}

accept方法太长了,这里只截取调用方法中的一小部分。在类初始化后,紧接着会调用accept函数并传入ClassVisitor,之后遵循Class文件的格式读取在构造函数中传入的二进制流,之后解析并通过visit方法将读取到的信息全部交给ClassVisitor处理。这里的这些visit方法可以在自定义ClassVisitor中重写。

ClassVisitor

ClassVisitor是一个抽象类,而在传入时需继承这个类并重写响应的visit方法。

public ClassVisitor(final int api, final ClassVisitor cv) {    if (api  Opcodes.ASM6) {        throw new IllegalArgumentException();    }    this.api = api;    this.cv = cv;}public void visit(int version, int access, String name, String signature,        String superName, String[] interfaces) {    if (cv != null) {        cv.visit(version, access, name, signature, superName, interfaces);    }}

首先是ClassVisitor的构造方法,这里有一个ClassVisitor参数,也就是说在ClassVisitor内部还可以有另一个ClassVisitor。

ClassVisitor中的visit方法都是相似的,这里只看visit,会继续调用传入的ClassVisitor的visit方法。

而在实例中传入的是ClassWriter方法,那么毫无疑问ClassWriter方法也是ClassVisitor的一个子类。

ClassVisitor

ClassWriter继承了ClassVisitor,同时也重写了ClassVisitor中的相应的方法,但更多了对class内容的保存。

public ClassWriter(final ClassReader classReader, final int flags) {    this(flags);    classReader.copyPool(this);    this.cr = classReader;}void copyPool(final ClassWriter classWriter) {    char[] buf = new char[maxStringLength];    int ll = items.length;    Item[] items2 = new Item[ll];    ······        int off = items[1] - 1;    classWriter.pool.putByteArray(b, off, header - off);    classWriter.items = items2;    classWriter.threshold = (int) (0.75d * ll);    classWriter.index = ll;}

在ClassWriter的初始化函数中多了一步,copyPool,在copyPool方法中可以看到ClassReader将读取的class信息加载到了ClassWriter之中,也就是说此时ClassWriter中也保存了一份class的数据。

@Overridepublic final MethodVisitor visitMethod(final int access, final String name,        final String desc, final String signature, final String[] exceptions) {    return new MethodWriter(this, access, name, desc, signature,            exceptions, compute);}

再看MethodVisit方法,当ClassVisitor中的visit方法被调用后,会继续调用ClassWriter中的visit方法,在这个方法中会返回一个Writer对象,而在重写的ClassVisitor中,获取了这个对象之后返回了新的继承了MethodVisitor的自定义内部类。(MethodVisitor是MethodWriter的父类)

private inner class BingyanMethodVisitor(api: Int, mv: MethodVisitor) : MethodVisitor(api, mv) {        override fun visitCode() {            super.visitCode()            mv.visitFieldInsn(GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;");            mv.visitMethodInsn(INVOKESTATIC, "java/lang/System", "currentTimeMillis", "()J", false);            mv.visitMethodInsn(INVOKEVIRTUAL, "java/io/PrintStream", "println", "(J)V", false);        }    }}

在这里重写了visitCode方法,在其中完成了字节码注入。这里的mv就是MethodWriter,在visitCode中调用了一系列MethodWriter的方法。

@Overridepublic void visitInsn(final int opcode) {    lastCodeOffset = code.length;    // adds the instruction to the bytecode of the method    code.putByte(opcode);    // update currentBlock    // Label currentBlock = this.currentBlock;    if (currentBlock != null) {        if (compute == FRAMES || compute == INSERTED_FRAMES) {            currentBlock.frame.execute(opcode, 0, null, null);        } else {            // updates current and max stack sizes            int size = stackSize + Frame.SIZE[opcode];            if (size > maxStackSize) {                maxStackSize = size;            }            stackSize = size;        }        // if opcode == ATHROW or xRETURN, ends current block (no successor)        if ((opcode >= Opcodes.IRETURN && opcode <= Opcodes.RETURN)                || opcode == Opcodes.ATHROW) {            noSuccessor();        }    }}

以一个方法举例,在visitInsn中,将opcode保存在了MethodWriter之中。而对于ClassWriter,通过了链式完成了对MethodWriter的保存。

if (cw.firstMethod == null) {    cw.firstMethod = this;} else {     cw.lastMethod.mv = this; }cw.lastMethod = this;

在MethodWriter的构造函数中有这样一步,cw就是ClassWriter,这里的firstMethod就是ClassWriter创建的第一个MethodWriter,而之后的每一个MethodWriter都会保存在上一个MethodWriter.mv中,也就是以链表的形式保存了所有的MethodWriter。

最后看toByteArray

public byte[] toByteArray() {    if (index > 0xFFFF) {        throw new RuntimeException("Class file too large!");    }    // computes the real size of the bytecode of this class    int size = 24 + 2 * interfaceCount;    ......        int nbMethods = 0;    MethodWriter mb = firstMethod;    while (mb != null) {        ++nbMethods;        size += mb.getSize();        mb = (MethodWriter) mb.mv;    }    int attributeCount = 0;    ......    // allocates a byte vector of this size, in order to avoid unnecessary    // arraycopy operations in the ByteVector.enlarge() method    ByteVector out = new ByteVector(size);    out.putInt(0xCAFEBABE).putInt(version);    out.putShort(index).putByteArray(pool.data, 0, pool.length);    int mask = Opcodes.ACC_DEPRECATED | ACC_SYNTHETIC_ATTRIBUTE            | ((access & ACC_SYNTHETIC_ATTRIBUTE) / TO_ACC_SYNTHETIC);    out.putShort(access & ~mask).putShort(name).putShort(superName);    out.putShort(interfaceCount);    for (int i = 0; i < interfaceCount; ++i) {        out.putShort(interfaces[i]);    }    ......    out.putShort(nbMethods);    mb = firstMethod;    while (mb != null) {        mb.put(out);        mb = (MethodWriter) mb.mv;    }    out.putShort(attributeCount);    ......    if (moduleWriter != null) {        out.putShort(newUTF8("Module"));        moduleWriter.put(out);        moduleWriter.putAttributes(out);    }      ......   return out.data;}

删除了大部分代码只保留了Method相关的,在toByteArray中首先统计了方法数与所需要的空间大小,之后创建ByteVector以class格式填充数据并输出。而在Method中,通过了链式调用完成了Method数据的填充。

总结

字节码增强技术在性能检测及减少冗余方面都有广泛的应用,而ASM相较于其他方法具有高性能,小巧的特点,但同时因为直接在字节码层面操作,同时也要求掌握一定的字节码语法。

注:图片及资料来源于互联网

f1e37f38cca442cddee05f8584250157.png

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值