Decompiling Java: Bytecode Execution, Stacks, and Control Flow
Decompiling Java: Opcodes, Stacks, and Execution Flow
Raw Bytecode Interpretation
JVM instructions consist of a one-byte opcode followed by optional operands. Consider the raw hexadecimal bytes for a default constructor: 2a b7 00 01 b1.
2a(aload_0): Pushes the local variable at index 0 (thethisreference) onto the operand stack.b7(invokespecial): Invokes an instance constructor method.00 01: References constant pool entry #1, representingjava/lang/Object."<init>":()V.b1(return): Returns void from the method.
Similarly, a basic main method printing a string yields bytes like b2 00 02 12 03 b6 00 04 b1.
b2(getstatic): Fetches a static field.00 02: Constant pool #2, e.g.,java/lang/System.out:Ljava/io/PrintStream;.12(ldc): Pushes a constant onto the stack.03: Constant pool #3, e.g., the string literal"Greetings".b6(invokevirtual): Invokes a virtual method.00 04: Constant pool #4, e.g.,java/io/PrintStream.println:(Ljava/lang/String;)V.b1(return).
Using the Javap Disassembler
The javap -v utility translates class files into human-readable formats adhering to the class file structure (magic number, version, constant pool, fields, methods, attributes).
public class App {
public static void main(String[] args) {
System.out.println("Greetings");
}
}Disassembling the compiled class reveals constant pool resolutions directly within the method bytecode, replacing raw hex offsets with readable comments.
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=1, args_size=1
0: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
3: ldc #3 // String Greetings
5: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
8: returnOperand Stack, Local Variables, and Runtime Constant Pool
During class loading, bytecode and the constant pool move into the method area (Metaspace in JDK 8). When a method executes, the JVM allocates a stack frame containing a local variable array and an operand stack.
public class Calculation {
public static void main(String[] args) {
int x = 5;
int y = Short.MAX_VALUE + 2;
int z = x + y;
System.out.println(z);
}
}The main method bytecode indicates stack=2, locals=4, args_size=1.
max_stack: Maximum depth of the operand stack (2).max_locals: Length of the local variable array (4 slots forargs,x,y,z).
Execution trace:
0: iconst_5 // Push 5
1: istore_1 // Store into slot 1 (x)
2: ldc #3 // Push constant pool #3 (32769)
4: istore_2 // Store into slot 2 (y)
5: iload_1 // Load x
6: iload_2 // Load y
7: iadd // Add x and y
8: istore_3 // Store result into slot 3 (z)
9: getstatic #4 // Field java/lang/System.out:Ljava/io/PrintStream;
12: iload_3 // Load z
13: invokevirtual #5 // Method java/io/PrintStream.println:(I)V
16: returnNotice that 5 uses iconst_5 (optimized for small integers), while 32769 exceeds the short range and resides in the runtime constant pool, loaded via ldc.
Increment Operators in Bytecode
The iinc instruction modifies a local variable directly without using the operand stack.
public class IncrementDemo {
public static void main(String[] args) {
int m = 8;
int n = m++ + ++m + m--; // 8 + 10 + 10 = 28
System.out.println(m); // 9
System.out.println(n); // 28
}
}Bytecode breakdown for the expression:
0: bipush 8
2: istore_1 // m = 8
3: iload_1 // Push m (8) to stack
4: iinc 1, 1 // Increment local m directly (m=9)
7: iinc 1, 1 // Increment local m directly (m=10)
10: iload_1 // Push m (10) to stack
11: iadd // Stack: 8 + 10 = 18
12: iload_1 // Push m (10) to stack
13: iinc 1, -1 // Decrement local m directly (m=9)
16: iadd // Stack: 18 + 10 = 28
17: istore_2 // n = 28The distinction between m++ and ++m boils down to the execution order of iload (pushing to the stack) and iinc (incrementing the local variable). Binary operations like addition always require loading operands onto the stack first.
Control Flow and Loop Instructions
Conditional Branching
| Opcode | Mnemonic | Condition |
|---|---|---|
| 0x99 | ifeq | == 0 |
| 0x9a | ifne | != 0 |
| 0x9b | iflt | < 0 |
| 0x9c | ifge | >= 0 |
| 0x9d | ifgt | > 0 |
| 0x9e | ifle | <= 0 |
| 0x9f | if_icmpeq | int == int |
| 0xa0 | if_icmpne | int != int |
| 0xa1 | if_icmplt | int < int |
| 0xa2 | if_icmpge | int >= int |
| 0xa3 | if_icmpgt | int > int |
| 0xa4 | if_icmple | int <= int |
| 0xa5 | if_acmpeq | reference == reference |
| 0xa6 | if_acmpne | reference != reference |
| 0xc6 | ifnull | == null |
| 0xc7 | ifnonnull | != null |
int val = 0;
if (val == 0) {
val = 5;
} else {
val = 15;
}Bytecode:
0: iconst_0
1: istore_1
2: iload_1
3: ifne 12 // Jump to 12 if val != 0
6: bipush 5
8: istore_1
9: goto 15 // Skip else block
12: bipush 15
14: istore_1
15: returnLoop Structures
while loop:
int count = 0;
while (count < 5) {
count++;
}0: iconst_0
1: istore_1
2: iload_1
3: iconst_5
4: if_icmpge 13 // Exit loop if count >= 5
7: iinc 1, 1
10: goto 2 // Jump back to condition
13: returndo-while loop:
int count = 0;
do {
count++;
} while (count < 5);0: iconst_0
1: istore_1
2: iinc 1, 1
5: iload_1
6: iconst_5
7: if_icmplt 2 // Jump back if count < 5
10: returnfor loop:
for (int idx = 0; idx < 5; idx++) {}0: iconst_0
1: istore_1
2: iload_1
3: iconst_5
4: if_icmpge 13
7: iinc 1, 1
10: goto 2
13: returnThe generated bytecode for for and while loops is identical.
Common Trap: Self-Assignment with Post-Increment
int idx = 0;
int total = 0;
while (idx < 5) {
total = total++;
idx++;
}
System.out.println(total); // Output: 0The expression total = total++ evaluates as follows: the current value of total (0) is pushed onto the operand stack. Then, the local variable total is incremented to 1. Finally, the original value (0) is popped from the stack and stored back into the local variable total, overwriting the increment. Thus, total remains 0.