Using Javassist, how do I add code to an empty loop?

672 views Asked by At

Using Javassist 3.20.0.GA, I am trying to inject code in to an empty loop.

Everything I try, I keep running in to a java.lang.VerifyError error at the point I attempt to create a new instance of the modified class.

I've attempted to isolate the issue to a small program that fails to run successfully.

import javassist.ClassPool;
import javassist.CtClass;
import javassist.CtMethod;
import javassist.bytecode.Bytecode;
import javassist.bytecode.CodeAttribute;
import javassist.bytecode.CodeIterator;
import javassist.bytecode.InstructionPrinter;
import javassist.bytecode.MethodInfo;
import javassist.compiler.Javac;

public class EmptyLoopTest {
    private void testEmptyLoopModification() throws Exception {
        ClassPool cp = ClassPool.getDefault();
        CtClass cc = cp.get(EmptyLoopTest.class.getName() + "$EmptyLoopClass");
        CtMethod m = cc.getDeclaredMethod("emptyLoopMethod");
        printMethod("Before modifications", m);

        MethodInfo methodInfo = m.getMethodInfo();
        CodeAttribute ca = methodInfo.getCodeAttribute();
        CodeIterator ci = ca.iterator();

        Javac jv = new Javac(cc);
        jv.compileStmnt("System.out.println(\"injected into loop\");");

        Bytecode b = jv.getBytecode();
        adjustCodeAttributeIfNeeded(b, ca);
        ci.insertAt(0, b.get());
        printMethod("After modifications", m);
        Class c = cc.toClass();

        logInfo("Attempting to create instance of modified class");
        c.newInstance();
    }
    private void adjustCodeAttributeIfNeeded(Bytecode b, CodeAttribute ca){
        int locals = b.getMaxLocals();
        int stack = b.getMaxStack();
        if(stack > ca.getMaxStack()) {
            ca.setMaxStack(stack);
        }
        if(locals > ca.getMaxLocals()) {
            ca.setMaxLocals(locals);
        }
    }

    private void printMethod(String title, CtMethod m){
        logInfo(title);
        InstructionPrinter instructionPrinter = new InstructionPrinter(System.out);
        instructionPrinter.print(m);
    }
    private void logInfo(String message){
        System.out.println("");
        System.out.println("------" + message);
    }

    public class EmptyLoopClass {
        public void emptyLoopMethod() {
            for(;;){
            }
        }
    }

    public static void main(String[] args) throws Exception {
        // have errors written to sysout so all output from this program is in order
        System.setErr(System.out);
        new EmptyLoopTest().testEmptyLoopModification();
    }
}

When I run this program, the following is written to the console..

------Before modifications
0: goto 0

------After modifications
0: getstatic #32 = Field java.lang.System.out(Ljava/io/PrintStream;)
3: ldc #34 = "injected into loop"
5: invokevirtual #40 = Method java.io.PrintStream.println((Ljava/lang/String;)V)
8: goto 8

------Attempting to create instance of modified class
Exception in thread "main" java.lang.VerifyError: Expecting a stackmap frame at branch target 8
Exception Details:
  Location:
    EmptyLoopTest$EmptyLoopClass.emptyLoopMethod()V @8: goto
  Reason:
    Expected stackmap frame at this location.
  Bytecode:
    0x0000000: b200 2012 22b6 0028 a700 00            
  Stackmap Table:
    same_frame(@0)

    at java.lang.Class.getDeclaredConstructors0(Native Method)
    at java.lang.Class.privateGetDeclaredConstructors(Class.java:2671)
    at java.lang.Class.getConstructor0(Class.java:3075)
    at java.lang.Class.newInstance(Class.java:412)
    at EmptyLoopTest.testEmptyLoopModification(EmptyLoopTest.java:35)
    at EmptyLoopTest.main(EmptyLoopTest.java:68)
    at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
    at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
    at java.lang.reflect.Method.invoke(Method.java:498)
    at com.intellij.rt.execution.application.AppMain.main(AppMain.java:147)

Process finished with exit code 1

If everything was working as expected, I'd expect the goto instruction after modifications to goto 0 instead of what it's currently showing, goto 8. It's as if the StackMapTable wasn't adjusted appropriately when I invoked javassist.bytecode.CodeIterator#insertAt.

Can anyone spot what I'm doing wrong here?

Thanks,

Eric

1

There are 1 answers

0
light_keeper On
  1. you create class like this c.newInstance(); so it supposed to have 0-parameters constructor - to be static inner class

    public static class EmptyLoopClass {
    public void emptyLoopMethod() {
        for(;;){
        }
    }
    
  2. you should call methodInfo.rebuildStackMap(cp); after code modifications. JVM uses StackMap to validate class file.

  3. Anyway you are ending up with bytecode

        3: ldc #34 = "injected into loop"
        5: invokevirtual #40 = Method java.io.PrintStream.println((Ljava/lang/String;)V)
        8: goto 8
    

    This code is not inside the loop, it is before loop. Actual loop is single instruction 8: goto 8 and you have to generate some additional loop code if you want to inject instructions inside it.