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