OS 自作に便利な C++ の機能 10 選(前編)
自作 OS Advent Calendar 2017 1 日目の記事です. 筆者が大好きな C++ の機能の中で,OS を書く際に便利なもの 10 個を紹介する予定です.
続きを読むはてなブログで画像タグが展開されないバグの検証テスト記事
画像タグを張りつける条件によってはタグが展開されないことを確認するテスト.以下の画像をサンプルとして使う
スペースによるコードブロック
の後だとなぜか展開されない.
[f:id:uchan_nos:20171126114150p:plain:w100]
2枚貼っても展開されない.
[f:id:uchan_nos:20171126114150p:plain:w100]
バッククオートによるコードブロック
を書くことによって展開動作が復活する.
※この挙動が治ってしまうと記事が意味不明になるので,治る前の様子をスクショしました.
UEFI + iPXE で自作 OS をネットワーク起動する
概要
開発マシン上に置いた自作 OS のカーネルを,ネットワーク経由でターゲットマシンに読み込ませ,起動させるやり方についての記事です.
ディスクイメージをネットワークブートする - Raphine Project の記事を大いに参考にしました.Raphine Project の記事は BIOS を対象にしていますが,本記事は BIOS ではなく UEFI で起動させることが主な違いです.
UEFI + USB メモリによる起動
ネットワーク起動を説明する前に,簡単に USB メモリでの起動を説明します.
USB メモリに入れたカーネルを UEFI で起動させるために,私は次の方法を用いています.
- USB メモリを FAT でフォーマットして
/EFI/BOOT/BOOTX64.EFI
に UEFI アプリとして作成した自作ブートローダを配置し- カーネル本体となる ELF ファイルを適当なパスに配置し
- ブートローダで ELF ファイルを読み取って起動する.
これは UEFI を使った起動方法の中で比較的シンプルな仕組みです.
UEFI を使う中で最もシンプルな起動方法は,大神祐真著の フルスクラッチで作る!UEFIベアメタルプログラミング で紹介された,UEFI アプリとして OS っぽいものを作る方法でしょう. この方法ではカーネル本体とブートローダを分ける必要がなくシンプルです.
ただ,個人的にはブートローダとカーネル本体は分けて作りたい1ので,2 段階のブート方式を採用しました.
ネットワーク起動
ネットワーク起動とは,ネットワーク上に起動用データを置いておき,それを読み取って起動する方法です. 標準的な規格に PXE というものがあります.
標準の PXE では DHCP サーバを立て,起動用データの置き場所などを指定する必要がありますが,Raphine Project の記事では iPXE という PXE の実装を利用しています. iPXE には,DHCP を使わずに IP アドレス直指定でデータの場所を指定することができる,TFTP サーバではなく HTTP サーバから起動用データを取得できる,という機能があります.めっちゃ楽です. 本記事でもそれにならいます.
タイトルの「UEFI + iPXE」は,iPXE 自体とそこから起動されるものを UEFI アプリとする,という 2 つの意味があります. 次のような構成で起動させます.
- USB メモリを FAT でフォーマットして
/EFI/BOOT/BOOTX64.EFI
に iPXE のバイナリを配置し- 自作ブートローダとカーネル本体となる ELF ファイルを HTTP サーバに配置し
- iPXE でブートローダーと ELF ファイルを読み取って起動する.
Raphine Project の記事では UEFI の代わりに BIOS で起動させるためのディスクイメージを用意しているので,上記構成とはちょっと違いますね.
iPXE 起動用 USB メモリの準備
まずは iPXE のビルドに必要な依存パッケージをインストールします2.
$ sudo apt-get install build-essential binutils-dev zlib1g-dev libiberty-dev liblzma-dev
次にダウンロード&ビルドします.
$ git clone git://git.ipxe.org/ipxe.git
$ cd ipxe/src
$ make bin-x86_64-efi/ipxe.efi
上記コマンドでは,アーキテクチャ x86_64 向けの UEFI アプリ(拡張子 .efi)を生成するように指示しています.
ビルドが完了したら,USB メモリにファイルをコピーします.
FAT でフォーマットしてある USB メモリが /mnt/usb
にマウントされていると仮定して説明します.
$ mkdir -p /mnt/usb/EFI/BOOT
$ cp bin-x86_64-efi/ipxe.efi /mnt/usb/EFI/BOOT/BOOTX64.EFI
これで iPXE 起動用の USB メモリは完成です.
ブートローダとカーネルの準備
自作ブートローダ(UEFI アプリ)とカーネル本体を適当なディレクトリに置いておき(仮に /path/to/files
とします),そこを HTTP サーバからアクセスできるようにします.
$ cd /path/to/files
$ python3 -m http.server 8080
Python を使うと手軽に HTTP サーバを構築できるのでおすすめです.
iPXE による起動
Raphine Project の手順の kernel と initrd を次のように設定すればできます.
起動用スクリプトの記述や iPXE バイナリへの埋め込み方法は Raphine Project の記事を参考にしてください.
iPXE の高速化
何も設定をいじらずに iPXE をビルドすると余計な機能が組み込まれてしまい,iPXE の初期化時間が延びる原因となります.
iPXE initialising devices...
私の環境では上記の状態で数十秒時間がかかりました.
そんな時は ipxe/src/config/general.h でビルドオプションを調整すると改善する可能性があります.
私の環境では次に示す WiFi 関係のオプションを #undef
したら非常に高速化しました.
#define CRYPTO_80211_WEP
#define CRYPTO_80211_WPA
#define CRYPTO_80211_WPA2
私が検証に使った MinnowBoard Turbot には無線 LAN インターフェースがないので,上記機能を初期化しようとしてタイムアウトしていた可能性がありますね.
Newlibビルドメモ
Newlibをclangを使って自作OS向けにビルドしたメモ
- 環境:Ubuntu 16.04、clang 3.8
- Newlibバージョン:d6cac3e1da1a117f8a93b91371f3f0a5c071219f
Newlibはなぜか、host=targetでconfigureしてしまうと何もビルドが走らない。 たとえホスト環境と同じ環境で動く自作OSをビルドする際も、ちょっと違うtargetを指定する必要があるらしい。 target=x86_64-none-elfとすることでビルドが走るようになった。
(2017/07/26追記:newlib-cygwinのトップディレクトリではなく、newlib-cygwin/newlibを直接ビルドすることでhost=targetでもビルドできた。詳しくは後述)
さて、target=Xとすると、Newlibのビルドスクリプトは X-cc
というクロスコンパイラが PATH が通ったところに存在することを求めてくる。
gccを使ってビルドするときは、クロスコンパイルをする際にクロスコンパイラがあることが一般的なので、まあこれは妥当な仕様なのだろう。
ところがclangはクロス環境別のclangコマンドを用意しなくても、–targetオプションで好きなバイナリを出力できる。
X-cc
という名前で、clang --target=X
という内容のスクリプトを用意してNewlibビルドスクリプトをだます。
今回、自作OSのバイナリを位置独立実行ファイルとしたかったので CFLAGS=-fPIC
を付けたい。
クロスコンパイルするので CFLAGS_FOR_TARGET
という変数に設定することがミソ。CFLAGS
だとlibc.aのビルドには影響を与えることができない。
git clone git://sourceware.org/git/newlib-cygwin.git echo 'clang --target=x86_64-none-elf "$@"' > SOMEWHERE_IN_PATH/x86_64-none-elf-cc chmod +x SOMEWHERE_IN_PATH/x86_64-none-elf-cc mkdir build-newlib cd build-newlib CC=clang ../newlib-cygwin/configure CFLAGS_FOR_TARGET='-g -O2 -fPIC' --prefix=$HOME/x86_64-none-elf --target=x86_64-none-elf make make install
ネイティブビルド
Newlibをビルドしている環境と、Newlibの対象の環境が等しい(host=target)の場合のビルド方法。
Newlibはx86なLinux環境でネイティブビルドを行うとlibtoolを用いたビルドになる。
で、おそらくその場合に --enable-multilib
オプションがデフォルトで有効になっていて、
手元の実験だと -m32
付きで64ビット向けソースコードをビルドしようとしてエラーになるという間抜けなことになった。
そこで --disable-multilib
でエラーを回避した。multilibは32ビット版と64ビット版を同時に生成するような機能らしいが、どうせ64ビット版ライブラリしか使わないので。
git clone git://sourceware.org/git/newlib-cygwin.git mkdir build-newlib cd build-newlib CC=clang ../newlib-cygwin/newlib/configure CFLAGS='-g -O2 -fPIC' --prefix=$HOME/x86_64-pc-linux-gnu --disable-multilib make make install
UEFI Memory Map メモ
UEFI で得られる Memory Map の実例。
Index | Type | PhysicalStart | VirtualStart | NumberOfPages | Attribute |
---|---|---|---|---|---|
0 | 3 EfiBootServicesCode | 00000000 | 00000000 | 1 | F |
1 | 7 EfiConventionalMemory | 00001000 | 00000000 | 9F | F |
2 | 7 EfiConventionalMemory | 00100000 | 00000000 | 700 | F |
3 | A EfiACPIMemoryNVS | 00800000 | 00000000 | 8 | F |
4 | 7 EfiConventionalMemory | 00808000 | 00000000 | 8 | F |
5 | A EfiACPIMemoryNVS | 00810000 | 00000000 | 8 | F |
6 | 7 EfiConventionalMemory | 00818000 | 00000000 | 8 | F |
7 | A EfiACPIMemoryNVS | 00820000 | 00000000 | E0 | F |
8 | 4 EfiBootServicesData | 00900000 | 00000000 | A00 | F |
9 | 7 EfiConventionalMemory | 01300000 | 00000000 | 2C36 | F |
10 | 4 EfiBootServicesData | 03F36000 | 00000000 | 20 | F |
11 | 7 EfiConventionalMemory | 03F56000 | 00000000 | 2C89 | F |
12 | 2 EfiLoaderData | 06BDF000 | 00000000 | 100 | F |
13 | 1 EfiLoaderCode | 06CDF000 | 00000000 | 3 | F |
14 | 3 EfiBootServicesCode | 06CE2000 | 00000000 | AA | F |
15 | A EfiACPIMemoryNVS | 06D8C000 | 00000000 | C | F |
16 | 0 EfiReservedMemoryType | 06D98000 | 00000000 | 17 | F |
17 | 6 EfiRuntimeServicesData | 06DAF000 | 00000000 | 20 | 800000000000000F |
18 | 7 EfiConventionalMemory | 06DCF000 | 00000000 | 167 | F |
19 | 4 EfiBootServicesData | 06F36000 | 00000000 | 106 | F |
20 | 7 EfiConventionalMemory | 0703C000 | 00000000 | 3 | F |
21 | 4 EfiBootServicesData | 0703F000 | 00000000 | 5FB | F |
22 | 7 EfiConventionalMemory | 0763A000 | 00000000 | C | F |
23 | 4 EfiBootServicesData | 07646000 | 00000000 | 689 | F |
24 | 7 EfiConventionalMemory | 07CCF000 | 00000000 | D | F |
25 | 3 EfiBootServicesCode | 07CDC000 | 00000000 | 173 | F |
26 | 5 EfiRuntimeServicesCode | 07E4F000 | 00000000 | 30 | 800000000000000F |
27 | 6 EfiRuntimeServicesData | 07E7F000 | 00000000 | 24 | 800000000000000F |
28 | 0 EfiReservedMemoryType | 07EA3000 | 00000000 | 4 | F |
29 | 9 EfiACPIReclaimMemory | 07EA7000 | 00000000 | 8 | F |
30 | A EfiACPIMemoryNVS | 07EAF000 | 00000000 | 4 | F |
31 | 4 EfiBootServicesData | 07EB3000 | 00000000 | 63 | F |
32 | 3 EfiBootServicesCode | 07F16000 | 00000000 | 18 | F |
33 | 4 EfiBootServicesData | 07F2E000 | 00000000 | 9 | F |
34 | 3 EfiBootServicesCode | 07F37000 | 00000000 | 11 | F |
35 | 7 EfiConventionalMemory | 07F48000 | 00000000 | 8 | F |
36 | 6 EfiRuntimeServicesData | 07F50000 | 00000000 | 20 | 800000000000000F |
37 | 7 EfiConventionalMemory | 07F70000 | 00000000 | 8 | F |
38 | A EfiACPIMemoryNVS | 07F78000 | 00000000 | 88 | F |
39 | 6 EfiRuntimeServicesData | FFE00000 | 00000000 | 200 | 8000000000000001 |
x86 OS自作ゼミの進め方
セキュリティ・キャンプ2017のx86 OS自作ゼミの進め方、最大限の成果を出す方法について思うところを書きます。
3日間は短い!
x86 OS自作ゼミは3日間しかありません(それでもセキュリティ・キャンプの中では最長なのですが)。OS自作という壮大なテーマに対して3日間というのはとても短いです。しかし短い中でも人に自慢できる成果が出た方が本人も楽しいだろうし、講師も皆さんの成果を見て楽しみたいと思っていますので、3日間である程度のモノが出来上がるようにしたいです。
3日間というと、独自のOSを1からそれなりの完成度まで持っていくには時間が到底足りません。「30日でできる!OS自作入門」をノンストップで読みまくると、それだけで3日間が終わるでしょう。それくらい短い時間です。
講師を最大限活用する
この3日間は、講師が皆さんのプログラミングをお手伝いする、というのが基本スタイルです。みっちり講義をするのではなく、皆さんが自分で手を動かして、講師がデバッグや設計のお手伝いをします。逆に、講師をどれだけ活用できるかが勝負と言ってもいいかもしれません。
ということで、受講者の皆さんには3日間という時間リソースと、講師という人的リソースを最大限に活用して成果を出せるようにネタを仕込んできていただきたいです。例えば、キャンプ当日までに基本的なコードは書いておいて、3日間は講師を捕まえてデバッグを手伝わせる、なんてのはいいやり方です。デバイスドライバを作ろうと思うのであれば、キャンプに来る前にそのデバイスに関する情報(IOポートの番号、デバイスのレジスタ表、サンプルコードなど)を集めておいて、3日間は調べ物に時間をかけなくて良いようにする、というのもいいですね。
関連リンク
- seccamp2017:x86 OS自作ゼミの概要とか講師紹介とか、応募課題の出題意図の説明とか