Skip to content

Instantly share code, notes, and snippets.

@mayoff
Last active August 14, 2023 15:09
Show Gist options
  • Star 55 You must be signed in to star a gist
  • Fork 11 You must be signed in to fork a gist
  • Save mayoff/7f1652cb33d9ed71555f8a37dc850c61 to your computer and use it in GitHub Desktop.
Save mayoff/7f1652cb33d9ed71555f8a37dc850c61 to your computer and use it in GitHub Desktop.
Debugging Objective-C blocks in lldb

The attached lldb command pblock command lets you peek inside an Objective-C block. It tries to tell you where to find the source code for the block, and the values captured by the block when it was created.

Consider this example program:

#import <Foundation/Foundation.h>

@interface Foo: NSObject
@end

@implementation Foo
@end

typedef void (^MyBlock)(int y);

MyBlock makeBlock(Foo *foo, int x) {
    return ^(int y){
        NSLog(@"foo=%@ x+y=%d\n", foo, x + y);
    };
}

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        Foo *foo = [Foo new];
        MyBlock block = makeBlock(foo, 7);
        block(8);            // <------------ breakpoint here
    }
    return 0;
}

Suppose you put a breakpoint at the call to block, stopping before the block is called. Then in the debugger, you can use pblock to find out what's about to happen:

(lldb) pblock block
/Users/mayoff/TestProjects/blockTest2/blockTest2/main.m:12
(__block_literal_1) *$0 = {
  __isa = 0x00007fffa2380160
  __flags = -1023410172
  __reserved = 0
  __FuncPtr = 0x0000000100000dc0 (blockTest2`__makeBlock_block_invoke at main.m:12)
  __descriptor = 0x0000000100001060
  foo = 0x0000000100580c10
  x = 7
}

The pblock command prints (if available) the source file and line number where the block body was defined, and the contents of the block literal, if debug information for it is available. Everything following the __descriptor field of the block literal is a value captured by the block when it was created.

Thanks to Jim Ingham and John McCall at WWDC 2018 for lots of help making this work.

command script import ~/pblock.py
import lldb
def print_block(debugger, userInput, context, result, internalDict):
# debugger: SBDebugger
# userInput: str
# context: SBExecutionContext
# result: SBCommandReturnObject
target = context.target
addressOfBlockLiteral = context.frame.EvaluateExpression(userInput)
if addressOfBlockLiteral.error.fail:
result.SetError(addressOfBlockLiteral.error)
return
# If lldb thinks addressOfBlockLiteral is a block type, then GetValueAsUnsigned doesn't
# work. Cast to a void pointer first.
voidPointerType = target.FindFirstType('void').GetPointerType()
if not voidPointerType.IsValid():
result.PutCString('Unable to find void* type for casting')
return
addressOfBlockLiteral = addressOfBlockLiteral.Cast(voidPointerType)
if not addressOfBlockLiteral.IsValid():
result.PutCString('Unable to cast to void*')
return
# https://clang.llvm.org/docs/Block-ABI-Apple.html
# struct Block_literal_1 {
# void *isa; // initialized to &_NSConcreteStackBlock or &_NSConcreteGlobalBlock
# int flags;
# int reserved;
# void (*invoke)(void *, ...);
addressOfBlockLiteralInvoke = addressOfBlockLiteral.unsigned + target.addr_size + 4 + 4
error = lldb.SBError()
blockLiteralInvoke = context.process.ReadPointerFromMemory(addressOfBlockLiteralInvoke, error)
if error.fail:
result.SetError(error)
return
addressOfInvokeFunction = lldb.SBAddress()
addressOfInvokeFunction.SetLoadAddress(blockLiteralInvoke, target)
symbolContext = addressOfInvokeFunction.GetSymbolContext(lldb.eSymbolContextEverything)
lineEntry = symbolContext.line_entry
if lineEntry.IsValid():
result.PutCString(str(lineEntry))
else:
symbolName = symbolContext.symbol.name
result.PutCString(symbolName)
invokeFunction = symbolContext.function
if not invokeFunction.IsValid():
result.PutCString("couldn't get block invoke function")
return
scope = invokeFunction.block
if not scope.IsValid():
result.PutCString("couldn't get top-level scope of invoke function")
return
scopeVariables = scope.GetVariables(target, True, False, False)
if not scopeVariables.IsValid():
result.PutCString("couldn't get top-level variables of invoke function")
return
blockDescriptorValue = scopeVariables.GetFirstValueByName('.block_descriptor')
if not blockDescriptorValue.IsValid():
result.PutCString("couldn't get .block_descriptor of invoke function")
return
blockDescriptorType = blockDescriptorValue.type
if not blockDescriptorType.IsValid():
result.PutCString("couldn't get type of .block_descriptor of invoke function")
return
typedBlockLiteralReference = addressOfBlockLiteral.Cast(blockDescriptorType)
if not typedBlockLiteralReference.IsValid():
result.PutCString("couldn't get typed reference to block literal")
return
typedBlockLiteral = typedBlockLiteralReference.deref
# XXX How to return this as a value that lldb will make available as e.g. $1
result.PutCString(str(typedBlockLiteral))
def __lldb_init_module(debugger, internal_dict):
debugger.HandleCommand('command script add -f ' + __name__ + '.print_block pblock')
@wimbledon
Copy link

I got the following error. Any ideas?

error: libarclite_iphoneos.a(arclite.o) failed to load objfile for /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/lib/arc/libarclite_iphoneos.a
error: memory read failed for 0x0

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment