内容简介:本文通过分析ByteKit的本地变量绑定(LocalVarsBinding)处理代码,结合Java Opcode手册、asm代码、javap反汇编字节码等工具,深入讲解每个指令的用法及在本场景的实际作用。结合上下文线索,从字节码的角度去理解ByteKit 本地变量绑定的实现过程。相关文章:Arthas ByteKit 为新开发的字节码工具库,基于ASM提供更高层的字节码处理能力,面向诊断/APM领域,不是通用的字节码库。ByteKit期望能提供一套简洁的API,让开发人员可以比较轻松的完成字节码增强。
前言
本文通过分析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
| Before | After |
|---|---|
| size | array 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
| Before | After |
|---|---|
| whatever | whatever |
| 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
| Before | After |
|---|---|
| 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举例说明,下面是不同类型对应的指令:
| type | inst | stack size |
|---|---|---|
| int | iload | 1 |
| float | fload | 1 |
| object | aload | 1 |
| double | dload | 2 |
| long | lload | 2 |
stack size 是占用的栈大小,2表示占用两个位置,我们看一下 iload 和 lload的文档。
iload的文档:
Description: push integer from the local variable
Stack
| Before | After |
|---|---|
| integer |
lload的文档:
Description: push long from local variable n
Stack
| Before | After |
|---|---|
| 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
| Before | After |
|---|---|
| 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
| Before | After |
|---|---|
| whatever1 | whatever1 |
| whatever2 | whatever2 |
| 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
| Before | After |
|---|---|
| one | two |
| two | one |
栈内容变为:
| 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
| Before | After |
|---|---|
| arg n | returned 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
| Before | After |
|---|---|
| whatever1 | whatever1 |
| whatever2 | whatever2 |
| whatever3 | whatever3 |
| 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
和后面的 index
, object 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框架的代码质量是可以保证的。
以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网
猜你喜欢:- Golang Echo数据绑定中time.Time类型绑定失败
- 如何在Symfony的表单中添加一个未绑定字段,否则绑定到一个实体?
- js双向绑定
- 延迟静态绑定——static
- 绑定自定义事件
- angular组件双向绑定
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。
HTML Dog
Patrick Griffiths / New Riders Press / 2006-11-22 / USD 49.99
For readers who want to design Web pages that load quickly, are easy to update, accessible to all, work on all browsers and can be quickly adapted to different media, this comprehensive guide represen......一起来看看 《HTML Dog》 这本书的介绍吧!