PolarSPARC

Introduction to Bytecode Handling with ASM


Bhaskar S 12/26/2021


Overview

ASM is a fast, robust, small, and low-level Java bytecode framework that can be used for either analysing existing java bytecode (from Java classes) or for dynamically generating or manipulating Java bytecode.

ASM uses the Visitor behavioral design pattern. This design pattern is used for separating the operations from an object structure hierarchy (where there are different object types). This results in the flexibility to add newer operations on the objects in the hierarchy without modifying them.

To elaborate, often times there is a need to perform some kind of an operation on each of the objects in the hierarchy, for example, say print. Adding this operation to each of the objects in the hierarchy is a little cumbersome when there are many objects in the hierarchy to deal with. Now, what happens when we have to add another operation, say get_size later ??? This will imply modifying all the objects in the hierarchy again.

To solve this problem, create a visitor class with all the operations and add an accept method to all the objects in the hierarchy which takes in the visitor as a parameter.

One can think of a Java class as an hierarchy of objects (or nodes). There is the class, then the fields, then the methods, and so on.

ASM framework provides two types of API interfaces:

Setup

The setup will be on a Ubuntu 20.04 LTS based Linux desktop. Ensure at least Java 11 or above is installed and setup. Also, ensure Apache Maven is installed and setup.

To setup the Java directory structure for the demonstrations in this article, execute the following commands:

$ cd $HOME

$ mkdir -p $HOME/java/JavaASM

$ cd $HOME/java/JavaASM

$ mkdir -p src/main/java src/main/resources target

$ mkdir -p src/main/java/com/polarsparc/asm

$ mkdir -p src/main/java/com/polarsparc/visitor


The following is the listing for the Maven project file pom.xml that will be used:


pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
          xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
          xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.polarsparc.asm</groupId>
    <artifactId>JavaASM</artifactId>
    <version>1.0</version>

    <properties>
        <maven.compiler.source>17</maven.compiler.source>
        <maven.compiler.target>17</maven.compiler.target>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.ow2.asm</groupId>
            <artifactId>asm</artifactId>
            <version>9.2</version>
        </dependency>
    </dependencies>
</project>

Visitor Pattern

Before we dig into ASM, let us get a grasp on the Visitor behavioral design pattern through an example.

The following is the code for FieldNode that represents a field in an object hierarchy:


Listing.1
/*
 * Description: Simple class that represents a Field
 * Author:      Bhaskar S
 * Date:        12/25/2021
 * Blog:        https://www.polarsparc.com
 */

package com.polarsparc.visitor;

public class FieldNode {
    private final String name;
    private final String type;

    public FieldNode(String n, String t) {
        this.name = n;
        this.type = t;
    }

    public String getName() {
        return name;
    }

    public String getType() {
        return type;
    }

    public void accept(Visitor v) {
        v.visit(this);
    }
}

The following is the code for MethodNode that represents a method in an object hierarchy:


Listing.2
/*
 * Description: Simple class that represents a Method
 * Author:      Bhaskar S
 * Date:        12/25/2021
 * Blog:        https://www.polarsparc.com
 */

package com.polarsparc.visitor;

public class MethodNode {
    private final String name;

    public MethodNode(String n) {
        this.name = n;
    }

    public String getName() {
        return name;
    }

    public void accept(Visitor v) {
        v.visit(this);
    }
}

The following is the code for Visitor that represents an interface for the various operations on the nodes of the object hierarchy:


Listing.3
/*
 * Description: Interface that indicates the various operations
 * Author:      Bhaskar S
 * Date:        12/25/2021
 * Blog:        https://www.polarsparc.com
 */

package com.polarsparc.visitor;

public interface Visitor {
    void visit(FieldNode node);
    void visit(MethodNode node);
}

The following is the code for NodeVisitor that implements the Visitor interface for printing the details of the nodes of the object hierarchy:


Listing.4
/*
 * Description: Concrete class that implements Visitor to perform print operation
 * Author:      Bhaskar S
 * Date:        12/25/2021
 * Blog:        https://www.polarsparc.com
 */

package com.polarsparc.visitor;

public class NodeVisitor implements Visitor {
    @Override
    public void visit(FieldNode node) {
        System.out.printf("FIELD: Name: %s, Type: %s\n", node.getName(), node.getType());
    }

    @Override
    public void visit(MethodNode node) {
        System.out.printf("METHOD: Name: %s\n", node.getName());
    }
}

Finally, the following is the code for DemoVisitor that demonstrates the Visitor pattern:


Listing.5
/*
 * Description: Demo of the Visitor Pattern
 * Author:      Bhaskar S
 * Date:        12/25/2021
 * Blog:        https://www.polarsparc.com
 */

package com.polarsparc.visitor;

public class DemoVisitor {
    public static void main(String[] args) {
        FieldNode fn = new FieldNode("name", "string");
        MethodNode mn = new MethodNode("greet");
        Visitor visitor = new NodeVisitor();
        fn.accept(visitor);
        mn.accept(visitor);
    }
}

Executing the code in Listing.5 above would result in the following output:

Output.1

FIELD: Name: name, Type: string
METHOD: Name: greet

Later, when we need to add a new operation, say getSize(), we can add it to the Visitor and not modify any of the nodes in the object hierarchy.

Hands-on with ASM

The following is the code for SimpleHello that will print a simple message on the console:


Listing.6
/*
 * Description: Simple Java class to print 'Hello ASM !!!'
 * Author:      Bhaskar S
 * Date:        12/25/2021
 * Blog:        https://www.polarsparc.com
 */

package com.polarsparc.asm;

public class SimpleHello {
    private final static String MESSAGE = "Hello ASM !!!";

    public static void main(String[] args) {
        System.out.println(MESSAGE);
    }
}

The following is the code for SimpleVisitorUsingASM that will demonstrate the event based approach of ASM that will print details of the different nodes (class, field, method, etc) of a Java class object hierachy:


Listing.7
/*
 * Description: Simple Java class to visit nodes of a class using ASM
 * Author:      Bhaskar S
 * Date:        12/25/2021
 * Blog:        https://www.polarsparc.com
 */

package com.polarsparc.asm;

import org.objectweb.asm.*;

public class SimpleVisitorUsingASM {
    static class SimpleMethodVisitor extends MethodVisitor {
        public SimpleMethodVisitor() {
            super(Opcodes.ASM9);
        }

        public void visitVarInsn(int opcode, int var) {
            System.out.printf("visitVarInsn: opcode = %d, var = %d\n", opcode, var);
        }

        public void visitLocalVariable(String name, String desc, String signature, Label start, Label end, int index) {
            System.out.printf("visitLocalVariable: Name = %s, Desc = %s, Signature = %s, Index = %d\n",
                    name, desc, signature, index);
        }

        public void visitMaxs(int maxStack, int maxLocals) {
            System.out.printf("visitMaxs: max stack = %d, max locals = %d\n", maxStack, maxLocals);
        }
    }

    static class SimpleClassVisitor extends ClassVisitor {
        public SimpleClassVisitor() {
            super(Opcodes.ASM9);
        }

        public FieldVisitor visitField(int access, String name, String desc, String signature, Object value) {
          System.out.printf("visitField: Name = %s, Desc = %s, Signature = %s, Value = %s\n",
                  name, desc, signature, value);

          return super.visitField(access, name, desc, signature, value);
        }

        public MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) {
            System.out.printf("visitMethod: Name = %s, Desc = %s, Signature = %s\n", name, desc, signature);

            return new SimpleMethodVisitor();
        }
    }

    public static void main(String[] args) {
        if (args.length != 1) {
            System.out.printf("Usage: java %s <class-name>\n", SimpleVisitorUsingASM.class.getName());
            System.exit(1);
        }

        try {
            ClassReader reader = new ClassReader(args[0]);
            reader.accept(new SimpleClassVisitor(), ClassReader.SKIP_FRAMES | ClassReader.SKIP_DEBUG);
        }
        catch (Exception ex) {
            ex.printStackTrace(System.out);
        }
    }
}

The code in Listing.7 above needs some explanation.

Executing the code in Listing.7 above would result in the following output:

Output.2

visitField: Name = MESSAGE, Desc = Ljava/lang/String;, Signature = null, Value = Hello ASM !!!
visitMethod: Name = <init>, Desc = ()V, Signature = null
visitVarInsn: opcode = 25, var = 0
visitMaxs: max stack = 1, max locals = 1
visitMethod: Name = main, Desc = ([Ljava/lang/String;)V, Signature = null
visitMaxs: max stack = 2, max locals = 1

In the next example, we will recreate the Java bytecode for SimpleHello using ASM.

Before we do that, we will dump the bytecode instructions from the SimpleHello class using the utility program javap that is installed as part of Java SE installation.

Open a terminal window and execute the following commands:

$ cd $HOME/java/JavaASM

$ javap -v target/classes/com/polarsparc/asm/SimpleHello.class

The following would be the typical output:

Output.3

Classfile com/polarsparc/asm/SimpleHello.class
Last modified Dec 25, 2021; size 639 bytes
SHA-256 checksum 6089eb88882ebc34754f1b1b093640d37a45db9c9bd3c03b7c00c738915bd3fd
Compiled from "SimpleHello.java"
public class com.polarsparc.asm.SimpleHello
minor version: 0
major version: 61
flags: (0x0021) ACC_PUBLIC, ACC_SUPER
this_class: #13                         // com/polarsparc/asm/SimpleHello
super_class: #2                         // java/lang/Object
interfaces: 0, fields: 1, methods: 2, attributes: 1
Constant pool:
  #1 = Methodref          #2.#3          // java/lang/Object."<init>":()V
  #2 = Class              #4             // java/lang/Object
  #3 = NameAndType        #5:#6          // "<init>":()V
  #4 = Utf8               java/lang/Object
  #5 = Utf8               <init>
  #6 = Utf8               ()V
  #7 = Fieldref           #8.#9          // java/lang/System.out:Ljava/io/PrintStream;
  #8 = Class              #10            // java/lang/System
  #9 = NameAndType        #11:#12        // out:Ljava/io/PrintStream;
#10 = Utf8               java/lang/System
#11 = Utf8               out
#12 = Utf8               Ljava/io/PrintStream;
#13 = Class              #14            // com/polarsparc/asm/SimpleHello
#14 = Utf8               com/polarsparc/asm/SimpleHello
#15 = String             #16            // Hello ASM !!!
#16 = Utf8               Hello ASM !!!
#17 = Methodref          #18.#19        // java/io/PrintStream.println:(Ljava/lang/String;)V
#18 = Class              #20            // java/io/PrintStream
#19 = NameAndType        #21:#22        // println:(Ljava/lang/String;)V
#20 = Utf8               java/io/PrintStream
#21 = Utf8               println
#22 = Utf8               (Ljava/lang/String;)V
#23 = Utf8               MESSAGE
#24 = Utf8               Ljava/lang/String;
#25 = Utf8               ConstantValue
#26 = Utf8               Code
#27 = Utf8               LineNumberTable
#28 = Utf8               LocalVariableTable
#29 = Utf8               this
#30 = Utf8               Lcom/polarsparc/asm/SimpleHello;
#31 = Utf8               main
#32 = Utf8               ([Ljava/lang/String;)V
#33 = Utf8               args
#34 = Utf8               [Ljava/lang/String;
#35 = Utf8               SourceFile
#36 = Utf8               SimpleHello.java
{
public com.polarsparc.asm.SimpleHello();
  descriptor: ()V
  flags: (0x0001) ACC_PUBLIC
  Code:
    stack=1, locals=1, args_size=1
        0: aload_0
        1: invokespecial #1                  // Method java/lang/Object."<init>":()V
        4: return
    LineNumberTable:
      line 10: 0
    LocalVariableTable:
      Start  Length  Slot  Name   Signature
          0       5     0  this   Lcom/polarsparc/asm/SimpleHello;

public static void main(java.lang.String[]);
  descriptor: ([Ljava/lang/String;)V
  flags: (0x0009) ACC_PUBLIC, ACC_STATIC
  Code:
    stack=2, locals=1, args_size=1
        0: getstatic     #7                  // Field java/lang/System.out:Ljava/io/PrintStream;
        3: ldc           #15                 // String Hello ASM !!!
        5: invokevirtual #17                 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
        8: return
    LineNumberTable:
      line 14: 0
      line 15: 8
    LocalVariableTable:
      Start  Length  Slot  Name   Signature
          0       9     0  args   [Ljava/lang/String;
}
SourceFile: "SimpleHello.java"

The Output.3 from above will guide us to invoke the appropriate ASM visitXXX calls to generate the equivalent Java bytecode.

The following is the code for SimpleHelloUsingASM that will generate the Java bytecode equivalent of SimpleHello using the ASM framework:


Listing.8
/*
 * Description: Simple Java class to print '*** Hello using ASM !!!' created using ASM
 * Author:      Bhaskar S
 * Date:        12/25/2021
 * Blog:        https://www.polarsparc.com
 */

package com.polarsparc.asm;

import org.objectweb.asm.ClassWriter;
import org.objectweb.asm.MethodVisitor;
import org.objectweb.asm.Opcodes;

import java.lang.reflect.Method;

public class SimpleHelloUsingASM {
    private static void createJavaClass(ClassWriter writer) {
        writer.visit(Opcodes.V17,
                Opcodes.ACC_PUBLIC,
                "com/polarsparc/asm/SimpleHelloASM",
                null,
                "java/lang/Object",
                null);
    }

    private static void createDefaultConstructor(ClassWriter writer) {
        MethodVisitor visitor = writer.visitMethod(Opcodes.ACC_PUBLIC,
                "<init>",
                "()V",
                null,
                null);
        visitor.visitCode();
        visitor.visitVarInsn(Opcodes.ALOAD, 0); // this
        visitor.visitMethodInsn(Opcodes.INVOKESPECIAL,
                "java/lang/Object",
                "<init>",
                "()V",
                false);
        visitor.visitInsn(Opcodes.RETURN);
        visitor.visitMaxs(1, 1);
        visitor.visitEnd();
    }

    private static void createStaticMainMethod(ClassWriter writer) {
        MethodVisitor visitor = writer.visitMethod(Opcodes.ACC_PUBLIC + Opcodes.ACC_STATIC,
                "main",
                "([Ljava/lang/String;)V",
                null,
                null);
        visitor.visitCode();
        visitor.visitFieldInsn(Opcodes.GETSTATIC,
                "java/lang/System",
                "out",
                "Ljava/io/PrintStream;");
        visitor.visitLdcInsn("*** Hello using ASM !!!");
        visitor.visitMethodInsn(Opcodes.INVOKEVIRTUAL,
                "java/io/PrintStream",
                "println",
                "(Ljava/lang/String;)V",
                false);
        visitor.visitInsn(Opcodes.RETURN);
        visitor.visitMaxs(2, 1);
        visitor.visitEnd();
        writer.visitEnd();
    }

    static class ByteCodeClassLoader extends ClassLoader {
        public Class<?> defile(String name, byte[] code) {
            return super.defineClass(name, code, 0, code.length);
        }
    }

    public static void main(String[] args) {
        ClassWriter writer = new ClassWriter(ClassWriter.COMPUTE_MAXS);
        createJavaClass(writer);
        createDefaultConstructor(writer);
        createStaticMainMethod(writer);

        ByteCodeClassLoader loader = new ByteCodeClassLoader();

        try {
            Class<?> helloUsingASMClazz = loader.defile("com.polarsparc.asm.SimpleHelloASM",
                    writer.toByteArray());
            Method main = helloUsingASMClazz.getMethod("main", String[].class);
            main.invoke(null, (Object) new String[] {});
        }
        catch (Exception ex) {
            ex.printStackTrace(System.out);
        }
    }
}

The code in Listing.8 above needs some explanation.

Executing the code in Listing.8 above would result in the following output:

Output.4

*** Hello using ASM !!!

References

Github - Source Code

ASM User Guide

ASM Java API

JVM Specification - Java SE 17



© PolarSPARC