Samael Wang (燒賣)
Bytecode 101 �for Android App Developers
About Me
Grindr Android Developer
since 2018
Why to Read Bytecode?
1. Devices are not born equal
Mismatch
1. Devices are not born equal
Mismatch
2. Understanding language
details
W/dalvikvm( 3892): VFY: register1 v1 type 17, wanted 5
W/dalvikvm( 3892): VFY: rejecting call to Lcom/example/verifyerror/Foo;.<init> (ZIILkotlin/jvm/internal/DefaultConstructorMarker;)V
W/dalvikvm( 3892): VFY: rejecting opcode 0x70 at 0x0040
3. Mitigating 3rd-party issues
Ever wonder how Jettifier or Firebase Performance Plugin work?
Common Bytecode Manipulation Tools:
3. Mitigating 3rd-party issues
e.g. changing the try-block in a 3rd-party jar
JVM Bytecode Basics
JVM Bytecode
Disassemble classes with javap
Java
sample/app/build/intermediates/javac/debug/classes
❯ javap -p -v com.example.bytecode.JavaBytecodeSample
Kotlin
sample/app/build/tmp/kotlin-classes/debug
❯ javap -p -v com.example.bytecode.MainActivity
3rd-party Libraries
~/.gradle/caches/modules-2/files-2.1/org.jxmpp/jxmpp-core/0.6.3/<hash>
❯ javap -p -v "jar:file://$PWD/jxmpp-core-0.6.3.jar\!/org/jxmpp/xml/splitter/XmlSplitter.class"
Constant Pool
Constant pool:
#1 = Methodref #6.#20 // java/lang/Object."<init>":()V
#2 = String #21 // Test
#3 = Methodref #22.#23 // java/lang/String.valueOf:(I)Ljava/lang/String;
#4 = Methodref #24.#25 // android/util/Log.d:(Ljava/lang/String;Ljava/lang/String;)I
#5 = Class #26 // com/example/bytecode/JavaBytecodeSample
#6 = Class #27 // java/lang/Object
#7 = Utf8 <init>
#8 = Utf8 ()V
Magic |
Version |
Constant Pool |
Access Flags |
This Class |
Super Class |
Interfaces |
Fields |
Methods |
Attributes |
Class file structure
Method Sample
public void testLoop();
descriptor: ()V
flags: (0x0001) ACC_PUBLIC
Code:
stack=2, locals=2, args_size=1
0: iconst_0
1: istore_1
2: iload_1
3: bipush 10
5: if_icmpge 24
8: ldc #2 // String Test
10: iload_1
11: invokestatic #3 // Method
java/lang/String.valueOf:(I)Ljava/lang/String;
14: invokestatic #4 // Method
android/util/Log.d:(Ljava/lang/String;Ljava/lang/String;)I
17: pop
18: iinc 1, 1
21: goto 2
24: return
LineNumberTable:
line 8: 0
line 9: 8
line 8: 18
line 11: 24
LocalVariableTable:
Start Length Slot Name Signature
2 22 1 i I
0 25 0 this Lcom/example/bytecode/JavaBytecodeSample;
7: public void testLoop() {
8: for (int i = 0; i < 10; i++) {
9: Log.d("Test", String.valueOf(i));
10: }
11: }
Method Descriptor
public void testLoop();
descriptor: ()V
flags: (0x0001) ACC_PUBLIC
Code:
stack=2, locals=2, args_size=1
Examples
int d(String tag, String msg) → (Ljava/lang/String;Ljava/lang/String;)I
void showUptime(long) → (J)V
Stack & Locals
public void testLoop();
descriptor: ()V
flags: (0x0001) ACC_PUBLIC
Code:
stack=2, locals=2, args_size=1
0: iconst_0
1: istore_1
2: iload_1
…
24: return
7: public void testLoop() {
8: for (int i = 0; i < 10; i++) {
9: Log.d("Test", String.valueOf(i));
10: }
11: }
LocalVariableTable:
Start Length Slot Name Signature
2 22 1 i I
0 25 0 this Lcom/example/bytecode/JavaBytecodeSample;
Local Variable Occupations
1 slot | 2 slots |
boolean byte char short int float�reference�returnAddress | long double |
Operand Stack
JVM Spec 2.6.2. Operand Stacks
Each entry on the operand stack can hold a value of any Java Virtual Machine type, including a value of type long or type double.
|
|
|
|
“Whatever” |
SP
Operand Stack
|
|
|
3 |
“Whatever” |
iconst_3
iload_1
iadd
istore_1
SP
|
8 |
this |
v0
v1
v2
Operand Stack
|
|
8 |
3 |
“Whatever” |
iconst_3
iload_1
iadd
istore_1
SP
|
8 |
this |
v0
v1
v2
Operand Stack
|
|
|
11 |
“Whatever” |
iconst_3
iload_1
iadd
istore_1
SP
|
8 |
this |
v0
v1
v2
Operand Stack
|
|
|
|
“Whatever” |
iconst_3
iload_1
iadd
istore_1
SP
|
11 |
this |
v0
v1
v2
LineNumberTable
public void testLoop();
descriptor: ()V
flags: (0x0001) ACC_PUBLIC
Code:
stack=2, locals=2, args_size=1
0: iconst_0
8: ldc #2 // String Test
18: iinc 1, 1
24: return
LineNumberTable:
line 8: 0
line 9: 8
line 8: 18
line 11: 24
7: public void testLoop() {
8: for (int i = 0; i < 10; i++) {
9: Log.d("Test", String.valueOf(i));
10: }
11: }
Exception table
0: new #5 // class java/lang/RuntimeException
3: dup
4: ldc #6 // String crash
6: invokespecial #7 // Method
java/lang/RuntimeException."<init>":(Ljava/lang/String;)V
9: athrow
10: astore_1
11: ldc #2 // String Test
13: ldc #8 // String error
15: invokestatic #4 // Method
android/util/Log.d:(Ljava/lang/String;Ljava/lang/String;)I
18: pop
19: return
Exception table:
from to target type
0 10 10 Class java/lang/RuntimeException
public void testException() {
try {
throw new RuntimeException("crash");
} catch (RuntimeException e) {
Log.d("Test", "error");
}
}
Invocations
Java
Log.d("Test", "error");
Bytecode Assembly
ldc #2 // String Test
ldc #8 // String error
invokestatic #4 // Method android/util/Log.d:(Ljava/lang/String;Ljava/lang/String;)I
pop
return
|
|
|
|
|
Invocations
Java
Log.d("Test", "error");
Bytecode Assembly
ldc #2 // String Test
ldc #8 // String error
invokestatic #4 // Method android/util/Log.d:(Ljava/lang/String;Ljava/lang/String;)I
pop
return
|
|
|
|
“Test” |
Invocations
Java
Log.d("Test", "error");
Bytecode Assembly
ldc #2 // String Test
ldc #8 // String error
invokestatic #4 // Method android/util/Log.d:(Ljava/lang/String;Ljava/lang/String;)I
pop
return
|
|
|
“error” |
“Test” |
Invocations
Java
Log.d("Test", "error");
Bytecode Assembly
ldc #2 // String Test
ldc #8 // String error
invokestatic #4 // Method android/util/Log.d:(Ljava/lang/String;Ljava/lang/String;)I
pop
return
|
|
|
|
12 |
Invocations
Java
Log.d("Test", "error");
Bytecode Assembly
ldc #2 // String Test
ldc #8 // String error
invokestatic #4 // Method android/util/Log.d:(Ljava/lang/String;Ljava/lang/String;)I
pop
return
|
|
|
|
|
Common Types of Invocations
| without “this” | with “this” |
no vtable lookup | invokestatic | invokespecial <init>, super, private |
requires vtable lookup | | invokevirtual invokeinterface |
“Show Kotlin Bytecode”
Dalvik/ART
Disassemble a dex/apk
baksmali
sample/app/build/intermediates/dex/debug/mergeDexDebug
❯ baksmali d classes.dex
apktool
sample/app/build/outputs/apk/debug
❯ apktool d app-debug.apk
Method Sample
.method public testLoop()V
.registers 4
.line 8
const/4 v0, 0x0
.local v0, "i":I
:goto_1
const/16 v1, 0xa
if-ge v0, v1, :cond_11
.line 9
invoke-static {v0},
Ljava/lang/String;->valueOf(I)Ljava/lang/String;
move-result-object v1
const-string v2, "Test"
invoke-static {v2, v1},
Landroid/util/Log;->d(
Ljava/lang/String;Ljava/lang/String;)I
.line 8
add-int/lit8 v0, v0, 0x1
goto :goto_1
.line 11
.end local v0 # "i":I
:cond_11
return-void
.end method
7: public void testLoop() {
8: for (int i = 0; i < 10; i++) {
9: Log.d("Test", String.valueOf(i));
10: }
11: }
Locals & Params
Java Example
public void testLoop() {
for (int i = 0; i < 10; i++) {
Log.d("Test", String.valueOf(i));
}
}
Kotlin Example
private fun showUptime(uptime: Long) {
txt_uptime.text =
getString(R.string.uptime, uptime)
}
.method public testLoop()V
.locals 3
.local v0, "i":I
…
.end local v0 # "i":I
…
.method private final showUptime(J)V
.locals 4
.param p1, "uptime" # J
Line Numbers
.line 8
add-int/lit8 v0, v0, 0x1
goto :goto_0
.line 11
.end local v0 # "i":I
:cond_0
return-void
7: public void testLoop() {
8: for (int i = 0; i < 10; i++) {
9: Log.d("Test", String.valueOf(i));
10: }
11: }
Exceptions
:try_start_0
const-string v1, "crash"
…
:try_end_0
.catch Ljava/lang/RuntimeException; {:try_start_0 .. :try_end_0} :catch_0
:catch_0
move-exception v0
…
return-void
public void testException() {
try {
throw new RuntimeException("crash");
} catch (RuntimeException e) {
Log.d("Test", "error");
}
}
Operating on Registers
const/4 v0, 0x3
const/4 v1, 0x8
add-int v0, v1, v0
this |
|
|
v0
v1
p0/v2
Operating on Registers
const/4 v0, 0x3
const/4 v1, 0x8
add-int v0, v1, v0
this |
|
3 |
v0
v1
p0/v2
Operating on Registers
const/4 v0, 0x3
const/4 v1, 0x8
add-int v0, v1, v0
this |
8 |
3 |
v0
v1
p0/v2
Operating on Registers
const/4 v0, 0x3
const/4 v1, 0x8
add-int v0, v1, v0
this |
8 |
11 |
v0
v1
p0/v2
Invocations
Java
Log.d("Test", "error");
Bytecode Assembly
const-string v0, "Test"
const-string v1, "error"
invoke-static {v0, v1}, Landroid/util/Log;->d(Ljava/lang/String;Ljava/lang/String;)I
move-result-object v0
this |
|
"Test" |
v0
v1
p0/v2
Invocations
Java
Log.d("Test", "error");
Bytecode Assembly
const-string v0, "Test"
const-string v1, "error"
invoke-static {v0, v1}, Landroid/util/Log;->d(Ljava/lang/String;Ljava/lang/String;)I
move-result-object v0
this |
"error” |
"Test" |
v0
v1
p0/v2
Invocations
Java
Log.d("Test", "error");
Bytecode Assembly
const-string v0, "Test"
const-string v1, "error"
invoke-static {v0, v1}, Landroid/util/Log;->d(Ljava/lang/String;Ljava/lang/String;)I
move-result-object v0
this |
"error” |
"Test" |
v0
v1
p0/v2
Invocations
Java
Log.d("Test", "error");
Bytecode Assembly
const-string v0, "Test"
const-string v1, "error"
invoke-static {v0, v1}, Landroid/util/Log;->d(Ljava/lang/String;Ljava/lang/String;)I
move-result-object v0
this |
"error” |
12 |
v0
v1
p0/v2
Invocation Opcode
Mapping
JVM | Dalvik |
invokestatic | invoke-static |
invokevirtual | invoke-virtual |
invokeinterface | invoke-interface |
invokespecial | invoke-super super invoke-direct <init>, private |
Appendix A
VerifyError
Case Study
KT-35616
fun errorResumeWithDefaultParametersPreL() {
GlobalScope.launch {
val foo = Foo(baz = randomInt())
Log.d(TAG, "foo=$foo")
}
}
private suspend fun randomInt(): Int = withContext(Dispatchers.IO) {
return@withContext Random.nextInt()
}
class Foo(val bar: Boolean = false, val baz: Int = 0)
W/dalvikvm( 3892): VFY: register1 v1 type 17, wanted 5
W/dalvikvm( 3892): VFY: rejecting call to Lcom/example/verifyerror/Foo;.<init> (ZIILkotlin/jvm/internal/DefaultConstructorMarker;)V
W/dalvikvm( 3892): VFY: rejecting opcode 0x70 at 0x0040
W/dalvikvm( 3892): VFY: rejected Lcom/example/verifyerror/Sample$errorResumeWithDefaultParameters$1;.invokeSuspend (Ljava/lang/Object;)Ljava/lang/Object;
W/dalvikvm( 3892): Verifier rejected class Lcom/example/verifyerror/Sample$errorResumeWithDefaultParameters$1;
Bytecode Verification
What the Verifier Said
W/dalvikvm( 3892): VFY: register1 v1 type 17, wanted 5
W/dalvikvm( 3892): VFY: rejecting call to Lcom/example/verifyerror/Foo;.<init>
(ZIILkotlin/jvm/internal/DefaultConstructorMarker;)V
W/dalvikvm( 3892): VFY: rejecting opcode 0x70 at 0x0040
W/dalvikvm( 3892): VFY: rejected
Lcom/example/verifyerror/Sample$errorResumeWithDefaultParameters$1;.invokeSuspend
(Ljava/lang/Object;)Ljava/lang/Object;
W/dalvikvm( 3892): Verifier rejected class
Lcom/example/verifyerror/Sample$errorResumeWithDefaultParameters$1;
The verification of this class has failed:
com.example.verifyerror.Sample$errorResumeWithDefaultParameters$1
What the Verifier Said
W/dalvikvm( 3892): VFY: register1 v1 type 17, wanted 5
W/dalvikvm( 3892): VFY: rejecting call to Lcom/example/verifyerror/Foo;.<init>
(ZIILkotlin/jvm/internal/DefaultConstructorMarker;)V
W/dalvikvm( 3892): VFY: rejecting opcode 0x70 at 0x0040
W/dalvikvm( 3892): VFY: rejected
Lcom/example/verifyerror/Sample$errorResumeWithDefaultParameters$1;.invokeSuspend
(Ljava/lang/Object;)Ljava/lang/Object;
W/dalvikvm( 3892): Verifier rejected class
Lcom/example/verifyerror/Sample$errorResumeWithDefaultParameters$1;
Because the verification of this function failed:
invokeSuspend(Ljava/lang/Object;)Ljava/lang/Object;
What the Verifier Said
W/dalvikvm( 3892): VFY: register1 v1 type 17, wanted 5
W/dalvikvm( 3892): VFY: rejecting call to Lcom/example/verifyerror/Foo;.<init>
(ZIILkotlin/jvm/internal/DefaultConstructorMarker;)V
W/dalvikvm( 3892): VFY: rejecting opcode 0x70 at 0x0040
W/dalvikvm( 3892): VFY: rejected
Lcom/example/verifyerror/Sample$errorResumeWithDefaultParameters$1;.invokeSuspend
(Ljava/lang/Object;)Ljava/lang/Object;
W/dalvikvm( 3892): Verifier rejected class
Lcom/example/verifyerror/Sample$errorResumeWithDefaultParameters$1;
Because the verification of an operation 0x70 (invoke-direct) at 0x0040 failed.
(refer https://source.android.com/devices/tech/dalvik/dalvik-bytecode)
What the Verifier Said
W/dalvikvm( 3892): VFY: register1 v1 type 17, wanted 5
W/dalvikvm( 3892): VFY: rejecting call to Lcom/example/verifyerror/Foo;.<init>
(ZIILkotlin/jvm/internal/DefaultConstructorMarker;)V
W/dalvikvm( 3892): VFY: rejecting opcode 0x70 at 0x0040
W/dalvikvm( 3892): VFY: rejected
Lcom/example/verifyerror/Sample$errorResumeWithDefaultParameters$1;.invokeSuspend
(Ljava/lang/Object;)Ljava/lang/Object;
W/dalvikvm( 3892): Verifier rejected class
Lcom/example/verifyerror/Sample$errorResumeWithDefaultParameters$1;
Which is a constructor call to Foo with this method descriptor
(ZIILkotlin/jvm/internal/DefaultConstructorMarker;)V
What the Verifier Said
W/dalvikvm( 3892): VFY: register1 v1 type 17, wanted 5
W/dalvikvm( 3892): VFY: rejecting call to Lcom/example/verifyerror/Foo;.<init>
(ZIILkotlin/jvm/internal/DefaultConstructorMarker;)V
W/dalvikvm( 3892): VFY: rejecting opcode 0x70 at 0x0040
W/dalvikvm( 3892): VFY: rejected
Lcom/example/verifyerror/Sample$errorResumeWithDefaultParameters$1;.invokeSuspend
(Ljava/lang/Object;)Ljava/lang/Object;
W/dalvikvm( 3892): Verifier rejected class
Lcom/example/verifyerror/Sample$errorResumeWithDefaultParameters$1;
Because the verification of register v1 failed.
The verifier was expecting for type 5 (kRegTypeBoolean) but found type 17 (kRegTypeInteger).
Locate the Invocation
.line 49
iget v1, p0, Lcom/example/verifyerror/Sample$errorResumeWithDefaultParametersPreL$1;->label:I
if-eqz v1, :cond_21
# restore state and continue execution
.local v0, "$this$launch":Lkotlinx/coroutines/CoroutineScope;
iget v1, p0, Lcom/example/verifyerror/Sample$errorResumeWithDefaultParametersPreL$1;->I$0:I
goto :goto_38
# save state and suspend
:cond_21
const/4 v4, 0x0
iput v4, p0, Lcom/example/verifyerror/Sample$errorResumeWithDefaultParametersPreL$1;->I$0:I
:goto_38
invoke-direct {v5, v1, v4, v3, v2},
Lcom/example/verifyerror/Foo;-><init>(ZIILkotlin/jvm/internal/DefaultConstructorMarker;)V
The suspension point at constructor invocation caused the parameters being recorded in the state machine.
However the stored type didn’t match the constructor. It should be Z rather than I.
On resuming it cause v1 being int rather than boolean.
Dalvik
Category 1
Registers
Conversion
Table
No conversion from int to boolean.
Works on ART
ART only check if it’s an integral type (int, short, byte, boolean).
Workaround
fun errorResumeWithDefaultParametersPreL() {
GlobalScope.launch {
val baz = randomInt()
val foo = Foo(baz = baz)
Log.d(TAG, "foo=$foo")
}
}
private suspend fun randomInt(): Int = withContext(Dispatchers.IO) {
return@withContext Random.nextInt()
}
class Foo(val bar: Boolean = false, val baz: Int = 0)
Prevent the suspension point from storing the constructor parameters.
Appendix B
Debugging with on-device framework.jar
Disassemble
Device Frameworks
File Types
OAT
ELF file which contains compiled machine code. Before Oreo it also contains the original DEX. The extension can be .oat or .odex.
VDEX
Introduced in Oreo to store original DEXs. OAT no longer contains DEXs.
ART
We don’t really care about this one, but it’s basically common object dumps to speed up process initialization.
OAT before Android O
Tools
Extract Device
System Frameworks
Android 10
❯ adb pull /system/framework/framework.jar
+ adb pull /system/framework/framework.jar
/system/framework/framework.jar: 1 file pulled, 0 skipped. 142.8 MB/s (27265346 bytes in 0.182s)
❯ unzip framework.jar
Archive: framework.jar
extracting: classes.dex
extracting: classes2.dex
extracting: classes3.dex
❯ for i in *.dex; do baksmali d $i; done
Extract Device
System Frameworks
Android 8.x ~ 9.x
❯ adb pull /system/framework/boot-framework.vdex
+ adb pull /system/framework/boot-framework.vdex
/system/framework/boot-framework.vdex: 1 file pulled, 0 skipped. 33.7 MB/s (29357052 bytes in 0.830s)
❯ /opt/vdex-extractor/tools/deodex/run.sh -i .
[INFO]: Processing 1 input Vdex files
[INFO]: 1 binaries have been successfully deodexed
❯ for i in vdexExtractor_deodexed/boot-framework/*.dex; do baksmali d $i; done
Extract Device
System Frameworks
Android 5.x ~ 7.x
❯ oat2dex devfw
07-31 16:14:03:213 Preparing boot jars from sony-g3426-RQ3007D4NJ
…
07-31 16:14:03:866 Pulling /system/framework/arm/boot-framework.oat -> /data/device-dump/android-n/boot-raw
❯ baksmali list dex boot-jar-with-dex/framework.jar
classes.dex
classes2.dex
❯ unzip boot-jar-with-dex/framework.jar
Archive: boot-jar-with-dex/framework.jar
inflating: classes.dex
inflating: classes2.dex
❯ for i in *.dex; do baksmali d $i; done
Extract Device
System Frameworks
Android Pre-L
❯ adb pull /system/framework/framework.jar
+ adb -s ZX1PD2R346 pull /system/framework/framework.jar
/system/framework/framework.jar: 1 file pulled, 0 skipped. 6.1 MB/s (3762126 bytes in 0.587s)
❯ baksmali d framework.jar
Run Debugger with Smali
Smalidea
https://github.com/JesusFreke/smalidea
Import Smali
Run