Tips and tricks for macOS lldb

If there's one thing that I've internalized from years of work, it's that if you do something twice, write it down. I reference stuff I wrote 6 years ago daily. I'm now hitting a lot of the same questions and patterns for answering in my lldb investigations, so here's my living public wiki.

What functions does a class have?

Load it in Hopper and search the string.

If you know the name of the class:

po [@"" __methodDescriptionForClass:[CKConversationList class]]

If you have a pointer

po [@"" __methodDescriptionForClass:[0x7aa2894a0 class]]

If neither works:

expr -l objc -- 
unsigned int n = 0;
void **methods = (void **)class_copyMethodList((Class)[[IMDChatRegistry sharedInstance] class], &n);
NSMutableString *s = [NSMutableString string];
for (unsigned int i = 0; i < n; i++) {
    const char *name = (const char *)sel_getName((SEL)method_getName(methods[i]));
    ((void (*)(id, SEL, NSString *, ...))objc_msgSend)(s, @selector(appendFormat:), @"%s\n", name);
}
(void)free(methods);
s

Where is a class defined?

image lookup -rn "IMDChatRegistry"

Find the class pointer and look it up.

expr (void*)objc_lookUpClass("IMDChatRegistry")
# (void *) $5 = 0x00000001f2108ce0
image lookup -a 0x00000001f2108ce0
#      Address: IMDaemonCore[0x00000001ee708ce0] (IMDaemonCore.__DATA_DIRTY.__objc_data + 480)
#      Summary: (void *)0x00790001f2109190

Where is a function defined?

image lookup -n "-[IMDaemonChatRequestHandler _loadChat:completionHandler:]"
p/x ((uint64_t)(void *)class_getMethodImplementation((Class)NSClassFromString(@"IMDaemonChatRequestHandler"), @selector(_loadChat:completionHandler:))) & 0x0000007fffffffff
0x000000010421240c
image lookup -a 0x000000010421240c
#      Address: imagent[0x000000010001240c] (imagent.__TEXT.__text + 66004)
#      Summary: imagent`___lldb_unnamed_symbol769

Make Hopper work in jumping

First get the slid address with image dump sections IMDaemonCore, taking the "Load Address" for .__TEXT (in this case 0x00000001ba246000), and in Hopper do Modify > Change File Base Address... to 0x00000001b6846000:

image dump sections IMDaemonCore
# Load Address 0x00000001ba246000

How do I see the code?

disassemble --frame --count 50

How do I see what writes/reads to a given variable?

We're looking for what interacts with IMChatRegistry's _cachedChatsInThreadNameMap. After connecting with lldb -n Messages we can run this to see that it's an offset of 0x98.

po ivar_getOffset((void *)class_getInstanceVariable((Class)objc_getClass("IMChatRegistry"), "_cachedChatsInThreadNameMap"))
# 0x98

A little back and forth with Claude created this script, which could be used to find the loaded named callpaths in IMChatRegistry that interacted with it:

script
import lldb
import re

PAC_MASK = 0x0000007FFFFFFFFF

def get_ivar_offset(target, class_name, ivar_name):
    options = lldb.SBExpressionOptions()
    options.SetLanguage(lldb.eLanguageTypeObjC)
    expr = f'(long)ivar_getOffset((void *)class_getInstanceVariable((Class)objc_getClass("{class_name}"), "{ivar_name}"))'
    val = target.EvaluateExpression(expr, options)
    return val.GetValueAsSigned() if val.IsValid() and val.GetError().Success() else None

def get_class_imps(target, class_name, metaclass=False):
    options = lldb.SBExpressionOptions()
    options.SetLanguage(lldb.eLanguageTypeObjC)
    cls_expr = (f'(Class)object_getClass((id)objc_getClass("{class_name}"))'
                if metaclass else f'(Class)objc_getClass("{class_name}")')
    n = target.EvaluateExpression(
        f'(unsigned int)({{ unsigned int n = 0; (void)class_copyMethodList({cls_expr}, &n); n; }})',
        options).GetValueAsUnsigned()
    if n == 0:
        return []
    arr_ptr = target.EvaluateExpression(
        f'(void **)class_copyMethodList({cls_expr}, (unsigned int *)0)',
        options).GetValueAsUnsigned()
    out = []
    for i in range(n):
        m = target.EvaluateExpression(f'(void *)((void **){arr_ptr})[{i}]', options).GetValueAsUnsigned()
        if not m:
            continue
        imp = target.EvaluateExpression(f'(void *)method_getImplementation((void *){m})', options).GetValueAsUnsigned()
        sel = target.EvaluateExpression(f'(char *)sel_getName((void *)method_getName((void *){m}))', options).GetSummary() or ""
        out.append((imp & PAC_MASK, sel.strip('"')))
    target.EvaluateExpression(f'(void)free((void *){arr_ptr})', options)
    return out

def build_method_ranges(target, class_name):
    ranges = []
    for imps, kind in [(get_class_imps(target, class_name, False), '-'),
                       (get_class_imps(target, class_name, True), '+')]:
        for imp_addr, sel in imps:
            saddr = lldb.SBAddress(imp_addr, target)
            sym = saddr.GetSymbol()
            if sym.IsValid():
                start = sym.GetStartAddress().GetLoadAddress(target)
                end = sym.GetEndAddress().GetLoadAddress(target)
            else:
                fn = saddr.GetFunction()
                if not fn.IsValid():
                    continue
                start = fn.GetStartAddress().GetLoadAddress(target)
                end = fn.GetEndAddress().GetLoadAddress(target)
            ranges.append((start, end, sel, kind))
    return ranges

def find_symbol_addr(target, name):
    for ctx in target.FindSymbols(name):
        sym = ctx.GetSymbol()
        if sym.IsValid():
            return sym.GetStartAddress().GetLoadAddress(target)
    return None

def decode_bl_target(addr, word):
    if (word & 0xFC000000) != 0x94000000:
        return None
    imm26 = word & 0x03FFFFFF
    if imm26 & 0x02000000:
        imm26 |= ~0x03FFFFFF
    return addr + (imm26 << 2)

def scan_method(process, start, end, offset, msgsend_addr):
    """Return list of (addr, mnemonic, xn, xt) for STR and msgSend-bound LDR at #offset."""
    imm12 = offset // 8
    str_pat = 0xF9000000 | (imm12 << 10)
    ldr_pat = 0xF9400000 | (imm12 << 10)
    mask = 0xFFFFFC00
    LOOKAHEAD = 6
    err = lldb.SBError()
    data = process.ReadMemory(start, end - start, err)
    if not err.Success():
        return []
    found = []
    seen_ldr = set()
    for i in range(0, len(data) & ~3, 4):
        w = int.from_bytes(data[i:i+4], 'little')
        op = w & mask
        xn = (w >> 5) & 0x1F
        xt = w & 0x1F
        if xn == 31:
            continue
        if op == str_pat:
            found.append((start + i, "str", xn, xt))
        elif op == ldr_pat and (start + i) not in seen_ldr:
            for k in range(1, LOOKAHEAD + 1):
                j = i + k * 4
                if j + 4 > len(data):
                    break
                tgt = decode_bl_target(start + j, int.from_bytes(data[j:j+4], 'little'))
                if tgt == msgsend_addr:
                    found.append((start + i, "ldr", xn, xt))
                    seen_ldr.add(start + i)
                    break
    return found

# --- main ---
target = lldb.debugger.GetSelectedTarget()
process = target.GetProcess()
CLASS = "IMChatRegistry"
IVAR = "_cachedChatsInThreadNameMap"

offset = get_ivar_offset(target, CLASS, IVAR)
ranges = build_method_ranges(target, CLASS)
msgsend = find_symbol_addr(target, "_objc_msgSend")

results = []
for start, end, sel, kind in ranges:
    for addr, mnem, xn, xt in scan_method(process, start, end, offset, msgsend):
        results.append((addr, kind, sel, mnem, xn, xt))

print(f"{CLASS}.{IVAR} @ #{offset:#x}  —  {len(results)} site(s):\n")
for addr, kind, sel, mnem, xn, xt in sorted(results):
    rt = f"x{xt}"
    print(f"  {addr:#x}  {kind}[{CLASS} {sel}]    {mnem} {rt}, [x{xn}, #{offset:#x}]")

prints out

0x1cedd0270  -[IMChatRegistry initAsListener:]    ldr x8, [x19, #0x98]
0x1cedd0274  -[IMChatRegistry initAsListener:]    str x0, [x19, #0x98]
0x1cedd17d4  -[IMChatRegistry _registerChatDictionary:forChat:isIncoming:newGUID:shouldPostNotification:]    ldr x0, [x8, #0x98]
0x1cedd17e8  -[IMChatRegistry _registerChatDictionary:forChat:isIncoming:newGUID:shouldPostNotification:]    ldr x0, [x8, #0x98]
0x1cedd236c  -[IMChatRegistry _clearMapsUsingChat:guids:]    ldr x0, [x21, #0x98]
0x1cedd30d8  -[IMChatRegistry _handleChatParticipantsDidChange:]    ldr x0, [x21, #0x98]
0x1cedd30e8  -[IMChatRegistry _handleChatParticipantsDidChange:]    ldr x0, [x21, #0x98]
0x1cedd59dc  -[IMChatRegistry _resetChatRegistry]    ldr x0, [x19, #0x98]
0x1cedd5b80  -[IMChatRegistry cachedChatsInThreadNameMap]    ldr x0, [x0, #0x98]