retrage.github.io

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のレジスタの構成を以下に示す.

EBC VM Registers

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が実行される. なお,スタックにプッシュされた引数については呼び出し元が責任を持つ.

EBC Stack Example

一般に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は以下の図に示すようにエンコードされる.

EBC 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を表す. 以上で得られたbcnを用いて次のようにデコードされたオフセットが表される.

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進数で表すを以下のようになる.

EBC Natural Indexing Example

先の説明を当てはめると

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は次のようにエンコードされる.

EBC XOR Encoding

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は以下の図のような構成となっている.

ebcvm Architecture

最初に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のエンコードで例外が発行されているものの, 確かに実行できていることがわかる.

参考文献