Created
August 14, 2024 03:00
-
-
Save quark-zju/d21a4d1b181f1cc6c70eb914b8e3e2ed to your computer and use it in GitHub Desktop.
Trace function calls via lldb
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/usr/bin/python3 | |
import json | |
import os | |
import re | |
import shlex | |
import subprocess | |
import sys | |
import time | |
write = sys.stdout.write | |
verbose = os.getenv("V") | |
pattern_match = lambda _x: True | |
def attach_pid(pid, pattern): | |
import lldb | |
debugger = lldb.debugger | |
target = debugger.CreateTarget("") | |
process = target.AttachToProcessWithID(lldb.SBListener(), pid, lldb.SBError()) | |
bp = set_breakpoints(target, pattern) | |
return target, process, bp | |
def new_process(args, pattern): | |
import lldb | |
debugger = lldb.debugger | |
target = debugger.CreateTarget(args[0]) | |
bp = set_breakpoints(target, pattern) | |
launch_info = target.GetLaunchInfo() | |
launch_info.SetArguments(args[1:], True) | |
error = lldb.SBError() | |
process = target.Launch(launch_info, error) | |
return target, process, bp | |
def trace2(debugger, command, exe_ctx, result, internal_dict): | |
# Update 'write' to write to $OUT. | |
global write | |
out = os.getenv("OUT") | |
if out: | |
f = open(out, "a", newline="\n") | |
write = f.write | |
pattern = os.getenv("PAT") or ".*" | |
# command only contains args | |
args = shlex.split(command) | |
if len(args) >= 1 and args[0]: | |
try: | |
pid = int(args[0]) | |
target, process, bp = attach_pid(pid, pattern) | |
except ValueError: | |
# not a pid, treat as args | |
target, process, bp = new_process(args, pattern) | |
else: | |
# used standalone in lldb | |
target = exe_ctx.target | |
process = exe_ctx.process | |
bp = set_breakpoints(target, pattern) | |
debug_write(f"BREAKPOINT SET ({bp.num_locations})\n") | |
# count = 0 | |
# for loc in bp.locations: | |
# count += 1 | |
# if count > 100: | |
# break | |
# print(loc) | |
wait_for_completion(process) | |
def wait_for_completion(process): | |
import lldb | |
state = process.GetState() | |
if state == lldb.eStateStopped: | |
process.Continue() | |
# Wait for the process to exit | |
try: | |
while True: | |
state = process.GetState() | |
if state == lldb.eStateExited: | |
write(f"Process exited with status {process.GetExitStatus()}\n") | |
break | |
elif state == lldb.eStateStopped: | |
# The process might hit other breakpoints; handle them if needed. | |
debug_write(f"Process stopped. Breaking...\n") | |
break | |
# Sleep for a bit to avoid busy-waiting | |
time.sleep(0.1) | |
except KeyboardInterrupt: | |
lldb.debugger.Terminate() | |
def set_breakpoints(target, pattern): | |
import lldb | |
global pattern_match | |
# find the main module name | |
try: | |
module_name = str(target.modules[0].file) | |
except IndexError: | |
raise RuntimeError("no main module found") | |
debug_write(f"MAIN MODULE: {module_name}\nBREAKPOINT PATTERN: {pattern}\n") | |
bp = target.BreakpointCreateByRegex(pattern, module_name) | |
assert bp.IsValid() | |
pattern_match = re.compile(pattern).match | |
bp.SetScriptCallbackFunction(f"{__name__}.breakpoint_callback") | |
return bp | |
def breakpoint_callback(frame, bp_loc, extra_args, internal_dict): | |
name = str(frame.name) | |
if not pattern_match(name): | |
debug_write(f" IGNORE: {name}\n") | |
# Disable noisy locations | |
bp_loc.SetEnabled(False) | |
return False | |
thread = frame.thread | |
# Figure out the stack depth | |
frame_depth = len(thread.frames) | |
prefix = " " * frame_depth | |
# Print args. Render "empty" properly. | |
args = frame.args | |
if args.GetSize() == 0: | |
args = "" | |
else: | |
args = str(args).replace("\n", " ") | |
if len(args) > 120: | |
args = args[:120] + "..." | |
write(f"{thread.name}>{prefix}{frame.name}({args})\n") | |
# Continue the process | |
return False | |
def debug_write(message): | |
if verbose: | |
write(message) | |
def __lldb_init_module(debugger, internal_dict): | |
debugger.HandleCommand(f"command script add -f {__name__}.trace2 trace2") | |
def main(): | |
import os, tempfile | |
python_source = open(__file__, "rb").read() | |
with tempfile.TemporaryDirectory(prefix="lldbscript") as dir: | |
python_script_path = os.path.join(dir, "trace2.py") | |
with open(python_script_path, "wb") as f: | |
f.write(python_source) | |
args = [ | |
os.getenv("LLDB") or "lldb", | |
#"-b", | |
#"--source-quietly", | |
"-o", | |
f"command script import {python_script_path}", | |
"-o", | |
f"trace2 {' '.join(map(shlex.quote, sys.argv[1:]))}", | |
] | |
subprocess.run(args) | |
if __name__ == "__main__": | |
main() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment