深入探索编译插桩技术(四、ASM 探秘,技术协会安卓部面试

4、插入指定的操作码

InsnList 主要提供了 四类 方法用于插入字节码,如下所示:

  • 1)、add(AbstractInsnNode insn)将一个操作码添加到 InsnList 的末尾
  • 2)、insert(AbstractInsnNode insn)将一个操作码插入到这个 InsnList 的开头
  • 3)、insert(AbstractInsnNode insnNode,AbstractInsnNode insn)将一个操作码插入到另一个操作码的下面
  • 4)、insertBefore(AbstractInsnNode insnNode,AbstractInsnNode insn) 将一个操作码插入到另一个操作码的上面

接下来看看如何使用这些方法插入指定的操作码,代码如下所示:

for(AbstractInsnNode ainNode : methodNode.instructions.toArray()) {
if(ainNode.getOpcode() == Opcodes.BIPUSH && ((IntInsnNode)ainNode).operand == 16) {
methodNode.instructions.insert(ainNode, new MethodInsnNode(Opcodes.INVOKEVIRTUAL, “java/awt/image/BufferedImage”, “getWidth”, “(Ljava/awt/image/ImageObserver;)I”));
methodNode.instructions.insert(ainNode, new InsnNode(Opcodes.ACONSTNULL));
methodNode.instructions.set(ainNode, new VarInsnNode(Opcodes.ALOAD, 1));
}
}

这样,我们就能将

BIPUSH 16

替换为

ALOAD 1
ACONSTNULL
INVOKEVIRTUAL java/awt/image/BufferedImage.getWidth(Ljava/awt/image/ImageObserver;)I

当我们操控完指定的类节点之后,就可以使用 ASM 的 ClassWriter 类来输出字节码,代码如下所示:

// 1、让 ClassWriter 自行计算最大栈深度和栈映射帧等信息
ClassWriter classWriter = new ClassWriter(ClassWriter.COMPUTEFRAMES);
classNode.accept(classWriter);
return classWriter.toByteArray();

关于 ClassWriter 的具体用法,我们会在 ASM Core API 这部分来进行逐步讲解。下面👇,我们就先来看看 ASM 的事件模型。

三、ASM 的事件模型(ASM Core API)

对象模型是由事件模型封装而成,因此事件模型的上手难度会更大一些。

对于事件模型来说,它 采用了设计模式中的访问者模式。它的出现是为了更好地解决这样一种需求:有 A 个元素和 N 种算法,每个算法都能作用于任意一个元素,并且在不同的元素上有不同的运行方式

在访问者模式出现之前,我们通常会在每一个元素对应的类中添加 N 个方法,然后在每一个方法中去实现一个算法,但是,这样的做法容易导致代码耦合性过高,并且可维护性差。

因此,访问者模式应运而生,我们可以 建立 N 个访问者,并且每一个访问者拥有一个算法及其内部的 A 种运行方式。当我们需要调用一个算法时,就让相应的访问者去访问元素,然后让访问者根据被访问对象选择相应的算法

需要注意的是,访问者并没有直接去操作元素,而是先让元素类调用 accept 方法接收访问者,然后,访问者在元素类的内部方法中开始调用 visit 方法访问当前的元素类。这样,访问者便能直接访问元素类中的内部私有成员,其优势在于 避免了暴露不必要的内部细节

要理解 ASM 的事件模型,我们就需要对其中的 两个重要成员的工作原理 有较深的了解。它们便是 类访问者 ClassVisitor 与 类读取(解析)者 ClassReader

从字节码的视角中,一个 Java 类由很多组件凝聚而成,而这之中便包括超类、接口、属性、域和方法等等。当我们在使用 ASM 进行操控时,可以将它们视为一个个与之对应的事件。因此 ASM 提供了一个 类访问者 ClassVisitor,以通过它来访问当前类的各个组件,当解析器 ClassReader 依次遇到上述的各个组件时,ClassVisitor 上对应的 visitor 事件处理器方法均会被一一调用

与类相似,方法也是由多个组件凝聚而成的,其对应着方法属性、注解及编译后的代码(Class 字节码)。ASM 的 MethodVisitor 提供了一种 hook(钩子)机制,以便能够访问方法中的每一个操作码,这样我们便能够对字节码文件进行细粒度地修改

下面,我们便来一一分析下它们。

1、类访问者 ClassVisitor

通常我们在使用 ASM 的访问者模式有一个模板代码,如下所示:

InputStream is = new FileInputStream(classFile);
// 1
ClassReader classReader = new ClassReader(is);
// 2
ClassWriter classWriter = new ClassWriter(ClassWriter.COMPUTE_MAXS);
// 3
ClassVisitor classVisitor = new TraceClassAdapter(Opcodes.ASM5, classWriter);
// 4
classReader.accept(classVisitor, ClassReader.EXPAND_FRAMES);

首先,在注释1处,我们 将目标文件转换为流的形式,并将它融入类读取器 ClassReader 之中。然后,在注释2处,我们 构建了一个类写入器 ClassWriter,其参数 COMPUTE_MAXS 的作用是将自动计算本地变量表最大值和操作数栈最大值的任务托付给了ASM。接着,在注释3处,新建了一个自定义的类访问器,这个自定义的 ClassVisitor 的作用是为了在每一个方法的开始和结尾处插入相应的记时代码,以便统计出每一个方法的耗时。最后,在注释4处,类读取器 ClassReader 实例这个被访问者调用了自身的 accept 方法接收了一个 classVisitor 实例,需要注意的是,第二个参数指定了 EXPAND_FRAMES,旨在说明在读取 class 的时候需要同时展开栈映射帧(StackMap Frame),如果我们需要使用自定义的 MethodVisitor 去修改方法中的指令时必须要指定这个参数,。

上面,我们说到了栈映射帧(StackMap Frame),它到底是什么呢?

栈映射帧 StackMap Frame

它是 Java 6 以后引入的一种验证机制,用于 检验 Java 字节码的正确性。它的工作方式是 记录每一个关键步骤完成后其方法中操作数栈的理论状态,然后,在实际运行的时候,ASM 会将其实际状态和理论状态对比,如果状态不一致则表明出现了错误

但栈映射帧的实现并不简单,因此通过调用 classReader 实例的 accept 方法我们便可以让 ASM 自动去计算栈映射帧,尽管这 会增加 50% 的额外运算。此外,可能会有小概率的情况遇到 栈映射帧验证失败 的情况,例如:VerifyError: Inconsistent stackmap frames at branch target 这个错误。

最常见的原因可能就是由于 字节码写错造成的,此时,我们应该去检查对应的字节码实现代码。此外,也可能是 JDK 版本的支持问题或是 ASM 自身的缺陷,但是,这种情况几乎不会发生。

2、类读取(解析)者 ClassVisitor

现在,让我们再回到上述注释4处的代码,在这里,我们调用了 classReader 的 accept 方法接收了一个访问者 classVisitor,下面,我们来看看其内部的实现,代码如下所示(源码实现较长,这里我们只需关注注释处的代码即可:

/**

  • Makes the given visitor visit the Java class of this {@link ClassReader}
  • . This class is the one specified in the constructor (see
  • {@link #ClassReader(byte[]) ClassReader}).
  • @param classVisitor
  •        the visitor that must visit this class.
    
  • @param flags
  •        option flags that can be used to modify the default behavior
    
  •        of this class. See {@link #SKIP_DEBUG}, {@link #EXPAND_FRAMES}
    
  •        , {@link #SKIP_FRAMES}, {@link #SKIP_CODE}.
    

*/
public void accept(final ClassVisitor classVisitor, final int flags) {
accept(classVisitor, new Attribute[0], flags);
}

在 accept 方法中又继续调用了 classReader 的另一个 accept 重载方法,如下所示:

public void accept(final ClassVisitor classVisitor,
fina

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值