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. Expect it to be heavy on code pointers and light on explanation.
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
Handle wait for offsets
Sometimes you want to break on something, the symbol tables are removed, but you need to break on it early. First, run normally, and get an address
p/x (void *)class_getMethodImplementation((Class)objc_getClass("IMChatRegistry"), (SEL)sel_registerName("_registerChatDictionary:forChat:isIncoming:newGUID:shouldPostNotification:"))
# 0x8f200001cedd0d80
(lldb) image lookup -a 0x8f200001cedd0d80
# Address: IMCore[0x00000001cb3d0d80] (IMCore.__TEXT.__text + 967872)
# Summary: IMCore`___lldb_unnamed_symbol7909
Then
# Trigger your waitfor
lldb --wait-for -n Messages
#
settings set target.process.stop-on-sharedlibrary-events true
br set -s IMCore -a 0x1cb3d0d80
settings set target.process.stop-on-sharedlibrary-events false
Break on regex
breakpoint set --func-regex "-\\[AMS.*\\]"
See XPC changes
-[NSXPCConnection _sendInvocation:orArguments:
# Print the connection details (ps -p 66515 confirms that this is imagent)
po $x0
<NSXPCConnection: 0x768129860> connection to service with pid 66515 named com.apple.imagent.desktop.auth
# Print the selector
p (char *)$x6
loadChatWithGUID:reply:
# Print the argument count
p/d $x4
2
# Print the arguments (we know there are 2)
po ((id *)$x3)[0]
iMessage;+;chat449880512908674466
po ((id *)$x3)[1]
<__NSMallocBlock__: 0x76ad14570>
signature: "v16@?0@"NSArray"8"
invoke : 0xee498001ced44920 (/System/iOSSupport/System/Library/PrivateFrameworks/IMCore.framework/Versions/A/IMCore`IMCoreSimulatedEnvironmentEnabled)
Longer strings
settings set target.max-string-summary-length 100000
Modify breakpoints
breakpoint modify -c '(BOOL)[(id)[(id)$x0 guid] containsString:@"+15049019053"]' 1
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]