EFI Byte Code解説
本記事は2018年11月10日に開催されたkernelvm 北陸 Part4において 発表した内容[10]をまとめたものである. ここではUEFIの持つ独自のbyte codeであるEFI Byte CodeとそのVMについてみていく.
EFI Byte Codeとは
UEFI Specification v2.7によれば, EFI Byte Code (EBC)について
platform- and processor-independent mechanisms for loading and executing EFI device drivers
とある. つまり,OSなどのplatform非依存かつ,processor非依存な EFI device driverを読み込み,実行するための仕組み,ということである.
EBCはPCI Express (PCIe)のOptionROM (OROM)に利用されることが想定されて設計されている. OROMはPCIeデバイスの内部に存在する記憶領域であり[8], UEFI向けのデバイスドライバなどが置かれている. UEFIはOROMから実行ファイルを読み出し,メモリ上に配置して実行する. このとき用いられる実行ファイルに含まれるコードは,native codeかEBCのいずれかである. OROMのにあるUEFI向けデバイスドライバにEBCが利用される理由の一つとして, 複数のアーキテクチャへの対応が挙げられる. しかし,実際にはx64マシンがほとんどであるためにEBCではなくx64 native codeの UEFI device driverが内蔵されているという話もある.(未確認)
EBC向けのドキュメントとツール
EBCはマイナーなため,EBCについての情報はあまり存在しない. 強いて挙げるとすれば,以下のものがある.
- UEFI Specification[2]
- TianoCore/EDK2 source code[3]
- いくつかのブログ記事[4][5]
このうち最も詳しいのがUEFI Specificationである.
また,EBC関連のツールとしては以下のようなものがある.
- Intel C Compiler for EFI Byte Code[6]
- fasmg-ebc[7]
Intel C Compiler for EFI Byte Codeは$995で有料であるため, 簡単に入手することができない.(そもそも個人向けに販売しているのか怪しい) fasmg-ebcはオープンソースなFlat Assembler (fasm)をベースにしたEBCのアセンブラである. fasmg-ebcは部分的に後述するEBCからのnative codeの呼び出しに対応している.
なお,一般的なGCC/ClangなどではEBC対応がなされていない. また,EBCの逆アセンブラも存在しない.
EBC VMの構成
EBCのVMは64-bit little endianとなっている. EBCのVMのレジスタの構成を以下に示す.
IP (Instruction Pointer),FLAGS,R0からR7までの 合計10の64-bit長のレジスタを持っている. FLAGSは比較演算の結果の入るCフラグとsingle-stepを示すSフラグの2つがある. 汎用レジスタであるR0-R7では, R0はスタックポインタを保持し, R7は関数からの返り値が代入される.
EBCのバイナリ形式と関数呼び出し規約
EBCではWindowsやUEFIで利用されているPortable Executable (PE)のPE32+を採用している.
また,UEFI側が自由にメモリ上に配置できるように,relocatable imageなっている.
面白いことに,PEヘッダにあるFileHeader->Machineは0x0ebc
となっている.
以下のfasmg-ebcで作成したEBCバイナリのヘッダ情報を
自作のpeheader[9]の出力として以下に示す.
Machine: ebc
Number of Section: 2
Time Stamp: 1541744287
Size of Optional Header: 240
32-bit Architecture
This file is DLL.
PE+
AddressOfEntryPoint: 1000
ImageBase: 400000
SectionAlignment: 1000
FileAlignment: 200
SizeOfImage: 3000
SizeOfHeaders: 200
Section 0
Name: .text
VirtualSize: 1c
VirtualAddress: 1000
SizeOfRawData: 200
PointerToRawData: 200
PointerToRelocations: 0
PointerToLinenumbers: 0
NumberOfRelocations: 0
NumberOfLinenumbers: 0
Characteristics: 60000020 r-x exec
Section 1
Name: .data
VirtualSize: 26
VirtualAddress: 2000
SizeOfRawData: 200
PointerToRawData: 400
PointerToRelocations: 0
PointerToLinenumbers: 0
NumberOfRelocations: 0
NumberOfLinenumbers: 0
Characteristics: c0000040 rw- inited
EBCの関数呼び出し規約はCDECLを採用している.
これはx64 UEFIのnative codeではMicrosoft x64 Calling Conventionを
採用している点で大きく異なる.
func(arg0, arg1, arg2)
のような関数があった場合,
以下の図に示すように引数を逆順にスタックにプッシュし,
最後に呼び出し元のアドレスをプッシュしてfunc
が実行される.
なお,スタックにプッシュされた引数については呼び出し元が責任を持つ.
一般にUEFIでは起動時に,UEFI本体より
IN EFI_HANDLE ImageHandle,
IN EFI_SYSTEM_TABLE *SystemTable
の2つの引数が渡される. EBCのVMでも同様にこれらの値が渡されることとなっており, VMの起動時にはあたかもNative codeから起動されたかのように これらの引数と呼び出し元のアドレスがEBC上のスタックにプッシュされた状態になる.
Natural Indexing
32-bitと64-bitの複数のアーキテクチャに対応するため, EBCではNatural Indexingという仕組みを持っている. これはあるアドレスに対するオフセットの値である符号付整数について, アーキテクチャによって異なる値となるように設計されたオフセットの表し方である. Natural Indexingは以下の図に示すようにエンコードされる.
ここで,Nはエンコード後の値の長さ(16, 32, 64-bitのいずれか)である.
MSBであるN bit目b
は符号を表し,0であれば正,1であれば負を表す.
次にN-1からN-3bitまでw
は後述するn
の長さを表す.
ただし,w
はデコード時に
A = w * N / 8
よりA
が計算される.
次にN-4からA
bitまでc
はConstant unitsを表し,
A
-1から0 bitまでn
はNatural unitsを表す.
以上で得られたb
,c
,n
を用いて次のようにデコードされたオフセットが表される.
Offset = (c + n * sizeof(VOID *)) * b
ここで,sizeof(VOID *)
はアーキテクチャ依存であり,
32-bitであれば4となり,64-bitであれば8となる.
Natural Indexingの例
では実際にエンコードされたNatural Indexを計算してみる.
値はUEFI Specificationに挙げられている例と同一である.
0xa048
という16-bitのIndexを考える.
これは2進数で表すを以下のようになる.
先の説明を当てはめると
N = 16
b = 1,
w = 2, A = 2 * 16 / 8 = 4
c = 4
n = 8
となる. 32-bit,64-bitそれぞれのアーキテクチャではデコードされた値は
32-bit: Offset = (4 + 8 * 4) * -1 = -36
64-bit: Offset = (4 + 8 * 8) * -1 = -68
となり,確かに同じNatural Indexでも異なる値にデコードされることがわかる.
EBCの命令セット
EBCの命令セットは可変長なCISC-likeな命令となっている. 全体では56種類の命令が存在する. 以下にEBCの命令の一覧を示す.
opcode ops[] = {
BREAK, /* 0x00 */
JMP, /* 0x01 */
JMP8, /* 0x02 */
CALL, /* 0x03 */
RET, /* 0x04 */
CMPeq, /* 0x05 */
CMPlte, /* 0x06 */
CMPgte, /* 0x07 */
CMPulte, /* 0x08 */
CMPugte, /* 0x09 */
NOT, /* 0x0a */
NEG, /* 0x0b */
ADD, /* 0x0c */
SUB, /* 0x0d */
MUL, /* 0x0e */
MULU, /* 0x0f */
DIV, /* 0x10 */
DIVU, /* 0x11 */
MOD, /* 0x12 */
MODU, /* 0x13 */
AND, /* 0x14 */
OR, /* 0x15 */
XOR, /* 0x16 */
SHL, /* 0x17 */
SHR, /* 0x18 */
ASHR, /* 0x19 */
EXTNDB, /* 0x1a */
EXTNDW, /* 0x1b */
EXTNDD, /* 0x1c */
MOVbw, /* 0x1d */
MOVww, /* 0x1e */
MOVdw, /* 0x1f */
MOVqw, /* 0x20 */
MOVbd, /* 0x21 */
MOVwd, /* 0x22 */
MOVdd, /* 0x23 */
MOVqd, /* 0x24 */
MOVsnw, /* 0x25 */
MOVsnd, /* 0x26 */
NOP, /* 0x27 */
MOVqq, /* 0x28 */
LOADSP, /* 0x29 */
STORESP, /* 0x2a */
PUSH, /* 0x2b */
POP, /* 0x2c */
CMPIeq, /* 0x2d */
CMPIlte, /* 0x2e */
CMPIgte, /* 0x2f */
CMPIulte,/* 0x30 */
CMPIugte,/* 0x31 */
MOVnw, /* 0x32 */
MOVnd, /* 0x33 */
NOP, /* 0x34 */
PUSHn, /* 0x35 */
POPn, /* 0x36 */
MOVI, /* 0x37 */
MOVIn, /* 0x38 */
MOVREL, /* 0x39 */
NOP, /* 0x3a */
NOP, /* 0x3b */
NOP, /* 0x3c */
NOP, /* 0x3d */
NOP, /* 0x3e */
NOP, /* 0x3f */
};
EBCの命令のOperand
EBCの命令は基本的に以下のような形式をとる.
INSTRUCTION Operand1, Operand2
各Operandは Direct, Indirect, Indirect with Index,Immediate の4種類の形式をとる.
DirectはR2
などと表し,Operandで指定されているレジスタの値を指す.
Indirectは@R2
などと表し,
Operandで指定されているレジスタの値のアドレスにあるメモリ上の値を指す.
Indirect with Indexでは
Operandで指定されているレジスタの値と
他にNatural Indexingによって指定されているオフセットの値を
足した値のアドレスにあるメモリ上の値を指す.
これは@R1(+n, +c)
の形式で表される.
Immediateは即値であり,指定された値そのままを指し,
0x1234
の形式で表される.
EBC命令の例: XOR
EBCの命令XOR
は以下の形式で表される.
XOR[32|64] {@}R1, {@}R2 {Index16|Immed16}
XORは次のようにエンコードされる.
Byte0-Bit7ではOperand2のImmediate/Indexの存在を表す.
もし1であればByte2からByte3には16-bitのImmediate/Indexの値が存在する.
Byte0-Bit6では操作が32/64-bitのどちらかであることを示す.
Byte0-Bit0からBit5はOpecodeを表し,XOR
では0x16
となっている.
Byte1-Bit7ではOperand2がDirectとIndirectのどちらであることを示す.
Byte1-Bit4からBit6ではOperand2のレジスタを表す.
Byte1-Bit3ではOperand1がDirectとIndirectのどちらであることを示す.
Byte1-Bit0からBit2ではOperand1のレジスタを表す.
Native codeの呼び出し
EBCにはCALL
という命令があるが,
これはEBCの呼び出しとnative codeの呼び出し(EXCALL
)の2種類がサポートされている.
EBCはEXCALL
をサポートするための命令をいくつか持っている.
以下にその命令を列挙する.
MOVn, MOVIn, MOVsn, POPn, PUSHn
EXCALL
は各アーキテクチャ依存の処理を行うため,
あるいはUEFIの容易するランタイムを呼び出すために用いられる.
EBC VMの例外
EBCのVMはEBCを実行した場合に発生する例外をハンドルできる必要がある. EBCの実行により発生する可能性のある例外を以下に示す.
Divide By 0
Debug Break
Invalid Opcode
Stack Fault
Alignment
Instruction Encoding
Bad Break
Undefined
各例外の詳細はUEFI Specificationを参考にしてほしい.
EBCのVMはEFI Debug Support Protocolをサポートする必要があり, このProtocol経由でデバッガをVMにattachすることができる. もしデバッガがattachされている場合, 例外はデバッガによって捕捉される. もし,デバッガがattachされていない場合は以下のいずれかの動作となるが, どの動作になるかは実装依存となっている.
- エラーメッセージを表示しシステムを停止
- システムをハング
- 例外を無視し,処理を続行
EBCアセンブリの例
以下にfasmg-ebcよりEBCアセンブリによるHello, worldの例を示す.
section '.text' code executable readable
EfiMain:
MOVn R1, @R0(EFI_MAIN_PARAMETERS.SystemTable)
MOVn R1, @R1(EFI_SYSTEM_TABLE.ConOut)
MOVREL R2, Hello
PUSHn R2 ; Push Pointer to Hello
PUSHn R1 ; Push Pointer to SystemTable->ConOut
CALLEX @R1(SIMPLE_TEXT_OUTPUT_INTERFACE.OutputString)
MOV R0, R0(+2,0)
RET
section '.data' data readable writeable
Hello: du "Hello EBC World!", 0x0D, 0x0A, 0x00
このアセンブリではエントリポイントはEfiMain
である.
最初にR0レジスタにあるスタックポインタの値から
SystemTableへのアドレスをR1に代入する.
次にR1の値からConOutへのアドレスのR1に代入する.
R2に文字列Hello
へのアドレスを代入する.
次にこれらのR2,R1の順にレジスタの値をスタックにプッシュする.
これはSimple Text Output ProtocolのOutputStringの
引数,
IN EFI_SIMPLE_TEXT_OUTPUT_PROTOCOL *This,
IN CHAR16 *String
に相当する.
最後にCALLEX
によりConOut->OutputStringを呼び出している.
ebcvm: ユーザ空間で動作するEBC VM
EBCの仕様の概要は以上のようなものとなっている. EBCはUEFIの規格で定められているものの, 少なくともオープンソースで用いられている事例が全く存在しない. また,EBCのVMはOVMFなどで実装されているが, ユーザ空間で簡単に利用することが難しい. そこで,UEFI SpecificationにあるEBCの仕様と fasmg-ebcの生成するEBCバイナリのみを元にしてEBCのVMを作成することにした. ebcvmのソースコードは以下GitHub上で公開している.
ebcvmは現在以下ものをサポートしている.
- EBCの全ての命令
- いくつかのnative code実行のエミュレーション
- 簡単なデバッガ
ebcvmの概観
ebcvmは以下の図のような構成となっている.
最初にVMが起動し,レジスタの初期化とメモリの初期化がなされる. LoaderがEBCバイナリを読み込みメモリ上に展開する. また,UEFIのランタイムもメモリ上に配置される. 初期化が完了すると,Decoderがバイトコードをデコードし, Executorがデコードされた命令を実行する. VMはEXCALLが発行されるとこれをトラップし, EFI Native Codeに処理を渡す. EFI Native CodeはエミュレートされてVMに処理が戻され, 処理が続行される. VMはまた例外をトラップし,Simple debuggerがattachされていれば これに処理を受け渡す.
ebcvmの実行例
先に示したEBCによるHello, worldの例のバイナリを ebcvmにより実行した例を以下に示す.
$ ./ebcvm sample/print.efi
Hello EBC World!
exception ENCODE: MOV
MOV
のエンコードで例外が発行されているものの,
確かに実行できていることがわかる.
参考文献
- [1] https://uefi.org/
- [2] https://uefi.org/specifications
- [3] https://github.com/tianocore/edk2
- [4] http://vzimmer.blogspot.com/2015/08/efi-byte-code.html
- [5] https://habr.com/post/201954/
- [6] https://software.intel.com/en-us/articles/intel-c-compiler-for-efi-byte-code-purchase/?_ga=2.177917472.440178022.1541746062-392223130.1541746062
- [7] https://github.com/pbatard/fasmg-ebc
- [8] http://opensecuritytraining.info/IntroBIOS_files/Day1_06_Advanced%20x86%20-%20BIOS%20and%20SMM%20Internals%20-%20PCI%20XROMs.pdf
- [9] https://github.com/retrage/peheader
- [10] https://speakerdeck.com/retrage/efi-byte-code-virtual-machine-for-fun-and-profit