我正在透明肮脏检测代理(tdd-agent)中工作。它可以很好地重新定义目标类来实现 setDirty()/isDirty() 并在检测到放置字段时设置它,但我想扩展它以检测对集合中 add() 的调用,例如。 我怎样才能检测到该呼叫?
当使用复杂参数进行调用时,生成的 asm 将字段和对方法的最终调用分开,我不知道如何检测和处理它。
例如:
private List<Foo> listaFoo = new ArrayList<>();
public void testNativeCollections(){
this.listaFoo.add(new Foo("text").setS("otro text"));
}
这个块生成这个asm:
aload 0 // reference to self
getfield test/OuterTarget.listaFoo:java.util.List
new test/Foo
dup
ldc "text" (java.lang.String)
invokespecial test/Foo.<init>(Ljava/lang/String;)V
ldc "otro text" (java.lang.String)
invokevirtual test/Foo.setS(Ljava/lang/String;)Ltest/Foo;
invokeinterface java/util/List.add(Ljava/lang/Object;)Z
pop
我需要将第一个 getfield 与最后一个调用接口配对,以检测此调用将修改对象,并在其之后插入对 setDirty() 的调用。主要问题是 getfield/invoke 之间的代码可能任意长且复杂。
你必须构建java自己的内部(例如,它的私有API,但是,java是开源的,就是这样)静态stackwalk分析代码,这是极其复杂的。一个好处是字节码生成已更改为更适合它(例如,
RET
不再由javac
生成,主要是因为这个原因)。
您需要将存在的每个操作码(而且有很多)映射到它在堆栈上的效果。例如,
ALOAD_0
(ASM 至少有助于扁平化;ALOAD_0
、ALOAD_1,
ALOAD_2,
ALOAD_3, and
ALOAD 常量are all flattened into just
aload` 有点帮助)具有弹出任何内容并推送 1 的效果对象入栈。
进行分析:
// stack at 0
aload 0 // stack at 1
getfield // pops 1, then pushes value. stack at 1, remember: stack[0] = listaFoo
new test/Foo // stack at 2
dup // stack at 3
ldc "text" (java.lang.String) //stack at 4
invokespecial test/Foo.<init>(Ljava/lang/String;)V // stack at 2
ldc "otro text" (java.lang.String) // stack at 3
invokevirtual test/Foo.setS(Ljava/lang/String;)Ltest/Foo; // stack at 2
invokeinterface java/util/List.add(Ljava/lang/Object;)Z // stack at 1
pop
invoke
的所有风格都需要分析签名以了解它对堆栈有什么影响(它拥有的每个参数都有-1堆栈,除了invokestatic
之外,所有其他版本都为隐式接收者参数额外提供-1堆栈 - 然后+1如果其返回类型不是 V
,则堆栈。
在这里,您的分析器应该能够注册最后一个
invokeinterface
是将您感兴趣的字段从堆栈中弹出作为 add
方法的接收者,从而将其限定为“脏”标志。
我认为不存在一个公开的库,并且以用于这样的分析的方式进行维护,但是,我还没有看那么多 - 也许现在你知道要寻找什么了。
JVM 在加载类时会自行进行此类分析。例如,如果您有以下字节码:
ALOAD_0
DUP
GETFIELD someIntField
IADD
验证器将用
VerifyError
中止,整个类甚至永远不会被加载:它分析了 IADD
指令,该指令从堆栈中弹出 2 个东西,如果它们都是整数并且压入,则 [A] 添加它们回到上面,或者 [B] 如果它们不是都是 int 值,那么一切都会崩溃 - 并且它分析这是 B 情况,因为堆栈上的是 this
和一个 int - 而不是 2 个 int。
它是如何做到的?应用相同的原则:分析每条指令并跟踪它对堆栈的作用。在这种情况下,它注册了堆栈上每个事物的类型(
ALOAD_0
- 好吧,stack[0]
中有一个对象。DUP
- 好吧,stack[1]
中有一个对象。GETFIELD someIntField
- 好吧,现在 stack[1]
中有一个 int。IADD
- 好的,检查 stack[current]
和 stack[current-1]
的类型?呃,其中之一是 int
- 验证错误!
你会做同样的事情,除了不跟踪“int”,“object”等,你具体跟踪:“啊,字段这个或那个”,一旦你点击一个方法签名的
INVOKEVIRTUAL
你知道“弄脏”了它的接收器(例如j/u/List.add(...)Z
),你可以使用堆栈分析来确切地知道它弄脏了什么。