#!/usr/bin/env python
# ----------------------------------------------------------------------
# Be sure to add the python path that points to the LLDB shared library.
# On MacOSX csh, tcsh:
# setenv PYTHONPATH /Applications/Xcode.app/Contents/SharedFrameworks/LLDB.framework/Resources/Python
# On MacOSX sh, bash:
# export PYTHONPATH=/Applications/Xcode.app/Contents/SharedFrameworks/LLDB.framework/Resources/Python
# ----------------------------------------------------------------------
import optparse
import os
import platform
import sys
import subprocess
# ----------------------------------------------------------------------
# Code that auto imports LLDB
# ----------------------------------------------------------------------
try:
# Just try for LLDB in case PYTHONPATH is already correctly setup
import lldb
except ImportError:
lldb_python_dirs = list()
# lldb is not in the PYTHONPATH, try some defaults for the current platform
platform_system = platform.system()
if platform_system == "Darwin":
# On Darwin, try the currently selected Xcode directory
xcode_dir = subprocess.check_output("xcode-select --print-path", shell=True)
if xcode_dir:
lldb_python_dirs.append(
os.path.realpath(
xcode_dir + "/../SharedFrameworks/LLDB.framework/Resources/Python"
)
)
lldb_python_dirs.append(
xcode_dir + "/Library/PrivateFrameworks/LLDB.framework/Resources/Python"
)
lldb_python_dirs.append(
"/System/Library/PrivateFrameworks/LLDB.framework/Resources/Python"
)
success = False
for lldb_python_dir in lldb_python_dirs:
if os.path.exists(lldb_python_dir):
if not (sys.path.__contains__(lldb_python_dir)):
sys.path.append(lldb_python_dir)
try:
import lldb
except ImportError:
pass
else:
print('imported lldb from: "%s"' % (lldb_python_dir))
success = True
break
if not success:
print(
"error: couldn't locate the 'lldb' module, please set PYTHONPATH correctly"
)
sys.exit(1)
def print_threads(process, options):
if options.show_threads:
for thread in process:
print("%s %s" % (thread, thread.GetFrameAtIndex(0)))
def run_commands(command_interpreter, commands):
return_obj = lldb.SBCommandReturnObject()
for command in commands:
command_interpreter.HandleCommand(command, return_obj)
if return_obj.Succeeded():
print(return_obj.GetOutput())
else:
print(return_obj)
if options.stop_on_error:
break
def main(argv):
description = """Debugs a program using the LLDB python API and uses asynchronous broadcast events to watch for process state changes."""
epilog = """Examples:
#----------------------------------------------------------------------
# Run "/bin/ls" with the arguments "-lAF /tmp/", and set a breakpoint
# at "malloc" and backtrace and read all registers each time we stop
#----------------------------------------------------------------------
% ./process_events.py --breakpoint malloc --stop-command bt --stop-command 'register read' -- /bin/ls -lAF /tmp/
"""
optparse.OptionParser.format_epilog = lambda self, formatter: self.epilog
parser = optparse.OptionParser(
description=description,
prog="process_events",
usage="usage: process_events [options] program [arg1 arg2]",
epilog=epilog,
)
parser.add_option(
"-v",
"--verbose",
action="store_true",
dest="verbose",
help="Enable verbose logging.",
default=False,
)
parser.add_option(
"-b",
"--breakpoint",
action="append",
type="string",
metavar="BPEXPR",
dest="breakpoints",
help='Breakpoint commands to create after the target has been created, the values will be sent to the "_regexp-break" command which supports breakpoints by name, file:line, and address.',
)
parser.add_option(
"-a",
"--arch",
type="string",
dest="arch",
help="The architecture to use when creating the debug target.",
default=None,
)
parser.add_option(
"--platform",
type="string",
metavar="platform",
dest="platform",
help='Specify the platform to use when creating the debug target. Valid values include "localhost", "darwin-kernel", "ios-simulator", "remote-freebsd", "remote-macosx", "remote-ios", "remote-linux".',
default=None,
)
parser.add_option(
"-l",
"--launch-command",
action="append",
type="string",
metavar="CMD",
dest="launch_commands",
help="LLDB command interpreter commands to run once after the process has launched. This option can be specified more than once.",
default=[],
)
parser.add_option(
"-s",
"--stop-command",
action="append",
type="string",
metavar="CMD",
dest="stop_commands",
help="LLDB command interpreter commands to run each time the process stops. This option can be specified more than once.",
default=[],
)
parser.add_option(
"-c",
"--crash-command",
action="append",
type="string",
metavar="CMD",
dest="crash_commands",
help="LLDB command interpreter commands to run in case the process crashes. This option can be specified more than once.",
default=[],
)
parser.add_option(
"-x",
"--exit-command",
action="append",
type="string",
metavar="CMD",
dest="exit_commands",
help="LLDB command interpreter commands to run once after the process has exited. This option can be specified more than once.",
default=[],
)
parser.add_option(
"-T",
"--no-threads",
action="store_false",
dest="show_threads",
help="Don't show threads when process stops.",
default=True,
)
parser.add_option(
"--ignore-errors",
action="store_false",
dest="stop_on_error",
help="Don't stop executing LLDB commands if the command returns an error. This applies to all of the LLDB command interpreter commands that get run for launch, stop, crash and exit.",
default=True,
)
parser.add_option(
"-n",
"--run-count",
type="int",
dest="run_count",
metavar="N",
help="How many times to run the process in case the process exits.",
default=1,
)
parser.add_option(
"-t",
"--event-timeout",
type="int",
dest="event_timeout",
metavar="SEC",
help="Specify the timeout in seconds to wait for process state change events.",
default=lldb.UINT32_MAX,
)
parser.add_option(
"-e",
"--environment",
action="append",
type="string",
metavar="ENV",
dest="env_vars",
help="Environment variables to set in the inferior process when launching a process.",
)
parser.add_option(
"-d",
"--working-dir",
type="string",
metavar="DIR",
dest="working_dir",
help="The current working directory when launching a process.",
default=None,
)
parser.add_option(
"-p",
"--attach-pid",
type="int",
dest="attach_pid",
metavar="PID",
help="Specify a process to attach to by process ID.",
default=-1,
)
parser.add_option(
"-P",
"--attach-name",
type="string",
dest="attach_name",
metavar="PROCESSNAME",
help="Specify a process to attach to by name.",
default=None,
)
parser.add_option(
"-w",
"--attach-wait",
action="store_true",
dest="attach_wait",
help="Wait for the next process to launch when attaching to a process by name.",
default=False,
)
try:
(options, args) = parser.parse_args(argv)
except:
return
attach_info = None
launch_info = None
exe = None
if args:
exe = args.pop(0)
launch_info = lldb.SBLaunchInfo(args)
if options.env_vars:
launch_info.SetEnvironmentEntries(options.env_vars, True)
if options.working_dir:
launch_info.SetWorkingDirectory(options.working_dir)
elif options.attach_pid != -1:
if options.run_count == 1:
attach_info = lldb.SBAttachInfo(options.attach_pid)
else:
print("error: --run-count can't be used with the --attach-pid option")
sys.exit(1)
elif not options.attach_name is None:
if options.run_count == 1:
attach_info = lldb.SBAttachInfo(options.attach_name, options.attach_wait)
else:
print("error: --run-count can't be used with the --attach-name option")
sys.exit(1)
else:
print(
"error: a program path for a program to debug and its arguments are required"
)
sys.exit(1)
# Create a new debugger instance
debugger = lldb.SBDebugger.Create()
debugger.SetAsync(True)
command_interpreter = debugger.GetCommandInterpreter()
# Create a target from a file and arch
if exe:
print("Creating a target for '%s'" % exe)
error = lldb.SBError()
target = debugger.CreateTarget(exe, options.arch, options.platform, True, error)
if target:
# Set any breakpoints that were specified in the args if we are launching. We use the
# command line command to take advantage of the shorthand breakpoint
# creation
if launch_info and options.breakpoints:
for bp in options.breakpoints:
debugger.HandleCommand("_regexp-break %s" % (bp))
run_commands(command_interpreter, ["breakpoint list"])
for run_idx in range(options.run_count):
# Launch the process. Since we specified synchronous mode, we won't return
# from this function until we hit the breakpoint at main
error = lldb.SBError()
if launch_info:
if options.run_count == 1:
print('Launching "%s"...' % (exe))
else:
print(
'Launching "%s"... (launch %u of %u)'
% (exe, run_idx + 1, options.run_count)
)
process = target.Launch(launch_info, error)
else:
if options.attach_pid != -1:
print("Attaching to process %i..." % (options.attach_pid))
else:
if options.attach_wait:
print(
'Waiting for next to process named "%s" to launch...'
% (options.attach_name)
)
else:
print(
'Attaching to existing process named "%s"...'
% (options.attach_name)
)
process = target.Attach(attach_info, error)
# Make sure the launch went ok
if process and process.GetProcessID() != lldb.LLDB_INVALID_PROCESS_ID:
pid = process.GetProcessID()
print("Process is %i" % (pid))
if attach_info:
# continue process if we attached as we won't get an
# initial event
process.Continue()
listener = debugger.GetListener()
# sign up for process state change events
stop_idx = 0
done = False
while not done:
event = lldb.SBEvent()
if listener.WaitForEvent(options.event_timeout, event):
if lldb.SBProcess.EventIsProcessEvent(event):
state = lldb.SBProcess.GetStateFromEvent(event)
if state == lldb.eStateInvalid:
# Not a state event
print("process event = %s" % (event))
else:
print(
"process state changed event: %s"
% (lldb.SBDebugger.StateAsCString(state))
)
if state == lldb.eStateStopped:
if stop_idx == 0:
if launch_info:
print("process %u launched" % (pid))
run_commands(
command_interpreter, ["breakpoint list"]
)
else:
print("attached to process %u" % (pid))
for m in target.modules:
print(m)
if options.breakpoints:
for bp in options.breakpoints:
debugger.HandleCommand(
"_regexp-break %s" % (bp)
)
run_commands(
command_interpreter,
["breakpoint list"],
)
run_commands(
command_interpreter, options.launch_commands
)
else:
if options.verbose:
print("process %u stopped" % (pid))
run_commands(
command_interpreter, options.stop_commands
)
stop_idx += 1
print_threads(process, options)
print("continuing process %u" % (pid))
process.Continue()
elif state == lldb.eStateExited:
exit_desc = process.GetExitDescription()
if exit_desc:
print(
"process %u exited with status %u: %s"
% (pid, process.GetExitStatus(), exit_desc)
)
else:
print(
"process %u exited with status %u"
% (pid, process.GetExitStatus())
)
run_commands(
command_interpreter, options.exit_commands
)
done = True
elif state == lldb.eStateCrashed:
print("process %u crashed" % (pid))
print_threads(process, options)
run_commands(
command_interpreter, options.crash_commands
)
done = True
elif state == lldb.eStateDetached:
print("process %u detached" % (pid))
done = True
elif state == lldb.eStateRunning:
# process is running, don't say anything,
# we will always get one of these after
# resuming
if options.verbose:
print("process %u resumed" % (pid))
elif state == lldb.eStateUnloaded:
print(
"process %u unloaded, this shouldn't happen"
% (pid)
)
done = True
elif state == lldb.eStateConnected:
print("process connected")
elif state == lldb.eStateAttaching:
print("process attaching")
elif state == lldb.eStateLaunching:
print("process launching")
else:
print("event = %s" % (event))
else:
# timeout waiting for an event
print(
"no process event for %u seconds, killing the process..."
% (options.event_timeout)
)
done = True
# Now that we are done dump the stdout and stderr
process_stdout = process.GetSTDOUT(1024)
if process_stdout:
print("Process STDOUT:\n%s" % (process_stdout))
while process_stdout:
process_stdout = process.GetSTDOUT(1024)
print(process_stdout)
process_stderr = process.GetSTDERR(1024)
if process_stderr:
print("Process STDERR:\n%s" % (process_stderr))
while process_stderr:
process_stderr = process.GetSTDERR(1024)
print(process_stderr)
process.Kill() # kill the process
else:
if error:
print(error)
else:
if launch_info:
print("error: launch failed")
else:
print("error: attach failed")
lldb.SBDebugger.Terminate()
if __name__ == "__main__":
main(sys.argv[1:])