ebcvm: A Usermode EFI Byte Code Virtual Machine

この記事は 自作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の概略図を以下に示す. f:id:retrage01:20181111222022p:plain

この図から分かるように,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しておくことで 実行に影響しないようにしている.

参考文献