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