###
### Load debuging information about GNU GRUB 2 modules into GDB
-### automatically. Needs readelf, Python and gdb_helper.py script
+### automatically. Needs readelf, objdump, Python and gdb_helper.py script
###
### Has to be launched from the writable and trusted
### directory containing *.image and *.module
source gdb_helper.py
+define dynamic_load_symbols
+ dynamic_load_kernel_exec_symbols $arg0
+
+ # We may have been very late to loading the kernel.exec symbols and
+ # and modules may already be loaded. So load symbols for any already
+ # loaded.
+ load_all_modules
+
+ if $is_grub_loaded()
+ runtime_load_module
+ end
+end
+document dynamic_load_symbols
+ Load debugging symbols from kernel.exec and any loaded modules given
+ the address of the .text segment of the UEFI binary in memory. Also
+ setup session to automatically load module symbols for modules loaded
+ in the future.
+end
+
define load_all_modules
set $this = grub_dl_head
while ($this != 0)
##### Convenience functions #####
+class IsGrubLoaded (gdb.Function):
+ """Return 1 if GRUB has been loaded in memory, otherwise 0.
+The hueristic used is checking if the first 4 bytes of the memory pointed
+to by the _start symbol are not 0. This is true for QEMU on the first run
+of GRUB. This may not be true on physical hardware, where memory is not
+necessarily cleared on soft reset. This may not also be true in QEMU on
+soft resets. Also this many not be true when chainloading GRUB.
+"""
+
+ def __init__ (self):
+ super (IsGrubLoaded, self).__init__ ("is_grub_loaded")
+
+ def invoke (self):
+ return int (gdb.parse_and_eval ("*(int *) _start")) != 0
+
+is_grub_loaded = IsGrubLoaded ()
+
class IsUserCommand (gdb.Function):
"""Set the second argument to true value if first argument is the name
of a user-defined command.
##### Commands #####
+# Loading symbols is complicated by the fact that kernel.exec is an ELF
+# ELF binary, but the UEFI runtime is PE32+. All the data sections of
+# the ELF binary are concatenated (accounting for ELF section alignment)
+# and put into one .data section of the PE32+ runtime image. So given
+# the load address of the .data PE32+ section we can determine the
+# addresses each ELF data section maps to. The UEFI application is
+# loaded into memory just as it is laid out in the file. It is not
+# assumed that the binary is available, but it is known that the .text
+# section directly precedes the .data section and that .data is EFI
+# page aligned. Using this, the .data offset can be found from the .text
+# address.
+class GrubLoadKernelExecSymbols (gdb.Command):
+ """Load debugging symbols from kernel.exec given the address of the
+.text segment of the UEFI binary in memory."""
+
+ PE_SECTION_ALIGN = 12
+
+ def __init__ (self):
+ super (GrubLoadKernelExecSymbols, self).__init__ ("dynamic_load_kernel_exec_symbols",
+ gdb.COMMAND_USER,
+ gdb.COMPLETE_EXPRESSION)
+
+ def invoke (self, arg, from_tty):
+ self.dont_repeat ()
+ args = gdb.string_to_argv (arg)
+
+ if len (args) != 1:
+ raise RuntimeError ("dynamic_load_kernel_exec_symbols expects exactly one argument")
+
+ sections = self.parse_objdump_sections ("kernel.exec")
+ pe_text = args[0]
+ text_size = [s['size'] for s in sections if s['name'] == '.text'][0]
+ pe_data_offset = self.alignup (text_size, self.PE_SECTION_ALIGN)
+
+ sym_load_cmd_parts = ["add-symbol-file", "kernel.exec", pe_text]
+ offset = 0
+ for section in sections:
+ if 'DATA' in section["flags"] or section["name"] == ".bss":
+ offset = self.alignup (offset, section["align"])
+ sym_load_cmd_parts.extend (["-s", section["name"], "(%s+0x%x+0x%x)" % (pe_text, pe_data_offset, offset)])
+ offset += section["size"]
+ gdb.execute (' '.join (sym_load_cmd_parts))
+
+ @staticmethod
+ def parse_objdump_sections (filename):
+ fields = ("idx", "name", "size", "vma", "lma", "fileoff", "align")
+ re_section = re.compile ("^\s*" + "\s+".join(["(?P<%s>\S+)" % f for f in fields]))
+ c = subprocess.run (["objdump", "-h", filename], text=True, capture_output=True)
+ section_lines = c.stdout.splitlines ()[5:]
+ sections = []
+
+ for i in range (len (section_lines) >> 1):
+ m = re_section.match (section_lines[i * 2])
+ s = dict (m.groupdict ())
+ for f in ("size", "vma", "lma", "fileoff"):
+ s[f] = int (s[f], 16)
+ s["idx"] = int (s["idx"])
+ s["align"] = int (s["align"].split ("**", 1)[1])
+ s["flags"] = section_lines[(i * 2) + 1].strip ().split (", ")
+ sections.append (s)
+ return sections
+
+ @staticmethod
+ def alignup (addr, align):
+ pad = (addr % (1 << align)) and 1 or 0
+ return ((addr >> align) + pad) << align
+
+dynamic_load_kernel_exec_symbols = GrubLoadKernelExecSymbols ()
+
+
class GrubLoadModuleSymbols (gdb.Command):
"""Load module symbols at correct locations.
Takes one argument which is a pointer to a grub_dl_t struct."""