Mastering Java ByteCode
Mastering Java ByteCode
Java Bytecode
at the Core of the JVM
By Anton Arhipov, JRebel Product Lead
h i s cod e . . .
e out of t
take a byt
In this Rebel Labs report you will learn how to read and write JVM bytecode
directly, so as to better understand how the runtime works, and be able to
disassemble key libraries that you depend on.
push 1 1
opcodes have some specific semantics attached:
push 1 1
iconst_1
push 2 2
1
push 2 2
iconst_2 1
add 3
iadd 3
The opcodes iconst_1 and iconst_2 put constants 1 and 2 to the stack.
The instruction iadd performs addition operation on the two integers
and leaves the result on the top of the stack.
Instructions are composed from a type prefix and the operation name. For instance, ‘i’ prefix
stands for ‘integer’ and therefore the iadd instruction indicates that the addition operation is
performed for integers.
Depending on the nature of the instructions, we can group these into several broader
groups:
There are also a number of instructions of more specialized tasks such as synchronization
and exception throwing.
We will start with a class that will serve as an entry point for our example application, the moving average calculator.
After the class file is compiled, to obtain the bytecode listing for the example above one needs to execute the following command: javap -c Main
The body of the constructor should be empty but there are a few instructions generated still. Why is that?
Every constructor makes a call to super(), right? It doesn’t happen automagically, and this is why some
bytecode instructions are generated into the default constructor. Basically, this is the super() call;
The main method creates an instance of MovingAverage class and returns. We will review the class
instantiation code in chapter 6.
You might have noticed that some of the instructions are referring to some numbered parameters with
#1, #2, #3. This are the references to the pool of constants. How can we find out what the constants are
and how can we see the constant pool in the listing? We can apply the -verbose argument to javap when
disassembling the class:
Classfile /Users/anton/work-src/demox/out/production/demox/algo/Main.class
Last modified Nov 20, 2012; size 446 bytes
MD5 checksum ae15693cf1a16a702075e468b8aaba74
Compiled from "Main.java"
public class algo.Main
SourceFile: "Main.java"
minor version: 0
major version: 51
flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
#1 = Methodref #5.#21 // java/lang/Object."<init>":()V
#2 = Class #22 // algo/MovingAverage
#3 = Methodref #2.#21 // algo/MovingAverage."<init>":()V
#4 = Class #23 // algo/Main
#5 = Class #24 // java/lang/Object
Theres a bunch of technical information about the class file: when it was compiled, the MD5 checksum,
which *.java file it was compiled from, which Java version it conforms to, etc.
You can also find the denoted constant definitions in the constant pool:
The constant definitions are composable, meaning the constant might be composed from other constants
referenced from the same table.
There are a few other things that reveal itself when using -verbose argument with javap. For instance there’s
more information printed about the methods:
The accessor flags are also generated for methods, but we can also see how deep a stack is required for
execution of the method, how many parameters it takes in, and how many local variable slots need to be
reserved in the local variables table.
Local variables In the debugger, we can drop frames one by one, however the state of the
fields will not be rolled back.
Operand
Stack
Constant
pool
The reason: Some of the opcodes have parameters that take up space in the bytecode array. For instance,
new occupies three slots in the array to operate: one for itself and two for the input parameters. Therefore,
the next instruction - dup - is located at the index 3.
0 1 2 3 4 5 6 7 8
Every instruction has its own HEX representation and if we use that we'll get the HEX string that represents
the method body:
0 1 2 3 4 5 6 7 8
ff 00 02 5g f7 00 03 4c f1
It is even possible to change the bytecode via HEX editor even though it is a bit fragile to
do so. Besides there are some better ways of doing this, like using bytecode manipulation
tools such as ASM or Javassist.
Not much to do with this knowledge at the moment, but now you know where these
numbers come from.
Here are some example instructions that juggle the values around the stack. Some basic instructions first: dup and pop. The dup instruction
duplicates the value on top of the stack. The pop instruction removes the top value from the stack.
There are some more complex instructions: swap, dup_x1 and dup2_x1, for instance. The swap instruction, as the name implies, swaps two
values on the top of the stack, e.g. A and B exchange positions (see example 4); dup_x1 inserts a copy of the top value into the stack two values
from the top (see example 5); dup2_x1 duplicates two top values and inserts beneath the third (example 6).
4) 5) 6)
dup B dup B dup B
pop A pop A pop A
swap swap B swap B
dup_x1 dup_x1 dup_x1 B
dup2_x1 dup2_x1 dup2_x1 A
The dup_x1 and dup2_x1 instructions seem to be a bit esoteric - why we would like to use the swap instruction but the problem is that it
would anyone need to apply such behavior - duplicating top values works only with one-word instructions, meaning it will not work with
under the existing values in the stack? Here’s a more practical example: doubles, and swap2 instruction does not exist. The workaround is then
how to swap 2 values of double type? The caveat is that double takes to use dup2_x2 instruction to duplicate the top double value below
two slots in the stack, which means that if we have two double values the bottom one, and then we can pop the top value using the pop2
on the stack they occupy four slots. To swap the two double values instruction. As a result, the two doubles will be swapped.
Let's now add some more code into our initial example:
int num1 = 1;
int num2 = 2;
ma.submit(num1);
ma.submit(num2);
R e be l y e t ?
y o u t ast edJ
Have
Shameless Advertisement -
we had too much white space here.
Code: LocalVariableTable:
0: new #2 // class algo/MovingAverage Start Length Slot Name Signature
3: dup 0 31 0 args [Ljava/lang/String;
4: invokespecial #3 // Method algo/MovingAverage."<init>":()V 8 23 1 ma Lalgo/MovingAverage;
7: astore_1 10 21 2 num1 I
12 19 3 num2 I
8: iconst_1 30 1 4 avg D
9: istore_2
10: iconst_2
11: istore_3
12: aload_1
13: iload_2
14: i2d
15: invokevirtual #4 // Method algo/MovingAverage.submit:(D)V
18: aload_1
19: iload_3
20: i2d
21: invokevirtual #4 // Method algo/MovingAverage.submit:(D)V
24: aload_1
25: invokevirtual #5 // Method algo/MovingAverage.getAvg:()D
28: dstore 4
12: aload_1
13: iload_2
14: i2d
15: invokevirtual #4 // Method algo/MovingAverage.submit:(D)V
STORE
After calling the getAvg() method the result of the execution locates on
the top of the stack and to store it to the local variable again the dstore
instruction is used since the target variable is of type double. The takeaway from this part is that whenever you want to assign
something to a local variable, it means you want to store it by using a
24: aload_1
25: invokevirtual #5 // Method algo/MovingAverage.getAvg:()D respective instruction, e.g. astore_1. The store instruction will always
28: dstore 4 remove the value from the top of the stack. The corresponding load
instruction will push the value from the local variables table to the stack,
however the value is not removed from the local variable.
We will now change our example so that it will handle an arbitrary number of numbers that can be submitted to the MovingAverage class:
Assume that the numbers variable is a static field in the same class. The bytecode that corresponds to the
loop that iterates over the numbers is as follows
The first instructions of the loop body are used to perform the comparison of the loop counter to
the array length:
18: iload 4
20: iload_3
21: if_icmpge 43
We load the values of i$ and len$ to the stack and call the if_icmpge to compare the values. The if_
icmpge instruction meaning is that if the one value is greater or equal than the other value, in our
case if i$ is greater or equal than len$, then the execution should proceed from the statement that
is marked with 43. If the condition does not hold, then the loop proceeds with the next iteration.
At the end of the loop it loop counter is incremented by 1 and the loop jumps back to the
beginning to validate the loop condition again:
long
From
In our example where an integer value is passed as a parameter to submit() method which actually takes double,
we can see that before actually calling the method the type conversion opcode is applied:
31: iload 5
33: i2d
34: invokevirtual #5 // Method algo/MovingAverage.submit:(D)V
It means we load a value of a local variable to the stack as an integer, and then apply i2d instruction to convert it
into double in order to be able to pass it as a parameter.
The only instruction that doesn't require the value on the stack is the increment instruction, iinc, which operates on
the value sitting in LocalVariableTable directly. All other operations are performed using the stack.
the compiler generated a sequence of opcodes that you can recognize as a While a call to <init> is a constructor invocation, there's another similar
pattern: method, <clinit> which is invoked even earlier. This is the static initializer
name of the class. The static initializer of the class isn't called directly, but
0: new #2 // class algo/MovingAverage
3: dup
triggered by one of the following instructions: new, getstatic, putstatic or
4: invokespecial #3 // Method algo/MovingAverage."<init>":()V invokestatic. That said, if you create a new instance of the class, access a
static field or call a static method, the static initializer is triggered.
When you see new, dup and invokespecial instructions together it must
ring a bell - this is the class instance creation! In fact, there is even more options to trigger the static initializer as described
in the Chapter 5.5 of JVM specification [4]
Why three instructions instead of one, you ask? The new instruction creates the
object but it doesn't call the constructor, for that, the invokespecial instruction
is called: it invokes the mysterious method called <init>, which is actually the
constructor. The dup instruction is used to duplicate the value on the top of
the stack. As the constructor call doesn't return a value, after calling the <init>
method on the object the object will be initialized but the stack will be empty so
we wouldn't be able to do anything with the object after it was initialized. This is
why we need to duplicate the reference in advance so that after the constructor
returns we can assign the object instance into a local variable or a field. Hence,
the next instruction is usually one of the following:
invokevirtual is used to call public, protected and package private methods class A
1: method1
if the target object of a concrete type. 2: method2
4: methodX
The new method is at index 4 and it looks like it is not any different from
method3 in this situation. However, what if theres another class, C, which
also implements the interface but does not belong to the same hierarchy as
A and B:
class C implements X
1: methodC
2: methodX
The interface method is not at the same position as in class B any more
and this is why runtime is more restricted in respect to invokinterface,
meaning it can do less assumptions in method resolution process than with
invokevirtual.
Next, we have to generate the default constructor and the main method. If
The most common scenario to generate bytecode that corresponds to you skip generating the default constructor nothing bad will happen, but it
the example source, is to create ClassWriter, visit the structure – fields, is still polite to generate one.
methods, etc, and after the job is done, write out the final bytes.
MethodVisitor constructor =
cw.visitMethod(
First, let’s construct the ClassWriter instance:
Opcodes.ACC_PUBLIC,
"<init>",
ClassWriter cw = new ClassWriter( "()V",
ClassWriter.COMPUTE_MAXS | null,
ClassWriter.COMPUTE_FRAMES); null);
The ClassWriter instance can be instantiated with some constants that constructor.visitCode();
indicate the behavior that the instance should have. COMPUTE_MAXS tells
ASM to automatically compute the maximum stack size and the maximum //super()
constructor.visitVarInsn(Opcodes.ALOAD, 0);
number of local variables of methods. COMPUTE_FRAMES flag makes ASM
constructor.visitMethodInsn(Opcodes.INVOKESPECIAL,
to automatically compute the stack map frames of methods from scratch. "java/lang/Object", "<init>", "()V");
constructor.visitInsn(Opcodes.RETURN);
constructor.visitMaxs(0, 0);
constructor.visitEnd();
MethodVisitor mv = cw.visitMethod(
Opcodes.ACC_PUBLIC + Opcodes.ACC_STATIC,
"main", "([Ljava/lang/String;)V", null, null);
mv.visitFieldInsn(Opcodes.GETSTATIC, "java/lang/System",
"out", "Ljava/io/PrintStream;");
mv.visitLdcInsn("Hello, World!");
mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/io/PrintStream",
"println", "(Ljava/lang/String;)V");
mv.visitInsn(Opcodes.RETURN);
mv.visitMaxs(0, 0);
mv.visitEnd();
By calling the visitMethod() again, we generated the new method definition with the name,
modifiers and the signature. Again, visitCode(), visitMaxs() and visitEnd() methods are used the
same way as in case with the constructor.
As you can see the code is full of constants, “flags” and “indicators” and the final code is not very
fluently readably by human eyes. At the same time, to write such code one needs to keep in mind
the bytecode execution plan to be able to produce correct version of bytecode. This is what makes
writing such code rather a complicated task. This is where everyone has his own approach it
writing code with ASM.
You can also apply the ASMifier directly, without the IDE plugin, as it is a
part of ASM libabray:
That is not supposed to tell you I am good at generating bytecode... no no.. I wouldn't be able to read it so good if I
had not the questionable pleasure of looking at it countless times, because there again was a pop of an empty stack or
something like that. It is more that the problems I have to look for tend to repeat themselves and I have a whit of what to
look for even before I fire up Textifier
Well.. one time maybe a little... I told you about the API I use to do a swap. In the beginning it was not working properly
of course. That was partially due to me misunderstanding one for those DUP instructions, but mainly it was because I
had a simple bug in my code in which I execute the 1-2 swap instead of the 2-1 swap (meaning swapping 1 and 2 slot
operands). So I was looking at the code, totally confused, thinking this should work, looking at my code... then thinking I
made it wrong with those dups and replacing the code with my new understanding...
All the while the code was not really all that wrong, only the swap cases where swapped. Anyway... after about a full day
of getting a headache from too much looking at the bytecode I finally found my mistake and looked at the code to find
it looks almost the same as before... and then it dawned on me, that it was only that simple mistake, that could have
been corrected in a minute and which took me a full day. Not really funny, but there I laughed a bit at myself actually.
Today I think it's a very simple way to have people test bytecode directly, for example for students. It makes writing
bytecode a lot easier than using ASM directly. However, I also received a lot of complains, people saying I opened the
Pandora box and that it would produce unreadable code in production :D (and I would definitely not recommend using
it in production). Yet, it's been more than one year the project is out, and I haven't heard of anyone using it, so probably
bytecode is really not that fun!
We also came across a few interesting things in HotSpot, for example, if you call an absent method on an array object (like
array.set()), you don't get a NoSuchMethodError, or anything like that. What you get (what we got on a HotSpot we had a
year ago, anyway) is... a native crash. Segmentation fault, if I am not mistaken. Our theory is that the vtable for arrays if so
optimized that it is not even there, and lookup crashes because of that.
t act Us
Co n
[email protected]
Estonia USA
Ülikooli 2, 5th floor 545 Boylston St., 4th flr.
Tartu, Estonia, 51003 Boston, MA, USA, 02116 This report is brought to you by:
Phone: +372 740 4533 Phone: 1(857)277-1199 Anton Arhipov, Erkki Lindpere, Ryan St. James & Oliver White
All rights reserved. 2012 (c) ZeroTurnaround OÜ 33