<- Back to README | Class Files | Generation API | Visitors ->
YABR provides two APIs for bytecode manipulation: the high-level Bytecode class and the low-level CodeWriter class.
The Bytecode class provides a convenient, high-level API for common bytecode operations.
// From a MethodEntry
Bytecode bytecode = new Bytecode(methodEntry);
// From a CodeWriter
Bytecode bytecode = new Bytecode(codeWriter);// Reference types (objects)
bytecode.addALoad(0); // Load 'this' (for instance methods)
bytecode.addALoad(1); // Load first reference parameter
// Integers
bytecode.addILoad(0); // Load int from local 0
bytecode.addILoad(1); // Load int from local 1
// Other primitives
bytecode.addLLoad(2); // Load long from local 2
bytecode.addFLoad(3); // Load float from local 3
bytecode.addDLoad(4); // Load double from local 4
// Generic load based on descriptor
bytecode.addLoad(1, "I"); // int
bytecode.addLoad(1, "J"); // long
bytecode.addLoad(1, "Ljava/lang/String;"); // object// Integers
bytecode.addIConst(0); // iconst_0
bytecode.addIConst(5); // iconst_5
bytecode.addIConst(100); // bipush 100
bytecode.addIConst(1000); // sipush 1000
bytecode.addIConst(100000); // ldc (from constant pool)
// Longs
bytecode.addLConst(0L); // lconst_0
bytecode.addLConst(1L); // lconst_1
bytecode.addLConst(100L); // ldc2_w
// Floats
bytecode.addFConst(0.0f); // fconst_0
bytecode.addFConst(1.0f); // fconst_1
bytecode.addFConst(2.0f); // fconst_2
bytecode.addFConst(3.14f); // ldc
// Doubles
bytecode.addDConst(0.0); // dconst_0
bytecode.addDConst(1.0); // dconst_1
bytecode.addDConst(2.718); // ldc2_w
// Null
bytecode.addAConstNull(); // aconst_null
// Strings
bytecode.addLdc("Hello, World!");bytecode.addAStore(1); // Store reference to local 1
bytecode.addIStore(2); // Store int to local 2ConstPool cp = bytecode.getConstPool();
// Using field reference index
int fieldRef = cp.getIndexOf(cp.findOrAddField("com/example/MyClass", "myField", "I"));
bytecode.addGetField(fieldRef);
bytecode.addPutField(fieldRef);
// Static fields
bytecode.addGetStatic(fieldRef);
bytecode.addPutStatic(fieldRef);
// Using strings (adds to constant pool automatically)
bytecode.addGetStatic("java/lang/System", "out", "Ljava/io/PrintStream;");// Using method reference index
int methodRef = cp.getIndexOf(cp.findOrAddMethodRef(classIdx, natIdx));
bytecode.addInvokeVirtual(methodRef);
bytecode.addInvokeSpecial(methodRef); // constructors, super calls
bytecode.addInvokeStatic(methodRef);
// Using strings
bytecode.addInvokeVirtual("java/io/PrintStream", "println", "(Ljava/lang/String;)V");
bytecode.addInvokeStatic("java/lang/Math", "max", "(II)I");
// Interface methods
bytecode.addInvokeInterface(interfaceMethodRef, argCount);
// Dynamic invocation
bytecode.addInvokeDynamic(cpIndex);// New instance
int classRef = cp.getIndexOf(cp.findOrAddClass("java/lang/StringBuilder"));
bytecode.addNew(classRef);// Labels
bytecode.defineLabel("loop_start");
// Goto
bytecode.addGoto("loop_start");
bytecode.addGotoW("loop_start"); // Wide goto for large offsets
// Increment local variable
bytecode.addIInc(0, 1); // local[0]++
bytecode.addIInc(1, -1); // local[1]--// Using ReturnType enum
bytecode.addReturn(ReturnType.RETURN); // void
bytecode.addReturn(ReturnType.IRETURN); // int
bytecode.addReturn(ReturnType.LRETURN); // long
bytecode.addReturn(ReturnType.FRETURN); // float
bytecode.addReturn(ReturnType.DRETURN); // double
bytecode.addReturn(ReturnType.ARETURN); // reference
// Using opcode
bytecode.addReturn(0xB1); // return
bytecode.addReturn(0xAC); // ireturn
// From descriptor
bytecode.addReturn(ReturnType.fromDescriptor("I")); // ireturn
bytecode.addReturn(ReturnType.fromDescriptor("V")); // returnBy default, instructions are appended to the end. You can insert before existing code:
bytecode.setInsertBefore(true);
bytecode.setInsertBeforeOffset(0); // Insert at beginning
// Now instructions will be inserted at the beginning
bytecode.addGetStatic("java/lang/System", "out", "Ljava/io/PrintStream;");
bytecode.addLdc("Method entry");
bytecode.addInvokeVirtual("java/io/PrintStream", "println", "(Ljava/lang/String;)V");After making changes, finalize to write back to the method:
bytecode.finalizeBytecode();
// Check if modifications were made
if (bytecode.isModified()) {
// Optionally compute stack frames
bytecode.computeFrames();
}For more control, use CodeWriter directly:
CodeWriter cw = new CodeWriter(methodEntry);
// Iterate existing instructions, or get a random-access snapshot
Iterable<Instruction> it = cw.getInstructions();
List<Instruction> instructions = cw.getInstructionList(); // bytecode-ordered, identity-indexed
// Insert at specific offset
cw.insertInvokeVirtual(offset, methodRefIndex);
cw.insertGetStatic(offset, fieldRefIndex);
cw.insertALoad(offset, varIndex);
cw.insertILoad(offset, varIndex);
cw.insertIInc(offset, varIndex, increment);
cw.insertLDC(offset, cpIndex);
cw.insertLDCW(offset, cpIndex);
// Append instructions
cw.appendInstruction(instruction);
cw.insertInstruction(offset, instruction);
// Get bytecode info
int size = cw.getBytecodeSize();
boolean endsWithReturn = cw.endsWithReturn();
// Write changes
cw.write();
cw.writeWithoutMaxStack(); // serialize without the CFG max_stack pass (provisional bound only)
// Frame computation
cw.computeFrames();
cw.forceComputeFrames();Beyond offset-based insertion, CodeWriter supports editing keyed on an instruction handle (the
Instruction object itself). Every structural edit runs a relink/layout pass that recomputes
instruction offsets, branch/switch targets, switch padding, the exception table, and the StackMapTable
- all by identity, so jumps and try/catch boundaries survive arbitrary shifts. (This also fixes former hazards where inserting inside a branch span left stale branch offsets, and where a spliced block's offsets collided with the host's and corrupted the exception table.)
CodeWriter cw = new CodeWriter(methodEntry);
Instruction handle = cw.getInstructions().iterator().next();
cw.insertBefore(handle, newInstr); // or a List<Instruction> (e.g. a spliced body)
cw.insertAfter(handle, newInstr);
cw.replaceInstruction(handle, replacement); // branches that targeted `handle` are retargeted
cw.removeInstruction(handle); // throws if `handle` is a branch/switch target
// When building branches that jump to a known instruction, register the target by identity:
cw.setBranchTarget(branchInstr, targetInstr);
cw.setSwitchTargets(switchInstr, defaultTarget, caseTargets);
// Replace the whole body (e.g. a cloned/grafted method body); offsets/frames are rebuilt:
cw.replaceBody(instructionList);
cw.replaceBody(instructionList, exceptionTableEntries);A ClonedRange (from cloneRangeWithTargets or CodeBuilder.assemble) carries its branch/switch
targets and any try/catch regions by identity, so replaceBody(cr) / insertBefore(handle, cr) /
insertAfter(handle, cr) splice an exception-bearing body that relinks correctly wherever it lands.
A ClonedRange can also carry branches that exit into the host: external labels (bind to a host
instruction before splicing via cr.bindLabel(name, hostInsn) or the
insertBefore/insertAfter(handle, cr, Map<String,Instruction>) overloads) and continuation
branches (auto-bound to the splice successor - insertBefore -> the handle, insertAfter -> the
instruction after it; replaceBody has no successor and rejects them). See
Generation API for authoring these via CodeBuilder.externalLabel / a tail label,
and the cloning section below for redirectReturns().
cloneRange copies a contiguous instruction range into a fresh, self-contained list, shifting every
local-variable index by localOffset and recomputing internal branch/switch targets for the cloned
block - the mechanical building block for inlining a method body.
List<Instruction> body = cw.cloneRange(first, last, localOffset);
host.insertBefore(callSite, body); // splice the cloned body in; relink wires it upShifted local-variable ops use the narrowest valid encoding: the compact xload_<n> form for index
0-3, the general form for 4-255, and the wide form beyond 255.
To splice a body across constant pools into a method the target already owns (e.g. merging into an
existing <clinit>), drive the public ConstPoolRemapper directly - it re-resolves every operand
(including bootstrap methods) into the target pool:
ConstPoolRemapper remap = new ConstPoolRemapper(sourceClass, targetClass);
CodeWriter.ClonedRange cr = sourceCw.cloneRangeWithTargets(first, last, base, targetPool, remap::remap);
targetCw.replaceBody(cr); // or insertBefore(handle, cr) to merge rather than replaceControl transfers that leave the range. A cloned range can exit into the host, reusing the identity-based splice binding:
- Out-of-range branches (a partial range with a
break/jump to a shared exit) carry their original target by identity, so the cloned branch resolves against the host at splice time. This is automatic and same-method-only: splicing such a range into a different method (where the carried target doesn't exist) throwsIllegalStateExceptionat splice rather than producing a class-loadVerifyError. (Cross-method relocation of an exiting branch isn't supported; rebind withsetBranchTargetif you must.) - Returns can be redirected so an inlined body continues after the splice point instead of
returning from the host - opt in on the produced range:
Each cloned
CodeWriter.ClonedRange body = sourceCw.cloneRangeWithTargets(first, last, base, pool, remap::remap); body.redirectReturns(); // cloned returns -> continuation gotos hostCw.insertBefore(continuationPoint, body); // they bind to the splice successor
returnbecomes a continuation branch (bound like a tail label:insertBefore-> the handle,insertAfter-> the instruction after it);replaceBodyhas no successor and rejects them. A value-returning body leaves its result on the stack at the continuation. LeaveredirectReturns()off to relocate a body whose returns should stay returns.
To fold several redirected bodies before one instruction, use insertChainBefore(at, bodies) - it
chains them so each body's continuation falls through into the next and the last into at. Do not
hand-stack them with repeated insertBefore(at, ...): that binds every body's continuation to at, so
earlier bodies skip later ones (a silent miscompile).
hostCw.insertChainBefore(callSuccessor, List.of(bodyA.redirectReturns(), bodyB.redirectReturns()));ConstPool/ClassFile can canonicalize every reference to a class from one name to another, and
MethodGrafter copies a method into another class, re-resolving its constant-pool references
symbolically into the target pool (and regenerating frames):
import com.tonic.analysis.MethodGrafter;
// Rewrite EVERY reference to Sound so it names Game (pool-wide):
int n = gameClass.redirectClassReferences("pkg/Sound", "pkg/Game"); // redirectOwner is an alias
// Move a method from one ClassFile into another (returns the new method on the target):
MethodEntry moved = MethodGrafter.graftMethod(soundClass, soundMethod, gameClass);
gameClass.redirectClassReferences("pkg/Sound", "pkg/Game"); // repoint its self-calls, if desired
// Or overwrite an existing method's body in place (same remapping), keeping the target's member entry
// and order - which is what makes a JVMTI live redefine accept the result (bodies may change, the member
// set may not):
MethodGrafter.replaceMethodBody(srcClass, srcMethod, targetClass, targetMethod);redirectClassReferences (and its alias redirectOwner) rewrites all reference kinds, since they
share a CONSTANT_Class: new/checkcast/instanceof/anewarray operands, exception catch_type,
member-ref owners, invokedynamic bootstrap handle/arg classes, and class names embedded in
method/field/MethodType descriptors and array class refs ([LB;). The ClassFile form also refreshes
the cached descriptor/owner on its fields and methods. Not rewritten: class names in generic
Signature attributes - use the Renamer for generic-aware full renames.
graftMethod and replaceMethodBody remap method/field/interface/class references, ldc constants
(String/Class/int/float/long/double/MethodHandle/MethodType), and invokedynamic/dynamic constants -
for the latter the referenced bootstrap method (handle + static arguments) is copied and remapped into
the target's BootstrapMethods attribute. Structural edits (including the relink after a graft) also
widen branches automatically (goto->goto_w, and an over-long conditional becomes an inverted
conditional skipping a goto_w) when an edit pushes a branch span past +/-32 KB.
The bootstrap-method table is reachable directly on ClassFile, which is what the graft path and
hand-built invokedynamic (e.g. CodeBuilder.assemble) both use:
BootstrapMethodsAttribute bsm = classFile.getBootstrapMethodsAttribute(); // null if none yet
int index = classFile.addBootstrapMethod(methodHandleIndex, argIndices); // find-or-create + dedupaddBootstrapMethod creates the BootstrapMethods attribute on first use, returns an existing entry's
index when the handle and static arguments already match, and otherwise appends - so repeated calls
never duplicate an identical bootstrap.
Verbose disassembly (DisassemblyOptions.verbose() / withResolveBootstraps, see
class-files.md) resolves these bootstraps for both
invokedynamic (// BSM: ...) and CONSTANT_Dynamic loads (// condy BSM: ...), rendering a
StringConcatFactory recipe with readable {arg}/{const} markers.
To serialize without a StackMapTable (e.g. for tooling that emits frame-free class versions or
recomputes frames downstream), strip the attribute before writing. A frameless class only loads where
frames aren't required (class major version < 50, or -Xverify:none).
classFile.stripStackMapTables();
byte[] bytes = classFile.write();// System.out.println("Hello");
bytecode.addGetStatic("java/lang/System", "out", "Ljava/io/PrintStream;");
bytecode.addLdc("Hello");
bytecode.addInvokeVirtual("java/io/PrintStream", "println", "(Ljava/lang/String;)V");// super();
bytecode.addALoad(0); // this
int superInit = cp.getIndexOf(cp.findOrAddMethodRef(
cp.getIndexOf(cp.findOrAddClass("java/lang/Object")),
cp.getIndexOf(cp.findOrAddNameAndType(
cp.getIndexOf(cp.findOrAddUtf8("<init>")),
cp.getIndexOf(cp.findOrAddUtf8("()V"))
))
));
bytecode.addInvokeSpecial(superInit);// this.field = value;
bytecode.addALoad(0); // this
bytecode.addILoad(1); // value parameter
bytecode.addPutField(fieldRef);
// return this.field;
bytecode.addALoad(0); // this
bytecode.addGetField(fieldRef);
bytecode.addReturn(ReturnType.IRETURN);// MyClass.staticField = 42;
bytecode.addIConst(42);
bytecode.addPutStatic(fieldRef);
// int x = MyClass.staticField;
bytecode.addGetStatic(fieldRef);
bytecode.addIStore(1);YABR supports all JVM instructions through the com.tonic.analysis.instruction package:
| Category | Examples |
|---|---|
| Constants | IConstInstruction, LConstInstruction, LdcInstruction |
| Loads | ALoadInstruction, ILoadInstruction, LLoadInstruction |
| Stores | AStoreInstruction, IStoreInstruction, LStoreInstruction |
| Stack | DupInstruction, PopInstruction, SwapInstruction |
| Math | ArithmeticInstruction, ArithmeticShiftInstruction, INegInstruction |
| Conversion | ConversionInstruction, NarrowingConversionInstruction, I2LInstruction |
| Comparison | CompareInstruction, ConditionalBranchInstruction |
| Control | GotoInstruction, ReturnInstruction, TableSwitchInstruction, LookupSwitchInstruction |
| Reference | GetFieldInstruction, PutFieldInstruction, NewInstruction |
| Invoke | InvokeVirtualInstruction, InvokeStaticInstruction |
The four owner-bearing invokes - Invoke{Virtual,Special,Static,Interface}Instruction - implement the
common interface InvokeInsn (getOwnerClass()/getMethodName()/getMethodDescriptor(), plus
isStatic()/isInterface()), so a call site of any kind is handled uniformly:
if (insn instanceof InvokeInsn call) {
cg.addEdge(call.getOwnerClass(), call.getMethodName(), call.getMethodDescriptor());
}invokedynamic (InvokeDynamicInstruction) is intentionally excluded - it has no owning class.
After modifying bytecode, you may need to recompute StackMapTable frames:
// Via Bytecode
bytecode.computeFrames();
// Via CodeWriter
codeWriter.computeFrames();
// Via ClassFile (all methods)
ClassFactory.computeFrames(classFile);
// Via ClassFile (specific method)
ClassFactory.computeFrames(classFile, "methodName", "(II)I");max_stack is computed over the control-flow graph, not a linear scan - it accounts for every
reachable path (loop back-edges, joins, and exception-handler entry states, where the JVM pushes the
caught exception). write() and the relink after any structural edit apply it automatically, so you
no longer need to set max_stack by hand after generating branches or loops. It is raise-only (never
under-counts; an existing larger value is left as-is). To force it explicitly:
int maxStack = codeWriter.computeMaxStack(); // CFG-correct; updates the CodeAttributeWhen a caller makes many edits across several passes and recomputes frames or max_stack once at the end,
writeWithoutMaxStack() serializes the relinked instructions without the per-write CFG max_stack pass. It sets a
provisional max_stack/max_locals from a linear scan only, so the final computeFrames() or computeMaxStack()
must still run before the class is used.
<- Back to README | Class Files | Generation API | Visitors ->