前言
在没完成watch功能之前,一直以为watch应该是最好实现的,但是写了快两天才写完!!主要是涉及到的知识点比较多。期间被两个问题卡着:
如何获取到方法的入参类型与值?
如何获取到方法的返回结果的类型与值?
还没看到阿尔萨斯是如何处理这两个问题的,不过在我写的iarthas里,我是通过字节码中的局部变量与操作数栈来解决第一个问题的。第二个问题我是通过字节码指令记录最后一次访问局部变量,因此获取返回结果的局部变量中的索引位置。现在没看懂没事,后文会详细的介绍。先来看一下我实现的watch方法:
1 | 4 |
可以看到一开始方法只是简单的打印出测试输出4 ,1 ,通过 Attach 到目标主机,对目标类的方法进行retransformer,加强了原方法,输出了监控方法的入参、出参的类型与值。github
结合watch与trace应该可以完成Doom中核心的功能:流量数据采集!!
局部变量与操作数栈
我们知道,Java 代码是在线程内部执行的。每个线程都有自己的执行栈,栈由帧组成。每个帧表示一个方法调用:每次调用一个方法时,会将一个新帧压入当前线程的执行栈。当方法返回时,会将这个帧从执行栈中弹出。
每一帧包括两部分:一个局部变量部分和一个操作数栈部分。局部变量部分包含可根据索引以随机顺序访问的变量。由名字可以看出,操作数栈部分是一个栈,其中包含了供字节代码指令用作操作数的值。用下面例子来说明:
1 | public void add(int x,int y){ |
调用add方法时会创建一个帧,一开始会为该帧的局部变量与操作数栈进行初始化,一般局部变量会将当前 this 押入到第0位置,再将两个方法参数 x , y 分别压入到第1和第2的位置,那么初始化好的局部变量就是:【this , x , y】,操作数栈还是为空:【】
add方法对应的字节码操作为如下:
1 | ILOAD 1 //1 |
当执行1,2两个指令的时候,ILOAD 1 会将局部变量的1号位置中的值压入到操作数栈中,ILOAD 2会将局部变量2号位置中的值压入操作数栈中,操作数栈对应的是【x , y】。
IADD会将当前对操作数栈进行出栈的操作,出栈两个,并执行ADD,将结果再压入到操作数栈中,操作数栈现在保存了【x+y】。
ISTORE 3 会对操作数栈进行出栈操作,并将值存储到局部变量的3号位置,最后局部变量对应的是:【this , x , y , x+y】,操作数栈为空:【】。
需要注意:局部变量部分和操作数栈部分中的每个槽(slot)可以保存除 long 和 double 变量之外的任意 Java 值。long 和 double 变量需要两个槽。如果忽略这点,在实现 watch 过程中会采坑的。
获取方法的入参类型与值
假设我要对以下方法进行 watch ,那么第一步我需要知道该方法所有入参的类型先!!我们可以通过 ASM 中visitMethod 来获取方法的描述符
1 | public int doAdd(int x,String s,long l ,Job j,Main n,double d) throws Exception{ |
ClassVisitor.visitMethod方法:
1 | /** |
一些 Java 类型的类型描述符:
因此我们可以获取到doAdd的方法描述符:(ILjava/lang/String;JLagent/Job;Lagent/Main;D)I 对应起来就是(int ,String ,agent.Job , agent.Main ,double),最后一个I表示返回类型为int。
回想一下前面讲到的局部变量和操作数栈,那么在调用 doAdd(int x,String s,long l ,Job j,Main n,double d)
方法时局部变量会初始化为:【this , x , s , l , j , n , d】,那么要打印出这个值,我们只需要把要打印的值的下标 LOAD 到操作数栈就可以了!!比如我要打印参数int x的值,那么只要得到 x 的下标1.
1 |
|
在上一节中我们提到long和double是会占用两个solt的,因此你要打印参数long l时,它的下标是3,但是agent.Job的下标就是5了,因为3,4都被long占用了,因此LOAD 的时候需要计算好参数的下标。 我用index来表示每个参数在局部变量中的下标位置。
另外一点也要注意的是:int类型对应的是ILOAD, double类型对应的DLOAD,即每个参数类型都要有对应的opcodes :
ILOAD, LLOAD, FLOAD, DLOAD 和 ALOAD 指令读取一个局部变量,并将它的值压到操作数栈中。它们的参数是必须读取的局部变量的索引 i。ILOAD 用于加载一个 boolean、byte、char、short或int局部变量。LLOAD、FLOAD和DLOAD分别用于加载long、float或double 值。(LLOAD 和 DLOAD 实际加载两个槽 i 和 i+1)。最后,ALOAD 用于加载任意非基元值,即对 象和数组引用。与之对应,ISTORE、LSTORE、FSTORE、DSTORE 和 ASTORE 指令从操作数栈中弹出一个值,并将它存储在由其索引 i 指定的局部变量中。
具体实现可以在代码上看。
获取方法返回结果类型与值
获取方法返回结果当时被卡了很久!!最后突然想到在执行Return的时候,它会将要返回的值从局部变量中LOAD出来到操作数栈,那么我们将最后操作局部变量visitVarInsn方法中的下标记录下来,这个下标就是返回值在局部变量的下标了!!
methodVisitor.visitLineNumber(18, label10);
methodVisitor.visitVarInsn(ILOAD, 11);
methodVisitor.visitInsn(IRETURN);
下标11就是返回值在局部变量中下标位置,因为return是最好一个操作,所以用lastVar来记录即可:
1 |
|
把局部变量LOAD出来打印一下即可!!
1 |
|
核心代码
WatchMethodVisitor 算是watch功能的核心,完整代码可以参考github
1 | public class WatchMethodVisitor extends MethodVisitor{ |
思考
结合前面的trace 与 watch功能,应该能够记录一次调用链路中完整的数据流量,那么回放功能可以参考tt的实现方式。