ted.neward@newardassociates.com | Blog: http://blogs.newardassociates.com | Github: tedneward | LinkedIn: tedneward
Recognize & understand JVM bytecode
Gain familiarity with bytecode tools
Use bytecode to gain deeper insight into Java language features
Instruction set for execution of the Java Virtual Machine
Conceptually similar to VB's P-code (or earlier precedents)
Provides portable representation of executable code
Can be interpreted or JIT compiled to native code
Most JVMs JIT-compile frequently-executed methods: "Hotspots"
Some experimental JVMs will re-JIT multiple times
"The assembly language of the Java generation"
Class file format could contain CPU instructions
In fact, could even contain multiple CPU's instructions
Avoid hit of JIT compilation at runtime, or interpretation
Or Java could just compile directly to native instructions
see GraalVM AOT
see gcj
So why bother?
Managed environments provide safety, robustness, security
Harder to do with raw CPU instructions
JVMIS can be optimized to particular CPU at runtime if desired
But optimizations don't have to be decided at compile-time
WORA
Other languages starting to encroach on the JVM
Groovy, JRuby, others starting to challenge Java's supremacy
Bytecode-manipulation toolkits becoming more popular
Better understanding of what Java compiler generates
Question: How are inner classes implemented?
Question: How are generics implemented (in 1.5)?
Question: What's the cost of J2SE 1.4's assert
keyword?
Question: What is a Java record
, really?
Helps understand Java at much deeper level
Better understanding of the limitations of obfuscation
Useful for 3rd-party debugging/spelunking
Java developer's best friend
disassemble any .class file
pass either .class file, or classname (via classpath)
-v
: verbose (prints metadata and constant table)
-p
: prints all members (public, protected, package, and private)
-c
: disassemble the instructions
-l
: prints line number and local variable tables
-s
: print internal type signatures
package com.newardassociates.demo; public class App { public static void main(String[] args) { System.out.println("Hello world!"); } }
$ javap -p -c -l com/newardassociates/demo/App.class Compiled from "App.java" public class com.newardassociates.demo.App { public com.newardassociates.demo.App(); Code: 0: aload_0 1: invokespecial #1 // Method java/lang/Object."<init>":()V 4: return LineNumberTable: line 6: 0 LocalVariableTable: Start Length Slot Name Signature 0 5 0 this Lcom/newardassociates/demo/App; public static void main(java.lang.String[]); Code: 0: getstatic #7 // Field java/lang/System.out:Ljava/io/PrintStream; 3: ldc #13 // String Hello world! 5: invokevirtual #15 // Method java/io/PrintStream.println:(Ljava/lang/String;)V 8: return LineNumberTable: line 8: 0 line 9: 8 LocalVariableTable: Start Length Slot Name Signature 0 9 0 args [Ljava/lang/String; }
Dynamically loaded, linked at runtime
Loaded via cooperating collection of ClassLoaders
Verified at load-time
One class per classfile
All operations produce/consume stack elements
Locals, incoming parameters, live on stack
Stack slots are 32 bits wide (longs/doubles == 2 slots)
May or may not use real stack if JITted
registers are often faster
thus JITters often elevate stack slots to be registers
"native" name format looks a bit different
primitives have single-letter codes (I, J, V, ...)
classes are L
slash-separated-fully-qualified-name;
format
array typenames are "[type;", with one "[" per dimension
"$" often used for synthesized class/field/method names
public String toString() { ... }
toString()Ljava/lang/String;
public BoardCell tictactoeBoard(int playerMove) { ... }
tictactoeBoard([I)[[Lcom.tictactoe.BoardCell
public byte getByteArray() { ... }
getByteArray()[B
public void write(byte, int, int) { ... }
write([BII)V
nested Point class inside of com.newardassociates.demo.App
Lcom/newardassociates/demo/App$Point;
Chapter 4, Java Virtual Machine Specification (jvms)
https://docs.oracle.com/javase/specs/jvms/se24/html/jvms-4.html
each class file contains the definition of a single class, interface, or module
"file" here is a loosely-used term
really it's just a bytearray, so could come from file, ZIP, database table, etc
"a stream of 8-bit bytes"
"16-bit and 32-bit quantities are constructed by reading in two and four consecutive 8-bit bytes, respectively"
"data items are always stored in big-endian order, where the high bytes come first"
"data types u1
, u2
, and u4
to represent an unsigned one-, two-, or four-byte quantity"
"A class file consists of a single ClassFile structure"
ClassFile structure
ClassFile { u4 magic; u2 minor_version; u2 major_version; u2 constant_pool_count; cp_info constant_pool[constant_pool_count-1]; u2 access_flags; u2 this_class; u2 super_class; // ...
ClassFile structure
ClassFile { // ... u2 interfaces_count; u2 interfaces[interfaces_count]; u2 fields_count; field_info fields[fields_count]; u2 methods_count; method_info methods[methods_count]; u2 attributes_count; attribute_info attributes[attributes_count]; }
magic
is always 0xCAFEBABE
minor_version
is pretty ignored
major_version
starts at 45 (Java 1.0.2, 1.1) and increments to 68 (Java24).
all the _count
s are u2
, which means no more than 64k entries allowed
yes, your class can have up to 65,000 fields
... or up to 65,000 methods
... or even 65,000 interfaces!
please don't do that
practical concern: constant pool entries (limited to u2 also)
essentially, each class is a single-rooted tree (the class)
class node contains metadata about the class
then we have a number of "_info" structures
cp_info
: Constant pool info
field_info
: for each field
method_info
: for each method
attribute_info
: generic name/value "blob" containing information
... has some metadata about it (name, base class, interfaces, etc)
... has a "pool" (table) of every constant used inside of it
... has 0-n attributes describing different aspects of it
... has 0-n fields and 0-n methods...
... each of which has 0-n attributes describing different aspects of them
any constant (even class, method, and field names) goes into the pool
each entry is a cp_info
, consisting of a u1
"tag" and byte array of "info"
17 different kinds of tags:
String, Integer, Float, Long, Double, Utf8
Class, Module, Package
Fieldref, Methodref, InterfaceMethodref, NameAndType
MethodHandle, MethodType, Dynamic
think of these as either raw values or pointers back into the constant pool
u2 access_flags
: bitmask of flags like PUBLIC, PRIVATE, STATIC, FINAL, VOLATILE, ...
u2 name_index
: CP index to the field's name
u2 descriptor_index
: CP index to field's descriptor
u2 attributes_count
/attribute_info
: array of attributes
u2 access_flags
: bitmask of flags like PUBLIC, PRIVATE, STATIC, FINAL, SYNCHRONIZED, SYNTHETIC, ...
u2 name_index
: CP index to method's name
u2 descriptor_index
: CP index to method's descriptor
u2 attributes_count
/attribute_info
: array of attributes
meat of the content of a classfile
30 different kinds of attributes
ConstantValue
Code
Exception
InnerClasses
LineNumberTable, LocalVariableTable, BootstrapMethodsTable, ...
most of the evolution of the JVM has been adding these attributes
u2 attribute_name_index
: CP index to "Code"
u4 attribute_length
: size of this attribute
u2 max_stack
: maximum depth of the operand stack of this method
u2 max_locals
: maximum number of locals allocated in this method
u4 code_length
: how many bytes of code?
u1 code[code_length]
: the code for the given method
u2 exception_table_length
: the exception handling tables
{ u2 start_pc
: starting point for handler
u2 end_pc
: ending for handler
u2 handler_pc
u2 catch_type
: type of catch handler
} exception_table[exception_table_length]
u2 attributes_count
: additional attributes (StackMapTable, Exceptions, ...)
attribute_info attributes[attributes_count]
https://javaalmanac.io/bytecode/
helpful resource for exploring bytecode
https://godbolt.org/
"Compiler Explorer"
Push a constant value onto the stack
aconst_null
: Push null
reference
tconst_
N: Push a constant N (-1, 0, 1, 2, 3, 4 or 5) of type t (i
, l
, d
, f
)
bipush
: Push a constant operand (-128 to 127) value
sipush
: Push a constant operand (-32k to 32k) value
dup
, dup2
: Duplicate top element of stack (pop, push, push)
pop
, pop2
: Remove top element of stack
ldc
X, ldc_w
X,ldc2_w
X: push constant_poolX onto stack
the "w" is for "wide" operands (e.g., take up 2 slots on the operand stack)
some assemblers/disassemblers will hide the constant pool index and just use value
local variables are a 0-indexed array
Local load: Push content of local var onto stack
tload
N: push local var (of type t) N onto stack
one for each data type (a
, d
, f
, i
, l
)
aload_0
: often the this
reference for an object
Local store: Pop top element of stack into local var
tstore
N: pop TOS into local var (of type t) N
astore
, istore
, ...: one for each data type (a
, d
, f
, i
, l
)
Data conversion: X-2
-Y opcode naming: d2f
, d2i
, d2l
, f2d
, f2i
, f2l
, ...
tadd
, trem
, tsub
, tmul
, tdiv
: + % - * / for all types t (d
, f
, i
, l
)
iand
, ior
, ishl
, ishr
, ixor
, ...: bitwise operations (int)
dcmpg
, dcmpl
, fcmpg
, fcmpl
, lcmp
: Comparison ops
Add method using locals: Java
public static int add() { int lhs = 5; int rhs = 28; return lhs + rhs; }
Add method using locals: Bytecode (preamble)
public static int add(); descriptor: ()I flags: (0x0009) ACC_PUBLIC, ACC_STATIC
Add method using locals: Bytecode (code)
Code: stack=2, locals=2, args_size=0 0: iconst_5 1: istore_0 2: bipush 28 4: istore_1 5: iload_0 6: iload_1 7: iadd 8: ireturn
Add method using locals: Bytecode (tables)
LineNumberTable: line 11: 0 line 12: 2 line 13: 5 LocalVariableTable: Start Length Slot Name Signature 2 7 0 lhs I 5 4 1 rhs I
Add method using parameters: Java
public static int add(int lhs, int rhs) { return lhs + rhs; }
Add method using locals: Bytecode (preamble)
public static int add(int, int); descriptor: (II)I flags: (0x0009) ACC_PUBLIC, ACC_STATIC
Add method using locals: Bytecode (code)
Code: stack=2, locals=2, args_size=2 0: iload_0 1: iload_1 2: iadd 3: ireturn
Add method using locals: Bytecode (tables)
LineNumberTable: line 6: 0 LocalVariableTable: Start Length Slot Name Signature 0 4 0 lhs I 0 4 1 rhs I
anewarray
: create new array of references (objects)
newarray
: create new array
multianewarray
: create new multidimensional array
arraylength
: get length of array
taload
: load reference from array
tastore
: store into reference array
where t is a
, b
, c
, d
, f
, i
, l
, s
b
is boolean
, c
is char
, s
is short
nop
: Do nothing
goto
, goto_w
: Branch always
jsr
, jsr_w
: Jump to location, push return location
ifeq
, ifge
, ifgt
, ifle
, iflt
, ifne
, ifnonnull
, ifnull
: Branch if true
Several opcodes to combine with comparison operations
if_icmpeq
: if (integer-comparison-equality) jump
if_icmpne
: if (integer-comparison-notequality) jump
if_1cmpeq
: if (reference-comparison-equality) jump
if_1cmpne
: if (reference-comparison-notequality) jump
ret
, areturn
, dreturn
, freturn
, ireturn
, lreturn
, return
lookupswitch
: switch/case implementation
Sum an array: Java
public static int sumArray() { int[] numbers = {12, 21, 37}; int total = 0; for (int i : numbers) { total += i; } return total; }
Initialize an array: Bytecode (code)
0: iconst_3 1: newarray int 3: dup 4: iconst_0 5: bipush 12 7: iastore 8: dup 9: iconst_1 10: bipush 21 12: iastore 13: dup 14: iconst_2 15: bipush 37 17: iastore 18: astore_0
Iterate over an array: Bytecode (code)
19: iconst_0 20: istore_1 21: aload_0 22: astore_2 23: aload_2 24: arraylength 25: istore_3 26: iconst_0 27: istore 4 29: iload 4 31: iload_3 32: if_icmpge 52 35: aload_2 36: iload 4 38: iaload 39: istore 5 41: iload_1 42: iload 5 44: iadd 45: istore_1 46: iinc 4, 1 49: goto 29
new
: Create object
getfield
, putfield
: Get/Put top of stack (TOS) from/to field
getstatic
, putstatic
: Get/Put from/to static field
checkcast
: Throw exception if top-of-stack is not of type
instanceof
: Push 1 if TOS is of type, else push 0
invokevirtual
: Invoke method on TOS using dynamic binding
invokestatic
: Invoke static method
invokespecial
: Invoke method on TOS w/o dynamic binding
invokeinterface
: Invoke method on TOS through interface
invokedynamic
: Invoke method on TOS
Create an object, invoke a method: Java
public static void main(String[] args) { System.out.println(new Greeter().getGreeting()); }
Create an object, invoke a method: Bytecode (code)
Code: stack=3, locals=1, args_size=1 0: getstatic #7 // Field java/lang/System.out:Ljava/io/PrintStream; 3: new #13 // class com/newardassociates/demo/Greeter 6: dup 7: invokespecial #15 // Method com/newardassociates/demo/Greeter."<init>":()V 10: invokevirtual #16 // Method com/newardassociates/demo/Greeter.getGreeting:()Ljava/lang/String; 13: invokevirtual #20 // Method java/io/PrintStream.println:(Ljava/lang/String;)V 16: return
Which is better:
concat Strings using +
operator
allocate StringBuffer
then append
?
What does the compiler do?
Think about it: JVM states that fundamental atom is a class
Class private boundaries are enforced: no class gets access to another class's private parts
So, without changing the JVM, how are inner classes handled?
Part of the goodness of the C/C++ assert() was zero overhead in non-debug, production builds
J2SE 1.4 introduced assert
keyword
What's the cost of using it, even if turned off?
We're being sold on the goodness of typesafe containers
But is there a cost?
How, without changing the JVM, are generics handled?
How'd they do this?
In Java, everything needs to be in a class!
But lambdas are just... out there?
If a lambda captures a reference from enclosing scope...
... is that reference mutable?
... is the object on the other side of that reference mutable?
... does the lamdba capture a copy, or the actual reference?
... you get the idea
offers insights into underlying platform
offers access to power Java doesn't provide
aids debugging and spelunking
crucial to understanding compiler optimizations and/or costs
way to justify all those college courses on assembly language
just plain fun!
Programming the Java Virtual Machine, by Engel
Inside the Java2 Virtual Machine, by Venners
JVM Specification, (latest), by Steele, et al
Architect, Engineering Manager/Leader, "force multiplier"
http://www.newardassociates.com
http://blogs.newardassociates.com
Sr Distinguished Engineer, Capital One
Educative (http://educative.io) Author
Performance Management for Engineering Managers
Books
Developer Relations Activity Patterns (w/Woodruff, et al; APress, forthcoming)
Professional F# 2.0 (w/Erickson, et al; Wrox, 2010)
Effective Enterprise Java (Addison-Wesley, 2004)
SSCLI Essentials (w/Stutz, et al; OReilly, 2003)
Server-Based Java Programming (Manning, 2000)
"The java.lang.classfile
package contains API models for reading, writing, and modifying Java class files, as specified in Chapter 4 of the Java Virtual Machine Specification."
reading classfiles
writing classfiles
transforming classfiles
https://jasmin.sourceforge.net/
https://github.com/davidar/jasmin
a Java Assembler Interface
"takes ASCII descriptions for Java classes, written in a simple assembler-like syntax using the Java Virtual Machine instructions set... and converts them into binary Java class files suitable for loading into a Java interpreter."
sadly, not kept up with latest versions of Java
Hello, Jasmin
.class public HelloWorld .super java/lang/Object ; ; standard initializer (calls java.lang.Object's initializer) ; .method public <init>()V aload_0 invokenonvirtual java/lang/Object/<init>()V return .end method
Hello, Jasmin
; ; main() - prints out Hello World ; .method public static main([Ljava/lang/String;)V .limit stack 2 ; up to two items can be pushed ; push System.out onto the stack getstatic java/lang/System/out Ljava/io/PrintStream; ; push a string onto the stack ldc "Hello Jasmin!" ; call the PrintStream.println() method. invokevirtual java/io/PrintStream/println(Ljava/lang/String;)V ; done return .end method
an assembler/disassembler for JVM bytecode
https://github.com/roscopeco/jasm
Java 11+ required to run (tool or Gradle plugin)
use as a library as well
Hello, jasm
package com.newardassociates.demo; // Keep in mind that most IDEs and tools will flag the use // of the MessageProvider below as an error, since they won't // know how to resolve the name (since they don't know about // jasm). public class App { public static void main(String[] args) { System.out.println(new MessageProvider().getMessage()); } }
public class com/newardassociates/demo/MessageProvider { public getMessage()java/lang/String { ldc "Hello, World" areturn } public <init>()V { aload 0 invokespecial java/lang/Object.<init>()V return } }
https://github.com/Storyyeller/Krakatau
decompiler, disassembler and assembler
assembler supports Java19 bytecode spec
"and even supports some undocumented features found in old versions of the JVM"
decompiler does not support Java8+ features ("such as lambdas")
under current development (Rust/Cargo), must build from source
Hello Krakatau assembler
.class public Foo .super java/lang/Object ; ([Ljava/lang/String;)V means "takes a single String[] argument and returns void" .method public static main : ([Ljava/lang/String;)V ; We have to put an upper bound on the number of locals and the operand stack ; Machine generated code will usually calculate the exact limits, but that's a pain to do ; when writing bytecode by hand, especially as we'll be making changes to the code. ; Therefore, we'll just set a value that's way more than we're using, 13 in this case .code stack 13 locals 13 ; Equivalent to "System.out" in Java code getstatic Field java/lang/System out Ljava/io/PrintStream; ; put our argument on the operand stack ldc "Hello World!" ; now invoke println() invokevirtual Method java/io/PrintStream println (Ljava/lang/Object;)V return .end code .end method .end class
https://javassist.org
https://github.com/Guardsquare/proguard-core - read/write/analyze/process bytecode
https://github.com/guardsquare/proguard-assembler - assembler and disassembler
https://github.com/Guardsquare/proguard - optimizer and obfuscator
Lots of bytecode-related functionality here (including pre-verification)
Explicit Android support
Creating HelloWorld programmatically
ProgramClass programClass = new ClassBuilder( VersionConstants.CLASS_VERSION_1_8, AccessConstants.PUBLIC, "HelloWorld", ClassConstants.NAME_JAVA_LANG_OBJECT) .addMethod( AccessConstants.PUBLIC | AccessConstants.STATIC, "main", "([Ljava/lang/String;)V", 50, code -> code .getstatic("java/lang/System", "out", "Ljava/io/PrintStream;") .ldc("Hello, world!") .invokevirtual("java/io/PrintStream", "println", "(Ljava/lang/String;)V") .return_()) .getProgramClass();
https://bytebuddy.net/
provides builder-style API
only light knowledge of Java classfile format or instructions necessary
focuses on higher-order constructs
uses ASM library for low-level manipulation
Hello, ByteBuddy
Class<?> dynamicType = new ByteBuddy() .subclass(Object.class) .method(ElementMatchers.named("toString")) .intercept(FixedValue.value("Hello World!")) .make() .load(getClass().getClassLoader()) .getLoaded(); assertThat(dynamicType.newInstance().toString(), is("Hello World!"));
https://asm.ow2.io/
all-purpose Java bytecode manipulation and analysis framework
can either modify existing or dynamically generate classes
used in a number of different projects (including Kotlin, Gradle, and OpenJDK itself)
https://commons.apache.org/proper/commons-bcel/
includes bytecode verifier ("Justice")
used for analysis, modification, and generation
Optimizing boolean expressions
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]); } };
Optimizing boolean expressions
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) { ... } }