diff --git a/.github/workflows/build-site.yml b/.github/workflows/build-site.yml index 5af9b263..56a70214 100644 --- a/.github/workflows/build-site.yml +++ b/.github/workflows/build-site.yml @@ -21,8 +21,6 @@ jobs: - name: 'Download Zola' run: curl -sL https://github.com/getzola/zola/releases/download/v0.12.1/zola-v0.12.1-x86_64-unknown-linux-gnu.tar.gz | tar zxv - - name: "Install Python Tools" - run: python -m pip install --upgrade pip setuptools wheel - name: 'Install Python Libraries' run: python -m pip install --user -r requirements.txt working-directory: "blog" diff --git a/.github/workflows/scheduled-builds.yml b/.github/workflows/scheduled-builds.yml index e7dc9328..39c6a0d5 100644 --- a/.github/workflows/scheduled-builds.yml +++ b/.github/workflows/scheduled-builds.yml @@ -28,6 +28,6 @@ jobs: - name: Invoke workflow uses: benc-uk/workflow-dispatch@v1.1 with: - workflow: Build Code + workflow: Code token: ${{ secrets.SCHEDULED_BUILDS_TOKEN }} ref: ${{ matrix.branch }} diff --git a/blog/before_build.py b/blog/before_build.py index 8e79b6d3..458fed76 100644 --- a/blog/before_build.py +++ b/blog/before_build.py @@ -2,7 +2,7 @@ # -*- coding: utf-8 -*- import io -import urllib2 +import urllib import datetime from github import Github @@ -23,7 +23,7 @@ with io.open("templates/auto/recent-updates.html", 'w', encoding='utf8') as rece recent_updates.truncate() relnotes_issues = g.search_issues("is:merged", repo="phil-opp/blog_os", type="pr", label="relnotes")[:100] - recent_relnotes_issues = filter(filter_date, relnotes_issues) + recent_relnotes_issues = list(filter(filter_date, relnotes_issues)) if len(recent_relnotes_issues) == 0: recent_updates.write(u"No notable updates recently.") @@ -58,8 +58,8 @@ month = 4 while True: url = "https://rust-osdev.com/this-month/" + str(year) + "-" + str(month).zfill(2) + "/" try: - urllib2.urlopen(url) - except urllib2.HTTPError as e: + urllib.request.urlopen(url) + except urllib.error.HTTPError as e: break month_str = datetime.date(1900, month, 1).strftime('%B') @@ -69,7 +69,7 @@ while True: month = month + 1 if month > 12: - month = 0 + month = 1 year = year + 1 lines.reverse() diff --git a/blog/config.toml b/blog/config.toml index b2c39a38..bdb4a06f 100644 --- a/blog/config.toml +++ b/blog/config.toml @@ -36,15 +36,65 @@ author = { name = "Philipp Oppermann" } [translations.en] lang_name = "English" +toc = "Table of Contents" +all_posts = "« All Posts" +comments = "Comments" +comments_notice = "Please leave your comments in English if possible." +readmore = "read more »" +not_translated = "(This post is not translated yet.)" +translated_content = "Translated Content:" +translated_content_notice = "This is a community translation of the _original.title_ post. It might be incomplete, outdated or contain errors. Please report any issues!" +translated_by = "Translation by" +word_separator = "and" [translations.zh-CN] lang_name = "Chinese (simplified)" +toc = "Table of Contents" +all_posts = "« All Posts" +comments = "Comments" +comments_notice = "Please leave your comments in English if possible." +readmore = "read more »" +not_translated = "(This post is not translated yet.)" +translated_content = "Translated Content:" +translated_content_notice = "This is a community translation of the _original.title_ post. It might be incomplete, outdated or contain errors. Please report any issues!" +translated_by = "Translation by" +word_separator = "and" [translations.zh-TW] lang_name = "Chinese (traditional)" +toc = "Table of Contents" +all_posts = "« All Posts" +comments = "Comments" +comments_notice = "Please leave your comments in English if possible." +readmore = "read more »" +not_translated = "(This post is not translated yet.)" +translated_content = "Translated Content:" +translated_content_notice = "This is a community translation of the _original.title_ post. It might be incomplete, outdated or contain errors. Please report any issues!" +translated_by = "Translation by" +word_separator = "and" [translations.ja] lang_name = "Japanese" +toc = "目次" +all_posts = "« すべての記事へ" +comments = "コメント" +comments_notice = "可能な限りコメントは英語で残すようにしてください。" +readmore = "もっと読む »" +not_translated = "(この記事はまだ翻訳されていません。)" +translated_content = "この記事は翻訳されたものです:" +translated_content_notice = "この記事は_original.title_をコミュニティの手により翻訳したものです。そのため、翻訳が完全・最新でなかったり、原文にない誤りを含んでいる可能性があります。問題があればこのissue上で報告してください!" +translated_by = "翻訳者:" +word_separator = "及び" [translations.fa] lang_name = "Persian" +toc = "فهرست مطالب" +all_posts = "« همه پست‌ها" +comments = "نظرات" +comments_notice = "لطفا نظرات خود را در صورت امکان به انگلیسی بنویسید." +readmore = "ادامه‌مطلب»" +not_translated = "(.این پست هنوز ترجمه نشده است)" +translated_content = "محتوای ترجمه شده:" +translated_content_notice = "این یک ترجمه از جامعه کاربران برای پست _original.title_ است. ممکن است ناقص، منسوخ شده یا دارای خطا باشد. لطفا هر گونه مشکل را در این ایشو گزارش دهید!" +translated_by = "ترجمه توسط" +word_separator = "و" diff --git a/blog/content/_index.fa.md b/blog/content/_index.fa.md index 493e0c96..5e1e57b1 100644 --- a/blog/content/_index.fa.md +++ b/blog/content/_index.fa.md @@ -2,12 +2,12 @@ template = "edition-2/index.html" +++ -

Writing an OS in Rust

+

نوشتن یک سیستم عامل با راست

-
+
-This blog series creates a small operating system in the [Rust programming language](https://www.rust-lang.org/). Each post is a small tutorial and includes all needed code, so you can follow along if you like. The source code is also available in the corresponding [Github repository](https://github.com/phil-opp/blog_os). +این مجموعه بلاگ یک سیستم عامل کوچک در [زبان برنامه نویسی Rust](https://www.rust-lang.org/) ایجاد می کند. هر پست یک آموزش کوچک است و شامل تمام کدهای مورد نیاز است ، بنابراین اگر دوست دارید می توانید آن را دنبال کنید. کد منبع نیز در [مخزن گیت‌هاب](https://github.com/phil-opp/blog_os) مربوطه موجود است. -Latest post: +اخرین پست:
diff --git a/blog/content/_index.ja.md b/blog/content/_index.ja.md index 493e0c96..c316a441 100644 --- a/blog/content/_index.ja.md +++ b/blog/content/_index.ja.md @@ -2,12 +2,12 @@ template = "edition-2/index.html" +++ -

Writing an OS in Rust

+

RustでOSを書く

-This blog series creates a small operating system in the [Rust programming language](https://www.rust-lang.org/). Each post is a small tutorial and includes all needed code, so you can follow along if you like. The source code is also available in the corresponding [Github repository](https://github.com/phil-opp/blog_os). +このブログシリーズでは、ちょっとしたオペレーティングシステムを[Rustプログラミング言語](https://www.rust-lang.org/)を使って作ります。それぞれの記事が小さなチュートリアルになっており、必要なコードも全て記事内に記されているので、一つずつ読み進めて行けば理解できるでしょう。対応した[Githubリポジトリ](https://github.com/phil-opp/blog_os)でソースコードを見ることもできます。 -Latest post: +最新記事:
diff --git a/blog/content/edition-2/posts/02-minimal-rust-kernel/index.ja.md b/blog/content/edition-2/posts/02-minimal-rust-kernel/index.ja.md index 847196aa..5cbbfafd 100644 --- a/blog/content/edition-2/posts/02-minimal-rust-kernel/index.ja.md +++ b/blog/content/edition-2/posts/02-minimal-rust-kernel/index.ja.md @@ -300,7 +300,7 @@ Rustコンパイラは、すべてのシステムにおいて、特定の組み [`IntoIterator::into_iter`]: https://doc.rust-lang.org/stable/core/iter/trait.IntoIterator.html#tymethod.into_iter -ありがたいことに、`compiler_builtins`クレートにはこれらの必要な関数すべての実装が含まれており、標準ではCライブラリの実装と競合しないように無効化されているだけなのです。これはcargoの[`build-std-features`]フラグを`["computer-builtins-mem"]`に設定することで有効化できます。`build-std`フラグと同じように、このフラグはコマンドラインで`-Z`フラグとして渡すこともできれば、`.cargo/config.toml`ファイルの`unstable`テーブルで設定することもできます。ビルド時は常にこのフラグをセットしたいので、設定ファイルを使う方が良いでしょう: +ありがたいことに、`compiler_builtins`クレートにはこれらの必要な関数すべての実装が含まれており、標準ではCライブラリの実装と競合しないように無効化されているだけなのです。これはcargoの[`build-std-features`]フラグを`["compiler-builtins-mem"]`に設定することで有効化できます。`build-std`フラグと同じように、このフラグはコマンドラインで`-Z`フラグとして渡すこともできれば、`.cargo/config.toml`ファイルの`unstable`テーブルで設定することもできます。ビルド時は常にこのフラグをセットしたいので、設定ファイルを使う方が良いでしょう: [`build-std-features`]: https://doc.rust-lang.org/nightly/cargo/reference/unstable.html#build-std-features @@ -313,12 +313,10 @@ build-std-features = ["compiler-builtins-mem"] (`compiler-builtins-mem`機能のサポートが追加されたのは[つい最近](https://github.com/rust-lang/rust/pull/77284)なので、`2019-09-30`以降のRust nightlyが必要です。) -このとき、裏で`compiler_builtins`クレートの[`mem`機能][`mem` feature]が有効化されています。これにより、このクレートの[`memcpy`などの実装][`memcpy` etc. implementations]に`#[no_mangle]`アトリビュートが適用され、リンカがこれらを利用できるようになっています。これらの関数は今のところ[最適化されておらず][not optimized]、性能は最高ではないかもしれないものの、少なくとも正しい実装ではあるということは知っておく価値があるでしょう。`x86_64`については、[これらの関数を特殊なアセンブリ命令を使って最適化する][memcpy rep movsb]プルリクエストが提出されています。 +このとき、裏で`compiler_builtins`クレートの[`mem`機能][`mem` feature]が有効化されています。これにより、このクレートの[`memcpy`などの実装][`memcpy` etc. implementations]に`#[no_mangle]`アトリビュートが適用され、リンカがこれらを利用できるようになっています。 [`mem` feature]: https://github.com/rust-lang/compiler-builtins/blob/eff506cd49b637f1ab5931625a33cef7e91fbbf6/Cargo.toml#L51-L52 [`memcpy` etc. implementations]: (https://github.com/rust-lang/compiler-builtins/blob/eff506cd49b637f1ab5931625a33cef7e91fbbf6/src/mem.rs#L12-L69) -[not optimized]: https://github.com/rust-lang/compiler-builtins/issues/339 -[memcpy rep movsb]: https://github.com/rust-lang/compiler-builtins/pull/365 この変更をもって、私達のカーネルはコンパイラに必要とされているすべての関数の有効な実装を手に入れたので、コードがもっと複雑になっても変わらずコンパイルできるでしょう。 diff --git a/blog/content/edition-2/posts/02-minimal-rust-kernel/index.md b/blog/content/edition-2/posts/02-minimal-rust-kernel/index.md index 4819ecc8..7567da92 100644 --- a/blog/content/edition-2/posts/02-minimal-rust-kernel/index.md +++ b/blog/content/edition-2/posts/02-minimal-rust-kernel/index.md @@ -310,12 +310,10 @@ build-std-features = ["compiler-builtins-mem"] (Support for the `compiler-builtins-mem` feature was only [added very recently](https://github.com/rust-lang/rust/pull/77284), so you need at least Rust nightly `2020-09-30` for it.) -Behind the scenes, this flag enables the [`mem` feature] of the `compiler_builtins` crate. The effect of this is that the `#[no_mangle]` attribute is applied to the [`memcpy` etc. implementations] of the crate, which makes them available to the linker. It's worth noting that these functions are [not optimized] right now, so their performance might not be the best, but at least they are correct. For `x86_64`, there is an open pull request to [optimize these functions using special assembly instructions][memcpy rep movsb]. +Behind the scenes, this flag enables the [`mem` feature] of the `compiler_builtins` crate. The effect of this is that the `#[no_mangle]` attribute is applied to the [`memcpy` etc. implementations] of the crate, which makes them available to the linker. [`mem` feature]: https://github.com/rust-lang/compiler-builtins/blob/eff506cd49b637f1ab5931625a33cef7e91fbbf6/Cargo.toml#L54-L55 [`memcpy` etc. implementations]: https://github.com/rust-lang/compiler-builtins/blob/eff506cd49b637f1ab5931625a33cef7e91fbbf6/src/mem.rs#L12-L69 -[not optimized]: https://github.com/rust-lang/compiler-builtins/issues/339 -[memcpy rep movsb]: https://github.com/rust-lang/compiler-builtins/pull/365 With this change, our kernel has valid implementations for all compiler-required functions, so it will continue to compile even if our code gets more complex. diff --git a/blog/content/edition-2/posts/02-minimal-rust-kernel/index.zh-CN.md b/blog/content/edition-2/posts/02-minimal-rust-kernel/index.zh-CN.md index 26840f64..72f8f23f 100644 --- a/blog/content/edition-2/posts/02-minimal-rust-kernel/index.zh-CN.md +++ b/blog/content/edition-2/posts/02-minimal-rust-kernel/index.zh-CN.md @@ -109,7 +109,7 @@ Nightly 版本的编译器允许我们在源码的开头插入**特性标签** "target-pointer-width": "64", "target-c-int-width": "32", "os": "none", - "executables": true, + "executables": true } ``` diff --git a/blog/content/edition-2/posts/03-vga-text-buffer/index.md b/blog/content/edition-2/posts/03-vga-text-buffer/index.md index 0bbf92c7..0029e751 100644 --- a/blog/content/edition-2/posts/03-vga-text-buffer/index.md +++ b/blog/content/edition-2/posts/03-vga-text-buffer/index.md @@ -305,6 +305,7 @@ We can add a dependency on the `volatile` crate by adding it to the `dependencie volatile = "0.2.6" ``` +Make sure to specify `volatile` version `0.2.6`. Newer versions of the crate are not compatible with this post. The `0.2.6` is the [semantic] version number. For more information, see the [Specifying Dependencies] guide of the cargo documentation. [semantic]: https://semver.org/ diff --git a/blog/content/edition-2/posts/04-testing/index.fa.md b/blog/content/edition-2/posts/04-testing/index.fa.md index 891d9ed6..3e119cd8 100644 --- a/blog/content/edition-2/posts/04-testing/index.fa.md +++ b/blog/content/edition-2/posts/04-testing/index.fa.md @@ -179,18 +179,18 @@ test-args = ["-device", "isa-debug-exit,iobase=0xf4,iosize=0x04"] به جای فراخوانی دستی دستورالعمل های اسمبلی `in` و `out`، ما از انتزاعات ارائه شده توسط کریت [`x86_64`] استفاده می‌کنیم. برای افزودن یک وابستگی به آن کریت، آن را به بخش `dependencies` در `Cargo.toml` اضافه می‌کنیم: -[`x86_64`]: https://docs.rs/x86_64/0.12.1/x86_64/ +[`x86_64`]: https://docs.rs/x86_64/0.13.2/x86_64/ ```toml # in Cargo.toml [dependencies] -x86_64 = "0.12.1" +x86_64 = "0.13.2" ``` اکنون می‌توانیم از نوع [`Port`] ارائه شده توسط کریت برای ایجاد عملکرد `exit_qemu` استفاده کنیم: -[`Port`]: https://docs.rs/x86_64/0.12.1/x86_64/instructions/port/struct.Port.html +[`Port`]: https://docs.rs/x86_64/0.13.2/x86_64/instructions/port/struct.Port.html ```rust // in src/main.rs diff --git a/blog/content/edition-2/posts/04-testing/index.ja.md b/blog/content/edition-2/posts/04-testing/index.ja.md new file mode 100644 index 00000000..de61e1ac --- /dev/null +++ b/blog/content/edition-2/posts/04-testing/index.ja.md @@ -0,0 +1,1032 @@ ++++ +title = "テスト" +weight = 4 +path = "ja/testing" +date = 2019-04-27 + +[extra] +chapter = "Bare Bones" +# Please update this when updating the translation +translation_based_on_commit = "dce5c9825bd4e7ea6c9530e999c9d58f80c585cc" +# GitHub usernames of the people that translated this post +translators = ["woodyZootopia", "JohnTitor"] ++++ + +この記事では、`no_std`な実行環境における単体テスト (unit test) 結合テスト (integration test) について学びます。Rustではカスタムテストフレームワークがサポートされているので、これを使ってカーネルの中でテスト関数を実行します。QEMUの外へとテストの結果を通知するため、QEMUと`bootimage`の様々な機能を使います。 + + + +このブログの内容は [GitHub] 上で公開・開発されています。何か問題や質問などがあれば issue をたててください (訳注: リンクは原文(英語)のものになります)。また[こちら][at the bottom]にコメントを残すこともできます。この記事の完全なソースコードは[`post-04` ブランチ][post branch]にあります。 + +[GitHub]: https://github.com/phil-opp/blog_os +[at the bottom]: #comments +[post branch]: https://github.com/phil-opp/blog_os/tree/post-04 + + + +## この記事を読む前に + +この記事は、(古い版の)[単体テスト][_Unit Testing_]と[結合テスト][_Integration Tests_]の記事を置き換えるものです。この記事は、あなたが[最小のカーネル][_A Minimal Rust Kernel_]の記事を2019-04-27以降に読んだことを前提にしています。主に、あなたの`.cargo/config.toml`ファイルが[標準のターゲットを設定して][sets a default target]おり、[ランナー実行ファイルを定義している][defines a runner executable]ことが条件となります。 + +
+ +**訳注:** [最小のカーネル][_A Minimal Rust Kernel_]の記事が日本語に翻訳されたのはこの日より後なので、あなたがこのサイトを日本語で閲覧している場合は特に問題はありません。 + +
+ +[_Unit Testing_]: @/edition-2/posts/deprecated/04-unit-testing/index.md +[_Integration Tests_]: @/edition-2/posts/deprecated/05-integration-tests/index.md +[_A Minimal Rust Kernel_]: @/edition-2/posts/02-minimal-rust-kernel/index.ja.md +[sets a default target]: @/edition-2/posts/02-minimal-rust-kernel/index.ja.md#biao-zhun-notagetutowosetutosuru +[defines a runner executable]: @/edition-2/posts/02-minimal-rust-kernel/index.ja.md#cargo-runwoshi-u + +## Rustにおけるテスト + +Rustには[テストフレームワークが組み込まれて][built-in test framework]おり、特別な設定なしに単体テストを走らせることができます。何らかの結果をアサーションを使って確認する関数を作り、その関数のヘッダに`#[test]`属性をつけるだけです。その上で`cargo test`を実行すると、あなたのクレートのすべてのテスト関数を自動で見つけて実行してくれます。 + +[built-in test framework]: https://doc.rust-jp.rs/book-ja/ch11-00-testing.html + +残念なことに、私達のカーネルのような`no_std`のアプリケーションにとっては、テストは少しややこしくなります。問題なのは、Rustのテストフレームワークは組み込みの[`test`]ライブラリを内部で使っており、これは標準ライブラリに依存しているということです。つまり、私達の`#[no_std]`のカーネルには標準のテストフレームワークは使えないのです。 + +[`test`]: https://doc.rust-lang.org/test/index.html + +私達のプロジェクト内で`cargo test`を実行しようとすればそれがわかります: + +``` +> cargo test + Compiling blog_os v0.1.0 (/…/blog_os) +error[E0463]: can't find crate for `test` +``` +`test`クレートは標準ライブラリに依存しているので、私達のベアメタルのターゲットでは使えません。`test`クレートを`#[no_std]`環境に持ってくるということは[不可能ではない][utest]のですが、非常に不安定であり、また`panic`マクロの再定義といった技巧 (ハック) が必要になってしまいます。 + +[utest]: https://github.com/japaric/utest + +### 独自のテストフレームワーク + +ありがたいことに、Rustでは、不安定な[`custom_test_frameworks` (独自のテストフレームワーク) ][`custom_test_frameworks`]機能を使えば標準のテストフレームワークを置き換えることができます。この機能には外部ライブラリは必要なく、したがって`#[no_std]`環境でも動きます。これは、`#[test_case]`属性をつけられたすべての関数のリストを引数としてユーザの指定した実行関数を呼び出すことで働きます。こうすることで、(実行関数の)実装内容によってテストプロセスを最大限コントロールできるようにしているのです。 + +[`custom_test_frameworks`]: https://doc.rust-lang.org/unstable-book/language-features/custom-test-frameworks.html + +標準のテストフレームワークと比べた欠点は、[`should_panic`テスト][`should_panic` tests]のような多くの高度な機能が利用できないということです。それらの機能が必要なら、自分で実装して提供してください、というわけです。これは私達にとって全く申し分のないことで、というのも、私達の非常に特殊な実行環境では、それらの高度な機能の標準の実装はいずれにせようまく働かないだろうからです。例えば、`#[should_panic]`属性はパニックを検知するためにスタックアンワインドを使いますが、これは私達のカーネルでは無効化しています。 + +[`should_panic` tests]: https://doc.rust-jp.rs/book-ja/ch11-01-writing-tests.html#should_panicでパニックを確認する + +私達のカーネルのための独自テストフレームワークを実装するため、以下を`main.rs`に追記します: + +```rust +// in src/main.rs + +#![feature(custom_test_frameworks)] +#![test_runner(crate::test_runner)] + +#[cfg(test)] +fn test_runner(tests: &[&dyn Fn()]) { + println!("Running {} tests", tests.len()); + for test in tests { + test(); + } +} +``` + +このランナーは短いデバッグメッセージを表示し、リスト内のそれぞれの関数を呼び出すだけです。引数の型である`&[&dyn Fn()]`は、[Fn()][_Fn()_]トレイトの[トレイトオブジェクト][_trait object_]参照の[スライス][_slice_]です。これは要するに、関数のように呼び出せる型への参照のリストです。この (test_runner) 関数はテストでない実行のときには意味がないので、`#[cfg(test)]`属性を使って、テスト時にのみこれがインクルードされるようにします。 + +[_slice_]: https://doc.rust-lang.org/std/primitive.slice.html +[_trait object_]: https://doc.rust-jp.rs/book-ja/ch17-02-trait-objects.html +[_Fn()_]: https://doc.rust-lang.org/std/ops/trait.Fn.html + +`cargo test`を実行すると、今度は成功しているはずです(もし失敗したなら、下の補足を読んでください)。しかし、依然として、`test_runner`からのメッセージではなく "Hello World" が表示されてしまっています。この理由は、`_start`関数がまだエントリポイントとして使われているからです。「独自のテストフレームワーク」機能は`test_runner`を呼び出す`main`関数を生成するのですが、私達は`#[no_main]`属性を使っており、独自のエントリポイントを与えてしまっているため、このmain関数は無視されてしまうのです。 + +
+ +**補足:** 現在、cargoには`cargo test`を実行すると、いくらかのケースにおいて "duplicate lang item" エラーになってしまうバグが存在します。これは、`Cargo.toml`内のプロファイルにおいて`panic = "abort"`を設定していたときに起こります。これを取り除けば`cargo test`はうまくいくはずです。これについて、より詳しく知りたい場合は[cargoのissue](https://github.com/rust-lang/cargo/issues/7359)を読んでください。 + +
+ +これを修正するために、まず生成される関数の名前を`reexport_test_harness_main`属性を使って`main`とは違うものに変える必要があります。そして、その改名 (リネーム) された関数を`_start`関数から呼び出せばよいです。 + +```rust +// in src/main.rs + +#![reexport_test_harness_main = "test_main"] + +#[no_mangle] +pub extern "C" fn _start() -> ! { + println!("Hello World{}", "!"); + + #[cfg(test)] + test_main(); + + loop {} +} +``` + +テストフレームワークのエントリ関数の名前を`test_main`に設定し、私達の`_start`エントリポイントから呼び出しています。`test_main`関数は通常の実行時には生成されていないので、[条件付きコンパイル][conditional compilation]を用いて、テスト時にのみこの関数への呼び出しが追記されるようにしています。 + +`cargo test`を実行すると、 `test_runner`からの "Running 0 tests" というメッセージが画面に表示されます。これで、テスト関数を作り始める準備ができました: + +```rust +// in src/main.rs + +#[test_case] +fn trivial_assertion() { + print!("trivial assertion... "); // "些末なアサーション……" + assert_eq!(1, 1); + println!("[ok]"); +} +``` + +`cargo test`を実行すると、以下の出力を得ます: + +![QEMU printing "Hello World!", "Running 1 tests", and "trivial assertion... [ok]"](qemu-test-runner-output.png) + +今、`test_runner`関数に渡される`test`のスライスは、`trivial_assertion`関数への参照を保持しています。`trivial assertion... [ok]`という画面の出力から、テストが呼び出され成功したことがわかります。 + +テストを実行したあとは、`test_runner`から`test_main`関数へとリターンし、さらに`_start`エントリポイント関数へとリターンします。エントリポイント関数がリターンすることは認められていないので、`_start`の最後では無限ループに入ります。しかし、`cargo test`にはすべてのテストを実行し終わった後に終了してほしいので、これは問題です。 + +## QEMUを終了する + +今の所、`_start`関数の最後で無限ループがあるので、`cargo test`を実行するたびにQEMUを手動で終了しないといけません。ユーザによる入力などのないスクリプトでも`cargo test`を実行したいので、これは不都合です。これに対する綺麗な解決法はOSをシャットダウンする適切な方法を実装することでしょう。これは[APM]か[ACPI]というパワーマネジメント標準規格へのサポートを実装する必要があるので、残念なことに比較的複雑です。 + +[APM]: https://wiki.osdev.org/APM +[ACPI]: https://wiki.osdev.org/ACPI + +しかし嬉しいことに、ある「脱出口」があるのです。QEMUは特殊な`isa-debug-exit`デバイスをサポートしており、これを使うとゲストシステムから簡単にQEMUを終了できます。これを有効化するためには、QEMUに`-device`引数を渡す必要があります。これは`Cargo.toml`に`package.metadata.bootimage.test-args`設定キーを追加することで行えます。 + +```toml +# in Cargo.toml + +[package.metadata.bootimage] +test-args = ["-device", "isa-debug-exit,iobase=0xf4,iosize=0x04"] +``` + +`bootimage runner`は、`test-args`をすべてのテスト実行可能ファイルの標準QEMUコマンドに追加します。通常の`cargo run`のとき、これらの引数は無視されます。 + +デバイス名 (`isa-debug-exit`) に加え、カーネルからそのデバイスにたどり着くための **I/Oポート** を指定する`iobase`と`iosize`という2つのパラメータを渡しています。 + +### I/Oポート + +CPUと周辺機器 (ペリフェラル) が通信するやり方には、 **memory-mapped (メモリマップされた) I/O** と **port-mapped (ポートマップされた) I/O** の2つがあります。memory-mapped I/Oについては、すでに[VGAテキストバッファ][VGA text buffer]にメモリアドレス`0xb8000`を使ってアクセスしたときに使っています。このアドレスはRAMではなく、VGAデバイス上にあるメモリにマップされているのです。 + +[VGA text buffer]: @/edition-2/posts/03-vga-text-buffer/index.ja.md + +一方、port-mapped I/Oは通信に別個のI/Oバスを使います。接続されたそれぞれの周辺機器は1つ以上のポート番号を持っています。それらのI/Oポートと通信するために、`in`と`out`という特別なCPU命令があり、これらはポート番号と1バイトのデータを受け取ります(`u16`や`u32`を送信できる、これらの亜種も存在します)。 + +`isa-debug-exit`はこのport-mapped I/Oを使います。`iobase`パラメータはどのポートにこのデバイスが繋がれているのか(`0xf4`はx86のI/Oバスにおいて[普通使われない][list of x86 I/O ports]ポートです)を、`iosize`はポートの大きさ(`0x04`は4バイトを意味します)を指定します。 + +[list of x86 I/O ports]: https://wiki.osdev.org/I/O_Ports#The_list + +### 「終了デバイス」を使う + +`isa-debug-exit`の機能は非常に単純です。値`value`が`iobase`により指定されたI/Oポートに書き込まれたら、QEMUは[終了ステータス][exit status]を`(value << 1) | 1`にして終了します。なので、このポートに`0`を書き込むと、QEMUは終了ステータス`(0 << 1) | 1 = 1`で、`1`を書き込むと終了ステータス`(1 << 1) | 1 = 3`で終了します。 + +[exit status]: https://ja.wikipedia.org/wiki/終了ステータス + +`in`と`out`のアセンブリ命令を手動で呼び出す代わりに、[`x86_64`]クレートによって提供されるabstraction (抽象化されたもの) を使います。このクレートへの依存を追加するため、`Cargo.toml`の`dependencies`セクションにこれを追加しましょう: + +[`x86_64`]: https://docs.rs/x86_64/0.13.2/x86_64/ + +```toml +# in Cargo.toml + +[dependencies] +x86_64 = "0.13.2" +``` + +これで、このクレートによって提供される[`Port`]型を使って`exit_qemu`関数を作ることができます。 + +[`Port`]: https://docs.rs/x86_64/0.13.2/x86_64/instructions/port/struct.Port.html + +```rust +// in src/main.rs + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[repr(u32)] +pub enum QemuExitCode { + Success = 0x10, + Failed = 0x11, +} + +pub fn exit_qemu(exit_code: QemuExitCode) { + use x86_64::instructions::port::Port; + + unsafe { + let mut port = Port::new(0xf4); + port.write(exit_code as u32); + } +} +``` + +この関数は新しい[`Port`]を`0xf4`(`isa-debug-exit`デバイスの`iobase`です)に作ります。そして、渡された終了コードをポートに書き込みます。`isa-device-exit`デバイスの`iosize`に4バイトを指定していたので、`u32`を使うことにします。I/Oポートへの書き込みは一般にあらゆる振る舞いを引き起こしうるので、これらの命令は両方unsafeです。 + +終了ステータスを指定するために、`QemuExitCode`enumを作ります。成功したら成功(`Success`)の終了コードで、そうでなければ失敗(`Failed`)の終了コードで終了しようというわけです。enumは`#[repr(u32)]`をつけることで、それぞれのヴァリアントが`u32`の整数として表されるようにしています。終了コード`0x10`を成功に、`0x11`を失敗に使います。終了コードの実際の値は、QEMUの標準の終了コードと被ってしまわない限りはなんでも構いません。例えば、成功の終了コードに`0`を使うと、変換後`(0 << 1) | 1 = 1`になってしまい、これはQEMUが実行に失敗したときの標準終了コードなのでよくありません。QEMUのエラーとテスト実行の成功が区別できなくなります。 + +というわけで、`test_runner`を更新して、すべてのテストが実行されたあとでQEMUを終了するようにできますね: + +```rust +fn test_runner(tests: &[&dyn Fn()]) { + println!("Running {} tests", tests.len()); + for test in tests { + test(); + } + /// new + exit_qemu(QemuExitCode::Success); +} +``` + +`cargo test`を実行すると、QEMUはテスト実行後即座に閉じるのがわかります。しかし、問題は、`Success`の終了コードを渡したのに、`cargo test`はテストが失敗したと解釈することです: + +``` +> cargo test + Finished dev [unoptimized + debuginfo] target(s) in 0.03s + Running target/x86_64-blog_os/debug/deps/blog_os-5804fc7d2dd4c9be +Building bootloader + Compiling bootloader v0.5.3 (/home/philipp/Documents/bootloader) + Finished release [optimized + debuginfo] target(s) in 1.07s +Running: `qemu-system-x86_64 -drive format=raw,file=/…/target/x86_64-blog_os/debug/ + deps/bootimage-blog_os-5804fc7d2dd4c9be.bin -device isa-debug-exit,iobase=0xf4, + iosize=0x04` +error: test failed, to rerun pass '--bin blog_os' +``` + +問題は、`cargo test`が`0`でないすべてのエラーコードを失敗と解釈してしまうことです。 + +### 成功の終了コード + +これを解決するために、`bootimage`は指定された終了コードを`0`へとマップする設定キー、`test-success-exit-code`を提供しています: + +```toml +[package.metadata.bootimage] +test-args = […] +test-success-exit-code = 33 # (0x10 << 1) | 1 +``` + +この設定を使うと、`bootimage`は私達の出した成功の終了コードを、終了コード0へとマップするので、`cargo test`は正しく成功を認識し、テストを失敗したと見做さなくなります。 + + +これで私達のテストランナーは、自動でQEMUを閉じ、結果を報告するようになりました。しかし、QEMUの画面が非常に短い時間開くのは見えますが、短すぎて結果が読めません。QEMUが終了したあともテストの結果が見られるように、コンソールに出力できたら良さそうです。 + +## コンソールに出力する + +テストの結果をコンソールで見るためには、カーネルからホストシステムにどうにかしてデータを送る必要があります。これを達成する方法は色々あり、例えばTCPネットワークインターフェースを通じてデータを送るというのが考えられます。しかし、ネットワークスタックを設定するのは非常に複雑なタスクなので、より簡単な解決策を取ることにしましょう。 + +### シリアルポート + +データを送る簡単な方法とは、[シリアルポート][serial port]という、最近のコンピュータにはもはや見られない古いインターフェース標準を使うことです。これはプログラムするのが簡単で、QEMUはシリアルを通じて送られたデータをホストの標準出力やファイルにリダイレクトすることができます。 + +[serial port]: https://ja.wikipedia.org/wiki/シリアルポート + +シリアルインターフェースを実装しているチップは[UART][UARTs]と呼ばれています。x86には[多くのUARTのモデルがありますが][lots of UART models]、幸運なことに、それらの違いは私達の必要としないような高度な機能だけです。今日よく見られるUARTはすべて[16550 UART]に互換性があるので、このモデルを私達のテストフレームワークに使いましょう。 + +[UARTs]: https://ja.wikipedia.org/wiki/UART +[lots of UART models]: https://en.wikipedia.org/wiki/Universal_asynchronous_receiver-transmitter#UART_models +[16550 UART]: https://ja.wikipedia.org/wiki/16550_UART + +[`uart_16550`]クレートを使ってUARTを初期化しデータをシリアルポートを使って送信しましょう。これを依存先として追加するため、`Cargo.toml`と`main.rs`を書き換えます: + +[`uart_16550`]: https://docs.rs/uart_16550 + +```toml +# in Cargo.toml + +[dependencies] +uart_16550 = "0.2.0" +``` + +`uart_16550`クレートにはUARTレジスタを表現する`SerialPort`構造体が含まれていますが、これのインスタンスは私達自身で作らなくてはいけません。そのため、以下の内容で新しい`serial`モジュールを作りましょう: + +```rust +// in src/main.rs + +mod serial; +``` + +```rust +// in src/serial.rs + +use uart_16550::SerialPort; +use spin::Mutex; +use lazy_static::lazy_static; + +lazy_static! { + pub static ref SERIAL1: Mutex = { + let mut serial_port = unsafe { SerialPort::new(0x3F8) }; + serial_port.init(); + Mutex::new(serial_port) + }; +} +``` + +[VGAテキストバッファ][vga lazy-static]のときのように、`lazy_static`とスピンロックを使って`static`なwriterインスタンスを作ります。`lazy_static`を使うことで、`init`メソッドが初回使用時にのみ呼び出されることを保証できます。 + +`isa-debug-exit`デバイスのときと同じように、UARTはport I/Oを使ってプログラムされています。UARTはより複雑で、様々なデバイスレジスタ群をプログラムするために複数のI/Oポートを使います。unsafeな`SerialPort::new`関数はUARTの最初のI/Oポートを引数とします。この引数から、すべての必要なポートのアドレスを計算することができます。ポートアドレス`0x3F8`を渡していますが、これは最初のシリアルインターフェースの標準のポート番号です。 + +[vga lazy-static]: @/edition-2/posts/03-vga-text-buffer/index.md#lazy-statics + +シリアルポートを簡単に使えるようにするために、`serial_print!`と`serial_println!`マクロを追加します: + +```rust +#[doc(hidden)] +pub fn _print(args: ::core::fmt::Arguments) { + use core::fmt::Write; + SERIAL1.lock().write_fmt(args).expect("Printing to serial failed"); +} + +/// シリアルインターフェースを通じてホストに出力する。 +#[macro_export] +macro_rules! serial_print { + ($($arg:tt)*) => { + $crate::serial::_print(format_args!($($arg)*)); + }; +} + +/// シリアルインターフェースを通じてホストに出力し、改行を末尾に追加する。 +#[macro_export] +macro_rules! serial_println { + () => ($crate::serial_print!("\n")); + ($fmt:expr) => ($crate::serial_print!(concat!($fmt, "\n"))); + ($fmt:expr, $($arg:tt)*) => ($crate::serial_print!( + concat!($fmt, "\n"), $($arg)*)); +} +``` + +この実装は私達の`print`および`println`マクロとよく似ています。`SerialPort`型はすでに[`fmt::Write`]トレイトを実装しているので、自前の実装を提供する必要はありません。 + +[`fmt::Write`]: https://doc.rust-lang.org/nightly/core/fmt/trait.Write.html + +これで、テストコードにおいてVGAテキストバッファの代わりにシリアルインターフェースに出力することができます: + +```rust +// in src/main.rs + +#[cfg(test)] +fn test_runner(tests: &[&dyn Fn()]) { + serial_println!("Running {} tests", tests.len()); + […] +} + +#[test_case] +fn trivial_assertion() { + serial_print!("trivial assertion... "); + assert_eq!(1, 1); + serial_println!("[ok]"); +} +``` + +`#[macro_export]`属性を使うことで、`serial_println`マクロはルート名前空間 (namespace) の直下に置かれるので、`use crate::serial::serial_println`とインポートするとうまくいかないということに注意してください。 + +### QEMUの引数 + +QEMUからのシリアル出力を見るために、出力を標準出力にリダイレクトしたいので、`-serial`引数を使う必要があります。 + +```toml +# in Cargo.toml + +[package.metadata.bootimage] +test-args = [ + "-device", "isa-debug-exit,iobase=0xf4,iosize=0x04", "-serial", "stdio" +] +``` + +これで`cargo test`を実行すると、テスト出力がコンソールに直接出力されているのが見えるでしょう: + +``` +> cargo test + Finished dev [unoptimized + debuginfo] target(s) in 0.02s + Running target/x86_64-blog_os/debug/deps/blog_os-7b7c37b4ad62551a +Building bootloader + Finished release [optimized + debuginfo] target(s) in 0.02s +Running: `qemu-system-x86_64 -drive format=raw,file=/…/target/x86_64-blog_os/debug/ + deps/bootimage-blog_os-7b7c37b4ad62551a.bin -device + isa-debug-exit,iobase=0xf4,iosize=0x04 -serial stdio` +Running 1 tests +trivial assertion... [ok] +``` + +しかし、テストが失敗したときは、私達のパニックハンドラはまだ`println`を使っているので、出力がQEMUの中に出てしまいます。これをシミュレートするには、`trivial_assertion`テストの中のアサーションを`assert_eq!(0, 1)`に変えればよいです: + +![QEMU printing "Hello World!" and "panicked at 'assertion failed: `(left == right)` + left: `0`, right: `1`', src/main.rs:55:5](qemu-failed-test.png) + +他のテスト出力がシリアルポートに出力されている一方、パニックメッセージはまだVGAバッファに出力されているのがわかります。このパニックメッセージは非常に役に立つので、コンソールでこのメッセージも見られたら非常に便利でしょう。 + +### パニック時のエラーメッセージを出力する + +パニック時にQEMUをエラーメッセージとともに終了するためには、[条件付きコンパイル][conditional compilation]を使うことで、テスト時に異なるパニックハンドラを使うことができます: + +[conditional compilation]: https://doc.rust-lang.org/1.30.0/book/first-edition/conditional-compilation.html + +```rust +// 前からあるパニックハンドラ +#[cfg(not(test))] // 新しく追加した属性 +#[panic_handler] +fn panic(info: &PanicInfo) -> ! { + println!("{}", info); + loop {} +} + +// テストモードで使うパニックハンドラ +#[cfg(test)] +#[panic_handler] +fn panic(info: &PanicInfo) -> ! { + serial_println!("[failed]\n"); + serial_println!("Error: {}\n", info); + exit_qemu(QemuExitCode::Failed); + loop {} +} +``` + +テストパニックハンドラには`println`の代わりに`serial_println`を使い、そのあと失敗の終了コードでQEMUを終了します。コンパイラには、`exit_qemu`の呼び出しのあと`isa-debug-exit`デバイスがプログラムを終了させているということはわからないので、やはり最後に無限ループを入れないといけないことに注意してください。 + +これでQEMUはテストが失敗したときも終了し、コンソールに役に立つエラーメッセージを表示するようになります: + +``` +> cargo test + Finished dev [unoptimized + debuginfo] target(s) in 0.02s + Running target/x86_64-blog_os/debug/deps/blog_os-7b7c37b4ad62551a +Building bootloader + Finished release [optimized + debuginfo] target(s) in 0.02s +Running: `qemu-system-x86_64 -drive format=raw,file=/…/target/x86_64-blog_os/debug/ + deps/bootimage-blog_os-7b7c37b4ad62551a.bin -device + isa-debug-exit,iobase=0xf4,iosize=0x04 -serial stdio` +Running 1 tests +trivial assertion... [failed] + +Error: panicked at 'assertion failed: `(left == right)` + left: `0`, + right: `1`', src/main.rs:65:5 +``` + +これですべてのテスト出力がコンソールで見られるようになったので、一瞬出てくるQEMUウィンドウはもはや必要ありません。ですので、これを完全に見えなくしてしまいましょう。 + +### QEMUを隠す + +すべてのテスト結果を`isa-debug-exit`デバイスとシリアルポートを使って通知できるので、QEMUのウィンドウはもはや必要ありません。これは、QEMUに`-display none`引数を渡すことで隠すことができます: + +```toml +# in Cargo.toml + +[package.metadata.bootimage] +test-args = [ + "-device", "isa-debug-exit,iobase=0xf4,iosize=0x04", "-serial", "stdio", + "-display", "none" +] +``` + +これでQEMUは完全にバックグラウンドで実行するようになり、ウィンドウはもう開きません。これで、ジャマが減っただけでなく、私達のテストフレームワークがグラフィカルユーザーインターフェースのない環境――たとえばCIサービスや[SSH]接続――でも使えるようになりました。 + +[SSH]: https://ja.wikipedia.org/wiki/Secure_Shell + +### タイムアウト + +`cargo test`はテストランナーが終了するまで待つので、絶対に終了しないテストがあるとテストランナーを永遠にブロックしかねません。これは悲しいですが、普通エンドレス (終了しない) ループを回避するのは簡単なので、実際は大きな問題ではありません。しかしながら、私達のケースでは、様々な状況でエンドレスループが発生しうるのです: + +- ブートローダーが私達のカーネルを読み込むのに失敗し、これによりシステムが延々と再起動し続ける。 +- BIOS/UEFIファームウェアがブートローダーの読み込みに失敗し、同様に延々と再起動し続ける。 +- 私達の関数のどれかの最後で、CPUが`loop {}`文に入ってしまう(例えば、QEMU終了デバイスがうまく動かなかったなどの理由で)。 +- CPU例外(今後説明します)がうまく捕捉されなかった場合などに、ハードウェアがシステムリセットを行う。 + +エンドレスループは非常に多くの状況で発生しうるので、`bootimage`はそれぞれのテスト実行ファイルに対し標準で5分のタイムアウトを設定しています。テストがこの時間内に終了しなかった場合は失敗したとみなされ、"Timed Out" エラーがコンソールに出力されます。この機能により、エンドレスループで詰まったテストが`cargo test`を永遠にブロックしてしまうことがないことが保証されます。 + +これを自分で試すこともできます。`trivial_assertion`テストに`loop {}`文を追加してください。`cargo test`を実行すると、5分後にテストがタイムアウトしたことが表示されるでしょう。タイムアウトまでの時間は`Cargo.toml`の`test-timeout`キーで[設定可能][bootimage config]です: + +[bootimage config]: https://github.com/rust-osdev/bootimage#configuration + +```toml +# in Cargo.toml + +[package.metadata.bootimage] +test-timeout = 300 # (単位は秒) +``` + +`trivial_assertion`テストがタイムアウトするのを待ちたくない場合は、上の値を一時的に下げても良いでしょう。 + +### 出力機能を自動で挿入する + +現在、私達の`trivial_assertion`テストは、自分のステータス情報を`serial_print!`/`serial_println!`を使って出力する必要があります: + +```rust +#[test_case] +fn trivial_assertion() { + serial_print!("trivial assertion... "); + assert_eq!(1, 1); + serial_println!("[ok]"); +} +``` + +私達の書くすべてのテストにこれらのprint文を手動で追加するのは煩わしいので、私達の`test_runner`を変更して、これらのメッセージを自動で出力するようにしましょう。そうするためには、`Testable`トレイトを作る必要があります: + +```rust +// in src/main.rs + +pub trait Testable { + fn run(&self) -> (); +} +``` + +ここで、[`Fn()`トレイト][`Fn()` trait]を持つ型`T`すべてにこのトレイトを実装してやるのがミソです: + +[`Fn()` trait]: https://doc.rust-lang.org/stable/core/ops/trait.Fn.html + +```rust +// in src/main.rs + +impl Testable for T +where + T: Fn(), +{ + fn run(&self) { + serial_print!("{}...\t", core::any::type_name::()); + self(); + serial_println!("[ok]"); + } +} +``` + +`run`関数を実装するに当たり、まず[`any::type_name`]を使って関数の名前を出力します。この関数はコンパイラの中に直接実装されており、すべての型の文字列による説明を返すことができます。関数の型はその名前なので、今回の場合まさに私達のやりたいことができています。文字`\t`は[タブ文字][tab character]であり、メッセージ`[ok]`の前にちょっとしたアラインメント(幅を整えるための空白)をつけます。 + +[`any::type_name`]: https://doc.rust-lang.org/stable/core/any/fn.type_name.html +[tab character]: https://ja.wikipedia.org/wiki/タブキー#タブ文字 + +関数名を出力したあとは、テスト関数を`self()`を使って呼び出します。これは、`self`が`Fn()`トレイトを実装していることが要求されているからこそ可能です。テスト関数がリターンしたら、`[ok]`を出力してこの関数がパニックしなかったことを示します。 + +最後に、`test_runner`をこの`Testable`トレイトを使うように更新します: + +```rust +// in src/main.rs + +#[cfg(test)] +pub fn test_runner(tests: &[&dyn Testable]) { + serial_println!("Running {} tests", tests.len()); + for test in tests { + test.run(); // ここを変更 + } + exit_qemu(QemuExitCode::Success); +} +``` + +変更点は2つだけで、`tests`引数の型を`&[&dyn Fn()]`から`&[&dyn Testable]`に変えたことと、`test()`の変わりに`test.run()`を呼ぶようにしたことです。 + +また、`trivial_assertion`のprint文は今や自動で出力されるようになったので、これを取り除きましょう: + +```rust +// in src/main.rs + +#[test_case] +fn trivial_assertion() { + assert_eq!(1, 1); +} +``` + +これで`cargo test`の出力は以下のようになるはずです: + +``` +Running 1 tests +blog_os::trivial_assertion... [ok] +``` + +いま、関数名には関数までのフルパスが含まれていますが、これは異なるモジュールのテスト関数が同じ名前を持っているときに便利です。それ以外の点において出力は前と同じですが、もう手動でテストにprint文を付け加える必要はありません。 + +## VGAバッファをテストする + +私達のテストフレームワークがうまく動くようになったので、私達のVGAバッファに関する実装のテストをいくつか作ってみましょう。まず、`println`がパニックすることなく成功することを確かめる非常に単純なテストを作ります: + +```rust +// in src/vga_buffer.rs + +#[test_case] +fn test_println_simple() { + println!("test_println_simple output"); +} +``` + +このテストは、適当な文字列をVGAバッファにただ出力するだけです。このテストがパニックすることなく終了したなら、`println`の呼び出しもまたパニックしなかったということです。 + +たくさんの行が出力され、行がスクリーンから押し出されたとしてもパニックが起きないことを確かめるために、もう一つテストを作ってみましょう: + +```rust +// in src/vga_buffer.rs + +#[test_case] +fn test_println_many() { + for _ in 0..200 { + println!("test_println_many output"); + } +} +``` + +出力された行が本当に画面に映っているのかを確かめるテスト関数も作ることができます: + +```rust +// in src/vga_buffer.rs + +#[test_case] +fn test_println_output() { + let s = "Some test string that fits on a single line"; + println!("{}", s); + for (i, c) in s.chars().enumerate() { + let screen_char = WRITER.lock().buffer.chars[BUFFER_HEIGHT - 2][i].read(); + assert_eq!(char::from(screen_char.ascii_character), c); + } +} +``` + +この関数はテスト用文字列を定義し、`println`を使って出力し、静的な`WRITER`――VGAテキストバッファを表現しています――上の表示文字を走査 (イテレート) しています。`println`は最後に出力された行につづけて出力し、即座に改行するので、`BUFFER_HEIGHT - 2`行目にこの文字列は現れるはずです。 + +[`enumerate`]を使うことで、変数`i`によって反復の回数を数え、これを`c`に対応する画面上の文字を読み込むのに使っています。画面の文字の`ascii_character`を`c`と比較することで、文字列のそれぞれの文字がVGAテキストバッファに確実に現れていることを確かめることができます。 + +[`enumerate`]: https://doc.rust-lang.org/core/iter/trait.Iterator.html#method.enumerate + +ご想像の通り、もっとたくさんテストを作っても良いです。例えば、非常に長い行を出力しても、うまく折り返され、パニックしないことをテストする関数や、改行・出力不可能な文字・非ユニコード文字などが適切に処理されることを確かめるような関数を作ることもできます。 + +ですが、この記事の残りでは、 **結合テスト** を作って、異なる構成要素 (コンポーネント) の相互作用をテストする方法を説明しましょう。 + +## 結合テスト +Rustにおける[結合テスト][integration tests]では、慣習としてプロジェクトのルートにおいた`tests`ディレクトリ (つまり`src`ディレクトリと同じ階層ですね) にテストプログラムを入れます。標準のテストフレームワークも、独自のテストフレームワークも、自動的にこのディレクトリにあるすべてのテストを実行します。 + +[integration tests]: https://doc.rust-jp.rs/book-ja/ch11-03-test-organization.html#結合テスト + +すべての結合テストは、独自の実行可能ファイルを持っており、私達の`main.rs`とは完全に独立しています。つまり、それぞれのテストに独自のエントリポイント関数を定義しないといけないということです。どのような仕組みになっているのかを詳しく見るために、`basic_boot`という名前で試しに結合テストを作ってみましょう: + +```rust +// in tests/basic_boot.rs + +#![no_std] +#![no_main] +#![feature(custom_test_frameworks)] +#![test_runner(crate::test_runner)] +#![reexport_test_harness_main = "test_main"] + +use core::panic::PanicInfo; + +#[no_mangle] // この関数の名前を変えない +pub extern "C" fn _start() -> ! { + test_main(); + + loop {} +} + +fn test_runner(tests: &[&dyn Fn()]) { + unimplemented!(); +} + +#[panic_handler] +fn panic(info: &PanicInfo) -> ! { + loop {} +} +``` + +結合テストは独立した実行ファイルであるので、クレート属性(`no_std`、`no_main`、`test_runner`など)をすべてもう一度与えないといけません。また、新しいエントリポイント関数`_start`も作らないといけません。これはテストエントリポイント関数`test_main`を呼び出します。結合テストの実行可能ファイルは、テストモードでないときはビルドされないので、`cfg(test)`属性は必要ありません。 + +今のところ、`test_runner`関数の中身として、常にパニックする[`unimplemented`]マクロを代わりに入れており、そして`panic`ハンドラにはただの`loop`を入れています。本当は、`serial_println`マクロと`exit_qemu`関数を使って、これらの関数を`main.rs`と全く同じように実装したいです。しかし問題は、テストが私達の`main.rs`実行ファイルとは完全に別にビルドされているので、これらの関数にアクセスすることができないということです。 + +[`unimplemented`]: https://doc.rust-lang.org/core/macro.unimplemented.html + +この段階で`cargo test`を実行したら、パニックハンドラによってエンドレスループに入ってしまうでしょう。QEMUを終了するキーボードショートカットである`Ctrl+c`を使わないといけません。 + +### ライブラリを作る + +結合テストに必要な関数を利用できるようにするために、`main.rs`からライブラリを分離してやる必要があります。こうすると、他のクレートや結合テスト実行ファイルがこれをインクルードできるようになります。これをするために、新しい`src/lib.rs`ファイルを作りましょう: + +```rust +// src/lib.rs + +#![no_std] + +``` + +`main.rs`と同じく、`lib.rs`は自動的にcargoに認識される特別なファイルです。ライブラリは別のコンパイル単位なので、`#![no_std]`属性を再び指定する必要があります。 + +`cargo test`がライブラリにも使えるようにするために、テストのための関数や属性を`main.rs`から`lib.rs`へと移す必要もあります。 + +```rust +// in src/lib.rs + +#![cfg_attr(test, no_main)] +#![feature(custom_test_frameworks)] +#![test_runner(crate::test_runner)] +#![reexport_test_harness_main = "test_main"] + +use core::panic::PanicInfo; + +pub trait Testable { + fn run(&self) -> (); +} + +impl Testable for T +where + T: Fn(), +{ + fn run(&self) { + serial_print!("{}...\t", core::any::type_name::()); + self(); + serial_println!("[ok]"); + } +} + +pub fn test_runner(tests: &[&dyn Testable]) { + serial_println!("Running {} tests", tests.len()); + for test in tests { + test.run(); + } + exit_qemu(QemuExitCode::Success); +} + +pub fn test_panic_handler(info: &PanicInfo) -> ! { + serial_println!("[failed]\n"); + serial_println!("Error: {}\n", info); + exit_qemu(QemuExitCode::Failed); + loop {} +} + +/// `cargo test`のときのエントリポイント +#[cfg(test)] +#[no_mangle] +pub extern "C" fn _start() -> ! { + test_main(); + loop {} +} + +#[cfg(test)] +#[panic_handler] +fn panic(info: &PanicInfo) -> ! { + test_panic_handler(info) +} +``` + +`test_runner`を(`main.rs`の)実行可能ファイルと結合テストの両方から利用可能にするために、`cfg(test)`属性をこれに適用せず、また、publicにします。パニックハンドラの実装もpublicな`test_panic_handler`関数へと分離することで、実行可能ファイルからも使えるようにしています。 + +`lib.rs`は`main.rs`とは独立にコンパイルされるので、ライブラリがテストモードでコンパイルされるときは`_start`エントリポイントとパニックハンドラを追加する必要があります。このような場合、[`cfg_attr`]クレート属性を使うことで、`no_main`属性を条件付きで有効化することができます。 + +[`cfg_attr`]: https://doc.rust-lang.org/reference/conditional-compilation.html#the-cfg_attr-attribute + +`QemuExitCode`enumと`exit_qemu`関数も移動し、publicにします: + +```rust +// in src/lib.rs + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[repr(u32)] +pub enum QemuExitCode { + Success = 0x10, + Failed = 0x11, +} + +pub fn exit_qemu(exit_code: QemuExitCode) { + use x86_64::instructions::port::Port; + + unsafe { + let mut port = Port::new(0xf4); + port.write(exit_code as u32); + } +} +``` + +これで、実行ファイルも結合テストもこれらの関数をライブラリからインポートでき、自前の実装を定義する必要はありません。`println`と`serial_println`も利用可能にするために、モジュールの宣言も移動させましょう: + +```rust +// in src/lib.rs + +pub mod serial; +pub mod vga_buffer; +``` + +モジュールをpublicにすることで、ライブラリの外からも使えるようにしています。`println`と`serial_println`マクロは、これらのモジュールの`_print`関数を使っているため、これらのマクロを使うためにも、この変更は必要です。 + +では、`main.rs`をこのライブラリを使うように更新しましょう: + +```rust +// src/main.rs + +#![no_std] +#![no_main] +#![feature(custom_test_frameworks)] +#![test_runner(blog_os::test_runner)] +#![reexport_test_harness_main = "test_main"] + +use core::panic::PanicInfo; +use blog_os::println; + +#[no_mangle] +pub extern "C" fn _start() -> ! { + println!("Hello World{}", "!"); + + #[cfg(test)] + test_main(); + + loop {} +} + +/// この関数はパニック時に呼ばれる。 +#[cfg(not(test))] +#[panic_handler] +fn panic(info: &PanicInfo) -> ! { + println!("{}", info); + loop {} +} + +#[cfg(test)] +#[panic_handler] +fn panic(info: &PanicInfo) -> ! { + blog_os::test_panic_handler(info) +} +``` + +ライブラリは通常の外部クレートと同じように使うことができます。名前は、私達のクレート名――今回なら`blog_os`――になります。上のコードでは、`blog_os::test_runner`関数を`test_runner`属性で、`blog_os::test_panic_handler`関数を`cfg(test)`のパニックハンドラで使っています。また、`println`マクロをインポートすることで、`_start`と`panic`関数で使えるようにもしています。 + +この時点で、`cargo run`と`cargo test`は再びうまく実行できるようになっているはずです。もちろん、`cargo test`は依然エンドレスループするはずですが(`ctrl+c`で終了できます)。結合テストに必要な関数を使ってこれを修正しましょう。 + +### 結合テストを完成させる + +`src/main.rs`と同じように、`tests/basic_boot.rs`実行ファイルは新しいライブラリから型をインポートできます。これで、テストを完成させるのに足りない要素をインポートすることができます。 + +```rust +// in tests/basic_boot.rs + +#![test_runner(blog_os::test_runner)] + +#[panic_handler] +fn panic(info: &PanicInfo) -> ! { + blog_os::test_panic_handler(info) +} +``` + +テストランナーを再実装することはせず、ライブラリの`test_runner`関数を使います。`panic`ハンドラとしては、`main.rs`でやったように`blog_os::test_panic_handler`関数を呼びます。 + +これで、`cargo test`は再び通常通り終了するはずです。実行すると、`lib.rs`、`main.rs`、そして`basic_boot.rs`を順にそれぞれビルドし、テストを実行するのが見えるはずです。`main.rs`と`basic_boot`結合テストに関しては、これらには`#[test_case]`のつけられた関数はないため、"Running 0 tests"と報告されるはずです。 + +これで、`basic_boot.rs`にテストを追加していくことができます。例えば、`println`がパニックすることなくうまく行くことを、VGAバッファのときのようにテストすることができます: + +```rust +// in tests/basic_boot.rs + +use blog_os::println; + +#[test_case] +fn test_println() { + println!("test_println output"); +} +``` + +`cargo test`を実行すると、テスト関数を見つけ出して実行しているのがわかるでしょう。 + +このテストは、VGAバッファのテストとほとんど同じであるため、今のところあまり意味がないように思われるかもしれません。しかし、将来的に`main.rs`の`_start`関数と`lib.rs`はどんどん大きくなり、`test_main`関数を実行する前に様々な初期化ルーチンを呼ぶようになるかもしれないので、これらの2つのテストは全然違う環境で実行されるようになるかもしれないのです。 + +`println`を`basic_boot`環境で(`_start`で初期化ルーチンを一切呼ぶことなく)テストすることにより、起動の直後に`println`が使えることが保証されます。私達は、例えばパニックメッセージの出力などを`println`に依存しているので、これは重要です。 + +### 今後のテスト + +結合テストの魅力は、これらが完全に独立した実行ファイルとして扱われることです。これにより、実行環境を完全にコントロールすることができるので、コードがCPUやハードウェアデバイスと正しく相互作用していることをテストすることができるのです。 + +`basic_boot`テストは結合テストの非常に簡単な例でした。今後、私達のカーネルは機能がより豊富になり、そして様々な方法でハードウェアと相互作用するようになります。結合テストを追加することにより、それらの相互作用が期待通り動く(また、期待通り動きつづけている)ことを確かめることができるのです。今後追加できるテストの例としては、以下があります: + +- **CPU例外 (exception) **: プログラムが不正な操作(例えばゼロで割るなど)を行った場合、CPUは例外を投げます(訳注:例外を発することを、英語でthrow an exceptionというのにちなんで、慣例的に「投げる」と表現します)。カーネルはそのような例外に対するハンドラ関数を登録しておくことができます。結合テストで、CPU例外が起こったときに、例外ハンドラが呼ばれていることや、例外が解決可能だった場合に実行が継続することを確かめることができるでしょう。 +- **ページテーブル**: ページテーブルは、どのメモリ領域が有効でアクセスできるかを定義しています。例えばプログラムを立ち上げるとき、このページテーブルを変更することで、新しいメモリ領域を割り当てることが可能です。結合テストで、ページテーブルに`_start`関数内で何らかの変更を施して、その変更が期待通りの効果を起こしているかを`#[test_case]`関数で確かめることができるでしょう。 +- **ユーザー空間 (スペース) プログラム**: ユーザー空間プログラムは、システムの資源 (リソース) に限られたアクセスしか持たないプログラムのことです。これらは例えば、カーネルのデータ構造や、他のプログラムのメモリにアクセスすることはできません。結合テストで、禁止された操作を実行するようなユーザー空間プログラムを起動し、カーネルがそれらをすべて防ぐことを確かめることができるでしょう。 + +ご想像のとおり、もっと多くのテストが可能です。このようなテストを追加することで、カーネルに新しい機能を追加したときや、コードをリファクタリングしたときに、これらを壊してしまっていないことを保証できます。これは、私達のカーネルがより大きく、より複雑になったときに特に重要になります。 + +### パニックしなければならないテスト + +標準ライブラリのテストフレームワークは、[`#[should_panic]`属性][should_panic]をサポートしています。これを使うと、失敗しなければならないテストを作ることができます。これは、例えば、関数が無効な引数を渡されたときに失敗することを確かめる場合などに便利です。残念なことに、この機能は標準ライブラリのサポートを必要とするため、`#[no_std]`クレートではこの属性はサポートされていません。 + +[should_panic]: https://doc.rust-jp.rs/rust-by-example-ja/testing/unit_testing.html#testing-panics + +`#[should_panic]`属性は使えませんが、パニックハンドラから成功のエラーコードで終了するような結合テストを作れば、似たような動きをさせることはできます。そのようなテストを`should_panic`という名前で作ってみましょう: + +```rust +// in tests/should_panic.rs + +#![no_std] +#![no_main] + +use core::panic::PanicInfo; +use blog_os::{QemuExitCode, exit_qemu, serial_println}; + +#[panic_handler] +fn panic(_info: &PanicInfo) -> ! { + serial_println!("[ok]"); + exit_qemu(QemuExitCode::Success); + loop {} +} +``` + +これは`_start`関数や、独自テストランナー属性などをまだ定義していないので未完成です。足りない部分を追加しましょう: + +```rust +// in tests/should_panic.rs + +#![feature(custom_test_frameworks)] +#![test_runner(test_runner)] +#![reexport_test_harness_main = "test_main"] + +#[no_mangle] +pub extern "C" fn _start() -> ! { + test_main(); + + loop {} +} + +pub fn test_runner(tests: &[&dyn Fn()]) { + serial_println!("Running {} tests", tests.len()); + for test in tests { + test(); + serial_println!("[test did not panic]"); + exit_qemu(QemuExitCode::Failed); + } + exit_qemu(QemuExitCode::Success); +} +``` + +このテストは、`lib.rs`の`test_runner`を使い回さず、自前の、テストがパニックせずリターンしたときに失敗の終了コードを出すような`test_runner`関数を定義しています(私達はテストにパニックしてほしいわけですから)。もしテスト関数が一つも定義されていなければ、このランナーは成功のエラーコードで終了します。ランナーは一つテストを実行したら必ず終了するので、1つ以上の`#[test_case]`関数を定義しても意味はありません。 + +では、失敗するはずのテストを追加してみましょう: + +```rust +// in tests/should_panic.rs + +use blog_os::serial_print; + +#[test_case] +fn should_fail() { + serial_print!("should_panic::should_fail...\t"); + assert_eq!(0, 1); +} +``` + +このテストは`assert_eq`を使って`0`と`1`が等しいことをアサートしています。これはもちろん失敗するので、私達のテストは望み通りパニックします。ここで、`Testable`トレイトは使っていないので、関数名は`serial_print!`を使って自分で出力しないといけないことに注意してください。 + +`cargo test --test should_panic`を使ってテストすると、テストが期待通りパニックし、成功したことがわかるでしょう。アサーションをコメントアウトしテストをもう一度実行すると、"test did not panic"というメッセージとともに、テストが確かに失敗することがわかります。 + +この方法の無視できない欠点は、テスト関数を一つしか使えないことです。`#[test_case]`関数が複数ある場合、パニックハンドラが呼び出された後で(プログラムの)実行を続けることはできないので、最初の関数のみが実行されます。この問題を解決するいい方法を私は知らないので、もしなにかアイデアがあったら教えてください! + +### ハーネス (harness) のないテスト + +
+ +**訳注:** ハーネスとは、もともとは馬具の一種を意味する言葉です。転じて「制御する道具」一般を指し、また[テストハーネス](https://en.wikipedia.org/wiki/Test_harness)というと(`test_runner`のように)複数のテストケースを処理し、その振る舞い・出力などを適切に処理・整形してくれるプログラムのことを指します。 + +
+ +(私達の`should_panic`テストのように)一つしかテスト関数を持たない結合テストでは、テストランナーは必ずしも必要というわけではありません。このような場合、テストランナーは完全に無効化してしまって、`_start`関数からテストを直接実行することができます。 + +このためには、`Cargo.toml`でこのテストの`harness`フラグを無効化することがカギとなります。これは、結合テストにテストランナーが使われるかを定義しています。これが`false`に設定されると、標準のテストランナーと独自のテストランナーの両方が無効化され、通常の実行ファイルのように扱われるようになります。 + +`should_panic`テストの`harness`フラグを無効化してみましょう: + +```toml +# in Cargo.toml + +[[test]] +name = "should_panic" +harness = false +``` + +これで、テストランナーに関係するコードを取り除いて、`should_panic`テストを大幅に簡略化することができます。結果として以下のようになります: + +```rust +// in tests/should_panic.rs + +#![no_std] +#![no_main] + +use core::panic::PanicInfo; +use blog_os::{exit_qemu, serial_print, serial_println, QemuExitCode}; + +#[no_mangle] +pub extern "C" fn _start() -> ! { + should_fail(); + serial_println!("[test did not panic]"); + exit_qemu(QemuExitCode::Failed); + loop{} +} + +fn should_fail() { + serial_print!("should_panic::should_fail...\t"); + assert_eq!(0, 1); +} + +#[panic_handler] +fn panic(_info: &PanicInfo) -> ! { + serial_println!("[ok]"); + exit_qemu(QemuExitCode::Success); + loop {} +} +``` + +`should_fail`関数を`_start`関数から直接呼び出して、もしリターンしたら失敗の終了コードで終了するようにしました。今`cargo test --test should_panic`を実行しても、以前と全く同じように振る舞います。 + +`should_panic`なテストを作るとき以外にも`harness`属性は有用なことがあります。例えば、それぞれのテスト関数が副作用を持っており、指定された順番で実行されないといけないときなどです。 + +## まとめ + +テストは、ある要素が望み通りの振る舞いをしていることを保証するのにとても便利なテクニックです。バグが存在しないことを証明することはできないとはいえ、バグを発見したり、特にリグレッションを防ぐのに便利な方法であることは間違いありません。 + +この記事では、私達のRust製カーネルでテストフレームワークを組み立てる方法を説明しました。Rustの独自 (カスタム) テストフレームワーク機能を使って、私達のベアメタル環境における、シンプルな`#[test_case]`属性のサポートを実装しました。私達のテストランナーは、QEMUの`isa-debug-exit`デバイスを使うことで、QEMUをテスト実行後に終了し、テストステータスを報告することができます。エラーメッセージを、VGAバッファの代わりにコンソールに出力するために、シリアルポートの単純なドライバを作りました。 + +`println`マクロのテストをいくつか作った後、記事の後半では結合テストについて見ました。結合テストは`tests`ディレクトリに置かれ、完全に独立した実行ファイルとして扱われることを学びました。結合テストから`exit_qemu`関数と`serial_println`マクロにアクセスできるようにするために、コードのほとんどをライブラリに移し、すべての実行ファイルと結合テストがインポートできるようにしました。結合テストはそれぞれ独自の環境で実行されるため、ハードウェアとの相互作用や、パニックするべきテストを作るといったことが可能になります。 + +QEMU内で現実に近い環境で実行できるテストフレームワークを手に入れました。今後の記事でより多くのテストを作っていくことで、カーネルがより複雑になってもメンテナンスし続けられるでしょう。 + +## 次は? + +次の記事では、**CPU例外**を見ていきます。この例外というのは、CPUによってなにか「不法行為」――例えば、ゼロ除算やマップされていないメモリページへのアクセス(いわゆる「ページフォルト」)――が行われたときに投げられます。これらの例外を捕捉してテストできるようにしておくことは、将来エラーをデバッグするときに非常に重要です。例外の処理はまた、キーボードをサポートするのに必要になる、ハードウェア割り込みの処理に非常に似てもいます。 diff --git a/blog/content/edition-2/posts/04-testing/index.md b/blog/content/edition-2/posts/04-testing/index.md index 15cb1e5b..3b0e501c 100644 --- a/blog/content/edition-2/posts/04-testing/index.md +++ b/blog/content/edition-2/posts/04-testing/index.md @@ -34,7 +34,7 @@ This post replaces the (now deprecated) [_Unit Testing_] and [_Integration Tests Rust has a [built-in test framework] that is capable of running unit tests without the need to set anything up. Just create a function that checks some results through assertions and add the `#[test]` attribute to the function header. Then `cargo test` will automatically find and execute all test functions of your crate. -[built-in test framework]: https://doc.rust-lang.org/book/second-edition/ch11-00-testing.html +[built-in test framework]: https://doc.rust-lang.org/book/ch11-00-testing.html Unfortunately it's a bit more complicated for `no_std` applications such as our kernel. The problem is that Rust's test framework implicitly uses the built-in [`test`] library, which depends on the standard library. This means that we can't use the default test framework for our `#[no_std]` kernel. @@ -174,18 +174,18 @@ The functionality of the `isa-debug-exit` device is very simple. When a `value` Instead of manually invoking the `in` and `out` assembly instructions, we use the abstractions provided by the [`x86_64`] crate. To add a dependency on that crate, we add it to the `dependencies` section in our `Cargo.toml`: -[`x86_64`]: https://docs.rs/x86_64/0.12.1/x86_64/ +[`x86_64`]: https://docs.rs/x86_64/0.13.2/x86_64/ ```toml # in Cargo.toml [dependencies] -x86_64 = "0.12.1" +x86_64 = "0.13.2" ``` Now we can use the [`Port`] type provided by the crate to create an `exit_qemu` function: -[`Port`]: https://docs.rs/x86_64/0.12.1/x86_64/instructions/port/struct.Port.html +[`Port`]: https://docs.rs/x86_64/0.13.2/x86_64/instructions/port/struct.Port.html ```rust // in src/main.rs @@ -836,7 +836,7 @@ At this point, `cargo run` and `cargo test` should work again. Of course, `cargo ### Completing the Integration Test -Like our `src/main.rs`, our `tests/basic_boot.rs` executable can import types from our new library. This allows us to import the missing components to complete our test. +Like our `src/main.rs`, our `tests/basic_boot.rs` executable can import types from our new library. This allows us to import the missing components to complete our test: ```rust // in tests/basic_boot.rs @@ -849,7 +849,7 @@ fn panic(info: &PanicInfo) -> ! { } ``` -Instead of reimplementing the test runner, we use the `test_runner` function from our library. For our `panic` handler, we call the `blog_os::test_panic_handler` function like we did in our `main.rs`. +Instead of reimplementing the test runner, we use the `test_runner` function from our library by changing the `#![test_runner(crate::test_runner)]` attribute to `#![test_runner(blog_os::test_runner)]`. We then don't need the `test_runner` stub function in `basic_boot.rs` anymore, so we can remove it. For our `panic` handler, we call the `blog_os::test_panic_handler` function like we did in our `main.rs`. Now `cargo test` exits normally again. When you run it, you see that it builds and runs the tests for our `lib.rs`, `main.rs`, and `basic_boot.rs` separately after each other. For the `main.rs` and the `basic_boot` integration test, it reports "Running 0 tests" since these files don't have any functions annotated with `#[test_case]`. diff --git a/blog/content/edition-2/posts/05-cpu-exceptions/index.fa.md b/blog/content/edition-2/posts/05-cpu-exceptions/index.fa.md new file mode 100644 index 00000000..85480d19 --- /dev/null +++ b/blog/content/edition-2/posts/05-cpu-exceptions/index.fa.md @@ -0,0 +1,472 @@ ++++ +title = "استثناهای پردازنده" +weight = 5 +path = "fa/cpu-exceptions" +date = 2018-06-17 + +[extra] +chapter = "Interrupts" +# Please update this when updating the translation +translation_based_on_commit = "a081faf3cced9aeb0521052ba91b74a1c408dcff" +# GitHub usernames of the people that translated this post +translators = ["hamidrezakp", "MHBahrampour"] +rtl = true ++++ + +استثناهای پردازنده در موقعیت های مختلف دارای خطا رخ می دهد ، به عنوان مثال هنگام دسترسی به آدرس حافظه نامعتبر یا تقسیم بر صفر. برای واکنش به آنها ، باید یک _جدول توصیف کننده وقفه_ تنظیم کنیم که توابع کنترل کننده را فراهم کند. در انتهای این پست ، هسته ما قادر به گرفتن [استثناهای breakpoint] و ادامه اجرای طبیعی پس از آن خواهد بود. + +[استثناهای breakpoint]: https://wiki.osdev.org/Exceptions#Breakpoint + + + +این بلاگ بصورت آزاد روی [گیت‌هاب] توسعه داده شده است. اگر مشکل یا سوالی دارید، لطفاً آن‌جا یک ایشو باز کنید. همچنین می‌توانید [در زیر] این پست کامنت بگذارید. سورس کد کامل این پست را می‌توانید در بِرَنچ [`post-05`][post branch] پیدا کنید. + +[گیت‌هاب]: https://github.com/phil-opp/blog_os +[در زیر]: #comments +[post branch]: https://github.com/phil-opp/blog_os/tree/post-05 + + + +## بررسی اجمالی +یک استثنا نشان می دهد که مشکلی در دستورالعمل فعلی وجود دارد. به عنوان مثال ، اگر دستورالعمل فعلی بخواهد تقسیم بر 0 کند ، پردازنده یک استثنا صادر می کند. وقتی یک استثنا اتفاق می افتد ، پردازنده کار فعلی خود را رها کرده و بسته به نوع استثنا ، بلافاصله یک تابع خاص کنترل کننده استثنا را فراخوانی می کند. + +در x86 حدود 20 نوع مختلف استثنا پردازنده وجود دارد. مهمترین آنها در زیر آمده اند: + +- **خطای صفحه**: خطای صفحه در دسترسی غیرقانونی به حافظه رخ می دهد. به عنوان مثال ، اگر دستورالعمل فعلی بخواهد از یک صفحه نگاشت نشده بخواند یا بخواهد در یک صفحه فقط خواندنی بنویسد. +- **کد نامعتبر**: این استثنا وقتی رخ می دهد که دستورالعمل فعلی نامعتبر است ، به عنوان مثال وقتی می خواهیم از [دستورالعمل های SSE] جدیدتر بر روی یک پردازنده قدیمی استفاده کنیم که آنها را پشتیبانی نمی کند. +- **خطای محافظت عمومی**: این استثنا دارای بیشترین دامنه علل است. این مورد در انواع مختلف نقض دسترسی مانند تلاش برای اجرای یک دستورالعمل ممتاز در کد سطح کاربر یا نوشتن فیلدهای رزرو شده در ثبات های پیکربندی رخ می دهد. +- **خطای دوگانه**: هنگامی که یک استثنا رخ می دهد ، پردازنده سعی می کند تابع کنترل کننده مربوطه را اجرا کند. اگر یک استثنا دیگر رخ دهد _هنگام فراخوانی تابع کنترل کننده استثنا_ ، پردازنده یک استثنای خطای دوگانه ایجاد می کند. این استثنا همچنین زمانی اتفاق می افتد که هیچ تابع کنترل کننده ای برای یک استثنا ثبت نشده باشد. +- **خطای سه‌گانه**: اگر در حالی که پردازنده سعی می کند تابع کنترل کننده خطای دوگانه را فراخوانی کند استثنایی رخ دهد ، این یک خطای سه‌گانه است. ما نمی توانیم یک خطای سه گانه را بگیریم یا آن را کنترل کنیم. بیشتر پردازنده ها ریست کردن خود و راه اندازی مجدد سیستم عامل واکنش نشان می دهند. + +[دستورالعمل های SSE]: https://en.wikipedia.org/wiki/Streaming_SIMD_Extensions + +برای مشاهده لیست کامل استثنا‌ها ، [ویکی OSDev][exceptions] را بررسی کنید. + +[exceptions]: https://wiki.osdev.org/Exceptions + +### جدول توصیف کننده وقفه +برای گرفتن و رسیدگی به استثنا‌ها ، باید اصطلاحاً _جدول توصیفگر وقفه_ (IDT) را تنظیم کنیم. در این جدول می توانیم برای هر استثنا پردازنده یک عملکرد تابع کننده مشخص کنیم. سخت افزار به طور مستقیم از این جدول استفاده می کند ، بنابراین باید از یک قالب از پیش تعریف شده پیروی کنیم. هر ورودی جدول باید ساختار 16 بایتی زیر را داشته باشد: + +Type| Name | Description +----|--------------------------|----------------------------------- +u16 | Function Pointer [0:15] | The lower bits of the pointer to the handler function. +u16 | GDT selector | Selector of a code segment in the [global descriptor table]. +u16 | Options | (see below) +u16 | Function Pointer [16:31] | The middle bits of the pointer to the handler function. +u32 | Function Pointer [32:63] | The remaining bits of the pointer to the handler function. +u32 | Reserved | + +[global descriptor table]: https://en.wikipedia.org/wiki/Global_Descriptor_Table + +قسمت گزینه ها (Options) دارای قالب زیر است: + +Bits | Name | Description +------|-----------------------------------|----------------------------------- +0-2 | Interrupt Stack Table Index | 0: Don't switch stacks, 1-7: Switch to the n-th stack in the Interrupt Stack Table when this handler is called. +3-7 | Reserved | +8 | 0: Interrupt Gate, 1: Trap Gate | If this bit is 0, interrupts are disabled when this handler is called. +9-11 | must be one | +12 | must be zero | +13‑14 | Descriptor Privilege Level (DPL) | The minimal privilege level required for calling this handler. +15 | Present | + +هر استثنا دارای یک اندیس از پیش تعریف شده در IDT است. به عنوان مثال استثنا کد نامعتبر دارای اندیس 6 و استثنا خطای صفحه دارای اندیس 14 است. بنابراین ، سخت افزار می تواند به طور خودکار عنصر مربوطه را برای هر استثنا بارگذاری کند. [جدول استثناها][exceptions] در ویکی OSDev ، اندیس های IDT کلیه استثناها را در ستون “Vector nr.” نشان داده است. + +هنگامی که یک استثنا رخ می دهد ، پردازنده تقریباً موارد زیر را انجام می دهد: + +1. برخی از ثبات‌ها را به پشته وارد می‌کند، از جمله اشاره گر دستورالعمل و ثبات [RFLAGS]. (بعداً در این پست از این مقادیر استفاده خواهیم کرد.) +2. عنصر مربوط به آن (استثنا) را از جدول توصیف کننده وقفه (IDT) می‌خواند. به عنوان مثال ، پردازنده هنگام رخ دادن خطای صفحه ، عنصر چهاردهم را می خواند. +3. وجود عنصر را بررسی می‌کند. اگر اینگونه نباشد یک خطای دوگانه ایجاد می‌کند. +4. اگر عنصر یک گیت وقفه است (بیت 40 تنظیم نشده است) وقفه های سخت افزاری را غیرفعال می‌کند. +5. انتخابگر مشخص شده [GDT] را در سگمنت CS بارگذاری می‌کند. +6. به تابع کنترل کننده مشخص شده می‌رود. + +[RFLAGS]: https://en.wikipedia.org/wiki/FLAGS_register +[GDT]: https://en.wikipedia.org/wiki/Global_Descriptor_Table + +در حال حاضر نگران مراحل 4 و 5 نباشید ، ما در مورد جدول توصیف کننده گلوبال و وقفه های سخت افزاری در پست های بعدی خواهیم آموخت. + +## یک نوع IDT + +به جای ایجاد نوع IDT خود ، از [ساختمان `InterruptDescriptorTable`] کرت `x86_64` استفاده خواهیم کرد که به این شکل است: + +[ساختمان `InterruptDescriptorTable`]: https://docs.rs/x86_64/0.13.2/x86_64/structures/idt/struct.InterruptDescriptorTable.html + +``` rust +#[repr(C)] +pub struct InterruptDescriptorTable { + pub divide_by_zero: Entry, + pub debug: Entry, + pub non_maskable_interrupt: Entry, + pub breakpoint: Entry, + pub overflow: Entry, + pub bound_range_exceeded: Entry, + pub invalid_opcode: Entry, + pub device_not_available: Entry, + pub double_fault: Entry, + pub invalid_tss: Entry, + pub segment_not_present: Entry, + pub stack_segment_fault: Entry, + pub general_protection_fault: Entry, + pub page_fault: Entry, + pub x87_floating_point: Entry, + pub alignment_check: Entry, + pub machine_check: Entry, + pub simd_floating_point: Entry, + pub virtualization: Entry, + pub security_exception: Entry, + // some fields omitted +} +``` + +فیلدها از نوع [` src/main.rs:53:1 + | +53 | / extern "x86-interrupt" fn breakpoint_handler(stack_frame: &mut InterruptStackFrame) { +54 | | println!("EXCEPTION: BREAKPOINT\n{:#?}", stack_frame); +55 | | } + | |_^ + | + = help: add #![feature(abi_x86_interrupt)] to the crate attributes to enable +``` + +این خطا به این دلیل رخ می دهد که قرارداد فراخوانی `x86-interrupt` هنوز ناپایدار است. به هر حال برای استفاده از آن ، باید صریحاً آن را با اضافه کردن `#![feature(abi_x86_interrupt)]` در بالای `lib.rs` فعال کنیم. + +### بارگیری IDT +برای اینکه پردازنده از جدول توصیف کننده وقفه جدید ما استفاده کند ، باید آن را با استفاده از دستورالعمل [`lidt`] بارگیری کنیم. ساختمان `InterruptDescriptorTable` از کرت ` x86_64` متد [`load`][InterruptDescriptorTable::load] را برای این کار فراهم می کند. بیایید سعی کنیم از آن استفاده کنیم: + +[`lidt`]: https://www.felixcloutier.com/x86/lgdt:lidt +[InterruptDescriptorTable::load]: https://docs.rs/x86_64/0.13.2/x86_64/structures/idt/struct.InterruptDescriptorTable.html#method.load + +```rust +// in src/interrupts.rs + +pub fn init_idt() { + let mut idt = InterruptDescriptorTable::new(); + idt.breakpoint.set_handler_fn(breakpoint_handler); + idt.load(); +} +``` + +اکنون هنگامی که می خواهیم آن را کامپایل کنیم ، خطای زیر رخ می دهد: + +``` +error: `idt` does not live long enough + --> src/interrupts/mod.rs:43:5 + | +43 | idt.load(); + | ^^^ does not live long enough +44 | } + | - borrowed value only lives until here + | + = note: borrowed value must be valid for the static lifetime... +``` + +پس متد `load` انتظار دریافت یک `static self'&` را دارد، این مرجعی است که برای تمام مدت زمان اجرای برنامه معتبر است. دلیل این امر این است که پردازنده در هر وقفه به این جدول دسترسی پیدا می کند تا زمانی که IDT دیگری بارگیری کنیم. بنابراین استفاده از طول عمر کوتاه تر از `static'` می تواند منجر به باگ های استفاده-بعد-از-آزادسازی شود. + +در واقع ، این دقیقاً همان چیزی است که در اینجا اتفاق می افتد. `idt` ما روی پشته ایجاد می شود ، بنابراین فقط در داخل تابع `init` معتبر است. پس از آن حافظه پشته برای توابع دیگر مورد استفاده مجدد قرار می گیرد ، بنابراین پردازنده حافظه پشته تصادفی را به عنوان IDT تفسیر می کند. خوشبختانه ، متد `InterruptDescriptorTable::load` این نیاز به طول عمر را در تعریف تابع خود اجباری می کند، بنابراین کامپایلر راست قادر است از این مشکل احتمالی در زمان کامپایل جلوگیری کند. + +برای رفع این مشکل، باید `idt` را در مکانی ذخیره کنیم که طول عمر `static'` داشته باشد. برای رسیدن به این هدف می توانیم IDT را با استفاده از [`Box`] بر روی حافظه Heap ایجاد کنیم و سپس آن را به یک مرجع `static'` تبدیل کنیم، اما ما در حال نوشتن هسته سیستم عامل هستیم و بنابراین هنوز Heap نداریم. + +[`Box`]: https://doc.rust-lang.org/std/boxed/struct.Box.html + + +به عنوان یک گزینه دیگر، می توانیم IDT را به صورت `static` ذخیره کنیم: +```rust +static IDT: InterruptDescriptorTable = InterruptDescriptorTable::new(); + +pub fn init_idt() { + IDT.breakpoint.set_handler_fn(breakpoint_handler); + IDT.load(); +} +``` + +با این وجود، یک مشکل وجود دارد: استاتیک‌ها تغییرناپذیر هستند، پس نمی توانیم ورودی بریک‌پوینت را از تابع `init` تغییر دهیم. می توانیم این مشکل را با استفاده از [`static mut`] حل کنیم: + +[`static mut`]: https://doc.rust-lang.org/1.30.0/book/second-edition/ch19-01-unsafe-rust.html#accessing-or-modifying-a-mutable-static-variable + +```rust +static mut IDT: InterruptDescriptorTable = InterruptDescriptorTable::new(); + +pub fn init_idt() { + unsafe { + IDT.breakpoint.set_handler_fn(breakpoint_handler); + IDT.load(); + } +} +``` + +در این روش بدون خطا کامپایل می شود اما مشکلات دیگری به همراه دارد. `static mut` بسیار مستعد Data Race هستند، بنابراین در هر دسترسی به یک [بلوک `unsafe`] نیاز داریم. + +[بلوک `unsafe`]: https://doc.rust-lang.org/1.30.0/book/second-edition/ch19-01-unsafe-rust.html#unsafe-superpowers + +#### Lazy Statics به نجات ما می‌آیند +خوشبختانه ماکرو `lazy_static` وجود دارد. ماکرو به جای ارزیابی یک `static` در زمان کامپایل ، مقداردهی اولیه آن را هنگام اولین ارجاع به آن انجام می دهد. بنابراین، می توانیم تقریباً همه کاری را در بلوک مقداردهی اولیه انجام دهیم و حتی قادر به خواندن مقادیر زمان اجرا هستیم. + +ما قبلاً کرت `lazy_static` را وارد کردیم وقتی [یک انتزاع برای بافر متن VGA ایجاد کردیم][vga text buffer lazy static]. بنابراین می توانیم مستقیماً از ماکرو `!lazy_static` برای ایجاد IDT استاتیک استفاده کنیم: + +[vga text buffer lazy static]: @/edition-2/posts/03-vga-text-buffer/index.md#lazy-statics + +```rust +// in src/interrupts.rs + +use lazy_static::lazy_static; + +lazy_static! { + static ref IDT: InterruptDescriptorTable = { + let mut idt = InterruptDescriptorTable::new(); + idt.breakpoint.set_handler_fn(breakpoint_handler); + idt + }; +} + +pub fn init_idt() { + IDT.load(); +} +``` + +توجه داشته باشید که چگونه این راه حل به هیچ بلوک `unsafe` نیاز ندارد. ماکرو `!lazy_static` از `unsafe` در پشت صحنه استفاده می کند ، اما در یک رابط امن به ما داده می شود. + +### اجرای آن + +آخرین مرحله برای کارکرد استثناها در هسته ما فراخوانی تابع `init_idt` از `main.rs` است. به جای فراخوانی مستقیم آن، یک تابع عمومی `init` را در `lib.rs` معرفی می کنیم: + +```rust +// in src/lib.rs + +pub fn init() { + interrupts::init_idt(); +} +``` + +با استفاده از این تابع اکنون یک مکان اصلی برای روالهای اولیه داریم که می تواند بین توابع مختلف `start_` در `main.rs` ، `lib.rs` و تست‌های یک‌پارچه به اشتراک گذاشته شود. + +اکنون می توانیم تابع `start_` در `main.rs` را به روز کنیم تا `init` را فراخوانی کرده و سپس یک استثنا بریک‌پوینت ایجاد کند: + +```rust +// in src/main.rs + +#[no_mangle] +pub extern "C" fn _start() -> ! { + println!("Hello World{}", "!"); + + blog_os::init(); // new + + // invoke a breakpoint exception + x86_64::instructions::interrupts::int3(); // new + + // as before + #[cfg(test)] + test_main(); + + println!("It did not crash!"); + loop {} +} +``` + +اکنون هنگامی که آن را در QEMU اجرا می کنیم (با استفاده از `cargo run`) ، موارد زیر را مشاهده می کنیم: + +![QEMU printing `EXCEPTION: BREAKPOINT` and the interrupt stack frame](qemu-breakpoint-exception.png) + +کار می کند! پردازنده با موفقیت تابع کنترل کننده بریک‌پوینت ما را فراخوانی می کند ، که پیام را چاپ می کند و سپس به تابع `start_` برمی گردد ، جایی که پیام `!It did not crash` چاپ شده است. + +می بینیم که قاب پشته وقفه، دستورالعمل و نشانگرهای پشته را در زمان وقوع استثنا به ما می گوید. این اطلاعات هنگام رفع اشکال استثناهای غیر منتظره بسیار مفید است. + +### افزودن یک تست + +بیایید یک تست ایجاد کنیم که از ادامه کار کد بالا اطمینان حاصل کند. ابتدا تابع `start_` را به روز می کنیم تا `init` را نیز فراخوانی کند: + +```rust +// in src/lib.rs + +/// Entry point for `cargo test` +#[cfg(test)] +#[no_mangle] +pub extern "C" fn _start() -> ! { + init(); // new + test_main(); + loop {} +} +``` + +بخاطر داشته باشید، این تابع `start_` هنگام اجرای`cargo test --lib` استفاده می شود، زیرا راست `lib.rs` را کاملاً مستقل از`main.rs` تست می‌کند. قبل از اجرای تست‌ها باید برای راه اندازی IDT در اینجا `init` فراخوانی شود. + +اکنون می توانیم یک تست `test_breakpoint_exception` ایجاد کنیم: + +```rust +// in src/interrupts.rs + +#[test_case] +fn test_breakpoint_exception() { + // invoke a breakpoint exception + x86_64::instructions::interrupts::int3(); +} +``` + +این تست تابع `int3` را فراخوانی می کند تا یک استثنا بریک‌پوینت ایجاد کند. با بررسی اینکه اجرا پس از آن ادامه دارد ، تأیید می کنیم که کنترل کننده بریک‌پوینت ما به درستی کار می کند. + +شما می توانید این تست جدید را با اجرای `cargo test` (همه تست‌ها) یا` cargo test --lib` (فقط تست های `lib.rs` و ماژول های آن) امتحان کنید. باید موارد زیر را در خروجی مشاهده کنید: + +``` +blog_os::interrupts::test_breakpoint_exception... [ok] +``` + +## خیلی جادویی بود؟ +قرارداد فراخوانی `x86-interrupt` و نوع [`InterruptDescriptorTable`] روند مدیریت استثناها را نسبتاً سر راست و بدون درد ساخته‌اند. اگر این برای شما بسیار جادویی بود و دوست دارید تمام جزئیات مهم مدیریت استثنا را بیاموزید، برای شما هم مطالبی داریم: مجموعه ["مدیریت استثناها با توابع برهنه"] ما، نحوه مدیریت استثنا‌ها بدون قرارداد فراخوانی`x86-interrupt` را نشان می دهد و همچنین نوع IDT خاص خود را ایجاد می کند. از نظر تاریخی، این پست‌ها مهمترین پست‌های مدیریت استثناها قبل از وجود قرارداد فراخوانی `x86-interrupt` و کرت `x86_64` بودند. توجه داشته باشید که این پست‌ها بر اساس [نسخه اول] این وبلاگ هستند و ممکن است قدیمی باشند. + +["مدیریت استثناها با توابع برهنه"]: @/edition-1/extra/naked-exceptions/_index.md +[`InterruptDescriptorTable`]: https://docs.rs/x86_64/0.13.2/x86_64/structures/idt/struct.InterruptDescriptorTable.html +[نسخه اول]: @/edition-1/_index.md + +## مرحله بعدی چیست؟ +ما اولین استثنای خود را با موفقیت گرفتیم و از آن بازگشتیم! گام بعدی اطمینان از این است که همه استثناها را می گیریم ، زیرا یک استثنا گرفته نشده باعث [خطای سه‌گانه] می شود که منجر به شروع مجدد سیستم می شود. پست بعدی توضیح می دهد که چگونه می توان با گرفتن صحیح [خطای دوگانه] از این امر جلوگیری کرد. + +[خطای سه‌گانه]: https://wiki.osdev.org/Triple_Fault +[خطای دوگانه]: https://wiki.osdev.org/Double_Fault#Double_Fault diff --git a/blog/content/edition-2/posts/05-cpu-exceptions/index.md b/blog/content/edition-2/posts/05-cpu-exceptions/index.md index daf46fa0..5d68b59c 100644 --- a/blog/content/edition-2/posts/05-cpu-exceptions/index.md +++ b/blog/content/edition-2/posts/05-cpu-exceptions/index.md @@ -84,7 +84,7 @@ Don't worry about steps 4 and 5 for now, we will learn about the global descript ## An IDT Type Instead of creating our own IDT type, we will use the [`InterruptDescriptorTable` struct] of the `x86_64` crate, which looks like this: -[`InterruptDescriptorTable` struct]: https://docs.rs/x86_64/0.12.1/x86_64/structures/idt/struct.InterruptDescriptorTable.html +[`InterruptDescriptorTable` struct]: https://docs.rs/x86_64/0.13.2/x86_64/structures/idt/struct.InterruptDescriptorTable.html ``` rust #[repr(C)] @@ -115,10 +115,10 @@ pub struct InterruptDescriptorTable { The fields have the type [`idt::Entry`], which is a struct that represents the fields of an IDT entry (see the table above). The type parameter `F` defines the expected handler function type. We see that some entries require a [`HandlerFunc`] and some entries require a [`HandlerFuncWithErrCode`]. The page fault even has its own special type: [`PageFaultHandlerFunc`]. -[`idt::Entry`]: https://docs.rs/x86_64/0.12.1/x86_64/structures/idt/struct.Entry.html -[`HandlerFunc`]: https://docs.rs/x86_64/0.12.1/x86_64/structures/idt/type.HandlerFunc.html -[`HandlerFuncWithErrCode`]: https://docs.rs/x86_64/0.12.1/x86_64/structures/idt/type.HandlerFuncWithErrCode.html -[`PageFaultHandlerFunc`]: https://docs.rs/x86_64/0.12.1/x86_64/structures/idt/type.PageFaultHandlerFunc.html +[`idt::Entry`]: https://docs.rs/x86_64/0.13.2/x86_64/structures/idt/struct.Entry.html +[`HandlerFunc`]: https://docs.rs/x86_64/0.13.2/x86_64/structures/idt/type.HandlerFunc.html +[`HandlerFuncWithErrCode`]: https://docs.rs/x86_64/0.13.2/x86_64/structures/idt/type.HandlerFuncWithErrCode.html +[`PageFaultHandlerFunc`]: https://docs.rs/x86_64/0.13.2/x86_64/structures/idt/type.PageFaultHandlerFunc.html Let's look at the `HandlerFunc` type first: @@ -195,7 +195,7 @@ So the _interrupt stack frame_ looks like this: In the `x86_64` crate, the interrupt stack frame is represented by the [`InterruptStackFrame`] struct. It is passed to interrupt handlers as `&mut` and can be used to retrieve additional information about the exception's cause. The struct contains no error code field, since only some few exceptions push an error code. These exceptions use the separate [`HandlerFuncWithErrCode`] function type, which has an additional `error_code` argument. -[`InterruptStackFrame`]: https://docs.rs/x86_64/0.12.1/x86_64/structures/idt/struct.InterruptStackFrame.html +[`InterruptStackFrame`]: https://docs.rs/x86_64/0.13.2/x86_64/structures/idt/struct.InterruptStackFrame.html ### Behind the Scenes The `x86-interrupt` calling convention is a powerful abstraction that hides almost all of the messy details of the exception handling process. However, sometimes it's useful to know what's happening behind the curtain. Here is a short overview of the things that the `x86-interrupt` calling convention takes care of: @@ -277,7 +277,7 @@ This error occurs because the `x86-interrupt` calling convention is still unstab In order that the CPU uses our new interrupt descriptor table, we need to load it using the [`lidt`] instruction. The `InterruptDescriptorTable` struct of the `x86_64` provides a [`load`][InterruptDescriptorTable::load] method function for that. Let's try to use it: [`lidt`]: https://www.felixcloutier.com/x86/lgdt:lidt -[InterruptDescriptorTable::load]: https://docs.rs/x86_64/0.12.1/x86_64/structures/idt/struct.InterruptDescriptorTable.html#method.load +[InterruptDescriptorTable::load]: https://docs.rs/x86_64/0.13.2/x86_64/structures/idt/struct.InterruptDescriptorTable.html#method.load ```rust // in src/interrupts.rs @@ -457,7 +457,7 @@ blog_os::interrupts::test_breakpoint_exception... [ok] The `x86-interrupt` calling convention and the [`InterruptDescriptorTable`] type made the exception handling process relatively straightforward and painless. If this was too much magic for you and you like to learn all the gory details of exception handling, we got you covered: Our [“Handling Exceptions with Naked Functions”] series shows how to handle exceptions without the `x86-interrupt` calling convention and also creates its own IDT type. Historically, these posts were the main exception handling posts before the `x86-interrupt` calling convention and the `x86_64` crate existed. Note that these posts are based on the [first edition] of this blog and might be out of date. [“Handling Exceptions with Naked Functions”]: @/edition-1/extra/naked-exceptions/_index.md -[`InterruptDescriptorTable`]: https://docs.rs/x86_64/0.12.1/x86_64/structures/idt/struct.InterruptDescriptorTable.html +[`InterruptDescriptorTable`]: https://docs.rs/x86_64/0.13.2/x86_64/structures/idt/struct.InterruptDescriptorTable.html [first edition]: @/edition-1/_index.md ## What's next? diff --git a/blog/content/edition-2/posts/06-double-faults/index.fa.md b/blog/content/edition-2/posts/06-double-faults/index.fa.md new file mode 100644 index 00000000..4ef866cd --- /dev/null +++ b/blog/content/edition-2/posts/06-double-faults/index.fa.md @@ -0,0 +1,569 @@ ++++ +title = "خطاهای دوگانه" +weight = 6 +path = "fa/double-fault-exceptions" +date = 2018-06-18 + +[extra] +chapter = "Interrupts" +# Please update this when updating the translation +translation_based_on_commit = "3ac829171218156c07ce9a27186fee58e3a5521e" +# GitHub usernames of the people that translated this post +translators = ["hamidrezakp", "MHBahrampour"] +rtl = true ++++ + +این پست به طور دقیق جزئیات استثنای خطای دوگانه (ترجمه: double fault exception) را بررسی می‌کند، این استثنا هنگامی رخ می‌دهد که CPU نتواند یک کنترل کننده استثنا را فراخوانی کند. با کنترل این استثنا، از بروز _خطاهای سه گانه_ (ترجمه: triple faults) کشنده که باعث ریست (کلمه: reset) شدن سیستم می‌شوند، جلوگیری می‌کنیم. برای جلوگیری از خطاهای سه گانه در همه موارد، ما همچنین یک _Interrupt Stack Table_ را تنظیم کرده‌ایم تا خطاهای دوگانه را روی یک پشته هسته جداگانه بگیرد. + + + +این بلاگ بصورت آزاد روی [گیت‌هاب] توسعه داده شده است. اگر شما مشکل یا سوالی دارید، لطفاً آن‌جا یک ایشو باز کنید. شما همچنین می‌توانید [در زیر] این پست کامنت بگذارید. منبع کد کامل این پست را می‌توانید در بِرَنچ [`post-06`][post branch] پیدا کنید. + +[گیت‌هاب]: https://github.com/phil-opp/blog_os +[در زیر]: #comments +[post branch]: https://github.com/phil-opp/blog_os/tree/post-06 + + + +## خطای دوگانه چیست؟ + +به عبارت ساده، خطای دوگانه یک استثنای به خصوص است و هنگامی رخ می‌دهد که CPU نتواند یک کنترل کننده استثنا را فراخوانی کند. به عنوان مثال، این اتفاق هنگامی رخ می‌دهد که یک page fault (ترجمه: خطای صفحه) رخ دهد اما هیچ کنترل کننده خطایی در [جدول توصیف کننده وقفه][IDT] (ترجمه: Interrupt Descriptor Table) ثبت نشده باشد. بنابراین به نوعی شبیه بلاک‌های همه گیر در زبان‌های برنامه‌نویسی با استثناها می‌باشد، به عنوان مثال `catch(...)` در ++C یا `catch(Exception e)` در جاوا و #C. + +[IDT]: @/edition-2/posts/05-cpu-exceptions/index.md#the-interrupt-descriptor-table + +خطای دوگانه مانند یک استثنای عادی رفتار می‌کند. دارای شماره وکتور (کلمه: vector) `8` است و ما می‌توانیم یک تابع طبیعی کنترل کننده برای آن در IDT تعریف کنیم. تهیه یک کنترل کننده خطای دوگانه بسیار مهم است، زیرا اگر یک خطای دوگانه کنترل نشود، یک خطای کشنده سه گانه رخ می‌دهد. خطاهای سه گانه قابل کشف نیستند و اکثر سخت افزارها با تنظیم مجدد سیستم واکنش نشان می‌دهند. + +### راه‌اندازی یک خطای دوگانه + +بیایید یک خطای دوگانه را با راه‌اندازی (ترجمه: triggering) یک استثنا برای آن ایجاد کنیم، ما هنوز یک تابع کنترل کننده تعریف نکرده‌ایم: + +```rust +// in src/main.rs + +#[no_mangle] +pub extern "C" fn _start() -> ! { + println!("Hello World{}", "!"); + + blog_os::init(); + + // trigger a page fault + unsafe { + *(0xdeadbeef as *mut u64) = 42; + }; + + // as before + #[cfg(test)] + test_main(); + + println!("It did not crash!"); + loop {} +} +``` + +برای نوشتن در آدرس نامعتبر `0xdeadbeef` از` unsafe` استفاده می‌کنیم. آدرس مجازی در جداول صفحه به آدرس فیزیکی مپ نمی‌شود، بنابراین خطای صفحه رخ می‌دهد. ما یک کنترل کننده خطای صفحه در [IDT] خود ثبت نکرده‌ایم، بنابراین یک خطای دوگانه رخ می‌دهد. + +حال وقتی هسته را اجرا می‌کنیم، می‌بینیم که وارد یک حلقه بوت بی‌پایان می‌شود. دلایل حلقه بوت به شرح زیر است: + +۱. سی‌پی‌یو سعی به نوشتن در `0xdeadbeef` دارد، که باعث خطای صفحه می‌شود. +۲. سی‌پی‌یو به ورودی مربوطه در IDT نگاه می‌کند و می‌بیند که هیچ تابع کنترل کننده‌ای مشخص نشده است. بنابراین، نمی‌تواند کنترل کننده خطای صفحه را فراخوانی کند و یک خطای دوگانه رخ می‌دهد. +۳. سی‌پی‌یو ورودی IDT کنترل کننده خطای دو گانه را بررسی می‌کند، اما این ورودی هم تابع کنترل کننده‌ای را مشخص نمی‌کند. بنابراین، یک خطای _سه‌گانه_ رخ می‌دهد. +۴. خطای سه گانه کشنده است. QEMU مانند اکثر سخت افزارهای واقعی به آن واکنش نشان داده دستور ریست شدن سیستم را صادر می‌کند. + +بنابراین برای جلوگیری از این خطای سه‌گانه، باید یک تابع کنترل کننده برای خطاهای صفحه یا یک کنترل کننده خطای دوگانه ارائه دهیم. ما می‌خواهیم در همه موارد از خطاهای سه گانه جلوگیری کنیم، بنابراین بیایید با یک کنترل کننده خطای دوگانه شروع کنیم که برای همه انواع استثنا بدون کنترل فراخوانی می‌شود. + +## کنترل کننده خطای دوگانه + +خطای دوگانه یک استثنا عادی با کد خطا است، بنابراین می‌توانیم یک تابع کنترل کننده مشابه کنترل کننده نقطه شکست (ترجمه: breakpoint) تعیین کنیم: + +```rust +// in src/interrupts.rs + +lazy_static! { + static ref IDT: InterruptDescriptorTable = { + let mut idt = InterruptDescriptorTable::new(); + idt.breakpoint.set_handler_fn(breakpoint_handler); + idt.double_fault.set_handler_fn(double_fault_handler); // new + idt + }; +} + +// new +extern "x86-interrupt" fn double_fault_handler( + stack_frame: &mut InterruptStackFrame, _error_code: u64) -> ! +{ + panic!("EXCEPTION: DOUBLE FAULT\n{:#?}", stack_frame); +} +``` + +کنترل کننده ما یک پیام خطای کوتاه چاپ می‌کند و قاب پشته استثنا را تخلیه می‌کند. کد خطای کنترل کننده خطای دوگانه همیشه صفر است، بنابراین دلیلی برای چاپ آن وجود ندارد. یک تفاوت در کنترل کننده نقطه شکست این است که کنترل کننده خطای دوگانه [_diverging_] \(ترجمه: واگرا) است. چون معماری `x86_64` امکان بازگشت از یک استثنا خطای دوگانه را ندارد. + +[_diverging_]: https://doc.rust-lang.org/stable/rust-by-example/fn/diverging.html + +حال وقتی هسته را اجرا می‌کنیم، باید ببینیم که کنترل کننده خطای دوگانه فراخوانی می‌شود: + +![QEMU printing `EXCEPTION: DOUBLE FAULT` and the exception stack frame](qemu-catch-double-fault.png) + +کار کرد! آن‌چه این بار اتفاق می‌افتد بصورت زیر است: + +۱. سی‌پی‌یو سعی به نوشتن در `0xdeadbeef` دارد، که باعث خطای صفحه می‌شود. +۲. مانند قبل، سی‌پی‌یو به ورودی مربوطه در IDT نگاه می‌کند و می‌بیند که هیچ تابع کنترل کننده‌ای مشخص نشده است. بنابراین، یک خطای دوگانه رخ می‌دهد. +۳. سی‌پی‌یو به کنترل کننده خطای دوگانه - که اکنون وجود دارد - می‌رود. + +خطای سه گانه (و حلقه بوت) دیگر رخ نمی‌دهد، زیرا اکنون CPU می‌تواند کنترل کننده خطای دوگانه را فراخوانی کند. + +این کاملاً ساده بود! پس چرا ما برای این موضوع به یک پست کامل نیاز داریم؟ خب، ما اکنون قادر به ردیابی _اکثر_ خطاهای دوگانه هستیم، اما مواردی وجود دارد که رویکرد فعلی ما کافی نیست. + +## علل رخ داد خطای دوگانه + +قبل از بررسی موارد خاص، باید علل دقیق خطاهای دوگانه را بدانیم. در بالا، ما از یک تعریف کاملا مبهم استفاده کردیم: + +> خطای دوگانه یک استثنای به خصوص است و هنگامی رخ می‌دهد که CPU نتواند یک کنترل کننده استثنا را فراخوانی کند. + +عبارت _“fails to invoke”_ دقیقا چه معنایی دارد؟ کنترل کننده وجود ندارد؟ کنترل کننده [خارج شده][swapped out] \(منظور این است که آیا صفحه مربوط به کنترل کننده از حافظه خارج شده)؟ و اگر کنترل کننده خودش باعث رخ دادن یک استثناها شود، چه اتفاقی رخ می‌دهد؟ + +[swapped out]: http://pages.cs.wisc.edu/~remzi/OSTEP/vm-beyondphys.pdf + +به عنوان مثال، چه اتفاقی می‌افتد اگر: + +۱. یک استثنای نقطه شکست رخ می‌دهد، آیا تابع کنترل کننده مربوطه خارج شده است؟ +۲. یک خطای صفحه رخ می‌دهد، آیا کنترل کننده خطای صفحه خارج شده است؟ +۳. کنترل کننده‌ی «تقسیم بر صفر» باعث رخ دادن یک استثنای نقطه شکست می‌شود، آیا کنترل کننده نقطه شکست خارج شده است؟ +۴. هسته ما پشته خود را سرریز می‌کند و آیا _صفحه محافظ_ (ترجمه: guard page) ضربه می‌خورد؟ + +خوشبختانه، کتابچه راهنمای AMD64 ([PDF][AMD64 manual]) یک تعریف دقیق دارد (در بخش 8.2.9). مطابق آن، "یک استثنای خطای دوگانه _می‌تواند_ زمانی اتفاق بیفتد که یک استثنا دوم هنگام کار با یک کنترل کننده استثنا قبلی (اول) رخ دهد". _"می تواند"_ مهم است: فقط ترکیبی بسیار خاص از استثناها منجر به خطای دوگانه می‌شود. این ترکیبات عبارتند از: + +استثنای اول | استثنای دوم +----------------|----------------- +[Divide-by-zero],
[Invalid TSS],
[Segment Not Present],
[Stack-Segment Fault],
[General Protection Fault] | [Invalid TSS],
[Segment Not Present],
[Stack-Segment Fault],
[General Protection Fault] +[Page Fault] | [Page Fault],
[Invalid TSS],
[Segment Not Present],
[Stack-Segment Fault],
[General Protection Fault] + +[Divide-by-zero]: https://wiki.osdev.org/Exceptions#Divide-by-zero_Error +[Invalid TSS]: https://wiki.osdev.org/Exceptions#Invalid_TSS +[Segment Not Present]: https://wiki.osdev.org/Exceptions#Segment_Not_Present +[Stack-Segment Fault]: https://wiki.osdev.org/Exceptions#Stack-Segment_Fault +[General Protection Fault]: https://wiki.osdev.org/Exceptions#General_Protection_Fault +[Page Fault]: https://wiki.osdev.org/Exceptions#Page_Fault + + +[AMD64 manual]: https://www.amd.com/system/files/TechDocs/24593.pdf + +بنابراین به عنوان مثال، یک خطای تقسیم بر صفر (ترجمه: Divide-by-zero) و به دنبال آن خطای صفحه (ترجمه: Page Fault)، خوب است (کنترل کننده خطای صفحه فراخوانی می‌شود)، اما خطای تقسیم بر صفر و به دنبال آن یک خطای محافظت عمومی (ترجمه: General Protection) منجر به خطای دوگانه می شود. + +با کمک این جدول می‌توانیم به سه مورد اول از سوال‌های بالا پاسخ دهیم: + +۱. اگر یک استثنای نقطه شکست اتفاق بیفتد و تابع مربوط به کنترل کننده آن خارج شده باشد، یک _خطای صفحه_ رخ می‌دهد و _کنترل کننده خطای صفحه_ فراخوانی می‌شود. +۲. اگر خطای صفحه رخ دهد و کنترل کننده خطای صفحه خارج شده باشد، یک _خطای دوگانه_ رخ می‌دهد و _کنترل کننده خطای دوگانه_ فراخوانی می‌شود. +۳. اگر یک کنترل کننده تقسیم بر صفر باعث استثنای نقطه شکست شود، CPU سعی می‌کند تا کنترل کننده نقطه شکست را فراخوانی کند. اگر کنترل کننده نقطه شکست خارج شده باشد، یک _خطای صفحه_ رخ می‌دهد و _کنترل کننده خطای صفحه_ فراخوانی می‌شود. + +در حقیقت، حتی موارد استثنا بدون تابع کنترل کننده در IDT نیز از این طرح پیروی می‌کند: وقتی استثنا رخ می‌دهد، CPU سعی می‌کند ورودی IDT مربوطه را بخواند. از آن‌جا که ورودی 0 است، که یک ورودی IDT معتبر نیست، یک _خطای محافظت کلی_ رخ می‌دهد. ما یک تابع کنترل کننده برای خطای محافظت عمومی نیز تعریف نکردیم، بنابراین یک خطای محافظت عمومی دیگر رخ می‌دهد. طبق جدول، این منجر به یک خطای دوگانه می‌شود. + +### سرریزِ پشته‌ی هسته + +بیایید به سوال چهارم نگاه کنیم: + +> چه اتفاقی می‌افتد اگر هسته ما پشته خود را سرریز کند و صفحه محافظ ضربه بخورد؟ + +یک صفحه محافظ یک صفحه حافظه ویژه در پایین پشته است که امکان تشخیصِ سرریز پشته را فراهم می‌کند. صفحه به هیچ قاب فیزیکی مپ نشده است، بنابراین دسترسی به آن باعث خطای صفحه می‌شود به جای اینکه بی صدا حافظه دیگر را خراب کند. بوت‌لودر یک صفحه محافظ برای پشته هسته ما تنظیم می‌کند، بنابراین سرریز پشته باعث _خطای صفحه_ می‌شود. + +هنگامی که خطای صفحه رخ می‌دهد، پردازنده به دنبال کنترل کننده خطای صفحه در IDT است و سعی می‌کند تا [قاب پشته وقفه][interrupt stack frame] را به پشته پوش می‌کند. با این حال، اشاره‌گر پشته فعلی هنوز به صفحه محافظی اشاره می‌کند که موجود نیست. بنابراین، خطای صفحه دوم رخ می‌دهد، که باعث خطای دوگانه می‌شود (مطابق جدول فوق). + +[interrupt stack frame]: @/edition-2/posts/05-cpu-exceptions/index.md#the-interrupt-stack-frame + +بنابراین حالا پردازنده سعی می‌کند _کنترل کننده خطای دوگانه_ را فراخوانی کند. با این حال، هنگام رخ دادن خطای دوگانه پردازنده سعی می‌کند تا قاب پشته استثنا را نیز پوش کند. اشاره‌گر پشته هنوز به سمت صفحه محافظ است، بنابراین یک خطای صفحه _سوم_ رخ می‌هد که باعث یک _خطای سه‌گانه_ و راه اندازی مجدد سیستم می‌شود. بنابراین کنترل کننده خطای دوگانه فعلی ما نمی‌تواند از خطای سه‌گانه در این مورد جلوگیری کند. + +بیایید خودمان امتحان کنیم! ما می‌توانیم با فراخوانی تابعی که به طور بی‌وقفه بازگشت می‌یابد، به راحتی سرریز پشته هسته را تحریک کنیم (باعث رخ دادن یک سرریز پشته هسته شویم): + +```rust +// in src/main.rs + +#[no_mangle] // don't mangle the name of this function +pub extern "C" fn _start() -> ! { + println!("Hello World{}", "!"); + + blog_os::init(); + + fn stack_overflow() { + stack_overflow(); // for each recursion, the return address is pushed + } + + // trigger a stack overflow + stack_overflow(); + + […] // test_main(), println(…), and loop {} +} +``` + +وقتی این کد را در QEMU امتحان می‌کنیم، می‌بینیم که سیستم دوباره وارد یک حلقه بوت می‌شود. + +بنابراین چگونه می‌توانیم از بروز این مشکل جلوگیری کنیم؟ ما نمی‌توانیم پوش کردن قاب پشته استثنا را حذف کنیم، زیرا پردازنده خود این کار را انجام می‌دهد. بنابراین ما باید به نحوی اطمینان حاصل کنیم که وقتی یک استثنای خطای دوگانه رخ می‌دهد، پشته همیشه معتبر است. خوشبختانه، معماری x86_64 راه حلی برای این مشکل دارد. + +## تعویض پشته‌ها + +معماری x86_64 قادر است در صورت وقوع یک استثنا به یک پشته از پیش تعریف شده و شناخته شده تعویض شود. این تعویض در سطح سخت افزاری اتفاق می‌افتد، بنابراین می‌توان آن را قبل از اینکه پردازنده قاب پشته استثنا را پوش کند، انجام داد. + +مکانیزم تعویض به عنوان _Interrupt Stack Table_ (IST) پیاده‌سازی می‌شود. IST جدولی است با 7 اشاره‌گر برای دسته های معروف. در شبه‌ کد شبیه Rust: + +```rust +struct InterruptStackTable { + stack_pointers: [Option; 7], +} +``` + +برای هر کنترل کننده استثنا، می‌توانیم یک پشته از IST از طریق فیلد `stack_pointers` مربوط به [IDT entry] انتخاب کنیم. به عنوان مثال، ما می‌توانیم از اولین پشته در IST برای کنترل کننده خطای دوگانه استفاده کنیم. هرگاه خطای دوگانه رخ دهد، پردازنده به طور خودکار به این پشته تغییر می‌کند. این تعویض قبل از پوش کردن هر چیزی اتفاق می‌افتد، بنابراین از خطای سه‌گانه جلوگیری می‌کند. + +[IDT entry]: @/edition-2/posts/05-cpu-exceptions/index.md#the-interrupt-descriptor-table + +### IST و TSS + +جدول پشته وقفه (ترجمه: Interrupt Stack Table: IST) بخشی از یک ساختار قدیمی است که به آن _[سگمنت وضعیت پروسه]_ \(Task State Segment: TSS) گفته می‌شود. TSS برای نگهداری اطلاعات مختلف (به عنوان مثال وضعیت ثبات پردازنده) در مورد یک پروسه در حالت 32 بیتی استفاده می‌شد و به عنوان مثال برای [تعویض سخت‌افزاری context] \(ترجمه: hardware context switching) استفاده می‌شد. با این حال، تعویض سخت‌افزاری context دیگر در حالت 64 بیتی پشتیبانی نمی‌شود و قالب TSS کاملاً تغییر کرده است. + +[سگمنت وضعیت پروسه]: https://en.wikipedia.org/wiki/Task_state_segment +[تعویض سخت‌افزاری context]: https://wiki.osdev.org/Context_Switching#Hardware_Context_Switching + +در x86_64، دیگر TSS هیچ اطلاعات خاصی برای پرسه‌ها ندارد. در عوض، دو جدول پشته را در خود جای داده است (IST یکی از آنهاست). تنها فیلد مشترک بین TSS 32-bit و TSS 64-bit اشاره‌گر به [بیت‌مپ مجوزهای پورت I/O] است. + +[بیت‌مپ مجوزهای پورت I/O]: https://en.wikipedia.org/wiki/Task_state_segment#I.2FO_port_permissions + +فرمت TSS 64-bit مانند زیر است: + +فیلد | نوع +------ | ---------------- +(reserved) | `u32` +Privilege Stack Table | `[u64; 3]` +(reserved) | `u64` +Interrupt Stack Table | `[u64; 7]` +(reserved) | `u64` +(reserved) | `u16` +I/O Map Base Address | `u16` + +وقتی سطح ممتاز تغییر می‌کند، پردازنده از _Privilege Stack Table_ استفاده می‌کند. به عنوان مثال، اگر یک استثنا در حالی که CPU در حالت کاربر است (سطح ممتاز 3) رخ دهد، CPU معمولاً قبل از فراخوانی کنترل کننده استثنا، به حالت هسته تغییر می‌کند (سطح امتیاز 0). در این حالت، CPU به پشته صفرم در جدول پشته ممتاز تغییر وضعیت می دهد (از آنجا که 0، سطح ممتاز هدف است). ما هنوز هیچ برنامه حالت کاربر نداریم، بنابراین اکنون این جدول را نادیده می‌گیریم. + +### ایجاد یک TSS + +بیایید یک TSS جدید ایجاد کنیم که شامل یک پشته خطای دوگانه جداگانه در جدول پشته وقفه خود باشد. برای این منظور ما به یک ساختار TSS نیاز داریم. خوشبختانه کریت `x86_64` از قبل حاوی [ساختار `TaskStateSegment`] است که می‌توانیم از آن استفاده کنیم. + +[ساختار `TaskStateSegment`]: https://docs.rs/x86_64/0.13.2/x86_64/structures/tss/struct.TaskStateSegment.html + +ما TSS را در یک ماژول جدید به نام `gdt` ایجاد می‌کنیم (نام این ماژول بعداً برای‌تان معنا پیدا می‌کند): + +```rust +// in src/lib.rs + +pub mod gdt; + +// in src/gdt.rs + +use x86_64::VirtAddr; +use x86_64::structures::tss::TaskStateSegment; +use lazy_static::lazy_static; + +pub const DOUBLE_FAULT_IST_INDEX: u16 = 0; + +lazy_static! { + static ref TSS: TaskStateSegment = { + let mut tss = TaskStateSegment::new(); + tss.interrupt_stack_table[DOUBLE_FAULT_IST_INDEX as usize] = { + const STACK_SIZE: usize = 4096 * 5; + static mut STACK: [u8; STACK_SIZE] = [0; STACK_SIZE]; + + let stack_start = VirtAddr::from_ptr(unsafe { &STACK }); + let stack_end = stack_start + STACK_SIZE; + stack_end + }; + tss + }; +} +``` + +ما از `lazy_static` استفاده می‌کنیم زیرا ارزیابی کننده ثابت راست هنوز آن‌قدر توانمند نیست که بتواند این مقداردهی اولیه را در زمان کامپایل انجام دهد. ما تعریف می‌کنیم که ورودی صفرم IST پشته خطای دوگانه است (هر اندیس دیگری از IST نیز قابل استفاده است). سپس آدرس بالای یک پشته خطای دوگانه را در ورودی صفرم می‌نویسیم. ما آدرس بالایی را می‌نویسیم زیرا پشته‌های x86 به سمت پایین رشد می‌کنند، یعنی از آدرس‌های بالا به آدرس‌های پایین می‌آیند. + +ما هنوز مدیریت حافظه را پیاده سازی نکرده‌ایم، بنابراین روش مناسبی برای اختصاص پشته جدید نداریم. در عوض، فعلاً از یک آرایه `static mut` به عنوان حافظه پشته استفاده میکنیم. `unsafe` لازم است زیرا هنگام دسترسی به استاتیک‌های تغییرپذیر (ترجمه: mutable)، کامپایلر نمی‌تواند عدم وجود رقابت بین داده ها را تضمین کند. مهم است که یک `static mut` باشد و نه یک استاتیک‌ تغییرناپذیر (ترجمه: immutable)، زیرا در غیر این صورت bootloader آن را به یک صفحه فقط خواندنی نگاشت می‌کند. ما در پست بعدی این را با یک تخصیص پشته مناسب جایگزین خواهیم کرد، سپس `unsafe` دیگر در این‌جا مورد نیاز نخواهد بود. + +توجه داشته باشید که این پشته خطای دوگانه فاقد صفحه محافظ در برابر سرریز پشته است. یعنی ما نباید هیچ کاری که اضافه شدن ایتمی در پشته شود را انجام دهیم زیرا سرریز پشته ممکن است حافظه زیر پشته را خراب کند. + +#### بارگذاری TSS + +اکنون که TSS جدیدی ایجاد کردیم، به روشی نیاز داریم که به CPU بگوییم باید از آن استفاده کند. متأسفانه این کمی دشوار است، زیرا TSS به دلایل تاریخی از سیستم سگمنت‌بندی (ترجمه: segmentation) استفاده می‌کند. به جای بارگذاری مستقیم جدول، باید توصیفگر سگمنت جدیدی را به [جدول توصیف‌گر سراسری] \(Global Descriptor Table: GDT) اضافه کنیم. سپس می‌توانیم TSS خود را با فراخوانی [دستور `ltr`] با اندیس GDT مربوطه بارگذاری کنیم. (دلیل این‌که نام ماژول را `gdt` گذاشتیم نیز همین بود). + +[جدول توصیف‌گر سراسری]: https://web.archive.org/web/20190217233448/https://www.flingos.co.uk/docs/reference/Global-Descriptor-Table/ +[دستور `ltr`]: https://www.felixcloutier.com/x86/ltr + +### جدول توصیف‌گر سراسری + +جدول توصیف‌گر سراسری (GDT) یک یادگاری است که قبل از این‌که صفحه‌بندی به صورت استاندارد تبدیل شود، برای [تقسیم‌بندی حافظه] استفاده می‌شد. این مورد همچنان در حالت 64 بیتی برای موارد مختلف مانند پیکربندی هسته/کاربر یا بارگذاری TSS مورد نیاز است. + +[تقسیم‌بندی حافظه]: https://en.wikipedia.org/wiki/X86_memory_segmentation + +جدول توصیف‌گر سراسری، ساختاری است که شامل _بخشهای_ برنامه است. قبل از اینکه صفحه‌بندی به استاندارد تبدیل شود، از آن در معماری‌های قدیمی استفاده می‌شد تا برنامه ها را از یکدیگر جدا کند. برای کسب اطلاعات بیشتر در مورد سگمنت‌بندی، فصل مربوط به این موضوع در [کتاب “Three Easy Pieces”] را مطالعه کنید. در حالی که سگمنت‌بندی در حالت 64 بیتی دیگر پشتیبانی نمی‌شود، GDT هنوز وجود دارد. بیشتر برای دو چیز استفاده می‌شود: جابجایی بین فضای هسته و فضای کاربر، و بارگذاری ساختار TSS. + +[کتاب “Three Easy Pieces”]: http://pages.cs.wisc.edu/~remzi/OSTEP/ + +#### ایجاد یک GDT + +بیایید یک `GDT` استاتیک ایجاد کنیم که شامل یک بخش برای TSS استاتیک ما باشد: + +```rust +// in src/gdt.rs + +use x86_64::structures::gdt::{GlobalDescriptorTable, Descriptor}; + +lazy_static! { + static ref GDT: GlobalDescriptorTable = { + let mut gdt = GlobalDescriptorTable::new(); + gdt.add_entry(Descriptor::kernel_code_segment()); + gdt.add_entry(Descriptor::tss_segment(&TSS)); + gdt + }; +} +``` + +ما دوباره از `lazy_static` استفاده می‌کنیم، زیرا ارزیابی کننده ثابت راست هنوز آن‌قدر توانمند نیست. ما یک GDT جدید با یک کد سگمنت و یک بخش TSS ایجاد می‌کنیم. + +#### بارگذاری GDT + +برای بارگذاری GDT، یک تابع جدید `gdt::init` ایجاد می‌کنیم که آن را از تابع `init` فراخوانی می‌کنیم: + +```rust +// in src/gdt.rs + +pub fn init() { + GDT.load(); +} + +// in src/lib.rs + +pub fn init() { + gdt::init(); + interrupts::init_idt(); +} +``` + +اکنون GDT ما بارگذاری شده است (از آن‌جا که تابع `start_`، تابع `init` را فراخوانی می‌کند)، اما هنوز حلقه بوت را هنگامِ سرریز پشته مشاهده می‌کنیم. + +### مراحل پایانی + +مشکل این است که سگمنت‌های GDT هنوز فعال نیستند زیرا سگمنت و ثبات‌های TSS هنوز حاوی مقادیر GDT قدیمی هستند. ما همچنین باید ورودی خطای دوگانه IDT را اصلاح کنیم تا از پشته جدید استفاده کند. + +به طور خلاصه، باید موارد زیر را انجام دهیم: + +۱. **بارگذاری مجدد ثبات کد سگمنت**: ما GDT خود را تغییر دادیم، بنابراین باید `cs`، ثبات کد سگمنت را بارگذاری مجدد کنیم. این مورد الزامی است زیرا انتخاب‌گر سگمنت قدیمی می‌تواند اکنون توصیف‌گر دیگری از GDT را نشان دهد (به عنوان مثال توصیف کننده TSS). +۲. **بارگذاری TSS**: ما یک GDT بارگذاری کردیم که شامل یک انتخاب‌گر TSS است، اما هنوز باید به CPU بگوییم که باید از آن TSS استفاده کند. +۳. **بروزرسانی ورودی IDT**: به محض این‌که TSS بارگذاری شد، CPU به یک جدول پشته وقفه معتبر (IST) دسترسی دارد. سپس می‌توانیم به CPU بگوییم که باید با تغییر در ورودی IDT خطای دوگانه از پشته خطای دوگانه جدید استفاده کند. + +برای دو مرحله اول، ما نیاز به دسترسی به متغیرهای` code_selector` و `tss_selector` در تابع `gdt::init` داریم. می‌توانیم با تبدیل آن‌ها به بخشی از استاتیک از طریق ساختار جدید `Selectors` به این هدف برسیم: + +```rust +// in src/gdt.rs + +use x86_64::structures::gdt::SegmentSelector; + +lazy_static! { + static ref GDT: (GlobalDescriptorTable, Selectors) = { + let mut gdt = GlobalDescriptorTable::new(); + let code_selector = gdt.add_entry(Descriptor::kernel_code_segment()); + let tss_selector = gdt.add_entry(Descriptor::tss_segment(&TSS)); + (gdt, Selectors { code_selector, tss_selector }) + }; +} + +struct Selectors { + code_selector: SegmentSelector, + tss_selector: SegmentSelector, +} +``` + +اکنون می‌توانیم با استفاده از انتخاب‌گرها، ثبات بخش `cs` را بارگذاری مجدد کرده و `TSS` را بارگذاری کنیم: + +```rust +// in src/gdt.rs + +pub fn init() { + use x86_64::instructions::segmentation::set_cs; + use x86_64::instructions::tables::load_tss; + + GDT.0.load(); + unsafe { + set_cs(GDT.1.code_selector); + load_tss(GDT.1.tss_selector); + } +} +``` + +ما با استفاده از [`set_cs`] ثبات کد سگمنت را بارگذاری مجدد می‌کنیم و برای بارگذاری TSS با از [`load_tss`] استفاده می‌کنیم. توابع به عنوان `unsafe` علامت گذاری شده‌اند، بنابراین برای فراخوانی آن‌ها به یک بلوک `unsafe` نیاز داریم. چون ممکن است با بارگذاری انتخاب‌گرهای نامعتبر، ایمنی حافظه از بین برود. + +[`set_cs`]: https://docs.rs/x86_64/0.13.2/x86_64/instructions/segmentation/fn.set_cs.html +[`load_tss`]: https://docs.rs/x86_64/0.13.2/x86_64/instructions/tables/fn.load_tss.html + +اکنون که یک TSS معتبر و جدول پشته‌ وقفه را بارگذاری کردیم، می‌توانیم اندیس پشته را برای کنترل کننده خطای دوگانه در IDT تنظیم کنیم: + +```rust +// in src/interrupts.rs + +use crate::gdt; + +lazy_static! { + static ref IDT: InterruptDescriptorTable = { + let mut idt = InterruptDescriptorTable::new(); + idt.breakpoint.set_handler_fn(breakpoint_handler); + unsafe { + idt.double_fault.set_handler_fn(double_fault_handler) + .set_stack_index(gdt::DOUBLE_FAULT_IST_INDEX); // new + } + + idt + }; +} +``` + +روش `set_stack_index` ایمن نیست زیرا فراخوان (ترجمه: caller) باید اطمینان حاصل کند که اندیس استفاده شده معتبر است و قبلاً برای استثنای دیگری استفاده نشده است. + +همین! اکنون CPU باید هر زمان که خطای دوگانه رخ داد، به پشته خطای دوگانه برود. بنابراین، ما می‌توانیم _همه_ خطاهای دوگانه، از جمله سرریزهای پشته هسته را بگیریم: + +![QEMU printing `EXCEPTION: DOUBLE FAULT` and a dump of the exception stack frame](qemu-double-fault-on-stack-overflow.png) + +از این به بعد هرگز نباید شاهد خطای سه‌گانه باشیم! برای اطمینان از اینکه موارد بالا را به طور تصادفی نقض نمی‌کنیم، باید یک تست برای این کار اضافه کنیم. + +## تست سرریز پشته + +برای آزمایش ماژول `gdt` جدید و اطمینان از اینکه مدیر خطای دوگانه به درستی هنگام سرریز پشته فراخوانی شده است، می‌توانیم یک تست یکپارچه اضافه کنیم. ایده این است که یک خطای دوگانه در تابع تست ایجاد کنید و تأیید کنید که مدیر خطای دوگانه فراخوانی می‌شود. + +بیایید با یک طرح مینیمال شروع کنیم: + + +```rust +// in tests/stack_overflow.rs + +#![no_std] +#![no_main] + +use core::panic::PanicInfo; + +#[no_mangle] +pub extern "C" fn _start() -> ! { + unimplemented!(); +} + +#[panic_handler] +fn panic(info: &PanicInfo) -> ! { + blog_os::test_panic_handler(info) +} +``` + +مانند تست `panic_handler`، تست [بدون یک test harness] اجرا خواهد شد. زیرا پس از یک خطای دوگانه نمی‌توانیم اجرا را ادامه دهیم، بنابراین بیش از یک تست منطقی نیست. برای غیرفعال کردن test harness برای این تست، موارد زیر را به `Cargo.toml` اضافه می‌کنیم: + +```toml +# in Cargo.toml + +[[test]] +name = "stack_overflow" +harness = false +``` + +[بدون یک test harness]: @/edition-2/posts/04-testing/index.md#no-harness-tests + +حال باید `cargo test --test stack_overflow` بصورت موفقیت‌آمیز کامپایل شود. البته این تست با شکست مواجه می‌شود، زیرا ماکروی `unimplemented` پنیک می‌کند. + +### پیاده‌سازی `start_` + +پیاده‌سازی تابع `start_` مانند این است: + +```rust +// in tests/stack_overflow.rs + +use blog_os::serial_print; + +#[no_mangle] +pub extern "C" fn _start() -> ! { + serial_print!("stack_overflow::stack_overflow...\t"); + + blog_os::gdt::init(); + init_test_idt(); + + // trigger a stack overflow + stack_overflow(); + + panic!("Execution continued after stack overflow"); +} + +#[allow(unconditional_recursion)] +fn stack_overflow() { + stack_overflow(); // for each recursion, the return address is pushed + volatile::Volatile::new(0).read(); // prevent tail recursion optimizations +} +``` + +برای راه‌اندازی یک GDT جدید، تابع `gdt::init` را فراخوانی می‌کنیم. به جای فراخوانی تابع `interrupts::init_idt`، تابع `init_test_idt` را فراخوانی می‌کنیم که بزودی توضیح داده می‌شود. زیرا ما می‌خواهیم یک مدیر خطای دوگانه سفارشی ثبت کنیم که به جای پنیک کردن، دستور `exit_qemu(QemuExitCode::Success)` را انجام می‌دهد. + +تابع `stack_overflow` تقریباً مشابه تابع موجود در `main.rs` است. تنها تفاوت این است که برای جلوگیری از بهینه‌سازی کامپایلر موسوم به [_tail call elimination_]، در پایان تابع، یک خواندنِ [فرارِ] \(ترجمه: volatile) اضافه به وسیله نوع [`Volatile`] انجام می‌دهیم. از جمله، این بهینه‌سازی به کامپایلر اجازه می‌دهد تابعی را که آخرین عبارت آن فراخوانی تابع بازگشتی است، به یک حلقه طبیعی تبدیل کند. بنابراین، هیچ قاب پشته اضافی برای فراخوانی تابع ایجاد نمی‌شود، پس استفاده از پشته ثابت می‌ماند. + +[volatile]: https://en.wikipedia.org/wiki/Volatile_(computer_programming) +[`Volatile`]: https://docs.rs/volatile/0.2.6/volatile/struct.Volatile.html +[_tail call elimination_]: https://en.wikipedia.org/wiki/Tail_call + +با این حال، در مورد ما، ما می‌خواهیم که سرریز پشته اتفاق بیفتد، بنابراین در انتهای تابع یک دستور خواندن فرار ساختگی اضافه می‌کنیم، که کامپایلر مجاز به حذف آن نیست. بنابراین، تابع دیگر _tail recursive_ نیست و از تبدیل به یک حلقه جلوگیری می‌شود. ما همچنین صفت `allow(unconditional_recursion)` را اضافه می‌کنیم تا اخطار کامپایلر را در مورد تکرار بی‌وقفه تابع خاموش نگه دارد. + +### تست IDT + +همانطور که در بالا ذکر شد، این تست به IDT مخصوص خود با یک مدیر خطای دوگانه سفارشی نیاز دارد. پیاده‌سازی به این شکل است: + +```rust +// in tests/stack_overflow.rs + +use lazy_static::lazy_static; +use x86_64::structures::idt::InterruptDescriptorTable; + +lazy_static! { + static ref TEST_IDT: InterruptDescriptorTable = { + let mut idt = InterruptDescriptorTable::new(); + unsafe { + idt.double_fault + .set_handler_fn(test_double_fault_handler) + .set_stack_index(blog_os::gdt::DOUBLE_FAULT_IST_INDEX); + } + + idt + }; +} + +pub fn init_test_idt() { + TEST_IDT.load(); +} +``` + +پیاده‌سازی بسیار شبیه IDT طبیعی ما در `interrupts.rs` است. مانند IDT عادی، برای مدیر خطای دوگانه به منظور جابجایی به پشته‌ای جداگانه، یک اندیس پشته را در IST تنظیم می‌کنیم. تابع `init_test_idt` با استفاده از روش `load`، آی‌دی‌تی را بر روی پردازنده بارگذاری می‌کند. + +### مدیر خطای دوگانه + +تنها قسمت جامانده، مدیر خطای دوگانه است که به این شکل پیاده‌سازی می‌شود: + +```rust +// in tests/stack_overflow.rs + +use blog_os::{exit_qemu, QemuExitCode, serial_println}; +use x86_64::structures::idt::InterruptStackFrame; + +extern "x86-interrupt" fn test_double_fault_handler( + _stack_frame: &mut InterruptStackFrame, + _error_code: u64, +) -> ! { + serial_println!("[ok]"); + exit_qemu(QemuExitCode::Success); + loop {} +} +``` + +هنگامی که مدیر خطای دوگانه فراخوانی می‌شود، از QEMU با یک کد خروج موفقیت‌آمیز خارج می‌شویم، که تست را بعنوان «قبول شده» علامت‌گذاری می‌داند. از آن‌جا که تست‌های یکپارچه اجرایی‌های کاملاً مجزایی هستند، باید صفت `[feature(abi_x86_interrupt)]!#` را در بالای فایل تست تنظیم کنیم. + +اکنون می‌توانیم تست را از طریق `cargo test --test stack_overflow` (یا `cargo test` برای اجرای همه تست‌ها) انجام دهیم. همانطور که انتظار می‌رفت، خروجی `stack_overflow... [ok ]` را در کنسول مشاهده می‌کنیم. خط `set_stack_index` را کامنت کنید: این امر باعث می‌شود تست از کار بیفتد. + +## خلاصه + +در این پست یاد گرفتیم که خطای دوگانه چیست و در چه شرایطی رخ می‌دهد. ما یک مدیر خطای دوگانه پایه اضافه کردیم که پیام خطا را چاپ می‌کند و یک تست یکپارچه برای آن اضافه کردیم. + +ما همچنین تعویض پشته پشتیبانی شده سخت‌افزاری را در استثناهای خطای دوگانه فعال کردیم تا در سرریز پشته نیز کار کند. در حین پیاده‌سازی آن، ما با سگمنت وضعیت پروسه (TSS)، جدول پشته وقفه (IST) و جدول توصیف کننده سراسری (GDT) آشنا شدیم، که برای سگمنت‌بندی در معماری‌های قدیمی استفاده می‌شد. + +## بعدی چیست؟ + +پست بعدی نحوه مدیریت وقفه‌های دستگاه‌های خارجی مانند تایمر، صفحه کلید یا کنترل کننده‌های شبکه را توضیح می‌دهد. این وقفه‌های سخت‌افزاری بسیار شبیه به استثناها هستند، به عنوان مثال آن‌ها هم از طریق IDT ارسال می‌شوند. با این حال، برخلاف استثناها، مستقیماً روی پردازنده رخ نمی‌دهند. در عوض، یک _interrupt controller_ این وقفه‌ها را جمع کرده و بسته به اولویت، آن‌ها را به CPU می‌فرستد. در بخش بعدی، مدیر وقفه [Intel 8259] \("PIC") را بررسی خواهیم کرد و نحوه پیاده‌سازی پشتیبانی صفحه کلید را یاد خواهیم گرفت. + +[Intel 8259]: https://en.wikipedia.org/wiki/Intel_8259 diff --git a/blog/content/edition-2/posts/06-double-faults/index.ja.md b/blog/content/edition-2/posts/06-double-faults/index.ja.md new file mode 100644 index 00000000..4e78ad82 --- /dev/null +++ b/blog/content/edition-2/posts/06-double-faults/index.ja.md @@ -0,0 +1,554 @@ ++++ +title = "Double Faults" +weight = 6 +path = "ja/double-fault-exceptions" +date = 2018-06-18 + +[extra] +chapter = "Interrupts" +# Please update this when updating the translation +translation_based_on_commit = "27ac0e1acc36f640d7045b427da2ed65b945756b" +# GitHub usernames of the people that translated this post +translators = ["garasubo"] ++++ + +この記事ではCPUが例外ハンドラの呼び出しに失敗したときに起きる、ダブルフォルト例外について詳細に見ていきます。この例外を処理することによって、システムリセットを起こす重大な**トリプルフォルト**を避けることができます。あらゆる場合においてトリプルフォルトを防ぐために、ダブルフォルトを異なるカーネルスタック上でキャッチするための**割り込みスタックテーブル**をセットアップしていきます。 + + + +このブログの内容は [GitHub] 上で公開・開発されています。何か問題や質問などがあれば issue をたててください(訳注: リンクは原文(英語)のものになります)。また[こちら][at the bottom]にコメントを残すこともできます。この記事の完全なソースコードは[`post-06` ブランチ][post branch]にあります。 + +[GitHub]: https://github.com/phil-opp/blog_os +[at the bottom]: #comments +[post branch]: https://github.com/phil-opp/blog_os/tree/post-06 + + + +## ダブルフォルトとは +簡単に言うとダブルフォルトとはCPUが例外ハンドラを呼び出すことに失敗したときに起きる特別な例外です。例えば、ページフォルトが起きたが、ページフォルトハンドラが[割り込みディスクリプタテーブル][IDT](IDT: Interrupt Descriptor Table)(訳注: 翻訳当時、リンク先未訳)に登録されていないときに発生します。つまり、C++での`catch(...)`や、JavaやC#の`catch(Exception e)`ような、例外のあるプログラミング言語のcatch-allブロックのようなものです。 + +[IDT]: @/edition-2/posts/05-cpu-exceptions/index.md#the-interrupt-descriptor-table + +ダブルフォルトは通常の例外のように振る舞います。ベクター番号`8`を持ち、IDTに通常のハンドラ関数として定義できます。ダブルフォルトがうまく処理されないと、より重大な例外である**トリプルフォルト**が起きてしまうため、ダブルフォルトハンドラを設定することはとても重要です。トリプルフォルトはキャッチできず、ほとんどのハードウェアはシステムリセットを起こします。 + +### ダブルフォルトを起こす +ハンドラ関数を定義していない例外を発生させることでダブルフォルトを起こしてみましょう。 + +```rust +// in src/main.rs + +#[no_mangle] +pub extern "C" fn _start() -> ! { + println!("Hello World{}", "!"); + + blog_os::init(); + + // ページフォルトを起こす + unsafe { + *(0xdeadbeef as *mut u64) = 42; + }; + + // 前回同様 + #[cfg(test)] + test_main(); + + println!("It did not crash!"); + loop {} +} +``` + +不正なアドレスである`0xdeadbeef`に書き込みを行うため`unsafe`を使います。この仮想アドレスはページテーブル上で物理アドレスにマップされていないため、ページフォルトが発生します。私達の[IDT]にはページフォルトが登録されていないため、ダブルフォルトが発生します。 + +今、私達のカーネルを起動すると、ブートループが発生します。この理由は以下の通りです: + +1. CPUが`0xdeadbeef`に書き込みを試みページフォルトを起こします。 +2. CPUはIDTに対応するエントリを探しに行き、ハンドラ関数が指定されていないことを発見します。結果、ページフォルトハンドラが呼び出せず、ダブルフォルトが発生します。 +3. CPUはダブルフォルトハンドラのIDTエントリを見にいきますが、このエントリもハンドラ関数を指定していません。結果、**トリプルフォルト**が発生します。 +4. トリプルフォルトは重大なエラーなので、QEMUはほとんどの実際のハードウェアと同様にシステムリセットを行います。 + +このトリプルフォルトを防ぐためには、ページフォルトかダブルフォルトのハンドラ関数を定義しないといけません。私達はすべての場合におけるトリプルフォルトを防ぎたいので、適切に処理できなかったすべての例外において呼び出されることになるダブルフォルトハンドラを定義するところからはじめましょう。 + +## ダブルフォルトハンドラ +ダブルフォルトは通常のエラーコードのある例外なので、ブレークポイントハンドラと同じようにハンドラ関数を指定することができます。 + +```rust +// in src/interrupts.rs + +lazy_static! { + static ref IDT: InterruptDescriptorTable = { + let mut idt = InterruptDescriptorTable::new(); + idt.breakpoint.set_handler_fn(breakpoint_handler); + idt.double_fault.set_handler_fn(double_fault_handler); // new + idt + }; +} + +// new +extern "x86-interrupt" fn double_fault_handler( + stack_frame: &mut InterruptStackFrame, _error_code: u64) -> ! +{ + panic!("EXCEPTION: DOUBLE FAULT\n{:#?}", stack_frame); +} +``` + +私達のハンドラは短いエラーメッセージを出力して、例外スタックフレームをダンプします。ダブルフォルトハンドラのエラーコードは常に`0`なので、出力する必要はないでしょう。ブレークポイントハンドラとの違いの一つは、ダブルフォルトハンドラは[発散する](diverging)(訳注: 翻訳当時、リンク先未訳)ということです。`x86_64`アーキテクチャではダブルフォルト例外から復帰することができないためです。 + +[発散する]: https://doc.rust-jp.rs/rust-by-example-ja/fn/diverging.html + +ここで私達のカーネルを起動すると、ダブルフォルトハンドラが呼び出されていることがわかることでしょう。 + +![QEMU printing `EXCEPTION: DOUBLE FAULT` and the exception stack frame](qemu-catch-double-fault.png) + +動きました!ここで何が起きているかというと、 + +1. CPUが`0xdeadbeef`に書き込みを試みページフォルトを起こします。 +2. 以前と同様に、CPUはIDT中の対応するエントリを見にいきますが、ハンドラ関数が定義されていないことを発見し、結果、ダブルフォルトが起きます。 +3. 今回はダブルフォルトハンドラが指定されているので、CPUはそれを適切に呼び出せます。 + +CPUはダブルフォルトハンドラを呼べるようになったので、トリプルフォルト(とブートループ)はもう起こりません。 + +ここまでは簡単です。ではなぜこの例外のために丸々一つの記事を用意したのでしょうか?実は、私達は**ほとんどの**ダブルフォルトをキャッチすることはできますが、このアプローチでは十分でないケースがいくつか存在するのです。 + +## ダブルフォルトの原因 +特別なケースを見ていく前に、ダブルフォルトの正確な原因を知る必要があります。ここまで、私達はとてもあいまいな定義を使ってきました。 + +> ダブルフォルトとはCPUが例外ハンドラを呼び出すことに失敗したときに起きる特別な例外です。 + +**「呼び出すことに失敗する」** とは正確には何を意味するのでしょうか?ハンドラが存在しない?ハンドラが[スワップアウト]された?また、ハンドラ自身が例外を発生させたらどうなるのでしょうか? + +[スワップアウト]: http://pages.cs.wisc.edu/~remzi/OSTEP/vm-beyondphys.pdf + +例えば以下のようなことが起こるとどうなるでしょう? + +1. ブレークポイント例外が発生したが、対応するハンドラがスワップアウトされていたら? +2. ページフォルトが発生したが、ページフォルトハンドラがスワップアウトされていたら? +3. ゼロ除算ハンドラがブレークポイント例外を起こしたが、ブレークポイントハンドラがスワップアウトされていたら? +4. カーネルがスタックをオーバーフローさせて**ガードページ**にヒットしたら? + +幸いにもAMD64のマニュアル([PDF][AMD64 manual])には正確な定義が書かれています(8.2.9章)。それによると「ダブルフォルト例外は直前の(一度目の)例外ハンドラの処理中に二度目の例外が発生したとき**起きうる** (can occur)」と書かれています。**起きうる**というのが重要で、とても特別な例外の組み合わせでのみダブルフォルトとなります。この組み合わせは以下のようになっています。 + +最初の例外 | 二度目の例外 +----------------|----------------- +[ゼロ除算],
[無効TSS],
[セグメント不在],
[スタックセグメントフォルト],
[一般保護違反] | [無効TSS],
[セグメント不在],
[スタックセグメントフォルト],
[一般保護違反] +[ページフォルト] | [ページフォルト],
[無効TSS],
[セグメント不在],
[スタックセグメントフォルト],
[一般保護違反] + +[ゼロ除算]: https://wiki.osdev.org/Exceptions#Divide-by-zero_Error +[無効TSS]: https://wiki.osdev.org/Exceptions#Invalid_TSS +[セグメント不在]: https://wiki.osdev.org/Exceptions#Segment_Not_Present +[スタックセグメントフォルト]: https://wiki.osdev.org/Exceptions#Stack-Segment_Fault +[一般保護違反]: https://wiki.osdev.org/Exceptions#General_Protection_Fault +[ページフォルト]: https://wiki.osdev.org/Exceptions#Page_Fault + + +[AMD64 manual]: https://www.amd.com/system/files/TechDocs/24593.pdf + +例えばページフォルトに続いてゼロ除算例外が起きた場合は問題ありません(ページフォルトハンドラが呼び出される)が、一般保護違反に続いてゼロ除算例外が起きた場合はダブルフォルトが発生します。 + +この表を見れば、先程の質問のうち最初の3つに答えることができます: + +1. ブレークポイント例外が発生して、対応するハンドラ関数がスワップアウトされている場合、**ページフォルト**が発生して**ページフォルトハンドラ**が呼び出される +2. ページフォルトが発生してページフォルトハンドラがスワップアウトされている場合、**ダブルフォルト**が発生して**ダブルフォルトハンドラ**が呼び出されます。 +3. ゼロ除算ハンドラがブレークポイント例外を発生させた場合、CPUはブレークポイントハンドラを呼び出そうとします。もしブレークポイントハンドラがスワップアウトされている場合、**ページフォルト**が発生して**ページフォルトハンドラ**が呼び出されます。 + +実際、IDTにハンドラ関数が指定されていない例外のケースでもこの体系に従っています。つまり、例外が発生したとき、CPUは対応するIDTエントリを読み込みにいきます。このエントリは0であり正しいIDTエントリではないので、**一般保護違反**が発生します。私達は一般保護違反のハンドラも定義していないので、新たな一般保護違反が発生します。表によるとこれはダブルフォルトを起こします。 + +### カーネルスタックオーバーフロー +4つ目の質問を見てみましょう。 + +> カーネルがスタックをオーバーフローさせてガードページにヒットしたら? + +ガードページはスタックの底にある特別なメモリページで、これによってスタックオーバーフローを検出することができます。このページはどの物理メモリにもマップされていないので、アクセスすることで警告なく他のメモリを破壊する代わりにページフォルトが発生します。ブートローダーはカーネルスタックのためにガードページをセットアップするので、スタックオーバーフローは**ページフォルト**を発生させることになります。 + +ページフォルトが起きるとCPUはIDT内のページフォルトハンドラを探しにいき、[割り込みスタックフレーム](訳注: 翻訳当時、リンク先未訳)をスタック上にプッシュしようと試みます。しかし、このスタックポインタは存在しないガードページを指しています。結果、二度目のページフォルトが発生して、ダブルフォルトが起きます(上の表によれば)。 + +[割り込みスタックフレーム]: @/edition-2/posts/05-cpu-exceptions/index.md#the-interrupt-stack-frame + +そして、ここでCPUは**ダブルフォルトハンドラ**を呼びにいきます。しかし、ダブルフォルト例外においてもCPUは例外スタックフレームをプッシュしようと試みます。スタックポインタはまだガードページを指しているので、**三度目の**ページフォルトが起きて、**トリプルフォルト**を発生させシステムは再起動します。そのため、私達の今のダブルフォルトハンドラではこの場合でのトリプルフォルトを避けることができません。 + +実際にやってみましょう。カーネルスタックオーバーフローは無限に再帰する関数を呼び出すことによって簡単に引き起こせます: + +```rust +// in src/main.rs + +#[no_mangle] // この関数の名前修飾をしない +pub extern "C" fn _start() -> ! { + println!("Hello World{}", "!"); + + blog_os::init(); + + fn stack_overflow() { + stack_overflow(); // 再帰呼び出しのために、リターンアドレスがプッシュされる + } + + // スタックオーバーフローを起こす + stack_overflow(); + + […] // test_main(), println(…), and loop {} +} +``` + +これをQEMUで試すと、再びブートループに入るのがわかります。 + +では、私達はどうすればこの問題を避けられるでしょうか?例外スタックフレームをプッシュすることは、CPU自身が行ってしまうので、省略できません。つまりどうにかしてダブルフォルト例外が発生したときスタックが常に正常であることを保証する必要があります。幸いにもx86_64アーキテクチャにはこの問題の解決策があります。 + +## スタックを切り替える +x86_64アーキテクチャは例外発生時に予め定義されている既知の正常なスタックに切り替えることができます。この切り替えはハードウェアレベルで発生するので、CPUが例外スタックフレームをプッシュする前に行うことができます。 + +切り替えの仕組みは**割り込みスタックテーブル**(IST: Interrupt Stack Table)として実装されています。ISTは7つの既知の正常なポインタのテーブルです。Rust風の疑似コードで表すとこのようになります: + +```rust +struct InterruptStackTable { + stack_pointers: [Option; 7], +} +``` + +各例外ハンドラに対して、私達は対応する[IDTエントリ](訳注: 翻訳当時、リンク先未訳)の`stack_pointers`フィールドを通してISTからスタックを選ぶことができます。例えば、IST中の最初のスタックをダブルフォルトハンドラのために使うことができます。そうすると、CPUがダブルフォルトが発生したときは必ず、このスタックに自動的に切り替えを行います。この切り替えは何かがプッシュされる前に起きるので、トリプルフォルトを防ぐことになります。 + +[IDTエントリ]: @/edition-2/posts/05-cpu-exceptions/index.md#the-interrupt-descriptor-table + +### ISTとTSS +割り込みスタックテーブル(IST)は **[タスクステートセグメント]**(TSS)というレガシーな構造体の一部です。TSSはかつては様々な32ビットモードでのタスクに関する情報(例:プロセッサのレジスタの状態)を保持していて、例えば[ハードウェアコンテキストスイッチング]に使われていました。しかし、ハードウェアコンテキストスイッチングは64ビットモードではサポートされなくなり、TSSのフォーマットは完全に変わりました。 + +[タスクステートセグメント]: https://ja.wikipedia.org/wiki/Task_state_segment +[ハードウェアコンテキストスイッチング]: https://wiki.osdev.org/Context_Switching#Hardware_Context_Switching + +x86_64ではTSSはタスク固有の情報は全く持たなくなりました。代わりに、2つのスタックテーブル(ISTがその1つ)を持つようになりました。唯一32ビットと64ビットのTSSで共通のフィールドは[I/Oポート許可ビットマップ]へのポインタのみです。 + +[I/Oポート許可ビットマップ]: https://ja.wikipedia.org/wiki/Task_state_segment#I/O許可ビットマップ + +64ビットのTSSは下記のようなフォーマットです: + +フィールド | 型 +------ | ---------------- +(予約済み) | `u32` +特権スタックテーブル | `[u64; 3]` +(予約済み) | `u64` +割り込みスタックテーブル | `[u64; 7]` +(予約済み) | `u64` +(予約済み) | `u16` +I/Oマップベースアドレス | `u16` + +**特権スタックテーブル**は特権レベルが変わった際にCPUが使用します。例えば、CPUがユーザーモード(特権レベル3)の時に例外が発生した場合、CPUは通常例外ハンドラを呼び出す前にカーネルモード(特権レベル0)に切り替わります。この場合、CPUは特権レベルスタックテーブルの0番目のスタックに切り替わります。ユーザーモードについてはまだ実装してないため、このテープルはとりあえず無視しておきましょう。 + +### TSSをつくる +割り込みスタックテーブルにダブルフォルト用のスタックを含めた新しいTSSをつくってみましょう。そのためにはTSS構造体が必要です。幸いにも、すでに`x86_64`クレートに[`TaskStateSegment`構造体]が含まれているので、これを使っていきます。 + +[`TaskStateSegment`構造体]: https://docs.rs/x86_64/0.12.1/x86_64/structures/tss/struct.TaskStateSegment.html + +新しい`gdt`モジュール内でTSSをつくります(名前の意味は後でわかるでしょう): + +```rust +// in src/lib.rs + +pub mod gdt; + +// in src/gdt.rs + +use x86_64::VirtAddr; +use x86_64::structures::tss::TaskStateSegment; +use lazy_static::lazy_static; + +pub const DOUBLE_FAULT_IST_INDEX: u16 = 0; + +lazy_static! { + static ref TSS: TaskStateSegment = { + let mut tss = TaskStateSegment::new(); + tss.interrupt_stack_table[DOUBLE_FAULT_IST_INDEX as usize] = { + const STACK_SIZE: usize = 4096 * 5; + static mut STACK: [u8; STACK_SIZE] = [0; STACK_SIZE]; + + let stack_start = VirtAddr::from_ptr(unsafe { &STACK }); + let stack_end = stack_start + STACK_SIZE; + stack_end + }; + tss + }; +} +``` + +Rustの定数評価機はこの初期化をコンパイル時に行うことがまだできないので`lazy_static`を使います。ここでは0番目のISTエントリをダブルフォルト用のスタックとして定義します(他のISTのインデックスでも動くでしょう)。そして、ダブルフォルト用スタックの先頭アドレスを0番目のエントリに書き込みます。先頭アドレスを書き込むのはx86のスタックは下、つまり高いアドレスから低いアドレスに向かって伸びていくからです。 + +私達はまだメモリ管理を実装していません。そのため、新しいスタックを確保する適切な方法がありません。その代わり今回は、スタックのストレージとして`static mut`な配列を使います。コンパイラが変更可能な静的変数がアクセスされるとき競合がないことを保証できないため`unsafe`が必要となります。これが不変の`static`ではなく`static mut`であることは重要です。そうでなければブートローダーはこれをリードオンリーのページにマップしてしまうからです。私達は後の記事でこの部分を適切なスタック確保処理に置き換えます。そうすればこの部分での`unsafe`は必要なくなります。 + +ちなみに、このダブルフォルトスタックはスタックオーバーフローに対する保護をするガードページを持ちません。つまり、スタックオーバーフローがスタックより下のメモリを破壊するかもしれないので、私達はダブルフォルトハンドラ内でスタックを多用すべきではないということです。 + +#### TSSを読み込む +新しいTSSをつくったので、CPUにそれを使うように教える方法が必要です。残念ながら、これはちょっと面倒くさいです。なぜならTSSは(歴史的な理由で)セグメンテーションシステムを使うためです。テーブルを直接読み込むのではなく、新しいセグメントディスクリプタを[グローバルディスクリプタテーブル](GDT: Global Descriptor Table)に追加する必要があります。そうすると各自のGDTインデックスで[`ltr`命令]を呼び出すことで私達のTSSを読み込むことができます。 + +[グローバルディスクリプタテーブル]: https://web.archive.org/web/20190217233448/https://www.flingos.co.uk/docs/reference/Global-Descriptor-Table/ +[`ltr`命令]: https://www.felixcloutier.com/x86/ltr + +### グローバルディスクリプタテーブル +グローバルディスクリプタテーブル(GDT)はページングがデファクトスタンダードになる以前は、[メモリセグメンテーション]のため使われていた古い仕組みです。カーネル・ユーザーモードの設定やTSSの読み込みなど、様々なことを行うために64ビットモードでも未だに必要です。 + +[メモリセグメンテーション]: https://ja.wikipedia.org/wiki/セグメント方式 + +GDTはプログラムの**セグメント**を含む構造です。ページングが標準になる以前に、プログラム同士を独立させるためにより古いアーキテクチャで使われていました。セグメンテーションに関するより詳しい情報は無料の[「Three Easy Pieces」]という本の同じ名前の章を見てください。セグメンテーションは64ビットモードではもうサポートされていませんが、GDTはまだ存在しています。GDTはカーネル空間とユーザー空間の切り替えと、TSS構造体の読み込みという主に2つのことに使われています。 + +[「Three Easy Pieces」]: http://pages.cs.wisc.edu/~remzi/OSTEP/ + +#### GDTをつくる +`TSS`の静的変数のセグメントを含む静的`GDT`をつくりましょう: + +```rust +// in src/gdt.rs + +use x86_64::structures::gdt::{GlobalDescriptorTable, Descriptor}; + +lazy_static! { + static ref GDT: GlobalDescriptorTable = { + let mut gdt = GlobalDescriptorTable::new(); + gdt.add_entry(Descriptor::kernel_code_segment()); + gdt.add_entry(Descriptor::tss_segment(&TSS)); + gdt + }; +} +``` + +先に紹介したコードと同様に、再び`lazy_static`を使います。コードセグメントとTSSセグメントを持つ新しいGDTを作成します。 + +#### GDTを読み込む + +GDTを読み込むために新しく`gdt::init`関数をつくり、これを`init`関数から呼び出します: + +```rust +// in src/gdt.rs + +pub fn init() { + GDT.load(); +} + +// in src/lib.rs + +pub fn init() { + gdt::init(); + interrupts::init_idt(); +} +``` + +これでGDTが読み込まれます(`_start`関数は`init`を呼び出すため)が、これではまだスタックオーバーフローでブートループが起きてしまいます。 + +### 最後のステップ + +問題はGDTセグメントとTSSレジスタが古いGDTからの値を含んでいるため、GDTセグメントがまだ有効になっていないことです。ダブルフォルト用のIDTエントリが新しいスタックを使うように変更する必要もあります。 + +まとめると、私達は次のようなことをする必要があります: + +1. **コードセグメントレジスタを再読み込みする**:GDTを変更したので、コードセグメントレジスタ`cs`を再読み込みする必要があります。これは、古いセグメントセレクタが異なるGDTディスクリプタ(例:TSSディスクリプタ)を指す可能性があるためです。 +2. **TSSをロードする**:TSSセレクタを含むGDTをロードしましたが、CPUにこのTSSを使うよう教えてあげる必要があります。 +3. **IDTエントリを更新する**:TSSがロードされると同時に、CPUは正常な割り込みスタックテーブル(IST)へアクセスできるようになります。そうしたら、ダブルフォルトIDTエントリを変更することで、CPUに新しいダブルフォルトスタックを使うよう教えてあげることができます。 + +最初の2つのステップのために、私達は`gdt::init`関数の中で`code_selector`と`tss_selector`変数にアクセスする必要があります。これは、その変数たちを新しい`Selectors`構造体を使い静的変数にすることで実装できます: + +```rust +// in src/gdt.rs + +use x86_64::structures::gdt::SegmentSelector; + +lazy_static! { + static ref GDT: (GlobalDescriptorTable, Selectors) = { + let mut gdt = GlobalDescriptorTable::new(); + let code_selector = gdt.add_entry(Descriptor::kernel_code_segment()); + let tss_selector = gdt.add_entry(Descriptor::tss_segment(&TSS)); + (gdt, Selectors { code_selector, tss_selector }) + }; +} + +struct Selectors { + code_selector: SegmentSelector, + tss_selector: SegmentSelector, +} +``` + +これで私達は`cs`セグメントレジスタを再読み込みし`TSS`を読み込むためにセレクタを使うことができます: + +```rust +// in src/gdt.rs + +pub fn init() { + use x86_64::instructions::segmentation::set_cs; + use x86_64::instructions::tables::load_tss; + + GDT.0.load(); + unsafe { + set_cs(GDT.1.code_selector); + load_tss(GDT.1.tss_selector); + } +} +``` + +[`set_cs`]を使ってコードセグメントレジスタを再読み込みして、[`load_tss`]を使ってTSSを読み込んでいます。これらの関数は`unsafe`とマークされているので、呼び出すには`unsafe`ブロックが必要です。`unsafe`なのは、不正なセレクタを読み込むことでメモリ安全性を壊す可能性があるからです。 + +[`set_cs`]: https://docs.rs/x86_64/0.12.1/x86_64/instructions/segmentation/fn.set_cs.html +[`load_tss`]: https://docs.rs/x86_64/0.12.1/x86_64/instructions/tables/fn.load_tss.html + +これで正常なTSSと割り込みスタックテーブルを読み込みこんだので、私達はIDT内のダブルフォルトハンドラにスタックインデックスをセットすることができます: + +```rust +// in src/interrupts.rs + +use crate::gdt; + +lazy_static! { + static ref IDT: InterruptDescriptorTable = { + let mut idt = InterruptDescriptorTable::new(); + idt.breakpoint.set_handler_fn(breakpoint_handler); + unsafe { + idt.double_fault.set_handler_fn(double_fault_handler) + .set_stack_index(gdt::DOUBLE_FAULT_IST_INDEX); // new + } + + idt + }; +} +``` + +`set_stack_index`メソッドは呼び出し側が、使われているインデックスが正しいものであり、かつ他の例外で使われていないかを確かめる必要があるため、`unsafe`です。 + +これで全部です。CPUはダブルフォルトが発生したら常にダブルフォルトスタックに切り替えるでしょう。よって、私達はカーネルスタックオーバーフローを含む**すべての**ダブルフォルトをキャッチすることができます。 + +![QEMU printing `EXCEPTION: DOUBLE FAULT` and a dump of the exception stack frame](qemu-double-fault-on-stack-overflow.png) + +これからはトリプルフォルトを見ることは二度とないでしょう。これらダブルフォルトのための実装を誤って壊していないことを保証するために、テストを追加しましょう。 + +## スタックオーバーフローテスト + +新しい`gdt`モジュールをテストしダブルフォルトハンドラがスタックオーバーフローで正しく呼ばれることを保証するために、結合テストを追加します。ここでの考えは、テスト関数内でダブルフォルトを引き起こしダブルフォルトハンドラが呼び出されていることを確かめる、というものです。 + +最小の骨組みから始めましょう: + +```rust +// in tests/stack_overflow.rs + +#![no_std] +#![no_main] + +use core::panic::PanicInfo; + +#[no_mangle] +pub extern "C" fn _start() -> ! { + unimplemented!(); +} + +#[panic_handler] +fn panic(info: &PanicInfo) -> ! { + blog_os::test_panic_handler(info) +} +``` + +`panic_handler`のテストと同様、テストは[テストハーネスなし]で実行されます。理由は私達はダブルフォルト後に実行を続けることができず、2つ以上のテストは意味をなさないためです。テストハーネスを無効にするために、以下を`Cargo.toml`に追加します: + +```toml +# in Cargo.toml + +[[test]] +name = "stack_overflow" +harness = false +``` + +[テストハーネスなし]: @/edition-2/posts/04-testing/index.ja.md#hanesu-harness-nonaitesuto + +これで`cargo test --test stack_overflow`でのコンパイルは成功するでしょう。`unimplemented`マクロがパニックを起こすため、テストはもちろん失敗します。 + +### `_start`を実装する + +`_start`関数の実装はこのようになります: + +```rust +// in tests/stack_overflow.rs + +use blog_os::serial_print; + +#[no_mangle] +pub extern "C" fn _start() -> ! { + serial_print!("stack_overflow::stack_overflow...\t"); + + blog_os::gdt::init(); + init_test_idt(); + + // スタックオーバーフローを起こす + stack_overflow(); + + panic!("Execution continued after stack overflow"); +} + +#[allow(unconditional_recursion)] +fn stack_overflow() { + stack_overflow(); // 再帰のたびにリターンアドレスがプッシュされる + volatile::Volatile::new(0).read(); // 末尾最適化を防ぐ +} +``` + +新しいGDTを初期化するために`gdt::init`関数を呼びます。そして`interrupts::init_idt`関数を呼び出す代わりに、すぐ後で説明する`init_test_idt`関数を呼びます。なぜなら、私達はパニックの代わりに`exit_qemu(QemuExitCode::Success)`を実行するカスタムしたダブルフォルトハンドラを登録したいためです。 + +`stack_overflow`関数は`main.rs`の中にある関数とほとんど同じです。唯一の違いは[**末尾呼び出し最適化**]と呼ばれるコンパイラの最適化を防ぐために[`Volatile`]タイプを使って関数の末尾で追加の[volatile]読み込みを行っていることです。この最適化の特徴として、コンパイラが、最後の文が再帰関数呼び出しである関数を通常のループに変換できるようになる、というものがあります。その結果として、追加のスタックフレームが関数呼び出しではつくられず、スタックの使用量が変わらないままとなります。 + +[volatile]: https://en.wikipedia.org/wiki/Volatile_(computer_programming) +[`Volatile`]: https://docs.rs/volatile/0.2.6/volatile/struct.Volatile.html +[**末尾呼び出し最適化**]: https://ja.wikipedia.org/wiki/末尾再帰#末尾呼出し最適化 + +しかし、ここではスタックオーバーフローを起こしたいので、コンパイラに削除されない、ダミーのvolatile読み込み文を関数の末尾に追加します。その結果、関数は**末尾再帰**ではなくなり、ループへの変換は防がれます。更に関数が無限に再帰することに対するコンパイラの警告をなくすために`allow(unconditional_recursion)`属性を追加します。 + +### IDTのテスト + +上で述べたように、テストはカスタムしたダブルフォルトハンドラを含む専用のIDTが必要です。実装はこのようになります: + +```rust +// in tests/stack_overflow.rs + +use lazy_static::lazy_static; +use x86_64::structures::idt::InterruptDescriptorTable; + +lazy_static! { + static ref TEST_IDT: InterruptDescriptorTable = { + let mut idt = InterruptDescriptorTable::new(); + unsafe { + idt.double_fault + .set_handler_fn(test_double_fault_handler) + .set_stack_index(blog_os::gdt::DOUBLE_FAULT_IST_INDEX); + } + + idt + }; +} + +pub fn init_test_idt() { + TEST_IDT.load(); +} +``` + +実装は`interrupts.rs`内の通常のIDTと非常に似ています。通常のIDT同様、分離されたスタックに切り替えるようダブルフォルトハンドラ用のISTにスタックインデックスをセットします。`init_test_idt`関数は`load`メソッドによりCPU上にIDTを読み込みます。 + +### ダブルフォルトハンドラ + +唯一欠けているのはダブルフォルトハンドラです。このようになります: + +```rust +// in tests/stack_overflow.rs + +use blog_os::{exit_qemu, QemuExitCode, serial_println}; +use x86_64::structures::idt::InterruptStackFrame; + +extern "x86-interrupt" fn test_double_fault_handler( + _stack_frame: &mut InterruptStackFrame, + _error_code: u64, +) -> ! { + serial_println!("[ok]"); + exit_qemu(QemuExitCode::Success); + loop {} +} +``` + +ダブルフォルトハンドラが呼ばれるとき、私達はQEMUを正常な終了コードで終了し、テストを成功とマークします。結合テストは完全に分けられた実行ファイルなので、私達はテストファイルの先頭で`#![feature(abi_x86_interrupt)]`属性を再びセットする必要があります。 + +これで私達は`cargo test --test stack_overflow`(もしくは全部のテストを走らせるよう`cargo test`)でテストを走らせることができます。期待していたとおり、`stack_overflow... [ok]`とコンソールに出力されるのがわかります。`set_stack_index`の行をコメントアウトすると、テストは失敗するでしょう。 + +## まとめ +この記事では私達はダブルフォルトが何であるかとどういう条件下で発生するかを学びました。エラーメッセージを出力する基本的なダブルフォルトハンドラと、そのための結合テストを追加しました。 + +また、私達はスタックオーバーフロー下でも動くよう、ダブルフォルト発生時にハードウェアがサポートするスタック切り替えを行うようにしました。実装していく中で、古いアーキテクチャでのセグメンテーションで使われていたタスクステートセグメント(TSS)、割り込みスタックテーブル(IST)、グローバルディスクリプタテーブル(GDT)についても学びました。 + +## 次は? +次の記事ではタイマーやキーボード、ネットワークコントローラのような、外部デバイスからの割り込みをどのように処理するかを説明します。これらのハードウェア割り込みは例外によく似ています。例えば、これらもIDTからディスパッチされます。しかしながら、例外とは違い、それらはCPU上で直接発生するものではありません。代わりに、**割り込みコントローラ**がこれらの割り込みを集めて、優先度によってそれらをCPUに送ります。次回、私達は[Intel 8259](PIC)割り込みコントローラを調べ、どのようにキーボードのサポートを実装するかを学びます。 + +[Intel 8259]: https://ja.wikipedia.org/wiki/Intel_8259 diff --git a/blog/content/edition-2/posts/06-double-faults/index.md b/blog/content/edition-2/posts/06-double-faults/index.md index 45a90bc8..54542733 100644 --- a/blog/content/edition-2/posts/06-double-faults/index.md +++ b/blog/content/edition-2/posts/06-double-faults/index.md @@ -229,7 +229,7 @@ The _Privilege Stack Table_ is used by the CPU when the privilege level changes. ### Creating a TSS Let's create a new TSS that contains a separate double fault stack in its interrupt stack table. For that we need a TSS struct. Fortunately, the `x86_64` crate already contains a [`TaskStateSegment` struct] that we can use. -[`TaskStateSegment` struct]: https://docs.rs/x86_64/0.12.1/x86_64/structures/tss/struct.TaskStateSegment.html +[`TaskStateSegment` struct]: https://docs.rs/x86_64/0.13.2/x86_64/structures/tss/struct.TaskStateSegment.html We create the TSS in a new `gdt` module (the name will make sense later): @@ -373,10 +373,10 @@ pub fn init() { } ``` -We reload the code segment register using [`set_cs`] and to load the TSS using [`load_tss`]. The functions are marked as `unsafe`, so we need an `unsafe` block to invoke them. The reason is that it might be possible to break memory safety by loading invalid selectors. +We reload the code segment register using [`set_cs`] and load the TSS using [`load_tss`]. The functions are marked as `unsafe`, so we need an `unsafe` block to invoke them. The reason is that it might be possible to break memory safety by loading invalid selectors. -[`set_cs`]: https://docs.rs/x86_64/0.12.1/x86_64/instructions/segmentation/fn.set_cs.html -[`load_tss`]: https://docs.rs/x86_64/0.12.1/x86_64/instructions/tables/fn.load_tss.html +[`set_cs`]: https://docs.rs/x86_64/0.13.2/x86_64/instructions/segmentation/fn.set_cs.html +[`load_tss`]: https://docs.rs/x86_64/0.13.2/x86_64/instructions/tables/fn.load_tss.html Now that we loaded a valid TSS and interrupt stack table, we can set the stack index for our double fault handler in the IDT: @@ -432,7 +432,7 @@ fn panic(info: &PanicInfo) -> ! { } ``` -Like our `panic_handler` test, the test will run [without a test harness]. The reason is that we can't continue execution after a double fault, so more than one test doesn't make sense. To disable, the test harness for the test, we add the following to our `Cargo.toml`: +Like our `panic_handler` test, the test will run [without a test harness]. The reason is that we can't continue execution after a double fault, so more than one test doesn't make sense. To disable the test harness for the test, we add the following to our `Cargo.toml`: ```toml # in Cargo.toml diff --git a/blog/content/edition-2/posts/07-hardware-interrupts/index.fa.md b/blog/content/edition-2/posts/07-hardware-interrupts/index.fa.md new file mode 100644 index 00000000..faeb8959 --- /dev/null +++ b/blog/content/edition-2/posts/07-hardware-interrupts/index.fa.md @@ -0,0 +1,738 @@ ++++ +title = "وقفه‌های سخت‌افزاری" +weight = 7 +path = "fa/hardware-interrupts" +date = 2018-10-22 + +[extra] +chapter = "Interrupts" +# Please update this when updating the translation +translation_based_on_commit = "b6ff79ac3290ea92c86763d49cc6c0ff4fb0ea30" +# GitHub usernames of the people that translated this post +translators = ["hamidrezakp", "MHBahrampour"] +rtl = true ++++ + +در این پست ما کنترل کننده قابل برنامه ریزی وقفه را تنظیم می کنیم تا وقفه های سخت افزاری را به درستی به پردازنده منتقل کند. برای مدیریت این وقفه‌ها ، موارد جدیدی به جدول توصیف کننده وقفه اضافه می کنیم ، دقیقاً مانند کارهایی که برای کنترل کننده های استثنا انجام دادیم. ما یاد خواهیم گرفت که چگونه وقفه های متناوب تایمر را گرفته و چگونه از صفحه کلید ورودی بگیریم. + + + +این بلاگ بصورت آزاد بر روی [گیت‌هاب] توسعه داده شده. اگر مشکل یا سوالی دارید، لطفاً آن‌جا یک ایشو باز کنید. همچنین می‌توانید [در زیر] این پست کامنت بگذارید. سورس کد کامل این پست را می‌توانید در بِرَنچ [`post-07`][post branch] پیدا کنید. + +[گیت‌هاب]: https://github.com/phil-opp/blog_os +[در زیر]: #comments +[post branch]: https://github.com/phil-opp/blog_os/tree/post-07 + + + +## مقدمه + +وقفه‌ها راهی برای اطلاع به پردازنده از دستگاه های سخت افزاری متصل ارائه می دهند. بنابراین به جای اینکه پردازنده به طور دوره‌ای صفحه کلید را برای کاراکترهای جدید بررسی کند(فرآیندی به نام [_polling_]) ، صفحه کلید می‌تواند هسته را برای هر فشردن کلید مطلع کند. این بسیار کارآمدتر است زیرا هسته فقط زمانی که اتفاقی افتاده است باید عمل کند. همچنین زمان واکنش سریع تری را فراهم می کند ، زیرا هسته می تواند بلافاصله و نه تنها در پول(کلمه: poll) بعدی واکنش نشان دهد. + +[_polling_]: https://en.wikipedia.org/wiki/Polling_(computer_science) + +اتصال مستقیم تمام دستگاه های سخت افزاری به پردازنده امکان پذیر نیست. در عوض ، یک _کنترل کننده وقفه_ جداگانه ، وقفه‌ها را از همه دستگاه‌ها جمع کرده و سپس پردازنده را مطلع می کند: + +``` + ____________ _____ + Timer ------------> | | | | + Keyboard ---------> | Interrupt |---------> | CPU | + Other Hardware ---> | Controller | |_____| + Etc. -------------> |____________| + +``` + +بیشتر کنترل کننده های وقفه قابل برنامه ریزی هستند ، به این معنی که آنها از اولویت های مختلف برای وقفه‌ها پشتیبانی می کنند. به عنوان مثال ، این اجازه را می دهند تا به وقفه های تایمر اولویت بیشتری نسبت به وقفه های صفحه کلید داد تا از زمان بندی دقیق اطمینان حاصل شود. + +بر خلاف استثناها ، وقفه های سخت افزاری _به صورت نا هم زمان_ اتفاق می افتند. این بدان معنی است که آنها کاملاً از کد اجرا شده مستقل هستند و در هر زمان ممکن است رخ دهند. بنابراین ما ناگهان شکلی از همروندی در هسته خود با تمام اشکالات احتمالی مرتبط با همروندی داریم. مدل مالکیت دقیق راست در اینجا به ما کمک می کند زیرا مانع حالت تغییر پذیری گلوبال است(mutable global state). با این حال، همچنان احتمال بن بست وجود دارد، همانطور که بعداً در این پست خواهیم دید. + +## The 8259 PIC + +[Intel 8259] یک کنترل کننده وقفه قابل برنامه ریزی (PIC) است که در سال 1976 معرفی شد. مدت طولانی است که با [APIC] جدید جایگزین شده است ، اما رابط آن هنوز به دلایل سازگاری در سیستم های فعلی پشتیبانی می شود. 8259 PIC به طور قابل ملاحظه ای آسان تر از APIC است ، بنابراین ما قبل از مهاجرت و استفاده از APIC در آینده، از آن برای معرفی وقفه استفاده خواهیم کرد. + +[APIC]: https://en.wikipedia.org/wiki/Intel_APIC_Architecture + +8259 دارای 8 خط وقفه و چندین خط برای برقراری ارتباط با پردازنده است. سیستم های معمولی در آن زمان به دو نمونه از 8259 PIC مجهز بودند ، یکی اصلی و دیگری PIC ثانویه که به یکی از خطوط وقفه اولیه متصل است: + +[Intel 8259]: https://en.wikipedia.org/wiki/Intel_8259 + +``` + ____________ ____________ +Real Time Clock --> | | Timer -------------> | | +ACPI -------------> | | Keyboard-----------> | | _____ +Available --------> | Secondary |----------------------> | Primary | | | +Available --------> | Interrupt | Serial Port 2 -----> | Interrupt |---> | CPU | +Mouse ------------> | Controller | Serial Port 1 -----> | Controller | |_____| +Co-Processor -----> | | Parallel Port 2/3 -> | | +Primary ATA ------> | | Floppy disk -------> | | +Secondary ATA ----> |____________| Parallel Port 1----> |____________| + +``` + +این نمودار نحوه اتصال معمول خطوط وقفه را نشان می دهد. می بینیم که بیشتر 15 خط دارای یک نگاشت ثابت هستند ، به عنوان مثال خط 4 PIC ثانویه به ماوس اختصاص داده شده است. + +هر کنترل کننده را می توان از طریق دو [پورت ورودی/خروجی] ، یک پورت "فرمان" و یک پورت "داده" پیکربندی کرد. برای کنترل کننده اصلی ، این پورت‌ها `0x20` (فرمان) و`0x21` (داده) هستند. برای کنترل کننده ثانویه آنها `0xa0` (فرمان) و `0xa1` (داده) هستند. برای اطلاعات بیشتر در مورد نحوه پیکربندی PIC ها ، به [مقاله‌ای در osdev.org] مراجعه کنید. + +[پورت ورودی/خروجی]: @/edition-2/posts/04-testing/index.md#i-o-ports +[مقاله‌ای در osdev.org]: https://wiki.osdev.org/8259_PIC + +### پیاده سازی + +پیکربندی پیش فرض PIC ها قابل استفاده نیست، زیرا اعداد بردار وقفه را در محدوده 15-0 به پردازنده می فرستد. این اعداد در حال حاضر توسط استثناهای پردازنده اشغال شده‌اند ، به عنوان مثال شماره 8 مربوط به یک خطای دوگانه است. برای رفع این مشکل همپوشانی، باید وقفه های PIC را به اعداد دیگری تغییر دهیم. دامنه واقعی مهم نیست به شرطی که با استثناها همپوشانی نداشته باشد ، اما معمولاً محدوده 47-32 انتخاب می شود، زیرا اینها اولین شماره های آزاد پس از 32 اسلات استثنا هستند. + +پیکربندی با نوشتن مقادیر ویژه در پورت های فرمان و داده PIC ها اتفاق می افتد. خوشبختانه قبلا کرت‌ای به نام [`pic8259_simple`] وجود دارد، بنابراین نیازی نیست که توالی راه اندازی اولیه را خودمان بنویسیم. در صورت علاقه‌مند بودن به چگونگی عملکرد آن، [کد منبع آن][pic crate source] را بررسی کنید، نسبتاً کوچک و دارای مستند خوبی است. + +[pic crate source]: https://docs.rs/crate/pic8259_simple/0.2.0/source/src/lib.rs + +برای افزودن کرت به عنوان وابستگی ، موارد زیر را به پروژه خود اضافه می کنیم: + +[`pic8259_simple`]: https://docs.rs/pic8259_simple/0.2.0/pic8259_simple/ + +```toml +# in Cargo.toml + +[dependencies] +pic8259_simple = "0.2.0" +``` + +انتزاع اصلی ارائه شده توسط کرت، ساختمان [`ChainedPics`] است که نمایانگر طرح اولیه/ثانویه PIC است که در بالا دیدیم. برای استفاده به روش زیر طراحی شده است: + +[`ChainedPics`]: https://docs.rs/pic8259_simple/0.2.0/pic8259_simple/struct.ChainedPics.html + +```rust +// in src/interrupts.rs + +use pic8259_simple::ChainedPics; +use spin; + +pub const PIC_1_OFFSET: u8 = 32; +pub const PIC_2_OFFSET: u8 = PIC_1_OFFSET + 8; + +pub static PICS: spin::Mutex = + spin::Mutex::new(unsafe { ChainedPics::new(PIC_1_OFFSET, PIC_2_OFFSET) }); +``` + +همانطور که در بالا اشاره کردیم، افست PIC ها را در محدوده 47-32 تنظیم می کنیم. با بسته بندی ساختمان `ChainedPics` در `Mutex` می توانیم دسترسی قابل تغییر و ایمن (از طریق [متد lock][spin mutex lock]) به آن داشته باشیم، که در مرحله بعدی به آن نیاز داریم. تابع `ChainedPics::new` ناامن است زیرا افست اشتباه ممکن است باعث رفتار نامشخص شود. + +[spin mutex lock]: https://docs.rs/spin/0.5.2/spin/struct.Mutex.html#method.lock + +اکنون می توانیم 8259 PIC را در تابع `init` خود مقدار دهی اولیه کنیم: + +```rust +// in src/lib.rs + +pub fn init() { + gdt::init(); + interrupts::init_idt(); + unsafe { interrupts::PICS.lock().initialize() }; // new +} +``` + +ما از تابع [`initialize`] برای انجام مقداردهی اولیه PIC استفاده می کنیم. مانند تابع `ChainedPics::new`، این تابع نیز ایمن نیست زیرا در صورت عدم پیکربندی صحیح PIC می تواند باعث رفتار نامشخص شود. + +[`initialize`]: https://docs.rs/pic8259_simple/0.2.0/pic8259_simple/struct.ChainedPics.html#method.initialize + +اگر همه چیز خوب پیش برود ، باید هنگام اجرای `cargo run` پیام "It did not crash" را ببینیم. + +## فعال‌سازی وقفه‌ها + +تاکنون هیچ اتفاقی نیفتاده است زیرا وقفه‌ها همچنان در تنظیمات پردازنده غیرفعال هستند. این بدان معناست که پردازنده به هیچ وجه به کنترل کننده وقفه گوش نمی دهد، بنابراین هیچ وقفه ای نمی تواند به پردازنده برسد. بیایید این را تغییر دهیم: + +```rust +// in src/lib.rs + +pub fn init() { + gdt::init(); + interrupts::init_idt(); + unsafe { interrupts::PICS.lock().initialize() }; + x86_64::instructions::interrupts::enable(); // new +} +``` + +تابع `interrupts::enable` از کرت `x86_64` دستورالعمل خاص `sti` را اجرا می کند (“set interrupts”) تا وقفه های خارجی را فعال کند. اکنون وقتی `cargo run` را امتحان می کنیم ، می بینیم که یک خطای دوگانه رخ می‌دهد: + +![QEMU printing `EXCEPTION: DOUBLE FAULT` because of hardware timer](qemu-hardware-timer-double-fault.png) + +دلیل این خطای دوگانه این است که تایمر سخت افزاری (به طور دقیق تر [Intel 8253]) به طور پیش فرض فعال است، بنابراین به محض فعال کردن وقفه‌ها ، شروع به دریافت وقفه های تایمر می کنیم. از آنجا که هنوز یک تابع کنترل کننده برای آن تعریف نکرده‌ایم ، کنترل کننده خطای دوگانه فراخوانی می شود. + +[Intel 8253]: https://en.wikipedia.org/wiki/Intel_8253 + +## مدیریت وقفه‌های تایمر + +همانطور که در شکل [بالا](#the-8259-pic) می بینیم، تایمر از خط 0 از PIC اصلی استفاده می کند. این به این معنی است که به صورت وقفه 32 (0 + افست 32) به پردازنده می رسد. به جای هارد-کد(Hardcode) کردن 32، آن را در یک اینام(enum) به نام `InterruptIndex` ذخیره می کنیم: + +```rust +// in src/interrupts.rs + +#[derive(Debug, Clone, Copy)] +#[repr(u8)] +pub enum InterruptIndex { + Timer = PIC_1_OFFSET, +} + +impl InterruptIndex { + fn as_u8(self) -> u8 { + self as u8 + } + + fn as_usize(self) -> usize { + usize::from(self.as_u8()) + } +} +``` + +اینام یک [اینام C مانند] است بنابراین ما می توانیم ایندکس را برای هر نوع به طور مستقیم مشخص کنیم. ویژگی `repr(u8)` مشخص می کند که هر نوع به عنوان `u8` نشان داده می شود. در آینده انواع بیشتری برای وقفه های دیگر اضافه خواهیم کرد. + +[اینام C مانند]: https://doc.rust-lang.org/reference/items/enumerations.html#custom-discriminant-values-for-fieldless-enumerations + +اکنون می توانیم یک تابع کنترل کننده برای وقفه تایمر اضافه کنیم: + +```rust +// in src/interrupts.rs + +use crate::print; + +lazy_static! { + static ref IDT: InterruptDescriptorTable = { + let mut idt = InterruptDescriptorTable::new(); + idt.breakpoint.set_handler_fn(breakpoint_handler); + […] + idt[InterruptIndex::Timer.as_usize()] + .set_handler_fn(timer_interrupt_handler); // new + + idt + }; +} + +extern "x86-interrupt" fn timer_interrupt_handler( + _stack_frame: &mut InterruptStackFrame) +{ + print!("."); +} +``` + +`timer_interrupt_handler` ما دارای امضای مشابه کنترل کننده های استثنای ما است ، زیرا پردازنده به طور یکسان به استثناها و وقفه های خارجی واکنش نشان می دهد (تنها تفاوت این است که برخی از استثناها کد خطا را در پشته ذخیره می‌کنند). ساختمان [`InterruptDescriptorTable`] تریت [`IndexMut`] را پیاده سازی می کند، بنابراین می توانیم از طریق سینتکس ایندکس‌دهی آرایه، به ایتم های جداگانه دسترسی پیدا کنیم. + +[`InterruptDescriptorTable`]: https://docs.rs/x86_64/0.13.2/x86_64/structures/idt/struct.InterruptDescriptorTable.html +[`IndexMut`]: https://doc.rust-lang.org/core/ops/trait.IndexMut.html + +در کنترل کننده وقفه تایمر، یک نقطه را روی صفحه چاپ می کنیم. همانطور که وقفه تایمر به صورت دوره ای اتفاق می افتد ، انتظار داریم که در هر تیک تایمر یک نقطه ظاهر شود. با این حال، هنگامی که آن را اجرا می کنیم می بینیم که فقط یک نقطه چاپ می شود: + +![QEMU printing only a single dot for hardware timer](qemu-single-dot-printed.png) + +### پایان وقفه + +دلیل این امر این است که PIC انتظار دارد یک سیگنال صریح "پایان وقفه" (EOI) از کنترل کننده وقفه ما دریافت کند. این سیگنال به PIC می گوید که وقفه پردازش شده و سیستم آماده دریافت وقفه بعدی است. بنابراین PIC فکر می کند ما هنوز مشغول پردازش وقفه تایمر اول هستیم و قبل از ارسال سیگنال بعدی با صبر و حوصله منتظر سیگنال EOI از ما هست. + +برای ارسال EOI ، ما دوباره از ساختمان ثابت `PICS` خود استفاده می کنیم: + +```rust +// in src/interrupts.rs + +extern "x86-interrupt" fn timer_interrupt_handler( + _stack_frame: &mut InterruptStackFrame) +{ + print!("."); + + unsafe { + PICS.lock() + .notify_end_of_interrupt(InterruptIndex::Timer.as_u8()); + } +} +``` + +`notify_end_of_interrupt` تشخیص می‌دهد که PIC اصلی یا ثانویه وقفه را ارسال کرده است و سپس از پورت های `command` و `data` برای ارسال سیگنال EOI به PIC های مربوطه استفاده می کند. اگر PIC ثانویه وقفه را ارسال کرد ، هر دو PIC باید مطلع شوند زیرا PIC ثانویه به یک خط ورودی از PIC اصلی متصل است. + +ما باید مراقب باشیم که از شماره بردار وقفه صحیح استفاده کنیم، در غیر این صورت می توانیم به طور تصادفی یک وقفه مهم ارسال نشده را حذف کنیم یا باعث هنگ سیستم خود شویم. این دلیل آن است که تابع ناامن است. + +اکنون هنگامی که `cargo run` را اجرا می کنیم، نقاطی را می بینیم که به صورت دوره ای روی صفحه ظاهر می شوند: + +![QEMU printing consecutive dots showing the hardware timer](qemu-hardware-timer-dots.gif) + +### پیکربندی تایمر + +تایمر سخت افزاری که ما از آن استفاده می کنیم ، _Progammable Interval Timer_ یا به اختصار PIT نامیده می شود. همانطور که از نام آن مشخص است ، می توان فاصله بین دو وقفه را پیکربندی کرد. ما در اینجا به جزئیات نمی پردازیم زیرا به زودی به [تایمر APIC] سوییچ خواهیم کرد، اما ویکی OSDev مقاله مفصلی درباره [پیکربندی PIT] دارد. + +[تایمر APIC]: https://wiki.osdev.org/APIC_timer +[پیکربندی PIT]: https://wiki.osdev.org/Programmable_Interval_Timer + +## بن‌بست ها + +اکنون نوعی همروندی در هسته خود داریم: وقفه های تایمر به صورت ناهمزمان اتفاق می افتند ، بنابراین می توانند تابع `start_` را در هر زمان قطع کنند. خوشبختانه سیستم مالکیت راست از بسیاری از مشکلات مربوط به همروندی در زمان کامپایل جلوگیری می کند. یک استثنا قابل توجه بن‌بست است. درصورتی که نخ(Thread) بخواهد قفلی را بدست آورد که هرگز آزاد نخواهد شد، بن‌بست به وجود می آید. بنابراین نخ به طور نامحدود هنگ می‌کند. + +ما می توانیم در هسته خود بن‌بست ایجاد کنیم. اگر به یاد داشته باشید، ماکرو `println` ما تابع `vga_buffer::_print` را فراخوانی می کند، که با استفاده از spinlock یک [`WRITER` گلوبال را قفل میکند][vga spinlock]. + +[vga spinlock]: @/edition-2/posts/03-vga-text-buffer/index.md#spinlocks + +```rust +// in src/vga_buffer.rs + +[…] + +#[doc(hidden)] +pub fn _print(args: fmt::Arguments) { + use core::fmt::Write; + WRITER.lock().write_fmt(args).unwrap(); +} +``` + +`WRITER` را قفل می کند، `write_fmt` را روی آن فراخوانی می کند و در انتهای تابع به طور ضمنی قفل آن را باز می کند. حال تصور کنید که در حالی که `WRITER` قفل شده است وقفه رخ دهد و کنترل کننده وقفه نیز سعی کند چیزی را چاپ کند: + +Timestep | _start | interrupt_handler +---------|------|------------------ +0 | calls `println!` |   +1 | `print` locks `WRITER` |   +2 | | **interrupt occurs**, handler begins to run +3 | | calls `println!` | +4 | | `print` tries to lock `WRITER` (already locked) +5 | | `print` tries to lock `WRITER` (already locked) +… | | … +_never_ | _unlock `WRITER`_ | + +`WRITER` قفل شده است ، بنابراین کنترل کننده وقفه منتظر می ماند تا آزاد شود. اما این هرگز اتفاق نمی افتد ، زیرا تابع `start_` فقط پس از بازگشت کنترل کننده وقفه ادامه می یابد. بنابراین کل سیستم هنگ است. + +### ایجاد بن‌بست + +ما می توانیم با چاپ چیزی در حلقه در انتهای تابع `start_` خود ، به راحتی چنین بن‌بست‌ای در هسته خود ایجاد کنیم: + +```rust +// in src/main.rs + +#[no_mangle] +pub extern "C" fn _start() -> ! { + […] + loop { + use blog_os::print; + print!("-"); // new + } +} +``` + +وقتی آن را در QEMU اجرا می کنیم ، خروجی به حالت زیر دریافت می‌کنیم: + +![QEMU output with many rows of hyphens and no dots](./qemu-deadlock.png) + +می بینیم که فقط تعداد محدودی خط فاصله ، تا زمانی که وقفه تایمر اول اتفاق بیفتد، چاپ می شود. سپس سیستم هنگ می‌کند زیرا تایمر هنگام تلاش برای چاپ یک نقطه باعث بن‌بست می‌شود. به همین دلیل است که در خروجی فوق هیچ نقطه‌ای مشاهده نمی‌کنیم. + +تعداد واقعی خط فاصله بین هر اجرا متفاوت است زیرا وقفه تایمر به صورت غیر همزمان انجام می شود. این عدم قطعیت، اشکال زدایی اشکالات مربوط به همروندی را بسیار دشوار می کند. + +### رفع بن‌بست + +برای جلوگیری از این بن‌بست ، تا زمانی که `Mutex` قفل شده باشد، می توانیم وقفه‌ها را غیرفعال کنیم: + +```rust +// in src/vga_buffer.rs + +/// Prints the given formatted string to the VGA text buffer +/// through the global `WRITER` instance. +#[doc(hidden)] +pub fn _print(args: fmt::Arguments) { + use core::fmt::Write; + use x86_64::instructions::interrupts; // new + + interrupts::without_interrupts(|| { // new + WRITER.lock().write_fmt(args).unwrap(); + }); +} +``` + +تابع [`without_interrupts`] یک [کلوژر] را گرفته و آن را در یک محیط بدون وقفه اجرا می کند. ما از آن استفاده می کنیم تا اطمینان حاصل کنیم که تا زمانی که `Mutex` قفل شده است ، هیچ وقفه ای رخ نمی دهد. اکنون هنگامی که هسته را اجرا می کنیم ، می بینیم که آن بدون هنگ کردن به کار خود ادامه می دهد. (ما هنوز هیچ نقطه ای را مشاهده نمی کنیم ، اما این به این دلیل است که سرعت حرکت آنها بسیار سریع است. سعی کنید سرعت چاپ را کم کنید، مثلاً با قرار دادن `for _ in 0..10000 {}` در داخل حلقه.) + +[`without_interrupts`]: https://docs.rs/x86_64/0.13.2/x86_64/instructions/interrupts/fn.without_interrupts.html +[کلوژر]: https://doc.rust-lang.org/book/second-edition/ch13-01-closures.html + +ما می توانیم همین تغییر را در تابع چاپ سریال نیز اعمال کنیم تا اطمینان حاصل کنیم که هیچ بن‌بستی در آن رخ نمی دهد: + +```rust +// in src/serial.rs + +#[doc(hidden)] +pub fn _print(args: ::core::fmt::Arguments) { + use core::fmt::Write; + use x86_64::instructions::interrupts; // new + + interrupts::without_interrupts(|| { // new + SERIAL1 + .lock() + .write_fmt(args) + .expect("Printing to serial failed"); + }); +} +``` + +توجه داشته باشید که غیرفعال کردن وقفه‌ها نباید یک راه حل کلی باشد. مشکل این است که بدترین حالت تأخیر در وقفه را افزایش می دهد ، یعنی زمانی که سیستم به وقفه واکنش نشان می دهد. بنابراین وقفه‌ها باید فقط برای مدت زمان کوتاه غیرفعال شوند. + +## رفع وضعیت رقابتی + +اگر `cargo test` را اجرا کنید ، ممکن است ببینید تست `test_println_output` با شکست مواجه می‌شود: + +``` +> cargo test --lib +[…] +Running 4 tests +test_breakpoint_exception...[ok] +test_println... [ok] +test_println_many... [ok] +test_println_output... [failed] + +Error: panicked at 'assertion failed: `(left == right)` + left: `'.'`, + right: `'S'`', src/vga_buffer.rs:205:9 +``` + +دلیل آن وجود یک _وضعیت رقابتی_ بین تست و کنترل کننده تایمر ماست. اگر به یاد داشته باشید ، تست به این شکل است: + +```rust +// in src/vga_buffer.rs + +#[test_case] +fn test_println_output() { + let s = "Some test string that fits on a single line"; + println!("{}", s); + for (i, c) in s.chars().enumerate() { + let screen_char = WRITER.lock().buffer.chars[BUFFER_HEIGHT - 2][i].read(); + assert_eq!(char::from(screen_char.ascii_character), c); + } +} +``` + +این تست یک رشته را در بافر VGA چاپ می کند و سپس با پیمایش دستی روی آرایه `buffer_chars` خروجی را بررسی می کند. وضعیت رقابتی رخ می دهد زیرا ممکن است کنترل کننده وقفه تایمر بین `println` و خواندن کاراکتر های صفحه اجرا شود. توجه داشته باشید که این یک رقابت داده(Data race) خطرناک نیست، که Rust در زمان کامپایل کاملاً از آن جلوگیری کند. برای جزئیات به [_Rustonomicon_][nomicon-races] مراجعه کنید. + +[nomicon-races]: https://doc.rust-lang.org/nomicon/races.html + +برای رفع این مشکل ، باید `WRITER` را برای مدت زمان کامل تست قفل نگه داریم ، به این ترتیب که کنترل کننده تایمر نمی تواند `.` را روی صفحه نمایش در میان کار تست بنویسد. تست اصلاح شده به این شکل است: + +```rust +// in src/vga_buffer.rs + +#[test_case] +fn test_println_output() { + use core::fmt::Write; + use x86_64::instructions::interrupts; + + let s = "Some test string that fits on a single line"; + interrupts::without_interrupts(|| { + let mut writer = WRITER.lock(); + writeln!(writer, "\n{}", s).expect("writeln failed"); + for (i, c) in s.chars().enumerate() { + let screen_char = writer.buffer.chars[BUFFER_HEIGHT - 2][i].read(); + assert_eq!(char::from(screen_char.ascii_character), c); + } + }); +} +``` + +ما تغییرات زیر را انجام دادیم: + +- ما با استفاده صریح از متد `()lock` ، نویسنده را برای کل تست قفل می کنیم. به جای `println` ، از ماکرو [`writeln`] استفاده می کنیم که امکان چاپ بر روی نویسنده قبلاً قفل شده را فراهم می کند. +- برای جلوگیری از یک بن‌بست دیگر ، وقفه‌ها را برای مدت زمان تست غیرفعال می کنیم. در غیر این صورت ممکن است تست در حالی که نویسنده هنوز قفل است قطع شود. +- از آنجا که کنترل کننده وقفه تایمر هنوز می تواند قبل از تست اجرا شود ، قبل از چاپ رشته `s` یک خط جدید `n\` اضافی چاپ می کنیم. به این ترتیب ، اگر که کنترل کننده تایمر تعدادی کاراکتر `.` را در خط فعلی چاپ کرده باشد، از شکست تست جلوگیری می کنیم. + +[`writeln`]: https://doc.rust-lang.org/core/macro.writeln.html + +اکنون با تغییرات فوق ، `cargo test` دوباره با قطعیت موفق می شود. + +این یک وضعیت رقابتی بسیار بی خطر بود که فقط باعث شکست تست می‌شد. همانطور که می توانید تصور کنید، اشکال زدایی سایر وضعیت‌های رقابتی به دلیل ماهیت غیر قطعی بودن آنها بسیار دشوارتر است. خوشبختانه، راست مانع از رقابت داده‌ها می شود ، که جدی‌ترین نوع وضعیت رقابتی است ، زیرا می تواند باعث انواع رفتارهای تعریف نشده ، از جمله کرش کردن سیستم و خراب شدن آرام و بی صدای حافظه شود. + +## دستورالعمل `hlt` + +تاکنون از یک حلقه خالی ساده در پایان توابع `start_` و` panic` استفاده می کردیم. این باعث می شود پردازنده به طور بی وقفه بچرخد و بنابراین مطابق انتظار عمل می کند. اما بسیار ناکارآمد است، زیرا پردازنده همچنان با سرعت کامل کار می کند حتی اگر کاری برای انجام نداشته باشد. هنگامی که هسته را اجرا می کنید می توانید این مشکل را در مدیر وظیفه خود مشاهده کنید: فرایند QEMU در کل مدت زمان نیاز به تقریباً 100٪ پردازنده دارد. + +کاری که واقعاً می خواهیم انجام دهیم این است که پردازنده را تا رسیدن وقفه بعدی متوقف کنیم. این اجازه می دهد پردازنده وارد حالت خواب شود که در آن انرژی بسیار کمتری مصرف می کند. [دستورالعمل `hlt`] دقیقاً همین کار را می کند. بیایید از این دستورالعمل برای ایجاد یک حلقه بی پایان با مصرف انرژی پایین استفاده کنیم: + +[دستورالعمل `hlt`]: https://en.wikipedia.org/wiki/HLT_(x86_instruction) + +```rust +// in src/lib.rs + +pub fn hlt_loop() -> ! { + loop { + x86_64::instructions::hlt(); + } +} +``` + +تابع `instructions::hlt` فقط یک [پوشش نازک] بر روی دستورالعمل اسمبلی است. این بی خطر است زیرا به هیچ وجه نمی تواند ایمنی حافظه را به خطر بیندازد. + +[پوشش نازک]: https://github.com/rust-osdev/x86_64/blob/5e8e218381c5205f5777cb50da3ecac5d7e3b1ab/src/instructions/mod.rs#L16-L22 + +اکنون می توانیم از این `hlt_loop` به جای حلقه های بی پایان در توابع` start_` و `panic` استفاده کنیم: + +```rust +// in src/main.rs + +#[no_mangle] +pub extern "C" fn _start() -> ! { + […] + + println!("It did not crash!"); + blog_os::hlt_loop(); // new +} + + +#[cfg(not(test))] +#[panic_handler] +fn panic(info: &PanicInfo) -> ! { + println!("{}", info); + blog_os::hlt_loop(); // new +} + +``` + +بیایید `lib.rs` را نیز به روز کنیم: + +```rust +// in src/lib.rs + +/// Entry point for `cargo test` +#[cfg(test)] +#[no_mangle] +pub extern "C" fn _start() -> ! { + init(); + test_main(); + hlt_loop(); // new +} + +pub fn test_panic_handler(info: &PanicInfo) -> ! { + serial_println!("[failed]\n"); + serial_println!("Error: {}\n", info); + exit_qemu(QemuExitCode::Failed); + hlt_loop(); // new +} +``` + +اکنون وقتی هسته خود را در QEMU اجرا می کنیم ، شاهد استفاده بسیار کمتری از پردازنده هستیم. + +## ورودی صفحه کلید + +اکنون که قادر به مدیریت وقفه های دستگاه های خارجی هستیم ، سرانجام قادر به پشتیبانی از ورودی صفحه کلید هستیم. این به ما امکان می دهد برای اولین بار با هسته خود تعامل داشته باشیم. + + + +[PS/2]: https://en.wikipedia.org/wiki/PS/2_port + +مانند تایمر سخت افزاری ، کنترل کننده صفحه کلید نیز به طور پیش فرض از قبل فعال شده است. بنابراین با فشار دادن یک کلید ، کنترل کننده صفحه کلید وقفه را به PIC ارسال می کند و آن را به پردازنده منتقل می کند. پردازنده به دنبال یک تابع کنترل کننده در IDT می‌گردد ، اما ایتم مربوطه خالی است. بنابراین یک خطای دوگانه رخ می دهد. + +پس بیایید یک تایع کنترل کننده برای وقفه صفحه کلید اضافه کنیم. این کاملاً مشابه نحوه تعریف کنترل کننده برای وقفه تایمر است ، فقط از یک شماره وقفه متفاوت استفاده می کند: + +```rust +// in src/interrupts.rs + +#[derive(Debug, Clone, Copy)] +#[repr(u8)] +pub enum InterruptIndex { + Timer = PIC_1_OFFSET, + Keyboard, // new +} + +lazy_static! { + static ref IDT: InterruptDescriptorTable = { + let mut idt = InterruptDescriptorTable::new(); + idt.breakpoint.set_handler_fn(breakpoint_handler); + […] + // new + idt[InterruptIndex::Keyboard.as_usize()] + .set_handler_fn(keyboard_interrupt_handler); + + idt + }; +} + +extern "x86-interrupt" fn keyboard_interrupt_handler( + _stack_frame: &mut InterruptStackFrame) +{ + print!("k"); + + unsafe { + PICS.lock() + .notify_end_of_interrupt(InterruptIndex::Keyboard.as_u8()); + } +} +``` + +همانطور که در شکل [بالا](#the-8259-pic) مشاهده می کنیم ، صفحه کلید از خط 1 در PIC اصلی استفاده می کند. این به این معنی است که به صورت وقفه 33 (1 + افست 32) به پردازنده می رسد. ما این ایندکس را به عنوان یک نوع جدید `Keyboard` به ای‌نام `InterruptIndex` اضافه می کنیم. نیازی نیست که مقدار را صریحاً مشخص کنیم ، زیرا این مقدار به طور پیش فرض برابر مقدار قبلی بعلاوه یک که 33 نیز می باشد ، هست. در کنترل کننده وقفه ، ما یک `k` چاپ می کنیم و سیگنال پایان وقفه را به کنترل کننده وقفه می فرستیم. + +اکنون می بینیم که وقتی کلید را فشار می دهیم `k` بر روی صفحه ظاهر می شود. با این حال ، این فقط برای اولین کلیدی که فشار می دهیم کار می کند ، حتی اگر به فشار دادن کلیدها ادامه دهیم ، دیگر `k` بر روی صفحه نمایش ظاهر نمی شود. این امر به این دلیل است که کنترل کننده صفحه کلید تا زمانی که اصطلاحاً _scancode_ را نخوانیم ، وقفه دیگری ارسال نمی کند. + +### خواندن اسکن‌کد ها + +برای اینکه بفهمیم _کدام_ کلید فشار داده شده است ، باید کنترل کننده صفحه کلید را جستجو کنیم. ما این کار را با خواندن از پورت داده کنترل کننده PS/2 ، که [پورت ورودی/خروجی] با شماره `0x60` است ، انجام می دهیم: + +[پورت ورودی/خروجی]: @/edition-2/posts/04-testing/index.md#i-o-ports + +```rust +// in src/interrupts.rs + +extern "x86-interrupt" fn keyboard_interrupt_handler( + _stack_frame: &mut InterruptStackFrame) +{ + use x86_64::instructions::port::Port; + + let mut port = Port::new(0x60); + let scancode: u8 = unsafe { port.read() }; + print!("{}", scancode); + + unsafe { + PICS.lock() + .notify_end_of_interrupt(InterruptIndex::Keyboard.as_u8()); + } +} +``` + +ما برای خواندن یک بایت از پورت داده صفحه کلید از نوع [`Port`] کرت `x86_64` استفاده می‌کنیم. این بایت [_اسکن کد_] نامیده می شود و عددی است که کلید فشرده شده / رها شده را نشان می دهد. ما هنوز کاری با اسکن کد انجام نمی دهیم ، فقط آن را روی صفحه چاپ می کنیم: + +[`Port`]: https://docs.rs/x86_64/0.13.2/x86_64/instructions/port/struct.Port.html +[_اسکن کد_]: https://en.wikipedia.org/wiki/Scancode + +![QEMU printing scancodes to the screen when keys are pressed](qemu-printing-scancodes.gif) + +تصویر بالا نشان می دهد که من آرام آرام "123" را تایپ می کنم. می بینیم که کلیدهای مجاور دارای اسکن کد مجاور هستند و فشار دادن یک کلید دارای اسکن کد متفاوت با رها کردن آن است. اما چگونه اسکن‌کدها را دقیقاً به کار اصلی آن کلید ترجمه کنیم؟ + +### تفسیر اسکن‌کد ها +سه استاندارد مختلف برای نگاشت بین اسکن کدها و کلیدها وجود دارد ، اصطلاحاً _مجموعه های اسکن کد_. هر سه به صفحه کلید رایانه های اولیه IBM برمی گردند: [IBM XT] ، [IBM 3270 PC] و [IBM AT]. خوشبختانه رایانه های بعدی روند تعریف مجموعه های جدید اسکن کد را ادامه ندادند ، بلکه مجموعه های موجود را تقلید و آنها را گسترش دادند. امروزه بیشتر صفحه کلیدها را می توان به گونه ای پیکربندی کرد که از هر کدام از سه مجموعه تقلید کند. + +[IBM XT]: https://en.wikipedia.org/wiki/IBM_Personal_Computer_XT +[IBM 3270 PC]: https://en.wikipedia.org/wiki/IBM_3270_PC +[IBM AT]: https://en.wikipedia.org/wiki/IBM_Personal_Computer/AT + +به طور پیش فرض ، صفحه کلیدهای PS/2 مجموعه شماره 1 ("XT") را تقلید می کنند. در این مجموعه ، 7 بیت پایین بایت اسکن‌کد، کلید را تعریف می کند و مهمترین بیت فشردن ("0") یا رها کردن ("1") را مشخص می کند. کلیدهایی که در صفحه کلید اصلی [IBM XT] وجود نداشتند ، مانند کلید enter روی کی‌پد ، دو اسکن کد به طور متوالی ایجاد می کنند: یک بایت فرار(escape) `0xe0` و سپس یک بایت نمایانگر کلید. برای مشاهده لیست تمام اسکن‌کدهای مجموعه 1 و کلیدهای مربوط به آنها ، [ویکی OSDev][scancode set 1] را مشاهده کنید. + +[scancode set 1]: https://wiki.osdev.org/Keyboard#Scan_Code_Set_1 + +برای ترجمه اسکن کدها به کلیدها ، می توانیم از عبارت match استفاده کنیم: + +```rust +// in src/interrupts.rs + +extern "x86-interrupt" fn keyboard_interrupt_handler( + _stack_frame: &mut InterruptStackFrame) +{ + use x86_64::instructions::port::Port; + + let mut port = Port::new(0x60); + let scancode: u8 = unsafe { port.read() }; + + // new + let key = match scancode { + 0x02 => Some('1'), + 0x03 => Some('2'), + 0x04 => Some('3'), + 0x05 => Some('4'), + 0x06 => Some('5'), + 0x07 => Some('6'), + 0x08 => Some('7'), + 0x09 => Some('8'), + 0x0a => Some('9'), + 0x0b => Some('0'), + _ => None, + }; + if let Some(key) = key { + print!("{}", key); + } + + unsafe { + PICS.lock() + .notify_end_of_interrupt(InterruptIndex::Keyboard.as_u8()); + } +} +``` + +کد بالا فشردن کلیدهای عددی 9-0 را ترجمه کرده و کلیه کلیدهای دیگر را نادیده می گیرد. از عبارت [match] برای اختصاص یک کاراکتر یا `None` به هر اسکن کد استفاده می کند. سپس با استفاده از [`if let`] اپشن `key` را از بین می برد. با استفاده از همان نام متغیر `key` در الگو که یک روش معمول برای از بین بردن انواع`Option` در راست است تعریف قبلی را [سایه می زنیم]. + +[match]: https://doc.rust-lang.org/book/ch06-02-match.html +[`if let`]: https://doc.rust-lang.org/book/ch18-01-all-the-places-for-patterns.html#conditional-if-let-expressions +[سایه می زنیم]: https://doc.rust-lang.org/book/ch03-01-variables-and-mutability.html#shadowing + +اکنون می توانیم اعداد را بنویسیم: + +![QEMU printing numbers to the screen](qemu-printing-numbers.gif) + +ترجمه کلیدهای دیگر نیز به همین روش کار می کند. خوشبختانه کرت ای با نام [`pc-keyboard`] برای ترجمه اسکن‌کد مجموعه های اسکن‌کد 1 و 2 وجود دارد ، بنابراین لازم نیست که خودمان این را پیاده سازی کنیم. برای استفاده از کرت ، آن را به `Cargo.toml` اضافه کرده و در`lib.rs` خود وارد می کنیم: + +[`pc-keyboard`]: https://docs.rs/pc-keyboard/0.5.0/pc_keyboard/ + +```toml +# in Cargo.toml + +[dependencies] +pc-keyboard = "0.5.0" +``` + +اکنون میتوانیم از این کرت برای باز نویسی `keyboard_interrupt_handler` استفاده کنیم: + +```rust +// in/src/interrupts.rs + +extern "x86-interrupt" fn keyboard_interrupt_handler( + _stack_frame: &mut InterruptStackFrame) +{ + use pc_keyboard::{layouts, DecodedKey, HandleControl, Keyboard, ScancodeSet1}; + use spin::Mutex; + use x86_64::instructions::port::Port; + + lazy_static! { + static ref KEYBOARD: Mutex> = + Mutex::new(Keyboard::new(layouts::Us104Key, ScancodeSet1, + HandleControl::Ignore) + ); + } + + let mut keyboard = KEYBOARD.lock(); + let mut port = Port::new(0x60); + + let scancode: u8 = unsafe { port.read() }; + if let Ok(Some(key_event)) = keyboard.add_byte(scancode) { + if let Some(key) = keyboard.process_keyevent(key_event) { + match key { + DecodedKey::Unicode(character) => print!("{}", character), + DecodedKey::RawKey(key) => print!("{:?}", key), + } + } + } + + unsafe { + PICS.lock() + .notify_end_of_interrupt(InterruptIndex::Keyboard.as_u8()); + } +} +``` + +ما از ماکرو `lazy_static` برای ایجاد یک شی ثابت [`Keyboard`] محافظت شده توسط Mutex استفاده می کنیم. `Keyboard` را با طرح صفحه کلید ایالات متحده و مجموعه اسکن کد 1 مقداردهی می کنیم. پارامتر [`HandleControl`] اجازه می دهد تا `ctrl+[a-z]` را به کاراکتر های `U+0001` تا `U+001A` نگاشت کنیم. ما نمی خواهیم چنین کاری انجام دهیم ، بنابراین از گزینه `Ignore` برای برخورد با `ctrl` مانند کلیدهای عادی استفاده می کنیم. + +[`HandleControl`]: https://docs.rs/pc-keyboard/0.5.0/pc_keyboard/enum.HandleControl.html + +در هر وقفه ، Mutex را قفل می کنیم ، اسکن کد را از کنترل کننده صفحه کلید می خوانیم و آن را به متد [`add_byte`] منتقل می کنیم ، که اسکن کد را به یک ` + +این بلاگ بصورت آزاد روی [گیت‌هاب] توسعه داده شده است. اگر شما مشکل یا سوالی دارید، لطفاً آن‌جا یک ایشو باز کنید. شما همچنین می‌توانید [در زیر] این پست کامنت بگذارید. منبع کد کامل این پست را می‌توانید در بِرَنچ [`post-08`][post branch] پیدا کنید. + +[گیت‌هاب]: https://github.com/phil-opp/blog_os +[در زیر]: #comments +[post branch]: https://github.com/phil-opp/blog_os/tree/post-08 + + + +## محافظت از حافظه + +یکی از وظایف اصلی یک سیستم‌عامل جداسازی (ایزوله کردن) برنامه‌ها از یکدیگر است. به عنوان مثال، مرورگر وب شما نباید در کار ویرایشگر متن تداخلی ایجاد کند. برای دستیابی به این هدف، سیستم‌عامل‌ها از قابلیتی سخت‌افزاری استفاده کرده تا اطمینان حاصل کنند که حافظه مربوط به یک پروسه، توسط پروسه‌ای دیگر غیر قابل دسترس است. رویکردهای مختلفی وجود دارد که به سخت‌افزار و پیاده‌سازی سیستم عامل بستگی دارد. + +به عنوان مثال‌، برخی از پردازنده‌های ARM Cortex-M (برای سیستم‌های تعبیه شده استفاده می‌شوند) دارای یک [_واحد محافظت از حافظه_] (Memory Protection Unit: MPU) هستند، که به شما این امکان را می‌دهد که تعداد کمی از ناحیه حافظه (مانند 8) را با مجوزهای دسترسی متفاوت تعریف کنید (به عنوان مثال عدم دسترسی، فقط خواندنی، خواندنی-نوشتنی). در هر دسترسی به حافظه، MPU اطمینان حاصل می‌کند که آدرس در ناحیه‌ای با مجوزهای دسترسی صحیح قرار دارد و در غیر این‌صورت یک استثنا ایجاد می‌کند. با تغییر ناحیه و مجوزهای دسترسی در هر تعویض پروسه (ترجمه: process switch)، سیستم‌عامل می‌تواند اطمینان حاصل کند که هر پروسه فقط به حافظه خود دسترسی پیدا می‌کند و بنابراین پروسه‌ها را ایزوله می‌کند. + +[_واحد محافظت از حافظه_]: https://developer.arm.com/docs/ddi0337/e/memory-protection-unit/about-the-mpu + +در x86، سخت‌افزار از دو روش مختلف برای محافظت از حافظه پشتیبانی می‌کند: [قطعه‌بندی] و [صفحه‌بندی]. + +[قطعه‌بندی]: https://en.wikipedia.org/wiki/X86_memory_segmentation +[صفحه‌بندی]: https://en.wikipedia.org/wiki/Virtual_memory#Paged_virtual_memory + +## قطعه‌بندی + +قطعه‌بندی قبلاً در سال 1978 برای افزایش میزان حافظه‌‌ی آدرس پذیر معرفی شده بود. وضعیت در آن زمان این بود که پردازنده‌ها فقط از آدرس‌های 16 بیتی استفاده می‌کردند که باعث کاهش حافظه آدرس پذیر به 64KiB می‌شد. برای دسترسی بیشتر از این 64KiB،‌ ثبات‌های قطعه‌ی اضافی معرفی شدند که هر کدام حاوی یک offset هستند. پردازنده به طور خودکار این آفست را بر روی هر دسترسی به حافظه اضافه می‌کند، بنابراین حداکثر ۱ مگابایت حافظه قابل دسترسی است. + +بسته به نوع دسترسی به حافظه، ثبات قطعه به طور خودکار توسط پردازنده انتخاب می‌شود: برای دستورالعمل‌های واکشی (ترجمه: fetching)، از کد `CS` و برای عملیات‌های پشته (push/pop) پشته قطعه `SS` استفاده می‌شود. سایر دستورالعمل‌ها ازقطعه‌ی داده `DS` یا قطعه‌ی اضافه `ES` استفاده می‌کنند. بعدها دو ثبات قطعه‌ی اضافی `FS` و `GS` اضافه شدند که می‌توانند آزادانه مورد استفاده قرار گیرند. + +در نسخه اول قطعه‌بندی، ثبات‌های قطعه مستقیماً شامل آفست بودند و هیچ كنترل دسترسی انجام نمی‌شد. بعدها با معرفی [_حالت محافظت شده_] این مورد تغییر کرد. هنگامی که پردازنده در این حالت اجرا می‌شود، توصیف کنندگان قطعه شامل یک فهرست در یک [_جدول توصیف‌کننده_] محلی یا سراسری هستند - که علاوه بر آدرس آفست - اندازه و مجوزهای دسترسی را نیز در خود دارد. با بارگذاری جدول‌‌های توصیف‌کننده سراسری/محلی برای هر فرآیند که دسترسی حافظه را به ناحیه حافظه خود فرآیند محدود می‌کند، سیستم‌عامل می‌تواند فرایندها را از یکدیگر جدا کند. + +[_حالت محافظت شده_]: https://en.wikipedia.org/wiki/X86_memory_segmentation#Protected_mode +[_جدول توصیف‌کننده_]: https://en.wikipedia.org/wiki/Global_Descriptor_Table + +با اصلاح آدرس‌های حافظه قبل از دسترسی واقعی، قطعه‌بندی از تکنیکی استفاده کرده است که اکنون تقریباً در همه جا استفاده می شود: _حافظه مجازی‌_. + +### حافظه مجازی + +ایده پشت حافظه مجازی این است که آدرس‌های حافظه را از دستگاه ذخیره‌سازی فیزیکی زیرین، دور کنید. به جای دسترسی مستقیم به دستگاه ذخیره‌سازی، ابتدا مرحله ترجمه انجام می‌شود. برای قطعه‌بندی، مرحله ترجمه، افزودن آدرس آفست قطعه‌ی فعال است. تصور کنید یک برنامه به آدرس حافظه `0x1234000` در قطعه‌ای با آفست` 0x1111000` دسترسی پیدا کند: آدرسی که واقعاً قابل دسترسی است `0x2345000` است. + +برای تمایز بین دو نوع آدرس، به آدرس‌های قبل از ترجمه _مجازی_ و به آدرس‌های بعد از ترجمه _فیزیکی_ گفته می‌شود. یک تفاوت مهم بین این دو نوع آدرس این است که آدرس‌های فیزیکی منحصربه‌فرد هستند و همیشه به همان مکان حافظه متمایز اشاره دارند. از طرف دیگر آدرس‌های مجازی به تابع ترجمه بستگی دارد. کاملاً ممکن است که دو آدرس مجازی مختلف به همان آدرس فیزیکی اشاره داشته باشند. همچنین، آدرس‌های مجازی یکسان می‌توانند هنگام استفاده از توابع ترجمه مختلف، به آدرس‌های فیزیکی مختلفی مراجعه کنند. + +برای مثال هنگامی که می‌خواهید یک برنامه را دو بار بصورت موازی اجرا کنید، این خاصیت مفید است. + +![Two virtual address spaces with address 0–150, one translated to 100–250, the other to 300–450](segmentation-same-program-twice.svg) + +در اینجا همان برنامه دو بار اجرا می‌شود ، اما با تابع‌های ترجمه مختلف. نمونه اول دارای آفست قطعه 100 است، بنابراین آدرس‌های مجازی 0–150 به آدرس های فیزیکی 100–250 ترجمه می‌شوند. نمونه دوم دارای آفست قطعه 300 است، که آدرس‌های مجازی 0–150 را به آدرس‌های فیزیکی 300–450 ترجمه می‌کند. این به هر دو برنامه این امکان را می‌دهد تا بدون تداخل با یکدیگر کد یکسانی را اجرا کنند و از آدرس‌های مجازی یکسان استفاده کنند. + +مزیت دیگر این است که برنامه‌ها می‌توانند در مکان‌های حافظه فیزیکی دلخواه قرار بگیرند، حتی اگر از آدرس‌های مجازی کاملاً متفاوتی استفاده کنند. بنابراین، سیستم‌عامل می‌تواند از مقدار کامل حافظه موجود بدون نیاز به کامپایل مجدد برنامه‌ها استفاده کند. + +### تکه‌تکه شدن + +تمایز بین آدرس‌های مجازی و فیزیکی قطعه‌بندی را واقعا قدرتمند می‌کند. با این حال، مشکل تکه‌تکه شدن (ترجمه: fragmentation) دارد. به عنوان مثال، تصور کنید که می‌خواهیم نسخه سوم برنامه‌ای را که در بالا دیدیم اجرا کنیم: + +![Three virtual address spaces, but there is not enough continuous space for the third](segmentation-fragmentation.svg) + +هیچ راهی برای نگاشت کردن نمونه سوم برنامه روی حافظه مجازی بدون همپوشانی وجود ندارد، حتی اگر حافظه آزاد بیش از اندازه کافی در دسترس باشد. مشکل این است که ما به حافظه _یکپارچه_ نیاز داریم و نمی‌توانیم از تکه‌های کوچک استفاده کنیم. + +یکی از راه‌های مقابله با این تکه‌تکه شدن، وقفه/مکث (pause) در اجرا است، انتقال قسمت‌های استفاده شده حافظه به سمت یکدیگر تا این قسمت‌ها به هم بچسبند و فضای تکه‌تکه شده بین آن‌ها پر شود، سپس به روزرسانی ترجمه و اجرای مجدد آن است: + +![Three virtual address spaces after defragmentation](segmentation-fragmentation-compacted.svg) + +اکنون فضای یکپارچه کافی برای شروع نمونه سوم برنامه ما وجود دارد. + +نقطه ضعف این فرآیند یکپارچه‌سازی (ترجمه: defragmentation) قطعات این است که نیاز به کپی کردن مقدار زیادی حافظه است که باعث کاهش کارایی می‌شود. همچنین لازم است قبل از اینکه حافظه بیش از حد تکه‌تکه شود، این کار به طور منظم انجام شود. این باعث می‌شود کارایی غیرقابل پیش‌بینی باشد، زیرا برنامه‌ها به طور تصادفی دچار وقفه می‌شوند و ممکن است ناپاسخگو (ترجمه: unresponsive) شوند. + +مشکل تکه‌تکه شدن یکی از دلایلی است که قطعه‌بندی دیگر توسط اکثر سیستم‌ها استفاده نمی‌شود. در واقع‌، قطعه‌بندی حتی در حالت 64 بیتی روی x86 دیگر پشتیبانی نمی‌شود. در عوض از _صفحه‌بندی_ استفاده می‌شود، که به طور کامل از مشکل تکه‌تکه شدن جلوگیری می‌کند. + +## صفحه‌بندی + +ایده این است که هر دو فضای حافظه مجازی و فیزیکی را به بلوک‌های کوچک و با اندازه ثابت تقسیم کنید. بلوک‌های فضای حافظه مجازی _صفحه‌ها_ و بلوک‌های فضای آدرس فیزیکی _قاب‌ها_ نامیده می‌شوند. هر صفحه را می‌توان به صورت جداگانه به یک قاب نگاشت کرد‌، که باعث می‌شود ناحیه حافظه بزرگتر در قاب‌های فیزیکی غیر یکپارچه تقسیم شوند. + +اگر مثالِ فضای حافظه تکه‌تکه شده را خلاصه کنیم، مزیت این امر قابل مشاهده می‌شود، اما این بار به جای قطعه‌بندی از صفحه‌بندی استفاده می‌کنیم: + +![With paging the third program instance can be split across many smaller physical areas](paging-fragmentation.svg) + +در این مثال یک صفحه با اندازه 50 بایت داریم، به این معنی که هر یک از ناحیه حافظه ما در سه صفحه تقسیم شده است. هر صفحه به صورت جداگانه به یک قاب نگاشت می‌شود، بنابراین می‌توان یک منطقه حافظه مجازی یکپارچه را به قاب‌های فیزیکی غیر یکپارچه نگاشت کرد. که به ما این امکان را می‌دهد تا نمونه سوم برنامه را بدون انجام هرگونه یکپارچه‌سازی شروع کنیم. + +### تکه‌تکه شدن مخفی + +در مقایسه با قطعه‌بندی‌، صفحه‌بندی به جای چند منطقه بزرگ و متغیر، از تعداد زیادی ناحیه حافظه کوچک و ثابت استفاده می‌کند. از آن‌جا که هر قاب دارای اندازه یکسانی است، هیچ قابی وجود ندارد که از سایز صفحه‌های موجود کوچکتر باشد، پس تکه‌تکه شدن رخ نمی‌دهد. + +یا _به نظر_ می‌رسد که هیچ تکه‌تکه‌ شدنی رخ نمی‌دهد. هنوز یک نوع تکه‌تکه‌ شدن نخفی وجود دارد، به اصطلاح _تکه‌تکه شدن داخلی_. تکه‌تکه شدن داخلی اتفاق می‌افتد زیرا همه ناحیه حافظه دقیقاً مضربی از اندازه صفحه نیستند. برنامه‌ای با اندازه 101 را در مثال بالا تصور کنید: هنوز به سه صفحه با اندازه 50 نیاز دارد، بنابراین 49 بایت بیش از حد مورد نیاز اشغال می‌کند. برای تمایز بین دو نوع تکه‌تکه‌ شدن، نوعی تکه‌تکه‌ شدنی که هنگام استفاده از قطعه‌بندی اتفاق می‌افتد، _قطعه‌بندی خارجی_ نامیده می‌شود. + +تکه‌تکه شدن داخلی تأسف آور است، اما اغلب بهتر از تکه‌تکه شدن خارجی است که با قطعه‌بندی رخ می‌دهد. این هنوز حافظه را هدر می‌دهد، اما به یکپارچه‌سازی نیاز ندارد و میزان تکه‌تکه شدن را قابل پیش‌بینی می‌کند (به طور متوسط نیم صفحه در هر منطقه حافظه). + +### جدول صفحه‌ها + +دیدیم که هر یک از میلیون‌ها صفحه بالقوه به صورت جداگانه در یک قاب نگاشت می‌شوند. این اطلاعات نگاشت باید در جایی ذخیره شود. قطعه‌بندی برای هر منطقه حافظه فعال از یک ثبات انتخابگرِ قطعه‌ی جداگانه استفاده می‌کند، که برای صفحه‌بندی امکان پذیر نیست زیرا صفحات بیشتری نسبت به ثبات‌ها وجود دارد. در عوض صفحه‌بندی از یک ساختار جدول به نام _page table_ برای ذخیره اطلاعات نگاشت استفاده می کند. + +برای مثال بالا، جدول‌های صفحه به صورت زیر است: + +![Three page tables, one for each program instance. For instance 1 the mapping is 0->100, 50->150, 100->200. For instance 2 it is 0->300, 50->350, 100->400. For instance 3 it is 0->250, 50->450, 100->500.](paging-page-tables.svg) + +می‌بینیم که هر نمونه‌ی برنامه جدول صفحه خاص خود را دارد. یک اشاره‌گر به جدولی که در حال حاضر فعال است، در یک رجیستر مخصوص CPU ذخیره می‌شود. در `x86`، این ثبات `CR3` است. وظیفه سیستم‌عامل این است که قبل از اجرای هر نمونه‌ی برنامه، این رجیستر را با اشاره‌گر به جدول صفحه‌ی صحیح بارگذاری کند. + +در هر دسترسی به حافظه، CPU اشاره‌گر جدول را از ثبات می‌خواند و قاب نگاشته شده را برای صفحه قابل دسترسی در جدول جستجو می‌کند. این کار کاملاً بصورت سخت‌افزاری و کاملاً شفاف برای برنامه‌ی در حال اجرا، انجام می‌شود. برای سرعت بخشیدن به روند ترجمه، بسیاری از معماری‌های CPU حافظه پنهان (ترجمه: cache) ویژه‌ای دارند که نتایج آخرین ترجمه‌ها را به خاطر می‌سپارد. + +بسته به معماری، ورودی‌های جدول صفحه همچنین می‌توانند ویژگی‌هایی مانند مجوزهای دسترسی را در فیلد پرچم‌ها ذخیره کنند. در مثال بالا، پرچم "r/w" صفحه را، خواندنی و قابل نوشتن می‌کند. + +### جدول های صفحه چند سطحی + +جدول‌های صفحه ساده که اخیراً دیدیم در فضاهای آدرس بزرگتر مشکل دارند: آن‌ها حافظه را هدر می‌دهند. به عنوان مثال، برنامه‌ای را تصور کنید که از چهار صفحه مجازی `0`، `000_000_1`، `050_000_1` و `100_000_1` استفاده کند (ما از `_` به عنوان جداکننده هزاران استفاده می‌کنیم): + +![Page 0 mapped to frame 0 and pages `1_000_000`–`1_000_150` mapped to frames 100–250](single-level-page-table.svg) + +این فقط به 4 قاب فیزیکی نیاز دارد، اما جدول صفحه بیش از یک میلیون ورودی دارد. ما نمی‌توانیم ورودی‌های خالی را حذف کنیم زیرا در این صورت CPU دیگر نمی‌تواند مستقیماً به ورودی صحیح در فرآیند ترجمه پرش کند (به عنوان مثال، دیگر تضمین نمی‌شود که صفحه چهارم از ورودی چهارم استفاده کند). + +برای کاهش حافظه هدر رفته، می‌توانیم از یک **جدول صفحه دو سطحی** استفاده کنیم. ایده این است که ما از جدول‌های صفحه مختلف برای ناحیه آدرس مختلف استفاده می‌کنیم. یک جدول اضافی با عنوان جدول صفحه _level 2_ شامل نگاشت بین ناحیه آدرس و جدول‌های صفحه (سطح 1) است. + +این بهتر است با یک مثال توضیح داده شود. بیایید تعریف کنیم که هر جدول صفحه 1 سطح مربوط به منطقه‌ای با اندازه `000_10` است. سپس جدول‌های زیر برای مثال نگاشت بالا وجود دارد: + +![Page 0 points to entry 0 of the level 2 page table, which points to the level 1 page table T1. The first entry of T1 points to frame 0, the other entries are empty. Pages `1_000_000`–`1_000_150` point to the 100th entry of the level 2 page table, which points to a different level 1 page table T2. The first three entries of T2 point to frames 100–250, the other entries are empty.](multilevel-page-table.svg) + +صفحه 0 در اولین بایت منطقه `000_10` قرار می‌گیرد، بنابراین از اولین ورودی جدول صفحه سطح 2 استفاده می‌کند. این ورودی به جدول صفحه 1 سطح T1 اشاره دارد که مشخص می کند صفحه `0` به قاب `0` اشاره می‌کند. + +صفحات `000_000_1` ،`050_000_1` و `100_000_1` همگی در منطقه صدم `000_10` بایت قرار می‌گیرند، بنابراین آن‌ها از ورودی صدم در جدول صفحه سطح 2 استفاده می‌کنند. این ورودی در جدول سطح 1 صفحه T2 متفاوت است که سه صفحه را با قاب‌های `100`، `150` و `200` نگاشت می‌کند. توجه داشته باشید که آدرس صفحه در جدول‌‌های سطح 1 شامل آفست منطقه نیست، به عنوان مثال، ورودی صفحه `050_000_1` فقط `50` است. + +ما هنوز 100 ورودی خالی در جدول سطح 2 داریم، اما بسیار کمتر از یک میلیون ورودی خالیِ قبل است. دلیل این پس‌انداز این است که نیازی به ایجاد جدول‌های صفحه سطح 1 برای ناحیه حافظه نگاشت نشده بین `000_10` و `000_000_1` نداریم. + +قاعده جدول‌های صفحه دو سطحی را می‌توان به سه، چهار یا بیشتر سطح گسترش داد. سپس ثبات جدول صفحه به جدول بالاترین سطح اشاره می‌کند، که به جدول سطح پایین بعدی اشاره می‌کند، که به سطح پایین بعدی اشاره می‌کند و این روال ادامه پیدا می‌کند. جدول صفحه سطح 1 سپس به قاب نگاشته شده اشاره می‌کند. این قاعده را به صورت کلی،‌ جدول صفحات _چند سطحی_ \(ترجمه: multilevel) یا _سلسله مراتبی‌_ \(ترجمه: hierarchical) می‌نامند. + +اکنون که از نحوه کار جدول‌های صفحه‌بندی و صفحه‌های چند سطحی مطلع شدیم، می‌توانیم به نحوه پیاده‌سازی در معماری x86_64 توجه کنیم (در ادامه فرض می‌کنیم CPU در حالت 64 بیتی کار می‌کند). + +## صفحه‌بندی در x86_64 + +معماری x86_64 از جدول صفحه 4 سطحی و اندازه صفحه 4KiB استفاده می‌کند. هر جدول صفحه، مستقل از سطح، دارای اندازه ثابت 512 ورودی است. اندازه هر ورودی 8 بایت است، پس بزرگی هر جدول 8B * 512 = 4KiB است و بنابراین دقیقاً در یک صفحه قرار می‌گیرد. + +اندیس جدول صفحه برای سطح مستقیماً از آدرس مجازی مشتق می‌شود: + +![Bits 0–12 are the page offset, bits 12–21 the level 1 index, bits 21–30 the level 2 index, bits 30–39 the level 3 index, and bits 39–48 the level 4 index](x86_64-table-indices-from-address.svg) + +می‌بینیم که هر اندیس جدول از 9 بیت تشکیل شده است، که منطقی است زیرا هر جدول دارای 512 = 9^2 ورودی است. کمترین 12 بیت در صفحه 4KiB آفست هستند (2^12 بایت = 4 کیلوبایت). بیت های 48 تا 64 کنار گذاشته می‌شوند، به این معنی که x86_64 در واقع 64 بیتی نیست زیرا فقط از آدرس های 48 بیتی پشتیبانی می‌کند. + +[جدول صفحه 5 سطحی]: https://en.wikipedia.org/wiki/Intel_5-level_paging + +حتی اگر بیت‌های 48 تا 64 کنار گذاشته‌شوند، نمی‌توان آن‌ها را روی مقادیر دلخواه تنظیم کرد. در عوض، همه بیت‌های این محدوده باید کپی از بیت 47 باشند تا آدرس‌ها منحصربه‌فرد باشند و extension های آینده مانند [جدول صفحه 5 سطحی] را ممکن کنند. این _sign-extension_ نامیده می‌شود زیرا بسیار شبیه به [extension علامت در مکمل دو] است. وقتی آدرس به درستی امضا نشده باشد، CPU یک استثنا را ارائه می‌دهد. + +[extension علامت در مکمل دو]: https://en.wikipedia.org/wiki/Two's_complement#Sign_extension + +شایان ذکر است که پردازنده‌های اخیر "Ice Lake" اینتل به صورت اختیاری از [جدول‌های صفحه 5 سطحی] پشتیبانی می‌کنند تا آدرس‌های مجازی را از 48 بیتی به 57 بیتی گسترش دهند. با توجه به این‌که بهینه‌سازی هسته ما برای یک CPU خاص در این مرحله منطقی نیست، ما در این پست فقط با جدول‌های صفحه 4 سطحیِ استاندارد کار خواهیم کرد. + +[جدول‌های صفحه 5 سطحی]: https://en.wikipedia.org/wiki/Intel_5-level_paging + +### مثالی از ترجمه + +بیایید مثالی بزنیم تا با جزئیات بفهمیم که روند ترجمه چگونه کار می‌کند: + +![An example 4-level page hierarchy with each page table shown in physical memory](x86_64-page-table-translation.svg) + +آدرس فیزیکی جدول صفحه سطح 4 که در حال حاضر فعال می‌باشد، و ریشه جدول صفحه سطح 4 است، در ثبات `CR3` ذخیره می‌شود. سپس هر ورودی جدول صفحه به قاب فیزیکی جدول سطح بعدی اشاره می‌کند. سپس ورودی جدول سطح 1 به قاب نگاشت شده اشاره می‌کند. توجه داشته باشید که تمام آدرس‌های موجود در جدول‌های صفحه فیزیکی هستند، به جای این‌که مجازی باشند، زیرا در غیر این‌صورت CPU نیاز به ترجمه آن آدرس‌ها نیز دارد (که این امر می‌تواند باعث بازگشت بی‌پایان شود). + +سلسله مراتب جدول صفحه بالا، دو صفحه را نگاشت می‌کند (به رنگ آبی). از اندیس‌های جدول صفحه می‌توان نتیجه گرفت که آدرس‌های مجازی این دو صفحه `0x803FE7F000` و `0x803FE00000` است. بیایید ببینیم چه اتفاقی می‌افتد وقتی برنامه سعی می‌کند از آدرس `0x803FE7F5CE` بخواند. ابتدا آدرس را به باینری تبدیل می‌کنیم و اندیس‌های جدول صفحه و آفست صفحه را برای آدرس تعیین می‌کنیم: + +![The sign extension bits are all 0, the level 4 index is 1, the level 3 index is 0, the level 2 index is 511, the level 1 index is 127, and the page offset is 0x5ce](x86_64-page-table-translation-addresses.png) + +با استفاده از این اندیس‌ها، اکنون می‌توانیم سلسله مراتب جدول صفحه را برای تعیین قاب نگاشته شده برای آدرس دنبال کنیم: + +- ما با خواندن آدرس جدول سطح 4 از ثبات `CR3` شروع می‌کنیم. +- اندیس سطح 4 برابر با 1 است، بنابراین ما به ورودی با اندیس 1 آن جدول نگاه می‌کنیم، که به ما می‌گوید جدول سطح 3 در آدرس 16KiB ذخیره شده است. +- ما جدول سطح 3 را از آن آدرس بارگیری می‌کنیم و ورودی با اندیس 0 را مشاهده می‌کنیم، که جدول سطح 2 در 24KiB را به ما نشان می‌دهد. +- اندیس سطح 2 برابر با 511 است، بنابراین ما برای یافتن آدرس جدول سطح 1 به آخرین ورودی آن صفحه نگاه می‌کنیم. +- از طریق ورودی با اندیس 127 جدول سطح 1، ما در نهایت متوجه می‌شویم که صفحه در قاب 12KiB، یا بصورت هگزادسیمال در 0x3000 نگاشت شده است. +- مرحله آخر افزودن آفست صفحه به آدرس قاب است تا آدرس فیزیکی 0x3000 + 0x5ce = 0x35ce بدست آید. + +![The same example 4-level page hierarchy with 5 additional arrows: "Step 0" from the CR3 register to the level 4 table, "Step 1" from the level 4 entry to the level 3 table, "Step 2" from the level 3 entry to the level 2 table, "Step 3" from the level 2 entry to the level 1 table, and "Step 4" from the level 1 table to the mapped frames.](x86_64-page-table-translation-steps.svg) + +مجوزهای صفحه در جدول سطح 1، مجوز "r" است، که به معنای فقط خواندن است. سخت‌افزار این مجوزها را اعمال می‌کند و اگر بخواهیم در آن صفحه بنویسیم یک استثنا را ایجاد می‌کند. مجوزها در صفحات سطح بالاتر مجوزهای احتمالی را در سطح پایین محدود می‌کنند، بنابراین اگر ورودی سطح 3 را فقط برای خواندن تنظیم کنیم، صفحه‌هایی که از این ورودی استفاده می‌کنند نیز قابل نوشتن نیستند، حتی اگر سطوح پایین‌تر مجوزهای خواندن/نوشتن را مشخص کرده باشند. + +توجه به این نکته مهم است که اگرچه این مثال فقط از یک نمونه از هر جدول استفاده می‌کند، به طور معمول از هر سطح در هر فضای آدرس چندین نمونه وجود دارد. در حالت حداکثری، موارد زیر وجود دارد: + +- یک جدول سطح 4، +- 512 جدول سطح 3 (زیرا جدول سطح 4 دارای 512 ورودی است)، +- 512 * 512 جدول سطح 2 (زیرا هر 512 جدولِ سطح 3 دارای 512 ورودی است)، و +- 512 * 512 * 512 جدول سطح 1 (512 ورودی برای هر جدول سطح 2). + +### قالب جدول صفحه + +جدول‌های صفحه در معماری x86_64 اساساً آرایه‌ای از 512 ورودی است. در سینتکس (کلمه: syntax) راست: + +```rust +#[repr(align(4096))] +pub struct PageTable { + entries: [PageTableEntry; 512], +} +``` + +همان‌طور که با ویژگی `repr` نشان داده شده است، جدول‌های صفحه باید صفحه تراز شوند، یعنی در یک مرز 4KiB تراز شوند. این نیاز تضمین می‌کند که یک جدول صفحه همیشه یک صفحه کامل را پر می‌کند و به بهینه‌سازی اجازه می‌دهد که ورودی‌ها را بسیار جمع و جور کند. + +هر ورودی 8 بایت (64 بیت) اندازه دارد و دارای قالب زیر است: + +Bit(s) | Name | Meaning +------ | ---- | ------- +0 | present | the page is currently in memory +1 | writable | it's allowed to write to this page +2 | user accessible | if not set, only kernel mode code can access this page +3 | write through caching | writes go directly to memory +4 | disable cache | no cache is used for this page +5 | accessed | the CPU sets this bit when this page is used +6 | dirty | the CPU sets this bit when a write to this page occurs +7 | huge page/null | must be 0 in P1 and P4, creates a 1GiB page in P3, creates a 2MiB page in P2 +8 | global | page isn't flushed from caches on address space switch (PGE bit of CR4 register must be set) +9-11 | available | can be used freely by the OS +12-51 | physical address | the page aligned 52bit physical address of the frame or the next page table +52-62 | available | can be used freely by the OS +63 | no execute | forbid executing code on this page (the NXE bit in the EFER register must be set) + +می‌بینیم که فقط بیت‌های 12–51 برای ذخیره آدرس قاب فیزیکی استفاده می‌شود، بیت‌های باقی‌مانده به عنوان پرچم استفاده می‌شوند یا توسط سیستم‌عامل می‌توانند آزادانه استفاده شوند. این امکان وجود دارد زیرا ما همیشه به یک آدرس تراز شده 4096 بایت، یا به یک جدول صفحه تراز شده با صفحه یا به شروع یک قاب نگاشت شده، اشاره می‌کنیم. این بدان معناست که بیت‌های 0–11 همیشه صفر هستند، بنابراین دلیلی برای ذخیره این بیت‌ها وجود ندارد زیرا سخت‌افزار می‌تواند آن‌ها را قبل از استفاده از آدرس صفر کند. این مورد در بیت‌های 52-63 نیز صدق می‌کند، زیرا معماری x86_64 فقط از آدرس‌های فیزیکی 52 بیتی پشتیبانی می‌کند (همان‌طور که فقط از آدرس‌های مجازی 48 بیتی پشتیبانی می‌کند). + +بیایید نگاهی دقیق‌تر به پرچم‌های موجود بیندازیم: + +- پرچم `present` صفحات نگاشت شده را از صفحات نگاشته نشده متمایز می‌کند. وقتی حافظه اصلی پر شود می‌توان از آن برای تعویض موقت صفحات روی دیسک استفاده کرد. وقتی متعاقباً به صفحه دسترسی پیدا شد، یک استثنای ویژه به نام _page fault_ اتفاق می‌افتد که سیستم‌عامل می‌تواند با بارگیری مجدد صفحه از دست رفته از دیسک و سپس ادامه برنامه‌، به آن واکنش نشان دهد. +- پرچم‌های `writable` و `no execute` به ترتیب کنترل می‌کنند که آیا محتوای صفحه، «قابل نوشتن» یا «حاوی دستورالعمل‌های اجرایی بودن» هستند. +- پرچم های `accessed` و `dirty` به طور خودکار هنگام پردازش یا نوشتن روی صفحه توسط CPU تنظیم می‌شوند. این اطلاعات می‌تواند توسط سیستم‌عامل مورد استفاده قرار گیرد. به عنوان مثال برای تصمیم‌گیری در مورد تعویض صفحه‌ها یا تغییر محتوای صفحه از آخرین ذخیره روی دیسک. +- پرچم‌های `write through caching` و `disable cache` امکان کنترل حافظه پنهان برای هر صفحه را به صورت جداگانه فراهم می‌کند. +- پرچم `user accessible` یک صفحه را در دسترس کد فضای کاربر قرار می‌دهد، در غیر این‌صورت فقط وقتی CPU در حالت هسته است، قابل دسترسی است. از این ویژگی می‌تواند برای سریع‌تر کردن [فراخوانی‌های سیستم] با نگه داشتن نگاشت هسته در حین اجرای برنامه فضای کاربر مورد استفاده قرار گیرد. با این وجود، آسیب‌پذیری [Spectre] می‌تواند به برنامه‌های فضای کاربر اجازه دهد این صفحات را بخوانند. +- پرچم `global` به سخت‌افزار سیگنال می‌دهد که یک صفحه در تمام فضاهای آدرس موجود است و بنابراین نیازی به حذف شدن از حافظه پنهان ترجمه نیست (به بخش TLB زیر مراجعه کنید) در تعویض‌های فضای آدرس. این پرچم معمولاً همراه با یک پرچم پاک شده `user accessible` برای نگاشت کد هسته در تمام فضاهای آدرس استفاده می‌شود. +- پرچم `large page` با اجازه دادن به ورودی جدول‌های صفحه سطح 2 یا سطح 3، اجازه ایجاد صفحاتی با اندازه بزرگتر را می‌دهد تا مستقیماً به یک قاب نگاشت شده اشاره کنند. با استفاده از این بیت، اندازه صفحه با ضریب 512 افزایش می‌یابد برای هر یک از 2MiB = 512 * 4KiB ورودی‌های سطح 2 یا 1GiB = 512 * 2MiB برای ورودی‌های سطح 3. مزیت استفاده از صفحات بزرگتر این است که به خطوط حافظه پنهان ترجمه کمتر و جدول‌های صفحه کمتر نیاز است. + +[فراخوانی‌های سیستم]: https://en.wikipedia.org/wiki/System_call +[Spectre]: https://en.wikipedia.org/wiki/Spectre_(security_vulnerability) + +کریت `x86_64` انواع مختلفی را برای [جدول‌های صفحه] و [ورودی‌های] آن‌ها فراهم می‌کند، بنابراین نیازی نیست که خودمان این ساختارها را ایجاد کنیم. + +[جدول‌های صفحه]: https://docs.rs/x86_64/0.13.2/x86_64/structures/paging/page_table/struct.PageTable.html +[ورودی‌های]: https://docs.rs/x86_64/0.13.2/x86_64/structures/paging/page_table/struct.PageTableEntry.html + +### بافر ترجمه Lookaside + +یک جدول صفحه 4 سطحی، ترجمه آدرس‌های مجازی را پُر هزینه‌ می‌کند، زیرا هر ترجمه به 4 دسترسی حافظه نیاز دارد. برای بهبود عملکرد، معماری x86_64 آخرین ترجمه‌ها را در _translation lookaside buffer_ یا به اختصار TLB ذخیره می‌کند. و این به ما اجازه می‌دهد تا از ترجمه کردن مجدد ترجمه‌هایی که در حافظه پنهان قرار دارند خودداری کنیم. + +برخلاف سایر حافظه‌های پنهان پردازنده، TLB کاملاً شفاف نبوده و با تغییر محتوای جدول‌های صفحه، ترجمه‌ها را به‌روز و حذف نمی‌کند. این بدان معنی است که هسته هر زمان که جدول صفحه را تغییر می‌دهد باید TLB را به صورت دستی به‌روز کند. برای انجام این کار، یک دستورالعمل ویژه پردازنده وجود دارد به نام [`invlpg`] ("صفحه نامعتبر") که ترجمه برای صفحه مشخص شده را از TLB حذف می‌کند، بنابراین دوباره از جدول صفحه در دسترسی بعدی بارگیری می‌شود. TLB همچنین می‌تواند با بارگیری مجدد رجیستر `CR3`، که یک تعویض فضای آدرس را شبیه‌سازی می‌کند، کاملاً فلاش (کلمه: flush) شود. کریت `x86_64` توابع راست را برای هر دو نوع در [ماژول `tlb`] فراهم می‌کند. + +[`invlpg`]: https://www.felixcloutier.com/x86/INVLPG.html +[ماژول `tlb`]: https://docs.rs/x86_64/0.13.2/x86_64/instructions/tlb/index.html + +مهم است که به یاد داشته باشید که TLB را روی هر جدول صفحه فلاش کنید، زیرا در غیر این‌صورت پردازنده ممکن است از ترجمه قدیمی استفاده کند، که می‌تواند منجر به باگ‌های غیرقطعی شود که اشکال‌زدایی آن بسیار سخت است. + +## پیاده‌سازی + +چیزی که ما هنوز به آن اشاره نکردیم: **هسته ما از قبل با صفحه‌بندی اجرا می‌شود**. بوت‌لودری که در پست ["یک هسته مینیمال با Rust"] اضافه کردیم، قبلاً یک سلسله مراتب صفحه‌بندی 4 سطح را تنظیم کرده است که هر صفحه از هسته ما را در یک قاب فیزیکی نگاشت می‌کند. بوت‌لودر این کار را انجام می‌دهد زیرا صفحه‌بندی در حالت 64 بیتی در x86_64 اجباری است. + +["یک هسته مینیمال با Rust"]: @/edition-2/posts/02-minimal-rust-kernel/index.fa.md#skht-dyskh-ymyj + +این بدان معناست که هر آدرس حافظه‌ای که در هسته خود استفاده کردیم یک آدرس مجازی بود. دسترسی به بافر VGA در آدرس `0xb8000` فقط به این دلیل کار کرد که بوت‌لودر آن صفحه حافظه را نگاشت یکتا (ترجمه: identity mapped) کرد، یعنی صفحه مجازی `0xb8000` را با فریم فیزیکی `0xb8000` نگاشت کرده است. + +صفحه‌بندی باعث می‌شود که هسته ما نسبتاً ایمن باشد، زیرا هر دسترسی به حافظه که از مرز خارج شود باعث ایجاد استثنای خطای صفحه، به جای نوشتن روی حافظه فیزیکی تصادفی می‌شود. بوت‌لودر حتی مجوزهای دسترسی صحیح را برای هر صفحه تنظیم کرده است، به این معنی که فقط صفحات حاوی کد قابل اجرا هستند و فقط صفحات داده قابل نوشتن هستند. + +### خطاهای صفحه + +بیایید سعی کنیم با دسترسی به برخی از حافظه‌های خارج از هسته، باعث ایجاد خطای صفحه شویم. ابتدا، یک کنترل‌کننده خطای صفحه ایجاد می‌کنیم و آن را در IDT ثبت می‌کنیم، به‌طوری که به جای یک [خطای دوگانه] یک استثنای خطای صفحه مشاهده می‌کنیم: + +[خطای دوگانه]: @/edition-2/posts/06-double-faults/index.fa.md + +```rust +// in src/interrupts.rs + +lazy_static! { + static ref IDT: InterruptDescriptorTable = { + let mut idt = InterruptDescriptorTable::new(); + + […] + + idt.page_fault.set_handler_fn(page_fault_handler); // new + + idt + }; +} + +use x86_64::structures::idt::PageFaultErrorCode; +use crate::hlt_loop; + +extern "x86-interrupt" fn page_fault_handler( + stack_frame: &mut InterruptStackFrame, + error_code: PageFaultErrorCode, +) { + use x86_64::registers::control::Cr2; + + println!("EXCEPTION: PAGE FAULT"); + println!("Accessed Address: {:?}", Cr2::read()); + println!("Error Code: {:?}", error_code); + println!("{:#?}", stack_frame); + hlt_loop(); +} +``` + +ثبات [`CR2`] به‌طور خودکار توسط CPU روی خطای صفحه تنظیم می‌شود و حاوی آدرس مجازی قابل دسترسی است که باعث رخ دادن خطای صفحه شده است. ما برای خواندن و چاپ آن از تابع [`Cr2::read`] کریت ` x86_64` استفاده می‌کنیم. نوع [`PageFaultErrorCode`] اطلاعات بیشتری در مورد نوع دسترسی به حافظه‌ای که باعث خطای صفحه شده است، فراهم می کند، به عنوان مثال این امر به دلیل خواندن یا نوشتن بوده است. به همین دلیل ما آن را نیز چاپ می‌کنیم. بدون رفع خطای صفحه نمی‌توانیم به اجرا ادامه دهیم، بنابراین در انتها یک [hlt_loop] اضافه می‌کنیم. + +[`CR2`]: https://en.wikipedia.org/wiki/Control_register#CR2 +[`Cr2::read`]: https://docs.rs/x86_64/0.13.2/x86_64/registers/control/struct.Cr2.html#method.read +[`PageFaultErrorCode`]: https://docs.rs/x86_64/0.13.2/x86_64/structures/idt/struct.PageFaultErrorCode.html +[LLVM bug]: https://github.com/rust-lang/rust/issues/57270 +[`hlt_loop`]: @/edition-2/posts/07-hardware-interrupts/index.md#the-hlt-instruction + +اکنون می‌توانیم به برخی از حافظه‌های خارج از هسته خود دسترسی پیدا کنیم: + +```rust +// in src/main.rs + +#[no_mangle] +pub extern "C" fn _start() -> ! { + println!("Hello World{}", "!"); + + blog_os::init(); + + // new + let ptr = 0xdeadbeaf as *mut u32; + unsafe { *ptr = 42; } + + // as before + #[cfg(test)] + test_main(); + + println!("It did not crash!"); + blog_os::hlt_loop(); +} +``` + +هنگامی که آن را اجرا می‌کنیم، می‌بینیم که کنترل‌کننده خطای صفحه ما صدا زده می‌شود: + +![EXCEPTION: Page Fault, Accessed Address: VirtAddr(0xdeadbeaf), Error Code: CAUSED_BY_WRITE, InterruptStackFrame: {…}](qemu-page-fault.png) + +ثبات `CR2` در واقع حاوی` 0xdeadbeaf` هست، آدرسی که سعی کردیم به آن دسترسی پیدا کنیم. کد خطا از طریق [`CAUSED_BY_WRITE`] به ما می‌گوید که خطا هنگام تلاش برای انجام یک عملیات نوشتن رخ داده است. حتی از طریق [بیت‌هایی که تنظیم _نشده‌اند_][`PageFaultErrorCode`] اطلاعات بیشتری به ما می‌دهد. به عنوان مثال، عدم تنظیم پرچم `PROTECTION_VIOLATION` به این معنی است که خطای صفحه رخ داده است زیرا صفحه هدف وجود ندارد. + +[`CAUSED_BY_WRITE`]: https://docs.rs/x86_64/0.13.2/x86_64/structures/idt/struct.PageFaultErrorCode.html#associatedconstant.CAUSED_BY_WRITE + +می‌بینیم که اشاره‌گر دستورالعمل فعلی `0x2031b2` می‌باشد، بنابراین می‌دانیم که این آدرس به یک صفحه کد اشاره دارد. صفحات کد توسط بوت‌لودر بصورت فقط خواندنی نگاشت می‌شوند، بنابراین خواندن از این آدرس امکان‌پذیر است اما نوشتن باعث خطای صفحه می‌شود. می‌توانید این کار را با تغییر اشاره‌گر `0xdeadbeaf` به `0x2031b2` امتحان کنید: + +```rust +// Note: The actual address might be different for you. Use the address that +// your page fault handler reports. +let ptr = 0x2031b2 as *mut u32; + +// read from a code page +unsafe { let x = *ptr; } +println!("read worked"); + +// write to a code page +unsafe { *ptr = 42; } +println!("write worked"); +``` + +با کامنت کردن خط آخر، می‌بینیم که دسترسی خواندن کار می‌کند، اما دسترسی نوشتن باعث خطای صفحه می‌شود: + +![QEMU with output: "read worked, EXCEPTION: Page Fault, Accessed Address: VirtAddr(0x2031b2), Error Code: PROTECTION_VIOLATION | CAUSED_BY_WRITE, InterruptStackFrame: {…}"](qemu-page-fault-protection.png) + +می‌بینیم که پیام _"read worked"_ چاپ شده است، که نشان می‌دهد عملیات خواندن هیچ خطایی ایجاد نکرده است. با این حال، به جای پیام _"write worked"_ خطای صفحه رخ می‌دهد. این بار پرچم [`PROTECTION_VIOLATION`] علاوه بر پرچم [`CAUSED_BY_WRITE`] تنظیم شده است، که نشان‌دهنده‌ وجود صفحه است، اما عملیات روی آن مجاز نیست. در این حالت نوشتن در صفحه مجاز نیست زیرا صفحات کد به صورت فقط خواندنی نگاشت می‌شوند. + +[`PROTECTION_VIOLATION`]: https://docs.rs/x86_64/0.13.2/x86_64/structures/idt/struct.PageFaultErrorCode.html#associatedconstant.PROTECTION_VIOLATION + +### دسترسی به جدول‌های صفحه + +بیایید سعی کنیم نگاهی به جدول‌های صفحه بیندازیم که نحوه نگاشت هسته را مشخص می‌کند: + +```rust +// in src/main.rs + +#[no_mangle] +pub extern "C" fn _start() -> ! { + println!("Hello World{}", "!"); + + blog_os::init(); + + use x86_64::registers::control::Cr3; + + let (level_4_page_table, _) = Cr3::read(); + println!("Level 4 page table at: {:?}", level_4_page_table.start_address()); + + […] // test_main(), println(…), and hlt_loop() +} +``` + +تابع [`Cr3::read`] از ` x86_64` جدول صفحه سطح 4 که در حال حاضر فعال است را از ثبات `CR3` برمی‌گرداند. یک تاپل (کلمه: tuple) از نوع [`PhysFrame`] و [`Cr3Flags`] برمی‌گرداند. ما فقط به قاب علاقه‌مَندیم، بنابراین عنصر دوم تاپل را نادیده می‌گیریم. + +[`Cr3::read`]: https://docs.rs/x86_64/0.13.2/x86_64/registers/control/struct.Cr3.html#method.read +[`PhysFrame`]: https://docs.rs/x86_64/0.13.2/x86_64/structures/paging/frame/struct.PhysFrame.html +[`Cr3Flags`]: https://docs.rs/x86_64/0.13.2/x86_64/registers/control/struct.Cr3Flags.html + +هنگامی که آن را اجرا می‌کنیم، خروجی زیر را مشاهده می‌کنیم: + +``` +Level 4 page table at: PhysAddr(0x1000) +``` + +بنابراین جدول صفحه سطح 4 که در حال حاضر فعال است در آدرس `0x100` در حافظه _فیزیکی_ ذخیره می‌شود، همان‌طور که توسط نوع بسته‌بندی [`PhysAddr`] نشان داده شده است. حال سوال این است: چگونه می‌توانیم از هسته خود به این جدول دسترسی پیدا کنیم؟ + +[`PhysAddr`]: https://docs.rs/x86_64/0.13.2/x86_64/addr/struct.PhysAddr.html + +دسترسی مستقیم به حافظه فیزیکی در هنگام فعال بودن صفحه‌بندی امکان پذیر نیست، زیرا برنامه‌ها به راحتی می‌توانند محافظت از حافظه (ترجمه: memory protection) را دور بزنند و در غیر این‌صورت به حافظه سایر برنامه‌ها دسترسی پیدا می‌کنند. بنابراین تنها راه دسترسی به جدول از طریق برخی از صفحه‌های مجازی است که به قاب فیزیکی در آدرس`0x1000` نگاشت شده. این مشکل ایجاد نگاشت برای قاب‌های جدول صفحه یک مشکل کلی است، زیرا هسته به طور مرتب به جدول‌های صفحه دسترسی دارد، به عنوان مثال هنگام اختصاص پشته برای یک نخِ (ترجمه: thread) جدید. + +راه حل‌های این مشکل در پست بعدی با جزئیات توضیح داده شده است. + +## خلاصه + +این پست دو روش حفاظت از حافظه را ارائه می‌دهد: تقسیم‌بندی و صفحه‌بندی. در حالی که اولی از ناحیه حافظه با اندازه متغیر استفاده می‌کند و از تکه‌تکه شدن خارجی رنج می‌برد، دومی از صفحات با اندازه ثابت استفاده می‌کند و امکان کنترل دقیق‌تر مجوزهای دسترسی را فراهم می‌کند. + +صفحه‌بندی اطلاعات نگاشت صفحات موجود در جدول‌های صفحه با یک یا چند سطح را ذخیره می‌کند. معماری x86_64 از جدول‌های صفحه با 4 سطح و اندازه صفحه 4KiB استفاده می‌کند. سخت‌افزار به‌طور خودکار جدول‌های صفحه را مرور می‌کند و ترجمه‌های حاصل را در TLB ذخیره می‌کند. این بافر به طور شفاف به‌روز نمی‌شود و باید به صورت دستی با تغییر جدول صفحه، فلاش شود. + +ما فهمیدیم که هسته ما در حال حاضر در بالای صفحه‌بندی اجرا می‌شود و دسترسی غیرقانونی حافظه باعث استثناهای خطای صفحه می‌شود. ما سعی کردیم به جدول‌های صفحه فعلی دسترسی پیدا کنیم، اما قادر به انجام این کار نبودیم زیرا ثبات CR3 یک آدرس فیزیکی را ذخیره می‌کند که ما نمی‌توانیم مستقیماً از هسته به آن دسترسی داشته باشیم. + +## بعدی چیست؟ + +در پست بعدی نحوه پیاده‌سازی پشتیبانی برای صفحه‌بندی در هسته توضیح داده شده است. که روش‌های مختلفی برای دسترسی به حافظه فیزیکی از هسته ارائه می‌دهد، که دسترسی به جدول‌های صفحه‌ای که هسته در آن اجرا می‌شود را امکان‌پذیر می‌کند. در این مرحله ما می‌توانیم توابع را برای ترجمه آدرس‌های مجازی به فیزیکی و ایجاد نگاشت‌های جدید در جدول‌های صفحه پیاده‌سازی کنیم. diff --git a/blog/content/edition-2/posts/08-paging-introduction/index.md b/blog/content/edition-2/posts/08-paging-introduction/index.md index 83edd069..172dc176 100644 --- a/blog/content/edition-2/posts/08-paging-introduction/index.md +++ b/blog/content/edition-2/posts/08-paging-introduction/index.md @@ -235,8 +235,8 @@ Let's take a closer look at the available flags: The `x86_64` crate provides types for [page tables] and their [entries], so we don't need to create these structures ourselves. -[page tables]: https://docs.rs/x86_64/0.12.1/x86_64/structures/paging/page_table/struct.PageTable.html -[entries]: https://docs.rs/x86_64/0.12.1/x86_64/structures/paging/page_table/struct.PageTableEntry.html +[page tables]: https://docs.rs/x86_64/0.13.2/x86_64/structures/paging/page_table/struct.PageTable.html +[entries]: https://docs.rs/x86_64/0.13.2/x86_64/structures/paging/page_table/struct.PageTableEntry.html ### The Translation Lookaside Buffer @@ -245,7 +245,7 @@ A 4-level page table makes the translation of virtual addresses expensive, becau Unlike the other CPU caches, the TLB is not fully transparent and does not update or remove translations when the contents of page tables change. This means that the kernel must manually update the TLB whenever it modifies a page table. To do this, there is a special CPU instruction called [`invlpg`] (“invalidate page”) that removes the translation for the specified page from the TLB, so that it is loaded again from the page table on the next access. The TLB can also be flushed completely by reloading the `CR3` register, which simulates an address space switch. The `x86_64` crate provides Rust functions for both variants in the [`tlb` module]. [`invlpg`]: https://www.felixcloutier.com/x86/INVLPG.html -[`tlb` module]: https://docs.rs/x86_64/0.12.1/x86_64/instructions/tlb/index.html +[`tlb` module]: https://docs.rs/x86_64/0.13.2/x86_64/instructions/tlb/index.html It is important to remember flushing the TLB on each page table modification because otherwise the CPU might keep using the old translation, which can lead to non-deterministic bugs that are very hard to debug. @@ -300,8 +300,8 @@ extern "x86-interrupt" fn page_fault_handler( The [`CR2`] register is automatically set by the CPU on a page fault and contains the accessed virtual address that caused the page fault. We use the [`Cr2::read`] function of the `x86_64` crate to read and print it. The [`PageFaultErrorCode`] type provides more information about the type of memory access that caused the page fault, for example whether it was caused by a read or write operation. For this reason we print it too. We can't continue execution without resolving the page fault, so we enter a [`hlt_loop`] at the end. [`CR2`]: https://en.wikipedia.org/wiki/Control_register#CR2 -[`Cr2::read`]: https://docs.rs/x86_64/0.12.1/x86_64/registers/control/struct.Cr2.html#method.read -[`PageFaultErrorCode`]: https://docs.rs/x86_64/0.12.1/x86_64/structures/idt/struct.PageFaultErrorCode.html +[`Cr2::read`]: https://docs.rs/x86_64/0.13.2/x86_64/registers/control/struct.Cr2.html#method.read +[`PageFaultErrorCode`]: https://docs.rs/x86_64/0.13.2/x86_64/structures/idt/struct.PageFaultErrorCode.html [LLVM bug]: https://github.com/rust-lang/rust/issues/57270 [`hlt_loop`]: @/edition-2/posts/07-hardware-interrupts/index.md#the-hlt-instruction @@ -335,7 +335,7 @@ When we run it, we see that our page fault handler is called: The `CR2` register indeed contains `0xdeadbeaf`, the address that we tried to access. The error code tells us through the [`CAUSED_BY_WRITE`] that the fault occurred while trying to perform a write operation. It tells us even more through the [bits that are _not_ set][`PageFaultErrorCode`]. For example, the fact that the `PROTECTION_VIOLATION` flag is not set means that the page fault occurred because the target page wasn't present. -[`CAUSED_BY_WRITE`]: https://docs.rs/x86_64/0.12.1/x86_64/structures/idt/struct.PageFaultErrorCode.html#associatedconstant.CAUSED_BY_WRITE +[`CAUSED_BY_WRITE`]: https://docs.rs/x86_64/0.13.2/x86_64/structures/idt/struct.PageFaultErrorCode.html#associatedconstant.CAUSED_BY_WRITE We see that the current instruction pointer is `0x2031b2`, so we know that this address points to a code page. Code pages are mapped read-only by the bootloader, so reading from this address works but writing causes a page fault. You can try this by changing the `0xdeadbeaf` pointer to `0x2031b2`: @@ -359,7 +359,7 @@ By commenting out the last line, we see that the read access works, but the writ We see that the _"read worked"_ message is printed, which indicates that the read operation did not cause any errors. However, instead of the _"write worked"_ message a page fault occurs. This time the [`PROTECTION_VIOLATION`] flag is set in addition to the [`CAUSED_BY_WRITE`] flag, which indicates that the page was present, but the operation was not allowed on it. In this case, writes to the page are not allowed since code pages are mapped as read-only. -[`PROTECTION_VIOLATION`]: https://docs.rs/x86_64/0.12.1/x86_64/structures/idt/struct.PageFaultErrorCode.html#associatedconstant.PROTECTION_VIOLATION +[`PROTECTION_VIOLATION`]: https://docs.rs/x86_64/0.13.2/x86_64/structures/idt/struct.PageFaultErrorCode.html#associatedconstant.PROTECTION_VIOLATION ### Accessing the Page Tables @@ -385,9 +385,9 @@ pub extern "C" fn _start() -> ! { The [`Cr3::read`] function of the `x86_64` returns the currently active level 4 page table from the `CR3` register. It returns a tuple of a [`PhysFrame`] and a [`Cr3Flags`] type. We are only interested in the frame, so we ignore the second element of the tuple. -[`Cr3::read`]: https://docs.rs/x86_64/0.12.1/x86_64/registers/control/struct.Cr3.html#method.read -[`PhysFrame`]: https://docs.rs/x86_64/0.12.1/x86_64/structures/paging/frame/struct.PhysFrame.html -[`Cr3Flags`]: https://docs.rs/x86_64/0.12.1/x86_64/registers/control/struct.Cr3Flags.html +[`Cr3::read`]: https://docs.rs/x86_64/0.13.2/x86_64/registers/control/struct.Cr3.html#method.read +[`PhysFrame`]: https://docs.rs/x86_64/0.13.2/x86_64/structures/paging/frame/struct.PhysFrame.html +[`Cr3Flags`]: https://docs.rs/x86_64/0.13.2/x86_64/registers/control/struct.Cr3Flags.html When we run it, we see the following output: @@ -397,7 +397,7 @@ Level 4 page table at: PhysAddr(0x1000) So the currently active level 4 page table is stored at address `0x1000` in _physical_ memory, as indicated by the [`PhysAddr`] wrapper type. The question now is: how can we access this table from our kernel? -[`PhysAddr`]: https://docs.rs/x86_64/0.12.1/x86_64/addr/struct.PhysAddr.html +[`PhysAddr`]: https://docs.rs/x86_64/0.13.2/x86_64/addr/struct.PhysAddr.html Accessing physical memory directly is not possible when paging is active, since programs could easily circumvent memory protection and access memory of other programs otherwise. So the only way to access the table is through some virtual page that is mapped to the physical frame at address `0x1000`. This problem of creating mappings for page table frames is a general problem, since the kernel needs to access the page tables regularly, for example when allocating a stack for a new thread. diff --git a/blog/content/edition-2/posts/09-paging-implementation/index.md b/blog/content/edition-2/posts/09-paging-implementation/index.md index 6ff0e3cb..a5896642 100644 --- a/blog/content/edition-2/posts/09-paging-implementation/index.md +++ b/blog/content/edition-2/posts/09-paging-implementation/index.md @@ -219,7 +219,7 @@ The above code assumes that the last level 4 entry with index `0o777` (511) is r Alternatively to performing the bitwise operations by hand, you can use the [`RecursivePageTable`] type of the `x86_64` crate, which provides safe abstractions for various page table operations. For example, the code below shows how to translate a virtual address to its mapped physical address: -[`RecursivePageTable`]: https://docs.rs/x86_64/0.12.1/x86_64/structures/paging/mapper/struct.RecursivePageTable.html +[`RecursivePageTable`]: https://docs.rs/x86_64/0.13.2/x86_64/structures/paging/mapper/struct.RecursivePageTable.html ```rust // in src/memory.rs @@ -437,7 +437,7 @@ fn kernel_main(boot_info: &'static BootInfo) -> ! { First, we convert the `physical_memory_offset` of the `BootInfo` struct to a [`VirtAddr`] and pass it to the `active_level_4_table` function. We then use the `iter` function to iterate over the page table entries and the [`enumerate`] combinator to additionally add an index `i` to each element. We only print non-empty entries because all 512 entries wouldn't fit on the screen. -[`VirtAddr`]: https://docs.rs/x86_64/0.12.1/x86_64/addr/struct.VirtAddr.html +[`VirtAddr`]: https://docs.rs/x86_64/0.13.2/x86_64/addr/struct.VirtAddr.html [`enumerate`]: https://doc.rust-lang.org/core/iter/trait.Iterator.html#method.enumerate When we run it, we see the following output: @@ -550,7 +550,7 @@ The `VirtAddr` struct already provides methods to compute the indexes into the p Inside the loop, we again use the `physical_memory_offset` to convert the frame into a page table reference. We then read the entry of the current page table and use the [`PageTableEntry::frame`] function to retrieve the mapped frame. If the entry is not mapped to a frame we return `None`. If the entry maps a huge 2MiB or 1GiB page we panic for now. -[`PageTableEntry::frame`]: https://docs.rs/x86_64/0.12.1/x86_64/structures/paging/page_table/struct.PageTableEntry.html#method.frame +[`PageTableEntry::frame`]: https://docs.rs/x86_64/0.13.2/x86_64/structures/paging/page_table/struct.PageTableEntry.html#method.frame Let's test our translation function by translating some addresses: @@ -604,20 +604,20 @@ Translating virtual to physical addresses is a common task in an OS kernel, ther The base of the abstraction are two traits that define various page table mapping functions: - The [`Mapper`] trait is generic over the page size and provides functions that operate on pages. Examples are [`translate_page`], which translates a given page to a frame of the same size, and [`map_to`], which creates a new mapping in the page table. -- The [`MapperAllSizes`] trait implies that the implementor implements `Mapper` for all pages sizes. In addition, it provides functions that work with multiple page sizes such as [`translate_addr`] or the general [`translate`]. +- The [`Translate`] trait provides functions that work with multiple page sizes such as [`translate_addr`] or the general [`translate`]. -[`Mapper`]: https://docs.rs/x86_64/0.12.1/x86_64/structures/paging/mapper/trait.Mapper.html -[`translate_page`]: https://docs.rs/x86_64/0.12.1/x86_64/structures/paging/mapper/trait.Mapper.html#tymethod.translate_page -[`map_to`]: https://docs.rs/x86_64/0.12.1/x86_64/structures/paging/mapper/trait.Mapper.html#method.map_to -[`MapperAllSizes`]: https://docs.rs/x86_64/0.12.1/x86_64/structures/paging/mapper/trait.MapperAllSizes.html -[`translate_addr`]: https://docs.rs/x86_64/0.12.1/x86_64/structures/paging/mapper/trait.MapperAllSizes.html#method.translate_addr -[`translate`]: https://docs.rs/x86_64/0.12.1/x86_64/structures/paging/mapper/trait.MapperAllSizes.html#tymethod.translate +[`Mapper`]: https://docs.rs/x86_64/0.13.2/x86_64/structures/paging/mapper/trait.Mapper.html +[`translate_page`]: https://docs.rs/x86_64/0.13.2/x86_64/structures/paging/mapper/trait.Mapper.html#tymethod.translate_page +[`map_to`]: https://docs.rs/x86_64/0.13.2/x86_64/structures/paging/mapper/trait.Mapper.html#method.map_to +[`Translate`]: https://docs.rs/x86_64/0.13.2/x86_64/structures/paging/mapper/trait.Translate.html +[`translate_addr`]: https://docs.rs/x86_64/0.13.2/x86_64/structures/paging/mapper/trait.Translate.html#method.translate_addr +[`translate`]: https://docs.rs/x86_64/0.13.2/x86_64/structures/paging/mapper/trait.Translate.html#tymethod.translate The traits only define the interface, they don't provide any implementation. The `x86_64` crate currently provides three types that implement the traits with different requirements. The [`OffsetPageTable`] type assumes that the complete physical memory is mapped to the virtual address space at some offset. The [`MappedPageTable`] is a bit more flexible: It only requires that each page table frame is mapped to the virtual address space at a calculable address. Finally, the [`RecursivePageTable`] type can be used to access page table frames through [recursive page tables](#recursive-page-tables). -[`OffsetPageTable`]: https://docs.rs/x86_64/0.12.1/x86_64/structures/paging/mapper/struct.OffsetPageTable.html -[`MappedPageTable`]: https://docs.rs/x86_64/0.12.1/x86_64/structures/paging/mapper/struct.MappedPageTable.html -[`RecursivePageTable`]: https://docs.rs/x86_64/0.12.1/x86_64/structures/paging/mapper/struct.RecursivePageTable.html +[`OffsetPageTable`]: https://docs.rs/x86_64/0.13.2/x86_64/structures/paging/mapper/struct.OffsetPageTable.html +[`MappedPageTable`]: https://docs.rs/x86_64/0.13.2/x86_64/structures/paging/mapper/struct.MappedPageTable.html +[`RecursivePageTable`]: https://docs.rs/x86_64/0.13.2/x86_64/structures/paging/mapper/struct.RecursivePageTable.html In our case, the bootloader maps the complete physical memory at a virtual address specfied by the `physical_memory_offset` variable, so we can use the `OffsetPageTable` type. To initialize it, we create a new `init` function in our `memory` module: @@ -643,11 +643,11 @@ unsafe fn active_level_4_table(physical_memory_offset: VirtAddr) The function takes the `physical_memory_offset` as an argument and returns a new `OffsetPageTable` instance with a `'static` lifetime. This means that the instance stays valid for the complete runtime of our kernel. In the function body, we first call the `active_level_4_table` function to retrieve a mutable reference to the level 4 page table. We then invoke the [`OffsetPageTable::new`] function with this reference. As the second parameter, the `new` function expects the virtual address at which the mapping of the physical memory starts, which is given in the `physical_memory_offset` variable. -[`OffsetPageTable::new`]: https://docs.rs/x86_64/0.12.1/x86_64/structures/paging/mapper/struct.OffsetPageTable.html#method.new +[`OffsetPageTable::new`]: https://docs.rs/x86_64/0.13.2/x86_64/structures/paging/mapper/struct.OffsetPageTable.html#method.new The `active_level_4_table` function should be only called from the `init` function from now on because it can easily lead to aliased mutable references when called multiple times, which can cause undefined behavior. For this reason, we make the function private by removing the `pub` specifier. -We now can use the `MapperAllSizes::translate_addr` method instead of our own `memory::translate_addr` function. We only need to change a few lines in our `kernel_main`: +We now can use the `Translate::translate_addr` method instead of our own `memory::translate_addr` function. We only need to change a few lines in our `kernel_main`: ```rust // in src/main.rs @@ -655,7 +655,7 @@ We now can use the `MapperAllSizes::translate_addr` method instead of our own `m fn kernel_main(boot_info: &'static BootInfo) -> ! { // new: different imports use blog_os::memory; - use x86_64::{structures::paging::MapperAllSizes, VirtAddr}; + use x86_64::{structures::paging::Translate, VirtAddr}; […] // hello world and blog_os::init @@ -676,7 +676,7 @@ fn kernel_main(boot_info: &'static BootInfo) -> ! { } ``` -We need to import the `MapperAllSizes` trait in order to use the [`translate_addr`] method it provides. +We need to import the `Translate` trait in order to use the [`translate_addr`] method it provides. When we run it now, we see the same translation results as before, with the difference that the huge page translation now also works: @@ -694,8 +694,8 @@ Until now we only looked at the page tables without modifying anything. Let's ch We will use the [`map_to`] function of the [`Mapper`] trait for our implementation, so let's take a look at that function first. The documentation tells us that it takes four arguments: the page that we want to map, the frame that the page should be mapped to, a set of flags for the page table entry, and a `frame_allocator`. The frame allocator is needed because mapping the given page might require creating additional page tables, which need unused frames as backing storage. -[`map_to`]: https://docs.rs/x86_64/0.12.1/x86_64/structures/paging/trait.Mapper.html#tymethod.map_to -[`Mapper`]: https://docs.rs/x86_64/0.12.1/x86_64/structures/paging/trait.Mapper.html +[`map_to`]: https://docs.rs/x86_64/0.13.2/x86_64/structures/paging/trait.Mapper.html#tymethod.map_to +[`Mapper`]: https://docs.rs/x86_64/0.13.2/x86_64/structures/paging/trait.Mapper.html #### A `create_example_mapping` Function @@ -734,8 +734,8 @@ In addition to the `page` that should be mapped, the function expects a mutable [impl-trait-arg]: https://doc.rust-lang.org/book/ch10-02-traits.html#traits-as-parameters [generic]: https://doc.rust-lang.org/book/ch10-00-generics.html -[`FrameAllocator`]: https://docs.rs/x86_64/0.12.1/x86_64/structures/paging/trait.FrameAllocator.html -[`PageSize`]: https://docs.rs/x86_64/0.12.1/x86_64/structures/paging/page/trait.PageSize.html +[`FrameAllocator`]: https://docs.rs/x86_64/0.13.2/x86_64/structures/paging/trait.FrameAllocator.html +[`PageSize`]: https://docs.rs/x86_64/0.13.2/x86_64/structures/paging/page/trait.PageSize.html The [`map_to`] method is unsafe because the caller must ensure that the frame is not already in use. The reason for this is that mapping the same frame twice could result in undefined behavior, for example when two different `&mut` references point to the same physical memory location. In our case, we reuse the VGA text buffer frame, which is already mapped, so we break the required condition. However, the `create_example_mapping` function is only a temporary testing function and will be removed after this post, so it is ok. To remind us of the unsafety, we put a `FIXME` comment on the line. @@ -747,8 +747,8 @@ The [`map_to`] function can fail, so it returns a [`Result`]. Since this is just [`Result`]: https://doc.rust-lang.org/core/result/enum.Result.html [`expect`]: https://doc.rust-lang.org/core/result/enum.Result.html#method.expect -[`MapperFlush`]: https://docs.rs/x86_64/0.12.1/x86_64/structures/paging/mapper/struct.MapperFlush.html -[`flush`]: https://docs.rs/x86_64/0.12.1/x86_64/structures/paging/mapper/struct.MapperFlush.html#method.flush +[`MapperFlush`]: https://docs.rs/x86_64/0.13.2/x86_64/structures/paging/mapper/struct.MapperFlush.html +[`flush`]: https://docs.rs/x86_64/0.13.2/x86_64/structures/paging/mapper/struct.MapperFlush.html#method.flush [must_use]: https://doc.rust-lang.org/std/result/#results-must-be-used #### A dummy `FrameAllocator` diff --git a/blog/content/edition-2/posts/10-heap-allocation/index.md b/blog/content/edition-2/posts/10-heap-allocation/index.md index d63f7eba..cdc23423 100644 --- a/blog/content/edition-2/posts/10-heap-allocation/index.md +++ b/blog/content/edition-2/posts/10-heap-allocation/index.md @@ -445,12 +445,12 @@ pub fn init_heap( The function takes mutable references to a [`Mapper`] and a [`FrameAllocator`] instance, both limited to 4KiB pages by using [`Size4KiB`] as generic parameter. The return value of the function is a [`Result`] with the unit type `()` as success variant and a [`MapToError`] as error variant, which is the error type returned by the [`Mapper::map_to`] method. Reusing the error type makes sense here because the `map_to` method is the main source of errors in this function. -[`Mapper`]:https://docs.rs/x86_64/0.12.1/x86_64/structures/paging/mapper/trait.Mapper.html -[`FrameAllocator`]: https://docs.rs/x86_64/0.12.1/x86_64/structures/paging/trait.FrameAllocator.html -[`Size4KiB`]: https://docs.rs/x86_64/0.12.1/x86_64/structures/paging/page/enum.Size4KiB.html +[`Mapper`]:https://docs.rs/x86_64/0.13.2/x86_64/structures/paging/mapper/trait.Mapper.html +[`FrameAllocator`]: https://docs.rs/x86_64/0.13.2/x86_64/structures/paging/trait.FrameAllocator.html +[`Size4KiB`]: https://docs.rs/x86_64/0.13.2/x86_64/structures/paging/page/enum.Size4KiB.html [`Result`]: https://doc.rust-lang.org/core/result/enum.Result.html -[`MapToError`]: https://docs.rs/x86_64/0.12.1/x86_64/structures/paging/mapper/enum.MapToError.html -[`Mapper::map_to`]: https://docs.rs/x86_64/0.12.1/x86_64/structures/paging/mapper/trait.Mapper.html#method.map_to +[`MapToError`]: https://docs.rs/x86_64/0.13.2/x86_64/structures/paging/mapper/enum.MapToError.html +[`Mapper::map_to`]: https://docs.rs/x86_64/0.13.2/x86_64/structures/paging/mapper/trait.Mapper.html#method.map_to The implementation can be broken down into two parts: @@ -464,18 +464,18 @@ The implementation can be broken down into two parts: - We use the [`Mapper::map_to`] method for creating the mapping in the active page table. The method can fail, therefore we use the [question mark operator] again to forward the error to the caller. On success, the method returns a [`MapperFlush`] instance that we can use to update the [_translation lookaside buffer_] using the [`flush`] method. -[`VirtAddr`]: https://docs.rs/x86_64/0.12.1/x86_64/addr/struct.VirtAddr.html -[`Page`]: https://docs.rs/x86_64/0.12.1/x86_64/structures/paging/page/struct.Page.html -[`containing_address`]: https://docs.rs/x86_64/0.12.1/x86_64/structures/paging/page/struct.Page.html#method.containing_address -[`Page::range_inclusive`]: https://docs.rs/x86_64/0.12.1/x86_64/structures/paging/page/struct.Page.html#method.range_inclusive -[`FrameAllocator::allocate_frame`]: https://docs.rs/x86_64/0.12.1/x86_64/structures/paging/trait.FrameAllocator.html#tymethod.allocate_frame +[`VirtAddr`]: https://docs.rs/x86_64/0.13.2/x86_64/addr/struct.VirtAddr.html +[`Page`]: https://docs.rs/x86_64/0.13.2/x86_64/structures/paging/page/struct.Page.html +[`containing_address`]: https://docs.rs/x86_64/0.13.2/x86_64/structures/paging/page/struct.Page.html#method.containing_address +[`Page::range_inclusive`]: https://docs.rs/x86_64/0.13.2/x86_64/structures/paging/page/struct.Page.html#method.range_inclusive +[`FrameAllocator::allocate_frame`]: https://docs.rs/x86_64/0.13.2/x86_64/structures/paging/trait.FrameAllocator.html#tymethod.allocate_frame [`None`]: https://doc.rust-lang.org/core/option/enum.Option.html#variant.None -[`MapToError::FrameAllocationFailed`]: https://docs.rs/x86_64/0.12.1/x86_64/structures/paging/mapper/enum.MapToError.html#variant.FrameAllocationFailed +[`MapToError::FrameAllocationFailed`]: https://docs.rs/x86_64/0.13.2/x86_64/structures/paging/mapper/enum.MapToError.html#variant.FrameAllocationFailed [`Option::ok_or`]: https://doc.rust-lang.org/core/option/enum.Option.html#method.ok_or [question mark operator]: https://doc.rust-lang.org/edition-guide/rust-2018/error-handling-and-panics/the-question-mark-operator-for-easier-error-handling.html -[`MapperFlush`]: https://docs.rs/x86_64/0.12.1/x86_64/structures/paging/mapper/struct.MapperFlush.html +[`MapperFlush`]: https://docs.rs/x86_64/0.13.2/x86_64/structures/paging/mapper/struct.MapperFlush.html [_translation lookaside buffer_]: @/edition-2/posts/08-paging-introduction/index.md#the-translation-lookaside-buffer -[`flush`]: https://docs.rs/x86_64/0.12.1/x86_64/structures/paging/mapper/struct.MapperFlush.html#method.flush +[`flush`]: https://docs.rs/x86_64/0.13.2/x86_64/structures/paging/mapper/struct.MapperFlush.html#method.flush The final step is to call this function from our `kernel_main`: diff --git a/blog/content/edition-2/posts/11-allocator-designs/index.md b/blog/content/edition-2/posts/11-allocator-designs/index.md index bf96864d..995a3beb 100644 --- a/blog/content/edition-2/posts/11-allocator-designs/index.md +++ b/blog/content/edition-2/posts/11-allocator-designs/index.md @@ -550,7 +550,7 @@ impl LinkedListAllocator { } ``` -The struct contains a `head` node that points to the first heap region. We are only interested in the value of the `next` pointer, so we set the `size` to 0 in the `ListNone::new` function. Making `head` a `ListNode` instead of just a `&'static mut ListNode` has the advantage that the implementation of the `alloc` method will be simpler. +The struct contains a `head` node that points to the first heap region. We are only interested in the value of the `next` pointer, so we set the `size` to 0 in the `ListNode::new` function. Making `head` a `ListNode` instead of just a `&'static mut ListNode` has the advantage that the implementation of the `alloc` method will be simpler. Like for the bump allocator, the `new` function doesn't initialize the allocator with the heap bounds. In addition to maintaining API compatibility, the reason is that the initialization routine requires to write a node to the heap memory, which can only happen at runtime. The `new` function, however, needs to be a [`const` function] that can be evaluated at compile time, because it will be used for initializing the `ALLOCATOR` static. For this reason, we again provide a separate, non-constant `init` method. @@ -948,8 +948,9 @@ For constructing a `FixedSizeBlockAllocator`, we provide the same `new` and `ini impl FixedSizeBlockAllocator { /// Creates an empty FixedSizeBlockAllocator. pub const fn new() -> Self { + const EMPTY: Option<&'static mut ListNode> = None; FixedSizeBlockAllocator { - list_heads: [None; BLOCK_SIZES.len()], + list_heads: [EMPTY; BLOCK_SIZES.len()], fallback_allocator: linked_list_allocator::Heap::empty(), } } @@ -965,7 +966,7 @@ impl FixedSizeBlockAllocator { } ``` -The `new` function just initializes the `list_heads` array with empty nodes and creates an [`empty`] linked list allocator as `fallback_allocator`. Since array initializations using non-`Copy` types are still unstable, we need to add **`#![feature(const_in_array_repeat_expressions)]`** to the beginning of our `lib.rs`. The reason that `None` is not `Copy` in this case is that `ListNode` does not implement `Copy`. Thus, the `Option` wrapper and its `None` variant are not `Copy` either. +The `new` function just initializes the `list_heads` array with empty nodes and creates an [`empty`] linked list allocator as `fallback_allocator`. The `EMPTY` constant is needed because to tell the Rust compiler that we want to initialize the array with a constant value. Initializing the array directly as `[None; BLOCK_SIZES.len()]` does not work because then the compiler requires that `Option<&'static mut ListNode>` implements the `Copy` trait, which is does not. This is a current limitation of the Rust compiler, which might go away in the future. [`empty`]: https://docs.rs/linked_list_allocator/0.8.0/linked_list_allocator/struct.Heap.html#method.empty diff --git a/blog/content/edition-2/posts/12-async-await/index.md b/blog/content/edition-2/posts/12-async-await/index.md index 69ddc684..bc58ec10 100644 --- a/blog/content/edition-2/posts/12-async-await/index.md +++ b/blog/content/edition-2/posts/12-async-await/index.md @@ -523,9 +523,9 @@ The internal pointer of our self-referential struct leads to a fundamental probl The `array` field starts at address 0x10014 and the `element` field at address 0x10020. It points to address 0x1001c because the last array element lives at this address. At this point, everything is still fine. However, an issue occurs when we move this struct to a different memory address: -![array at 0x10024 with fields 1, 2, and 3; element at address 0x10030, still pointing to 0x1001c, even though the last array element now lives at 0x1002a](self-referential-struct-moved.svg) +![array at 0x10024 with fields 1, 2, and 3; element at address 0x10030, still pointing to 0x1001c, even though the last array element now lives at 0x1002c](self-referential-struct-moved.svg) -We moved the struct a bit so that it starts at address `0x10024` now. This could for example happen when we pass the struct as a function argument or assign it to a different stack variable. The problem is that the `element` field still points to address `0x1001c` even though the last `array` element now lives at address `0x1002a`. Thus, the pointer is dangling with the result that undefined behavior occurs on the next `poll` call. +We moved the struct a bit so that it starts at address `0x10024` now. This could for example happen when we pass the struct as a function argument or assign it to a different stack variable. The problem is that the `element` field still points to address `0x1001c` even though the last `array` element now lives at address `0x1002c`. Thus, the pointer is dangling with the result that undefined behavior occurs on the next `poll` call. #### Possible Solutions @@ -1744,8 +1744,8 @@ impl Executor { Since we call `sleep_if_idle` directly after `run_ready_tasks`, which loops until the `task_queue` becomes empty, checking the queue again might seem unnecessary. However, a hardware interrupt might occur directly after `run_ready_tasks` returns, so there might be a new task in the queue at the time the `sleep_if_idle` function is called. Only if the queue is still empty, we put the CPU to sleep by executing the `hlt` instruction through the [`instructions::hlt`] wrapper function provided by the [`x86_64`] crate. -[`instructions::hlt`]: https://docs.rs/x86_64/0.12.1/x86_64/instructions/fn.hlt.html -[`x86_64`]: https://docs.rs/x86_64/0.12.1/x86_64/index.html +[`instructions::hlt`]: https://docs.rs/x86_64/0.13.2/x86_64/instructions/fn.hlt.html +[`x86_64`]: https://docs.rs/x86_64/0.13.2/x86_64/index.html Unfortunately, there is still a subtle race condition in this implementation. Since interrupts are asynchronous and can happen at any time, it is possible that an interrupt happens right between the `is_empty` check and the call to `hlt`: @@ -1758,9 +1758,9 @@ if self.task_queue.is_empty() { In case this interrupt pushes to the `task_queue`, we put the CPU to sleep even though there is now a ready task. In the worst case, this could delay the handling of a keyboard interrupt until the next keypress or the next timer interrupt. So how do we prevent it? -The answer is to disable interrupts on the CPU before the check and atomically enable them again together with the `hlt` instruction. This way, all interrupts that happen in between are delayed after the `hlt` instruction so that no wake-ups are missed. To implement this approach, we can use the [`enable_interrupts_and_hlt`] function provided by the [`x86_64`] crate. This function is only available since version 0.9.6, so you might need to update your `x86_64` dependency to use it. +The answer is to disable interrupts on the CPU before the check and atomically enable them again together with the `hlt` instruction. This way, all interrupts that happen in between are delayed after the `hlt` instruction so that no wake-ups are missed. To implement this approach, we can use the [`interrupts::enable_and_hlt`][`enable_and_hlt`] function provided by the [`x86_64`] crate. This function is only available since version 0.9.6, so you might need to update your `x86_64` dependency to use it. -[`enable_interrupts_and_hlt`]: https://docs.rs/x86_64/0.12.1/x86_64/instructions/interrupts/fn.enable_interrupts_and_hlt.html +[`enable_and_hlt`]: https://docs.rs/x86_64/0.13.2/x86_64/instructions/interrupts/fn.enable_and_hlt.html The updated implementation of our `sleep_if_idle` function looks like this: @@ -1769,11 +1769,11 @@ The updated implementation of our `sleep_if_idle` function looks like this: impl Executor { fn sleep_if_idle(&self) { - use x86_64::instructions::interrupts::{self, enable_interrupts_and_hlt}; + use x86_64::instructions::interrupts::{self, enable_and_hlt}; interrupts::disable(); if self.task_queue.is_empty() { - enable_interrupts_and_hlt(); + enable_and_hlt(); } else { interrupts::enable(); } @@ -1781,7 +1781,7 @@ impl Executor { } ``` -To avoid race conditions, we disable interrupts before checking whether the `task_queue` is empty. If it is, we use the [`enable_interrupts_and_hlt`] function to enable interrupts and put the CPU to sleep as a single atomic operation. In case the queue is no longer empty, it means that an interrupt woke a task after `run_ready_tasks` returned. In that case, we enable interrupts again and directly continue execution without executing `hlt`. +To avoid race conditions, we disable interrupts before checking whether the `task_queue` is empty. If it is, we use the [`enable_and_hlt`] function to enable interrupts and put the CPU to sleep as a single atomic operation. In case the queue is no longer empty, it means that an interrupt woke a task after `run_ready_tasks` returned. In that case, we enable interrupts again and directly continue execution without executing `hlt`. Now our executor properly puts the CPU to sleep when there is nothing to do. We can see that the QEMU process has a much lower CPU utilization when we run our kernel using `cargo run` again. diff --git a/blog/content/edition-2/posts/deprecated/04-unit-testing/index.md b/blog/content/edition-2/posts/deprecated/04-unit-testing/index.md index 689f4518..81a80343 100644 --- a/blog/content/edition-2/posts/deprecated/04-unit-testing/index.md +++ b/blog/content/edition-2/posts/deprecated/04-unit-testing/index.md @@ -34,7 +34,7 @@ Alternatively, consider reading the new [_Testing_] post instead. It sets up a s ## Unit Tests for `no_std` Binaries Rust has a [built-in test framework] that is capable of running unit tests without the need to set anything up. Just create a function that checks some results through assertions and add the `#[test]` attribute to the function header. Then `cargo test` will automatically find and execute all test functions of your crate. -[built-in test framework]: https://doc.rust-lang.org/book/second-edition/ch11-00-testing.html +[built-in test framework]: https://doc.rust-lang.org/book/ch11-00-testing.html Unfortunately it's a bit more complicated for `no_std` applications such as our kernel. If we run `cargo test` (without adding any test yet), we get the following error: @@ -250,7 +250,7 @@ error[E0277]: the trait bound `volatile::Volatile: core: The problem is that array construction in Rust requires that the contained type is [`Copy`]. The `ScreenChar` is `Copy`, but the `Volatile` wrapper is not. There is currently no easy way to circumvent this without using [`unsafe`], but fortunately there is the [`array_init`] crate that provides a safe interface for such operations. [`Copy`]: https://doc.rust-lang.org/core/marker/trait.Copy.html -[`unsafe`]: https://doc.rust-lang.org/book/second-edition/ch19-01-unsafe-rust.html +[`unsafe`]: https://doc.rust-lang.org/book/ch19-01-unsafe-rust.html [`array_init`]: https://docs.rs/array-init To use that crate, we add the following to our `Cargo.toml`: diff --git a/blog/static/css/edition-2/main.css b/blog/static/css/edition-2/main.css index 4a5c693e..32587de6 100644 --- a/blog/static/css/edition-2/main.css +++ b/blog/static/css/edition-2/main.css @@ -452,8 +452,13 @@ a strong { .right-to-left { direction: rtl; + font-family: Vazir; } -.left-to-right, .right-to-left pre, .right-to-left table { +.left-to-right, .right-to-left pre, .right-to-left table, .right-to-left[id="toc-aside"] { direction: ltr; } + +.status-update-list li { + margin-bottom: .5rem; +} diff --git a/blog/templates/edition-2/macros.html b/blog/templates/edition-2/macros.html index 77ffac7b..3db67187 100644 --- a/blog/templates/edition-2/macros.html +++ b/blog/templates/edition-2/macros.html @@ -15,11 +15,11 @@

{{ post.title }}

{{ post.summary | safe }} - read more » + {{ trans(key="readmore", lang=lang) | safe }} {%- if lang and not_translated and lang != config.default_language -%} {%- endif -%}
@@ -28,7 +28,7 @@ {% macro toc(toc) %}
- Table of Contents + {{ trans(key="toc", lang=lang) }} {% endif %} {% endfor %} - +
{% endmacro toc %} diff --git a/blog/templates/edition-2/page.html b/blog/templates/edition-2/page.html index 7ccfa1f2..af9fe75e 100644 --- a/blog/templates/edition-2/page.html +++ b/blog/templates/edition-2/page.html @@ -6,9 +6,9 @@ {% block title %}{{ page.title }} | {{ config.title }}{% endblock title %} {% block header %} {% if lang != "en" -%} - + {%- else -%} - + {%- endif %} {% endblock header %} @@ -17,8 +17,8 @@ {%- endblock description %} {% block toc_aside %} -
{% endif %} + {%- if page.lang != "en" %} -
+
{% set translations = page.translations | filter(attribute="lang", value="en") %} {% set original = translations.0 %}

- Translated Content: - This is a community translation of the {{ original.title }} post. It might be incomplete, outdated or contain errors. Please report any issues! + {{ trans(key="translated_content", lang=lang) }} + {{ trans(key="translated_content_notice", lang=lang) | + replace(from="_original.permalink_", to=original.permalink) | + replace(from="_original.title_", to=original.title) | safe }}

{%- if page.extra.translators %}

- Translation by {% for user in page.extra.translators -%} + {{ trans(key="translated_by", lang=lang) }} {% for user in page.extra.translators -%} {%- if not loop.first -%} - {%- if loop.last %}, and {% else %}, {% endif -%} + {%- if loop.last %} {{ trans(key="word_separator", lang=lang) }} {% else %}, {% endif -%} {%- endif -%} @{{user}} {%- endfor %}. @@ -70,11 +73,11 @@

{% endif %} -
+
{{ page.content | replace(from="", to=macros::toc(toc=page.toc)) | safe }}
- {%- endif %} -
+

About Me

I'm a Rust freelancer with a master's degree in computer science. I love systems programming, open source software, and new challenges.

- If you want to work with me, reach out on LinkedIn or write me at job@phil-opp.com. + If you want to work with me, reach out on LinkedIn or write me at job@phil-opp.com.

diff --git a/blog/templates/status-update-section.html b/blog/templates/status-update-section.html index 99d66d77..e479b453 100644 --- a/blog/templates/status-update-section.html +++ b/blog/templates/status-update-section.html @@ -10,7 +10,7 @@

{{ section.description }}

{% endblock introduction %} -
    +
      {% include "auto/status-updates.html" %} {% for page in section.pages %}
    • {{ page.title }}