2015年3月26日木曜日

Google Container Engineでハマったところ

Google Container Engine (GKE)はKubernetesのクラスタを作り,Dockerコンテナを大量に上げるまで自動でやってくれたりするサービス.
VMコンテナ自体はGCEと同様に管理できます.

で,クラスタなんかもGoogle Cloud SDKのgcloudコマンドで色々と手元からCLIでできるのですが,ハマりました.

ハマりポイント

gcloud preview container kubectl create -f sample.json --cluster="<CLUSTER NAME>"
これでsample.jsonにswagger形式で記述したクラスタが上がる……はずなのですが
なぜか手元ではタイムアウト.次のようなメッセージが飛びます
ERROR: F0326 10:43:53.728513    5117 get.go:166] Get https://130.211.179.83/api/v1beta1/pods?namespace=default: dial tcp 130.211.179.83:443: i/o timeout
kubectl get podsなど,他のkubectlのコマンドも同様にタイムアウトしてハマってました.

これは,一度作ったクラスタを削除して,また同じ名前でクラスタを作成した後の操作でした.
実はこれがハマったところのポイントで,
$HOME/.config/gcloud/kubernetes/<PROJECT>.<ZONE>.<CLUSTER NAME>/
に配置されている,cluster.jsonに,
{
    "endpoint": "130.211.179.83"
}
のような形式でgcloudコマンドを通してそのプロジェクトへkubectlをする時のエンドポイントのIPアドレスがキャッシュされてました.
なので,以前同じ名前でクラスタを作って,gcloudコマンドを使った事がある場合,その前の時のIPアドレスをつかってkubectlを発行するため,タイムアウトしてしまうというオチでした.

このcluster.jsonを削除してしまえば解決します.

2015年3月24日火曜日

EdgeRouterLite-3をGentooで運用しはじめました+IPv6導入(本篇)

前提

  • これまでのネットワークは,BaffaoのBBR-4MGをブロードバンドルータにしてた
  • 100Mなネットワークで,IPv6未対応だったので,IPv6+GbEにしたい
  • 既存ネットワークのトポロジは下図の通り.
  • ただし,これからの記事でアドレスについてはxxxxで伏せさせていただきます
ファイアウォールはBBルーターとパーソナルファイアウォール任せな感じの,よくある一般的なネットワークっぽい.APは諸事情によりRaspberryPiにやらせてて,RaspberryPiにはUSB HDDが接続してありLAN内のファイルサーバとなっている.

これを,次のようにした.

いままで市販のBBルーター任せであんまり考えてなかったファイアウォールルールをイチから見直した事と, 外から自宅にアクセスするために外部にもBBルータの機能で静的ポートフォワードしていたRaspberryPiを, サブネットで分離してちゃんとDMZとして機能するようにさせたところがポイント.とはいえDMZは全部フォワードしてるわけじゃなくて,結局静的ポートフォワードでやってます.

図に書き忘れましたが,IPv4 PPPoEの出口がppp0,IPv6 PPPoEの出口がppp1になってます.

IPv6だと,NATが必要ないのでちょっと設定に戸惑いました.むしろNATとか頭から追い出してちゃんと考えればIPv6のほうがラクっぽいかも.

ネットワーク図がアレゲなのは勘弁してください……
あと,セキュリティの事を言うとまだまだ足りないとこが沢山ある気がしますね.
本当はルーターに色々やらせすぎなんだとおもいますが,ウチは負荷がかかるネットワークではないのでそこは良いかな.

なお,フレッツ系のプロバイダで,IPoEなIPv6が使えない環境だったので,
PPPoEを2セッション張る事にしています.

実際どのようにしたか,以下に記します.
参考: Home Router - Gentoo Wiki

設定(IPv4)

このように設定して,
# cd /etc/init.d && ln -s net.lo /etc/init.d/net.ppp0
# rc-update add net.ppp0
# rc-update start net.ppp0
これでIPv4のPPPoEは自動起動します.
ユーザーネーム,パスワード,dnsサーバーはISPから通知されたものを記入しておきます.

次にこれはdnsmasqの設定です.DHCPサーバーと,簡易DNSキャッシュサーバーをやってくれます.他にも色々機能はありますが……
とりあえずこのようにeth1とeth2にDHCPを振っておきました.
他にもDMZのRaspberryPiと内部ネットワークのPS3にもMACアドレスに対して固定IPを振ってます.

最後に,iptables.

これでNATが有効になり,パケットがフォワーディングされます.
ここで,RaspberryPiに接続したHDDに録画データを溜めてるのですが,
これをDLNAでPS3から再生したかったので,minidlnaを導入してます.
そしてサブネット越えのため,igmpproxyも導入しました.

参考にしたGentoo WikiはLAN側のサブネットは1つである前提であるため,2つのサブネットのルーターの設定に最初悩みました.特に,dnsmasqのタグ機能に最初気がつかなかったところと,iptablesのフォワードのルール.

設定(IPv6)

まず最初に,IPv6の場合のPPPoEについて調べました.

図はNTT東日本の次世代ネットワークに関する方法のページより,NGN IPv6 ISP接続<トンネル方式> UNI仕様書から拝借.

フレッツ系のISPでIPv6 PPPoE接続の場合,対向サーバーとPPPoEクライアントはリンクローカルアドレスで通信が確立されますが,グローバルアドレスがまだ払われません.
IPv4ではIPCPでアドレスが払われますが,IPv6CPではプレフィクスが払われる直前まででシーケンスが終わってるためです.

なので,ここでDHCPv6-PDにより,プレフィクスを上位より払い出してもらう必要があります.フレッツの場合,/56のプレフィクスが払い出されます.下位64bitは自分で決めて,PPPのインターフェイスにそれを設定する必要があります.

まず,PPPoEの設定について.IPv4の時のコンフィグファイルに次を追記.
更に
# cd /etc/init.d && ln -s net.lo /etc/init.d/net.ppp1
# rc-update add net.ppp1
# rc-update start net.ppp1
次にDHCPv6クライアント.

私は,Gentooのportage treeにあったのでdibblerをつかいました.
dhcpcdISC dhcpだとpppのインターフェイスに対しては使えないので,別の物を使う必要があります.KAMEプロジェクトのWIDE DHCPv6でも良いとおもいます.

dibblerの場合,デーモン起動すると/var/lib/dibbler/以下にxmlでプレフィクス情報等が吐き出されるので,それを使って自分で設定します.
24xx:xxxx:xxxx:xxxx/56が払われたとして,ppp1の下位64bitは0000:0000:0000:0001を割り振るとします.
# ip -6 addr add 24xx:xxxx:xxxx:xxxx::1 dev ppp1
# ip -6 route add dev ppp1

これでルーターからはping6コマンド等で外との疎通を確認できるはずです.

さて,LANのノードにもプレフィクスを教えてやらねばなりません.
そのためには,DHCPv6でステートフルに設定するか,SLAACでステートレスに設定するかの二通りがありますが,私は後者を使いました.

SLAACはRA(Router Advertisation,ルーター広告)を使います.
LAN側にルーターがRAを流せば,LAN側のノードは受け取ったプレフィクスと,自分のイーサネットインターフェイスのMACアドレスを組み合わせて,自動にグローバルアドレスを生成するので楽です.

また,SLAACでグローバルアドレスを設定すると,デフォルトゲートウェイは自動的にRAを流したノードに設定されます.

DNS情報だけは別の手段で(たとえばdnsmasq)教えなければならない……という過去があったようですが,今はRAでDNSも通知できるそうです.

RA通知にはradvdを使いました.radvdのコンフィグは以下です.
これでLANに向けてプレフィクスが広告されます.
よって,IPv4とちがい特にiptablesを設定したりしなくても,個々のノードにIPv6のグローバルアドレスが振られるため通信が可能となります.
しかし,フィルタリングをしないとIPv4+NATよりも脆弱な状況です.

なので最後にIPv6のiptablesを設定しました.

ICMPv6はICMPと違い,ARPの代わりに近隣探索等も熟すので,許可してあります.
が,もうちょっと本当は絞れる気がするので今は試行錯誤しています.

補足

本来,起動スクリプトで,IPv6 PPPoE → dibbler → dibblerが/var/lib/dibblerに出力したxmlをパースしてpppインターフェイスへグローバルアドレス割り当て,ルーティング設定 → さらにそのプレフィクス情報を使ってradvd.confを自動生成 → radvd起動
という風にすべきだとおもいます.
しかし,ISPから払い出されるプレフィクスは半固定で,滅多な事がないと変化がないようなので,プレフィクス部分を決め打ちにするコンフィグファイルで対応してしまいました.

また,上記設定について,説明を端折りましたが,rc-updateコマンドでそれぞれのデーモンは登録する必要がありますね.

EdgeRouterLite-3をGentooで運用しはじめました+IPv6導入(OS環境構築篇)

先日,@syuu1228先生からEdgeRouterLite-3を頂く機会があったので,
自宅のネットワークを全面的に直しました.

EdgeRouterLiteとは

RJ45ポートは左から,コンソール,eth0,eth1,eth2.右横にUSBポート.

Ubiquiti社の製品,EdgeRouterシリーズの下位モデル.EdgeRouterLite-3はギガビットポートを3つ持つルーターで,この3つの使い道は特に限定されてません.
2ポートをブリッジしてWAN+LANなよくあるBBルーターでも良し,1WAN2LANなルーターでも良いし,2WANにしてロードバランスとか冗長化するのも良いかもしれません.

性能としては,OCTEONのMIPS64 500MHzのデュアルコアに,DDR2メモリが512MBと,
結構高性能です.ハードウェアスペックだけを見れば,定価10万円くらいのYAMAHA RTX1200以上の性能が出ると噂です.ポートが少ないですが…….価格はUS$100とかなりお手軽,ただし日本だと代理店が無いので
楽天から買うと2万円ぐらいになるかもしれません.

詳しくはInternetWatchのこのレビューがわかりやすいでしょう.
どっちでAzureにつなぐか? Ubiquiti Networks「EdgeRouter LITE(ERLite-3)」を試す

個人的には公式のプロダクトのページのCiscoに喧嘩売りにいってるプロモーションビデオが素敵で好きです.

環境構築

EdgeRouterLiteにGentoo入れました.
IPv6をどうしたか,が気になるかたはつづきの記事を読んで,この記事の残りはすっ飛ばして良いです.

参考リンク: MIPS/ERLite-3 Gentoo Wiki

EdgeRouterLiteはu-bootが収められてるNORメモリと,OSが収められてるUSBフラッシュメモリなストレージデバイスの2つの記憶領域を持っています.
後者はデフォルトだと2GBのものがボード上のUSBポートに挿してあり,EdgeOSというVytta,VyOSベースのOSがインストールされています.

これを別の,Gentooをインストールした大容量のUSBフラッシュメモリに挿し替えました.


私はこれを買いましたが,べつにこれでなくとも良いと思います.
Gentoo Wikiにて,いくつか動作確認済みのUSBメモリが記載されています.
が,スペースが限られるので,あんまり適当なのだと挿さらない可能性が高いです.

それでは肝心の構築の手順です.

ストレージ環境構築

まず,このフラッシュメモリデバイスはいきなりERLiteに挿さず,なにかしらのLinuxマシンに挿して,作業を行ないます.
まずはフォーマット.

fdiskをするまえに,Gentoo Wikiの情報によると元のディスクの先頭2048ブロックはバイナリが書き込んであるようなので,元のディスクから,
# dd if=/dev/sdb of=/dev/sdc bs=2048 count=1
とその部分を新たなディスクにコピーします.
本当は,先頭80ブロックに8086バイナリが入ってて,その後0x01BEからパーティションテーブルが始まってて,パーティションテーブルの後ろはzero fillされてるだけなので,よくわからないです.が,u-bootが決め打ちで最初のパーティションのアドレス読みに行く可能性高いですし,ここは何も考えず元のディスク通りにする事に.

先頭80ブロックのバイナリは http://www2.onlinedisassembler.com/odaweb/CfbHic/0#view/tab-assembly/offset/00000000 で,RaspberryPiのSDにもこれが書き込まれるようですのでもし興味ある方居れば調べると良いかも.

後にfdiskを行ないます.今回は元ディスクのパーティション構成をそのまま参考に,/bootと/を作ります.また,それに追加してswapも作成しました.swapを作らねばgccのemergeに失敗します.
1K-blocksで表記して,順番に,

/dev/sda1     140805 blocks (/boot)
/dev/sda2 28441604 blocks (/)
/dev/sda3                     残り (swap)

という風にパーティションを切り,/bootをext2,/をext4にしました.
/bootは,u-bootが読めるのがfat32かext2なので注意./については,カーネルにFSのモジュールが組み込んであればなんでも良いです.
私はbtrfsにしようとしたところ,なぜかマウントできなくなったりする現象が発生したので,止めました.

Stage3 tarball

フォーマットが終わったら,/にするパーティションを,/mnt/ERLiteにmountしたものとして作業を進めます.
http://distfiles.gentoo.org/experimental/mips/stages/mips64r2/2014/stage3-mips64r2_multilib-20140904.tar.bz2
からstage3のtarballと,
http://distfiles.gentoo.org/snapshots/portage-latest.tar.bz2
からportage treeのtarballをDLし,どちらもを/mnt/ERLiteに展開します.
# tar xvf stage3*.tar.bz2 -C /mnt/ERLite
# tar xvf portage-latest.tar.bz2 -C /mnt/ERLite/usr

Linux kernel

仕上げにカーネルを準備します./bootにするパーティションを/mnt/ERLite-kernelにmountしたもとのします.
まず,MIPS64のクロスコンパイラを用意しましょう.mips*elと,末尾にelが付くものはリトルエンディアン用なので使えません.Gentoo Linuxなら,
# emerge crossdev && crosdev -t mips64-unknow-linux-gnu
とするだけです.
ArchLinuxならば,
$ yaourt -S cross-mips64-linux-gnu-gcc
とすればインストールできます.(できるようになりました)
Archのほうはこれでインストールできなかったら私ががんばってPKGBUILD修正します.

次にビルド.
https://github.com/xypron/kernel-edgerouter/tree/master/config 
こちらにERLite用にコンフィグを作っている方が居られるので,これをそのまま使わせてもらいましょう.
つまり,
# wget https://raw.githubusercontent.com/xypron/kernel-edgerouter/master/config/config-3.18
# wget https://www.kernel.org/pub/linux/kernel/v3.x/linux-3.18.9.tar.xz
# tar xvf linux-3.18.9.tar.xz
# cp config-3.18 linux-3.18.9/.config
# cd linux-3.18.9
# ARCH=mips CROSS_COMPILE=mips64-unknown-linux-gnu- make
# ARCH=mips CROSS_COMPILE=mips64-unknown-linux-gnu- make modules_install INSTALL_MOD_PATH=/mnt/ERLite-kernel
# cp vmlinux /mnt/ERLite-kernel/vmlinux.64
# md5sum vmlinux > /mnt/ERLite-kernel/vmlinux.64.md5
これでインストール完了です. ArchLinuxで,上記のようにyaourtでgccを作成していた場合は,CROSS_COMPILEにmips64-unknown-linux-gnu-ではなく,mips64-linux-gnu-を指定する必要があります.

ただし,GCCとカーネルの準備はやや面倒です.
そこで,私の環境で使ってるビルド済みのカーネル(3.18.9)をここに置いておきます.
https://drive.google.com/file/d/0BwiUnMv7MhiJQ0lBRklXRktndEE/view?usp=sharing
これを,/にするパーティションを/mnt/ERLiteに,/bootにするパーティションを/mnt/ERLite/bootにmountした上で,
# tar xvf erlite-3-kernel.tar.bz2 -C /mnt/ERLite
とすればカーネル導入のための一切が省略できるかとおもいます.

システムコンフィグ

Gentoo Wikiには記載ありませんが,stage3を展開した後に,/etc/fstabを自分のパーティションに合わせたり,/etc/conf.d/以下に自分の好みでキーマップや環境変数を変更するといった,若干のシステムコンフィグをしておきましょう.
/etc/inittabでシリアルコンソールの設定もしておきましょう.

ブートローダー

ではブートローダーです.u-bootの設定を変更するために,RJ45な端子を持つシリアルコンソールケーブルが必要です.


既にUSB to RS-232Cなケーブルを持ってる人は,ノーブランドで300円ぐらいのRJ45 to RS-232CもAmazonにありますが,私はそれをポチったらシンガポールからの発送になり一ヶ月くらい待つハメになりました.オススメしません.
そして,これをPCに接続し,minicom等を用いてシリアルコンソールを開きます.
ERLite-3からUSBフラッシュメモリは外して置いてください.

シリアルの設定は,ボーレートが115200,データが8bit,ストップビットが1bit,パリティビットは無し.フロー制御もソフトウェア/ハードウェアどちらも無し.

そして電源を入れるとシリアルにu-bootのプロンプトが表示されるので,
> setenv bootcmd "ext2load usb 0 $loadaddr vmlinux.64;bootoctlinux $loadaddr coremask=0x3 root=/dev/sda2 rootdelay=15 rw mtdparts=phys_mapped_flash:512k(boot0),512k(boot1),64k@3072k(eeprom) net.ifnames=0 console=ttyS0,115200 panic=5"
> saveenv
としましょう.u-bootの操作はうっかりするとERLite-3が文鎮化する可能性があるのでお気をつけ下さい.
また,このbootcmdの最初に指定されてるext2loadは,/bootがfat32ならfatloadに,ext2ならext2loadにしましょう.ただ,体感的にはfatloadのほうが動作が速く感じました.

OS環境構築終了

ひとまずこれでGentoo環境は構築完了です.あとは,nano以外を使う方は好みのエディタをインストールしたりすると良いでしょう.また,とりあえずsshdを入れて適当にIPアドレスの設定だけはしておくとコンソールケーブルに繋げなくて良くなるので便利です.ここから,ルーターを構築します.

つづき

2015年3月5日木曜日

Intel Memory Protection Extensions (MPX)

つい先日,Linusが,次のような苦言を呈しました.
Why would I want to enable this in my kernel when there are no actual CPU's out yet that support it?
「どうして実際にCPUがまだサポートしてない機能をボクのカーネルで有効にしたいと思えるんだ? 」
このような苦言にも関わらず,これは3.19へとマージされました.
これはMemory Protection Extensions (MPX)という機能のサポートについてでした.
ところで,MPXとはいったいなんでしょうか?


MPXとは

MPXとは,Intelのtick-tackのtick,次の新アーキテクチャであるSkylakeにて実装予定の新機能です.
これは,具体的には配列の境界チェックをハードウェアでサポートする事でexploitを防ぐ機能となります.

Intel CPUの命令リファレンス[PDF]の第9章がまるまるその機能に割かれています.

MPXのため,BNDレジスタが4本追加され,このBNDレジスタは1本128bitです.
このレジスタは上位と下位でそれぞれアクセス可能で,64bitが2つという事になります.
BNDレジスタの上位はUB(UpperBound),下位はLB(LowerBound)と名付けられており,
勘が良い方は気がつかれたかもしれませんが,配列の上界のアドレスと下界のアドレスをそれぞれ格納します(そのため64bitが2つなんですね)

また,設定レジスタがユーザーモードのためにBNDCFGU,スーパーバイザーモードのためにIA32_BNDCFGS,状態レジスタとしてBNDSTATUSが追加されています.

レジスタ構成

拡張レジスタ



BNDCFGU,BNDCFGS

BNDSTATUS

命令

BNDMK b, m: BNDレジスタbにLowerBound(LB)とUpperBound(UB)を作成
BNDCL b, r/m: メモリ参照かレジスタにあるアドレスについて,LBをチェック
BNDCU b, r/m: メモリ参照かレジスタにあるアドレスについて,UBをチェック (1の補数表現)
BNDCNb, r/m: メモリ参照かレジスタにあるアドレスについて,UBをチェック (1の補数表現の否定)
BNDMOV b, b/m: BNDレジスタかメモリから,LBとUBをコピー/ロード
BNDMOV b/m, b: BNDレジスタかメモリに,BNDレジスタのLBとUBをストア
BNDLDX b, mib: 境界値のロード( SIB:Scale Index Baseアドレッシングを使う )
BNDSTX mib, b: 境界値のストア( BNDLDX同様SIBつかう )

実際に使う

Intelが提供するSDE( Software Development Emulator)を使う事で,実際に試す事ができます.SDEを用いると他にもAVX,AVX-512といったものも試せるので,対応プロセッサを持っていない人に良いのではないでしょうか.

使い方は https://code.google.com/p/address-sanitizer/wiki/IntelMemoryProtectionExtensions に記述されてますが,情報が古いです.
ので以下に補足含めて説明.
環境がLinuxであることを前提で話を進めます.

準備1:ダウンロード

まず,https://software.intel.com/en-us/protected-download/267266/144917
よりライセンスの同意にチェックを入れた後に,
sde-external-*-lin.tar.bz2と*-mpx-runtime-external-lin.tar.bz2をダウンロード.
*のところは日時,バージョンが入りますが,新しいものを選べば良いです.
次に,https://software.intel.com/en-us/articles/intel-software-development-emulator#gcc
よりgcc_install_5.0.0-mpx-*.tar.gzとbinutils-gdb_install_*.tar.gzをダウンロード
こちらも*のところはリビジョンやバージョン,日時が入りますが新しいものを選びましょう.

準備2:解凍,パス設定

では,準備を進めましょう.解凍時のディレクトリ名は自分の環境に合わせて変更してください.
$ mkdir $HOME/mpx_test
$ export MPX_HOME="$HOME/mpx_test"
$ cd $MPX_HOME
$ tar xvf ../sde-external-6.22.0-2014-03-06-lin.tar.bz2
$ tar xvf ../2014-02-13-mpx-runtime-external-lin.tar.bz2
$ tar xvf ../binutils-gdb_install_2.24.51.20140422.tar.gz
$ tar xvf ../gcc_install_5.0.0-mpx-r214719.tar.gz
$ export SDE_KIT=$MPX_HOME/sde-external-6.22.0-2014-03-06-lin
$ export MPX_RUNTIME_LIB=$MPX_HOME/2014-02-13-mpx-runtime-external-lin
$ export MPX_BINUTILS=$MPX_HOME/binutils-gdb_install_2.24.51.20140422
$ export MPX_GCC=$MPX_HOME/gcc_install_5.0.0-mpx-r214719
これで,準備は完了です.上記のサイトではGCCは自分でビルドするようにありますが,
いまは配布されているバイナリで動作します.

コンパイル

サンプルコードはこの通り.そして,これを次のようにコンパイルします.
$MPX_GCC/bin/gcc -fcheck-pointer-bounds -mmpx -L$MPX_RUNTIME_LIB -B$MPX_BINUTILS/bin -lmpx-runtime64 -Wl,-rpath,$MPX_RUNTIME_LIB global_buffer_overflow.c
参考サイトでは-fcheck-pointersというオプションでしたが,これは-fcheck-pointer-boundsに変更されているので注意を.
ここで,
$ CHKP_RT_MODE=count $SDE_KIT/sde -mpx-mode -- ./a.out
と実行すると……
g: 0x600bc0 0x600be8
Bound violation detected,status 0x1 at 0x400695
finishing
はい,ちゃんと境界をチェックできてますね.
なお,SDEでこれを試すのに,カーネルでセキュリティモジュールのYamaが有効な場合,ptrace_scopeを無効にする必要があります.
$ echo 0 | sudo tee /proc/sys/kernel/yama/ptrace_scope
また,上記サンプルコードを-S -O0でアセンブリコードに出力したものは以下になります.
ちゃんとbnd*な命令が使われていることが確認できます.

2015年2月26日木曜日

ハッピーバースデイ,僕 〜そして恐怖のシュールストレミング〜

2/7は私の誕生日でした.
GSoCとか,今させてもらってるバイトとか,色々と学ぶところが多く,
これからの一年も色々と学んでいきたいなあと思う所存.

誕生日は色々と頂いてしまいました ...

2014年12月25日木曜日

ブートローダーは4行で実装される

この記事はUEFI Advent Calender自作OS Advent Calenderのためにかかれました。
22日(+3日)の記事です。

遅れてもうしわけないです。

ブートローダー

まず、自作OSにはブートローダーによってカーネルをロードしなければはじまりません。
特段複雑なローダーがなくとも、たとえばBIOSから直接起動するのなら
そのための形式で用意せねばなりません。
しかし、これが自作の入口としてかなりの関門になってしまっているのではないでしょうか。

なので自作OSからはちょっと離れますが、ブートローダーについて書きます。

UEFI

ということで、UEFI Appsとしてブートローダーを実装する話です。
まず、UEFIとは何かについてはUEFI Advent Calender 1日目の記事を、
そしてこの作業の前知識としてUEFI Advent Calender 12日目の記事を参照していただけると良いです。

一応ざっとUEFIについて触れておくと、
BIOSの代替規格のファームウェアで、多機能かつ高機能。また、可搬性も考えられています。

UEFIのファームウェア上で直接動作できるUEFI Appを記述するには、
tianocoreがBSDLで公開しているEDKというツールキットか、
Gnu toolchainと合わせて使えるgnu-efiというBSDLのライブラリを用います。
EDKでも一応gcc等が使えます。

EDKは、パッケージとして豊富にライブラリが揃っており、libcやbsd socket、pythonがつかえます。
gnu-efiは、Linux/OS Xの上で開発するにはEDKより手軽にはじめる事ができます。

UEFI AppはWindows同様PE形式のバイナリなので、
実はEDKやgnu-efiがなくとも*.dllなバイナリ(つまりrelocatableでsharedなPEバイナリ)を作成してから、objcopyコマンドとかで必要な部分だけ取り出して拡張子がefiのファイルにするとかでも一応動作できるバイナリを作る事は可能です(が、面倒です)

UEFIのAPIであるProtocolは初期状態でもFile I/OやGraphicsの処理まで揃っていますが、
自分でUEFI Driverを実装してこのドライバをロードすればProtocolを増やせます。
今回はDriverは実装しません。

実装

さて、そしてUEFI Appとして実装されたブートローダーがこちらになります。(料理番組風)
後でコードの説明をするので、最初は読み飛ばしても結構です。

いきなりコード貼ってしまいましたが、そんなにコード量はないですね。

Protocol

さて、UEFI Appですが、uefi_mainというエントリポイントが
EFI_IMAGE_HANDLE、EFI_SYSTEM_TABLEという引数をとって起動していますね。
InitializeLibを行なっているのは、gnu-efi独特の作法です。

その後早速Print();なんてやってますが、これでいきなり文字列出力ができてしまいます。
単純ですね。

その次からが、肝心のブートローダーです。

UEFIのAPIは、ただ関数コールすれば良いものではありません。
全てのProtocolには、それぞれの構造体が定義されています。
BIOSメーカーでありUEFIコンソーシアムの参加企業でもあるPhoenixのwikiに、
構造体の一覧が見やすく書いてあるのでこれを見ると良いです。

UEFIのProtocolは、まず使いたいProtocolと同名の型の構造体を宣言し、
OpenProtocol();にその構造体と、使いたいProtocolのGUIDと、いくつかのオプションを渡すことで、
渡した構造体のメンバに欲しい情報であったりとか、必要な関数ポインタが代入されます。

そこで、その情報を使うなり、関数ポインタから関数を呼ぶなりして、Protocolは使われます。

しかし、OpenProtocl();自体もある種のProtocolの関数であるため、そのような関数がグローバルに
宣言されているわけではないです。

ではどうやってProtocolを呼ぶかというと、UEFI Appの起動時に
BSという大域変数(EDKの場合はgBS)にEFI_BOOT_SERVICEのProtocol構造体に
必要な情報が詰められて最初から使えるようになっています。
よって、BS->OpenProtocol();のようにして、Protocolを呼び出します。

このブートローダーのコードでは、Print();の次にuefi_call_wrapper();という関数の
第一引数にBS->OpenProtocolを渡していますが、
これはUEFI Protocolの呼び出しが一般的なC言語の関数呼び出し(cdecl規約)ではなく
Win32API同様のstdcall規約を用いているためのwrapperです。

uefi_call_wrapper(つかいたいProtocolの関数ポインタ, その関数の引数の数, その関数への引数);
という使い方になります。

ブートローダー仕組み

では、ブートローダーの本格的な部分である17行目以降から解説します。

まず、エントリポイントの引数であったEFI_IMAGE_HANDLE型のImageHandleを使って、
EFI_LOADED_IMAGE_PROTOCOLを構造体LoadedImageParentに入れています。

このIMAGEというのは、実行メモリイメージの事(だと思われます)
ImageHandleについては実行している自分自身のハンドルになります。

LOADED_IMAGE_PROTOCOLというのは、自分自身やBS->LoadImage()によって
既にメモリ上にロード済みのイメージに対して情報を得る事ができます。

続く23行目では先程入手した自分自身のハンドルのDevicePathを使って、
\vmlinuzというファイルパスのファイルをEFI_DEVICE_PATH型の変数に代入しています。

これはどういうことかというと、
自分自身はブートローダーなので、かならずEFIのシステムパーティションに置かれています。
このパーティションはHDDの先頭のパーティションでFAT32です。
なので、自分自身のDevicePathはこのシステムパーティションのDeviceそのものを指す事になります。
更に、\vmlinuzということなので、このシステムパーティションのroot directoryにあるvmlinuzファイルについてのDevicePathをPathという変数に格納した事になります。
このDevicePathはブートローダーがカーネルをロードするのに用います。

29行目のBS->LoadImage()で呼び出しているのがそれです。これでカーネルはメモリに展開されています。

46行目のBS->StartImage()でこのロードされたイメージを起動するだけで、カーネルは起動されます。

29行目と46行目の間の処理は、ロードしたカーネルイメージについてLOADED_IMAGE_PROTOCOLを手に入れ、LoadOptionsというメンバに起動オプションを
指定することで、カーネルコマンドラインを指定しています。

自作OSをつくる上では、エラー処理や後処理を省くと、
17行目 23行目 29行目 46行目
この4行の処理のみでブートローダーは完成してしまっています。

さらに、USBメモリなどでEFIシステムパーティションのみで完結させる構成のディレクトリに
このようなブートローダーと自作カーネルとあと必要なバイナリや設定ファイルを適当に配置して
しまうだけで、おそらくさくっと自作OSの入口に立つ事が可能でしょう。



と、いうことで、自作OSをしたい方は是非、UEFIを前提に考えてみてください!

なお、このブートローダーでLinuxカーネルを起動してみたい場合、
EFIシステムパーティションのroot directoryにvmlinuzとrenameされたカーネルを配置し、
11行目のカーネルコマンドラインを自分の環境に合わせて書きなおしてください。
また、initrdについては度外視しているので、カーネルにroot partitionのファイルシステムのドライバを組み込んでおかないと、起動時にroot partitionがmountできずカーネルパニックを起こします。

このブートローダー自体は、EFIシステムパーティションの好きな所において、
bcdeditなりblessなりefibootmgrなりでUEFIにそのPathを登録するか、
UEFI Shellから直接このバイナリをたたいてやってください。

上記コードのMakefileも含めたリポジトリはこちらになります。
https://github.com/orumin/SimpleMyLoader

2014年12月20日土曜日

UEFIアプリケーションのはじめかた

UEFI Advent Calender 12日目だったoruminです
盛大に遅刻かましてもはや遅刻とさえ言えない感じです。

UEFIのアプリケーション、まずどう作るかわからない方も多いかと思います。

まず、UEFIが実行できる実行形式について、これはWindowsと同じPEバイナリです。

また、EDKのようなツールキットを用いる必要がありますが、
今回はgnu-efiで説明します。

gnu-efiとは、BSDLなEFIアプリケーション開発用ライブラリです。
LinuxやBSDでの開発に親和性が高いと思われます。

これは最近だとaptやpacmanでそのままバイナリインストールが可能です。

そして、次にこのようなファイルを用意します




前者がUEFIでのHello, World!です。efi_mainがエントリポイント、
ImageHandleもSystemTableもUEFIのAPI(Protocol)を呼び出し、使うのに必要なパラメータとなります。
今回は、gnu-efiの初期化をしてから文字列を出力するだけなので特に使用していません。
デバッグ文字列出力は
SystemTable->ConOut->OutputString(SystemTable->ConOut, L"Hello, EFI!\n");
でも良いのですが。

後者のMakefileについては、書いてある通りです。
ライブラリとインクルードファイルの指定、
それにリンカスクリプトやスタートアップルーチンはgnu-efiのものを使うように指定。
shared objectな実行ファイルを作ってから、
objcopyコマンドで必要な部分を抜き出す(ELFヘッダは要らないので)
という流れになります。
CFLAGSについては、色々ごちゃっと指定してますが、
まず必須なのは-fPICと-fshort-wchar、そしてx86_64であれば-DEFI_FUNCTION_WRAPPERだけで、あとのは特段必ず指定しないといけないものではないです。

さて、これでmakeをしますと、hello.efiが作成されるので、

あとはEFIシステムパーティション直下にでも置いてあげて、起動時にUEFI Shellを立ち上げてから
hello.efi
とコマンドを入力してみてください。Hello, EFI!と文字列が出たら成功です!

UEFIの開発やProtocolについては、1日目の記事の最後に書いたドキュメントがオススメですが、
リファレンスよりも手軽にProtocolの型や名前だけ把握するには、
Phoenixのwikiが便利です。