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 0x00000001ba246000), and in Hopper do 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]