背景/目的
LinuxOSの仕組みや挙動について深い知見を得たいという興味・関心のもと、1か月くらい「ゼロからのOS自作入門」を読み進めていました。
(自分には1か月使っても8章までしか読み進められませんでした…)
本当は全て読み終わってから学べたことや、新たに分かったことをアウトプットとして記事にまとめようと思っていたのですが、分量が多いため忘れないうちにまとめておこうと思い、この段階で記事にしました。
(※ 記載内容に指摘事項などあれば適宜コメントをいただければと思います)
各章での実施事項 + 得られた知見について
アウトプットとして以下2つの内容について記述してみます。
- 2つの章ごとに、大まかな実装内容を要約する
- 個人的に得られた知見/関連事項/参考情報などをまとめる
第1章 ~ 第2章
■ 実装内容の要約
大きく2点のことを実施しており、1つがHello Worldを出力するEFIファイルを作成/実行すること、もう1つがUEFIの機能を使い、自身のメモリマップ情報をcsvファイルに出力することです。
■ 得られた知見や参考情報等
本章で理解が深められた内容としてUEFIで実現される「物理メモリアドレス」と「論理メモリアドレス」の紐づけでした。OS起動後であれば、ページテーブル内で、その対応関係を紐づけることは把握していたものの、UEFI内では以下のように、EFI_MEMORY_DESCRIPTOR構造体で管理されている様子でした (参考[1])
typedef struct {
UINT32 Type;
EFI_PHYSICAL_ADDRESS PhysicalStart;
EFI_VIRTUAL_ADDRESS VirtualStart;
UINT64 NumberOfPages;
UINT64 Attribute;
} EFI_MEMORY_DESCRIPTOR;
概念のみ把握していた事項を具体的にどのように実装されているかを確認でき、理解を深められました。
第3章 ~ 第4章
■ 実装内容の要約
この章から実際にOS kernelを作成し始め、QEMUからOS kernelを呼び出します。最終的にはkernelを書き換えて、図形の描画ができるところまで進めます。
ここから少し難易度が上がり、レジスタで扱われる機械語命令を参照したり、特定のメモリアドレスを指定したリンク操作を行ったりと比較的低レイヤの知識が要求されます。
■ 得られた知見や参考情報等
以下、複数あるため箇条書きでまとめさせていただきます。
・配置newという考え方
メモリ上の保存位置のみを決定してしまい、そこにオブジェクトを配置してしまう操作を配置newといいます。本書ではこの時点でmallocやnewなどを実装していない点から、オブジェクトを適切にメモリ上に載せる上で便利な考え方だと思いました。
・メモリに対するセキュリティ属性付与
書籍の通りreadelfにて、kernel.elfのプログラムヘッダを読むと、LOADセグメント中に、メモリへマッピングする際の権限情報としてR(=Readable), W(=Writable), E(=Executable)の3つの情報が参照できます。書籍内では、この時点では読み飛ばして差し支えない項目として記載されています。
この項目を参照していて、気が付いた事項として以前に読んだ「What Every Programmer Should Know About Memory」 (参考[2])の中でメモリアクセス制御に関する記載があったことを思い出しました。
(古い資料だとは思いましたが今も使える内容が多いように思います!)
これはmmapシステムコールを実行する場合の話ではありますが、メモリページへ「読み」、「書き」、「実行」の権限を指定する際、それぞれ、
・PROT_READ
・PROT_WRITE
・PROT_EXEC
の3種類を指定しているという内容です。当初は「どこを参照して権限を付与しているのだろう?」と思っていましたが、ELFファイル内のヘッダ情報を改めて参照することで、その疑問を払拭できました。
・GitHubのIssueページにおけるトラブルシューティング
本書はサポートページとして、GitHubのIssue(参考[3])にて質疑応答を実施しています。2024/10現在、書籍内のDay03における実装はLLVM-7でのビルドを前提としている関係上、LLD-14以降を使ってしまうと、想定外のリンク結果となりkernelが起動してこないという問題に見舞われることが確認されています。 (参考[4])
私自身も、上記問題に遭遇し、自己解決こそしたものの、同様の事象に遭遇した方々がIssueで議論しておりました。そのため、GithubのIssueに解決方針とその手順を共有することで、無事に他の方もkernelを起動できたとの返信を貰えました。個人的にはIssueに投稿することはあまり無い経験だったため、嬉しくもありつつ、いい機会だったと感じています。
第5章 ~ 第6章
■ 実装内容の要約
printkという関数を作成して、文字を画面上に出力したり、USBマウスを認識させ、カーソルを操作できるようにしたりと視覚的な機能を実装します。
特にマウスからの操作を入力とさせる実装が難しく、PCIバスに繋がったデバイス一覧を探索し、マウスデバイスが見つかったら、そのデバイスをポーリングし続けることで、カーソルにマウスの動きを反映させます。
■ 得られた知見や参考情報等
・マルチコア環境でMMIO上のデータを第三者が参照できてしまう脆弱性について
<MMIOとは>
MMIO自体は、CPUがメインメモリへアクセスするのと同様の方法で読み書きできるレジスタです。そのため、MMIO自体にもメモリアドレスが付与されており、そのメモリ領域にはI/Oデバイスにアクセスするための命令が保存されます。
(書籍内ではマウス制御を行う過程で、MMIOレジスタの値を参照して利用します)
<MMIOを使った脆弱性について>
※下記はMMIOについて気になり、個人的に調べた関連情報です。
メインメモリ上に命令データを保存しているということは、攻撃者が標的ホストのMMIOに直接アクセスできて(例:マルチコア搭載の仮想環境など)、かつ信頼されていない第三者にMMIOのアクセスを許可していれば、その命令データを盗聴できることを意味します。 (参考[5])
確かに攻撃手法として使えるなと感心してしまいました。
2022/06に発表された脆弱性と、比較的最近、報告されている事象のようで、MMIOについての理解を深める上で、参考になるニュースだと感じておりました。
第7章 ~ 第8章
■ 実装内容の要約
この2章で実施したことは大きく2点あります。
1つが前章で実装したマウス操作をポーリング処理ではなく割り込み処理に変更し、処理効率を改善すること。
もう1つがメモリ管理機能をUEFI側からOS側へと引き渡し、OS側でメモリ領域の確保と解放を実現できるようにすることです。
(※ 特にメモリ管理の章は難易度が高く、理解するのに時間を要しました)
■ 得られた知見や参考情報等
・OS起動に際して必要なメモリ上の要素
書籍内の実装でOS側のメモリ領域に作成したデータは下記の3点があります。
- OSが利用するstack領域
- ページテーブル
- GDT (Global Descriptor Table)
以下では上記3要素についての概要を記載させていただきます。
<OSが利用するstack領域>
OS/UEFIそれぞれで利用するメモリ領域を分ける操作を実施します。これは関数の呼び出しとかなり似ている動作かと理解しました。
すなわち、UEFIプログラム(Main.c)の中で呼び出す関数がOS(kernel.elf)であると解釈すれば、UEFIが使っているメモリ領域をOS側が上書きしないようにstack領域を設定できるものと理解しました。
<ページテーブル>
ページテーブルはメモリ領域を固定長(ページ/一般に4KiB)に分割した際、それぞれのページが使用中なのかや、物理メモリアドレス/論理メモリアドレスの対応がどのように紐づいているか等のメモリに関する情報を管理するためのテーブルです。
こちらのサイト (参考[6]) に掲載されている図が非常に分かりやすく、イメージが付くかもしれません。
補足情報として、ページテーブル内で物理メモリアドレス⇔論理メモリアドレスの変換操作を高速化することを目的としてTLB cacheが使われます。 (参考[7])
<GDT (Global Descriptor Table)>
※3項目の中で一番難しい内容だと思います。
GDTとはメモリ領域に様々な属性を設定するためのデータ構造であり、プロセッサ1つにつき1つのGDTが必要となります。
GDTが保存されたメモリアドレスは、GDTR (GDT Register) というレジスタに登録され、以降プロセッサはこのレジスタの登録情報を参照することで、CPUの実行モードをユーザモード/カーネルモードなどと切り替える動作を実現します。
// 詳細にはdescriptor_privilege_level(DPL)という変数で切り替わるとのこと
では具体的にGDTがどのように実装されているかというと、長さが3の配列(gdt[0]~gdt[2])で構成されています。書籍内では下記のように扱っています。
- gdt[0]: Null Descriptor //これは慣習的に(?)使われないことになっている
- gdt[1]: Code Segment //この中で命令の権限設定や64bit mode実行を指定する
- gdt[2]: Data Segment //こちらも8章の中ではほとんど使われていない
上記のうち、後者2つを合わせてSegement Descriptorと呼ばれています。
書籍に記載はないものの、GDTはLDT(Local Descriptor Table)と呼ばれるプロセス毎にメモリ情報を設定するテーブル群を管理する役割も担っています。
各Descriptor Tableはテーブル内で定義されたメモリ領域にのみアクセスできることから、GDTがLDTを適切に管理することによって、OSがメモリを競合させないようにプログラムを実行できていることが分かりました。
※ Descriptor Table の参考情報としては、参考[8]や参考[9]のブログが分かりやすくまとまっているなと思いました。
所感
ここまで読み進めてみて、比較的難易度の高い書籍だと感じました。
個人的には8章までの中で最も技術的に面白いと感じた箇所はメモリ管理を取り扱った8章だと思います。現在、第9章を読み進めているので、次回以降も続きの内容をまとめていく予定です。
以上
コメントを残す