xref: /llvm-project/lldb/examples/python/crashlog.py (revision 4c4f0d81f47cf9ad785dc2ea323ec2f0aedb72df)
1#!/usr/bin/env python3
2
3# ----------------------------------------------------------------------
4# Be sure to add the python path that points to the LLDB shared library.
5#
6# To use this in the embedded python interpreter using "lldb":
7#
8#   cd /path/containing/crashlog.py
9#   lldb
10#   (lldb) script import crashlog
11#   "crashlog" command installed, type "crashlog --help" for detailed help
12#   (lldb) crashlog ~/Library/Logs/DiagnosticReports/a.crash
13#
14# The benefit of running the crashlog command inside lldb in the
15# embedded python interpreter is when the command completes, there
16# will be a target with all of the files loaded at the locations
17# described in the crash log. Only the files that have stack frames
18# in the backtrace will be loaded unless the "--load-all" option
19# has been specified. This allows users to explore the program in the
20# state it was in right at crash time.
21#
22# On MacOSX csh, tcsh:
23#   ( setenv PYTHONPATH /path/to/LLDB.framework/Resources/Python ; ./crashlog.py ~/Library/Logs/DiagnosticReports/a.crash )
24#
25# On MacOSX sh, bash:
26#   PYTHONPATH=/path/to/LLDB.framework/Resources/Python ./crashlog.py ~/Library/Logs/DiagnosticReports/a.crash
27# ----------------------------------------------------------------------
28
29import abc
30import argparse
31import concurrent.futures
32import contextlib
33import datetime
34import json
35import os
36import platform
37import plistlib
38import re
39import shlex
40import string
41import subprocess
42import sys
43import tempfile
44import threading
45import time
46import uuid
47
48
49print_lock = threading.RLock()
50
51try:
52    # First try for LLDB in case PYTHONPATH is already correctly setup.
53    import lldb
54except ImportError:
55    # Ask the command line driver for the path to the lldb module. Copy over
56    # the environment so that SDKROOT is propagated to xcrun.
57    command = (
58        ["xcrun", "lldb", "-P"] if platform.system() == "Darwin" else ["lldb", "-P"]
59    )
60    # Extend the PYTHONPATH if the path exists and isn't already there.
61    lldb_python_path = subprocess.check_output(command).decode("utf-8").strip()
62    if os.path.exists(lldb_python_path) and not sys.path.__contains__(lldb_python_path):
63        sys.path.append(lldb_python_path)
64    # Try importing LLDB again.
65    try:
66        import lldb
67    except ImportError:
68        print(
69            "error: couldn't locate the 'lldb' module, please set PYTHONPATH correctly"
70        )
71        sys.exit(1)
72
73from lldb.utils import symbolication
74from lldb.plugins.scripted_process import INTEL64_GPR, ARM64_GPR
75
76
77def read_plist(s):
78    if sys.version_info.major == 3:
79        return plistlib.loads(s)
80    else:
81        return plistlib.readPlistFromString(s)
82
83
84class CrashLog(symbolication.Symbolicator):
85    class Thread:
86        """Class that represents a thread in a darwin crash log"""
87
88        def __init__(self, index, app_specific_backtrace, arch):
89            self.index = index
90            self.id = index
91            self.images = list()
92            self.frames = list()
93            self.idents = list()
94            self.registers = dict()
95            self.reason = None
96            self.name = None
97            self.queue = None
98            self.crashed = False
99            self.app_specific_backtrace = app_specific_backtrace
100            self.arch = arch
101
102        def dump_registers(self, prefix=""):
103            registers_info = None
104            sorted_registers = {}
105
106            def sort_dict(d):
107                sorted_keys = list(d.keys())
108                sorted_keys.sort()
109                return {k: d[k] for k in sorted_keys}
110
111            if self.arch:
112                if "x86_64" == self.arch:
113                    registers_info = INTEL64_GPR
114                elif "arm64" in self.arch:
115                    registers_info = ARM64_GPR
116                else:
117                    print("unknown target architecture: %s" % self.arch)
118                    return
119
120                # Add registers available in the register information dictionary.
121                for reg_info in registers_info:
122                    reg_name = None
123                    if reg_info["name"] in self.registers:
124                        reg_name = reg_info["name"]
125                    elif (
126                        "generic" in reg_info and reg_info["generic"] in self.registers
127                    ):
128                        reg_name = reg_info["generic"]
129                    else:
130                        # Skip register that are present in the register information dictionary but not present in the report.
131                        continue
132
133                    reg_val = self.registers[reg_name]
134                    sorted_registers[reg_name] = reg_val
135
136                unknown_parsed_registers = {}
137                for reg_name in self.registers:
138                    if reg_name not in sorted_registers:
139                        unknown_parsed_registers[reg_name] = self.registers[reg_name]
140
141                sorted_registers.update(sort_dict(unknown_parsed_registers))
142
143            else:
144                sorted_registers = sort_dict(self.registers)
145
146            for reg_name, reg_val in sorted_registers.items():
147                print("%s    %-8s = %#16.16x" % (prefix, reg_name, reg_val))
148
149        def dump(self, prefix=""):
150            if self.app_specific_backtrace:
151                print(
152                    "%Application Specific Backtrace[%u] %s"
153                    % (prefix, self.index, self.reason)
154                )
155            else:
156                print("%sThread[%u] %s" % (prefix, self.index, self.reason))
157            if self.frames:
158                print("%s  Frames:" % (prefix))
159                for frame in self.frames:
160                    frame.dump(prefix + "    ")
161            if self.registers:
162                print("%s  Registers:" % (prefix))
163                self.dump_registers(prefix)
164
165        def dump_symbolicated(self, crash_log, options):
166            this_thread_crashed = self.app_specific_backtrace
167            if not this_thread_crashed:
168                this_thread_crashed = self.did_crash()
169                if options.crashed_only and this_thread_crashed == False:
170                    return
171
172            print("%s" % self)
173            display_frame_idx = -1
174            for frame_idx, frame in enumerate(self.frames):
175                disassemble = (
176                    this_thread_crashed or options.disassemble_all_threads
177                ) and frame_idx < options.disassemble_depth
178
179                # Except for the zeroth frame, we should subtract 1 from every
180                # frame pc to get the previous line entry.
181                pc = frame.pc & crash_log.addr_mask
182                pc = pc if frame_idx == 0 or pc == 0 else pc - 1
183                symbolicated_frame_addresses = crash_log.symbolicate(
184                    pc, options.verbose
185                )
186
187                if symbolicated_frame_addresses:
188                    symbolicated_frame_address_idx = 0
189                    for symbolicated_frame_address in symbolicated_frame_addresses:
190                        display_frame_idx += 1
191                        print("[%3u] %s" % (frame_idx, symbolicated_frame_address))
192                        if (
193                            (options.source_all or self.did_crash())
194                            and display_frame_idx < options.source_frames
195                            and options.source_context
196                        ):
197                            source_context = options.source_context
198                            line_entry = (
199                                symbolicated_frame_address.get_symbol_context().line_entry
200                            )
201                            if line_entry.IsValid():
202                                strm = lldb.SBStream()
203                                if line_entry:
204                                    crash_log.debugger.GetSourceManager().DisplaySourceLinesWithLineNumbers(
205                                        line_entry.file,
206                                        line_entry.line,
207                                        source_context,
208                                        source_context,
209                                        "->",
210                                        strm,
211                                    )
212                                source_text = strm.GetData()
213                                if source_text:
214                                    # Indent the source a bit
215                                    indent_str = "    "
216                                    join_str = "\n" + indent_str
217                                    print(
218                                        "%s%s"
219                                        % (
220                                            indent_str,
221                                            join_str.join(source_text.split("\n")),
222                                        )
223                                    )
224                        if symbolicated_frame_address_idx == 0:
225                            if disassemble:
226                                instructions = (
227                                    symbolicated_frame_address.get_instructions()
228                                )
229                                if instructions:
230                                    print()
231                                    symbolication.disassemble_instructions(
232                                        crash_log.get_target(),
233                                        instructions,
234                                        frame.pc,
235                                        options.disassemble_before,
236                                        options.disassemble_after,
237                                        frame.index > 0,
238                                    )
239                                    print()
240                        symbolicated_frame_address_idx += 1
241                else:
242                    print(frame)
243            if self.registers:
244                print()
245                self.dump_registers()
246            elif self.crashed:
247                print()
248                print("No thread state (register information) available")
249
250        def add_ident(self, ident):
251            if ident not in self.idents:
252                self.idents.append(ident)
253
254        def did_crash(self):
255            return self.reason is not None
256
257        def __str__(self):
258            if self.app_specific_backtrace:
259                s = "Application Specific Backtrace[%u]" % self.index
260            else:
261                s = "Thread[%u]" % self.index
262            if self.reason:
263                s += " %s" % self.reason
264            return s
265
266    class Frame:
267        """Class that represents a stack frame in a thread in a darwin crash log"""
268
269        def __init__(self, index, pc, description):
270            self.pc = pc
271            self.description = description
272            self.index = index
273
274        def __str__(self):
275            if self.description:
276                return "[%3u] 0x%16.16x %s" % (self.index, self.pc, self.description)
277            else:
278                return "[%3u] 0x%16.16x" % (self.index, self.pc)
279
280        def dump(self, prefix):
281            print("%s%s" % (prefix, str(self)))
282
283    class DarwinImage(symbolication.Image):
284        """Class that represents a binary images in a darwin crash log"""
285
286        dsymForUUIDBinary = "/usr/local/bin/dsymForUUID"
287        if not os.path.exists(dsymForUUIDBinary):
288            try:
289                dsymForUUIDBinary = (
290                    subprocess.check_output("which dsymForUUID", shell=True)
291                    .decode("utf-8")
292                    .rstrip("\n")
293                )
294            except:
295                dsymForUUIDBinary = ""
296
297        dwarfdump_uuid_regex = re.compile("UUID: ([-0-9a-fA-F]+) \(([^\(]+)\) .*")
298
299        def __init__(
300            self, text_addr_lo, text_addr_hi, identifier, version, uuid, path, verbose
301        ):
302            symbolication.Image.__init__(self, path, uuid)
303            self.add_section(
304                symbolication.Section(text_addr_lo, text_addr_hi, "__TEXT")
305            )
306            self.identifier = identifier
307            self.version = version
308            self.verbose = verbose
309
310        def show_symbol_progress(self):
311            """
312            Hide progress output and errors from system frameworks as they are plentiful.
313            """
314            if self.verbose:
315                return True
316            return not (
317                self.path.startswith("/System/Library/")
318                or self.path.startswith("/usr/lib/")
319            )
320
321        def find_matching_slice(self):
322            dwarfdump_cmd_output = subprocess.check_output(
323                'dwarfdump --uuid "%s"' % self.path, shell=True
324            ).decode("utf-8")
325            self_uuid = self.get_uuid()
326            for line in dwarfdump_cmd_output.splitlines():
327                match = self.dwarfdump_uuid_regex.search(line)
328                if match:
329                    dwarf_uuid_str = match.group(1)
330                    dwarf_uuid = uuid.UUID(dwarf_uuid_str)
331                    if self_uuid == dwarf_uuid:
332                        self.resolved_path = self.path
333                        self.arch = match.group(2)
334                        return True
335            if not self.resolved_path:
336                self.unavailable = True
337                if self.show_symbol_progress():
338                    print(
339                        (
340                            "error\n    error: unable to locate '%s' with UUID %s"
341                            % (self.path, self.get_normalized_uuid_string())
342                        )
343                    )
344                return False
345
346        def locate_module_and_debug_symbols(self):
347            # Don't load a module twice...
348            if self.resolved:
349                return True
350            # Mark this as resolved so we don't keep trying
351            self.resolved = True
352            uuid_str = self.get_normalized_uuid_string()
353            if self.show_symbol_progress():
354                with print_lock:
355                    print("Getting symbols for %s %s..." % (uuid_str, self.path))
356            # Keep track of unresolved source paths.
357            unavailable_source_paths = set()
358            if os.path.exists(self.dsymForUUIDBinary):
359                dsym_for_uuid_command = "%s %s" % (self.dsymForUUIDBinary, uuid_str)
360                s = subprocess.check_output(dsym_for_uuid_command, shell=True)
361                if s:
362                    try:
363                        plist_root = read_plist(s)
364                    except:
365                        with print_lock:
366                            print(
367                                (
368                                    "Got exception: ",
369                                    sys.exc_info()[1],
370                                    " handling dsymForUUID output: \n",
371                                    s,
372                                )
373                            )
374                        raise
375                    if plist_root:
376                        plist = plist_root[uuid_str]
377                        if plist:
378                            if "DBGArchitecture" in plist:
379                                self.arch = plist["DBGArchitecture"]
380                            if "DBGDSYMPath" in plist:
381                                self.symfile = os.path.realpath(plist["DBGDSYMPath"])
382                            if "DBGSymbolRichExecutable" in plist:
383                                self.path = os.path.expanduser(
384                                    plist["DBGSymbolRichExecutable"]
385                                )
386                                self.resolved_path = self.path
387                            if "DBGSourcePathRemapping" in plist:
388                                path_remapping = plist["DBGSourcePathRemapping"]
389                                for _, value in path_remapping.items():
390                                    source_path = os.path.expanduser(value)
391                                    if not os.path.exists(source_path):
392                                        unavailable_source_paths.add(source_path)
393            if not self.resolved_path and os.path.exists(self.path):
394                if not self.find_matching_slice():
395                    return False
396            if not self.resolved_path and not os.path.exists(self.path):
397                try:
398                    mdfind_results = (
399                        subprocess.check_output(
400                            [
401                                "/usr/bin/mdfind",
402                                "com_apple_xcode_dsym_uuids == %s" % uuid_str,
403                            ]
404                        )
405                        .decode("utf-8")
406                        .splitlines()
407                    )
408                    found_matching_slice = False
409                    for dsym in mdfind_results:
410                        dwarf_dir = os.path.join(dsym, "Contents/Resources/DWARF")
411                        if not os.path.exists(dwarf_dir):
412                            # Not a dSYM bundle, probably an Xcode archive.
413                            continue
414                        with print_lock:
415                            print('falling back to binary inside "%s"' % dsym)
416                        self.symfile = dsym
417                        for filename in os.listdir(dwarf_dir):
418                            self.path = os.path.join(dwarf_dir, filename)
419                            if self.find_matching_slice():
420                                found_matching_slice = True
421                                break
422                        if found_matching_slice:
423                            break
424                except:
425                    pass
426            if (self.resolved_path and os.path.exists(self.resolved_path)) or (
427                self.path and os.path.exists(self.path)
428            ):
429                with print_lock:
430                    print("Resolved symbols for %s %s..." % (uuid_str, self.path))
431                    if len(unavailable_source_paths):
432                        for source_path in unavailable_source_paths:
433                            print(
434                                "Could not access remapped source path for %s %s"
435                                % (uuid_str, source_path)
436                            )
437                return True
438            else:
439                self.unavailable = True
440            return False
441
442    def __init__(self, debugger, path, verbose):
443        """CrashLog constructor that take a path to a darwin crash log file"""
444        symbolication.Symbolicator.__init__(self, debugger)
445        self.path = os.path.expanduser(path)
446        self.info_lines = list()
447        self.system_profile = list()
448        self.threads = list()
449        self.backtraces = list()  # For application specific backtraces
450        self.idents = (
451            list()
452        )  # A list of the required identifiers for doing all stack backtraces
453        self.errors = list()
454        self.exception = dict()
455        self.crashed_thread_idx = -1
456        self.version = -1
457        self.target = None
458        self.verbose = verbose
459        self.process_id = None
460        self.process_identifier = None
461        self.process_path = None
462        self.process_arch = None
463
464    def dump(self):
465        print("Crash Log File: %s" % (self.path))
466        if self.backtraces:
467            print("\nApplication Specific Backtraces:")
468            for thread in self.backtraces:
469                thread.dump("  ")
470        print("\nThreads:")
471        for thread in self.threads:
472            thread.dump("  ")
473        print("\nImages:")
474        for image in self.images:
475            image.dump("  ")
476
477    def set_main_image(self, identifier):
478        for i, image in enumerate(self.images):
479            if image.identifier == identifier:
480                self.images.insert(0, self.images.pop(i))
481                break
482
483    def find_image_with_identifier(self, identifier):
484        for image in self.images:
485            if image.identifier == identifier:
486                return image
487        regex_text = "^.*\.%s$" % (re.escape(identifier))
488        regex = re.compile(regex_text)
489        for image in self.images:
490            if regex.match(image.identifier):
491                return image
492        return None
493
494    def create_target(self):
495        if self.target is None:
496            self.target = symbolication.Symbolicator.create_target(self)
497            if self.target:
498                return self.target
499            # We weren't able to open the main executable as, but we can still
500            # symbolicate
501            print("crashlog.create_target()...2")
502            if self.idents:
503                for ident in self.idents:
504                    image = self.find_image_with_identifier(ident)
505                    if image:
506                        self.target = image.create_target(self.debugger)
507                        if self.target:
508                            return self.target  # success
509            print("crashlog.create_target()...3")
510            for image in self.images:
511                self.target = image.create_target(self.debugger)
512                if self.target:
513                    return self.target  # success
514            print("crashlog.create_target()...4")
515            print("error: Unable to locate any executables from the crash log.")
516            print("       Try loading the executable into lldb before running crashlog")
517            print(
518                "       and/or make sure the .dSYM bundles can be found by Spotlight."
519            )
520        return self.target
521
522    def get_target(self):
523        return self.target
524
525
526class CrashLogFormatException(Exception):
527    pass
528
529
530class CrashLogParseException(Exception):
531    pass
532
533
534class InteractiveCrashLogException(Exception):
535    pass
536
537
538class CrashLogParser:
539    @staticmethod
540    def create(debugger, path, options):
541        data = JSONCrashLogParser.is_valid_json(path)
542        if data:
543            parser = JSONCrashLogParser(debugger, path, options)
544            parser.data = data
545            return parser
546        else:
547            return TextCrashLogParser(debugger, path, options)
548
549    def __init__(self, debugger, path, options):
550        self.path = os.path.expanduser(path)
551        self.options = options
552        self.crashlog = CrashLog(debugger, self.path, self.options.verbose)
553
554    @abc.abstractmethod
555    def parse(self):
556        pass
557
558
559class JSONCrashLogParser(CrashLogParser):
560    @staticmethod
561    def is_valid_json(path):
562        def parse_json(buffer):
563            try:
564                return json.loads(buffer)
565            except:
566                # The first line can contain meta data. Try stripping it and
567                # try again.
568                head, _, tail = buffer.partition("\n")
569                return json.loads(tail)
570
571        with open(path, "r", encoding="utf-8") as f:
572            buffer = f.read()
573        try:
574            return parse_json(buffer)
575        except:
576            return None
577
578    def __init__(self, debugger, path, options):
579        super().__init__(debugger, path, options)
580
581    def parse(self):
582        try:
583            self.parse_process_info(self.data)
584            self.parse_images(self.data["usedImages"])
585            self.parse_main_image(self.data)
586            self.parse_threads(self.data["threads"])
587            if "asi" in self.data:
588                self.crashlog.asi = self.data["asi"]
589            # FIXME: With the current design, we can either show the ASI or Last
590            # Exception Backtrace, not both. Is there a situation where we would
591            # like to show both ?
592            if "asiBacktraces" in self.data:
593                self.parse_app_specific_backtraces(self.data["asiBacktraces"])
594            if "lastExceptionBacktrace" in self.data:
595                self.parse_last_exception_backtraces(
596                    self.data["lastExceptionBacktrace"]
597                )
598            self.parse_errors(self.data)
599            thread = self.crashlog.threads[self.crashlog.crashed_thread_idx]
600            reason = self.parse_crash_reason(self.data["exception"])
601            if thread.reason:
602                thread.reason = "{} {}".format(thread.reason, reason)
603            else:
604                thread.reason = reason
605        except (KeyError, ValueError, TypeError) as e:
606            raise CrashLogParseException(
607                "Failed to parse JSON crashlog: {}: {}".format(type(e).__name__, e)
608            )
609
610        return self.crashlog
611
612    def get_used_image(self, idx):
613        return self.data["usedImages"][idx]
614
615    def parse_process_info(self, json_data):
616        self.crashlog.process_id = json_data["pid"]
617        self.crashlog.process_identifier = json_data["procName"]
618        if "procPath" in json_data:
619            self.crashlog.process_path = json_data["procPath"]
620
621    def parse_crash_reason(self, json_exception):
622        self.crashlog.exception = json_exception
623        exception_type = json_exception["type"]
624        exception_signal = " "
625        if "signal" in json_exception:
626            exception_signal += "({})".format(json_exception["signal"])
627
628        if "codes" in json_exception:
629            exception_extra = " ({})".format(json_exception["codes"])
630        elif "subtype" in json_exception:
631            exception_extra = " ({})".format(json_exception["subtype"])
632        else:
633            exception_extra = ""
634        return "{}{}{}".format(exception_type, exception_signal, exception_extra)
635
636    def parse_images(self, json_images):
637        for json_image in json_images:
638            img_uuid = uuid.UUID(json_image["uuid"])
639            low = int(json_image["base"])
640            high = low + int(json_image["size"]) if "size" in json_image else low
641            name = json_image["name"] if "name" in json_image else ""
642            path = json_image["path"] if "path" in json_image else ""
643            version = ""
644            darwin_image = self.crashlog.DarwinImage(
645                low, high, name, version, img_uuid, path, self.options.verbose
646            )
647            if "arch" in json_image:
648                darwin_image.arch = json_image["arch"]
649                if path == self.crashlog.process_path:
650                    self.crashlog.process_arch = darwin_image.arch
651            self.crashlog.images.append(darwin_image)
652
653    def parse_main_image(self, json_data):
654        if "procName" in json_data:
655            proc_name = json_data["procName"]
656            self.crashlog.set_main_image(proc_name)
657
658    def parse_frames(self, thread, json_frames):
659        idx = 0
660        for json_frame in json_frames:
661            image_id = int(json_frame["imageIndex"])
662            json_image = self.get_used_image(image_id)
663            ident = json_image["name"] if "name" in json_image else ""
664            thread.add_ident(ident)
665            if ident not in self.crashlog.idents:
666                self.crashlog.idents.append(ident)
667
668            frame_offset = int(json_frame["imageOffset"])
669            image_addr = self.get_used_image(image_id)["base"]
670            pc = image_addr + frame_offset
671
672            if "symbol" in json_frame:
673                symbol = json_frame["symbol"]
674                location = 0
675                if "symbolLocation" in json_frame and json_frame["symbolLocation"]:
676                    location = int(json_frame["symbolLocation"])
677                image = self.crashlog.images[image_id]
678                image.symbols[symbol] = {
679                    "name": symbol,
680                    "type": "code",
681                    "address": frame_offset - location,
682                }
683
684            thread.frames.append(self.crashlog.Frame(idx, pc, frame_offset))
685
686            # on arm64 systems, if it jump through a null function pointer,
687            # we end up at address 0 and the crash reporter unwinder
688            # misses the frame that actually faulted.
689            # But $lr can tell us where the last BL/BLR instruction used
690            # was at, so insert that address as the caller stack frame.
691            if idx == 0 and pc == 0 and "lr" in thread.registers:
692                pc = thread.registers["lr"]
693                for image in self.data["usedImages"]:
694                    text_lo = image["base"]
695                    text_hi = text_lo + image["size"]
696                    if text_lo <= pc < text_hi:
697                        idx += 1
698                        frame_offset = pc - text_lo
699                        thread.frames.append(self.crashlog.Frame(idx, pc, frame_offset))
700                        break
701
702            idx += 1
703
704    def parse_threads(self, json_threads):
705        idx = 0
706        for json_thread in json_threads:
707            thread = self.crashlog.Thread(idx, False, self.crashlog.process_arch)
708            if "name" in json_thread:
709                thread.name = json_thread["name"]
710                thread.reason = json_thread["name"]
711            if "id" in json_thread:
712                thread.id = int(json_thread["id"])
713            if json_thread.get("triggered", False):
714                self.crashlog.crashed_thread_idx = idx
715                thread.crashed = True
716                if "threadState" in json_thread:
717                    thread.registers = self.parse_thread_registers(
718                        json_thread["threadState"]
719                    )
720            if "queue" in json_thread:
721                thread.queue = json_thread.get("queue")
722            self.parse_frames(thread, json_thread.get("frames", []))
723            self.crashlog.threads.append(thread)
724            idx += 1
725
726    def parse_asi_backtrace(self, thread, bt):
727        for line in bt.split("\n"):
728            frame_match = TextCrashLogParser.frame_regex.search(line)
729            if not frame_match:
730                print("error: can't parse application specific backtrace.")
731                return False
732
733            frame_id = (
734                frame_img_name
735            ) = (
736                frame_addr
737            ) = (
738                frame_symbol
739            ) = frame_offset = frame_file = frame_line = frame_column = None
740
741            if len(frame_match.groups()) == 3:
742                # Get the image UUID from the frame image name.
743                (frame_id, frame_img_name, frame_addr) = frame_match.groups()
744            elif len(frame_match.groups()) == 5:
745                (
746                    frame_id,
747                    frame_img_name,
748                    frame_addr,
749                    frame_symbol,
750                    frame_offset,
751                ) = frame_match.groups()
752            elif len(frame_match.groups()) == 7:
753                (
754                    frame_id,
755                    frame_img_name,
756                    frame_addr,
757                    frame_symbol,
758                    frame_offset,
759                    frame_file,
760                    frame_line,
761                ) = frame_match.groups()
762            elif len(frame_match.groups()) == 8:
763                (
764                    frame_id,
765                    frame_img_name,
766                    frame_addr,
767                    frame_symbol,
768                    frame_offset,
769                    frame_file,
770                    frame_line,
771                    frame_column,
772                ) = frame_match.groups()
773
774            thread.add_ident(frame_img_name)
775            if frame_img_name not in self.crashlog.idents:
776                self.crashlog.idents.append(frame_img_name)
777
778            description = ""
779            if frame_img_name and frame_addr and frame_symbol:
780                description = frame_symbol
781                frame_offset_value = 0
782                if frame_offset:
783                    description += " + " + frame_offset
784                    frame_offset_value = int(frame_offset, 0)
785                for image in self.crashlog.images:
786                    if image.identifier == frame_img_name:
787                        image.symbols[frame_symbol] = {
788                            "name": frame_symbol,
789                            "type": "code",
790                            "address": int(frame_addr, 0) - frame_offset_value,
791                        }
792
793            thread.frames.append(
794                self.crashlog.Frame(int(frame_id), int(frame_addr, 0), description)
795            )
796
797        return True
798
799    def parse_app_specific_backtraces(self, json_app_specific_bts):
800        thread = self.crashlog.Thread(
801            len(self.crashlog.threads), True, self.crashlog.process_arch
802        )
803        thread.queue = "Application Specific Backtrace"
804        if self.parse_asi_backtrace(thread, json_app_specific_bts[0]):
805            self.crashlog.threads.append(thread)
806        else:
807            print("error: Couldn't parse Application Specific Backtrace.")
808
809    def parse_last_exception_backtraces(self, json_last_exc_bts):
810        thread = self.crashlog.Thread(
811            len(self.crashlog.threads), True, self.crashlog.process_arch
812        )
813        thread.queue = "Last Exception Backtrace"
814        self.parse_frames(thread, json_last_exc_bts)
815        self.crashlog.threads.append(thread)
816
817    def parse_thread_registers(self, json_thread_state, prefix=None):
818        registers = dict()
819        for key, state in json_thread_state.items():
820            if key == "rosetta":
821                registers.update(self.parse_thread_registers(state))
822                continue
823            if key == "x":
824                gpr_dict = {str(idx): reg for idx, reg in enumerate(state)}
825                registers.update(self.parse_thread_registers(gpr_dict, key))
826                continue
827            if key == "flavor":
828                if not self.crashlog.process_arch:
829                    if state == "ARM_THREAD_STATE64":
830                        self.crashlog.process_arch = "arm64"
831                    elif state == "X86_THREAD_STATE":
832                        self.crashlog.process_arch = "x86_64"
833                continue
834            try:
835                value = int(state["value"])
836                registers["{}{}".format(prefix or "", key)] = value
837            except (KeyError, ValueError, TypeError):
838                pass
839        return registers
840
841    def parse_errors(self, json_data):
842        if "reportNotes" in json_data:
843            self.crashlog.errors = json_data["reportNotes"]
844
845
846class TextCrashLogParser(CrashLogParser):
847    parent_process_regex = re.compile(r"^Parent Process:\s*(.*)\[(\d+)\]")
848    thread_state_regex = re.compile(r"^Thread \d+ crashed with")
849    thread_instrs_regex = re.compile(r"^Thread \d+ instruction stream")
850    thread_regex = re.compile(r"^Thread (\d+).*:")
851    app_backtrace_regex = re.compile(r"^Application Specific Backtrace (\d+).*:")
852
853    class VersionRegex:
854        version = r"\(.+\)|(?:arm|x86_)[0-9a-z]+"
855
856    class FrameRegex(VersionRegex):
857        @classmethod
858        def get(cls):
859            index = r"^(\d+)\s+"
860            img_name = r"(.+?)\s+"
861            version = r"(?:" + super().version + r"\s+)?"
862            address = r"(0x[0-9a-fA-F]{4,})"  # 4 digits or more
863
864            symbol = """
865                        (?:
866                            [ ]+
867                            (?P<symbol>.+)
868                            (?:
869                                [ ]\+[ ]
870                                (?P<symbol_offset>\d+)
871                            )
872                            (?:
873                                [ ]\(
874                                (?P<file_name>[^:]+):(?P<line_number>\d+)
875                                (?:
876                                    :(?P<column_num>\d+)
877                                )?
878                            )?
879                        )?
880                       """
881
882            return re.compile(
883                index + img_name + version + address + symbol, flags=re.VERBOSE
884            )
885
886    frame_regex = FrameRegex.get()
887    null_frame_regex = re.compile(r"^\d+\s+\?\?\?\s+0{4,} +")
888    image_regex_uuid = re.compile(
889        r"(0x[0-9a-fA-F]+)"  # img_lo
890        r"\s+-\s+"  #   -
891        r"(0x[0-9a-fA-F]+)\s+"  # img_hi
892        r"[+]?(.+?)\s+"  # img_name
893        r"(?:(" + VersionRegex.version + r")\s+)?"  # img_version
894        r"(?:<([-0-9a-fA-F]+)>\s+)?"  # img_uuid
895        r"(\?+|/.*)"  # img_path
896    )
897    exception_type_regex = re.compile(
898        r"^Exception Type:\s+(EXC_[A-Z_]+)(?:\s+\((.*)\))?"
899    )
900    exception_codes_regex = re.compile(
901        r"^Exception Codes:\s+(0x[0-9a-fA-F]+),\s*(0x[0-9a-fA-F]+)"
902    )
903    exception_extra_regex = re.compile(r"^Exception\s+.*:\s+(.*)")
904
905    class CrashLogParseMode:
906        NORMAL = 0
907        THREAD = 1
908        IMAGES = 2
909        THREGS = 3
910        SYSTEM = 4
911        INSTRS = 5
912
913    def __init__(self, debugger, path, options):
914        super().__init__(debugger, path, options)
915        self.thread = None
916        self.app_specific_backtrace = False
917        self.parse_mode = self.CrashLogParseMode.NORMAL
918        self.parsers = {
919            self.CrashLogParseMode.NORMAL: self.parse_normal,
920            self.CrashLogParseMode.THREAD: self.parse_thread,
921            self.CrashLogParseMode.IMAGES: self.parse_images,
922            self.CrashLogParseMode.THREGS: self.parse_thread_registers,
923            self.CrashLogParseMode.SYSTEM: self.parse_system,
924            self.CrashLogParseMode.INSTRS: self.parse_instructions,
925        }
926        self.symbols = {}
927
928    def parse(self):
929        with open(self.path, "r", encoding="utf-8") as f:
930            lines = f.read().splitlines()
931
932        idx = 0
933        lines_count = len(lines)
934        while True:
935            if idx >= lines_count:
936                break
937
938            line = lines[idx]
939            line_len = len(line)
940
941            if line_len == 0:
942                if self.thread:
943                    if self.parse_mode == self.CrashLogParseMode.THREAD:
944                        if self.thread.index == self.crashlog.crashed_thread_idx:
945                            self.thread.reason = ""
946                            if hasattr(self.crashlog, "thread_exception"):
947                                self.thread.reason += self.crashlog.thread_exception
948                            if hasattr(self.crashlog, "thread_exception_data"):
949                                self.thread.reason += (
950                                    " (%s)" % self.crashlog.thread_exception_data
951                                )
952                        if self.app_specific_backtrace:
953                            self.crashlog.backtraces.append(self.thread)
954                        else:
955                            self.crashlog.threads.append(self.thread)
956                    self.thread = None
957
958                empty_lines = 1
959                while (
960                    idx + empty_lines < lines_count
961                    and len(lines[idx + empty_lines]) == 0
962                ):
963                    empty_lines = empty_lines + 1
964
965                if (
966                    empty_lines == 1
967                    and idx + empty_lines < lines_count - 1
968                    and self.parse_mode != self.CrashLogParseMode.NORMAL
969                ):
970                    # check if next line can be parsed with the current parse mode
971                    next_line_idx = idx + empty_lines
972                    if self.parsers[self.parse_mode](lines[next_line_idx]):
973                        # If that suceeded, skip the empty line and the next line.
974                        idx = next_line_idx + 1
975                        continue
976                self.parse_mode = self.CrashLogParseMode.NORMAL
977
978            self.parsers[self.parse_mode](line)
979
980            idx = idx + 1
981
982        return self.crashlog
983
984    def parse_exception(self, line):
985        if not line.startswith("Exception"):
986            return False
987        if line.startswith("Exception Type:"):
988            self.crashlog.thread_exception = line[15:].strip()
989            exception_type_match = self.exception_type_regex.search(line)
990            if exception_type_match:
991                exc_type, exc_signal = exception_type_match.groups()
992                self.crashlog.exception["type"] = exc_type
993                if exc_signal:
994                    self.crashlog.exception["signal"] = exc_signal
995        elif line.startswith("Exception Subtype:"):
996            self.crashlog.thread_exception_subtype = line[18:].strip()
997            if "type" in self.crashlog.exception:
998                self.crashlog.exception[
999                    "subtype"
1000                ] = self.crashlog.thread_exception_subtype
1001        elif line.startswith("Exception Codes:"):
1002            self.crashlog.thread_exception_data = line[16:].strip()
1003            if "type" not in self.crashlog.exception:
1004                return False
1005            exception_codes_match = self.exception_codes_regex.search(line)
1006            if exception_codes_match:
1007                self.crashlog.exception["codes"] = self.crashlog.thread_exception_data
1008                code, subcode = exception_codes_match.groups()
1009                self.crashlog.exception["rawCodes"] = [
1010                    int(code, base=16),
1011                    int(subcode, base=16),
1012                ]
1013        else:
1014            if "type" not in self.crashlog.exception:
1015                return False
1016            exception_extra_match = self.exception_extra_regex.search(line)
1017            if exception_extra_match:
1018                self.crashlog.exception["message"] = exception_extra_match.group(1)
1019        return True
1020
1021    def parse_normal(self, line):
1022        if line.startswith("Process:"):
1023            (self.crashlog.process_name, pid_with_brackets) = (
1024                line[8:].strip().split(" [")
1025            )
1026            self.crashlog.process_id = pid_with_brackets.strip("[]")
1027        elif line.startswith("Path:"):
1028            self.crashlog.process_path = line[5:].strip()
1029        elif line.startswith("Identifier:"):
1030            self.crashlog.process_identifier = line[11:].strip()
1031        elif line.startswith("Version:"):
1032            version_string = line[8:].strip()
1033            matched_pair = re.search("(.+)\((.+)\)", version_string)
1034            if matched_pair:
1035                self.crashlog.process_version = matched_pair.group(1)
1036                self.crashlog.process_compatability_version = matched_pair.group(2)
1037            else:
1038                self.crashlog.process = version_string
1039                self.crashlog.process_compatability_version = version_string
1040        elif line.startswith("Code Type:"):
1041            if "ARM-64" in line:
1042                self.crashlog.process_arch = "arm64"
1043            elif "X86-64" in line:
1044                self.crashlog.process_arch = "x86_64"
1045        elif self.parent_process_regex.search(line):
1046            parent_process_match = self.parent_process_regex.search(line)
1047            self.crashlog.parent_process_name = parent_process_match.group(1)
1048            self.crashlog.parent_process_id = parent_process_match.group(2)
1049        elif line.startswith("Exception"):
1050            self.parse_exception(line)
1051            return
1052        elif line.startswith("Crashed Thread:"):
1053            self.crashlog.crashed_thread_idx = int(line[15:].strip().split()[0])
1054            return
1055        elif line.startswith("Triggered by Thread:"):  # iOS
1056            self.crashlog.crashed_thread_idx = int(line[20:].strip().split()[0])
1057            return
1058        elif line.startswith("Report Version:"):
1059            self.crashlog.version = int(line[15:].strip())
1060            return
1061        elif line.startswith("System Profile:"):
1062            self.parse_mode = self.CrashLogParseMode.SYSTEM
1063            return
1064        elif (
1065            line.startswith("Interval Since Last Report:")
1066            or line.startswith("Crashes Since Last Report:")
1067            or line.startswith("Per-App Interval Since Last Report:")
1068            or line.startswith("Per-App Crashes Since Last Report:")
1069            or line.startswith("Sleep/Wake UUID:")
1070            or line.startswith("Anonymous UUID:")
1071        ):
1072            # ignore these
1073            return
1074        elif line.startswith("Thread"):
1075            thread_state_match = self.thread_state_regex.search(line)
1076            if thread_state_match:
1077                self.app_specific_backtrace = False
1078                thread_state_match = self.thread_regex.search(line)
1079                thread_idx = int(thread_state_match.group(1))
1080                self.parse_mode = self.CrashLogParseMode.THREGS
1081                self.thread = self.crashlog.threads[thread_idx]
1082                return
1083            thread_insts_match = self.thread_instrs_regex.search(line)
1084            if thread_insts_match:
1085                self.parse_mode = self.CrashLogParseMode.INSTRS
1086                return
1087            thread_match = self.thread_regex.search(line)
1088            if thread_match:
1089                self.app_specific_backtrace = False
1090                self.parse_mode = self.CrashLogParseMode.THREAD
1091                thread_idx = int(thread_match.group(1))
1092                self.thread = self.crashlog.Thread(
1093                    thread_idx, False, self.crashlog.process_arch
1094                )
1095                return
1096            return
1097        elif line.startswith("Binary Images:"):
1098            self.parse_mode = self.CrashLogParseMode.IMAGES
1099            return
1100        elif line.startswith("Application Specific Backtrace"):
1101            app_backtrace_match = self.app_backtrace_regex.search(line)
1102            if app_backtrace_match:
1103                self.parse_mode = self.CrashLogParseMode.THREAD
1104                self.app_specific_backtrace = True
1105                idx = int(app_backtrace_match.group(1))
1106                self.thread = self.crashlog.Thread(
1107                    idx, True, self.crashlog.process_arch
1108                )
1109        elif line.startswith("Last Exception Backtrace:"):  # iOS
1110            self.parse_mode = self.CrashLogParseMode.THREAD
1111            self.app_specific_backtrace = True
1112            idx = 1
1113            self.thread = self.crashlog.Thread(idx, True, self.crashlog.process_arch)
1114        self.crashlog.info_lines.append(line.strip())
1115
1116    def parse_thread(self, line):
1117        if line.startswith("Thread"):
1118            return False
1119        if self.null_frame_regex.search(line):
1120            print('warning: thread parser ignored null-frame: "%s"' % line)
1121            return False
1122        frame_match = self.frame_regex.search(line)
1123        if not frame_match:
1124            print('error: frame regex failed for line: "%s"' % line)
1125            return False
1126
1127        frame_id = (
1128            frame_img_name
1129        ) = (
1130            frame_addr
1131        ) = frame_symbol = frame_offset = frame_file = frame_line = frame_column = None
1132
1133        if len(frame_match.groups()) == 3:
1134            # Get the image UUID from the frame image name.
1135            (frame_id, frame_img_name, frame_addr) = frame_match.groups()
1136        elif len(frame_match.groups()) == 5:
1137            (
1138                frame_id,
1139                frame_img_name,
1140                frame_addr,
1141                frame_symbol,
1142                frame_offset,
1143            ) = frame_match.groups()
1144        elif len(frame_match.groups()) == 7:
1145            (
1146                frame_id,
1147                frame_img_name,
1148                frame_addr,
1149                frame_symbol,
1150                frame_offset,
1151                frame_file,
1152                frame_line,
1153            ) = frame_match.groups()
1154        elif len(frame_match.groups()) == 8:
1155            (
1156                frame_id,
1157                frame_img_name,
1158                frame_addr,
1159                frame_symbol,
1160                frame_offset,
1161                frame_file,
1162                frame_line,
1163                frame_column,
1164            ) = frame_match.groups()
1165
1166        self.thread.add_ident(frame_img_name)
1167        if frame_img_name not in self.crashlog.idents:
1168            self.crashlog.idents.append(frame_img_name)
1169
1170        description = ""
1171        # Since images are parsed after threads, we need to build a
1172        # map for every image with a list of all the symbols and addresses
1173        if frame_img_name and frame_addr and frame_symbol:
1174            description = frame_symbol
1175            frame_offset_value = 0
1176            if frame_offset:
1177                description += " + " + frame_offset
1178                frame_offset_value = int(frame_offset, 0)
1179            if frame_img_name not in self.symbols:
1180                self.symbols[frame_img_name] = list()
1181            self.symbols[frame_img_name].append(
1182                {
1183                    "name": frame_symbol,
1184                    "address": int(frame_addr, 0) - frame_offset_value,
1185                }
1186            )
1187
1188        self.thread.frames.append(
1189            self.crashlog.Frame(int(frame_id), int(frame_addr, 0), description)
1190        )
1191
1192        return True
1193
1194    def parse_images(self, line):
1195        image_match = self.image_regex_uuid.search(line)
1196        if image_match:
1197            (
1198                img_lo,
1199                img_hi,
1200                img_name,
1201                img_version,
1202                img_uuid,
1203                img_path,
1204            ) = image_match.groups()
1205
1206            image = self.crashlog.DarwinImage(
1207                int(img_lo, 0),
1208                int(img_hi, 0),
1209                img_name.strip(),
1210                img_version.strip() if img_version else "",
1211                uuid.UUID(img_uuid),
1212                img_path,
1213                self.options.verbose,
1214            )
1215            unqualified_img_name = os.path.basename(img_path)
1216            if unqualified_img_name in self.symbols:
1217                for symbol in self.symbols[unqualified_img_name]:
1218                    image.symbols[symbol["name"]] = {
1219                        "name": symbol["name"],
1220                        "type": "code",
1221                        # NOTE: "address" is actually the symbol image offset
1222                        "address": symbol["address"] - int(img_lo, 0),
1223                    }
1224
1225            self.crashlog.images.append(image)
1226            return True
1227        else:
1228            if self.options.debug:
1229                print("error: image regex failed for: %s" % line)
1230            return False
1231
1232    def parse_thread_registers(self, line):
1233        # "r12: 0x00007fff6b5939c8  r13: 0x0000000007000006  r14: 0x0000000000002a03  r15: 0x0000000000000c00"
1234        reg_values = re.findall("([a-z0-9]+): (0x[0-9a-f]+)", line, re.I)
1235        for reg, value in reg_values:
1236            self.thread.registers[reg] = int(value, 16)
1237        return len(reg_values) != 0
1238
1239    def parse_system(self, line):
1240        self.crashlog.system_profile.append(line)
1241        return True
1242
1243    def parse_instructions(self, line):
1244        pass
1245
1246
1247def save_crashlog(debugger, command, exe_ctx, result, dict):
1248    usage = "save_crashlog [options] <output-path>"
1249    description = """Export the state of current target into a crashlog file"""
1250    parser = argparse.ArgumentParser(
1251        description=description,
1252        prog="save_crashlog",
1253        formatter_class=argparse.ArgumentDefaultsHelpFormatter,
1254    )
1255    parser.add_argument(
1256        "output",
1257        metavar="output-file",
1258        type=argparse.FileType("w", encoding="utf-8"),
1259        nargs=1,
1260    )
1261    parser.add_argument(
1262        "-v",
1263        "--verbose",
1264        action="store_true",
1265        dest="verbose",
1266        help="display verbose debug info",
1267        default=False,
1268    )
1269    try:
1270        options = parser.parse_args(shlex.split(command))
1271    except Exception as e:
1272        result.SetError(str(e))
1273        return
1274    target = exe_ctx.target
1275    if target:
1276        out_file = options.output
1277        identifier = target.executable.basename
1278        process = exe_ctx.process
1279        if process:
1280            pid = process.id
1281            if pid != lldb.LLDB_INVALID_PROCESS_ID:
1282                out_file.write("Process:         %s [%u]\n" % (identifier, pid))
1283        out_file.write("Path:            %s\n" % (target.executable.fullpath))
1284        out_file.write("Identifier:      %s\n" % (identifier))
1285        out_file.write(
1286            "\nDate/Time:       %s\n"
1287            % (datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S"))
1288        )
1289        out_file.write(
1290            "OS Version:      Mac OS X %s (%s)\n"
1291            % (
1292                platform.mac_ver()[0],
1293                subprocess.check_output("sysctl -n kern.osversion", shell=True).decode(
1294                    "utf-8"
1295                ),
1296            )
1297        )
1298        out_file.write("Report Version:  9\n")
1299        for thread_idx in range(process.num_threads):
1300            thread = process.thread[thread_idx]
1301            out_file.write("\nThread %u:\n" % (thread_idx))
1302            for frame_idx, frame in enumerate(thread.frames):
1303                frame_pc = frame.pc
1304                frame_offset = 0
1305                if frame.function:
1306                    block = frame.GetFrameBlock()
1307                    block_range = block.range[frame.addr]
1308                    if block_range:
1309                        block_start_addr = block_range[0]
1310                        frame_offset = frame_pc - block_start_addr.GetLoadAddress(
1311                            target
1312                        )
1313                    else:
1314                        frame_offset = frame_pc - frame.function.addr.GetLoadAddress(
1315                            target
1316                        )
1317                elif frame.symbol:
1318                    frame_offset = frame_pc - frame.symbol.addr.GetLoadAddress(target)
1319                out_file.write(
1320                    "%-3u %-32s 0x%16.16x %s"
1321                    % (frame_idx, frame.module.file.basename, frame_pc, frame.name)
1322                )
1323                if frame_offset > 0:
1324                    out_file.write(" + %u" % (frame_offset))
1325                line_entry = frame.line_entry
1326                if line_entry:
1327                    if options.verbose:
1328                        # This will output the fullpath + line + column
1329                        out_file.write(" %s" % (line_entry))
1330                    else:
1331                        out_file.write(
1332                            " %s:%u" % (line_entry.file.basename, line_entry.line)
1333                        )
1334                        column = line_entry.column
1335                        if column:
1336                            out_file.write(":%u" % (column))
1337                out_file.write("\n")
1338
1339        out_file.write("\nBinary Images:\n")
1340        for module in target.modules:
1341            text_segment = module.section["__TEXT"]
1342            if text_segment:
1343                text_segment_load_addr = text_segment.GetLoadAddress(target)
1344                if text_segment_load_addr != lldb.LLDB_INVALID_ADDRESS:
1345                    text_segment_end_load_addr = (
1346                        text_segment_load_addr + text_segment.size
1347                    )
1348                    identifier = module.file.basename
1349                    module_version = "???"
1350                    module_version_array = module.GetVersion()
1351                    if module_version_array:
1352                        module_version = ".".join(map(str, module_version_array))
1353                    out_file.write(
1354                        "    0x%16.16x - 0x%16.16x  %s (%s - ???) <%s> %s\n"
1355                        % (
1356                            text_segment_load_addr,
1357                            text_segment_end_load_addr,
1358                            identifier,
1359                            module_version,
1360                            module.GetUUIDString(),
1361                            module.file.fullpath,
1362                        )
1363                    )
1364        out_file.close()
1365    else:
1366        result.SetError("invalid target")
1367
1368
1369class Symbolicate:
1370    def __init__(self, debugger, internal_dict):
1371        pass
1372
1373    def __call__(self, debugger, command, exe_ctx, result):
1374        SymbolicateCrashLogs(debugger, shlex.split(command), result, True)
1375
1376    def get_short_help(self):
1377        return "Symbolicate one or more darwin crash log files."
1378
1379    def get_long_help(self):
1380        arg_parser = CrashLogOptionParser()
1381        return arg_parser.format_help()
1382
1383
1384def SymbolicateCrashLog(crash_log, options):
1385    if options.debug:
1386        crash_log.dump()
1387    if not crash_log.images:
1388        print("error: no images in crash log")
1389        return
1390
1391    if options.dump_image_list:
1392        print("Binary Images:")
1393        for image in crash_log.images:
1394            if options.verbose:
1395                print(image.debug_dump())
1396            else:
1397                print(image)
1398
1399    target = crash_log.create_target()
1400    if not target:
1401        return
1402
1403    if options.load_all_images:
1404        for image in crash_log.images:
1405            image.resolve = True
1406    elif options.crashed_only:
1407        for thread in crash_log.threads:
1408            if thread.did_crash():
1409                for ident in thread.idents:
1410                    for image in crash_log.find_images_with_identifier(ident):
1411                        image.resolve = True
1412
1413    futures = []
1414    loaded_images = []
1415    with tempfile.TemporaryDirectory() as obj_dir:
1416        with concurrent.futures.ThreadPoolExecutor() as executor:
1417
1418            def add_module(image, target, obj_dir):
1419                return image, image.add_module(target, obj_dir)
1420
1421            for image in crash_log.images:
1422                futures.append(
1423                    executor.submit(
1424                        add_module, image=image, target=target, obj_dir=obj_dir
1425                    )
1426                )
1427            for future in concurrent.futures.as_completed(futures):
1428                image, err = future.result()
1429                if err:
1430                    print(err)
1431                else:
1432                    loaded_images.append(image)
1433
1434    if crash_log.backtraces:
1435        for thread in crash_log.backtraces:
1436            thread.dump_symbolicated(crash_log, options)
1437            print()
1438
1439    for thread in crash_log.threads:
1440        thread.dump_symbolicated(crash_log, options)
1441        print()
1442
1443    if crash_log.errors:
1444        print("Errors:")
1445        for error in crash_log.errors:
1446            print(error)
1447
1448
1449def load_crashlog_in_scripted_process(debugger, crashlog_path, options, result):
1450    crashlog = CrashLogParser.create(debugger, crashlog_path, options).parse()
1451
1452    target = lldb.SBTarget()
1453    # 1. Try to use the user-provided target
1454    if options.target_path:
1455        target = debugger.CreateTarget(options.target_path)
1456        if not target:
1457            raise InteractiveCrashLogException(
1458                "couldn't create target provided by the user (%s)" % options.target_path
1459            )
1460
1461    # 2. If the user didn't provide a target, try to create a target using the symbolicator
1462    if not target or not target.IsValid():
1463        target = crashlog.create_target()
1464    # 3. If that didn't work, create a dummy target
1465    if target is None or not target.IsValid():
1466        arch = crashlog.process_arch
1467        if not arch:
1468            raise InteractiveCrashLogException(
1469                "couldn't create find the architecture to create the target"
1470            )
1471        target = debugger.CreateTargetWithFileAndArch(None, arch)
1472    # 4. Fail
1473    if target is None or not target.IsValid():
1474        raise InteractiveCrashLogException("couldn't create target")
1475
1476    ci = debugger.GetCommandInterpreter()
1477    if not ci:
1478        raise InteractiveCrashLogException("couldn't get command interpreter")
1479
1480    ci.HandleCommand("script from lldb.macosx import crashlog_scripted_process", result)
1481    if not result.Succeeded():
1482        raise InteractiveCrashLogException(
1483            "couldn't import crashlog scripted process module"
1484        )
1485
1486    structured_data = lldb.SBStructuredData()
1487    structured_data.SetFromJSON(
1488        json.dumps(
1489            {"file_path": crashlog_path, "load_all_images": options.load_all_images}
1490        )
1491    )
1492    launch_info = lldb.SBLaunchInfo(None)
1493    launch_info.SetProcessPluginName("ScriptedProcess")
1494    launch_info.SetScriptedProcessClassName(
1495        "crashlog_scripted_process.CrashLogScriptedProcess"
1496    )
1497    launch_info.SetScriptedProcessDictionary(structured_data)
1498    launch_info.SetLaunchFlags(lldb.eLaunchFlagStopAtEntry)
1499
1500    error = lldb.SBError()
1501    process = target.Launch(launch_info, error)
1502
1503    if not process or error.Fail():
1504        raise InteractiveCrashLogException("couldn't launch Scripted Process", error)
1505
1506    process.GetScriptedImplementation().set_crashlog(crashlog)
1507    process.Continue()
1508
1509    if not options.skip_status:
1510
1511        @contextlib.contextmanager
1512        def synchronous(debugger):
1513            async_state = debugger.GetAsync()
1514            debugger.SetAsync(False)
1515            try:
1516                yield
1517            finally:
1518                debugger.SetAsync(async_state)
1519
1520        with synchronous(debugger):
1521            run_options = lldb.SBCommandInterpreterRunOptions()
1522            run_options.SetStopOnError(True)
1523            run_options.SetStopOnCrash(True)
1524            run_options.SetEchoCommands(True)
1525
1526            commands_stream = lldb.SBStream()
1527            commands_stream.Print("process status --verbose\n")
1528            commands_stream.Print("thread backtrace --extended true\n")
1529            error = debugger.SetInputString(commands_stream.GetData())
1530            if error.Success():
1531                debugger.RunCommandInterpreter(True, False, run_options, 0, False, True)
1532
1533
1534def CreateSymbolicateCrashLogOptions(
1535    command_name, description, add_interactive_options
1536):
1537    usage = "crashlog [options] <FILE> [FILE ...]"
1538    arg_parser = argparse.ArgumentParser(
1539        description=description,
1540        prog="crashlog",
1541        usage=usage,
1542        formatter_class=argparse.ArgumentDefaultsHelpFormatter,
1543    )
1544    arg_parser.add_argument(
1545        "reports",
1546        metavar="FILE",
1547        type=str,
1548        nargs="*",
1549        help="crash report(s) to symbolicate",
1550    )
1551
1552    arg_parser.add_argument(
1553        "--version",
1554        "-V",
1555        dest="version",
1556        action="store_true",
1557        help="Show crashlog version",
1558        default=False,
1559    )
1560    arg_parser.add_argument(
1561        "--verbose",
1562        "-v",
1563        action="store_true",
1564        dest="verbose",
1565        help="display verbose debug info",
1566        default=False,
1567    )
1568    arg_parser.add_argument(
1569        "--debug",
1570        "-g",
1571        action="store_true",
1572        dest="debug",
1573        help="display verbose debug logging",
1574        default=False,
1575    )
1576    arg_parser.add_argument(
1577        "--load-all",
1578        "-a",
1579        action="store_true",
1580        dest="load_all_images",
1581        help="load all executable images, not just the images found in the "
1582        "crashed stack frames, loads stackframes for all the threads in "
1583        "interactive mode.",
1584        default=False,
1585    )
1586    arg_parser.add_argument(
1587        "--images",
1588        action="store_true",
1589        dest="dump_image_list",
1590        help="show image list",
1591        default=False,
1592    )
1593    arg_parser.add_argument(
1594        "--debug-delay",
1595        type=int,
1596        dest="debug_delay",
1597        metavar="NSEC",
1598        help="pause for NSEC seconds for debugger",
1599        default=0,
1600    )
1601    arg_parser.add_argument(
1602        "--crashed-only",
1603        "-c",
1604        action=argparse.BooleanOptionalAction,
1605        dest="crashed_only",
1606        help="only symbolicate the crashed thread",
1607        default=True,
1608    )
1609    arg_parser.add_argument(
1610        "--disasm-depth",
1611        "-d",
1612        type=int,
1613        dest="disassemble_depth",
1614        help="set the depth in stack frames that should be disassembled",
1615        default=1,
1616    )
1617    arg_parser.add_argument(
1618        "--disasm-all",
1619        "-D",
1620        action="store_true",
1621        dest="disassemble_all_threads",
1622        help="enabled disassembly of frames on all threads (not just the crashed thread)",
1623        default=False,
1624    )
1625    arg_parser.add_argument(
1626        "--disasm-before",
1627        "-B",
1628        type=int,
1629        dest="disassemble_before",
1630        help="the number of instructions to disassemble before the frame PC",
1631        default=4,
1632    )
1633    arg_parser.add_argument(
1634        "--disasm-after",
1635        "-A",
1636        type=int,
1637        dest="disassemble_after",
1638        help="the number of instructions to disassemble after the frame PC",
1639        default=4,
1640    )
1641    arg_parser.add_argument(
1642        "--source-context",
1643        "-C",
1644        type=int,
1645        metavar="NLINES",
1646        dest="source_context",
1647        help="show NLINES source lines of source context",
1648        default=4,
1649    )
1650    arg_parser.add_argument(
1651        "--source-frames",
1652        type=int,
1653        metavar="NFRAMES",
1654        dest="source_frames",
1655        help="show source for NFRAMES",
1656        default=4,
1657    )
1658    arg_parser.add_argument(
1659        "--source-all",
1660        action="store_true",
1661        dest="source_all",
1662        help="show source for all threads, not just the crashed thread",
1663        default=False,
1664    )
1665    if add_interactive_options:
1666        arg_parser.add_argument(
1667            "-i",
1668            "--interactive",
1669            action="store_true",
1670            help="parse a crash log and load it in a ScriptedProcess",
1671            default=False,
1672        )
1673        arg_parser.add_argument(
1674            "-b",
1675            "--batch",
1676            action="store_true",
1677            help="dump symbolicated stackframes without creating a debug session",
1678            default=True,
1679        )
1680        arg_parser.add_argument(
1681            "--target",
1682            "-t",
1683            dest="target_path",
1684            help="the target binary path that should be used for interactive crashlog (optional)",
1685            default=None,
1686        )
1687        arg_parser.add_argument(
1688            "--skip-status",
1689            "-s",
1690            dest="skip_status",
1691            action="store_true",
1692            help="prevent the interactive crashlog to dump the process status and thread backtrace at launch",
1693            default=False,
1694        )
1695    return arg_parser
1696
1697
1698def CrashLogOptionParser():
1699    description = """Symbolicate one or more darwin crash log files to provide source file and line information,
1700inlined stack frames back to the concrete functions, and disassemble the location of the crash
1701for the first frame of the crashed thread.
1702If this script is imported into the LLDB command interpreter, a "crashlog" command will be added to the interpreter
1703for use at the LLDB command line. After a crash log has been parsed and symbolicated, a target will have been
1704created that has all of the shared libraries loaded at the load addresses found in the crash log file. This allows
1705you to explore the program as if it were stopped at the locations described in the crash log and functions can
1706be disassembled and lookups can be performed using the addresses found in the crash log."""
1707    return CreateSymbolicateCrashLogOptions("crashlog", description, True)
1708
1709
1710def SymbolicateCrashLogs(debugger, command_args, result, is_command):
1711    arg_parser = CrashLogOptionParser()
1712
1713    if not len(command_args):
1714        arg_parser.print_help()
1715        return
1716
1717    try:
1718        options = arg_parser.parse_args(command_args)
1719    except Exception as e:
1720        result.SetError(str(e))
1721        return
1722
1723    # Interactive mode requires running the crashlog command from inside lldb.
1724    if options.interactive and not is_command:
1725        lldb_exec = (
1726            subprocess.check_output(["/usr/bin/xcrun", "-f", "lldb"])
1727            .decode("utf-8")
1728            .strip()
1729        )
1730        sys.exit(
1731            os.execv(
1732                lldb_exec,
1733                [
1734                    lldb_exec,
1735                    "-o",
1736                    "command script import lldb.macosx",
1737                    "-o",
1738                    "crashlog {}".format(shlex.join(command_args)),
1739                ],
1740            )
1741        )
1742
1743    if options.version:
1744        print(debugger.GetVersionString())
1745        return
1746
1747    if options.debug:
1748        print("command_args = %s" % command_args)
1749        print("options", options)
1750        print("args", options.reports)
1751
1752    if options.debug_delay > 0:
1753        print("Waiting %u seconds for debugger to attach..." % options.debug_delay)
1754        time.sleep(options.debug_delay)
1755    error = lldb.SBError()
1756
1757    def should_run_in_interactive_mode(options, ci):
1758        if options.interactive:
1759            return True
1760        elif options.batch:
1761            return False
1762        # elif ci and ci.IsInteractive():
1763        #     return True
1764        else:
1765            return False
1766
1767    ci = debugger.GetCommandInterpreter()
1768
1769    if options.reports:
1770        for crashlog_file in options.reports:
1771            crashlog_path = os.path.expanduser(crashlog_file)
1772            if not os.path.exists(crashlog_path):
1773                raise FileNotFoundError(
1774                    "crashlog file %s does not exist" % crashlog_path
1775                )
1776            if should_run_in_interactive_mode(options, ci):
1777                try:
1778                    load_crashlog_in_scripted_process(
1779                        debugger, crashlog_path, options, result
1780                    )
1781                except InteractiveCrashLogException as e:
1782                    result.SetError(str(e))
1783            else:
1784                crash_log = CrashLogParser.create(
1785                    debugger, crashlog_path, options
1786                ).parse()
1787                SymbolicateCrashLog(crash_log, options)
1788
1789
1790if __name__ == "__main__":
1791    # Create a new debugger instance
1792    debugger = lldb.SBDebugger.Create()
1793    result = lldb.SBCommandReturnObject()
1794    SymbolicateCrashLogs(debugger, sys.argv[1:], result, False)
1795    lldb.SBDebugger.Destroy(debugger)
1796
1797
1798def __lldb_init_module(debugger, internal_dict):
1799    debugger.HandleCommand(
1800        "command script add -o -c lldb.macosx.crashlog.Symbolicate -C disk-file crashlog"
1801    )
1802    debugger.HandleCommand(
1803        "command script add -o -f lldb.macosx.crashlog.save_crashlog -C disk-file save_crashlog"
1804    )
1805    print(
1806        '"crashlog" and "save_crashlog" commands have been installed, use '
1807        'the "--help" options on these commands for detailed help.'
1808    )
1809