]>
Commit | Line | Data |
---|---|---|
1a4d82fc JJ |
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 | |
a1dfa0c6 | 5 | # file. However, this command are execute `immediately`: the command of a `run` or `continue` |
1a4d82fc JJ |
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 | ||
16 | from __future__ import print_function | |
17 | import lldb | |
18 | import os | |
19 | import sys | |
20 | import threading | |
1a4d82fc | 21 | import re |
1a4d82fc JJ |
22 | import time |
23 | ||
0731742a XL |
24 | try: |
25 | import thread | |
26 | except ModuleNotFoundError: | |
27 | # The `thread` module was renamed to `_thread` in Python 3. | |
28 | import _thread as thread | |
29 | ||
1a4d82fc JJ |
30 | # Set this to True for additional output |
31 | DEBUG_OUTPUT = False | |
32 | ||
85aaf69f | 33 | |
1a4d82fc | 34 | def print_debug(s): |
9e0c209e | 35 | """Print something if DEBUG_OUTPUT is True""" |
85aaf69f SL |
36 | global DEBUG_OUTPUT |
37 | if DEBUG_OUTPUT: | |
38 | print("DEBUG: " + str(s)) | |
1a4d82fc JJ |
39 | |
40 | ||
41 | def normalize_whitespace(s): | |
9e0c209e | 42 | """Replace newlines, tabs, multiple spaces, etc with exactly one space""" |
85aaf69f | 43 | return re.sub("\s+", " ", s) |
1a4d82fc JJ |
44 | |
45 | ||
1a4d82fc | 46 | def breakpoint_callback(frame, bp_loc, dict): |
85aaf69f | 47 | """This callback is registered with every breakpoint and makes sure that the |
e1599b0c XL |
48 | frame containing the breakpoint location is selected """ |
49 | ||
50 | # HACK(eddyb) print a newline to avoid continuing an unfinished line. | |
51 | print("") | |
85aaf69f | 52 | print("Hit breakpoint " + str(bp_loc)) |
1a4d82fc | 53 | |
85aaf69f SL |
54 | # Select the frame and the thread containing it |
55 | frame.thread.process.SetSelectedThread(frame.thread) | |
56 | frame.thread.SetSelectedFrame(frame.idx) | |
1a4d82fc | 57 | |
85aaf69f SL |
58 | # Returning True means that we actually want to stop at this breakpoint |
59 | return True | |
1a4d82fc JJ |
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 | |
64 | new_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 | |
68 | registered_breakpoints = set() | |
69 | ||
85aaf69f | 70 | |
1a4d82fc | 71 | def execute_command(command_interpreter, command): |
9e0c209e | 72 | """Executes a single CLI command""" |
85aaf69f SL |
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(): | |
abe05a73 | 82 | print(normalize_whitespace(res.GetOutput() or ''), end='\n') |
85aaf69f SL |
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)) | |
106 | else: | |
107 | print(res.GetError()) | |
1a4d82fc JJ |
108 | |
109 | ||
110 | def start_breakpoint_listener(target): | |
85aaf69f SL |
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) | |
1a4d82fc JJ |
137 | |
138 | ||
139 | def start_watchdog(): | |
85aaf69f SL |
140 | """Starts a watchdog thread that will terminate the process after a certain |
141 | period of time""" | |
142 | watchdog_start_time = time.clock() | |
143 | watchdog_max_time = watchdog_start_time + 30 | |
144 | ||
145 | def watchdog(): | |
146 | while time.clock() < watchdog_max_time: | |
147 | time.sleep(1) | |
148 | print("TIMEOUT: lldb_batchmode.py has been running for too long. Aborting!") | |
149 | thread.interrupt_main() | |
150 | ||
151 | # Start the listener and let it run as a daemon | |
152 | watchdog_thread = threading.Thread(target=watchdog) | |
153 | watchdog_thread.daemon = True | |
154 | watchdog_thread.start() | |
1a4d82fc JJ |
155 | |
156 | #################################################################################################### | |
157 | # ~main | |
158 | #################################################################################################### | |
159 | ||
74b04a01 | 160 | |
1a4d82fc | 161 | if len(sys.argv) != 3: |
85aaf69f SL |
162 | print("usage: python lldb_batchmode.py target-path script-path") |
163 | sys.exit(1) | |
1a4d82fc JJ |
164 | |
165 | target_path = sys.argv[1] | |
166 | script_path = sys.argv[2] | |
167 | ||
168 | print("LLDB batch-mode script") | |
169 | print("----------------------") | |
170 | print("Debugger commands script is '%s'." % script_path) | |
171 | print("Target executable is '%s'." % target_path) | |
172 | print("Current working directory is '%s'" % os.getcwd()) | |
173 | ||
174 | # Start the timeout watchdog | |
175 | start_watchdog() | |
176 | ||
177 | # Create a new debugger instance | |
178 | debugger = lldb.SBDebugger.Create() | |
179 | ||
180 | # When we step or continue, don't return from the function until the process | |
181 | # stops. We do this by setting the async mode to false. | |
182 | debugger.SetAsync(False) | |
183 | ||
184 | # Create a target from a file and arch | |
185 | print("Creating a target for '%s'" % target_path) | |
186 | target_error = lldb.SBError() | |
187 | target = debugger.CreateTarget(target_path, None, None, True, target_error) | |
188 | ||
189 | if not target: | |
85aaf69f SL |
190 | print("Could not create debugging target '" + target_path + "': " + |
191 | str(target_error) + ". Aborting.", file=sys.stderr) | |
192 | sys.exit(1) | |
1a4d82fc JJ |
193 | |
194 | ||
195 | # Register the breakpoint callback for every breakpoint | |
196 | start_breakpoint_listener(target) | |
197 | ||
198 | command_interpreter = debugger.GetCommandInterpreter() | |
199 | ||
200 | try: | |
85aaf69f | 201 | script_file = open(script_path, 'r') |
1a4d82fc | 202 | |
85aaf69f SL |
203 | for line in script_file: |
204 | command = line.strip() | |
205 | if command == "run" or command == "r" or re.match("^process\s+launch.*", command): | |
206 | # Before starting to run the program, let the thread sleep a bit, so all | |
207 | # breakpoint added events can be processed | |
208 | time.sleep(0.5) | |
209 | if command != '': | |
210 | execute_command(command_interpreter, command) | |
1a4d82fc JJ |
211 | |
212 | except IOError as e: | |
85aaf69f SL |
213 | print("Could not read debugging script '%s'." % script_path, file=sys.stderr) |
214 | print(e, file=sys.stderr) | |
215 | print("Aborting.", file=sys.stderr) | |
216 | sys.exit(1) | |
1a4d82fc | 217 | finally: |
a7813a04 | 218 | debugger.Terminate() |
85aaf69f | 219 | script_file.close() |