xref: /xnu-12377.81.4/tools/lldbmacros/xnu.py (revision 043036a2b3718f7f0be807e2870f8f47d3fa0796)
1import sys, os, re, time, getopt, shlex, inspect, xnudefines
2import lldb
3import uuid
4import base64
5import json
6from importlib import reload
7from importlib.util import find_spec
8from functools import wraps
9from ctypes import c_ulonglong as uint64_t
10from ctypes import c_void_p as voidptr_t
11import core
12from core import caching
13from core.standard import *
14from core.configuration import *
15from core.kernelcore import *
16from utils import *
17from core.lazytarget import *
18
19if os.environ.get('DEBUG_LLDB_PYTHON'):
20    import debugpy
21    debugpy.listen(5678, in_process_debug_adapter=True)
22    debugpy.wait_for_client()
23
24MODULE_NAME=__name__
25
26""" Kernel Debugging macros for lldb.
27    Please make sure you read the README COMPLETELY BEFORE reading anything below.
28    It is very critical that you read coding guidelines in Section E in README file.
29"""
30
31COMMON_HELP_STRING = """
32    -h  Show the help string for the command.
33    -c [always|auto|never|0|1]
34                            Control the colorized output of certain commands
35    -o <path/to/filename>   The output of this command execution will be saved to file. Parser information or errors will
36                            not be sent to file though. eg /tmp/output.txt
37    -s <filter_string>      The "filter_string" param is parsed to python regex expression and each line of output
38                            will be printed/saved only if it matches the expression.
39    -v [-v...]  Each additional -v will increase the verbosity of the command.
40    -p <plugin_name>        Send the output of the command to plugin. Please see README for usage of plugins.
41"""
42# End Utility functions
43# Debugging specific utility functions
44
45#decorators. Not to be called directly.
46
47def static_var(var_name, initial_value):
48    def _set_var(obj):
49        setattr(obj, var_name, initial_value)
50        return obj
51    return _set_var
52
53def header(initial_value):
54    def _set_header(obj):
55        setattr(obj, 'header', initial_value)
56        return obj
57    return _set_header
58
59def md_header(fmt, args):
60    def _set_md_header(obj):
61        header = "|" + "|".join(fmt.split(" ")).format(*args) + "|"
62
63        colhead = map(lambda fmt, col: "-"*len(fmt.format(col)), fmt.split(" "), args)
64        sub_header = "|" + "|".join(colhead) + "|"
65        setattr(obj, 'markdown', "\n".join([header, sub_header]))
66        return obj
67    return _set_md_header
68
69# holds type declarations done by xnu.
70#DONOTTOUCHME: Exclusive use of lldb_type_summary only.
71lldb_summary_definitions = {}
72def lldb_type_summary(types_list):
73    """ A function decorator to register a summary for a type in lldb.
74        params: types_list - [] an array of types that you wish to register a summary callback function. (ex. ['task *', 'task_t'])
75        returns: Nothing. This is a decorator.
76    """
77    def _get_summary(obj):
78        summary_function_name = "LLDBSummary" + obj.__name__
79
80        def _internal_summary_function(lldbval, internal_dict):
81            args, _, _, _ = inspect.getargspec(obj)
82            if 'O' in args:
83                stream = CommandOutput(summary_function_name, fhandle=sys.stdout)
84                with RedirectStdStreams(stdout=stream), caching.ImplicitContext(lldbval):
85                    return '\n' + obj.header + '\n' + obj(core.value(lldbval), O=stream)
86
87            out_string = ""
88            if internal_dict != None and len(obj.header) > 0 :
89                out_string += "\n" + obj.header +"\n"
90            with caching.ImplicitContext(lldbval):
91                out_string += obj(core.value(lldbval))
92            return out_string
93
94        myglobals = globals()
95        myglobals[summary_function_name] = _internal_summary_function
96        summary_function = myglobals[summary_function_name]
97        summary_function.__doc__ = obj.__doc__
98
99        global lldb_summary_definitions
100        for single_type in types_list:
101            if config['showTypeSummary']:
102                if single_type in lldb_summary_definitions:
103                    lldb.debugger.HandleCommand("type summary delete --category kernel \""+ single_type + "\"")
104                lldb.debugger.HandleCommand("type summary add \""+ single_type +"\" --category kernel --python-function " + MODULE_NAME + "." + summary_function_name)
105            lldb_summary_definitions[single_type] = obj
106
107        return obj
108    return _get_summary
109
110#
111# Exception handling from commands
112#
113
114_LLDB_WARNING = (
115    "*********************  LLDB found an exception  *********************\n"
116    "{lldb_version}\n\n"
117    "  There has been an uncaught exception.\n"
118    "  It could be because the debugger was disconnected.\n"
119    "\n"
120    "  In order to debug the macro being run, run the macro again\n"
121    "  with the `--debug` flag to get richer information, for example:\n"
122    "\n"
123    "      (lldb) showtask --debug 0x1234\n"
124    "\n"
125    "  In order to file a bug report instead, run the macro again\n"
126    "  with the `--radar` flag which will produce a tarball to\n"
127    "  attach to your report, for example:\n"
128    "\n"
129    "      (lldb) showtask --radar 0x1234\n"
130    "********************************************************************\n"
131)
132
133def _format_exc(exc, vt):
134    import traceback, textwrap
135
136    out_str = ""
137
138    w = textwrap.TextWrapper(width=100, placeholder="...", max_lines=3)
139    tb = traceback.TracebackException.from_exception(exc, limit=None, lookup_lines=True, capture_locals=True)
140
141    for frame in tb.stack:
142        out_str += (
143            f"File \"{vt.DarkBlue}{frame.filename}\"{vt.Magenta}@{frame.lineno}{vt.Reset} "
144            f"in {vt.Bold}{vt.DarkCyan}{frame.name}{vt.Reset}\n"
145        )
146        out_str += "  Locals:\n"
147        for name, value in frame.locals.items():
148            variable = f"    {vt.Bold}{vt.DarkGreen}{name}{vt.Reset} = "
149            first = True
150            for wline in w.wrap(str(value)):
151                if first:
152                    out_str += variable + f"{vt.Oblique}{wline}\n"
153                    first = False
154                else:
155                    out_str += " " * (len(name) + 7) + wline + "\n"
156                out_str += vt.EndOblique
157
158        out_str += "  " + "-" * 100 + "\n"
159        try:
160            src = open(frame.filename, "r")
161        except IOError:
162            out_str += "    < Sources not available >\n"
163        else:
164            with src:
165                lines = src.readlines()
166
167                startline = frame.lineno - 3 if frame.lineno > 2 else 0
168                endline = min(frame.lineno + 2, len(lines))
169                for lineno in range(startline, endline):
170
171                    if lineno + 1 == frame.lineno:
172                        fmt = vt.Bold + vt.Default
173                        marker = '>'
174                    else:
175                        fmt = vt.Default
176                        marker = ' '
177
178                    out_str += f"{fmt}  {marker} {lineno + 1:5}  {lines[lineno].rstrip()}{vt.Reset}\n"
179
180        out_str += "  " + "-" * 100 + "\n"
181        out_str += "\n"
182
183    return out_str
184
185_RADAR_URL = "rdar://new/problem?title=LLDB%20macro%20failed%3A%20{}&attachments={}"
186
187def diagnostic_report(result, exc, stream, cmd_name, debug_opts, lldb_log_fname=None):
188    """ Collect diagnostic report for radar submission.
189
190        @param exc (Exception type)
191            Exception being reported.
192
193        @param stream (OutputObject)
194            Command's output stream to support formattting.
195
196        @param cmd_name (str)
197            Name of command being executed.
198
199        @param debug_opts ([str])
200            List of active debugging options (--debug, --radar, --pdb)
201
202        @param lldb_log_fname (str)
203            LLDB log file name to collect (optional)
204    """
205
206    # Print prologue common to all exceptions handling modes.
207    print(stream.VT.DarkRed + _LLDB_WARNING.format(lldb_version=lldb.SBDebugger.GetVersionString()))
208    print(stream.VT.Bold + stream.VT.DarkGreen + type(exc).__name__ +
209          stream.VT.Default + ": {}".format(str(exc)) + stream.VT.Reset)
210    print()
211
212    if not debug_opts:
213        result.SetError(f"{type(exc).__name__}: {exc}")
214        raise exc
215
216    #
217    # Display enhanced diagnostics when requested.
218    #
219    if "--debug" in debug_opts:
220        # Format exception for terminal
221        print(_format_exc(exc, stream.VT))
222
223        print("version:")
224        print(lldb.SBDebugger.GetVersionString())
225        print()
226
227    #
228    # Construct tar bundle for radar attachement
229    #
230    if "--radar" in debug_opts:
231        import tarfile, urllib.parse
232        print("Creating radar bundle ...")
233
234        itime = int(time.time())
235        tar_fname = "/tmp/debug.{:d}.tar".format(itime)
236
237        with tarfile.open(tar_fname, "w") as tar:
238            # Collect LLDB log. It can't be unlinked here because it is still used
239            # for the whole duration of xnudebug debug enable.
240            if lldb_log_fname is not None:
241                print("  Adding {}".format(lldb_log_fname))
242                tar.add(lldb_log_fname, "radar/lldb.log")
243                os.unlink(lldb_log_fname)
244
245            # Collect traceback
246            tb_fname = "/tmp/tb.{:d}.log".format(itime)
247            print("  Adding {}".format(tb_fname))
248            with open(tb_fname,"w") as f:
249                f.write(f"{type(exc).__name__}: {str(exc)}\n\n")
250                f.write(_format_exc(exc, NOVT()))
251                f.write("version:\n")
252                f.write(f"{lldb.SBDebugger.GetVersionString()}\n")
253                f.write("loaded images:\n")
254                f.write(lldb_run_command("image list") + "\n")
255                f.write("lldb settings:\n")
256                f.write(lldb_run_command("settings show") + "\n")
257            tar.add(tb_fname, "radar/traceback.log")
258            os.unlink(tb_fname)
259
260        # Radar submission
261        print()
262        print(stream.VT.DarkRed + "Please attach {} to your radar or open the URL below to create one:".format(tar_fname) + stream.VT.Reset)
263        print()
264        print("  " + _RADAR_URL.format(urllib.parse.quote(cmd_name),urllib.parse.quote(tar_fname)))
265        print()
266        print("Don't forget to mention where the coredump came from (e.g. original Radar ID), or attach it :)")
267        print()
268
269    # Enter pdb when requested.
270    if "--pdb" in debug_opts:
271        print("Starting debugger ...")
272        import pdb
273        pdb.post_mortem(exc.__traceback__)
274
275    return False
276
277#global cache of documentation for lldb commands exported by this module
278#DONOTTOUCHME: Exclusive use of lldb_command only.
279lldb_command_documentation = {}
280
281_DEBUG_OPTS = { "--debug", "--radar", "--pdb" }
282
283def lldb_command(cmd_name, option_string = '', fancy=False):
284    """ A function decorator to define a command with name 'cmd_name' in the lldb scope to call python function.
285        params: cmd_name - str : name of command to be set in lldb prompt.
286            option_string - str: getopt like option string. Only CAPITAL LETTER options allowed.
287                                 see README on Customizing command options.
288            fancy - bool       : whether the command will receive an 'O' object to do fancy output (tables, indent, color)
289    """
290    if option_string != option_string.upper():
291        raise RuntimeError("Cannot setup command with lowercase option args. %s" % option_string)
292
293    def _cmd(obj):
294        def _internal_command_function(debugger, command, exe_ctx, result, internal_dict):
295            global config, lldb_run_command_state
296            stream = CommandOutput(cmd_name, result)
297            # need to avoid printing on stdout if called from lldb_run_command.
298            if 'active' in lldb_run_command_state and lldb_run_command_state['active']:
299                debuglog('Running %s from lldb_run_command' % command)
300            else:
301                result.SetImmediateOutputFile(sys.__stdout__)
302
303            command_args = shlex.split(command)
304            lldb.debugger.HandleCommand('type category disable kernel')
305            def_verbose_level = config['verbosity']
306
307            # Filter out debugging arguments and enable logging
308            debug_opts = [opt for opt in command_args if opt in _DEBUG_OPTS]
309            command_args = [opt for opt in command_args if opt not in _DEBUG_OPTS]
310            lldb_log_filename = None
311
312            if "--radar" in debug_opts:
313                lldb_log_filename = "/tmp/lldb.{:d}.log".format(int(time.time()))
314                lldb_run_command("log enable --file {:s} lldb api".format(lldb_log_filename))
315                lldb_run_command("log enable --file {:s} gdb-remote packets".format(lldb_log_filename))
316                lldb_run_command("log enable --file {:s} kdp-remote packets".format(lldb_log_filename))
317
318            try:
319                stream.setOptions(command_args, option_string)
320                if stream.verbose_level != 0:
321                    config['verbosity'] +=  stream.verbose_level
322                with RedirectStdStreams(stdout=stream), caching.ImplicitContext(exe_ctx):
323                    args = { 'cmd_args': stream.target_cmd_args }
324                    if option_string:
325                        args['cmd_options'] = stream.target_cmd_options
326                    if fancy:
327                        args['O'] = stream
328                    obj(**args)
329            except KeyboardInterrupt:
330                print("Execution interrupted by user")
331            except (ArgumentError, NotImplementedError) as arg_error:
332                if str(arg_error) != HELP_ARGUMENT_EXCEPTION_SENTINEL:
333                    formatted_err = f"{type(arg_error).__name__}: {arg_error}"
334                    print(formatted_err)
335                    result.SetError(formatted_err)
336                print("{0:s}:\n        {1:s}".format(cmd_name, obj.__doc__.strip()))
337                return False
338            except Exception as exc:
339                if "--radar" in debug_opts: lldb_run_command("log disable")
340                return diagnostic_report(result, exc, stream, cmd_name, debug_opts, lldb_log_filename)
341
342            if config['showTypeSummary']:
343                lldb.debugger.HandleCommand('type category enable kernel' )
344
345            if stream.pluginRequired :
346                plugin = LoadXNUPlugin(stream.pluginName)
347                if plugin == None :
348                    print("Could not load plugins."+stream.pluginName)
349                    return
350                plugin.plugin_init(kern, config, lldb, kern.IsDebuggerConnected())
351                return_data = plugin.plugin_execute(cmd_name, result.GetOutput())
352                ProcessXNUPluginResult(return_data)
353                plugin.plugin_cleanup()
354
355            #restore the verbose level after command is complete
356            config['verbosity'] = def_verbose_level
357
358            return
359
360        myglobals = globals()
361        command_function_name = obj.__name__+"Command"
362        myglobals[command_function_name] =  _internal_command_function
363        myglobals[f"XnuCommandSentinel{cmd_name}"] = None
364        command_function = myglobals[command_function_name]
365        if not obj.__doc__:
366            print("ERROR: Cannot register command({:s}) without documentation".format(cmd_name))
367            return obj
368        obj.__doc__ += "\n" + COMMON_HELP_STRING
369        command_function.__doc__ = obj.__doc__
370        global lldb_command_documentation
371        lldb_command_documentation[cmd_name] = (obj.__name__, obj.__doc__.lstrip(), option_string)
372
373        script_add_command = f"command script add -o -f {MODULE_NAME}.{command_function_name} {cmd_name}"
374        lldb.debugger.HandleCommand(script_add_command)
375
376        setattr(obj, 'fancy', fancy)
377        if fancy:
378            @wraps(obj)
379            def wrapped_fun(cmd_args=None, cmd_options={}, O=None):
380                if O is None:
381                    stream = CommandOutput(cmd_name, fhandle=sys.stdout)
382                    with RedirectStdStreams(stdout=stream):
383                        return obj(cmd_args, cmd_options, O=stream)
384                else:
385                    return obj(cmd_args, cmd_options, O)
386            return wrapped_fun
387        return obj
388    return _cmd
389
390
391def lldb_alias(alias_name, cmd_line):
392    """ define an alias in the lldb command line.
393        A programatic way of registering an alias. This basically does
394        (lldb)command alias alias_name "cmd_line"
395        ex.
396        lldb_alias('readphys16', 'readphys 16')
397    """
398    alias_name = alias_name.strip()
399    cmd_line = cmd_line.strip()
400    lldb.debugger.HandleCommand("command alias " + alias_name + " "+ cmd_line)
401
402def SetupLLDBTypeSummaries(reset=False):
403    global lldb_summary_definitions, MODULE_NAME
404    if reset:
405            lldb.debugger.HandleCommand("type category delete  kernel ")
406    for single_type in list(lldb_summary_definitions.keys()):
407        summary_function = lldb_summary_definitions[single_type]
408        lldb_cmd = "type summary add \""+ single_type +"\" --category kernel --python-function " + MODULE_NAME + ".LLDBSummary" + summary_function.__name__
409        debuglog(lldb_cmd)
410        lldb.debugger.HandleCommand(lldb_cmd)
411    if config['showTypeSummary']:
412            lldb.debugger.HandleCommand("type category enable  kernel")
413    else:
414            lldb.debugger.HandleCommand("type category disable kernel")
415
416    return
417
418def LoadXNUPlugin(name):
419    """ Try to load a plugin from the plugins directory.
420    """
421    retval = None
422    name=name.strip()
423    try:
424        module_obj = __import__('plugins.'+name, globals(), locals(), [], -1)
425        module_obj = module_obj.__dict__[name]
426        defs = dir(module_obj)
427        if 'plugin_init' in defs and 'plugin_execute' in defs and 'plugin_cleanup' in defs:
428            retval = module_obj
429        else:
430            print("Plugin is not correctly implemented. Please read documentation on implementing plugins")
431    except:
432        print("plugin not found :"+name)
433
434    return retval
435
436def ProcessXNUPluginResult(result_data):
437    """ Look at the returned data from plugin and see if anymore actions are required or not
438        params: result_data - list of format (status, out_string, more_commands)
439    """
440    ret_status = result_data[0]
441    ret_string = result_data[1]
442    ret_commands = result_data[2]
443
444    if not ret_status:
445        print("Plugin failed: " + ret_string)
446        return
447    print(ret_string)
448    if len(ret_commands) >= 0:
449        for cmd in ret_commands:
450            print("Running command on behalf of plugin:" + cmd)
451            lldb.debugger.HandleCommand(cmd)
452    return
453
454# holds tests registered with xnu.
455#DONOTTOUCHME: Exclusive use of xnudebug_test only
456lldb_command_tests = {}
457def xnudebug_test(test_name):
458    """ A function decoratore to register a test with the framework. Each test is supposed to be of format
459        def Test<name>(kernel_target, config, lldb_obj, isConnected )
460
461        NOTE: The testname should start with "Test" else exception will be raised.
462    """
463    def _test(obj):
464        global lldb_command_tests
465        if obj.__name__.find("Test") != 0 :
466            print("Test name ", obj.__name__ , " should start with Test")
467            raise ValueError
468        lldb_command_tests[test_name] = (test_name, obj.__name__, obj, obj.__doc__)
469        return obj
470    return _test
471
472
473# End Debugging specific utility functions
474# Kernel Debugging specific classes and accessor methods
475
476# global access object for target kernel
477
478def GetObjectAtIndexFromArray(array_base: value, index: int):
479    """ Subscript indexing for arrays that are represented in C as pointers.
480        for ex. int *arr = malloc(20*sizeof(int));
481        now to get 3rd int from 'arr' you'd do
482        arr[2] in C
483        GetObjectAtIndexFromArray(arr_val,2)
484        params:
485            array_base : core.value - representing a pointer type (ex. base of type 'ipc_entry *')
486            index : int - 0 based index into the array
487        returns:
488            core.value : core.value of the same type as array_base_val but pointing to index'th element
489    """
490    array_base_val = array_base.GetSBValue()
491    base_address = array_base_val.GetValueAsAddress()
492    size = array_base_val.GetType().GetPointeeType().GetByteSize()
493    obj_address = base_address + (index * size)
494    obj = kern.CreateValueFromAddress(obj_address, array_base_val.GetType().GetPointeeType().name)
495    return addressof(obj)
496
497
498kern: KernelTarget = None
499
500def GetLLDBThreadForKernelThread(thread_obj):
501    """ Get a reference to lldb.SBThread representation for kernel thread.
502        params:
503            thread_obj : core.cvalue - thread object of type thread_t
504        returns
505            lldb.SBThread - lldb thread object for getting backtrace/registers etc.
506    """
507    tid = unsigned(thread_obj.thread_id)
508    lldb_process = LazyTarget.GetProcess()
509    sbthread = lldb_process.GetThreadByID(tid)
510    if not sbthread.IsValid():
511        # in case lldb doesnt know about this thread, create one
512        if hasattr(lldb_process, "CreateOSPluginThread"):
513            debuglog("creating os plugin thread on the fly for {0:d} 0x{1:x}".format(tid, thread_obj))
514            lldb_process.CreateOSPluginThread(tid, unsigned(thread_obj))
515        else:
516            raise RuntimeError("LLDB process does not support CreateOSPluginThread.")
517        sbthread = lldb_process.GetThreadByID(tid)
518
519    if not sbthread.IsValid():
520        raise RuntimeError("Unable to find lldb thread for tid={0:d} thread = {1:#018x} (#16049947: have you put 'settings set target.load-script-from-symbol-file true' in your .lldbinit?)".format(tid, thread_obj))
521
522    return sbthread
523
524def GetKextSymbolInfo(load_addr):
525    """ Get a string descriptiong load_addr <kextname> + offset
526        params:
527            load_addr - int address value of pc in backtrace.
528        returns: str - kext name + offset string. If no cached data available, warning message is returned.
529    """
530    symbol_name = "None"
531    symbol_offset = load_addr
532    kmod_val = kern.globals.kmod
533    if not kern.arch.startswith('arm64'):
534        for kval in IterateLinkedList(kmod_val, 'next'):
535            if load_addr >= unsigned(kval.address) and \
536                load_addr <= (unsigned(kval.address) + unsigned(kval.size)):
537                symbol_name = kval.name
538                symbol_offset = load_addr - unsigned(kval.address)
539                break
540        return "{:#018x} {:s} + {:#x} \n".format(load_addr, symbol_name, symbol_offset)
541
542    # only for arm64 we do lookup for split kexts.
543    if not GetAllKextSummaries.cached():
544        if str(GetConnectionProtocol()) != "core":
545            return "{:#018x} ~ kext info not available. please run 'showallkexts' once ~ \n".format(load_addr)
546
547    for kval in GetAllKextSummaries():
548        text_seg = text_segment(kval.segments)
549        if load_addr >= text_seg.vmaddr and \
550            load_addr <= (text_seg.vmaddr + text_seg.vmsize):
551            symbol_name = kval.name
552            symbol_offset = load_addr - text_seg.vmaddr
553            break
554    return "{:#018x} {:s} + {:#x} \n".format(load_addr, symbol_name, symbol_offset)
555
556def GetThreadBackTrace(thread_obj: value, verbosity = vHUMAN, prefix = ""):
557    """ Get a string to display back trace for a thread.
558        params:
559            thread_obj - core.cvalue : a thread object of type thread_t.
560            verbosity - int : either of vHUMAN, vSCRIPT or vDETAIL to describe the verbosity of output
561            prefix - str : a string prefix added before the line for each frame.
562            isContinuation - bool : is thread a continuation?
563        returns:
564            str - a multi line string showing each frame in backtrace.
565    """
566    kernel_stack = unsigned(thread_obj.kernel_stack)
567    is_continuation = not bool(kernel_stack)
568    thread_val = GetLLDBThreadForKernelThread(thread_obj)
569    out_string = ""
570    reserved_stack = unsigned(thread_obj.reserved_stack)
571    if not is_continuation:
572        if kernel_stack and reserved_stack:
573            out_string += f"{prefix}reserved_stack = {reserved_stack:#018x}\n"
574        out_string += f"{prefix}kernel_stack = {kernel_stack:#018x}\n"
575    else:
576        out_string += prefix + "continuation ="
577    iteration = 0
578    last_frame_p = 0
579    for frame in thread_val.frames:
580        addr = frame.GetPCAddress()
581        load_addr = addr.GetLoadAddress(LazyTarget.GetTarget())
582        function = frame.GetFunction()
583        frame_p = frame.GetFP()
584        mod_name = frame.GetModule().GetFileSpec().GetFilename()
585
586        if iteration == 0 and not is_continuation:
587            out_string += f"{prefix}stacktop = {frame_p:#018x}\n"
588
589        if not function:
590            # No debug info for 'function'.
591            out_string += prefix
592            if not is_continuation:
593                out_string += f"{frame_p:#018x} "
594
595            symbol = frame.GetSymbol()
596            if not symbol:
597                out_string += GetKextSymbolInfo(load_addr)
598            else:
599                file_addr = addr.GetFileAddress()
600                start_addr = symbol.GetStartAddress().GetFileAddress()
601                symbol_name = symbol.GetName()
602                symbol_offset = file_addr - start_addr
603                out_string += f"{load_addr:#018x} {mod_name}`{symbol_name} + {symbol_offset:#x} \n"
604        else:
605            # Debug info is available for 'function'.
606            inlined_suffix= " [inlined]" if frame.IsInlined() else ''
607            func_name = f"{frame.GetFunctionName()}{inlined_suffix}"
608            # file_name = frame.GetLineEntry().GetFileSpec().GetFilename()
609            # line_num = frame.GetLineEntry().GetLine()
610            if is_continuation and frame.IsInlined():
611                debuglog("Skipping frame for thread {:#018x} since its inlined".format(thread_obj))
612                continue
613            out_string += prefix
614            if not is_continuation:
615                out_string += f"{frame_p:#018x} "
616
617            if len(frame.arguments) > 0:
618                func_args = str(frame.arguments).replace('\n', ', ')
619                out_string += f"{load_addr:#018x} {func_name}({func_args}) \n"
620            else:
621                out_string += f"{load_addr:#018x} {func_name}(void) \n"
622
623        iteration += 1
624        if frame_p:
625            last_frame_p = frame_p
626
627    if not is_continuation and last_frame_p:
628        out_string += prefix + "stackbottom = {:#018x}".format(last_frame_p)
629    out_string = out_string.replace("variable not available","")
630    return out_string
631
632def GetSourceInformationForAddress(addr):
633    """ convert and address to function +offset information.
634        params: addr - int address in the binary to be symbolicated
635        returns: string of format "0xaddress: function + offset"
636    """
637    try:
638        return str(kern.SymbolicateFromAddress(addr, fullSymbol=True)[0])
639    except:
640        return '{0:<#x} <unknown: use `addkextaddr {0:#x}` to resolve>'.format(addr)
641
642def GetFrameLocalVariable(variable_name, frame_no=0):
643    """ Find a local variable by name
644        params:
645          variable_name: str - name of variable to search for
646        returns:
647          core.value - if the variable is found.
648          None   - if not found or not Valid
649    """
650    retval = None
651    sbval = None
652    lldb_SBThread = LazyTarget.GetProcess().GetSelectedThread()
653    frame = lldb_SBThread.GetSelectedFrame()
654    if frame_no :
655      frame = lldb_SBThread.GetFrameAtIndex(frame_no)
656    if frame :
657      sbval = frame.FindVariable(variable_name)
658    if sbval and sbval.IsValid():
659      retval = core.cvalue.value(sbval)
660    return retval
661
662# Begin Macros for kernel debugging
663
664@lldb_command('kgmhelp')
665def KernelDebugCommandsHelp(cmd_args=None):
666    """ Show a list of registered commands for kenel debugging.
667    """
668    global lldb_command_documentation
669    print("List of commands provided by " + MODULE_NAME + " for kernel debugging.")
670    cmds = list(lldb_command_documentation.keys())
671    cmds.sort()
672    for cmd in cmds:
673        if isinstance(lldb_command_documentation[cmd][-1], str):
674            print(" {0: <20s} - {1}".format(cmd , lldb_command_documentation[cmd][1].split("\n")[0].strip()))
675        else:
676            print(" {0: <20s} - {1}".format(cmd , "No help string found."))
677    print('Each of the functions listed here accept the following common options. ')
678    print(COMMON_HELP_STRING)
679    print('Additionally, each command implementation may have more options. "(lldb) help <command> " will show these options.')
680    return None
681
682
683@lldb_command('showraw')
684def ShowRawCommand(cmd_args=None):
685    """ A command to disable the kernel summaries and show data as seen by the system.
686        This is useful when trying to read every field of a struct as compared to brief summary
687
688        Usage: showraw foo_command foo_arg1 foo_arg2 ...
689    """
690    if cmd_args is None or len(cmd_args) == 0:
691        raise ArgumentError("'showraw' requires a command to run")
692
693    command = " ".join(cmd_args)
694    lldb.debugger.HandleCommand('type category disable kernel' )
695    lldb.debugger.HandleCommand(command)
696    lldb.debugger.HandleCommand('type category enable kernel' )
697
698
699@lldb_command('xnudebug')
700def XnuDebugCommand(cmd_args=None):
701    """  command interface for operating on the xnu macros. Allowed commands are as follows
702        reload:
703            Reload a submodule from the xnu/tools/lldb directory. Do not include the ".py" suffix in modulename.
704            usage: xnudebug reload <modulename> (eg. memory, process, stats etc)
705        flushcache:
706            remove any cached data held in static or dynamic data cache.
707            usage: xnudebug flushcache
708        test:
709            Start running registered test with <name> from various modules.
710            usage: xnudebug test <name> (eg. test_memstats)
711        testall:
712            Go through all registered tests and run them
713        debug:
714            Toggle state of debug configuration flag.
715        profile:
716            Profile an lldb command and write its profile info to a file.
717            usage: xnudebug profile <path_to_profile> <cmd...>
718
719            e.g. `xnudebug profile /tmp/showallstacks_profile.prof showallstacks
720        coverage:
721            Collect coverage for an lldb command and save it to a file.
722            usage: xnudebug coverage <path_to_coverage_file> <cmd ...>
723
724            e.g. `xnudebug coverage /tmp/showallstacks_coverage.cov showallstacks`
725            An HTML report can then be generated via `coverage html --data-file=<path>`
726    """
727    global config
728    command_args = cmd_args
729    if len(command_args) == 0:
730        raise ArgumentError("No command specified.")
731    supported_subcommands = ['debug', 'reload', 'test', 'testall', 'flushcache', 'profile', 'coverage']
732    subcommand = GetLongestMatchOption(command_args[0], supported_subcommands, True)
733
734    if len(subcommand) == 0:
735        raise ArgumentError("Subcommand (%s) is not a valid command. " % str(command_args[0]))
736
737    subcommand = subcommand[0].lower()
738    if subcommand == 'debug':
739        if command_args[-1].lower().find('dis') >=0 and config['debug']:
740            config['debug'] = False
741            print("Disabled debug logging.")
742        elif command_args[-1].lower().find('dis') < 0 and not config['debug']:
743            config['debug'] = True
744            EnableLLDBAPILogging()  # provided by utils.py
745            print("Enabled debug logging. \nPlease run 'xnudebug debug disable' to disable it again. ")
746
747    if subcommand == 'flushcache':
748        print("Current size of cache: {}".format(caching.GetSizeOfCache()))
749        caching.ClearAllCache()
750
751    if subcommand == 'reload':
752        module_name = command_args[-1]
753        if module_name in sys.modules:
754            reload(sys.modules[module_name])
755            print(module_name + " is reloaded from " + sys.modules[module_name].__file__)
756        else:
757            print("Unable to locate module named ", module_name)
758
759    if subcommand == 'testall':
760        for test_name in list(lldb_command_tests.keys()):
761            print("[BEGIN]", test_name)
762            res = lldb_command_tests[test_name][2](kern, config, lldb, True)
763            if res:
764                print("[PASSED] {:s}".format(test_name))
765            else:
766                print("[FAILED] {:s}".format(test_name))
767
768    if subcommand == 'test':
769        test_name = command_args[-1]
770        if test_name in lldb_command_tests:
771            test = lldb_command_tests[test_name]
772            print("Running test {:s}".format(test[0]))
773            if test[2](kern, config, lldb, True) :
774                print("[PASSED] {:s}".format(test[0]))
775            else:
776                print("[FAILED] {:s}".format(test[0]))
777            return ""
778        else:
779            print("No such test registered with name: {:s}".format(test_name))
780            print("XNUDEBUG Available tests are:")
781            for i in list(lldb_command_tests.keys()):
782                print(i)
783        return None
784
785    if subcommand == 'profile':
786        save_path = command_args[1]
787
788        import cProfile, pstats, io
789
790        pr = cProfile.Profile()
791        pr.enable()
792
793        lldb.debugger.HandleCommand(" ".join(command_args[2:]))
794
795        pr.disable()
796        pr.dump_stats(save_path)
797
798        print("")
799        print("=" * 80)
800        print("")
801
802        s = io.StringIO()
803        ps = pstats.Stats(pr, stream=s)
804        ps.strip_dirs()
805        ps.sort_stats('cumulative')
806        ps.print_stats(30)
807        print(s.getvalue().rstrip())
808        print("")
809
810        print(f"Profile info saved to \"{save_path}\"")
811
812    if subcommand == 'coverage':
813        coverage_module = find_spec('coverage')
814        if not coverage_module:
815            print("Missing 'coverage' module. Please install it for the interpreter currently running.`")
816            return
817
818        save_path = command_args[1]
819
820        import coverage
821        cov = coverage.Coverage(data_file=save_path)
822        cov.start()
823
824        lldb.debugger.HandleCommand(" ".join(command_args[2:]))
825
826        cov.stop()
827        cov.save()
828
829        print(cov.report())
830        print(f"Coverage info saved to: \"{save_path}\"")
831
832
833    return False
834
835
836
837@lldb_command('showversion')
838def ShowVersion(cmd_args=None):
839    """ Read the kernel version string from a fixed address in low
840        memory. Useful if you don't know which kernel is on the other end,
841        and need to find the appropriate symbols. Beware that if you've
842        loaded a symbol file, but aren't connected to a remote target,
843        the version string from the symbol file will be displayed instead.
844        This macro expects to be connected to the remote kernel to function
845        correctly.
846
847    """
848    print(kern.version)
849
850def ProcessPanicStackshot(panic_stackshot_addr, panic_stackshot_len, cmd_options):
851    """ Process the panic stackshot from the panic header, saving it to a file if it is valid
852        params: panic_stackshot_addr : start address of the panic stackshot binary data
853                panic_stackshot_len : length of the stackshot binary data
854        returns: nothing
855    """
856    if not panic_stackshot_addr:
857        print("No panic stackshot available (invalid addr)")
858        return
859
860    if not panic_stackshot_len:
861        print("No panic stackshot available (zero length)")
862        return;
863
864    if "-D" in cmd_options:
865        dir_ = cmd_options["-D"]
866        if os.path.exists(dir_):
867            if not os.access(dir_, os.W_OK):
868                print("Write access to {} denied".format(dir_))
869                return
870        else:
871            try:
872                os.makedirs(dir_)
873            except OSError as e:
874                print("An error occurred {} while creating a folder : {}".format(e, dir_))
875                return
876    else:
877        dir_ = "/tmp"
878
879    id = str(uuid.uuid4())[:8]
880    ss_binfile = os.path.join(dir_, "panic_%s.bin" % id)
881    ss_ipsfile = os.path.join(dir_, "panic_%s.ips" % id)
882
883    if not SaveDataToFile(panic_stackshot_addr, panic_stackshot_len, ss_binfile, None):
884        print("Failed to save stackshot binary data to file")
885        return
886
887    from kcdata import decode_kcdata_file
888    try:
889        with open(ss_binfile, "rb") as binfile:
890            decode_kcdata_file(binfile, ss_ipsfile)
891        print("Saved ips stackshot file as %s" % ss_ipsfile)
892    except Exception as e:
893        print("Failed to decode the stackshot: %s" % str(e))
894
895def ParseEmbeddedPanicLog(panic_header, cmd_options={}):
896    panic_buf = Cast(panic_header, 'char *')
897    panic_log_magic = unsigned(panic_header.eph_magic)
898    panic_log_begin_offset = unsigned(panic_header.eph_panic_log_offset)
899    panic_log_len = unsigned(panic_header.eph_panic_log_len)
900    other_log_begin_offset = unsigned(panic_header.eph_other_log_offset)
901    other_log_len = unsigned(panic_header.eph_other_log_len)
902    expected_panic_magic = xnudefines.EMBEDDED_PANIC_MAGIC
903    panic_stackshot_addr = unsigned(panic_header) + unsigned(panic_header.eph_stackshot_offset)
904    panic_stackshot_len = unsigned(panic_header.eph_stackshot_len)
905    panic_header_flags = unsigned(panic_header.eph_panic_flags)
906
907    warn_str = ""
908    out_str = ""
909
910    if panic_log_magic != 0 and panic_log_magic != expected_panic_magic:
911        warn_str += "BAD MAGIC! Found 0x%x expected 0x%x" % (panic_log_magic,
912                    expected_panic_magic)
913
914    if warn_str:
915        print("\n %s" % warn_str)
916        if panic_log_begin_offset == 0:
917            return
918
919    if "-S" in cmd_options:
920        if panic_header_flags & xnudefines.EMBEDDED_PANIC_STACKSHOT_SUCCEEDED_FLAG:
921            ProcessPanicStackshot(panic_stackshot_addr, panic_stackshot_len, cmd_options)
922        else:
923            print("No panic stackshot available")
924    elif "-D" in cmd_options:
925        print("-D option must be specified along with the -S option")
926        return
927
928    panic_log_curindex = 0
929    while panic_log_curindex < panic_log_len:
930        p_char = str(panic_buf[(panic_log_begin_offset + panic_log_curindex)])
931        out_str += p_char
932        panic_log_curindex += 1
933
934    if other_log_begin_offset != 0:
935        other_log_curindex = 0
936        while other_log_curindex < other_log_len:
937            p_char = str(panic_buf[(other_log_begin_offset + other_log_curindex)])
938            out_str += p_char
939            other_log_curindex += 1
940
941    print(out_str)
942    return
943
944def ParseMacOSPanicLog(panic_header, cmd_options={}):
945    panic_buf = Cast(panic_header, 'char *')
946    panic_log_magic = unsigned(panic_header.mph_magic)
947    panic_log_begin_offset = unsigned(panic_header.mph_panic_log_offset)
948    panic_log_len = unsigned(panic_header.mph_panic_log_len)
949    other_log_begin_offset = unsigned(panic_header.mph_other_log_offset)
950    other_log_len = unsigned(panic_header.mph_other_log_len)
951    cur_debug_buf_ptr_offset = (unsigned(kern.globals.debug_buf_ptr) - unsigned(panic_header))
952    if other_log_begin_offset != 0 and (other_log_len == 0 or other_log_len < (cur_debug_buf_ptr_offset - other_log_begin_offset)):
953        other_log_len = cur_debug_buf_ptr_offset - other_log_begin_offset
954    expected_panic_magic = xnudefines.MACOS_PANIC_MAGIC
955
956    # use the global if it's available (on an x86 corefile), otherwise refer to the header
957    if hasattr(kern.globals, "panic_stackshot_buf"):
958        panic_stackshot_addr = unsigned(kern.globals.panic_stackshot_buf)
959        panic_stackshot_len = unsigned(kern.globals.panic_stackshot_len)
960    else:
961        panic_stackshot_addr = unsigned(panic_header) + unsigned(panic_header.mph_stackshot_offset)
962        panic_stackshot_len = unsigned(panic_header.mph_stackshot_len)
963
964    panic_header_flags = unsigned(panic_header.mph_panic_flags)
965
966    warn_str = ""
967    out_str = ""
968
969    if panic_log_magic != 0 and panic_log_magic != expected_panic_magic:
970        warn_str += "BAD MAGIC! Found 0x%x expected 0x%x" % (panic_log_magic,
971                    expected_panic_magic)
972
973    if warn_str:
974        print("\n %s" % warn_str)
975        if panic_log_begin_offset == 0:
976            return
977
978    if "-S" in cmd_options:
979        if panic_header_flags & xnudefines.MACOS_PANIC_STACKSHOT_SUCCEEDED_FLAG:
980            ProcessPanicStackshot(panic_stackshot_addr, panic_stackshot_len, cmd_options)
981        else:
982            print("No panic stackshot available")
983    elif "-D" in cmd_options:
984        print("-D option must be specified along with the -S option")
985        return
986
987    panic_log_curindex = 0
988    while panic_log_curindex < panic_log_len:
989        p_char = str(panic_buf[(panic_log_begin_offset + panic_log_curindex)])
990        out_str += p_char
991        panic_log_curindex += 1
992
993    if other_log_begin_offset != 0:
994        other_log_curindex = 0
995        while other_log_curindex < other_log_len:
996            p_char = str(panic_buf[(other_log_begin_offset + other_log_curindex)])
997            out_str += p_char
998            other_log_curindex += 1
999
1000    print(out_str)
1001    return
1002
1003def ParseAURRPanicLog(panic_header, cmd_options={}):
1004    reset_cause = {
1005        0x0: "OTHER",
1006        0x1: "CATERR",
1007        0x2: "SWD_TIMEOUT",
1008        0x3: "GLOBAL RESET",
1009        0x4: "STRAIGHT TO S5",
1010    }
1011
1012    expected_panic_magic = xnudefines.AURR_PANIC_MAGIC
1013
1014    panic_buf = Cast(panic_header, 'char *')
1015
1016    try:
1017        # This line will blow up if there's not type info for this struct (older kernel)
1018        # We fall back to manual parsing below
1019        aurr_panic_header = Cast(panic_header, 'struct efi_aurr_panic_header *')
1020        panic_log_magic = unsigned(aurr_panic_header.efi_aurr_magic)
1021        panic_log_version = unsigned(aurr_panic_header.efi_aurr_version)
1022        panic_log_reset_cause = unsigned(aurr_panic_header.efi_aurr_reset_cause)
1023        panic_log_reset_log_offset = unsigned(aurr_panic_header.efi_aurr_reset_log_offset)
1024        panic_log_reset_log_len = unsigned(aurr_panic_header.efi_aurr_reset_log_len)
1025    except Exception as e:
1026        print("*** Warning: kernel symbol file has no type information for 'struct efi_aurr_panic_header'...")
1027        print("*** Warning: trying to manually parse...")
1028        aurr_panic_header = Cast(panic_header, "uint32_t *")
1029        panic_log_magic = unsigned(aurr_panic_header[0])
1030        # panic_log_crc = unsigned(aurr_panic_header[1])
1031        panic_log_version = unsigned(aurr_panic_header[2])
1032        panic_log_reset_cause = unsigned(aurr_panic_header[3])
1033        panic_log_reset_log_offset = unsigned(aurr_panic_header[4])
1034        panic_log_reset_log_len = unsigned(aurr_panic_header[5])
1035
1036    if panic_log_magic != 0 and panic_log_magic != expected_panic_magic:
1037        print("BAD MAGIC! Found 0x%x expected 0x%x" % (panic_log_magic,
1038                    expected_panic_magic))
1039        return
1040
1041    print("AURR Panic Version: %d" % (panic_log_version))
1042
1043    # When it comes time to extend this in the future, please follow the
1044    # construct used below in ShowPanicLog()
1045    if panic_log_version in (xnudefines.AURR_PANIC_VERSION, xnudefines.AURR_CRASHLOG_PANIC_VERSION):
1046        # AURR Report Version 1 (AURR/MacEFI) or 2 (Crashlog)
1047        # see macefifirmware/Vendor/Apple/EfiPkg/AppleDebugSupport/Library/Debugger.h
1048        print("Reset Cause: 0x%x (%s)" % (panic_log_reset_cause, reset_cause.get(panic_log_reset_cause, "UNKNOWN")))
1049
1050        # Adjust panic log string length (cap to maximum supported values)
1051        if panic_log_version == xnudefines.AURR_PANIC_VERSION:
1052            max_string_len = panic_log_reset_log_len
1053        elif panic_log_version == xnudefines.AURR_CRASHLOG_PANIC_VERSION:
1054            max_string_len = xnudefines.CRASHLOG_PANIC_STRING_LEN
1055
1056        panic_str_offset = 0
1057        out_str = ""
1058
1059        while panic_str_offset < max_string_len:
1060            p_char = str(panic_buf[panic_log_reset_log_offset + panic_str_offset])
1061            out_str += p_char
1062            panic_str_offset += 1
1063
1064        print(out_str)
1065
1066        # Save Crashlog Binary Data (if available)
1067        if "-S" in cmd_options and panic_log_version == xnudefines.AURR_CRASHLOG_PANIC_VERSION:
1068            crashlog_binary_offset = panic_log_reset_log_offset + xnudefines.CRASHLOG_PANIC_STRING_LEN
1069            crashlog_binary_size = (panic_log_reset_log_len > xnudefines.CRASHLOG_PANIC_STRING_LEN) and (panic_log_reset_log_len - xnudefines.CRASHLOG_PANIC_STRING_LEN) or 0
1070
1071            if 0 == crashlog_binary_size:
1072                print("No crashlog data found...")
1073                return
1074
1075            # Save to file
1076            ts = int(time.time())
1077            ss_binfile = "/tmp/crashlog_%d.bin" % ts
1078
1079            if not SaveDataToFile(panic_buf + crashlog_binary_offset, crashlog_binary_size, ss_binfile, None):
1080                print("Failed to save crashlog binary data to file")
1081                return
1082    else:
1083        return ParseUnknownPanicLog(panic_header, cmd_options)
1084
1085    return
1086
1087def ParseUnknownPanicLog(panic_header, cmd_options={}):
1088    magic_ptr = Cast(panic_header, 'uint32_t *')
1089    panic_log_magic = dereference(magic_ptr)
1090    print("Unrecognized panic header format. Magic: 0x%x..." % unsigned(panic_log_magic))
1091    print("Panic region starts at 0x%08x" % int(panic_header))
1092    print("Hint: To dump this panic header in order to try manually parsing it, use this command:")
1093    print(" (lldb) memory read -fx -s4 -c64 0x%08x" % int(panic_header))
1094    print(" ^ that will dump the first 256 bytes of the panic region")
1095    ## TBD: Hexdump some bits here to allow folks to poke at the region manually?
1096    return
1097
1098
1099@lldb_command('paniclog', 'SMD:')
1100def ShowPanicLog(cmd_args=None, cmd_options={}):
1101    """ Display the paniclog information
1102        usage: (lldb) paniclog
1103        options:
1104            -v : increase verbosity
1105            -S : parse stackshot data (if panic stackshot available)
1106            -D : Takes a folder name for stackshot. This must be specified along with the -S option.
1107            -M : parse macOS panic area (print panic string (if available), and/or capture crashlog info)
1108            -E : Takes a file name and redirects the ext paniclog output to the file
1109    """
1110
1111    if "-M" in cmd_options:
1112        if not hasattr(kern.globals, "mac_panic_header"):
1113            print("macOS panic data requested but unavailable on this device")
1114            return
1115        panic_header = kern.globals.mac_panic_header
1116        # DEBUG HACK FOR TESTING
1117        #panic_header = kern.GetValueFromAddress(0xfffffff054098000, "uint32_t *")
1118    else:
1119        panic_header = kern.globals.panic_info
1120
1121    if hasattr(panic_header, "eph_magic"):
1122        panic_log_magic = unsigned(panic_header.eph_magic)
1123    elif hasattr(panic_header, "mph_magic"):
1124        panic_log_magic = unsigned(panic_header.mph_magic)
1125    else:
1126        print("*** Warning: unsure of panic header format, trying anyway")
1127        magic_ptr = Cast(panic_header, 'uint32_t *')
1128        panic_log_magic = int(dereference(magic_ptr))
1129
1130    if panic_log_magic == 0:
1131        # No panic here..
1132        return
1133
1134    panic_parsers = {
1135        int(xnudefines.AURR_PANIC_MAGIC)     : ParseAURRPanicLog,
1136        int(xnudefines.MACOS_PANIC_MAGIC)    : ParseMacOSPanicLog,
1137        int(xnudefines.EMBEDDED_PANIC_MAGIC) : ParseEmbeddedPanicLog,
1138    }
1139
1140    # Find the right parser (fall back to unknown parser above)
1141    parser = panic_parsers.get(panic_log_magic, ParseUnknownPanicLog)
1142
1143    # execute it
1144    return parser(panic_header, cmd_options)
1145
1146@lldb_command('extpaniclog', 'F:')
1147def ProcessExtensiblePaniclog(cmd_args=None, cmd_options={}):
1148    """ Write the extensible paniclog information to a file
1149        usage: (lldb) paniclog
1150        options:
1151            -F : Output file name
1152    """
1153
1154    if not "-F" in cmd_options:
1155        print("Output file name is needed: Use -F")
1156        return
1157
1158    panic_header = kern.globals.panic_info
1159    process = LazyTarget().GetProcess()
1160    error = lldb.SBError()
1161    EXT_PANICLOG_MAX_SIZE = 32 # 32 is the max size of the string in Data ID
1162
1163    ext_paniclog_len = unsigned(panic_header.eph_ext_paniclog_len)
1164    if ext_paniclog_len == 0:
1165        print("Cannot find extensible paniclog")
1166        return
1167
1168    ext_paniclog_addr = unsigned(panic_header) + unsigned(panic_header.eph_ext_paniclog_offset)
1169
1170    ext_paniclog_bytes = process.chkReadMemory(ext_paniclog_addr, ext_paniclog_len);
1171
1172    idx = 0;
1173    ext_paniclog_ver_bytes = ext_paniclog_bytes[idx:idx+sizeof('uint32_t')]
1174    ext_paniclog_ver = int.from_bytes(ext_paniclog_ver_bytes, 'little')
1175
1176    idx += sizeof('uint32_t')
1177    no_of_logs_bytes = ext_paniclog_bytes[idx:idx+sizeof('uint32_t')]
1178    no_of_logs = int.from_bytes(no_of_logs_bytes, 'little')
1179
1180    idx += sizeof('uint32_t')
1181
1182    ext_paniclog = dict()
1183
1184    logs_processed = 0
1185    for _ in range(no_of_logs):
1186        uuid_bytes = ext_paniclog_bytes[idx:idx+sizeof('uuid_t')]
1187        ext_uuid = str(uuid.UUID(bytes=uuid_bytes))
1188
1189        idx += sizeof('uuid_t')
1190        flags_bytes = ext_paniclog_bytes[idx:idx+sizeof('uint32_t')]
1191        flags = int.from_bytes(flags_bytes, 'little')
1192
1193        idx += sizeof('ext_paniclog_flags_t')
1194        data_id_bytes = ext_paniclog_bytes[idx:idx + EXT_PANICLOG_MAX_SIZE].split(b'\0')[0]
1195        data_id = data_id_bytes.decode('utf-8')
1196        data_id_len = len(data_id_bytes)
1197
1198        idx += data_id_len + 1
1199        data_len_bytes = ext_paniclog_bytes[idx:idx+sizeof('uint32_t')]
1200        data_len = int.from_bytes(data_len_bytes, 'little')
1201
1202        idx += sizeof('uint32_t')
1203        data_bytes = ext_paniclog_bytes[idx:idx+data_len]
1204        data = base64.b64encode(data_bytes).decode('ascii')
1205
1206        idx += data_len
1207
1208        temp_dict = dict(Data_Id=data_id, Data=data)
1209
1210        ext_paniclog.setdefault(ext_uuid, []).append(temp_dict)
1211
1212        logs_processed += 1
1213
1214    if logs_processed < no_of_logs:
1215        print("** Warning: Extensible paniclog might be corrupted **")
1216
1217    with open(cmd_options['-F'], 'w') as out_file:
1218        out_file.write(json.dumps(ext_paniclog))
1219        print("Wrote extensible paniclog to %s" % cmd_options['-F'])
1220
1221    return
1222
1223@lldb_command('showbootargs')
1224def ShowBootArgs(cmd_args=None):
1225    """ Display boot arguments passed to the target kernel
1226    """
1227    bootargs = Cast(kern.GetGlobalVariable('PE_state').bootArgs, 'boot_args *')
1228    bootargs_cmd = bootargs.CommandLine
1229    print(str(bootargs_cmd))
1230
1231# The initialization code to add your commands
1232_xnu_framework_init = False
1233def __lldb_init_module(debugger: lldb.SBDebugger, internal_dict):
1234    global kern, lldb_command_documentation, config, _xnu_framework_init
1235    if _xnu_framework_init:
1236        return
1237    _xnu_framework_init = True
1238    debugger.HandleCommand('type summary add --regex --summary-string "${var%s}" -C yes -p -v "char *\[[0-9]*\]"')
1239    debugger.HandleCommand('type format add --format hex -C yes uintptr_t')
1240    debugger.HandleCommand('type format add --format hex -C yes cpumap_t')
1241    kern = KernelTarget(debugger)
1242    if not hasattr(lldb.SBValue, 'GetValueAsAddress'):
1243        warn_str = "WARNING: lldb version is too old. Some commands may break. Please update to latest lldb."
1244        if os.isatty(sys.__stdout__.fileno()):
1245            warn_str = VT.DarkRed + warn_str + VT.Default
1246        print(warn_str)
1247    print("xnu debug macros loaded successfully. Run showlldbtypesummaries to enable type summaries.")
1248    # print(f"xnu debugger ID: {debugger.GetID()}")
1249
1250__lldb_init_module(lldb.debugger, None)
1251
1252@lldb_command("showlldbtypesummaries")
1253def ShowLLDBTypeSummaries(cmd_args=[]):
1254    """ Enable/Disable kernel type summaries. Default is disabled.
1255        Usage: showlldbtypesummaries [enable|disable]
1256        default is enable
1257    """
1258    global config
1259    action = "enable"
1260    trailer_msg = ''
1261    if len(cmd_args) > 0 and cmd_args[0].lower().find('disable') >=0:
1262        action = "disable"
1263        config['showTypeSummary'] = False
1264        trailer_msg = "Please run 'showlldbtypesummaries enable' to enable the summary feature."
1265    else:
1266        config['showTypeSummary'] = True
1267        SetupLLDBTypeSummaries(True)
1268        trailer_msg = "Please run 'showlldbtypesummaries disable' to disable the summary feature."
1269    lldb_run_command("type category "+ action +" kernel")
1270    print("Successfully "+action+"d the kernel type summaries. %s" % trailer_msg)
1271
1272@lldb_command('walkqueue_head', 'S')
1273def WalkQueueHead(cmd_args=[], cmd_options={}):
1274    """ walk a queue_head_t and list all members in it. Note this is for queue_head_t. refer to osfmk/kern/queue.h
1275        Option: -S - suppress summary output.
1276        Usage: (lldb) walkqueue_head  <queue_entry *> <struct type> <fieldname>
1277        ex:    (lldb) walkqueue_head  0x7fffff80 "thread *" "task_threads"
1278
1279    """
1280    global lldb_summary_definitions
1281    if cmd_args is None or len(cmd_args) == 0:
1282        raise ArgumentError("invalid arguments")
1283    if len(cmd_args) != 3:
1284        raise ArgumentError("insufficient arguments")
1285    queue_head = kern.GetValueFromAddress(cmd_args[0], 'struct queue_entry *')
1286    el_type = cmd_args[1]
1287    field_name = cmd_args[2]
1288    showsummary = False
1289    if el_type in lldb_summary_definitions:
1290        showsummary = True
1291    if '-S' in cmd_options:
1292        showsummary = False
1293
1294    for i in IterateQueue(queue_head, el_type, field_name):
1295        if showsummary:
1296            print(lldb_summary_definitions[el_type](i))
1297        else:
1298            print("{0: <#020x}".format(i))
1299
1300
1301
1302@lldb_command('walklist_entry', 'SE')
1303def WalkList(cmd_args=[], cmd_options={}):
1304    """ iterate over a list as defined with LIST_ENTRY in bsd/sys/queue.h
1305        params:
1306            object addr  - value : address of object
1307            element_type - str   : Type of the next element
1308            field_name   - str   : Name of the field in next element's structure
1309
1310        Options: -S - suppress summary output.
1311                 -E - Iterate using SLIST_ENTRYs
1312
1313        Usage: (lldb) walklist_entry  <obj with list_entry *> <struct type> <fieldname>
1314        ex:    (lldb) walklist_entry  0x7fffff80 "struct proc *" "p_sibling"
1315
1316    """
1317    global lldb_summary_definitions
1318    if cmd_args is None or len(cmd_args) == 0:
1319        raise ArgumentError("invalid arguments")
1320    if len(cmd_args) != 3:
1321        raise ArgumentError("insufficient arguments")
1322    el_type = cmd_args[1]
1323    queue_head = kern.GetValueFromAddress(cmd_args[0], el_type)
1324    field_name = cmd_args[2]
1325    showsummary = False
1326    if el_type in lldb_summary_definitions:
1327        showsummary = True
1328    if '-S' in cmd_options:
1329        showsummary = False
1330    if '-E' in cmd_options:
1331        prefix = 's'
1332    else:
1333        prefix = ''
1334    elt = queue_head
1335    while unsigned(elt) != 0:
1336        i = elt
1337        elt = elt.__getattr__(field_name).__getattr__(prefix + 'le_next')
1338        if showsummary:
1339            print(lldb_summary_definitions[el_type](i))
1340        else:
1341            print("{0: <#020x}".format(i))
1342
1343def trace_parse_Copt(Copt):
1344    """Parses the -C option argument and returns a list of CPUs
1345    """
1346    cpusOpt = Copt
1347    cpuList = cpusOpt.split(",")
1348    chosen_cpus = []
1349    for cpu_num_string in cpuList:
1350        try:
1351            if '-' in cpu_num_string:
1352                parts = cpu_num_string.split('-')
1353                if len(parts) != 2 or not (parts[0].isdigit() and parts[1].isdigit()):
1354                    raise ArgumentError("Invalid cpu specification: %s" % cpu_num_string)
1355                firstRange = int(parts[0])
1356                lastRange = int(parts[1])
1357                if firstRange >= kern.globals.real_ncpus or lastRange >= kern.globals.real_ncpus:
1358                    raise ValueError()
1359                if lastRange < firstRange:
1360                    raise ArgumentError("Invalid CPU range specified: `%s'" % cpu_num_string)
1361                for cpu_num in range(firstRange, lastRange + 1):
1362                    if cpu_num not in chosen_cpus:
1363                        chosen_cpus.append(cpu_num)
1364            else:
1365                chosen_cpu = int(cpu_num_string)
1366                if chosen_cpu < 0 or chosen_cpu >= kern.globals.real_ncpus:
1367                    raise ValueError()
1368                if chosen_cpu not in chosen_cpus:
1369                    chosen_cpus.append(chosen_cpu)
1370        except ValueError:
1371            raise ArgumentError("Invalid CPU number specified.  Valid range is 0..%d" % (kern.globals.real_ncpus - 1))
1372
1373    return chosen_cpus
1374
1375
1376IDX_CPU = 0
1377IDX_RINGPOS = 1
1378IDX_RINGENTRY = 2
1379def Trace_cmd(cmd_args=[], cmd_options={}, headerString=lambda:"", entryString=lambda x:"", ring='', entries_per_cpu=0, max_backtraces=0):
1380    """Generic trace dumper helper function
1381    """
1382
1383    if '-S' in cmd_options:
1384        field_arg = cmd_options['-S']
1385        try:
1386            getattr(kern.PERCPU_GET(ring, 0)[0], field_arg)
1387            sort_key_field_name = field_arg
1388        except AttributeError:
1389            raise ArgumentError("Invalid sort key field name `%s'" % field_arg)
1390    else:
1391            sort_key_field_name = 'start_time_abs'
1392
1393    if '-C' in cmd_options:
1394        chosen_cpus = trace_parse_Copt(cmd_options['-C'])
1395    else:
1396        chosen_cpus = [x for x in range(kern.globals.real_ncpus)]
1397
1398    try:
1399        limit_output_count = int(cmd_options['-N'])
1400    except ValueError:
1401        raise ArgumentError("Invalid output count `%s'" % cmd_options['-N']);
1402    except KeyError:
1403        limit_output_count = None
1404
1405    reverse_sort = '-R' in cmd_options
1406    backtraces = '-B' in cmd_options
1407
1408    # entries will be a list of 3-tuples, each holding the CPU on which the iotrace entry was collected,
1409    # the original ring index, and the iotrace entry.
1410    entries = []
1411    for x in chosen_cpus:
1412        ring_slice = [(x, y, kern.PERCPU_GET(ring, x)[y]) for y in range(entries_per_cpu)]
1413        entries.extend(ring_slice)
1414
1415    total_entries = len(entries)
1416
1417    entries.sort(key=lambda x: getattr(x[IDX_RINGENTRY], sort_key_field_name), reverse=reverse_sort)
1418
1419    if limit_output_count is not None and limit_output_count > total_entries:
1420        print ("NOTE: Output count `%d' is too large; showing all %d entries" % (limit_output_count, total_entries));
1421        limit_output_count = total_entries
1422
1423    if len(chosen_cpus) < kern.globals.real_ncpus:
1424        print("NOTE: Limiting to entries from cpu%s %s" % ("s" if len(chosen_cpus) > 1 else "", str(chosen_cpus)))
1425
1426    if limit_output_count is not None and limit_output_count < total_entries:
1427        entries_to_display = limit_output_count
1428        print("NOTE: Limiting to the %s" % ("first entry" if entries_to_display == 1 else ("first %d entries" % entries_to_display)))
1429    else:
1430        entries_to_display = total_entries
1431
1432    print(headerString())
1433
1434    for x in range(entries_to_display):
1435        print(entryString(entries[x]))
1436
1437        if backtraces:
1438            for btidx in range(max_backtraces):
1439                nextbt = entries[x][IDX_RINGENTRY].backtrace[btidx]
1440                if nextbt == 0:
1441                    break
1442                print("\t" + GetSourceInformationForAddress(nextbt))
1443
1444
1445@lldb_command('iotrace', 'C:N:S:RB')
1446def IOTrace_cmd(cmd_args=[], cmd_options={}):
1447    """ Prints the iotrace ring buffers for all CPUs by default.
1448        Arguments:
1449          -B                              : Print backtraces for each ring entry
1450          -C <cpuSpec#>[,...,<cpuSpec#N>] : Limit trace entries to those generated by the specified CPUs (each cpuSpec can be a
1451                                            single CPU number or a range separated by a dash (e.g. "0-3"))
1452          -N <count>                      : Limit output to the first <count> entries (across all chosen CPUs)
1453          -R                              : Display results in reverse-sorted order (oldest first; default is newest-first)
1454          -S <sort_key_field_name>        : Sort output by specified iotrace_entry_t field name (instead of by timestamp)
1455    """
1456    MAX_IOTRACE_BACKTRACES = 16
1457
1458    if not hasattr(kern.globals, 'iotrace_entries_per_cpu'):
1459        print("Sorry, iotrace is not supported.")
1460        return
1461
1462    if kern.globals.iotrace_entries_per_cpu == 0:
1463        print("Sorry, iotrace is disabled.")
1464        return
1465
1466    hdrString = lambda : "%-19s %-8s %-10s %-20s SZ  %-18s %-17s DATA" % (
1467        "START TIME",
1468        "DURATION",
1469        "CPU#[RIDX]",
1470        "      TYPE",
1471        "   VIRT ADDR",
1472        "   PHYS ADDR")
1473
1474    entryString = lambda x : "%-20u(%6u) %6s[%02d] %-20s %-2d 0x%016x 0x%016x 0x%x" % (
1475        x[IDX_RINGENTRY].start_time_abs,
1476        x[IDX_RINGENTRY].duration,
1477        "CPU%d" % x[IDX_CPU],
1478        x[IDX_RINGPOS],
1479        str(x[IDX_RINGENTRY].iotype).split("=")[1].strip(),
1480        x[IDX_RINGENTRY].size,
1481        x[IDX_RINGENTRY].vaddr,
1482        x[IDX_RINGENTRY].paddr,
1483        x[IDX_RINGENTRY].val)
1484
1485    Trace_cmd(cmd_args, cmd_options, hdrString, entryString, 'iotrace_ring',
1486        kern.globals.iotrace_entries_per_cpu, MAX_IOTRACE_BACKTRACES)
1487
1488
1489@lldb_command('ttrace', 'C:N:S:RB')
1490def TrapTrace_cmd(cmd_args=[], cmd_options={}):
1491    """ Prints the iotrace ring buffers for all CPUs by default.
1492        Arguments:
1493          -B                              : Print backtraces for each ring entry
1494          -C <cpuSpec#>[,...,<cpuSpec#N>] : Limit trace entries to those generated by the specified CPUs (each cpuSpec can be a
1495                                            single CPU number or a range separated by a dash (e.g. "0-3"))
1496          -N <count>                      : Limit output to the first <count> entries (across all chosen CPUs)
1497          -R                              : Display results in reverse-sorted order (oldest first; default is newest-first)
1498          -S <sort_key_field_name>        : Sort output by specified traptrace_entry_t field name (instead of by timestamp)
1499    """
1500    MAX_TRAPTRACE_BACKTRACES = 8
1501
1502    if kern.arch != "x86_64":
1503        print("Sorry, ttrace is an x86-only command.")
1504        return
1505
1506    hdrString = lambda : "%-30s CPU#[RIDX] VECT INTERRUPTED_THREAD PREMLV INTRLV INTERRUPTED_PC" % (
1507        "START TIME   (DURATION [ns])")
1508    entryString = lambda x : "%-20u(%6s) %8s[%02d] 0x%02x 0x%016x %6d %6d %s" % (
1509        x[IDX_RINGENTRY].start_time_abs,
1510        str(x[IDX_RINGENTRY].duration) if hex(x[IDX_RINGENTRY].duration) != "0xffffffffffffffff" else 'inprog',
1511        "CPU%d" % x[IDX_CPU],
1512        x[IDX_RINGPOS],
1513        int(x[IDX_RINGENTRY].vector),
1514        x[IDX_RINGENTRY].curthread,
1515        x[IDX_RINGENTRY].curpl,
1516        x[IDX_RINGENTRY].curil,
1517        GetSourceInformationForAddress(x[IDX_RINGENTRY].interrupted_pc))
1518
1519    Trace_cmd(cmd_args, cmd_options, hdrString, entryString, 'traptrace_ring',
1520        kern.globals.traptrace_entries_per_cpu, MAX_TRAPTRACE_BACKTRACES)
1521
1522# Yields an iterator over all the sysctls from the provided root.
1523# Can optionally filter by the given prefix
1524def IterateSysctls(root_oid, prefix="", depth = 0, parent = ""):
1525    headp = root_oid
1526    for pp in IterateListEntry(headp, 'oid_link', 's'):
1527        node_str = ""
1528        if prefix != "":
1529            node_str = str(pp.oid_name)
1530            if parent != "":
1531                node_str = parent + "." + node_str
1532                if node_str.startswith(prefix):
1533                    yield pp, depth, parent
1534        else:
1535            yield pp, depth, parent
1536        type = pp.oid_kind & 0xf
1537        if type == 1 and pp.oid_arg1 != 0:
1538            if node_str == "":
1539                next_parent = str(pp.oid_name)
1540                if parent != "":
1541                    next_parent = parent + "." + next_parent
1542            else:
1543                next_parent = node_str
1544            # Only recurse if the next parent starts with our allowed prefix.
1545            # Note that it's OK if the parent string is too short (because the prefix might be for a deeper node).
1546            prefix_len = min(len(prefix), len(next_parent))
1547            if next_parent[:prefix_len] == prefix[:prefix_len]:
1548                for x in IterateSysctls(Cast(pp.oid_arg1, "struct sysctl_oid_list *"), prefix, depth + 1, next_parent):
1549                    yield x
1550
1551@lldb_command('showsysctls', 'P:')
1552def ShowSysctls(cmd_args=[], cmd_options={}):
1553    """ Walks the list of sysctl data structures, printing out each during traversal.
1554        Arguments:
1555          -P <string> : Limit output to sysctls starting with the specified prefix.
1556    """
1557    if '-P' in cmd_options:
1558        _ShowSysctl_prefix = cmd_options['-P']
1559        allowed_prefixes = _ShowSysctl_prefix.split('.')
1560        if allowed_prefixes:
1561            for x in range(1, len(allowed_prefixes)):
1562                allowed_prefixes[x] = allowed_prefixes[x - 1] + "." + allowed_prefixes[x]
1563    else:
1564        _ShowSysctl_prefix = ''
1565        allowed_prefixes = []
1566
1567    for sysctl, depth, parentstr in IterateSysctls(kern.globals.sysctl__children, _ShowSysctl_prefix):
1568        if parentstr == "":
1569            parentstr = "<none>"
1570        headp = sysctl
1571        st = (" " * depth * 2) + str(sysctl.GetSBValue().Dereference()).replace("\n", "\n" + (" " * depth * 2))
1572        print('parent = "%s"' % parentstr, st[st.find("{"):])
1573
1574@lldb_command('showexperiments', 'F')
1575def ShowExperiments(cmd_args=[], cmd_options={}):
1576    """ Shows any active kernel experiments being run on the device via trial.
1577        Arguments:
1578        -F: Scan for changed experiment values even if no trial identifiers have been set.
1579    """
1580    treatment_id = str(kern.globals.trial_treatment_id)
1581    experiment_id = str(kern.globals.trial_experiment_id)
1582    deployment_id = kern.globals.trial_deployment_id._GetValueAsSigned()
1583    if treatment_id == "" and experiment_id == "" and deployment_id == -1:
1584        print("Device is not enrolled in any kernel experiments.")
1585        if not '-F' in cmd_options:
1586            return
1587    else:
1588        print("""Device is enrolled in a kernel experiment:
1589    treatment_id: %s
1590    experiment_id: %s
1591    deployment_id: %d""" % (treatment_id, experiment_id, deployment_id))
1592
1593    print("Scanning sysctl tree for modified factors...")
1594
1595    kExperimentFactorFlag = 0x00100000
1596
1597    formats = {
1598            "IU": gettype("unsigned int *"),
1599            "I": gettype("int *"),
1600            "LU": gettype("unsigned long *"),
1601            "L": gettype("long *"),
1602            "QU": gettype("uint64_t *"),
1603            "Q": gettype("int64_t *")
1604    }
1605
1606    for sysctl, depth, parentstr in IterateSysctls(kern.globals.sysctl__children):
1607        if sysctl.oid_kind & kExperimentFactorFlag:
1608            spec = cast(sysctl.oid_arg1, "struct experiment_spec *")
1609            # Skip if arg2 isn't set to 1 (indicates an experiment factor created without an experiment_spec).
1610            if sysctl.oid_arg2 == 1:
1611                if spec.modified == 1:
1612                    fmt = str(sysctl.oid_fmt)
1613                    ptr = spec.ptr
1614                    t = formats.get(fmt, None)
1615                    if t:
1616                        value = cast(ptr, t)
1617                    else:
1618                        # Unknown type
1619                        continue
1620                    name = str(parentstr) + "." + str(sysctl.oid_name)
1621                    print("%s = %d (Default value is %d)" % (name, dereference(value), spec.original_value))
1622
1623
1624from memory import *
1625from process import *
1626from ipc import *
1627from pmap import *
1628from ioreg import *
1629from mbufs import *
1630from net import *
1631from skywalk import *
1632from kext import *
1633from kdp import *
1634from userspace import *
1635from pci import *
1636from scheduler import *
1637from recount import *
1638from misc import *
1639from apic import *
1640from structanalyze import *
1641from ipcimportancedetail import *
1642from bank import *
1643from turnstile import *
1644from kasan import *
1645from waitq import *
1646from usertaskgdbserver import *
1647from ktrace import *
1648from microstackshot import *
1649from xnutriage import *
1650from kmtriage import *
1651from kevent import *
1652from workqueue import *
1653from ulock import *
1654from ntstat import *
1655from zonetriage import *
1656from sysreg import *
1657from counter import *
1658from refgrp import *
1659from workload import *
1660from log import showLogStream, show_log_stream_info
1661from nvram import *
1662from exclaves import *
1663from memorystatus import *
1664from vm_pageout import *
1665from taskinfo import *
1666