Skip to content

Smali

Reading and patching Dalvik bytecode (Smali) for Android application analysis and modification.


What Is Smali?

When Android compiles your Java/Kotlin source it produces .dex (Dalvik Executable) files — a compact binary format the Android Runtime executes. Smali is the human-readable text representation of that bytecode.

Think of it like Python's .pyc bytecode. Python source compiles to .pyc; you can't easily read .pyc raw — but tools like dis show you the bytecode as text. Smali is the same idea for Android:

Python:  .py source  →  .pyc bytecode   →  dis.dis() shows human-readable text
Android: .java source →  .dex bytecode  →  Smali is that human-readable text

apktool d app.apk disassembles every .dex into .smali files. You can edit those files and apktool b rebuilds the .dex. The roundtrip is lossless.

Why bother with Smali instead of just reading jadx output?

  • jadx may fail to decompile certain obfuscated methods; Smali always works
  • jadx shows approximate Java — Smali shows the actual instructions that run
  • Patching happens at the Smali level; you need to read it to know what you are changing
  • Some bugs only make sense at the bytecode level (register reuse, branch targets)

Types

Every type in Smali is written as a descriptor — a compact string. Think of them like Python type annotations, just much shorter.

Descriptor Java type Python equivalent
V void None (no return value)
Z boolean bool
B byte int (small, −128 to 127)
S short int (medium)
C char str (single character)
I int int
J long (64-bit) int (big number)
F float float
D double (64-bit) float (higher precision)
Ljava/lang/String; String str
Ljava/util/List; List list
[I int[] List[int]
[Ljava/lang/String; String[] List[str]
[[I int[][] List[List[int]]

Object types always start with L, use / instead of ., and end with ;:

com.example.MyClass  →  Lcom/example/MyClass;
java.lang.String     →  Ljava/lang/String;

Array types prefix with [ once per dimension:

int[]     →  [I               (like List[int])
String[]  →  [Ljava/lang/String;   (like List[str])
int[][]   →  [[I              (like List[List[int]])

Wide types (J and D) are 64-bit and always occupy two consecutive registers. If v2 holds a long, v3 is also consumed — you cannot use v3 independently.


Method Signatures

A method signature in Smali fully describes where a method lives, its name, its parameters, and its return type. You see these in invoke-* calls and when searching smali files for a method to patch.

Compare a Python function definition with its Smali reference:

def is_rooted(self, build_tag: str, flags: int) -> bool:
    ...
Lcom/example/app/Utils;->isRooted(Ljava/lang/String;I)Z

Both describe the same method — same class, name, parameters, return type — just in different formats.

Anatomy

Lcom/example/app/Utils;->isRooted(Ljava/lang/String;I)Z
│──────────────────────│  │──────│ │──────────────────│ │
      class (where)         name       parameters       return
Part Smali Python equivalent
Class Lcom/example/app/Utils; the class the method belongs to
-> separator (always literal) . as in obj.method
Name isRooted function name
Parameters (Ljava/lang/String;I) (build_tag: str, flags: int)
Return type Z -> bool

Parameters are listed one after another inside () with no commas or spaces between them.

Full Method Declaration

def is_rooted(self, build_tag: str, flags: int) -> bool:
    return False
.method public isRooted(Ljava/lang/String;I)Z
    .registers 4   # total = p0(this) + p1(build_tag) + p2(flags) + v0(local)

    const/4 v0, 0x0    # v0 = 0 (False)
    return v0
.end method

Breaking down .method public isRooted(Ljava/lang/String;I)Z:

Part Meaning
public Access modifier — also private, protected, static, final
isRooted Method name
Ljava/lang/String; First parameter: String buildTag
I Second parameter: int flags
Z Return type: boolean

Common Method Modifiers

.method public myMethod()V              # normal instance method
.method public static myMethod()V       # static — like @staticmethod in Python
.method private myMethod()V             # private — not accessible outside class
.method public constructor <init>()V    # constructor — like __init__
.method static constructor <clinit>()V  # runs at class load — like module-level code
.method public final myMethod()V        # cannot be overridden
.method public synchronized myMethod()V # holds a thread lock while running

Reading a Call Site

When you see an invoke-* line during patching, read the method reference after it like a Python function call:

invoke-virtual {v1, v2, v3}, Lcom/example/Auth;->login(Ljava/lang/String;Ljava/lang/String;)Z

Think of it as:

result = v1.login(v2, v3)  # v2: str username, v3: str password → returns bool
  • v1 is the object (self)
  • v2, v3 are the arguments — two Strings
  • Return type Z (bool) — captured with move-result v0

Registers

Registers are the named boxes that hold values in a method — exactly like variables in Python. Instead of choosing names like x or count, you get a fixed set of numbered boxes: v0, v1, v2

x = 5
name = "Alice"
const/4 v0, 0x5            # v0 = 5
const-string v1, "Alice"   # v1 = "Alice"

Register Naming

Name Meaning Python analogy
v0, v1, v2 Local registers — general scratch space local variables
p0 this in instance methods; first param in static methods self
p1, p2 Method parameters in order function arguments

p registers are just aliases for the high end of the register file. If .registers 5 with 2 params (p0 = this, p1 = arg), then p0 = v3 and p1 = v4 internally. You never need to worry about this — just use p0 for this and p1, p2… for parameters.

Declaring Register Count

You must have exactly one of these on every method — apktool won't assemble without it. Two choices, use one or the other, never both:

.registers 4    # total = locals + parameters (including p0/this)
.locals 2       # just the local count — apktool calculates total for you

Which number do you put?

  • .locals N — count only your local v registers. Parameters (p0, p1…) are not counted here; apktool adds them automatically. This is the easiest to use when patching because you don't have to think about how many parameters there are.
  • .registers N — the full total: every v and every p register combined.

Concrete example

Method checkPin(String inputPin) on an instance:
p0 = this, p1 = inputPin → 2 parameter slots
You use v0, v1, v2 as locals → 3 local slots

.locals 3       # just the 3 locals — correct, easiest
# — OR —
.registers 5    # 3 locals + 2 params (this + inputPin) = 5 total

When reading existing smali files you'll see whichever form apktool used.
When patching and you add a new instruction that needs an extra register, just bump the number up by 1:

.locals 2      .locals 3   # I added one more local variable (v2)
def greet(self, name: str, age: int) -> str:
    message = "Hello"   # 1 local variable
    return message
.method public greet(Ljava/lang/String;I)Ljava/lang/String;
    .locals 1              # 1 local: v0 = message  (p0/p1/p2 are automatic)

    const-string v0, "Hello"
    return-object v0
.end method

Wide Registers (long / double)

long and double are 64-bit — too big for one register, so they spill into two consecutive slots called a wide pair. Refer to the pair by the lower number:

const-wide/16 v0, 0x0   # v0 AND v1 are both consumed — holds a long
long-to-int v0, v0       # narrow back to int; now fits in v0 alone

Never use v1 independently while v0 holds a wide value — you will corrupt it.


Fields

Fields are variables that belong to an object (instance fields) or a class (static fields) — like attributes in Python.

class Session:
    TAG = "MyApp"          # class variable (static field)

    def __init__(self):
        self.token = None  # instance variable (instance field)
.field public static final TAG:Ljava/lang/String; = "MyApp"
.field private mToken:Ljava/lang/String;

Format: .field <modifiers> <name>:<descriptor>

Reading a Field (iget / sget)

To read self.token in Python you just write self.token. In Smali you need an explicit instruction for every field read.

value = self.token            # read instance field
inst  = Session.instance      # read class (static) field
logged = self.is_logged_in    # read boolean instance field
count = self.retry_count      # read int instance field
# Read instance field  (i = instance)
iget-object v0, p0, Lcom/example/app/Session;->mToken:Ljava/lang/String;
# v0 = self.token

# Read static field  (s = static)
sget-object v0, Lcom/example/app/Session;->sInstance:Lcom/example/app/Session;
# v0 = Session.sInstance

# Read boolean instance field
iget-boolean v0, p0, Lcom/example/app/Auth;->isLoggedIn:Z
# v0 = self.is_logged_in

# Read int instance field
iget v0, p0, Lcom/example/app/Config;->retryCount:I
# v0 = self.retry_count

Pick the right iget variant based on the field's type (the part after : in the field declaration):

Instruction Field type Python type
iget int, float int, float
iget-wide long, double big number
iget-object any object (L…;, arrays) object, str, list
iget-boolean boolean bool
iget-byte byte small int
iget-char char single-char str
iget-short short medium int

sget-* is the static version — same variants, no object register needed.

Writing a Field (iput / sput)

self.is_logged_in = True      # write instance field
Session.instance = None       # write class (static) field
# Write instance field
const/4 v0, 0x1
iput-boolean v0, p0, Lcom/example/app/Auth;->isLoggedIn:Z
# p0.isLoggedIn = True (1)

# Write static field
const/4 v0, 0x0
sput-object v0, Lcom/example/app/Session;->sInstance:Lcom/example/app/Session;
# Session.sInstance = null

Invoke — Calling Methods

Every method call is an invoke-* instruction. The key difference from Python: getting the return value is a separate step using move-result.

result = obj.some_method(arg1, arg2)   # call + capture result in one line
invoke-virtual {v1, v2, v3}, Lcom/example/Foo;->someMethod(TypeType)ReturnType
move-result v0   # separate step: v0 = the return value

Invoke Types

Which invoke-* to use depends on how the method is called:

Instruction When to use Python analogy
invoke-virtual Normal instance method call (dispatched at runtime) obj.method()
invoke-static Static method — no object ClassName.method()
invoke-direct Constructor <init> or private method obj.__init__()
invoke-interface Method declared on an interface calling via an abstract type
invoke-super Superclass version of the current method super().method()

Full Syntax

invoke-<type> {registers}, FullyQualifiedClass->methodName(ParamTypes)ReturnType

For instance methods (virtual, direct, interface, super): first register is the object, then parameters left to right. For static methods: only parameter registers, no object register.

Examples

Static call:

is_empty = TextUtils.is_empty(my_string)
invoke-static {v1}, Landroid/text/TextUtils;->isEmpty(Ljava/lang/CharSequence;)Z
move-result v0    # v0 = is_empty (bool)

Instance call:

match = my_str.equalsIgnoreCase(other_str)
# v1 = my_str, v2 = other_str
invoke-virtual {v1, v2}, Ljava/lang/String;->equalsIgnoreCase(Ljava/lang/String;)Z
move-result v0    # v0 = match (bool)

Constructor — creating a new object:

sb = StringBuilder()
sb2 = StringBuilder("hello")
# No args — split into allocate + init
new-instance v0, Ljava/lang/StringBuilder;
invoke-direct {v0}, Ljava/lang/StringBuilder;-><init>()V
# No move-result — constructors are void

# With argument
new-instance v0, Ljava/lang/StringBuilder;
const-string v1, "hello"
invoke-direct {v0, v1}, Ljava/lang/StringBuilder;-><init>(Ljava/lang/String;)V

Interface call:

count = my_list.size()
# v1 = a List object
invoke-interface {v1}, Ljava/util/List;->size()I
move-result v0    # v0 = count (int)

Super call:

super().on_create(saved_state)
invoke-super {p0, p1}, Landroid/app/Activity;->onCreate(Landroid/os/Bundle;)V
# void — no move-result

Retrieving the Return Value

After any non-void invoke-*, immediately grab the result:

Instruction Use when return type is…
move-result v0 int, float, boolean, byte, char, short
move-result-wide v0 long, double
move-result-object v0 any object (L…;, arrays)

For void methods, skip move-result entirely.

/range Variants

When a method takes more than 5 arguments, use the /range form to specify registers as a contiguous range:

invoke-virtual/range {v0 .. v6}, Lcom/example/Foo;->bigMethod(IIIIIII)V

Common Instructions

Constants — Putting a Value into a Register

x = 0        # small int
y = 100      # larger int
s = "hello"  # string
const/4 v0, 0x0            # v0 = 0     (small ints −8 to 7: use const/4)
const/16 v1, 0x64          # v1 = 100   (16-bit signed)
const v2, 0x1234           # v2 = 0x1234 (32-bit)
const-string v3, "hello"   # v3 = "hello"
const-class v4, Ljava/lang/String;  # v4 = String.class object

const-wide loads a 64-bit value into a register pair:

const-wide/16 v0, 0x0      # v0+v1 = 0L  (long)

Move — Copying a Value

y = x     # copy a number
z = obj   # copy an object reference
move v1, v0           # v1 = v0  (int or float)
move-object v1, v0    # v1 = v0  (object reference)
move-wide v2, v0      # v2+v3 = v0+v1  (long or double)

After an invoke-*, use move-result to grab the return value:

move-result v0             # grab int/float/bool return
move-result-object v0      # grab object return
move-result-wide v0        # grab long/double return
move-exception v0          # at start of a catch block — grab the caught exception

Return

return           # void function (implicit)
return x         # return a value
return-void            # void method
return v0              # return int / float / boolean / byte / char / short
return-wide v0         # return long / double
return-object v0       # return an object

Arithmetic

result = a + b
result = a - b
result = a * b
result = a // b   # integer division
result = a % b    # modulo
result = a & b    # bitwise AND
result = a | b    # bitwise OR
result = a ^ b    # XOR
result = a << b   # left shift
result = a >> b   # right shift (signed)
add-int v0, v1, v2         # v0 = v1 + v2
sub-int v0, v1, v2         # v0 = v1 - v2
mul-int v0, v1, v2         # v0 = v1 * v2
div-int v0, v1, v2         # v0 = v1 // v2
rem-int v0, v1, v2         # v0 = v1 % v2
and-int v0, v1, v2         # v0 = v1 & v2
or-int v0, v1, v2          # v0 = v1 | v2
xor-int v0, v1, v2         # v0 = v1 ^ v2
shl-int v0, v1, v2         # v0 = v1 << v2
shr-int v0, v1, v2         # v0 = v1 >> v2 (signed)
ushr-int v0, v1, v2        # v0 = v1 >>> v2 (unsigned — no Python equivalent)

Shorthand forms when one operand is the destination:

add-int/2addr v0, v1       # v0 += v1
mul-int/lit8 v0, v1, 0x2   # v0 = v1 * 2   (literal constant)
add-int/lit16 v0, v1, 0x64 # v0 = v1 + 100

The same operations exist for long (add-long), float (add-float), double (add-double).

Type Conversions — Casting

x = int(some_float)    # float → int
x = float(some_int)    # int → float
float-to-int v0, v1        # v0 = int(v1)
int-to-float v0, v1        # v0 = float(v1)
int-to-long v0, v2         # v0 (wide pair) = long(v2)
long-to-int v0, v2         # v0 = int(v2)  — narrows wide pair v2,v3

Object / Array Instructions

obj = MyClass()              # create object
arr = [0] * length           # create array
n = len(arr)                 # length
x = arr[i]                   # read element
arr[i] = x                   # write element
result = isinstance(obj, str)  # type check
s = (str) obj                # cast
new-instance v0, Lcom/example/MyClass;    # allocate (still need invoke-direct <init>)
new-array v0, v1, [I                      # v0 = new int[v1]
array-length v0, v1                       # v0 = len(v1)
aget v0, v1, v2                           # v0 = v1[v2]  (int/float array)
aput v0, v1, v2                           # v1[v2] = v0
aget-object v0, v1, v2                    # v0 = v1[v2]  (object array)
aput-object v0, v1, v2                    # v1[v2] = v0  (object)
instance-of v0, v1, Ljava/lang/String;    # v0 = isinstance(v1, String) → 1 or 0
check-cast v0, Ljava/lang/String;         # v0 = (String) v0  (throws if wrong type)

Null Checks

if obj is None:
    ...
if obj is not None:
    ...
if-eqz v0, :is_null     # jump if v0 == null (0)
if-nez v0, :not_null    # jump if v0 != null

Control Flow

This is what you edit most during patching. Every if, else, loop, and early return becomes a branch or jump in Smali.

Labels — Named Jump Targets

Labels mark a point in the code that a branch instruction can jump to. They start with : and can be named anything.

:done
    return-void

Apktool generates names like :cond_0, :goto_0. You can use any :name you like.

goto — Unconditional Jump

Jumps to a label unconditionally — no condition, always taken:

goto :exit

# the code below is skipped
const/4 v0, 0x2

:exit
    return-void

goto/16 and goto/32 are larger variants for long-distance jumps — apktool picks the right size.

if-* — Conditional Branches

if-* jumps to a label when the condition is true and falls through when false.

Key difference from Python

Python's if runs the indented block when true. Smali's if-* jumps away when true and falls through when false. When reading Smali, flip your mental model: if-eqz means "if this is zero/false, skip to the label — otherwise keep going here."

Comparing a register to zero / null:

Instruction Jump if… Python equivalent
if-eqz v0, :label v0 == 0 or null if v0 == 0:
if-nez v0, :label v0 != 0 or not null if v0 != 0:
if-ltz v0, :label v0 < 0 if v0 < 0:
if-lez v0, :label v0 <= 0 if v0 <= 0:
if-gtz v0, :label v0 > 0 if v0 > 0:
if-gez v0, :label v0 >= 0 if v0 >= 0:

Comparing two registers:

Instruction Jump if… Python equivalent
if-eq v0, v1, :label v0 == v1 if v0 == v1:
if-ne v0, v1, :label v0 != v1 if v0 != v1:
if-lt v0, v1, :label v0 < v1 if v0 < v1:
if-le v0, v1, :label v0 <= v1 if v0 <= v1:
if-gt v0, v1, :label v0 > v1 if v0 > v1:
if-ge v0, v1, :label v0 >= v1 if v0 >= v1:

if/else — Side by Side

if is_logged_in:
    load_dashboard()
else:
    show_login()
# is_logged_in is in v0  (1 = True, 0 = False)
if-eqz v0, :else           # if v0 == 0 (False): jump to :else

# --- if-body (is_logged_in was True, we didn't jump) ---
invoke-virtual {p0}, Lcom/example/app/Main;->loadDashboard()V
goto :end                  # skip the else-body

:else
# --- else-body ---
invoke-virtual {p0}, Lcom/example/app/Main;->showLogin()V

:end
return-void

The Smali pattern for if/else is always:

  1. if-* — jumps to :else when condition is false (inverted from Python)
  2. If-body instructions
  3. goto :end — skip past else-body
  4. :else label + else-body
  5. :end label

while / for Loops — Side by Side

i = 0
while i < 10:
    do_something(i)
    i += 1
const/4 v0, 0x0              # i = 0

:loop_start
const/16 v1, 0xa             # v1 = 10
if-ge v0, v1, :loop_end      # if i >= 10: exit loop

invoke-static {v0}, Lcom/example/Foo;->doSomething(I)V
add-int/lit8 v0, v0, 0x1     # i += 1
goto :loop_start             # back to top

:loop_end
return-void

switchpacked-switch and sparse-switch

Java switch on an int compiles to one of two forms:

  • packed-switch — consecutive values (0, 1, 2, 3…)
  • sparse-switch — non-consecutive values
if x == 0:
    result = "zero"
elif x == 1:
    result = "one"
else:
    result = "unknown"
packed-switch v0, :pswitch_data   # jump based on v0's value

# default (no case matched)
const-string v1, "unknown"
goto :end

:pswitch_0        # case 0
const-string v1, "zero"
goto :end

:pswitch_1        # case 1
const-string v1, "one"
goto :end

:end
return-object v1

:pswitch_data
.packed-switch 0x0           # first key = 0
    :pswitch_0
    :pswitch_1
.end packed-switch

Exception Handling — try / catch

try:
    f.delete()
except Exception as e:
    e.print_stack_trace()
:try_start
    invoke-virtual {v0}, Ljava/io/File;->delete()Z
:try_end

.catch Ljava/lang/Exception; {:try_start .. :try_end} :catch_block

:catch_block
    move-exception v1          # v1 = the caught exception object
    invoke-virtual {v1}, Ljava/lang/Exception;->printStackTrace()V
    return-void

Reading Smali — Worked Example

This walks through a complete method three ways: Python pseudocode (what it does), then the Smali annotated line by line so you can see exactly how each Python step maps.

The Method

def check_pin(self, input_pin: str) -> bool:
    stored_pin = self.prefs.get_string("pin", "0000")
    if stored_pin == input_pin:
        self.is_unlocked = True
        return True
    return False
public boolean checkPin(String inputPin) {
    String storedPin = prefs.getString("pin", "0000");
    if (storedPin.equals(inputPin)) {
        isUnlocked = true;
        return true;
    }
    return false;
}
.method public checkPin(Ljava/lang/String;)Z
# def check_pin(self, input_pin: str) -> bool:

    .registers 5
    # p0 = self (this)               ← the Lock object
    # p1 = input_pin (String)         ← the argument
    # v0 = scratch (prefs → storedPin → return value)
    # v1 = scratch ("pin" key literal)
    # v2 = scratch ("0000" default → result of equals())

    # stored_pin = self.prefs.getString("pin", "0000")
    iget-object v0, p0, Lcom/example/app/Lock;->prefs:Landroid/content/SharedPreferences;
    # v0 = self.prefs
    const-string v1, "pin"
    const-string v2, "0000"
    invoke-interface {v0, v1, v2}, Landroid/content/SharedPreferences;->getString(Ljava/lang/String;Ljava/lang/String;)Ljava/lang/String;
    # call: v0.getString(v1, v2)
    move-result-object v0    # v0 = storedPin  ← grab the String return value

    # if stored_pin.equals(input_pin):
    invoke-virtual {v0, p1}, Ljava/lang/String;->equals(Ljava/lang/Object;)Z
    # call: v0.equals(p1)
    move-result v2           # v2 = True/False (1 or 0)

    if-eqz v2, :cond_false   # if v2 == 0 (False): jump to :cond_false
    # (if equals() returned True we fall through to the next lines)

    # self.is_unlocked = True
    const/4 v2, 0x1
    iput-boolean v2, p0, Lcom/example/app/Lock;->isUnlocked:Z
    # p0.isUnlocked = 1 (True)

    # return True
    const/4 v0, 0x1
    return v0

    :cond_false
    # return False
    const/4 v0, 0x0
    return v0

.end method

Register Map for This Method

Register Role Python name
p0 this — the Lock object self
p1 argument inputPin input_pin
v0 prefs ref → storedPin → return value reused local
v1 string literal "pin" "pin"
v2 string "0000" → result of equals() stored_pin / result

Smali Patching

Step 1 — Find the Method in the Smali Files

You need to find which .smali file and which .method block to edit. There are three ways.

From jadx — copy the class path

Open the APK in jadx. Navigate to the method you want to patch. The package path in jadx maps directly to the file path:

jadx shows:  com.example.app.auth.LoginManager.checkPin()
smali file:  smali/com/example/app/auth/LoginManager.smali
method:      .method ... checkPin(...)

Replace . with /, add .smali, look in the smali/ folder apktool produced.

Text search across all smali files

After apktool d app.apk -o decoded/:

# Find by method name
grep -rl "checkPin" decoded/smali/

# Find by string literal that appears in the method
grep -rl '"wrong pin"' decoded/smali/

# Find by class short name
grep -rl "class.*LoginManager" decoded/smali/

# Narrow to method declaration
grep -n "\.method.*checkPin" decoded/smali/com/example/app/auth/LoginManager.smali

Find by string constant (most reliable)

If you can identify a string the target method uses (from the jadx source, Logcat output, or UI text), find it in smali:

grep -rl '"pin"' decoded/smali/
# Returns the exact smali file — the method using that string is nearby

Then open that file and search for the string within it to find the method block.

Cross-reference with line numbers (jadx + baksmali)

jadx shows Java line numbers that correspond to .line directives in smali. Search for .line 84 if jadx showed the problem at line 84:

grep -n "\.line 84" decoded/smali/com/example/app/auth/LoginManager.smali

Step 2 — Understand What to Change

Before editing, read the method completely. Identify:

  1. Which register holds the value you care about
  2. Which branch instruction decides the path — if-eqz, if-nez, if-eq, etc.
  3. What the label names are (:cond_0, :cond_1, etc.)

Common targets and their Smali signatures:

Goal What to find
Bypass boolean check if-eqz / if-nez on the result of a compare or method call
Always return true Any return v0 where v0 could be 0
Skip a method call invoke-* call you want to NOP out
Change a constant const-string, const, const/4 loading a value
Flip a branch Change if-eqzif-nez, if-ltif-ge, etc.

Step 3 — Common Patches

Always Return true

Replaces the entire method body with an unconditional return 1 (true):

.method public checkPin(Ljava/lang/String;)Z
    .registers 2
    const/4 v0, 0x1
    return v0
.end method

Always Return false

.method public isRooted()Z
    .registers 2
    const/4 v0, 0x0
    return v0
.end method

Always Return void (stub out a method)

.method public reportAnalytics(Ljava/lang/String;)V
    .registers 1
    return-void
.end method

Flip a Branch

Original — skips the admin check when isAdmin is false:

invoke-virtual {p0}, Lcom/example/app/User;->isAdmin()Z
move-result v0
if-eqz v0, :not_admin   # if false, skip

Patched — always enters the admin block regardless of isAdmin():

invoke-virtual {p0}, Lcom/example/app/User;->isAdmin()Z
move-result v0
if-nez v0, :not_admin   # flipped: if TRUE, skip (never happens for real admin)

Or simpler — just force the register before the branch:

invoke-virtual {p0}, Lcom/example/app/User;->isAdmin()Z
move-result v0
const/4 v0, 0x1        # overwrite result with true regardless
if-eqz v0, :not_admin  # branch is now never taken

NOP Out a Method Call

Replace an invoke-* line with nop (each instruction is replaced by a single nop; apktool handles sizing):

# Original:
invoke-static {v0}, Lcom/example/app/RootDetect;->check()V

# Patched:
nop

Warning

Only NOP a void call. If the call returns a value that is used in move-result afterwards, you must also remove or patch the move-result — otherwise the register will hold garbage.

Change a String Constant

# Original:
const-string v0, "https://api.example.com"

# Patched — point to your intercepting server:
const-string v0, "https://192.168.1.100:8080"

Skip a Block with goto

Instead of understanding and flipping a complex branch, just insert a goto that jumps over the block you want to skip:

# Original:
invoke-static {}, Lcom/example/app/License;->verify()V   # launches verification
... lots of code ...
:continue
# rest of method

# Patched — insert goto right before the verify call:
goto :continue
invoke-static {}, Lcom/example/app/License;->verify()V   # now unreachable
...
:continue

Add a New Local Register

If your patch needs an extra register (e.g. you want to call an additional method), increase .locals:

# Original:
.locals 2

# Patched — adds one more local:
.locals 3
# Now v2 is available as scratch

Step 4 — Rebuild, Sign, and Install

# 1. Decompile
apktool d app.apk -o decoded/

# 2. Edit the target smali file
#    e.g. decoded/smali/com/example/app/auth/LoginManager.smali

# 3. Rebuild
apktool b decoded/ -o patched.apk

# If apktool reports errors, check:
#   - Register count covers all registers you use
#   - Every invoke of a non-void method has a move-result
#   - Labels referenced in branch instructions exist in the method
#   - Wide-type registers are used in pairs

# 4. Generate a signing key (one-time — reuse for all your test APKs)
keytool -genkey -v \
    -keystore test.keystore \
    -alias testkey \
    -keyalg RSA \
    -keysize 2048 \
    -validity 9999

# 5a. Sign with apksigner (preferred — supports v2/v3 signatures, Android 7+)
apksigner sign \
    --ks test.keystore \
    --ks-key-alias testkey \
    --out patched_signed.apk \
    patched.apk

# 5b. Sign with jarsigner (older — only v1 signatures, works on older devices)
jarsigner -keystore test.keystore patched.apk testkey

# 6. Verify the signature
apksigner verify --verbose patched_signed.apk

# 7. Install
adb install -r patched_signed.apk

# If install fails with INSTALL_FAILED_UPDATE_INCOMPATIBLE:
# the original app is installed with a different signing cert — uninstall first
adb uninstall com.example.app
adb install patched_signed.apk

Common Build Errors and Fixes

Error Cause Fix
Register count too small Added instructions use registers beyond .locals count Increase .locals N
Expected move-result Non-void invoke-* not followed by move-result Add move-result vX or change method to void
Label not found Branch target label typo or removed Fix label name or add missing label
INSTALL_FAILED_UPDATE_INCOMPATIBLE Installed cert differs from test cert adb uninstall com.example.app first
apktool b outputs warnings about resources Resource decoding quirks Try apktool b --use-aapt2 decoded/
App crashes at modified method Logic error in patch — wrong register, bad branch Verify with adb logcat and inspect the exception