Skip to content

BytecodeManipulation

Alexander Gottwald edited this page Nov 28, 2015 · 1 revision

Bytecode manipulation

Some of the modding ideas require changes within the method body. This can be achieved with bytecode manipulation. Bytecode manipulation requires some understanding of the low level Java bytecode and may cause strange errors if something goes just a tiny bit wrong.

Changing an if-condition

One idea for a meditation mod was to change requirements for the questions. The checks are located in Cults.java

if (cultist.getLevel() * 10 - meditation.knowledge < 30.0 || meditation.knowledge > 90.0) {
    mayIncreaseLevel = true;
    difficulty2 = 1 + cultist.getLevel() * 10;
}

The idea was to change this to

if (cultist.getLevel() * levelUpMultiplier + 10 >= meditation.knowledge) {
    mayIncreaseLevel = true;
    difficulty2 = 1 + cultist.getLevel() * 10;
}

Although writing such a condition by hand can be done it's much easier to move the check into a method of the mod. The method must be static because we can't insert an object reference to the mod into the byte code. Any configuration options from the mod we want to use must eiter be static or bound to a singleton. We go with a static field for the levelUpModifier

So in the mod we define

public static boolean mayIncreaseLevel(Cultist cultist, Skill meditation) {
    return cultist.getLevel() * levelUpMultiplier + 10 >= meditation.knowledge;
}

Now we want to manipulate the original code to

if (MeditationMod.mayIncreaseLevel(cultist, meditation)) {
    mayIncreaseLevel = true;
    difficulty2 = 1 + cultist.getLevel() * 10;
}

To achieve this we have to first identify the bytecode from the original code. Looking at bytecode disassembly we find

// Call cultist.getLevel()
1013: aload           cultist
1015: invokevirtual   com/wurmonline/server/players/Cultist.getLevel:()B

// (double)(cultist.getLevel() * 10)
1018: bipush          10
1020: imul           
1021: i2d           
 
// Read meditation.knowledge
1022: aload           meditation
1024: getfield        com/wurmonline/server/skills/Skill.knowledge:D

// (double)(cultist.getLevel() * 10) - meditation.knowledge     
1027: dsub

// compare with 30.0
1028: ldc2_w          30.0
1031: dcmpg          

// if less jump to 1047 
1032: iflt            1047

// Read meditation.knowledge
1035: aload           meditation
1037: getfield        com/wurmonline/server/skills/Skill.knowledge:D

// Compare with 90
1040: ldc2_w          90.0
1043: dcmpl          

// if less or equal jump to 1066
1044: ifle            1066

// Set mayIncreaseLevel to true
1047: iconst_1       
1048: istore          mayIncreaseLevel

The replacement is actually quite "simple"

1013: aload           cultist
1015: aload           meditation
1017: invokestatic    net/xyp/wurmunlimited/mods/medmod/MedMod.getMayIncreaseLevel:(Lcom/wurmonline/server/players/Cultist;Lcom/wurmonline/server/skills/Skill;)Z
1020: ifeq            1066
1023: nop            
...
1046: nop            

To gap after the code is filled with the NoOperation marker NOP (0).

Now that we have identified the code and create a replacement we can go and modify the byte code

//
// Load some classes
// 
ClassPool classPool = HookManager.getInstance().getClassPool();
CtClass ctCults = classPool.get("com.wurmonline.server.players.Cults");
CtClass ctCultist = classPool.get("com.wurmonline.server.players.Cultist");
CtClass ctSkill = classPool.get("com.wurmonline.server.skills.Skill");

// Parameter types of the method to change
CtClass[] paramTypes = {
                classPool.get("com.wurmonline.server.creatures.Creature"),
                CtPrimitiveType.intType,
                classPool.get("com.wurmonline.server.behaviours.Action"),
                CtPrimitiveType.floatType,
                classPool.get("com.wurmonline.server.items.Item"),
};

// Load the method
CtMethod method = ctCults.getMethod("meditate", Descriptor.ofMethod(CtPrimitiveType.booleanType, paramTypes));

// 
// MethodInfo contains everything java needs to know about the method
// CodeAttribute contains the byte code
//
MethodInfo methodInfo = method.getMethodInfo();
CodeAttribute codeAttribute = methodInfo.getCodeAttribute();

// 
// We need to map variable names to their indices.
// The indices are fixed in the code but since we have debug information we can actually use those
//
LocalNameLookup localNames = new LocalNameLookup((LocalVariableAttribute) codeAttribute.getAttribute(LocalVariableAttribute.tag));

//
// This is the code we want to replace
//
Bytecode bytecode = new Bytecode(methodInfo.getConstPool());
bytecode.addAload(localNames.get("cultist"));                   // 1013: aload           cultist
bytecode.addInvokevirtual(ctCultist, "getLevel", "()B");        // 1015: invokevirtual   com/wurmonline/server/players/Cultist.getLevel:()B
bytecode.add(Bytecode.BIPUSH, 10);                              // 1018: bipush          10
bytecode.add(Bytecode.IMUL);                                    // 1020: imul
bytecode.add(Bytecode.I2D);                                     // 1021: i2d  
bytecode.addAload(localNames.get("meditation"));                // 1022: aload           meditation 
bytecode.addGetfield(ctSkill, "knowledge", "D");                // 1024: getfield        com/wurmonline/server/skills/Skill.knowledge:D
bytecode.add(Bytecode.DSUB);                                    // 1027: dsub           
bytecode.addLdc2w(30.0);                                        // 1028: ldc2_w          30.0
bytecode.add(Bytecode.DCMPG);                                   // 1031: dcmpg
bytecode.add(Bytecode.IFLT);                                    // 1032: iflt            1047 (+15, jump targets are relative)
bytecode.add(0, 15);                                            //                       Jump target
bytecode.addAload(localNames.get("meditation"));                // 1035: aload           meditation
bytecode.addGetfield(ctSkill, "knowledge", "D");                // 1037: getfield        com/wurmonline/server/skills/Skill.knowledge:D
bytecode.addLdc2w(90.0);                                        // 1040: ldc2_w          90.0
bytecode.add(Bytecode.DCMPL);                                   // 1043: dcmpl          
bytecode.add(Bytecode.IFLE);                                    // 1044: ifle            1066 (+22)
bytecode.add(0, 22);                                            //                       Jump target
byte[] search = bytecode.get();
                
//      
// Create a new bytecode segment for the replacement
//
bytecode = new Bytecode(methodInfo.getConstPool());
bytecode.addAload(localNames.get("cultist"));                   // 1013: aload           cultist
bytecode.addAload(localNames.get("meditation"));                // 1015: aload           meditation
bytecode.addInvokestatic(classPool.get(this.getClass().getName()), "mayIncreaseLevel", Descriptor.ofMethod(CtPrimitiveType.booleanType , new CtClass[] { ctCultist, ctSkill}));
                                                                // 1017: invokestatic    MeditationMod.mayIncreaseLevel:(Lcom/wurmonline/server/players/Cultist;Lcom/wurmonline/server/skills/Skill;)Z
// now add some NOPs to fill the gap between current code and the jump at the end
// We add nops first to keep the jump offset the same
bytecode.addGap(search.length - bytecode.length() - 3);         // 1020-1043: nop
bytecode.add(Bytecode.IFEQ);                                    // 1044: ifeq            1066 (+22)
bytecode.add(0, 22);                                            //                       Jump target
byte[] replacement = bytecode.get();

// Replace the code. This will only replace one occurrance of the code
new CodeReplacer(codeAttribute).replaceCode(search, replacement);

// Rebuild the stack map. This is very important. Without this there may be verification errors
methodInfo.rebuildStackMap(classPool);

Clone this wiki locally