借り初めのひみつきち

仮ブログです。

妖精さんとなかよくなるほうほう

年初まで x64 や WebAssembly で Rust を色々いじってましたが、少しお休みして x86 でコードを書いてみようと思います。

動かす環境はベアメタル ʕ•ᴥ•ʔ になりますが、 Rust はベアメタルの適切なターゲットがありません。 CPU さえ合ってれば OS はどうでも良さそうだったのでメジャーなターゲットでいくつか試してみました。 しかし、どうやらターゲットごとに適切なリンカーが必要になるようです。

リンカーに困ったら LLVM

ということで、 LLVM をインストールして LLD を読み込むように指定しました。

具体的には .cargo/config に

[target.i586-unknown-linux-gnu]
linker = "/opt/homebrew/opt/llvm/bin/ld.lld"

みたいな記述を加えます。 これ、環境依存ですよね・・・

いかがだったでしょうか?

ということで、 ELF 形式のバイナリ出力できるようになったのでローダーを作るための準備をします。

Program Header と Section Header

ELF には Program Header と Section Header というよく似た2種類のヘッダーがあります。何が違うでしょうか?

ELF というのは Executable and Linking Format の略で、アセンブルコンパイルの出力である .o ファイルも、それらをリンクした実行ファイルも両方サポートしちゃう規格だぜということになります。

Section Header は主に .o ファイルの状態で使うヘッダーで、セクションの役割ごとに細かく分かれています。 Program Header はリンクされた実行可能ファイルだけに存在するヘッダーで、 Section Header に比べると実行時にメモリに読み込むための最低限の情報だけにまとまっています。 つまり、通常の ELF ローダーは Program Header だけ見れば OK ということになります。

Section Header では別のセクションに分かれていても、アドレスがすぐ近くにあって属性が同じ場合 Program Header では同じセグメントにまとまっていることがよくあります。

Program Header には、セグメントの属性、ファイル上の位置とサイズ、メモリ上のアドレスとサイズ、などの情報があるので、それらをパースして実行に必要なアドレスにセグメントの内容をコピーしていきます。

リロケーションがなければこれだけでロードできます。簡単ですね。

GOT - 神ではない

ELF には GOT というテーブルがあります。これは PIC/PIE というリロケーショナルなコードの実行に必要な ELF 特有のデータ構造となります。

アーキテクチャによっては GOT がないとまともに動かせない場合もあったりした気がしますが、 x86 で配置固定のコードを書く場合は全く必要ありません。 むしろ GOT へアクセスするために無駄な機械語が生成されてメモリ消費的にも実行速度的にも無駄です。

ということで、 GOT 使わないコードに書き換えましょう。

.cargo/config に

[build]
rustflags = ["-C", "relocation-model=static"]

のような記述を追加すると配置が完全に固定されたオブジェクトになる代わりに GOT を一切使わなくなります。

配置アドレス決まってるしこれでいいのです。

おめーのせきねぇです?・ヮ・

このようにして ELF さんと仲良くなりましたが、そもそも ELF って割といろんなメタデータが含まれていてバイナリが大きくなります。 デバッグには便利なメタデータですが、実行時にはセクションの配置情報とコード・データセグメントの内容以外必要ありません。 今回のターゲットは割とメモリをタイトにしたいので無駄なメタデータは削減したいです。

ということでバイナリを変換します。

objcopy でバイナリに変換した場合はメモリ上に展開したイメージそのまま出力されて ELF の状態から失われる情報量が多すぎます。 それに実はセグメントの隙間のパディングとか bss もそのままバイナリに出力されるので効率もあまりよくないです。 一方、 strip した場合は ELF として汎用的な情報が多く残っているのでもうちょっと削減したいです。

折半案として独自のツールを作ってセグメントの配置情報と内容だけ別ファイルに抜き出して独自のバイナリ形式にすることにします。 何年か前に似たようなことやったな・・・

ロードに必要な情報をまとめてみましょう。

セグメントの属性

全てのセグメントが読み書き実行可能でその他のメタデータなどがない場合は不要ですが、今回は念の為最低限の情報を残します。

ファイル上のセグメントデータの位置

ファイル上のどこからセグメントの内容を読み込めばいいのか判断するために必要ですが、 最初のセグメントがヘッダの直後につづき、2つ目以降のセグメントは以前のセグメントデータの直後に続いてるというルールがある場合は不要です。

ファイル上のセグメントデータのサイズ

実際に何バイトコピーすればいいのかわからないので必要な情報です。

メモリ上のアドレス

メモリ上のどこに配置すればいいのかわからないので必要な情報です。

メモリ上のサイズ

bss のように初期値 0 のデータを含む場合はファイル上のセグメントサイズとメモリ上のセグメントサイズが異なるので必要になりますが、 全てのセグメントが連続していてファイルヘッダーで bss のサイズがわかる場合、この情報は不要です。

アライン情報など

仮想メモリを使ってストレージ上にスワップする場合、リロケーションをする場合などにセグメントのアライン情報が必要になる場合があります。 現状必要ないし、今後も必要になる可能性が低いのでカットします。

自分の足を撃ち抜く方法

以上のように必要な情報だけを抜き出した単純なヘッダに変換したバイナリができました。 いよいよローダーを作ります。

世の中には ブートローダーをほぼ Rust だけで作ってしまう強者 もいるようです。 しかし、低レベルすぎる操作は高級言語だと逆に使いづらい面もあるかと思ったので、ローダー部分はアセンブリで書くことにしました。

    movzx edx, byte [ebp + N_SECS]
    lea ebx, [ebp + OFF_SECHDR]
    mov esi, edx
    shl esi, 4
    add esi, ebx
.loop:
    mov al, [ebx]
    and al, 0x07
    jz .no_load
    mov ecx, [ebx + S_FILESZ]
    jecxz .no_load
    mov edi, [ebx + S_VADDR]
    rep movsb
.no_load:
    add ebx, SIZE_SECHDR
    dec edx
    jnz .loop

結構シンプルですね。 出来上がったバイナリを自作 PC エミュレーターで動かしてみます。

f:id:neriring16:20210119232112p:plain

これで x86 のベアメタルで Rust が使えるようになりました💪