retrage.github.io

OVMFのデバッグ

ここではgdbを用いたOVMFのデバッグ方法について説明する. すでにOVMFのデバッグについて書かれた記事[1]が存在するが, ここでは特別なツールなどは使わずに通常のgdbでOVMFをデバッグする.

UEFIにおけるコードの配置

UEFIでは(少なくともx64では)フラットな単一のメモリ空間が用意され, ファームウェア本体もUEFI Applicationも同一の空間内にメモリ保護なしに 配置される,このため,複雑なことは一切せずにシンボル情報とベースアドレスさえ 分かっていればどのようなUEFIのコードであっても通常のデバッガでソースコードレベルデバッグが 可能となっている. また,OVMFでは各機能がモジュール化されており, ロードされる際には通常のUEFI ImageであるPEとしてロードされる. これはおそらく多くの人がデバッグしたいであろうBootServicesでも同様である. BootServicesはDxeCore.efiとして存在し,以下のように起動中にロードされる.

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

EDK2におけるUEFI Imageのデバッグシンボル

OVMFを含むEDK2ではデバッグビルド(-b DEBUG)を行うと 実行ファイル*.efiとデバッグシンボル情報*.debugが生成される. このとき,使うツールチェーンによって生成されるデバッグシンボル情報の形式が異なる点に 注意する必要がある. おそらくLinuxなどでビルドする場合に最も用いられるであろうgccではmingw32ではなく 通常のELFを出力するgccでコンパイルがなされ, 用意されたリンカスクリプトを元にリンクを行ったあと, UEFI Imageの実行形式であるPEへと変換がなされる. このため,gcc(GCC5など)ではデバッグ情報はELFのものとなっており, これは通常のgdbで解釈可能なものとなっている. 一方,Visual Studioや最近追加されたclang/lldでのビルド(CLANG9)[2] ではELFではなくPE/COFFを直接生成するため,デバッグ情報はpdbとなっているはずである(未確認).

以上をまとめると次のようになる.

  • OVMFのコードはフラットな単一のメモリ空間に配置される
  • gccでのビルドされたEDK2のUEFI Imageは(ELFターゲットな)gdbでデバッグ可能

以下では実際に特別なパッケージやデバッガを使わずにOVMF本体をデバッグしていく.

EDK2のビルド

何十回もやっているであろう作業なので説明は割愛. 普通にgccでデバッグビルドでビルドする.

$ 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

デバッグをしやすくするために以下のようなMakefileを作成する. ここで注意したいのは0x402でdebugconを接続しておき, OVMFからのデバッグ情報(debug.log)を記録しておくことである[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

実際にデバッグを行う前にdebug.logを取得するため 普通に実行する.startup.nshなどを用意しておくと便利かもしれない.

$ make run

これでdebug.logが取得できたこれには以下のようにどのUEFI Imageが どこにロードされるかが記載されている.

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

次に*.efiのPEバイナリからテキスト領域(.text)のRVAを取得する. これはELFであればreadelfなどでできるが, 今回は手前味噌ではあるが, 私が過去に作ったretrage/peinfo[3] を用いる.

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

peinfoでは以下のような情報が取得できる. ここではVirtualAddressさえ取得できればよい. なお,この値はRVAである点に注意する必要がある.

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

得られたdebug.logとpeinfoを用いて以下のようなスクリプトを実行する. これはシンボル情報を追加するadd-symbol-fileを出力していくもので, debug.logで得られた各UEFI Imageのベースアドレスとpeinfoで得られたVirualAddressを 加算してそのUEFI Imageのテキスト領域がロードされるアドレスを計算するものである.

#!/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

以上のようにして生成されたgdb scriptは以下のようになっている.

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

以上で準備は完了である.通常のデバッグ時同様にデバッグ可能である.

$ make debug

ここではBootServices->HandleProtocol()にブレークポイントを置いてみる.

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

以下のようにブレークポイントで止まり,ソースコードレベルデバッグが可能になっていることがわかる.

   ┌──/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追記

tnishinagaさんから上記のスクリプトを改良して 複数パスに対応したスクリプトを教えていただきました. ありがとうございます.

#!/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

参考文献