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