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