1# This script allows to use LLDB in a way similar to GDB's batch mode. That is, given a text file
2# containing LLDB commands (one command per line), this script will execute the commands one after
3# the other.
4# LLDB also has the -s and -S commandline options which also execute a list of commands from a text
5# file. However, this command are execute `immediately`: the command of a `run` or `continue`
6# command will be executed immediately after the `run` or `continue`, without waiting for the next
7# breakpoint to be hit. This a command sequence like the following will not yield reliable results:
8#
9#   break 11
10#   run
11#   print x
12#
13# Most of the time the `print` command will be executed while the program is still running will thus
14# fail. Using this Python script, the above will work as expected.
15
16from __future__ import print_function
17import lldb
18import os
19import sys
20import threading
21import re
22import time
23
24try:
25    import thread
26except ModuleNotFoundError:
27    # The `thread` module was renamed to `_thread` in Python 3.
28    import _thread as thread
29
30# Set this to True for additional output
31DEBUG_OUTPUT = True
32
33
34def print_debug(s):
35    """Print something if DEBUG_OUTPUT is True"""
36    global DEBUG_OUTPUT
37    if DEBUG_OUTPUT:
38        print("DEBUG: " + str(s))
39
40
41def normalize_whitespace(s):
42    """Replace newlines, tabs, multiple spaces, etc with exactly one space"""
43    return re.sub("\s+", " ", s)
44
45
46def breakpoint_callback(frame, bp_loc, dict):
47    """This callback is registered with every breakpoint and makes sure that the
48    frame containing the breakpoint location is selected """
49
50    # HACK(eddyb) print a newline to avoid continuing an unfinished line.
51    print("")
52    print("Hit breakpoint " + str(bp_loc))
53
54    # Select the frame and the thread containing it
55    frame.thread.process.SetSelectedThread(frame.thread)
56    frame.thread.SetSelectedFrame(frame.idx)
57
58    # Returning True means that we actually want to stop at this breakpoint
59    return True
60
61
62# This is a list of breakpoints that are not registered with the breakpoint callback. The list is
63# populated by the breakpoint listener and checked/emptied whenever a command has been executed
64new_breakpoints = []
65
66# This set contains all breakpoint ids that have already been registered with a callback, and is
67# used to avoid hooking callbacks into breakpoints more than once
68registered_breakpoints = set()
69
70
71def execute_command(command_interpreter, command):
72    """Executes a single CLI command"""
73    global new_breakpoints
74    global registered_breakpoints
75
76    res = lldb.SBCommandReturnObject()
77    print(command)
78    command_interpreter.HandleCommand(command, res)
79
80    if res.Succeeded():
81        if res.HasResult():
82            print(normalize_whitespace(res.GetOutput() or ''), end='\n')
83
84        # If the command introduced any breakpoints, make sure to register
85        # them with the breakpoint
86        # callback
87        while len(new_breakpoints) > 0:
88            res.Clear()
89            breakpoint_id = new_breakpoints.pop()
90
91            if breakpoint_id in registered_breakpoints:
92                print_debug("breakpoint with id %s is already registered. Ignoring." %
93                            str(breakpoint_id))
94            else:
95                print_debug("registering breakpoint callback, id = " + str(breakpoint_id))
96                callback_command = ("breakpoint command add -F breakpoint_callback " +
97                                    str(breakpoint_id))
98                command_interpreter.HandleCommand(callback_command, res)
99                if res.Succeeded():
100                    print_debug("successfully registered breakpoint callback, id = " +
101                                str(breakpoint_id))
102                    registered_breakpoints.add(breakpoint_id)
103                else:
104                    print("Error while trying to register breakpoint callback, id = " +
105                          str(breakpoint_id) + ", message = " + str(res.GetError()))
106    else:
107        print(res.GetError())
108
109
110def start_breakpoint_listener(target):
111    """Listens for breakpoints being added and adds new ones to the callback
112    registration list"""
113    listener = lldb.SBListener("breakpoint listener")
114
115    def listen():
116        event = lldb.SBEvent()
117        try:
118            while True:
119                if listener.WaitForEvent(120, event):
120                    if lldb.SBBreakpoint.EventIsBreakpointEvent(event) and \
121                            lldb.SBBreakpoint.GetBreakpointEventTypeFromEvent(event) == \
122                            lldb.eBreakpointEventTypeAdded:
123                        global new_breakpoints
124                        breakpoint = lldb.SBBreakpoint.GetBreakpointFromEvent(event)
125                        print_debug("breakpoint added, id = " + str(breakpoint.id))
126                        new_breakpoints.append(breakpoint.id)
127        except:
128            print_debug("breakpoint listener shutting down")
129
130    # Start the listener and let it run as a daemon
131    listener_thread = threading.Thread(target=listen)
132    listener_thread.daemon = True
133    listener_thread.start()
134
135    # Register the listener with the target
136    target.GetBroadcaster().AddListener(listener, lldb.SBTarget.eBroadcastBitBreakpointChanged)
137
138
139def start_watchdog():
140    """Starts a watchdog thread that will terminate the process after a certain
141    period of time"""
142
143    try:
144        from time import clock
145    except ImportError:
146        from time import perf_counter as clock
147
148    watchdog_start_time = clock()
149    watchdog_max_time = watchdog_start_time + 30
150
151    def watchdog():
152        while clock() < watchdog_max_time:
153            time.sleep(1)
154        print("TIMEOUT: lldb_batchmode.py has been running for too long. Aborting!")
155        thread.interrupt_main()
156
157    # Start the listener and let it run as a daemon
158    watchdog_thread = threading.Thread(target=watchdog)
159    watchdog_thread.daemon = True
160    watchdog_thread.start()
161
162####################################################################################################
163# ~main
164####################################################################################################
165
166
167if len(sys.argv) != 3:
168    print("usage: python lldb_batchmode.py target-path script-path")
169    sys.exit(1)
170
171target_path = sys.argv[1]
172script_path = sys.argv[2]
173
174print("LLDB batch-mode script")
175print("----------------------")
176print("Debugger commands script is '%s'." % script_path)
177print("Target executable is '%s'." % target_path)
178print("Current working directory is '%s'" % os.getcwd())
179
180# Start the timeout watchdog
181start_watchdog()
182
183# Create a new debugger instance
184debugger = lldb.SBDebugger.Create()
185
186# When we step or continue, don't return from the function until the process
187# stops. We do this by setting the async mode to false.
188debugger.SetAsync(False)
189
190# Create a target from a file and arch
191print("Creating a target for '%s'" % target_path)
192target_error = lldb.SBError()
193target = debugger.CreateTarget(target_path, None, None, True, target_error)
194
195if not target:
196    print("Could not create debugging target '" + target_path + "': " +
197          str(target_error) + ". Aborting.", file=sys.stderr)
198    sys.exit(1)
199
200
201# Register the breakpoint callback for every breakpoint
202start_breakpoint_listener(target)
203
204command_interpreter = debugger.GetCommandInterpreter()
205
206try:
207    script_file = open(script_path, 'r')
208
209    for line in script_file:
210        command = line.strip()
211        if command == "run" or command == "r" or re.match("^process\s+launch.*", command):
212            # Before starting to run the program, let the thread sleep a bit, so all
213            # breakpoint added events can be processed
214            time.sleep(0.5)
215        if command != '':
216            execute_command(command_interpreter, command)
217
218except IOError as e:
219    print("Could not read debugging script '%s'." % script_path, file=sys.stderr)
220    print(e, file=sys.stderr)
221    print("Aborting.", file=sys.stderr)
222    sys.exit(1)
223finally:
224    debugger.Terminate()
225    script_file.close()
226