Patching Smali Files
Given a .smali method and a patch instruction:
- Read the full file, locate the target method, note
.locals Nand register usage. - Apply the minimal correct edit (correct smali syntax, correct register handling).
- Bump
.localsif a new register is introduced. - Round-trip validate: assemble → disassemble → reassemble, diff to confirm stability.
- If valid: backup original, write patched file, generate report, zip everything.
- If invalid: stop, do not zip, return only the error report.
No shortcuts. No silent guesses without documenting them.
Progress:
- [ ] 1. Ingest & parse: read file(s) line by line, index classes/fields/methods
- [ ] 2. Locate target: find exact method(s) matching user instruction
- [ ] 3. Analyze: signature, .locals count, register map (p0..pN, v0..vN), control flow, return points
- [ ] 4. Plan patch: decide exact instructions to insert/modify/remove
- [ ] 5. Register safety check: any new register needed? -> increment .locals; never reuse a live register
- [ ] 6. Apply edit: rewrite only the targeted lines, preserve everything else byte-for-byte
- [ ] 7. Dead-code check: ensure no instructions placed after return/throw in same block
- [ ] 8. Round-trip validate: smali assemble -> baksmali disassemble -> smali reassemble
- [ ] 9. Diff check: compare before/after, confirm only intended changes exist
- [ ] 10. If validation fails: fix or abort with error report (no partial output)
- [ ] 11. If validation passes: backup original (note path in report), save patched file
- [ ] 12. Generate transparent report (per file, per method)
- [ ] 13. Package modified files + report into ZIP
- [ ] 14. Deliver ZIP + report (or error-only report if failed)
Step Details
Step 1-3 (Read & Analyze):
- Parse
.class,.super,.field,.methodblocks. - For each target method, record: full signature,
.localsvalue, parameter registers (p0=this if non-static), local registers in use at each point, allreturn*/goto/if-*/invoke-*lines and branch labels.
Step 4-5 (Plan & Register Safety):
- Never reuse a register that is live (holds a value still needed later in the method).
- If injecting new logic (e.g., logging, a new invoke), allocate a fresh register:
new_v = old .locals count, then.locals (old+1). - If parameter registers exist, remember Dalvik convention: locals
v0..v(N-1)come before parameter registersp0..pMin the raw register file — inserting a new local shifts nothing if you only increase.locals, but double-check any explicit high-register (v15,v16) math. - Document exact register(s) chosen and why in the report.
Step 6-7 (Apply & Dead-code Check):
- Only touch lines within the exact method block instructed. Never modify sibling methods or other classes.
- If instruction is "add before return", insert immediately before the
return*opcode, never after. - Reject/flag any accidental placement after
return,return-void,return-object, orthrowwithin the same basic block.
Step 8-9 (Validate):
- Run:
smali assemble patched.smali -o patched.dex(or equivalent), thenbaksmali disassemble patched.dex -o out/, then reassemble again. - Compare final re-disassembled smali against the intended patched version — must match with zero unexpected diffs, zero warnings, zero errors.
- If tooling unavailable in environment, perform static syntax/semantic self-check (opcode validity, register range validity, label resolution,
.localsvs max register used) and clearly state in the report that live round-trip tooling was not executed.
Step 10-11 (Pass/Fail Gate):
- Fail → produce error report only: what failed, exact error/warning text, which line, why. No ZIP delivered.
- Pass → copy original file to
backup/<filename>.orig.smali, write patched file, proceed to packaging.
Step 12 (Report): see format below.
Step 13-14 (Package & Deliver):
- ZIP structure:
patch_result.zip ├── patched/<ClassName>.smali ├── backup/<ClassName>.orig.smali └── report.md - Multiple files: repeat steps 1-11 per file, one section per file in the same report, one ZIP total.
Markdown# Smali Patch Report
Instruction (user)
"ubah return value jadi selalu true"
Analysis
- .locals: 1 (before) -> 1 (after, no new register needed)
- Original return path: line 12,
return v0
Before
SMALI.method public bypassCheck()Z .locals 1 invoke-static {}, Lcom/example/Checker;->check()Z move-result v0 return v0 .end method
After
SMALI.method public bypassCheck()Z .locals 1 const/4 v0, 0x1 return v0 .end method
Reasoning
Replaced the check invocation with a hardcoded true (0x1), matching
the requested behavior "always return true". Original invoke removed
since its result is no longer used and dead invoke would be pointless
(not dead code after return, so still safe to remove entirely).
Assumptions
None — instruction was unambiguous.
Validation
- Assemble: OK
- Disassemble: OK
- Reassemble diff: identical to intended patch
- Warnings: none
- Errors: none
Backup
Original saved at: backup/Target.orig.smali
Example 1: Register-safe injection
Input: "Inject logging: write 'method called' to debug_log.txt at the start of onCreate, current .locals 2."
Output:
- Detect
.locals 2→ v0, v1 in use. New register needed for log logic → bump to.locals 3, usev2. - Insert file-write invoke sequence at top of method body (before any existing instruction, after the method header/locals/param directives).
- Report documents: new register
v2, reason ("avoid clobbering v0/v1 which hold existing state"), before/after snippet, validation result.
Example 2: Ambiguous instruction
Input: "ubah logic validasi jadi lebih longgar" (make validation logic looser) — no specifics.
Output:
- Do not guess wildly. Pick the most logical minimal interpretation: e.g., if method returns boolean based on
if-eqz/if-nezcomparison, invert or bypass that specific comparison. - Explicitly document under Assumptions: "Instruksi ambigu. Diasumsikan 'longgar' berarti melewati pengecekan
if-eqz v0, :faildengan mengubahnya menjadigoto :successtanpa menghapus kode lain. Jika ini bukan maksud user, mohon berikan detail kondisi mana yang ingin diubah."
Example 3: Failed validation
Input: Patch introduces a branch to a label that doesn't exist after edit.
Output: Return error report only:
Markdown# Smali Patch Report - FAILED
Error
Reassemble failed: unresolved label :cond_5 referenced at line 20,
no matching label found after edit (original label removed during patch).
Status
ZIP not generated. Please review the instruction or provide clarification.
- Always print the exact register map before editing — never patch "blind".
- Prefer the smallest possible diff that satisfies the instruction; don't refactor unrelated code.
- When adding registers, always recompute
.localsas(max register index used) + 1. - Treat
p0(this, for instance methods) as untouchable unless explicitly instructed. - Always run the round-trip check even for "trivial" one-line changes — trivial edits still break register counts often.
- When multiple methods/files are touched, keep one report section per method, grouped per file, in a single combined report.
- Don't reuse a live register just because it's numerically convenient — this silently corrupts unrelated values.
- Don't forget to update
.localsafter adding a register — this causes a hard verifier error (VFYfailure) on-device even if it assembles. - Don't insert any instruction after
return*/throwin the same block — dead code that baksmali/smali will reject or silently strip. - Don't touch other methods/classes "while you're at it" — scope creep violates the transparency and instruction-compliance requirement.
- Don't ship a ZIP when validation has warnings — treat warnings as failures.
- Don't silently resolve ambiguity — always state the assumption explicitly in the report.
- Don't skip the backup step, even for a "quick" patch.