この記事は 自作OS Advent Calendar 2018 の19日目の記事として書かれた. ここでは,フルスクラッチで開発したEFI Byte Code Virtual Machineである ebcvm[1]と開発したのELVMのEBCバックエンド[2]について紹介する.
EFI Byte Codeについて
すでにEFI Bye Codeを解説した記事を書いている[3]ため. そちらを参考にしていただきたい.
ebcvmの概要
ebcvmは*nix向けに開発したEFI Byte Code Virtual Machine実装である. TianoCore/EDK2にはEBCのVMの実装が既に存在する[4]が, ここではそちらの実装を使わずにUEFI Specificationとオンラインで公開されている ごく少数の EBCバイナリを参考に実装を行なった. ebcvmの開発のモチベーションとしては(ほとんど)規格書のみで実装できるか ということに興味があったため,という側面が強い.
ebcvmの概略図を以下に示す.
この図から分かるように,ebcvmは以下の部分で構成される.
- Virtual Machine
- Memory
- Registers
- Decoder
- Executor
- EFI Native Code Emulator
- Loader
- Debugger
ebcvmでは規格書に記載されている全ての命令が実装されている.
EBCではほぼ全ての操作がVM内部で閉じている. しかし,例外としてEFIのNative Codeの呼び出しが可能となっている. (仕様上,EBCからNative, NativeからEBCどちらも可能となっている.) このため,EBCからSystemTable以下にあるBootServicesやRuntimeServicesへの アクセスが可能となっている. 特に,文字の入出力についてはConIn, ConOut, StdErrを使わざるを得ないので, ebcvmでもNative Codeの呼び出しに対応する必要がある. このため,EFIの用意するNative CodeのEmulationを行なっている. 現状,ebcvmでは以下のごく一部のNative Codeのみ対応している.
- BootServices->AllocatePool
- ConIn->ReadKeyStroke
- ConOut->OutputString
UEFIではFirmware側が動的にApplicationをメモリ上に配置できるように
UEFI Application/Driverはrelocatableなものとなっている.
EBCのバイナリでもこれは同様である.
そのため,ebcvmのローダもrelocationに対応しており,
--reloc=1
のオプションを実行時に渡すことで
relocationが行われるようになっている.
EBCは現状GNU binutils, GCC, LLVM/Clangなど一般的なコンパイラでは 対応されていない. このため,デバッグを容易にするためにebcvmでは 簡単なデバッガを実装している. 以下に主なebcvmのデバッガの機能を示す.
- continue - continue program
- reg - show registers
- examine - show memory
- disassemble - disassemble memory
- memmap - show memory map
- backtrace - show backtrace
ebcvmのテスト
ebcvmはCで書かれているが,
できるだけ正確にEBCの動作を再現できるように
DecoderとExecutorについてはテストコードを書いている.
冪等性を確保した設計にし,テストを容易にすることが重要である.
初期の段階ではC++では一般的なGoogle Test[5]の導入も考えたが,
依存するライブラリはできるだけ少なくしたかったため,
Cでベタ書きしたテストケースと
test.sh
というテストを行うシェルスクリプトを置くことで
テストを行えるようにした.
Decoderでは実装が405行に対してテストコードが2277行, Executorでは実装が1221行に対してテストコードが8490行 となっている.
ELVMのEBCバックエンド
先のEBC解説記事でも言及したように, EBCにはIntel C Compiler for EFI Byte Code[6]以外に 利用可能なコンパイラが存在しない. そこで,自由なEBCの開発を行えるようにするため, ELVMのEBCバックエンドを開発した.. LEVMはShinichiro Hamaji(@shinh)さんによって開発された EsoLangVM Compiler Infrastructureである. ELVMでは内部で使われるELVM IR(EIR)の命令の種類が少ないため, LLVMと比較して簡単にバックエンドを追加することができる. ELVMのEBCバックエンドを追加することにより, Cコンパイラである8cc[7]から出力されるEIRからEBCバイナリが生成可能となる. これにより(バイナリが非効率などの問題があるにせよ) 自由なEBC向けCコンパイラを入手することができるようになる.
デザイン
EBCバックエンドのデザインはx86-linuxバックエンドを参考にしている.
EBCバックエンドでは外部のEBCアセンブラを使わずに
直接PEバイナリを出力している.
EBCバックエンドでは,最初にEIR内部のprogram couner(pc)と
バイナリ内部でのアドレスの対応表pc2addr
を作成する.
これによって得られた値を元に各セクションのアドレス,サイズを決定し,
それらの値を埋め込んだPEヘッダを出力する.
次にinit_state_ebc
が実行される.
ここでは主にBootServices->AllocatePool
により
メモリ領域を確保し,その領域を初期化する.
EBCバックエンドでは.text
,.rodata
の2つのセクションが作成される.
.text
には実際のEBCのコード,
.rodata
にはpc2addr
の対応表が書き込まれる.
以上の流れでEBCバイナリが直接出力される.
ELVMではA, B, C, D, BP, SPのレジスタが定義されている. EBCではR0からR7までの64-bit幅のレジスタが用意されており, このうちR0はスタックポインタとして予約されている. そのため,EBCバックエンドではR1-R6をそれぞれA-SPに割り当てており, 残りのR7を自由なレジスタとして利用している. 実際の実装では,Native Code呼び出しなどで R1, R2, R3などを利用せざるを得ない場合があり, この場合はレジスタの値をスタックにPUSHしておくことで 実行に影響しないようにしている.
参考文献
- [1] https://github.com/yabits/ebcvm
- [2] https://github.com/retrage/elvm/tree/retrage/ebc-v2
- [3] https://retrage.github.io/2018/11/11/efi-byte-code-myth.html
- [4] https://github.com/tianocore/edk2/tree/master/MdeModulePkg/Universal/EbcDxe
- [5] https://github.com/google/googletest
- [6] https://software.intel.com/en-us/articles/intel-c-compiler-for-efi-byte-code-purchase
- [7] https://github.com/rui314/8cc