blob: d027ce4af6d30c69752c080cfc38fa9e2958d183 [file] [log] [blame]
# Copyright 2016 The Fuchsia Authors
#
# Use of this source code is governed by a MIT-style
# license that can be found in the LICENSE file or at
# https://opensource.org/licenses/MIT
# GDB support for zircon kernel.
# TODO(dje): gdb should let us use a better command class than COMMAND_DATA.
# TODO(dje): Add arm64 support.
from __future__ import absolute_import
from __future__ import division
from __future__ import print_function
import sys
import gdb
import gdb.printing
import re
from gdb.unwinder import Unwinder
if sys.version_info > (3,):
long = int
# The command prefix, passed in from gdbinit.py.
_ZIRCON_COMMAND_PREFIX = "zircon"
_KERNEL_EXCEPTION_UNWINDER_PARAMETER = "kernel-exception-unwinder"
_THREAD_MAGIC = 0x74687264
_BOOT_MAGIC = 0x544f4f42
_KERNEL_BASE_ADDRESS = "KERNEL_BASE_ADDRESS"
print("Loading zircon.elf-gdb.py ...")
def _get_architecture():
# TODO(dje): gdb doesn't provide us with a simple way to do this.
return gdb.execute("show architecture", to_string=True)
def _is_x86_64():
"""Return True if we're on an x86-64 platform."""
return re.search(r"x86-64", _get_architecture())
def _is_arm64():
"""Return True if we're on an aarch64 platform."""
return re.search(r"aarch64", _get_architecture())
# The default is 2 seconds which is too low.
# But don't override something the user set.
# [If the user set it to the default, too bad. :-)]
# TODO(dje): Alas this trips over upstream PR 20084.
#_DEFAULT_GDB_REMOTETIMEOUT = 2
#if int(gdb.parameter("remotetimeout")) == _DEFAULT_GDB_REMOTETIMEOUT:
# gdb.execute("set remotetimeout 10")
class _ZirconPrefix(gdb.Command):
"""zircon command prefix"""
def __init__(self):
super(_ZirconPrefix, self).__init__(
"%s" % (_ZIRCON_COMMAND_PREFIX), gdb.COMMAND_DATA, prefix=True)
class _InfoZircon(gdb.Command):
"""info zircon command prefix"""
def __init__(self):
super(_InfoZircon, self).__init__(
"info %s" % (_ZIRCON_COMMAND_PREFIX), gdb.COMMAND_DATA, prefix=True)
class _SetZircon(gdb.Command):
"""set zircon command prefix"""
def __init__(self):
super(_SetZircon, self).__init__(
"set %s" % (_ZIRCON_COMMAND_PREFIX), gdb.COMMAND_DATA, prefix=True)
class _ShowZircon(gdb.Command):
"""show zircon command prefix"""
def __init__(self):
super(_ShowZircon, self).__init__(
"show %s" % (_ZIRCON_COMMAND_PREFIX), gdb.COMMAND_DATA, prefix=True)
class _ZirconMaxInfoThreads(gdb.Parameter):
"""Parameter to limit output of "info zircon threads" command.
This parameter is an escape hatch to catch corrupted lists.
We don't want "info zircon threads" to loop forever.
The value is the maximum number of threads that will be printed.
"""
set_doc = "Set the maximum number of zircon threads to print."
show_doc = "Show the maximum number of zircon threads to print."
def __init__(self):
super(_ZirconMaxInfoThreads, self).__init__(
"%s max-info-threads" % (_ZIRCON_COMMAND_PREFIX), gdb.COMMAND_DATA,
gdb.PARAM_UINTEGER)
self.value = 1000
def get_show_string(self, pvalue):
return "Maximum number of threads to print is " + pvalue + "."
def get_set_string(self):
# Ugh. There doesn't seem to be a way to implement a gdb parameter in
# Python that will be silent when the user changes the value.
if self.value is None:
value = "unlimited"
else:
value = self.value
return "Maximum number of threads to print been set to %s." % (value)
class _ZirconMaxInfoProcesses(gdb.Parameter):
"""Parameter to limit output of "info zircon processes" command.
This parameter is an escape hatch to catch corrupted lists.
We don't want "info zircon processes" to loop forever.
The value is the maximum number of processes that will be printed.
"""
set_doc = "Set the maximum number of zircon processes to print."
show_doc = "Show the maximum number of zircon processes to print."
def __init__(self):
super(_ZirconMaxInfoProcesses, self).__init__(
"%s max-info-processes" % (_ZIRCON_COMMAND_PREFIX),
gdb.COMMAND_DATA, gdb.PARAM_UINTEGER)
self.value = 1000
def get_show_string(self, pvalue):
return "Maximum number of processes to print is " + pvalue + "."
def get_set_string(self):
# Ugh. There doesn't seem to be a way to implement a gdb parameter in
# Python that will be silent when the user changes the value.
if self.value is None:
value = "unlimited"
else:
value = self.value
return "Maximum number of processes to print been set to %s." % (value)
class _ZirconMaxInfoHandles(gdb.Parameter):
"""Parameter to limit output of "info zircon handles" command.
This parameter is an escape hatch to catch corrupted lists.
We don't want "info zircon handles" to loop forever.
The value is the maximum number of handles that will be printed.
"""
set_doc = "Set the maximum number of zircon handles to print."
show_doc = "Show the maximum number of zircon handles to print."
def __init__(self):
super(_ZirconMaxInfoHandles, self).__init__(
"%s max-info-handles" % (_ZIRCON_COMMAND_PREFIX), gdb.COMMAND_DATA,
gdb.PARAM_UINTEGER)
self.value = 1000
def get_show_string(self, pvalue):
return "Maximum number of handles to print is " + pvalue + "."
def get_set_string(self):
# Ugh. There doesn't seem to be a way to implement a gdb parameter in
# Python that will be silent when the user changes the value.
if self.value is None:
value = "unlimited"
else:
value = self.value
return "Maximum number of handles to print been set to %s." % (value)
def containerof(node_ptr, type_name, member_name):
"""Python version of zircon's containerof macro."""
# TODO(dje): This could only be computed once.
# For more popular types, compute all possible once.
char_ptr = gdb.lookup_type("char").pointer()
ptr = node_ptr.cast(char_ptr)
type_object_ptr = gdb.lookup_type(type_name).pointer()
offsetof = long(gdb.Value(0).cast(type_object_ptr)[member_name].address)
return (ptr - offsetof).cast(type_object_ptr)
def _build_zircon_pretty_printers():
pp = gdb.printing.RegexpCollectionPrettyPrinter("zircon")
# Insert printer registration here.
#pp.add_printer("foo", "^foo$", _ZirconFooPrinter)
return pp
def register_zircon_pretty_printers(obj):
if obj is None:
obj = gdb
gdb.printing.register_pretty_printer(
obj, _build_zircon_pretty_printers(), replace=True)
def _get_thread_list():
""" Return a list of all Thread threads.
The result is constrained by "zircon max-info-threads".
"""
threads = []
head = gdb.parse_and_eval("&thread_list")
t = head["next"]
count = 0
max_threads = gdb.parameter(
"%s max-info-threads" % (_ZIRCON_COMMAND_PREFIX))
int_type = gdb.lookup_type("int")
ptr_size = int_type.pointer().sizeof
# Note: A "corrupted" list can happen for a short time while an
# element is being added/removed. And, in non-stop mode, the list can
# change while we're at it. This isn't a problem in all-stop mode, but
# non-stop mode is generally preferable. We'll see how this works in
# practice.
while t and t != head:
if max_threads is not None and count >= max_threads:
break
# Catch misaligned pointers.
# Casting to int shouldn't be necessary, but the python API doesn't
# support creating an int from a pointer.
# We assume the object is aligned at least as great as a pointer.
if (t.cast(int_type) & (ptr_size - 1)) != 0:
break
# TODO(dje): Do a range check?
thread_ptr = containerof(t, "thread", "thread_list_node")
if thread_ptr["magic"] != _THREAD_MAGIC:
break
# TODO(dje): Move this to a routine, more list printers will want this.
threads.append(thread_ptr)
t = t["next"]
count += 1
return threads
def _get_process_list():
"""Return list of all processes.
The result is constrained by "zircon max-info-processes".
"""
processes = []
head = gdb.parse_and_eval("&process_list")
head = head["head_"]
p = head
count = 0
max_processes = gdb.parameter(
"%s max-info-processes" % (_ZIRCON_COMMAND_PREFIX))
int_type = gdb.lookup_type("int")
ptr_size = int_type.pointer().sizeof
# Note: A "corrupted" list can happen for a short time while an
# element is being added/removed. And, in non-stop mode, the list can
# change while we're at it. This isn't a problem in all-stop mode, but
# non-stop mode is generally preferable. We'll see how this works in
# practice.
while p:
if max_processes is not None and count >= max_processes:
break
# Catch misaligned pointers.
# Casting to int shouldn't be necessary, but the python API doesn't
# support creating an int from a pointer.
# We assume the object is aligned at least as great as a pointer.
if (p.cast(int_type) & (ptr_size - 1)) != 0:
break
# TODO(dje): Do a range check?
# TODO(dje): Move this to a routine, more list printers will want this.
processes.append(p)
p = p["next_"]
count += 1
if p == head:
break
return processes
def _get_handle_list(process):
"""Return list of all handles of process.
The result is constrained by "zircon max-info-handles".
"""
handles = []
head = process["handles_"]
head = head["head_"]
h = head
count = 0
max_handles = gdb.parameter(
"%s max-info-handles" % (_ZIRCON_COMMAND_PREFIX))
int_type = gdb.lookup_type("int")
ptr_size = int_type.pointer().sizeof
# Note: A "corrupted" list can happen for a short time while an
# element is being added/removed. And, in non-stop mode, the list can
# change while we're at it. This isn't a problem in all-stop mode, but
# non-stop mode is generally preferable. We'll see how this works in
# practice.
while h:
if max_handles is not None and count >= max_handles:
break
# Catch misaligned pointers.
# Casting to int shouldn't be necessary, but the python API doesn't
# support creating an int from a pointer.
# We assume the object is aligned at least as great as a pointer.
if (h.cast(int_type) & (ptr_size - 1)) != 0:
break
# TODO(dje): Do a range check?
# TODO(dje): Move this to a routine, more list printers will want this.
handles.append(h)
h = h["next_"]
count += 1
if h == head:
break
return handles
def _print_thread_summary(thread, number, tls_entry, user_thread_ptr_t):
user_thread = thread["tls"][tls_entry]
if user_thread:
user_thread_ptr = user_thread.cast(user_thread_ptr_t)
# TODO(dje): Why is str necessary here? Otherwise ->
# "Cannot convert value to int."
pid = str(user_thread_ptr["process_"]["id_"])
else:
pid = "kern"
name = str(thread["name"].lazy_string().value()).strip('"')
print(
"%3d %5s %#16x %-32s %s" %
(number, pid, thread.address, name, thread["state"]))
class _InfoZirconThreads(gdb.Command):
"""info zircon threads command
This command prints a list of all zircon threads.
TODO: Allow specifying which threads to print.
"""
def __init__(self):
super(_InfoZirconThreads, self).__init__(
"info %s threads" % (_ZIRCON_COMMAND_PREFIX), gdb.COMMAND_USER)
def invoke(self, arg, from_tty):
# Do this first to make sure the previous value gets cleared out.
# There's no way to unset a convenience var, so KISS.
gdb.execute("set $zx_threads = (Thread*[1]) { 0 }")
tls_entry_lkuser = gdb.parse_and_eval("TLS_ENTRY_LKUSER")
threads = _get_thread_list()
num_threads = len(threads)
# The array is origin-1-indexed. Have a null first entry to KISS.
gdb.execute("set $zx_threads = (Thread*[%d]) { 0 }" % (num_threads + 1))
# Populate the array first, before printing the summary, to make sure this
# gets done even if there's an error during printing.
num = 1
for thread_ptr in threads:
gdb.execute(
"set $zx_threads[%d] = (Thread*) %u" % (num, thread_ptr))
num += 1
# Translating gdb values to python often trips over these. Heads up.
save_print_address = "yes" if gdb.parameter("print address") else "no"
save_print_symbol = "yes" if gdb.parameter("print symbol") else "no"
gdb.execute("set print address off")
gdb.execute("set print symbol off")
print(
"%3s %5s %-18s %-32s %s" %
("Num", "Pid", "Thread*", "Name", "State"))
# Make sure we restore these when we're done.
try:
user_thread_ptr_t = gdb.lookup_type("UserThread").pointer()
num = 1
for thread_ptr in threads:
# TODO(dje): remove dereference
_print_thread_summary(
thread_ptr.dereference(), num, tls_entry_lkuser,
user_thread_ptr_t)
num += 1
finally:
gdb.execute("set print address %s" % (save_print_address))
gdb.execute("set print symbol %s" % (save_print_symbol))
if num_threads:
print("Note: Each thread is now available in $zx_threads[num].")
else:
print("<no threads>")
def _print_process_summary(process, number):
state = str(process["state_"])
state = state.replace("ProcessDispatcher::", "")
print("%3d %#16x %4u %s" % (number, process.address, process["id_"], state))
class _InfoZirconProcesses(gdb.Command):
"""info zircon processes command
This command prints a list of all zircon processes.
TODO: Allow specifying which processes to print.
"""
def __init__(self):
super(_InfoZirconProcesses, self).__init__(
"info %s processes" % (_ZIRCON_COMMAND_PREFIX), gdb.COMMAND_USER)
def invoke(self, arg, from_tty):
# Do this first to make sure the previous value gets cleared out.
# There's no way to unset a convenience var, so KISS.
gdb.execute("set $zx_processes = (ProcessDispatcher*[1]) { 0 }")
tls_entry_lkuser = gdb.parse_and_eval("TLS_ENTRY_LKUSER")
processes = _get_process_list()
num_processes = len(processes)
# The array is origin-1-indexed. Have a null first entry to KISS.
gdb.execute(
"set $zx_processes = (ProcessDispatcher*[%d]) { 0 }" %
(num_processes + 1))
# Populate the array first, before printing the summary, to make sure this
# gets done even if there's an error during printing.
num = 1
for process_ptr in processes:
gdb.execute(
"set $zx_processes[%d] = (ProcessDispatcher*) %u" %
(num, process_ptr))
num += 1
# Translating gdb values to python often trips over these. Heads up.
save_print_address = "yes" if gdb.parameter("print address") else "no"
save_print_symbol = "yes" if gdb.parameter("print symbol") else "no"
gdb.execute("set print address off")
gdb.execute("set print symbol off")
print(
"%3s %-18s %4s %s" % ("Num", "ProcessDispatcher*", "Pid", "State"))
# Make sure we restore these when we're done.
try:
num = 1
for process_ptr in processes:
_print_process_summary(process_ptr.dereference(), num)
num += 1
finally:
gdb.execute("set print address %s" % (save_print_address))
gdb.execute("set print symbol %s" % (save_print_symbol))
if num_processes:
print("Note: Each process is now available in $zx_processes[num].")
else:
print("<no processes>")
def _print_handle_summary(handle, number):
process_id = handle["process_id_"]
rights = handle["rights_"]
dispatcher = handle["dispatcher_"]["ptr_"]
# TODO(dje): This is a hack to get the underlying type from the vtable.
# The python API should support this directly.
dispatcher_text = gdb.execute(
"output *(Dispatcher*) %s" % (dispatcher), to_string=True)
dispatcher_split_text = dispatcher_text.split(" ", 1)
if len(dispatcher_split_text) == 2:
dispatcher_type = dispatcher_split_text[0].strip("()")
else:
dispatcher_type = "Dispatcher"
dispatcher_text = "(%s*) %s" % (dispatcher_type, dispatcher)
print(
" %3d %-18s %4u %#8x %s" %
(number, handle.address, process_id, rights, dispatcher_text))
class _InfoZirconHandles(gdb.Command):
"""info zircon handles command
This command prints a list of all zircon handles.
TODO: Allow specifying which handles to print.
"""
def __init__(self):
super(_InfoZirconHandles, self).__init__(
"info %s handles" % (_ZIRCON_COMMAND_PREFIX), gdb.COMMAND_USER)
def invoke(self, arg, from_tty):
processes = _get_process_list()
# Translating gdb values to python often trips over these. Heads up.
save_print_address = "yes" if gdb.parameter("print address") else "no"
save_print_symbol = "yes" if gdb.parameter("print symbol") else "no"
gdb.execute("set print address on")
gdb.execute("set print symbol off")
# Make sure we restore these when we're done.
try:
for p in processes:
handles = _get_handle_list(p)
num_handles = len(handles)
print("Process %u" % (p["id_"]))
print(
" %3s %-18s %4s %8s %s" %
("Num", "Handle*", "Pid", "Rights", "Dispatcher"))
num = 1
for handle_ptr in handles:
_print_handle_summary(handle_ptr.dereference(), num)
num += 1
if not num_handles:
print(" <no handles>")
finally:
gdb.execute("set print address %s" % (save_print_address))
gdb.execute("set print symbol %s" % (save_print_symbol))
class _ZirconKernelExceptionUnwinder(gdb.Parameter):
"""Parameter to enable zircon kernel exception unwinding.
This parameter is an escape hatch in case there are problems with the unwinder.
N.B. Perhaps this command should flush registers.
It doesn't now to avoid side-effects, and the user is responsible for typing
"flushregs" if s/he wants to reprint a recent backtrace.
"""
set_doc = "Set whether the zircon kernel exception unwinder is enabled."
show_doc = "Show whether the zircon kernel exception unwinder is enabled."
def __init__(self):
super(_ZirconKernelExceptionUnwinder, self).__init__(
"%s %s" %
(_ZIRCON_COMMAND_PREFIX, _KERNEL_EXCEPTION_UNWINDER_PARAMETER),
gdb.COMMAND_DATA, gdb.PARAM_BOOLEAN)
self.value = True
def get_show_string(self, pvalue):
value = "enabled" if self.value else "disabled"
return "The kernel exception unwinder is %s." % (value)
def get_set_string(self):
# Ugh. There doesn't seem to be a way to implement a gdb parameter in
# Python that will be silent when the user changes the value.
value = "enabled" if self.value else "disabled"
return "The kernel exception unwinder is %s." % (value)
class _Amd64KernelExceptionUnwinder(Unwinder):
# See arch/x86/64/exceptions.S.
AT_IFRAME_SETUP = "interrupt_common_iframe_set_up_for_debugger"
INTERRUPT_COMMON = "interrupt_common"
class FrameId(object):
def __init__(self, sp, pc):
self.sp = sp
self.pc = pc
@staticmethod
def lookup_minsym(minsym_name):
"""Return the address of "minimal symbol" minsym_name."""
# TODO(dje): What's here now is a quick hack to get things going.
# GDB's python API doesn't yet provide the ability to look up minsyms.
try:
output = gdb.execute("output %s" % (minsym_name), to_string=True)
except (gdb.error):
return None
symbol_value = None
if not output.startswith("No symbol"):
symbol_match = re.search(r"0x[0-9a-f]+", output)
if symbol_match is not None:
symbol_value = long(symbol_match.group(0), 16)
return symbol_value
@staticmethod
def is_user_space(iframe):
"""Return True if iframe is from user space."""
# See arch/x86/include/arch/x86/descriptor.h:SELECTOR_PL.
return (iframe["cs"] & 3) != 0
def __init__(self):
super(_Amd64KernelExceptionUnwinder,
self).__init__("AMD64 kernel exception unwinder")
# We assume uintptr_t is present in the debug info.
# We *could* use unsigned long, but it's a bit of an obfuscation.
self.uintptr_t = gdb.lookup_type("uintptr_t")
# We assume "unsigned int" is 32 bits.
self.uint32_t = gdb.lookup_type("unsigned int")
self.iframe_ptr_t = gdb.lookup_type("iframe_t").pointer()
# We need to know when the pc is at the point where it has called
# x86_exception_handler.
self.at_iframe_setup = self.lookup_minsym(
_Amd64KernelExceptionUnwinder.AT_IFRAME_SETUP)
self.interrupt_common_begin_addr = self.lookup_minsym(
_Amd64KernelExceptionUnwinder.INTERRUPT_COMMON)
def is_in_icommon(self, pc):
"""Return True if pc is in the interrupt_common function."""
# First do the preferred test.
# If the pc is here then we've called into x86_exception_handler.
if self.at_iframe_setup is not None and pc == self.at_iframe_setup:
return True
# Fall back to this in case the special symbol doesn't exist.
if self.interrupt_common_begin_addr is None:
return False
end_addr = self.interrupt_common_begin_addr + 64
return pc >= self.interrupt_common_begin_addr and pc < end_addr
def __call__(self, pending_frame):
try:
# Punt if disabled.
if not gdb.parameter(
"%s %s" %
(_ZIRCON_COMMAND_PREFIX, _KERNEL_EXCEPTION_UNWINDER_PARAMETER)):
return None
# Note: We use rip,rsp here instead of pc,sp to work around bug 20128
#pc = pending_frame.read_register("pc").cast(self.uintptr_t)
pc = pending_frame.read_register("rip").cast(self.uintptr_t)
#print "icommon unwinder, pc = %#x" % (long(str(pc))) # work around bug 20126
if not self.is_in_icommon(pc):
return None
sp = pending_frame.read_register("rsp").cast(self.uintptr_t)
#print "icommon unwinder, sp = %#x" % (long(str(sp)))
iframe = sp.cast(self.iframe_ptr_t)
# This is only for kernel faults, not user-space ones.
if self.is_user_space(iframe):
return None
frame_id = self.FrameId(sp, pc)
unwind_info = pending_frame.create_unwind_info(frame_id)
unwind_info.add_saved_register("rip", iframe["ip"])
unwind_info.add_saved_register("rsp", iframe["user_sp"])
unwind_info.add_saved_register("rax", iframe["rax"])
unwind_info.add_saved_register("rbx", iframe["rbx"])
unwind_info.add_saved_register("rcx", iframe["rcx"])
unwind_info.add_saved_register("rdx", iframe["rdx"])
unwind_info.add_saved_register("rbp", iframe["rbp"])
unwind_info.add_saved_register("rsi", iframe["rsi"])
unwind_info.add_saved_register("r8", iframe["r8"])
unwind_info.add_saved_register("r9", iframe["r9"])
unwind_info.add_saved_register("r10", iframe["r10"])
unwind_info.add_saved_register("r11", iframe["r11"])
unwind_info.add_saved_register("r12", iframe["r12"])
unwind_info.add_saved_register("r13", iframe["r13"])
unwind_info.add_saved_register("r14", iframe["r14"])
unwind_info.add_saved_register("r15", iframe["r15"])
# Flags is recorded as 64 bits, but gdb needs to see 32.
unwind_info.add_saved_register(
"eflags", iframe["flags"].cast(self.uint32_t))
#print "Unwind info:"
#print unwind_info
return unwind_info
except (gdb.error, RuntimeError):
return None
_ZirconPrefix()
_InfoZircon()
_SetZircon()
_ShowZircon()
_ZirconMaxInfoThreads()
_ZirconMaxInfoProcesses()
_ZirconMaxInfoHandles()
_InfoZirconThreads()
_InfoZirconProcesses()
_InfoZirconHandles()
_ZirconKernelExceptionUnwinder()
_ull = gdb.lookup_type("unsigned long long")
_uint = gdb.lookup_type("unsigned int")
def _cast(value, t, shift=64):
return int(value.cast(t)) & ((1 << shift) - 1)
def _cast_ull(value):
""" Cast a value to unsigned long long """
global _ull
return _cast(value, _ull)
def _cast_uint(value):
""" Cast a value to unsigned int"""
global _uint
return _cast(value, _uint, 32)
def _read_symbol_address(name):
""" Read the address of a symbol """
addr = gdb.parse_and_eval("&" + name)
try:
if addr is not None:
return _cast_ull(addr)
except gdb.MemoryError:
pass
print("Can't find %s to lookup KASLR relocation" % name)
return None
def _read_pointer(addr):
""" Read a pointer in an address """
value = gdb.parse_and_eval("*(unsigned long*)0x%x" % addr)
try:
if value is not None:
return _cast_ull(value)
except gdb.MemoryError:
pass
print("Can't read 0x%x to lookup KASLR ptr value" % addr)
return None
def _read_uint(addr):
""" Read a uint """
value = gdb.parse_and_eval("*(unsigned int*)0x%x" % addr)
try:
if value is not None:
return _cast_uint(value)
except gdb.MemoryError:
pass
print("Can't read 0x%x to lookup KASLR uint value" % addr)
return None
def _offset_symbols_and_breakpoints(kernel_relocated_base=None):
""" Using the KASLR relocation address, reload symbols and breakpoints """
print("Update symbols and breakpoints for KASLR")
base_address = _read_symbol_address(_KERNEL_BASE_ADDRESS)
if not base_address:
return False
load_start = _read_symbol_address("IMAGE_LOAD_START")
if not load_start:
return False
if not kernel_relocated_base:
kernel_relocated_base = _read_symbol_address("kernel_relocated_base")
if not kernel_relocated_base:
return False
relocated = _read_pointer(kernel_relocated_base)
if not relocated:
print("Failed to fetch KASLR base address")
return False
# There is no api for symbol management.
# Everything has to be done by custom commands
sym = gdb.execute("info target", to_string=True)
m = re.match("^Symbols from \"([^\"]+)\"", sym)
if not m:
print("Error: Cannot find the target symbol")
return False
sym_path = m.group(1)
# Identify all section addresses
x = gdb.execute("info target", to_string=True)
m = re.findall("\s0x([a-f0-9]+) - 0x[a-f0-9]+ is (.*)", x)
offset = base_address - relocated
sections = dict([(name, int(addr, 16)) for addr, name in m])
if len(sections) == 0:
print("Error: Failed to find sections in binary")
return False
if ".text" not in sections:
print("Error: Failed to find .text section")
return False
# Do not prompt the user
confirm_was_enabled = gdb.parameter("confirm")
if confirm_was_enabled:
gdb.execute("set confirm off", to_string=True)
# Disable auto loading to prevent the script to be reloaded
auto_loading_enabled = gdb.parameter("auto-load python-scripts")
if auto_loading_enabled:
gdb.execute("set auto-load python-scripts off", to_string=True)
# Remove all symbols
gdb.execute("symbol-file", to_string=True)
# Update all addresses to the relocated address
sections = dict(
[(name, addr - offset) for name, addr in sections.items()])
text_addr = sections[".text"]
del sections[".text"]
args = ["-s %s 0x%x" % (name, addr) for name, addr in sections.items()]
# Reload the ELF with all sections set
gdb.execute("add-symbol-file \"%s\" 0x%x -readnow %s" \
% (sym_path, text_addr, " ".join(args)), to_string=True)
if auto_loading_enabled:
gdb.execute("set auto-load python-scripts on", to_string=True)
if confirm_was_enabled:
gdb.execute("set confirm on", to_string=True)
# Verify it works as expected
code_start = _read_symbol_address("__code_start")
if not kernel_relocated_base:
return False
expected = relocated + (load_start - base_address)
if code_start != expected:
print("Error: Incorrect relocation for __code_start 0x%x vs 0x%x" \
% (expected, code_start))
return False
print("KASLR: Correctly reloaded kernel at 0x%x" % relocated)
return True
class KASLRBreakpoint(gdb.Breakpoint):
""" Helper class to setup breakpoints to load symbols for KASLR. """
def __init__(self):
self._is_valid = False
def is_valid(self):
if gdb.Breakpoint.is_valid(self) == False:
return False
return self._is_valid
def stop(self):
# A breakpoint cannot change anything so get callback on the following stop
gdb.events.stop.connect(self._stop_callback_internal)
return True
# Callback through events so the state can be changed
def _stop_callback_internal(self, event):
gdb.events.stop.disconnect(self._stop_callback_internal)
self.delete()
# Call the top callback
self._stop_callback(event)
class KASLRBootWatchpoint(KASLRBreakpoint):
""" Watchpoint to catch read access to KASLR relocated address
The assumption is the address was written before it is read. It is an
architecture independent way to start at the right moment if the relocated
KASLR symbol name is the same.
"""
def __init__(self, pc):
KASLRBreakpoint.__init__(self)
base_address = _read_symbol_address(_KERNEL_BASE_ADDRESS)
if not base_address:
return
kernel_relocated_base = _read_symbol_address("kernel_relocated_base")
if not kernel_relocated_base:
return
self._relocated_base_offset = kernel_relocated_base
if _is_x86_64():
# x86_64 uses the physical address
self._relocated_base_offset -= base_address
elif _is_arm64():
# Search from pc to find BOOT tag
found = False
for addr in range(pc, pc + 0x10000000, 0x10000):
data = _read_uint(addr)
if data == _BOOT_MAGIC:
self._relocated_base_offset -= base_address
self._relocated_base_offset += addr
found = True
break
if found == False:
print("Error: Could not found BOOT_MAGIC")
return
self._is_valid = True
gdb.Breakpoint.__init__(
self,
"*0x%x" % self._relocated_base_offset,
gdb.BP_WATCHPOINT,
gdb.WP_READ,
internal=True)
self.silence = True
# Callback from KASLRBreakpoint when it is safe to change state
def _stop_callback(self, event):
# Load symbols using load_offset
_offset_symbols_and_breakpoints(self._relocated_base_offset)
class KASLRStartBreakpoint(KASLRBreakpoint):
""" Breakpoint to catch _start
The ZBI boot path result with memory being moved before _start on x86_64.
In this case, break on _start before setting up the watchpoint.
"""
def __init__(self):
KASLRBreakpoint.__init__(self)
base_address = _read_symbol_address(_KERNEL_BASE_ADDRESS)
if not base_address:
return
_start = _read_symbol_address("_start")
if not _start:
return
# Get the physical address
_start -= base_address
self._is_valid = True
gdb.Breakpoint.__init__(self, "*0x%x" % _start, internal=True)
self.silence = True
# Callback from KASLRBreakpoint when it is safe to change state
def _stop_callback(self, event):
pc = _cast_ull(gdb.parse_and_eval("$pc"))
x = KASLRBootWatchpoint(pc)
if not x.is_valid():
print("Error: Failed create KASLR boot watchpoint after _stat")
return
print("Watchpoint set on KASLR relocated base variable")
gdb.execute("continue")
def _align(addr, shift):
b64 = (1 << 64) - 1
mask = (1 << shift) - 1
return addr & (~mask & b64)
def _identify_offset_to_reload(pc):
""" From $pc, search the base address to the page size
Used when attaching to an existing debugging session
"""
print("Search KASLR base address based on $pc")
if (pc >> 63) != 1:
print("Error: Didn't break into kernel code")
return False
base_address = _read_symbol_address(_KERNEL_BASE_ADDRESS)
if not base_address:
return False
end = _read_symbol_address("_end")
if not end:
return False
kernel_relocated_base = _read_symbol_address("kernel_relocated_base")
if not kernel_relocated_base:
return False
offset = kernel_relocated_base - base_address
max_size = end - base_address
# Search base+offset == base for each previous page until the max size
found = False
addr = pc
while addr > (pc - max_size):
addr = _align(addr - 1, 12)
target = addr + offset
value = _read_pointer(target)
if value == None:
break
if addr == value:
found = True
break
if found == False:
print("Error: Failed to find the KASLR relocation from the $pc")
return False
return _offset_symbols_and_breakpoints(target)
def _is_earlyboot_pc(pc):
""" Early boot if the top 32-bit is zero """
return not (pc >> 32)
def _KASLR_stop_event(event):
""" Called on first stop after debugger started """
gdb.events.stop.disconnect(_KASLR_stop_event)
pc = _cast_ull(gdb.parse_and_eval("$pc"))
if not _is_earlyboot_pc(pc):
# If not early boot, try to find the kernel base and adapt
_identify_offset_to_reload(pc)
return
x = None
# ARM64 map _start at random place in memory, directly use the watchpoint
if _is_arm64():
x = KASLRBootWatchpoint(pc)
else:
x = KASLRStartBreakpoint()
if not x.is_valid():
print("Error: Failed create KASLR boot first breakpoint/watchpoint")
return
gdb.execute("continue")
def _install():
current_objfile = gdb.current_objfile()
# gdb.current_objfile() is only set when autoloading;
# otherwise we can try to assume the first loaded objfile is the kernel.
if not current_objfile and gdb.objfiles():
current_objfile = gdb.objfiles()[0]
if not current_objfile:
print("Warning: no object file set")
return
register_zircon_pretty_printers(current_objfile)
if current_objfile is not None and _is_x86_64():
gdb.unwinder.register_unwinder(
current_objfile, _Amd64KernelExceptionUnwinder(), True)
print(
"Zircon extensions installed for {}".format(
current_objfile.filename))
if not _is_x86_64() and not _is_arm64():
print(
"Warning: Unsupported architecture, KASLR support will be experimental"
)
gdb.events.stop.connect(_KASLR_stop_event)
_install()