Wednesday, May 25, 2011

Using GDB to debug UEFI.

GDB, the GNU debugger, is a natural choice for debugging TianoCore EDK2 firmware. In the context of playing around with OVMF (the virtual machine EDK2 target) on QEMU or Xen, you can use the QEMU/Xen debugging support.

On QEMU that means passing the '-s' option. For OVMF that command to invoke would be something like -
fjnh84@fjnh84-desktop:~/mine/edk2/OvmfPkg$ ./ -A IA32 qemu -s
Initializing workspace
Loading previous configuration from $WORKSPACE/Conf/
WORKSPACE: /home/fjnh84/mine/edk2
EDK_TOOLS_PATH: /home/fjnh84/mine/edk2/BaseTools
using prebuilt tools
Running: qemu -L /home/fjnh84/mine/edk2/Build/OvmfIa32/DEBUG_GCC44/QEMU -hda fat:/home/fjnh84/mine/edk2/Build/OvmfIa32/DEBUG_GCC44/IA32 -s
Could not open option rom 'vapic.bin': No such file or directory
pci_add_option_rom: failed to find romfile "pxe-rtl8139.bin"
For Xen it means running the 'gdbsx' tool, something like -
fjnh84@fjnh84-desktop:~/mine/edk2/OvmfPkg$ gdbsx -a domid 32 1234
In either case, then you would attach a debugger with something like -
(gdb) target remote localhost:1234
As per my earlier post you would then reload UEFI symbols, etc.

Then you start running into problems.
(gdb) print gST
$1 = (EFI_SYSTEM_TABLE **) 0

First of all, the values of .bss globals are bogus. If you look at the address of the symbols, it ends up being the unrelocated value relative to the VMA of the symbol file.

Haven't quite root caused the issue (.bss variables don't seem to be adjusted for the actual objfile location), but a simple workaround is to always link with an ld script that forces all sections to go to .text in the output. Watch out for the the COMMON variables too, the existing script doesn't take care of that.

Since I use the GCC44 toolchain, I modified $(EDK_TOOLS_PATH)/Scripts/gcc4.4-ld-script to be like -
 /* . = 0 + SIZEOF_HEADERS; */
 . = 0x280;
 .text ALIGN(0x20) :
   *(.text .stub .text.* .gnu.linkonce.t.*)
   . = ALIGN(0x20);
   *(.rodata .rodata.* .gnu.linkonce.r.*)
   *(.data .data.* .gnu.linkonce.d.*)
   . = ALIGN(0x20);
   *(.bss .bss.* COMMON)
   . = ALIGN(0x20);
   *(.got .got.*)
   . = ALIGN(0x20);
   *(.rela .rela.*)
   . = ALIGN(0x20);
 } =0x90909090
 /DISCARD/ : {
   *(.note.GNU-stack) *(.gnu_debuglink)

And of course fixed my tools_def.txt like this for IA32 -

Additionally, whatever frame you select prior to dumping the address 'gST', you will always get the same value. Problem is, all UEFI code lives in the same address space, but the different drivers are separate programs. GDB expects globals to be unique, and assumes all loaded symbols are part of the same program. When a global is looked up, gdb scans all partial symtabs starting from the first known objfile, which is the first file that was added using 'add-symbol-file', thus it is a first fit type of deal.

I poked around GDB a bit (7.1, a bit old, but w/e), and decided it was reasonably easy to teach GDB the concept of 'symbol file scope', something often referred to as 'module scope' in other debuggers. Ultimately, that meant -
  • Looking in objfile containing current scope block before looking at all objfiles.
  • Modifying lookup_symtab code to also handle symbol file names (only handled source names before).
  • Fixing location completion so auto-complete works correctly for symbol file names.
  • Adding a command to dump loaded symbol files.
  • Modify above command to also dump modules matching a particular source file name.
Whereas before GDB had file scope, e.g.-
(gdb) print 'BdsConnect.c'::BdsLibConnectAllDriversToAllControllers
$11 = {void (void)} 0x17eb2619 <BdsLibConnectAllDriversToAllControllers>
Now you also get symbol file scope -
(gdb) print 'BdsDxe.dll'::BdsLibConnectAllDriversToAllControllers
$12 = {void (void)} 0x17eb2619 <BdsLibConnectAllDriversToAllControllers>
The 'list-symbol-files' command is useful too -
(gdb) list-symbol-files DiskIo.c Dispatcher.c EhciDxe.dll
               .text, [0x17e4c240-0x17e4f440)
               .text, [0x17fb1240-0x17fcb8c0)
               .text, [0x17deb240-0x17df3920)

Patch is at

Works like a charm :-).
(gdb) print *'DxeCore.dll'::gST
$14 = {Hdr = {Signature = 6076298535811760713, Revision = 131102, HeaderSize = 72, CRC32 = 2051810456, Reserved = 0}, FirmwareVendor = 0x17f7f990,
  FirmwareRevision = 65536, ConsoleInHandle = 0x17c06d90, ConIn = 0x17e67424, ConsoleOutHandle = 0x17c06790, ConOut = 0x17e6752c, StandardErrorHandle = 0x17191690,
  StdErr = 0x17e675cc, RuntimeServices = 0x17f7ff10, BootServices = 0x17fc9ae8, NumberOfTableEntries = 6, ConfigurationTable = 0x17f7ed90}
(gdb) print &'DxeCore.dll'::gST
$15 = (EFI_SYSTEM_TABLE **) 0x17fca6f0
(gdb) print &'IsaBusDxe.dll'::gST
$16 = (EFI_SYSTEM_TABLE **) 0x17e21da4

No comments:

Post a Comment