Skip to content

UEFIアプリケーションをデバッグする

   

はじめに

この記事は自作OS Advent Calendar 2021の11日目の記事として書かれました。

元ネタは、こちらです。

UEFIアプリケーションをデバッグする

『ゼロからのOS自作入門』(通称:みかん本)を進める上で、多くの壁に直面します。Linux/UNIXコマンドに不慣れであったり、MikanOSをビルドする環境の構築が上手くいかなったりなど、最初の方でつまずくことが多くあります。これらの壁については、『ゼロからのOS自作入門』のWikiなどで補足されています。

これらの最初の壁を乗り越えた後に、直面するのがUEFIアプリケーションとして実装するMikanOSブートローダのデバッグです。

MikanOSブートローダの開発では、フレームバッファの情報収集や、MikanOSカーネルのロードなど、メモリ操作を伴う機能を実装します。これらの機能が正常に動作しない場合、問題の原因追求と解決のために、GDBを利用したデバッグが有効です。

以下、この記事では、QEMUを使用した開発環境にてGDBを用いたデバッグができるようになるまでの手順を説明します。

デバッグ環境の構築

おおまかな流れ

  1. debug.log出力対応のOVMFイメージの作成
  2. QEMUを起動しdebug.logを取得
  3. gdbscriptの作成
  4. 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.efi0x0003E45E000 にロードされていることがわかります。

gdbscriptの作成

gdbで利用するシンボルファイルLoader.debugを読み込むためのgdbscriptを用意します。Loader.debugと同じディレクトリに以下の内容のファイルを作成します。

[gdbscript]
add-symbol-file ./Loader.debug 0x0003E45E240 -s .data 0x0003E460680

上記のファイルに記述されている、0x0003E45E2400x0003E460680は、.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 :1234GDBとの接続が確立するとPAUSE状態になる
ブレークポイントを設定する-
(gdb) c で動作を再開するQEMUのPAUSEが解除される
ブレークポイントで動作が止まるQEMUがPAUSE状態になる
(gdb) p valueで変数内容の確認
(gdb) n or s でステップ実行で動作確認ステップ実行ごとに処理が行われる
  • (gdb) target remote :1234 は,startup.nshのpauseを解除する前に実行する.

以下の図は、206行目をブレークポイントに設定して、GDBのContinueを実行した様子を示しています。

Debug efi app

ローカル変数のtimeには、200行目でEFI_TIME型のデータが格納されているので、GDBのp timeでデータの内部を見ることができます。更に、nで一度ステップ実行しているので処理はMemoryMapを取得する箇所まで進んでおり、QEMUの画面から206行目のPrint文を実行していることがわかります。

おわりに

この記事では、QEMUを利用したEFIアプリケーションのデバッグ方法について、環境構築から実際にデバッグできるようなるまでの一連の流れを説明しました。

記事を作成するにあたり、環境構築を1から行ったことで、理解がまた深まりました。

今回実行した内容は、以下のリポジトリにまとめています。ぜひ参考にしてみてください。

なにかあれば、上記リポジトリのissueか下記コメント欄まで。

参考文献