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