This article has been authorized personal public account “Hongyang” original release.

Recovery of the double rest, ready to pick up the blog this matter, will try to write every blog, ready to write a “road to progress” series, I hope to be useful to you.


Yes, read a lot of ASM entry articles, all feel that the article is very easy to write, stand too high, I personally think to be able to write ASM related code, can understand the bytecode is essential, so this article will bytecode as the entry point, with a simple introduction to ASM.

Java Class file structure

It is well known that *. Java files are compiled by Javac to generate *. Class files, which are loaded by the Java virtual machine.

Any Java virtual machine that can load a class file must be able to read the contents of the class file according to some rule.

This rule is then the internal structure of the *.class file.

For example, let’s write a very simple Java class hello.java:

public class Hello{ public static void main(String[] args){ System.out.println(Hello.class.getSuperclass().getName()); }}Copy the code

Then we compile and generate the Hello.class file.

At this point let’s take a look at the hello. class file structure. We drag the hello. class file into an Editor (010 Editor for this article) that supports hexadecimal viewing.

You can see that the entire file is binary code, which is shown in hexadecimal format. Of course, this hexadecimal pair of code is not readable, but if you are familiar with class files, you probably know what the first hexadecimal characters CAFEBABE mean.

CAFEBABE is the magic number of the class file. Java and coffee are known to have some secrets, as can be seen from the icon, so it is not surprising that the magic is CAFEBABE.

Um… The rest is hard to read…

Of course you can choose to search for “Java file Format” and you’ll find very detailed blog posts that tell you what each binary means.

This way you can put together a complete picture of the class file format with a detailed specification.

It’s a boring thing, but we’re programmers.

If class files could be parsed according to some fixed format, wouldn’t we be able to write a program to parse all class files?

Yes, that’s right.

Once parsed, we can also store the fields in the data format we set, modify the data structure to the exposed interface, and then export it back to the file in the class file format.

In this way, our program can not only parse class files, but also modify class files.

Yes, of course, we can think of all these procedures, there must be a mature solution on the market:

So, here comes the main character of today’s article:

ASM, one of the most mature open source libraries, acts as a “suite” to help parse and modify class files.

To start with an appetizer, modify the class inheritance relationship

You may wonder, once we are familiar with the structure of the class folder, can we edit the class file by making a few changes?

That’s right.

Let’s show you how to modify the class file. Of course, I’m not going to use ASM, so we’re going to manually modify it.

Here’s another example:

public class Hello{ public static void main(String[] args){ System.out.println(Hello.class.getSuperclass().getName()); }}Copy the code

Our main() method prints the class name of its parent.

So… I’m going to change the parent of Hello, which currently inherits from Object.

As we described earlier, we just need to find the field area in the hello. class file that represents its parent and modify it.

Well, to see how we can find the corresponding area of the class file, we can use the 010 Editor, which contains the class file template, to help us see the structure of each section clearly:

You can see that our class file contains the access modifier, the current class name, and the parent class name after a series of constant pools.

The parent name corresponds to the value 7, which represents the seventh element in the constant pool. We find the seventh constant:

U1 tag =7 means this is a Class constant with an index of 25.

Let’s move on to constant number 25:

The length is 16 and the character array is: 106,97… This string of numbers.

This string of numbers is actually ASCII code, you can find any code table:

Java /lang/Object.

After all this work we finally found the code and encoding for the hexadecimal code of the parent class.

Let’s now replace Hello’s inherited class with Java /lang/Number.

Just change the hexadecimal code corresponding to Object to Number.

Before the change:

After the change:

In detail, you can see that 4F is changed into 4E, which is converted into base 10:79, the corresponding ASCII code of 79 is N. You can see that we have changed Object into Number according to the rule of if.

Then we save it and execute it:

I executed Java before and after the change, and you can see that the parent class name we printed has been changed.

We can even javap:

Right? There was a change in inheritance.

As you can see, as long as we can find the specified area, we can change the binary code of that area, so that we can do whatever we want with the class file.

Isn’t that easy?

But it’s not that simple.

Because the file we are modifying is extremely simple, we cannot arbitrarily modify the contents of a constant pool if the contents are very large.

Second, Java /lang/Number is exactly the same length as Java /lang/Object, otherwise we would have to do a lot of alignment work.

Therefore, modifying the class file is not so easy.

Don’t worry, we have ASM.

However, just because a library like ASM exists doesn’t mean that you don’t need to know about class files to modify them.

At the very least, it seems to me that the ASM class library is not intended to be like Hibernate, where developers who don’t know SQL can write code to manipulate databases.

We still need to understand the internal composition of the class file, and when we modify the code, we need to understand how the local variable table works as the code executes, how instructions affect stack frames, and so on.

Let’s look down and you’ll see.

Start introducing ASM

The introduction of the ASM

Ok, now let’s start learning ASM in earnest.

First let’s go to ASM’s official website:

asm.ow2.io/

You can find the latest version on the website, as well as a detailed User Guide that basically covers all the apis.

The latest version is now 9.1, so try it out:

// https://mvnrepository.com/artifact/org.ow2.asm/asm-commons implementation group: 'org.ow2.asm', name: 'the asm - Commons' version:' 9.1 'Copy the code

Try parsing the Class file

From a learning perspective, we can learn how to read the internal parts of a class file before modifying it.

For example, I want to get all the method names and field names inside a compiled *.class file at compile time.

Tree Api

What is the preferred way to analyze class files?

It must be: I give you a class file and you return me a ClassNode object with a method or Field like List<Methood> or List<Field>.

Um… Want to pour the United States, ASM is so simple thing?

For example, if you have a ClassNode class, you can use the following class:

A node that represents a class.
Copy the code

Refers to a class file.

Everything we want inside a class should be available either directly or indirectly through the class API.

That’s right, yes, take a look:

As long as we construct a ClassNode object from a class file, we can do whatever we want.

Take a look at the code:

First we write a User:

public class User { private String name; private int age; public String getName() { return name; } public int getAge() { return age; }}Copy the code

Then we want to get all the methods and fields contained in user.class:

public class TreeApiTest { public static void main(String[] args) throws Exception { Class clazz = User.class; String clazzFilePath = Utils.getClassFilePath(clazz); ClassReader classReader = new ClassReader(new FileInputStream(clazzFilePath)); ClassNode classNode = new ClassNode(Opcodes.ASM5); classReader.accept(classNode, 0); List<MethodNode> methods = classNode.methods; List<FieldNode> fields = classNode.fields; System.out.println("methods:"); for (MethodNode methodNode : methods) { System.out.println(methodNode.name + ", " + methodNode.desc); } System.out.println("fields:"); for (FieldNode fieldNode : fields) { System.out.println(fieldNode.name + ", " + fieldNode.desc); }}}Copy the code

The above code has a helper Class utils.getClassFilepath method that I will post here. The main purpose is to find its path in AS through the Class object.

public static String getClassFilePath(Class clazz) {
        // file:/Users/zhy/hongyang/repo/BlogDemo/app/build/intermediates/javac/debug/classes/
        String buildDir = clazz.getProtectionDomain().getCodeSource().getLocation().getFile();
        String fileName = clazz.getSimpleName() + ".class";
        File file = new File(buildDir + clazz.getPackage().getName().replaceAll("[.]", "/") + "/", fileName);
        return file.getAbsolutePath();
    }
Copy the code

Take a look at the flow of our code:

  1. First we get the path to the class file;
  2. And then you hand it over to ClassReader
  3. Construct a ClassNode object
  4. The classReader.accept () method is called to complete the class traversal and record the relevant information to the ClassNode object.

At this point, we can use ClassNode to retrieve the desired information, and look at the output:

methods:
	<init>, ()V
	getName, ()Ljava/lang/String;
	getAge, ()I
fields:
	name, Ljava/lang/String;
	age, I
Copy the code

Have you noticed how much simpler it could be to just read a class file?

The above API, called the Tree API, parses the class file, stores the information to ClassNode, and then reads it from ClassNode, similar to the way an XML file is parsed and reads the entire XML file into memory.

Core Api

If you look at blogs, there are not many of them. Most of them are based on the “event-driven” API, that is, when parsing a class file, each “node” will be handed to you. We are similar to listening for the “node” parsing event.

public class VisitApiTest { public static void main(String[] args) throws Exception { Class clazz = User.class; String clazzFilePath = Utils.getClassFilePath(clazz); ClassReader classReader = new ClassReader(new FileInputStream(clazzFilePath)); ClassVisitor classVisitor = new ClassVisitor(Opcodes.ASM5) { @Override public FieldVisitor visitField(int access, String name, String descriptor, String signature, Object value) { System.out.println("visit field:" + name + " , desc = " + descriptor); return super.visitField(access, name, descriptor, signature, value); } @Override public MethodVisitor visitMethod(int access, String name, String descriptor, String signature, String[] exceptions) { System.out.println("visit method:" + name + " , desc = " + descriptor); return super.visitMethod(access, name, descriptor, signature, exceptions); }}; classReader.accept(classVisitor, 0); }}Copy the code

Let’s look at the output:

visit field:name , desc = Ljava/lang/String;
visit field:age , desc = I
visit method:<init> , desc = ()V
visit method:getName , desc = ()Ljava/lang/String;
visit method:getAge , desc = ()I
Copy the code

Let’s go through the steps again:

  1. First we get the path to the class file;
  2. And then pass it to ClassReader;
  3. Construct a ClassVisitor object;
  4. Pass the ClassVisitor object to the classReader.accept () method to receive the “node” callback information for parsing the class file;

When parsing is done, save all the information to a specific object that we can read. One is to participate in the parsing process, listening for the callback of the parsing node, and output the structure.

Imagine for a moment that we have a custom ClassVisitor, visitMethod method, that stores the listening method declaration information into a List. Would we have a simple ClassNode implementation?

So ClassNode is also a subclass of ClassVisitor, right?

That’s right!

public class ClassNode extends ClassVisitor 
Copy the code

By now, you should have learned how to read the contents of a class file:

class-> ClassReader.accept(ClassVisitor)

I even accidentally mastered the principles of ClassNode, which was really fun. So, let’s move on.

Simple bytecode modification

Change the bytecode and it won’t be that easy. Brace yourself.

Let’s take an official example:

public class C { public void m() throws Exception { Thread.sleep(100); }}Copy the code

Is amended as:

public class C { public static long timer; public void m() throws Exception { timer -= System.currentTimeMillis(); Thread.sleep(100); timer += System.currentTimeMillis(); }}Copy the code

It’s kind of like the code we add time-consuming operations to.

We learned earlier how to read and iterate through and parse a class file, but we haven’t tried to write back to a class file, so if we make some changes to the class file, we’re going to try to write back to overwrite the original class file.

New ClassWriter

Before we look at the code, let’s think about what you would do if we implemented read-write:

When we first iterate, we get all the information in the class, then call writeToFile and write it directly.

So a ClassWriter is a ClassVisitor, right?

All it does is save information as it traverses, and then support writing files in class file format.

Um… Got it right, seven, eight, eight.

Let’s see if the ClassWriter is the ClassVisitor:

public class ClassWriter extends ClassVisitor
Copy the code

I already have a mental picture of the next code:

public class ClassWriterTest { public static void main(String[] args) throws Exception { Class clazz = C.class; String clazzFilePath = Utils.getClassFilePath(clazz); ClassReader classReader = new ClassReader(new FileInputStream(clazzFilePath)); ClassWriter classWriter = new ClassWriter(0); classReader.accept(classWriter, 0); Byte [] bytes = classwriter.tobytearray (); FileOutputStream fos = new FileOutputStream("/Users/zhy/Desktop/copyed.class"); fos.write(bytes); fos.flush(); fos.close(); }}Copy the code

With the above code, we have copied a class, and if we pass in the same path, we have modified the class (which we haven’t modified yet, of course).

The only difference is that instead of writeToFile, there is a toByteArray method, because byte arrays are more generic.

Start trying to add fields

Although we’ve learned the basics of ClassWriter, I seem to have found a problem.

The ClassReader. Accept method can only accept a ClassVisitor object because we have to modify the bytecode, so the ClassWriter must be passed in.

So how do we pass in another ClassVisitor object to modify the bytecode?

Let’s look at the constructor of a ClassVisitor:

public ClassVisitor(final int api, final ClassVisitor classVisitor) 
Copy the code

We can pass in an actual object to the ClassVisitor, act as the proxy object, and we’ll duplicate the method we need to intercept, kind of ContextWrapper.

First we try to add a field. If it is the Tree API, we can add a FieldNode via ClassNode.

But instead of using the VISIT API, we’ll subclass ClassVisitor:

public class AddTimerClassVisitor extends ClassVisitor { public AddTimerClassVisitor(int api, ClassVisitor classVisitor) { super(api, classVisitor); }}Copy the code

At this point, we’ll delegate the actual ClassVisitor object. What we’ll do is:

Insert a field in the right place;

The rest goes to the ClassVisitor that is passed in.

So what is the right location?

We need to know which methods the ClassVisitor will execute, and what order the methods are in:

visit visitSource? visitOuterClass? ( visitAnnotation |
   visitAttribute )*
   ( visitInnerClass | visitField | visitMethod )*
   visitEnd
Copy the code

You can see that when ClassVistor iterates through a class, the order of the calls is as follows. The * indicates that the method may not be called, and the * indicates that it may be called zero or more times.

So our appropriate location, first of all, is to select the place that must be called; Secondly, it is best to execute it only once; Finally, because there may be fields inside, we can collect information to prevent the same name field from being inserted, so we choose the visitEnd method.

Method is selected, so how do I insert a field?

Let’s think about this:

VisitField calls the classWriter. visitField method, which will assume that the field exists once you call it.

That’s easy. Let’s look at the code:

public class AddTimerClassVisitor extends ClassVisitor {

    public AddTimerClassVisitor(int api, ClassVisitor classVisitor) {
        super(api, classVisitor);
    }


    @Override
    public void visitEnd() {

        FieldVisitor fv = cv.visitField(Opcodes.ACC_PUBLIC + Opcodes.ACC_STATIC, "timer",
                "J", null, null);
        if (fv != null) {
            fv.visitEnd();
        }
        cv.visitEnd();
    }
}
Copy the code

Demo code, the actual production environment needs to do more stringent conditions verification.

We added a timer field to the original class, the access modifier is public static, and its type is J, which is of type LONG.

We put the code together:

public class ClassWriterTest { public static void main(String[] args) throws Exception { Class clazz = C.class; String clazzFilePath = Utils.getClassFilePath(clazz); ClassReader classReader = new ClassReader(new FileInputStream(clazzFilePath)); ClassWriter classWriter = new ClassWriter(0); AddTimerClassVisitor addTimerClassVisitor = new AddTimerClassVisitor(Opcodes.ASM5, classWriter); classReader.accept(addTimerClassVisitor, 0); Byte [] bytes = classwriter.tobytearray (); FileOutputStream fos = new FileOutputStream("/Users/zhy/Desktop/copyed.class"); fos.write(bytes); fos.flush(); fos.close(); }}Copy the code

And then we run it.

Run the generated class and decompile it to see:

Happy…

Next we will try to modify the bytecode by adding the time information printed to the method.

Change the method

Through the above learning, we will execute the visitMethod method of ClassVisitor for the method traversal, and the modification method must be dependent on this method, so let’s take a look at this method in detail:

# ClassVisitor public MethodVisitor visitMethod( final int access, final String name, final String descriptor, final String signature, final String[] exceptions) { if (cv ! = null) { return cv.visitMethod(access, name, descriptor, signature, exceptions); } return null; }Copy the code

As you can see, the parameters of this method contain all the declarative information of the method, but do not contain information about the actual running code, namely instruction information.

The return value of this method is not NULL, but a MethodVisitor, so we must have a ClassReader that iterates through the class file: So we give you the method declaration information, and then we give it back a MethodVisitor, and it gets that MethodVisitor, and it starts iterating through all the information inside that method.

So… We need to customize a MethodVisitor to complete the insertion of the code.

First a bit of code:

public class AddTimerClassVisitor extends ClassVisitor { public AddTimerClassVisitor(int api, ClassVisitor classVisitor) { super(api, classVisitor); } @Override public MethodVisitor visitMethod(int access, String name, String descriptor, String signature, String[] exceptions) { MethodVisitor methodVisitor = super.visitMethod(access, name, descriptor, signature, exceptions); MethodVisitor newMethodVisitor = new MethodVisitor(api, methodVisitor) { }; return newMethodVisitor; }...Copy the code

We just copied the visitMethod in the AddTimerClassVisitor, and inside that we customized a MethodVisitor to delegate a wave of original objects.

Question again? By analogy, we should be able to guess that the MethodVisitor design is similar to that of the ClassVisitor, with a bunch of visitXXX methods. This time we are modifying the bytecode to inject code before and after the method. Which methods should we choose to duplicate?

This requires that we know the order in which the various visitXXX methods in MethodVisitor are executed:

visitAnnotationDefault?
(visitAnnotation |visitParameterAnnotation |visitAttribute )* ( visitCode
(visitTryCatchBlock |visitLabel |visitFrame |visitXxxInsn | visitLocalVariable |visitLineNumber )*
visitMaxs )? visitEnd
Copy the code

First, iterate over some annotations and parameter information. Walk through the entire method starting with visitCode.

Our injection is:

  1. Method start: We choose to duplicate the visitCode method;
  2. Before RETURN: we choose to duplicate visitXxxInsn, and then check whether the current command is RETURN;

The problem is that we still don’t know how to write the injected code.

Yeah, you’re going to have to know a lot about bytecode, or I’m not going to be able to write it very well, but I’m going to do my best to guide you through the derivation.

The first thing we need to understand is that the code we’re adding is going to be injected as bytecode instructions, so we need to take a look at what’s happening at the bytecode level.

So before we modify, we need to look at the corresponding method bytecode before and after modification respectively:

 public void m() throws Exception {
       Thread.sleep(100);
      }
Copy the code

Corresponding bytecode:

public void m() throws java.lang.Exception;
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=2, locals=1, args_size=1
         0: ldc2_w        #2                  // long 100l
         3: invokestatic  #4                  // Method java/lang/Thread.sleep:(J)V
         6: return
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       7     0  this   Lcom/imooc/blogdemo/blog03/C;
    Exceptions:
      throws java.lang.Exception
}

Copy the code
public void m() throws Exception {
        timer -= System.currentTimeMillis();
        Thread.sleep(100);
        timer += System.currentTimeMillis();
    }
Copy the code

Corresponding bytecode:

I’m going to box these two pieces of code, which are the bytecode instructions that we added, so the actual code that we’re going to write corresponds to these two bits of bytecode that we added.

Let’s look at the first instruction we add to our method:

@Override
public void visitCode() {
    mv.visitCode();

    mv.visitFieldInsn(GETSTATIC, mOwner, "timer", "J");
    mv.visitMethodInsn(INVOKESTATIC, "java/lang/System",
            "currentTimeMillis", "()J");
    mv.visitInsn(LSUB);
    mv.visitFieldInsn(PUTSTATIC, mOwner, "timer", "J");

}
Copy the code

If you look at it closely, it actually compares to our boxed bytecode:

0: getstatic     #2                  // Field timer:J
3: invokestatic  #3                  // Method java/lang/System.currentTimeMillis:()J
6: lsub
7: putstatic     #2                  // Field timer:J
Copy the code

Basically no difference at all.

But let’s explain these lines of bytecode:

  1. Take the current class static variable timer and push it onto the operand stack
  2. System.system.currenttimemillis is called, and the method returns the value pushed onto the operand stack;
  3. Call “timer-system.system.currentTimemillis”, result is stack
  4. Assign the value of 3 to the timer field again;

In code, this is:

timer -= System.currentTimeMillis();
Copy the code

Similarly, we write the code before the RETURN method:

@Override
public void visitInsn(int opcode) {

    if ((opcode >= IRETURN && opcode <= RETURN) || opcode == ATHROW) {
        mv.visitFieldInsn(GETSTATIC, mOwner, "timer", "J");
        mv.visitMethodInsn(INVOKESTATIC, "java/lang/System",
                "currentTimeMillis", "()J");
        mv.visitInsn(LADD);
        mv.visitFieldInsn(PUTSTATIC, mOwner, "timer", "J");
    }
    mv.visitInsn(opcode);
}
Copy the code

You can compare bytecode in the same way as above.

One difference is that for the RETURN instruction, we judge multiple, because we do not know the RETURN value of the current method. If we are sure that the method does not RETURN a value, we just judge RETURN.

Ok, let’s post the full code:

package com.imooc.blogdemo.blog03; import org.objectweb.asm.ClassVisitor; import org.objectweb.asm.FieldVisitor; import org.objectweb.asm.MethodVisitor; import org.objectweb.asm.Opcodes; import static org.objectweb.asm.Opcodes.*; public class AddTimerClassVisitor extends ClassVisitor { private String mOwner; public AddTimerClassVisitor(int api, ClassVisitor classVisitor) { super(api, classVisitor); } @Override public void visit(int version, int access, String name, String signature, String superName, String[] interfaces) { super.visit(version, access, name, signature, superName, interfaces); mOwner = name; } @Override public MethodVisitor visitMethod(int access, String name, String descriptor, String signature, String[] exceptions) { MethodVisitor methodVisitor = super.visitMethod(access, name, descriptor, signature, exceptions); if (methodVisitor ! = null && ! name.equals("<init>")) { MethodVisitor newMethodVisitor = new MethodVisitor(api, methodVisitor) { @Override public void visitCode() { mv.visitCode(); mv.visitFieldInsn(GETSTATIC, mOwner, "timer", "J"); mv.visitMethodInsn(INVOKESTATIC, "java/lang/System", "currentTimeMillis", "()J"); mv.visitInsn(LSUB); mv.visitFieldInsn(PUTSTATIC, mOwner, "timer", "J"); } @Override public void visitInsn(int opcode) { if ((opcode >= IRETURN && opcode <= RETURN) || opcode == ATHROW) { mv.visitFieldInsn(GETSTATIC, mOwner, "timer", "J"); mv.visitMethodInsn(INVOKESTATIC, "java/lang/System", "currentTimeMillis", "()J"); mv.visitInsn(LADD); mv.visitFieldInsn(PUTSTATIC, mOwner, "timer", "J"); } mv.visitInsn(opcode); }}; return newMethodVisitor; } return methodVisitor; } @Override public void visitEnd() { FieldVisitor fv = cv.visitField(Opcodes.ACC_PUBLIC + Opcodes.ACC_STATIC, "timer", "J", null, null); if (fv ! = null) { fv.visitEnd(); } cv.visitEnd(); }}Copy the code

Note: This is actually an example from the official documentation.

Then we run it and generate the copyed.class file on the desktop.

Perfect. Decompile it. Okay.

At this point, don’t be happy too early, can decompile does not mean you code without errors!

Let’s verify the correctness of the file:

A simple change to the output file path:

byte[] bytes = classWriter.toByteArray(); / / modify, For a moment to Java execution FileOutputStream fos = new FileOutputStream ("/Users/zhy/Desktop/com/imooc/blogdemo/blog03 / Arthur c. lass "); fos.write(bytes); fos.flush(); fos.close();Copy the code

Then we cut to the desktop and execute:

java com.imooc.blogdemo.blog03.C
Copy the code

Output:

192:Desktop zhy$ java com.imooc.blogdemo.blog03.C Error: A JNI error has occurred, please check your installation and try again Exception in thread "main" java.lang.VerifyError: Operand stack overflow Exception Details: Location: com/imooc/blogdemo/blog03/C.m()V @3: invokestatic Reason: Exceeded max stack size. Current Frame: bci: @3 flags: { } locals: { 'com/imooc/blogdemo/blog03/C' } stack: { long, long_2nd } Bytecode: 0x0000000: b200 12b8 0018 65b3 0012 1400 19b8 0020 0x0000010: b200 2412 26b6 002c b200 12b8 0018 61b3 0x0000020: 0012 b1 at java.lang.Class.getDeclaredMethods0(Native Method) at java.lang.Class.privateGetDeclaredMethods(Class.java:2701) at java.lang.Class.privateGetMethodRecursive(Class.java:3048)  at java.lang.Class.getMethod0(Class.java:3018) at java.lang.Class.getMethod(Class.java:1784) at sun.launcher.LauncherHelper.validateMainClass(LauncherHelper.java:544) at sun.launcher.LauncherHelper.checkAndLoadMain(LauncherHelper.java:526)Copy the code

An error! We talk about the boss of the world, the results run, slap face.

Why is that?

Cause: Exceeded Max Stack size

Looks like we forgot one detail.

What details?

We need to go back to the bytecode before and after the code modification, and there is one detail we missed:

Public void m() throws java.lang.exception; Descriptor: ()V flags: ACC_PUBLIC Code: stack=2, locals=1, args_size=1 // Public void m() throws java.lang.exception; descriptor: ()V flags: ACC_PUBLIC Code: stack=4, locals=1, args_size=1Copy the code

Did you notice that one of the stack values of the method changed?

What does this value mean?

He was referring to the depth of the operand stack we need, our Java bytecode instruction, many of these instructions will pressure stack operation, and then some instructions or method invocation will pop up the stack to perform the operation Numbers, for example lsub pops out the inside of the operand stack two long value to do subtraction, after the completion of the pressed the stack again.

In this method all instructions are executed, and the required stack depth is determined at compile time. From the result of decompilation, we need to change to 4.

@Override public MethodVisitor visitMethod(int access, String name, String descriptor, String signature, String[] exceptions) { MethodVisitor methodVisitor = super.visitMethod(access, name, descriptor, signature, exceptions); MethodVisitor newMethodVisitor = new MethodVisitor(api, @override public void visitCode() {// omit} @override public void visitCode(int opcode) {// omit} @Override public void visitMaxs(int maxStack, int maxLocals) { mv.visitMaxs(4, maxLocals); }}; return newMethodVisitor; }Copy the code

Run it again, generate C.lass, and then Java again:

Desktop zhy $Java com. Imooc. Blogdemo. Blog03. C error: in the class. Com imooc. Blogdemo. Blog03. C can not find the main method, please will be the main method is defined as: public static void main(String[] args)Copy the code

This time the error is ok because we didn’t write the main() method.

But that wasn’t the end of the story.

Does it make sense for us to write maxStack as 4?

mv.visitMaxs(4, maxLocals);
Copy the code

Obviously doesn’t make sense, we’re just targeting our single method, maybe one of the previous methods maxStack was 10.

So this modification is extremely unreasonable, so we don’t know how much is appropriate after our modification, so we can define an incremental value.

In the bytecode we just modified:

CurrentTimeMillis // 2 LSUB // 4, 2 put static timer // 2Copy the code

If you look at the code inserted before RETURN, the maximum number we can push is 4, which means we can increase the stack depth by 4 operands.

You might be wondering, why is getStatic Timer stack 2 instead of 1?

Because long takes two places.

So we should write:

@Override
public void visitMaxs(int maxStack, int maxLocals) {
    mv.visitMaxs( maxStack + 4, maxLocals);
}
Copy the code

And then verify that, that’s fine.

But the stack becomes 6, and actually 4 is enough.

So is there a better way?

Some!

When we build ClassWriter, the code looks like this:

ClassWriter classWriter = new ClassWriter(0);
Copy the code

Note that the constructor passes in a 0 and actually accepts a flag, which actually has a flag:

ClassWriter.COMPUTE_FRAMES
Copy the code

We can build when passed in:

 ClassWriter classWriter = new ClassWriter(ClassWriter.COMPUTE_MAXS);
Copy the code

This will remove the visitMaxs code, which will automatically recalculate stackSize for us.

At that point, you run it again, decompile it again, and you see stack =4 again.

That’s it for a preliminary bytecode modification case.

If you want to modify a class file using ASM, you must:

  1. Bytecode instructions should be very clear;
  2. Understand the operand stack;

Not only that, of course, but you should also understand the local variable scale and some of the principles behind the compilation process.

It’s just that we haven’t touched on the example so far, and we’ll write about other details later.

You can add the wechat official account: Hongyang, so that you can receive the articles in the first time.