mirror of
https://github.com/phil-opp/blog_os.git
synced 2025-12-16 14:27:49 +00:00
Merge branch 'master' into edition-3
This commit is contained in:
2
.github/workflows/build-site.yml
vendored
2
.github/workflows/build-site.yml
vendored
@@ -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"
|
||||
|
||||
2
.github/workflows/scheduled-builds.yml
vendored
2
.github/workflows/scheduled-builds.yml
vendored
@@ -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 }}
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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 <strong><a href=\"_original.permalink_\">_original.title_</a></strong> 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 <strong><a href=\"_original.permalink_\">_original.title_</a></strong> 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 <strong><a href=\"_original.permalink_\">_original.title_</a></strong> 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 = "この記事は<strong><a href=\"_original.permalink_\">_original.title_</a></strong>をコミュニティの手により翻訳したものです。そのため、翻訳が完全・最新でなかったり、原文にない誤りを含んでいる可能性があります。問題があれば<a href=\"https://github.com/phil-opp/blog_os/issues/906\">このissue</a>上で報告してください!"
|
||||
translated_by = "翻訳者:"
|
||||
word_separator = "及び"
|
||||
|
||||
[translations.fa]
|
||||
lang_name = "Persian"
|
||||
toc = "فهرست مطالب"
|
||||
all_posts = "« همه پستها"
|
||||
comments = "نظرات"
|
||||
comments_notice = "لطفا نظرات خود را در صورت امکان به انگلیسی بنویسید."
|
||||
readmore = "ادامهمطلب»"
|
||||
not_translated = "(.این پست هنوز ترجمه نشده است)"
|
||||
translated_content = "محتوای ترجمه شده:"
|
||||
translated_content_notice = "این یک ترجمه از جامعه کاربران برای پست <strong><a href=\"_original.permalink_\">_original.title_</a></strong> است. ممکن است ناقص، منسوخ شده یا دارای خطا باشد. لطفا هر گونه مشکل را در <a href=\"https://github.com/phil-opp/blog_os/issues/908\">این ایشو</a> گزارش دهید!"
|
||||
translated_by = "ترجمه توسط"
|
||||
word_separator = "و"
|
||||
|
||||
@@ -2,12 +2,12 @@
|
||||
template = "edition-2/index.html"
|
||||
+++
|
||||
|
||||
<h1 style="visibility: hidden; height: 0px; margin: 0px; padding: 0px;">Writing an OS in Rust</h1>
|
||||
<h1 style="visibility: hidden; height: 0px; margin: 0px; padding: 0px;">نوشتن یک سیستم عامل با راست</h1>
|
||||
|
||||
<div class="front-page-introduction">
|
||||
<div class="front-page-introduction right-to-left">
|
||||
|
||||
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: <!-- latest-post -->
|
||||
اخرین پست: <!-- latest-post -->
|
||||
|
||||
</div>
|
||||
|
||||
@@ -2,12 +2,12 @@
|
||||
template = "edition-2/index.html"
|
||||
+++
|
||||
|
||||
<h1 style="visibility: hidden; height: 0px; margin: 0px; padding: 0px;">Writing an OS in Rust</h1>
|
||||
<h1 style="visibility: hidden; height: 0px; margin: 0px; padding: 0px;">RustでOSを書く</h1>
|
||||
|
||||
<div class="front-page-introduction">
|
||||
|
||||
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: <!-- latest-post -->
|
||||
最新記事: <!-- latest-post -->
|
||||
|
||||
</div>
|
||||
|
||||
@@ -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
|
||||
|
||||
この変更をもって、私達のカーネルはコンパイラに必要とされているすべての関数の有効な実装を手に入れたので、コードがもっと複雑になっても変わらずコンパイルできるでしょう。
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -109,7 +109,7 @@ Nightly 版本的编译器允许我们在源码的开头插入**特性标签**
|
||||
"target-pointer-width": "64",
|
||||
"target-c-int-width": "32",
|
||||
"os": "none",
|
||||
"executables": true,
|
||||
"executables": true
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
@@ -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/
|
||||
|
||||
@@ -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
|
||||
|
||||
1032
blog/content/edition-2/posts/04-testing/index.ja.md
Normal file
1032
blog/content/edition-2/posts/04-testing/index.ja.md
Normal file
File diff suppressed because it is too large
Load Diff
@@ -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]`.
|
||||
|
||||
|
||||
472
blog/content/edition-2/posts/05-cpu-exceptions/index.fa.md
Normal file
472
blog/content/edition-2/posts/05-cpu-exceptions/index.fa.md
Normal file
@@ -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
|
||||
|
||||
<!-- more -->
|
||||
|
||||
این بلاگ بصورت آزاد روی [گیتهاب] توسعه داده شده است. اگر مشکل یا سوالی دارید، لطفاً آنجا یک ایشو باز کنید. همچنین میتوانید [در زیر] این پست کامنت بگذارید. سورس کد کامل این پست را میتوانید در بِرَنچ [`post-05`][post branch] پیدا کنید.
|
||||
|
||||
[گیتهاب]: https://github.com/phil-opp/blog_os
|
||||
[در زیر]: #comments
|
||||
[post branch]: https://github.com/phil-opp/blog_os/tree/post-05
|
||||
|
||||
<!-- toc -->
|
||||
|
||||
## بررسی اجمالی
|
||||
یک استثنا نشان می دهد که مشکلی در دستورالعمل فعلی وجود دارد. به عنوان مثال ، اگر دستورالعمل فعلی بخواهد تقسیم بر 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<HandlerFunc>,
|
||||
pub debug: Entry<HandlerFunc>,
|
||||
pub non_maskable_interrupt: Entry<HandlerFunc>,
|
||||
pub breakpoint: Entry<HandlerFunc>,
|
||||
pub overflow: Entry<HandlerFunc>,
|
||||
pub bound_range_exceeded: Entry<HandlerFunc>,
|
||||
pub invalid_opcode: Entry<HandlerFunc>,
|
||||
pub device_not_available: Entry<HandlerFunc>,
|
||||
pub double_fault: Entry<HandlerFuncWithErrCode>,
|
||||
pub invalid_tss: Entry<HandlerFuncWithErrCode>,
|
||||
pub segment_not_present: Entry<HandlerFuncWithErrCode>,
|
||||
pub stack_segment_fault: Entry<HandlerFuncWithErrCode>,
|
||||
pub general_protection_fault: Entry<HandlerFuncWithErrCode>,
|
||||
pub page_fault: Entry<PageFaultHandlerFunc>,
|
||||
pub x87_floating_point: Entry<HandlerFunc>,
|
||||
pub alignment_check: Entry<HandlerFuncWithErrCode>,
|
||||
pub machine_check: Entry<HandlerFunc>,
|
||||
pub simd_floating_point: Entry<HandlerFunc>,
|
||||
pub virtualization: Entry<HandlerFunc>,
|
||||
pub security_exception: Entry<HandlerFuncWithErrCode>,
|
||||
// some fields omitted
|
||||
}
|
||||
```
|
||||
|
||||
فیلدها از نوع [`<idt::Entry<F`] هستند ، این ساختمانی است که فیلد های یک عنصر IDT را نشان می دهد (به جدول بالا مراجعه کنید). پارامتر نوع `F`، نوع تابع کنترل کننده مورد انتظار را تعریف می کند. می بینیم که برخی از عناصر به یک [`HandlerFunc`] و برخی دیگر به [`HandlerFuncWithErrCode`] نیاز دارند. خطای صفحه حتی نوع خاص خود را دارد: [`PageFaultHandlerFunc`].
|
||||
|
||||
[`<idt::Entry<F`]: 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
|
||||
|
||||
بیایید ابتدا به نوع `HandlerFunc` نگاه کنیم:
|
||||
|
||||
```rust
|
||||
type HandlerFunc = extern "x86-interrupt" fn(_: &mut InterruptStackFrame);
|
||||
```
|
||||
|
||||
این یک [نوع مستعار(type alias)] برای نوع "`extern "x86-interrupt" fn` است. کلمه کلیدی `extern` تابعی را با یک [قرارداد فراخوانی خارجی] تعریف می کند و اغلب برای برقراری ارتباط با کد C استفاده می شود(`extern "C" fn`) . اما قرارداد فراخوانی `x86-interrupt` چیست؟
|
||||
|
||||
[نوع مستعار(type alias)]: https://doc.rust-lang.org/book/ch19-04-advanced-types.html#creating-type-synonyms-with-type-aliases
|
||||
[قرارداد فراخوانی خارجی]: https://doc.rust-lang.org/nomicon/ffi.html#foreign-calling-conventions
|
||||
|
||||
## قرارداد فراخوانی وقفه
|
||||
استثناها کاملاً شبیه فراخوانی توابع هستند: پردازنده به اولین دستورالعمل تابع فراخوانی شده می رود و آن را اجرا می کند. پس از آن پردازنده به آدرس بازگشت می پرد و اجرای تابع اصلی را ادامه می دهد.
|
||||
|
||||
با این وجود ، تفاوت عمده ای بین فراخوانی استثناها و توابع وجود دارد: یک فراخوانی تابع توسط یک کامپایلر که دستور "فراخوانی" در آن درج شده است ، انجام می شود ، در حالی که یک استثنا ممکن است در _هر_ دستورالعملی رخ دهد. برای درک عواقب این تفاوت ، باید فراخوانی توابع را با جزئیات بیشتری بررسی کنیم.
|
||||
|
||||
[قرارداد فراخوانی] جزئیات فراخوانی تابع را مشخص می کند. به عنوان مثال ، آنها مشخص میکنند که پارامترهای تابع کجا قرار می گیرند (به عنوان مثال در ثباتها یا بر روی پشته) و نحوه بازگشت نتایج. در x86_64 لینوکس ، قوانین زیر برای توابع C اعمال می شود (مشخص شده در [System V ABI]):
|
||||
|
||||
[قرارداد فراخوانی]: https://en.wikipedia.org/wiki/Calling_convention
|
||||
[System V ABI]: https://refspecs.linuxbase.org/elf/x86_64-abi-0.99.pdf
|
||||
|
||||
- شش آرگومان اول با نوع عدد صحیح در ثباتهای`rdi`, `rsi`, `rdx`, `rcx`, `r8`, `r9` منتقل می شوند
|
||||
- آرگومان های اضافی بر روی پشته منتقل می شوند
|
||||
- نتایج درون`rax` و `rdx` بر می گردند
|
||||
|
||||
توجه داشته باشید که راست از C ABI پیروی نمی کند (در واقع ، [هنوز حتی یک Rust ABI وجود ندارد][rust abi]) ، بنابراین این قوانین فقط برای توابع اعلام شده به عنوان `extern "C" fn` اعمال می شود.
|
||||
|
||||
[rust abi]: https://github.com/rust-lang/rfcs/issues/600
|
||||
|
||||
### ثبات های حفظ شده و تغییرشونده (Scratch)
|
||||
قرارداد فراخوانی، ثباتها را به دو دسته ثبات های _محفوظ شده_ و _تغییرشونده_ تقسیم می کند.
|
||||
|
||||
مقادیر ثباتهای _محفوظ شده_، در فراخوانی تابع باید بدون تغییر باقی بمانند. بنابراین یک تابع فراخوانی شده (_“callee”_) فقط در صورتی مجاز است این ثباتها را تغییر دهد، که مقادیر اصلی آنها را قبل از بازگشت، برگرداند. بنابراین به این ثباتها _"callee-saved"_ گفته می شود. یک الگوی عمومی این است که ثباتها در آغاز تابع بر روی پشته ذخیره شده و درست قبل از بازگشت از پشته برداشته شده و مقدار دهی شوند.
|
||||
|
||||
در مقابل، یک تابع فراخوانی شده مجاز است که بدون محدودیت ، ثباتهای _تغییرشونده_ را دوباره بنویسد. اگر فراخواننده (_"caller"_) بخواهد مقدار یک ثبات تغییرشونده را در یک فراخوانی تابع حفظ کند ، لازم است قبل از فراخوانی تابع (به عنوان مثال بوسیله اضافه و برداشتن از روی پشته) آن را پشتیبان گیری و بازیابی کند. بنابراین ثباتهای تغییرشونده _caller-saved_ هستند.
|
||||
|
||||
در x86_64 ، قرارداد فراخوانی C ثباتهای محفوظ شده و تغییرشونده زیر را مشخص می کند:
|
||||
|
||||
preserved registers | scratch registers
|
||||
---|---
|
||||
`rbp`, `rbx`, `rsp`, `r12`, `r13`, `r14`, `r15` | `rax`, `rcx`, `rdx`, `rsi`, `rdi`, `r8`, `r9`, `r10`, `r11`
|
||||
_callee-saved_ | _caller-saved_
|
||||
|
||||
کامپایلر این قوانین را می داند ، بنابراین کد را متناسب با آن تولید می کند. به عنوان مثال ، بیشتر توابع با `push rbp` شروع می شوند که پشتیبان گیری از`rbp` روی پشته است (زیرا این یک ثبات _caller-saved_).
|
||||
|
||||
### حفظ کلیه ثباتها
|
||||
برخلاف فراخوانی تابع ، استثناها می توانند در _هر_ دستورالعملی رخ دهند. در بیشتر موارد ، ما حتی در زمان کامپایل نمی دانیم که کد تولید شده استثنا ایجاد می کند یا نه. به عنوان مثال ، کامپایلر نمی تواند بداند که آیا یک دستورالعمل باعث سرریز شدن پشته یا خطای صفحه می شود.
|
||||
|
||||
از آنجا که نمی دانیم چه زمانی استثنا رخ میدهد ، نمی توانیم قبل از آن از هیچ ثباتی پشتیبان گیری کنیم. این بدان معناست که ما نمی توانیم از قرارداد فراخوانیای استفاده کنیم که متکی به ثباتهای caller-saved برای کنترل کننده های استثنا هست. در عوض ، به یک قرارداد فراخوانی نیاز داریم که _همه_ ثباتها را حفظ کند. قرارداد فراخوانی `x86-interrupt` چنین قرارداد فراخوانی است ، بنابراین تضمین می کند که تمام مقادیر ثباتها در هنگام بازگشت تابع به مقادیر اصلی خود بازگردند.
|
||||
|
||||
توجه داشته باشید که این بدان معنا نیست که همه ثباتها در ورود به تابع در پشته ذخیره می شوند. در عوض ، کامپایلر فقط از ثباتهایی که توسط تابع تغییر میکنند ، پشتیبان تهیه می کند. به این ترتیب ، کد بسیار کارآمدی برای توابع کوتاه که فقط از چند ثبات استفاده می کنند ، تولید می شود.
|
||||
|
||||
### قاب پشته وقفه (The Interrupt Stack Frame)
|
||||
در یک فراخوانی عادی تابع (با استفاده از دستورالعمل `call`) ، پردازنده قبل از پرش به تابع هدف ، آدرس بازگشت را در پشته ذخیره میکند. در هنگام بازگشت تابع (با استفاده از دستورالعمل `ret`) ، پردازنده این آدرس بازگشت را از پشته برمیدارد و به آن می پرد. بنابراین قاب پشته یک فراخوانی عادی تابع به این شکل است:
|
||||
|
||||

|
||||
|
||||
با این وجود، برای کنترل کننده های استثنا و وقفه، ذخیره آدرس برگشت در پشته کافی نیست، زیرا کنترل کننده های وقفه غالباً در context دیگری اجرا می شوند (نشانگر پشته ، پرچم های پردازنده و غیره). در عوض، پردازنده در صورت وقفه مراحل زیر را انجام می دهد:
|
||||
|
||||
1. **تراز کردن اشارهگر پشته**: در هر دستورالعمل امکان رخ دادن وقفه وجود دارد، بنابراین اشارهگر پشته نیز می تواند هر مقداری داشته باشد. با این حال ، برخی از دستورالعمل های پردازنده (به عنوان مثال برخی از دستورالعمل های SSE) نیاز دارند که اشارهگر پشته در مرز 16 بایت تراز شود ، بنابراین پردازنده درست پس از وقفه چنین ترازی را انجام می دهد.
|
||||
2. **تعویض پشتهها** (در بعضی موارد): تعویض پشته زمانی اتفاق می افتد که سطح امتیاز پردازنده (CPU privilege level) تغییر می کند، به عنوان مثال وقتی یک استثنا در یک برنامه حالت کاربر رخ می دهد. همچنین می توان تعویض پشته را برای وقفه های خاص با استفاده از به اصطلاح _Interrupt Stack Table_ پیکربندی کرد (در پست بعدی توضیح داده شده).
|
||||
3. **پوش کردن اشارهگر قدیمی پشته**: پردازنده مقادیر اشارهگر پشته (`rsp`) و سگمنت پشته (`ss`) را در زمان وقوع وقفه (قبل از تراز کردن) پوش میکند. این امکان را فراهم می کند تا هنگام بازگشت از کنترل کننده وقفه ، اشارهگر اصلی پشته بازیابی شود.
|
||||
4. **پوش کردن و بهروزرسانی ثبات `RFLAGS`**: ثبات [`RFLAGS`] شامل بیت های مختلف کنترل و وضعیت است. در هنگام وقوع وقفه ، پردازنده برخی از بیتها را تغییر میدهد و مقدار قدیمی را پوش میکند.
|
||||
5. **پوش کردن اشارهگر دستورالعمل**: قبل از پرش به تابع کنترل کننده وقفه ، پردازنده اشارهگر دستورالعمل (`rip`) و سگمنت کد (`cs`) را پوش میکند. این مشابه با پوش کردن آدرس برگشت یک تابع عادی است.
|
||||
6. ** پوش کردن کد خطا** (برای برخی استثناها): برای برخی از استثنا های خاص مانند خطاهای صفحه ، پردازنده یک کد خطا را پوش میکند که علت استثنا را توصیف می کند.
|
||||
7. **فراخوانی کنترل کننده وقفه**: پردازنده آدرس و توصیف کننده سگمنت تابع کنترل کننده وقفه را از قسمت مربوطه در IDT می خواند. سپس با بارگذاری مقادیر در ثبات های `rip` و `cs` این کنترل کننده را فراخوانی می کند.
|
||||
|
||||
[`RFLAGS`]: https://en.wikipedia.org/wiki/FLAGS_register
|
||||
|
||||
بنابراین _interrupt stack frame_ به این شکل است:
|
||||
|
||||

|
||||
|
||||
در کرت `x86_64` ، فریم پشته وقفه توسط ساختمان [`InterruptStackFrame`] نشان داده می شود. این ساختمان به عنوان `&mut` به کنترل کننده وقفه منتقل می شود و می تواند برای دریافت اطلاعات بیشتر در مورد علت استثنا استفاده شود. ساختمان بدون فیلد کد خطا است ، زیرا فقط برخی از استثناها کد خطا را پوش میکنند. این استثناها از نوع تابع جداگانه [`HandlerFuncWithErrCode`] استفاده میکنند ، که دارای یک آرگومان اضافی `error_code` است.
|
||||
|
||||
[`InterruptStackFrame`]: https://docs.rs/x86_64/0.13.2/x86_64/structures/idt/struct.InterruptStackFrame.html
|
||||
|
||||
### پشت صحنه
|
||||
قرارداد فراخوانی `x86-interrupt` یک انتزاع قدرتمند است که تقریباً تمام جزئیات پیچیده فرآیند مدیریت استثناها را پنهان می کند. با این حال ، گاهی اوقات مفید است که بدانیم پشت پرده چه اتفاقی می افتد. در اینجا یک مرور کوتاه از مواردی که قرارداد فراخوانی `x86-interrupt` انجام میدهد را میبینید:
|
||||
|
||||
- **دریافت آرگومان ها**: بیشتر قرارداد های فراخوانی انتظار دارند که آرگومان ها در ثباتها منتقل شوند. این برای کنترل کننده های استثنا امکان پذیر نیست ، زیرا ما نباید قبل از تهیه نسخه پشتیبان از مقادیر ثباتها ، آنها را بازنویسی کنیم. در عوض، قرارداد فراخوانی `x86-interrupt` آگاه است که آرگومان ها از قبل در مکان خاصی بر روی پشته قرار دارند.
|
||||
- **بازگشت با استفاده از `iretq`**: از آنجا که قاب پشته وقفه با قاب پشته صدا زدن توابع معمولی کاملاً متفاوت است، نمی توانیم از طریق دستورالعمل` ret` از توابع کنترل کننده برگردیم. در عوض، باید از دستور `iretq` استفاده شود.
|
||||
- **مدیریت کد خطا**: کد خطا که برای برخی استثناها به پشته اضافه می شود ، کارها را بسیار پیچیده تر می کند. تراز بندی پشته را تغییر می دهد (به قسمت بعدی مراجعه کنید) و باید قبل از بازگشت، از پشته خارج شود. قرارداد فراخوانی `x86-interrupt` تمام پیچیدگیها را برطرف می کند. با این حال، نمی داند کدام تابع کنترل کننده برای کدام استثنا استفاده می شود، بنابراین باید این اطلاعات را از تعداد آرگومان های تابع استخراج کند. این بدان معناست که برنامه نویس همچنان مسئول استفاده صحیح هر نوع تابع برای هر استثنا است. خوشبختانه نوع `InterruptDescriptorTable` که توسط کرت `x86_64` تعریف شده است، استفاده از انواع تابع صحیح را تضمین می کند.
|
||||
- **تراز کردن پشته**: برخی دستورالعملها (به ویژه دستورالعمل های SSE) وجود دارند که به یک تراز پشته 16 بایتی نیاز دارند. پردازنده این تراز را هر زمان که یک استثنا اتفاق می افتد تضمین می کند ، اما برای برخی از استثناها بعداً هنگامی که یک کد خطا را به پشته اضافه میکند، دوباره آن را از بین می برد. قرارداد فراخوانی `x86-interrupt` با تنظیم مجدد پشته در این حالت این مشکل را برطرف میکند.
|
||||
|
||||
اگر به جزئیات بیشتر علاقه مندید: ما همچنین یک سری پست داریم که مدیریت استثنا با استفاده از [توابع برهنه] را توضیح میدهند. ([در انتهای این پست][too-much-magic]).
|
||||
|
||||
[توابع برهنه]: https://github.com/rust-lang/rfcs/blob/master/text/1201-naked-fns.md
|
||||
[too-much-magic]: #khyly-jdwyy-bwd
|
||||
|
||||
## پیاده سازی
|
||||
اکنون که تئوری را فهمیدیم ، وقت آن رسیده است که استثناهای پردازنده را در هسته خود کنترل کنیم. ما با ایجاد یک ماژول جدید وقفهها در `src/interrupts.rs` شروع خواهیم کرد، که ابتدا یک تابع `init_idt` ایجاد می کند که یک `InterruptDescriptorTable` جدید ایجاد می کند:
|
||||
|
||||
``` rust
|
||||
// in src/lib.rs
|
||||
|
||||
pub mod interrupts;
|
||||
|
||||
// in src/interrupts.rs
|
||||
|
||||
use x86_64::structures::idt::InterruptDescriptorTable;
|
||||
|
||||
pub fn init_idt() {
|
||||
let mut idt = InterruptDescriptorTable::new();
|
||||
}
|
||||
```
|
||||
|
||||
اکنون می توانیم توابع کنترل کننده را اضافه کنیم. با اضافه کردن یک کنترل کننده برای [استثنا بریکپوینت] شروع می کنیم. استثنای بریکپوینت، استثنای عالی برای آزمایش مدیریت استثنا است. تنها هدف آن مکث موقت برنامه هنگام اجرای دستورالعمل بریکپوینت `int3` است.
|
||||
|
||||
[استثنا بریکپوینت]: https://wiki.osdev.org/Exceptions#Breakpoint
|
||||
|
||||
استثنا بریکپوینت معمولاً در دیباگرها به کار می رود: وقتی کاربر بریکپوینت را تعیین می کند ، دیباگر دستورالعمل مربوطه را با دستورالعمل `int3` بازنویسی می کند تا پردازنده هنگام رسیدن به آن خط، استثنای بریکپوینت را ایجاد کند. هنگامی که کاربر می خواهد برنامه را ادامه دهد، دیباگر دوباره دستورالعمل `int3` را با دستورالعمل اصلی جایگزین می کند و برنامه را ادامه می دهد. برای جزئیات بیشتر ، سری ["_دیباگرها چطور کار میکنند_"] را ببینید.
|
||||
|
||||
["_دیباگرها چطور کار میکنند_"]: https://eli.thegreenplace.net/2011/01/27/how-debuggers-work-part-2-breakpoints
|
||||
|
||||
برای موارد استفاده ما، نیازی به بازنویسی دستورالعمل نداریم. در عوض، فقط میخواهیم هنگام اجرای دستورالعمل بریکپوینت پیامی چاپ کنیم و سپس برنامه را ادامه دهیم. بنابراین بیایید یک تابع ساده `breakpoint_handler` ایجاد کنیم و آن را به IDT خود اضافه کنیم:
|
||||
|
||||
```rust
|
||||
// in src/interrupts.rs
|
||||
|
||||
use x86_64::structures::idt::{InterruptDescriptorTable, InterruptStackFrame};
|
||||
use crate::println;
|
||||
|
||||
pub fn init_idt() {
|
||||
let mut idt = InterruptDescriptorTable::new();
|
||||
idt.breakpoint.set_handler_fn(breakpoint_handler);
|
||||
}
|
||||
|
||||
extern "x86-interrupt" fn breakpoint_handler(
|
||||
stack_frame: &mut InterruptStackFrame)
|
||||
{
|
||||
println!("EXCEPTION: BREAKPOINT\n{:#?}", stack_frame);
|
||||
}
|
||||
```
|
||||
|
||||
کنترل کننده ما فقط یک پیام را خارج می کند و قاب پشته وقفه را زیبا چاپ می کند.
|
||||
|
||||
هنگامی که می خواهیم آن را کامپایل کنیم ، خطای زیر رخ می دهد:
|
||||
|
||||
```
|
||||
error[E0658]: x86-interrupt ABI is experimental and subject to change (see issue #40180)
|
||||
--> 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`) ، موارد زیر را مشاهده می کنیم:
|
||||
|
||||

|
||||
|
||||
کار می کند! پردازنده با موفقیت تابع کنترل کننده بریکپوینت ما را فراخوانی می کند ، که پیام را چاپ می کند و سپس به تابع `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
|
||||
@@ -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<F>`], 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<F>`]: 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<F>`]: 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?
|
||||
|
||||
569
blog/content/edition-2/posts/06-double-faults/index.fa.md
Normal file
569
blog/content/edition-2/posts/06-double-faults/index.fa.md
Normal file
@@ -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_ را تنظیم کردهایم تا خطاهای دوگانه را روی یک پشته هسته جداگانه بگیرد.
|
||||
|
||||
<!-- more -->
|
||||
|
||||
این بلاگ بصورت آزاد روی [گیتهاب] توسعه داده شده است. اگر شما مشکل یا سوالی دارید، لطفاً آنجا یک ایشو باز کنید. شما همچنین میتوانید [در زیر] این پست کامنت بگذارید. منبع کد کامل این پست را میتوانید در بِرَنچ [`post-06`][post branch] پیدا کنید.
|
||||
|
||||
[گیتهاب]: https://github.com/phil-opp/blog_os
|
||||
[در زیر]: #comments
|
||||
[post branch]: https://github.com/phil-opp/blog_os/tree/post-06
|
||||
|
||||
<!-- toc -->
|
||||
|
||||
## خطای دوگانه چیست؟
|
||||
|
||||
به عبارت ساده، خطای دوگانه یک استثنای به خصوص است و هنگامی رخ میدهد که 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
|
||||
|
||||
حال وقتی هسته را اجرا میکنیم، باید ببینیم که کنترل کننده خطای دوگانه فراخوانی میشود:
|
||||
|
||||

|
||||
|
||||
کار کرد! آنچه این بار اتفاق میافتد بصورت زیر است:
|
||||
|
||||
۱. سیپییو سعی به نوشتن در `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],<br>[Invalid TSS],<br>[Segment Not Present],<br>[Stack-Segment Fault],<br>[General Protection Fault] | [Invalid TSS],<br>[Segment Not Present],<br>[Stack-Segment Fault],<br>[General Protection Fault]
|
||||
[Page Fault] | [Page Fault],<br>[Invalid TSS],<br>[Segment Not Present],<br>[Stack-Segment Fault],<br>[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<StackPointer>; 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 مانند زیر است:
|
||||
|
||||
فیلد | نوع
|
||||
------ | ----------------
|
||||
<span style="opacity: 0.5">(reserved)</span> | `u32`
|
||||
Privilege Stack Table | `[u64; 3]`
|
||||
<span style="opacity: 0.5">(reserved)</span> | `u64`
|
||||
Interrupt Stack Table | `[u64; 7]`
|
||||
<span style="opacity: 0.5">(reserved)</span> | `u64`
|
||||
<span style="opacity: 0.5">(reserved)</span> | `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 باید هر زمان که خطای دوگانه رخ داد، به پشته خطای دوگانه برود. بنابراین، ما میتوانیم _همه_ خطاهای دوگانه، از جمله سرریزهای پشته هسته را بگیریم:
|
||||
|
||||

|
||||
|
||||
از این به بعد هرگز نباید شاهد خطای سهگانه باشیم! برای اطمینان از اینکه موارد بالا را به طور تصادفی نقض نمیکنیم، باید یک تست برای این کار اضافه کنیم.
|
||||
|
||||
## تست سرریز پشته
|
||||
|
||||
برای آزمایش ماژول `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
|
||||
554
blog/content/edition-2/posts/06-double-faults/index.ja.md
Normal file
554
blog/content/edition-2/posts/06-double-faults/index.ja.md
Normal file
@@ -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が例外ハンドラの呼び出しに失敗したときに起きる、ダブルフォルト例外について詳細に見ていきます。この例外を処理することによって、システムリセットを起こす重大な**トリプルフォルト**を避けることができます。あらゆる場合においてトリプルフォルトを防ぐために、ダブルフォルトを異なるカーネルスタック上でキャッチするための**割り込みスタックテーブル**をセットアップしていきます。
|
||||
|
||||
<!-- more -->
|
||||
|
||||
このブログの内容は [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
|
||||
|
||||
<!-- toc -->
|
||||
|
||||
## ダブルフォルトとは
|
||||
簡単に言うとダブルフォルトとは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
|
||||
|
||||
ここで私達のカーネルを起動すると、ダブルフォルトハンドラが呼び出されていることがわかることでしょう。
|
||||
|
||||

|
||||
|
||||
動きました!ここで何が起きているかというと、
|
||||
|
||||
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)」と書かれています。**起きうる**というのが重要で、とても特別な例外の組み合わせでのみダブルフォルトとなります。この組み合わせは以下のようになっています。
|
||||
|
||||
最初の例外 | 二度目の例外
|
||||
----------------|-----------------
|
||||
[ゼロ除算],<br>[無効TSS],<br>[セグメント不在],<br>[スタックセグメントフォルト],<br>[一般保護違反] | [無効TSS],<br>[セグメント不在],<br>[スタックセグメントフォルト],<br>[一般保護違反]
|
||||
[ページフォルト] | [ページフォルト],<br>[無効TSS],<br>[セグメント不在],<br>[スタックセグメントフォルト],<br>[一般保護違反]
|
||||
|
||||
[ゼロ除算]: 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<StackPointer>; 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は下記のようなフォーマットです:
|
||||
|
||||
フィールド | 型
|
||||
------ | ----------------
|
||||
<span style="opacity: 0.5">(予約済み)</span> | `u32`
|
||||
特権スタックテーブル | `[u64; 3]`
|
||||
<span style="opacity: 0.5">(予約済み)</span> | `u64`
|
||||
割り込みスタックテーブル | `[u64; 7]`
|
||||
<span style="opacity: 0.5">(予約済み)</span> | `u64`
|
||||
<span style="opacity: 0.5">(予約済み)</span> | `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はダブルフォルトが発生したら常にダブルフォルトスタックに切り替えるでしょう。よって、私達はカーネルスタックオーバーフローを含む**すべての**ダブルフォルトをキャッチすることができます。
|
||||
|
||||

|
||||
|
||||
これからはトリプルフォルトを見ることは二度とないでしょう。これらダブルフォルトのための実装を誤って壊していないことを保証するために、テストを追加しましょう。
|
||||
|
||||
## スタックオーバーフローテスト
|
||||
|
||||
新しい`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
|
||||
@@ -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
|
||||
|
||||
738
blog/content/edition-2/posts/07-hardware-interrupts/index.fa.md
Normal file
738
blog/content/edition-2/posts/07-hardware-interrupts/index.fa.md
Normal file
@@ -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
|
||||
+++
|
||||
|
||||
در این پست ما کنترل کننده قابل برنامه ریزی وقفه را تنظیم می کنیم تا وقفه های سخت افزاری را به درستی به پردازنده منتقل کند. برای مدیریت این وقفهها ، موارد جدیدی به جدول توصیف کننده وقفه اضافه می کنیم ، دقیقاً مانند کارهایی که برای کنترل کننده های استثنا انجام دادیم. ما یاد خواهیم گرفت که چگونه وقفه های متناوب تایمر را گرفته و چگونه از صفحه کلید ورودی بگیریم.
|
||||
|
||||
<!-- more -->
|
||||
|
||||
این بلاگ بصورت آزاد بر روی [گیتهاب] توسعه داده شده. اگر مشکل یا سوالی دارید، لطفاً آنجا یک ایشو باز کنید. همچنین میتوانید [در زیر] این پست کامنت بگذارید. سورس کد کامل این پست را میتوانید در بِرَنچ [`post-07`][post branch] پیدا کنید.
|
||||
|
||||
[گیتهاب]: https://github.com/phil-opp/blog_os
|
||||
[در زیر]: #comments
|
||||
[post branch]: https://github.com/phil-opp/blog_os/tree/post-07
|
||||
|
||||
<!-- toc -->
|
||||
|
||||
## مقدمه
|
||||
|
||||
وقفهها راهی برای اطلاع به پردازنده از دستگاه های سخت افزاری متصل ارائه می دهند. بنابراین به جای اینکه پردازنده به طور دورهای صفحه کلید را برای کاراکترهای جدید بررسی کند(فرآیندی به نام [_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<ChainedPics> =
|
||||
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` را امتحان می کنیم ، می بینیم که یک خطای دوگانه رخ میدهد:
|
||||
|
||||

|
||||
|
||||
دلیل این خطای دوگانه این است که تایمر سخت افزاری (به طور دقیق تر [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
|
||||
|
||||
در کنترل کننده وقفه تایمر، یک نقطه را روی صفحه چاپ می کنیم. همانطور که وقفه تایمر به صورت دوره ای اتفاق می افتد ، انتظار داریم که در هر تیک تایمر یک نقطه ظاهر شود. با این حال، هنگامی که آن را اجرا می کنیم می بینیم که فقط یک نقطه چاپ می شود:
|
||||
|
||||

|
||||
|
||||
### پایان وقفه
|
||||
|
||||
دلیل این امر این است که 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` را اجرا می کنیم، نقاطی را می بینیم که به صورت دوره ای روی صفحه ظاهر می شوند:
|
||||
|
||||

|
||||
|
||||
### پیکربندی تایمر
|
||||
|
||||
تایمر سخت افزاری که ما از آن استفاده می کنیم ، _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 اجرا می کنیم ، خروجی به حالت زیر دریافت میکنیم:
|
||||
|
||||

|
||||
|
||||
می بینیم که فقط تعداد محدودی خط فاصله ، تا زمانی که وقفه تایمر اول اتفاق بیفتد، چاپ می شود. سپس سیستم هنگ میکند زیرا تایمر هنگام تلاش برای چاپ یک نقطه باعث بنبست میشود. به همین دلیل است که در خروجی فوق هیچ نقطهای مشاهده نمیکنیم.
|
||||
|
||||
تعداد واقعی خط فاصله بین هر اجرا متفاوت است زیرا وقفه تایمر به صورت غیر همزمان انجام می شود. این عدم قطعیت، اشکال زدایی اشکالات مربوط به همروندی را بسیار دشوار می کند.
|
||||
|
||||
### رفع بنبست
|
||||
|
||||
برای جلوگیری از این بنبست ، تا زمانی که `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 اجرا می کنیم ، شاهد استفاده بسیار کمتری از پردازنده هستیم.
|
||||
|
||||
## ورودی صفحه کلید
|
||||
|
||||
اکنون که قادر به مدیریت وقفه های دستگاه های خارجی هستیم ، سرانجام قادر به پشتیبانی از ورودی صفحه کلید هستیم. این به ما امکان می دهد برای اولین بار با هسته خود تعامل داشته باشیم.
|
||||
|
||||
<aside class="post_aside">
|
||||
|
||||
توجه داشته باشید که ما فقط نحوه مدیریت صفحه کلیدهای [PS/2] را توضیح می دهیم ، نه صفحه کلیدهای USB. هر چند که، مادربورد صفحه کلیدهای USB را به عنوان دستگاه های PS/2 برای پشتیبانی از نرم افزارهای قدیمی تقلید میکند ، بنابراین تا زمانی که هسته ما از USB پشتیبانی نمیکند می توانیم با اطمینان از صفحه کلیدهای USB چشم پوشی کنیم.
|
||||
|
||||
</aside>
|
||||
|
||||
[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
|
||||
|
||||

|
||||
|
||||
تصویر بالا نشان می دهد که من آرام آرام "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
|
||||
|
||||
اکنون می توانیم اعداد را بنویسیم:
|
||||
|
||||

|
||||
|
||||
ترجمه کلیدهای دیگر نیز به همین روش کار می کند. خوشبختانه کرت ای با نام [`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<Keyboard<layouts::Us104Key, ScancodeSet1>> =
|
||||
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`] منتقل می کنیم ، که اسکن کد را به یک `<Option<KeyEvent` ترجمه می کند. [`KeyEvent`] حاوی كلیدی است كه باعث رویداد شده و آیا این یک رویداد فشردن یا رها کردن بوده است.
|
||||
|
||||
[`Keyboard`]: https://docs.rs/pc-keyboard/0.5.0/pc_keyboard/struct.Keyboard.html
|
||||
[`add_byte`]: https://docs.rs/pc-keyboard/0.5.0/pc_keyboard/struct.Keyboard.html#method.add_byte
|
||||
[`KeyEvent`]: https://docs.rs/pc-keyboard/0.5.0/pc_keyboard/struct.KeyEvent.html
|
||||
|
||||
برای تفسیر این رویداد کلید ، آن را به متد [`process_keyevent`] منتقل می کنیم ، که در صورت امکان رویداد کلید را به یک کاراکتر ترجمه می کند. به عنوان مثال ، بسته به فشردن کلید shift ، یک رویداد فشردن کلید `A` را به یک حرف کوچک `a` یا یک حرف بزرگ `A` ترجمه می کند.
|
||||
|
||||
[`process_keyevent`]: https://docs.rs/pc-keyboard/0.5.0/pc_keyboard/struct.Keyboard.html#method.process_keyevent
|
||||
|
||||
با استفاده از این کنترل کننده وقفه اصلاح شده اکنون می توانیم متن بنویسیم:
|
||||
|
||||

|
||||
|
||||
### پیکربندی صفحه کلید
|
||||
|
||||
امکان پیکربندی برخی از جنبه های صفحه کلید PS/2 وجود دارد، به عنوان مثال از کدام اسکن کد استفاده کند. ما در اینجا به آن نمی پردازیم زیرا این پست به اندازه کافی طولانی شده است ، اما ویکی OSDev دارای نمای کلی [دستورات پیکربندی] ممکن است.
|
||||
|
||||
[دستورات پیکربندی]: https://wiki.osdev.org/PS/2_Keyboard#Commands
|
||||
|
||||
## خلاصه
|
||||
|
||||
در این پست نحوه فعال سازی و مدیریت وقفه های خارجی توضیح داده شد. ما در مورد 8259 PIC و طرح اولیه/ثانویه آن ، نگاشت دوباره اعداد وقفه و سیگنال "پایان وقفه" اطلاعات کسب کردیم. ما کنترل کنندههایی را برای تایمر سخت افزاری و صفحه کلید پیاده سازی کردیم و درباره دستورات `hlt` اطلاعاتی کسب کردیم که پردازنده را تا وقفه بعدی متوقف می کند.
|
||||
|
||||
اکنون ما قادر به تعامل با هسته خود هستیم و برخی از عناصر اساسی برای ایجاد یک پوسته(Shell) کوچک یا بازی های ساده را داریم.
|
||||
|
||||
## مرحله بعدی چیست؟
|
||||
|
||||
وقفه های تایمر برای یک سیستم عامل ضروری است، زیرا راهی برای قطع دوره ای روند اجرا و کنترل مجدد هسته فراهم می کند. سپس هسته می تواند به فرایند دیگری سوییچ کند و این توهم را ایجاد کند که چندین فرآیند به طور موازی اجرا می شوند.
|
||||
|
||||
اما قبل از اینکه بتوانیم فرایندها یا نخها را ایجاد کنیم ، به روشی برای اختصاص حافظه برای آنها نیاز داریم. در پست های بعدی، مدیریت حافظه مورد بررسی قرار می گیرد تا این عنصر اساسی ایجاد شود.
|
||||
@@ -208,7 +208,7 @@ extern "x86-interrupt" fn timer_interrupt_handler(
|
||||
|
||||
Our `timer_interrupt_handler` has the same signature as our exception handlers, because the CPU reacts identically to exceptions and external interrupts (the only difference is that some exceptions push an error code). The [`InterruptDescriptorTable`] struct implements the [`IndexMut`] trait, so we can access individual entries through array indexing syntax.
|
||||
|
||||
[`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
|
||||
[`IndexMut`]: https://doc.rust-lang.org/core/ops/trait.IndexMut.html
|
||||
|
||||
In our timer interrupt handler, we print a dot to the screen. As the timer interrupt happens periodically, we would expect to see a dot appearing on each timer tick. However, when we run it we see that only a single dot is printed:
|
||||
@@ -333,8 +333,8 @@ pub fn _print(args: fmt::Arguments) {
|
||||
|
||||
The [`without_interrupts`] function takes a [closure] and executes it in an interrupt-free environment. We use it to ensure that no interrupt can occur as long as the `Mutex` is locked. When we run our kernel now we see that it keeps running without hanging. (We still don't notice any dots, but this is because they're scrolling by too fast. Try to slow down the printing, e.g. by putting a `for _ in 0..10000 {}` inside the loop.)
|
||||
|
||||
[`without_interrupts`]: https://docs.rs/x86_64/0.12.1/x86_64/instructions/interrupts/fn.without_interrupts.html
|
||||
[closure]: https://doc.rust-lang.org/book/second-edition/ch13-01-closures.html
|
||||
[`without_interrupts`]: https://docs.rs/x86_64/0.13.2/x86_64/instructions/interrupts/fn.without_interrupts.html
|
||||
[closure]: https://doc.rust-lang.org/book/ch13-01-closures.html
|
||||
|
||||
We can apply the same change to our serial printing function to ensure that no deadlocks occur with it either:
|
||||
|
||||
@@ -580,7 +580,7 @@ extern "x86-interrupt" fn keyboard_interrupt_handler(
|
||||
|
||||
We use the [`Port`] type of the `x86_64` crate to read a byte from the keyboard's data port. This byte is called the [_scancode_] and is a number that represents the key press/release. We don't do anything with the scancode yet, we just print it to the screen:
|
||||
|
||||
[`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
|
||||
[_scancode_]: https://en.wikipedia.org/wiki/Scancode
|
||||
|
||||

|
||||
|
||||
420
blog/content/edition-2/posts/08-paging-introduction/index.fa.md
Normal file
420
blog/content/edition-2/posts/08-paging-introduction/index.fa.md
Normal file
@@ -0,0 +1,420 @@
|
||||
+++
|
||||
title = "مقدمهای بر صفحهبندی"
|
||||
weight = 8
|
||||
path = "fa/paging-introduction"
|
||||
date = 2019-01-14
|
||||
|
||||
[extra]
|
||||
chapter = "Memory Management"
|
||||
# Please update this when updating the translation
|
||||
translation_based_on_commit = "f692c5b377460e872bca2d3fcec787f4a0d1ec9b"
|
||||
# GitHub usernames of the people that translated this post
|
||||
translators = ["hamidrezakp", "MHBahrampour"]
|
||||
rtl = true
|
||||
+++
|
||||
|
||||
در این پست _صفحهبندی_، یک طرح مدیریت حافظه بسیار رایج که ما نیز برای سیستمعامل خود استفاده خواهیم کرد، معرفی میشود. این پست توضیح میدهد که چرا ایزوله سازی حافظه مورد نیاز است، قطعهبندی چگونه کار میکند، _حافظه مجازی_ چیست و چگونه صفحهبندی مشکلات تقسیم حافظه را حل می کند. همچنین طرح جدولهای صفحه چند سطحی را در معماری x86_64 بررسی میکند.
|
||||
|
||||
<!-- more -->
|
||||
|
||||
این بلاگ بصورت آزاد روی [گیتهاب] توسعه داده شده است. اگر شما مشکل یا سوالی دارید، لطفاً آنجا یک ایشو باز کنید. شما همچنین میتوانید [در زیر] این پست کامنت بگذارید. منبع کد کامل این پست را میتوانید در بِرَنچ [`post-08`][post branch] پیدا کنید.
|
||||
|
||||
[گیتهاب]: https://github.com/phil-opp/blog_os
|
||||
[در زیر]: #comments
|
||||
[post branch]: https://github.com/phil-opp/blog_os/tree/post-08
|
||||
|
||||
<!-- toc -->
|
||||
|
||||
## محافظت از حافظه
|
||||
|
||||
یکی از وظایف اصلی یک سیستمعامل جداسازی (ایزوله کردن) برنامهها از یکدیگر است. به عنوان مثال، مرورگر وب شما نباید در کار ویرایشگر متن تداخلی ایجاد کند. برای دستیابی به این هدف، سیستمعاملها از قابلیتی سختافزاری استفاده کرده تا اطمینان حاصل کنند که حافظه مربوط به یک پروسه، توسط پروسهای دیگر غیر قابل دسترس است. رویکردهای مختلفی وجود دارد که به سختافزار و پیادهسازی سیستم عامل بستگی دارد.
|
||||
|
||||
به عنوان مثال، برخی از پردازندههای 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` است.
|
||||
|
||||
برای تمایز بین دو نوع آدرس، به آدرسهای قبل از ترجمه _مجازی_ و به آدرسهای بعد از ترجمه _فیزیکی_ گفته میشود. یک تفاوت مهم بین این دو نوع آدرس این است که آدرسهای فیزیکی منحصربهفرد هستند و همیشه به همان مکان حافظه متمایز اشاره دارند. از طرف دیگر آدرسهای مجازی به تابع ترجمه بستگی دارد. کاملاً ممکن است که دو آدرس مجازی مختلف به همان آدرس فیزیکی اشاره داشته باشند. همچنین، آدرسهای مجازی یکسان میتوانند هنگام استفاده از توابع ترجمه مختلف، به آدرسهای فیزیکی مختلفی مراجعه کنند.
|
||||
|
||||
برای مثال هنگامی که میخواهید یک برنامه را دو بار بصورت موازی اجرا کنید، این خاصیت مفید است.
|
||||
|
||||

|
||||
|
||||
در اینجا همان برنامه دو بار اجرا میشود ، اما با تابعهای ترجمه مختلف. نمونه اول دارای آفست قطعه 100 است، بنابراین آدرسهای مجازی 0–150 به آدرس های فیزیکی 100–250 ترجمه میشوند. نمونه دوم دارای آفست قطعه 300 است، که آدرسهای مجازی 0–150 را به آدرسهای فیزیکی 300–450 ترجمه میکند. این به هر دو برنامه این امکان را میدهد تا بدون تداخل با یکدیگر کد یکسانی را اجرا کنند و از آدرسهای مجازی یکسان استفاده کنند.
|
||||
|
||||
مزیت دیگر این است که برنامهها میتوانند در مکانهای حافظه فیزیکی دلخواه قرار بگیرند، حتی اگر از آدرسهای مجازی کاملاً متفاوتی استفاده کنند. بنابراین، سیستمعامل میتواند از مقدار کامل حافظه موجود بدون نیاز به کامپایل مجدد برنامهها استفاده کند.
|
||||
|
||||
### تکهتکه شدن
|
||||
|
||||
تمایز بین آدرسهای مجازی و فیزیکی قطعهبندی را واقعا قدرتمند میکند. با این حال، مشکل تکهتکه شدن (ترجمه: fragmentation) دارد. به عنوان مثال، تصور کنید که میخواهیم نسخه سوم برنامهای را که در بالا دیدیم اجرا کنیم:
|
||||
|
||||

|
||||
|
||||
هیچ راهی برای نگاشت کردن نمونه سوم برنامه روی حافظه مجازی بدون همپوشانی وجود ندارد، حتی اگر حافظه آزاد بیش از اندازه کافی در دسترس باشد. مشکل این است که ما به حافظه _یکپارچه_ نیاز داریم و نمیتوانیم از تکههای کوچک استفاده کنیم.
|
||||
|
||||
یکی از راههای مقابله با این تکهتکه شدن، وقفه/مکث (pause) در اجرا است، انتقال قسمتهای استفاده شده حافظه به سمت یکدیگر تا این قسمتها به هم بچسبند و فضای تکهتکه شده بین آنها پر شود، سپس به روزرسانی ترجمه و اجرای مجدد آن است:
|
||||
|
||||

|
||||
|
||||
اکنون فضای یکپارچه کافی برای شروع نمونه سوم برنامه ما وجود دارد.
|
||||
|
||||
نقطه ضعف این فرآیند یکپارچهسازی (ترجمه: defragmentation) قطعات این است که نیاز به کپی کردن مقدار زیادی حافظه است که باعث کاهش کارایی میشود. همچنین لازم است قبل از اینکه حافظه بیش از حد تکهتکه شود، این کار به طور منظم انجام شود. این باعث میشود کارایی غیرقابل پیشبینی باشد، زیرا برنامهها به طور تصادفی دچار وقفه میشوند و ممکن است ناپاسخگو (ترجمه: unresponsive) شوند.
|
||||
|
||||
مشکل تکهتکه شدن یکی از دلایلی است که قطعهبندی دیگر توسط اکثر سیستمها استفاده نمیشود. در واقع، قطعهبندی حتی در حالت 64 بیتی روی x86 دیگر پشتیبانی نمیشود. در عوض از _صفحهبندی_ استفاده میشود، که به طور کامل از مشکل تکهتکه شدن جلوگیری میکند.
|
||||
|
||||
## صفحهبندی
|
||||
|
||||
ایده این است که هر دو فضای حافظه مجازی و فیزیکی را به بلوکهای کوچک و با اندازه ثابت تقسیم کنید. بلوکهای فضای حافظه مجازی _صفحهها_ و بلوکهای فضای آدرس فیزیکی _قابها_ نامیده میشوند. هر صفحه را میتوان به صورت جداگانه به یک قاب نگاشت کرد، که باعث میشود ناحیه حافظه بزرگتر در قابهای فیزیکی غیر یکپارچه تقسیم شوند.
|
||||
|
||||
اگر مثالِ فضای حافظه تکهتکه شده را خلاصه کنیم، مزیت این امر قابل مشاهده میشود، اما این بار به جای قطعهبندی از صفحهبندی استفاده میکنیم:
|
||||
|
||||

|
||||
|
||||
در این مثال یک صفحه با اندازه 50 بایت داریم، به این معنی که هر یک از ناحیه حافظه ما در سه صفحه تقسیم شده است. هر صفحه به صورت جداگانه به یک قاب نگاشت میشود، بنابراین میتوان یک منطقه حافظه مجازی یکپارچه را به قابهای فیزیکی غیر یکپارچه نگاشت کرد. که به ما این امکان را میدهد تا نمونه سوم برنامه را بدون انجام هرگونه یکپارچهسازی شروع کنیم.
|
||||
|
||||
### تکهتکه شدن مخفی
|
||||
|
||||
در مقایسه با قطعهبندی، صفحهبندی به جای چند منطقه بزرگ و متغیر، از تعداد زیادی ناحیه حافظه کوچک و ثابت استفاده میکند. از آنجا که هر قاب دارای اندازه یکسانی است، هیچ قابی وجود ندارد که از سایز صفحههای موجود کوچکتر باشد، پس تکهتکه شدن رخ نمیدهد.
|
||||
|
||||
یا _به نظر_ میرسد که هیچ تکهتکه شدنی رخ نمیدهد. هنوز یک نوع تکهتکه شدن نخفی وجود دارد، به اصطلاح _تکهتکه شدن داخلی_. تکهتکه شدن داخلی اتفاق میافتد زیرا همه ناحیه حافظه دقیقاً مضربی از اندازه صفحه نیستند. برنامهای با اندازه 101 را در مثال بالا تصور کنید: هنوز به سه صفحه با اندازه 50 نیاز دارد، بنابراین 49 بایت بیش از حد مورد نیاز اشغال میکند. برای تمایز بین دو نوع تکهتکه شدن، نوعی تکهتکه شدنی که هنگام استفاده از قطعهبندی اتفاق میافتد، _قطعهبندی خارجی_ نامیده میشود.
|
||||
|
||||
تکهتکه شدن داخلی تأسف آور است، اما اغلب بهتر از تکهتکه شدن خارجی است که با قطعهبندی رخ میدهد. این هنوز حافظه را هدر میدهد، اما به یکپارچهسازی نیاز ندارد و میزان تکهتکه شدن را قابل پیشبینی میکند (به طور متوسط نیم صفحه در هر منطقه حافظه).
|
||||
|
||||
### جدول صفحهها
|
||||
|
||||
دیدیم که هر یک از میلیونها صفحه بالقوه به صورت جداگانه در یک قاب نگاشت میشوند. این اطلاعات نگاشت باید در جایی ذخیره شود. قطعهبندی برای هر منطقه حافظه فعال از یک ثبات انتخابگرِ قطعهی جداگانه استفاده میکند، که برای صفحهبندی امکان پذیر نیست زیرا صفحات بیشتری نسبت به ثباتها وجود دارد. در عوض صفحهبندی از یک ساختار جدول به نام _page table_ برای ذخیره اطلاعات نگاشت استفاده می کند.
|
||||
|
||||
برای مثال بالا، جدولهای صفحه به صورت زیر است:
|
||||
|
||||

|
||||
|
||||
میبینیم که هر نمونهی برنامه جدول صفحه خاص خود را دارد. یک اشارهگر به جدولی که در حال حاضر فعال است، در یک رجیستر مخصوص CPU ذخیره میشود. در `x86`، این ثبات `CR3` است. وظیفه سیستمعامل این است که قبل از اجرای هر نمونهی برنامه، این رجیستر را با اشارهگر به جدول صفحهی صحیح بارگذاری کند.
|
||||
|
||||
در هر دسترسی به حافظه، CPU اشارهگر جدول را از ثبات میخواند و قاب نگاشته شده را برای صفحه قابل دسترسی در جدول جستجو میکند. این کار کاملاً بصورت سختافزاری و کاملاً شفاف برای برنامهی در حال اجرا، انجام میشود. برای سرعت بخشیدن به روند ترجمه، بسیاری از معماریهای CPU حافظه پنهان (ترجمه: cache) ویژهای دارند که نتایج آخرین ترجمهها را به خاطر میسپارد.
|
||||
|
||||
بسته به معماری، ورودیهای جدول صفحه همچنین میتوانند ویژگیهایی مانند مجوزهای دسترسی را در فیلد پرچمها ذخیره کنند. در مثال بالا، پرچم "r/w" صفحه را، خواندنی و قابل نوشتن میکند.
|
||||
|
||||
### جدول های صفحه چند سطحی
|
||||
|
||||
جدولهای صفحه ساده که اخیراً دیدیم در فضاهای آدرس بزرگتر مشکل دارند: آنها حافظه را هدر میدهند. به عنوان مثال، برنامهای را تصور کنید که از چهار صفحه مجازی `0`، `000_000_1`، `050_000_1` و `100_000_1` استفاده کند (ما از `_` به عنوان جداکننده هزاران استفاده میکنیم):
|
||||
|
||||

|
||||
|
||||
این فقط به 4 قاب فیزیکی نیاز دارد، اما جدول صفحه بیش از یک میلیون ورودی دارد. ما نمیتوانیم ورودیهای خالی را حذف کنیم زیرا در این صورت CPU دیگر نمیتواند مستقیماً به ورودی صحیح در فرآیند ترجمه پرش کند (به عنوان مثال، دیگر تضمین نمیشود که صفحه چهارم از ورودی چهارم استفاده کند).
|
||||
|
||||
برای کاهش حافظه هدر رفته، میتوانیم از یک **جدول صفحه دو سطحی** استفاده کنیم. ایده این است که ما از جدولهای صفحه مختلف برای ناحیه آدرس مختلف استفاده میکنیم. یک جدول اضافی با عنوان جدول صفحه _level 2_ شامل نگاشت بین ناحیه آدرس و جدولهای صفحه (سطح 1) است.
|
||||
|
||||
این بهتر است با یک مثال توضیح داده شود. بیایید تعریف کنیم که هر جدول صفحه 1 سطح مربوط به منطقهای با اندازه `000_10` است. سپس جدولهای زیر برای مثال نگاشت بالا وجود دارد:
|
||||
|
||||

|
||||
|
||||
صفحه 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 است و بنابراین دقیقاً در یک صفحه قرار میگیرد.
|
||||
|
||||
اندیس جدول صفحه برای سطح مستقیماً از آدرس مجازی مشتق میشود:
|
||||
|
||||

|
||||
|
||||
میبینیم که هر اندیس جدول از 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
|
||||
|
||||
### مثالی از ترجمه
|
||||
|
||||
بیایید مثالی بزنیم تا با جزئیات بفهمیم که روند ترجمه چگونه کار میکند:
|
||||
|
||||

|
||||
|
||||
آدرس فیزیکی جدول صفحه سطح 4 که در حال حاضر فعال میباشد، و ریشه جدول صفحه سطح 4 است، در ثبات `CR3` ذخیره میشود. سپس هر ورودی جدول صفحه به قاب فیزیکی جدول سطح بعدی اشاره میکند. سپس ورودی جدول سطح 1 به قاب نگاشت شده اشاره میکند. توجه داشته باشید که تمام آدرسهای موجود در جدولهای صفحه فیزیکی هستند، به جای اینکه مجازی باشند، زیرا در غیر اینصورت CPU نیاز به ترجمه آن آدرسها نیز دارد (که این امر میتواند باعث بازگشت بیپایان شود).
|
||||
|
||||
سلسله مراتب جدول صفحه بالا، دو صفحه را نگاشت میکند (به رنگ آبی). از اندیسهای جدول صفحه میتوان نتیجه گرفت که آدرسهای مجازی این دو صفحه `0x803FE7F000` و `0x803FE00000` است. بیایید ببینیم چه اتفاقی میافتد وقتی برنامه سعی میکند از آدرس `0x803FE7F5CE` بخواند. ابتدا آدرس را به باینری تبدیل میکنیم و اندیسهای جدول صفحه و آفست صفحه را برای آدرس تعیین میکنیم:
|
||||
|
||||

|
||||
|
||||
با استفاده از این اندیسها، اکنون میتوانیم سلسله مراتب جدول صفحه را برای تعیین قاب نگاشته شده برای آدرس دنبال کنیم:
|
||||
|
||||
- ما با خواندن آدرس جدول سطح 4 از ثبات `CR3` شروع میکنیم.
|
||||
- اندیس سطح 4 برابر با 1 است، بنابراین ما به ورودی با اندیس 1 آن جدول نگاه میکنیم، که به ما میگوید جدول سطح 3 در آدرس 16KiB ذخیره شده است.
|
||||
- ما جدول سطح 3 را از آن آدرس بارگیری میکنیم و ورودی با اندیس 0 را مشاهده میکنیم، که جدول سطح 2 در 24KiB را به ما نشان میدهد.
|
||||
- اندیس سطح 2 برابر با 511 است، بنابراین ما برای یافتن آدرس جدول سطح 1 به آخرین ورودی آن صفحه نگاه میکنیم.
|
||||
- از طریق ورودی با اندیس 127 جدول سطح 1، ما در نهایت متوجه میشویم که صفحه در قاب 12KiB، یا بصورت هگزادسیمال در 0x3000 نگاشت شده است.
|
||||
- مرحله آخر افزودن آفست صفحه به آدرس قاب است تا آدرس فیزیکی 0x3000 + 0x5ce = 0x35ce بدست آید.
|
||||
|
||||

|
||||
|
||||
مجوزهای صفحه در جدول سطح 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();
|
||||
}
|
||||
```
|
||||
|
||||
هنگامی که آن را اجرا میکنیم، میبینیم که کنترلکننده خطای صفحه ما صدا زده میشود:
|
||||
|
||||

|
||||
|
||||
ثبات `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");
|
||||
```
|
||||
|
||||
با کامنت کردن خط آخر، میبینیم که دسترسی خواندن کار میکند، اما دسترسی نوشتن باعث خطای صفحه میشود:
|
||||
|
||||

|
||||
|
||||
میبینیم که پیام _"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 یک آدرس فیزیکی را ذخیره میکند که ما نمیتوانیم مستقیماً از هسته به آن دسترسی داشته باشیم.
|
||||
|
||||
## بعدی چیست؟
|
||||
|
||||
در پست بعدی نحوه پیادهسازی پشتیبانی برای صفحهبندی در هسته توضیح داده شده است. که روشهای مختلفی برای دسترسی به حافظه فیزیکی از هسته ارائه میدهد، که دسترسی به جدولهای صفحهای که هسته در آن اجرا میشود را امکانپذیر میکند. در این مرحله ما میتوانیم توابع را برای ترجمه آدرسهای مجازی به فیزیکی و ایجاد نگاشتهای جدید در جدولهای صفحه پیادهسازی کنیم.
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -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`
|
||||
|
||||
@@ -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`:
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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:
|
||||
|
||||

|
||||

|
||||
|
||||
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.
|
||||
|
||||
|
||||
@@ -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<vga_buffer::ScreenChar>: 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`:
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -15,11 +15,11 @@
|
||||
<h2 class="post-title"><a href="{{ post.path | safe }}">{{ post.title }}</a></h2>
|
||||
<div class="post-summary">
|
||||
{{ post.summary | safe }}
|
||||
<a class="read-more" href="{{ post.path | safe }}"><em>read more »</em></a>
|
||||
<a class="read-more" href="{{ post.path | safe }}"><em>{{ trans(key="readmore", lang=lang) | safe }}</em></a>
|
||||
|
||||
{%- if lang and not_translated and lang != config.default_language -%}
|
||||
<aside class="no-translation">
|
||||
(This post is not translated yet.)
|
||||
{{ trans(key="not_translated", lang=lang) }}
|
||||
</aside>
|
||||
{%- endif -%}
|
||||
</div>
|
||||
@@ -28,7 +28,7 @@
|
||||
|
||||
{% macro toc(toc) %}
|
||||
<details id = "toc-inline">
|
||||
<summary><b>Table of Contents</b></summary>
|
||||
<summary><b>{{ trans(key="toc", lang=lang) }}</b></summary>
|
||||
<ul>
|
||||
{% for h2 in toc %}<li>
|
||||
<a href="#{{h2.id | safe}}">{{ h2.title | safe }}</a>
|
||||
@@ -38,7 +38,7 @@
|
||||
</li>{% endfor %}
|
||||
</ul>{% endif %}
|
||||
</li>{% endfor %}
|
||||
<li class="toc-comments-link"><a href="#comments">Comments</a></li>
|
||||
<li class="toc-comments-link"><a href="#comments">{{ trans(key="comments", lang=lang) }}</a></li>
|
||||
</ul>
|
||||
</details>
|
||||
{% endmacro toc %}
|
||||
|
||||
@@ -6,9 +6,9 @@
|
||||
{% block title %}{{ page.title }} | {{ config.title }}{% endblock title %}
|
||||
{% block header %}
|
||||
{% if lang != "en" -%}
|
||||
<aside id="all-posts-link"><a href="{{ config.base_url | safe }}/{{ lang }}" title="All Posts">« All Posts</a></aside>
|
||||
<aside id="all-posts-link"><a href="{{ config.base_url | safe }}/{{ lang }}" title="All Posts">{{ trans(key="all_posts", lang=lang) }}</a></aside>
|
||||
{%- else -%}
|
||||
<aside id="all-posts-link"><a href="{{ config.base_url | safe }}" title="All Posts">« All Posts</a></aside>
|
||||
<aside id="all-posts-link"><a href="{{ config.base_url | safe }}" title="All Posts">{{ trans(key="all_posts", lang=lang) }}</a></aside>
|
||||
{%- endif %}
|
||||
{% endblock header %}
|
||||
|
||||
@@ -17,8 +17,8 @@
|
||||
{%- endblock description %}
|
||||
|
||||
{% block toc_aside %}
|
||||
<aside id="toc-aside">
|
||||
<h2>Table of Contents</h2>
|
||||
<aside id="toc-aside" class="{% if page.extra.rtl %}right-to-left{% endif %}">
|
||||
<h2>{{ trans(key="toc", lang=lang) }}</h2>
|
||||
<ol>
|
||||
{% for h2 in page.toc %}<li>
|
||||
<a href="#{{h2.id | safe}}">{{ h2.title | safe }}</a>
|
||||
@@ -28,7 +28,7 @@
|
||||
</li>{% endfor %}
|
||||
</ol>{% endif %}
|
||||
</li>{% endfor %}
|
||||
<li class="toc-comments-link"><a href="#comments">Comments</a></li>
|
||||
<li class="toc-comments-link"><a href="#comments">{{ trans(key="comments", lang=lang) }}</a></li>
|
||||
</ol>
|
||||
</aside>
|
||||
{% endblock toc_aside %}
|
||||
@@ -49,19 +49,22 @@
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
|
||||
{%- if page.lang != "en" %}
|
||||
<div class="warning">
|
||||
<div class="warning{% if page.extra.rtl %} right-to-left{% endif %}">
|
||||
{% set translations = page.translations | filter(attribute="lang", value="en") %}
|
||||
{% set original = translations.0 %}
|
||||
<p>
|
||||
<b>Translated Content:</b>
|
||||
This is a community translation of the <strong><a href="{{ original.permalink }}">{{ original.title }}</a></strong> post. It might be incomplete, outdated or contain errors. Please report any issues!
|
||||
<b>{{ trans(key="translated_content", lang=lang) }}</b>
|
||||
{{ trans(key="translated_content_notice", lang=lang) |
|
||||
replace(from="_original.permalink_", to=original.permalink) |
|
||||
replace(from="_original.title_", to=original.title) | safe }}
|
||||
</p>
|
||||
{%- if page.extra.translators %}
|
||||
<p>
|
||||
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 -%}
|
||||
<a href="https://github.com/{{user}}">@{{user}}</a>
|
||||
{%- endfor %}.
|
||||
@@ -70,11 +73,11 @@
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div class="{% if page.extra.rtl %}right-to-left{% endif %}">
|
||||
<div class="{% if page.extra.rtl %}right-to-left{% endif %}">
|
||||
{{ page.content | replace(from="<!-- toc -->", to=macros::toc(toc=page.toc)) | safe }}
|
||||
</div>
|
||||
|
||||
<div class="post-footer-support">
|
||||
<div class="post-footer-support{% if page.extra.rtl %} right-to-left{% endif %}">
|
||||
<h2>Support Me</h2>
|
||||
{{ snippets::support() }}
|
||||
</div>
|
||||
@@ -91,11 +94,11 @@
|
||||
|
||||
<hr>
|
||||
<section>
|
||||
<h2 id="comments">Comments</h2>
|
||||
<h2 id="comments" class="{% if page.extra.rtl %}right-to-left{% endif %}">{{ trans(key="comments", lang=lang) }}</h2>
|
||||
|
||||
{%- if page.lang != "en" %}
|
||||
<p>
|
||||
Please leave your comments in English if possible.
|
||||
<p class="{% if page.extra.rtl %}right-to-left{% endif %}">
|
||||
{{ trans(key="comments_notice", lang=lang) }}
|
||||
</p>
|
||||
{% endif %}
|
||||
|
||||
@@ -118,12 +121,12 @@
|
||||
</div>
|
||||
{%- endif %}
|
||||
|
||||
<div class="block">
|
||||
<div class="block{% if page.extra.rtl %} left-to-right{% endif %}">
|
||||
<h2>About Me</h2>
|
||||
<p>
|
||||
I'm a Rust freelancer with a master's degree in computer science. I love systems programming, open source software, and new challenges.
|
||||
</p><p>
|
||||
If you want to work with me, reach out on <a href = "https://www.linkedin.com/in/phil-opp/">LinkedIn</a> or write me at <a href="mailto:job@phil-opp.com">job@phil-opp.com</a>.
|
||||
If you want to work with me, reach out on <a href="https://www.linkedin.com/in/phil-opp/">LinkedIn</a> or write me at <a href="mailto:job@phil-opp.com">job@phil-opp.com</a>.
|
||||
</p>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
<p>{{ section.description }}</p>
|
||||
{% endblock introduction %}
|
||||
|
||||
<div><ul>
|
||||
<div class="status-update-list"><ul>
|
||||
{% include "auto/status-updates.html" %}
|
||||
{% for page in section.pages %}
|
||||
<li><b><a href="{{ page.path | safe }}">{{ page.title }}</a></b></li>
|
||||
|
||||
Reference in New Issue
Block a user