2014年1月29日水曜日

UEFIアプリケーション/ドライバー開発の話、メモ、その他

最近得られた知見をメモしておくね.

リンクとか

UEFI 仕様書 http://www.uefi.org/specs/download
Phoenix wiki http://wiki.phoenix.com/wiki/index.php/UEFI

とりあえずここらへん.仕様書のページ数はけっこうあるけど、
案外読み易いのでとりあえずここみましょう.
ヘンにググるよりProtocolとかこっち読んだほうが見つかったりします.
Phoenix wikiのUEFIの項は最初のリンク、Phoenix Developer Information About UEFI
Protocolのdefinitionが網羅的に載ってるのでUEFIのAPIの引数とかプロトタイプ宣言
探したくなったら役立ちます.

OSDev Wiki http://wiki.osdev.org/UEFI
多くのOSS系のプロジェクトでEFIを使うときはIntelのEDKよりgnu-efiを用いてる例
が多いですが、このwikiはgnu-efiとgcc toolchainのUEFIターゲットの導入をわりと丁寧に書いてあります. gnu-efiを使うとgdbでブレークポイントデバッグできるので、良さげ.

x86asm.net http://x86asm.net/articles/uefi-programming-first-steps/index.html
Othersの項にいくつかUEFIについての記事がある模様.
gnu-efiを使ってHello Worldを作るにあたって、なぜかアセンブリでのプログラムを解説している
稀有なサイト.

tianocore http://sourceforge.net/apps/mediawiki/tianocore/index.php?title=EDK_II_User_Documentation
EDKのホスティングをしているtianocoreのサイト(sourceforge)にあるユーザーマニュアル

EDK II Module Writer's Guide v 0.7Driver Writer Guide V1.01は必読.
まずEDKでビルドするのにConf書いたりWindowsの設定ファイルやドライバとかで使うinfとか書かなきゃいけないので、それらをどう書くかをまず知るのに前者がないとわからなくなる.
後者は様々なProtocolのサンプルコードがあるので、Specificationやwikiで一覧を見ただけじゃわからないところを補完できる.

メモ

導入
gnu-efiは上記のOSDev wikiにある通り、まあ普通にツールチェインを導入するだけ
EDKはちょっとわかり辛い気がするのでメモ(かなり悩んだ)
まずsourceforgeのsvnか、github.com/tianocoreからかどちらでも良いので、EDKを取ってくる
~/src/edk2にEDKを配置したとして、
export EDK_TOOLS_PATH=$HOME/src/edk2/BaseToolsという環境変数を実行し、
~/src/edk2/に移動し、
$ source edksetup.sh BaseToolsを実行.
あとは諸々設定されるので、buildコマンドを実行するとあとはUEFIアプリをビルドしてくれます.

ポイントは、ここで何がビルドされるか.
$EDK_TOOLS_PATH/Confに設定ファイル郡があって、
target.txtのACTIVE_PLATFORM変数に指定したUEFIアプリケーションパッケージをビルドします.
パッケージは$EDK_TOOLS_PATH/*Pkgとして多数用意されており、良いサンプルコードです.
このときTOOL_CHAIN_TAG変数に使うコンパイラも指定.

パッケージの作成設定
パッケージの設定は<pkg_base_name>Pkg/<pkg_base_name>Pkg{.dec,.dsc}がそれにあたる.
この場合の<pkg_base_name>は適当で、MyAppPkgとかそんな感じで適当にパッケージ名を決定.
*.decはほとんど書くことなくて、どっかのパッケージからパクってGUIDだけはuuidgenコマンドとかで適当に生成してやれば良い(とおもう)
*dscはDefinesはどこかからパクってきて、名前とGUIDだけ書き換えておくのはdecとおなじで良いとおもう.ただし、[LibraryClasses]が難敵.
ここに、他のパッケージに含まれる関数とか使いたい時にライブラリとしてロードするものを指定しておく.たとえば、LibCを自分のパッケージで使いたいとき、
このセクションに
LibC|StdLib/LibC/LibC.inf
としておく.ここで |の左は、そのライブラリのエイリアス.あとで実際にアプリケーションを書く時、infを書く必要があるのだがその時書くライブラリ名を一々ファイルパス書かずに済むためのエイリアスっぽい.
|の右のパスはライブラリの定義場所だが、プレフィクスとして頭に$EDK_PATH_TOOLS/が付いてると考えると良いと思う.
そして、[Components]セクションに、
MyAppPkg/MyApp/MyApp.inf
とかと書いて、自分が実際に作るアプリケーションの定義ファイルへのパスを書いておく.

さて、ここで出てくるinfファイルは、
AppPkg/Applications/Hello.infを参考にすると良いとおもう.
dscのDifinitionsセクションと書くことは殆どおなじだけど、アプリケーションのエントリポイントを指定する必要がある.普通のC言語同様、main( argc, **argv )形式のエントリポイントを使いたければ
ShellCEntryLibを指定する(これをするにはパッケージのdscの[LibraryClasses]にShellCEntryLibを書いた上でinfの[LibraryClasses]にも指定する必要アリ)が、 これをするとおそらくUEFI Shell以外から起動できなりそう(これは不明、試してない)
とくにライブラリ入れずUefiMainとかって指定してやればエントリポイントがUefiMainになる他、
Main引数が(EFI_HANDLE ImageHandle, EFI_SYSTEM_TABLE SystemTable)とかになる.

実際にアプリケーションでEDKのライブラリを使うには、パッケージのdscの[LibraryClasses]に指定したものに限定されるが、そこの|の左側のエイリアスをinfの[LibraryClasses]にも追加するだけ
あと別のパッケージからライブラリを読みこむのに[Packages]セクションで<pkg name>/<pkg name>.decを書く必要あり.

また、このinfファイルの[sources]セクションでソースコード名、[includes]セクションでインクルードファイルを指定する必要があり、指定されたものだけが実際にビルドされる.

ここまで書いておいてなんだが、自分でパッケージ作らなくてもAppPkg/Applications/にフォルダとinf、ソースコードを追加して、AppPkg.dscの[components]セクションに作ったinfファイルを追加するだけでもUEFIアプリケーションはつくれたりするので、お手軽.

ただし、たとえば標準にない(FAT以外の)ファイルシステムをオープンするアプリを作る必要がある時などは、パッケージのフォルダに<Driver Name>Dxeというフォルダを作成し[components]に追加し、そこにドライバを作成、配置するということがママあるので、自前でパッケージを作って必要なライブラリを追加しておいたほうが良い気もする.

ちなみにDxeはDriver Execution Environment、UEFIの起動においてUEFI BootManagerの前にロードされUEFIのAPIを実際に提供するドライバ郡.
また、<foo>PeiというフォルダをつくってそこにPre EFI Initialization、EFIドライバを読む前の手順のプログラムを追加することもあるかもしれない

ここらへんのことは私の勘が多分に含まれており、上記リンク集に挙げた
EDK II Module Writer's Guide v 0.7に設定ファイルの書き方など、良く載っている.
たぶんこれを熟読したほうが良い(面倒だからあまり読んでないです)

ドライバについて
infファイルの、MODULE_TYPE変数にUEFI_APPLICATIONかUEFI_DRIVERか選べるので後者を書いておく.
UEFI DRIVERはUEFI APPLICATIONと違いエントリポイントだけでなくアンエントリポイントも必要.どちらも変数に名前を指定.
あとはソースにエントリポイントとアンエントリポイントを書くだけ、簡単ですね.
ただしドライバはおそらくリエントラントにしておく必要がありそう(要出典)

UEFIドライバはShellCEntryLibつかえないだろうしLibCは諦めてUEFI Protocolなプログラムをしましょう.

あと、UEFI Shellやブートローダー側にAPIとして見える関数を指定して、そのProtocolだと一意に識別できるGUIDも設定する必要がある.

また、UEFI Protocolを使うには、SystemTable->BootServicesのメンバにある関数を使っていく.
たとえば、LocateHandleで特定のプロトコルについてハンドルを取得し、
OpenProtocolでハンドルとプロトコル名をつかってそのハンドルについてのプロトコルをオープン、
プロトコル名と同名の変数に開いたプロトコルがロードされるので、
その変数のメンバにある関数を使ってやればAPIが叩ける
使い終わったらCloseProtocol.
 たとえば、LocateHandleで引数にgEfiBlockIoProtocolGuidを指定してfor文でまわしておけば、Bufferに指定した変数に配列の形でブロックI/Oができるデバイスのハンドルが全部格納されるので、実際ブロックI/OしたいデバイスのハンドルだけOpenProtocolの引数に渡してやれば、
OpenProtoclはEFI_BLOCK_IO_PROTOCL*型な変数に開いたプロトコルをくれるので、
あとは(EFI_BLOCK_IO_PROTOCOL*)fooとかとすると
foo->ReadBlocks( ... とかするとブロックのリーディングをしてくれるわけである.

なんともヤヤコシイしvoidもunsigned intもVOIDとかUINTNとかとして再定義されてたりする
Win32APIっぽさ満載なCソースコードが生まれてくるのでなかなかマゾ度高いです.
sprintf系関数がMSDNに合わせて第二引数が第一引数のバッファのサイズになってるとか、
規格違反なカンジが素敵.

VisualStudioだとEDKつかってブレークポントデバッグとかできちゃうらしいのでこっちのほうがわりとおすすめ.
UEFIドライバ作るのにもUEFI Driver Wizardとかあるし…

アプリケーション全般
エントリポイントにShellCEntryLibを使わなかったとき、なぜかprintf系が動かなかった.
当初#define Print(a) SystemTable->Conout->OutputString(SystemTable->Conout, a)
とかやって出力していたが、
EDKはPrintというフォーマットストリングを処理できる出力関数を持っているらしい.
これに気がつかなくて友人巻き込んで大分時間かけてしまったりしてた.
あと、ヒープの確保はmalloc() free()でなくてAllocatePool()もしくはAllocateZeroPool()とFreePool()
を使う.テストにでるよ.

書籍
UEFIの書籍はあんまりないけど(むしろまったく?)
Intelのペーパーブックがamazon comのほうにある.
Harnessing the UEFI Shell
ただし2010年の本で、EDKのサンプルコードのところで微妙に今と似わないコードの書き方があるように思われるが、おそらくこれが一番公式な書籍なので参考にはなる.
ただし、tianocoreで公開されてる上記リンク集にあるマニュアルで十分な気もするので、
高い金を払って輸入する必要があるかは微妙



まあこんなところ.読みやすさを意識してない文章ですいませんが、
興味ある人は頑張って汲み取ってくださいな