应用诊断利器Arthas ByteKit 深度解读(2):本地变量及参数绑定

前言

本文通过分析ByteKit的本地变量绑定(LocalVarsBinding)处理代码,结合Java Opcode手册、asm代码、javap反汇编字节码等工具,深入讲解每个指令的用法及在本场景的实际作用。结合上下文线索,从字节码的角度去理解ByteKit 本地变量绑定的实现过程。

相关文章:开源诊断利器Arthas ByteKit 深度解读(1):基本原理介绍

简介

Arthas ByteKit 为新开发的字节码工具库,基于ASM提供更高层的字节码处理能力,面向诊断/APM领域,不是通用的字节码库。ByteKit期望能提供一套简洁的API,让开发人员可以比较轻松的完成字节码增强。

本文分析的是本地变量绑定@Binding.LocalVars Object[] vars的实现逻辑。本地变量绑定的作用是通过注解@Binding.LocalVars 将目标代码拦截位置的本地变量映射到Object[] vars数组,在增强逻辑中可以直接通过vars数组进行访问。

一般是要结合本地变量名称绑定@Binding.LocalVarNames String[] varNames获取本地变量的名称和值。本地变量名称绑定(LocalVarNamesBinding)的实现逻辑与本地变量值绑定(LocalVarsBinding)类似,本文不再赘述。

代码概览

为了方便讲解,对LocalVarsBinding 的代码加上注释:

public class LocalVarsBinding extends Binding{

    @Override
    public void pushOntoStack(InsnList instructions, BindingContext bindingContext) {

        // 获取当前绑定位置的第一条指令
        AbstractInsnNode currentInsnNode = bindingContext.getLocation().getInsnNode();
        // 获取当前指令位置上有效的本地变量列表
        List<LocalVariableNode> results = AsmOpUtils
                .validVariables(bindingContext.getMethodProcessor().getMethodNode().localVariables, currentInsnNode);

        // 创建对象数组:new Object[ result.size() ]
        AsmOpUtils.push(instructions, results.size());
        AsmOpUtils.newArray(instructions, AsmOpUtils.OBJECT_TYPE);

        // 遍历本地变量列表,将每个变量添加到上面的Object数组中
        for (int i = 0; i < results.size(); ++i) {
            // dup : 复制栈顶最后一条指令,即上面的 object array ref
            AsmOpUtils.dup(instructions);

            // 写入的数组 index
            AsmOpUtils.push(instructions, i);

            // 读取本地变量,压入栈顶
            LocalVariableNode variableNode = results.get(i);
            AsmOpUtils.loadVar(instructions, Type.getType(variableNode.desc), variableNode.index);
            // 将primitive 类型转换为box对象
            AsmOpUtils.box(instructions, Type.getType(variableNode.desc));

            // 保存栈顶的变量到数组
            AsmOpUtils.arrayStore(instructions, AsmOpUtils.OBJECT_TYPE);
        }

    }

    @Override
    public Type getType(BindingContext bindingContext) {
        return AsmOpUtils.OBJECT_TYPE;
    }

}

currentInsnNode 为此绑定类开始处理的第一条指令,是在处理@AtEnter,@AtLine,@AtExit 等注解时进行匹配获得的,与本文主题无太大关系,这里不做展开。

下面顺着代码依次展开,追本溯源,力图从JVM字节码、ASM两个层面讲明白。

读取有效的本地变量列表

首先看一下AsmOpUtils.validVariables()方法,到底是怎么获取到当前指令位置可以访问的本地变量列表。

public static List<LocalVariableNode> validVariables(List<LocalVariableNode> localVariables,
        AbstractInsnNode currentInsnNode) {
    List<LocalVariableNode> results = new ArrayList<LocalVariableNode>();

    // find out current valid local variables
    for (LocalVariableNode localVariableNode : localVariables) {
        // 判断[start - end) 指令链表是否包含currentInsnNode 指令
        for (AbstractInsnNode iter = localVariableNode.start; iter != null
                && (!iter.equals(localVariableNode.end)); iter = iter.getNext()) {
            if (iter.equals(currentInsnNode)) {
                results.add(localVariableNode);
                break;
            }
        }
    }
    return results;
}

查看ASM LocalVariableNode 类的属性,可以看到 start 为第一条可以访问此变量的指令(包含),end 为最后一条指令(不包含,即end指令不能访问此变量):

public class LocalVariableNode {

  /** The name of a local variable. */
  public String name;

  /** The first instruction corresponding to the scope of this local variable (inclusive). */
  public LabelNode start;

  /** The last instruction corresponding to the scope of this local variable (exclusive). */
  public LabelNode end;

  /** The local variable's index. */
  public int index;

不着急往下看,我们回顾一下JVM字节码中的LocalVariableTable,分析下面样例代码的LocalVariableTable:Start 为当前Method字节码偏移位置,Length 为变量有效的范围,Slot 为变量槽, Name为变量名,Signature为变量类型签名。

public void test1(int a, long b, double c, String d) {
    int var1 = a;
    long var2 = b;
    String var3 = d;
}
  public void test1(int, long, double, java.lang.String);
    descriptor: (IJDLjava/lang/String;)V
    flags: ACC_PUBLIC
    Code:
      stack=2, locals=11, args_size=5
         0: iload_1
         1: istore        7
         3: lload_2
         4: lstore        8
         6: aload         6
         8: astore        10
        10: return
      LineNumberTable:
        line 10: 0
        line 11: 3
        line 12: 6
        line 14: 10
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0      11     0  this   Lcom/taobao/arthas/bytekit/asm/interceptor/LocalVarsTest;
            0      11     1     a   I
            0      11     2     b   J
            0      11     4     c   D
            0      11     6     d   Ljava/lang/String;
            3       8     7  var1   I
            6       5     8  var2   J
           10       1    10  var3   Ljava/lang/String;

借用一句话来描述:本地变量的访问范围刚好符合栈后进先出的特点,前面定义的变量的作用范围比后面的要大,后面定义的变量范围比前面的小。当指令的偏移位置满足本地变量的[Start, Start + Length) 时,可以访问此本地变量。

到这里疑问就来了:为什么ASM的LocalVariableNode 的start, end是指令引用,而不是指令偏移位置?

从指令解析的角度来说,直接使用指令偏移位置是最简单的,但不要忘记了,asm的主要目标是提供更轻量级的字节码修改功能,一旦字节码被修改,原来的指令偏移位置就失去意义。我的理解是与其每次修改指令都修改本地变量表的偏移位置,还不如使用指令的引用更加容易维护这个变量作用范围。asm在生成最终字节码时,会自动重建 LocalVariableTable。

讲完本地变量表及asm的封装后,就很容易想到,LocalVariableNode的[start - end)的指令链表包含的所有指令都可以访问这个变量。所以要判断某个指令S是否可以访问本地变量V,只需要判断指令S是否包含在在本地变量V的[start - end)的指令链表中。

创建Object数组

本节分析的代码片段:

  // 创建对象数组:new Object[ result.size() ]
  AsmOpUtils.push(instructions, results.size());
  AsmOpUtils.newArray(instructions, AsmOpUtils.OBJECT_TYPE);

AsmOpUtils.newArray() 的代码:

public static void newArray(final InsnList insnList, final Type type) {
    insnList.add(new TypeInsnNode(Opcodes.ANEWARRAY, type.getInternalName()));
}

从JVM Opcode Reference 查询anewarray指令:

Description: allocate a new array of objects
Stack

BeforeAfter
sizearray ref

Example
Java---------------------
String x = new String [3]
Jasm-------------------
bipush 3 ; set size
anewarray java/lang/String ; make a new String array
astore_1 ; and stick it in variable 1

这个指令很好理解,将栈顶的 size取出,作为新建数组的长度,然后将创建的数组对象引用压入栈顶。就是说要先将数组长度压入栈,然后调用anewarray指令(参数为数组元素类型)。本节分析的两行代码相当于 new Object[ result.size() ]

数组元素赋值

本节分析的代码片段:

    // dup : 复制栈顶最后一条指令,即上面的 object array ref
    AsmOpUtils.dup(instructions);

    // 写入的数组 index
    AsmOpUtils.push(instructions, i);

    // 读取本地变量,压入栈顶
        ...

    // 保存栈顶的变量到数组
    AsmOpUtils.arrayStore(instructions, AsmOpUtils.OBJECT_TYPE);

1)AsmOpUtils.dup(instructions)
dup 指令的文档

Description: duplicate top of stack
Stack

BeforeAfter
whateverwhatever

whatever

这个指令将栈顶的数据项复制一条,且将复制的数据项压入栈顶。上面的文档中栈变化可能有点不清晰,实际上是栈顶增长了,表达为下面的方式肯更加恰当:

Before
whatever
others
After
whatever
whatever
others

大家了解文档中栈比较表格的意思后也不影响理解,栈底部空白就是其它数据项,后面文档中的栈变化不再单独列举。本节第一行AsmOpUtils.dup(instructions) 复制当前栈顶的数据项,其实就是刚刚创建的Object数组引用。

当前栈内容为:

Stack
object array ref (dup)
object array ref

2)AsmOpUtils.push(instructions, i)

    public static void push(InsnList insnList, final int value) {
        if (value >= -1 && value <= 5) {
            // 优先使用常量指令压栈,不需要参数( iconst_0, iconst_1, ..., iconst_5 )
            insnList.add(new InsnNode(Opcodes.ICONST_0 + value));
        } else if (value >= Byte.MIN_VALUE && value <= Byte.MAX_VALUE) {
            // byte 范围,bipush 压入
            insnList.add(new IntInsnNode(Opcodes.BIPUSH, value));
        } else if (value >= Short.MIN_VALUE && value <= Short.MAX_VALUE) {
            // short范围,sipush 压入
            insnList.add(new IntInsnNode(Opcodes.SIPUSH, value));
        } else {
            // 超出short范围,使用加载常量指令ldc
            insnList.add(new LdcInsnNode(value));
        }
    }

看起来代码很多,其实就是将value压入栈顶,这个方法封装了不同的场景处理逻辑,简化上层调用。
当前栈内容为:

Stack
index
object array ref (dup)
object array ref

3)AsmOpUtils.arrayStore(instructions, AsmOpUtils.OBJECT_TYPE)
这里的数组是Object[],对应的是aastore 指令:

Description: store value in array[index]
Stack

BeforeAfter
value
index
array

将value保存到array[index], 注意栈的数据顺序,栈顶为value,接着是index,接着是array引用。本节的场景中,遍历读取本地变量放到栈上,然后保存到数组指定位置中。
每次循环都是相同的处理过程:

  • 用dup指令复制栈顶数据项( object array ) ,栈顶 +1 数据项

  • push 数组索引index,栈顶 +1 数据项

  • load var,栈顶 +1 数据项

  • aastore 指令保存 var到数组,消耗栈顶3条数据项,恢复到循环前的栈状态

加载本地变量

本节分析的代码片段:

// 读取本地变量,压入栈顶
LocalVariableNode variableNode = results.get(i);
AsmOpUtils.loadVar(instructions, Type.getType(variableNode.desc),  variableNode.index);

AsmOpUtils.loadVar() 方法的代码如下:

public static void loadVar(final InsnList instructions, Type type, final int index) {
    instructions.add(new VarInsnNode(type.getOpcode(Opcodes.ILOAD), index));
}

com.alibaba.arthas.deps.org.objectweb.asm.Type#getOpcode() 代码如下:

/**
* Returns a JVM instruction opcode adapted to this {@link Type}. This method must not be used for
* method types.
*
* @param opcode a JVM instruction opcode. This opcode must be one of ILOAD, ISTORE, IALOAD,
*     IASTORE, IADD, ISUB, IMUL, IDIV, IREM, INEG, ISHL, ISHR, IUSHR, IAND, IOR, IXOR and
*     IRETURN.
* @return an opcode that is similar to the given opcode, but adapted to this {@link Type}. For
*     example, if this type is {@code float} and {@code opcode} is IRETURN, this method returns
*     FRETURN.
*/
public int getOpcode(final int opcode) {
...
    switch (sort) {
    case VOID:
      if (opcode != Opcodes.IRETURN) {
        throw new UnsupportedOperationException();
      }
      return Opcodes.RETURN;
    case BOOLEAN:
    case BYTE:
    case CHAR:
    case SHORT:
    case INT:
      return opcode;
    case FLOAT:
      return opcode + (Opcodes.FRETURN - Opcodes.IRETURN);
    case LONG:
      return opcode + (Opcodes.LRETURN - Opcodes.IRETURN);
    case DOUBLE:
      return opcode + (Opcodes.DRETURN - Opcodes.IRETURN);
    case ARRAY:
    case OBJECT:
    case INTERNAL:
      if (opcode != Opcodes.ILOAD && opcode != Opcodes.ISTORE && opcode != Opcodes.IRETURN) {
        throw new UnsupportedOperationException();
      }
      return opcode + (Opcodes.ARETURN - Opcodes.IRETURN);
    case METHOD:
      throw new UnsupportedOperationException();
    default:
      throw new AssertionError();
  }
...
}

上面代码作用是获取type实例对应的opcode,用iload举例说明,下面是不同类型对应的指令:

typeinststack size
intiload1
floatfload1
objectaload1
doubledload2
longlload2

stack size 是占用的栈大小,2表示占用两个位置,我们看一下 iload 和 lload的文档。
iload的文档:

Description: push integer from the local variable
Stack

BeforeAfter

integer

lload的文档:

Description: push long from local variable n
Stack

BeforeAfter

long word1

long word2

分析了部分JVM字节码指令,发现涉及到long/double的指令的数据大都是2个word,其它的int, float, object指令的数据占1个word,不清楚的话查看Opcode手册。
有上面的知识基础后,可以知道加载局部变量的数据是不定长度的,可能为1个word或者2个word。其中2个word的数据占用2个数据项,不满足上一小节aastore指令的循环要求,当然从语法上也是错误的,Object [] 不能直接赋值int/long,需要转换为box包装类型。

当前栈内容,如果local var type为 byte/short/int/float/object等 :

Stack
local var
index
object array ref
object array ref

如果local var type为 long/double ( type size 为2):

Stack
local var word1
local var word2
index
object array ref
object array ref

将primitive 类型转换为box对象

本节分析的代码片段:

// 将primitive 类型转换为box对象
AsmOpUtils.box(instructions, Type.getType(variableNode.desc));

AsmOpUtils.box() 代码如下:

public static void box(final InsnList instructions, Type type) {
    // 本身为对象类型,不需要包装
    if (type.getSort() == Type.OBJECT || type.getSort() == Type.ARRAY) {
        return;
    }

    if (type == Type.VOID_TYPE) {
        // push null
        instructions.add(new InsnNode(Opcodes.ACONST_NULL));
    } else {
        // 获取primitive type的包装类型
        Type boxed = getBoxedType(type);
        // new instance. 创建包装类型对象
        newInstance(instructions, boxed);
        if (type.getSize() == 2) {
            // Pp -> Ppo -> oPpo -> ooPpo -> ooPp -> o
            // dupX2
            dupX2(instructions);
            // dupX2
            dupX2(instructions);
            // pop
            pop(instructions);
        } else {
            // p -> po -> opo -> oop -> o
            // dupX1
            dupX1(instructions);
            // swap
            swap(instructions);
        }
        invokeConstructor(instructions, boxed, new Method("<init>", Type.VOID_TYPE, new Type[] { type }));
    }
}

这是本文最为神奇的一段代码,其中 dupX2 - dupX2 - pop以及它的注释着实让人摸不着头脑,后面会深入解读。我们先分析下面两行代码:

// 获取primitive type的包装类型
Type boxed = getBoxedType(type);
// new instance. 创建包装类型对象
newInstance(instructions, boxed);

AsmOpUtils.getBoxedType() 及 newInstance() 代码如下:

// 返回type对应的包装类型
public static Type getBoxedType(final Type type) {
    switch (type.getSort()) {
    case Type.BYTE:
        return BYTE_TYPE;
    case Type.BOOLEAN:
        return BOOLEAN_TYPE;
    case Type.SHORT:
        return SHORT_TYPE;
    case Type.CHAR:
        return CHARACTER_TYPE;
    case Type.INT:
        return INTEGER_TYPE;
    case Type.FLOAT:
        return FLOAT_TYPE;
    case Type.LONG:
        return LONG_TYPE;
    case Type.DOUBLE:
        return DOUBLE_TYPE;
    }
    return type;
}

// 使用new指令创建对象,将对象的引用压入栈顶
public static void newInstance(final InsnList instructions, final Type type) {
    instructions.add(new TypeInsnNode(Opcodes.NEW, type.getInternalName()));
}

new 指令文档:

Description: create a new object
Stack

BeforeAfter

object reference

分支1:local var为1个word

接下来我们先看 type.getSize() == 1 分支,即local var为1个word,记住当前栈的内容:

Stack
box object ref
local var
index
object array ref
object array ref

再看 type.getSize() == 1 分支的处理代码:

// p -> po -> opo -> oop -> o
// dupX1
dupX1(instructions);
// swap
swap(instructions);

1) dup_x1指令文档:

Description: duplicate top word of stack and put duplicate beneath second word of stack
Stack

BeforeAfter
whatever1whatever1
whatever2whatever2

whatever1

这个指令的作用是复制栈顶第1个word,插入到第2个word下面。结合本小节的stack内容,执行dup_x1指令后,栈内容变为:

Stack
box object ref
local var
box object ref (dup_x1 插入)
index
object array ref
object array ref

2) 接着执行swap(instructions)

public static void swap(final InsnList insnList) {
    insnList.add(new InsnNode(Opcodes.SWAP));
}

swap指令,其文档如下:

Description: swap top two words on stack
Stack

BeforeAfter
onetwo
twoone

栈内容变为:

Stack
local var (swap 交换)
box object ref (swap 交换)
box object ref (dup_x1 插入)
index
object array ref
object array ref

3) 接着执行invokeConstructor()语句:

invokeConstructor(instructions, boxed, new Method("<init>", Type.VOID_TYPE, new Type[] { type }))

invokeConstructor()方法的代码:

public static void invokeConstructor(final InsnList instructions, final Type type, final Method method) {
    String owner = type.getSort() == Type.ARRAY ? type.getDescriptor() : type.getInternalName();
    instructions.add(new MethodInsnNode(Opcodes.INVOKESPECIAL, 
        owner, method.getName(), method.getDescriptor(), false));
}

即插入invokespecial 指令,其文档为:

Description: calls a special class method.
n is the number of arguments to the method.
the long method name is really a path name, the name of the class,
the parenthesized argument list of the method called, and the return type.
Primitive types are represented by their capitalized first letter, ie I for an integer.
Constructors are path followed by <init>()V

Stack

BeforeAfter
arg nreturned value

arg 1
object reference

Example
Jasm-------------------
invoke packages/myMath/multiplyMatrix([[F, [[F)V ;multiplies two two-dimensional matrices of floats

invokespecial 指令将栈顶的参数及对象引用依次出栈,然后将调用结果入栈。出栈顺序与代码书写顺序相反,从右至左的顺序出栈,反之,入栈顺序为从左至右与代码书写顺序一致。

Invoke instance method; special handling for superclass, private, and instance initialization method invocations

invokespecial 用于调用实例构造方法;对超类,私有和实例初始化方法调用的特殊处理。invokespecial 不仅是调用构造函数,还有其它特殊用法,如果要深入了解,可以参考此文档。

invokespecial 指令需要指定构造函数方法签名,以java.lang.Boolean为例,调用构造函数的指令参数:Method java/lang/Boolean."<init>":(Z)V
<init> 为构造函数方法名称,(Z) 为参数类型列表,Z 为boolean参数类型,V 为返回值类型void。

Section 4.3.2 of the JVM Spec:

Character     Type          Interpretation
------------------------------------------
B             byte          signed byte
C             char          Unicode character
D             double        double-precision floating-point value
F             float         single-precision floating-point value
I             int           integer
J             long          long integer
L<classname>; reference     an instance of class 
S             short         signed short
Z             boolean       true or false
[             reference     one array dimension

这里的invokespecial 调用的是构造函数,无返回值入栈(关于这点,欢迎补充说明材料)。box对象构造函数只有1个参数,指令执行后栈内容变为:

Stack
box object ref (dup_x1 插入)
index
object array ref
object array ref

4) 执行语句AsmOpUtils.arrayStore(instructions, AsmOpUtils.OBJECT_TYPE)

参考前一小节 “数组元素赋值”

执行aastore指令,取出栈顶3个word,保存box object ref到数组中指定位置,栈内容恢复到循环遍历本地变量之前的状态。执行后的栈状态为:

Stack
object array ref

建议仔细阅读本小节内容,完全掌握后再看分支2的内容。

分支2:local var 为2个word

分支1已经包含了读取本地变量、保存到Object[]数组的整个流程,分支2是针对local var 为2个word(type size 为2)的情况进行特殊说明。本小节分析的代码片段:

if (type.getSize() == 2) {
    // Pp -> Ppo -> oPpo -> ooPpo -> ooPp -> o
    // dupX2
    dupX2(instructions);
    // dupX2
    dupX2(instructions);
    // pop
    pop(instructions);
}

dup_x2指令文档:

Description: duplicate top word of stack and put duplicate beneath third word of stack
Stack

BeforeAfter
whatever1whatever1
whatever2whatever2
whatever3whatever3

whatever1

dup_x2指令复制栈顶第一个word,插入到第3个word下面。单独看这个指令是比较难理解其用法,结合本小节的local var 为2个word来理解,一下就豁然开朗了。

dup_x2之前的栈状态为(请回顾“加载本地变量”小节最后结论):

Stack
box object ref (newInstance)
local var word1
local var word2
index
object array ref
object array ref

执行第一个dup_x2指令后的栈内容:

Stack
box object ref (newInstance)
local var word1
local var word2
box object ref (dup_x2)
index
object array ref
object array ref

呵呵,是不是很惊喜,dup_x2刚好解决了local var 2 word的问题,其作用和分支1的dup_x1类似,在local var后面插入box对象引用。
但这里的local var 是2 word,继续使用swap指令达不到交换 local var 和 box object ref 的目的,于是有了第2个dup_x2指令,执行后栈内容为:

Stack
box object ref (newInstance)
local var word1
local var word2
box object ref (dup_x2 #2)
box object ref (dup_x2 #1)
index
object array ref
object array ref

再执行pop 指令,移除栈顶多余的box object ref ,dup_x2 + pop 合起来的作用就是交换了栈顶的box object ref 和 2个word的 local var !

Stack
local var word1
local var word2
box object ref (dup_x2 #2)
box object ref (dup_x2 #1)
index
object array ref
object array ref

到这里,栈的内容已经准备好,后面继续执行下面两行语句:

调用box对象构造函数初始化,从栈取出 local var 2 word 和 第一个box object ref

// 调用box对象构造函数初始化
invokeConstructor(instructions, boxed, new Method("<init>", Type.VOID_TYPE, new Type[] { type }));

当前栈数据:

Stack
box object ref (dup_x2 #1)
index
object array ref
object array ref

保存栈顶的变量到数组,从栈取出第二个box object ref和后面的indexobject array ref,栈顶为剩下的一个object array ref,恢复到遍历本地变量之前的状态。

// 保存栈顶的变量到数组
AsmOpUtils.arrayStore(instructions, AsmOpUtils.OBJECT_TYPE);

当前栈数据:

Stack
object array ref

至此,分支2执行后的栈状态与分支1是一致的,不影响后续遍历执行。后面有完整的代码及反汇编的字节码,可以对照理解。本文尽量深入细节讲解每一部分,可能导致整体连贯性不足,可以动手整理一下,画出每个语句执行后的栈内容,这样可以更加清晰的理解认识。

完整示例代码

1)ByteKit 代码:

public class LocalVarsDemo extends AbstractDemo {

    public static class SampleInterceptor {

        @AtLine(lines = {-1}, inline = false, suppressHandler = PrintExceptionSuppressHandler.class)
        public static void atEnter(@Binding.This Object object,
                                   @Binding.LocalVars Object[] vars,
                                   @Binding.MethodName String methodName) {
            System.out.println("atEnter, vars: " + Arrays.asList(vars)+", vars len: "+vars.length);
        }

    }

    public void testMain() throws Exception {

        // 对Sample.class字节码增强,返回增强后的字节码
        byte[] bytes = super.enhanceClass(Sample.class, new String[]{"hello"}, SampleInterceptor.class);

        //增强前测试
        System.out.println("Before reTransform ...");
        new Sample().hello("world", 50, 1.0 );

        // 通过 reTransform 增强类
        AgentUtils.reTransform(Sample.class, bytes);

        //测试结果
        System.out.println("After reTransform ...");
        new Sample().hello("world", 50, 1.0 );
    }

    public static void main(String[] args) throws Exception {
        new LocalVarsDemo().testMain();
    }

}
public class AbstractDemo {

    protected AbstractDemo() {
        AgentUtils.install();
    }

    protected byte[] enhanceClass(Class targetClass, String[] targetMethodNames, Class interceptorClass) throws Exception {
        // 解析定义的 Interceptor类 和相关的注解
        DefaultInterceptorClassParser interceptorClassParser = new DefaultInterceptorClassParser();
        List<InterceptorProcessor> processors = interceptorClassParser.parse(interceptorClass);

        // 加载字节码
        ClassNode classNode = AsmUtils.loadClass(targetClass);

        List<String> methodNameList = Arrays.asList(targetMethodNames);

        // 对加载到的字节码做增强处理
        for (MethodNode methodNode : classNode.methods) {
            if (methodNameList.contains(methodNode.name)) {
                MethodProcessor methodProcessor = new MethodProcessor(classNode, methodNode);
                for (InterceptorProcessor interceptor : processors) {
                    interceptor.process(methodProcessor);
                }
            }
        }

        // 获取增强后的字节码
        byte[] bytes = AsmUtils.toBytes(classNode);
        return bytes;
    }

    public static class PrintExceptionSuppressHandler {

        @ExceptionHandler(inline = true)
        public static void onSuppress(@Binding.Throwable Throwable e, @Binding.Class Object clazz) {
            System.out.println("exception handler: " + clazz);
            e.printStackTrace();
        }
    }
}

2)目标类原始代码:

 1 package com.example;
 2 
 3 public class Sample {
 4
 5   public String hello(String str, long num1, double num2) {
 6        Long num3 = 0L;
 7        num3 += num1;
 8        String result = "hello " + str;
 9        return result;
10   }
11
12 }

3)目标类原始字节码

  public java.lang.String hello(java.lang.String, long, double);
    descriptor: (Ljava/lang/String;JD)Ljava/lang/String;
    flags: ACC_PUBLIC
    Code:
      stack=4, locals=8, args_size=4
         0: lconst_0
         1: invokestatic  #2                  // Method java/lang/Long.valueOf:(J)Ljava/lang/Long;
         4: astore        6
         6: aload         6
         8: invokevirtual #3                  // Method java/lang/Long.longValue:()J
        11: lload_2
        12: ladd
        13: invokestatic  #2                  // Method java/lang/Long.valueOf:(J)Ljava/lang/Long;
        16: astore        6
        18: new           #4                  // class java/lang/StringBuilder
        21: dup
        22: invokespecial #5                  // Method java/lang/StringBuilder."<init>":()V
        25: ldc           #6                  // String hello
        27: invokevirtual #7                  // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
        30: aload_1
        31: invokevirtual #7                  // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
        34: invokevirtual #8                  // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
        37: astore        7
        39: aload         7
        41: areturn
      LineNumberTable:
        line 6: 0
        line 7: 6
        line 8: 18
        line 9: 39
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0      42     0  this   Lcom/example/Sample;
            0      42     1   str   Ljava/lang/String;
            0      42     2  num1   J
            0      42     4  num2   D
            6      36     6  num3   Ljava/lang/Long;
           39       3     7 result   Ljava/lang/String;

4)目标类增强后的字节码
javap生成的内容太长,节选前面的部分。

  public java.lang.String hello(java.lang.String, long, double);
    descriptor: (Ljava/lang/String;JD)Ljava/lang/String;
    flags: ACC_PUBLIC
    Code:
      stack=9, locals=16, args_size=4
         0: aload_0
         1: iconst_4
         2: anewarray     #4                  // class java/lang/Object
         5: dup
         6: iconst_0
         7: aload_0
         8: aastore
         9: dup
        10: iconst_1
        11: aload_1
        12: aastore
        13: dup
        14: iconst_2
        15: lload_2
        16: new           #17                 // class java/lang/Long
        19: dup_x2
        20: dup_x2
        21: pop
        22: invokespecial #20                 // Method java/lang/Long."<init>":(J)V
        25: aastore
        26: dup
        27: iconst_3
        28: dload         4
        30: new           #22                 // class java/lang/Double
        33: dup_x2
        34: dup_x2
        35: pop
        36: invokespecial #25                 // Method java/lang/Double."<init>":(D)V
        39: aastore
        40: ldc           #26                 // String hello
        42: invokestatic  #32                 // Method com/example/LocalVarsDemo$SampleInterceptor.atEnter:(Ljava/lang/Object;[Ljava/lang/Object;Ljava/lang/String;)V
        45: goto          88
        48: ldc           #2                  // class com/example/Sample
        50: astore        9
        52: astore        8
        54: getstatic     #38                 // Field java/lang/System.out:Ljava/io/PrintStream;
        57: new           #40                 // class java/lang/StringBuilder
        60: dup
        61: invokespecial #41                 // Method java/lang/StringBuilder."<init>":()V
        64: ldc           #43                 // String exception handler:
        66: invokevirtual #47                 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
        69: aload         9
        71: invokevirtual #50                 // Method java/lang/StringBuilder.append:(Ljava/lang/Object;)Ljava/lang/StringBuilder;
        74: invokevirtual #54                 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
        77: invokevirtual #60                 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
        80: aload         8
        82: invokevirtual #63                 // Method java/lang/Throwable.printStackTrace:()V
        85: goto          88
        88: lconst_0
        89: invokestatic  #67                 // Method java/lang/Long.valueOf:(J)Ljava/lang/Long;
        92: astore        6
        94: aload_0
        95: iconst_5
        96: anewarray     #4                  // class java/lang/Object
        99: dup
       100: iconst_0
       101: aload_0
       102: aastore
       103: dup
       104: iconst_1
       105: aload_1
       106: aastore
       107: dup
       108: iconst_2
       109: lload_2
       110: new           #17                 // class java/lang/Long
       113: dup_x2
       114: dup_x2
       115: pop
       116: invokespecial #20                 // Method java/lang/Long."<init>":(J)V
       119: aastore
...
      LineNumberTable:
        line 6: 0
        line 7: 94
        line 8: 199
        line 9: 313
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0     415     0  this   Lcom/example/Sample;
            0     415     1   str   Ljava/lang/String;
            0     415     2  num1   J
            0     415     4  num2   D
           94     321     6  num3   Ljava/lang/Long;
          313     102     7 result   Ljava/lang/String;

5)目标类增强后的字节码反编译得到的源码

package com.example;

import com.example.LocalVarsDemo.SampleInterceptor;

public class Sample {
   public String hello(String str, long num1, double num2) {
      try {
         SampleInterceptor.atEnter(this, new Object[]{this, str, new Long(num1), new Double(num2)}, "hello");
      } catch (Throwable var19) {
         Class var9 = Sample.class;
         System.out.println("exception handler: " + var9);// 6
         var19.printStackTrace();
      }

      Long num3 = 0L;

      try {
         SampleInterceptor.atEnter(this, new Object[]{this, str, new Long(num1), new Double(num2), num3}, "hello");
      } catch (Throwable var18) {
         Class var11 = Sample.class;
         System.out.println("exception handler: " + var11);
         var18.printStackTrace();
      }

      num3 = num3 + num1;// 7

      try {
         SampleInterceptor.atEnter(this, new Object[]{this, str, new Long(num1), new Double(num2), num3}, "hello");
      } catch (Throwable var17) {
         Class var13 = Sample.class;
         System.out.println("exception handler: " + var13);// 8
         var17.printStackTrace();
      }

      String result = "hello " + str;

      try {
         SampleInterceptor.atEnter(this, new Object[]{this, str, new Long(num1), new Double(num2), num3, result}, "hello");
      } catch (Throwable var16) {
         Class var15 = Sample.class;
         System.out.println("exception handler: " + var15);// 9
         var16.printStackTrace();
      }

      return result;
   }
}

参数绑定(ArgsBinding)

参数绑定的核心代码片段:

public static void loadArgArray(final InsnList instructions, MethodNode methodNode) {
    boolean isStatic = AsmUtils.isStatic(methodNode);
    // 获取参数类型数组
    Type[] argumentTypes = Type.getArgumentTypes(methodNode.desc);
    // 创建数组 Object[argumentTypes.length]
    push(instructions, argumentTypes.length);
    newArray(instructions, OBJECT_TYPE);
    // 变量参数类型列表
    for (int i = 0; i < argumentTypes.length; i++) {
        // 复制 object array ref
        dup(instructions);
        // 数组index
        push(instructions, i);
        // 加载指定参数var
        loadArg(isStatic, instructions, argumentTypes, i);
        // 转换为box对象
        box(instructions, argumentTypes[i]);
        // 保存到数组
        arrayStore(instructions, OBJECT_TYPE);
    }
}

public static void loadArg(boolean staticAccess, final InsnList instructions, Type[] argumentTypes, int i) {
    // 计算参数i在的LocalVariableTable 的index
    final int index = getArgIndex(staticAccess, argumentTypes, i);
    final Type type = argumentTypes[i];
    instructions.add(new VarInsnNode(type.getOpcode(Opcodes.ILOAD), index));
}

static int getArgIndex(boolean staticAccess, final Type[] argumentTypes, final int arg) {
    //非静态方法的第一个本地变量为对象自身的引用 this
    int index = staticAccess ? 0 : 1;
    for (int i = 0; i < arg; i++) {
          // 变量类型size( long/double 为2,其它为1 )
        index += argumentTypes[i].getSize();
    }
    return index;
}

处理过程与本地变量非常相似,唯一的区别是加载参数变量的方式不同。其中loadArg() 中需要计算参数i在的LocalVariableTable 的index (slot),我们对比下面的方法签名及LocalVariableTable:

    public String hello(String str, long num1, double num2)
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0     415     0  this   Lcom/example/Sample;
            0     415     1   str   Ljava/lang/String;
            0     415     2  num1   J
            0     415     4  num2   D
           94     321     6  num3   Ljava/lang/Long;
          313     102     7 result   Ljava/lang/String;
  • slot 0:this 引用,Object类型,type size=1

  • slot 1:参数 str,String类型,type size=1

  • slot 2:参数 num1,long类型,type size=2

  • slot 4:参数 num2,double类型,type size=2

  • slot 6:局部变量num3,Long类型,type size=1

  • slot 7:局部变量result,String类型,type size=1

可以看到方法参数和方法体局部变量都是定义在LocalVariableTable中,按照slot排序后:

  • this (静态方法无this)

  • 参数1

  • 参数n

  • 局部变量1

  • 局部变量n

分析发现asm LocalVariableNode.index的值实际上就是slot的值 (关于这点,欢迎补充说明材料),参数n+1的slot = 参数n slot + 参数n类型size。注意局部变量num3为Long,box类型,类型size为1(按照OBJECT类型来计算)。

相关资源

推荐一个JVM字节码指令手册,包含每个指令栈变化的描述:JVM Opcode Reference

结论

Arthas ByteKit 本地变量绑定实现逻辑是按需动态插入Java字节码,不同于常见的asm ClassReader/ClassWriter + ClassVisitor/MethodVisitor处理模式。动态插入Java字节码优点是比Visitor模式直观,可控性更强,缺点是要求对Java字节码有比较深厚的功力。Arthas ByteKit 本身只是提供10多种特定模式的变量绑定,有较好的通用性,只要保证每处修改字节码的功能稳定,整体来说ByteKit框架的代码质量是可以保证的。

已标记关键词 清除标记
©️2020 CSDN 皮肤主题: 大白 设计师:CSDN官方博客 返回首页