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