📜 ⬆️ ⬇️

Modifying bytecode java virtual machine

This post is a continuation of the article about the byte-code of the Java virtual machine, and we believe that the reader has an idea of ​​its structure. The most common library for modifying bytecode is ASM from object web. Most high-level libraries are built on it, in particular cglib.

The ASM library has two APIs. To better imagine the difference between them, we will draw the following analogy. Class is a kind of tree. The root of it is the class itself. Variables, methods, subclasses are its leaves. Instructions - leaves the methods. This way you can draw a parallel with XML and its two types of parsers. The first version of Core API is similar to SAX parser. When you need to read, create, or make changes, a class tree is traversed. The second option (Tree API) works on the trailer DOM parser. First, a representation tree is built, and then the necessary manipulations are performed with it. Obviously, the first API is less resource intensive, more suitable for making small changes. The second requires more resources, but also gives more flexibility. We will consider only the first version of the API.


')
To create a class using the Core API, you need to bypass its view tree. Consider a more specific example. We want to profile the next class.

public class Example {

public void run ( String name ) {
try {
Thread . sleep ( 5 ) ;
System . out . println ( "Currennt time is " + new Date ( System . currentTimeMillis ( ) ) ) ;
} catch ( InterruptedException e ) {
e . printStackTrace ( ) ;
}
System . out . println ( name ) ;
}

}


For example, get the runtime method runtime information. To do this, create a successor.

public class Test extends Example {

@Override
public void run ( String name ) {
long l = System . currentTimeMillis ( ) ;
super . run ( name ) ;
System . out . println ( ( System . currentTimeMillis ( ) - l ) ) ;
}
}


But how can you create it using ASM.

ClassWriter cw = new ClassWriter ( ClassWriter . COMPUTE_MAXS ) ;
cw . visit ( Opcodes . V1_5 , Opcodes . ACC_PUBLIC , "org/Test" , null , "org/example/Example" , null ) ;
cw . visitField ( Opcodes . ACC_PRIVATE , "name" , "Ljava/lang/String;" , null , null ) . visitEnd ( ) ;

MethodVisitor methodVisitor = cw . visitMethod ( Opcodes . ACC_PUBLIC , "<init>" , "()V" , null , null ) ;
methodVisitor . visitCode ( ) ;
methodVisitor . visitVarInsn ( Opcodes . ALOAD , 0 ) ;
methodVisitor . visitMethodInsn ( Opcodes . INVOKESPECIAL , "org/example/Example" , "<init>" , "()V" ) ;
methodVisitor . visitInsn ( Opcodes . RETURN ) ;
methodVisitor . visitMaxs ( 1 , 1 ) ;
methodVisitor . visitEnd ( ) ;

methodVisitor = cw . visitMethod ( Opcodes . ACC_PUBLIC , "run" , "(Ljava/lang/String;)V" , null , null ) ;
methodVisitor . visitCode ( ) ;
methodVisitor . visitMethodInsn ( Opcodes . INVOKESTATIC , "java/lang/System" , "currentTimeMillis" , "()J" ) ;
methodVisitor . visitVarInsn ( Opcodes . LSTORE , 2 ) ;
methodVisitor . visitVarInsn ( Opcodes . ALOAD , 0 ) ;
methodVisitor . visitVarInsn ( Opcodes . ALOAD , 1 ) ;
methodVisitor . visitMethodInsn ( Opcodes . INVOKESPECIAL , "org/example/Example" , "run" , "(Ljava/lang/String;)V" ) ;
methodVisitor . visitFieldInsn ( Opcodes . GETSTATIC , "java/lang/System" , "out" , "Ljava/io/PrintStream;" ) ;
methodVisitor . visitMethodInsn ( Opcodes . INVOKESTATIC , "java/lang/System" , "currentTimeMillis" , "()J" ) ;
methodVisitor . visitVarInsn ( Opcodes . LLOAD , 2 ) ;
methodVisitor . visitInsn ( Opcodes . LSUB ) ;
methodVisitor . visitMethodInsn ( Opcodes . INVOKEVIRTUAL , "java/io/PrintStream" , "println" , "(J)V" ) ;
methodVisitor . visitInsn ( Opcodes . RETURN ) ;
methodVisitor . visitMaxs ( 5 , 4 ) ;
methodVisitor . visitEnd ( ) ;

cw . visitEnd ( ) ;
return cw . toByteArray ( ) ;


So we got an array of bytes in which the structure of the new class is stored. Now we need this new class to load. To do this, you need your own ClassLoader, since the standard class loading method has a protected modifier.

class MyClassLoader extends ClassLoader {

public Class defineClass ( String name , byte [ ] b ) {
return defineClass ( name , b , 0 , b . length ) ;
}
}


And how can this be done?

MyClassLoader myClassLoader = new MyClassLoader ( ) ;
Class bClass = myClassLoader . defineClass ( "org.Test" , Generator . getBytecodeForClass ( ) ) ;

Constructor constructor = bClass . getConstructor ( ) ;
Object o = constructor . newInstance ( ) ;

Example e = ( Example ) o ;
e . run ( "test" ) ;


Thus, we can create a quick profiling utility that, using reflection, receives information and uses it to create the desired class. For a similar trailer built libraries that implement AOP.

Consider another example. Suppose we need to test the class that calls System.currentTimeMillis (). To do this, we just need to learn how to replace the currentTimeMillis () call with a call to another static method. We act like this: we read a class, bypassing its tree and, where necessary, changing the method call.


ClassReader cr = new ClassReader ( "org.example.Example" ) ;
ClassWriter cw = new ClassWriter ( cr , ClassWriter . COMPUTE_MAXS ) ;

ClassVisitor cv = new ReplaceStaticMethodClassAdapter ( cw ) ;
cr . accept ( cv , ClassReader . SKIP_DEBUG | ClassReader . SKIP_FRAMES ) ;

return cw . toByteArray ( ) ;

//----------------------------------------------------------------------------


private static class ReplaceStaticMethodClassAdapter extends ClassAdapter {

public RepaleStaticMethodClassAdapter ( ClassVisitor classVisitor ) {
super ( classVisitor ) ;
}

@Override
public MethodVisitor visitMethod ( int i , String s , String s1 , String s2 , String [ ] strings ) {
return new RepalceStaticMethodAdapter ( super . visitMethod ( i , s , s1 , s2 , strings ) ) ;
}

private class RepalceStaticMethodAdapter extends MethodAdapter {
public RepalceStaticMethodAdapter ( MethodVisitor methodVisitor ) {
super ( methodVisitor ) ;
}

@Override
public void visitMethodInsn ( int i , String s , String s1 , String s2 ) {
if ( i = = Opcodes . INVOKESTATIC & & "java/lang/System" .equals ( s ) & & "currentTimeMillis" .equals ( s1 ) & & "()J" .equals ( s2 ) ) {
super . visitMethodInsn ( Opcodes . INVOKESTATIC , "org/example/Generator" , "myTime" , "()J" ) ;
} else {
super . visitMethodInsn ( i , s , s1 , s2 ) ;
}
}
}
}


Loading a modified class has some special features. In jvm, classes are defined not only by their name, but also by the ClassLoader that was used when loading it. A class loaded using MyClassLoader will be of a different type than the class loaded by default ClassLoder. So call the method, we will need to use reflection.

MyClassLoader myClassLoader = new MyClassLoader ( ) ;
Class aClass = myClassLoader . defineClass ( "org.example.Example" , Generator . getModifedClass ( ) ) ;
Constructor constructor = aClass . getConstructor ( ) ;
Object o = constructor . newInstance ( ) ;
Method method = aClass . getMethod ( "run" , String . class ) ;
method . invoke ( o , "test" ) ;


You can also load both the Test and Example classes using MyClassLoader, and if the Test class calls the run method, the Example class will call the modified method.

A working example can be taken from here .

Modifying bytecode java virtual machine

Source: https://habr.com/ru/post/72409/


All Articles