ιldb: A Command-Line Scripting Layer for LLDB Debugging

About the author. I’m Colin Xiao — an independent reverse engineer, mostly working solo on contract jobs. I dig into compiled code of every kind, including but not limited to: Android and iOS apps, x86/x64/ARM native binaries, packers and obfuscators, DRM and license checks, network protocols and APIs, crypto and signature algorithms, malware, game binaries, and embedded/IoT firmware. I also handle large-scale data collection and vulnerability research. If you have any project in mind, I’d be glad to take it on — whatever it is, as long as it’s within my wheelhouse. Just reach out. You can reach me on X at @colinxiao_lab or on Telegram at https://t.me/ColinXiao666.

This research is published for educational and defensive purposes. The target sample is anonymized throughout.


ιldb: A Command-Line Scripting Layer for LLDB Debugging

Reverse engineering iOS binaries in LLDB means repeating the same dance over and over: copy an address out of IDA, paste it into LLDB, set a breakpoint, read memory, copy the result back somewhere. ιldb is a scripting layer on top of LLDB that turns those recurring operations into short, memorable commands — and, crucially, lets you save, annotate, and replay command sequences so the “copy address to a text file, paste it back later” loop disappears.

Installation and setup

  1. Drop the ιldb.py file into your LLDB plugin directory.

  2. Add this line to ~/.lldbinit to load the script (replace the path with your own):

    command script import ***/ιldb.py
  3. Once loaded, you’ll see the full list of registered commands:

    The registered command list

Command management

save — save a command to the record

Saves LLDB history entries into cmd_record.json, with several formats:

# save the most recent command, no description
save

# save the most recent command with a description
save "this is the most recent command"

# save specific history entries by index, with descriptions
save 0 "history entry 0" 2 "history entry 2"

# mixed: save several entries, some without descriptions
save 0 2 "this is a command"

show — list saved commands

show

Example output:

[ command list ]
0. using       —— select a module (later mark commands use it)
1. mark        —— breakpoint by module offset (multiple addresses supported)
2. markd       —— breakpoint by dynamic address (multiple addresses supported)
3. memread     —— read memory
4. process     —— launch a process

rm — delete saved commands

# delete one entry
rm 1

# delete several entries
rm 0 2 4

The updated list is printed automatically after a delete.

exec — execute a saved command

# execute the entry at index 1
exec 1

The command content and its description are shown as it runs:

Executing a saved command

Debugging commands

using — select the debug module

Selects the module that later mark commands compute offsets against:

# use the app's main module
using

# use a specific module
using SwiftDemo.debug.dylib

Selecting a module

mark — breakpoint by module offset

Sets breakpoints from module-relative offsets, multiple addresses supported:

# single address
mark 0x1063c2c10

# multiple addresses
mark 0x1063c2c10 1063c2c18 0x1063c2c20

# an address range (addresses must start with 0x)
mark [0x1063c2c10, 0x1063c2c20]

If IDA says encryptWithChaCha20Poly1305 is at offset A8F4, mark drops the breakpoint for you:

Breakpoint by offset

markd — breakpoint by dynamic address

# single address
markd 0x12345678

# multiple addresses
markd 0x12345678 0x87654321

Breakpoint by dynamic address

dy — compute a dynamic memory address

Computes the dynamic address of an offset relative to the module selected with using:

dy 0xA8F4

Computing a dynamic address

offset — compute a static offset

# offset of a given address
offset 0x1063c2c10

# offset of the current PC register
offset

Computing an offset

memwrite — patch memory

Patches memory at a dynamic address, with endian conversion applied by default:

# the address is a dynamic address, not an offset
memwrite 0x1063c2c10 1f2003d5

Addresses and machine code don’t need the 0x prefix — they’re treated as hex by default.

Patching memory

memread — read memory

Several formats:

# read multiple addresses
memread $x8 0x12345678

# read the pointer at an address, then read what it points to
memread -ptr ($x8 + 0x20)

# read a specific byte count
memread -c 0x100 $x8
memread --count 0x200 0x12345678

Reading memory

nop — fill memory with NOP instructions

# single address
nop 0x1063c2c10

# multiple addresses
nop 0x1063c2c10 1063c2c18 0x1063c2c20

# an address range (addresses must start with 0x)
nop [0x1063c2c10, 0x1063c2c20]

ptr — resolve a pointer

# pointer from register expressions
ptr ($x8 + 0x8) ($x8 + 0x20)

# pointer from a direct address
ptr 0x12345678

ss — parse a Swift String

Tries to interpret the contents of a register as a Swift String; you name one register and it reads the other automatically:

Parsing a Swift String

sd — parse a Swift Data

The Data counterpart to ss:

Parsing a Swift Data

Configuration files

ιldb uses two main config files:

cmd_config.json

Command configuration, containing:

  • cmd_script — the command-to-script mapping
  • cmd_alias — command aliases
  • cus_cmd — custom command list (auto-executed when the script loads)
  • cmd_notes — command notes (printed to the terminal as reminders, not executed)

cmd_record.json

Saved command records. Each record holds:

  • command — the actual LLDB command
  • desc — the command description

Command-to-script mapping

Command Function Description
using usingModule select the debug module
mark markBreakPointByOffsetAddress breakpoint by module offset
markd markBreakPointByDynamicAddress breakpoint by dynamic address
dy calcDynamicMemoryAddress compute dynamic memory address
offset calcStaticOffsetAddress compute static offset
memread readMemory read memory
memwrite writeMemory write memory
nop nopMemory fill with NOP instructions
ptr getPointer resolve a pointer
save saveCmd save a command
show showCmd list saved commands
rm removeCmd delete a command
exec execCmd execute a command
ss parseSwiftString parse a Swift String object
sd parseSwiftData parse a Swift Data object

A full debugging workflow

# 1. select the debug module
using SwiftDemo.debug.dylib

# 2. set breakpoints
mark 0x1063c2c10
markd 0x12345678

# 3. save frequently used commands
save "select debug module"
save 0 "breakpoint at entry" 1 "breakpoint at key function"

# 4. list saved commands
show

# 5. execute saved commands
exec 0
exec 1

# 6. read memory
memread $x8

# 7. patch memory
memwrite 0x1063c2c10 d503201f

# 8. NOP out a range
nop [0x1063c2c10, 0x1063c2c20]

Notes

  1. All indices (save, show, rm, exec) are zero-based.
  2. Address ranges like [0x1063c2c10, 0x1063c2c20] must use the 0x prefix.
  3. Single and multiple addresses don’t require the 0x prefix.
  4. Memory operations are hexadecimal by default.
  5. Command history lives in ιldb/config/cmd_record.json.

ιldb also supports adding custom commands and aliases by editing cmd_config.json, so you can shape it to your own workflow.


Colin Xiao — I take on contract reverse-engineering work, and writeups like this are how I show what I can do. If you need help with anything in this space — including but not limited to the areas I cover above — I’m available. No project is too big or too small — if it’s reverse engineering and I can do it, I’ll take it. DM me on X (@colinxiao_lab) or Telegram (@ColinXiao666).

Get in touch