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本体をデバッグしていく.
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