[英]ASM skips Classes if COMPUTE_FRAMES is set in ClassWriter
我一直在研究與maven-surfire-plugin一起運行的Java代理。 代理應該能夠使用ASM庫在三個不同的點將方法調用注入到已加載的方法中:1)在每個方法的開頭; 2)在每種方法的末尾; 3)在某些行(見下文)。 為此,我實現了一個premain方法,該方法向Java工具中添加了一個新的轉換器。 然后,transform方法為應轉換的每個類創建一個新的ClassWriter和ClassVisitor(屬於ASM庫)。
@Override
public void visitLineNumber(int line, Label start) {
if(methodLines.first().equals(line)) {
mv.visitMethodInsn(INVOKESTATIC, "de/ugoe/cs/listener/CallHelper", "raiseDepth", "()V", false);
}
if(mutationLines != null && mutationLines.contains(line)) {
mv.visitLdcInsn(fqn);
mv.visitLdcInsn(new Integer(line));
mv.visitMethodInsn(INVOKESTATIC, "de/ugoe/cs/listener/CallHelper", "hitMutation", "(Ljava/lang/String;I)V", false);
}
mv.visitLineNumber(line, start);
if(methodLines.last().equals(line)) {
mv.visitMethodInsn(INVOKESTATIC, "de/ugoe/cs/listener/CallHelper", "lowerDepth", "()V", false);
}
}
不幸的是,我對此有些麻煩。 如果為ClassWriter
設置了COMPUTE_FRAMES
標志,則不會出現任何錯誤,但是某些類將被代理跳過並且不進行轉換。 經過一番研究,我發現這樣做的原因(很可能是)ClassWriter的getCommonSuperClass
方法,該方法預先加載了該類。
如果未設置COMPUTE_FRAMES
標志,則Expected stackmap frame at this location
出現“ Expected stackmap frame at this location
錯誤,但無法解決。
有人對此問題有解決方案嗎?
如此答案中所述 ,ASM計算(最特定)公共超類的方法不一定會重現原始類的堆棧映射框架。 它不僅需要訪問類(可以解決),而且可以訪問類,而原始代碼從未引用過,這是因為原始代碼使用了更抽象的類型或接口類型,或者因為原始框架實際上刪除隨后未使用的值,而不是聲明合並的類型。
因此,最好的方法是根據您所做的代碼修改,基於原始幀計算堆棧映射幀。 對於您的預期用例,這很簡單,因為您無需更改代碼的分支結構,而只需注入代碼即可使堆棧狀態與插入的代碼片段之前的狀態完全相同。
因此,原則上應該只使用原始幀。 為了實現這一目標,不指定COMPUTE_FRAMES
到ClassWriter
並沒有指定SKIP_FRAMES
到ClassReader
。 僅在原始大小小於2的情況下,才需要調整最大堆棧大小,以確保方法參數有足夠的空間。
座席的實際問題來自嘗試使用源代碼行來確定插入呼叫的代碼位置。 為了說明這一點,請考慮以下示例:
public class Example {
public static void main(String[] args) {
for(int i = 0; i < 10; i++) {
System.out.println(i);
}
}
}
我使用以下代碼顯示將對您的訪客進行哪些ASM調用:
public static void main(String[] args) throws IOException {
ClassReader cr = new ClassReader("Example");
cr.accept(new ClassVisitor(Opcodes.ASM5) {
@Override
public MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) {
System.out.println(name+desc);
return new PrintingVisitor();
}
}, 0);
}
static class PrintingVisitor extends MethodVisitor {
final Map<Label,Integer> labels = new HashMap<>();
public PrintingVisitor() {
super(Opcodes.ASM5);
}
private String name(Label label) {
return "label_"+labels.merge(label, labels.size(), (a,b) -> a);
}
@Override public void visitCode() {
System.out.println("visitCode()");
}
@Override public void visitFrame(int type, int nLocal, Object[] local, int nStack, Object[] stack) {
System.out.println("visitFrame()");
}
@Override public void visitLabel(Label label) {
System.out.println("."+name(label));
}
@Override public void visitLineNumber(int line, Label start) {
System.out.println(".line "+line+", "+name(start));
}
@Override public void visitJumpInsn(int opcode, Label label) {
System.out.println(get(opcode)+" "+name(label));
}
@Override public void visitInsn(int opcode) {
System.out.println(get(opcode));
}
@Override
public void visitIincInsn(int var, int increment) {
System.out.println("iinc "+var+", "+increment);
}
@Override public void visitEnd() {
System.out.println();
}
}
static String get(int opcode) {
// for simplification, just the ones we need
switch(opcode) {
case Opcodes.RETURN: return "return";
case Opcodes.ICONST_0: return "iconst_0";
case Opcodes.ILOAD: return "iload";
case Opcodes.IF_ICMPGE: return "if_icmpge";
case Opcodes.GOTO: return "goto";
default: return "<"+opcode+">";
}
}
產生(使用javac
):
main([Ljava/lang/String;)V
visitCode()
.label_0
.line 3, label_0
iconst_0
.label_1
visitFrame()
if_icmpge label_2
.label_3
.line 4, label_3
.label_4
.line 3, label_4
iinc 1, 1
goto label_1
.label_2
.line 6, label_2
visitFrame()
return
.label_5
這表明:
for
循環語句的位置關聯的代碼 visitFrame()
在visitFrame()
之前報告, visitFrame()
描述了循環結束分支目標的堆棧狀態。 label_2
既用於報告源代碼行,又用作if_icmpge
指令的目標。 在將visitLabel
調用委派給ClassWriter
,您要定義分支目標,而分支目標則需要堆棧映射框架,因此visitLabel
和visitFrame
調用之間必須沒有代碼,但必須使用用來插入代碼的visitLineNumber
調用,就在他們之間。 解決方案:
直接在visitCode()
調用中插入代碼以visitCode()
方法的開頭。 那是在其他任何事情發生之前,並且不會與任何后續操作沖突:
@Override public void visitCode() { super.visitCode(); mv.visitMethodInsn(INVOKESTATIC, "de/ugoe/cs/listener/CallHelper", "raiseDepth", "()V", false); }
要在方法末尾注入代碼,只需使用可以終止方法的精確指令即可,即
@Override public void visitInsn(int opcode) { switch(opcode) { case RETURN: case ARETURN: case IRETURN: case LRETURN: case FRETURN: case DRETURN: case ATHROW: mv.visitMethodInsn(INVOKESTATIC, "de/ugoe/cs/listener/CallHelper", "lowerDepth", "()V", false); } super.visitInsn(opcode); }
注意,這在每種情況下都不足以獲得finally
類似的語義來調用該方法。 例如,當被調用的方法引發異常或運行時生成該異常時,例如取消引用null
或除以零時,該方法可能不會被調用,但是您的原始代碼有問題。
為了在任意源代碼行處注入代碼,沒有直接的解決方案。 如圖所示,源代碼行未將1:1映射到字節碼位置,並且報告的位置可能在無法注入的位置。 最好選擇其他准則,例如易於識別的代碼構造,例如已知的方法調用,以在其之前或之后插入。
聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.