はじめに
この記事は自作OS Advent Calendar 2021の11日目の記事として書かれました。
元ネタは、こちらです。
UEFIアプリケーションをデバッグする
『ゼロからのOS自作入門』(通称:みかん本)を進める上で、多くの壁に直面します。Linux/UNIXコマンドに不慣れであったり、MikanOSをビルドする環境の構築が上手くいかなったりなど、最初の方でつまずくことが多くあります。これらの壁については、『ゼロからのOS自作入門』のWikiなどで補足されています。
これらの最初の壁を乗り越えた後に、直面するのがUEFIアプリケーションとして実装するMikanOSブートローダのデバッグです。
MikanOSブートローダの開発では、フレームバッファの情報収集や、MikanOSカーネルのロードなど、メモリ操作を伴う機能を実装します。これらの機能が正常に動作しない場合、問題の原因追求と解決のために、GDBを利用したデバッグが有効です。
以下、この記事では、QEMUを使用した開発環境にてGDBを用いたデバッグができるようになるまでの手順を説明します。
デバッグ環境の構築
おおまかな流れ
debug.log
出力対応のOVMFイメージの作成- QEMUを起動し
debug.log
を取得 gdbscript
の作成- GDB+QEMUでのデバッグ
debug.log出力対応のOVMFイメージのビルド
まず、UEFIモードで起動したQEMUが、debug.log
を出力するようにします。これを実現するためには、debug.log
出力に対応したOVMFイメージが必要であるため、EDK2をビルドし直します。
ここでは~
の直下にedk2
ディレクトリが作成されているものします。edk2
ディレクトリは、GitHub上のtianocore/edk2 をcloneしたディレクトリです。
(記事を書くにあたり、edk2は2021/12/6時点で最新のstableリリースedk2-stable202111を利用しています)
$ cd ~/edk2
$ git checkout edk2-stable202111
$ make -C BaseTools/Source/C
この2行の作業は、mikanos-buildでansibleを利用して開発環境を構築する際に実行済みです。実行していない場合、次のOVMFのビルドに失敗するので明記しておきます。
$ cd ~/edk2
$ source ./edksetup.sh
$ build -p OvmfPkg/OvmfPkgX64.dsc -b DEBUG -a X64 -t CLANG38
最後のbuildで-b DEBUG
を指定することで、debug.log
の出力に対応したOVMFイメージが生成されます。
$ ls ~/edk2/Build/OvmfX64/DEBUG_CLANG38/FV/ |grep OVMF_
OVMF_CODE.fd
OVMF_VARS.fd
作成したOVMFイメージを、別のディレクトリに移動しておきましょう。
$ mkdir -p ~/debug-efiapp/ovmf
$ cp ~/edk2/Build/OvmfX64/DEBUG_CLANG38/FV/OVMF_* ~/debug-efiapp/ovmf/
$ ls debug-efiapp/ovmf/
OVMF_CODE.fd OVMF_VARS.fd
デバッグしたいEFIアプリケーションを用意
~/debug-efiapp
ディレクトリ以下にEFIアプリケーションのソースファイルを用意します。
$ cd ~/debug-efiapp
$ mkdir LoaderPkg
...
$ ls LoaderPkg
Loader.inf LoaderPkg.dec LoaderPkg.dsc Main.c frame_buffer_config.hpp
ビルドします。この時デッバグシンボルファイルが生成されるように、-b DEBUG
オプションを指定します。
$ ln -s ~/debug-efiapp/LoaderPkg ~/edk2/LoaderPkg
$ cd edk2/
$ source edksetup.sh --reconfigure
$ build -p LoaderPkg/LoaderPkg.dsc -b DEBUG -a X64 -t CLANG38
$ ls ~/edk2/LoaderPkg/Build/X64/DEBUG_CLANG38/X64
Loader.debug Loader.efi LoaderPkg MdePkg TOOLS_DEF.X64
ビルド生成物のLoader.efi
がEFIアプリケーションで、Loader.debug
がシンボルファイルです。
QEMU実行時のdebug.log取得
QEMUで利用する Virtual FAT disk image 用のディレクトリを作成し、Loader.efiを配置します。
$ mkdir ~/debug-efiapp/efi
$ cp ~/edk2/LoaderPkg/Build/X64/DEBUG_CLANG38/X64/Loader.efi ~/debug-efiapp/efi/
このままではQEMU起動後にLoader.efi
が実行されないので、EFI Shell用のstartup.nsh
を用意します。以下のスクリプトでは、startup.nsh
実行直後に一時停止します。何かキー入力すれば、Loader.efi
が実行されます。
[~debug-efiapp/efi/startup.nsh]
pause
fs0:Loader.efi
QEMUを起動します。-debugcon file:debug.log -global isa-debugcon.iobase=0x402
を追加することで、debug.log
が取得できます。-s
はQEMUのgdbserverをtcp::1234で起動するオプションです。
OVMF_DIR=`realpath ~/debug-efiapp/ovmf`
EFI_DIR=`realpath ~/debug-efiapp/efi`
sudo qemu-system-x86_64 -m 1G -boot menu=on,splash-time=8000\
-drive if=pflash,format=raw,readonly,file=$OVMF_DIR/OVMF_CODE.fd \
-drive if=pflash,format=raw,file=$OVMF_DIR/OVMF_VARS.fd \
-drive if=ide,index=0,media=disk,format=raw,file=fat:rw:$EFI_DIR \
-device nec-usb-xhci,id=xhci -device usb-mouse -device usb-kbd \
-debugcon file:debug.log -global isa-debugcon.iobase=0x402 \
-monitor stdio \
-s
Loader.efi
実行後に、debug.logを確認すると、以下の記述が見つかります。
[debug.log]
Loading driver at 0x0003E45E000 EntryPoint=0x0003E45F8CB Loader.efi
上の記述では,Loader.efi
が 0x0003E45E000
にロードされていることがわかります。
gdbscriptの作成
gdbで利用するシンボルファイルLoader.debug
を読み込むためのgdbscript
を用意します。Loader.debug
と同じディレクトリに以下の内容のファイルを作成します。
[gdbscript]
add-symbol-file ./Loader.debug 0x0003E45E240 -s .data 0x0003E460680
上記のファイルに記述されている、0x0003E45E240
と0x0003E460680
は、.text
セクションと.data
セクションの物理アドレスになります。算出方法は以下のとおりです。
gdbのinfo file
を利用して、各セクションのオフセットアドレスを取得します。
$ gdb efi/Loader.efi
GNU gdb (Debian 10.1-1.7) 10.1.90.20210103-git
...
(gdb) info file
Symbols from "efi/Loader.efi".
Local exec file:
`efi/Loader.efi', file type pei-x86-64.
Entry point: 0x18cb
0x0000000000000240 - 0x0000000000002680 is .text
0x0000000000002680 - 0x0000000000002840 is .data
0x0000000000002840 - 0x0000000000002880 is .reloc
debug.log
から取得したベースアドレスをもとに.text
,.data
の物理アドレスを計算します。
.text: 0x0003E45E000 + 0x00240 = 0x0003E45E240
.data: 0x0003E45E000 + 0x02680 = 0x0003E460680
いざGDBでデバッグ
GDB操作用とQEMU操作用にターミナルは2つ立ち上げましょう。
GDB側での操作 | QEMU側での操作 |
---|---|
$ gdb efi/Loader.efi --tui | - |
(gdb) source ./gdbscript | - |
- | QEMUを起動し,EFI Shellの起動を待つ |
(gdb) target remote :1234 | GDBとの接続が確立するとPAUSE状態になる |
ブレークポイントを設定する | - |
(gdb) c で動作を再開する | QEMUのPAUSEが解除される |
ブレークポイントで動作が止まる | QEMUがPAUSE状態になる |
(gdb) p value で変数内容の確認 | |
(gdb) n or s でステップ実行で動作確認 | ステップ実行ごとに処理が行われる |
(gdb) target remote :1234
は,startup.nsh
のpauseを解除する前に実行する.
以下の図は、206行目をブレークポイントに設定して、GDBのContinueを実行した様子を示しています。
ローカル変数のtimeには、200行目でEFI_TIME型のデータが格納されているので、GDBのp time
でデータの内部を見ることができます。更に、n
で一度ステップ実行しているので処理はMemoryMapを取得する箇所まで進んでおり、QEMUの画面から206行目のPrint文を実行していることがわかります。
おわりに
この記事では、QEMUを利用したEFIアプリケーションのデバッグ方法について、環境構築から実際にデバッグできるようなるまでの一連の流れを説明しました。
記事を作成するにあたり、環境構築を1から行ったことで、理解がまた深まりました。
今回実行した内容は、以下のリポジトリにまとめています。ぜひ参考にしてみてください。
なにかあれば、上記リポジトリのissueか下記コメント欄まで。