Analysis of Android D8 Compiler Register Allocation Bug Causing VerifyError Crash
This article investigates an Android D8 compiler bug where improper register allocation for long and double values leads to a VerifyError crash at runtime, explains the underlying Dalvik verification rules, demonstrates the faulty bytecode generation, and presents practical fixes and preventive measures.
The article analyzes a crash caused by the Android D8 compiler allocating registers for a long value in a non‑consecutive manner, which triggers a java.lang.VerifyError at runtime.
Background : Developers observed a VerifyError that was not always reproducible; the code compiled fine but crashed when executed.
Test code (simplified) :
public class Test {
private static long DEFAULT_VALUE = -1L;
private static HashMap
cache = new HashMap<>();
public static void test() {
Time ap = cache.get("AP");
long ad = ap != null ? ap.d : DEFAULT_VALUE;
Time sp = cache.get("SP");
long sv = sp != null ? sp.d : DEFAULT_VALUE;
long tt = DEFAULT_VALUE;
if (ap != null && sp != null) {
tt = sp.e - ap.s;
}
Time fp = cache.get("FP");
long fd = fp != null ? fp.d : DEFAULT_VALUE;
Time mp = cache.get("MP");
long md = mp != null ? mp.d : DEFAULT_VALUE;
long sd = DEFAULT_VALUE;
if (sp != null && mp != null) {
sd = mp.s - sp.s;
}
if (isTestChannel()) {
Data.INSTANCE.setTt(tt);
Data.INSTANCE.setAd(ad);
Data.INSTANCE.setFd(fd);
Data.INSTANCE.setMd(md);
Data.INSTANCE.setSv(sv);
Data.INSTANCE.setSd(sd);
}
JSONObject params = new JSONObject();
try {
params.put("ad", ad);
params.put("sv", sv);
params.put("tt", tt);
params.put("fd", fd);
params.put("md", md);
} catch (JSONException e) {
e.printStackTrace();
}
Log.d("params", params.toString());
}
public static class Time {
public long s;
public long e;
public long d;
}
private static boolean isTestChannel() { return true; }
private static class Data {
static Data INSTANCE = new Data();
void setTt(long l) {}
void setAd(long l) {}
void setFd(long l) {}
void setMd(long l) {}
void setSv(long l) {}
void setSd(long l) {}
}
}The compiled code runs but immediately crashes with:
java.lang.VerifyError: Verifier rejected class com.liyang.myapplication.Test: void com.liyang.myapplication.Test.test() failed to verify: void com.liyang.myapplication.Test.test(): [0xB4] Rejecting invocation, long or double parameter at index 2 is not a pair: 15 + 0.Analysis of the cause : Dalvik verifies that a long/double occupies two consecutive virtual registers. In the generated invoke-virtual instruction the registers for the long argument are v15 and v0 , which are not consecutive, so verification fails.
Dalvik requires consecutive registers because many -wide instructions implicitly use the next register (e.g., iget-wide v10, v9, … stores the 64‑bit value in v10 and v11 ). Therefore, invoke instructions must also reference a pair of adjacent registers.
The issue is not always reproducible because the strict verification of consecutive registers was added in Android 7.0; devices below that version do not crash, though data may still be corrupted.
Data corruption example on Android 5.0 shows an incorrect md value due to the mis‑aligned registers.
Why the Dalvik bytecode is wrong : The D8 compiler’s register allocator incorrectly assigns v15 to the first part of the long and then increments to v16 for the second part, which exceeds the allowed v0‑v15 range. The relevant allocator code is:
protected int fillArgumentRegisters(DexBuilder builder, int[] registers) {
assert !this.needsRangedInvoke(builder);
int i = 0;
Iterator var4 = this.arguments().iterator();
while (var4.hasNext()) {
Value value = (Value) var4.next();
int register = builder.argumentOrAllocateRegister(value, this.getNumber());
if (register + value.requiredRegisters() - 1 > 15) {
register = builder.allocatedRegister(value, this.getNumber());
}
assert register + value.requiredRegisters() - 1 <= 15;
for (int j = 0; j < value.requiredRegisters(); ++j) {
assert i < 5;
registers[i++] = register++;
}
}
return i;
}Because a long requires two registers, the allocator picks v15 for the first and then blindly uses v16 for the second, violating the Dalvik constraint.
Solution : Insert move instructions and use the ranged invoke form to ensure consecutive registers, e.g.:
// Fixed version
move-object/from16 v20, v14
move-object/from16 v21, v0
move-wide/from16 v22, v15
invoke-virtual/range {v20 .. v23}, Lorg/json/JSONObject;->put(Ljava/lang/String;J)Lorg/json/JSONObject;In practice the team preferred simpler work‑arounds such as making DEFAULT_VALUE final or reordering variable definitions to avoid the faulty allocation, and they plan to turn this into a compile‑time error to catch it early.
Overall, the article provides a complete troubleshooting process for the D8‑induced VerifyError and offers guidance for developers encountering similar issues.
Watermelon Video Tech Team
Technical practice sharing from Watermelon Video
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.