Apache Commons logo Commons BCEL

The BCEL API

The BCEL API abstracts from the concrete circumstances of the Java Virtual Machine and how to read and write binary Java class files. The API mainly consists of three parts:

  1. A package that contains classes that describe "static" constraints of class files, i.e., reflects the class file format and is not intended for byte code modifications. The classes may be used to read and write class files from or to a file. This is useful especially for analyzing Java classes without having the source files at hand. The main data structure is called JavaClass which contains methods, fields, etc..
  2. A package to dynamically generate or modify JavaClass or Method objects. It may be used to insert analysis code, to strip unnecessary information from class files, or to implement the code generator back-end of a Java compiler.
  3. Various code examples and utilities like a class file viewer, a tool to convert class files into HTML, and a converter from class files to the Jasmin assembly language.

JavaClass

The "static" component of the BCEL API resides in the package org.apache.bcel.classfile and closely represents class files. All of the binary components and data structures declared in the JVM specification and described in section 2 are mapped to classes. Figure 3 shows an UML diagram of the hierarchy of classes of the BCEL API. Figure 8 in the appendix also shows a detailed diagram of the ConstantPool components.


Figure 3: UML diagram for the JavaClass API

The top-level data structure is JavaClass, which in most cases is created by a ClassParser object that is capable of parsing binary class files. A JavaClass object basically consists of fields, methods, symbolic references to the super class and to the implemented interfaces.

The constant pool serves as some kind of central repository and is thus of outstanding importance for all components. ConstantPool objects contain an array of fixed size of Constant entries, which may be retrieved via the getConstant() method taking an integer index as argument. Indexes to the constant pool may be contained in instructions as well as in other components of a class file and in constant pool entries themselves.

Methods and fields contain a signature, symbolically defining their types. Access flags like public static final occur in several places and are encoded by an integer bit mask, e.g., public static final matches to the Java expression

int access_flags = ACC_PUBLIC | ACC_STATIC | ACC_FINAL;

As mentioned in section 2.1 already, several components may contain attribute objects: classes, fields, methods, and Code objects (introduced in section 2.3). The latter is an attribute itself that contains the actual byte code array, the maximum stack size, the number of local variables, a table of handled exceptions, and some optional debugging information coded as LineNumberTable and LocalVariableTable attributes. Attributes are in general specific to some data structure, i.e., no two components share the same kind of attribute, though this is not explicitly forbidden. In the figure the Attribute classes are stereotyped with the component they belong to.

Class repository

Using the provided Repository class, reading class files into a JavaClass object is quite simple:

JavaClass clazz = Repository.lookupClass("java.lang.String");

The repository also contains methods providing the dynamic equivalent of the instanceof operator, and other useful routines:

if (Repository.instanceOf(clazz, super_class)) {
    ...
}
      

Accessing class file data

Information within the class file components may be accessed like Java Beans via intuitive set/get methods. All of them also define a toString() method so that implementing a simple class viewer is very easy. In fact all of the examples used here have been produced this way:

System.out.println(clazz);
printCode(clazz.getMethods());
...
public static void printCode(Method[] methods) {
    for (int i = 0; i < methods.length; i++) {
        System.out.println(methods[i]);
        
        Code code = methods[i].getCode();
        if (code != null) // Non-abstract method
        System.out.println(code);
    }
}
      

Analyzing class data

Last but not least, BCEL supports the Visitor design pattern, so one can write visitor objects to traverse and analyze the contents of a class file. Included in the distribution is a class JasminVisitor that converts class files into the Jasmin assembler language.

ClassGen

This part of the API (package org.apache.bcel.generic) supplies an abstraction level for creating or transforming class files dynamically. It makes the static constraints of Java class files like the hard-coded byte code addresses "generic". The generic constant pool, for example, is implemented by the class ConstantPoolGen which offers methods for adding different types of constants. Accordingly, ClassGen offers an interface to add methods, fields, and attributes. Figure 4 gives an overview of this part of the API.


Figure 4: UML diagram of the ClassGen API

Types

We abstract from the concrete details of the type signature syntax (see 2.5) by introducing the Type class, which is used, for example, by methods to define their return and argument types. Concrete sub-classes are BasicType, ObjectType, and ArrayType which consists of the element type and the number of dimensions. For commonly used types the class offers some predefined constants. For example, the method signature of the main method as shown in section 2.5 is represented by:

Type return_type = Type.VOID;
Type[] arg_types = new Type[] { new ArrayType(Type.STRING, 1) };
      

Type also contains methods to convert types into textual signatures and vice versa. The sub-classes contain implementations of the routines and constraints specified by the Java Language Specification.

Generic fields and methods

Fields are represented by FieldGen objects, which may be freely modified by the user. If they have the access rights static final, i.e., are constants and of basic type, they may optionally have an initializing value.

Generic methods contain methods to add exceptions the method may throw, local variables, and exception handlers. The latter two are represented by user-configurable objects as well. Because exception handlers and local variables contain references to byte code addresses, they also take the role of an instruction targeter in our terminology. Instruction targeters contain a method updateTarget() to redirect a reference. This is somewhat related to the Observer design pattern. Generic (non-abstract) methods refer to instruction lists that consist of instruction objects. References to byte code addresses are implemented by handles to instruction objects. If the list is updated the instruction targeters will be informed about it. This is explained in more detail in the following sections.

The maximum stack size needed by the method and the maximum number of local variables used may be set manually or computed via the setMaxStack() and setMaxLocals() methods automatically.

Instructions

Modeling instructions as objects may look somewhat odd at first sight, but in fact enables programmers to obtain a high-level view upon control flow without handling details like concrete byte code offsets. Instructions consist of an opcode (sometimes called tag), their length in bytes and an offset (or index) within the byte code. Since many instructions are immutable (stack operators, e.g.), the InstructionConstants interface offers shareable predefined "fly-weight" constants to use.

Instructions are grouped via sub-classing, the type hierarchy of instruction classes is illustrated by (incomplete) figure in the appendix. The most important family of instructions are the branch instructions, e.g., goto, that branch to targets somewhere within the byte code. Obviously, this makes them candidates for playing an InstructionTargeter role, too. Instructions are further grouped by the interfaces they implement, there are, e.g., TypedInstructions that are associated with a specific type like ldc, or ExceptionThrower instructions that may raise exceptions when executed.

All instructions can be traversed via accept(Visitor v) methods, i.e., the Visitor design pattern. There is however some special trick in these methods that allows to merge the handling of certain instruction groups. The accept() do not only call the corresponding visit() method, but call visit() methods of their respective super classes and implemented interfaces first, i.e., the most specific visit() call is last. Thus one can group the handling of, say, all BranchInstructions into one single method.

For debugging purposes it may even make sense to "invent" your own instructions. In a sophisticated code generator like the one used as a backend of the Barat framework for static analysis one often has to insert temporary nop (No operation) instructions. When examining the produced code it may be very difficult to track back where the nop was actually inserted. One could think of a derived nop2 instruction that contains additional debugging information. When the instruction list is dumped to byte code, the extra data is simply dropped.

One could also think of new byte code instructions operating on complex numbers that are replaced by normal byte code upon load-time or are recognized by a new JVM.

Instruction lists

An instruction list is implemented by a list of instruction handles encapsulating instruction objects. References to instructions in the list are thus not implemented by direct pointers to instructions but by pointers to instruction handles. This makes appending, inserting and deleting areas of code very simple and also allows us to reuse immutable instruction objects (fly-weight objects). Since we use symbolic references, computation of concrete byte code offsets does not need to occur until finalization, i.e., until the user has finished the process of generating or transforming code. We will use the term instruction handle and instruction synonymously throughout the rest of the paper. Instruction handles may contain additional user-defined data using the addAttribute() method.

Appending: One can append instructions or other instruction lists anywhere to an existing list. The instructions are appended after the given instruction handle. All append methods return a new instruction handle which may then be used as the target of a branch instruction, e.g.:

InstructionList il = new InstructionList();
...
GOTO g = new GOTO(null);
il.append(g);
...
// Use immutable fly-weight object
InstructionHandle ih = il.append(InstructionConstants.ACONST_NULL);
g.setTarget(ih);
      

Inserting: Instructions may be inserted anywhere into an existing list. They are inserted before the given instruction handle. All insert methods return a new instruction handle which may then be used as the start address of an exception handler, for example.

InstructionHandle start = il.insert(insertion_point, InstructionConstants.NOP);
...
mg.addExceptionHandler(start, end, handler, "java.io.IOException");
      

Deleting: Deletion of instructions is also very straightforward; all instruction handles and the contained instructions within a given range are removed from the instruction list and disposed. The delete() method may however throw a TargetLostException when there are instruction targeters still referencing one of the deleted instructions. The user is forced to handle such exceptions in a try-catch clause and redirect these references elsewhere. The peep hole optimizer described in the appendix gives a detailed example for this.

try {
    il.delete(first, last);
} catch (TargetLostException e) {
    for (InstructionHandle target : e.getTargets()) {
        for (InstructionTargeter targeter : target.getTargeters()) {
            targeter.updateTarget(target, new_target);
        }
    }
}
      

Finalizing: When the instruction list is ready to be dumped to pure byte code, all symbolic references must be mapped to real byte code offsets. This is done by the getByteCode() method which is called by default by MethodGen.getMethod(). Afterwards you should call dispose() so that the instruction handles can be reused internally. This helps to improve memory usage.

InstructionList il = new InstructionList();

ClassGen  cg = new ClassGen("HelloWorld", "java.lang.Object",
        "<generated>", ACC_PUBLIC | ACC_SUPER, null);
MethodGen mg = new MethodGen(ACC_STATIC | ACC_PUBLIC,
        Type.VOID, new Type[] { new ArrayType(Type.STRING, 1) }, 
        new String[] { "argv" }, "main", "HelloWorld", il, cp);
...
cg.addMethod(mg.getMethod());
il.dispose(); // Reuse instruction handles of list
      

Code example revisited

Using instruction lists gives us a generic view upon the code: In Figure 5 we again present the code chunk of the readInt() method of the factorial example in section 2.6: The local variables n and e1 both hold two references to instructions, defining their scope. There are two gotos branching to the iload at the end of the method. One of the exception handlers is displayed, too: it references the start and the end of the try block and also the exception handler code.


Figure 5: Instruction list for readInt() method

Instruction factories

To simplify the creation of certain instructions the user can use the supplied InstructionFactory class which offers a lot of useful methods to create instructions from scratch. Alternatively, he can also use compound instructions: When producing byte code, some patterns typically occur very frequently, for instance the compilation of arithmetic or comparison expressions. You certainly do not want to rewrite the code that translates such expressions into byte code in every place they may appear. In order to support this, the BCEL API includes a compound instruction (an interface with a single getInstructionList() method). Instances of this class may be used in any place where normal instructions would occur, particularly in append operations.

Example: Pushing constants Pushing constants onto the operand stack may be coded in different ways. As explained in section 2.2 there are some "short-cut" instructions that can be used to make the produced byte code more compact. The smallest instruction to push a single 1 onto the stack is iconst_1, other possibilities are bipush (can be used to push values between -128 and 127), sipush (between -32768 and 32767), or ldc (load constant from constant pool).

Instead of repeatedly selecting the most compact instruction in, say, a switch, one can use the compound PUSH instruction whenever pushing a constant number or string. It will produce the appropriate byte code instruction and insert entries into to constant pool if necessary.

InstructionFactory f  = new InstructionFactory(class_gen);
InstructionList    il = new InstructionList();
...
il.append(new PUSH(cp, "Hello, world"));
il.append(new PUSH(cp, 4711));
...
il.append(f.createPrintln("Hello World"));
...
il.append(f.createReturn(type));
      

Code patterns using regular expressions

When transforming code, for instance during optimization or when inserting analysis method calls, one typically searches for certain patterns of code to perform the transformation at. To simplify handling such situations BCEL introduces a special feature: One can search for given code patterns within an instruction list using regular expressions. In such expressions, instructions are represented by their opcode names, e.g., LDC, one may also use their respective super classes, e.g., "IfInstruction". Meta characters like +, *, and (..|..) have their usual meanings. Thus, the expression

"NOP+(ILOAD|ALOAD)*"

represents a piece of code consisting of at least one NOP followed by a possibly empty sequence of ILOAD and ALOAD instructions.

The search() method of class org.apache.bcel.util.InstructionFinder gets a regular expression and a starting point as arguments and returns an iterator describing the area of matched instructions. Additional constraints to the matching area of instructions, which can not be implemented via regular expressions, may be expressed via code constraint objects.

Example: Optimizing boolean expressions

In Java, boolean values are mapped to 1 and to 0, respectively. Thus, the simplest way to evaluate boolean expressions is to push a 1 or a 0 onto the operand stack depending on the truth value of the expression. But this way, the subsequent combination of boolean expressions (with &&, e.g) yields long chunks of code that push lots of 1s and 0s onto the stack.

When the code has been finalized these chunks can be optimized with a peep hole algorithm: An IfInstruction (e.g. the comparison of two integers: if_icmpeq) that either produces a 1 or a 0 on the stack and is followed by an ifne instruction (branch if stack value 0) may be replaced by the IfInstruction with its branch target replaced by the target of the ifne instruction:

CodeConstraint constraint = new CodeConstraint() {
    public boolean checkCode(InstructionHandle[] match) {
        IfInstruction if1 = (IfInstruction) match[0].getInstruction();
        GOTO g = (GOTO) match[2].getInstruction();
        return (if1.getTarget() == match[3]) &&
            (g.getTarget() == match[4]);
    }
};

InstructionFinder f = new InstructionFinder(il);
String pat = "IfInstruction ICONST_0 GOTO ICONST_1 NOP(IFEQ|IFNE)";

for (Iterator e = f.search(pat, constraint); e.hasNext(); ) {
    InstructionHandle[] match = (InstructionHandle[]) e.next();;
    ...
    match[0].setTarget(match[5].getTarget()); // Update target
    ...
    try {
        il.delete(match[1], match[5]);
    } catch (TargetLostException ex) {
        ...
    }
}
      

The applied code constraint object ensures that the matched code really corresponds to the targeted expression pattern. Subsequent application of this algorithm removes all unnecessary stack operations and branch instructions from the byte code. If any of the deleted instructions is still referenced by an InstructionTargeter object, the reference has to be updated in the catch-clause.

Example application: The expression:

        if ((a == null) || (i < 2))
        System.out.println("Ooops");
      

can be mapped to both of the chunks of byte code shown in figure 6. The left column represents the unoptimized code while the right column displays the same code after the peep hole algorithm has been applied:

              5:  aload_0
              6:  ifnull        #13
              9:  iconst_0
              10: goto          #14
              13: iconst_1
              14: nop
              15: ifne          #36
              18: iload_1
              19: iconst_2
              20: if_icmplt     #27
              23: iconst_0
              24: goto          #28
              27: iconst_1
              28: nop
              29: ifne          #36
              32: iconst_0
              33: goto          #37
              36: iconst_1
              37: nop
              38: ifeq          #52
              41: getstatic     System.out
              44: ldc           "Ooops"
              46: invokevirtual println
              52: return
            
              10: aload_0
              11: ifnull        #19
              14: iload_1
              15: iconst_2
              16: if_icmpge     #27
              19: getstatic     System.out
              22: ldc           "Ooops"
              24: invokevirtual println
              27: return