2017年12月23日土曜日

BitVisor の UEFI 向けのブートローダーを読む(2nd stage)

この記事は BitVisor Advent Calender 2017,20 日目の記事(大遅刻)です。

UEFI 向けのローダーでも読んで記事書くかーって思ってたら先にやられてしまってウンウン唸ってましたが, 彼の記事の続きを勝手に書けば良いことに気がつきました。

では元気よく,UEFI の 1st stage loader から entry_func に飛んだ後を追っていきましょう。
いきなり findgrep を使っても良いのですが,折角手掛りがあるのでまずリンカスクリプト, bitvisor.lds を読んでみます。1st stage loader を読む記事に因ると,bitvisor.elf0x10000 だけ読んで,paddr (おそらく物理アドレス,physical address の略でしょう)に読んで,そこからエントリポイントを計算して,entry_func に飛んでるらしいです。

bitvisor.lds リンカスクリプト 2 行目,phys = 0x00100000; となっており,11 行目, .entry : AT (phys + (code - head)) { をみるとこのアドレスは bitvisor.elf.entry というセクションになってるらしいので,このセクションを探せば良さそう。ついでに,このスクリプトから bitvisor.elf がメモリに置かれるときの仮想アドレスの開始位置もわかります。

core/entry.s .entry セクションをみると,まず multiboot のヘッダが書いてあって,次に 32-bit のコードが始まってます。test 命令の結果,もしリアルモードであるならば,jc によってリアルモード用の初期化コードに飛んでますが,今回は割愛。
で,その後,32-bit なら jnc 命令で分岐せず multiboot のためのコードにジャンプ,ロングモードなら uefi64_entry に分岐しているわけですが,この分岐のやりかたが結構トリッキーというか,あんまりちゃんとなんでこれでいいのかわかってないなあと思ったら, BitVisor本体のブート仕様 2 年前のえいらくさんの記事にしっかり書いてあった……。やだ,もしかしてこの記事を書く必要なかった……?
でももう書き始めちゃったのでこのまま書いちゃいます。

core/entry.s まず後々 uefi_init 関数の引数として 1st stage loader から渡された引数を渡すために,整数として渡されてる rcx, rdx を ポインタとして扱うため rdi, rsi へ,r8rdx にコピーしておいて,準備してますね。そのあとは uefi_init を呼び出すまでページテーブル準備してるだけっぽいですね。

core/entry.s entry_pd とかのページディレクトリとかの定義はこの通り。まあこれも見たとおり。uefi64_entry 17 行目(この Gist での行数。以下,注釈なく行数が書かれた場合基本的に Gist に貼ったコードでの行数とする。)で ecx レジスタはゼロにされており,また,18 行目でページディレクトリのアドレスを ebx レジスタに(entry_pd ラベルは相対アドレスなので,インストラクションポインタに足して絶対アドレスを取ってる,で合ってますよね……?→えいらくさんから解説頂きました。mov a, %raxmov a(%rip), %rax は同じだけど前者が絶対アドレス,後者が %rip 相対アドレスになるそうです。 ),19 行目でこの行に続く 20 行目のラベルの位置の仮想アドレス計算して,eax レジスタにロードしているようです。ここで出てくる head は,リンカスクリプトの 9 行目で定義されてるアドレスですね。21 行目〜25 行目のループで,さきほど計算した仮想アドレスを格納している rax レジスタとページディレクトリの物理アドレスを格納している rbx を利用して,ecx をループカウンタにしてエントリを 512 コほど設定しています。

そのあとは,entry_pd の先頭アドレスから 7 バイト目を entry_pd の最初に設定したり,add を利用して PML4 テーブルと PDP テーブルを設定してますね。また,この時,rax レジスタの中身が,head-0x100000 にページテーブルのぶんのアドレスを足したアドレスになってるので,ここから head-0x100000 を引いたアドレスを uefi_entry_physoff に入れて,このエントリポイントの物理アドレスのオフセットを格納しているようです。

ここからは簡単で,セグメントレジスタを全部スタックに積んで,cr3 のアドレスとスタックポインタのアドレスをそれぞれ uefi_entry_cr3uefi_entry_rsp に記録し,また,uefi_entry_ret という関数の終了処理をする部分のアドレスを uefi_entry_ret_addr に記録しています。これは,おそらく,えいらくさんのスライドを参考にするに VMM から UEFI に戻るときにこれらの記録したアドレスをひっぱり出してまた UEFI に帰ってくるためではないでしょうか?
さて,その後設定した PML4 テーブルのアドレスを cr3 レジスタに設定し,スタックのアドレスを設定し,57 行目のラベルにジャンプします。ここで,uefi_init 関数をコールしています。

core/uefi.c uefi_init 関数はこの通り。
まずは 1st stage loader で entry_func の引数として渡されている UEFI ローダーの引数とブートオプションをこの関数の引数として受けとってます。
頻りに uefi_entry_pcpy を呼び出してますが,これは,

core/entry.s をみると一瞬ページテーブルを UEFI に戻して,第二引数に渡した UEFI で扱う SystemTable とかの情報を第一引数で渡した変数の物理アドレスに書き込んでから元のページテーブルに戻してるっぽい。uefi_entry_pcpyadd とか sub とかを rdx レジスタにしてるのは書き込みたい変数のサイズを確認してるからで,movsqmovsb でオペランドが指定されてないのは,この関数の第一引数と第二引数として既に rdirsi が設定されてるから第二引数から第一引数へデータがコピーされてる,んですよね?
これによって,60 行目まで SystemTableBootServices が持つ重要な関数のアドレスや情報をひっぱってきてます。 その後,起動時のオプションから,bitvisor.elf のファイルハンドルと,読み込んだアドレスと,そのサイズを取得しています。その後,

core/mm.c これを使って BitVisor のサイズを取得してから,86 行目でこのサイズに必要なぶんだけ UEFI の機能でページを確保しています。 そのあと,96 行目(_PRINT ("Load");)から 107 行目(_PRINT ("ing");)の間でアライメントして余計なページを開放してるみたいですね(ちょっと本当にそうなのか自信がないです)。しかしこの“Loading”の表示のしかたちょっと面白くて好きです。
108 行目では改めて UEFI の機能で確保したページに BitVisor の読み込んだバイナリをコピーしてます。続く 112 行目から続くループで,64 KiB しかロードしてなかった bitvisor.elf の残りのデータも全部ロードしています。
その後,ロードが完了したら uefi_entry_start を呼び出し,core/entry.s に戻ってきています。
core/entry.s まずこの関数には引数として改めてロードした BitVisor の確保されているアドレスの先頭位置が rdi レジスタに保存されています。 また,関数のプロローグでまたページテーブルやスタックを UEFI のものに戻したあとに,DIFFPHYS に設定されている値 0x40000000,つまりいままで 2nd stage loader が使ってた仮想アドレスのローダーが読まれてた位置と BitVisor がロードされているアドレスを足した値を entry_pml4 などのラベルから引いたアドレスに,rdi の値を設定しています。これで,読み込み直して BitVisor の物理アドレスがズレた分だけ動かした値をページテーブルに改めて設定しているようですね。466 行であらためて cr3 レジスタに新しいページテーブルを登録した後に,BitVisor のロードされている先頭アドレスを vmm_start_phys に保存して,BitVisor がロードされている物理アドレスを保存しておきます。また,GDTIDT,セグメントレジスタ,UEFI のページテーブルへのアドレスといったものは,全部 calluefi_uefi_* へ保存しています。これの実体は core/calluefi_asm.s にあります。
479 行目から 485 行目ではメモリの初期化をしており,486 行目から 492 行目は cr4 レジスタの設定と cr3, cr4 レジスタの保存をして,GDTR のアドレスをロードしてから,496 行目で callmain64 を呼び出しています。

core/entry.s ds,es,ss を 0x20,fs,gs をゼロに設定し,スタックポインタを設定,最初は bspinit_done に何も触れてないので jne で分岐せずにそのまま処理を続け,vmm_main に飛ぶことで,ようやく BitVisor 本体の処理が始まります。なお, マルチコアで,BSP にならなかったコアだと,bspinit_done が既に設定されている状態で jne で分岐し,apinitproc0 の呼び出しのほうに行くと思われます


とりあえず,BitVisor 本体の処理に飛ぶまでの,VMM Loader(2nd stage)を読んできました。間違って読んでる箇所があったら教えてください。 BitVisor はなんとなくどういうことに使われてるかは知ってたけどちゃんとした処理は知らなかったので,最初のローダーの部分だけでも,後々の切り替えや UEFI に戻るための仕掛けなんかがとてもなるほどと感心しました。すごい。どうせなのでこの後の部分も色々読もうと思います。

2017年12月17日日曜日

Chinachu を SELinux 有効にして使う

https://github.com/Chinachu/docker-mirakurun-chinachu

Chinachu の docker コンテナの README から抜粋
- SELinuxの無効化推奨
無効化推奨,じゃあないんだよ!有効化しろよ!ということで,SELinux を enforcing するまでの道程です。

まず最初に,ディストリビューション。Chinachu は Debian や Ubuntu での導入記事を多く見受けますが,今回は REHL/Fedora/CentOS 系統を使います。
SELinux のデフォルトのポリシーが充実してるのがその理由。
次に,Chinachu はコンテナ化したものを用います。Chinacu は npm のモジュールをローカルにインストールしたり ffmpeg をビルドしたり,パッケージ以外から導入するものが多すぎなので,それに対してラベル付けたりポリシーを書いたりするのがダルいからです。

私は Fedora 27 で設定を行い,この記事を書いてます。

では前置きはここまでにして,以下手順

1. SELinux 有効化
CentOS の場合は先にパッケージを入れておきましょう。
# yum -y install selinux-policy-targeted setools
とはいっても Red Hat 系のディストリビューションはデフォルトで有効のはずです。CentOS はパッケージいれてから再起動が必要ですが。

デ フ ォ ル ト で 有 効 の は ず で す が

もし無効化してたなら,とりあえず /etc/selinux/config の中で SELINUX=permissive にして再起動しときましょう。
なお,現状だと,docker のストレージに btrfs を使っていて,それで docker pull をすると,SELinux が enforcing のときだとコケるっぽいです。解決中。まあ pull のときだけ permissive にしてサービス動かしだしてから enforcing でも困らん気がする。

2. ラベリング

docker-compose.yml の volumes を次のように設定していると仮定します。
services:
    mirakurun:
        (略)
        volumes:
            - /etc/localtime:/etc/localtime:ro
            - /opt/tvs/mirakurun/conf:/usr/local/etc/mirakurun
            - /opt/tvs/mirakurun/db:/usr/local/var/db/mirakurun
    chinachu:
        (略)
        volumes:
            - /etc/localtime:/etc/localtime:ro
            - /opt/tvs/chinachu/conf/config.json:/usr/local/chinachu/config.json
            - /opt/tvs/chinachu/conf/rules.json:/usr/local/chinachu/rules.json
            - /opt/tvs/chinachu/data:/usr/local/chinachu/data
            - /opt/tvs/recorded:/usr/local/chinachu/recorded
そこで,次のようにします。
# semanage fcontext -a -t container_file_t '/opt/tvs/mirakurun/conf(/.*)?
# semanage fcontext -a -t container_file_t '/opt/tvs/mirakurun/db(/.*)?
# semanage fcontext -a -t container_file_t '/opt/tvs/chinachu/conf(/.*)?
# semanage fcontext -a -t container_file_t '/opt/tvs/chinachu/data(/.*)?
# semanage fcontext -a -t container_file_t '/opt/tvs/recorded(/.*)?
たまに chcon -t でラベル付けすると書いてあるブログとか見ますが,chcon は一時的なラベル付けなので,何かの拍子にラベルがデフォルトになることがあるので注意です。
docker からアクセスするディレクトリ/ファイルなので,container_file_t を設定します。このラベルはラベル

/etc/selinux/targeted/contexts/files/file_contexts.local に,先ほど設定したラベルがあるか確認してください。
Twitter で @yomi322 さんに指摘されましたが,volumes の設定を次のように変更するだけで良いようです(また,おそらくこのほうが MCS によるコンテナ間の分離も行われて善さそうです。)
services:
    mirakurun:
        (略)
        volumes:
            - /etc/localtime:/etc/localtime:ro
            - /opt/tvs/mirakurun/conf:/usr/local/etc/mirakurun:Z
            - /opt/tvs/mirakurun/db:/usr/local/var/db/mirakurun:Z
    chinachu:
        (略)
        volumes:
            - /etc/localtime:/etc/localtime:ro
            - /opt/tvs/chinachu/conf/config.json:/usr/local/chinachu/config.json:Z
            - /opt/tvs/chinachu/conf/rules.json:/usr/local/chinachu/rules.json:Z
            - /opt/tvs/chinachu/data:/usr/local/chinachu/data:Z
            - /opt/tvs/recorded:/usr/local/chinachu/recorded:Z

volume の各エントリの最後に :Z を付けてますがこれで勝手にラベリングするようです。他のコンテナと共有する volume の場合は,:Z(大文字)ではなく :z にしましょう。

3. 反映
# restorecon -R -v /opt/tvs
設定したラベルも含めてコンテキストを設定どおりに反映します。
これで,コンテナ内からホストのストレージ(の一部)へ読み書きができます。
最後に,/etc/selinux/config の中で SELINUX=enforcing にしてから,
# setenforce 1
として SELinux の強制アクセス制御を有効化します。
これだけで SELinux を有効化しながら Chinachu が使えます!やったあ!

たぶん素で Chinachu を動かすと,もっと色々手間がかかります。コンテナで隔離することは SELinux の設定の簡便さにも寄与してるんですね。
もし何か問題が起きた場合,# setenforce 0 にして enforcing から permissive に切り替え,そのあと監査ログである audit.log を眺めながら問題を修正していきましょう。


P.S.
samba_share_t を設定して samba も動かしてたのですが,Chinachu の volumes に設定しているディレクトリも samba で公開したかったので,結局 samba も docker 化して samba_share_t ではなく container_file_t にしてしまうことにしました。
ついでに docker-compose の version 3 に対応した設定ファイルにして,https://github.com/CWSpear/local-persist

ところで,この記事は,samba の docker 化の動作確認として別ホストの Kodi から samba 経由で録画を再生しているのですが,適当に選んだ録画がたまたま「ゆるゆり! なちゅやちゅみ!+」でした。再生しながらちなつちゃんが出てきたところで,そういえば Chinachu ってゆるゆり!由来だった!と気がついたのですが,自分の頭の血の巡りの悪さに愕然としたところです。

2017年12月10日日曜日

Facebook の特定の公開ページの投稿を全部クロールする(ひなビタ♪)

ひなビタ♪ Advent Calender 用の記事です。

やほやほっ,おるみんです!

みなさんは日向美ビタースイーツ♪というバンドをご存知でしょうか。
架空の地方の商店街,日向美商店街を舞台とする,町興しをテーマに少女たちが奮闘したり日常を過ごしたりするガールズバンドです。
最近はひなビタ♪の舞台のモデル,鳥取県倉吉市ともコラボを積極的にしているみたいですよ。

私の地元と近い場所だったり,鄙びた商店街という舞台だったり,自分自身の体験や実感と重なったりするところも多くて思わずハマっちゃいました☆[1]

さて,このひなビタ♪なのですが,音楽 CD や KONAMI のアーケードリズムゲームなどへの展開は勿論,小説や SNS での投稿といったマルチメディア展開をしています。
主なストーリーラインや彼女たちの音楽制作の様子といったものが Facebook を中心に展開される,非常に珍しいコンテンツと言えるでしょう。

しかし,Facebook でコンテンツを追うのは大変です。
試しにひなビタ♪のページを PC のブラウザで自動スクロールさせて古いポストを開かせようとしたところ,メモリ不足により半分くらいスクロールしたところでタブが死にました。
iPhone のアプリの場合,最後までスクロールできましたが,バッテリーが 30% くらい持ってかれた上に,スクロールをしていたのが家への帰路でしたが,数十分〜一時間くらいずっとスクロールする破目になりました。もうしません。

そこで,次のようなものを作りました。

orumin/hinabitter_read: ひなビタ♪の Facebook 投稿を快適に読みたい https://github.com/orumin/hinabitter_read
使い方は README.md の通りです。
アクセストークンを取得するコールバックは,
facebookのタイムラインへpythonから投稿する - Qiita 
をパク……参考にしました。

とりあえずこれで,2017/12/09 現在 2.4MB にもなるテキストファイルが取得できます。
今回は力足らずここまでなのですが,できれば近いうちに画像や投稿のシェアについてもちゃんと取得するようにした上で,整形して EPUB かなんかにして Kindle にメールで送り付けてシュッと読めるようにしたいですね!
2017/12/10 追記: なんか一応画像や投稿のシェアも取得した上で,青空文庫形式に整形してダンプするようにしました。でも AozoraEpub3 に喰わせると無の EPUB が出力されるので何がダメなのかちょっとわかりません……。素のテキストで青空文庫リーダーに読ませるぶんにはちゃんと表示されました。

[1]: 昔からわりと気になってたけど,ちゃんとコンテンツを追うようになりました。
以前ちくパを作ろうとした(作った)ことはあります

2017年12月8日金曜日

Apple SuperDrive を Linux で使う

Apple Inc. 謹製の CD/DVD ドライブである SuperDrive,これは実は素で Linux から使えません。
これは,sg3_utiils に入ってる sg_raw コマンドで,SCSI のメッセージとして特定のマジックを送るとドライブが起きて使えるようになります。
$ sg_raw /dev/sr0 EA 00 00 00 00 00 01
しかし,これを毎回手動でやりたくはありませんよね?そこで,次のようにします。
このように udev のルールを書いてしまえば解決です。このファイルは,/etc/udev/rules.d に配置しましょう。
尚,お気付きかと思いますが,pre-OS な環境,つまり BIOS や UEFI からは Apple のデバイスでないとこのドライブは使えないと思います。UEFI のドライバを自作したらあるいは,可能かもしれませんが。

2017年11月13日月曜日

特定のコンテナの中で動作してるプロセスの物理メモリ消費量の総計を MB 単位で出力するワンライナー

docker exec -it chinachu echo "scale=3; $(ps aux | awk '{sum += $6}END{print sum}')/1024" | dos2unix | bc
docker execps aux の RSS 値を全部足して bc(1) に渡してるだけ。
あれ,これ docker コンテナにワンライナー渡したのを bc で計算してるけど,そもそもコンテナからは ps(1) の出力だけさせて手元で足し合せたりしたほうが良いのでは,と今ここまで書いてから気がつきました。
echo "scale=3; $(docker exec -it chinachu ps aux|awk '{sum += $6}END{print sum}')/1024" | bc
これでいいですね。

P.S.
chinachu のところは好きなコンテナ名にしてね。

P.S. その 2
docker stats --format "{{.MemUsage}}" --no-stream chinachu
でよかった……(無駄なことをした……)unarist さんありがとうございます。

2017年10月25日水曜日

Unix 系 OS で コマンドの実行時により正確なタイムスタンプをつける

前回の記事の続編のようなもの
Unix 系 OS でコマンド実行間にタイムスタンプを付ける
前回の設定では精度が秒なので,ミリ秒やマイクロ秒が欲しいときに困る。

次のようにシェル組込みの trap コマンドを使うことでより細かい日時を正確にとれる
$ trap 'echo -e "\n$(date -I"ns")\n"' DEBUG
Bash でも Zsh でも trap は特定のシグナルのハンドラ(のようなもの)をシェルに設定するものだが, シグナルではなく DEBUG を指定するとコマンドやシェル関数の実行直前に設定したコマンドを実行してくれる。
ここでは,date(1)-I あるいは --iso-8601= でナノ秒単位の ISO 8601 形式のフォーマットを指定しているが,
date +"%F %T.%N"
のように + でフォーマットを與えるのでも良かろう。

ちなみにこれを自分の手元の zsh でやったらプロンプトの文字列に色々仕込みすぎてたせいでコマンド実行前にかなりタイムタンプが出てきた上にプロンプトが若干壊れました。

2017年10月23日月曜日

Unix 系 OS でコマンド実行間にタイムスタンプを付ける

Bash や Zsh 向け。POSIX sh だとたぶん出来ない。

Bash の例
20 2017-10-23 18:30:54 echo $PATH | sed -e 's/:/\n/g'
21 2017-10-23 18:30:54 uname -a
22 2017-10-23 18:30:54 exit
23 2017-10-23 18:30:56 cd
24 2017-10-23 18:31:04 HISTTIMEFORMAT='%F %T '
25 2017-10-23 18:31:06 history
Zsh の例
16384 2017-10-23 18:37:02 popd
16385 2017-10-23 18:38:30 ls
16386 2017-10-23 18:38:31 pwd
16387 2017-10-23 18:38:34 cd Desktop
16388 2017-10-23 18:38:37 cd
16389 2017-10-23 18:38:39 exit
ではどうやってやるか紹介。
まず Bash,これは次のように環境変数を設定すれば良い
export HISTTIMEFORMAT="%F %T "
この環境変数は日時以外も色々文字列を埋めることができる。ここで使える書式指定子は,おおむね strftime(3) に従っているのでこれの man page を見ると良い。
だがしかしミリ秒やマイクロ秒といった単位は使えないのか %L%N は無視される……。

次に Zsh。
setopt extended_history
これを .zshrc に書けば,history コマンドを使うときに,
history -i
an zshbuiltinan zshbuiltinなどオプションを與えることで実行時間が出てくる。
-i 以外にも様々なオプションがあり,
-d オプションは日付しか出さないが,-f を加えるとアメリカ式で年月日も表示する。
Bash のように好きなフォーマットで出力するには,
history -t "%F %T "
のように -t オプションに strftime(3) に準じる書式指定子を埋めたフォーマット文字列を渡せば良い。常にこの表示が好ましいなら alias を設定すると良いと思われる。

history で指定できる他のオプションは,man 1 zshbuiltins にて fc コマンドのオプション一覧を見ると良い。