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