Search code examples
debugginglldb

The registered function with SetScriptCallbackFunction doesn't continue execution after calling StepOut() inside


I have a question about LLDB Python scripting. I'm running a sample script from the book "Advanced Apple Debugging & Reverse Engineering": https://github.com/kodecocodes/dbg-materials/blob/editions/4.0/22-script-bridging-classes-and-hierarchy/projects/final/BreakAfterRegex.py This script defines a command called bar, which does the following:

  1. Creates a breakpoint for a function using a regular expression
  2. When the breakpoint is hit, it steps out to the function's return
  3. Evaluates the return value using the expression command and outputs the result to stdout
  4. Continues execution (because the breakpointHandler function registered with SetScriptCallbackFunction returns False)

However, when I load this script in LLDB (lldb-1500.0.22.8 on macOS Sonoma 14.5) and execute the command, step 4 doesn't occur. The execution doesn't continue unless I manually enter the c command in LLDB. If I remove the thread.StepOver() call, step 4 works correctly. How should I modify the script to ensure that step 4 (continuing execution) works properly?

Steps to reproduced are as follows.

$ lldb --version
lldb-1500.0.22.8
Apple Swift version 5.9 (swiftlang-5.9.0.128.108 clang-1500.0.40.1)
$ cat test.m
#import <Foundation/Foundation.h>

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        NSString *filePath = @"/tmp/hoge.txt";
        
        NSString *content = @"Hello World";
        
        NSError *error;
        BOOL success = [content writeToFile:filePath
                                 atomically:YES
                                   encoding:NSUTF8StringEncoding
                                      error:&error];
        
        if (success) {
            NSLog(@"Success");
        } else {
            NSLog(@"Error");
        }
    }
    return 0;
}
$ clang -framework Foundation test.m -o test
$ lldb ./test
(lldb) target create "./test"
Current executable set to '/Users/user/test' (x86_64).
(lldb) command script import ./lldb/BreakAfterRegex.py 
(lldb) bar NSObject.init\]                                                                                                                                          SBBreakpoint: id = 1, regex = 'NSObject.init\]', locations = 2
(lldb) r
Process 2754 launched: '/Users/user/test' (x86_64)
(lldb) ********************************************************************************
breakpoint: -[NSObject init]
object: <_NSThreadData: 0x600002fe8000>
stopped:-[_NSThreadData init]
Process 2754 stopped
* thread #1, queue = 'com.apple.main-thread', stop reason = step out
    frame #0: 0x00007ff8130211c2 Foundation`-[_NSThreadData init] + 55
Foundation`-[_NSThreadData init]:
->  0x7ff8130211c2 <+55>: movq   0x411c5737(%rip), %rcx
    0x7ff8130211c9 <+62>: movq   (%rcx), %rcx
    0x7ff8130211cc <+65>: cmpq   -0x8(%rbp), %rcx
    0x7ff8130211d0 <+69>: jne    0x7ff8130211d8            ; <+77>
Target 0: (test) stopped.
(lldb) c
Process 2754 resuming
(lldb) ********************************************************************************
breakpoint: -[NSObject init]
object: <NSProcessInfo: 0x6000010e0000>
stopped:__28+[NSProcessInfo processInfo]_block_invoke
Process 2754 stopped
* thread #1, queue = 'com.apple.main-thread', stop reason = step out
    frame #0: 0x00007ff81284b59b Foundation`__28+[NSProcessInfo processInfo]_block_invoke + 32
Foundation`:
->  0x7ff81284b59b <+32>: movq   %rax, 0x4080027e(%rip)    ; processInfoCache
    0x7ff81284b5a2 <+39>: popq   %rbp
    0x7ff81284b5a3 <+40>: retq   

Foundation`-[NSProcessInfo arguments]:
    0x7ff81284b5a4 <+0>:  pushq  %rbp
Target 0: (test) stopped.
(lldb) c
Process 2754 resuming
2024-08-12 16:43:30.871560+0900 test[2754:88642] Success
Process 2754 exited with status = 0 (0x00000000) 

After running the r command, breakpoints are hit several times, but it should not occur because the breakpointHandler function returns False.


Solution

  • I looked at this a bit further. The problem is that the callback installed by the bar lldb command runs the StepOut command and then runs return False. But because running code in a breakpoint command can recursively re-enter the breakpoint commands (and lldb's command interpreter doesn't handle that), a breakpoint callback stops running when it calls any code that runs the target, which means that it never runs the return False line.

    This example was written before lldb had "scripted thread plans" which are the real way to have lldb run the process a bit for you and then react to wherever that stops next. For instance, the FinishPrintAndContinue thread plan from:

    https://github.com/llvm/llvm-project/blob/main/lldb/examples/python/scripted_step.py

    shows an example of how to do this. So the "correct" way to write the breakpoint callback is to make a scripted step class that does the reporting the way the example did, then use the SBThread::StepUsingScriptedThreadPlan - passing False for resume_immediately, then return False from the breakpoint command. That will cause the process to resume, and then your scripted thread plan will take over, and do the step-out and recording in the normal lldb event loop.