retrage.github.io

Debugging OVMF with GDB

In this blog post, I will describe how to debug OVMF using GDB without any special tool unlike another post[1].

Code Mapping in UEFI

On x64 UEFI, it provides flat single address memory space and place the firmware itself and UEFI images on the space without any memory protection. In this way, we can do source code level debugging any UEFI code with debugger. On OVMF, each feature is modularized and the module is loaded as UEFI image. BootServices is included in DxeCore.efi, loaded at boot time.

Notify: PPI Guid: EE16160A-E8BE-47A6-820A-C6900DB0250A, Peim notify entry point: 836CA9
PlatformPei: ClearCacheOnMpServicesAvailable
DiscoverPeimsAndOrderWithApriori(): Found 0x0 PEI FFS files in the 1th FV
DXE IPL Entry
Loading PEIM D6A2CB7F-6A18-4E2F-B43B-9920A733700A
Loading PEIM at 0x00007EA8000 EntryPoint=0x00007EAB0BC DxeCore.efi
Loading DXE CORE at 0x00007EA8000 EntryPoint=0x00007EAB0BC

Debug Symbols in EDK2

EDK2 build system generates debug symbol information *.debug along with executables *.efi on debug build (-b DEBUG). If you use gcc (example: GCC5), it compiles source code to ELF object files, link with custom linker script, and convert to PE format. Thus, the debug info is for ELF and can be recognized by GDB. On the other hand, Visual Studio and clang/lld (CLANG9)[2] generates PE/COFF directly and the debug info may be PDB.

To summarize, the points are:

  • OVMF code is placed on the flat single memory space.
  • GDB can debug EDK2 UEFI image built with gcc

Let’s take a look at how to debug OVMF.

Building EDK2

Build EDK2 using gcc as usual.

$ git clone git@github.com:tianocore/edk2.git
$ cd edk2
$ git submodule update --init --recursive
$ make -C BaseTools
$ source ./edksetup.sh
$ build -p OvmfPkg/OvmfPkgX64.dsc -b DEBUG -a X64 -t GCC5

To make debugging easy, create a Makefile as follow. Note that we have to connect debugcon at 0x402 to dump debug information (debug.log) from OVMF[4].

#!/usr/bin/env make

SHELL=/bin/bash

LOG=debug.log
OVMFBASE=edk2/Build/OvmfX64/DEBUG_GCC5/
OVMFCODE=$(OVMFBASE)/FV/OVMF_CODE.fd
OVMFVARS=$(OVMFBASE)/FV/OVMF_VARS.fd
QEMU=qemu-system-x86_64
QEMUFLAGS=-drive format=raw,file=fat:rw:image \
          -drive if=pflash,format=raw,readonly,file=$(OVMFCODE) \
          -drive if=pflash,format=raw,file=$(OVMFVARS) \
          -debugcon file:$(LOG) -global isa-debugcon.iobase=0x402 \
          -serial stdio \
          -nographic \
          -nodefaults

run:
    $(QEMU) $(QEMUFLAGS)

debug:
    $(QEMU) $(QEMUFLAGS) -s -S

.PHONY: run debug

Before debugging, run the firmware to get debug.log. It may be better to provide startup.nsh script.

$ make run

Now, we have debug.log. It includes the addresses of loaded UEFI images like this:

Loading PEIM at 0x00007EA8000 EntryPoint=0x00007EAB0BC DxeCore.efi

Next, extract text section (.text) RVA from *.efi PE binaries. This can be done by readelf if it is ELF, but the images are PE format. Here we use retrage/peinfo[3].

$ git clone git@github.com:retrage/peinfo.git
$ cd peinfo
$ make

peinfo extracts section information from a binary. This time we want to know VirtualAddress in RVA.

Name: .text
VirtualSize: 0x000204c0
VirtualAddress: 0x00000240
SizeOfRawData: 0x000204c0
PointerToRawData: 0x00000240
PointerToRelocations: 0x00000000
PointerToLinenumbers: 0x00000000
NumberOfRelocations: 0x0000
NumberOfLinenumbers: 0x0000
Characteristics: 0x60000020

Run following bash script with debug.log and peinfo. This outputs a snippet of GDB script that adds symbol information (add-symbol-file). It calculates the address of UEFI image text section from base address and VirtualAddress.

#!/bin/bash

LOG="debug.log"
BUILD="edk2/Build/OvmfX64/DEBUG_GCC5/X64"
PEINFO="peinfo/peinfo"

cat ${LOG} | grep Loading | grep -i efi | while read LINE; do
  BASE="`echo ${LINE} | cut -d " " -f4`"
  NAME="`echo ${LINE} | cut -d " " -f6 | tr -d "[:cntrl:]"`"
  ADDR="`${PEINFO} ${BUILD}/${NAME} \
        | grep -A 5 text | grep VirtualAddress | cut -d " " -f2`"
  TEXT="`python -c "print(hex(${BASE} + ${ADDR}))"`"
  SYMS="`echo ${NAME} | sed -e "s/\.efi/\.debug/g"`"
  echo "add-symbol-file ${BUILD}/${SYMS} ${TEXT}"
done
$ bash gen_symbol_offsets.sh > gdbscript

The generated GDB script is like this:

add-symbol-file edk2/Build/OvmfX64/DEBUG_GCC5/X64/PcdPeim.debug 0x82c380
add-symbol-file edk2/Build/OvmfX64/DEBUG_GCC5/X64/ReportStatusCodeRouterPei.debug 0x831080
add-symbol-file edk2/Build/OvmfX64/DEBUG_GCC5/X64/StatusCodeHandlerPei.debug 0x833100
add-symbol-file edk2/Build/OvmfX64/DEBUG_GCC5/X64/PlatformPei.debug 0x835100
add-symbol-file edk2/Build/OvmfX64/DEBUG_GCC5/X64/PeiCore.debug 0x7ee8240
add-symbol-file edk2/Build/OvmfX64/DEBUG_GCC5/X64/DxeIpl.debug 0x7ee3240
add-symbol-file edk2/Build/OvmfX64/DEBUG_GCC5/X64/S3Resume2Pei.debug 0x7edf240
add-symbol-file edk2/Build/OvmfX64/DEBUG_GCC5/X64/CpuMpPei.debug 0x7ed6240
add-symbol-file edk2/Build/OvmfX64/DEBUG_GCC5/X64/DxeCore.debug 0x7ea8240
add-symbol-file edk2/Build/OvmfX64/DEBUG_GCC5/X64/DevicePathDxe.debug 0x7b8f240

Now we are ready.

$ make debug

Let’s place a breakpoint at BootServices->HandleProtocol().

(gdb) source gdbscript
(gdb) b CoreHandleProtocol
(gdb) target remote localhost:1234
(gdb) c

The debugger stops, and we can do source code level debug.

   ┌──/home/akira/src/ovmf-debug/edk2/MdeModulePkg/Core/Dxe/Hand/Handle.c──────┐
   │933     CoreHandleProtocol (                                               │
   │934       IN EFI_HANDLE       UserHandle,                                  │
   │935       IN EFI_GUID         *Protocol,                                   │
   │936       OUT VOID            **Interface                                  │
   │937       )                                                                │
B+>│938     {                                                                  │
   │939       return CoreOpenProtocol (                                        │
   │940               UserHandle,                                              │
   │941               Protocol,                                                │
   │942               Interface,                                               │
   │943               gDxeCoreImageHandle,                                     │
   │944               NULL,                                                    │
   │945               EFI_OPEN_PROTOCOL_BY_HANDLE_PROTOCOL                     │
   └───────────────────────────────────────────────────────────────────────────┘
remote Thread 1 In: CoreHandleProtocol                      L938  PC: 0x7eb6ad4 



(gdb) 

2019/12/05 Postscript

tnishinaga gave me the improved version of the script to support multiple search paths. Thank you!

#!/bin/bash

LOG="debug.log"
BUILD="./Build"
SEARCHPATHS="./Build/OvmfX64/DEBUG_GCC5/X64/ ./Build/Edk2SamplePkgX64/DEBUG_GCC5/X64/"
PEINFO="peinfo/peinfo"

cat ${LOG} | grep Loading | grep -i efi | while read LINE; do
  BASE="`echo ${LINE} | cut -d " " -f4`"
  NAME="`echo ${LINE} | cut -d " " -f6 | tr -d "[:cntrl:]"`"
  EFIFILE="`find ${SEARCHPATHS} -name ${NAME} -maxdepth 1 -type f`"
  ADDR="`${PEINFO} ${EFIFILE} \
        | grep -A 5 text | grep VirtualAddress | cut -d " " -f2`"
  TEXT="`python -c "print(hex(${BASE} + ${ADDR}))"`"
  SYMS="`echo ${NAME} | sed -e "s/\.efi/\.debug/g"`"
  SYMFILE="`find ${SEARCHPATHS} -name ${SYMS} -maxdepth 1 -type f`"
  echo "add-symbol-file ${SYMFILE} ${TEXT}"
done

References