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