Fading Coder

One Final Commit for the Last Sprint

Home > Tech > Content

Decompiling Java: Bytecode Execution, Stacks, and Control Flow

Tech 1

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 (the this reference) onto the operand stack.
  • b7 (invokespecial): Invokes an instance constructor method.
  • 00 01: References constant pool entry #1, representing java/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: return

Operand 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 for args, 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: return

Notice 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 = 28

The 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

OpcodeMnemonicCondition
0x99ifeq== 0
0x9aifne!= 0
0x9biflt< 0
0x9cifge>= 0
0x9difgt> 0
0x9eifle<= 0
0x9fif_icmpeqint == int
0xa0if_icmpneint != int
0xa1if_icmpltint < int
0xa2if_icmpgeint >= int
0xa3if_icmpgtint > int
0xa4if_icmpleint <= int
0xa5if_acmpeqreference == reference
0xa6if_acmpnereference != reference
0xc6ifnull== null
0xc7ifnonnull!= 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: return

Loop 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: return

do-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: return

for 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: return

The 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: 0

The 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.

Tags: JavaJVM

Related Articles

Understanding Strong and Weak References in Java

Strong References Strong reference are the most prevalent type of object referencing in Java. When an object has a strong reference pointing to it, the garbage collector will not reclaim its memory. F...

Comprehensive Guide to SSTI Explained with Payload Bypass Techniques

Introduction Server-Side Template Injection (SSTI) is a vulnerability in web applications where user input is improper handled within the template engine and executed on the server. This exploit can r...

Implement Image Upload Functionality for Django Integrated TinyMCE Editor

Django’s Admin panel is highly user-friendly, and pairing it with TinyMCE, an effective rich text editor, simplifies content management significantly. Combining the two is particular useful for bloggi...

Leave a Comment

Anonymous

◎Feel free to join the discussion and share your thoughts.