diff --git a/.github/workflows/build-site.yml b/.github/workflows/blog.yml similarity index 87% rename from .github/workflows/build-site.yml rename to .github/workflows/blog.yml index 56a70214..fd51ce5f 100644 --- a/.github/workflows/build-site.yml +++ b/.github/workflows/blog.yml @@ -1,4 +1,4 @@ -name: Build Site +name: Blog on: push: @@ -20,7 +20,7 @@ jobs: - uses: actions/checkout@v1 - 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 + run: curl -sL https://github.com/getzola/zola/releases/download/v0.15.3/zola-v0.15.3-x86_64-unknown-linux-gnu.tar.gz | tar zxv - name: 'Install Python Libraries' run: python -m pip install --user -r requirements.txt working-directory: "blog" @@ -46,7 +46,7 @@ jobs: - uses: actions/checkout@v1 - 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 + run: curl -sL https://github.com/getzola/zola/releases/download/v0.15.3/zola-v0.15.3-x86_64-unknown-linux-gnu.tar.gz | tar zxv - name: "Run zola check" run: ../zola check @@ -59,16 +59,16 @@ jobs: steps: - uses: actions/checkout@v1 - - run: curl -L https://git.io/misspell | bash - name: "Install misspell" - - run: bin/misspell -error blog/content - name: "Check for common typos" + - name: Typo Check + uses: crate-ci/typos@v1.1.9 + with: + files: blog deploy_site: name: "Deploy Generated Site" runs-on: ubuntu-latest needs: [build_site, check_spelling] - if: github.ref == 'refs/heads/master' && (github.event_name == 'push' || github.event_name == 'schedule') + if: github.ref == 'refs/heads/main' && (github.event_name == 'push' || github.event_name == 'schedule') steps: - name: "Download Generated Site" diff --git a/blog/before_build.py b/blog/before_build.py index 458fed76..56f7d4c1 100644 --- a/blog/before_build.py +++ b/blog/before_build.py @@ -65,7 +65,7 @@ while True: month_str = datetime.date(1900, month, 1).strftime('%B') link = 'This Month in Rust OSDev (' + month_str + " " + str(year) + ") " - lines.append(u"
  • " + link + "
  • \n") + lines.append(u"
  • " + link + "
  • \n") month = month + 1 if month > 12: @@ -78,7 +78,7 @@ with io.open("templates/auto/status-updates.html", 'w', encoding='utf8') as stat status_updates.truncate() for line in lines: - status_updates.write("" + line + "") + status_updates.write(line) with io.open("templates/auto/status-updates-truncated.html", 'w', encoding='utf8') as status_updates: status_updates.truncate() @@ -86,4 +86,4 @@ with io.open("templates/auto/status-updates-truncated.html", 'w', encoding='utf8 for index, line in enumerate(lines): if index == 5: break - status_updates.write("" + line + "") + status_updates.write(line) diff --git a/blog/config.toml b/blog/config.toml index bdb4a06f..d96b4457 100644 --- a/blog/config.toml +++ b/blog/config.toml @@ -1,21 +1,17 @@ -title = "Writing an OS in Rust" -base_url = "https://os.phil-opp.com" -description = "This blog series creates a small operating system in the Rust programming language. Each post is a small tutorial and includes all needed code." -highlight_code = true -highlight_theme = "visual-studio-dark" +base_url = "https://os.phil-opp.com" + generate_feed = true feed_filename = "rss.xml" compile_sass = true +minify_html = false -languages = [ - { code = "zh-CN" }, # Chinese (simplified) - { code = "zh-TW" }, # Chinese (traditional) - { code = "ja" }, # Japanese - { code = "fa" }, # Persian -] +ignored_content = ["*/README.md", "LICENSE-CC-BY-NC"] -ignored_content = ["*/README.md"] +[markdown] +highlight_code = true +highlight_theme = "visual-studio-dark" +smart_punctuation = true [link_checker] skip_prefixes = [ @@ -28,14 +24,20 @@ skip_prefixes = [ skip_anchor_prefixes = [ "https://github.com/", # see https://github.com/getzola/zola/issues/805 "https://docs.rs/x86_64/0.1.2/src/", # source code highlight + "https://doc.rust-jp.rs/book-ja/", # seems like Zola has problems with Japanese anchor names ] [extra] subtitle = "Philipp Oppermann's blog" author = { name = "Philipp Oppermann" } +default_language = "en" +languages = ["en", "zh-CN", "zh-TW", "fr", "ja", "fa", "ru"] -[translations.en] -lang_name = "English" +[languages.en] +title = "Writing an OS in Rust" +description = "This blog series creates a small operating system in the Rust programming language. Each post is a small tutorial and includes all needed code." +[languages.en.translations] +lang_name = "English (original)" toc = "Table of Contents" all_posts = "« All Posts" comments = "Comments" @@ -47,33 +49,45 @@ translated_content_notice = "This is a community translation of the _original.title_ post. It might be incomplete, outdated or contain errors. Please report any issues!" -translated_by = "Translation by" -word_separator = "and" +toc = "目录" +all_posts = "« 所有文章" +comments = "评论" +comments_notice = "请尽可能使用英语评论。" +readmore = "更多 »" +not_translated = "(该文章还没有被翻译。)" +translated_content = "翻译内容:" +translated_content_notice = "这是对原文章 _original.title_ 的社区中文翻译。它可能不完整,过时或者包含错误。可以在 这个 Issue 上评论和提问!" +translated_by = "翻译者:" +word_separator = "和" -[translations.zh-TW] +# Chinese (traditional) +[languages.zh-TW] +title = "Writing an OS in Rust" +description = "This blog series creates a small operating system in the Rust programming language. Each post is a small tutorial and includes all needed code." +[languages.zh-TW.translations] lang_name = "Chinese (traditional)" -toc = "Table of Contents" -all_posts = "« All Posts" -comments = "Comments" -comments_notice = "Please leave your comments in English if possible." -readmore = "read more »" -not_translated = "(This post is not translated yet.)" -translated_content = "Translated Content:" -translated_content_notice = "This is a community translation of the _original.title_ post. It might be incomplete, outdated or contain errors. Please report any issues!" -translated_by = "Translation by" -word_separator = "and" +toc = "目錄" +all_posts = "« 所有文章" +comments = "評論" +comments_notice = "請儘可能使用英語評論。" +readmore = "更多 »" +not_translated = "(該文章還沒有被翻譯。)" +translated_content = "翻譯內容:" +translated_content_notice = "這是對原文章 _original.title_ 的社區中文翻譯。它可能不完整,過時或者包含錯誤。可以在 這個 Issue 上評論和提問!" +translated_by = "翻譯者:" +word_separator = "和" -[translations.ja] +# Japanese +[languages.ja] +title = "Writing an OS in Rust" +description = "This blog series creates a small operating system in the Rust programming language. Each post is a small tutorial and includes all needed code." +[languages.ja.translations] lang_name = "Japanese" toc = "目次" all_posts = "« すべての記事へ" @@ -86,7 +100,11 @@ translated_content_notice = "この記事は_original.title_ است. ممکن است ناقص، منسوخ شده یا دارای خطا باشد. لطفا هر گونه مشکل را در این ایشو گزارش دهید!" translated_by = "ترجمه توسط" word_separator = "و" + +# Russian +[languages.ru] +title = "Writing an OS in Rust" +description = "This blog series creates a small operating system in the Rust programming language. Each post is a small tutorial and includes all needed code." +[languages.ru.translations] +lang_name = "Russian" +toc = "Содержание" +all_posts = "« Все посты" +comments = "Комментарии" +comments_notice = "Пожалуйста, оставляйте комментарии на английском по возможности." +readmore = "читать дальше »" +not_translated = "(Этот пост еще не переведен.)" +translated_content = "Переведенное содержание:" +translated_content_notice = "Это перевод сообщества поста _original.title_. Он может быть неполным, устаревшим или содержать ошибки. Пожалуйста, сообщайте о любых проблемах!" +translated_by = "Перевод сделан" +word_separator = "и" + +# French +[languages.fr] +title = "Writing an OS in Rust" +description = "This blog series creates a small operating system in the Rust programming language. Each post is a small tutorial and includes all needed code." +[languages.fr.translations] +lang_name = "French" +toc = "Table des matières" +all_posts = "« Tous les articles" +comments = "Commentaires" +comments_notice = "Veuillez commenter en Anglais si possible." +readmore = "Voir plus »" +not_translated = "(Cet article n'est pas encore traduit.)" +translated_content = "Contenu traduit : " +translated_content_notice = "Ceci est une traduction communautaire de l'article _original.title_. Il peut être incomplet, obsolète ou contenir des erreurs. Veuillez signaler les quelconques problèmes !" +translated_by = "Traduit par : " +word_separator = "et" diff --git a/blog/content/_index.fr.md b/blog/content/_index.fr.md new file mode 100644 index 00000000..069dceff --- /dev/null +++ b/blog/content/_index.fr.md @@ -0,0 +1,13 @@ ++++ +template = "edition-2/index.html" ++++ + +

    Écrire un OS en Rust

    + +
    + +L'objectif de ce blog est de créer un petit système d'exploitation avec le [langage de programmation Rust](https://www.rust-lang.org/). Chaque article est un petit tutoriel et comprend tout le code nécessaire, vous pouvez donc essayer en même temps si vous le souhaitez. Le code source est aussi disponible dans le [dépôt GitHub](https://github.com/phil-opp/blog_os) correspondant. + +Dernier article : + +
    diff --git a/blog/content/_index.ru.md b/blog/content/_index.ru.md new file mode 100644 index 00000000..a9bb07ec --- /dev/null +++ b/blog/content/_index.ru.md @@ -0,0 +1,13 @@ ++++ +template = "edition-2/index.html" ++++ + +

    Собственная операционная система на Rust

    + +
    + +Этот блог посвящен написанию маленькой операционной системы на [языке программирования Rust](https://www.rust-lang.org/). Каждый пост — это маленькое руководство, включающее в себя весь необходимый код, — вы сможете следовать ему, если пожелаете. Исходный код также доступен в соотвестующем [репозитории на Github](https://github.com/phil-opp/blog_os). + +Последний пост: + +
    diff --git a/blog/content/_index.zh-CN.md b/blog/content/_index.zh-CN.md index 493e0c96..6000a287 100644 --- a/blog/content/_index.zh-CN.md +++ b/blog/content/_index.zh-CN.md @@ -2,12 +2,12 @@ template = "edition-2/index.html" +++ -

    Writing an OS in Rust

    +

    用Rust写一个操作系统

    -This blog series creates a small operating system in the [Rust programming language](https://www.rust-lang.org/). Each post is a small tutorial and includes all needed code, so you can follow along if you like. The source code is also available in the corresponding [Github repository](https://github.com/phil-opp/blog_os). - -Latest post: +这个博客系列用[Rust编程语言](https://www.rust-lang.org/)编写了一个小操作系统。每篇文章都是一个小教程,并且包含了所有代码,你可以跟着一起学习。源代码也放在了[Github 仓库](https://github.com/phil-opp/blog_os)。 + +最新文章:
    diff --git a/blog/content/edition-1/extra/naked-exceptions/02-better-exception-messages/index.md b/blog/content/edition-1/extra/naked-exceptions/02-better-exception-messages/index.md index 17289421..9f98c962 100644 --- a/blog/content/edition-1/extra/naked-exceptions/02-better-exception-messages/index.md +++ b/blog/content/edition-1/extra/naked-exceptions/02-better-exception-messages/index.md @@ -628,7 +628,7 @@ bitflags! { - When the `PROTECTION_VIOLATION` flag is set, the page fault was caused e.g. by a write to a read-only page. If it's not set, it was caused by accessing a non-present page. - The `CAUSED_BY_WRITE` flag specifies if the fault was caused by a write (if set) or a read (if not set). -- The `USER_MODE` flag is set when the fault occurred in non-priviledged mode. +- The `USER_MODE` flag is set when the fault occurred in non-privileged mode. - The `MALFORMED_TABLE` flag is set when the page table entry has a 1 in a reserved field. - When the `INSTRUCTION_FETCH` flag is set, the page fault occurred while fetching the next instruction. diff --git a/blog/content/edition-1/extra/naked-exceptions/03-returning-from-exceptions/index.md b/blog/content/edition-1/extra/naked-exceptions/03-returning-from-exceptions/index.md index 5ce204a0..7858f197 100644 --- a/blog/content/edition-1/extra/naked-exceptions/03-returning-from-exceptions/index.md +++ b/blog/content/edition-1/extra/naked-exceptions/03-returning-from-exceptions/index.md @@ -144,7 +144,7 @@ EXCEPTION: BREAKPOINT at 0x110970 So let's disassemble the instruction at `0x110970` and its predecessor: -```shell +```bash > objdump -d build/kernel-x86_64.bin | grep -B1 "110970:" 11096f: cc int3 110970: 48 c7 01 2a 00 00 00 movq $0x2a,(%rcx) @@ -426,7 +426,7 @@ The page fault is gone and we see the _“It did not crash”_ message again! So the page fault occurred because our exception handler didn't preserve the scratch register `rax`. Our new `handler!` macro fixes this problem by saving all scratch registers (including `rax`) before calling exception handlers. Thus, `rax` still contains the valid memory address when `rust-main` continues execution. ## Multimedia Registers -When we discussed calling conventions above, we assummed that a x86_64 CPU only has the following 16 registers: `rax`, `rbx`, `rcx`, `rdx`, `rsi`, `rdi`, `rsp`, `rbp`, `r8`, `r9`, `r10`, `r11`.`r12`, `r13`, `r14`, and `r15`. These registers are called _general purpose registers_ since each of them can be used for arithmetic and load/store instructions. +When we discussed calling conventions above, we assumed that a x86_64 CPU only has the following 16 registers: `rax`, `rbx`, `rcx`, `rdx`, `rsi`, `rdi`, `rsp`, `rbp`, `r8`, `r9`, `r10`, `r11`.`r12`, `r13`, `r14`, and `r15`. These registers are called _general purpose registers_ since each of them can be used for arithmetic and load/store instructions. However, modern CPUs also have a set of _special purpose registers_, which can be used to improve performance in several use cases. On x86_64, the most important set of special purpose registers are the _multimedia registers_. These registers are larger than the general purpose registers and can be used to speed up audio/video processing or matrix calculations. For example, we could use them to add two 4-dimensional vectors _in a single CPU instruction_: diff --git a/blog/content/edition-1/posts/08-kernel-heap/index.md b/blog/content/edition-1/posts/08-kernel-heap/index.md index 3b3e5dbd..9013ba56 100644 --- a/blog/content/edition-1/posts/08-kernel-heap/index.md +++ b/blog/content/edition-1/posts/08-kernel-heap/index.md @@ -415,7 +415,7 @@ pub fn init(boot_info: &BootInformation) { We've just moved the code to a new function. However, we've sneaked some improvements in: - An additional `.filter(|s| s.is_allocated())` in the calculation of `kernel_start` and `kernel_end`. This ignores all sections that aren't loaded to memory (such as debug sections). Thus, the kernel end address is no longer artificially increased by such sections. -- We use the `start_address()` and `end_address()` methods of `boot_info` instead of calculating the adresses manually. +- We use the `start_address()` and `end_address()` methods of `boot_info` instead of calculating the addresses manually. - We use the alternate `{:#x}` form when printing kernel/multiboot addresses. Before, we used `0x{:x}`, which leads to the same result. For a complete list of these “alternate” formatting forms, check out the [std::fmt documentation]. [std::fmt documentation]: https://doc.rust-lang.org/nightly/std/fmt/index.html#sign0 @@ -675,7 +675,7 @@ I created the [linked_list_allocator] crate to handle all of these cases. It con We need to add the extern crate to our `Cargo.toml` and our `lib.rs`: -``` shell +``` bash > cargo add linked_list_allocator ``` diff --git a/blog/content/edition-1/posts/09-handling-exceptions/index.md b/blog/content/edition-1/posts/09-handling-exceptions/index.md index 09685ae4..e3b896ed 100644 --- a/blog/content/edition-1/posts/09-handling-exceptions/index.md +++ b/blog/content/edition-1/posts/09-handling-exceptions/index.md @@ -379,7 +379,7 @@ Note how this solution requires no `unsafe` blocks or `unwrap` calls. > ##### Aside: How does the `lazy_static!` macro work? > -> The macro generates a `static` of type `Once`. The [`Once`][spin::Once] type is provided by the `spin` crate and allows deferred one-time initialization. It is implemented using an [`AtomicUsize`] for synchronization and an [`UnsafeCell`] for storing the (possibly unitialized) value. So this solution also uses `unsafe` behind the scenes, but it is abstracted away in a safe interface. +> The macro generates a `static` of type `Once`. The [`Once`][spin::Once] type is provided by the `spin` crate and allows deferred one-time initialization. It is implemented using an [`AtomicUsize`] for synchronization and an [`UnsafeCell`] for storing the (possibly uninitialized) value. So this solution also uses `unsafe` behind the scenes, but it is abstracted away in a safe interface. [spin::Once]: https://docs.rs/spin/0.4.5/spin/struct.Once.html [`AtomicUsize`]: https://doc.rust-lang.org/nightly/core/sync/atomic/struct.AtomicUsize.html diff --git a/blog/content/edition-2/extra/building-on-android/index.md b/blog/content/edition-2/extra/building-on-android/index.md index 2e0bcb39..0ec2235a 100644 --- a/blog/content/edition-2/extra/building-on-android/index.md +++ b/blog/content/edition-2/extra/building-on-android/index.md @@ -13,7 +13,7 @@ I finally managed to get `blog_os` building on my Android phone using [termux](h ### Install Termux and Nightly Rust -First, install [termux](https://termux.com/) from the [Google Play Store](https://play.google.com/store/apps/details?id=com.termux) or from [F-Droid](https://f-droid.org/packages/com.termux/). After installing, open it and perform the following steps: +First, install [termux](https://termux.com/) from the [Google Play Store](https://play.google.com/store/apps/details?id=com.termux) or from F-Droid. After installing, open it and perform the following steps: - Install fish shell, set as default shell, and launch it: ``` diff --git a/blog/content/edition-2/posts/01-freestanding-rust-binary/index.fa.md b/blog/content/edition-2/posts/01-freestanding-rust-binary/index.fa.md index 91254005..cf9a603e 100644 --- a/blog/content/edition-2/posts/01-freestanding-rust-binary/index.fa.md +++ b/blog/content/edition-2/posts/01-freestanding-rust-binary/index.fa.md @@ -23,6 +23,7 @@ rtl = true [گیت‌هاب]: https://github.com/phil-opp/blog_os [در زیر]: #comments + [post branch]: https://github.com/phil-opp/blog_os/tree/post-01 @@ -160,7 +161,7 @@ fn panic(_info: &PanicInfo) -> ! { [آیتم زبان `eh_personality`]: https://github.com/rust-lang/rust/blob/edb368491551a77d77a48446d4ee88b35490c565/src/libpanic_unwind/gcc.rs#L11-L45 [بازکردن پشته (Stack Unwinding)]: https://www.bogotobogo.com/cplusplus/stackunwinding.php [libunwind]: https://www.nongnu.org/libunwind/ -[مدیریت اکسپشن ساخت یافته]: https://docs.microsoft.com/de-de/windows/win32/debug/structured-exception-handling +[مدیریت اکسپشن ساخت یافته]: https://docs.microsoft.com/en-us/windows/win32/debug/structured-exception-handling ### غیرفعال کردن Unwinding @@ -457,7 +458,7 @@ rustflags = ["-C", "link-args=-e __start -static -nostartfiles"] -## خلاصه +## خلاصه {#summary} یک باینری مستقل مینیمال راست مانند این است: diff --git a/blog/content/edition-2/posts/01-freestanding-rust-binary/index.fr.md b/blog/content/edition-2/posts/01-freestanding-rust-binary/index.fr.md new file mode 100644 index 00000000..5c70882a --- /dev/null +++ b/blog/content/edition-2/posts/01-freestanding-rust-binary/index.fr.md @@ -0,0 +1,524 @@ ++++ +title = "A Freestanding Rust Binary" +weight = 1 +path = "fr/freestanding-rust-binary" +date = 2018-02-10 + +[extra] +chapter = "Bare Bones" +# Please update this when updating the translation +translation_based_on_commit = "3e87916b6c2ed792d1bdb8c0947906aef9013ac1" +# GitHub usernames of the people that translated this post +translators = ["Alekzus"] ++++ + +La première étape pour créer notre propre noyau de système d'exploitation est de créer un exécutable Rust qui ne relie pas la bibliothèque standard. Cela rend possible l'exécution du code Rust sur la ["bare machine"][machine nue] sans système d'exploitation sous-jacent. + +[machine nue]: https://en.wikipedia.org/wiki/Bare_machine + + + +Ce blog est développé sur [GitHub]. Si vous avez un problème ou une question, veuillez ouvrir une issue. Vous pouvez aussi laisser un commentaire [en bas de page]. Le code source complet de cet article est disponible sur la branche [`post-01`][post branch]. + +[GitHub]: https://github.com/phil-opp/blog_os +[en bas de page]: #comments + +[post branch]: https://github.com/phil-opp/blog_os/tree/post-01 + + + +## Introduction +Pour écrire un noyau de système d'exploitation, nous avons besoin d'un code qui ne dépend pas de fonctionnalités de système d'exploitation. Cela signifie que nous ne pouvons pas utiliser les fils d'exécution, les fichiers, la mémoire sur le tas, le réseau, les nombres aléatoires, la sortie standard ou tout autre fonctionnalité nécessitant une abstraction du système d'exploitation ou un matériel spécifique. Cela a du sens, étant donné que nous essayons d'écrire notre propre OS et nos propres pilotes. +Cela signifie que nous ne pouvons pas utiliser la majeure partie de la [bibliothèque standard de Rust]. Il y a néanmoins beaucoup de fonctionnalités de Rust que nous _pouvons_ utiliser. Par exemple, nous pouvons utiliser les [iterators], les [closures], le [pattern matching], l'[option] et le [result], le [string formatting], et bien-sûr l'[ownership system]. Ces fonctionnalités permettent l'écriture d'un noyeau d'une façon expressive et haut-niveau sans se soucier des [comportements indéfinis] ou de la [sécurité de la mémoire]. + +[option]: https://doc.rust-lang.org/core/option/ +[result]:https://doc.rust-lang.org/core/result/ +[bibliothèque standard de Rust]: https://doc.rust-lang.org/std/ +[iterators]: https://doc.rust-lang.org/book/ch13-02-iterators.html +[closures]: https://doc.rust-lang.org/book/ch13-01-closures.html +[pattern matching]: https://doc.rust-lang.org/book/ch06-00-enums.html +[string formatting]: https://doc.rust-lang.org/core/macro.write.html +[ownership system]: https://doc.rust-lang.org/book/ch04-00-understanding-ownership.html +[comportement non-défini]: https://www.nayuki.io/page/undefined-behavior-in-c-and-cplusplus-programs +[sécurité de la mémoire]: https://tonyarcieri.com/it-s-time-for-a-memory-safety-intervention + +Pour créer un noyau d'OS en Rust, nous devons créer un exécutable qui peut tourner sans système d'exploitation sous-jacent. Un tel exécutable est appelé “freestanding” (autoporté) ou “bare-metal”. +Cet article décrit les étapes nécessaires pour créer un exécutable Rust autoporté et explique pourquoi ces étapes sont importantes. Si vous n'êtes intéressé que par un exemple minimal, vous pouvez **[aller au résumé](#resume)**. + +## Désactiver la Bibliothèque Standard + +Par défaut, tous les crates Rust relient la [bibliothèque standard], qui dépend du système d'exploitation pour les fonctionnalités telles que les fils d'exécution, les fichiers ou le réseau. Elle dépend aussi de la bibliothèque standard de C `libc`, qui intéragit de près avec les services de l'OS. Comme notre plan est d'écrire un système d'exploitation, nous ne pouvons pas utiliser des bibliothèques dépendant de l'OS. Nous devons donc désactiver l'inclusion automatique de la bibliothèque standard en utilisant l'[attribut `no std`]. + +[bibliothèque standard]: https://doc.rust-lang.org/std/ +[attribut `no std`]: https://doc.rust-lang.org/1.30.0/book/first-edition/using-rust-without-the-standard-library.html + +Nous commencons par créer un nouveau projet d'application cargo. La manière la plus simple de faire est avec la ligne de commande : + +``` +cargo new blog_os --bin --edition 2018 +``` + +J'ai nommé le projet `blog_os`, mais vous pouvez bien-sûr choisir le nom qu'il vous convient. Le flag `--bin` indique que nous voulons créer un exécutable (contrairement à une bibliothèque) et le flag `--edition 2018` indique que nous voulons utiliser l'[édition 2018] de Rust pour notre crate. Quand nous lançons la commande, cargo crée la structure de répertoire suivante pour nous : + +[édition 2018]: https://doc.rust-lang.org/nightly/edition-guide/rust-2018/index.html + +``` +blog_os +├── Cargo.toml +└── src + └── main.rs +``` + +Le fichier `Cargo.toml` contient la configuration de la crate, par exemple le nom de la crate, l'auteur, le numéro de [versionnage sémantique] et les dépendances. Le fichier `src/main.rs` contient le module racine de notre crate et notre fonction `main`. Vous pouvez compiler votre crate avec `cargo build` et ensuite exécuter l'exécutable compilé `blog_os` dans le sous-dossier `target/debug`. + +[versionnage sémantique]: https://semver.org/ + +### L'Attribut `no_std` + +Pour l'instant, notre crate relie la bilbiothèque standard implicitement. Désactivons cela en ajoutant l'[attribut `no std`] : + +```rust +// main.rs + +#![no_std] + +fn main() { + println!("Hello, world!"); +} +``` + +Quand nous essayons maintenant de compiler (avec `cargo build)`, l'erreur suivante se produit : + +``` +error: cannot find macro `println!` in this scope + --> src/main.rs:4:5 + | +4 | println!("Hello, world!"); + | ^^^^^^^ +``` + +La raison est que la [macro `println`] fait partie de la bibliothèque standard, que nous ne pouvons plus utiliser. Nous ne pouvons donc plus afficher de texte avec. Cela est logique, car `println` écrit dans la [sortie standard], qui est un descripteur de fichier spécial fourni par le système d'eploitation. + +[macro `println`]: https://doc.rust-lang.org/std/macro.println.html +[sortie standard]: https://fr.wikipedia.org/wiki/Flux_standard#Sortie_standard + +Supprimons l'affichage et essayons à nouveau avec une fonction main vide : + +```rust +// main.rs + +#![no_std] + +fn main() {} +``` + +``` +> cargo build +error: `#[panic_handler]` function required, but not found +error: language item required, but not found: `eh_personality` +``` + +Maintenant le compilateur a besoin d'une fonction `#[panic_handler]` et d'un _objet de langage_. + +## Implémentation de Panic + +L'attribut `panic_handler` définit la fonction que le compilateur doit appeler lorsqu'un [panic] arrive. La bibliothèque standard fournit sa propre fonction de gestion de panic mais dans un environnement `no_std`, nous avons besoin de le définir nous-mêmes : + +[panic]: https://doc.rust-lang.org/stable/book/ch09-01-unrecoverable-errors-with-panic.html + +```rust +// dans main.rs + +use core::panic::PanicInfo; + +/// Cette fonction est appelée à chaque panic. +#[panic_handler] +fn panic(_info: &PanicInfo) -> ! { + loop {} +} +``` + +Le [paramètre `PanicInfo`][PanicInfo] contient le fichier et la ligne où le panic a eu lieu et le message optionnel de panic. La fonction ne devrait jamais retourner quoi que ce soit, elle est donc marquée comme [fonction divergente] en retournant le [type “never”] `!`. Nous ne pouvons pas faire grand chose dans cette fonction pour le moment, nous bouclons donc indéfiniment. + +[PanicInfo]: https://doc.rust-lang.org/nightly/core/panic/struct.PanicInfo.html +[fonction divergente]: https://doc.rust-lang.org/1.30.0/book/first-edition/functions.html#diverging-functions +[type “never”]: https://doc.rust-lang.org/nightly/std/primitive.never.html + +## L'Objet de Langage `eh_personality` + +Les objets de langage sont des fonctions et des types spéciaux qui sont requis par le compilateur de manière interne. Par exemple, le trait [`Copy`] est un objet de langage qui indique au compilateur quels types possèdent la [sémantique copy][`Copy`]. Quand nous regardons l'[implémentation][copy code] du code, nous pouvons voir qu'il possède l'attribut spécial `#[lang = copy]` qui le définit comme étant un objet de langage. + +[`Copy`]: https://doc.rust-lang.org/nightly/core/marker/trait.Copy.html +[copy code]: https://github.com/rust-lang/rust/blob/485397e49a02a3b7ff77c17e4a3f16c653925cb3/src/libcore/marker.rs#L296-L299 + +Bien qu'il soit possible de fournir des implémentations personnalisées des objets de langage, cela ne devrait être fait qu'en dernier recours. La raison est que les objets de langages sont des détails d'implémentation très instables et qui ne sont même pas vérifiés au niveau de leur type (donc le compilateur ne vérifie même pas qu'une fonction possède les bons types d'arguments). Heureusement, il y a une manière plus robuste de corriger l'erreur d'objet de langage ci-dessus. + +L'[objet de langage `eh_personality`] marque une fonction qui est utilisée pour l'implémentation du [déroulement de pile]. Par défaut, Rust utilise le déroulement de pile pour exécuter les destructeurs de chaque variable vivante sur la pile en cas de [panic]. Cela assure que toute la mémoire utilisée est libérée et permet au fil d'exécution parent d'attraper la panic et de continuer l'exécution. Le déroulement toutefois est un processus compliqué et nécessite des bibliothèques spécifiques à l'OS ([libunwind] pour Linux ou [gestion structurée des erreurs] pour Windows), nous ne voulons donc pas l'utiliser pour notre système d'exploitation. + +[objet de langage `eh_personality`]: https://github.com/rust-lang/rust/blob/edb368491551a77d77a48446d4ee88b35490c565/src/libpanic_unwind/gcc.rs#L11-L45 +[déroulement de pile]: https://docs.microsoft.com/fr-fr/cpp/cpp/exceptions-and-stack-unwinding-in-cpp?view=msvc-160 +[libunwind]: https://www.nongnu.org/libunwind/ +[gestion structurée des erreurs]: https://docs.microsoft.com/fr-fr/windows/win32/debug/structured-exception-handling + +### Désactiver le Déroulement + +Il y a d'autres cas d'utilisation pour lesquels le déroulement n'est pas souhaité. Rust offre donc une option pour [interrompre après un panic]. Cela désactive la génération de symboles de déroulement et ainsi réduit considérablement la taille de l'exécutable. Il y a de multiples endroit où nous pouvons désactiver le déroulement. Le plus simple est d'ajouter les lignes suivantes dans notre `Cargo.toml` : + +```toml +[profile.dev] +panic = "abort" + +[profile.release] +panic = "abort" +``` + +Cela configure la stratégie de panic à `abort` pour le profil `dev` (utilisé pour `cargo build`) et le profil `release` (utilisé pour `cargo build --release`). Maintenant l'objet de langage `eh_personality` ne devrait plus être requis. + +[interrompre après un panic]: https://github.com/rust-lang/rust/pull/32900 + +Nous avons dorénavant corrigé les deux erreurs ci-dessus. Toutefois, si nous essayons de compiler, une autre erreur apparaît : + +``` +> cargo build +error: requires `start` lang_item +``` + +L'objet de langage `start` manque à notre programme. Il définit le point d'entrée. + +## L'attribut `start` + +On pourrait penser que la fonction `main` est la première fonction appelée lorsqu'un programme est exécuté. Toutefois, la plupart des langages ont un [environnement d'exécution] qui est responsable des tâches telles que le ramassage des miettes (ex: dans Java) ou les fils d'exécution logiciel (ex: les goroutines dans Go). Cet environnement doit être appelé avant `main` puisqu'il a besoin de s'initialiser. + +[environnement d'exécution]: https://fr.wikipedia.org/wiki/Environnement_d%27ex%C3%A9cution + +Dans un exécutable Rust classique qui relie la bibliothèque standard, l'exécution commence dans une bibliothèque d'environnement d'exécution C appelé `crt0` (“C runtime zero”). Elle configure l'environnement pour une application C. Cela comprend la création d'une pile et le placement des arguments dans les bons registres. L'environnement d'exécution C appelle ensuite [le point d'entrée de l'environnement d'exécution de Rust][rt::lang_start], qui est marqué par l'objet de langage `start`. Rust possède un environnement d'exécution très minime, qui se charge de petites tâches telles que la configuration des guardes de dépassement de pile ou l'affichage de la trace d'appels lors d'un panic. L'environnement d'exécution finit par appeler la fonction `main`. + +[rt::lang_start]: https://github.com/rust-lang/rust/blob/bb4d1491466d8239a7a5fd68bd605e3276e97afb/src/libstd/rt.rs#L32-L73 + +Notre exécutable autoporté n'a pas accès à l'environnement d'exécution de Rust ni à `crt0`. Nous avons donc besion de définir notre propre point d'entrée. Implémenter l'objet de langage `start` n'aiderait pas car nous aurions toujours besoin de `crt0`. Nous avons plutôt besoin de réécrire le point d'entrée de `crt0` directement. + +### Réécrire le Point d'Entrée + +Pour indiquer au compilateur que nous ne voulons pas utiliser la chaîne de point d'entrée normale, nous ajoutons l'attribut `#![no_main]`. + +```rust +#![no_std] +#![no_main] + +use core::panic::PanicInfo; + +/// Cette fonction est appelée à chaque panic. +#[panic_handler] +fn panic(_info: &PanicInfo) -> ! { + loop {} +} +``` + +Vous remarquerez peut-être que nous avons retiré la fonction `main`. La raison est que la présence de cette fonction n'a pas de sens sans un environnement d'exécution sous-jacent qui l'appelle. À la place, nous réécrivons le point d'entrée du système d'exploitation avec notre propre fonction `_start` : + +```rust +#[no_mangle] +pub extern "C" fn _start() -> ! { + loop {} +} +``` + +En utilisant l'attribut `#[no_mangle]`, nous désactivons la [décoration de nom] pour assurer que le compilateur Rust crée une fonction avec le nom `_start`. Sans cet attribut, le compilateur génèrerait un symbol obscure `_ZN3blog_os4_start7hb173fedf945531caE` pour donner un nom unique à chaque fonction. L'attribut est nécessaire car nous avons besoin d'indiquer le nom de la fonction de point d'entrée à l'éditeur de lien (*linker*) dans l'étape suivante. + +Nous devons aussi marquer la fonction avec `extern C` pour indiquer au compilateur qu'il devrait utiliser la [convention de nommage] de C pour cette fonction (au lieu de la convention de nommage de Rust non-spécifiée). Cette fonction se nomme `_start` car c'est le nom par défaut des points d'entrée pour la plupart des systèmes. + +[décoration de nom]: https://fr.wikipedia.org/wiki/D%C3%A9coration_de_nom +[convention de nommage]: https://fr.wikipedia.org/wiki/Convention_de_nommage + +Le type de retour `!` signifie que la fonction est divergente, c-à-d qu'elle n'a pas le droit de retourner quoi que ce soit. Cela est nécessaire car le point d'entrée n'est pas appelé par une fonction, mais invoqué directement par le système d'exploitation ou par le chargeur d'amorçage. Donc au lieu de retourner une valeur, le point d'entrée doit invoquer l'[appel système `exit`] du système d'exploitation. Dans notre cas, arrêter la machine pourrait être une action convenable, puisqu'il ne reste rien d'autre à faire si un exécutable autoporté s'arrête. Pour l'instant, nous remplissons la condition en bouclant indéfiniement. + +[appel système `exit`]: https://fr.wikipedia.org/wiki/Appel_syst%C3%A8me + +Quand nous lançons `cargo build`, nous obtenons une erreur de _linker_. + +## Erreurs de Linker + +Le linker est un programme qui va transformer le code généré en exécutable. Comme le format de l'exécutable differt entre Linux, Windows et macOS, chaque système possède son propre linker qui lève une erreur différente. La cause fondamentale de cette erreur est la même : la configuration par défaut du linker part du principe que notre programme dépend de l'environnement d'exécution de C, ce qui n'est pas le cas. + +Pour résoudre les erreurs, nous devons indiquer au linker qu'il ne doit pas inclure l'environnement d'exécution de C. Nous pouvons faire cela soit en passant un ensemble précis d'arguments, soit en compilant pour une cible bare metal. + +### Compiler pour une Cible Bare Metal + +Par défaut Rust essaie de compiler un exécutable qui est compatible avec l'environnment du système actuel. Par exemple, si vous utilisez Windows avec `x86_64`, Rust essaie de compiler un exécutable Windows `.exe` qui utilises des instructions `x86_64`. Cet environnement est appelé système "hôte". + +Pour décrire plusieurs environnements, Rust utilise une chaîne de caractères appelée [_triplé cible_]. Vous pouvez voir le triplé cible de votre système hôte en lançant la commande `rustc --version --verbose` : + +[_triplé cible_]: https://clang.llvm.org/docs/CrossCompilation.html#target-triple + +``` +rustc 1.35.0-nightly (474e7a648 2019-04-07) +binary: rustc +commit-hash: 474e7a6486758ea6fc761893b1a49cd9076fb0ab +commit-date: 2019-04-07 +host: x86_64-unknown-linux-gnu +release: 1.35.0-nightly +LLVM version: 8.0 +``` + +La sortie ci-dessus provient d'un système Linux `x86_64`. Nous pouvons voir que le triplé `host` est `x86_64-unknown-linux-gnu`, qui inclut l'architecture du CPU (`x86_64`), le vendeur (`unknown`), le système d'exploitation (`linux`) et l'[ABI] (`gnu`). + +[ABI]: https://fr.wikipedia.org/wiki/Application_binary_interface + +En compilant pour notre triplé hôte, le compilateur Rust ainsi que le linker supposent qu'il y a un système d'exploitation sous-jacent comme Linux ou Windows qui utilise l'environnement d'exécution C par défaut, ce qui cause les erreurs de linker. Donc pour éviter ces erreurs, nous pouvons compiler pour un environnement différent sans système d'exploitation sous-jacent. + +Un exemple d'un tel envrironnement est le triplé cible `thumbv7em-none-eabihf`, qui décrit un système [ARM] [embarqué]. Les détails ne sont pas importants, tout ce qui compte est que le triplé cible n'a pas de système d'exploitation sous-jacent, ce qui est indiqué par le `none` dans le triplé cible. Pour pouvoir compilé pour cette cible, nous avons besoin de l'ajouter dans rustup : + +[embarqué]: https://fr.wikipedia.org/wiki/Syst%C3%A8me_embarqu%C3%A9 +[ARM]: https://fr.wikipedia.org/wiki/Architecture_ARM + +``` +rustup target add thumbv7em-none-eabihf +``` + +Cela télécharge une copie de la bibliothèque standard (et core) pour le système. Maintenant nous pouvons compiler notre exécutable autoporté pour cette cible : + +``` +cargo build --target thumbv7em-none-eabihf +``` + +En donnant un argument `--target`, nous effectuons une [compilation croisée][cross_compile] de notre exécutable pour un système bare metal. Comme le système cible n'a pas de système d'exploitation, le linker n'essaie pas de lier l'environnement d'exécution C et notre compilation réussit sans erreur de linker. + +[cross compile]: https://en.wikipedia.org/wiki/Cross_compiler + +C'est l'approche que nous allons utiliser pour construire notre noyau d'OS. Plutôt que `thumbv7em-none-eabihf`, nous allons utiliser une [cible personnalisée][custom target] qui décrit un environnement bare metal `x86_64`. Les détails seront expliqués dans le prochain article. + +[custom target]: https://doc.rust-lang.org/rustc/targets/custom.html + +### Arguments du Linker + +Au lieu de compiler pour un système bare metal, il est aussi possible de résoudre les erreurs de linker en passant un ensemble précis d'arguments au linker. Ce n'est pas l'approche que nous allons utiliser pour notre noyau. Cette section est donc optionnelle et fournis uniquement à titre de complétude. Cliquez sur _"Arguments du Linker"_ ci-dessous pour montrer le contenu optionel. + +
    + +Arguments du Linker + +Dans cette section nous allons parler des erreurs de linker qui se produisent sur Linux, Windows et macOS. Nous allons aussi apprendre à résoudre ces erreurs en passant des arguments complémentaires au linker. À noter que le format de l'exécutable et le linker diffèrent entre les systèmes d'exploitation. Il faut donc un ensemble d'arguments différent pour chaque système. + +#### Linux + +Sur Linux, voici l'erreur de linker qui se produit (raccourcie) : + +``` +error: linking with `cc` failed: exit code: 1 + | + = note: "cc" […] + = note: /usr/lib/gcc/../x86_64-linux-gnu/Scrt1.o: In function `_start': + (.text+0x12): undefined reference to `__libc_csu_fini' + /usr/lib/gcc/../x86_64-linux-gnu/Scrt1.o: In function `_start': + (.text+0x19): undefined reference to `__libc_csu_init' + /usr/lib/gcc/../x86_64-linux-gnu/Scrt1.o: In function `_start': + (.text+0x25): undefined reference to `__libc_start_main' + collect2: error: ld returned 1 exit status +``` + +Le problème est que le linker inclut par défaut la routine de démarrage de l'environnement d'exécution de C, qui est aussi appelée `_start`. Elle requiert des symboles de la bibliothèque standard de C `libc` que nous n'incluons pas à cause de l'attribut `no_std`. Le linker ne peut donc pas résoudre ces références. Pour résoudre cela, nous pouvons indiquer au linker qu'il ne devrait pas lier la routine de démarrage de C en passant l'argument `-nostartfiles`. + +Une façon de passer des attributs au linker via cargo est la commande `cargo rustc`. Cette commande se comporte exactement comme `cargo build`, mais permet aussi de donner des options à `rustc`, le compilateur Rust sous-jacent. `rustc` possède le flag `-C link-arg`, qui donne un argument au linker. Combinés, notre nouvelle commande ressemble à ceci : + +``` +cargo rustc -- -C link-arg=-nostartfiles +``` + +Dorénavant notre crate compile en tant qu'exécutable Linux autoporté ! + +Nous n'avions pas besoin de spécifier le nom de notre point d'entrée de façon explicite car le linker cherche par défaut une fonction nommée `_start`. + +#### Windows + +Sur Windows, une erreur de linker différente se produit (raccourcie) : + +``` +error: linking with `link.exe` failed: exit code: 1561 + | + = note: "C:\\Program Files (x86)\\…\\link.exe" […] + = note: LINK : fatal error LNK1561: entry point must be defined +``` + +Cette erreur signifie que le linker ne peut pas trouver le point d'entrée. Sur Windows, le nom par défaut du point d'entrée [dépend du sous-système utilisé][windows-subsystems]. Pour le sous-système `CONSOLE`, le linker cherche une fonction nommée `mainCRTStartup` et pour le sous-système `WINDOWS`, il cherche une fonction nomée `WinMainCRTStartup`. Pour réécrire la valeur par défaut et indiquer au linker de chercher notre fonction `_start` à la place, nous pouvons donner l'argument `/ENTRY` au linker : + +[windows-subsystems]: https://docs.microsoft.com/fr-fr/cpp/build/reference/entry-entry-point-symbol?view=msvc-160 + +``` +cargo rustc -- -C link-arg=/ENTRY:_start +``` + +Vu le format d'argument différent nous pouvons clairement voir que le linker Windows est un programme totalement différent du linker Linux. + +Maintenant une erreur de linker différente se produit : + +``` +error: linking with `link.exe` failed: exit code: 1221 + | + = note: "C:\\Program Files (x86)\\…\\link.exe" […] + = note: LINK : fatal error LNK1221: a subsystem can't be inferred and must be + defined +``` + +Cette erreur se produit car les exécutables Windows peuvent utiliser différents [sous-systèmes][windows-subsystems]. Pour les programmes normaux, ils sont inférés en fonction du nom du point d'entrée : s'il est nommé `main`, le sous-système `CONSOLE` est utilisé. Si le point d'entrée est nommé `WinMain`, alors le sous-sytème `WINDOWS` est utilisé. Comme notre fonction `_start` possède un nom différent, nous devons préciser le sous-système explicitement : + +``` +cargo rustc -- -C link-args="/ENTRY:_start /SUBSYSTEM:console" +``` + +Ici nous utilisons le sous-système `CONSOLE`, mais le sous-système `WINDOWS` pourrait fonctionner aussi. Au lieu de donner `-C link-arg` plusieurs fois, nous utilisons `-C link-args` qui utilise des arguments séparés par des espaces. + +Avec cette commande, notre exécutable devrait compiler avec succès sous Windows. + +#### macOS + +Sur macOS, voici l'erreur de linker qui se produit (raccourcie) : + +``` +error: linking with `cc` failed: exit code: 1 + | + = note: "cc" […] + = note: ld: entry point (_main) undefined. for architecture x86_64 + clang: error: linker command failed with exit code 1 […] +``` + +Cette erreur nous indique que le linker ne peut pas trouver une fonction de point d'entrée avec le nom par défaut `main` (pour une quelconque raison, toutes les fonctions sur macOS sont précédées de `_`). Pour configurer le point d'entrée sur notre fonction `_start`, nous donnons l'argument `-e` au linker : + +``` +cargo rustc -- -C link-args="-e __start" +``` + +L'argument `-e` spécifie le nom de la fonction de point d'entrée. Comme toutes les fonctions ont un préfixe supplémentaire `_` sur macOS, nous devons configurer le point d'entrée comme étant `__start` au lieu de `_start`. + +Maintenant l'erreur de linker suivante se produit : + +``` +error: linking with `cc` failed: exit code: 1 + | + = note: "cc" […] + = note: ld: dynamic main executables must link with libSystem.dylib + for architecture x86_64 + clang: error: linker command failed with exit code 1 […] +``` + +macOS [ne supporte pas officiellement les bibliothèques liées de façon statique] et necéessite que les programmes lient la bibliothèque `libSystem` par défaut. Pour réécrire ceci et lier une bibliothèque statique, nous donnons l'argument `-static` au linker : + +[ne supporte pas officiellement les bibliothèques liées de façon statique]: https://developer.apple.com/library/archive/qa/qa1118/_index.html + +``` +cargo rustc -- -C link-args="-e __start -static" +``` + +Cela ne suffit toujours pas, une troisième erreur de linker se produit : + +``` +error: linking with `cc` failed: exit code: 1 + | + = note: "cc" […] + = note: ld: library not found for -lcrt0.o + clang: error: linker command failed with exit code 1 […] +``` + +Cette erreur se produit car les programmes sous macOS lient `crt0` (“C runtime zero”) par défaut. Ceci est similaire à l'erreur que nous avions eu sous Linux et peut aussi être résolue en ajoutant l'argument `-nostartfiles` au linker : + +``` +cargo rustc -- -C link-args="-e __start -static -nostartfiles" +``` + +Maintenant notre programme compile avec succès sous macOS. + +#### Unifier les Commandes de Compilation + +À cet instant nous avons différentes commandes de compilation en fonction de la plateforme hôte, ce qui n'est pas idéal. Pour éviter cela, nous pouvons créer un ficher nommé `.cargo/config.toml` qui contient les arguments spécifiques aux plateformes : + +```toml +# dans .cargo/config.toml + +[target.'cfg(target_os = "linux")'] +rustflags = ["-C", "link-arg=-nostartfiles"] + +[target.'cfg(target_os = "windows")'] +rustflags = ["-C", "link-args=/ENTRY:_start /SUBSYSTEM:console"] + +[target.'cfg(target_os = "macos")'] +rustflags = ["-C", "link-args=-e __start -static -nostartfiles"] +``` + +La clé `rustflags` contient des arguments qui sont automatiquement ajoutés à chaque appel de `rustc`. Pour plus d'informations sur le fichier `.cargo/config.toml`, allez voir la [documentation officielle](https://doc.rust-lang.org/cargo/reference/config.html) + +Maintenant notre programme devrait être compilable sur les trois plateformes avec un simple `cargo build`. + +#### Devriez-vous Faire Ça ? + +Bien qu'il soit possible de compiler un exécutable autoporté pour Linux, Windows et macOS, ce n'est probablement pas une bonne idée. La raison est que notre exécutable s'attend toujours à trouver certaines choses, par exemple une pile initialisée lorsque la fonction `_start` est appelée. Sans l'environnement d'exécution C, certains de ces conditions peuvent ne pas être remplies, ce qui pourrait faire planter notre programme, avec par exemple une erreur de segmentation. + +Si vous voulez créer un exécutable minimal qui tourne sur un système d'exploitation existant, include `libc` et mettre l'attribut `#[start]` come décrit [ici](https://doc.rust-lang.org/1.16.0/book/no-stdlib.html) semble être une meilleure idée. + +
    + +## Résumé + +Un exécutable Rust autoporté minimal ressemble à ceci : + +`src/main.rs`: + +```rust +#![no_std] // ne pas lier la bibliothèque standard Rust +#![no_main] // désactiver tous les points d'entrée au niveau de Rust + +use core::panic::PanicInfo; + +#[no_mangle] // ne pas décorer le nom de cette fonction +pub extern "C" fn _start() -> ! { + // cette fonction est le point d'entrée, comme le linker cherche une fonction + // nomée `_start` par défaut + loop {} +} + +/// Cette fonction est appelée à chaque panic. +#[panic_handler] +fn panic(_info: &PanicInfo) -> ! { + loop {} +} +``` + +`Cargo.toml`: + +```toml +[package] +name = "crate_name" +version = "0.1.0" +authors = ["Author Name "] + +# le profile utilisé pour `cargo build` +[profile.dev] +panic = "abort" # désactive le déroulement de la pile lors d'un panic + +# le profile utilisé pour `cargo build --release` +[profile.release] +panic = "abort" # désactive le déroulement de la pile lors d'un panic +``` + +Pour compiler cet exécutable, nous devons compiler pour une cible bare metal telle que `thumbv7em-none-eabihf` : + +``` +cargo build --target thumbv7em-none-eabihf +``` + +Sinon, nous pouvons aussi compiler pour le système hôte en donnant des arguments supplémentaires pour le linker : + +```bash +# Linux +cargo rustc -- -C link-arg=-nostartfiles +# Windows +cargo rustc -- -C link-args="/ENTRY:_start /SUBSYSTEM:console" +# macOS +cargo rustc -- -C link-args="-e __start -static -nostartfiles" +``` + +À noter que ceci est juste un exemple minimal d'un exécutable Rust autoporté. Cet exécutable s'attend à de nombreuses choses, comme par exemple le fait qu'une pile soit initialisée lorsque la fonction `_start` est appelée. **Donc pour une réelle utilisation d'un tel exécutable, davantages d'étapes sont requises.** + +## Et ensuite ? + +Le [poste suivant][next post] explique les étapes nécessaires pour transformer notre exécutable autoporté minimal en noyau de système d'opération. Cela comprend la création d'une cible personnalisée, l'intégration de notre exécutable avec un chargeur d'amorçage et l'apprentissage de comment imprimer quelque chose sur l'écran. + +[next post]: @/edition-2/posts/02-minimal-rust-kernel/index.md diff --git a/blog/content/edition-2/posts/01-freestanding-rust-binary/index.ja.md b/blog/content/edition-2/posts/01-freestanding-rust-binary/index.ja.md index c74a6268..f25e4182 100644 --- a/blog/content/edition-2/posts/01-freestanding-rust-binary/index.ja.md +++ b/blog/content/edition-2/posts/01-freestanding-rust-binary/index.ja.md @@ -22,6 +22,7 @@ translators = ["JohnTitor"] [GitHub]: https://github.com/phil-opp/blog_os [comments]: #comments + [post branch]: https://github.com/phil-opp/blog_os/tree/post-01 @@ -45,7 +46,7 @@ OS カーネルを書くためには、いかなる OS の機能にも依存し Rust で OS カーネルを書くには、基盤となる OS なしで動く実行環境をつくる必要があります。そのような実行環境はフリースタンディング環境やベアメタルのように呼ばれます。 -この記事では、フリースタンディングな Rust のバイナリをつくるために必要なステップを紹介し、なぜそれが必要なのかを説明します。もし最小限の説明だけを読みたいのであれば **[概要](#概要)** まで飛ばしてください。 +この記事では、フリースタンディングな Rust のバイナリをつくるために必要なステップを紹介し、なぜそれが必要なのかを説明します。もし最小限の説明だけを読みたいのであれば **[概要](#summary)** まで飛ばしてください。 ## 標準ライブラリの無効化 @@ -160,7 +161,7 @@ language item はコンパイラによって内部的に必要とされる特別 [`eh_personality` language item]: https://github.com/rust-lang/rust/blob/edb368491551a77d77a48446d4ee88b35490c565/src/libpanic_unwind/gcc.rs#L11-L45 [stack unwinding]: https://www.bogotobogo.com/cplusplus/stackunwinding.php [libunwind]: https://www.nongnu.org/libunwind/ -[structured exception handling]: https://docs.microsoft.com/de-de/windows/win32/debug/structured-exception-handling +[structured exception handling]: https://docs.microsoft.com/en-us/windows/win32/debug/structured-exception-handling ### アンワインドの無効化 @@ -462,7 +463,7 @@ rustflags = ["-C", "link-args=-e __start -static -nostartfiles"] -## 概要 +## 概要 {#summary} 最小限の独立した Rust バイナリは次のようになります: @@ -528,4 +529,4 @@ cargo rustc -- -C link-args="-e __start -static -nostartfiles" [次の記事][next post]では、この独立したバイナリを最小限の OS カーネルにするために必要なステップを説明しています。カスタムターゲットの作成、実行可能ファイルとブートローダの組み合わせ、画面に何か文字を表示する方法について説明しています。 -[next post]: @/edition-2/posts/02-minimal-rust-kernel/index.md +[next post]: @/edition-2/posts/02-minimal-rust-kernel/index.ja.md diff --git a/blog/content/edition-2/posts/01-freestanding-rust-binary/index.md b/blog/content/edition-2/posts/01-freestanding-rust-binary/index.md index 9464ac10..6ba6829c 100644 --- a/blog/content/edition-2/posts/01-freestanding-rust-binary/index.md +++ b/blog/content/edition-2/posts/01-freestanding-rust-binary/index.md @@ -18,6 +18,7 @@ This blog is openly developed on [GitHub]. If you have any problems or questions [GitHub]: https://github.com/phil-opp/blog_os [at the bottom]: #comments + [post branch]: https://github.com/phil-opp/blog_os/tree/post-01 @@ -154,7 +155,7 @@ The [`eh_personality` language item] marks a function that is used for implement [`eh_personality` language item]: https://github.com/rust-lang/rust/blob/edb368491551a77d77a48446d4ee88b35490c565/src/libpanic_unwind/gcc.rs#L11-L45 [stack unwinding]: https://www.bogotobogo.com/cplusplus/stackunwinding.php [libunwind]: https://www.nongnu.org/libunwind/ -[structured exception handling]: https://docs.microsoft.com/de-de/windows/win32/debug/structured-exception-handling +[structured exception handling]: https://docs.microsoft.com/en-us/windows/win32/debug/structured-exception-handling ### Disabling Unwinding diff --git a/blog/content/edition-2/posts/01-freestanding-rust-binary/index.ru.md b/blog/content/edition-2/posts/01-freestanding-rust-binary/index.ru.md new file mode 100644 index 00000000..d6192f0b --- /dev/null +++ b/blog/content/edition-2/posts/01-freestanding-rust-binary/index.ru.md @@ -0,0 +1,523 @@ ++++ +title = "Независимый бинарный файл на Rust" +weight = 1 +path = "ru/freestanding-rust-binary" +date = 2018-02-10 + +[extra] +chapter = "С нуля" +translators = ["MrZloHex"] ++++ + +Первый шаг в создании собственного ядра операционной системы — это создание исполняемого файла на Rust, который не будет подключать стандартную библиотеку. Именно это дает возможность запускать Rust код на [голом железе][bare metal] без слоя операционной системы. + +[bare metal]: https://en.wikipedia.org/wiki/Bare_machine + + + +Этот блог открыто разрабатывается на [GitHub]. Если у вас возникли какие-либо проблемы или вопросы, пожалуйста, создайте _issue_. Также вы можете оставлять комментарии [в конце страницы][at the bottom]. Полный исходный код для этого поста вы можете найти в репозитории в ветке [`post-01`][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-01 + + + +## Введение +Для того, чтобы написать ядро операционной системы, нужен код, который не зависит от операционной системы и ее возможностей. Это означает, что нельзя использовать потоки, файлы, [кучу][heap], сети, случайные числа, стандартный вывод или другие возможности, которые зависят от ОС или определённого железа. + +[heap]: https://en.wikipedia.org/wiki/Heap_(data_structure) + +Это значит, что нельзя использовать большую часть [стандартной библиотеки Rust][Rust Standard library], но остается множество других возможностей Rust, которые _можно использовать_. Например, [итераторы][iterators], [замыкания][closures], [сопоставление с образцом][pattern matching], [`Option`][option] и [`Result`][result], [форматирование строк][string formatting] и, конечно же, [систему владения][ownership system]. Эти функции дают возможность для написания ядра в очень выразительном и высоко-уровневом стиле, не беспокоясь о [неопределенном поведении][undefined behavior] или [сохранности памяти][memory safety]. + +[option]: https://doc.rust-lang.org/core/option/ +[result]:https://doc.rust-lang.org/core/result/ +[Rust standard library]: https://doc.rust-lang.org/std/ +[iterators]: https://doc.rust-lang.org/book/ch13-02-iterators.html +[closures]: https://doc.rust-lang.org/book/ch13-01-closures.html +[pattern matching]: https://doc.rust-lang.org/book/ch06-00-enums.html +[string formatting]: https://doc.rust-lang.org/core/macro.write.html +[ownership system]: https://doc.rust-lang.org/book/ch04-00-understanding-ownership.html +[undefined behavior]: https://www.nayuki.io/page/undefined-behavior-in-c-and-cplusplus-programs +[memory safety]: https://tonyarcieri.com/it-s-time-for-a-memory-safety-intervention + +Чтобы создать ядро ОС на Rust, нужно создать исполняемый файл, который мог бы запускаться без ОС. + +Этот пост описывает необходимые шаги для создания независимого исполняемого файла на Rust и объясняет, почему эти шаги нужны. Если вам интересен только минимальный пример, можете сразу перейти к __[итогам](#summary)__. + +## Отключение стандартной библиотеки +По умолчанию, все Rust-крейты подключают [стандартную библиотеку][standard library], которая зависит от возможностей операционной системы, таких как потоки, файлы, сети. Она также зависит от стандартной библиотки C `libc`, которая очень тесно взаимодействует с возможностями ОС. Так как мы хотим написать операционную систему, мы не можем использовать библиотеки, которые зависят от операционной системы. Поэтому необходимо отключить автоматические подключение стандартной библиотеки через [атрибут `no_std`][attribute]. + +[standard library]: https://doc.rust-lang.org/std/ +[attribute]: https://doc.rust-lang.org/1.30.0/book/first-edition/using-rust-without-the-standard-library.html + +Мы начнем с создания нового проекта cargo. Самый простой способ сделать это — через командную строку: + +``` +cargo new blog_os --bin -- edition 2018 +``` + +Я назвал этот проект `blog_os`, но вы можете назвать как вам угодно. Флаг `--bin` указывает на то, что мы хотим создать исполняемый файл (а не библиотеку), а флаг `--edition 2018` указывает, что мы хотим использовать [редакцию Rust 2018][edition] для нашего крейта. После выполнения команды cargo создаст каталог со следующей структурой: + +[edition]: https://doc.rust-lang.org/nightly/edition-guide/rust-2018/index.html + +``` +blog_os +├── Cargo.toml +└── src + └── main.rs +``` + +`Cargo.toml` содержит данные и конфигурацию крейта, такие как _название, автор, [семантическую версию][semantic version]_ и _зависимости_ от других крейтов. Файл `src/main.rs` содержит корневой модуль нашего крейта и функцию `main`. Можно скомпилировать крейт с помощью `cargo build` и запустить скомпилированную программу `blog_os` в поддиректории `target/debug`. + +[semantic version]: https://semver.org/ + +### Атрибут `no_std` + +В данный момент наш крейт неявно подключает стандартную библиотеку. Это можно исправить путем добавления [атрибута `no_std`][attribute]: + +```rust +// main.rs + +#![no_std] + +fn main() { + println!("Hello, world!"); +} +``` + +Если сейчас попробовать скомпилировать программу (с помоцью команды `cargo build`), то появится следующая ошибка: + +``` +error: cannot find macro `println!` in this scope + --> src/main.rs:4:5 + | +4 | println!("Hello, world!"); + | ^^^^^^^ +``` + +Эта ошибка объясняется тем, что [макрос `println`][macro] — часть стандартной библиотеки, которая была отключена. Поэтому у нас больше нет возможность выводить что-либо на экран. Это логично, так как `println` печатает через [стандартный вывод][standard output], который, в свою очередь, является специальным файловым дескриптором, предоставляемым операционной системой. + +[macro]: https://doc.rust-lang.org/std/macro.println.html +[standard output]: https://en.wikipedia.org/wiki/Standard_streams#Standard_output_.28stdout.29 + +Давайте уберем макрос `println` и попробуем скомпилировать еще раз: + +```rust +// main.rs + +#![no_std] + +fn main() {} +``` + +``` +> cargo build +error: `#[panic_handler]` function required, but not found +error: language item required, but not found: `eh_personality` +``` + +Сейчас компилятор не может найти функцию `#[panic_handler]` и «элемент языка». + +## Реализация _паники_ + +Атрибут `pаnic_handler` определяет функцию, которая должна вызываться, когда происходит [паника (panic)][panic]. Стандартная библиотека предоставляет собственную функцию обработчика паники, но после отключения стандартной библиотеки мы должны написать собственный обработчик: + +[panic]: https://doc.rust-lang.org/stable/book/ch09-01-unrecoverable-errors-with-panic.html + +```rust +// in main.rs + +use core::panic::PanicInfo; + +/// This function is called on panic. +#[panic_handler] +fn panic(_info: &PanicInfo) -> ! { + loop {} +} +``` + +Параметр [`PanicInfo`][PanicInfo] содержит название файла и строку, где произошла паника, и дополнительное сообщение с пояснением. Эта функция никогда не должна возвратиться, и такая функция называется [расходящейся][diverging functions] и она возращает [пустой тип]["never" type] `!`. Пока что мы ничего не можем сделать в этой функции, поэтому мы просто войдем в бесконечный цикл. + +[PanicInfo]: https://doc.rust-lang.org/nightly/core/panic/struct.PanicInfo.html +[diverging functions]: https://doc.rust-lang.org/1.30.0/book/first-edition/functions.html#diverging-functions +["never" type]: https://doc.rust-lang.org/nightly/std/primitive.never.html + +## Элемент языка `eh_personality` + +Элементы языка — это специальные функции и типы, которые необходимы компилятору. Например, трейт [`Copy`] указывает компилятору, у каких типов есть [_семантика копирования_][`Copy`]. Если мы посмотрим на [реализацию][copy code] этого трейта, то увидим специальный атрибут `#[lang = "copy"]`, который говорит, что этот трейт является элементом языка. + +[`Copy`]: https://doc.rust-lang.org/nightly/core/marker/trait.Copy.html +[copy code]: https://github.com/rust-lang/rust/blob/485397e49a02a3b7ff77c17e4a3f16c653925cb3/src/libcore/marker.rs#L296-L299 + +Несмотря на то, что можно предоставить свою реализацию элементов языка, это следует делать только в крайних случаях. Причина в том, что элементы языка являются крайне нестабильными деталями реализации, и компилятор даже не проверяет в них согласованность типов (поэтому он даже не проверяет, имеет ли функция правильные типы аргументов). К счастью, существует более стабильный способ исправить вышеупомянутую ошибку. + +Элемент языка [`eh_personality`][language item] указывает на функцию, которая используется для реализации [раскрутки стека][stack unwinding]. По умолчанию, Rust использует раскрутку для запуска деструктуров для всех _живых_ переменных на стеке в случае [паники][panic]. Это гарантирует, что вся использованная память будет освобождена, и позволяет родительскому потоку перехватить панику и продолжить выполнение. Раскрутка — очень сложный процесс и требует некоторых специльных библиотек ОС (например, [libunwind] для Linux или [structured exception handling] для Windows), так что мы не должны использовать её для нашей операционной системы. + +[language item]: https://github.com/rust-lang/rust/blob/edb368491551a77d77a48446d4ee88b35490c565/src/libpanic_unwind/gcc.rs#L11-L45 +[stack unwinding]: https://www.bogotobogo.com/cplusplus/stackunwinding.php +[libunwind]: https://www.nongnu.org/libunwind/ +[structured exception handling]: https://docs.microsoft.com/de-de/windows/win32/debug/structured-exception-handling + +### Отключение раскрутки + +Существуют и другие случаи использования, для которых раскрутка нежелательна, поэтому Rust предоставляет опцию [прерывания выполнения при панике][abort on panic]. Это отключает генерацию информации о символах раскрутки и, таким образом, значительно уменьшает размер бинарного файла. Есть несколько мест, где мы можем отключить раскрутку. Самый простой способ — добавить следующие строки в наш `Cargo.toml`: + +```toml +[profile.dev] +panic = "abort" + +[profile.release] +panic = "abort" +``` + +Это устанавливает стратегию паники на `abort` (прерывание) как для профиля `dev` (используемого для `cargo build`), так и для профиля `release` (используемого для `cargo build --release`). Теперь элемент языка `eh_personality` больше не должен требоваться. + +[abort on panic]: https://github.com/rust-lang/rust/pull/32900 + +Теперь мы исправили обе вышеуказанные ошибки. Однако, если мы сейчас попытаемся скомпилировать программу, возникнет другая ошибка: + +``` +> cargo build +error: requires `start` lang_item +``` + +В нашей программе отсутствует элемент языка `start`, который определяет начальную точку входа программы. + +## Аттрибут `start` + +Можно подумать, что функция `main` — это первая функция, вызываемая при запуске программы. Однако в большинстве языков есть [среда выполнения][runtime system], которая отвечает за такие вещи, как сборка мусора (например, в Java) или программные потоки (например, goroutines в Go). Эта система выполнения должна быть вызвана до `main`, поскольку ей необходимо инициализировать себя. + +[runtime system]: https://en.wikipedia.org/wiki/Runtime_system + +В типичном исполнимом файле Rust, который использует стандартную библиотеку, выполнение начинается в runtime-библиотеке C под названием `crt0` ("C runtime zero"), которая создает окружение для C-приложения. Это включает создание стека и размещение аргументов в нужных регистрах. Затем C runtime вызывает [точку входа для Rust-приложения][rt::lang_start], которая обозначается элементом языка `start`. Rust имеет очень маленький runtime, который заботится о некоторых мелочах, таких как установка защиты от переполнения стека или вывод сообщения при панике. Затем рантайм вызывает функцию `main`. + +[rt::lang_start]: https://github.com/rust-lang/rust/blob/bb4d1491466d8239a7a5fd68bd605e3276e97afb/src/libstd/rt.rs#L32-L73 + +Наш независимый исполняемый файл не имеет доступа к runtime Rust и `crt0`, поэтому нам нужно определить собственную точку входа. Реализация языкового элемента `start` не поможет, поскольку он все равно потребует `crt0`. Вместо этого нам нужно напрямую переопределить точку входа `crt0`. + +### Переопределение точки входа + +Чтобы сообщить компилятору Rust, что мы не хотим использовать стандартную цепочку точек входа, мы добавляем атрибут `#![no_main]`. + +```rust +#![no_std] +#![no_main] + +use core::panic::PanicInfo; + +/// This function is called on panic. +#[panic_handler] +fn panic(_info: &PanicInfo) -> ! { + loop {} +} +``` + +Можно заметить, что мы удалили функцию `main`. Причина в том, что `main` не имеет смысла без стандартного runtime, которая ее вызывает. Вместо этого мы переопределим точку входа операционной системы с помощью нашей собственной функции `_start`: + +```rust +#[no_mangle] +pub extern "C" fn _start() -> ! { + loop {} +} +``` + +Используя атрибут `#[no_mangle]`, мы отключаем [искажение имен][name mangling], чтобы гарантировать, что компилятор Rust сгенерирует функцию с именем `_start`. Без этого атрибута компилятор генерировал бы какой-нибудь загадочный символ `_ZN3blog_os4_start7hb173fedf945531caE`, чтобы дать каждой функции уникальное имя. Атрибут необходим, потому что на следующем этапе нам нужно сообщить имя функции точки входа компоновщику. + +Мы также должны пометить функцию как `extern "C"`, чтобы указать компилятору, что он должен использовать [соглашение о вызове C][C calling convention] для этой функции (вместо неопределенного соглашения о вызове Rust). Причина именования функции `_start` в том, что это имя точки входа по умолчанию для большинства систем. + +[name mangling]: https://en.wikipedia.org/wiki/Name_mangling +[C calling convention]: https://en.wikipedia.org/wiki/Calling_convention + +Возвращаемый `!` означает, что функция является расходящейся, т.е. не имеет права возвращаться. Это необходимо, поскольку точка входа не вызывается никакой функцией, а вызывается непосредственно операционной системой или загрузчиком. Поэтому вместо возврата точка входа должна, например, вызвать [системный вызов `exit`][`exit` system call] операционной системы. В нашем случае разумным действием может быть выключение машины, поскольку ничего не останется делать, если независимый исполнимый файл завершит исполнение. Пока что мы выполняем это требование путем бесконечного цикла. + +[`exit` system call]: https://en.wikipedia.org/wiki/Exit_(system_call) + +Если мы выполним `cargo build` сейчас, мы получим ошибку компоновщика (_linker_ error). + +## Ошибки компоновщика + +Компоновщик — это программа, которая объединяет сгенерированный код в исполняемый файл. Поскольку формат исполняемого файла отличается в Linux, Windows и macOS, в каждой системе есть свой компоновщик, и каждый покажет свою ошибку. Основная причина ошибок одна и та же: конфигурация компоновщика по умолчанию предполагает, что наша программа зависит от C runtime, а это не так. + +Чтобы устранить ошибки, нам нужно сообщить компоновщику, что он не должен включать C runtime. Мы можем сделать это, передав компоновщику определенный набор аргументов или выполнив компиляцию для голого железа. + +### Компиляция для голого железа + +По умолчанию Rust пытается создать исполняемый файл, который может быть запущен в окружении вашей текущей системы. Например, если вы используете Windows на `x86_64`, Rust пытается создать исполняемый файл Windows `.exe`, который использует инструкции `x86_64`. Это окружение называется вашей "хост-системой". + +Для описания различных окружений Rust использует строку [_target triple_]. Вы можете узнать тройку вашей хост-системы, выполнив команду `rustc --version --verbose`: + +[_target triple_]: https://clang.llvm.org/docs/CrossCompilation.html#target-triple + +``` +rustc 1.35.0-nightly (474e7a648 2019-04-07) +binary: rustc +commit-hash: 474e7a6486758ea6fc761893b1a49cd9076fb0ab +commit-date: 2019-04-07 +host: x86_64-unknown-linux-gnu +release: 1.35.0-nightly +LLVM version: 8.0 +``` + +Приведенный выше результат получен от системы `x86_64` Linux. Мы видим, что тройка `host` — это `x86_64-unknown-linux-gnu`, которая включает архитектуру процессора (`x86_64`), производителя (`unknown`), операционную систему (`linux`) и [ABI] (`gnu`). + +[ABI]: https://en.wikipedia.org/wiki/Application_binary_interface + +Компилируя для тройки нашего хоста, компилятор Rust и компоновщик предполагают наличие базовой операционной системы, такой как Linux или Windows, которая по умолчанию использует C runtime, что вызывает ошибки компоновщика. Поэтому, чтобы избежать ошибок компоновщика, мы можем настроить компиляцию для другого окружения без базовой операционной системы. + +Примером такого "голого" окружения является тройка `thumbv7em-none-eabihf`, которая описывает [ARM] архитектуру. Детали не важны, важно лишь то, что тройка не имеет базовой операционной системы, на что указывает `none` в тройке. Чтобы иметь возможность компилировать для этой системы, нам нужно добавить ее в rustup: + +[ARM]: https://en.wikipedia.org/wiki/ARM_architecture + +``` +rustup target add thumbv7em-none-eabihf +``` + +Это загружает копию стандартной библиотеки (и `core`) для системы. Теперь мы можем собрать наш независимый исполняемый файл для этой системы: + +``` +cargo build --target thumbv7em-none-eabihf +``` + +Передавая аргумент `--target`, мы [кросс-компилируем][cross compile] наш исполняемый файл для голого железа. Поскольку система, под которую мы компилируем, не имеет операционной системы, компоновщик не пытается компоновать C runtime, и наша компиляция проходит успешно без каких-либо ошибок компоновщика. + +[cross compile]: https://en.wikipedia.org/wiki/Cross_compiler + +Именно этот подход мы будем использовать для сборки ядра нашей ОС. Вместо `thumbv7em-none-eabihf` мы будем использовать [custom target], который описывает окружение для архитектуры `x86_64`. Подробности будут описаны в следующем посте. + +[custom target]: https://doc.rust-lang.org/rustc/targets/custom.html + +### Аргументы компоновщика + +Вместо компиляции под голое железо, ошибки компоновщика можно исправить, передав ему определенный набор аргументов. Мы не будем использовать этот подход для нашего ядра, поэтому данный раздел является необязательным и приводится только для полноты картины. Щелкните на _"Аргументы компоновщика"_ ниже, чтобы показать необязательное содержание. + +
    + +Аргументы компоновщика + +В этом разделе мы рассмотрим ошибки компоновщика, возникающие в Linux, Windows и macOS, и объясним, как их решить, передав компоновщику дополнительные аргументы. Обратите внимание, что формат исполняемого файла и компоновщик отличаются в разных операционных системах, поэтому для каждой системы требуется свой набор аргументов. + +#### Linux + +На Linux возникает следующая ошибка компоновщика (сокращенно): + +``` +error: linking with `cc` failed: exit code: 1 + | + = note: "cc" […] + = note: /usr/lib/gcc/../x86_64-linux-gnu/Scrt1.o: In function `_start': + (.text+0x12): undefined reference to `__libc_csu_fini' + /usr/lib/gcc/../x86_64-linux-gnu/Scrt1.o: In function `_start': + (.text+0x19): undefined reference to `__libc_csu_init' + /usr/lib/gcc/../x86_64-linux-gnu/Scrt1.o: In function `_start': + (.text+0x25): undefined reference to `__libc_start_main' + collect2: error: ld returned 1 exit status +``` + +Проблема заключается в том, что компоновщик по умолчанию включает процедуру запуска C runtime, которая также называется `_start`. Она требует некоторых символов стандартной библиотеки C `libc`, которые мы не включаем из-за атрибута `no_std`, поэтому компоновщик не может подключить эти библиотеки, поэтому появляются ошибки. Чтобы решить эту проблему, мы можем сказать компоновщику, что он не должен компоновать процедуру запуска C, передав флаг `-nostartfiles`. + +Одним из способов передачи атрибутов компоновщика через cargo является команда `cargo rustc`. Команда ведет себя точно так же, как `cargo build`, но позволяет передавать опции `rustc`, базовому компилятору Rust. У `rustc` есть флаг `-C link-arg`, который передает аргумент компоновщику. В совокупности наша новая команда сборки выглядит следующим образом: + +``` +cargo rustc -- -C link-arg=-nostartfiles +``` + +Теперь наш крейт собирается как независимый исполняемый файл в Linux! + +Нам не нужно было явно указывать имя нашей функции точки входа, поскольку компоновщик по умолчанию ищет функцию с именем `_start`. + +#### Windows + +В Windows возникает другая ошибка компоновщика (сокращенно): + +``` +error: linking with `link.exe` failed: exit code: 1561 + | + = note: "C:\\Program Files (x86)\\…\\link.exe" […] + = note: LINK : fatal error LNK1561: entry point must be defined +``` + +Ошибка "точка входа должна быть определена" (_"entry point must be defined"_) означает, что компоновщик не может найти точку входа. В Windows имя точки входа по умолчанию [зависит от используемой подсистемы][windows-subsystems]. Для подсистемы `CONSOLE` компоновщик ищет функцию с именем `mainCRTStartup`, а для подсистемы `WINDOWS` - функцию с именем `WinMainCRTStartup`. Чтобы переопределить названия точки входа на `_start`, мы можем передать компоновщику аргумент `/ENTRY`: + +[windows-subsystems]: https://docs.microsoft.com/en-us/cpp/build/reference/entry-entry-point-symbol + +``` +cargo rustc -- -C link-arg=/ENTRY:_start +``` + +Из разного формата аргументов мы ясно видим, что компоновщик Windows - это совершенно другая программа, чем компоновщик Linux. + +Теперь возникает другая ошибка компоновщика: + +``` +error: linking with `link.exe` failed: exit code: 1221 + | + = note: "C:\\Program Files (x86)\\…\\link.exe" […] + = note: LINK : fatal error LNK1221: a subsystem can't be inferred and must be + defined +``` + +Эта ошибка возникает из-за того, что исполняемые файлы Windows могут использовать различные [подсистемы][windows-subsystems]. Для обычных программ они определяются в зависимости от имени точки входа: если точка входа называется `main`, то используется подсистема `CONSOLE`, а если точка входа называется `WinMain`, то используется подсистема `WINDOWS`. Поскольку наша функция `_start` имеет другое имя, нам нужно явно указать подсистему: + +``` +cargo rustc -- -C link-args="/ENTRY:_start /SUBSYSTEM:console" +``` + +Здесь мы используем подсистему `CONSOLE`, но подойдет и подсистема `WINDOWS`. Вместо того, чтобы передавать `-C link-arg` несколько раз, мы используем `-C link-args`, который принимает список аргументов, разделенных пробелами. + +С помощью этой команды наш исполняемый файл должен успешно скомпилироваться под Windows. + +#### macOS + +На macOS возникает следующая ошибка компоновщика (сокращенно): + +``` +error: linking with `cc` failed: exit code: 1 + | + = note: "cc" […] + = note: ld: entry point (_main) undefined. for architecture x86_64 + clang: error: linker command failed with exit code 1 […] +``` + +Это сообщение об ошибке говорит нам, что компоновщик не может найти функцию точки входа с именем по умолчанию `main` (по какой-то причине в macOS все функции имеют префикс `_`). Чтобы установить точку входа в нашу функцию `_start`, мы передаем аргумент компоновщика `-e`: + +``` +cargo rustc -- -C link-args="-e __start" +``` + +Флаг `-e` задает имя функции точки входа. Поскольку в macOS все функции имеют дополнительный префикс `_`, нам нужно установить точку входа на `__start` вместо `_start`. + +Теперь возникает следующая ошибка компоновщика: + +``` +error: linking with `cc` failed: exit code: 1 + | + = note: "cc" […] + = note: ld: dynamic main executables must link with libSystem.dylib + for architecture x86_64 + clang: error: linker command failed with exit code 1 […] +``` + +macOS [официально не поддерживает статически скомпонованные исполняемые файлы][static binary] и по умолчанию требует от программ компоновки библиотеки `libSystem`. Чтобы переопределить это поведение и скомпоновать статический исполняемый файл, передадим компоновщику флаг `-static`: + +[static binary]: https://developer.apple.com/library/archive/qa/qa1118/_index.html + +``` +cargo rustc -- -C link-args="-e __start -static" +``` + +Этого все равно недостаточно, так как возникает третья ошибка компоновщика: + +``` +error: linking with `cc` failed: exit code: 1 + | + = note: "cc" […] + = note: ld: library not found for -lcrt0.o + clang: error: linker command failed with exit code 1 […] +``` + +Эта ошибка возникает из-за того, что программы на macOS по умолчанию ссылаются на `crt0` ("C runtime zero"). Она похожа на ошибку под Linux и тоже может быть решена добавлением аргумента компоновщика `-nostartfiles`: + +``` +cargo rustc -- -C link-args="-e __start -static -nostartfiles" +``` + +Теперь наша программа должна успешно скомпилироваться на macOS. + +#### Объединение команд сборки + +Сейчас у нас разные команды сборки в зависимости от платформы хоста, что не идеально. Чтобы избежать этого, мы можем создать файл с именем `.cargo/config.toml`, который будет содержать аргументы для конкретной платформы: + +```toml +# in .cargo/config.toml + +[target.'cfg(target_os = "linux")'] +rustflags = ["-C", "link-arg=-nostartfiles"] + +[target.'cfg(target_os = "windows")'] +rustflags = ["-C", "link-args=/ENTRY:_start /SUBSYSTEM:console"] + +[target.'cfg(target_os = "macos")'] +rustflags = ["-C", "link-args=-e __start -static -nostartfiles"] +``` + +Ключ `rustflags` содержит аргументы, которые автоматически добавляются к каждому вызову `rustc`. Более подробную информацию о файле `.cargo/config.toml` можно найти в [официальной документации](https://doc.rust-lang.org/cargo/reference/config.html). + +Теперь наша программа должна собираться на всех трех платформах с помощью простой `cargo build`. + +#### Должны ли вы это делать? + +Хотя можно создать независимый исполняемый файл для Linux, Windows и macOS, это, вероятно, не очень хорошая идея. Причина в том, что наш исполняемый файл все еще ожидает различных вещей, например, инициализации стека при вызове функции `_start`. Без C runtime некоторые из этих требований могут быть не выполнены, что может привести к сбою нашей программы, например, из-за ошибки сегментации. + +Если вы хотите создать минимальный исполняемый файл, запускаемый поверх существующей операционной системы, то включение `libc` и установка атрибута `#[start]`, как описано [здесь] (https://doc.rust-lang.org/1.16.0/book/no-stdlib.html), вероятно, будет идеей получше. + +
    + +## Итоги {#summary} + +Минимальный независимый исполняемый бинарный файл Rust выглядит примерно так: + +`src/main.rs`: + +```rust +#![no_std] // don't link the Rust standard library +#![no_main] // disable all Rust-level entry points + +use core::panic::PanicInfo; + +#[no_mangle] // don't mangle the name of this function +pub extern "C" fn _start() -> ! { + // this function is the entry point, since the linker looks for a function + // named `_start` by default + loop {} +} + +/// This function is called on panic. +#[panic_handler] +fn panic(_info: &PanicInfo) -> ! { + loop {} +} +``` + +`Cargo.toml`: + +```toml +[package] +name = "crate_name" +version = "0.1.0" +authors = ["Author Name "] + +# the profile used for `cargo build` +[profile.dev] +panic = "abort" # disable stack unwinding on panic + +# the profile used for `cargo build --release` +[profile.release] +panic = "abort" # disable stack unwinding on panic +``` + +Чтобы собрать этот исполняемый файл, его надо скомпилировать для голого железа, например, `thumbv7em-none-eabihf`: + +``` +cargo build --target thumbv7em-none-eabihf +``` + +В качестве альтернативы, мы можем скомпилировать его для хост-системы, передав дополнительные аргументы компоновщика: + +```bash +# Linux +cargo rustc -- -C link-arg=-nostartfiles +# Windows +cargo rustc -- -C link-args="/ENTRY:_start /SUBSYSTEM:console" +# macOS +cargo rustc -- -C link-args="-e __start -static -nostartfiles" +``` + +Обратите внимание, что это лишь минимальный пример независимого бинарного файла Rust. Этот бинарник ожидает различных вещей, например, инициализацию стека при вызове функции `_start`. **Поэтому для любого реального использования такого бинарного файла потребуется совершить еще больше действий**. + +## Что дальше? + +В [следующем посте][next post] описаны шаги, необходимые для превращения нашего независимого бинарного файла в минимальное ядро операционной системы. Сюда входит создание custom target, объединение нашего исполняемого файла с загрузчиком и изучение, как вывести что-то на экран. + +[next post]: @/edition-2/posts/02-minimal-rust-kernel/index.ru.md diff --git a/blog/content/edition-2/posts/01-freestanding-rust-binary/index.zh-CN.md b/blog/content/edition-2/posts/01-freestanding-rust-binary/index.zh-CN.md index d8cf37bc..6be3b1df 100644 --- a/blog/content/edition-2/posts/01-freestanding-rust-binary/index.zh-CN.md +++ b/blog/content/edition-2/posts/01-freestanding-rust-binary/index.zh-CN.md @@ -21,6 +21,7 @@ translators = ["luojia65", "Rustin-Liu", "TheBegining"] [GitHub]: https://github.com/phil-opp/blog_os [at the bottom]: #comments + [post branch]: https://github.com/phil-opp/blog_os/tree/post-01 @@ -128,7 +129,7 @@ fn panic(_info: &PanicInfo) -> ! { 我们可以自己实现语言项,但这是下下策:目前来看,语言项是高度不稳定的语言细节实现,它们不会经过编译期类型检查(所以编译器甚至不确保它们的参数类型是否正确)。幸运的是,我们有更稳定的方式,来修复上面的语言项错误。 -`eh_personality` 语言项标记的函数,将被用于实现**栈展开**([stack unwinding](https://www.bogotobogo.com/cplusplus/stackunwinding.php))。在使用标准库的情况下,当 panic 发生时,Rust 将使用栈展开,来运行在栈上所有活跃的变量的**析构函数**(destructor)——这确保了所有使用的内存都被释放,允许调用程序的**父进程**(parent thread)捕获 panic,处理并继续运行。但是,栈展开是一个复杂的过程,如 Linux 的 [libunwind](https://www.nongnu.org/libunwind/) 或 Windows 的**结构化异常处理**([structured exception handling, SEH](https://docs.microsoft.com/de-de/windows/win32/debug/structured-exception-handling)),通常需要依赖于操作系统的库;所以我们不在自己编写的操作系统中使用它。 +`eh_personality` 语言项标记的函数,将被用于实现**栈展开**([stack unwinding](https://www.bogotobogo.com/cplusplus/stackunwinding.php))。在使用标准库的情况下,当 panic 发生时,Rust 将使用栈展开,来运行在栈上所有活跃的变量的**析构函数**(destructor)——这确保了所有使用的内存都被释放,允许调用程序的**父进程**(parent thread)捕获 panic,处理并继续运行。但是,栈展开是一个复杂的过程,如 Linux 的 [libunwind](https://www.nongnu.org/libunwind/) 或 Windows 的**结构化异常处理**([structured exception handling, SEH](https://docs.microsoft.com/en-us/windows/win32/debug/structured-exception-handling)),通常需要依赖于操作系统的库;所以我们不在自己编写的操作系统中使用它。 ### 禁用栈展开 diff --git a/blog/content/edition-2/posts/01-freestanding-rust-binary/index.zh-TW.md b/blog/content/edition-2/posts/01-freestanding-rust-binary/index.zh-TW.md index 868ca6a0..4494d478 100644 --- a/blog/content/edition-2/posts/01-freestanding-rust-binary/index.zh-TW.md +++ b/blog/content/edition-2/posts/01-freestanding-rust-binary/index.zh-TW.md @@ -21,6 +21,7 @@ translators = ["wusyong"] [GitHub]: https://github.com/phil-opp/blog_os [at the bottom]: #comments + [post branch]: https://github.com/phil-opp/blog_os/tree/post-01 @@ -43,7 +44,7 @@ translators = ["wusyong"] 為了在 Rust 中建立 OS 核心,我們需要建立一個無須底層作業系統即可運行的執行檔,這類的執行檔通常稱為「獨立式(freestanding)」或「裸機(bare-metal)」的執行檔。 -這篇文章描述了建立一個獨立的 Rust 執行檔的必要步驟,並解釋為什麼需要這些步驟。如果您只對簡單的範例感興趣,可以直接跳到 **[總結](#總結)**。 +這篇文章描述了建立一個獨立的 Rust 執行檔的必要步驟,並解釋為什麼需要這些步驟。如果您只對簡單的範例感興趣,可以直接跳到 **[總結](#summary)**。 ## 停用標準函式庫 @@ -154,7 +155,7 @@ Language item 是一些編譯器需求的特殊函式或類型。舉例來說, [stack unwinding]: https://www.bogotobogo.com/cplusplus/stackunwinding.php [libunwind]: https://www.nongnu.org/libunwind/ -[structured exception handling]: https://docs.microsoft.com/de-de/windows/win32/debug/structured-exception-handling +[structured exception handling]: https://docs.microsoft.com/en-us/windows/win32/debug/structured-exception-handling ### 停用回溯 @@ -450,7 +451,7 @@ rustflags = ["-C", "link-args=-e __start -static -nostartfiles"] -## 總結 +## 總結 {#summary} 一個最小的 Rust 獨立執行檔會看起來像這樣: @@ -516,4 +517,4 @@ cargo rustc -- -C link-args="-e __start -static -nostartfiles" [下一篇文章][next post] 將會講解如何將我們的獨立執行檔轉成最小的作業系統核心。這包含建立自訂目標、用啟動程式組合我們的執行檔,還有學習如何輸出一些東西到螢幕上。 -[next post]: @/edition-2/posts/02-minimal-rust-kernel/index.md \ No newline at end of file +[next post]: @/edition-2/posts/02-minimal-rust-kernel/index.md diff --git a/blog/content/edition-2/posts/02-minimal-rust-kernel/disable-red-zone/index.ru.md b/blog/content/edition-2/posts/02-minimal-rust-kernel/disable-red-zone/index.ru.md new file mode 100644 index 00000000..05fa97e1 --- /dev/null +++ b/blog/content/edition-2/posts/02-minimal-rust-kernel/disable-red-zone/index.ru.md @@ -0,0 +1,29 @@ ++++ +title = "Отключение красной зоны" +weight = 1 +path = "ru/red-zone" +template = "edition-2/extra.html" ++++ + +[Красная зона][red zone] — это оптимизация [System V ABI], которая позволяет функциям временно использовать 128 байт ниже своего стекового кадра без корректировки указателя стека: + +[red zone]: https://eli.thegreenplace.net/2011/09/06/stack-frame-layout-on-x86-64#the-red-zone +[System V ABI]: https://wiki.osdev.org/System_V_ABI + + + +![stack frame with red zone](red-zone.svg) + +На рисунке показан стековый фрейм функции с `n` локальных переменных. При входе в функцию указатель стека корректируется, чтобы освободить место в стеке для адреса возврата и локальных переменных. + +Красная зона определяется как 128 байт ниже скорректированного указателя стека. Функция может использовать эту зону для временных данных, которые не нужны при всех вызовах функции. Таким образом, в некоторых случаях (например, в небольших листовых функциях) можно обойтись без двух инструкций для корректировки указателя стека. + +Однако такая оптимизация приводит к огромным проблемам при работе с исключениями или аппаратными прерываниями. Предположим, что во время использования функцией красной зоны происходит исключение: + +![red zone overwritten by exception handler](red-zone-overwrite.svg) + +Процессор и обработчик исключений перезаписывают данные в красной зоне. Но эти данные все еще нужны прерванной функции. Поэтому функция не будет работать правильно, когда мы вернемся из обработчика исключений. Это может привести к странным ошибкам, на отладку которых [уйдут недели][take weeks to debug]. + +[take weeks to debug]: https://forum.osdev.org/viewtopic.php?t=21720 + +Чтобы избежать подобных ошибок при реализации обработки исключений в будущем, мы отключим красную зону с самого начала. Это достигается путем добавления строки `"disable-redzone": true` в наш целевой конфигурационный файл. diff --git a/blog/content/edition-2/posts/02-minimal-rust-kernel/disable-simd/index.ru.md b/blog/content/edition-2/posts/02-minimal-rust-kernel/disable-simd/index.ru.md new file mode 100644 index 00000000..8a1feaa1 --- /dev/null +++ b/blog/content/edition-2/posts/02-minimal-rust-kernel/disable-simd/index.ru.md @@ -0,0 +1,44 @@ ++++ +title = "Отключение SIMD" +weight = 2 +path = "ru/disable-simd" +template = "edition-2/extra.html" ++++ + +Инструкции [Single Instruction Multiple Data (SIMD)] способны выполнять операцию (например, сложение) одновременно над несколькими словами данных, что может значительно ускорить работу программ. Архитектура `x86_64` поддерживает различные стандарты SIMD: + +[Single Instruction Multiple Data (SIMD)]: https://en.wikipedia.org/wiki/SIMD + + + +- [MMX]: Набор инструкций _Multi Media Extension_ был представлен в 1997 году и определяет восемь 64-битных регистров, называемых `mm0` - `mm7`. Эти регистры являются псевдонимами регистров [x87 блока с плавающей запятой][x87 floating point unit]. +- [SSE]: Набор инструкций _Streaming SIMD Extensions_ был представлен в 1999 году. Вместо повторного использования регистров с плавающей запятой он добавляет совершенно новый набор регистров. Шестнадцать новых регистров называются `xmm0` - `xmm15` и имеют размер 128 бит каждый. +- [AVX]: _Advanced Vector Extensions_ - это расширения, которые еще больше увеличивают размер мультимедийных регистров. Новые регистры называются `ymm0` - `ymm15` и имеют размер 256 бит каждый. Они расширяют регистры `xmm`, поэтому, например, `xmm0` - это нижняя половина `ymm0`. + +[MMX]: https://en.wikipedia.org/wiki/MMX_(instruction_set) +[x87 floating point unit]: https://en.wikipedia.org/wiki/X87 +[SSE]: https://en.wikipedia.org/wiki/Streaming_SIMD_Extensions +[AVX]: https://en.wikipedia.org/wiki/Advanced_Vector_Extensions + +Используя такие стандарты SIMD, программы часто могут значительно ускориться. Хорошие компиляторы способны автоматически преобразовывать обычные циклы в такой SIMD-код с помощью процесса, называемого [автовекторизацией][auto-vectorization]. + +[auto-vectorization]: https://en.wikipedia.org/wiki/Automatic_vectorization + +Однако большие регистры SIMD приводят к проблемам в ядрах ОС. Причина в том, что ядро должно создавать резервные копии всех регистров, которые оно использует, в память при каждом аппаратном прерывании, потому что они должны иметь свои первоначальные значения, когда прерванная программа продолжает работу. Поэтому, если ядро использует SIMD-регистры, ему приходится резервировать гораздо больше данных (512-1600 байт), что заметно снижает производительность. Чтобы избежать этого снижения производительности, мы хотим отключить функции `sse` и `mmx` (функция `avx` отключена по умолчанию). + +Мы можем сделать это через поле `features` в нашей целевой спецификации. Чтобы отключить функции `mmx` и `sse`, мы добавим их с минусом: + +```json +"features": "-mmx,-sse" +``` + +## Числа с плавающей точкой +К сожалению для нас, архитектура `x86_64` использует регистры SSE для операций с числами с плавающей точкой. Таким образом, каждое использование чисел с плавающей точкой с отключенным SSE вызовёт ошибку в LLVM. Проблема в том, что библиотека `core` уже использует числа с плавающей точкой (например, в ней реализованы трейты для `f32` и `f64`), поэтому недостаточно избегать чисел с плавающей точкой в нашем ядре. + +К счастью, LLVM поддерживает функцию `soft-float`, эмулирующую все операции с числавами с плавающей точкой через программные функции, основанные на обычных целых числах. Это позволяет использовать плавающие числа в нашем ядре без SSE, просто это будет немного медленнее. + +Чтобы включить функцию `soft-float` для нашего ядра, мы добавим ее в строку `features` в спецификации цели с префиксом плюс: + +```json +"features": "-mmx,-sse,+soft-float" +``` diff --git a/blog/content/edition-2/posts/02-minimal-rust-kernel/index.fa.md b/blog/content/edition-2/posts/02-minimal-rust-kernel/index.fa.md index ef4d0164..b5396c8c 100644 --- a/blog/content/edition-2/posts/02-minimal-rust-kernel/index.fa.md +++ b/blog/content/edition-2/posts/02-minimal-rust-kernel/index.fa.md @@ -23,6 +23,7 @@ rtl = true [گیت‌هاب]: https://github.com/phil-opp/blog_os [در زیر]: #comments + [post branch]: https://github.com/phil-opp/blog_os/tree/post-02 @@ -95,7 +96,7 @@ rtl = true همانطور که ممکن است به یاد داشته باشید، باینری مستقل را از طریق `cargo` ایجاد کردیم، اما با توجه به سیستم عامل، به نام‌های ورودی و پرچم‌های کامپایل مختلف نیاز داشتیم. به این دلیل که `cargo` به طور پیش فرض برای سیستم میزبان بیلد می‌کند، بطور مثال سیستمی که از آن برای نوشتن هسته استفاده می‌کنید. این چیزی نیست که ما برای هسته خود بخواهیم‌، زیرا منطقی نیست که هسته سیستم عامل‌مان را روی یک سیستم عامل دیگر اجرا کنیم. در عوض، ما می‌خواهیم هسته را برای یک _سیستم هدف_ کاملاً مشخص کامپایل کنیم. -### نصب Rust Nightly +### نصب Rust Nightly {#installing-rust-nightly} راست دارای سه کانال انتشار است: _stable_, _beta_, and _nightly_ (ترجمه از چپ به راست: پایدار، بتا و شبانه). کتاب Rust تفاوت بین این کانال‌ها را به خوبی توضیح می‌دهد، بنابراین یک دقیقه وقت بگذارید و [آن را بررسی کنید](https://doc.rust-lang.org/book/appendix-07-nightly-rust.html#choo-choo-release-channels-and-riding-the-trains). برای ساخت یک سیستم عامل به برخی از ویژگی‌های آزمایشی نیاز داریم که فقط در کانال شبانه موجود است‌، بنابراین باید نسخه شبانه Rust را نصب کنیم. @@ -202,19 +203,19 @@ For more information, see our post on [disabling SIMD](@/edition-2/posts/02-mini ```json { - "llvm-target": "x86_64-unknown-none", - "data-layout": "e-m:e-i64:64-f80:128-n8:16:32:64-S128", - "arch": "x86_64", - "target-endian": "little", - "target-pointer-width": "64", - "target-c-int-width": "32", - "os": "none", - "executables": true, - "linker-flavor": "ld.lld", - "linker": "rust-lld", - "panic-strategy": "abort", - "disable-redzone": true, - "features": "-mmx,-sse,+soft-float" + "llvm-target": "x86_64-unknown-none", + "data-layout": "e-m:e-i64:64-f80:128-n8:16:32:64-S128", + "arch": "x86_64", + "target-endian": "little", + "target-pointer-width": "64", + "target-c-int-width": "32", + "os": "none", + "executables": true, + "linker-flavor": "ld.lld", + "linker": "rust-lld", + "panic-strategy": "abort", + "disable-redzone": true, + "features": "-mmx,-sse,+soft-float" } ``` diff --git a/blog/content/edition-2/posts/02-minimal-rust-kernel/index.ja.md b/blog/content/edition-2/posts/02-minimal-rust-kernel/index.ja.md index 5cbbfafd..2858a722 100644 --- a/blog/content/edition-2/posts/02-minimal-rust-kernel/index.ja.md +++ b/blog/content/edition-2/posts/02-minimal-rust-kernel/index.ja.md @@ -22,11 +22,12 @@ translators = ["woodyZootopia", "JohnTitor"] [GitHub]: https://github.com/phil-opp/blog_os [at the bottom]: #comments + [post branch]: https://github.com/phil-opp/blog_os/tree/post-02 -## 起動 (Boot) のプロセス +## 起動 (Boot) のプロセス {#the-boot-process} コンピュータを起動すると、マザーボードの [ROM] に保存されたファームウェアのコードを実行し始めます。このコードは、[起動時の自己テスト (power-on self test) ][power-on self-test]を実行し、使用可能なRAMを検出し、CPUとハードウェアを事前初期化 (pre-initialize) します。その後、ブータブル (bootable) ディスクを探し、オペレーティングシステムのカーネルを起動 (boot) します。 [ROM]: https://ja.wikipedia.org/wiki/Read_only_memory @@ -91,7 +92,7 @@ x86には2つのファームウェアの標準規格があります:"Basic Inp 覚えていますか、この独立したバイナリは`cargo`を使ってビルドしましたが、オペレーティングシステムに依って異なるエントリポイント名とコンパイルフラグが必要なのでした。これは`cargo`は標準では **ホストシステム**(あなたの使っているシステム)向けにビルドするためです。例えばWindows上で走るカーネルというのはあまり意味がなく、私達の望む動作ではありません。代わりに、明確に定義された **ターゲットシステム** 向けにコンパイルできると理想的です。 -### RustのNightly版をインストールする +### RustのNightly版をインストールする {#installing-rust-nightly} Rustには**stable**、**beta**、**nightly**の3つのリリースチャンネルがあります。Rust Bookはこれらの3つのチャンネルの違いをとても良く説明しているので、一度[確認してみてください](https://doc.rust-jp.rs/book-ja/appendix-07-nightly-rust.html)。オペレーティングシステムをビルドするには、nightlyチャンネルでしか利用できないいくつかの実験的機能を使う必要があるので、Rustのnightly版をインストールすることになります。 Rustの実行環境を管理するのには、[rustup]を強くおすすめします。nightly、beta、stable版のコンパイラをそれぞれインストールすることができますし、アップデートするのも簡単です。現在のディレクトリにnightlyコンパイラを使うようにするには、`rustup override set nightly`と実行してください。もしくは、`rust-toolchain`というファイルに`nightly`と記入してプロジェクトのルートディレクトリに置くことでも指定できます。Nightly版を使っていることは、`rustc --version`と実行することで確かめられます。表示されるバージョン名の末尾に`-nightly`とあるはずです。 @@ -196,19 +197,19 @@ SIMDを無効化することによる問題に、`x86_64`における浮動小 ```json { - "llvm-target": "x86_64-unknown-none", - "data-layout": "e-m:e-i64:64-f80:128-n8:16:32:64-S128", - "arch": "x86_64", - "target-endian": "little", - "target-pointer-width": "64", - "target-c-int-width": "32", - "os": "none", - "executables": true, - "linker-flavor": "ld.lld", - "linker": "rust-lld", - "panic-strategy": "abort", - "disable-redzone": true, - "features": "-mmx,-sse,+soft-float" + "llvm-target": "x86_64-unknown-none", + "data-layout": "e-m:e-i64:64-f80:128-n8:16:32:64-S128", + "arch": "x86_64", + "target-endian": "little", + "target-pointer-width": "64", + "target-c-int-width": "32", + "os": "none", + "executables": true, + "linker-flavor": "ld.lld", + "linker": "rust-lld", + "panic-strategy": "abort", + "disable-redzone": true, + "features": "-mmx,-sse,+soft-float" } ``` diff --git a/blog/content/edition-2/posts/02-minimal-rust-kernel/index.md b/blog/content/edition-2/posts/02-minimal-rust-kernel/index.md index 7567da92..7e5fa8c3 100644 --- a/blog/content/edition-2/posts/02-minimal-rust-kernel/index.md +++ b/blog/content/edition-2/posts/02-minimal-rust-kernel/index.md @@ -18,6 +18,7 @@ This blog is openly developed on [GitHub]. If you have any problems or questions [GitHub]: https://github.com/phil-opp/blog_os [at the bottom]: #comments + [post branch]: https://github.com/phil-opp/blog_os/tree/post-02 @@ -193,19 +194,19 @@ Our target specification file now looks like this: ```json { - "llvm-target": "x86_64-unknown-none", - "data-layout": "e-m:e-i64:64-f80:128-n8:16:32:64-S128", - "arch": "x86_64", - "target-endian": "little", - "target-pointer-width": "64", - "target-c-int-width": "32", - "os": "none", - "executables": true, - "linker-flavor": "ld.lld", - "linker": "rust-lld", - "panic-strategy": "abort", - "disable-redzone": true, - "features": "-mmx,-sse,+soft-float" + "llvm-target": "x86_64-unknown-none", + "data-layout": "e-m:e-i64:64-f80:128-n8:16:32:64-S128", + "arch": "x86_64", + "target-endian": "little", + "target-pointer-width": "64", + "target-c-int-width": "32", + "os": "none", + "executables": true, + "linker-flavor": "ld.lld", + "linker": "rust-lld", + "panic-strategy": "abort", + "disable-redzone": true, + "features": "-mmx,-sse,+soft-float" } ``` @@ -306,6 +307,7 @@ Fortunately, the `compiler_builtins` crate already contains implementations for [unstable] build-std-features = ["compiler-builtins-mem"] +build-std = ["core", "compiler_builtins"] ``` (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.) diff --git a/blog/content/edition-2/posts/02-minimal-rust-kernel/index.ru.md b/blog/content/edition-2/posts/02-minimal-rust-kernel/index.ru.md new file mode 100644 index 00000000..6f3690b8 --- /dev/null +++ b/blog/content/edition-2/posts/02-minimal-rust-kernel/index.ru.md @@ -0,0 +1,502 @@ ++++ +title = "Минимально возможное ядро на Rust" +weight = 2 +path = "ru/minimal-rust-kernel" +date = 2018-02-10 + +[extra] +chapter = "С нуля" +translators = ["MrZloHex"] ++++ + +В этом посте мы создадим минимальное 64-битное ядро на Rust для архитектуры x86_64. Мы будем отталкиваться от [независимого бинарного файла][freestanding Rust binary] из предыдущего поста для создания загрузочного образа диска, который может что-то выводить на экран. + +[freestanding Rust binary]: @/edition-2/posts/01-freestanding-rust-binary/index.ru.md + + +Этот блог открыто разрабатывается на [GitHub]. Если у вас возникли какие-либо проблемы или вопросы, пожалуйста, создайте _issue_. Также вы можете оставлять комментарии [в конце страницы][at the bottom]. Полный исходный код для этого поста вы можете найти в репозитории в ветке [`post-02`][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-02 + + + +## Последовательность процессов запуска {#the-boot-process} + +Когда вы включаете компьютер, он начинает выполнять код микропрограммы, который хранится в [ПЗУ][ROM] материнской платы. Этот код выполняет [самотестирование при включении][power-on self-test], определяет доступную оперативную память и выполняет предварительную инициализацию процессора и аппаратного обеспечения. После этого он ищет загрузочный диск и начинает загрузку ядра операционной системы. + +[ROM]: https://en.wikipedia.org/wiki/Read-only_memory +[power-on self-test]: https://en.wikipedia.org/wiki/Power-on_self-test + +Для архитектуры x86 существует два стандарта прошивки: “Basic Input/Output System“ ("Базовая система ввода/вывода" **[BIOS]**) и более новый “Unified Extensible Firmware Interface” ("Унифицированный расширяемый интерфейс прошивки" **[UEFI]**). Стандарт BIOS - старый, но простой и хорошо поддерживаемый на любой машине x86 с 1980-х годов. UEFI, напротив, более современный и имеет гораздо больше возможностей, но более сложен в настройке (по крайней мере, на мой взгляд). + +[BIOS]: https://en.wikipedia.org/wiki/BIOS +[UEFI]: https://en.wikipedia.org/wiki/Unified_Extensible_Firmware_Interface + +В данный момент, мы обеспечиваем поддержку только BIOS, но планируется поддержка и UEFI. Если вы хотите помочь нам в этом, обратитесь к [Github issue](https://github.com/phil-opp/blog_os/issues/349). + +## Запуск BIOS + +Почти все системы x86 имеют поддержку загрузки BIOS, включая более новые машины на базе UEFI, которые используют эмулированный BIOS. Это замечательно, потому что вы можете использовать одну и ту же логику загрузки на всех машинах из прошлых веков. Но такая широкая совместимость одновременно является и самым большим недостатком загрузки BIOS, поскольку это означает, что перед загрузкой процессор переводится в 16-битный режим совместимости под названием [реальный режим], чтобы архаичные загрузчики 1980-х годов все еще работали. + +Но давайте начнем с самого начала: + +Когда вы включаете компьютер, он загружает BIOS из специальной флэш-памяти, расположенной на материнской плате. BIOS запускает процедуры самодиагностики и инициализации оборудования, затем ищет загрузочные диски. Если он находит такой, управление передается _загрузчику_, который представляет собой 512-байтовую порцию исполняемого кода, хранящуюся в начале диска. Большинство загрузчиков имеют размер более 512 байт, поэтому загрузчики обычно разделяются на небольшой первый этап, который помещается в 512 байт, и второй этап, который впоследствии загружается первым этапом. + +Загрузчик должен определить расположение образа ядра на диске и загрузить его в память. Он также должен переключить процессор из 16-битного [реального режима][real mode] сначала в 32-битный [защищенный режим][protected mode], а затем в 64-битный [длинный режим][long mode], где доступны 64-битные регистры и вся основная память. Третья задача - запросить определенную информацию (например, карту памяти) у BIOS и передать ее ядру ОС. + +[real mode]: https://en.wikipedia.org/wiki/Real_mode +[protected mode]: https://en.wikipedia.org/wiki/Protected_mode +[long mode]: https://en.wikipedia.org/wiki/Long_mode +[memory segmentation]: https://en.wikipedia.org/wiki/X86_memory_segmentation + +Написание загрузчика немного громоздко, поскольку требует использования языка ассемблера и множества неинтересных действий, таких как "запишите это магическое значение в этот регистр процессора". Поэтому мы не рассматриваем создание загрузчика в этом посте и вместо этого предоставляем инструмент под названием [bootimage], который автоматически добавляет загрузчик к вашему ядру. + +[bootimage]: https://github.com/rust-osdev/bootimage + +Если вы заинтересованы в создании собственного загрузчика: Оставайтесь с нами, набор постов на эту тему уже запланирован! + +#### Стандарт Multiboot + +Чтобы избежать того, что каждая операционная система реализует свой собственный загрузчик, который совместим только с одной ОС, [Free Software Foundation] в 1995 году создал открытый стандарт загрузчика под названием [Multiboot]. Стандарт определяет интерфейс между загрузчиком и операционной системой, так что любой совместимый с Multiboot загрузчик может загружать любую совместимую с Multiboot операционную систему. Эталонной реализацией является [GNU GRUB], который является самым популярным загрузчиком для систем Linux. + +[Free Software Foundation]: https://en.wikipedia.org/wiki/Free_Software_Foundation +[Multiboot]: https://wiki.osdev.org/Multiboot +[GNU GRUB]: https://en.wikipedia.org/wiki/GNU_GRUB + +Чтобы сделать ядро совместимым с Multiboot, нужно просто вставить так называемый [Multiboot заголовок][Multiboot header] в начало файла ядра. Это делает загрузку ОС в GRUB очень простой. Однако у GRUB и стандарта Multiboot есть и некоторые проблемы: + +[Multiboot header]: https://www.gnu.org/software/grub/manual/multiboot/multiboot.html#OS-image-format + +- Они поддерживают только 32-битный защищенный режим. Это означает, что для перехода на 64-битный длинный режим необходимо выполнить конфигурацию процессора. +- Они предназначены для того, чтобы упростить загрузчик вместо ядра. Например, ядро должно быть связано с [скорректированным размером страницы по умолчанию][adjusted default page size], потому что иначе GRUB не сможет найти заголовок Multiboot. Другой пример - [информация запуска][boot information], которая передается ядру, содержит множество структур, зависящих от архитектуры, вместо того, чтобы предоставлять чистые абстракции. +- И GRUB, и стандарт Multiboot документированы очень скудно. +- GRUB должен быть установлен на хост-системе, чтобы создать загрузочный образ диска из файла ядра. Это усложняет разработку под Windows или Mac. + +[adjusted default page size]: https://wiki.osdev.org/Multiboot#Multiboot_2 +[boot information]: https://www.gnu.org/software/grub/manual/multiboot/multiboot.html#Boot-information-format + +Из-за этих недостатков мы решили не использовать GRUB или стандарт Multiboot. Однако мы планируем добавить поддержку Multiboot в наш инструмент [bootimage], чтобы можно было загружать ваше ядро и на системе GRUB. Если вы заинтересованы в написании ядра, совместимого с Multiboot, ознакомьтесь с [первым выпуском][first edition] этой серии блогов. + +[first edition]: @/edition-1/_index.md + +### UEFI + +(На данный момент мы не предоставляем поддержку UEFI, но мы бы хотели! Если вы хотите помочь, пожалуйста, сообщите нам об этом в [Github issue](https://github.com/phil-opp/blog_os/issues/349).) + +## Минимально возможное ядро + +Теперь, когда мы примерно знаем, как запускается компьютер, пришло время создать собственное минимально возможное ядро. Наша цель - создать образ диска, который при загрузке выводит на экран "Hello World!". Для этого мы будем используем [Независимый бинарный файл на Rust][freestanding Rust binary] из предыдущего поста. + +Как вы помните, мы собирали независимый бинарный файл с помощью `cargo`, но в зависимости от операционной системы нам требовались разные имена точек входа и флаги компиляции. Это потому, что `cargo` по умолчанию компилирует для _хостовой системы_, то есть системы, на которой вы работаете. Это не то, что мы хотим для нашего ядра, потому что ядро, работающее поверх, например, Windows, не имеет особого смысла. Вместо этого мы хотим компилировать для четко определенной _целевой системы_. + +### Установка Rust Nightly {#installing-rust-nightly} + +Rust имеет три релизных канала: _stable_, _beta_ и _nightly_. В книге Rust Book очень хорошо объясняется разница между этими каналами, поэтому уделите минуту и [ознакомьтесь с ней](https://doc.rust-lang.org/book/appendix-07-nightly-rust.html#choo-choo-release-channels-and-riding-the-trains). Для создания операционной системы нам понадобятся некоторые экспериментальные возможности, которые доступны только на канале nightly, поэтому нам нужно установить nightly версию Rust. + +Для управления установками Rust я настоятельно рекомендую [rustup]. Он позволяет устанавливать nightly, beta и stable компиляторы рядом друг с другом и облегчает их обновление. С помощью rustup вы можете использовать nightly компилятор для текущего каталога, выполнив команду `rustup override set nightly`. В качестве альтернативы вы можете добавить файл `rust-toolchain` с содержимым `nightly` в корневой каталог проекта. Вы можете проверить, установлена ли у вас версия nightly, выполнив команду `rustc --version`: Номер версии должен содержать `-nightly` в конце. + +[rustup]: https://www.rustup.rs/ + +Nightly версия компилятора позволяет нам подключать различные экспериментальные возможности с помощью так называемых _флагов_ в верхней части нашего файла. Например, мы можем включить экспериментальный [макрос `asm!``asm!` macro] для встроенного ассемблера, добавив `#![feature(asm)]` в начало нашего `main.rs`. Обратите внимание, что такие экспериментальные возможности совершенно нестабильны, что означает, что будущие версии Rust могут изменить или удалить их без предварительного предупреждения. По этой причине мы будем использовать их только в случае крайней необходимости. + +[`asm!` macro]: https://doc.rust-lang.org/unstable-book/library-features/asm.html + +### Спецификация целевой платформы + +Cargo поддерживает различные целевые системы через параметр `--target`. Цель описывается так называемой тройкой _[target triple]_, которая описывает архитектуру процессора, производителя, операционную систему и [ABI]. Например, тройка целей `x86_64-unknown-linux-gnu` описывает систему с процессором `x86_64`, неизвестным поставщиком и операционной системой Linux с GNU ABI. Rust поддерживает [множество различных целевых троек][platform-support], включая `arm-linux-androideabi` для Android или [`wasm32-unknown-unknown` для WebAssembly](https://www.hellorust.com/setup/wasm-target/). + +[target triple]: https://clang.llvm.org/docs/CrossCompilation.html#target-triple +[ABI]: https://stackoverflow.com/a/2456882 +[platform-support]: https://forge.rust-lang.org/release/platform-support.html +[custom-targets]: https://doc.rust-lang.org/nightly/rustc/targets/custom.html + +Однако для нашей целевой системы нам требуются некоторые специальные параметры конфигурации (например, отсутствие базовой ОС), поэтому ни одна из [существующих целевых троек][platform-support] не подходит. К счастью, Rust позволяет нам определить [custom target][custom-targets] через JSON-файл. Например, JSON-файл, описывающий цель `x86_64-unknown-linux-gnu`, выглядит следующим образом: + +```json +{ + "llvm-target": "x86_64-unknown-linux-gnu", + "data-layout": "e-m:e-i64:64-f80:128-n8:16:32:64-S128", + "arch": "x86_64", + "target-endian": "little", + "target-pointer-width": "64", + "target-c-int-width": "32", + "os": "linux", + "executables": true, + "linker-flavor": "gcc", + "pre-link-args": ["-m64"], + "morestack": false +} +``` + +Большинство полей требуется LLVM для генерации кода для данной платформы. Например, поле [`data-layout`] определяет размер различных типов целых чисел, чисел с плавающей точкой и указателей. Затем есть поля, которые Rust использует для условной компиляции, такие как `target-pointer-width`. Третий вид полей определяет, как должен быть собран крейт. Например, поле `pre-link-args` определяет аргументы, передаваемые [компоновщику][linker]. + +[`data-layout`]: https://llvm.org/docs/LangRef.html#data-layout +[linker]: https://en.wikipedia.org/wiki/Linker_(computing) + +Для нашего ядра тоже нужна архитектура `x86_64`, поэтому наша спецификация цели будет очень похожа на приведенную выше. Начнем с создания файла `x86_64-blog_os.json` (выберите любое имя, которое вам нравится) с общим содержанием: + +```json +{ + "llvm-target": "x86_64-unknown-none", + "data-layout": "e-m:e-i64:64-f80:128-n8:16:32:64-S128", + "arch": "x86_64", + "target-endian": "little", + "target-pointer-width": "64", + "target-c-int-width": "32", + "os": "none", + "executables": true +} +``` + +Обратите внимание, что мы изменили ОС в поле `llvm-target` и `os` на `none`, потому что мы будем работать на голом железе. + +Добавляем дополнительные параметры для сборки ядра: + +```json +"linker-flavor": "ld.lld", +"linker": "rust-lld", +``` + +Вместо того чтобы использовать компоновщик по умолчанию платформы (который может не поддерживать цели Linux), мы используем кроссплатформенный компоновщик [LLD], поставляемый вместе с Rust, для компоновки нашего ядра. + +[LLD]: https://lld.llvm.org/ + +```json +"panic-strategy": "abort", +``` + +Этот параметр указывает, что цель не поддерживает [раскрутку стека][stack unwinding] при панике, поэтому вместо этого программа должна прерваться напрямую. Это имеет тот же эффект, что и опция `panic = "abort"` в нашем Cargo.toml, поэтому мы можем удалить ее оттуда. (Обратите внимание, что в отличие от опции Cargo.toml, эта опция также будет применяться, когда мы перекомпилируем библиотеку `core` позже в этом посте. Поэтому не забудьте добавить эту опцию, даже если вы предпочтете оставить опцию в Cargo.toml). + +[stack unwinding]: https://www.bogotobogo.com/cplusplus/stackunwinding.php + +```json +"disable-redzone": true, +``` + +Мы пишем ядро, поэтому в какой-то момент нам понадобится обрабатывать прерывания. Чтобы сделать это безопасно, мы должны отключить определенную оптимизацию указателя стека, называемую _"красной зоной"_, поскольку в противном случае она приведет к повреждениям стека. Для получения дополнительной информации см. нашу отдельную статью об [отключении красной зоны][disabling the red zone]. + +[disabling the red zone]: @/edition-2/posts/02-minimal-rust-kernel/disable-red-zone/index.ru.md + +```json +"features": "-mmx,-sse,+soft-float", +``` + +Поле `features` включает/выключает функции целевой платформы. Мы отключаем функции `mmx` и `sse`, добавляя к ним минус, и включаем функцию `soft-float`, добавляя к ней плюс. Обратите внимание, что между разными флагами не должно быть пробелов, иначе LLVM не сможет интерпретировать строку features. + +Функции `mmx` и `sse` определяют поддержку инструкций [Single Instruction Multiple Data (SIMD)], которые часто могут значительно ускорить работу программ. Однако использование больших регистров SIMD в ядрах ОС приводит к проблемам с производительностью. Причина в том, что ядру необходимо восстановить все регистры в исходное состояние перед продолжением прерванной программы. Это означает, что ядро должно сохранять полное состояние SIMD в основной памяти при каждом системном вызове или аппаратном прерывании. Поскольку состояние SIMD очень велико (512-1600 байт), а прерывания могут происходить очень часто, эти дополнительные операции сохранения/восстановления значительно снижают производительность. Чтобы избежать этого, мы отключили SIMD для нашего ядра (не для приложений, работающих поверх него!). + +[Single Instruction Multiple Data (SIMD)]: https://en.wikipedia.org/wiki/SIMD + +Проблема с отключением SIMD заключается в том, что операции с числами с плавающей точкой на `x86_64` по умолчанию требуют регистров SIMD. Чтобы решить эту проблему, мы добавили функцию `soft-float`, которая эмулирует все операции с числами с плавающей точкой через программные функции, основанные на обычных целых числах. + +Для получения дополнительной информации см. наш пост об [отключении SIMD](@/edition-2/posts/02-minimal-rust-kernel/disable-simd/index.ru.md). + +#### Соединяем все вместе + +Наша спецификация целовой платформы выглядит следующим образом: + +```json +{ + "llvm-target": "x86_64-unknown-none", + "data-layout": "e-m:e-i64:64-f80:128-n8:16:32:64-S128", + "arch": "x86_64", + "target-endian": "little", + "target-pointer-width": "64", + "target-c-int-width": "32", + "os": "none", + "executables": true, + "linker-flavor": "ld.lld", + "linker": "rust-lld", + "panic-strategy": "abort", + "disable-redzone": true, + "features": "-mmx,-sse,+soft-float" +} +``` + +### Компиляция ядра + +Компиляция для нашей новой целевой платформы будет использовать соглашения Linux (я не совсем уверен почему — предполагаю, что это просто поведение LLVM по умолчанию). Это означает, что нам нужна точка входа с именем `_start`, как описано в [предыдущем посте][previous post]: + +[previous post]: @/edition-2/posts/01-freestanding-rust-binary/index.ru.md + +```rust +// src/main.rs + +#![no_std] // don't link the Rust standard library +#![no_main] // disable all Rust-level entry points + +use core::panic::PanicInfo; + +/// This function is called on panic. +#[panic_handler] +fn panic(_info: &PanicInfo) -> ! { + loop {} +} + +#[no_mangle] // don't mangle the name of this function +pub extern "C" fn _start() -> ! { + // this function is the entry point, since the linker looks for a function + // named `_start` by default + loop {} +} +``` + +Обратите внимание, что точка входа должна называться `_start` независимо от используемой вами ОС. + +Теперь мы можем собрать ядро для нашей новой цели, передав имя файла JSON в качестве `--target`: + +``` +> cargo build --target x86_64-blog_os.json + +error[E0463]: can't find crate for `core` +``` + +Не получается! Ошибка сообщает нам, что компилятор Rust больше не может найти [библиотеку `core`][`core` library]. Эта библиотека содержит основные типы Rust, такие как `Result`, `Option` и итераторы, и неявно связана со всеми `no_std` модулями. + +[`core` library]: https://doc.rust-lang.org/nightly/core/index.html + +Проблема в том, что корневая (`core`) библиотека распространяется вместе с компилятором Rust как _прекомпилированная_ библиотека. Поэтому она действительна только для поддерживаемых тройных хостов (например, `x86_64-unknown-linux-gnu`), но не для нашей пользовательской целевой платформы. Если мы хотим скомпилировать код для других целевых платформ, нам нужно сначала перекомпилировать `core` для этих целей. + +### Функция `build-std` + +Вот тут-то и приходит на помощь функция [`build-std`][`build-std` feature] в cargo. Она позволяет перекомпилировать `core` и другие стандартные библиотеки по требованию, вместо того, чтобы использовать предварительно скомпилированные версии, поставляемые вместе с установкой Rust. Эта функция очень новая и еще не закончена, поэтому она помечена как "нестабильная" и доступна только на [nightly Rust]. + +[`build-std` feature]: https://doc.rust-lang.org/nightly/cargo/reference/unstable.html#build-std +[nightly Rust]: #installing-rust-nightly + +Чтобы использовать эту функцию, нам нужно создать файл [конфигурации cargo][cargo configuration] по пути `.cargo/config.toml` со следующим содержимым: + +[cargo configuration]: https://doc.rust-lang.org/cargo/reference/config.html + + +```toml +# in .cargo/config.toml + +[unstable] +build-std = ["core", "compiler_builtins"] +``` + +Это говорит cargo, что он должен перекомпилировать библиотеки `core` и `compiler_builtins`. Последняя необходима, поскольку `core` зависит от неё. Чтобы перекомпилировать эти библиотеки, cargo нужен доступ к исходному коду rust, который мы можем установить с помощью команды `rustup component add rust-src`. + +
    + +**Note:** Ключ конфигурации `unstable.build-std` требует как минимум Rust nightly от 2020-07-15. + +
    + +``` +> cargo build --target x86_64-blog_os.json + Compiling core v0.0.0 (/…/rust/src/libcore) + Compiling rustc-std-workspace-core v1.99.0 (/…/rust/src/tools/rustc-std-workspace-core) + Compiling compiler_builtins v0.1.32 + Compiling blog_os v0.1.0 (/…/blog_os) + Finished dev [unoptimized + debuginfo] target(s) in 0.29 secs +``` + +Мы видим, что `cargo build` теперь перекомпилирует библиотеки `core`, `rustc-std-workspace-core` (зависимость от `compiler_builtins`) и `compiler_builtins` для нашей пользовательской целевой платформы. + +#### Внутренние функции, работающие с памятью + +Компилятор Rust предполагает, что определенный набор встроенных функций доступен для всех систем. Большинство этих функций обеспечивается модулем `compiler_builtins`, который мы только что перекомпилировали. Однако в этом модуле есть некоторые функции, связанные с памятью, которые не включены по умолчанию, потому что они обычно предоставляются библиотекой C в системе. Эти функции включают `memset`, которая устанавливает все байты в блоке памяти в заданное значение, `memcpy`, которая копирует один блок памяти в другой, и `memcmp`, которая сравнивает два блока памяти. Хотя ни одна из этих функций нам сейчас не понадобилась для компиляции нашего ядра, они потребуются, как только мы добавим в него дополнительный код (например, при копировании структур). + +Поскольку мы не можем ссылаться на С библиотеку хостовой операционной системы, нам нужен альтернативный способ предоставления этих функций компилятору. Одним из возможных подходов для этого может быть реализация наших собственных функций `memset` и т.д. и применение к ним атрибута `#[no_mangle]` (чтобы избежать автоматического переименования во время компиляции). Однако это опасно, поскольку малейшая ошибка в реализации этих функций может привести к неопределенному поведению. Например, при реализации `memcpy` с помощью цикла `for` вы можете получить бесконечную рекурсию, поскольку циклы `for` неявно вызывают метод трейта [`IntoIterator::into_iter`], который может снова вызвать `memcpy`. Поэтому хорошей идеей будет повторное использование существующих, хорошо протестированных реализаций. + +[`IntoIterator::into_iter`]: https://doc.rust-lang.org/stable/core/iter/trait.IntoIterator.html#tymethod.into_iter + +К счастью, модуль `compiler_builtins` уже содержит реализации всех необходимых функций, они просто отключены по умолчанию, чтобы не столкнуться с реализациями из С библиотеки. Мы можем включить их, установив флаг cargo [`build-std-features`] на `["compiler-builtins-mem"]`. Как и флаг `build-std`, этот флаг может быть передан в командной строке как флаг `-Z` или настроен в таблице `unstable` в файле `.cargo/config.toml`. Поскольку мы всегда хотим собирать с этим флагом, вариант с конфигурационным файлом имеет для нас больше смысла: + +[`build-std-features`]: https://doc.rust-lang.org/nightly/cargo/reference/unstable.html#build-std-features + +```toml +# in .cargo/config.toml + +[unstable] +build-std-features = ["compiler-builtins-mem"] +build-std = ["core", "compiler_builtins"] +``` + +(Поддержка функции `compiler-builtins-mem` была [добавлена совсем недавно](https://github.com/rust-lang/rust/pull/77284), поэтому для нее вам нужен как минимум Rust nightly `2020-09-30`). + +За кулисами этот флаг включает функцию [`mem`][`mem` feature] крейта `compiler_builtins`. Это приводит к тому, что атрибут `#[no_mangle]` применяется к [реализациям `memcpy` и т.п.][`memcpy` etc. implementations] из этого крейта, что делает их доступными для компоновщика. + +[`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 + +Благодаря этому изменению наше ядро имеет валидные реализации для всех функций, требуемых компилятором, поэтому оно будет продолжать компилироваться, даже если наш код станет сложнее. + +#### Переопределение цели по умолчанию + +Чтобы избежать передачи параметра `--target` при каждом вызове `cargo build`, мы можем переопределить цель по умолчанию. Для этого мы добавим следующее в наш файл [конфигураций cargo][cargo configuration] по пути `.cargo/config.toml`: + +```toml +# in .cargo/config.toml + +[build] +target = "x86_64-blog_os.json" +``` + +С этой конфигурацией `cargo` будет использовать нашу цель `x86_64-blog_os.json`, если не передан явный аргумент `--target`. Это означает, что теперь мы можем собрать наше ядро с помощью простой `cargo build`. Чтобы узнать больше о параметрах конфигурации cargo, ознакомьтесь с [официальной документацией][cargo configuration]. + +Теперь мы можем скомпилировать наше ядро под голое железо с помощью простой `cargo build`. Однако наша точка входа `_start`, которая будет вызываться загрузчиком, все еще пуста. Пришло время вывести что-нибудь на экран. + +### Вывод на экран + +Самым простым способом печати текста на экран на данном этапе является [текстовый буфер VGA][VGA text buffer]. Это специальная область памяти, сопоставленная с аппаратным обеспечением VGA, которая содержит содержимое, отображаемое на экране. Обычно он состоит из 25 строк, каждая из которых содержит 80 символьных ячеек. Каждая символьная ячейка отображает символ ASCII с некоторыми цветами переднего и заднего плана. Вывод на экран выглядит следующим образом: + +[VGA text buffer]: https://en.wikipedia.org/wiki/VGA-compatible_text_mode + +![screen output for common ASCII characters](https://upload.wikimedia.org/wikipedia/commons/f/f8/Codepage-437.png) + +Точную разметку буфера VGA мы обсудим в следующем посте, где мы напишем первый небольшой драйвер для него. Для печати "Hello World!" нам достаточно знать, что буфер расположен по адресу `0xb8000` и что каждая символьная ячейка состоит из байта ASCII и байта цвета. + +Реализация выглядит следующим образом: + +```rust +static HELLO: &[u8] = b"Hello World!"; + +#[no_mangle] +pub extern "C" fn _start() -> ! { + let vga_buffer = 0xb8000 as *mut u8; + + for (i, &byte) in HELLO.iter().enumerate() { + unsafe { + *vga_buffer.offset(i as isize * 2) = byte; + *vga_buffer.offset(i as isize * 2 + 1) = 0xb; + } + } + + loop {} +} +``` + +Сначала мы приводим целое число `0xb8000` к [сырому указателю][raw pointer]. Затем мы [итерируем][iterate] по байтам [статической][static] [байтовой строки][byte string] `HELLO`. Мы используем метод [`enumerate`], чтобы дополнительно получить бегущую переменную `i`. В теле цикла for мы используем метод [`offset`] для записи байта строки и соответствующего байта цвета (`0xb` - светло-голубой). + +[iterate]: https://doc.rust-lang.org/stable/book/ch13-02-iterators.html +[static]: https://doc.rust-lang.org/book/ch10-03-lifetime-syntax.html#the-static-lifetime +[`enumerate`]: https://doc.rust-lang.org/core/iter/trait.Iterator.html#method.enumerate +[byte string]: https://doc.rust-lang.org/reference/tokens.html#byte-string-literals +[raw pointer]: https://doc.rust-lang.org/stable/book/ch19-01-unsafe-rust.html#dereferencing-a-raw-pointer +[`offset`]: https://doc.rust-lang.org/std/primitive.pointer.html#method.offset + +Обратите внимание, что вокруг всех записей в память стоит блок [`unsafe`]. Причина в том, что компилятор Rust не может доказать, что сырые указатели, которые мы создаем, действительны. Они могут указывать куда угодно и привести к повреждению данных. Помещая их в блок `unsafe`, мы, по сути, говорим компилятору, что абсолютно уверены в правильности операций. Обратите внимание, что блок `unsafe` не отключает проверки безопасности Rust. Он только позволяет вам делать [пять дополнительных вещей][five additional things]. + +[`unsafe`]: https://doc.rust-lang.org/stable/book/ch19-01-unsafe-rust.html +[five additional things]: https://doc.rust-lang.org/stable/book/ch19-01-unsafe-rust.html#unsafe-superpowers + +Я хочу подчеркнуть, что **это не тот способ, которым стоит что-либо делать в Rust!** Очень легко ошибиться при работе с сырыми указателями внутри блоков `unsafe`: например, мы можем легко записать за конец буфера, если не будем осторожны. + +Поэтому мы хотим минимизировать использование `unsafe` настолько, насколько это возможно. Rust дает нам возможность сделать это путем создания безопасных абстракций. Например, мы можем создать тип буфера VGA, который инкапсулирует всю небезопасность и гарантирует, что извне _невозможно_ сделать что-либо неправильно. Таким образом, нам понадобится лишь минимальное количество блоков `unsafe` и мы можем быть уверены, что не нарушаем [безопасность памяти][memory safety]. Мы создадим такую безопасную абстракцию буфера VGA в следующем посте. + +[memory safety]: https://en.wikipedia.org/wiki/Memory_safety + +## Запуск ядра + +Теперь, когда у нас есть исполняемый файл, который делает что-то ощутимое, пришло время запустить его. Сначала нам нужно превратить наше скомпилированное ядро в загрузочный образ диска, связав его с загрузчиком. Затем мы можем запустить образ диска в виртуальной машине [QEMU] или загрузить его на реальном оборудовании с помощью USB-носителя. + +### Создание загрузочного образа + +Чтобы превратить наше скомпилированное ядро в загрузочный образ диска, нам нужно связать его с загрузчиком. Как мы узнали в [разделе о загрузке], загрузчик отвечает за инициализацию процессора и загрузку нашего ядра. + +[разделе о загрузке]: #the-boot-process + +Вместо того чтобы писать собственный загрузчик, который является самостоятельным проектом, мы используем модуль [`bootloader`]. Этот модуль реализует базовый BIOS-загрузчик без каких-либо C-зависимостей, только Rust и встроенный ассемблер. Чтобы использовать его для загрузки нашего ядра, нам нужно добавить зависимость от него: + +[`bootloader`]: https://crates.io/crates/bootloader + +```toml +# in Cargo.toml + +[dependencies] +bootloader = "0.9.8" +``` + +Добавление загрузчика в качестве зависимости недостаточно для создания загрузочного образа диска. Проблема в том, что нам нужно связать наше ядро с загрузчиком после компиляции, но в cargo нет поддержки [скриптов после сборки][post-build scripts]. + +[post-build scripts]: https://github.com/rust-lang/cargo/issues/545 + +Для решения этой проблемы мы создали инструмент `bootimage`, который сначала компилирует ядро и загрузчик, а затем соединяет их вместе для создания загрузочного образа диска. Чтобы установить инструмент, выполните следующую команду в терминале: + +``` +cargo install bootimage +``` + +Для запуска `bootimage` и сборки загрузчика вам необходимо установить компонент rustup `llvm-tools-preview`. Это можно сделать, выполнив команду `rustup component add llvm-tools-preview`. + +После установки `bootimage` и добавления компонента `llvm-tools-preview` мы можем создать образ загрузочного диска, выполнив команду: + +``` +> cargo bootimage +``` + +Мы видим, что инструмент перекомпилирует наше ядро с помощью `cargo build`, поэтому он автоматически подхватит все внесенные вами изменения. После этого он компилирует загрузчик, что может занять некоторое время. Как и все зависимости модулей, он собирается только один раз, а затем кэшируется, поэтому последующие сборки будут происходить гораздо быстрее. Наконец, `bootimage` объединяет загрузчик и ваше ядро в загрузочный образ диска. + +После выполнения команды вы должны увидеть загрузочный образ диска с именем `bootimage-blog_os.bin` в каталоге `target/x86_64-blog_os/debug`. Вы можете загрузить его в виртуальной машине или скопировать на USB-накопитель, чтобы загрузить его на реальном оборудовании. (Обратите внимание, что это не образ CD, который имеет другой формат, поэтому запись на CD не работает). + +#### Как этот работает? + +Инструмент `bootimage` выполняет следующие действия за кулисами: + +- Компилирует наше ядро в файл [ELF]. +- Компилирует зависимость загрузчика как отдельный исполняемый файл. +- Он связывает байты ELF-файла ядра с загрузчиком. + +[ELF]: https://en.wikipedia.org/wiki/Executable_and_Linkable_Format +[rust-osdev/bootloader]: https://github.com/rust-osdev/bootloader + +При запуске загрузчик читает и разбирает приложенный файл ELF. Затем он сопоставляет сегменты программы с виртуальными адресами в таблицах страниц, обнуляет секцию `.bss` и устанавливает стек. Наконец, он считывает адрес точки входа (наша функция `_start`) и переходит к ней. + +### Запуск через QEMU + +Теперь мы можем загрузить образ диска в виртуальной машине. Чтобы загрузить его в [QEMU], выполните следующую команду: + +[QEMU]: https://www.qemu.org/ + +``` +> qemu-system-x86_64 -drive format=raw,file=target/x86_64-blog_os/debug/bootimage-blog_os.bin +warning: TCG doesn't support requested feature: CPUID.01H:ECX.vmx [bit 5] +``` + +Откроется отдельное окно, которое выглядит следующим образом: + +![QEMU showing "Hello World!"](qemu.png) + +Мы видим, что наш "Hello World!" отображается на экране. + +### Настоящая машина + +Также можно записать его на USB-накопитель и загрузить на реальной машине: + +``` +> dd if=target/x86_64-blog_os/debug/bootimage-blog_os.bin of=/dev/sdX && sync +``` + +Где `sdX` - имя устройства вашего USB-накопителя. **Внимательно проверьте**, что вы выбрали правильное имя устройства, потому что все, что находится на этом устройстве, будет перезаписано. + +После записи образа на USB-накопитель его можно запустить на реальном оборудовании, загрузившись с него. Для загрузки с USB-накопителя вам, вероятно, потребуется использовать специальное меню загрузки или изменить порядок загрузки в конфигурации BIOS. Обратите внимание, что в настоящее время это не работает на машинах с UEFI, так как модуль `bootloader` пока не имеет поддержки UEFI. + +### Использование `cargo run` + +Чтобы облегчить запуск нашего ядра в QEMU, мы можем установить ключ конфигурации `runner` для cargo: + +```toml +# in .cargo/config.toml + +[target.'cfg(target_os = "none")'] +runner = "bootimage runner" +``` + +Таблица `target.'cfg(target_os = "none")'` применяется ко всем целям, которые установили поле `"os"` своего конфигурационного файла цели на `"none"`. Это включает нашу цель `x86_64-blog_os.json`. Ключ `runner` указывает команду, которая должна быть вызвана для `cargo run`. Команда запускается после успешной сборки с путем к исполняемому файлу, переданному в качестве первого аргумента. Более подробную информацию смотрите в [документации по cargo][cargo configuration]. + +Команда `bootimage runner` специально разработана для использования в качестве исполняемого файла `runner`. Она связывает заданный исполняемый файл с зависимостью загрузчика проекта, а затем запускает QEMU. Более подробную информацию и возможные варианты конфигурации смотрите в [Readme of `bootimage`]. + +[Readme of `bootimage`]: https://github.com/rust-osdev/bootimage + +Теперь мы можем использовать `cargo run` для компиляции нашего ядра и его загрузки в QEMU. + +## Что дальше? + +В следующем посте мы более подробно рассмотрим текстовый буфер VGA и напишем безопасный интерфейс для него. Мы также добавим поддержку макроса `println`. diff --git a/blog/content/edition-2/posts/02-minimal-rust-kernel/index.zh-CN.md b/blog/content/edition-2/posts/02-minimal-rust-kernel/index.zh-CN.md index 72f8f23f..8a7076d6 100644 --- a/blog/content/edition-2/posts/02-minimal-rust-kernel/index.zh-CN.md +++ b/blog/content/edition-2/posts/02-minimal-rust-kernel/index.zh-CN.md @@ -21,6 +21,7 @@ This blog is openly developed on [GitHub]. If you have any problems or questions [GitHub]: https://github.com/phil-opp/blog_os [at the bottom]: #comments + [post branch]: https://github.com/phil-opp/blog_os/tree/post-02 @@ -152,19 +153,19 @@ Nightly 版本的编译器允许我们在源码的开头插入**特性标签** ```json { - "llvm-target": "x86_64-unknown-none", - "data-layout": "e-m:e-i64:64-f80:128-n8:16:32:64-S128", - "arch": "x86_64", - "target-endian": "little", - "target-pointer-width": "64", - "target-c-int-width": "32", - "os": "none", - "executables": true, - "linker-flavor": "ld.lld", - "linker": "rust-lld", - "panic-strategy": "abort", - "disable-redzone": true, - "features": "-mmx,-sse,+soft-float" + "llvm-target": "x86_64-unknown-none", + "data-layout": "e-m:e-i64:64-f80:128-n8:16:32:64-S128", + "arch": "x86_64", + "target-endian": "little", + "target-pointer-width": "64", + "target-c-int-width": "32", + "os": "none", + "executables": true, + "linker-flavor": "ld.lld", + "linker": "rust-lld", + "panic-strategy": "abort", + "disable-redzone": true, + "features": "-mmx,-sse,+soft-float" } ``` diff --git a/blog/content/edition-2/posts/03-vga-text-buffer/index.fa.md b/blog/content/edition-2/posts/03-vga-text-buffer/index.fa.md index 68a004bd..cdf40906 100644 --- a/blog/content/edition-2/posts/03-vga-text-buffer/index.fa.md +++ b/blog/content/edition-2/posts/03-vga-text-buffer/index.fa.md @@ -24,6 +24,7 @@ rtl = true [گیت‌هاب]: https://github.com/phil-opp/blog_os [در زیر]: #comments + [post branch]: https://github.com/phil-opp/blog_os/tree/post-03 @@ -488,7 +489,7 @@ error[E0017]: references in statics may only refer to immutable values مسئله در مورد `ColorCode::new` با استفاده از توابع [`const` functions] قابل حل است ، اما مشکل اساسی اینجاست که Rust's const evaluator قادر به تبدیل اشاره‌گر‌های خام به رفرنس در زمان کامپایل نیست. شاید روزی جواب دهد ، اما تا آن زمان ، ما باید راه حل دیگری پیدا کنیم. -[`const` functions]: https://doc.rust-lang.org/unstable-book/language-features/const-fn.html +[`const` functions]: https://doc.rust-lang.org/reference/const_eval.html#const-functions ### استاتیک‌های تنبل (Lazy Statics) یکبار مقداردهی اولیه استاتیک‌ها با توابع غیر ثابت یک مشکل رایج در راست است. خوشبختانه ، در حال حاضر راه حل خوبی در کرتی به نام [lazy_static] وجود دارد. این کرت ماکرو `lazy_static!` را فراهم می کند که یک `استاتیک` را با تنبلی مقدار‌دهی اولیه می کند. به جای محاسبه مقدار آن در زمان کامپایل ، `استاتیک` به تنبلی هنگام اولین دسترسی به آن، خود را مقداردهی اولیه می‌کند. بنابراین ، مقداردهی اولیه در زمان اجرا اتفاق می افتد تا کد مقدار دهی اولیه پیچیده و دلخواه امکان پذیر باشد. diff --git a/blog/content/edition-2/posts/03-vga-text-buffer/index.ja.md b/blog/content/edition-2/posts/03-vga-text-buffer/index.ja.md index 0f063bf6..dd9fac5a 100644 --- a/blog/content/edition-2/posts/03-vga-text-buffer/index.ja.md +++ b/blog/content/edition-2/posts/03-vga-text-buffer/index.ja.md @@ -23,6 +23,7 @@ translators = ["woodyZootopia", "JohnTitor"] [GitHub]: https://github.com/phil-opp/blog_os [at the bottom]: #comments + [post branch]: https://github.com/phil-opp/blog_os/tree/post-03 @@ -495,7 +496,7 @@ error[E0017]: references in statics may only refer to immutable values `ColorCode::new`に関する問題は[`const`関数][`const` functions]を使って解決できるかもしれませんが、ここでの根本的な問題は、Rustのconst evaluatorがコンパイル時に生ポインタを参照へと変えることができないということです。いつかうまく行くようになるのかもしれませんが、その時までは、別の方法を行わなければなりません。 -[`const` functions]: https://doc.rust-lang.org/unstable-book/language-features/const-fn.html +[`const` functions]: https://doc.rust-lang.org/reference/const_eval.html#const-functions ### 怠けた (Lazy) 静的変数 定数でない関数で一度だけ静的変数を初期化したい、というのはRustにおいてよくある問題です。嬉しいことに、[lazy_static]というクレートにすでに良い解決方法が存在します。このクレートは、初期化が後回しにされる`static`を定義する`lazy_static!`マクロを提供します。その値をコンパイル時に計算する代わりに、この`static`は最初にアクセスされたときに初めて初期化します。したがって、初期化は実行時に起こるので、どんなに複雑な初期化プログラムも可能ということです。 diff --git a/blog/content/edition-2/posts/03-vga-text-buffer/index.md b/blog/content/edition-2/posts/03-vga-text-buffer/index.md index 0029e751..9176ba4d 100644 --- a/blog/content/edition-2/posts/03-vga-text-buffer/index.md +++ b/blog/content/edition-2/posts/03-vga-text-buffer/index.md @@ -19,6 +19,7 @@ This blog is openly developed on [GitHub]. If you have any problems or questions [GitHub]: https://github.com/phil-opp/blog_os [at the bottom]: #comments + [post branch]: https://github.com/phil-opp/blog_os/tree/post-03 @@ -484,7 +485,7 @@ To understand what's happening here, we need to know that statics are initialize The issue about `ColorCode::new` would be solvable by using [`const` functions], but the fundamental problem here is that Rust's const evaluator is not able to convert raw pointers to references at compile time. Maybe it will work someday, but until then, we have to find another solution. -[`const` functions]: https://doc.rust-lang.org/unstable-book/language-features/const-fn.html +[`const` functions]: https://doc.rust-lang.org/reference/const_eval.html#const-functions ### Lazy Statics The one-time initialization of statics with non-const functions is a common problem in Rust. Fortunately, there already exists a good solution in a crate named [lazy_static]. This crate provides a `lazy_static!` macro that defines a lazily initialized `static`. Instead of computing its value at compile time, the `static` laziliy initializes itself when it's accessed the first time. Thus, the initialization happens at runtime so that arbitrarily complex initialization code is possible. diff --git a/blog/content/edition-2/posts/03-vga-text-buffer/index.zh-CN.md b/blog/content/edition-2/posts/03-vga-text-buffer/index.zh-CN.md index 6f875411..c5eaf2db 100644 --- a/blog/content/edition-2/posts/03-vga-text-buffer/index.zh-CN.md +++ b/blog/content/edition-2/posts/03-vga-text-buffer/index.zh-CN.md @@ -22,6 +22,7 @@ This blog is openly developed on [GitHub]. If you have any problems or questions [GitHub]: https://github.com/phil-opp/blog_os [at the bottom]: #comments + [post branch]: https://github.com/phil-opp/blog_os/tree/post-03 @@ -449,7 +450,7 @@ error[E0017]: references in statics may only refer to immutable values 为了明白现在发生了什么,我们需要知道一点:一般的变量在运行时初始化,而静态变量在编译时初始化。Rust编译器规定了一个称为**常量求值器**([const evaluator](https://rustc-dev-guide.rust-lang.org/const-eval.html))的组件,它应该在编译时处理这样的初始化工作。虽然它目前的功能较为有限,但对它的扩展工作进展活跃,比如允许在常量中 panic 的[一篇 RFC 文档](https://github.com/rust-lang/rfcs/pull/2345)。 -关于 `ColorCode::new` 的问题应该能使用**常函数**([`const` functions](https://doc.rust-lang.org/unstable-book/language-features/const-fn.html))解决,但常量求值器还存在不完善之处,它还不能在编译时直接转换裸指针到变量的引用——也许未来这段代码能够工作,但在那之前,我们需要寻找另外的解决方案。 +关于 `ColorCode::new` 的问题应该能使用**常函数**([`const` functions](https://doc.rust-lang.org/reference/const_eval.html#const-functions))解决,但常量求值器还存在不完善之处,它还不能在编译时直接转换裸指针到变量的引用——也许未来这段代码能够工作,但在那之前,我们需要寻找另外的解决方案。 ### 延迟初始化 diff --git a/blog/content/edition-2/posts/04-testing/index.fa.md b/blog/content/edition-2/posts/04-testing/index.fa.md index 3e119cd8..2e065c56 100644 --- a/blog/content/edition-2/posts/04-testing/index.fa.md +++ b/blog/content/edition-2/posts/04-testing/index.fa.md @@ -21,6 +21,7 @@ rtl = true [گیت‌هاب]: https://github.com/phil-opp/blog_os [در زیر]: #comments + [post branch]: https://github.com/phil-opp/blog_os/tree/post-04 @@ -179,18 +180,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.13.2/x86_64/ +[`x86_64`]: https://docs.rs/x86_64/0.14.2/x86_64/ ```toml # in Cargo.toml [dependencies] -x86_64 = "0.13.2" +x86_64 = "0.14.2" ``` اکنون می‌توانیم از نوع [`Port`] ارائه شده توسط کریت برای ایجاد عملکرد `exit_qemu` استفاده کنیم: -[`Port`]: https://docs.rs/x86_64/0.13.2/x86_64/instructions/port/struct.Port.html +[`Port`]: https://docs.rs/x86_64/0.14.2/x86_64/instructions/port/struct.Port.html ```rust // in src/main.rs diff --git a/blog/content/edition-2/posts/04-testing/index.ja.md b/blog/content/edition-2/posts/04-testing/index.ja.md index de61e1ac..6f8837f5 100644 --- a/blog/content/edition-2/posts/04-testing/index.ja.md +++ b/blog/content/edition-2/posts/04-testing/index.ja.md @@ -20,6 +20,7 @@ translators = ["woodyZootopia", "JohnTitor"] [GitHub]: https://github.com/phil-opp/blog_os [at the bottom]: #comments + [post branch]: https://github.com/phil-opp/blog_os/tree/post-04 @@ -183,18 +184,18 @@ CPUと周辺機器 (ペリフェラル) `in`と`out`のアセンブリ命令を手動で呼び出す代わりに、[`x86_64`]クレートによって提供されるabstraction (抽象化されたもの) を使います。このクレートへの依存を追加するため、`Cargo.toml`の`dependencies`セクションにこれを追加しましょう: -[`x86_64`]: https://docs.rs/x86_64/0.13.2/x86_64/ +[`x86_64`]: https://docs.rs/x86_64/0.14.2/x86_64/ ```toml # in Cargo.toml [dependencies] -x86_64 = "0.13.2" +x86_64 = "0.14.2" ``` これで、このクレートによって提供される[`Port`]型を使って`exit_qemu`関数を作ることができます。 -[`Port`]: https://docs.rs/x86_64/0.13.2/x86_64/instructions/port/struct.Port.html +[`Port`]: https://docs.rs/x86_64/0.14.2/x86_64/instructions/port/struct.Port.html ```rust // in src/main.rs diff --git a/blog/content/edition-2/posts/04-testing/index.md b/blog/content/edition-2/posts/04-testing/index.md index 3b0e501c..94b8d4ec 100644 --- a/blog/content/edition-2/posts/04-testing/index.md +++ b/blog/content/edition-2/posts/04-testing/index.md @@ -6,6 +6,7 @@ date = 2019-04-27 [extra] chapter = "Bare Bones" +comments_search_term = 1009 +++ This post explores unit and integration testing in `no_std` executables. We will use Rust's support for custom test frameworks to execute test functions inside our kernel. To report the results out of QEMU, we will use different features of QEMU and the `bootimage` tool. @@ -16,6 +17,7 @@ This blog is openly developed on [GitHub]. If you have any problems or questions [GitHub]: https://github.com/phil-opp/blog_os [at the bottom]: #comments + [post branch]: https://github.com/phil-opp/blog_os/tree/post-04 @@ -174,18 +176,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.13.2/x86_64/ +[`x86_64`]: https://docs.rs/x86_64/0.14.2/x86_64/ ```toml # in Cargo.toml [dependencies] -x86_64 = "0.13.2" +x86_64 = "0.14.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.13.2/x86_64/instructions/port/struct.Port.html +[`Port`]: https://docs.rs/x86_64/0.14.2/x86_64/instructions/port/struct.Port.html ```rust // in src/main.rs diff --git a/blog/content/edition-2/posts/04-testing/index.zh-CN.md b/blog/content/edition-2/posts/04-testing/index.zh-CN.md index 9231740c..a343d67a 100644 --- a/blog/content/edition-2/posts/04-testing/index.zh-CN.md +++ b/blog/content/edition-2/posts/04-testing/index.zh-CN.md @@ -19,6 +19,7 @@ translators = ["luojia65", "Rustin-Liu"] [GitHub]: https://github.com/phil-opp/blog_os [at the bottom]: #comments + [post branch]: https://github.com/phil-opp/blog_os/tree/post-04 @@ -173,18 +174,18 @@ test-args = ["-device", "isa-debug-exit,iobase=0xf4,iosize=0x04"] 这里我们使用 [`x86_64`] crate提供的抽象,而不是手动调用`in`或`out`指令。为了添加对该crate的依赖,我们可以将其添加到我们的 `Cargo.toml`中的 `dependencies` 小节中去: -[`x86_64`]: https://docs.rs/x86_64/0.7.5/x86_64/ +[`x86_64`]: https://docs.rs/x86_64/0.14.2/x86_64/ ```toml # in Cargo.toml [dependencies] -x86_64 = "0.11.0" +x86_64 = "0.14.2" ``` 现在我们可以使用crate中提供的[`Port`] 类型来创建一个`exit_qemu` 函数了: -[`Port`]: https://docs.rs/x86_64/0.7.0/x86_64/instructions/port/struct.Port.html +[`Port`]: https://docs.rs/x86_64/0.14.2/x86_64/instructions/port/struct.Port.html ```rust // in src/main.rs diff --git a/blog/content/edition-2/posts/05-cpu-exceptions/index.fa.md b/blog/content/edition-2/posts/05-cpu-exceptions/index.fa.md index 85480d19..91ba4c3e 100644 --- a/blog/content/edition-2/posts/05-cpu-exceptions/index.fa.md +++ b/blog/content/edition-2/posts/05-cpu-exceptions/index.fa.md @@ -23,6 +23,7 @@ rtl = true [گیت‌هاب]: https://github.com/phil-opp/blog_os [در زیر]: #comments + [post branch]: https://github.com/phil-opp/blog_os/tree/post-05 @@ -90,7 +91,7 @@ Bits | Name | Description به جای ایجاد نوع IDT خود ، از [ساختمان `InterruptDescriptorTable`] کرت `x86_64` استفاده خواهیم کرد که به این شکل است: -[ساختمان `InterruptDescriptorTable`]: https://docs.rs/x86_64/0.13.2/x86_64/structures/idt/struct.InterruptDescriptorTable.html +[ساختمان `InterruptDescriptorTable`]: https://docs.rs/x86_64/0.14.2/x86_64/structures/idt/struct.InterruptDescriptorTable.html ``` rust #[repr(C)] @@ -121,15 +122,15 @@ pub struct InterruptDescriptorTable { فیلدها از نوع [` src/main.rs:53:1 | -53 | / extern "x86-interrupt" fn breakpoint_handler(stack_frame: &mut InterruptStackFrame) { +53 | / extern "x86-interrupt" fn breakpoint_handler(stack_frame: InterruptStackFrame) { 54 | | println!("EXCEPTION: BREAKPOINT\n{:#?}", stack_frame); 55 | | } | |_^ @@ -283,7 +284,7 @@ error[E0658]: x86-interrupt ABI is experimental and subject to change (see issue برای اینکه پردازنده از جدول توصیف کننده وقفه جدید ما استفاده کند ، باید آن را با استفاده از دستورالعمل [`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 +[InterruptDescriptorTable::load]: https://docs.rs/x86_64/0.14.2/x86_64/structures/idt/struct.InterruptDescriptorTable.html#method.load ```rust // in src/interrupts.rs @@ -462,7 +463,7 @@ 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 +[`InterruptDescriptorTable`]: https://docs.rs/x86_64/0.14.2/x86_64/structures/idt/struct.InterruptDescriptorTable.html [نسخه اول]: @/edition-1/_index.md ## مرحله بعدی چیست؟ diff --git a/blog/content/edition-2/posts/05-cpu-exceptions/index.ja.md b/blog/content/edition-2/posts/05-cpu-exceptions/index.ja.md new file mode 100644 index 00000000..41faf2fd --- /dev/null +++ b/blog/content/edition-2/posts/05-cpu-exceptions/index.ja.md @@ -0,0 +1,471 @@ ++++ +title = "CPU例外" +weight = 5 +path = "ja/cpu-exceptions" +date = 2018-06-17 + +[extra] +chapter = "Interrupts" +# Please update this when updating the translation +translation_based_on_commit = "a8a6b725cff2e485bed76ff52ac1f18cec08cc7b" +# GitHub usernames of the people that translated this post +translators = ["woodyZootopia"] ++++ + +CPU例外は、例えば無効なメモリアドレスにアクセスしたときやゼロ除算したときなど、様々なミスによって発生します。それらに対処するために、ハンドラ関数を提供する **割り込み記述子表 (interrupt descriptor table) ** を設定しなくてはなりません。この記事を読み終わる頃には、私達のカーネルは[ブレークポイント例外][breakpoint exceptions]を捕捉し、その後通常の実行を継続できるようになっているでしょう。 + +[breakpoint exceptions]: https://wiki.osdev.org/Exceptions#Breakpoint + + + +このブログの内容は [GitHub] 上で公開・開発されています。何か問題や質問などがあれば issue をたててください (訳注: リンクは原文(英語)のものになります)。また[こちら][at the bottom]にコメントを残すこともできます。この記事の完全なソースコードは[`post-05` ブランチ][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-05 + + + +## 概要 +例外とは、今実行している命令はなにかおかしいぞ、ということを示すものです。例えば、現在の命令がゼロ除算を実行しようとしているとき、CPUは例外を発します。例外が起こると、CPUは現在行われている作業に割り込み、例外の種類に従って、即座に特定の例外ハンドラ関数を呼びます。 + +x86には20種類のCPU例外があります。中でも重要なものは: + +- **ページフォルト (Page Fault) **: ページフォルトは不正なメモリアクセスの際に発生します。例えば、現在の命令がマップされていないページから読み込もうとしたり、読み込み専用のページに書き込もうとしたときに生じます。 +- **無効な (Invalid) 命令コード (Opcode) **: この例外は現在の命令が無効であるときに発生します。例えば、[SSE命令][SSE instructions]という新しい命令をサポートしていない旧式のCPU上でこれを実行しようとしたときに生じます。 +- **一般保護違反 (General Protection Fault) **: これは、例外の中でも、最もいろいろな理由で発生しうるものです。ユーザーレベルのコードで特権命令 (privileged instruction) を実行しようとしたときや、設定レジスタの保護領域に書き込もうとしたときなど、様々な種類のアクセス違反によって生じます。 +- **ダブルフォルト (Double Fault) **: 何らかの例外が起こったとき、CPUは対応するハンドラ関数を呼び出そうとします。 この例外ハンドラを **呼び出している間に** 別の例外が起こった場合、CPUはダブルフォルト例外を出します。この例外はまた、ある例外に対してハンドラ関数が登録されていないときにも起こります。 +- **トリプルフォルト (Triple Fault) **: CPUがダブルフォルトのハンドラ関数を呼び出そうとしている間に例外が発生すると、CPUは **トリプルフォルト** という致命的な例外を発します。トリプルフォルトを捕捉したり処理したりすることはできません。これが起こると、多くのプロセッサは自らをリセットしてOSを再起動することで対応します。 + +[SSE instructions]: https://en.wikipedia.org/wiki/Streaming_SIMD_Extensions + +例外の完全な一覧を見たい場合は、[OSDev wiki][exceptions]を見てください。 + +[exceptions]: https://wiki.osdev.org/Exceptions + +### 割り込み記述子表 +例外を捕捉し処理するためには、いわゆる割り込み記述子表 (Interrupt Descriptor Table, IDT) を設定しないといけません。この表にそれぞれのCPU例外に対するハンドラ関数を指定することができます。ハードウェアはこの表を直接使うので、決められたフォーマットに従わないといけません。それぞれのエントリは以下の16バイトの構造を持たなければなりません: + +型 | 名前 | 説明 +----|--------------------------|----------------------------------- +u16 | 関数ポインタ [0:15] | ハンドラ関数へのポインタの下位ビット。 +u16 | GDTセレクタ | [大域記述子表 (Global Descriptor Table)][global descriptor table] におけるコードセグメントのセレクタ。 +u16 | オプション | (下を参照) +u16 | 関数ポインタ [16:31] | ハンドラ関数へのポインタの中位ビット。 +u32 | 関数ポインタ [32:63] | ハンドラ関数へのポインタの上位ビット。 +u32 | 予約済 | + +[global descriptor table]: https://en.wikipedia.org/wiki/Global_Descriptor_Table + +オプション部は以下のフォーマットになっています: + +ビット | 名前 | 説明 +--------|------------------------------------------------------------------------------------------------|------------------------------------- +0-2 | 割り込みスタックテーブルインデックス | 0ならスタックを変えない。1から7なら、ハンドラが呼ばれたとき、割り込みスタック表のその数字のスタックに変える。 +3-7 | 予約済 | +8 | 0: 割り込みゲート、1: トラップゲート | 0なら、このハンドラが呼ばれたとき割り込みは無効化される。 +9-11 | 1にしておかないといけない | +12 | 0にしておかないといけない | +13‑14 | 記述子の特権レベル (Descriptor Privilege Level) (DPL) | このハンドラを呼ぶ際に必要になる最低限の特権レベル。 +15 | Present | + +それぞれの例外がIDTの何番目に対応するかは事前に定義されています。例えば、「無効な命令コード」の例外は6番目で、「ページフォルト」例外は14番目です。これにより、ハードウェアがそれぞれの例外に対応するIDTの設定を(特に設定の必要なく)自動的に読み出せるというわけです。OSDev wikiの[「例外表」][exceptions]の "Vector nr." 列に、すべての例外についてIDTの何番目かが記されています。 + +例外が起こると、ざっくりCPUは以下のことを行います: + +1. 命令ポインタと[RFLAGS]レジスタ(これらの値は後で使います)を含むレジスタをスタックにプッシュする。 +2. 割り込み記述子表から対応するエントリを読む。例えば、ページフォルトが起こったときはCPUは14番目のエントリを読む。 +3. エントリが存在しているのかチェックする。そうでなければダブルフォルトを起こす。 +4. エントリが割り込みゲートなら(40番目のビットが0なら)ハードウェア割り込みを無効にする。 +5. 指定された[GDT]セレクタをCSセグメントに読み込む。 +6. 指定されたハンドラ関数にジャンプする。 + +[RFLAGS]: https://en.wikipedia.org/wiki/FLAGS_register +[GDT]: https://en.wikipedia.org/wiki/Global_Descriptor_Table + +ステップ4と5について今深く考える必要はありません。今後の記事で大域記述子表 (Global Descriptor Table, 略してGDT) とハードウェア割り込みについては学んでいきます。 + +## IDT型 +自前でIDTの型を作る代わりに、`x86_64`クレートの[`InterruptDescriptorTable`構造体][`InterruptDescriptorTable` struct]を使います。こんな見た目をしています: + +[`InterruptDescriptorTable` struct]: https://docs.rs/x86_64/0.14.2/x86_64/structures/idt/struct.InterruptDescriptorTable.html + +``` rust +#[repr(C)] +pub struct InterruptDescriptorTable { + pub divide_by_zero: Entry, + pub debug: Entry, + pub non_maskable_interrupt: Entry, + pub breakpoint: Entry, + pub overflow: Entry, + pub bound_range_exceeded: Entry, + pub invalid_opcode: Entry, + pub device_not_available: Entry, + pub double_fault: Entry, + pub invalid_tss: Entry, + pub segment_not_present: Entry, + pub stack_segment_fault: Entry, + pub general_protection_fault: Entry, + pub page_fault: Entry, + pub x87_floating_point: Entry, + pub alignment_check: Entry, + pub machine_check: Entry, + pub simd_floating_point: Entry, + pub virtualization: Entry, + pub security_exception: Entry, + // いくつかのフィールドは省略している +} +``` + +この構造体のフィールドは[`idt::Entry`]という型を持っています。これはIDTのエントリのフィールド(上の表を見てください)を表す構造体です。型パラメータ`F`は、期待されるハンドラ関数の型を表します。エントリの中には、[`HandlerFunc`]型を要求するものや、[`HandlerFuncWithErrCode`]型を要求するものがあることがわかります。ページフォルトに至っては、[`PageFaultHandlerFunc`]という自分専用の型を要求していますね。 + +[`idt::Entry`]: https://docs.rs/x86_64/0.14.2/x86_64/structures/idt/struct.Entry.html +[`HandlerFunc`]: https://docs.rs/x86_64/0.14.2/x86_64/structures/idt/type.HandlerFunc.html +[`HandlerFuncWithErrCode`]: https://docs.rs/x86_64/0.14.2/x86_64/structures/idt/type.HandlerFuncWithErrCode.html +[`PageFaultHandlerFunc`]: https://docs.rs/x86_64/0.14.2/x86_64/structures/idt/type.PageFaultHandlerFunc.html + +まず`HandlerFunc`型を見てみましょう: + +```rust +type HandlerFunc = extern "x86-interrupt" fn(_: InterruptStackFrame); +``` + +これは、`extern "x86-interrupt" fn`型への[型エイリアス][type alias]です。`extern`は[外部呼び出し規約][foreign calling convention]に従う関数を定義するのに使われ、おもに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 +[foreign calling convention]: https://doc.rust-lang.org/nomicon/ffi.html#foreign-calling-conventions + +## 例外の呼び出し規約 +例外は関数呼び出しと非常に似ています。CPUが呼び出された関数の最初の命令にジャンプし、それを実行します。その後、CPUはリターンアドレスにジャンプし、親関数の実行を続けます。 + +しかし、例外と関数呼び出しには大きな違いが一つあるのです:関数呼び出しはコンパイラによって挿入された`call`命令によって自発的に引き起こされますが、例外は **どんな命令の実行中でも** 起こる可能性があるのです。この違いの結果を理解するためには、関数呼び出しについてより詳しく見ていく必要があります。 + +[呼び出し規約][Calling conventions]は関数呼び出しについて事細かく指定しています。例えば、関数のパラメータがどこに置かれるべきか(例えば、レジスタなのかスタックなのか)や、結果がどのように返されるべきかを指定しています。x86_64上のLinuxでは、C言語の関数に関しては以下のルールが適用されます(これは[System V ABI]で指定されています): + +[Calling conventions]: https://en.wikipedia.org/wiki/Calling_convention +[System V ABI]: https://refspecs.linuxbase.org/elf/x86_64-abi-0.99.pdf + +- 最初の6つの整数引数は、レジスタ`rdi`, `rsi`, `rdx`, `rcx`, `r8`, `r9`で渡される +- 追加の引数はスタックで渡される +- 結果は`rax`と`rdx`で返される + +注意してほしいのは、RustはC言語のABIに従っていない(実は、[RustにはABIすらまだありません][rust abi])ので、このルールは`extern "C" fn`と宣言された関数にしか適用されないということです。 + +[rust abi]: https://github.com/rust-lang/rfcs/issues/600 + +### PreservedレジスタとScratchレジスタ +呼び出し規約はレジスタを2種類に分けています:preserved (保存) レジスタとscratch (下書き) レジスタです。 + +preservedレジスタの値は関数呼び出しの前後で変化してはいけません。ですので、呼び出された関数(訳注:callの受け身で"callee"と呼ばれます)は、リターンする前にその値をもとに戻す場合に限り、その値を上書きできます。そのため、これらのレジスタはcallee-saved (呼び出し先によって保存される) と呼ばれます。よくとられる方法は、関数の最初でそのレジスタをスタックに保存し、リターンする直前にその値をもとに戻すことです。 + +それとは対照的に、呼び出された関数はscratchレジスタを何の制限もなく上書きすることができます。呼び出し元の関数がscratchレジスタの値を関数呼び出しの前後で保存したいなら、関数呼び出しの前に自分で(スタックにプッシュするなどして)バックアップしておいて、もとに戻す必要があります。なので、scratchレジスタはcaller-saved (呼び出し元によって保存される) です。 + +x86_64においては、C言語の呼び出し規約は以下のpreservedレジスタとscratchレジスタを指定します: + +preservedレジスタ | scratchレジスタ +--- | --- +`rbp`, `rbx`, `rsp`, `r12`, `r13`, `r14`, `r15` | `rax`, `rcx`, `rdx`, `rsi`, `rdi`, `r8`, `r9`, `r10`, `r11` +_callee-saved_ | _caller-saved_ + +コンパイラはこれらのルールを知っているので、それにしたがってコードを生成します。例えば、ほとんどの関数は`push rbp`から始まるのですが、これは`rbp`をスタックにバックアップしているのです(`rbp`はcallee-savedなレジスタであるため)。 + +### すべてのレジスタを保存する +関数呼び出しとは対象的に、例外は **どんな命令の最中にも** 起きる可能性があります。多くの場合、生成されたコードが例外を引き起こすのかどうかは、コンパイル時には見当も付きません。例えば、コンパイラはある命令がスタックオーバーフローやページフォルトを起こすのか知ることができません。 + +いつ例外が起きるのかわからない以上、レジスタを事前にバックアップしておくことは不可能です。つまり、caller-savedレジスタを利用する呼び出し規約は、例外ハンドラには使えないということです。代わりに、 **すべてのレジスタを** 保存する規約を使わないといけません。`x86-interrupt`呼び出し規約はそのような呼び出し規約なので、関数が戻るときにすべてのレジスタが元の値に戻されることを保証してくれるというわけです。 + +これは、関数の初めにすべてのレジスタがスタックに保存されるということを意味しないことに注意してください。その代わりに、コンパイラは関数によって上書きされてしまうレジスタのみをバックアップします。こうすれば、数個のレジスタしか使わない短い関数に対して、とても効率的なコードが生成できるでしょう。 + +### 割り込み時のスタックフレーム +通常の関数呼び出し(`call`命令を使います)においては、CPUは対象の関数にジャンプする前にリターンアドレスをプッシュします。関数がリターンするとき(`ret`命令を使います)、CPUはこのリターンアドレスをポップし、そこにジャンプします。そのため、通常の関数呼び出しの際のスタックフレームは以下のようになっています: + +![function stack frame](function-stack-frame.svg) + +しかし、例外と割り込みハンドラについては、リターンアドレスをプッシュするだけではだめです。なぜなら、割り込みハンドラはしばしば(スタックポインタや、CPUフラグなどが)異なる状況で実行されるからです。ですので、代わりに、CPUは割り込みが起こると以下の手順を実行します。 + +1. **スタックポインタをアラインする**: 割り込みはあらゆる命令において発生しうるので、スタックポインタもあらゆる値を取る可能性があります。しかし、CPU命令のうちいくつか(例えばSSE命令の一部など)はスタックポインタが16バイトの倍数になっていることを要求するので、そうなるようにCPUは割り込みの直後にスタックポインタを揃え (アラインし) ます。 +2. (場合によっては)**スタックを変更する**: スタックの変更は、例えばCPU例外がユーザーモードのプログラムで起こった場合のような、CPUの特権レベルを変更するときに起こります。いわゆる割り込みスタック表 (Interrupt Stack Table) を使うことで、特定の割り込みに対しスタックを変更するよう設定することも可能です。割り込みスタック表については次の記事で説明します。 +3. **古いスタックポインタをプッシュする**: CPUは、割り込みが発生した際の(アラインされる前の)スタックポインタレジスタ(`rsp`)とスタックセグメントレジスタ(`ss`)の値をプッシュします。これにより、割り込みハンドラからリターンしてきたときにもとのスタックポインタを復元することが可能になります。 +4. **`RFLAGS`レジスタをプッシュして更新する**: [`RFLAGS`]レジスタは状態や制御のための様々なビットを保持しています。割り込みに入るとき、CPUはビットのうちいくつかを変更し古い値をプッシュしておきます。 +5. **命令ポインタをプッシュする**: 割り込みハンドラ関数にジャンプする前に、CPUは命令ポインタ(`rip`)とコードセグメント(`cs`)をプッシュします。これは通常の関数呼び出しにおける戻り値のプッシュに対応します。 +6. (例外によっては)**エラーコードをプッシュする**: ページフォルトのような特定の例外の場合、CPUはエラーコードをプッシュします。これは、例外の原因を説明するものです。 +7. **割り込みハンドラを呼び出す**: CPUは割り込みハンドラ関数のアドレスとセグメント記述子 (segment descriptor) をIDTの対応するフィールドから読み出します。そして、この値を`rip`と`cs`レジスタに書き出してから、ハンドラを呼び出します。 + +[`RFLAGS`]: https://en.wikipedia.org/wiki/FLAGS_register + +ですので、割り込み時のスタックフレーム (interrupt stack frame) は以下のようになります: + +![interrupt stack frame](exception-stack-frame.svg) + +`x86_64`クレートにおいては、割り込み時のスタックフレームは[`InterruptStackFrame`]構造体によって表現されます。これは割り込みハンドラに`&mut`として渡されるため、これを使うことで例外の原因に関して追加で情報を手に入れることができます。例外のすべてがエラーコードをプッシュするわけではないので、この構造体にはエラーコードのためのフィールドはありません。これらの例外は[`HandlerFuncWithErrCode`]という別の関数型を使いますが、これらは追加で`error_code`引数を持ちます。 + +[`InterruptStackFrame`]: https://docs.rs/x86_64/0.14.2/x86_64/structures/idt/struct.InterruptStackFrame.html + +### 舞台裏では何が +`x86-interrupt`呼び出し規約は、この例外処理 (ハンドル) プロセスのややこしいところをほぼ全て隠蔽してくれる、強力な抽象化です。しかし、その後ろで何が起こっているのかを知っておいたほうが良いこともあるでしょう。以下に、`x86-interrupt`呼び出し規約がやってくれることを簡単なリストにして示しました。 + +- **引数を取得する**: 多くの呼び出し規約においては、引数はレジスタを使って渡されることを想定しています。例外ハンドラにおいては、スタックにバックアップする前にレジスタの値を上書きしてはいけないので、これは不可能です。その代わり、`x86-interrupt`呼び出し規約は、引数が既に特定のオフセットでスタック上にあることを認識しています。 +- **`iretq`を使ってリターンする**: 割り込み時のスタックフレームは通常の関数呼び出しのスタックフレームとは全く異なるため、通常の `ret` 命令を使ってハンドラ関数から戻ることはできません。その代わりに、`iretq` 命令を使う必要があります。 +- **エラーコードを処理する**: いくつかの例外の場合、エラーコードがプッシュされるのですが、これが状況をより複雑にします。エラーコードはスタックのアラインメントを変更し(次の箇条を参照)、リターンする前にスタックからポップされる必要があるのです。`x86-interrupt`呼び出し規約は、このややこしい仕組みをすべて処理してくれます。しかし、どのハンドラ関数がどの例外に使われているかは呼び出し規約側にはわからないので、関数の引数の数からその情報を推測する必要があります。つまり、プログラマはやはりそれぞれの例外に対して正しい関数型を使う責任があるということです。幸いにも、`x86_64`クレートで定義されている`InterruptDescriptorTable`型が、正しい関数型が確実に使われるようにしてくれます。 +- **スタックをアラインする**: 一部の命令(特にSSE命令)には、16バイトのスタックアラインメントを必要とするものがあります。CPUは例外が発生したときには必ずこのようにスタックが整列 (アライン) されることを保証しますが、例外の中には、エラーコードをプッシュして再びスタックの整列を壊してしまうものもあります。この場合、`x86-interrupt`の呼び出し規約は、スタックを再整列させることでこの問題を解決します。 + +もしより詳しく知りたい場合は、例外の処理について[naked function][naked functions]を使って説明する一連の記事があります。[この記事の最下部][too-much-magic]にそこへのリンクがあります。 + +[naked functions]: https://github.com/rust-lang/rfcs/blob/master/text/1201-naked-fns.md +[too-much-magic]: #sasuganijian-dan-sugi + +## 実装 +理屈は理解したので、私達のカーネルでCPUの例外を実際に処理していきましょう。まず、`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(); +} +``` + +これで、ハンドラ関数を追加していくことができます。まず、[ブレークポイント例外][breakpoint exception]のハンドラを追加するところから始めましょう。ブレークポイント例外は、例外処理のテストをするのにうってつけの例外なのです。この例外の唯一の目的は、ブレークポイント命令`int3`が実行された時、プログラムを一時停止させることです。 + +[breakpoint exception]: https://wiki.osdev.org/Exceptions#Breakpoint + +ブレークポイント例外はよくデバッガによって使われます。ユーザーがブレークポイントを設定すると、デバッガが対応する命令を`int3`命令で置き換え、その行に到達したときにCPUがブレークポイント例外を投げるようにするのです。ユーザがプログラムを続行したい場合は、デバッガは`int3`命令をもとの命令に戻してプログラムを再開します。より詳しく知るには、[How debuggers work]["_How debuggers work_"]というシリーズ記事を読んでください。 + +["_How debuggers work_"]: 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: 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: InterruptStackFrame) { +54 | | println!("EXCEPTION: BREAKPOINT\n{:#?}", stack_frame); +55 | | } + | |_^ + | + = help: add #![feature(abi_x86_interrupt)] to the crate attributes to enable +``` + +このエラーは、`x86-interrupt`呼び出し規約がまだ不安定なために発生します。これを強制的に使うためには、`lib.rs`の最初に`#![feature(abi_x86_interrupt)]`を追記して、この機能を明示的に有効化してやる必要があります。 + +### IDTを読み込む +CPUがこの割り込みディスクリプタテーブル(IDT)を使用するためには、[`lidt`]命令を使ってこれを読み込む必要があります。`x86_64`の`InterruptDescriptorTable`構造体には、そのための[`load`][InterruptDescriptorTable::load]というメソッド関数が用意されています。それを使ってみましょう: + +[`lidt`]: https://www.felixcloutier.com/x86/lgdt:lidt +[InterruptDescriptorTable::load]: https://docs.rs/x86_64/0.14.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`メソッドは(`idt`に)`&'static self`、つまりプログラムの実行されている間ずっと有効な参照を期待しています。これは、私達が別のIDTを読み込まない限り、CPUは割り込みのたびにこの表にアクセスするからです。そのため、`'static`より短いライフタイムの場合、use-after-free (メモリ解放後にアクセス) バグが発生する可能性があります。 + +実際、これはまさにここで起こっていることです。私達の`idt`はスタック上に生成されるので、`init`関数の中でしか有効ではないのです。この関数が終わると、このスタックメモリは他の関数に使い回されるので、CPUはどこかもわからないスタックメモリをIDTとして解釈してしまうのです。幸運にも、`InterruptDescriptorTable::load`メソッドは関数定義にこのライフタイムの要件を組み込んでいるので、Rustコンパイラはこのバグをコンパイル時に未然に防ぐことができたというわけです。 + +この問題を解決するには、`idt`を`'static`なライフタイムの場所に格納する必要があります。これを達成するには、[`Box`]を使ってIDTをヒープに割当て、続いてそれを`'static`な参照に変換すればよいです。しかし、私達はOSのカーネルを書いている途中であり、(まだ)ヒープを持っていません。 + +[`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(); +} +``` + +しかし、問題が発生します:staticは不変 (イミュータブル) なので、`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`はデータ競合を非常に起こしやすいので、アクセスするたびに[unsafeブロック][`unsafe` block]が必要になります。 + +[`unsafe` block]: https://doc.rust-lang.org/1.30.0/book/second-edition/ch19-01-unsafe-rust.html#unsafe-superpowers + +#### Lazy Staticsにおまかせ +幸いにも、例の`lazy_static`マクロが存在します。このマクロは`static`をコンパイル時に評価する代わりに、最初に参照されたときに初期化を行います。このため、初期化時にはほとんどすべてのことができ、実行時にのみ決定する値を読み込むこともできます。 + +[VGAテキストバッファの抽象化をした][vga text buffer lazy static]ときに、すでに`lazy_static`クレートはインポートしました。そのため、すぐに`lazy_static!`マクロを使って静的なIDTを作ることができます。 + +[vga text buffer lazy static]: @/edition-2/posts/03-vga-text-buffer/index.ja.md#dai-keta-lazy-jing-de-bian-shu +```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`を使ってはいるのですが、これは安全なインターフェースの中に抽象化されているのです。 + +### 実行する + +カーネルで例外を動作させるための最後のステップは、`main.rs`から`init_idt`関数を呼び出すことです。直接呼び出す代わりに、より一般的な`init`関数を`lib.rs`に導入します: + +```rust +// in src/lib.rs + +pub fn init() { + interrupts::init_idt(); +} +``` + +この関数により、`main.rs`、`lib.rs`および結合テストにおける、異なる`_start`関数で共有される、初期化ルーチンの「中央広場」ができました。 + +`main.rs`内の`_start`関数を更新して、`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 {} +} +``` + +(`cargo run`を使って)QEMU内でこれを実行すると、以下のようになります + +![QEMU printing `EXCEPTION: BREAKPOINT` and the interrupt stack frame](qemu-breakpoint-exception.png) + +うまくいきました!CPUは私達のブレークポイントハンドラを呼び出すのに成功し、これがメッセージを出力し、そのあと`_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 {} +} +``` + +Rustのテストでは、`main.rs`とは全く無関係に`lib.rs`をテストするので、この`_start`関数は`cargo test --lib`を実行する際に使用されることを思い出してください。テストを実行する前に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`]型のおかげで、例外処理のプロセスは比較的わかりやすく、面倒なところはありませんでした。「これではさすがに簡単すぎる、例外処理の闇をすべて学び尽くしたい」というあなた向けの記事もあります:私達の[Handling Exceptions with Naked Functions][“Handling Exceptions with Naked Functions”]シリーズ(未訳)では、`x86-interrupt`呼び出し規約を使わずに例外を処理する方法を学び、さらには独自のIDT型を定義します。`x86-interrupt`呼び出し規約や、`x86_64`クレートが存在する前は、これらの記事が主な例外処理に関する記事でした。なお、これらの記事はこのブログの[第1版][first edition]をもとにしているので、内容が古くなっている可能性があることに注意してください。 + +[“Handling Exceptions with Naked Functions”]: @/edition-1/extra/naked-exceptions/_index.md +[`InterruptDescriptorTable`]: https://docs.rs/x86_64/0.14.2/x86_64/structures/idt/struct.InterruptDescriptorTable.html +[first edition]: @/edition-1/_index.md + +## 次は? +例外を捕捉し、そこから戻ってくることに成功しました!次のステップは、すべての例外を捕捉できるようにすることです。なぜなら、補足されなかった例外は致命的な[トリプルフォルト][triple fault]を引き起こし、これはシステムリセットにつながってしまうからです。次の記事では、[ダブルフォルト][double faults]を正しく捕捉することで、これを回避できることを説明します。 + +[triple fault]: https://wiki.osdev.org/Triple_Fault +[double faults]: https://wiki.osdev.org/Double_Fault#Double_Fault diff --git a/blog/content/edition-2/posts/05-cpu-exceptions/index.md b/blog/content/edition-2/posts/05-cpu-exceptions/index.md index 5d68b59c..4bf60143 100644 --- a/blog/content/edition-2/posts/05-cpu-exceptions/index.md +++ b/blog/content/edition-2/posts/05-cpu-exceptions/index.md @@ -18,6 +18,7 @@ This blog is openly developed on [GitHub]. If you have any problems or questions [GitHub]: https://github.com/phil-opp/blog_os [at the bottom]: #comments + [post branch]: https://github.com/phil-opp/blog_os/tree/post-05 @@ -84,7 +85,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.13.2/x86_64/structures/idt/struct.InterruptDescriptorTable.html +[`InterruptDescriptorTable` struct]: https://docs.rs/x86_64/0.14.2/x86_64/structures/idt/struct.InterruptDescriptorTable.html ``` rust #[repr(C)] @@ -115,15 +116,15 @@ pub struct InterruptDescriptorTable { The fields have the type [`idt::Entry`], which is a struct that represents the fields of an IDT entry (see the table above). The type parameter `F` defines the expected handler function type. We see that some entries require a [`HandlerFunc`] and some entries require a [`HandlerFuncWithErrCode`]. The page fault even has its own special type: [`PageFaultHandlerFunc`]. -[`idt::Entry`]: https://docs.rs/x86_64/0.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 +[`idt::Entry`]: https://docs.rs/x86_64/0.14.2/x86_64/structures/idt/struct.Entry.html +[`HandlerFunc`]: https://docs.rs/x86_64/0.14.2/x86_64/structures/idt/type.HandlerFunc.html +[`HandlerFuncWithErrCode`]: https://docs.rs/x86_64/0.14.2/x86_64/structures/idt/type.HandlerFuncWithErrCode.html +[`PageFaultHandlerFunc`]: https://docs.rs/x86_64/0.14.2/x86_64/structures/idt/type.PageFaultHandlerFunc.html Let's look at the `HandlerFunc` type first: ```rust -type HandlerFunc = extern "x86-interrupt" fn(_: &mut InterruptStackFrame); +type HandlerFunc = extern "x86-interrupt" fn(_: InterruptStackFrame); ``` It's a [type alias] for an `extern "x86-interrupt" fn` type. The `extern` keyword defines a function with a [foreign calling convention] and is often used to communicate with C code (`extern "C" fn`). But what is the `x86-interrupt` calling convention? @@ -180,7 +181,7 @@ On a normal function call (using the `call` instruction), the CPU pushes the ret For exception and interrupt handlers, however, pushing a return address would not suffice, since interrupt handlers often run in a different context (stack pointer, CPU flags, etc.). Instead, the CPU performs the following steps when an interrupt occurs: 1. **Aligning the stack pointer**: An interrupt can occur at any instructions, so the stack pointer can have any value, too. However, some CPU instructions (e.g. some SSE instructions) require that the stack pointer is aligned on a 16 byte boundary, therefore the CPU performs such an alignment right after the interrupt. -2. **Switching stacks** (in some cases): A stack switch occurs when the CPU privilege level changes, for example when a CPU exception occurs in an user mode program. It is also possible to configure stack switches for specific interrupts using the so-called _Interrupt Stack Table_ (described in the next post). +2. **Switching stacks** (in some cases): A stack switch occurs when the CPU privilege level changes, for example when a CPU exception occurs in a user mode program. It is also possible to configure stack switches for specific interrupts using the so-called _Interrupt Stack Table_ (described in the next post). 3. **Pushing the old stack pointer**: The CPU pushes the values of the stack pointer (`rsp`) and the stack segment (`ss`) registers at the time when the interrupt occurred (before the alignment). This makes it possible to restore the original stack pointer when returning from an interrupt handler. 4. **Pushing and updating the `RFLAGS` register**: The [`RFLAGS`] register contains various control and status bits. On interrupt entry, the CPU changes some bits and pushes the old value. 5. **Pushing the instruction pointer**: Before jumping to the interrupt handler function, the CPU pushes the instruction pointer (`rip`) and the code segment (`cs`). This is comparable to the return address push of a normal function call. @@ -195,7 +196,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.13.2/x86_64/structures/idt/struct.InterruptStackFrame.html +[`InterruptStackFrame`]: https://docs.rs/x86_64/0.14.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: @@ -249,7 +250,7 @@ pub fn init_idt() { } extern "x86-interrupt" fn breakpoint_handler( - stack_frame: &mut InterruptStackFrame) + stack_frame: InterruptStackFrame) { println!("EXCEPTION: BREAKPOINT\n{:#?}", stack_frame); } @@ -263,7 +264,7 @@ When we try to compile it, the following error occurs: 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) { +53 | / extern "x86-interrupt" fn breakpoint_handler(stack_frame: InterruptStackFrame) { 54 | | println!("EXCEPTION: BREAKPOINT\n{:#?}", stack_frame); 55 | | } | |_^ @@ -277,7 +278,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.13.2/x86_64/structures/idt/struct.InterruptDescriptorTable.html#method.load +[InterruptDescriptorTable::load]: https://docs.rs/x86_64/0.14.2/x86_64/structures/idt/struct.InterruptDescriptorTable.html#method.load ```rust // in src/interrupts.rs @@ -457,7 +458,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.13.2/x86_64/structures/idt/struct.InterruptDescriptorTable.html +[`InterruptDescriptorTable`]: https://docs.rs/x86_64/0.14.2/x86_64/structures/idt/struct.InterruptDescriptorTable.html [first edition]: @/edition-1/_index.md ## What's next? diff --git a/blog/content/edition-2/posts/06-double-faults/index.fa.md b/blog/content/edition-2/posts/06-double-faults/index.fa.md index 4ef866cd..05ca782d 100644 --- a/blog/content/edition-2/posts/06-double-faults/index.fa.md +++ b/blog/content/edition-2/posts/06-double-faults/index.fa.md @@ -21,6 +21,7 @@ rtl = true [گیت‌هاب]: https://github.com/phil-opp/blog_os [در زیر]: #comments + [post branch]: https://github.com/phil-opp/blog_os/tree/post-06 @@ -89,7 +90,7 @@ lazy_static! { // new extern "x86-interrupt" fn double_fault_handler( - stack_frame: &mut InterruptStackFrame, _error_code: u64) -> ! + stack_frame: InterruptStackFrame, _error_code: u64) -> ! { panic!("EXCEPTION: DOUBLE FAULT\n{:#?}", stack_frame); } @@ -242,7 +243,7 @@ I/O Map Base Address | `u16` بیایید یک TSS جدید ایجاد کنیم که شامل یک پشته خطای دوگانه جداگانه در جدول پشته وقفه خود باشد. برای این منظور ما به یک ساختار TSS نیاز داریم. خوشبختانه کریت `x86_64` از قبل حاوی [ساختار `TaskStateSegment`] است که می‌توانیم از آن استفاده کنیم. -[ساختار `TaskStateSegment`]: https://docs.rs/x86_64/0.13.2/x86_64/structures/tss/struct.TaskStateSegment.html +[ساختار `TaskStateSegment`]: https://docs.rs/x86_64/0.14.2/x86_64/structures/tss/struct.TaskStateSegment.html ما TSS را در یک ماژول جدید به نام `gdt` ایجاد می‌کنیم (نام این ماژول بعداً برای‌تان معنا پیدا می‌کند): @@ -391,8 +392,8 @@ pub fn init() { ما با استفاده از [`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 +[`set_cs`]: https://docs.rs/x86_64/0.14.2/x86_64/instructions/segmentation/fn.set_cs.html +[`load_tss`]: https://docs.rs/x86_64/0.14.2/x86_64/instructions/tables/fn.load_tss.html اکنون که یک TSS معتبر و جدول پشته‌ وقفه را بارگذاری کردیم، می‌توانیم اندیس پشته را برای کنترل کننده خطای دوگانه در IDT تنظیم کنیم: @@ -543,7 +544,7 @@ 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, + _stack_frame: InterruptStackFrame, _error_code: u64, ) -> ! { serial_println!("[ok]"); diff --git a/blog/content/edition-2/posts/06-double-faults/index.ja.md b/blog/content/edition-2/posts/06-double-faults/index.ja.md index 4e78ad82..fbce089c 100644 --- a/blog/content/edition-2/posts/06-double-faults/index.ja.md +++ b/blog/content/edition-2/posts/06-double-faults/index.ja.md @@ -20,15 +20,15 @@ translators = ["garasubo"] [GitHub]: https://github.com/phil-opp/blog_os [at the bottom]: #comments + [post branch]: https://github.com/phil-opp/blog_os/tree/post-06 ## ダブルフォルトとは -簡単に言うとダブルフォルトとはCPUが例外ハンドラを呼び出すことに失敗したときに起きる特別な例外です。例えば、ページフォルトが起きたが、ページフォルトハンドラが[割り込みディスクリプタテーブル][IDT](IDT: Interrupt Descriptor Table)(訳注: 翻訳当時、リンク先未訳)に登録されていないときに発生します。つまり、C++での`catch(...)`や、JavaやC#の`catch(Exception e)`ような、例外のあるプログラミング言語のcatch-allブロックのようなものです。 - -[IDT]: @/edition-2/posts/05-cpu-exceptions/index.md#the-interrupt-descriptor-table +簡単に言うとダブルフォルトとはCPUが例外ハンドラを呼び出すことに失敗したときに起きる特別な例外です。例えば、ページフォルトが起きたが、ページフォルトハンドラが[割り込みディスクリプタテーブル][IDT](IDT: Interrupt Descriptor Table)に登録されていないときに発生します。つまり、C++での`catch(...)`や、JavaやC#の`catch(Exception e)`ような、例外のあるプログラミング言語のcatch-allブロックのようなものです。 +[IDT]: @/edition-2/posts/05-cpu-exceptions/index.ja.md#ge-riip-miji-shu-zi-biao ダブルフォルトは通常の例外のように振る舞います。ベクター番号`8`を持ち、IDTに通常のハンドラ関数として定義できます。ダブルフォルトがうまく処理されないと、より重大な例外である**トリプルフォルト**が起きてしまうため、ダブルフォルトハンドラを設定することはとても重要です。トリプルフォルトはキャッチできず、ほとんどのハードウェアはシステムリセットを起こします。 ### ダブルフォルトを起こす @@ -85,7 +85,7 @@ lazy_static! { // new extern "x86-interrupt" fn double_fault_handler( - stack_frame: &mut InterruptStackFrame, _error_code: u64) -> ! + stack_frame: InterruptStackFrame, _error_code: u64) -> ! { panic!("EXCEPTION: DOUBLE FAULT\n{:#?}", stack_frame); } @@ -233,7 +233,7 @@ I/Oマップベースアドレス | `u16` ### TSSをつくる 割り込みスタックテーブルにダブルフォルト用のスタックを含めた新しいTSSをつくってみましょう。そのためにはTSS構造体が必要です。幸いにも、すでに`x86_64`クレートに[`TaskStateSegment`構造体]が含まれているので、これを使っていきます。 -[`TaskStateSegment`構造体]: https://docs.rs/x86_64/0.12.1/x86_64/structures/tss/struct.TaskStateSegment.html +[`TaskStateSegment`構造体]: https://docs.rs/x86_64/0.14.2/x86_64/structures/tss/struct.TaskStateSegment.html 新しい`gdt`モジュール内でTSSをつくります(名前の意味は後でわかるでしょう): @@ -379,8 +379,8 @@ pub fn init() { [`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 +[`set_cs`]: https://docs.rs/x86_64/0.14.2/x86_64/instructions/segmentation/fn.set_cs.html +[`load_tss`]: https://docs.rs/x86_64/0.14.2/x86_64/instructions/tables/fn.load_tss.html これで正常なTSSと割り込みスタックテーブルを読み込みこんだので、私達はIDT内のダブルフォルトハンドラにスタックインデックスをセットすることができます: @@ -489,7 +489,7 @@ fn stack_overflow() { しかし、ここではスタックオーバーフローを起こしたいので、コンパイラに削除されない、ダミーのvolatile読み込み文を関数の末尾に追加します。その結果、関数は**末尾再帰**ではなくなり、ループへの変換は防がれます。更に関数が無限に再帰することに対するコンパイラの警告をなくすために`allow(unconditional_recursion)`属性を追加します。 -### IDTのテスト +### IDTのテスト 上で述べたように、テストはカスタムしたダブルフォルトハンドラを含む専用のIDTが必要です。実装はこのようになります: @@ -530,7 +530,7 @@ 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, + _stack_frame: InterruptStackFrame, _error_code: u64, ) -> ! { serial_println!("[ok]"); diff --git a/blog/content/edition-2/posts/06-double-faults/index.md b/blog/content/edition-2/posts/06-double-faults/index.md index 54542733..02a13d01 100644 --- a/blog/content/edition-2/posts/06-double-faults/index.md +++ b/blog/content/edition-2/posts/06-double-faults/index.md @@ -16,6 +16,7 @@ This blog is openly developed on [GitHub]. If you have any problems or questions [GitHub]: https://github.com/phil-opp/blog_os [at the bottom]: #comments + [post branch]: https://github.com/phil-opp/blog_os/tree/post-06 @@ -81,7 +82,7 @@ lazy_static! { // new extern "x86-interrupt" fn double_fault_handler( - stack_frame: &mut InterruptStackFrame, _error_code: u64) -> ! + stack_frame: InterruptStackFrame, _error_code: u64) -> ! { panic!("EXCEPTION: DOUBLE FAULT\n{:#?}", stack_frame); } @@ -229,7 +230,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.13.2/x86_64/structures/tss/struct.TaskStateSegment.html +[`TaskStateSegment` struct]: https://docs.rs/x86_64/0.14.2/x86_64/structures/tss/struct.TaskStateSegment.html We create the TSS in a new `gdt` module (the name will make sense later): @@ -330,7 +331,7 @@ The problem is that the GDT segments are not yet active because the segment and In summary, we need to do the following: -1. **Reload code segment register**: We changed our GDT, so we should reload `cs`, the code segment register. This is required since the old segment selector could point a different GDT descriptor now (e.g. a TSS descriptor). +1. **Reload code segment register**: We changed our GDT, so we should reload `cs`, the code segment register. This is required since the old segment selector could point to a different GDT descriptor now (e.g. a TSS descriptor). 2. **Load the TSS** : We loaded a GDT that contains a TSS selector, but we still need to tell the CPU that it should use that TSS. 3. **Update the IDT entry**: As soon as our TSS is loaded, the CPU has access to a valid interrupt stack table (IST). Then we can tell the CPU that it should use our new double fault stack by modifying our double fault IDT entry. @@ -375,8 +376,8 @@ pub fn init() { 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.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 +[`set_cs`]: https://docs.rs/x86_64/0.14.2/x86_64/instructions/segmentation/fn.set_cs.html +[`load_tss`]: https://docs.rs/x86_64/0.14.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: @@ -526,7 +527,7 @@ 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, + _stack_frame: InterruptStackFrame, _error_code: u64, ) -> ! { serial_println!("[ok]"); @@ -545,6 +546,6 @@ In this post we learned what a double fault is and under which conditions it occ We also enabled the hardware supported stack switching on double fault exceptions so that it also works on stack overflow. While implementing it, we learned about the task state segment (TSS), the contained interrupt stack table (IST), and the global descriptor table (GDT), which was used for segmentation on older architectures. ## What's next? -The next post explains how to handle interrupts from external devices such as timers, keyboards, or network controllers. These hardware interrupts are very similar to exceptions, e.g. they are also dispatched through the IDT. However, unlike exceptions, they don't arise directly on the CPU. Instead, an _interrupt controller_ aggregates these interrupts and forwards them to CPU depending on their priority. In the next we will explore the [Intel 8259] \(“PIC”) interrupt controller and learn how to implement keyboard support. +The next post explains how to handle interrupts from external devices such as timers, keyboards, or network controllers. These hardware interrupts are very similar to exceptions, e.g. they are also dispatched through the IDT. However, unlike exceptions, they don't arise directly on the CPU. Instead, an _interrupt controller_ aggregates these interrupts and forwards them to CPU depending on their priority. In the next post we will explore the [Intel 8259] \(“PIC”) interrupt controller and learn how to implement keyboard support. [Intel 8259]: https://en.wikipedia.org/wiki/Intel_8259 diff --git a/blog/content/edition-2/posts/07-hardware-interrupts/index.fa.md b/blog/content/edition-2/posts/07-hardware-interrupts/index.fa.md index faeb8959..5a5319dc 100644 --- a/blog/content/edition-2/posts/07-hardware-interrupts/index.fa.md +++ b/blog/content/edition-2/posts/07-hardware-interrupts/index.fa.md @@ -21,6 +21,7 @@ rtl = true [گیت‌هاب]: https://github.com/phil-opp/blog_os [در زیر]: #comments + [post branch]: https://github.com/phil-opp/blog_os/tree/post-07 @@ -80,29 +81,29 @@ Secondary ATA ----> |____________| Parallel Port 1----> |____________| پیکربندی پیش فرض PIC ها قابل استفاده نیست، زیرا اعداد بردار وقفه را در محدوده 15-0 به پردازنده می فرستد. این اعداد در حال حاضر توسط استثناهای پردازنده اشغال شده‌اند ، به عنوان مثال شماره 8 مربوط به یک خطای دوگانه است. برای رفع این مشکل همپوشانی، باید وقفه های PIC را به اعداد دیگری تغییر دهیم. دامنه واقعی مهم نیست به شرطی که با استثناها همپوشانی نداشته باشد ، اما معمولاً محدوده 47-32 انتخاب می شود، زیرا اینها اولین شماره های آزاد پس از 32 اسلات استثنا هستند. -پیکربندی با نوشتن مقادیر ویژه در پورت های فرمان و داده PIC ها اتفاق می افتد. خوشبختانه قبلا کرت‌ای به نام [`pic8259_simple`] وجود دارد، بنابراین نیازی نیست که توالی راه اندازی اولیه را خودمان بنویسیم. در صورت علاقه‌مند بودن به چگونگی عملکرد آن، [کد منبع آن][pic crate source] را بررسی کنید، نسبتاً کوچک و دارای مستند خوبی است. +پیکربندی با نوشتن مقادیر ویژه در پورت های فرمان و داده PIC ها اتفاق می افتد. خوشبختانه قبلا کرت‌ای به نام [`pic8259`] وجود دارد، بنابراین نیازی نیست که توالی راه اندازی اولیه را خودمان بنویسیم. در صورت علاقه‌مند بودن به چگونگی عملکرد آن، [کد منبع آن][pic crate source] را بررسی کنید، نسبتاً کوچک و دارای مستند خوبی است. -[pic crate source]: https://docs.rs/crate/pic8259_simple/0.2.0/source/src/lib.rs +[pic crate source]: https://docs.rs/crate/pic8259/0.10.1/source/src/lib.rs برای افزودن کرت به عنوان وابستگی ، موارد زیر را به پروژه خود اضافه می کنیم: -[`pic8259_simple`]: https://docs.rs/pic8259_simple/0.2.0/pic8259_simple/ +[`pic8259`]: https://docs.rs/pic8259/0.10.1/pic8259/ ```toml # in Cargo.toml [dependencies] -pic8259_simple = "0.2.0" +pic8259 = "0.10.1" ``` انتزاع اصلی ارائه شده توسط کرت، ساختمان [`ChainedPics`] است که نمایانگر طرح اولیه/ثانویه PIC است که در بالا دیدیم. برای استفاده به روش زیر طراحی شده است: -[`ChainedPics`]: https://docs.rs/pic8259_simple/0.2.0/pic8259_simple/struct.ChainedPics.html +[`ChainedPics`]: https://docs.rs/pic8259/0.10.1/pic8259/struct.ChainedPics.html ```rust // in src/interrupts.rs -use pic8259_simple::ChainedPics; +use pic8259::ChainedPics; use spin; pub const PIC_1_OFFSET: u8 = 32; @@ -130,7 +131,7 @@ pub fn init() { ما از تابع [`initialize`] برای انجام مقداردهی اولیه PIC استفاده می کنیم. مانند تابع `ChainedPics::new`، این تابع نیز ایمن نیست زیرا در صورت عدم پیکربندی صحیح PIC می تواند باعث رفتار نامشخص شود. -[`initialize`]: https://docs.rs/pic8259_simple/0.2.0/pic8259_simple/struct.ChainedPics.html#method.initialize +[`initialize`]: https://docs.rs/pic8259/0.10.1/pic8259/struct.ChainedPics.html#method.initialize اگر همه چیز خوب پیش برود ، باید هنگام اجرای `cargo run` پیام "It did not crash" را ببینیم. @@ -205,7 +206,7 @@ lazy_static! { } extern "x86-interrupt" fn timer_interrupt_handler( - _stack_frame: &mut InterruptStackFrame) + _stack_frame: InterruptStackFrame) { print!("."); } @@ -213,7 +214,7 @@ extern "x86-interrupt" fn timer_interrupt_handler( `timer_interrupt_handler` ما دارای امضای مشابه کنترل کننده های استثنای ما است ، زیرا پردازنده به طور یکسان به استثناها و وقفه های خارجی واکنش نشان می دهد (تنها تفاوت این است که برخی از استثناها کد خطا را در پشته ذخیره می‌کنند). ساختمان [`InterruptDescriptorTable`] تریت [`IndexMut`] را پیاده سازی می کند، بنابراین می توانیم از طریق سینتکس ایندکس‌دهی آرایه، به ایتم های جداگانه دسترسی پیدا کنیم. -[`InterruptDescriptorTable`]: https://docs.rs/x86_64/0.13.2/x86_64/structures/idt/struct.InterruptDescriptorTable.html +[`InterruptDescriptorTable`]: https://docs.rs/x86_64/0.14.2/x86_64/structures/idt/struct.InterruptDescriptorTable.html [`IndexMut`]: https://doc.rust-lang.org/core/ops/trait.IndexMut.html در کنترل کننده وقفه تایمر، یک نقطه را روی صفحه چاپ می کنیم. همانطور که وقفه تایمر به صورت دوره ای اتفاق می افتد ، انتظار داریم که در هر تیک تایمر یک نقطه ظاهر شود. با این حال، هنگامی که آن را اجرا می کنیم می بینیم که فقط یک نقطه چاپ می شود: @@ -230,7 +231,7 @@ extern "x86-interrupt" fn timer_interrupt_handler( // in src/interrupts.rs extern "x86-interrupt" fn timer_interrupt_handler( - _stack_frame: &mut InterruptStackFrame) + _stack_frame: InterruptStackFrame) { print!("."); @@ -251,7 +252,7 @@ extern "x86-interrupt" fn timer_interrupt_handler( ### پیکربندی تایمر -تایمر سخت افزاری که ما از آن استفاده می کنیم ، _Progammable Interval Timer_ یا به اختصار PIT نامیده می شود. همانطور که از نام آن مشخص است ، می توان فاصله بین دو وقفه را پیکربندی کرد. ما در اینجا به جزئیات نمی پردازیم زیرا به زودی به [تایمر APIC] سوییچ خواهیم کرد، اما ویکی OSDev مقاله مفصلی درباره [پیکربندی PIT] دارد. +تایمر سخت افزاری که ما از آن استفاده می کنیم ، _Programmable Interval Timer_ یا به اختصار PIT نامیده می شود. همانطور که از نام آن مشخص است ، می توان فاصله بین دو وقفه را پیکربندی کرد. ما در اینجا به جزئیات نمی پردازیم زیرا به زودی به [تایمر APIC] سوییچ خواهیم کرد، اما ویکی OSDev مقاله مفصلی درباره [پیکربندی PIT] دارد. [تایمر APIC]: https://wiki.osdev.org/APIC_timer [پیکربندی PIT]: https://wiki.osdev.org/Programmable_Interval_Timer @@ -338,7 +339,7 @@ pub fn _print(args: fmt::Arguments) { تابع [`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 +[`without_interrupts`]: https://docs.rs/x86_64/0.14.2/x86_64/instructions/interrupts/fn.without_interrupts.html [کلوژر]: https://doc.rust-lang.org/book/second-edition/ch13-01-closures.html ما می توانیم همین تغییر را در تابع چاپ سریال نیز اعمال کنیم تا اطمینان حاصل کنیم که هیچ بن‌بستی در آن رخ نمی دهد: @@ -543,7 +544,7 @@ lazy_static! { } extern "x86-interrupt" fn keyboard_interrupt_handler( - _stack_frame: &mut InterruptStackFrame) + _stack_frame: InterruptStackFrame) { print!("k"); @@ -568,7 +569,7 @@ extern "x86-interrupt" fn keyboard_interrupt_handler( // in src/interrupts.rs extern "x86-interrupt" fn keyboard_interrupt_handler( - _stack_frame: &mut InterruptStackFrame) + _stack_frame: InterruptStackFrame) { use x86_64::instructions::port::Port; @@ -585,7 +586,7 @@ extern "x86-interrupt" fn keyboard_interrupt_handler( ما برای خواندن یک بایت از پورت داده صفحه کلید از نوع [`Port`] کرت `x86_64` استفاده می‌کنیم. این بایت [_اسکن کد_] نامیده می شود و عددی است که کلید فشرده شده / رها شده را نشان می دهد. ما هنوز کاری با اسکن کد انجام نمی دهیم ، فقط آن را روی صفحه چاپ می کنیم: -[`Port`]: https://docs.rs/x86_64/0.13.2/x86_64/instructions/port/struct.Port.html +[`Port`]: https://docs.rs/x86_64/0.14.2/x86_64/instructions/port/struct.Port.html [_اسکن کد_]: https://en.wikipedia.org/wiki/Scancode ![QEMU printing scancodes to the screen when keys are pressed](qemu-printing-scancodes.gif) @@ -609,7 +610,7 @@ extern "x86-interrupt" fn keyboard_interrupt_handler( // in src/interrupts.rs extern "x86-interrupt" fn keyboard_interrupt_handler( - _stack_frame: &mut InterruptStackFrame) + _stack_frame: InterruptStackFrame) { use x86_64::instructions::port::Port; @@ -668,7 +669,7 @@ pc-keyboard = "0.5.0" // in/src/interrupts.rs extern "x86-interrupt" fn keyboard_interrupt_handler( - _stack_frame: &mut InterruptStackFrame) + _stack_frame: InterruptStackFrame) { use pc_keyboard::{layouts, DecodedKey, HandleControl, Keyboard, ScancodeSet1}; use spin::Mutex; diff --git a/blog/content/edition-2/posts/07-hardware-interrupts/index.md b/blog/content/edition-2/posts/07-hardware-interrupts/index.md index a54b53fc..bc0aa50a 100644 --- a/blog/content/edition-2/posts/07-hardware-interrupts/index.md +++ b/blog/content/edition-2/posts/07-hardware-interrupts/index.md @@ -16,6 +16,7 @@ This blog is openly developed on [GitHub]. If you have any problems or questions [GitHub]: https://github.com/phil-opp/blog_os [at the bottom]: #comments + [post branch]: https://github.com/phil-opp/blog_os/tree/post-07 @@ -75,29 +76,29 @@ Each controller can be configured through two [I/O ports], one “command” por The default configuration of the PICs is not usable, because it sends interrupt vector numbers in the range 0–15 to the CPU. These numbers are already occupied by CPU exceptions, for example number 8 corresponds to a double fault. To fix this overlapping issue, we need to remap the PIC interrupts to different numbers. The actual range doesn't matter as long as it does not overlap with the exceptions, but typically the range 32–47 is chosen, because these are the first free numbers after the 32 exception slots. -The configuration happens by writing special values to the command and data ports of the PICs. Fortunately there is already a crate called [`pic8259_simple`], so we don't need to write the initialization sequence ourselves. In case you are interested how it works, check out [its source code][pic crate source], it's fairly small and well documented. +The configuration happens by writing special values to the command and data ports of the PICs. Fortunately there is already a crate called [`pic8259`], so we don't need to write the initialization sequence ourselves. In case you are interested how it works, check out [its source code][pic crate source], it's fairly small and well documented. -[pic crate source]: https://docs.rs/crate/pic8259_simple/0.2.0/source/src/lib.rs +[pic crate source]: https://docs.rs/crate/pic8259/0.10.1/source/src/lib.rs To add the crate as dependency, we add the following to our project: -[`pic8259_simple`]: https://docs.rs/pic8259_simple/0.2.0/pic8259_simple/ +[`pic8259`]: https://docs.rs/pic8259/0.10.1/pic8259/ ```toml # in Cargo.toml [dependencies] -pic8259_simple = "0.2.0" +pic8259 = "0.10.1" ``` The main abstraction provided by the crate is the [`ChainedPics`] struct that represents the primary/secondary PIC layout we saw above. It is designed to be used in the following way: -[`ChainedPics`]: https://docs.rs/pic8259_simple/0.2.0/pic8259_simple/struct.ChainedPics.html +[`ChainedPics`]: https://docs.rs/pic8259/0.10.1/pic8259/struct.ChainedPics.html ```rust // in src/interrupts.rs -use pic8259_simple::ChainedPics; +use pic8259::ChainedPics; use spin; pub const PIC_1_OFFSET: u8 = 32; @@ -125,7 +126,7 @@ pub fn init() { We use the [`initialize`] function to perform the PIC initialization. Like the `ChainedPics::new` function, this function is also unsafe because it can cause undefined behavior if the PIC is misconfigured. -[`initialize`]: https://docs.rs/pic8259_simple/0.2.0/pic8259_simple/struct.ChainedPics.html#method.initialize +[`initialize`]: https://docs.rs/pic8259/0.10.1/pic8259/struct.ChainedPics.html#method.initialize If all goes well we should continue to see the "It did not crash" message when executing `cargo run`. @@ -200,7 +201,7 @@ lazy_static! { } extern "x86-interrupt" fn timer_interrupt_handler( - _stack_frame: &mut InterruptStackFrame) + _stack_frame: InterruptStackFrame) { print!("."); } @@ -208,7 +209,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.13.2/x86_64/structures/idt/struct.InterruptDescriptorTable.html +[`InterruptDescriptorTable`]: https://docs.rs/x86_64/0.14.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: @@ -225,7 +226,7 @@ To send the EOI, we use our static `PICS` struct again: // in src/interrupts.rs extern "x86-interrupt" fn timer_interrupt_handler( - _stack_frame: &mut InterruptStackFrame) + _stack_frame: InterruptStackFrame) { print!("."); @@ -246,7 +247,7 @@ When we now execute `cargo run` we see dots periodically appearing on the screen ### Configuring the Timer -The hardware timer that we use is called the _Progammable Interval Timer_ or PIT for short. Like the name says, it is possible to configure the interval between two interrupts. We won't go into details here because we will switch to the [APIC timer] soon, but the OSDev wiki has an extensive article about the [configuring the PIT]. +The hardware timer that we use is called the _Programmable Interval Timer_ or PIT for short. Like the name says, it is possible to configure the interval between two interrupts. We won't go into details here because we will switch to the [APIC timer] soon, but the OSDev wiki has an extensive article about the [configuring the PIT]. [APIC timer]: https://wiki.osdev.org/APIC_timer [configuring the PIT]: https://wiki.osdev.org/Programmable_Interval_Timer @@ -333,7 +334,7 @@ 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.13.2/x86_64/instructions/interrupts/fn.without_interrupts.html +[`without_interrupts`]: https://docs.rs/x86_64/0.14.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: @@ -538,7 +539,7 @@ lazy_static! { } extern "x86-interrupt" fn keyboard_interrupt_handler( - _stack_frame: &mut InterruptStackFrame) + _stack_frame: InterruptStackFrame) { print!("k"); @@ -563,7 +564,7 @@ To find out _which_ key was pressed, we need to query the keyboard controller. W // in src/interrupts.rs extern "x86-interrupt" fn keyboard_interrupt_handler( - _stack_frame: &mut InterruptStackFrame) + _stack_frame: InterruptStackFrame) { use x86_64::instructions::port::Port; @@ -580,7 +581,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.13.2/x86_64/instructions/port/struct.Port.html +[`Port`]: https://docs.rs/x86_64/0.14.2/x86_64/instructions/port/struct.Port.html [_scancode_]: https://en.wikipedia.org/wiki/Scancode ![QEMU printing scancodes to the screen when keys are pressed](qemu-printing-scancodes.gif) @@ -604,7 +605,7 @@ To translate the scancodes to keys, we can use a match statement: // in src/interrupts.rs extern "x86-interrupt" fn keyboard_interrupt_handler( - _stack_frame: &mut InterruptStackFrame) + _stack_frame: InterruptStackFrame) { use x86_64::instructions::port::Port; @@ -663,7 +664,7 @@ Now we can use this crate to rewrite our `keyboard_interrupt_handler`: // in/src/interrupts.rs extern "x86-interrupt" fn keyboard_interrupt_handler( - _stack_frame: &mut InterruptStackFrame) + _stack_frame: InterruptStackFrame) { use pc_keyboard::{layouts, DecodedKey, HandleControl, Keyboard, ScancodeSet1}; use spin::Mutex; diff --git a/blog/content/edition-2/posts/08-paging-introduction/index.fa.md b/blog/content/edition-2/posts/08-paging-introduction/index.fa.md index aa50cba1..27b0714e 100644 --- a/blog/content/edition-2/posts/08-paging-introduction/index.fa.md +++ b/blog/content/edition-2/posts/08-paging-introduction/index.fa.md @@ -21,6 +21,7 @@ rtl = true [گیت‌هاب]: https://github.com/phil-opp/blog_os [در زیر]: #comments + [post branch]: https://github.com/phil-opp/blog_os/tree/post-08 @@ -166,7 +167,7 @@ rtl = true ![An example 4-level page hierarchy with each page table shown in physical memory](x86_64-page-table-translation.svg) آدرس فیزیکی جدول صفحه سطح 4 که در حال حاضر فعال می‌باشد، و ریشه جدول صفحه سطح 4 است، در ثبات `CR3` ذخیره می‌شود. سپس هر ورودی جدول صفحه به قاب فیزیکی جدول سطح بعدی اشاره می‌کند. سپس ورودی جدول سطح 1 به قاب نگاشت شده اشاره می‌کند. توجه داشته باشید که تمام آدرس‌های موجود در جدول‌های صفحه فیزیکی هستند، به جای این‌که مجازی باشند، زیرا در غیر این‌صورت CPU نیاز به ترجمه آن آدرس‌ها نیز دارد (که این امر می‌تواند باعث بازگشت بی‌پایان شود). - + سلسله مراتب جدول صفحه بالا، دو صفحه را نگاشت می‌کند (به رنگ آبی). از اندیس‌های جدول صفحه می‌توان نتیجه گرفت که آدرس‌های مجازی این دو صفحه `0x803FE7F000` و `0x803FE00000` است. بیایید ببینیم چه اتفاقی می‌افتد وقتی برنامه سعی می‌کند از آدرس `0x803FE7F5CE` بخواند. ابتدا آدرس را به باینری تبدیل می‌کنیم و اندیس‌های جدول صفحه و آفست صفحه را برای آدرس تعیین می‌کنیم: ![The sign extension bits are all 0, the level 4 index is 1, the level 3 index is 0, the level 2 index is 511, the level 1 index is 127, and the page offset is 0x5ce](x86_64-page-table-translation-addresses.png) @@ -239,17 +240,17 @@ Bit(s) | Name | Meaning کریت `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 +[جدول‌های صفحه]: https://docs.rs/x86_64/0.14.2/x86_64/structures/paging/page_table/struct.PageTable.html +[ورودی‌های]: https://docs.rs/x86_64/0.14.2/x86_64/structures/paging/page_table/struct.PageTableEntry.html ### بافر ترجمه Lookaside -یک جدول صفحه 4 سطحی، ترجمه آدرس‌های مجازی را پُر هزینه‌ می‌کند، زیرا هر ترجمه به 4 دسترسی حافظه نیاز دارد. برای بهبود عملکرد، معماری x86_64 آخرین ترجمه‌ها را در _translation lookaside buffer_ یا به اختصار TLB ذخیره می‌کند. و این به ما اجازه می‌دهد تا از ترجمه کردن مجدد ترجمه‌هایی که در حافظه پنهان قرار دارند خودداری کنیم. +یک جدول صفحه 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`]: https://docs.rs/x86_64/0.14.2/x86_64/instructions/tlb/index.html مهم است که به یاد داشته باشید که TLB را روی هر جدول صفحه فلاش کنید، زیرا در غیر این‌صورت پردازنده ممکن است از ترجمه قدیمی استفاده کند، که می‌تواند منجر به باگ‌های غیرقطعی شود که اشکال‌زدایی آن بسیار سخت است. @@ -288,7 +289,7 @@ use x86_64::structures::idt::PageFaultErrorCode; use crate::hlt_loop; extern "x86-interrupt" fn page_fault_handler( - stack_frame: &mut InterruptStackFrame, + stack_frame: InterruptStackFrame, error_code: PageFaultErrorCode, ) { use x86_64::registers::control::Cr2; @@ -304,8 +305,8 @@ extern "x86-interrupt" fn page_fault_handler( ثبات [`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 +[`Cr2::read`]: https://docs.rs/x86_64/0.14.2/x86_64/registers/control/struct.Cr2.html#method.read +[`PageFaultErrorCode`]: https://docs.rs/x86_64/0.14.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 @@ -339,7 +340,7 @@ pub extern "C" fn _start() -> ! { ثبات `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 +[`CAUSED_BY_WRITE`]: https://docs.rs/x86_64/0.14.2/x86_64/structures/idt/struct.PageFaultErrorCode.html#associatedconstant.CAUSED_BY_WRITE می‌بینیم که اشاره‌گر دستورالعمل فعلی `0x2031b2` می‌باشد، بنابراین می‌دانیم که این آدرس به یک صفحه کد اشاره دارد. صفحات کد توسط بوت‌لودر بصورت فقط خواندنی نگاشت می‌شوند، بنابراین خواندن از این آدرس امکان‌پذیر است اما نوشتن باعث خطای صفحه می‌شود. می‌توانید این کار را با تغییر اشاره‌گر `0xdeadbeaf` به `0x2031b2` امتحان کنید: @@ -363,7 +364,7 @@ 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 +[`PROTECTION_VIOLATION`]: https://docs.rs/x86_64/0.14.2/x86_64/structures/idt/struct.PageFaultErrorCode.html#associatedconstant.PROTECTION_VIOLATION ### دسترسی به جدول‌های صفحه @@ -389,9 +390,9 @@ pub extern "C" fn _start() -> ! { تابع [`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 +[`Cr3::read`]: https://docs.rs/x86_64/0.14.2/x86_64/registers/control/struct.Cr3.html#method.read +[`PhysFrame`]: https://docs.rs/x86_64/0.14.2/x86_64/structures/paging/frame/struct.PhysFrame.html +[`Cr3Flags`]: https://docs.rs/x86_64/0.14.2/x86_64/registers/control/struct.Cr3Flags.html هنگامی که آن را اجرا می‌کنیم، خروجی زیر را مشاهده می‌کنیم: @@ -401,7 +402,7 @@ 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 +[`PhysAddr`]: https://docs.rs/x86_64/0.14.2/x86_64/addr/struct.PhysAddr.html دسترسی مستقیم به حافظه فیزیکی در هنگام فعال بودن صفحه‌بندی امکان پذیر نیست، زیرا برنامه‌ها به راحتی می‌توانند محافظت از حافظه (ترجمه: memory protection) را دور بزنند و در غیر این‌صورت به حافظه سایر برنامه‌ها دسترسی پیدا می‌کنند. بنابراین تنها راه دسترسی به جدول از طریق برخی از صفحه‌های مجازی است که به قاب فیزیکی در آدرس`0x1000` نگاشت شده. این مشکل ایجاد نگاشت برای قاب‌های جدول صفحه یک مشکل کلی است، زیرا هسته به طور مرتب به جدول‌های صفحه دسترسی دارد، به عنوان مثال هنگام اختصاص پشته برای یک نخِ (ترجمه: thread) جدید. diff --git a/blog/content/edition-2/posts/08-paging-introduction/index.ja.md b/blog/content/edition-2/posts/08-paging-introduction/index.ja.md new file mode 100644 index 00000000..03be001c --- /dev/null +++ b/blog/content/edition-2/posts/08-paging-introduction/index.ja.md @@ -0,0 +1,428 @@ ++++ +title = "ページング入門" +weight = 8 +path = "ja/paging-introduction" +date = 2019-01-14 + +[extra] +chapter = "Memory Management" +# Please update this when updating the translation +translation_based_on_commit = "3315bfe2f63571f5e6e924d58ed32afd8f39f892" +# GitHub usernames of the people that translated this post +translators = ["woodyZootopia", "JohnTitor"] ++++ + +この記事では**ページング**を紹介します。これは、私達のオペレーティングシステムにも使う、とても一般的なメモリ管理方式です。なぜメモリの分離が必要なのか、**セグメンテーション**がどういう仕組みなのか、**仮想メモリ**とは何なのか、ページングがいかにしてメモリ断片化 (フラグメンテーション) の問題を解決するのかを説明します。また、x86_64アーキテクチャにおける、マルチレベルページテーブルのレイアウトについても説明します。 + + + +このブログの内容は [GitHub] 上で公開・開発されています。何か問題や質問などがあれば issue をたててください(訳注: リンクは原文(英語)のものになります)。また[こちら][at the bottom]にコメントを残すこともできます。この記事の完全なソースコードは[`post-08` ブランチ][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-08 + + + +## メモリの保護 + +オペレーティングシステムの主な役割の一つに、プログラムを互いに分離するということがあります。例えば、ウェブブラウザがテキストエディタに干渉してはいけません。この目的を達成するために、オペレーティングシステムはハードウェアの機能を利用して、あるプロセスのメモリ領域に他のプロセスがアクセスできないようにします。ハードウェアやOSの実装によって、さまざまなアプローチがあります。 + +例として、ARM Cortex-Mプロセッサ(組み込みシステムに使われています)のいくつかには、[メモリ保護ユニット][_Memory Protection Unit_] (Memory Protection Unit, MPU) が搭載されており、異なるアクセス権限(例えば、アクセス不可、読み取り専用、読み書きなど)を持つメモリ領域を少数(例えば8個)定義できます。MPUは、メモリアクセスのたびに、そのアドレスが正しいアクセス権限を持つ領域にあるかどうかを確認し、そうでなければ例外を投げます。プロセスを変更するごとにその領域とアクセス権限を変更すれば、オペレーティングシステムはそれぞれのプロセスが自身のメモリにのみアクセスすることを保証し、したがってプロセスを互いに分離できます。 + +[_Memory Protection Unit_]: https://developer.arm.com/docs/ddi0337/e/memory-protection-unit/about-the-mpu + +x86においては、ハードウェアは2つの異なるメモリ保護の方法をサポートしています:[セグメンテーション][segmentation]と[ページング][paging]です。 + +[segmentation]: https://en.wikipedia.org/wiki/X86_memory_segmentation +[paging]: https://en.wikipedia.org/wiki/Virtual_memory#Paged_virtual_memory + +## セグメンテーション + +セグメンテーションは1978年にはすでに導入されており、当初の目的はアドレス可能なメモリの量を増やすことでした。当時、CPUは16bitのアドレスしか使えなかったので、アドレス可能なメモリは64KiBに限られていました。この64KiBを超えてアクセスするために、セグメントレジスタが追加され、このそれぞれにオフセットアドレスを格納するようになりました。CPUがメモリにアクセスするとき、毎回このオフセットを自動的に加算するようにすることで、最大1MiBのメモリにアクセスできるようになりました。 + +メモリアクセスの種類によって、セグメントレジスタは自動的にCPUによって選ばれます。命令の引き出し (フェッチ) にはコードセグメント`CS`が使用され、スタック操作(プッシュ・ポップ)にはスタックセグメント`SS`が使用されます。その他の命令では、データセグメント`DS`やエクストラセグメント`ES`が使用されます。その後、自由に使用できる`FS`と`GS`というセグメントレジスタも追加されました。 + +セグメンテーションの初期バージョンでは、セグメントレジスタは直接オフセットを格納しており、アクセス制御は行われていませんでした。これは後に[プロテクトモード (protected mode) ][_protected mode_]が導入されたことで変更されました。CPUがこのモードで動作している時、セグメント記述子 (ディスクリプタ) 局所 (ローカル) または大域 (グローバル) [**記述子表 (ディスクリプタテーブル) **][_descriptor table_]を格納します。これには(オフセットアドレスに加えて)セグメントのサイズとアクセス権限が格納されます。それぞれのプロセスに対し、メモリアクセスをプロセスのメモリ領域にのみ制限するような大域/局所記述子表をロードすることで、OSはプロセスを互いに隔離できます。 + +[_protected mode_]: https://en.wikipedia.org/wiki/X86_memory_segmentation#Protected_mode +[_descriptor table_]: https://en.wikipedia.org/wiki/Global_Descriptor_Table + +メモリアドレスを実際にアクセスされる前に変更するという点において、セグメンテーションは今やほぼすべての場所で使われている**仮想メモリ**というテクニックをすでに採用していたと言えます。 + +### 仮想メモリ + +仮想メモリの背景にある考え方は、下層にある物理的なストレージデバイスからメモリアドレスを抽象化することです。ストレージデバイスに直接アクセスするのではなく、先に何らかの変換ステップが踏まれます。セグメンテーションの場合、この変換ステップとはアクティブなセグメントのオフセットアドレスを追加することです。例えば、オフセット`0x1111000`のセグメントにあるプログラムが`0x1234000`というメモリアドレスにアクセスすると、実際にアクセスされるアドレスは`0x2345000`になります。 + +この2種類のアドレスを区別するため、変換前のアドレスを **仮想(アドレス)** と、変換後のアドレスを **物理(アドレス)** と呼びます。この2種類のアドレスの重要な違いの一つは、物理アドレスは常に同じ一意なメモリ位置を指すということです。いっぽう仮想アドレス(の指す場所)は変換する関数に依存します。二つの異なる仮想アドレスが同じ物理アドレスを指すということは十分にありえます。また、変換関数が異なっていれば、同じ仮想アドレスが別の物理アドレスを示すということもありえます。 + +この特性が役立つ例として、同じプログラムを2つ並行して実行するという状況が挙げられます。 + +![Two virtual address spaces with address 0–150, one translated to 100–250, the other to 300–450](segmentation-same-program-twice.svg) + +同じプログラムを2つ実行していますが、別の変換関数が使われています。1つ目の実体 (インスタンス) ではセグメントのオフセットが100なので、0から150の仮想アドレスは100から250に変換されます。2つ目のインスタンスではオフセットが300なので、0から150の仮想アドレスが300から450に変換されます。これにより、プログラムが互いに干渉することなく同じコード、同じ仮想アドレスを使うことができます。 + +もう一つの利点は、プログラムが全く異なる仮想アドレスを使っていたとしても、物理メモリ上の任意の場所に置けるということです。したがって、OSはプログラムを再コンパイルすることなく利用可能なメモリをフルに活用できます。 + +### 断片化 (fragmentation) + +物理アドレスと仮想アドレスを分けることにより、セグメンテーションは非常に強力なものとなっています。しかし、これにより断片化という問題が発生します。例として、上で見たプログラムの3つ目を実行したいとしましょう: + +![Three virtual address spaces, but there is not enough continuous space for the third](segmentation-fragmentation.svg) + +開放されているメモリは十分にあるにも関わらず、プログラムのインスタンスを重ねることなく物理メモリに対応づけることはできません。ここで必要なのは **連続した** メモリであり、開放されているメモリが小さな塊であっては使えないためです。 + +この断片化に対処する方法の一つは、実行を一時停止し、メモリの使用されている部分を寄せ集めて、変換関数を更新し、実行を再開することでしょう: + +![Three virtual address spaces after defragmentation](segmentation-fragmentation-compacted.svg) + +これで、プログラムの3つ目のインスタンスを開始するのに十分な連続したスペースができました。 + +このデフラグメンテーションという処理の欠点は、大量のメモリをコピーしなければならず、パフォーマンスを低下させてしまうことです。また、メモリが断片化しすぎる前に定期的に実行しないといけません。そうすると、プログラムが時々一時停止して反応がなくなるので、性能が予測不可能になってしまいます。 + +ほとんどのシステムでセグメンテーションが用いられなくなった理由の一つに、この断片化の問題があります。実際、x86の64ビットモードでは、セグメンテーションはもはやサポートされていません。代わりに **ページング** が使用されており、これにより断片化の問題は完全に回避されます。 + +## ページング + +ページングの考え方は、仮想メモリ空間と物理メモリ空間の両方を、サイズの固定された小さなブロックに分割するというものです。仮想メモリ空間のブロックは **ページ** と呼ばれ、物理アドレス空間のブロックは **フレーム** と呼ばれます。各ページはフレームに独立してマッピングできるので、大きなメモリ領域を連続していない物理フレームに分割することが可能です。 + +この方法の利点は、上のメモリ空間断片化の状況をもう一度、セグメンテーションの代わりにページングを使って見てみれば明らかになります: + +![With paging the third program instance can be split across many smaller physical areas](paging-fragmentation.svg) + +この例では、ページサイズは50バイトなので、それぞれのメモリ領域が3つのページに分割されます。それぞれのページは個別にフレームに対応付けられるので、連続した仮想メモリ領域を非連続な物理フレームへと対応付けられるのです。これにより、デフラグを事前に実行することなく、3つ目のプログラムのインスタンスを開始できるようになります。 + +### 隠された断片化 + +少ない数の可変なサイズのメモリ領域を使っていたセグメンテーションと比べると、ページングでは大量の小さい固定サイズのメモリ領域を使います。すべてのフレームが同じ大きさなので、「小さすぎて使えないフレーム」などというものは存在せず、したがって断片化も起きません。 + +というより、**目に見える** 断片化は起きていない、という方が正しいでしょう。**内部 (internal) 断片化**と呼ばれる、目に見えない断片化は依然として起こっています。内部断片化は、すべてのメモリ領域がページサイズの整数倍ぴったりにはならないために生じます。例えば、上の例でサイズが101のプログラムを考えてみてください:この場合でもサイズ50のページが3つ必要で、必要な量より49バイト多く占有します。これらの2種類の断片化を区別するため、セグメンテーションを使うときに起きる断片化は **外部 (external) 断片化** と呼ばれます。 + +内部断片化が起こるのは残念なことですが、セグメンテーションで発生していた外部断片化よりも優れていることが多いです。確かにメモリ領域は無駄にしますが、デフラグメンテーションをする必要がなく、また断片化の量も予想できるからです(平均するとメモリ領域ごとにページの半分)。 + +### ページテーブル + +最大で何百万ものページがそれぞれ独立にフレームに対応付けられることを見てきました。この対応付けの情報はどこかに保存されなければなりません。セグメンテーションでは、有効なメモリ領域ごとに個別のセグメントセレクタを使っていましたが、ページングではレジスタよりも遥かに多くのページが使われるので、これは不可能です。代わりにページングでは **ページテーブル** と呼ばれる (テーブル) 構造を使って対応付の情報を保存します。 + +上の例では、ページテーブルは以下のようになります: + +![Three page tables, one for each program instance. For instance 1 the mapping is 0->100, 50->150, 100->200. For instance 2 it is 0->300, 50->350, 100->400. For instance 3 it is 0->250, 50->450, 100->500.](paging-page-tables.svg) + +それぞれのプログラムのインスタンスが独自のページテーブルを持っているのが分かります。現在有効なテーブルへのポインタは、特殊なCPUのレジスタに格納されます。`x86`においては、このレジスタは`CR3`と呼ばれています。それぞれのプログラムのインスタンスを実行する前に、正しいページテーブルを指すポインタをこのレジスタにロードするのはOSの役割です。 + +それぞれのメモリアクセスにおいて、CPUはテーブルへのポインタをレジスタから読み出し、テーブル内のアクセスされたページから対応するフレームを見つけ出します。これは完全にハードウェア内で行われ、実行しているプログラムからはこの動作は見えません。変換プロセスを高速化するために、多くのCPUアーキテクチャは前回の変換の結果を覚えておく専用のキャッシュを持っています。 + +アーキテクチャによっては、ページテーブルのエントリは"Flags"フィールドにあるアクセス権限のような属性も保持できます。上の例では、"r/w"フラグがあることにより、このページは読み書きのどちらも可能だということを示しています。 + +### 複数層 (Multilevel) ページテーブル + +上で見たシンプルなページテーブルは、アドレス空間が大きくなってくると問題が発生します:メモリが無駄になるのです。たとえば、`0`, `1_000_000`, `1_000_050` および `1_000_100`(3ケタごとの区切りとして`_`を用いています)の4つの仮想ページを使うプログラムを考えてみましょう。 + +![Page 0 mapped to frame 0 and pages `1_000_000`–`1_000_150` mapped to frames 100–250](single-level-page-table.svg) + +このプログラムはたった4つしか物理フレームを必要としていないのに、テーブルには100万以上ものエントリが存在してしまっています。空のエントリを省略した場合、変換プロセスにおいてCPUが正しいエントリに直接ジャンプできなくなってしまうので、それはできません(たとえば、4つめのページが4つめのエントリを使っていることが保証されなくなってしまいます)。 + +この無駄になるメモリを減らせる、 **2層ページテーブル** を使ってみましょう。発想としては、それぞれのアドレス領域に異なるページテーブルを使うというものです。**レベル2** ページテーブルと呼ばれる追加のページテーブルは、アドレス領域と(レベル1の)ページテーブルのあいだの対応を格納します。 + +これを理解するには、例を見るのが一番です。それぞれのレベル1テーブルは大きさ`10_000`の領域に対応するとします。すると、以下のテーブルが上のマッピングの例に対応するものとなります: + +![Page 0 points to entry 0 of the level 2 page table, which points to the level 1 page table T1. The first entry of T1 points to frame 0, the other entries are empty. Pages `1_000_000`–`1_000_150` point to the 100th entry of the level 2 page table, which points to a different level 1 page table T2. The first three entries of T2 point to frames 100–250, the other entries are empty.](multilevel-page-table.svg) + +ページ0は最初の`10_000`バイト領域に入るので、レベル2ページテーブルの最初のエントリを使います。このエントリはT1というレベル1ページテーブルを指し、このページテーブルはページ`0`がフレーム`0`に対応すると指定します。 + +ページ`1_000_000`, `1_000_050`および`1_000_100`はすべて、`10_000`バイトの大きさの領域100個目に入るので、レベル2ページテーブルの100個目のエントリを使います。このエントリは、T2という別のレベル1テーブルを指しており、このレベル1テーブルはこれらの3つのページをフレーム`100`, `150`および`200`に対応させています。レベル1テーブルにおけるページアドレスには領域のオフセットは含まれていない、つまり例えば、ページ`1_000_050`のエントリは単に`50`である、ということに注意してください。 + +レベル2テーブルにはまだ100個の空のエントリがありますが、前の100万にくらべればこれはずっと少ないです。このように節約できる理由は、`10_000`から`10_000_000`の、対応付けのないメモリ領域のためのレベル1テーブルを作る必要がないためです。 + +2層ページテーブルの原理は、3、4、それ以上に多くの層に拡張できます。このとき、ページテーブルレジスタは最も高いレベルのテーブルを指し、そのテーブルは次に低いレベルのテーブルを指し、それはさらに低いレベルのものを、と続きます。そして、レベル1のテーブルは対応するフレームを指します。この原理は一般に **複数層 (multilevel) ** ページテーブルや、 **階層型 (hierarchical) ** ページテーブルと呼ばれます。 + +ページングと複数層ページテーブルの仕組みが理解できたので、x86_64アーキテクチャにおいてどのようにページングが実装されているのかについて見ていきましょう(以下では、CPUは64ビットモードで動いているとします)。 + +## x86_64におけるページング + +x86_64アーキテクチャは4層ページテーブルを使っており、ページサイズは4KiBです。それぞれのページテーブルは、層によらず512のエントリを持っています。それぞれのエントリの大きさは8バイトなので、それぞれのテーブルは512 * 8B = 4KiBであり、よってぴったり1ページに収まります。 + +(各)レベルのページテーブルインデックスは、仮想アドレスから直接求められます: + +![Bits 0–12 are the page offset, bits 12–21 the level 1 index, bits 21–30 the level 2 index, bits 30–39 the level 3 index, and bits 39–48 the level 4 index](x86_64-table-indices-from-address.svg) + +それぞれのテーブルインデックスは9ビットからなることがわかります。それぞれのテーブルに2^9 = 512エントリあることを考えるとこれは妥当です。最下位の12ビットは4KiBページ内でのオフセット(2^12バイト = 4KiB)です。48ビットから64ビットは捨てられます。つまり、x86_64は48ビットのアドレスにしか対応しておらず、そのため(64ビットアーキテクチャなどとよく呼ばれるが)実際には64ビットではないということです。 + +[5-level page table]: https://en.wikipedia.org/wiki/Intel_5-level_paging + +48ビットから64ビットが捨てられるからといって、任意の値にしてよいということではありません。アドレスを一意にし、5層ページテーブルのような将来の拡張に備えるため、この範囲のすべてのビットは47ビットの値と同じにしないといけません。これは、[2の補数における符号拡張][sign extension in two's complement]によく似ているので、 **符号 (sign) 拡張 (extension) ** とよばれています。アドレスが適切に符号拡張されていない場合、CPUは例外を投げます。 + +[sign extension in two's complement]: https://en.wikipedia.org/wiki/Two's_complement#Sign_extension + +近年発売されたIntelのIce LakeというCPUは、[5層ページテーブル][5-level page tables]を使用することもでき、そうすると仮想アドレスが48ビットから57ビットまで延長されるということは書いておく価値があるでしょう。いまの段階で私たちのカーネルをこの特定のCPUに最適化する意味はないので、この記事では標準の4層ページテーブルのみを使うことにします。 + +[5-level page tables]: https://en.wikipedia.org/wiki/Intel_5-level_paging + +### 変換の例 + +この変換の仕組みをより詳細に理解するために、例を挙げて見てみましょう。 + +![An example 4-level page hierarchy with each page table shown in physical memory](x86_64-page-table-translation.svg) + +現在有効なレベル4ページテーブルの物理アドレス、つまりレベル4ページテーブルの「 (root) 」は`CR3`レジスタに格納されています。それぞれのページテーブルエントリは、次のレベルのテーブルの物理フレームを指しています。そして、レベル1のテーブルは対応するフレームを指しています。なお、ページテーブル内のアドレスは全て仮想ではなく物理アドレスであることに注意してください。さもなければ、CPUは(変換プロセス中に)それらのアドレスも変換しなくてはならず、無限再帰に陥ってしまうかもしれないからです。 + +上のページテーブル階層構造は、最終的に(青色の)2つのページへの対応を行っています。ページテーブルのインデックスから、これらの2つのページの仮想アドレスは`0x803FE7F000`と`0x803FE00000`であると推論できます。プログラムがアドレス`0x803FE7F5CE`から読み込もうとしたときに何が起こるかを見てみましょう。まず、アドレスを2進数に変換し、アドレスのページテーブルインデックスとページオフセットが何であるかを決定します: + +![The sign extension bits are all 0, the level 4 index is 1, the level 3 index is 0, the level 2 index is 511, the level 1 index is 127, and the page offset is 0x5ce](x86_64-page-table-translation-addresses.png) + +これらのインデックス情報をもとにページテーブル階層構造を移動して、このアドレスに対応するフレームを決定します: + +- まず、`CR3`レジスタからレベル4テーブルのアドレスを読み出します。 +- レベル4のインデックスは1なので、このテーブルの1つ目のインデックスを見ます。すると、レベル3テーブルはアドレス16KiBに格納されていると分かります。 +- レベル3テーブルをそのアドレスから読み出し、インデックス0のエントリを見ると、レベル2テーブルは24KiBにあると教えてくれます。 +- レベル2のインデックスは511なので、このページの最後のエントリを見て、レベル1テーブルのアドレスを見つけます。 +- レベル1テーブルの127番目のエントリを読むことで、ついに対象のページは12KiB(16進数では0x3000)のフレームに対応づけられていると分かります。 +- 最後のステップは、ページオフセットをフレームアドレスに足して、物理アドレスを得ることです。0x3000 + 0x5ce = 0x35ce + +![The same example 4-level page hierarchy with 5 additional arrows: "Step 0" from the CR3 register to the level 4 table, "Step 1" from the level 4 entry to the level 3 table, "Step 2" from the level 3 entry to the level 2 table, "Step 3" from the level 2 entry to the level 1 table, and "Step 4" from the level 1 table to the mapped frames.](x86_64-page-table-translation-steps.svg) + +レベル1テーブルにあるこのページの権限は`r`であり、これは読み込み専用という意味です。これらのような権限に対する侵害はハードウェアによって保護されており、このページに書き込もうとした場合は例外が投げられます。より高いレベルのページにおける権限は、下のレベルにおいて可能な権限を制限します。たとえばレベル3エントリを読み込み専用にした場合、下のレベルで読み書きを許可したとしても、このエントリを使うページはすべて書き込み不可になります。 + +この例ではそれぞれのテーブルの実体 (インスタンス) を1つずつしか使いませんでしたが、普通、それぞれのアドレス空間において、各レベルに対して複数のインスタンスが使われるということは知っておく価値があるでしょう。最大で + +- 1個のレベル4テーブル +- 512個のレベル3テーブル(レベル4テーブルには512エントリあるので) +- 512 * 512個のレベル2テーブル(512個のレベル3テーブルそれぞれに512エントリあるので) +- 512 * 512 * 512個のレベル1テーブル(それぞれのレベル2テーブルに512エントリあるので) + +があります。 + +### ページテーブルの形式 + +x86_64アーキテクチャにおけるページテーブルは詰まるところ512個のエントリの配列です。Rustの構文では以下のようになります: + +```rust +#[repr(align(4096))] +pub struct PageTable { + entries: [PageTableEntry; 512], +} +``` + +`repr`属性で示されるように、ページテーブルはアラインされる必要があります。つまり4KiBごとの境界に揃えられる必要がある、ということです。この条件により、ページテーブルはつねにページひとつを完全に使うので、エントリをとてもコンパクトにできる最適化が可能になります。 + +それぞれのエントリは8バイト(64ビット)の大きさであり、以下の形式です: + +ビット | 名前 | 意味 +------ | ---- | ------- +0 | present | このページはメモリ内にある +1 | writable | このページへの書き込みは許可されている +2 | user accessible | 0の場合、カーネルモードのみこのページにアクセスできる +3 | write through caching | 書き込みはメモリに対して直接行われる +4 | disable cache | このページにキャッシュを使わない +5 | accessed | このページが使われているとき、CPUはこのビットを1にする +6 | dirty | このページへの書き込みが行われたとき、CPUはこのビットを1にする +7 | huge page/null | P1とP4においては0で、P3においては1GiBのページを、P2においては2MiBのページを作る +8 | global | キャッシュにあるこのページはアドレス空間変更の際に初期化されない(CR4レジスタのPGEビットが1である必要がある) +9-11 | available | OSが自由に使える +12-51 | physical address | ページ単位にアラインされた、フレームまたは次のページテーブルの52bit物理アドレス +52-62 | available | OSが自由に使える +63 | no execute | このページにおいてプログラムを実行することを禁じる(EFERレジスタのNXEビットが1である必要がある) + +12-51ビットだけが物理フレームアドレスを格納するのに使われていて、残りのビットはフラグやオペレーティングシステムが自由に使うようになっていることがわかります。これが可能なのは、常に4096バイト単位のページに揃え (アライン) られたアドレス(ページテーブルか、対応づけられたフレームの先頭)を指しているからです。これは、0-11ビットは常にゼロであることを意味し、したがってこれらのビットを格納しておく必要はありません。アドレスを使用する前に、ハードウェアがそれらのビットをゼロとして(追加して)やれば良いからです。また、52-63ビットについても格納しておく必要はありません。なぜならx86_64アーキテクチャは52ビットの物理アドレスしかサポートしていないからです(仮想アドレスを48ビットしかサポートしていないのと似ています)。 + +上のフラグについてより詳しく見てみましょう: + +- `present`フラグは、対応付けられているページとそうでないページを区別します。このフラグは、メインメモリが一杯になったとき、ページを一時的にディスクにスワップしたいときに使うことができます。後でページがアクセスされたら、 **ページフォルト** という特別な例外が発生するので、オペレーティングシステムは不足しているページをディスクから読み出すことでこれに対応し、プログラムを再開します。 +- `writable`と`no execute`フラグはそれぞれ、このページの中身が書き込み可能かと、実行可能な命令であるかを制御します。 +- `accessed`と`dirty`フラグは、ページへの読み込みか書き込みが行われたときにCPUによって自動的に1にセットされます。この情報はオペレーティングシステムによって活用でき、例えば、どのページをスワップするかや、ページの中身が最後にディスクに保存されて以降に修正されたかを確認できます。 +- `write through caching`と`disable cache`フラグで、キャッシュの制御をページごとに独立して行うことができます。 +- `user accessible`フラグはページをユーザー空間のコードが利用できるようにします。このフラグが1になっていない場合、CPUがカーネルモードのときにのみアクセスできます。この機能は、ユーザ空間のプログラムが実行している間もカーネル(の使用しているメモリ)を対応付けたままにしておくことで、[システムコール][system calls]を高速化するために使うことができます。しかし、[Spectre]脆弱性を使うと、この機能があるにもかかわらず、ユーザ空間プログラムがこれらのページを読むことができてしまいます。 +- `global`フラグは、このページはすべてのアドレス空間で利用可能であり、よってアドレス空間の変更時に変換キャッシュ(TLBに関する下のセクションを読んでください)から取り除く必要がないことをハードウェアに伝えます。このフラグはカーネルコードをすべてのアドレス空間に対応付けるため、一般的に`user accsessible`フラグと一緒に使われます。 +- `huge page`フラグを使うと、レベル2か3のページが対応付けられたフレームを直接指すようにすることで、より大きいサイズのページを作ることができます。このビットが1のとき、ページの大きさは512倍になるので、レベル2のエントリの場合は2MiB = 512 * 4KiBに、レベル3のエントリの場合は1GiB = 512 * 2MiBにもなります。大きいページを使うことのメリットは、必要な変換キャッシュのラインの数やページテーブルの数が少なくなることです。 + +[system calls]: https://en.wikipedia.org/wiki/System_call +[Spectre]: https://en.wikipedia.org/wiki/Spectre_(security_vulnerability) + +`x86_64`クレートが[ページテーブル][page tables]とその[エントリ][entries]のための型を提供してくれているので、これらの構造体を私達自身で作る必要はありません。 + +[page tables]: https://docs.rs/x86_64/0.14.2/x86_64/structures/paging/page_table/struct.PageTable.html +[entries]: https://docs.rs/x86_64/0.14.2/x86_64/structures/paging/page_table/struct.PageTableEntry.html + +### トランスレーション・ルックアサイド・バッファ + +4層ページテーブルを使うと、仮想アドレスを変換するたびに4回メモリアクセスを行わないといけないので、変換のコストは大きくなります。性能改善のために、x86_64アーキテクチャは、直前数回の変換内容を **トランスレーション・ルックアサイド・バッファ (translation lookaside buffer, TLB)** と呼ばれるところにキャッシュします。これにより、前の変換がまだキャッシュされているなら、変換をスキップできます。 + +他のCPUキャッシュと異なり、TLBは完全に透明ではなく、ページテーブルの内容が変わったときに変換内容を更新したり取り除いたりしてくれません(訳注:キャッシュが透明 (transparent) であるとは、利用者がキャッシュの存在を意識する必要がないという意味)。つまり、カーネルがページテーブルを変更したときは、カーネル自らTLBを更新しないといけないということです。これを行うために、[`invlpg`]("invalidate page"、ページを無効化の意)という特別なCPU命令があります。これは指定されたページの変換をTLBから取り除き、次のアクセスの際に再び読み込まれるようにします。また、TLBは`CR3`レジスタを再設定することでもflushできます。`CR3`レジスタの再設定は、アドレス空間が変更されたという状況を模擬するのです。`x86_64`クレートの[`tlb`モジュール][`tlb` module]が、両方のやり方のRust関数を提供しています。 + +
    + +**訳注:** flushは「(溜まった水を)どっと流す」「(トイレなどを)水で洗い流す」という意味の言葉です。そのためコンピュータサイエンスにおいて「キャッシュなどに溜められていたデータを(場合によっては適切な出力先に書き込みながら)削除する」という意味を持つようになりました。ここではどこかに出力しているわけではないので、「初期化」と同じような意味と考えて差し支えないでしょう。 + +
    + +[`invlpg`]: https://www.felixcloutier.com/x86/INVLPG.html +[`tlb` module]: https://docs.rs/x86_64/0.14.2/x86_64/instructions/tlb/index.html + +ページテーブルを修正したときは毎回TLBをflushしないといけないということはしっかりと覚えておいてください。これを行わないと、CPUは古い変換を使いつづけるかもしれず、これはデバッグの非常に難しい、予測不能なバグに繋がるかもしれないためです。 + +## 実装 + +ひとつ言っていなかったことがあります:**わたしたちのカーネルはすでにページングを使っています**。[Rustでつくる最小のカーネル]["A minimal Rust Kernel"]の記事で追加したブートローダは、すでに私たちのカーネルのすべてのページを物理フレームに対応付けるような4層ページ階層構造を設定しているのです。ブートローダがこれを行う理由は、x86_64の64ビットモードにおいてページングは必須となっているからです。 + +["A minimal Rust kernel"]: @/edition-2/posts/02-minimal-rust-kernel/index.ja.md#butoimeziwozuo-ru + +つまり、私達がカーネルにおいて使ってきたすべてのメモリアドレスは、仮想アドレスだったということです。アドレス`0xb8000`にあるVGAバッファへのアクセスが上手くいっていたのは、ひとえにブートローダがこのメモリページを **恒等対応** させていた、つまり、仮想ページ`0xb8000`を物理フレーム`0xb8000`に対応させていたからです。 + +ページングにより、境界外メモリアクセスをしてもおかしな物理メモリに書き込むのではなくページフォルト例外を起こすようになっているため、私達のカーネルはすでに比較的安全になっていました。ブートローダはそれぞれのページに正しい権限を設定することさえしてくれるので、コードを含むページだけが実行可能であり、データを含むページだけが書き込み可能になっています。 + +### ページフォルト + +カーネルの外のメモリにアクセスすることによって、ページフォルトを引き起こしてみましょう。まず、通常の[ダブルフォルト][double fault]ではなくページフォルト例外が得られるように、ページフォルト処理関数 (ハンドラ) を作ってIDTに追加しましょう: + +[double fault]: @/edition-2/posts/06-double-faults/index.ja.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); // ここを追加 + + idt + }; +} + +use x86_64::structures::idt::PageFaultErrorCode; +use crate::hlt_loop; + +extern "x86-interrupt" fn page_fault_handler( + stack_frame: 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によって自動的に設定されており、その値はアクセスされページフォルトを引き起こした仮想アドレスになっています。`x86_64`クレートの[`Cr2::read`]関数を使ってこれを読み込み出力します。[`PageFaultErrorCode`]型は、ページフォルトを引き起こしたメモリアクセスの種類についてより詳しい情報を提供します(例えば、読み込みと書き込みのどちらによるものなのか、など)。そのため、これも出力します。ページフォルトを解決しない限り実行を継続することはできないので、最後は[`hlt_loop`]に入ります。 + +[`CR2`]: https://en.wikipedia.org/wiki/Control_register#CR2 +[`Cr2::read`]: https://docs.rs/x86_64/0.14.2/x86_64/registers/control/struct.Cr2.html#method.read +[`PageFaultErrorCode`]: https://docs.rs/x86_64/0.14.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(); + + // ここを追加 + let ptr = 0xdeadbeaf as *mut u32; + unsafe { *ptr = 42; } + + // ここはこれまでと同じ + #[cfg(test)] + test_main(); + + println!("It did not crash!"); + blog_os::hlt_loop(); +} +``` + +これを実行すると、ページフォルトハンドラが呼びだされたのを見ることができます: + +![EXCEPTION: Page Fault, Accessed Address: VirtAddr(0xdeadbeaf), Error Code: CAUSED_BY_WRITE, InterruptStackFrame: {…}](qemu-page-fault.png) + +`CR2`レジスタは確かに私達がアクセスしようとしていたアドレスである`0xdeadbeaf`を格納しています。エラーコードが[`CAUSED_BY_WRITE`]なので、この障害 (フォルト) write (書き込み) 操作の実行中に発生したのだと分かります。更に、[1にセットされていないビット][`PageFaultErrorCode`]からも情報を得ることができます。例えば、`PROTECTION_VIOLATION`フラグが1にセットされていないことから、ページフォルトは対象のページが存在しなかったために発生したのだと分かります。 + +[`CAUSED_BY_WRITE`]: https://docs.rs/x86_64/0.14.2/x86_64/structures/idt/struct.PageFaultErrorCode.html#associatedconstant.CAUSED_BY_WRITE + +ページフォルトを起こした時点での命令ポインタは`0x2031b2`であるので、このアドレスはコードページを指しているとわかります。コードページはブートローダによって読み込み専用に指定されているので、このアドレスからの読み込みは大丈夫ですが、このページへの書き込みはページフォルトを起こします。`0xdeadbeaf`へのポインタを`0x2031b2`に変更して、これを試してみましょう。 + +```rust +// 注意:実際のアドレスは個々人で違うかもしれません。 +// あなたのページフォルトハンドラが報告した値を使ってください。 +let ptr = 0x2031b2 as *mut u32; + +// コードページから読み込む +unsafe { let x = *ptr; } +println!("read worked"); + +// コードページへと書き込む +unsafe { *ptr = 42; } +println!("write worked"); +``` + +最後の2行をコメントアウトすると、読み込みアクセスだけになるので実行は成功しますが、そうしなかった場合ページフォルトが発生します: + +![QEMU with output: "read worked, EXCEPTION: Page Fault, Accessed Address: VirtAddr(0x2031b2), Error Code: PROTECTION_VIOLATION | CAUSED_BY_WRITE, InterruptStackFrame: {…}"](qemu-page-fault-protection.png) + +"read worked"というメッセージが表示されますが、これは読み込み操作が何のエラーも発生させなかったことを示しています。しかし、"write worked"のメッセージではなく、ページフォルトが発生してしまいました。今回は[`CAUSED_BY_WRITE`]フラグに加えて[`PROTECTION_VIOLATION`]フラグがセットされています。これは、ページは存在していたものの、それに対する今回の操作が許可されていなかったということを示します。今回の場合、ページへの書き込みは、コードページが読み込み専用に指定されているため許可されていませんでした。 + +[`PROTECTION_VIOLATION`]: https://docs.rs/x86_64/0.14.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(…), hlt_loop() などが続く +} +``` + +`x86_64`クレートの[`Cr3::read`]関数は、現在有効なレベル4ページテーブルを`CR3`レジスタから読みとって返します。この関数は[`PhysFrame`]型と[`Cr3Flags`]型のタプルを返します。私達はフレームにしか興味がないので、タプルの2つ目の要素は無視しました。 + +[`Cr3::read`]: https://docs.rs/x86_64/0.14.2/x86_64/registers/control/struct.Cr3.html#method.read +[`PhysFrame`]: https://docs.rs/x86_64/0.14.2/x86_64/structures/paging/frame/struct.PhysFrame.html +[`Cr3Flags`]: https://docs.rs/x86_64/0.14.2/x86_64/registers/control/struct.Cr3Flags.html + +これを実行すると、以下の出力を得ます: + +``` +Level 4 page table at: PhysAddr(0x1000) +``` + +というわけで、現在有効なレベル4ページテーブルは、[`PhysAddr`]ラッパ型が示すように、 **物理** メモリのアドレス`0x1000`に格納されています。ここで疑問が生まれます:このテーブルに私達のカーネルからアクセスするにはどうすればいいのでしょう? + +[`PhysAddr`]: https://docs.rs/x86_64/0.14.2/x86_64/addr/struct.PhysAddr.html + +ページングが有効なとき、物理メモリに直接アクセスすることはできません。もしそれができたとしたら、プログラムは容易くメモリ保護を回避して他のプログラムのメモリにアクセスできてしまうだろうからです。ですので、テーブルにアクセスする唯一の方法は、アドレス`0x1000`の物理フレームに対応づけられているような仮想ページにアクセスすることです。ページテーブルの存在するフレームへの対応づけは(実用上も必要になる)一般的な問題です。なぜなら、例えば新しいスレッドのためにスタックを割り当てるときなど、カーネルは日常的にページテーブルにアクセスする必要があるためです。 + +この問題への解決策は次の記事で詳細に論じます。 + +## まとめ + +この記事では2つのメモリ保護技術を紹介しました:セグメンテーションとページングです。前者は可変サイズのメモリ領域を使用するため外部断片化の問題が存在するのに対し、後者は固定サイズのページを使用するためアクセス権限に関して遥かに細やかな制御が可能となっていました。 + +ページングは、(仮想メモリと物理メモリの)対応情報を1層以上のページテーブルに格納します。x86_64アーキテクチャにおいては4層ページテーブルが使用され、ページサイズは4KiBです。ハードウェアは自動的にページテーブルを辿り、変換の結果をトランスレーション・ルックアサイド・バッファ (TLB) にキャッシュします。このバッファは自動的に更新されない(「透明ではない」)ので、ページテーブルの変更時には明示的にflushする必要があります。 + +私達のカーネルは既にページングによって動いており、不正なメモリアクセスはページフォルト例外を発生させるということを学びました。現在有効なページテーブルへとアクセスしたかったのですが、CR3レジスタに格納されている物理アドレスはカーネルから直接アクセスできないものであるため、それはできませんでした。 + +## 次は? + +次の記事では、私達のカーネルをページングに対応させる方法について説明します。私達のカーネルから物理メモリにアクセスする幾つかの方法を示すので、これらを用いれば私達のカーネルが動作しているページテーブルにアクセスできます。そうすると、仮想アドレスを物理アドレスに変換する関数を実装でき、ページテーブルに新しい対応づけを作れるようになります。 diff --git a/blog/content/edition-2/posts/08-paging-introduction/index.md b/blog/content/edition-2/posts/08-paging-introduction/index.md index 172dc176..283b339a 100644 --- a/blog/content/edition-2/posts/08-paging-introduction/index.md +++ b/blog/content/edition-2/posts/08-paging-introduction/index.md @@ -16,6 +16,7 @@ This blog is openly developed on [GitHub]. If you have any problems or questions [GitHub]: https://github.com/phil-opp/blog_os [at the bottom]: #comments + [post branch]: https://github.com/phil-opp/blog_os/tree/post-08 @@ -235,8 +236,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.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 +[page tables]: https://docs.rs/x86_64/0.14.2/x86_64/structures/paging/page_table/struct.PageTable.html +[entries]: https://docs.rs/x86_64/0.14.2/x86_64/structures/paging/page_table/struct.PageTableEntry.html ### The Translation Lookaside Buffer @@ -245,7 +246,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.13.2/x86_64/instructions/tlb/index.html +[`tlb` module]: https://docs.rs/x86_64/0.14.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. @@ -284,7 +285,7 @@ use x86_64::structures::idt::PageFaultErrorCode; use crate::hlt_loop; extern "x86-interrupt" fn page_fault_handler( - stack_frame: &mut InterruptStackFrame, + stack_frame: InterruptStackFrame, error_code: PageFaultErrorCode, ) { use x86_64::registers::control::Cr2; @@ -300,8 +301,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.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 +[`Cr2::read`]: https://docs.rs/x86_64/0.14.2/x86_64/registers/control/struct.Cr2.html#method.read +[`PageFaultErrorCode`]: https://docs.rs/x86_64/0.14.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 +336,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.13.2/x86_64/structures/idt/struct.PageFaultErrorCode.html#associatedconstant.CAUSED_BY_WRITE +[`CAUSED_BY_WRITE`]: https://docs.rs/x86_64/0.14.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 +360,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.13.2/x86_64/structures/idt/struct.PageFaultErrorCode.html#associatedconstant.PROTECTION_VIOLATION +[`PROTECTION_VIOLATION`]: https://docs.rs/x86_64/0.14.2/x86_64/structures/idt/struct.PageFaultErrorCode.html#associatedconstant.PROTECTION_VIOLATION ### Accessing the Page Tables @@ -385,9 +386,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.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 +[`Cr3::read`]: https://docs.rs/x86_64/0.14.2/x86_64/registers/control/struct.Cr3.html#method.read +[`PhysFrame`]: https://docs.rs/x86_64/0.14.2/x86_64/structures/paging/frame/struct.PhysFrame.html +[`Cr3Flags`]: https://docs.rs/x86_64/0.14.2/x86_64/registers/control/struct.Cr3Flags.html When we run it, we see the following output: @@ -397,7 +398,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.13.2/x86_64/addr/struct.PhysAddr.html +[`PhysAddr`]: https://docs.rs/x86_64/0.14.2/x86_64/addr/struct.PhysAddr.html Accessing physical memory directly is not possible when paging is active, since programs could easily circumvent memory protection and access memory of other programs otherwise. So the only way to access the table is through some virtual page that is mapped to the physical frame at address `0x1000`. This problem of creating mappings for page table frames is a general problem, since the kernel needs to access the page tables regularly, for example when allocating a stack for a new thread. diff --git a/blog/content/edition-2/posts/09-paging-implementation/index.ja.md b/blog/content/edition-2/posts/09-paging-implementation/index.ja.md new file mode 100644 index 00000000..bfb3d52b --- /dev/null +++ b/blog/content/edition-2/posts/09-paging-implementation/index.ja.md @@ -0,0 +1,1017 @@ ++++ +title = "ページングの実装" +weight = 9 +path = "ja/paging-implementation" +date = 2019-03-14 + +[extra] +chapter = "Memory Management" +translation_based_on_commit = "27ab4518acbb132e327ed4f4f0508393e9d4d684" +translators = ["woodyZootopia", "garasubo"] ++++ + +この記事では私達のカーネルをページングに対応させる方法についてお伝えします。まずページテーブルの物理フレームにカーネルがアクセスできるようにする様々な方法を示し、それらの利点と欠点について議論します。次にアドレス変換関数を、ついで新しい対応付け (マッピング) を作るための関数を実装します。 + + + +このブログの内容は [GitHub] 上で公開・開発されています。何か問題や質問などがあれば issue をたててください (訳注: リンクは原文(英語)のものになります)。また[こちら][at the bottom]にコメントを残すこともできます。この記事の完全なソースコードは[`post-09` ブランチ][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-09 + + + +## 導入 + +[1つ前の記事][previous post]ではページングの概念を説明しました。セグメンテーションと比較することによってページングのメリットを示し、ページングとページテーブルの仕組みを説明し、そして`x86_64`における4層ページテーブルの設計を導入しました。ブートローダはすでにページテーブルの階層構造を設定してしまっているので、私達のカーネルは既に仮想アドレス上で動いているということを学びました。これにより、不正なメモリアクセスは、任意の物理メモリを書き換えてしまうのではなくページフォルト例外を発生させるので、安全性が向上しています。 + +[previous post]: @/edition-2/posts/08-paging-introduction/index.ja.md + +記事の最後で、[ページテーブルにカーネルからアクセスできない][end of previous post]という問題が起きていました。この問題は、ページテーブルは物理メモリ内に格納されている一方、私達のカーネルは既に仮想アドレス上で実行されているために発生します。この記事ではその続きとして、私達のカーネルからページテーブルのフレームにアクセスするための様々な方法を探ります。それぞれの方法の利点と欠点を議論し、カーネルに採用する手法を決めます。 + +[end of previous post]: @/edition-2/posts/08-paging-introduction/index.ja.md#peziteburuhenoakusesu + +この方法を実装するには、ブートローダーからの補助が必要になるので、まずこれに設定を加えます。その後で、ページテーブルの階層構造を移動して、仮想アドレスを物理アドレスに変換する関数を実装します。最後に、ページテーブルに新しいマッピングを作る方法と、それを作るための未使用メモリを見つける方法を学びます。 + +## ページテーブルにアクセスする + +私達のカーネルからページテーブルにアクセスするのは案外難しいです。この問題を理解するために、前回の記事の4層ページテーブルをもう一度見てみましょう: + +![An example 4-level page hierarchy with each page table shown in physical memory](../paging-introduction/x86_64-page-table-translation.svg) + +ここで重要なのは、それぞれのページテーブルのエントリは次のテーブルの**物理**アドレスであるということです。これにより、それらのアドレスに対しては変換せずにすみます。もしこの変換が行われたとしたら、性能的にも良くないですし、容易に変換の無限ループに陥りかねません。 + +問題は、私達のカーネル自体も仮想アドレスの上で動いているため、カーネルから直接物理アドレスにアクセスすることができないということです。例えば、アドレス`4KiB`にアクセスしたとき、私達は**仮想**アドレス`4KiB`にアクセスしているのであって、レベル4ページテーブルが格納されている**物理**アドレス`4KiB`にアクセスしているのではありません。物理アドレス`4KiB`にアクセスしたいなら、それにマップさせられている何らかの仮想アドレスを通じてのみ可能です。 + +そのため、ページテーブルのフレームにアクセスするためには、どこかの仮想ページをそれにマッピングしなければいけません。このような、任意のページテーブルのフレームにアクセスできるようにしてくれるマッピングを作る方法にはいくつかあります。 + +### 恒等マッピング + +シンプルな方法として、**すべてのページテーブルを恒等対応 (マップ) させる**ということが考えられるでしょう: + +![A virtual and a physical address space with various virtual pages mapped to the physical frame with the same address](identity-mapped-page-tables.svg) + +この例では、恒等マップしたいくつかのページテーブルのフレームが見てとれます。こうすることで、ページテーブルの物理アドレスは仮想アドレスと同じ値になり、よってCR3レジスタから始めることで全ての階層のページテーブルに簡単にアクセスできます。 + +しかし、この方法では仮想アドレス空間が散らかってしまい、大きいサイズの連続したメモリを見つけることが難しくなります。例えば、上の図において、[ファイルをメモリにマップする][memory-mapping a file]ために1000KiBの大きさの仮想メモリ領域を作りたいとします。`28KiB`を始点として領域を作ろうとすると、`1004KiB`のところで既存のページと衝突してしまうのでうまくいきません。そのため、`1008KiB`のような、十分な広さでマッピングのない領域が見つかるまで更に探さないといけません。これは[セグメンテーション][segmentation]の時に見た断片化の問題に似ています。 + +[memory-mapping a file]: https://en.wikipedia.org/wiki/Memory-mapped_file +[segmentation]: @/edition-2/posts/08-paging-introduction/index.ja.md#duan-pian-hua-fragmentation + +同様に、新しいページテーブルを作ることもずっと難しくなります。なぜなら、対応するページがまだ使われていない物理フレームを見つけないといけないからです。例えば、メモリマップト (に対応づけられた) ファイルのために`1008KiB`から1000KiBにわたって仮想メモリを占有したとしましょう。すると、物理アドレス`1000KiB`から`2008KiB`までのフレームは、もう恒等マッピングを作ることができないので使用することができません。 + +### 固定オフセットのマッピング + +仮想アドレス空間を散らかしてしまうという問題を回避するために、**ページテーブルのマッピングのために別のメモリ領域を使う**ことができます。ページテーブルを恒等マップさせる代わりに、仮想アドレス空間で一定の補正値 (オフセット) をおいてマッピングしてみましょう。例えば、オフセットを10TiBにしてみましょう: + +![The same figure as for the identity mapping, but each mapped virtual page is offset by 10 TiB.](page-tables-mapped-at-offset.svg) + +`10TiB`から`10TiB+物理メモリ全体の大きさ`の範囲の仮想メモリをページテーブルのマッピング専用に使うことで、恒等マップのときに存在していた衝突問題を回避しています。このように巨大な領域を仮想アドレス空間内に用意するのは、仮想アドレス空間が物理メモリの大きさより遥かに大きい場合にのみ可能です。x86_64で用いられている48bit(仮想)アドレス空間は256TiBもの大きさがあるので、これは問題ではありません。 + +この方法では、新しいページテーブルを作るたびに新しいマッピングを作る必要があるという欠点があります。また、他のアドレス空間のページテーブルにアクセスすることができると新しいプロセスを作るときに便利なのですが、これも不可能です。 + +### 物理メモリ全体をマップする {#map-the-complete-physical-memory} + +これらの問題はページテーブルのフレームだけと言わず**物理メモリ全体をマップして**しまえば解決します: + +![The same figure as for the offset mapping, but every physical frame has a mapping (at 10TiB + X) instead of only page table frames.](map-complete-physical-memory.svg) + +この方法を使えば、私達のカーネルは他のアドレス空間を含め任意の物理メモリにアクセスできます。用意する仮想メモリの範囲は以前と同じであり、違うのは全てのページがマッピングされているということです。 + +この方法の欠点は、物理メモリへのマッピングを格納するために、追加でページテーブルが必要になるところです。これらのページテーブルもどこかに格納されなければならず、したがって物理メモリの一部を占有することになります。これはメモリの量が少ないデバイスにおいては問題となりえます。 + +しかし、x86_64においては、通常の4KiBサイズのページに代わって、大きさ2MiBの[huge page][huge pages]をマッピングに使うことができます。こうすれば、例えば32GiBの物理メモリをマップするのにはレベル3テーブル1個とレベル2テーブル32個があればいいので、たったの132KiBしか必要ではありません。huge pagesは、トランスレーション・ルックアサイド・バッファ (TLB) のエントリをあまり使わないので、キャッシュ的にも効率が良いです。 + +[huge pages]: https://en.wikipedia.org/wiki/Page_%28computer_memory%29#Multiple_page_sizes + +### 一時的な対応 (マッピング) + +物理メモリの量が非常に限られたデバイスについては、アクセスする必要があるときだけ**ページテーブルのフレームを一時的にマップする**という方法が考えられます。そのような一時的なマッピングを作りたいときには、たった一つだけ恒等マップさせられたレベル1テーブルがあれば良いです: + +![A virtual and a physical address space with an identity mapped level 1 table, which maps its 0th entry to the level 2 table frame, thereby mapping that frame to page with address 0](temporarily-mapped-page-tables.svg) + +この図におけるレベル1テーブルは仮想アドレス空間の最初の2MiBを制御しています。なぜなら、このテーブルにはCR3レジスタから始めて、レベル4、3、2のページテーブルの0番目のエントリを辿ることで到達できるからです。その8番目のエントリは、アドレス`32 KiB`の仮想アドレスページをアドレス`32 KiB`の物理アドレスページにマップするので、レベル1テーブル自体を恒等マップしています。この図ではその恒等マッピングを`32 KiB`のところの横向きの(茶色の)矢印で表しています。 + +恒等マップさせたレベル1テーブルに書き込むことによって、カーネルは最大511個の一時的なマッピングを作ることができます(512から、恒等マッピングに必要な1つを除く)。上の例では、カーネルは2つの一時的なマッピングを作りました: + +- レベル1テーブルの0番目のエントリをアドレス`24 KiB`のフレームにマップすることで、破線の矢印で示されているように`0 KiB`の仮想ページからレベル2ページテーブルの物理フレームへの一時的なマッピングを行いました。 +- レベル1テーブルの9番目のエントリをアドレス`4 KiB`のフレームにマップすることで、破線の矢印で示されているように`36 KiB`の仮想ページからレベル4ページテーブルの物理フレームへの一時的なマッピングを行いました。 + +これで、カーネルは`0 KiB`に書き込むことによってレベル2ページテーブルに、`36 KiB`に書き込むことによってレベル4ページテーブルにアクセスできるようになりました。 + +任意のページテーブルに一時的なマッピングを用いてアクセスする手続きは以下のようになるでしょう: + +- 恒等マッピングしているレベル1テーブルのうち、使われていないエントリを探す。 +- そのエントリを私達のアクセスしたいページテーブルの物理フレームにマップする。 +- そのエントリにマップされている仮想ページを通じて、対象のフレームにアクセスする。 +- エントリを未使用に戻すことで、一時的なマッピングを削除する。 + +この方法では、同じ512個の仮想ページをマッピングを作成するために再利用するため、物理メモリは4KiBしか必要としません。欠点としては、やや面倒であるということが言えるでしょう。特に、新しいマッピングを作る際に複数のページテーブルの変更が必要になるかもしれず、上の手続きを複数回繰り返さなくてはならないかもしれません。 + +### 再帰的ページテーブル + +他に興味深いアプローチとして**再帰的にページテーブルをマップする**方法があり、この方法では追加のページテーブルは一切不要です。発想としては、レベル4ページテーブルのエントリのどれかをレベル4ページテーブル自体にマップするのです。こうすることにより、仮想アドレス空間の一部を予約しておき、現在及び将来のあらゆるページテーブルフレームをその空間にマップしているのと同じことになります。 + +これがうまく行く理由を説明するために、例を見てみましょう: + +![An example 4-level page hierarchy with each page table shown in physical memory. Entry 511 of the level 4 page is mapped to frame 4KiB, the frame of the level 4 table itself.](recursive-page-table.png) + +[この記事の最初での例][example at the beginning of this post]との唯一の違いは、レベル4テーブルの511番目に、物理フレーム`4 KiB`すなわちレベル4テーブル自体のフレームにマップされたエントリが追加されていることです。 + +[example at the beginning of this post]: #peziteburuniakusesusuru + +CPUにこのエントリを辿らせるようにすると、レベル3テーブルではなく、そのレベル4テーブルに再び到達します。これは再帰関数(自らを呼び出す関数)に似ているので、**再帰的 (recursive) ページテーブル**と呼ばれます。CPUはレベル4テーブルのすべてのエントリはレベル3テーブルを指していると思っているので、CPUはいまレベル4テーブルをレベル3テーブルとして扱っているということに注目してください。これがうまく行くのは、x86_64においてはすべてのレベルのテーブルが全く同じレイアウトを持っているためです。 + +実際に変換を始める前に、この再帰エントリを1回以上たどることで、CPUのたどる階層の数を短くできます。例えば、一度再帰エントリを辿ったあとでレベル3テーブルに進むと、CPUはレベル3テーブルをレベル2テーブルだと思い込みます。同様に、レベル2テーブルをレベル1テーブルだと、レベル1テーブルをマップされた(物理)フレームだと思います。CPUがこれを物理フレームだと思っているということは、レベル1ページテーブルを読み書きできるということを意味します。下の図はこの5回の変換ステップを示しています: + +![The above example 4-level page hierarchy with 5 arrows: "Step 0" from CR4 to level 4 table, "Step 1" from level 4 table to level 4 table, "Step 2" from level 4 table to level 3 table, "Step 3" from level 3 table to level 2 table, and "Step 4" from level 2 table to level 1 table.](recursive-page-table-access-level-1.png) + +同様に、変換の前に再帰エントリを2回たどることで、階層移動の回数を2回に減らせます: + +![The same 4-level page hierarchy with the following 4 arrows: "Step 0" from CR4 to level 4 table, "Steps 1&2" from level 4 table to level 4 table, "Step 3" from level 4 table to level 3 table, and "Step 4" from level 3 table to level 2 table.](recursive-page-table-access-level-2.png) + +ステップごとにこれを見てみましょう:まず、CPUはレベル4テーブルの再帰エントリをたどり、レベル3テーブルに着いたと思い込みます。同じ再帰エントリを再びたどり、レベル2テーブルに着いたと考えます。しかし実際にはまだレベル4テーブルから動いていません。CPUが異なるエントリをたどると、レベル3テーブルに到着するのですが、CPUはレベル1にすでにいるのだと思っています。そのため、次のエントリはレベル2テーブルを指しているのですが、CPUはマップされた物理フレームを指していると思うので、私達はレベル2テーブルを読み書きできるというわけです。 + +レベル3や4のテーブルにアクセスするのも同じやり方でできます。レベル3テーブルにアクセスするためには、再帰エントリを3回たどることでCPUを騙し、すでにレベル1テーブルにいると思い込ませます。そこで別のエントリをたどりレベル3テーブルに着くと、CPUはそれをマップされたフレームとして扱います。レベル4テーブル自体にアクセスするには、再帰エントリを4回辿ればCPUはそのレベル4テーブル自体をマップされたフレームとして扱ってくれるというわけです(下の青紫の矢印)。 + +![The same 4-level page hierarchy with the following 3 arrows: "Step 0" from CR4 to level 4 table, "Steps 1,2,3" from level 4 table to level 4 table, and "Step 4" from level 4 table to level 3 table. In blue the alternative "Steps 1,2,3,4" arrow from level 4 table to level 4 table.](recursive-page-table-access-level-3.png) + +この概念を理解するのは難しいかもしれませんが、実際これは非常にうまく行くのです。 + +下のセクションでは、再帰エントリをたどるための仮想アドレスを構成する方法について説明します。私達の(OSの)実装には再帰的ページングは使わないので、これを読まずに記事の続きを読み進めても構いません。もし興味がおありでしたら、下の「アドレス計算」をクリックして展開してください。 + +--- + +
    +

    アドレス計算

    + +実際の変換の前に再帰的移動を1回または複数回行うことですべての階層のテーブルにアクセスできるということを見てきました。4つのテーブルそれぞれのどのインデックスが使われるかは仮想アドレスから直接計算されていましたから、再帰エントリを使うためには特別な仮想アドレスを作り出す必要があります。ページテーブルのインデックスは仮想アドレスから以下のように計算されていたことを思い出してください: + +![Bits 0–12 are the page offset, bits 12–21 the level 1 index, bits 21–30 the level 2 index, bits 30–39 the level 3 index, and bits 39–48 the level 4 index](../paging-introduction/x86_64-table-indices-from-address.svg) + +あるページをマップしているレベル1テーブルにアクセスしたいとします。上で学んだように、このためには再帰エントリを1度辿ってからレベル4,3,2のインデックスへと続けていく必要があります。これをするために、それぞれのアドレスブロックを一つ右にずらし、レベル4のインデックスがあったところに再帰エントリのインデックスをセットします: + +![Bits 0–12 are the offset into the level 1 table frame, bits 12–21 the level 2 index, bits 21–30 the level 3 index, bits 30–39 the level 4 index, and bits 39–48 the index of the recursive entry](table-indices-from-address-recursive-level-1.svg) + +そのページのレベル2テーブルにアクセスしたい場合、それぞれのブロックを2つ右にずらし、レベル4と3のインデックスがあったところに再帰エントリのインデックスをセットします: + +![Bits 0–12 are the offset into the level 2 table frame, bits 12–21 the level 3 index, bits 21–30 the level 4 index, and bits 30–39 and bits 39–48 are the index of the recursive entry](table-indices-from-address-recursive-level-2.svg) + +レベル3テーブルにアクセスする場合、それぞれのブロックを3つ右にずらし、レベル4,3,2のインデックスがあったところに再帰インデックスを使います: + +![Bits 0–12 are the offset into the level 3 table frame, bits 12–21 the level 4 index, and bits 21–30, bits 30–39 and bits 39–48 are the index of the recursive entry](table-indices-from-address-recursive-level-3.svg) + +最後に、レベル4テーブルにはそれぞれのブロックを4ブロックずらし、オフセットを除いてすべてのアドレスブロックに再帰インデックスを使うことでアクセスできます: + +![Bits 0–12 are the offset into the level l table frame and bits 12–21, bits 21–30, bits 30–39 and bits 39–48 are the index of the recursive entry](table-indices-from-address-recursive-level-4.svg) + +これで、4つの階層すべてのページテーブルの仮想アドレスを計算できます。また、インデックスをページテーブルエントリのサイズ倍、つまり8倍することによって、特定のページテーブルエントリを指すアドレスを計算できます。 + +下の表は、それぞれの種類のフレームにアクセスするためのアドレス構造をまとめたものです: + +……の仮想アドレス | アドレス構造([8進][octal]) +------------------- | ------------------------------- +ページ | `0o_SSSSSS_AAA_BBB_CCC_DDD_EEEE` +レベル1テーブルエントリ | `0o_SSSSSS_RRR_AAA_BBB_CCC_DDDD` +レベル2テーブルエントリ | `0o_SSSSSS_RRR_RRR_AAA_BBB_CCCC` +レベル3テーブルエントリ | `0o_SSSSSS_RRR_RRR_RRR_AAA_BBBB` +レベル4テーブルエントリ | `0o_SSSSSS_RRR_RRR_RRR_RRR_AAAA` + +[octal]: https://en.wikipedia.org/wiki/Octal + +ただし、`AAA`がレベル4インデックス、`BBB`がレベル3インデックス、`CCC`がレベル2インデックス、`DDD`がマップされたフレームのレベル1インデックス、`EEE`がオフセットです。`RRR`が再帰エントリのインデックスです。インデックス(3ケタ)をオフセット(4ケタ)に変換するときは、8倍(ページテーブルエントリのサイズ倍)しています。 + +`SSSSS`は符号拡張ビットで、すなわち47番目のビットのコピーです。これはx86_64におけるアドレスの特殊な要求の一つです。これは[前回の記事][sign extension]で説明しました。 + +[sign extension]: @/edition-2/posts/08-paging-introduction/index.ja.md#x86-64niokerupezingu + +[8進][octal]数を用いたのは、8進数の1文字が3ビットを表すため、9ビットからなるそれぞれのページテーブルをきれいに分けることができるためです。4ビットからなる16進ではこうはいきません。 + +##### Rustのコードでは…… + +これらのアドレスをRustのコードで構成するには、ビット演算を用いるとよいです: + +```rust +// この仮想アドレスに対応するページテーブルにアクセスしたい +let addr: usize = […]; + +let r = 0o777; // 再帰インデックス +let sign = 0o177777 << 48; // 符号拡張 + +// 変換したいアドレスのページテーブルインデックスを取得する +let l4_idx = (addr >> 39) & 0o777; // レベル4インデックス +let l3_idx = (addr >> 30) & 0o777; // レベル3インデックス +let l2_idx = (addr >> 21) & 0o777; // レベル2インデックス +let l1_idx = (addr >> 12) & 0o777; // レベル1インデックス +let page_offset = addr & 0o7777; + +// テーブルアドレスを計算する +let level_4_table_addr = + sign | (r << 39) | (r << 30) | (r << 21) | (r << 12); +let level_3_table_addr = + sign | (r << 39) | (r << 30) | (r << 21) | (l4_idx << 12); +let level_2_table_addr = + sign | (r << 39) | (r << 30) | (l4_idx << 21) | (l3_idx << 12); +let level_1_table_addr = + sign | (r << 39) | (l4_idx << 30) | (l3_idx << 21) | (l2_idx << 12); +``` + +上のコードは、レベル4エントリの最後(インデックス`0o777`すなわち511)が再帰マッピングしていると仮定しています。この仮定は正しくないので,このコードは動作しません。ブートローダに再帰マッピングを設定させる方法については後述します。 + +ビット演算を自前で行う代わりに、`x86_64`クレートの[`RecursivePageTable`]型を使うこともできます。これは様々なページ操作の安全な抽象化を提供します。例えば、以下のコードは仮想アドレスをマップされた物理アドレスに変換する方法を示しています。 + +[`RecursivePageTable`]: https://docs.rs/x86_64/0.14.2/x86_64/structures/paging/mapper/struct.RecursivePageTable.html + +```rust +// in src/memory.rs + +use x86_64::structures::paging::{Mapper, Page, PageTable, RecursivePageTable}; +use x86_64::{VirtAddr, PhysAddr}; + +/// レベル4アドレスからRecursivePageTableインスタンスをつくる +let level_4_table_addr = […]; +let level_4_table_ptr = level_4_table_addr as *mut PageTable; +let recursive_page_table = unsafe { + let level_4_table = &mut *level_4_table_ptr; + RecursivePageTable::new(level_4_table).unwrap(); +} + + +/// 与えられた仮想アドレスの物理アドレスを取得する +let addr: u64 = […] +let addr = VirtAddr::new(addr); +let page: Page = Page::containing_address(addr); + +// 変換を実行する +let frame = recursive_page_table.translate_page(page); +frame.map(|frame| frame.start_address() + u64::from(addr.page_offset())) +``` + +繰り返しになりますが、このコード(が正しく実行される)には正しい再帰マッピングがなされていることが必要となります。そのようなマッピングがあるのなら、空欄になっている`level_4_table_addr`は最初のコード例を使って計算すればよいです。 + +
    + +--- + +再帰的ページングは、ページテーブルのたった一つのマッピングがいかに強力に使えるかを示す興味深いテクニックです。比較的実装するのが簡単であり、ほとんど設定も必要でない(一つ再帰エントリを作るだけ)ので、ページングを使って最初に実装するのに格好の対象でしょう。 + +しかし、いくつか欠点もあります: + +- 大量の仮想メモリ領域(512GiB)を占有してしまう。私達の使っている48bitアドレス空間は巨大なのでこのことはさしたる問題にはなりませんが、キャッシュの挙動が最適でなくなってしまうかもしれません。 +- 現在有効なアドレス空間にしか簡単にはアクセスできない。他のアドレス空間にアクセスするのは再帰エントリを変更することで可能ではあるものの、もとに戻すためには一時的なマッピングが必要。これを行う方法については[カーネルをリマップする][_Remap The Kernel_](未訳、また旧版のため情報が古い)という記事を読んでください。 +- x86のページテーブルの方式に強く依存しており、他のアーキテクチャでは動作しないかもしれない。 + +[_Remap The Kernel_]: https://os.phil-opp.com/remap-the-kernel/#overview + +## ブートローダによる補助 + +これらのアプローチはすべて、準備のためにページテーブルに対する修正が必要になります。例えば、物理メモリへのマッピングを作ったり、レベル4テーブルのエントリを再帰的にマッピングしたりなどです。問題は、これらの必要なマッピングを作るためには、すでにページテーブルにアクセスできるようになっていなければいけないということです。 + +つまり、私達のカーネルが使うページテーブルを作っている、ブートローダの手助けが必要になるということです。ブートローダはページテーブルにアクセスできますから、私達の必要とするどんなマッピングも作れます。`bootloader`クレートは上の2つのアプローチをどちらもサポートしており、現在の実装においては[cargoのfeatures][cargo features]を使ってこれらをコントロールします。 + +[cargo features]: https://doc.rust-lang.org/cargo/reference/features.html#the-features-section + +- `map_physical_memory` featureを使うと、全物理メモリを仮想アドレス空間のどこかにマッピングします。そのため、カーネルはすべての物理メモリにアクセスでき、[上で述べた方法に従って物理メモリ全体をマップする](#map-the-complete-physical-memory)ことができます。 +- `recursive_page_table` featureでは、ブートローダはレベル4ページテーブルのエントリを再帰的にマッピングします。これによりカーネルは[再帰的ページテーブル](#zai-gui-de-peziteburu)で述べた方法に従ってページテーブルにアクセスすることができます。 + +私達のカーネルには、シンプルでプラットフォーム非依存かつ(ページテーブルのフレームでないメモリにもアクセスできるので)より強力である1つ目の方法を採ることにします。必要なブートローダの機能 (feature) を有効化するために、`map_physical_memory` featureを`bootloader`のdependencyに追加します。 + + +```toml +[dependencies] +bootloader = { version = "0.9.8", features = ["map_physical_memory"]} +``` + +この機能を有効化すると、ブートローダは物理メモリの全体を、ある未使用の仮想アドレス空間にマッピングします。この仮想アドレスの範囲をカーネルに伝えるために、ブートローダは**boot information**構造体を渡します。 + + +### Boot Information + +`bootloader`クレートは、カーネルに渡されるすべての情報を格納する[`BootInfo`]構造体を定義しています。この構造体はまだ開発の初期段階にあり、将来の[対応していないsemverの][semver-incompatible]ブートローダのバージョンに更新した際には、うまく動かなくなることが予想されます。`map_physical_memory` featureが有効化されているので、いまこれは`memory_map`と`physical_memory_offset`という2つのフィールドを持っています: + +[`BootInfo`]: https://docs.rs/bootloader/0.9.3/bootloader/bootinfo/struct.BootInfo.html +[semver-incompatible]: https://doc.rust-lang.org/stable/cargo/reference/specifying-dependencies.html#caret-requirements + +- `memory_map`フィールドは、利用可能な物理メモリの情報の概要を保持しています。システムの利用可能な物理メモリがどのくらいかや、どのメモリ領域がVGAハードウェアのようなデバイスのために予約されているかをカーネルに伝えます。これらのメモリマッピングはBIOSやUEFIファームウェアから取得できますが、それが可能なのはブートのごく初期に限られます。そのため、これらをカーネルが後で取得することはできないので、ブートローダによって提供する必要があるわけです。このメモリマッピングは後で必要となります。 +- `physical_memory_offset`は、物理メモリのマッピングの始まっている仮想アドレスです。このオフセットを物理アドレスに追加することによって、対応する仮想アドレスを得られます。これによって、カーネルから任意の物理アドレスにアクセスできます。 + +ブートローダは`BootInfo`構造体を`_start`関数の`&'static BootInfo`引数という形でカーネルに渡します。この引数は私達の関数ではまだ宣言していなかったので追加します: + +```rust +// in src/main.rs + +use bootloader::BootInfo; + +#[no_mangle] +pub extern "C" fn _start(boot_info: &'static BootInfo) -> ! { // 新しい引数 + […] +} +``` + +今までこの引数を無視していましたが、x86_64の呼出し規約は最初の引数をCPUレジスタに渡していたため、これは問題ではありませんでした。つまり、引数が宣言されていなかったとき、それが単に無視されていたわけです。しかし、もし引数の型を間違えてしまうと、コンパイラが私達のエントリポイント関数の正しい型シグネチャがわからなくなってしまうので問題です。 + +### `entry_point`マクロ + +私達の`_start`関数はブートローダから外部呼び出しされるので、私達の関数のシグネチャに対する検査は行われません。これにより、この関数はコンパイルエラーなしにあらゆる引数を取ることができるので、いざ実行時にエラーになったり未定義動作を起こしたりしてしまいます。 + +私達のエントリポイント関数が常にブートローダの期待する正しいシグネチャを持っていることを保証するために、`bootloader`クレートは[`entry_point`]マクロによって、Rustの関数を型チェックしたうえでエントリポイントとして定義する方法を提供します。私達のエントリポイント関数をこのマクロを使って書き直してみましょう: + +[`entry_point`]: https://docs.rs/bootloader/0.6.4/bootloader/macro.entry_point.html + +```rust +// in src/main.rs + +use bootloader::{BootInfo, entry_point}; + +entry_point!(kernel_main); + +fn kernel_main(boot_info: &'static BootInfo) -> ! { + […] +} +``` + +このマクロがより低レベルな本物の`_start`エントリポイントを定義してくれるので、`extern "C"`や`no_mangle`をエントリポイントに使う必要はもうありません。`kernel_main`関数は今や完全に普通のRustの関数なので、自由に名前をつけることができます。そして重要なのは、この関数は型チェックされているので、間違った関数シグネチャ(例えば引数を増やしたり引数の型を変えたり)にするとコンパイルエラーが発生するということです。 + +`lib.rs`に同じ変更を施しましょう: + +```rust +// in src/lib.rs + +#[cfg(test)] +use bootloader::{entry_point, BootInfo}; + +#[cfg(test)] +entry_point!(test_kernel_main); + +/// `cargo test`のエントリポイント +#[cfg(test)] +fn test_kernel_main(_boot_info: &'static BootInfo) -> ! { + // 前と同じ + init(); + test_main(); + hlt_loop(); +} +``` + +こちらのエントリポイントはテストモードのときにのみ使用するので、`#[cfg(test)]`属性をすべての要素に付しています。`main.rs`の`kernel_main`関数と混同しないよう、`test_kernel_main`という別の名前をつけました。いまのところ`BootInfo`引数は使わないので、引数名の先頭に`_`をつけることでunused variable (未使用変数) 警告が出てくるのを防いでいます。 + +## 実装 + +物理メモリへのアクセスができるようになったので、いよいよページテーブルのコードを実装できます。そのためにまず、現在有効な、私達のカーネルが使用しているページテーブルを見てみます。次に、与えられた仮想アドレスがマップされている物理アドレスを返す変換関数を作ります。最後に、新しいマッピングを作るためにページテーブルを修正してみます。 + +始める前に、`memory`モジュールを作ります: + +```rust +// in src/lib.rs + +pub mod memory; +``` + +また、このモジュールに対応するファイル`src/memory.rs`を作ります。 + +### ページテーブルにアクセスする + +[前の記事の最後][end of the previous post]で、私達のカーネルの実行しているページテーブルを見てみようとしましたが、`CR3`レジスタの指す物理フレームにアクセスすることができなかったためそれはできませんでした。この続きとして、`active_level_4_table`という、現在有効 (アクティブ) なレベル4ページテーブルへの参照を返す関数を定義するところから始めましょう: + +[end of the previous post]: @/edition-2/posts/08-paging-introduction/index.ja.md#peziteburuhenoakusesu + +```rust +// in src/memory.rs + +use x86_64::{ + structures::paging::PageTable, + VirtAddr, +}; + +/// 有効なレベル4テーブルへの可変参照を返す。 +/// +/// この関数はunsafeである:全物理メモリが、渡された +/// `physical_memory_offset`(だけずらしたうえ)で +/// 仮想メモリへとマップされていることを呼び出し元が +/// 保証しなければならない。また、`&mut`参照が複数の +/// 名称を持つこと (mutable aliasingといい、動作が未定義) +/// につながるため、この関数は一度しか呼び出してはならない。 +pub unsafe fn active_level_4_table(physical_memory_offset: VirtAddr) + -> &'static mut PageTable +{ + use x86_64::registers::control::Cr3; + + let (level_4_table_frame, _) = Cr3::read(); + + let phys = level_4_table_frame.start_address(); + let virt = physical_memory_offset + phys.as_u64(); + let page_table_ptr: *mut PageTable = virt.as_mut_ptr(); + + &mut *page_table_ptr // unsafe +} +``` + +まず、有効なレベル4テーブルの物理フレームを`CR3`レジスタから読みます。その開始物理アドレスを取り出し、`u64`に変換し、`physical_memory_offset`に足すことでそのページテーブルフレームに対応する仮想アドレスを得ます。最後に、`as_mut_ptr`メソッドを使ってこの仮想アドレスを`*mut PageTable`生ポインタに変換し、これから`&mut PageTable`参照を作ります(ここがunsafe)。`&`参照ではなく`&mut`参照にしているのは、後でこのページテーブルを変更するためです。 + +Rustは`unsafe fn`の中身全体を大きな`unsafe`ブロックであるかのように扱うので、ここでunsafeブロックを使う必要はありません。これでは、(unsafeを意図した)最後の行より前の行に間違ってunsafeな操作を書いても気づけないので、コードがより危険になります。また、どこがunsafeな操作であるのかを探すのも非常に難しくなります。そのため、この挙動を変更する[RFC](https://github.com/rust-lang/rfcs/pull/2585)が提案されています。 + +この関数を使って、レベル4テーブルのエントリを出力してみましょう: + +```rust +// in src/main.rs + +fn kernel_main(boot_info: &'static BootInfo) -> ! { + use blog_os::memory::active_level_4_table; + use x86_64::VirtAddr; + + println!("Hello World{}", "!"); + blog_os::init(); + + let phys_mem_offset = VirtAddr::new(boot_info.physical_memory_offset); + let l4_table = unsafe { active_level_4_table(phys_mem_offset) }; + + for (i, entry) in l4_table.iter().enumerate() { + if !entry.is_unused() { + println!("L4 Entry {}: {:?}", i, entry); + } + } + + // as before + #[cfg(test)] + test_main(); + + println!("It did not crash!"); + blog_os::hlt_loop(); +} +``` + +まず、`BootInfo`構造体の`physical_memory_offset`を[`VirtAddr`]に変換し、`active_level_4_table`関数に渡します。つぎに`iter`関数を使ってページテーブルのエントリをイテレートし、[`enumerate`]コンビネータをつかってそれぞれの要素にインデックス`i`を追加します。全512エントリを出力すると画面に収まらないので、 (から) でないエントリのみ出力します。 + +[`VirtAddr`]: https://docs.rs/x86_64/0.14.2/x86_64/addr/struct.VirtAddr.html +[`enumerate`]: https://doc.rust-lang.org/core/iter/trait.Iterator.html#method.enumerate + +実行すると、以下の出力を得ます: + +![QEMU printing entry 0 (0x2000, PRESENT, WRITABLE, ACCESSED), entry 1 (0x894000, PRESENT, WRITABLE, ACCESSED, DIRTY), entry 31 (0x88e000, PRESENT, WRITABLE, ACCESSED, DIRTY), entry 175 (0x891000, PRESENT, WRITABLE, ACCESSED, DIRTY), and entry 504 (0x897000, PRESENT, WRITABLE, ACCESSED, DIRTY)](qemu-print-level-4-table.png) + +いくつかの空でないエントリがあり、いずれも異なるレベル3テーブルにマップさせられていることがわかります。このようにたくさんの領域があるのは、カーネルコード、カーネルスタック、物理メモリマッピング、ブート情報が互いに離れたメモリ領域を使っているためです。 + +ページテーブルを更に辿りレベル3テーブルを見るには、エントリに対応するフレームを取り出し再び仮想アドレスに変換すればよいです: + +```rust +// src/main.rsのforループ内にて…… + +use x86_64::structures::paging::PageTable; + +if !entry.is_unused() { + println!("L4 Entry {}: {:?}", i, entry); + + // このエントリから物理アドレスを得て、それを変換する + let phys = entry.frame().unwrap().start_address(); + let virt = phys.as_u64() + boot_info.physical_memory_offset; + let ptr = VirtAddr::new(virt).as_mut_ptr(); + let l3_table: &PageTable = unsafe { &*ptr }; + + // レベル3テーブルの空でないエントリを出力する + for (i, entry) in l3_table.iter().enumerate() { + if !entry.is_unused() { + println!(" L3 Entry {}: {:?}", i, entry); + } + } +} +``` + +レベル2やレベル1のテーブルも、同じ手続きをレベル3とレベル2のエントリに対して繰り返すことで見ることができます。お察しの通りそれを書くとかなり長くなるので、コードの全てはここには示しません。 + +ページテーブルを手作業で辿ると、CPUが変換を行う仕組みを理解できて面白いです。しかし、多くの場合は与えられた仮想アドレスに対応する物理アドレスにのみ興味があるので、そのための関数を作りましょう。 + +### アドレスの変換 + +仮想アドレスを物理アドレスに変換するには、4層のページテーブルを辿って対応するフレームにたどり着けばよいです。この変換を行う関数を作りましょう: + +```rust +// in src/memory.rs + +use x86_64::PhysAddr; + +/// 与えられた仮想アドレスを対応する物理アドレスに変換し、 +/// そのアドレスがマップされていないなら`None`を返す。 +/// +/// この関数はunsafeである。なぜなら、呼び出し元は全物理メモリが与えられた +/// `physical_memory_offset`(だけずらした上)でマップされていることを +/// 保証しなくてはならないからである。 +pub unsafe fn translate_addr(addr: VirtAddr, physical_memory_offset: VirtAddr) + -> Option +{ + translate_addr_inner(addr, physical_memory_offset) +} +``` + +`unsafe`の範囲を制限するために、この関数は、すぐにunsafeでない`translate_addr_inner`関数に制御を渡しています。先に述べたように、Rustはunsafeな関数の全体をunsafeブロックとして扱ってしまいます。呼び出した非公開の (プライベートな) unsafeでない関数の中にコードを書くことで、それぞれのunsafeな操作を明確にします。 + +非公開な内部の関数に本当の実装を書いていきます: + +```rust +// in src/memory.rs + +/// `translate_addr`により呼び出される非公開関数。 +/// +/// Rustはunsafeな関数の全体をunsafeブロックとして扱ってしまうので、 +/// unsafeの範囲を絞るためにこの関数はunsafeにしていない。 +/// この関数をモジュール外から呼び出すときは、 +/// unsafeな関数`translate_addr`を使って呼び出すこと。 +fn translate_addr_inner(addr: VirtAddr, physical_memory_offset: VirtAddr) + -> Option +{ + use x86_64::structures::paging::page_table::FrameError; + use x86_64::registers::control::Cr3; + + // 有効なレベル4フレームをCR3レジスタから読む + let (level_4_table_frame, _) = Cr3::read(); + + let table_indexes = [ + addr.p4_index(), addr.p3_index(), addr.p2_index(), addr.p1_index() + ]; + let mut frame = level_4_table_frame; + + // 複数層のページテーブルを辿る + for &index in &table_indexes { + // フレームをページテーブルの参照に変換する + let virt = physical_memory_offset + frame.start_address().as_u64(); + let table_ptr: *const PageTable = virt.as_ptr(); + let table = unsafe {&*table_ptr}; + + // ページテーブルエントリを読んで、`frame`を更新する + let entry = &table[index]; + frame = match entry.frame() { + Ok(frame) => frame, + Err(FrameError::FrameNotPresent) => return None, + Err(FrameError::HugeFrame) => panic!("huge pages not supported"), + //huge pageはサポートしていません + }; + } + + // ページオフセットを足すことで、目的の物理アドレスを計算する + Some(frame.start_address() + u64::from(addr.page_offset())) +} +``` + +先程作った`active_level_4_table`関数を再利用せず、`CR3`レジスタからレベル4フレームを読み出すコードを再び書いています。これは簡単に試作するためであり、後でもっと良い方法で作り直すのでご心配なく。 + +`Virtaddr`構造体には、(仮想メモリの)インデックスから4つの階層のページテーブルを計算してくれるメソッドが備わっています。この4つのインデックスを配列に格納することで、これらを`for`ループを使って辿ります。`for`ループを抜けたら、最後に計算した`frame`を覚えているので、物理アドレスを計算できます。この`frame`は、forループの中ではページテーブルのフレームを指していて、最後のループのあと(すなわちレベル1エントリを辿ったあと)では対応する(物理)フレームを指しています。 + +ループの中では、前と同じように`physical_memory_offset`を使ってフレームをページテーブルの参照に変換します。次に、そのページテーブルのエントリを読み、[`PageTableEntry::frame`]関数を使って対応するフレームを取得します。もしエントリがフレームにマップされていなければ`None`を返します。もしエントリが2MiBや1GiBのhuge pageにマップされていたら、今のところはpanicすることにします。 + +[`PageTableEntry::frame`]: https://docs.rs/x86_64/0.14.2/x86_64/structures/paging/page_table/struct.PageTableEntry.html#method.frame + +いくつかのアドレスを変換して、この変換関数がうまく行くかテストしてみましょう: + +```rust +// in src/main.rs + +fn kernel_main(boot_info: &'static BootInfo) -> ! { + // 新しいインポート + use blog_os::memory::translate_addr; + + […] // hello world と blog_os::init + + let phys_mem_offset = VirtAddr::new(boot_info.physical_memory_offset); + + let addresses = [ + // 恒等対応しているVGAバッファのページ + 0xb8000, + // コードページのどこか + 0x201008, + // スタックページのどこか + 0x0100_0020_1a10, + // 物理アドレス "0" にマップされている仮想アドレス + boot_info.physical_memory_offset, + ]; + + for &address in &addresses { + let virt = VirtAddr::new(address); + let phys = unsafe { translate_addr(virt, phys_mem_offset) }; + println!("{:?} -> {:?}", virt, phys); + } + + […] // test_main(), "it did not crash" の出力, および hlt_loop() +} +``` + +実行すると、以下の出力を得ます: + +![0xb8000 -> 0xb8000, 0x201008 -> 0x401008, 0x10000201a10 -> 0x279a10, "panicked at 'huge pages not supported'](qemu-translate-addr.png) + +期待したとおり、恒等マップしているアドレス`0xb8000`は同じ物理アドレスに変換されました。コードページとスタックページは物理アドレスのどこかしかに変換されていますが、その場所はブートローダがカーネルの初期マッピングをどのようにつくったかによります。また、下から12ビットは変換のあとも常に同じであるということも注目に値します:この部分は[ページオフセット][_page offset_]であり、変換には関わらないためです。 + +[_page offset_]: @/edition-2/posts/08-paging-introduction/index.ja.md#x86-64niokerupezingu + +それぞれの物理アドレスは`physical_memory_offset`を足すことでアクセスできるわけですから、`physical_memory_offset`自体を変換すると物理アドレス`0`を指すはずです。しかし、効率よくマッピングを行うためにここではhuge pageが使われており、これはまだサポートしていないので変換には失敗しています。 + +### `OffsetPageTable`を使う + +仮想アドレスから物理アドレスへの変換はOSのカーネルがよく行うことですから、`x86_64`クレートはそのための抽象化を提供しています。この実装はすでにhuge pageや`translate_addr`以外の様々な関数もサポートしているので、以下ではhuge pageのサポートを自前で実装する代わりにこれを使うことにします。 + +この抽象化の基礎となっているのは、様々なページテーブルマッピング関数を定義している2つのトレイトです。 + +- [`Mapper`]トレイトはページサイズを型引数とする汎用型 (ジェネリクス) で、ページに対して操作を行う関数を提供します。例えば、[`translate_page`]は与えられたページを同じサイズのフレームに変換し、[`map_to`]はページテーブルに新しいマッピングを作成します。 +- [`Translate`] トレイトは[`translate_addr`]や一般の[`translate`]のような、さまざまなページサイズに対して動くような関数を提供します。 + +[`Mapper`]: https://docs.rs/x86_64/0.14.2/x86_64/structures/paging/mapper/trait.Mapper.html +[`translate_page`]: https://docs.rs/x86_64/0.14.2/x86_64/structures/paging/mapper/trait.Mapper.html#tymethod.translate_page +[`map_to`]: https://docs.rs/x86_64/0.14.2/x86_64/structures/paging/mapper/trait.Mapper.html#method.map_to +[`Translate`]: https://docs.rs/x86_64/0.14.2/x86_64/structures/paging/mapper/trait.Translate.html +[`translate_addr`]: https://docs.rs/x86_64/0.14.2/x86_64/structures/paging/mapper/trait.Translate.html#method.translate_addr +[`translate`]: https://docs.rs/x86_64/0.14.2/x86_64/structures/paging/mapper/trait.Translate.html#tymethod.translate + +これらのトレイトはインターフェイスを定義しているだけであり、その実装は何一つ提供していません。`x86_64`クレートは現在、このトレイトを実装する型を異なる要件に合わせて3つ用意しています。[`OffsetPageTable`]型は、全物理メモリがあるオフセットで仮想アドレスにマップしていることを前提とします。[`MappedPageTable`]はもう少し融通が効き、それぞれのページテーブルフレームが(そのフレームから)計算可能な仮想アドレスにマップしていることだけを前提とします。最後に[`RecursivePageTable`]型は、ページテーブルのフレームに[再帰的ページテーブル](#zai-gui-de-peziteburu)を使ってアクセスするときに使えます。 + +[`OffsetPageTable`]: https://docs.rs/x86_64/0.14.2/x86_64/structures/paging/mapper/struct.OffsetPageTable.html +[`MappedPageTable`]: https://docs.rs/x86_64/0.14.2/x86_64/structures/paging/mapper/struct.MappedPageTable.html +[`RecursivePageTable`]: https://docs.rs/x86_64/0.14.2/x86_64/structures/paging/mapper/struct.RecursivePageTable.html + +私達の場合、ブートローダは全物理メモリを`physical_memory_offset`変数で指定された仮想アドレスで物理メモリにマップしているので、`OffsetPageTable`型が使えます。これを初期化するために、`memory`モジュールに新しく`init`関数を作りましょう: + +```rust +use x86_64::structures::paging::OffsetPageTable; + +/// 新しいOffsetPageTableを初期化する。 +/// +/// この関数はunsafeである:全物理メモリが、渡された +/// `physical_memory_offset`(だけずらしたうえ)で +/// 仮想メモリへとマップされていることを呼び出し元が +/// 保証しなければならない。また、`&mut`参照が複数の +/// 名称を持つこと (mutable aliasingといい、動作が未定義) +/// につながるため、この関数は一度しか呼び出してはならない。 +pub unsafe fn init(physical_memory_offset: VirtAddr) -> OffsetPageTable<'static> { + let level_4_table = active_level_4_table(physical_memory_offset); + OffsetPageTable::new(level_4_table, physical_memory_offset) +} + +// これは非公開にする +unsafe fn active_level_4_table(physical_memory_offset: VirtAddr) + -> &'static mut PageTable +{…} +``` + +この関数は`physical_memory_offset`を引数としてとり、`'static`ライフタイムを持つ`OffsetPageTable`を作って返します。このライフタイムは、私達のカーネルが実行している間この実体 (インスタンス) はずっと有効であるという意味です。関数の中ではまず`active_level_4_table`関数を呼び出し、レベル4ページテーブルへの可変参照を取得します。次に[`OffsetPageTable::new`]関数をこの参照を使って呼び出します。この`new`関数の第二引数には、物理メモリのマッピングの始まる仮想アドレスが入ることになっています。つまり`physical_memory_offset`です。 + +[`OffsetPageTable::new`]: https://docs.rs/x86_64/0.14.2/x86_64/structures/paging/mapper/struct.OffsetPageTable.html#method.new + +可変参照が複数の名称を持つと未定義動作を起こす可能性があるので、今後`active_level_4_table`関数は`init`関数から一度呼び出されることを除いては呼び出されてはなりません。そのため、`pub`指定子を外してこの関数を非公開にしています。 + +これで、自前の`memory::translate_addr`関数の代わりに`Translate::translate_addr`メソッドを使うことができます。これには`kernel_main`を数行だけ書き換えればよいです: + +```rust +// in src/main.rs + +fn kernel_main(boot_info: &'static BootInfo) -> ! { + // インポートが追加・変更されている + use blog_os::memory; + use x86_64::{structures::paging::Translate, VirtAddr}; + + […] // hello worldとblog_os::init + + let phys_mem_offset = VirtAddr::new(boot_info.physical_memory_offset); + // 追加:mapperを初期化 + let mapper = unsafe { memory::init(phys_mem_offset) }; + + let addresses = […]; // 前と同じ + + for &address in &addresses { + let virt = VirtAddr::new(address); + // 追加:`mapper.translate_addr`メソッドを使う + let phys = mapper.translate_addr(virt); + println!("{:?} -> {:?}", virt, phys); + } + + […] // test_main(), "it did not crash" の出力, および hlt_loop() +} +``` + +[`translate_addr`]メソッドを使うために、それを提供している`Translate`トレイトをインポートする必要があります。 + +これを実行すると、同じ変換結果が得られますが、今度はhuge pageの変換もうまく行っています: + +![0xb8000 -> 0xb8000, 0x201008 -> 0x401008, 0x10000201a10 -> 0x279a10, 0x18000000000 -> 0x0](qemu-mapper-translate-addr.png) + +想定通り、`0xb8000`やコード・スタックアドレスの変換結果は自前の変換関数と同じになっています。また、`physical_memory_offset`は物理アドレス`0x0`にマップされているのもわかります。 + +`MappedPageTable`型の変換関数を使うことで、huge pageをサポートする手間が省けます。また`map_to`のような他のページング関数も利用でき、これは次のセクションで使います。 + +この時点で、自作した`memory::translate_addr`関数や`memory::translate_addr_inner`関数はもう必要ではないので削除して構いません。 + +### 新しいマッピングを作る + +これまでページテーブルを見てきましたが、それに対する変更は行っていませんでした。ページテーブルに対する変更として、マッピングのなかったページにマッピングを作ってみましょう。 + +これを実装するには[`Mapper`]トレイトの[`map_to`]関数を使うので、この関数について少し見てみましょう。ドキュメントによると四つ引数があります:マッピングに使うページ、ページをマップさせるフレーム、ページテーブルエントリにつかうフラグの集合、そして`frame_allocator`です。フレームアロケータ (frame allocator) (フレームを割り当てる (アロケートする) 機能を持つ)が必要な理由は、与えられたページをマップするために追加でページテーブルを作成する必要があるかもしれず、これを格納するためには使われていないフレームが必要となるからです。 + +[`map_to`]: https://docs.rs/x86_64/0.14.2/x86_64/structures/paging/trait.Mapper.html#tymethod.map_to +[`Mapper`]: https://docs.rs/x86_64/0.14.2/x86_64/structures/paging/trait.Mapper.html + +#### `create_example_mapping`関数 + +私達が実装していく最初のステップとして、`create_example_mapping`関数という、与えられた仮想ページを`0xb8000`すなわちVGAテキストバッファの物理フレームにマップする関数を作ってみましょう。このフレームを選んだ理由は、マッピングが正しくなされたかをテストするのが容易だからです:マッピングしたページに書き込んで、それが画面に現れるか確認するだけでよいのですから。 + +`create_example_mapping`は以下のようになります: + +```rust +// in src/memory.rs + +use x86_64::{ + PhysAddr, + structures::paging::{Page, PhysFrame, Mapper, Size4KiB, FrameAllocator} +}; + +/// 与えられたページをフレーム`0xb8000`に試しにマップする。 +pub fn create_example_mapping( + page: Page, + mapper: &mut OffsetPageTable, + frame_allocator: &mut impl FrameAllocator, +) { + use x86_64::structures::paging::PageTableFlags as Flags; + + let frame = PhysFrame::containing_address(PhysAddr::new(0xb8000)); + let flags = Flags::PRESENT | Flags::WRITABLE; + + let map_to_result = unsafe { + // FIXME: unsafeであり、テストのためにのみ行う + mapper.map_to(page, frame, flags, frame_allocator) + }; + map_to_result.expect("map_to failed").flush(); +} +``` + +この関数は、マップする`page`に加え`OffsetPageTable`のインスタンスと`frame_allocator`への可変参照を引数に取ります。`frame_allocator`引数は[`impl Trait`][impl-trait-arg]構文により[`FrameAllocator`]トレイトを実装するあらゆる型の[汎用型][generic]になっています。`FrameAllocator`トレイトは[`PageSize`]トレイトを実装するなら(トレイト引数のサイズが)4KiBでも2MiBや1GiBのhuge pageでも構わない汎用 (ジェネリック) トレイトです。私達は4KiBのマッピングのみを作りたいので、ジェネリック引数は`Size4KiB`にしています。 + +[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.14.2/x86_64/structures/paging/trait.FrameAllocator.html +[`PageSize`]: https://docs.rs/x86_64/0.14.2/x86_64/structures/paging/page/trait.PageSize.html + +[`map_to`]メソッドは、呼び出し元がフレームはまだ使われていないことを保証しないといけないので、unsafeです。なぜなら、同じフレームを二度マップすると(例えば2つの異なる`&mut`参照が物理メモリの同じ場所を指すことで)未定義動作を起こす可能性があるからです。今回、VGAテキストバッファのフレームという、すでにマップされているフレームを再度使っているので、この要件を破ってしまっています。しかしながら、`create_example_mapping`関数は一時的なテスト関数であり、この記事のあとには取り除かれるので大丈夫です。この危険性のことを忘れないようにするために、その行に`FIXME` (`要修正`) コメントをつけておきます。 + +`map_to`関数が`page`と`unused_frame`に加えてフラグの集合と`frame_allocator`への参照を取りますが、これについてはすぐに説明します。フラグについては、`PRESENT`フラグという有効なエントリ全てに必須のフラグと、`WRITABLE`フラグという対応するページを書き込み可能にするフラグをセットしています。フラグの一覧については、前記事の[ページテーブルの形式][_Page Table Format_]を参照してください。 + +[_Page Table Format_]: @/edition-2/posts/08-paging-introduction/index.ja.md#peziteburunoxing-shi + +[`map_to`]関数は失敗しうるので、[`Result`]を返します。これは失敗しても構わない単なるテストコードなので、エラーが起きたときは[`expect`]を使ってパニックしてしまうことにします。この関数は成功したとき[`MapperFlush`]型を返します。この型の[`flush`]メソッドを使うと、新しくマッピングしたページをトランスレーション・ルックアサイド・バッファ (TLB) から簡単にflushすることができます。この型は`Result`と同じく[`#[must_use]`][must_use]属性を使っており、使用し忘れると警告を出します。 + +[`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.14.2/x86_64/structures/paging/mapper/struct.MapperFlush.html +[`flush`]: https://docs.rs/x86_64/0.14.2/x86_64/structures/paging/mapper/struct.MapperFlush.html#method.flush +[must_use]: https://doc.rust-lang.org/std/result/#results-must-be-used + +#### ダミーの`FrameAllocator` + +`create_example_mapping`関数を呼べるようにするためには、まず`FrameAllocator`トレイトを実装する型を作成する必要があります。上で述べたように、このトレイトは新しいページのためのフレームを`map_to`が必要としたときに割り当てる役割を持っています。 + +単純なケースを考えましょう:新しいページテーブルを作る必要がないと仮定してしまいます。この場合、常に`None`を返すフレームアロケータで十分です。私達のマッピング関数をテストするために、そのような`EmptyFrameAllocator`を作ります。 + +```rust +// in src/memory.rs + +/// つねに`None`を返すFrameAllocator +pub struct EmptyFrameAllocator; + +unsafe impl FrameAllocator for EmptyFrameAllocator { + fn allocate_frame(&mut self) -> Option { + None + } +} +``` + +`FrameAllocator`を実装するのはunsafeです。なぜなら、実装する人は、実装したアロケータが未使用のフレームのみ取得することを保証しなければならないからです。さもなくば、例えば二つの仮想ページが同じ物理フレームにマップされたときに未定義動作が起こるかもしれません。この`EmptyFrameAllocator`は`None`しか返さないので、これは問題ではありません。 + +#### 仮想ページを選ぶ + +`create_example_mapping`関数に渡すための単純なフレームアロケータを手に入れました。しかし、このアロケータは常に`None`を返すので、マッピングを作る際に追加のページテーブルフレームが必要でなかったときにのみうまく動作します。いつ追加のページテーブルフレームが必要でありいつそうでないのかを知るために、例をとって考えてみましょう: + +![A virtual and a physical address space with a single mapped page and the page tables of all four levels](required-page-frames-example.svg) + +この図の左は仮想アドレス空間を、右は物理アドレス空間を、真ん中はページテーブルを示します。このページテーブルが格納されている物理フレームが破線で示されています。仮想アドレス空間は一つのマップされたページをアドレス`0x803fe00000`に持っており、これは青色で示されています。このページをフレームに変換するために、CPUは4層のページテーブルを辿り、アドレス36KiBのフレームに到達します。 + +また、この図はVGAテキストバッファの物理フレームを赤色で示しています。私達の目的は、`create_example_mapping`関数を使ってまだマップされていない仮想ページをこのフレームにマップすることです。私達の`EmptyFrameAllocator`は常に`None`を返すので、アロケータからフレームを追加する必要がないようにマッピングを作りたいです。これができるかは、私達がマッピングにどの仮想ページを使うかに依存します。 + + +この図の仮想アドレス空間には、2つの候補となるページを黄色で示しています。ページのうち一つはアドレス`0x803fe00000`で、これは(青で示された)マップされているページの3つ前です。レベル4と3のテーブルのインデックスは青いページと同じですが、レベル2と1のインデックスは違います([前の記事][page-table-indices]を参照)。レベル2テーブルのインデックスが違うということは、異なるレベル1テーブルが使われることを意味します。そんなレベル1テーブルは存在しないので、もしこちらを使っていたら、使われていない物理フレームを追加(でアロケート)する必要が出てきます。対して、2つ目のアドレス`0x803fe02000`にある候補のページは、青のページと同じレベル1ページテーブルを使うのでこの問題は発生しません。よって、必要となるすべてのページテーブルはすでに存在しています。 + +[page-table-indices]: @/edition-2/posts/08-paging-introduction/index.ja.md#x86-64niokerupezingu + +まとめると、新しいマッピングを作るときの難易度は、マッピングしようとしている仮想ページに依存するということです。作ろうとしているページのレベル1ページテーブルがすでに存在すると最も簡単で、エントリをそのページに一つ書き込むだけです。ページがレベル3のテーブルすら存在しない領域にある場合が最も難しく、その場合まずレベル3,2,1のページテーブルを新しく作る必要があります。 + +`EmptyFrameAllocator`を使って`create_example_mapping`を呼び出すためには、すべての(階層の)ページテーブルがすでに存在しているページを選ぶ必要があります。そんなページを探すにあたっては、ブートローダが自分自身を仮想アドレス空間の最初の1メガバイトに読み込んでいるということを利用できます。つまり、この領域のすべてのページについて、レベル1テーブルがきちんと存在しているということです。したがって、試しにマッピングを作るときに、このメモリ領域のいずれかの未使用ページ、例えばアドレス`0`を使えばよいです。普通このページは、ヌルポインタの参照外しがページフォルトを引き起こすことを保証するために使用しないので、ブートローダもここをマップさせてはいないはずです。 + +#### マッピングを作る + +というわけで、`create_example_mapping`関数を呼び出すために必要なすべての引数を手に入れたので、仮想アドレス`0`をマップするよう`kernel_main`関数を変更していきましょう。このページをVGAテキストバッファのフレームにマップすると、以後、画面に書き込むことができるようになるはずです。実装は以下のようになります: + +```rust +// in src/main.rs + +fn kernel_main(boot_info: &'static BootInfo) -> ! { + use blog_os::memory; + use x86_64::{structures::paging::Page, VirtAddr}; // 新しいインポート + + […] // hello worldとblog_os::init + + let phys_mem_offset = VirtAddr::new(boot_info.physical_memory_offset); + let mut mapper = unsafe { memory::init(phys_mem_offset) }; + let mut frame_allocator = memory::EmptyFrameAllocator; + + // 未使用のページをマップする + let page = Page::containing_address(VirtAddr::new(0)); + memory::create_example_mapping(page, &mut mapper, &mut frame_allocator); + + // 新しいマッピングを使って、文字列`New!`を画面に書き出す + let page_ptr: *mut u64 = page.start_address().as_mut_ptr(); + unsafe { page_ptr.offset(400).write_volatile(0x_f021_f077_f065_f04e)}; + + […] // test_main(), "it did not crash" printing, および hlt_loop() +} +``` + +まず、`mapper`と`frame_allocator`インスタンスの可変参照を渡して`create_example_mapping`を呼ぶことで、アドレス`0`のページにマッピングを作っています。これはVGAテキストバッファのフレームにマップしているので、これに書き込んだものは何であれ画面に出てくるはずです。 + +次にページを生ポインタに変更して、オフセット`400`に値を書き込みます。このページの最初に書き込むとVGAバッファの一番上の行になり、次のprintlnで即座に画面外に流れていってしまうので、それを避けています。値`0x_f021_f077_f065_f04e`は、白背景の"New!"という文字列を表します。[VGAテキストモードの記事][in the _“VGA Text Mode”_ post]で学んだように、VGAバッファへの書き込みはvolatileでなければならないので、[`write_volatile`]メソッドを使っています。 + +[in the _“VGA Text Mode”_ post]: @/edition-2/posts/03-vga-text-buffer/index.ja.md#volatile +[`write_volatile`]: https://doc.rust-lang.org/std/primitive.pointer.html#method.write_volatile + +QEMUで実行すると、以下の出力を得ます: + +![QEMU printing "It did not crash!" with four completely white cells in the middle of the screen](qemu-new-mapping.png) + +画面の "New!" はページ`0`への書き込みによるものなので、ページテーブルへの新しいマッピングの作成が成功したということを意味します。 + +このマッピングが成功したのは、アドレス`0`を管轄するレベル1テーブルがすでに存在していたからに過ぎません。レベル1テーブルがまだ存在しないページをマッピングしようとすると、`map_to`関数は新しいページテーブルを作るために`EmptyFrameAllocator`からフレームを割り当てようとしてエラーになります。`0`の代わりに`0xdeadbeaf000`をマッピングしようとするとそれが発生するのが見られます。 + +```rust +// in src/main.rs + +fn kernel_main(boot_info: &'static BootInfo) -> ! { + […] + let page = Page::containing_address(VirtAddr::new(0xdeadbeaf000)); + […] +} +``` + +これを実行すると、以下のエラーメッセージとともにパニックします: + +``` +panicked at 'map_to failed: FrameAllocationFailed', /…/result.rs:999:5 +``` + +レベル1テーブルがまだ存在していないページをマップするためには、ちゃんとした`FrameAllocator`を作らないといけません。しかし、どのフレームが未使用で、どのフレームが利用可能かはどうすればわかるのでしょう? + +### フレームを割り当てる + +新しいページテーブルを作成するためには、ちゃんとしたフレームアロケータを作る必要があります。このためには、ブートローダによって渡される`BootInfo`構造体の一部である`memory_map`を使います: + +```rust +// in src/memory.rs + +use bootloader::bootinfo::MemoryMap; + +/// ブートローダのメモリマップから、使用可能な +/// フレームを返すFrameAllocator +pub struct BootInfoFrameAllocator { + memory_map: &'static MemoryMap, + next: usize, +} + +impl BootInfoFrameAllocator { + /// 渡されたメモリマップからFrameAllocatorを作る。 + /// + /// この関数はunsafeである:呼び出し元は渡された + /// メモリマップが有効であることを保証しなければ + /// ならない。特に、`USABLE`なフレームは実際に + /// 未使用でなくてはならない。 + pub unsafe fn init(memory_map: &'static MemoryMap) -> Self { + BootInfoFrameAllocator { + memory_map, + next: 0, + } + } +} +``` + +この構造体は2つのフィールドを持ちます。ブートローダによって渡されたメモリマップへの`'static`な参照と、アロケータが次に返すべきフレームの番号を覚えておくための`next`フィールドです。 + +[_Boot Information_](#boot-information)節で説明したように、このメモリマップはBIOS/UEFIファームウェアから提供されます。これはブートプロセスのごく初期にのみ取得できますが、ブートローダがそのための関数を既に呼んでくれています。メモリマップは`MemoryRegion`構造体のリストからなり、この構造体はそれぞれのメモリ領域の開始アドレス、長さ、型(未使用か、予約済みかなど)を格納しています。 + +`init`関数は`BootInfoFrameAllocator`を与えられたメモリマップで初期化します。`next`フィールドは`0`で初期化し、フレームを割当てるたびに値を増やすことで同じフレームを二度返すことを防ぎます。メモリマップのusable (使用可能) とされているフレームが他のどこかで使われたりしていないかは知ることができないので、この`init`関数はそれを呼び出し元に追加で保証させるために`unsafe`でないといけません。 + +#### `usable_frames`メソッド + +`FrameAllocator`トレイトを実装していく前に、渡されたメモリマップをusableなフレームのイテレータに変換する補助メソッドを追加します: + +```rust +// in src/memory.rs + +use bootloader::bootinfo::MemoryRegionType; + +impl BootInfoFrameAllocator { + /// メモリマップによって指定されたusableなフレームのイテレータを返す。 + fn usable_frames(&self) -> impl Iterator { + // メモリマップからusableな領域を得る + let regions = self.memory_map.iter(); + let usable_regions = regions + .filter(|r| r.region_type == MemoryRegionType::Usable); + // それぞれの領域をアドレス範囲にmapで変換する + let addr_ranges = usable_regions + .map(|r| r.range.start_addr()..r.range.end_addr()); + // フレームの開始アドレスのイテレータへと変換する + let frame_addresses = addr_ranges.flat_map(|r| r.step_by(4096)); + // 開始アドレスから`PhysFrame`型を作る + frame_addresses.map(|addr| PhysFrame::containing_address(PhysAddr::new(addr))) + } +} +``` + +この関数はイテレータのコンビネータメソッドを使って、最初に与えられる`MemoryMap`を使用可能な物理フレームのイテレータに変換します: + +- まず`iter`メソッドを使ってメモリマップを[`MemoryRegion`]のイテレータに変える。 +- 次に[`filter`]メソッドを使って、予約済みなどの理由で使用不可能な領域を飛ばすようにする。ブートローダは作ったマッピングに使ったメモリマップはきちんと更新するので、私達のカーネル(コード、データ、スタック)に使われているフレームやブート情報を格納するのに使われているフレームはすでに`InUse` (`使用中`) などでマークされています。そのため`Usable`なフレームは他の場所では使われていないはずとわかります。 +- つぎに、[`map`]コンビネータとRustの[range構文][range syntax]を使って、メモリ領域のイテレータからアドレス範囲のイテレータへと変換する。 +- つぎに、アドレス範囲から[`step_by`]で4096個ごとにアドレスを選び、[`flat_map`]を使うことでフレームの最初のアドレスのイテレータを得る。4096バイト(=4KiB)はページのサイズに等しいので、それぞれのフレームの開始地点のアドレスが得られます。ブートローダのページは使用可能なメモリ領域をすべてアラインするので、ここで改めてアラインや丸めを行う必要はありません。`map`ではなく[`flat_map`]を使うことで、`Iterator>`ではなく`Iterator`を得ています。 +- 最後に、開始アドレスの型を`PhysFrame`に変更することで`Iterator`を得ている。 + +[`MemoryRegion`]: https://docs.rs/bootloader/0.6.4/bootloader/bootinfo/struct.MemoryRegion.html +[`filter`]: https://doc.rust-lang.org/core/iter/trait.Iterator.html#method.filter +[`map`]: https://doc.rust-lang.org/core/iter/trait.Iterator.html#method.map +[range syntax]: https://doc.rust-lang.org/core/ops/struct.Range.html +[`step_by`]: https://doc.rust-lang.org/core/iter/trait.Iterator.html#method.step_by +[`flat_map`]: https://doc.rust-lang.org/core/iter/trait.Iterator.html#method.flat_map + +この関数の戻り型は[`impl Trait`]機能を用いています。こうすると、`PhysFrame`をitemの型として持つような[`Iterator`]トレイトを実装する何らかの型を返すのだと指定できます。これは重要です――なぜなら、戻り値の型は名前のつけられないクロージャ型に依存し、**具体的な名前をつけるのが不可能**だからです。 + +[`impl Trait`]: https://doc.rust-lang.org/book/ch10-02-traits.html#returning-types-that-implement-traits +[`Iterator`]: https://doc.rust-lang.org/core/iter/trait.Iterator.html + +#### `FrameAllocator`トレイトを実装する + +これで`FrameAllocator`トレイトを実装できます: + +```rust +// in src/memory.rs + +unsafe impl FrameAllocator for BootInfoFrameAllocator { + fn allocate_frame(&mut self) -> Option { + let frame = self.usable_frames().nth(self.next); + self.next += 1; + frame + } +} +``` + +まず`usable_frames`メソッドを使ってメモリマップからusableなフレームのイテレータを得ます。つぎに、[`Iterator::nth`]関数で`self.next`番目の(つまり`(self.next - 1)`だけ飛ばして)フレームを得ます。このフレームを返してリターンする前に、`self.next`を1だけ増やして次の呼び出しで1つ後のフレームが得られるようにします。 + +[`Iterator::nth`]: https://doc.rust-lang.org/core/iter/trait.Iterator.html#method.nth + +この実装は割当てを行うごとに`usable_frames`アロケータを作り直しているので、最適とは言い難いです。イテレータを構造体のフィールドとして直接格納するほうが良いでしょう。すると`nth`メソッドを使う必要はなくなり、割り当てのたびに[`next`]を使えばいいだけです。このアプローチの問題は、今の所構造体のフィールドに`impl Trait`型(の変数)を格納することができないことです。いつの日か、[named existential type][_named existential types_]が完全に実装されたときにはこれが可能になるかもしれません。 + +[`next`]: https://doc.rust-lang.org/core/iter/trait.Iterator.html#tymethod.next +[_named existential types_]: https://github.com/rust-lang/rfcs/pull/2071 + +#### `BootInfoFrameAllocator`を使う + +`kernel_main`関数を修正して`EmptyFrameAllocator`のインスタンスの代わりに`BootInfoFrameAllocator`を渡しましょう: + +```rust +// in src/main.rs + +fn kernel_main(boot_info: &'static BootInfo) -> ! { + use blog_os::memory::BootInfoFrameAllocator; + […] + let mut frame_allocator = unsafe { + BootInfoFrameAllocator::init(&boot_info.memory_map) + }; + […] +} +``` + +ブート情報を使うフレームアロケータのおかげでマッピングは成功し、白背景に黒文字の"New!"が再び画面に現れました。舞台裏では、`map_to`メソッドが不足しているページテーブルを以下のやり方で作っています: + +- 渡された`frame_allocator`を使って未使用のフレームを割り当ててもらう。 +- フレームをゼロで埋めることで、新しい空のページテーブルを作る。 +- 上位のテーブルのエントリをそのフレームにマップする。 +- 次の層で同じことを続ける。 + +`create_example_mapping`関数はただのお試しコードにすぎませんが、今や私達は任意のページにマッピングを作れるようになりました。これは、今後の記事で行うメモリ割り当てやマルチスレッディングにおいて不可欠です。 + +[上](#create-example-mappingguan-shu)で説明したような未定義動作を誤って引き起こしてしまうことのないよう、この時点で`create_example_mapping`関数を再び取り除いておきましょう。 + +## まとめ + +この記事ではページテーブルのある物理フレームにアクセスするための様々なテクニックを学びました。恒等マップ、物理メモリ全体のマッピング、一時的なマッピング、再帰的ページテーブルなどです。このうち、シンプルでポータブル (アーキテクチャ非依存) で強力な、物理メモリ全体のマッピングを選びました。 + +ページテーブルにアクセスできなければ物理メモリをマップされないので、ブートローダの補助が必要でした。`bootloader`クレートはcargoのfeaturesというオプションを通じて、必要となるマッピングの作成をサポートしています。さらに、必要となる情報をエントリポイント関数の`&BootInfo`引数という形で私達のカーネルに渡してくれます。 + +実装についてですが、最初はページテーブルを辿る変換関数を自分の手で実装し、そのあとで`x86_64`クレートの`MappedPageTable`型を使いました。また、ページテーブルに新しいマッピングを作る方法や、そのために必要な`FrameAllocator`をブートローダに渡されたメモリマップをラップすることで作る方法を学びました。 + +## 次は? + +次の記事では、私達のカーネルのためのヒープメモリ領域を作り、それによって[メモリの割り当て][allocate memory]を行ったり各種の[コレクション型][collection types]を使うことが可能になります。 + +[allocate memory]: https://doc.rust-lang.org/alloc/boxed/struct.Box.html +[collection types]: https://doc.rust-lang.org/alloc/collections/index.html diff --git a/blog/content/edition-2/posts/09-paging-implementation/index.md b/blog/content/edition-2/posts/09-paging-implementation/index.md index a5896642..e9803ff2 100644 --- a/blog/content/edition-2/posts/09-paging-implementation/index.md +++ b/blog/content/edition-2/posts/09-paging-implementation/index.md @@ -16,6 +16,7 @@ This blog is openly developed on [GitHub]. If you have any problems or questions [GitHub]: https://github.com/phil-opp/blog_os [at the bottom]: #comments + [post branch]: https://github.com/phil-opp/blog_os/tree/post-09 @@ -219,7 +220,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.13.2/x86_64/structures/paging/mapper/struct.RecursivePageTable.html +[`RecursivePageTable`]: https://docs.rs/x86_64/0.14.2/x86_64/structures/paging/mapper/struct.RecursivePageTable.html ```rust // in src/memory.rs @@ -273,11 +274,11 @@ This means that we need the help of the bootloader, which creates the page table - The `map_physical_memory` feature maps the complete physical memory somewhere into the virtual address space. Thus, the kernel can access all physical memory and can follow the [_Map the Complete Physical Memory_](#map-the-complete-physical-memory) approach. - With the `recursive_page_table` feature, the bootloader maps an entry of the level 4 page table recursively. This allows the kernel to access the page tables as described in the [_Recursive Page Tables_](#recursive-page-tables) section. -We choose the first approach for our kernel since it is simple, platform-independent, and more powerful (it also allows to access non-page-table-frames). To enable the required bootloader support, we add the `map_physical_memory` feature to our `bootloader` dependency: +We choose the first approach for our kernel since it is simple, platform-independent, and more powerful (it also allows access to non-page-table-frames). To enable the required bootloader support, we add the `map_physical_memory` feature to our `bootloader` dependency: ```toml [dependencies] -bootloader = { version = "0.9.3", features = ["map_physical_memory"]} +bootloader = { version = "0.9.8", features = ["map_physical_memory"]} ``` With this feature enabled, the bootloader maps the complete physical memory to some unused virtual address range. To communicate the virtual address range to our kernel, the bootloader passes a _boot information_ structure. @@ -437,7 +438,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.13.2/x86_64/addr/struct.VirtAddr.html +[`VirtAddr`]: https://docs.rs/x86_64/0.14.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 +551,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.13.2/x86_64/structures/paging/page_table/struct.PageTableEntry.html#method.frame +[`PageTableEntry::frame`]: https://docs.rs/x86_64/0.14.2/x86_64/structures/paging/page_table/struct.PageTableEntry.html#method.frame Let's test our translation function by translating some addresses: @@ -558,9 +559,8 @@ Let's test our translation function by translating some addresses: // in src/main.rs fn kernel_main(boot_info: &'static BootInfo) -> ! { - // new imports + // new import use blog_os::memory::translate_addr; - use x86_64::VirtAddr; […] // hello world and blog_os::init @@ -606,20 +606,20 @@ The base of the abstraction are two traits that define various page table mappin - 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 [`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.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 +[`Mapper`]: https://docs.rs/x86_64/0.14.2/x86_64/structures/paging/mapper/trait.Mapper.html +[`translate_page`]: https://docs.rs/x86_64/0.14.2/x86_64/structures/paging/mapper/trait.Mapper.html#tymethod.translate_page +[`map_to`]: https://docs.rs/x86_64/0.14.2/x86_64/structures/paging/mapper/trait.Mapper.html#method.map_to +[`Translate`]: https://docs.rs/x86_64/0.14.2/x86_64/structures/paging/mapper/trait.Translate.html +[`translate_addr`]: https://docs.rs/x86_64/0.14.2/x86_64/structures/paging/mapper/trait.Translate.html#method.translate_addr +[`translate`]: https://docs.rs/x86_64/0.14.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.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 +[`OffsetPageTable`]: https://docs.rs/x86_64/0.14.2/x86_64/structures/paging/mapper/struct.OffsetPageTable.html +[`MappedPageTable`]: https://docs.rs/x86_64/0.14.2/x86_64/structures/paging/mapper/struct.MappedPageTable.html +[`RecursivePageTable`]: https://docs.rs/x86_64/0.14.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: +In our case, the bootloader maps the complete physical memory at a virtual address specified 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: ```rust use x86_64::structures::paging::OffsetPageTable; @@ -643,7 +643,7 @@ 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.13.2/x86_64/structures/paging/mapper/struct.OffsetPageTable.html#method.new +[`OffsetPageTable::new`]: https://docs.rs/x86_64/0.14.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. @@ -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.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 +[`map_to`]: https://docs.rs/x86_64/0.14.2/x86_64/structures/paging/trait.Mapper.html#tymethod.map_to +[`Mapper`]: https://docs.rs/x86_64/0.14.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.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 +[`FrameAllocator`]: https://docs.rs/x86_64/0.14.2/x86_64/structures/paging/trait.FrameAllocator.html +[`PageSize`]: https://docs.rs/x86_64/0.14.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.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 +[`MapperFlush`]: https://docs.rs/x86_64/0.14.2/x86_64/structures/paging/mapper/struct.MapperFlush.html +[`flush`]: https://docs.rs/x86_64/0.14.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` @@ -782,7 +782,7 @@ The graphic shows the virtual address space on the left, the physical address sp Additionally, the graphic shows the physical frame of the VGA text buffer in red. Our goal is to map a previously unmapped virtual page to this frame using our `create_example_mapping` function. Since our `EmptyFrameAllocator` always returns `None`, we want to create the mapping so that no additional frames are needed from the allocator. This depends on the virtual page that we select for the mapping. -The graphic shows two canditate pages in the virtual address space, both marked in yellow. One page is at address `0x803fdfd000`, which is 3 pages before the mapped page (in blue). While the level 4 and level 3 page table indices are the same as for the blue page, the level 2 and level 1 indices are different (see the [previous post][page-table-indices]). The different index into the level 2 table means that a different level 1 table is used for this page. Since this level 1 table does not exist yet, we would need to create it if we chose that page for our example mapping, which would require an additional unused physical frame. In contrast, the second candidate page at address `0x803fe02000` does not have this problem because it uses the same level 1 page table than the blue page. Thus, all required page tables already exist. +The graphic shows two candidate pages in the virtual address space, both marked in yellow. One page is at address `0x803fdfd000`, which is 3 pages before the mapped page (in blue). While the level 4 and level 3 page table indices are the same as for the blue page, the level 2 and level 1 indices are different (see the [previous post][page-table-indices]). The different index into the level 2 table means that a different level 1 table is used for this page. Since this level 1 table does not exist yet, we would need to create it if we chose that page for our example mapping, which would require an additional unused physical frame. In contrast, the second candidate page at address `0x803fe02000` does not have this problem because it uses the same level 1 page table than the blue page. Thus, all required page tables already exist. [page-table-indices]: @/edition-2/posts/08-paging-introduction/index.md#paging-on-x86-64 diff --git a/blog/content/edition-2/posts/09-paging-implementation/qemu-print-level-4-table.png b/blog/content/edition-2/posts/09-paging-implementation/qemu-print-level-4-table.png index 39001d20..8594cf52 100644 Binary files a/blog/content/edition-2/posts/09-paging-implementation/qemu-print-level-4-table.png and b/blog/content/edition-2/posts/09-paging-implementation/qemu-print-level-4-table.png differ diff --git a/blog/content/edition-2/posts/10-heap-allocation/index.md b/blog/content/edition-2/posts/10-heap-allocation/index.md index cdc23423..c56b5f10 100644 --- a/blog/content/edition-2/posts/10-heap-allocation/index.md +++ b/blog/content/edition-2/posts/10-heap-allocation/index.md @@ -16,6 +16,7 @@ This blog is openly developed on [GitHub]. If you have any problems or questions [GitHub]: https://github.com/phil-opp/blog_os [at the bottom]: #comments + [post branch]: https://github.com/phil-opp/blog_os/tree/post-10 @@ -106,7 +107,7 @@ We see that the `z[1]` slot is free again and can be reused for the next `alloca Apart from memory leaks, which are unfortunate but don't make the program vulnerable to attackers, there are two common types of bugs with more severe consequences: -- When we accidentally continue to use a variable after calling `deallocate` on it, we have a so-called **use-after-free** vulnerability. Such a bug causes undefined behavior and can often exploited by attackers to execute arbitrary code. +- When we accidentally continue to use a variable after calling `deallocate` on it, we have a so-called **use-after-free** vulnerability. Such a bug causes undefined behavior and can often be exploited by attackers to execute arbitrary code. - When we accidentally free a variable twice, we have a **double-free** vulnerability. This is problematic because it might free a different allocation that was allocated in the same spot after the first `deallocate` call. Thus, it can lead to an use-after-free vulnerability again. These types of vulnerabilities are commonly known, so one might expect that people learned how to avoid them by now. But no, such vulnerabilities are still regularly found, for example this recent [use-after-free vulnerability in Linux][linux vulnerability] that allowed arbitrary code execution. This shows that even the best programmers are not always able to correctly handle dynamic memory in complex projects. @@ -445,12 +446,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.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 +[`Mapper`]:https://docs.rs/x86_64/0.14.2/x86_64/structures/paging/mapper/trait.Mapper.html +[`FrameAllocator`]: https://docs.rs/x86_64/0.14.2/x86_64/structures/paging/trait.FrameAllocator.html +[`Size4KiB`]: https://docs.rs/x86_64/0.14.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.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 +[`MapToError`]: https://docs.rs/x86_64/0.14.2/x86_64/structures/paging/mapper/enum.MapToError.html +[`Mapper::map_to`]: https://docs.rs/x86_64/0.14.2/x86_64/structures/paging/mapper/trait.Mapper.html#method.map_to The implementation can be broken down into two parts: @@ -464,18 +465,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.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 +[`VirtAddr`]: https://docs.rs/x86_64/0.14.2/x86_64/addr/struct.VirtAddr.html +[`Page`]: https://docs.rs/x86_64/0.14.2/x86_64/structures/paging/page/struct.Page.html +[`containing_address`]: https://docs.rs/x86_64/0.14.2/x86_64/structures/paging/page/struct.Page.html#method.containing_address +[`Page::range_inclusive`]: https://docs.rs/x86_64/0.14.2/x86_64/structures/paging/page/struct.Page.html#method.range_inclusive +[`FrameAllocator::allocate_frame`]: https://docs.rs/x86_64/0.14.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.13.2/x86_64/structures/paging/mapper/enum.MapToError.html#variant.FrameAllocationFailed +[`MapToError::FrameAllocationFailed`]: https://docs.rs/x86_64/0.14.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.13.2/x86_64/structures/paging/mapper/struct.MapperFlush.html +[`MapperFlush`]: https://docs.rs/x86_64/0.14.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.13.2/x86_64/structures/paging/mapper/struct.MapperFlush.html#method.flush +[`flush`]: https://docs.rs/x86_64/0.14.2/x86_64/structures/paging/mapper/struct.MapperFlush.html#method.flush The final step is to call this function from our `kernel_main`: @@ -528,7 +529,7 @@ To use the crate, we first need to add a dependency on it in our `Cargo.toml`: # in Cargo.toml [dependencies] -linked_list_allocator = "0.8.0" +linked_list_allocator = "0.9.0" ``` Then we can replace our dummy allocator with the allocator provided by the crate: @@ -548,7 +549,7 @@ The struct is named `LockedHeap` because it uses the [`spinning_top::Spinlock`] Setting the `LockedHeap` as global allocator is not enough. The reason is that we use the [`empty`] constructor function, which creates an allocator without any backing memory. Like our dummy allocator, it always returns an error on `alloc`. To fix this, we need to initialize the allocator after creating the heap: -[`empty`]: https://docs.rs/linked_list_allocator/0.8.0/linked_list_allocator/struct.LockedHeap.html#method.empty +[`empty`]: https://docs.rs/linked_list_allocator/0.9.0/linked_list_allocator/struct.LockedHeap.html#method.empty ```rust // in src/allocator.rs @@ -571,8 +572,8 @@ pub fn init_heap( We use the [`lock`] method on the inner spinlock of the `LockedHeap` type to get an exclusive reference to the wrapped [`Heap`] instance, on which we then call the [`init`] method with the heap bounds as arguments. It is important that we initialize the heap _after_ mapping the heap pages, since the [`init`] function already tries to write to the heap memory. [`lock`]: https://docs.rs/lock_api/0.3.3/lock_api/struct.Mutex.html#method.lock -[`Heap`]: https://docs.rs/linked_list_allocator/0.8.0/linked_list_allocator/struct.Heap.html -[`init`]: https://docs.rs/linked_list_allocator/0.8.0/linked_list_allocator/struct.Heap.html#method.init +[`Heap`]: https://docs.rs/linked_list_allocator/0.9.0/linked_list_allocator/struct.Heap.html +[`init`]: https://docs.rs/linked_list_allocator/0.9.0/linked_list_allocator/struct.Heap.html#method.init After initializing the heap, we can now use all allocation and collection types of the built-in [`alloc`] crate without error: diff --git a/blog/content/edition-2/posts/11-allocator-designs/index.md b/blog/content/edition-2/posts/11-allocator-designs/index.md index 995a3beb..eb19d14f 100644 --- a/blog/content/edition-2/posts/11-allocator-designs/index.md +++ b/blog/content/edition-2/posts/11-allocator-designs/index.md @@ -16,6 +16,7 @@ This blog is openly developed on [GitHub]. If you have any problems or questions [GitHub]: https://github.com/phil-opp/blog_os [at the bottom]: #comments + [post branch]: https://github.com/phil-opp/blog_os/tree/post-11 @@ -172,9 +173,6 @@ Note that we don't perform any bounds checks or alignment adjustments, so this i error[E0594]: cannot assign to `self.next` which is behind a `&` reference --> src/allocator/bump.rs:29:9 | -26 | unsafe fn alloc(&self, layout: Layout) -> *mut u8 { - | ----- help: consider changing this to be a mutable reference: `&mut self` -... 29 | self.next = alloc_start + layout.size(); | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ `self` is a `&` reference, so the data it refers to cannot be written ``` @@ -186,8 +184,6 @@ The error occurs because the [`alloc`] and [`dealloc`] methods of the `GlobalAll [`alloc`]: https://doc.rust-lang.org/alloc/alloc/trait.GlobalAlloc.html#tymethod.alloc [`dealloc`]: https://doc.rust-lang.org/alloc/alloc/trait.GlobalAlloc.html#tymethod.dealloc -Note that the compiler suggestion to change `&self` to `&mut self` in the method declaration does not work here. The reason is that the method signature is defined by the `GlobalAlloc` trait and can't be changed on the implementation side. (I opened an [issue](https://github.com/rust-lang/rust/issues/68049) in the Rust repository about the invalid suggestion.) - #### `GlobalAlloc` and Mutability Before we look at a possible solution to this mutability problem, let's try to understand why the `GlobalAlloc` trait methods are defined with `&self` arguments: As we saw [in the previous post][global-allocator], the global heap allocator is defined by adding the `#[global_allocator]` attribute to a `static` that implements the `GlobalAlloc` trait. Static variables are immutable in Rust, so there is no way to call a method that takes `&mut self` on the static allocator. For this reason, all the methods of `GlobalAlloc` only take an immutable `&self` reference. @@ -638,7 +634,7 @@ impl LinkedListAllocator { } ``` -The method uses a `current` variable and a [`while let` loop] to iterate over the list elements. At the beginning, `current` is set to the (dummy) `head` node. On each iteration, it is then updated to to the `next` field of the current node (in the `else` block). If the region is suitable for an allocation with the given size and alignment, the region is removed from the list and returned together with the `alloc_start` address. +The method uses a `current` variable and a [`while let` loop] to iterate over the list elements. At the beginning, `current` is set to the (dummy) `head` node. On each iteration, it is then updated to the `next` field of the current node (in the `else` block). If the region is suitable for an allocation with the given size and alignment, the region is removed from the list and returned together with the `alloc_start` address. [`while let` loop]: https://doc.rust-lang.org/reference/expressions/loop-expr.html#predicate-pattern-loops @@ -966,15 +962,15 @@ impl FixedSizeBlockAllocator { } ``` -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. +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 it 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 +[`empty`]: https://docs.rs/linked_list_allocator/0.9.0/linked_list_allocator/struct.Heap.html#method.empty If you haven't done so already for the `LinkedListAllocator` implementation, you also need to add **`#![feature(const_mut_refs)]`** to the beginning of your `lib.rs`. The reason is that any use of mutable reference types in const functions is still unstable, including the `Option<&'static mut ListNode>` array element type of the `list_heads` field (even if we set it to `None`). The unsafe `init` function only calls the [`init`] function of the `fallback_allocator` without doing any additional initialization of the `list_heads` array. Instead, we will initialize the lists lazily on `alloc` and `dealloc` calls. -[`init`]: https://docs.rs/linked_list_allocator/0.8.0/linked_list_allocator/struct.Heap.html#method.init +[`init`]: https://docs.rs/linked_list_allocator/0.9.0/linked_list_allocator/struct.Heap.html#method.init For convenience, we also create a private `fallback_alloc` method that allocates using the `fallback_allocator`: @@ -997,9 +993,9 @@ impl FixedSizeBlockAllocator { Since the [`Heap`] type of the `linked_list_allocator` crate does not implement [`GlobalAlloc`] (as it's [not possible without locking]). Instead, it provides an [`allocate_first_fit`] method that has a slightly different interface. Instead of returning a `*mut u8` and using a null pointer to signal an error, it returns a `Result, ()>`. The [`NonNull`] type is an abstraction for a raw pointer that is guaranteed to be not the null pointer. By mapping the `Ok` case to the [`NonNull::as_ptr`] method and the `Err` case to a null pointer, we can easily translate this back to a `*mut u8` type. -[`Heap`]: https://docs.rs/linked_list_allocator/0.8.0/linked_list_allocator/struct.Heap.html +[`Heap`]: https://docs.rs/linked_list_allocator/0.9.0/linked_list_allocator/struct.Heap.html [not possible without locking]: #globalalloc-and-mutability -[`allocate_first_fit`]: https://docs.rs/linked_list_allocator/0.8.0/linked_list_allocator/struct.Heap.html#method.allocate_first_fit +[`allocate_first_fit`]: https://docs.rs/linked_list_allocator/0.9.0/linked_list_allocator/struct.Heap.html#method.allocate_first_fit [`NonNull`]: https://doc.rust-lang.org/nightly/core/ptr/struct.NonNull.html [`NonNull::as_ptr`]: https://doc.rust-lang.org/nightly/core/ptr/struct.NonNull.html#method.as_ptr @@ -1019,7 +1015,7 @@ fn list_index(layout: &Layout) -> Option { } ``` -The block must be have at least the size and alignment required by the given `Layout`. Since we defined that the block size is also its alignment, this means that the `required_block_size` is the [maximum] of the layout's [`size()`] and [`align()`] attributes. To find the next-larger block in the `BLOCK_SIZES` slice, we first use the [`iter()`] method to get an iterator and then the [`position()`] method to find the index of the first block that is as least as large as the `required_block_size`. +The block must have at least the size and alignment required by the given `Layout`. Since we defined that the block size is also its alignment, this means that the `required_block_size` is the [maximum] of the layout's [`size()`] and [`align()`] attributes. To find the next-larger block in the `BLOCK_SIZES` slice, we first use the [`iter()`] method to get an iterator and then the [`position()`] method to find the index of the first block that is as least as large as the `required_block_size`. [maximum]: https://doc.rust-lang.org/core/cmp/trait.Ord.html#method.max [`size()`]: https://doc.rust-lang.org/core/alloc/struct.Layout.html#method.size @@ -1129,7 +1125,7 @@ unsafe fn dealloc(&self, ptr: *mut u8, layout: Layout) { Like in `alloc`, we first use the `lock` method to get a mutable allocator reference and then the `list_index` function to get the block list corresponding to the given `Layout`. If the index is `None`, no fitting block size exists in `BLOCK_SIZES`, which indicates that the allocation was created by the fallback allocator. Therefore we use its [`deallocate`][`Heap::deallocate`] to free the memory again. The method expects a [`NonNull`] instead of a `*mut u8`, so we need to convert the pointer first. (The `unwrap` call only fails when the pointer is null, which should never happen when the compiler calls `dealloc`.) -[`Heap::deallocate`]: https://docs.rs/linked_list_allocator/0.8.0/linked_list_allocator/struct.Heap.html#method.deallocate +[`Heap::deallocate`]: https://docs.rs/linked_list_allocator/0.9.0/linked_list_allocator/struct.Heap.html#method.deallocate If `list_index` returns a block index, we need to add the freed memory block to the list. For that, we first create a new `ListNode` that points to the current list head (by using [`Option::take`] again). Before we write the new node into the freed memory block, we first assert that the current block size specified by `index` has the required size and alignment for storing a `ListNode`. Then we perform the write by converting the given `*mut u8` pointer to a `*mut ListNode` pointer and then calling the unsafe [`write`][`pointer::write`] method on it. The last step is to set the head pointer of the list, which is currently `None` since we called `take` on it, to our newly written `ListNode`. For that we convert the raw `new_node_ptr` to a mutable reference. @@ -1178,7 +1174,7 @@ On the implementation side, there are various things that we could improve in ou - Instead of only allocating blocks lazily using the fallback allocator, it might be better to pre-fill the lists to improve the performance of initial allocations. - To simplify the implementation, we only allowed block sizes that are powers of 2 so that we could use them also as the block alignment. By storing (or calculating) the alignment in a different way, we could also allow arbitrary other block sizes. This way, we could add more block sizes, e.g. for common allocation sizes, in order to minimize the wasted memory. - We currently only create new blocks, but never free them again. This results in fragmentation and might eventually result in allocation failure for large allocations. It might make sense to enforce a maximum list length for each block size. When the maximum length is reached, subsequent deallocations are freed using the fallback allocator instead of being added to the list. -- Instead of falling back to a linked list allocator, we could a special allocator for allocations greater than 4KiB. The idea is to utilize [paging], which operates on 4KiB pages, to map a continuous block of virtual memory to non-continuous physical frames. This way, fragmentation of unused memory is no longer a problem for large allocations. +- Instead of falling back to a linked list allocator, we could have a special allocator for allocations greater than 4KiB. The idea is to utilize [paging], which operates on 4KiB pages, to map a continuous block of virtual memory to non-continuous physical frames. This way, fragmentation of unused memory is no longer a problem for large allocations. - With such a page allocator, it might make sense to add block sizes up to 4KiB and drop the linked list allocator completely. The main advantages of this would be reduced fragmentation and improved performance predictability, i.e. better worse-case performance. [paging]: @/edition-2/posts/08-paging-introduction/index.md diff --git a/blog/content/edition-2/posts/12-async-await/index.ja.md b/blog/content/edition-2/posts/12-async-await/index.ja.md new file mode 100644 index 00000000..121bcfc1 --- /dev/null +++ b/blog/content/edition-2/posts/12-async-await/index.ja.md @@ -0,0 +1,1831 @@ ++++ +title = "Async/Await" +weight = 12 +path = "ja/async-await" +date = 2020-03-27 + +[extra] +chapter = "Multitasking" +# Please update this when updating the translation +translation_based_on_commit = "bf4f88107966c7ab1327c3cdc0ebfbd76bad5c5f" +# GitHub usernames of the people that translated this post +translators = ["kahirokunn", "garasubo", "sozysozbot", "woodyZootopia"] ++++ + +この記事では、Rustの**協調的マルチタスク**と**async/await**機能について説明します。Rustのasync/await機能については、`Future` trait の設計、ステートマシンの変換、 **pinning** などを含めて詳しく説明します。そして、非同期キーボードタスクと基本的なexecutorを作成することで、カーネルにasync/awaitの基本的なサポートを追加します。 + + + +このブログの内容は [GitHub] 上で公開・開発されています。何か問題や質問などがあれば issue をたててください (訳注: リンクは原文(英語)のものになります)。また[こちら][at the bottom]にコメントを残すこともできます。この記事の完全なソースコードは[`post-12` ブランチ][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-12 + + + +## マルチタスク + +ほとんどのOSの基本機能のひとつに、複数のタスクを同時に実行できる[**マルチタスク**]というものがあります。例えば、この記事をご覧になっている間も、テキストエディタやターミナルウィンドウなど、他のプログラムを開いていることでしょう。また、ブラウザのウィンドウを1つだけ開いていたとしても、デスクトップのウィンドウを管理したり、アップデートをチェックしたり、ファイルのインデックスを作成したりと、さまざまなバックグラウンドタスクがあるはずです。 + +[**マルチタスク**]: https://en.wikipedia.org/wiki/Computer_multitasking + +一見、すべてのタスクが並行して実行されているように見えますが、1つのCPUコアで同時に実行できるのは1つのタスクだけです。タスクが並列に実行されているように見せるために、OSは実行中のタスクを素早く切り替えて、それぞれのタスクが少しずつ進むようにしています。コンピュータは高速なので、ほとんどの場合、私達がこの切り替えに気づくことはありません。 + +シングルコアのCPUは一度に1つのタスクしか実行できませんが、マルチコアのCPUは複数のタスクを真の意味で並列に実行することができます。例えば、8コアのCPUであれば、8つのタスクを同時に実行することができます。マルチコアCPUの設定方法については、今後の記事でご紹介します。この記事では、わかりやすくするために、シングルコアのCPUに焦点を当てます。(なお、マルチコアCPUには、最初は1つのアクティブコアしかないので、ここではシングルコアCPUとして扱っても問題はありません)。 + +マルチタスクには2つの形態があります。**協調的**マルチタスクでは、タスクが定期的にCPUの制御を放棄することで、他のタスクの処理を進めます。**非協調的**マルチタスクは、OSの機能を利用して、任意の時点でスレッドを強制的に一時停止させて切り替えるものです。以下では、この2つのマルチタスクについて、それぞれの長所と短所を説明します。 + +### 非協調的マルチタスク + +非協調的マルチタスクの考え方は、タスクを切り替えるタイミングをOSが制御するというものです。そのためには、割り込みのたびにCPUの制御権がOS側に戻ってくることを利用します。これにより、システムに新しい入力があったときに、タスクを切り替えることができます。例えば、マウスを動かしたときやネットワークパケットが届いたときなどにタスクを切り替えることができます。OSは、ハードウェアのタイマーを設定して、その時間が経過したら割り込みを送るようにすることで、タスクの実行が許される正確な時間を決定することもできます。 + +ハードウェア割り込みでのタスク切り替え処理を下図に示します: + +![](regain-control-on-interrupt.svg) + +最初の行では、CPUがプログラム`A`のタスク`A1`を実行しています。他のすべてのタスクは一時停止しています。2行目では、CPUにハードウェア割り込みが入ります。[**ハードウェア割り込み**](訳注: 翻訳当時、リンク先未訳)の記事で説明したように、CPUは直ちにタスク`A1`の実行を停止し、割り込み記述子テーブル(IDT)に定義されている割り込みハンドラにジャンプします。この割り込みハンドラを介して、OSは再びCPUを制御できるようになり、タスク`A1`の継続ではなく、タスク`B1`に切り替えることができます。 + +[**ハードウェア割り込み**]: @/edition-2/posts/07-hardware-interrupts/index.md + +#### 状態の保存 + +タスクは任意の時点で中断されるため、計算の途中である可能性もあります。後で再開できるようにするために、OSは、タスクの[コールスタック]やすべてのCPUレジスタの値など、タスクの状態全体をバックアップする必要があります。この作業を[コンテキスト・スイッチ (context switch)] といいます。 + +[コールスタック]: https://ja.wikipedia.org/wiki/%E3%82%B3%E3%83%BC%E3%83%AB%E3%82%B9%E3%82%BF%E3%83%83%E3%82%AF +[コンテキスト・スイッチ (context switch)]: https://ja.wikipedia.org/wiki/%E3%82%B3%E3%83%B3%E3%83%86%E3%82%AD%E3%82%B9%E3%83%88%E3%82%B9%E3%82%A4%E3%83%83%E3%83%81 + +コールスタックは非常に大きくなる可能性があるため、OSは通常、各タスクのスイッチでコールスタックの内容をバックアップする代わりに、各タスクに個別のコールスタックを設定します。このような独立したスタックを持つタスクは、[_thread of execution_](略して**スレッド**)と呼ばれます。タスクごとに独立したスタックを使用することで、コンテキスト・スイッチの際に保存する必要があるのはレジスタの内容だけになります(プログラム・カウンタとスタック・ポインタを含む)。この方法を取ることで、コンテキスト・スイッチの性能上のオーバーヘッドが最小限になります。これは、コンテキスト・スイッチが1秒間に100回も行われることがあるため、非常に重要なことです。 + +[_thread of execution_]: https://ja.wikipedia.org/wiki/%E3%82%B9%E3%83%AC%E3%83%83%E3%83%89_(%E3%82%B3%E3%83%B3%E3%83%94%E3%83%A5%E3%83%BC%E3%82%BF) + +#### 議論 + +非協調的マルチタスクの主な利点は、OSがタスクの許容実行時間を完全に制御できることです。これにより、各タスクが協力しなくても、CPU時間を公平に確保できることが保証されます。これは、サードパーティのタスクを実行する場合や、複数のユーザーがシステムを共有する場合に特に重要です。 + +非協調的マルチタスクの欠点は、各タスクが独自のスタックを必要とすることです。共有スタックと比べると、タスクごとのメモリ使用量が多くなり、システム内のタスク数が制限されることが多くなります。また、タスクがレジスタのごく一部しか使用していない場合でも、タスクが切り替わるたびにOSは常にCPUレジスタの状態を完全に保存しなければならないというデメリットもあります。 + +非協調的マルチタスクとスレッドは、信頼されていないユーザースペース・プログラムの実行を可能にする、OSの基本的な構成要素です。これらの概念については、今後の記事で詳しく説明します。しかし今回は、カーネルにも有用な機能を提供する協調的マルチタスクに焦点を当てます。 + +### 協調的マルチタスク + +協調的マルチタスクでは、実行中のタスクを任意のタイミングで強制的に停止させるのではなく、各タスクが自発的にCPUの制御を放棄するまで実行させます。これにより、例えば、I/O操作を待つ必要がある場合など、都合の良いタイミングでタスクは一時停止することができます。 + +協調的マルチタスクは、言語レベルで使われることが多いです。具体的には、[コルーチン]や[async/await]などの形で登場します。これは、プログラマやコンパイラがプログラムに[_yield_]操作を挿入することで、CPUの制御を放棄し、他のタスクを実行させるというものです。例えば、複雑なループの各反復の後に yield を挿入することができます。 + +[コルーチン]: https://ja.wikipedia.org/wiki/%E3%82%B3%E3%83%AB%E3%83%BC%E3%83%81%E3%83%B3 +[async/await]: https://rust-lang.github.io/async-book/01_getting_started/04_async_await_primer.html +[_yield_]: https://en.wikipedia.org/wiki/Yield_(multithreading) + +協調的マルチタスクは[非同期I/O]と組み合わせるのが一般的です。非同期I/O では、操作が終了するまで待って、その間に他のタスクが実行できないようにする代わりに、操作がまだ終了していない場合は"not ready"というステータスを返します。この場合、待機中のタスクは yieldを実行して他のタスクを実行させることができます。 + +[非同期I/O]: https://ja.wikipedia.org/wiki/%E9%9D%9E%E5%90%8C%E6%9C%9FIO + +#### 状態の保存 + +タスクは自分で一時停止のポイントを決めるので、OSがタスクの状態を保存しなくてよくなります。その代わり、自分が停止する直前に継続するのに必要になる状態だけを保存することができ、その結果、パフォーマンスが向上することが多いです。例えば、複雑な計算を終えたばかりのタスクは、中間結果を必要としないため、計算の最終結果をバックアップするだけで済むかもしれません。 + +言語でサポートされている協調タスクの実装では、一時停止する前にコールスタックの必要な部分をバックアップすることもできることが多いです。例えば、Rustのasync/awaitの実装では、まだ必要なすべてのローカル変数を、自動的に生成された構造体に格納しています(後述)。一時停止の前にコールスタックの関連部分をバックアップすることで、すべてのタスクが単一のコールスタックを共有することができ、タスクごとのメモリ消費量が大幅に少なくなります。これにより、メモリ不足に陥ることなく、ほぼ任意の数の協調タスクを作成することができます。 + +#### 議論 + +協調的マルチタスクの欠点は、非協力的なタスクが無制限の時間実行できる可能性があることです。そのため、悪意のあるタスクやバグのあるタスクが他のタスクの実行を妨げ、システム全体の速度を低下させたり、ブロックしたりすることがあります。このような理由から、協調的マルチタスクは、すべてのタスクが協調することがわかっている場合にのみ使用する必要があります。反例として、任意のユーザーレベルプログラムの協調にOSを依存させるのはよくありません。 + +しかし、協調的マルチタスクは、パフォーマンスやメモリの面で非常に優れているため、非同期処理と組み合わせて、 **プログラムの中で** 使用するのには適した手法です。OSのカーネルは、非同期のハードウェアとやりとりする、パフォーマンスが非常に重要なプログラムであるため、協調的マルチタスクは同時実行の実装に適したアプローチであると言えます。 + +## RustのAsync/Await + +Rust言語は、async/awaitという形で協調的マルチタスクのファーストクラス(訳注:第一級オブジェクトの意)のサポートを提供しています。async/awaitとは何か、どのように機能するのかを探る前に、Rustで **future** と非同期プログラミングがどのように機能するのかを理解する必要があります。 + +### Future + +**future** は、まだ利用できない可能性のある値を表します。例えば、他のタスクで計算された整数や、ネットワークからダウンロードされたファイルなどが考えられます。futureは、値が利用可能になるまで待つのではなく、値が必要になるまで実行を続けることを可能にします。 + +#### 例 + +future の概念は、小さな例で説明するのが一番です: + +![シーケンス図: main は `read_file` を呼び出して戻るまでブロックされ、次に `foo()` を呼び出して戻るまでブロックされます。同じ処理が繰り返されますが、今回は `async_read_file` が呼ばれ、すぐに future が返されます。そして `foo()` が再び呼ばれ、今度はファイルのロードと同時に実行されます。ファイルは `foo()` が戻る前に利用可能になります。](async-example.svg) + +このシーケンス図は、ファイルシステムからファイルを読み込み、関数 `foo` を呼び出す `main` 関数を示しています。この処理は2回繰り返されます。すなわち、同期的な `read_file` の呼び出しと、非同期的な `async_read_file` の呼び出しです。 + +同期呼び出しの場合、`main`関数はファイルシステムからファイルが読み込まれるまで待つ必要があります。それが終わって初めて、`foo`関数を呼び出すことができ、結果を再び待つ必要があります。 + +非同期の `async_read_file` 呼び出しでは、ファイルシステムがすぐにfutureを返し、バックグラウンドで非同期にファイルをロードします。これにより、`main`関数は`foo`をより早く呼び出すことができ、`foo`はファイルのロードと並行して実行されます。この例では、ファイルのロードは `foo` が戻る前に終了しているので、`main` は `foo` が戻った後にさらに待つことなく、ファイルをすぐに処理することができます。 + +#### RustにおけるFuture + +Rustでは、futureは[`Future`]という trait で表され、次のようになります: + +[`Future`]: https://doc.rust-lang.org/nightly/core/future/trait.Future.html + +```rust +pub trait Future { + type Output; + fn poll(self: Pin<&mut Self>, cx: &mut Context) -> Poll; +} +``` + +[関連型] `Output` は非同期値の型を指定します。例えば、上の図の `async_read_file` 関数は、`Output` を `File` に設定した `Future` インスタンスを返します。 + +[関連型]: https://doc.rust-lang.org/book/ch19-03-advanced-traits.html#specifying-placeholder-types-in-trait-definitions-with-associated-types + +[`poll`]メソッドは、その値がすでに利用可能かどうかをチェックすることができます。このメソッドは、以下のような [`Poll`] 列挙体を返します。 + +[`poll`]: https://doc.rust-lang.org/nightly/core/future/trait.Future.html#tymethod.poll +[`Poll`]: https://doc.rust-lang.org/nightly/core/task/enum.Poll.html + +```rust +pub enum Poll { + Ready(T), + Pending, +} +``` + +値が既に利用可能な場合(例えば、ファイルがディスクから完全に読み込まれた場合)、その値は `Ready` variantにラップされて返されます。それ以外の場合は、`Pending` variantが返され、呼び出し側に値がまだ利用できないことを知らせます。 + +`poll`メソッドは2つの引数を取ります。`self: Pin<&mut Self>`と`cx: &mut Context`です。前者は通常の `&mut self` の参照のように動作しますが、`self` の値がそのメモリロケーションに [ピン留め/固定 (pin)][_pinned_] されるという違いがあります。`Pin`とその必要性を理解するには、まずasync/awaitの仕組みを理解しなければなりません。そのため、それについてはこの記事の後半で説明します。 + +[_pinned_]: https://doc.rust-lang.org/nightly/core/pin/index.html + +`cx: &mut Context`パラメータの目的は、ファイルシステムのロードなどの非同期タスクに[`Waker`]インスタンスを渡すことです。この `Waker` によって、非同期タスクは自分(またはその一部)が終了したこと、例えばファイルがディスクから読み込まれたことを通知することができます。メインタスクは`Future`が準備できたら通知されることを知っているので、`poll`を何度も何度も呼び出す必要はありません。このプロセスについては、後ほど独自の waker 型を実装する際に詳しく説明します。 + +[`Waker`]: https://doc.rust-lang.org/nightly/core/task/struct.Waker.html + +### Futureとの連携 + +futureがどのように定義されているか、また、`poll`メソッドの基本的な考え方を理解しました。しかし、futureを効果的に使う方法はまだわかっていません。問題は、futureが非同期タスクの結果を表していて、それがまだ利用できない可能性があることです。しかし、実際には、これらの値が次の計算のためにすぐに必要になることがよくあります。そこで問題となるのは、どうすれば必要になったときに効率的にfutureの値を取り出すことができるかということです。 + +#### Futureを待つ + +1つの可能な答えは、futureの準備が整うまで待つことです。これは次のようなものです: + +```rust +let future = async_read_file("foo.txt"); +let file_content = loop { + match future.poll(…) { + Poll::Ready(value) => break value, + Poll::Pending => {}, // 何もしない + } +} +``` + +ここでは、`poll`をループで何度も呼び出すことで、futureを「積極的」に待つようにしています。`poll`の引数はここでは重要ではないので、省略しています。この解決策はうまくいきはしますが、値が利用可能になるまでCPUを忙しくさせているので、非常に非効率的です。 + +より効率的なアプローチは、futureが利用可能になるまで現在のスレッドを **ブロック** することです。もちろん、これはスレッドがある場合にのみ可能なことで、この解決策は、少なくとも現時点では私たちのカーネルでは機能しません。ブロッキングがサポートされているシステムでも、非同期タスクが同期タスクに戻ってしまい、並列タスクの潜在的なパフォーマンスの利点が阻害されてしまうため、ブロッキングは好まれません。 + +#### Futureコンビネータ + +待機する代わりに、Futureコンビネータを使うこともできます。Futureコンビネータは `map` のようなメソッドで、[`Iterator`] のメソッドと同じように、futureを連鎖させたり組み合わせたりすることができます。これらのコンビネータはfutureを待つのではなくfutureを返し、それによって`poll`のmap操作を適用します。 + +[`Iterator`]: https://doc.rust-lang.org/stable/core/iter/trait.Iterator.html + +例として、`Future`を`Future`に変換するためのシンプルな`string_len`コンビネータは次のようになります: + +```rust +struct StringLen { + inner_future: F, +} + +impl Future for StringLen where F: Future { + type Output = usize; + + fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { + match self.inner_future.poll(cx) { + Poll::Ready(s) => Poll::Ready(s.len()), + Poll::Pending => Poll::Pending, + } + } +} + +fn string_len(string: impl Future) + -> impl Future +{ + StringLen { + inner_future: string, + } +} + +// 使用例 +fn file_len() -> impl Future { + let file_content_future = async_read_file("foo.txt"); + string_len(file_content_future) +} +``` + +このコードは、[**ピン留め**](pinning)を扱っていないので、完全には動作しませんが、例としては十分です。基本的なアイデアは、`string_len` 関数が、与えられた `Future` インスタンスを、新しい `StringLen` 構造体にラップするというもので、この構造体も `Future` を実装しています。ラップされたfutureがポーリングされると、内部のfutureをポーリングします。値がまだ準備できていない場合は、ラップされたfutureからも `Poll::Pending` が返されます。値の準備ができていれば、`Poll::Ready` variantから文字列が抽出され、その長さが計算されます。その後、再び `Poll::Ready` にラップされて返されます。 + +[**ピン留め**]: https://doc.rust-lang.org/stable/core/pin/index.html + +この`string_len`関数を使えば、非同期の文字列を待つことなく、その長さを計算することができます。この関数は再び`Future`を返すので、呼び出し側は返された値を直接扱うことはできず、再びコンビネータ関数を使う必要があります。このようにして、呼び出しグラフ全体が非同期になったので、どこかの時点で、例えばmain関数の中で、一度に複数のfutureを効率的に待つことができるようになりました。 + +コンビネータ関数を手動で書くのは難しいので、ライブラリで提供されることが多いです。Rustの標準ライブラリ自体はまだコンビネータのメソッドを提供していませんが、半公式(かつ`no_std`互換)の[`futures`]クレートは提供しています。その[`FutureExt`] traitは、[`map`]や[`then`]といった高レベルのコンビネータメソッドを提供しており、これを使って任意のクロージャで結果を操作することができます。 + +[`futures`]: https://docs.rs/futures/0.3.4/futures/ +[`FutureExt`]: https://docs.rs/futures/0.3.4/futures/future/trait.FutureExt.html +[`map`]: https://docs.rs/futures/0.3.4/futures/future/trait.FutureExt.html#method.map +[`then`]: https://docs.rs/futures/0.3.4/futures/future/trait.FutureExt.html#method.then + +##### 利点 + +Futureコンビネータの大きな利点は、操作を非同期に保つことができることです。非同期I/Oインターフェイスと組み合わせることで、このアプローチは非常に高いパフォーマンスを実現します。Futureコンビネータは通常のtrait実装付き構造体として実装されているため、コンパイラはこれを非常によく最適化できます。詳細については、Rustのエコシステムにfutureが追加されたことを発表した[_Zero-cost futures in Rust_]の記事を参照してください。 + +[_Zero-cost futures in Rust_]: https://aturon.github.io/blog/2016/08/11/futures/ + +##### 欠点 {#drawbacks} + +Futureコンビネータを使うと、非常に効率的なコードを書くことができますが、型システムやクロージャベースのインターフェイスのため、状況によっては使いにくいことがあります。例えば、次のようなコードを考えてみましょう: + +```rust +fn example(min_len: usize) -> impl Future { + async_read_file("foo.txt").then(move |content| { + if content.len() < min_len { + Either::Left(async_read_file("bar.txt").map(|s| content + &s)) + } else { + Either::Right(future::ready(content)) + } + }) +} +``` + +([Try it on the playground](https://play.rust-lang.org/?version=stable&mode=debug&edition=2018&gist=91fc09024eecb2448a85a7ef6a97b8d8)) + +ここでは、ファイル `foo.txt` を読み込んでから、[`then`] コンビネータを使って、ファイルの内容に基づいて 2 番目の future を連鎖させています。もしコンテンツの長さが与えられた `min_len` よりも小さければ、別の `bar.txt` ファイルを読み込んで、[`map`] コンビネータを使って `content` に追加します。それ以外の場合は、`foo.txt` の内容のみを返します。 + +`min_len` のライフタイムエラーが発生するのを防ぐため、`then` に渡すクロージャには [`move` キーワード]を使用する必要があります。[`Either`] ラッパーを使う理由は、if と else のブロックは常に同じ型でなければならないからです。ブロックの中で異なるfutureの型を返しているので、ラッパーの型を使って単一の型に統一する必要があります。[`ready`] 関数とは、もう既に手元にあるデータを、『一瞬で準備の完了するfuture』へと変換する関数です。`Either` ラッパーはラップされた値が`Future`を実装していることを期待しているので、ここではこの関数が必要です。 + +[`move` キーワード]: https://doc.rust-lang.org/std/keyword.move.html +[`Either`]: https://docs.rs/futures/0.3.4/futures/future/enum.Either.html +[`ready`]: https://docs.rs/futures/0.3.4/futures/future/fn.ready.html + +ご想像のとおり、大規模なプロジェクトでは非常に複雑なコードになることがあります。特に、借用や異なるライフタイムが関係する場合は複雑になります。このような理由から、Rustにasync/awaitのサポートを追加するために多くの作業が行われ、非同期のコードを圧倒的にシンプルに書くことができるようになりました。 + +### Async/Awaitパターン + +async/awaitの背後にある考え方は、プログラマに、見た目は通常の同期コードのように見えるが、コンパイラによって非同期コードに変換されるコードを書かせることです。これは `async` と `await` という2つのキーワードに基づいて動作します。キーワード `async` は、関数のシグネチャの中で使用することができ、同期関数を、futureの値を返す非同期関数に変えることができます: + +```rust +async fn foo() -> u32 { + 0 +} + +// 上記はコンパイラによって次のように変換されます: +fn foo() -> impl Future { + future::ready(0) +} +``` + +このキーワードだけではそれほど便利ではありません。しかし、`async`関数の中では、`await`キーワードを使って、futureの値を非同期に取得することができます: + +```rust +async fn example(min_len: usize) -> String { + let content = async_read_file("foo.txt").await; + if content.len() < min_len { + content + &async_read_file("bar.txt").await + } else { + content + } +} +``` + +([Try it on the playground](https://play.rust-lang.org/?version=stable&mode=debug&edition=2018&gist=d93c28509a1c67661f31ff820281d434)) + +この関数は、[上記](#drawbacks)のコンビネータ関数を使った `example` 関数をそのまま翻訳したものです。 `.await` 演算子を使うことで、クロージャや `Either` 型を必要とせずに future の値を取得することができます。その結果、まるで通常の同期コードを書いているかのように非同期コードを書くことができます。 + +#### ステートマシンへの変換 + +舞台裏で何をしているかというと、`async`関数の本体を[**ステートマシン (state machine)**]に変換し、`.await`を呼び出すたびに異なる状態を表すようにしています。上記の `example` 関数の場合、コンパイラは以下の4つの状態を持つステートマシンを作成します: + +[**ステートマシン (state machine)**]: https://en.wikipedia.org/wiki/Finite-state_machine + +![Four states: start, waiting on foo.txt, waiting on bar.txt, end](async-state-machine-states.svg) + +各ステートは、関数の異なる待ち状態を表しています。 **"Start"** と **"End"** の状態は、関数の実行開始時と終了時を表しています。 **"Waiting on foo.txt"** の状態は、関数が最初の`async_read_file` の結果を待っていることを表しています。同様に、 **"Waiting on bar.txt"** 状態は、関数が2つ目の`async_read_file`の結果を待っている待ち状態を表しています。 + +ステートマシンは、各 `poll` 呼び出しを可能な状態遷移に変換することで、`Future` traitを実装しています: + +![Four states: start, waiting on foo.txt, waiting on bar.txt, end](async-state-machine-basic.svg) + +この図では、矢印で状態の切り替えを、ダイヤ形で条件分岐を表現しています。例えば、`foo.txt`のファイルが準備できていない場合、 **"no"** と書かれたパスが取られ、 **"Waiting on foo.txt"** の状態になります。それ以外の場合は、 **"yes"** のパスが取られます。キャプションのない小さな赤いダイヤは、`example`関数の`if content.len() < 100`の分岐を表しています。 + +最初の `poll` 呼び出しで関数が開始され、まだ準備ができていないfutureに到達するまで実行されていることがわかります。パス上のすべてのfutureが準備できていれば、関数は **"End"** 状態まで実行でき、そこで結果を `Poll::Ready` でラップして返します。そうでなければ、ステートマシンは待機状態になり、`Poll::Pending`を返します。次の `poll` 呼び出し時には、ステートマシンは最後の待ち状態から開始し、最後の操作を再試行します。 + +#### 状態を保存 + +最後に待機していた状態から継続できるようにするために、ステートマシンは現在の状態を内部的に追跡する必要があります。さらに、次の `poll` 呼び出しで実行を継続するために必要なすべての変数を保存する必要があります。ここでコンパイラが威力を発揮します。コンパイラは、どの変数がいつ使われるかを知っているので、必要な変数だけを持つ構造体を自動的に生成することができます。 + +例として、コンパイラは上記の `example` 関数に対して以下のような構造体を生成します: + +```rust +// `example`関数は既に上の方で定義されていましたが、画面をスクロールして探さなくても良いように、ここに再び定義しておきます +async fn example(min_len: usize) -> String { + let content = async_read_file("foo.txt").await; + if content.len() < min_len { + content + &async_read_file("bar.txt").await + } else { + content + } +} + +// コンパイラが生成したState構造体です: + +struct StartState { + min_len: usize, +} + +struct WaitingOnFooTxtState { + min_len: usize, + foo_txt_future: impl Future, +} + +struct WaitingOnBarTxtState { + content: String, + bar_txt_future: impl Future, +} + +struct EndState {} +``` + +"start" と **"Waiting on foo.txt"** の状態では、`min_len`パラメータを保存する必要があります。これは後に`content.len()`と比較する際に必要になるからです。 **"Waiting on foo.txt"** 状態では、さらに`foo_txt_future`が格納されます。これは、`async_read_file`呼び出しが返したfutureを表します。このfutureは、ステートマシンが継続する際に再びポーリングされる必要があるため、保存する必要があります。 + +**"Waiting on bar.txt"** の状態には、`bar.txt`の準備ができた後の文字列の連結に必要な`content`変数が含まれています。また、`bar.txt`がロード中であることを表す`bar_txt_future`も格納されています。この構造体には、`min_len`変数は含まれていません。これは、`content.len()`の比較の後では、もはや必要ないからです。 **"end"** の状態では、関数はすでに完了まで実行されているので、変数は格納されません。 + +これはコンパイラが生成しうるコードの一例に過ぎないことに注意してください。構造体の名前やフィールドのレイアウトは実装においては枝葉末節であり、異なる可能性があります。 + +#### 完全なステートマシンの型 + +具体的にコンパイラがどのようなコードを生成するのかは実装依存ですが、`example` 関数に対してどのようなステートマシンが生成されうるかを想像することは、理解を助けることになります。異なる状態を表し、必要な変数を含む構造体はすでに定義されています。これらの構造体の上にステートマシンを作成します。そのためには、これらの構造体を[`enum`]構造体にまとめるという方法があります: + +[`enum`]: https://doc.rust-lang.org/book/ch06-01-defining-an-enum.html + +```rust +enum ExampleStateMachine { + Start(StartState), + WaitingOnFooTxt(WaitingOnFooTxtState), + WaitingOnBarTxt(WaitingOnBarTxtState), + End(EndState), +} +``` + +各状態に対応して個別のenum variantを定義し、対応するstate構造体をフィールドとして各variantに追加しています。状態の遷移を実装するために、コンパイラは `example` 関数に基づいて `Future` traitの実装を生成します: + +```rust +impl Future for ExampleStateMachine { + type Output = String; // `example`の返り値の型 + + fn poll(self: Pin<&mut Self>, cx: &mut Context) -> Poll { + loop { + match self { // TODO: ピン留めを処理する + ExampleStateMachine::Start(state) => {…} + ExampleStateMachine::WaitingOnFooTxt(state) => {…} + ExampleStateMachine::WaitingOnBarTxt(state) => {…} + ExampleStateMachine::End(state) => {…} + } + } + } +} +``` + +関数 `example` の戻り値であるため、futureの `Output` 型は `String` となります。`poll`関数を実装するために、`loop` の中で現在の状態に対するmatch文を使います。これは、可能な限り次の状態に切り替え、継続できないときには明示的に `return Poll::Pending` を使用するというものです。 + +簡単のため、ここでは簡略化したコードのみを示し、[ピン留め][**ピン留め**]、所有権、寿命などは扱っていません。そのため、このコードと以下のコードは疑似コードとして扱い、直接使用しないでください。もちろん、実際にコンパイラが生成したコードは、おそらく異なる方法ではあるものの、すべてを正しく処理します。 + +コードの抜粋が長大になるのを防ぐために、各マッチアームのコードを別々に紹介します。まず、`Start`の状態から始めましょう: + +```rust +ExampleStateMachine::Start(state) => { + // from body of `example` + let foo_txt_future = async_read_file("foo.txt"); + // `.await` operation + let state = WaitingOnFooTxtState { + min_len: state.min_len, + foo_txt_future, + }; + *self = ExampleStateMachine::WaitingOnFooTxt(state); +} +``` + +関数の冒頭ではステートマシンが `Start` 状態にあります。このとき、`example`関数の中身を最初の`.await`まですべて実行します。`.await`の操作を処理するために、`self`ステートマシンの状態を`WaitingOnFooTxt`に変更し、`WaitingOnFooTxtState`構造体の構築を行います。 + +`match self {...}`文はloopで実行されるので、実行は`WaitingOnFooTxt`アームにジャンプします: + +```rust +ExampleStateMachine::WaitingOnFooTxt(state) => { + match state.foo_txt_future.poll(cx) { + Poll::Pending => return Poll::Pending, + Poll::Ready(content) => { + // from body of `example` + if content.len() < state.min_len { + let bar_txt_future = async_read_file("bar.txt"); + // `.await` operation + let state = WaitingOnBarTxtState { + content, + bar_txt_future, + }; + *self = ExampleStateMachine::WaitingOnBarTxt(state); + } else { + *self = ExampleStateMachine::End(EndState)); + return Poll::Ready(content); + } + } + } +} +``` + +このマッチアームでは、まず `foo_txt_future` の `poll` 関数を呼び出します。もし準備ができていなければ、ループを抜けて `Poll::Pending` を返します。この場合、`self`は`WaitingOnFooTxt`状態のままなので、ステートマシンの次の`poll`呼び出しは同じマッチアームに入り、`foo_txt_future`のポーリングを再試行することになります。 + +`foo_txt_future`の準備ができていたら、その結果を`content`変数に代入して、引き続き`example`関数のコードを実行します。`content.len()`がstate構造体に保存されている`min_len`よりも小さければ、`bar.txt`ファイルが非同期に読み込まれます。`.await`の操作を再び状態の変化に変換し、今回は`WaitingOnBarTxt`の状態にします。ループ内で `match` を実行しているので、その後新しい状態のマッチアームにすぐにジャンプし、そこで `bar_txt_future` がポーリングされます。 + +`else`の分岐に入った場合、それ以上の`.await`操作は発生しません。関数の最後に到達したため、`Poll::Ready`でラップされた`content`を返します。また、現在の状態を `End` に変更します。 + +`WaitingOnBarTxt`の状態のコードは以下のようになります: + +```rust +ExampleStateMachine::WaitingOnBarTxt(state) => { + match state.bar_txt_future.poll(cx) { + Poll::Pending => return Poll::Pending, + Poll::Ready(bar_txt) => { + *self = ExampleStateMachine::End(EndState)); + // from body of `example` + return Poll::Ready(state.content + &bar_txt); + } + } +} +``` + +`WaitingOnFooTxt`の状態と同様に、まず`bar_txt_future`をポーリングします。まだ保留中 (pending) であれば、ループを抜けて `Poll::Pending` を返します。そうでなければ、`example`関数の最後の操作(`content`変数とfutureからの結果の連結)を行います。ステートマシンを `End` 状態に更新して、`Poll::Ready` でラップされた結果を返します。 + +最後に、`End`状態のコードは以下のようになります: + +```rust +ExampleStateMachine::End(_) => { + panic!("poll called after Poll::Ready was returned"); + // "Poll::Readyが返された後にpollが呼び出されました" +} +``` + +Futureは `Poll::Ready` を返した後、再びポーリングされるべきではありません。したがって、すでに `End` の状態にあるときに `poll` が呼ばれるとパニックするようにしましょう。 + +コンパイラが生成するステートマシンとその `Future` traitの実装はこのようになっている**かもしれません**。実際には、コンパイラは異なる方法でコードを生成しています。 (一応、現在は[_generators_]をベースにした実装になっていますが、これはあくまでも実装の詳細です。) + +[_generators_]: https://doc.rust-lang.org/nightly/unstable-book/language-features/generators.html + +パズルの最後のピースは、生成された `example` 関数自体のコードです。関数のヘッダは次のように定義されていたことを思い出してください: + +```rust +async fn example(min_len: usize) -> String +``` + +関数本体はすべてステートマシンによって実装されたので、この関数がするべきことはステートマシンを初期化して返すことだけです。これを行う自動生成コードは次のようになります: + +```rust +fn example(min_len: usize) -> ExampleStateMachine { + ExampleStateMachine::Start(StartState { + min_len, + }) +} +``` + +この関数は、`async`修飾子を持たなくなり、`Future` traitを実装した`ExampleStateMachine`型を明示的に返すようになりました。予想通り、ステートマシンは `Start` 状態で構築され、対応するstate構造体は `min_len` パラメータで初期化されます。 + +この関数は、ステートマシンの実行を開始しないことに注意してください。これは『最初にポーリングされるまで何もしない』という、Rustにおけるfutureの基本的な設計上の決定を反映したものです。 + +### ピン留め + +この記事の中で、すでに何度も「ピン留め」について触れています。今こそ、ピン留めとは何か、なぜピン留めが必要なのかを探る時です。 + +#### 自己参照構造体 + +上で説明したように、ステートマシン変換では、各待ち状態のローカル変数を構造体に格納します。私たちの `example` 関数のような小さな例では、これは簡単で、特に問題にはなりませんでした。しかし、変数が相互に参照し合う場合には、問題が難しくなります。例えば、次の関数を考えてみましょう: + +```rust +async fn pin_example() -> i32 { + let array = [1, 2, 3]; + let element = &array[2]; + async_write_file("foo.txt", element.to_string()).await; + *element +} +``` + +この関数は、内容が `1`, `2`, `3` の小さな `array` を作成します。そして、配列の最後の要素への参照を作成し、それを `element` 変数に格納します。次に、文字列に変換された数値を非同期的に `foo.txt` ファイルに書き込みます。最後に、`element`で参照していた数値を返します。 + +この関数は1つの `await` オペレーションを使用するため、結果として得られるステートマシンには start、end、"waiting on write" の 3 つの状態があります。この関数は引数を取らないので、開始状態 (start) の構造体は空です。先ほどと同じように、end状態の時点で関数は終了しているので、この状態の構造体も空になります。"waiting on write"の状態を表す構造体はもっと面白いです: + +```rust +struct WaitingOnWriteState { + array: [1, 2, 3], + element: 0x1001c, // 配列の最後の要素のアドレス +} +``` + +戻り値には `element` が必要であり、 `array` は `element` によって参照されるので、`array` と `element` の両方の変数を格納する必要があります。`element`は参照なので、参照されている要素への **ポインタ** (つまり、メモリ上のアドレス)を格納します。ここでは、メモリアドレスの例として、`0x1001c` を使用しました。実際には、`array`フィールドの最後の要素のアドレスである必要がありますので、構造体がメモリ内のどこに存在するかに依存します。このような内部ポインタを持つ構造体は、フィールドの1つから自分自身を参照するため、 **自己参照**構造体と呼ばれます。 + +#### 自己参照構造体の問題点 + +自己参照構造体の内部ポインタには根本的な問題があり、それは構造体のメモリレイアウトを見ると明らかになります: + +![array at 0x10014 with fields 1, 2, and 3; element at address 0x10020, pointing to the last array element at 0x1001c](self-referential-struct.svg) + +`array`フィールドはアドレス0x10014から始まり、`element`フィールドはアドレス0x10020から始まります。最後の配列要素がこのアドレスにあるので、アドレス0x1001cを指しています。この時点では、まだすべてが順調です。しかし、この構造体を別のメモリアドレスに移動させると問題が発生します: + +![array at 0x10024 with fields 1, 2, and 3; element at address 0x10030, still pointing to 0x1001c, even though the last array element now lives at 0x1002c](self-referential-struct-moved.svg) + +構造体を少し移動して、アドレス `0x10024` から始まるようにしました。これは、構造体を関数の引数として渡したり、別のスタック変数に代入したりしたときに起こります。問題は、最後の `array` 要素のアドレスが `0x1002c` になったにもかかわらず、`element` フィールドは未だアドレス `0x1001c` 番地を指していることです。そのため、ポインタがダングリングし(訳注:無効な場所を指すという意味)、次の `poll` 呼び出し時に未定義の動作が発生してしまいます。 + +#### 考えられる解決策 + +ダングリングポインタ問題を解決するための基本的なアプローチは3つあります: + +- **ムーブの際にポインタを更新する:** このアイデアは、構造体がメモリ内で移動するたびに内部ポインタを更新し、移動後も有効になるようにするものです。残念ながら、この方法では Rust に大規模な変更を加える必要があり、その結果、パフォーマンスが大幅に低下する可能性があります。その理由は、ある種のランタイムがすべての構造体のフィールドの型を追跡し、移動操作のたびにポインタの更新が必要かどうかをチェックする必要があるからです。 +- **自己参照のかわりにオフセットを格納する:**: ポインタを更新する必要性を回避するために、コンパイラは自己参照を構造体の先頭からのオフセットとして格納することを試みるという手があります。例えば、上記の `WaitingOnWriteState` 構造体の `element` フィールドは、値が 8 の `element_offset` フィールドという形式で保存することもできるでしょう。これは、参照先の配列要素が構造体の先頭から 8 バイト後に始まるからです。構造体を移動してもオフセットは変わらないので、フィールドの更新は必要ありません。 + + このアプローチの問題点は、コンパイラがすべての自己参照を検出する必要があることです。これは、参照の値がユーザーの入力に依存する可能性があるため、コンパイル時には不可能です。そのため、参照を分析して状態構造体を正しく作成するために、再びランタイムシステムが必要になります。これではランタイムのコストがかかるだけでなく、ある種のコンパイラの最適化もできなくなるため、同じく大きなパフォーマンスの低下を招くことになります。 +- **構造体のムーブを禁止する:** 上で見たように、ダングリングポインタが発生するのは、構造体をメモリ上でムーブさせたときだけです。自己参照構造体に対するムーブ操作を完全に禁止することで、この問題も回避することができます。この方法の大きな利点は、実行時 (ランタイム) の追加コストなしに、型システムのレベルで実装できることです。欠点は、自己参照をしているかもしれない構造体の移動操作の問題を解決する負担がプログラマにかかってしまうことです。 + +**ゼロコスト抽象化**(抽象化は実行時のコストを増やしてはならないという原則) を提供するというRustの理念から、Rustは3つ目の解決策を選択しました。そのために [RFC2349](https://github.com/rust-lang/rfcs/blob/master/text/2349-pin.md) で提案されたのが [**pinning (ピン留め)**][**ピン留め**] APIです。以下では、このAPIの概要を説明し、async/awaitやfutureでどのように動作するかを説明します。 + +#### ヒープ上の値 + +まず最初に、[ヒープ上に確保]された値は、ほとんどの場合、すでに固定のメモリアドレスを持っているということに気づきます。これらの値は、`allocate` の呼び出しで作成され、`Box`のようなポインタ型で参照されます。ポインタ型を移動することは可能ですが、ポインタが指すヒープ上の値は、再び `deallocate` 呼び出しで解放されるまで、同じメモリアドレスに留まります。 + +[ヒープ上に確保]: @/edition-2/posts/10-heap-allocation/index.md + +ヒープ割り当てを利用して、自己参照型の構造体を作成してみましょう: + +```rust +fn main() { + let mut heap_value = Box::new(SelfReferential { + self_ptr: 0 as *const _, + }); + let ptr = &*heap_value as *const SelfReferential; + heap_value.self_ptr = ptr; + println!("heap value at: {:p}", heap_value); + println!("internal reference: {:p}", heap_value.self_ptr); +} + +struct SelfReferential { + self_ptr: *const Self, +} +``` + +([Try it on the playground][playground-self-ref]) + +[playground-self-ref]: https://play.rust-lang.org/?version=stable&mode=debug&edition=2018&gist=ce1aff3a37fcc1c8188eeaf0f39c97e8 + +`SelfReferential` という名前のシンプルな構造体を作成します。この構造体には1つのポインタフィールドが含まれます。まず、この構造体をNULLポインタで初期化し、`Box::new`を使ってヒープ上に確保します。次に、ヒープに割り当てられた構造体のメモリアドレスを決定し、それを `ptr` 変数に格納します。最後に、`ptr`変数を`self_ptr`フィールドに代入して、構造体を自己参照にします。 + +このコードを[playground][playground-self-ref]で実行すると、ヒープ値のアドレスとその内部ポインタが等しいことがわかります。これは、`self_ptr`フィールドが有効な自己参照であることを意味します。`heap_value` 変数は単なるポインタなので、それを移動させても(例えば関数に渡しても)構造体自体のアドレスは変わらないので、ポインタを移動させても`self_ptr`は有効なままです。 + +しかし、この例を破綻させてしまう方法はまだあります。`Box`からその中身をムーブしたり、その内容を置き換えたりすることができます: + +```rust +let stack_value = mem::replace(&mut *heap_value, SelfReferential { + self_ptr: 0 as *const _, +}); +println!("value at: {:p}", &stack_value); +println!("internal reference: {:p}", stack_value.self_ptr); +``` + +([Try it on the playground](https://play.rust-lang.org/?version=stable&mode=debug&edition=2018&gist=e160ee8a64cba4cebc1c0473dcecb7c8)) + +ここでは、[`mem::replace`]関数を使用して、ヒープに割り当てられた値を新しい構造体のインスタンスで置き換えています。これにより、元の `heap_value` をスタックに移動させることができますが、構造体の `self_ptr` フィールドは、古いヒープアドレスを指し示すダングリングポインタになっています。この例をplaygroundで実行してみると、出力された **"value at:"** と **"internal reference:"** の行には、たしかに異なるポインタが表示されていることがわかります。つまり、値をヒープに割り当てるだけでは、自己参照を安全にするには不十分なのです。 + +[`mem::replace`]: https://doc.rust-lang.org/nightly/core/mem/fn.replace.html + +上記の破綻を許した根本的な問題は、`Box`によって、ヒープに割り当てられた値への`&mut T`参照を得ることができることです。この `&mut` 参照によって、 [`mem::replace`] や [`mem::swap`] などのメソッドを使って、ヒープに割り当てられた値を無効にすることが可能になります。この問題を解決するためには、自己参照構造体への `&mut` 参照が作成できないようにする必要があります。 + +[`mem::swap`]: https://doc.rust-lang.org/nightly/core/mem/fn.swap.html + +#### `Pin>`と`Unpin` + +ピン留めのAPIは、[`Pin`]ラッパー型と[`Unpin`]マーカーtraitという形で、`&mut T`問題に対する解決策を提供します。これらの型の背景にある考え方は、(`Pin`によって)ラップされた値への `&mut` 参照を取得するために使用できる `Pin` のすべてのメソッド (例えば、[`get_mut`][pin-get-mut] や [`deref_mut`][pin-deref-mut]) を `Unpin` trait に限定することです。`Unpin` traitは[**自動trait**][_auto trait_]であり、明示的に使用しないよう宣言した型を除くすべての型に対して自動的に実装されます。自己参照構造体は `Unpin` を使用しないようにさせることで、`Pin>` 型から `&mut T` を得る (安全な) 方法を無くすことができます。その結果、それらの内部の自己参照が有効であることが保証されます。 + +[`Pin`]: https://doc.rust-lang.org/stable/core/pin/struct.Pin.html +[`Unpin`]: https://doc.rust-lang.org/nightly/std/marker/trait.Unpin.html +[pin-get-mut]: https://doc.rust-lang.org/nightly/core/pin/struct.Pin.html#method.get_mut +[pin-deref-mut]: https://doc.rust-lang.org/nightly/core/pin/struct.Pin.html#impl-DerefMut +[_auto trait_]: https://doc.rust-lang.org/reference/special-types-and-traits.html#auto-traits + +例として、上記の `SelfReferential` 型を更新して、`Unpin` を使用しないようにしてみましょう: + +```rust +use core::marker::PhantomPinned; + +struct SelfReferential { + self_ptr: *const Self, + _pin: PhantomPinned, +} +``` + +[`PhantomPinned`]型の2つ目のフィールド `_pin` を追加することで`Unpin`を使用しないようにします。この型はゼロサイズのマーカー型で、`Unpin` trait を実装**しない**ようにするためだけに置かれています。[自動trait][_auto trait_]の仕組み上、`Unpin`ではないフィールドが1つでもあれば、構造体全体が`Unpin`を使用しないようになります。 + +[`PhantomPinned`]: https://doc.rust-lang.org/nightly/core/marker/struct.PhantomPinned.html + +第二のステップは、上の例の `Box` 型を `Pin>` 型に変更することです。これを行う最も簡単な方法は、ヒープに値を割り当てるために、[`Box::new`]関数ではなく[`Box::pin`]関数を使用することです: + +[`Box::pin`]: https://doc.rust-lang.org/nightly/alloc/boxed/struct.Box.html#method.pin +[`Box::new`]: https://doc.rust-lang.org/nightly/alloc/boxed/struct.Box.html#method.new + +```rust +let mut heap_value = Box::pin(SelfReferential { + self_ptr: 0 as *const _, + _pin: PhantomPinned, +}); +``` + +`Box::new` を `Box::pin` に変更することに加えて、構造体を初期化するコード(イニシャライザ)に新しい `_pin` フィールドを追加する必要があります。`PhantomPinned` はゼロサイズの型なので、初期化に必要なのはその型名だけです。 + +今、[調整した例を実行してみると](https://play.rust-lang.org/?version=stable&mode=debug&edition=2018&gist=961b0db194bbe851ff4d0ed08d3bd98a)、動作しなくなっていることがわかります: + +``` +error[E0594]: cannot assign to data in a dereference of `std::pin::Pin>` + --> src/main.rs:10:5 + | +10 | heap_value.self_ptr = ptr; + | ^^^^^^^^^^^^^^^^^^^^^^^^^ cannot assign + | + = help: trait `DerefMut` is required to modify through a dereference, but it is not implemented for `std::pin::Pin>` + +error[E0596]: cannot borrow data in a dereference of `std::pin::Pin>` as mutable + --> src/main.rs:16:36 + | +16 | let stack_value = mem::replace(&mut *heap_value, SelfReferential { + | ^^^^^^^^^^^^^^^^ cannot borrow as mutable + | + = help: trait `DerefMut` is required to modify through a dereference, but it is not implemented for `std::pin::Pin>` +``` + +どちらのエラーも、`Pin>` 型が `DerefMut` trait を実装しなくなったために発生します。これはまさに求めていた結果であり、というのも、`DerefMut` trait は `&mut` 参照を返してしまうからで、私達はこれを防ぎたかったのです。これは、`Unpin` を使用しないようにして、`Box::new` を `Box::pin` に変更したからこそ起こる現象です。 + +ここで問題になるのは、コンパイラが16行目の型の移動を禁止するだけでなく、10行目の`self_ptr`フィールドの初期化も禁止してしまうことです。これは、コンパイラが `&mut` 参照の有効な使用と無効な使用を区別できないために起こります。初期化が再びうまくいくようにするには、安全ではない [`get_unchecked_mut`] メソッドを使用する必要があります: + +[`get_unchecked_mut`]: https://doc.rust-lang.org/nightly/core/pin/struct.Pin.html#method.get_unchecked_mut + +```rust +// フィールドを変更しても構造体全体が移動するわけではないので、安全です。 +unsafe { + let mut_ref = Pin::as_mut(&mut heap_value); + Pin::get_unchecked_mut(mut_ref).self_ptr = ptr; +} +``` + +([Try it on the playground](https://play.rust-lang.org/?version=stable&mode=debug&edition=2018&gist=b9ebbb11429d9d79b3f9fffe819e2018)) + +[`get_unchecked_mut`] 関数は `Pin>` ではなく `Pin<&mut T>` に対して動作するため、事前に [`Pin::as_mut`] を使用して値を変換する必要があります。その後、`get_unchecked_mut` が返す `&mut` 参照を使って、`self_ptr` フィールドを設定することができます。 + +[`Pin::as_mut`]: https://doc.rust-lang.org/nightly/core/pin/struct.Pin.html#method.as_mut + +これで残された唯一のエラーは、`mem::replace`における期待どおりのエラーです。この操作は、ヒープに割り当てられた値をスタックに移動させようとするもので、`self_ptr` フィールドに格納されている自己参照を破壊することになります。`Unpin`を使用するのをやめ、`Pin>` を使用することで、コンパイル時にこの操作を防ぐことができ、自己参照構造体を安全に扱うことができます。先ほど見たように、コンパイラは自己参照の生成が安全であることを(まだ)証明することができないので、unsafe ブロックを使用して、自分で正しさを検証する必要があります。 + +#### スタックのピン留めと `Pin<&mut T>` + +前のセクションでは、`Pin>` を使って、ヒープに割り当てられた自己参照の値を安全に作成する方法を学びました。この方法はうまく機能し、(初期化の際にunsafeであったことを除けば)比較的安全ですが、必要なヒープの割り当てにはパフォーマンス上のコストがかかります。Rust は常に可能な限り **ゼロコスト抽象化** を提供したいと考えていますので、pinning API では、スタックに割り当てられた値を指す `Pin<&mut T>` インスタンスを作成することもできます。 + +ラップされた値の所有権を持つ `Pin>` インスタンスとは異なり、`Pin<&mut T>` インスタンスはラップされた値を一時的に借用しているだけです。これは、プログラマーが自分で保証をしなければならないことが増えることになるので、事態をより複雑にしています。最も重要なことは、`Pin<&mut T>` は、参照される `T` のライフタイム全体にわたってピン留めされていなければならないということですが、これはスタックベースの変数の場合には検証が困難です。この問題を解決するために、[`pin-utils`]のようなクレートが存在しますが、自分が何をしているかを本当に理解していない限り、スタック変数のpinはお勧めできません。 + +[`pin-utils`]: https://docs.rs/pin-utils/0.1.0-alpha.4/pin_utils/ + +詳しくは、[`pin` module]と[`Pin::new_unchecked`]メソッドのドキュメントをご覧ください。 + +[`pin` module]: https://doc.rust-lang.org/nightly/core/pin/index.html +[`Pin::new_unchecked`]: https://doc.rust-lang.org/nightly/core/pin/struct.Pin.html#method.new_unchecked + +#### ピン留めとFuture + +この記事ですでに見たように、[`Future::poll`]メソッドは、`Pin<&mut Self>`パラメータの形でピンを使用しています: + +[`Future::poll`]: https://doc.rust-lang.org/nightly/core/future/trait.Future.html#tymethod.poll + +```rust +fn poll(self: Pin<&mut Self>, cx: &mut Context) -> Poll +``` + +このメソッドが通常の`&mut self`ではなく`self: Pin<&mut Self>`を取る理由は、[上][self-ref-async-await]で見たように、async/awaitから生成されるfutureのインスタンスはしばしば自己参照しているためです。`Self` を `Pin` にラップして、async/await から生成された自己参照のfutureに対して、コンパイラに `Unpin` を選択させることで、`poll` 呼び出しの間にfutureがメモリ内で移動しないことが保証されます。これにより、すべての内部参照が有効であることが保証されます。 + +[self-ref-async-await]: @/edition-2/posts/12-async-await/index.md#self-referential-structs + +注目すべきは、最初の `poll` 呼び出しの前にfutureを移動させることは問題ないということです。これは、futureがlazyであり、最初にポーリングされるまで何もしないという事実に起因しています。そのため、生成されたステートマシンの `start` 状態には、関数の引数だけが含まれており、内部参照は含まれていません。`poll` を呼び出すためには、呼び出し側はまずfutureを `Pin` にラップしなければなりません。これにより、futureがメモリ上で移動できなくなります。スタック上で正しく pin するのは、ヒープ上でするよりも難しいので、[`Box::pin`]と[`Pin::as_mut`]を組み合わせて使用することをお勧めします。 + +[`futures`]: https://docs.rs/futures/0.3.4/futures/ + +スタック変数のピン留めを使ってfutureのコンビネータ関数を安全に実装する方法を知りたい場合は、比較的短い `futures` クレートの [`map` コンビネータメソッドのソースコード][map-src] と pin のドキュメントの [projections and structural pinning] のセクションを見てください。 + + +[map-src]: https://docs.rs/futures-util/0.3.4/src/futures_util/future/future/map.rs.html +[projections and structural pinning]: https://doc.rust-lang.org/stable/std/pin/index.html#projections-and-structural-pinning + +### Executor と Waker + +async/awaitを使えば、完全に非同期的なfutureを簡単に扱うことができます。しかし、上で学んだように、futureはポーリングされるまで何もしません。つまり、どこかの時点で`poll`を呼ばないと、非同期コードは実行されないということです。 + +単一のfutureであれば、[上述のように](#futurewodai-tu)ループを使って常に手動で各futureを待つことができます。しかし、この方法は非常に効率が悪く、多数のfutureを作成するプログラムでは実用的ではありません。この問題を解決する最も一般的な方法は、システム内のすべてのfutureが終了するまでポーリングする責任を負う、グローバルな **executor** を定義することです。 + +#### Executor + +executorの目的は、 `spawn` のようなメソッドを使って、独立したタスクとしてfutureを生成 (spawn) できるようにすることです。そして、executor はすべてのfutureが完了するまでポーリングする責任を担うのです。すべてのfutureを中央集権的に管理することの大きな利点は、あるfutureが `Poll::Pending` を返すたびに、executorが別のfutureに切り替えることができることです。このようにして、非同期の処理が並行して実行され、CPUをずっと忙しくしておけます。 + +多くのexecutorの実装では、システムが複数のCPUコアを持っている場合にそれを生かすことができるようになっています。これらの実装では、十分な作業量があればすべてのコアを利用できる[スレッドプール][thread pool]を作成したり、[work stealing]などの手法を用いてコア間の負荷を分散させたりします。また、低レイテンシーとメモリオーバーヘッドに最適化した、組み込みシステム用の特別なexecutorの実装もあります。 + +[thread pool]: https://en.wikipedia.org/wiki/Thread_pool +[work stealing]: https://en.wikipedia.org/wiki/Work_stealing + +futureを何度もポーリングすることによるオーバーヘッドを避けるために、executorsは通常、Rustのfutureがサポートする **waker** APIを利用します。 + +#### Waker + +waker APIの背景にある考え方は、特別な[`Waker`]型が[`Context`]型にラップされて、`poll`の各呼び出しに渡されるというものです。この `Waker` 型はexecutorによって作成され、非同期タスクがその(部分的な)完了を知らせるために使用することができます。その結果、executorは以前に `Poll::Pending` を返したfutureに対して、対応するwakerから通知されるまでの間 `poll` を呼び出す必要がありません。 + +[`Context`]: https://doc.rust-lang.org/nightly/core/task/struct.Context.html + +これは、小さな例で説明するのが一番です: + +```rust +async fn write_file() { + async_write_file("foo.txt", "Hello").await; +} +``` + +この関数は文字列 "Hello" を `foo.txt` ファイルに非同期的に書き込みます。ハードディスクへの書き込みには時間がかかるので、このfutureの最初の `poll` 呼び出しはおそらく `Poll::Pending` を返すでしょう。しかし、ハードディスクドライバは `poll` 呼び出しに渡された `Waker` を内部に保存し、ファイルがディスクに書き込まれたときにそれを使ってexecutorに通知します。これにより、executorはwakerの通知を受け取るまでの間、再びfutureを `poll` して時間を無駄にせずにすみます。 + +この記事の実装セクションで、wakerをサポートした独自のexecutorを作成する際に、`Waker`型がどのように機能するかを詳しく見ていきます。 + +### 協調的マルチタスク? + +この記事の冒頭で、非協調的マルチタスクと協調的マルチタスクについて説明しました。非協調的マルチタスクは、OSが実行中のタスクを強制的に切り替えることに依存していますが、協調的マルチタスクでは、タスクが定期的に **yield** 操作によって自発的にCPUの制御を放棄する必要があります。協調的マルチタスクの大きな利点は、タスクが自分で状態を保存できることです。これにより、コンテキスト・スイッチの効率が向上し、タスク間で同じコールスタックを共有することが可能になります。 + +すぐにはわからないかもしれませんが、futureとasync/awaitは、協調的マルチタスクの実装になっています: + +- 簡単に言ってしまえば、executorに追加される各futureが1つの協調的タスクです。 +- future は、明示的なyield operationを使用する代わりに、`Poll::Pending`(もしくは終了時に`Poll::Ready`)を返すことで、CPU コアの制御を放棄します。 + - futureがCPUの制御を手放すことを強制するものは何もありません。やろうと思えば、例えばループを無限に回すなどして、`poll`から決してリターンしないようにすることができます。 + - それぞれのfutureは、executor内の他のfutureの実行をブロックできるため、悪意がないことを信用する必要があります。 +- futureは、次の `poll` 呼び出しで実行を継続するために必要なすべての状態を内部に保存します。async/awaitでは、コンパイラが必要なすべての変数を自動的に検出し、生成されたステートマシンの内部に格納します。 + - 継続に必要な最低限の状態のみが保存されます。 + - `poll`メソッドはreturn時にコールスタックを放棄するので、スタックの同じ場所を他のfutureのポーリングに使用することができます。 + +futureとasync/awaitは、協調的マルチタスクのパターンに完全に一致していることがわかります。単に使用している用語が異なるだけです。以下では、"task "と "future"という用語を同じものとして扱います。 + +## 実装 + +future と async/await に基づいた協調的マルチタスクが Rust でどのように動作するかを理解したので、私達のカーネルにそのサポートを追加しましょう。[`Future`] trait は `core` ライブラリの一部であり、async/await は言語自体の機能なので、これらを`#![no_std]` カーネルで使用するために特別なことをする必要はありません。唯一の要件は、Rust の nightly `2020-03-25` 以降を使用することです。なぜなら、async/await はこれ以前は `no_std` に対応していなかったからです。 + +それ以降のnightlyでは、`main.rs` で async/await を使うことができます: + +```rust +// in src/main.rs + +async fn async_number() -> u32 { + 42 +} + +async fn example_task() { + let number = async_number().await; + println!("async number: {}", number); +} +``` + +関数 `async_number` は `async fn` なので、コンパイラはこれを `Future` を実装したステートマシンに変換します。この関数は `42` しか返さないので、できあがったfutureは最初の `poll` 呼び出しですぐに `Poll::Ready(42)` を返します。`async_number`と同様に、`example_task`関数も`async fn`です。この関数は `async_number` が返す数値を待ち、`println` マクロを使ってその数値を表示します。 + +`example_task` が返す future を実行するには、それが`Poll::Ready` を返すことで完了を知らせてくれるまで、`poll` を呼び出し続ける必要があります。そのためには、シンプルなexecutorの型を作成する必要があります。 + +### タスク + +executorの実装を始める前に、新しい `task` モジュールを `Task` 型で作成します: + +```rust +// in src/lib.rs + +pub mod task; +``` + +```rust +// in src/task/mod.rs + +use core::{future::Future, pin::Pin}; +use alloc::boxed::Box; + +pub struct Task { + future: Pin>>, +} +``` + +`Task` 構造体は、ピン留めされ、ヒープに割り当てられ、 (から) の型 `()` を出力として持つ、動的にディスパッチされるfutureのnewtypeのラッパーです。詳細を見てみましょう: + +- 私たちは、タスクに関連するfutureが `()` を返すことを要求しています。これは、タスクが結果を一切返さず、副作用のためだけに実行されることを意味します。例えば、上で定義した`example_task`関数は、戻り値はありませんが、副作用として画面に何かを表示します。 +- `dyn`キーワードは、`Box`に[_trait object_]を格納することを示しています。これはfuture上のメソッドが[**動的にディスパッチされる**][_dynamically dispatched_]ことを意味しており、`Task` 型に異なる型のfutureを格納することが可能になります。各 `async fn` はそれぞれ異なる型を持っており,私達は複数の異なるタスクを作成できるようにしたいので、これは重要です。 +- [pinningについて]で学んだように、`Pin` 型は、値をheap上に配置し、その値への `&mut` 参照の作成を防ぐことで、メモリ内で値が移動できないようにします。これは、async/awaitによって生成されたfutureが自己参照構造体である可能性があるため、重要です。つまり、futureが移動されたときに無効になるような自分自身へのポインタを含む可能性があります。 + +[_trait object_]: https://doc.rust-lang.org/book/ch17-02-trait-objects.html +[_dynamically dispatched_]: https://doc.rust-lang.org/book/ch17-02-trait-objects.html#trait-objects-perform-dynamic-dispatch +[pinningについて]: #pinliu-me + +future から新しい `Task` 構造体を作成できるように、`new` 関数を作成します: + +```rust +// in src/task/mod.rs + +impl Task { + pub fn new(future: impl Future + 'static) -> Task { + Task { + future: Box::pin(future), + } + } +} +``` + +この関数は、出力型が `()` の任意のfutureを受け取り、[`Box::pin`] 関数を使ってそれをメモリにピン留めします。そして、Box化されたfutureを `Task` 構造体でラップして返します。ここで `'static` ライフタイムが必要なのは、返された `Task` が任意の時間だけ生き続けることができるので、futureもその時間だけ有効である必要があるからです。 + +`poll`メソッドも追加して、executorが格納されたfutureをポーリングできるようにしましょう: + +```rust +// in src/task/mod.rs + +use core::task::{Context, Poll}; + +impl Task { + fn poll(&mut self, context: &mut Context) -> Poll<()> { + self.future.as_mut().poll(context) + } +} +``` + +`Future` trait の [`poll`] メソッドは `Pin>` 型で呼び出されることを期待しているので、[`Pin::as_mut`] メソッドを使って `self.future` フィールドをまず `Pin<&mut T>` 型に変換します。そして、変換された `self.future` フィールドに対して `poll` を呼び出し、その結果を返します。`Task::poll`メソッドは、これから作成するexecutorからのみ呼び出されるべきものなので、この関数は`task`モジュールのプライベートなものにしています。 + +### 単純なExecutor + +executorは非常に複雑なものになる可能性があるので、より機能的なexecutorを実装していく前に、あえて非常に基本的なexecutorを作ることから始めます。そのために、まず新しい `task::simple_executor` サブモジュールを作成します: + +```rust +// in src/task/mod.rs + +pub mod simple_executor; +``` + +```rust +// in src/task/simple_executor.rs + +use super::Task; +use alloc::collections::VecDeque; + +pub struct SimpleExecutor { + task_queue: VecDeque, +} + +impl SimpleExecutor { + pub fn new() -> SimpleExecutor { + SimpleExecutor { + task_queue: VecDeque::new(), + } + } + + pub fn spawn(&mut self, task: Task) { + self.task_queue.push_back(task) + } +} +``` + +この構造体には、[`VecDeque`]型の`task_queue`フィールドが1つ含まれています。これは要するに、両端でpushとpopの操作ができるvectorです。この型を使うのは、`spawn` メソッドによって新しいタスクを末尾に挿入し、次のタスクを実行する際先頭からpopしたいからです。これにより、単純な[FIFO queue](_"first in, first out"_)が得られます。 + +[`VecDeque`]: https://doc.rust-lang.org/stable/alloc/collections/vec_deque/struct.VecDeque.html +[FIFO queue]: https://en.wikipedia.org/wiki/FIFO_(computing_and_electronics) + +#### ダミーのWaker + +`poll`メソッドを呼び出すためには、[`Context`]型を作成して、[`Waker`]型をラップする必要があります。簡単に始めるために、まず何もしないダミーのwakerを作ります。このために、さまざまな `Waker` のメソッドの実装を定義した [`RawWaker`] インスタンスを作成し、 [`Waker::from_raw`] 関数を使用して `Waker` に変換します: + +[`RawWaker`]: https://doc.rust-lang.org/stable/core/task/struct.RawWaker.html +[`Waker::from_raw`]: https://doc.rust-lang.org/stable/core/task/struct.Waker.html#method.from_raw + +```rust +// in src/task/simple_executor.rs + +use core::task::{Waker, RawWaker}; + +fn dummy_raw_waker() -> RawWaker { + todo!(); +} + +fn dummy_waker() -> Waker { + unsafe { Waker::from_raw(dummy_raw_waker()) } +} +``` + +`from_raw` 関数はunsafeです。なぜならば、プログラマがドキュメントに書かれた `RawWaker` の要件を守らないと、未定義の動作が発生する可能性があるからです。`dummy_raw_waker` 関数の実装を見る前に、まず `RawWaker` 型がどのように動作するかを理解しましょう。 + +##### `RawWaker` + +[`RawWaker`] 型では、プログラマが [_virtual method table_] (_vtable_) を明示的に定義する必要があります。このテーブルは、`RawWaker` がクローンされたり、起こされたり、ドロップされたりしたときに呼び出されるべき関数を指定します。このvtableのレイアウトは[`RawWakerVTable`]という型で定義されています。各関数は、基本的には(例えばヒープ上に確保された)構造体への**型消去された** `&self` ポインタである `*const ()` 引数を受け取ります。通常の参照ではなく `*const ()` ポインタを使う理由は、`RawWaker` の型は非ジェネリックであるべきで、かつ任意の型をサポートする必要があるからです。関数に渡されるポインタの値は [`RawWaker::new`] に渡される `data` ポインタです。 + +[_virtual method table_]: https://en.wikipedia.org/wiki/Virtual_method_table +[`RawWakerVTable`]: https://doc.rust-lang.org/stable/core/task/struct.RawWakerVTable.html +[`RawWaker::new`]: https://doc.rust-lang.org/stable/core/task/struct.RawWaker.html#method.new + +通常、`RawWaker` は [`Box`] や [`Arc`] 型にラップされた、ヒープに割り当てられた構造体に対して作成されます。このような型では、 [`Box::into_raw`] のようなメソッドを使用して、 `Box` を `*const T` ポインタに変換することができます。更にこのポインタを `*const ()` 無名(関数)ポインタにキャストして、 `RawWaker::new` に渡すことができます。各vtable関数はどれも`*const ()`を引数として受け取るので、各関数は安全にポインタを`Box`や`&T`にキャストし直して操作することができます。想像できると思いますが、この処理は非常に危険で、ミスにより未定義動作を引き起こすことが多いです。このような理由から、`RawWaker` を自分の手で作成することは、必要な場合を除いてお勧めできません。 + +[`Box`]: https://doc.rust-lang.org/stable/alloc/boxed/struct.Box.html +[`Arc`]: https://doc.rust-lang.org/stable/alloc/sync/struct.Arc.html +[`Box::into_raw`]: https://doc.rust-lang.org/stable/alloc/boxed/struct.Box.html#method.into_raw + +##### ダミーの`RawWaker` + +自分の手で `RawWaker` を作成することはお勧めできませんが、何もしないダミーの `Waker` を作成する方法は今のところ他にありません。幸いなことに、何かをさせたいわけではないので、`dummy_raw_waker`関数の実装は比較的安全です: + +```rust +// in src/task/simple_executor.rs + +use core::task::RawWakerVTable; + +fn dummy_raw_waker() -> RawWaker { + fn no_op(_: *const ()) {} + fn clone(_: *const ()) -> RawWaker { + dummy_raw_waker() + } + + let vtable = &RawWakerVTable::new(clone, no_op, no_op, no_op); + RawWaker::new(0 as *const (), vtable) +} +``` + +まず、`no_op`と`clone`という2つの内部関数を定義します。`no_op`関数は`*const ()`のポインタを受け取り、何もしません。また、`clone`関数は`*const ()`のポインタを受け取り、`dummy_raw_waker`を再度呼び出して新しい`RawWaker`を返します。これらの2つの関数を使って最小限の `RawWakerVTable` を作成します。`clone`関数はクローン作成のために用いられ、それ以外の操作には`no_op`関数が用いられます。`RawWaker`は何もしないので、クローンを作る代わりに`clone`から新しい`RawWaker`を返しても問題はありません。 + +`vtable`を作成した後、[`RawWaker::new`]関数を使って`RawWaker`を作成します。どのvtable関数も渡された `*const ()` を使用しないので、 これが何であっても構いません。そのため、単にnullポインタを渡します。 + +#### `run`メソッド + +これで `Waker` インスタンスを作成する方法ができたので、これを使ってexecutorに `run` メソッドを実装することができます。最もシンプルな `run` メソッドは、キューに入っているすべてのタスクを、すべて完了するまでループで繰り返しポーリングするものです。これは `Waker` 型からの通知を利用していないのであまり効率的ではありませんが、とりあえず実行させるための簡易な方法です: + +```rust +// in src/task/simple_executor.rs + +use core::task::{Context, Poll}; + +impl SimpleExecutor { + pub fn run(&mut self) { + while let Some(mut task) = self.task_queue.pop_front() { + let waker = dummy_waker(); + let mut context = Context::from_waker(&waker); + match task.poll(&mut context) { + Poll::Ready(()) => {} // taskの完了 + Poll::Pending => self.task_queue.push_back(task), + } + } + } +} +``` + +この関数は `while let` ループを使用して、`task_queue` 内のすべてのタスクを処理します。各タスクでは、まず `dummy_waker` 関数が返す `Waker` インスタンスをラップして `Context` 型を作成します。そして、この `context` を引数にして `Task::poll` メソッドを呼び出します。もし `poll` メソッドが `Poll::Ready` を返せば、タスクは終了し、次のタスクに進むことができます。タスクがまだ `Poll::Pending` であれば、そのタスクを再びキューの後ろに追加して、次のループの繰り返しで再びポーリングされるようにします。 + +#### 試してみよう + +`SimpleExecutor` 型ができたので、`example_task` 関数で返されたタスクを `main.rs`で実行してみましょう: + +```rust +// in src/main.rs + +use blog_os::task::{Task, simple_executor::SimpleExecutor}; + +fn kernel_main(boot_info: &'static BootInfo) -> ! { + // […] `init_heap`を含む初期化ルーチン + + let mut executor = SimpleExecutor::new(); + executor.spawn(Task::new(example_task())); + executor.run(); + + // […] test_main, "it did not crash" のメッセージ, hlt_loop +} + +// 以下は、上の方で既に定義されているexample_task関数です. +// 上にスクロールして探さなくても良いようにするために、ここにも書いておきます. + +async fn async_number() -> u32 { + 42 +} + +async fn example_task() { + let number = async_number().await; + println!("async number: {}", number); +} +``` + +実行してみると、期待通り **"async number: 42"** のメッセージがスクリーンに表示されています: + +![QEMU printing "Hello World", "async number: 42", and "It did not crash!"](qemu-simple-executor.png) + +この例で起こる様々なステップをまとめてみましょう: + +- まず、`SimpleExecutor`型の新しいインスタンスが、`task_queue`が (から) の状態で作成されます。 +- 次に、非同期の `example_task` 関数が呼び出され、futureを返します。このfutureを `Task` 型でラップすることで、ヒープに移動させてピン留めし、`spawn` メソッドでタスクをexecutorの `task_queue` に追加します。 +- そして、`run`メソッドを呼び出して、キューの中の一つのタスクの実行を開始します。これは、以下のような作業からなります: + - `task_queue` の先頭からタスクをpopします。 + - タスク用の `RawWaker` を作成し、それを [`Waker`] インスタンスに変換し、そこから [`Context`] インスタンスを作成します。 + - 先ほど作成した `Context` を使って、タスクのfutureに [`poll`] メソッドを呼び出します。 + - この `example_task` は何かを待つわけではないので、最初の `poll` 呼び出し一回で関数の最後まで実行することができます。ここで **"async number: 42"** の行が表示されます。 + - この `example_task` は直接 `Poll::Ready` を返すので、タスクキューには戻されません。 +- `run`メソッドは、`task_queue`が空になったらリターンします。`kernel_main`関数の実行は継続され、 **"It did not crash!"** というメッセージが表示されます。 + +### 非同期キーボード入力 + +私たちのシンプルなexecutorは、`Waker`通知を利用せず、単純にすべてのタスクを完了するまでループさせます。今回の例では、最初の `poll` 呼び出しで `example_task` が最後まで実行されて終了するので、これは問題になりませんでした。適切な `Waker` の実装によるパフォーマンス上の利点を見るためには、まず真の意味で非同期なタスクを作成する必要があります。つまり、最初の `poll` 呼び出しでは `Poll::Pending` を返す可能性の高いタスクです。 + +すでに**ハードウェア割り込み**という非同期なタスクが私達のシステムにはあるので、それをこのために使うことができます。[_Interrupts_]の項でご紹介したように、ハードウェアによる割り込みは、外部からの任意のタイミングで発生させることができます。例えば、ハードウェア・タイマーは、あらかじめ定義された時間が経過すると、CPUに割り込みを送ります。CPUは割り込みを受信すると、即座に割り込み記述子表 (interrupt descriptor table, IDT) で定義された対応するハンドラ関数に制御を移します。 + +[_Interrupts_]: @/edition-2/posts/07-hardware-interrupts/index.md + +以下では、キーボード割り込みを利用した非同期タスクを作成します。キーボード割り込みは、非決定論的であり、かつlatency-criticalであるため、これに適した候補となります。非決定論的とは、次にいつキーが押されるかはユーザーに完全に依存しているため、これを予測する方法がないということです。latency-criticalとは、キーボード入力を即座に処理したいということで、そうしないとユーザーはラグを感じることになります。このようなタスクを効率的にサポートするためには、executorが `Waker` 通知を適切にサポートすることが不可欠となります。 + +#### スキャンコードキュー + +現在、キーボードの入力は割り込みハンドラで直接処理しています。割り込みハンドラは重要な作業を中断する可能性がある以上できるだけ短くする必要があるため、これは長期的に考えると良いアイデアではありません。このようにするのではなく、割り込みハンドラは必要最小限の作業(例: キーボードのスキャンコードの読み取りなど)のみを行い、残りの作業(例: スキャンコードの解釈など)はバックグラウンドタスクに任せるべきです。 + +バックグラウンドタスクに作業を委ねるための一般的な方式は、ある種のキューを作成することです。割り込みハンドラは仕事の一単位をキューにpushし、バックグラウンドタスクはキュー内の仕事を処理します。この考え方を今回のキーボード割込みに適用すると、割込みハンドラはキーボードからスキャンコードを読み取って、それをキューにpushし終わり次第、returnするということになります。キーボードタスクは、キューの反対側に位置し、pushされた各スキャンコードを解釈して処理します: + +![Scancode queue with 8 slots on the top. Keyboard interrupt handler on the bottom left with a "push scancode" arrow to the left of the queue. Keyboard task on the bottom right with a "pop scancode" queue coming from the right side of the queue.](scancode-queue.svg) + +そのキューを簡単に実装したものとしてmutexでラップした [`VecDeque`]が使えるかもしれません。しかし、割り込みハンドラにmutexを使用することは、デッドロックにつながりやすいため、あまり良いアイデアではありません。例えば、キーボードタスクがキューをロックしているときにユーザがキーを押すと、割込みハンドラは再度ロックを取得しようとして、無期限にハングアップしてしまいます。この方法のもう一つの問題点は、`VecDeque`が満杯になったときに新しいヒープの割り当てを行うことで、自動的に容量を増やしてしまうことです。これは、私達のアロケータが内部でmutexを使用しているため、これまたデッドロックを引き起こす可能性があります。さらに、ヒープが断片化されていると、ヒープの割り当てに失敗したり、かなりの時間がかかったりするという問題もあります。 + +これらの問題を防ぐためには、`push`操作にmutexやアロケートを必要としないキューの実装が必要です。このようなキューは、要素のpushとpopにロックを使用しない[atomic operations]を用いることで実装できます。この方法では、`&self`の参照しか必要としない`push`と`pop`の操作を作成することができ、したがって、mutexなしで使用することができます。`push`の際のアロケートを避けるために、あらかじめ割り当てられた固定サイズのバッファ上にキューを作ります。これにより、キューは **有界** (最大の長さを持つという意味)になりますが、実際には、キューの長さに妥当な上限を定義することが可能な場合が多いので、これは大きな問題ではありません。 + +[atomic operations]: https://doc.rust-lang.org/core/sync/atomic/index.html + +##### `crossbeam`クレート + +このようなキューを正しく効率的に実装するのは非常に難しいので、既存の、よくテストされた実装を使うことをお勧めします。並行プログラミングのために様々なmutexを使用しない型を実装している人気のあるRustプロジェクトの1つに[`crossbeam`]があります。このプロジェクトでは、[`ArrayQueue`]という名前の型が提供されており、これは今回のケースでまさに必要なものです。そして幸運なことに、この型はアロケーションをサポートしている `no_std` のクレートと完全に互換性があります。 + +[`crossbeam`]: https://github.com/crossbeam-rs/crossbeam +[`ArrayQueue`]: https://docs.rs/crossbeam/0.7.3/crossbeam/queue/struct.ArrayQueue.html + +この型を使用するには、`crossbeam-queue` クレートへの依存関係を追加する必要があります: + +```toml +# in Cargo.toml + +[dependencies.crossbeam-queue] +version = "0.2.1" +default-features = false +features = ["alloc"] +``` + +デフォルトでは、クレートは標準ライブラリに依存しています。`no_std`互換にするためには、そのデフォルト機能を無効にして、代わりに`alloc`機能を有効にする必要があります(メインの `crossbeam` クレートに依存しても、ここでは動作しないことに注意してください。なぜなら、`no_std` に対する `queue` モジュールのエクスポートがないからです。これを修正するために [pull request](https://github.com/crossbeam-rs/crossbeam/pull/480) を提出しましたが、まだ crates.io でリリースされていませんでした)。 + +##### キューの実装 + +`ArrayQueue`型を使って、新しい`task::keyboard`モジュールの中に、グローバルなスキャンコードキューを作ることができます: + +```rust +// in src/task/mod.rs + +pub mod keyboard; +``` + +```rust +// in src/task/keyboard.rs + +use conquer_once::spin::OnceCell; +use crossbeam_queue::ArrayQueue; + +static SCANCODE_QUEUE: OnceCell> = OnceCell::uninit(); +``` + +[`ArrayQueue::new`]はヒープの割り当てを行いますが、これはコンパイル時には([まだ][const-heap-alloc])できないので、静的変数を直接初期化することはできません。代わりに、[`conquer_once`]クレートの[`OnceCell`]型を使用して、静的な値の安全な1回限りの初期化を行うことができます。このクレートをインクルードするには、`Cargo.toml`に依存関係として追加する必要があります: + +[`ArrayQueue::new`]: https://docs.rs/crossbeam/0.7.3/crossbeam/queue/struct.ArrayQueue.html#method.new +[const-heap-alloc]: https://github.com/rust-lang/const-eval/issues/20 +[`OnceCell`]: https://docs.rs/conquer-once/0.2.0/conquer_once/raw/struct.OnceCell.html +[`conquer_once`]: https://docs.rs/conquer-once/0.2.0/conquer_once/index.html + +```toml +# in Cargo.toml + +[dependencies.conquer-once] +version = "0.2.0" +default-features = false +``` + +ここで、[`OnceCell`]プリミティブの代わりに、[`lazy_static`]マクロを使うこともできます。しかし、`OnceCell`型には、初期化が割込みハンドラ内で行われないようにすることで、割込みハンドラがヒープの割り当てを行うことを防ぐことができるという利点があります。 + +[`lazy_static`]: https://docs.rs/lazy_static/1.4.0/lazy_static/index.html + +#### キューを埋める + +スキャンコードキューを埋めるために、新しい `add_scancode` 関数を作成し、割り込みハンドラから呼び出すことにします: + +```rust +// in src/task/keyboard.rs + +use crate::println; + +/// キーボード割り込みハンドラから呼び出される +/// +/// 処理をブロックしたり、アロケートをしてはいけない +pub(crate) fn add_scancode(scancode: u8) { + if let Ok(queue) = SCANCODE_QUEUE.try_get() { + if let Err(_) = queue.push(scancode) { + println!("WARNING: scancode queue full; dropping keyboard input"); + // "警告:スキャンコードキューがいっぱいです。キーボード入力を取り零しています" + } + } else { + println!("WARNING: scancode queue uninitialized"); + // "警告:スキャンコードキューが初期化されていません" + } +} +``` + +初期化されたキューへの参照を得るために、[`OnceCell::try_get`]を使用します。キューがまだ初期化されていない場合は、キーボードのスキャンコードを無視して、警告を表示します。この関数でキューの初期化を試みないことは重要です。なぜなら、この関数は割り込みハンドラから呼び出されますが、この割り込みハンドラはヒープの割り当てを行うべきではないためです。この関数は、`main.rs`から呼び出し可能であってはならないので、`pub(crate)`を使用して、`lib.rs`からのみ利用できるようにしています。 + +[`OnceCell::try_get`]: https://docs.rs/conquer-once/0.2.0/conquer_once/raw/struct.OnceCell.html#method.try_get + +[`ArrayQueue::push`]メソッドは`&self`の参照のみを必要とするため、この静的なキューのpushメソッドを呼び出すのは非常に簡単です。`ArrayQueue`型は必要な同期をすべて自分で行うので、ここではmutexのラッパーは必要ありません。キューがいっぱいになった場合には、警告を表示します。 + +[`ArrayQueue::push`]: https://docs.rs/crossbeam/0.7.3/crossbeam/queue/struct.ArrayQueue.html#method.push + +キーボード割り込みで`add_scancode`関数を呼び出すために、`interrupts`モジュール内の`keyboard_interrupt_handler`関数を更新します: + +```rust +// in src/interrupts.rs + +extern "x86-interrupt" fn keyboard_interrupt_handler( + _stack_frame: InterruptStackFrame +) { + use x86_64::instructions::port::Port; + + let mut port = Port::new(0x60); + let scancode: u8 = unsafe { port.read() }; + crate::task::keyboard::add_scancode(scancode); // new + + unsafe { + PICS.lock() + .notify_end_of_interrupt(InterruptIndex::Keyboard.as_u8()); + } +} +``` + +この関数からキーボードを扱うコードをすべて削除し、代わりに `add_scancode` 関数の呼び出しを追加しました。この関数の残りの部分は以前と同じです。 + +予想通り、`cargo run` を使ってプロジェクトを実行しても、キーの入力が画面に表示されなくなりました。代わりに、キーを押すたびにスキャンコードキューが初期化されていないという警告が表示されます。 + +#### スキャンコードストリーム + +`SCANCODE_QUEUE`を初期化し、キューから非同期的にスキャンコードを読み取るために、新しい`ScancodeStream`型を作成します: + +```rust +// in src/task/keyboard.rs + +pub struct ScancodeStream { + _private: (), +} + +impl ScancodeStream { + pub fn new() -> Self { + SCANCODE_QUEUE.try_init_once(|| ArrayQueue::new(100)) + .expect("ScancodeStream::new should only be called once"); + // "ScancodeStream::new は一度しか呼び出されてはなりません" + ScancodeStream { _private: () } + } +} +``` + +`_private`フィールドの目的は、モジュールの外部から構造体を構築できないようにすることです。これにより、この型を構築するには、`new`関数が唯一の方法となります。この関数では、まず、`SCANCODE_QUEUE`という静的変数を初期化しようとします。既に初期化されている場合にはパニックを起こすようにすることによって、1つの`ScancodeStream`インスタンスしか作成できないようにします。 + +非同期タスクがスキャンコードを利用できるようにするためには、次のステップとしてキューから次のスキャンコードを取り出そうとする `poll` のようなメソッドを実装します。これは、私たちの型に[`Future`]特性を実装するべきであるように聞こえますが、これはここではうまくいきません。問題は、`Future` trait は単一の非同期値を抽象化するだけであり、`Poll` メソッドが `Poll::Ready` を返した後は二度と呼び出されないことを期待しているということです。しかし、私たちのスキャンコードキューは複数の非同期値を含んでいるので、さらにポーリングを行っても問題ありません。 + +##### `Stream` trait + +複数の非同期値を生じる型はよく使われるので、[`futures`]クレートはそのような型のための便利な抽象化である[`Stream`] traitを提供しています。この trait は次のように定義されています: + +[`Stream`]: https://rust-lang.github.io/async-book/05_streams/01_chapter.html + +```rust +pub trait Stream { + type Item; + + fn poll_next(self: Pin<&mut Self>, cx: &mut Context) + -> Poll>; +} +``` + +この定義は、[`Future`] traitとよく似ていますが、以下のような違いがあります: + +- 関連型の名前は、`Output`ではなく`Item`です。 +- `Stream` trait では、`Poll` を返す `poll` メソッドの代わりに、`Poll>` を返す `poll_next` メソッドが定義されています(`Option` が追加されていることに注意)。 + +また、意味の上でも違いはあります。`poll_next` は、ストリームが終了したことを知らせる `Poll::Ready(None)` が返されるまで繰り返し呼び出すことができるのです。この点で、このメソッドは [`Iterator::next`] メソッドに似ています(このメソッドも最後の値の後に `None` を返す)。 + +[`Iterator::next`]: https://doc.rust-lang.org/stable/core/iter/trait.Iterator.html#tymethod.next + +##### `Stream`を実装する + +では、`SCANCODE_QUEUE`の値を非同期に提供するために、`ScancodeStream`に`Stream`型を実装してみましょう。そのためにはまず、`Stream`型がある`futures-util`クレートへの依存関係を追加する必要があります: + +```toml +# in Cargo.toml + +[dependencies.futures-util] +version = "0.3.4" +default-features = false +features = ["alloc"] +``` + +このクレートが`no_std` と互換性を持つようにするためデフォルトの機能を無効にし、アロケーションベースの型を利用できるように `alloc` 機能を有効にしています(これは後で必要になります)。(なお、`futures-util` クレートを再エクスポートしているメインの `futures` クレートの方に依存関係を追加することもできますが、この場合は依存関係の数が増え、コンパイル時間が長くなります) + +これで、`Stream`というtraitをインポートして実装できるようになりました: + +```rust +// in src/task/keyboard.rs + +use core::{pin::Pin, task::{Poll, Context}}; +use futures_util::stream::Stream; + +impl Stream for ScancodeStream { + type Item = u8; + + fn poll_next(self: Pin<&mut Self>, cx: &mut Context) -> Poll> { + let queue = SCANCODE_QUEUE.try_get().expect("not initialized"); + match queue.pop() { + Ok(scancode) => Poll::Ready(Some(scancode)), + Err(crossbeam_queue::PopError) => Poll::Pending, + } + } +} +``` + +まず、[`OnceCell::try_get`]メソッドを使って、初期化されたスキャンコードキューへの参照を取得します。`new`関数でキューを初期化しているので、これが失敗することはないはずです。したがって、初期化されていない場合には`expect`メソッドを使ってパニックを起こすようにしても大丈夫です。次に、[`ArrayQueue::pop`]メソッドを使って、キューから次の要素を取得しようとします。もし成功すれば、`Poll::Ready(Some(...))`でラップされたスキャンコードを返します。失敗した場合は、キューが空であることを意味します。その場合は、`Poll::Pending`を返します。 + +[`ArrayQueue::pop`]: https://docs.rs/crossbeam/0.7.3/crossbeam/queue/struct.ArrayQueue.html#method.pop + +#### Wakerをサポートする + +`Futures::poll`メソッドと同様に、`Stream::poll_next`メソッドは、`Poll::Pending`が返された後、非同期タスクが準備ができたらexecutorに通知することを要求します。こうすることで、executorは通知されるまで同じタスクを再度ポーリングする必要がなくなり、待機中のタスクのパフォーマンスオーバーヘッドを大幅に削減することができます。 + +この通知を送るために、タスクは渡された[`Context`]参照から[`Waker`]を取り出してどこかに保存しなければなりません。タスクの準備ができたら、保存されている `Waker` に対して [`wake`] メソッドを呼び出して、タスクが再びポーリングされるべきであることをexecutorに通知しなければなりません。 + +##### AtomicWaker + +`Waker`通知を`ScancodeStream`に実装するためには、ポーリング呼び出しが終わってから次のポーリング呼び出しまでの間`Waker`を保存できる場所が必要です。これは `add_scancode` 関数からアクセスできる必要があるため、`ScancodeStream` 自身のフィールドとして保存することはできません。これを解決するには、`futures-util` クレートが提供する [`AtomicWaker`] 型の静的変数を使用します。`ArrayQueue`型と同様に、この型はアトミックな命令に基づいており、静的変数に安全に保存でき、並行的に安全に変更することもできます。 + +[`AtomicWaker`]: https://docs.rs/futures-util/0.3.4/futures_util/task/struct.AtomicWaker.html + +[`AtomicWaker`]型を使って、静的な`WAKER`を定義してみましょう: + +```rust +// in src/task/keyboard.rs + +use futures_util::task::AtomicWaker; + +static WAKER: AtomicWaker = AtomicWaker::new(); +``` + +アイデアとしては、`poll_next`では、現在のwakerをこの静的変数に格納し、`add_scancode`関数では、新しいスキャンコードがキューに追加されたときに、`wake`関数を呼び出すというものです。 + +##### Wakerを保存する + +`poll`/`poll_next` が要求する前提条件として、タスクが `Poll::Pending` を返したときに、渡された `Waker` のwakeup (目覚まし) が起こるように登録することというのがあります。この要求を満たすために、`poll_next` の実装を変更してみましょう: + +```rust +// in src/task/keyboard.rs + +impl Stream for ScancodeStream { + type Item = u8; + + fn poll_next(self: Pin<&mut Self>, cx: &mut Context) -> Poll> { + let queue = SCANCODE_QUEUE + .try_get() + .expect("scancode queue not initialized"); + // "スキャンコードキューが初期化されていない" + + // 近道 + if let Ok(scancode) = queue.pop() { + return Poll::Ready(Some(scancode)); + } + + WAKER.register(&cx.waker()); + match queue.pop() { + Ok(scancode) => { + WAKER.take(); + Poll::Ready(Some(scancode)) + } + Err(crossbeam_queue::PopError) => Poll::Pending, + } + } +} +``` + +前回と同様に、まず [`OnceCell::try_get`] 関数を使用して、初期化されたスキャンコードキューへの参照を取得します。そして、キューからの `pop` を試みてみて、成功したら `Poll::Ready` を返します。このようにすれば、キューが空でないときにwakerを登録することによるパフォーマンスのオーバーヘッドを回避することができます。 + +最初の `queue.pop()` の呼び出しが成功しなかった場合、キューは空であるかもしれません。かもしれないというのは、割り込みハンドラがチェックの直後に非同期的にキューを満たした可能性があるからです。この競合状態は次のチェックでも発生する可能性があるので、2回目のチェックの前に `WAKER` 静的変数に `Waker` を登録する必要があります。こうすることで、`Poll::Pending`を返す前にwakeupが起こるかもしれませんが、チェックの後にpushされた全てのスキャンコードに対してwakeupが得られることは保証されます。 + +渡された [`Context`] に含まれる `Waker` を [`AtomicWaker::register`] 関数で登録した後、2回目のキューからのpopを試みます。成功すると `Poll::Ready` を返します。また、wakerの通知が不要になったので、[`AtomicWaker::take`]を使って先ほど登録したwakerを削除します。もし `queue.pop()` が再び失敗した場合は、先ほどと同様に `Poll::Pending` を返しますが、今回のプログラムではwakerが登録されたうえでリターンするようになっています。 + +[`AtomicWaker::register`]: https://docs.rs/futures-util/0.3.4/futures_util/task/struct.AtomicWaker.html#method.register +[`AtomicWaker::take`]: https://docs.rs/futures/0.3.4/futures/task/struct.AtomicWaker.html#method.take + +(まだ)`Poll::Pending`を返さなかったタスクに対してwakeupが発生する方法は2つあることに注意してください。1つは、`Poll::Pending`を返す直前にwakeupが発生する、前述の競合状態です。もうひとつの方法は、wakeupを登録した後にキューが空でなくなり、`Poll::Ready`が返される場合です。これらの偽のwakeupは防ぐことができないので、executorはこれらを正しく処理する必要があります。 + + +##### 保存されているWakerを起こす + +保存されている`Waker`を起こすために、`add_scancode`関数の中に`WAKER.wake()`の呼び出しを追加します: + +```rust +// in src/task/keyboard.rs + +pub(crate) fn add_scancode(scancode: u8) { + if let Ok(queue) = SCANCODE_QUEUE.try_get() { + if let Err(_) = queue.push(scancode) { + println!("WARNING: scancode queue full; dropping keyboard input"); + } else { + WAKER.wake(); // new + } + } else { + println!("WARNING: scancode queue uninitialized"); + } +} +``` + +今回行った唯一の変更点は、スキャンコードキューへのpushが成功した場合の`WAKER.wake()`への呼び出しを追加したことです。このメソッドは、`WAKER` staticにwakerが登録されていれば、同じ名前の[`wake`]メソッドをそのwakerに対して呼び出すことにより、executorに通知します。そうでなければ、この操作はno-op、つまり何も起こりません。 + +[`wake`]: https://doc.rust-lang.org/stable/core/task/struct.Waker.html#method.wake + +キューにpushした後で`wake`を呼び出すというのが重要で、そうしないと、キューがまだ空なのにタスクが尚早にwakeされてしまう可能性があります。これは例えば、起こされたタスクを別のCPUコアで同時に開始するようなマルチスレッドのexecutorを使用している場合などに起こります。まだ私達はスレッドをサポートしてはいませんが、近日中にサポートを追加する予定であり、その際に問題が発生しないようにしたいと考えています。 + +#### キーボードタスク + +さて、`ScancodeStream`に`Stream` traitを実装したので、これを使って非同期のキーボードタスクを作ることができます: + +```rust +// in src/task/keyboard.rs + +use futures_util::stream::StreamExt; +use pc_keyboard::{layouts, DecodedKey, HandleControl, Keyboard, ScancodeSet1}; +use crate::print; + +pub async fn print_keypresses() { + let mut scancodes = ScancodeStream::new(); + let mut keyboard = Keyboard::new(layouts::Us104Key, ScancodeSet1, + HandleControl::Ignore); + + while let Some(scancode) = scancodes.next().await { + 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), + } + } + } + } +} +``` + +このコードは、この記事で修正する前の[keyboard interrupt handler]にあったコードと非常によく似ています。唯一の違いは、I/O portからスキャンコードを読み込むのではなく、`ScancodeStream`からスキャンコードを取得することです。このために、まず新しい `Scancode` ストリームを作成し、次に [`StreamExt`] traitが提供する [`next`] メソッドを繰り返し使用して、ストリーム内の次の要素を返す `Future` を取得します。これに `await` 演算子を用いることで、futureの結果を非同期的に待ちます。 + +[keyboard interrupt handler]: @/edition-2/posts/07-hardware-interrupts/index.md#interpreting-the-scancodes +[`next`]: https://docs.rs/futures-util/0.3.4/futures_util/stream/trait.StreamExt.html#method.next +[`StreamExt`]: https://docs.rs/futures-util/0.3.4/futures_util/stream/trait.StreamExt.html + +ストリームが終了の合図として `None` を返すまで、`while let` を使ってループします。`poll_next` メソッドが `None` を返すことはないので、これは事実上の無限ループとなり、`print_keypresses` タスクは決して終了しません。 + +`main.rs`の中で、`print_keypresses`タスクをexecutorに追加して、キーボード入力を復活させましょう: + +```rust +// in src/main.rs + +use blog_os::task::keyboard; // new + +fn kernel_main(boot_info: &'static BootInfo) -> ! { + + // […] init_heap、test_mainを含む初期化ルーチン。 + + let mut executor = SimpleExecutor::new(); + executor.spawn(Task::new(example_task())); + executor.spawn(Task::new(keyboard::print_keypresses())); // new + executor.run(); + + // […] "it did not crash" message, hlt_loop +} +``` + +ここで`cargo run`を実行すると、キーボード入力が再び機能することがわかります: + +![QEMU printing ".....H...e...l...l..o..... ...W..o..r....l...d...!"](qemu-keyboard-output.gif) + +コンピュータのCPU使用率を監視してみると、`QEMU`プロセスがCPUをずっと忙しくしていることがわかります。これは、`SimpleExecutor` がループで何度も何度もタスクをポーリングするからです。つまり、キーボードのキーを何も押さなくても、executorは `print_keypresses` タスクの `poll` を繰り返し呼び出しています。 + +### WakerをサポートするExecutor + +このパフォーマンスの問題を解決するためには、`Waker`の通知を適切に利用するexecutorを作成する必要があります。この方法では、次のキーボード割り込みが発生したときにexecutorに通知されるので、`print_keypresses`タスクを何度もポーリングする必要はありません。 + +#### タスクID + +waker通知を適切にサポートするexecutorを作成するための最初のステップは、各タスクに一意のIDを与えることです。これは、どのタスクが起こされるべきかを指定する方法が必要だからです。まず、新しい `TaskId` ラッパー型を作成します: + +```rust +// in src/task/mod.rs + +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] +struct TaskId(u64); +``` + +`TaskId` 構造体は `u64` の単純なラッパー型です。`TaskId`構造体は、print可能、コピー可能、比較可能、ソート可能にするために、いくつかのtraitを継承します。最後の`Ord`が重要なのは、後ほど `TaskId`型を [`BTreeMap`] のキーとして使用したいからです。 + +[`BTreeMap`]: https://doc.rust-lang.org/alloc/collections/btree_map/struct.BTreeMap.html + +新しい一意なIDを作成する為に,`TaskId::new`関数を作ります: + +```rust +use core::sync::atomic::{AtomicU64, Ordering}; + +impl TaskId { + fn new() -> Self { + static NEXT_ID: AtomicU64 = AtomicU64::new(0); + TaskId(NEXT_ID.fetch_add(1, Ordering::Relaxed)) + } +} +``` + +この関数は、各IDが一度だけ割り当てられることを保証するために、[`AtomicU64`]型の静的な`NEXT_ID`変数を使用します。[`fetch_add`]メソッドは、1回のアトミックな操作で、値を増やし更に前の値を返します。つまり、`TaskId::new` メソッドが並列に呼ばれた場合でも、すべてのIDが一度だけ返されることになります。[`Ordering`]パラメータは、コンパイラが命令ストリームにおける`fetch_add`操作の順序を変更することを許可するかどうかを定義します。ここではIDが一意であることだけを要求しているので、最も弱い要求を持つ`Relaxed`という順序づけで十分です。 + +[`AtomicU64`]: https://doc.rust-lang.org/core/sync/atomic/struct.AtomicU64.html +[`fetch_add`]: https://doc.rust-lang.org/core/sync/atomic/struct.AtomicU64.html#method.fetch_add +[`Ordering`]: https://doc.rust-lang.org/core/sync/atomic/enum.Ordering.html + +これで、`Task` 型に `id` フィールドを追加して拡張することができます: + +```rust +// in src/task/mod.rs + +pub struct Task { + id: TaskId, // new + future: Pin>>, +} + +impl Task { + pub fn new(future: impl Future + 'static) -> Task { + Task { + id: TaskId::new(), // new + future: Box::pin(future), + } + } +} +``` + +新しい`id`フィールドにより、特定のタスクを起こすために必要な、一意な名前をタスクに付けることが可能になります。 + +#### `Executor`型 + +新しい `Executor`型を `task::executor` モジュールで作成します: + +```rust +// in src/task/mod.rs + +pub mod executor; +``` + +```rust +// in src/task/executor.rs + +use super::{Task, TaskId}; +use alloc::{collections::BTreeMap, sync::Arc}; +use core::task::Waker; +use crossbeam_queue::ArrayQueue; + +pub struct Executor { + tasks: BTreeMap, + task_queue: Arc>, + waker_cache: BTreeMap, +} + +impl Executor { + pub fn new() -> Self { + Executor { + tasks: BTreeMap::new(), + task_queue: Arc::new(ArrayQueue::new(100)), + waker_cache: BTreeMap::new(), + } + } +} +``` + +`SimpleExecutor`で行ったようにタスクを[`VecDeque`]に格納する代わりに、タスクIDを格納する`task_queue`と、実際の`Task`インスタンスを格納する`tasks`という名前の[`BTreeMap`]を使用します。このマップは、特定のタスクを効率的に継続できるように、`TaskId`でインデックスされています。 + +`task_queue`フィールドはタスクIDの[`ArrayQueue`]で、**参照カウント** を実装している[`Arc`]型にラップされています。参照カウントは、複数の所有者の間で値の所有権を共有することを可能にします。これは、ヒープ上に値を割り当て、その値への有効な参照の数をカウントすることで動作します。有効な参照の数がゼロになったら、その値は不要なので、解放することができます。 + +この `Arc` 型を `task_queue` に使用しているのは、executorとwakerの間で共有されるからです。考え方としては、wakerは起こされたタスクのIDをキューにpushします。executorはキューの受信側におり、`tasks`マップからIDによって起こされたタスクを取り出し、それを実行します。[`SegQueue`]のような無制限のキューではなく、固定サイズのキューを使う理由は、アロケートを行ってはならない割り込みハンドラがこのキューにpushするからです。 + +`Executor` 型には、`task_queue` と `tasks` マップに加えて、`waker_cache` フィールドがあり、これもマップです。このマップはタスクが作成された後にそのタスクの[`Waker`]をキャッシュします。これには2つの理由があります。1つ目は、同じタスクの複数回のwakeupに対して、毎回新しいwakerを作成するのではなく、同じwakerを再利用することでパフォーマンスを向上させるためです。2つ目は、参照カウントされるwakerが割り込みハンドラ内で解放されないようにするためです。これはデッドロックにつながる可能性があるからです(これについては後で詳しく説明します)。 + +[`Arc`]: https://doc.rust-lang.org/stable/alloc/sync/struct.Arc.html +[`SegQueue`]: https://docs.rs/crossbeam-queue/0.2.1/crossbeam_queue/struct.SegQueue.html + +`Executor`を作成するために、簡単な`new`関数を用意しました。`task_queue`の容量は100としていますが、これは当面の間は十分すぎる量です。将来的に100以上のタスクが同時に発生するような場合には、このサイズは簡単に増やすことができます。 + +#### タスクの生成 + +`SimpleExecutor`と同じように、`Executor`型の`spawn`メソッドを用意しています。このメソッドは、与えられたタスクを`tasks`マップに追加し、そのIDを`task_queue`にpushすることで、すぐにタスクを起動します: + +```rust +// in src/task/executor.rs + +impl Executor { + pub fn spawn(&mut self, task: Task) { + let task_id = task.id; + if self.tasks.insert(task.id, task).is_some() { + panic!("task with same ID already in tasks"); + // "同じIDのタスクがすでにtasks内に存在" + } + self.task_queue.push(task_id).expect("queue full"); + } +} +``` + +同じIDのタスクがすでにマップ内に存在する場合、[`BTreeMap::insert`]メソッドはそれを返します。各タスクはユニークなIDを持っているので、このようなことは絶対に起こってはならず、この場合は私達のコードにバグがあることになるのでパニックします。同様に、`task_queue` がいっぱいになったときもパニックします。 + +#### Tasksの実行 + +`task_queue`内のすべてのタスクを実行するには、プライベートの`run_ready_tasks`メソッドを作成します: + +```rust +// in src/task/executor.rs + +use core::task::{Context, Poll}; + +impl Executor { + fn run_ready_tasks(&mut self) { + // 借用チェッカのエラーを回避するために`self`を分配する + let Self { + tasks, + task_queue, + waker_cache, + } = self; + + while let Ok(task_id) = task_queue.pop() { + let task = match tasks.get_mut(&task_id) { + Some(task) => task, + None => continue, // タスクはもう存在しない + }; + let waker = waker_cache + .entry(task_id) + .or_insert_with(|| TaskWaker::new(task_id, task_queue.clone())); + let mut context = Context::from_waker(waker); + match task.poll(&mut context) { + Poll::Ready(()) => { + // タスクは完了したので、タスクとそのキャッシュされたwakerを取り除く + tasks.remove(&task_id); + waker_cache.remove(&task_id); + } + Poll::Pending => {} + } + } + } +} +``` + +この関数の基本的な考え方は、私たちの `SimpleExecutor` と似ています。`task_queue` にあるすべてのタスクをループし、各タスクのwakerを作成し、ポーリングします。しかし、自分で保留中のタスクを `task_queue` の最後に戻すのではなく、`TaskWaker` の実装に、待機中のタスクをqueueに戻すことを任せます。このwaker型の実装については、後ほどご紹介します。 + +この `run_ready_tasks` メソッドの実装の詳細を見てみましょう: + +- 借用チェッカのエラーを避けるために、[**分配 (destructuring)**][_destructuring_]を使って`self`を3つのフィールドに分割しています。というのも、私たちの実装ではクロージャの中から `self.task_queue` にアクセスする必要があるのですが、今のRustはそれを行うために `self` を完全に借用してしまうのです。これは借用チェッカの基本的な問題であり、[RFC 2229]が[実装][RFC 2229 impl]されたときに解決されるでしょう。 + +- popされた各タスクIDに対して、`tasks`マップから対応するタスクの可変参照を取得します。私達の `ScancodeStream` の実装では、タスクをスリープさせる必要があるかどうかをチェックする前にwakeupを登録するので、もはや存在しないタスクに対してwakeupが発生することがあります。この場合には、単純にwakeupを無視して、キューから次のIDを取得して続行します。 + +- poll毎にwakerを作成することによるパフォーマンスのオーバーヘッドを避けるために、`waker_cache`マップを使用して、作成された各タスクのwakerを保存します。これには、[`BTreeMap::entry`]メソッドと[`Entry::or_insert_with`]を組み合わせて使用し、新しいwakerがまだ存在しない場合には新しいwakerを作成して、そのwakerへのミュータブルな参照を取得します。新しいwakerを作成するには、`task_queue` をクローンして、タスク ID と共に `TaskWaker::new` 関数に渡します (実装は後述)。`task_queue` は `Arc` にラップされているので、`clone` は値の参照カウントを増やすだけで、同じヒープに割り当てられたキューを指しています。このようにwakerを再利用することは、すべてのwakerの実装で可能なわけではありませんが、私たちの `TaskWaker` 型ではそれが可能であることに注意してください。 + +[_destructuring_]: https://doc.rust-jp.rs/book-ja/ch18-03-pattern-syntax.html#%E6%A7%8B%E9%80%A0%E4%BD%93%E3%82%92%E5%88%86%E9%85%8D%E3%81%99%E3%82%8B +[RFC 2229]: https://github.com/rust-lang/rfcs/pull/2229 +[RFC 2229 impl]: https://github.com/rust-lang/rust/issues/53488 + +[`BTreeMap::entry`]: https://doc.rust-lang.org/alloc/collections/btree_map/struct.BTreeMap.html#method.entry +[`Entry::or_insert_with`]: https://doc.rust-lang.org/alloc/collections/btree_map/enum.Entry.html#method.or_insert_with + +タスクは `Poll::Ready` を返すと終了します。その場合、[`BTreeMap::remove`]メソッドを使って `tasks` マップからタスクを削除します。また、キャッシュされたwakerがあれば、それも削除します。 + +[`BTreeMap::remove`]: https://doc.rust-lang.org/alloc/collections/btree_map/struct.BTreeMap.html#method.remove + +#### Wakerの設計 + +wakerの仕事は、起こされたタスクのIDをexecutorの`task_queue`にpushすることです。新しい `TaskWaker` 構造体を作成して、タスクの ID と `task_queue` への参照を格納することで、これを実装します: + +```rust +// in src/task/executor.rs + +struct TaskWaker { + task_id: TaskId, + task_queue: Arc>, +} +``` + +`task_queue`の所有権はexecutorとwakerの間で共有されるので、[`Arc`]ラッパー型を使って、参照カウント式の共有所有権を実装します。 + +[`Arc`]: https://doc.rust-lang.org/stable/alloc/sync/struct.Arc.html + +wakeオペレーションの実装は非常にシンプルです: + +```rust +// in src/task/executor.rs + +impl TaskWaker { + fn wake_task(&self) { + self.task_queue.push(self.task_id).expect("task_queue full"); + } +} +``` + +参照されている `task_queue` に `task_id` をpushします。[`ArrayQueue`]型の変更には共有参照だけがあればよいので、このメソッドは `&mut self` ではなく `&self` に実装することができます。 + +##### `Wake` Trait + +Futureのポーリングに`TaskWaker`型を使うには、まずこれを[`Waker`]インスタンスに変換する必要があります。これは [`Future::poll`] メソッドが引数として [`Context`] インスタンスを取り、このインスタンスは `Waker` 型からしか構築できないためです。これは [`RawWaker`] 型の実装を提供することによって可能ですが、代わりに `Arc` ベースの [`Wake`][wake-trait] trait を実装し、標準ライブラリが提供する [`From`] の実装を使用して `Waker` を構築する方が、よりシンプルで安全でしょう。 + +traitの実装は以下のようにします: + +[wake-trait]: https://doc.rust-lang.org/nightly/alloc/task/trait.Wake.html + +```rust +// in src/task/executor.rs + +use alloc::task::Wake; + +impl Wake for TaskWaker { + fn wake(self: Arc) { + self.wake_task(); + } + + fn wake_by_ref(self: &Arc) { + self.wake_task(); + } +} +``` + +Waker は通常、executorと非同期タスクの間で共有されるので、この trait メソッドでは、`Self` インスタンスを、参照カウントされた所有権を実装する [`Arc`] 型でラップする必要があります。つまり、これらのメソッドを呼び出すためには、`TaskWaker` を `Arc` に移動させる必要があります。 + +`wake`と`wake_by_ref`メソッドの違いは、後者は`Arc`への参照のみを必要とするのに対し、前者は`Arc`の所有権を取得するため、しばしば参照カウントの増加を必要とすることです。すべての型が参照によるwakeをサポートしているわけではないので、`wake_by_ref` メソッドを実装するかは自由ですが、不必要な参照カウントの変更を避けることができるので、パフォーマンスの向上につながります。今回の例では、両方の trait メソッドで単純に `wake_task` 関数を呼び出すようにします。この関数は、共有の `&self` 参照しか要求しません。 + +##### Wakerを生成する + +`Waker` 型は、`Wake` traitを実装したすべての `Arc` でラップされた値からの [`From`] 変換をサポートしているので、`Executor::run_ready_tasks` メソッドで必要となる `TaskWaker::new` 関数を実装することができます: + +[`From`]: https://doc.rust-lang.org/nightly/core/convert/trait.From.html + +```rust +// in src/task/executor.rs + +impl TaskWaker { + fn new(task_id: TaskId, task_queue: Arc>) -> Waker { + Waker::from(Arc::new(TaskWaker { + task_id, + task_queue, + })) + } +} +``` + +渡された `task_id` と `task_queue` を使って `TaskWaker` を作成します。次に `TaskWaker` を `Arc` で囲み、`Waker::from` の実装を使用してそれを [`Waker`] に変換します。この `from` メソッドは、`TaskWaker` 型の [`RawWakerVTable`] と [`RawWaker`] インスタンスの構築を行います。このメソッドの詳細について興味のある場合は、[`alloc`クレート内での実装][waker-from-impl]をご覧ください。 + +[waker-from-impl]: https://github.com/rust-lang/rust/blob/cdb50c6f2507319f29104a25765bfb79ad53395c/src/liballoc/task.rs#L58-L87 + +#### `run`メソッド + +wakerの実装ができたので、いよいよexecutorの`run`メソッドを構築します: + +```rust +// in src/task/executor.rs + +impl Executor { + pub fn run(&mut self) -> ! { + loop { + self.run_ready_tasks(); + } + } +} +``` + +このメソッドは `run_ready_tasks` 関数の呼び出しをループするだけです。理論的には、`tasks`マップが空になったときにこの関数からリターンすることもできますが、`keyboard_task`が終了しないのでそれは起こらず、よって単純な`loop`で十分です。この関数は決してリターンしませんので、`!`という戻り値の型を使って、コンパイラにこの関数が[発散する (diverging) ][diverging]ことを示します。 + +[diverging]: https://doc.rust-lang.org/stable/rust-by-example/fn/diverging.html + +これで、`kernel_main`で、`SimpleExecutor`の代わりに新しい`Executor`を使うように変更することができます: + +```rust +// in src/main.rs + +use blog_os::task::executor::Executor; // new + +fn kernel_main(boot_info: &'static BootInfo) -> ! { + // init_heap、test_mainを含む初期化ルーチンを省略 + + let mut executor = Executor::new(); // new + executor.spawn(Task::new(example_task())); + executor.spawn(Task::new(keyboard::print_keypresses())); + executor.run(); +} +``` + +必要なのは、インポート部(`use`のところ)と型名を変更することだけです。関数 `run` は発散する関数となっているので、コンパイラはこの関数が決してリターンしないことを認識し、そのため`kernel_main` 関数の最後に `hlt_loop` を呼び出す必要はもうありません。 + +ここで、`cargo run`を使ってカーネルを実行すると、キーボード入力が変わらず正常に動作することがわかります: + +![QEMU printing ".....H...e...l...l..o..... ...a..g..a....i...n...!"](qemu-keyboard-output-again.gif) + +しかし、QEMUのCPU使用量は全く減っていません。その理由は、CPUをずっとbusy状態にしているからです。タスクが再び起こされるまでポーリングすることはなくなりましたが、`task_queue`をチェックし続けるbusy loop(忙しないループの意)に入っているのです。この問題を解決するには、やるべき仕事がなくなったらCPUをスリープさせる必要があります。 + +#### 何もすることがない (idle) ならスリープする + +基本的な考え方は、`task_queue`が空になったときに[`hlt`命令][`hlt` instruction]を実行するというものです。この命令は、次の割り込みが来るまでCPUをスリープ状態にします。割り込みが入るとCPUがすぐに活動を再開するので、割り込みハンドラが`task_queue`にpushされたときにも直接反応できるようになっています。 + +[`hlt` instruction]: https://en.wikipedia.org/wiki/HLT_(x86_instruction) + +これを実現するために、executorに新しい`sleep_if_idle`メソッドを作成し、`run`メソッドから呼び出します: + +```rust +// in src/task/executor.rs + +impl Executor { + pub fn run(&mut self) -> ! { + loop { + self.run_ready_tasks(); + self.sleep_if_idle(); // new + } + } + + fn sleep_if_idle(&self) { + if self.task_queue.is_empty() { + x86_64::instructions::hlt(); + } + } +} +``` + +`sleep_if_idle`は、`task_queue`が空になるまでループする`run_ready_tasks`の直後に呼び出されるので、キューを再度チェックする必要はないと思われるかもしれません。しかし、`run_ready_tasks` がリターンしてきた直後にハードウェア割り込みが発生する可能性があるため、`sleep_if_idle` 関数が呼ばれた時点ではキューに新しいタスクがあるかもしれません。キューがまだ空であった場合のみ、[`x86_64`]クレートが提供する[`instructions::hlt`]ラッパー関数を介して`hlt`命令を実行することで、CPUをスリープさせます。 + +[`instructions::hlt`]: https://docs.rs/x86_64/0.14.2/x86_64/instructions/fn.hlt.html +[`x86_64`]: https://docs.rs/x86_64/0.14.2/x86_64/index.html + +残念ながら、この実装には微妙な競合状態が残っています。割り込みは非同期であり、いつでも発生する可能性があるため、`is_empty` のチェックと `hlt` の呼び出しの間に割り込みが発生する可能性があります: + +```rust +if self.task_queue.is_empty() { + /// <--- 割り込みがここで起きる可能性があります + x86_64::instructions::hlt(); +} +``` + +この割り込みが`task_queue`にpushされた場合、タスクの準備ができているにもかかわらず、CPUをスリープ状態にしてしまいます。最悪の場合、キーボード割り込みの処理が次のkeypressや次のタイマー割り込みまで遅れることになります。では、これを防ぐにはどうしたらよいでしょうか? + +その答えは、チェックの前にCPUの割り込みを無効にし、`hlt`命令と一緒にアトミックに再度有効にすることです。この方法では、その間に発生するすべての割り込みが `hlt` 命令の後に遅延されるため、wakeupが失敗することはありません。この方法を実装するには、[`x86_64`]クレートが提供する[`interrupts::enable_and_hlt`][`enable_and_hlt`]関数を使用します。 + +[`enable_and_hlt`]: https://docs.rs/x86_64/0.14.2/x86_64/instructions/interrupts/fn.enable_and_hlt.html + +更新された `sleep_if_idle` 関数の実装は次のようになります: + +```rust +// in src/task/executor.rs + +impl Executor { + fn sleep_if_idle(&self) { + use x86_64::instructions::interrupts::{self, enable_and_hlt}; + + interrupts::disable(); + if self.task_queue.is_empty() { + enable_and_hlt(); + } else { + interrupts::enable(); + } + } +} +``` + +競合状態を避けるために、`task_queue` が空であるかどうかを確認する前に、割り込みを無効にします。空いていれば、[`enable_and_hlt`]関数を使用して、単一のアトミック操作として割り込みを有効にしCPUをスリープさせます。キューが空でない場合は、`run_ready_tasks` がリターンしてきた後に、割り込みがタスクを起動したことを意味します。その場合は、再び割り込みを有効にして、`hlt`を実行せずにすぐに実行を継続します。 + +これで、実行することがないときには、executorが適切にCPUをスリープ状態にするようになりました。再び`cargo run`を使ってカーネルを実行すると、QEMUプロセスのCPU使用率が大幅に低下していることがわかります。 + +#### 考えられる機能拡張 + +executorは、効率的な方法でタスクを実行できるようになりました。待機中のタスクのポーリングを避けるためにwaker通知を利用し、現在やるべきことがないときはCPUをスリープさせます。しかし、このexecutorはまだ非常に基本的なものであり、機能を拡張する方法はたくさんあります: + +- **スケジューリング**: 現在、我々は[`VecDeque`]型を使用して、先入れ先出し(FIFO)戦略を`task_queue`に実装しています。これはしばしば **ラウンドロビン (round robin)** スケジューリングとも呼ばれます。この戦略は、すべてのワークロードにとって最も効率的であるとは限りません。例えば、レイテンシーが重要なタスクや、I/Oを大量に行うタスクを優先させることは意味があるかもしれません。詳しくは、[_Operating Systems: Three Easy Pieces_]の[スケジューリングの章][scheduling chapter]や、[スケジューリングに関するWikipediaの記事][scheduling-wiki]をご覧ください。 +- **タスクの発生 (spawn)**: 現在、私たちの `Executor::spawn` メソッドは `&mut self` の参照を必要とするため、`run` メソッドを開始した後は利用できません。この問題を解決するには、追加で `Spawner` 型を作成します。この型は、ある種のキューをexecutorと共有し、タスク自身の中からタスクを作成することができます。このキューには、例えば `task_queue` を直接使用することもできますし、executorが実行ループの中でチェックする別のキューを使用することもできます。 +- **スレッドを活用する**: まだスレッドのサポートはしていませんが、次の投稿で追加する予定です。これにより、複数のexecutorのインスタンスを異なるスレッドで起動することが可能になります。このアプローチの利点は、複数のタスクが同時に実行できるため、長時間実行するタスクによって課せられる遅延を減らすことができることです。また、この方法では、複数のCPUコアを利用することもできます。 +- **負荷の分配**: スレッドをサポートするようにした場合、すべてのCPUコアが利用されるように、executor間でどのようにタスクを分配するかが重要になります。このための一般的なテクニックは、[_work stealing_]です。 + +[scheduling chapter]: http://pages.cs.wisc.edu/~remzi/OSTEP/cpu-sched.pdf +[_Operating Systems: Three Easy Pieces_]: http://pages.cs.wisc.edu/~remzi/OSTEP/ +[scheduling-wiki]: https://en.wikipedia.org/wiki/Scheduling_(computing) +[_work stealing_]: https://en.wikipedia.org/wiki/Work_stealing + +## まとめ + +この記事ではまず、**マルチタスク**について紹介し、実行中のタスクを定期的に強制的に中断させる**非協調的**マルチタスクと、タスクが自発的にCPUの制御を放棄するまで実行させてやる**協調的**マルチタスクの違いを説明しました。 + +次に、Rustがサポートする**async/await**がどのようにして協調的マルチタスクの言語レベルの実装を提供しているかを調べました。Rustは、非同期タスクを抽象化するポーリングベースの`Future` traitをベースにして実装しています。async/awaitを使うと、通常の同期コードとほぼ同じようにfutureを扱うことができます。違いは、非同期関数が再び `Future` を返すことで、それを実行するためにはどこかの時点でこの`Future`をexecutorに追加する必要があります。 + +舞台裏では、コンパイラが async/await コードを **ステートマシン** に変換し、各 `.await` オペレーションが可能な待ち状態に対応するようにします。対象のプログラムに関する知識を活用することで、コンパイラは各待ち状態に必要な最小限の状態のみを保存することができ、その結果、タスクあたりのメモリ消費量は非常に小さくなります。一つの課題は、生成されたステートマシンに **自己参照**構造体が含まれている可能性があることです。例えば、非同期関数のローカル変数の一方が他方を参照している場合などです。ポインタの無効化を防ぐために、Rustは`Pin`型を用いて、futureが最初にポーリングされた後は、メモリ内で移動できないようにしています。 + +私たちの**実装**では、まず、`Waker`型を全く使わずに、busy loopですべてのspawnされたタスクをポーリングする非常に基本的なexecutorを作成しました。次に、非同期のキーボードタスクを実装することで、waker通知の利点を示しました。このタスクは、`crossbeam`クレートが提供する`ArrayQueue`というmutexを使用しない型を使って、静的な`SCANCODE_QUEUE`を定義します。キーボード割り込みハンドラは、キーの入力を直接処理する代わりに、受信したすべてのスキャンコードをキューに入れ、登録されている `Waker` を起こして、新しい入力が利用可能であることを通知します。受信側では、`ScancodeStream`型を作成して、キュー内の次のスキャンコードに変化する`Future`を提供しています。これにより、非同期の `print_keypresses` タスクを作成することができました。このタスクは、キュー内のスキャンコードを解釈して出力するために async/await を使用します。 + +キーボードタスクのwaker通知を利用するために、新しい `Executor` 型を作成しました。この型は、準備のできたタスクに `Arc` で共有された `task_queue` を使用します。 私たちは`TaskWaker`型を実装し、起こされたタスクのIDを直接この`task_queue`にpushし、それをexecutorが再びポーリングするようにしました。また、実行可能なタスクがないときに電力を節約するために、`hlt`命令を用いてCPUをスリープさせる機能を追加しました。最後に、マルチコアへの対応など、executorの拡張の可能性について述べました。 + +## 次は? + +async/waitを使うことで、カーネルで基本的な協調的マルチタスクをサポートできるようになりました。協調的マルチタスクは非常に効率的ですが、個々のタスクが長く実行しすぎる場合、他のタスクの実行が妨げられ、遅延の問題が発生します。このため、カーネルに非協調的マルチタスクのサポートを追加することは理にかなっています。 + +次回は、非協調的マルチタスクの最も一般的な形態である **スレッド** を紹介します。スレッドは、長時間実行されるタスクの問題を解決するだけでなく、将来的に複数のCPUコアを利用したり、信頼できないユーザープログラムを実行したりするための準備にもなります。 diff --git a/blog/content/edition-2/posts/12-async-await/index.md b/blog/content/edition-2/posts/12-async-await/index.md index bc58ec10..fbe7e69f 100644 --- a/blog/content/edition-2/posts/12-async-await/index.md +++ b/blog/content/edition-2/posts/12-async-await/index.md @@ -16,6 +16,7 @@ This blog is openly developed on [GitHub]. If you have any problems or questions [GitHub]: https://github.com/phil-opp/blog_os [at the bottom]: #comments + [post branch]: https://github.com/phil-opp/blog_os/tree/post-12 @@ -91,7 +92,7 @@ However, the strong performance and memory benefits of cooperative multitasking ## Async/Await in Rust -The Rust language provides first-class support for cooperative multitasking in form of async/await. Before we can explore what async/await is and how it works, we need to understand how _futures_ and asynchronous programming work in Rust. +The Rust language provides first-class support for cooperative multitasking in the form of async/await. Before we can explore what async/await is and how it works, we need to understand how _futures_ and asynchronous programming work in Rust. ### Futures @@ -420,7 +421,7 @@ ExampleStateMachine::WaitingOnFooTxt(state) => { }; *self = ExampleStateMachine::WaitingOnBarTxt(state); } else { - *self = ExampleStateMachine::End(EndState)); + *self = ExampleStateMachine::End(EndState); return Poll::Ready(content); } } @@ -441,7 +442,7 @@ ExampleStateMachine::WaitingOnBarTxt(state) => { match state.bar_txt_future.poll(cx) { Poll::Pending => return Poll::Pending, Poll::Ready(bar_txt) => { - *self = ExampleStateMachine::End(EndState)); + *self = ExampleStateMachine::End(EndState); // from body of `example` return Poll::Ready(state.content + &bar_txt); } @@ -687,7 +688,7 @@ For further reading, check out the documentation of the [`pin` module] and the [ #### Pinning and Futures -As we already saw in this post, the [`Future::poll`] method uses pinning in form of a `Pin<&mut Self>` parameter: +As we already saw in this post, the [`Future::poll`] method uses pinning in the form of a `Pin<&mut Self>` parameter: [`Future::poll`]: https://doc.rust-lang.org/nightly/core/future/trait.Future.html#tymethod.poll @@ -1025,7 +1026,7 @@ We already have some kind of asynchronicity in our system that we can use for th [_Interrupts_]: @/edition-2/posts/07-hardware-interrupts/index.md -In the following, we will create an asynchronous task based on the keyboard interrupt. The keyboard interrupt is a good candidate for this because it is both non-deterministic and latency-critical. Non-deteministic means that there is no way to predict when the next key press will occur because it is entirely dependent on the user. Latency-critical means that we want to handle the keyboard input in a timely manner, otherwise the user will feel a lag. To support such a task in an efficient way, it will be essential that the executor has proper support for `Waker` notifications. +In the following, we will create an asynchronous task based on the keyboard interrupt. The keyboard interrupt is a good candidate for this because it is both non-deterministic and latency-critical. Non-deterministic means that there is no way to predict when the next key press will occur because it is entirely dependent on the user. Latency-critical means that we want to handle the keyboard input in a timely manner, otherwise the user will feel a lag. To support such a task in an efficient way, it will be essential that the executor has proper support for `Waker` notifications. #### Scancode Queue @@ -1136,7 +1137,7 @@ To call the `add_scancode` function on keyboard interrupts, we update our `keybo // in src/interrupts.rs extern "x86-interrupt" fn keyboard_interrupt_handler( - _stack_frame: &mut InterruptStackFrame + _stack_frame: InterruptStackFrame ) { use x86_64::instructions::port::Port; @@ -1418,7 +1419,7 @@ The `TaskId` struct is a simple wrapper type around `u64`. We derive a number of [`BTreeMap`]: https://doc.rust-lang.org/alloc/collections/btree_map/struct.BTreeMap.html -To create a new unique ID, we create a `TaskID::new` function: +To create a new unique ID, we create a `TaskId::new` function: ```rust use core::sync::atomic::{AtomicU64, Ordering}; @@ -1643,7 +1644,7 @@ impl Wake for TaskWaker { } ``` -The trait is still unstable, so we have to add **`#![feature(wake_trait)]`** to the top of our `lib.rs` to use it. Since wakers are commonly shared between the executor and the asynchronous tasks, the trait methods require that the `Self` instance is wrapped in the [`Arc`] type, which implements reference-counted ownership. This means that we have to move our `TaskWaker` to an `Arc` in order to call them. +Since wakers are commonly shared between the executor and the asynchronous tasks, the trait methods require that the `Self` instance is wrapped in the [`Arc`] type, which implements reference-counted ownership. This means that we have to move our `TaskWaker` to an `Arc` in order to call them. The difference between the `wake` and `wake_by_ref` methods is that the latter only requires a reference to the `Arc`, while the former takes ownership of the `Arc` and thus often requires an increase of the reference count. Not all types support waking by reference, so implementing the `wake_by_ref` method is optional, however it can lead to better performance because it avoids unnecessary reference count modifications. In our case, we can simply forward both trait methods to our `wake_task` function, which requires only a shared `&self` reference. @@ -1744,8 +1745,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.13.2/x86_64/instructions/fn.hlt.html -[`x86_64`]: https://docs.rs/x86_64/0.13.2/x86_64/index.html +[`instructions::hlt`]: https://docs.rs/x86_64/0.14.2/x86_64/instructions/fn.hlt.html +[`x86_64`]: https://docs.rs/x86_64/0.14.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 +1759,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 [`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. +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. -[`enable_and_hlt`]: https://docs.rs/x86_64/0.13.2/x86_64/instructions/interrupts/fn.enable_and_hlt.html +[`enable_and_hlt`]: https://docs.rs/x86_64/0.14.2/x86_64/instructions/interrupts/fn.enable_and_hlt.html The updated implementation of our `sleep_if_idle` function looks like this: @@ -1789,7 +1790,7 @@ Now our executor properly puts the CPU to sleep when there is nothing to do. We Our executor is now able to run tasks in an efficient way. It utilizes waker notifications to avoid polling waiting tasks and puts the CPU to sleep when there is currently no work to do. However, our executor is still quite basic and there are many possible ways to extend its functionality: -- **Scheduling:** We currently use the [`VecDeque`] type to implement a _first in first out_ (FIFO) strategy for our `task_queue`, which is often also called _round robin_ scheduling. This strategy might not be the most efficient for all workloads. For example, it might make sense to prioritize latency-critical tasks or tasks that do a lot of I/O. See the [scheduling chapter] of the [_Operating Systems: Three Easy Pieces_] book or the [Wikipedia article on scheduling][scheduling-wiki] for more information. +- **Scheduling**: We currently use the [`VecDeque`] type to implement a _first in first out_ (FIFO) strategy for our `task_queue`, which is often also called _round robin_ scheduling. This strategy might not be the most efficient for all workloads. For example, it might make sense to prioritize latency-critical tasks or tasks that do a lot of I/O. See the [scheduling chapter] of the [_Operating Systems: Three Easy Pieces_] book or the [Wikipedia article on scheduling][scheduling-wiki] for more information. - **Task Spawning**: Our `Executor::spawn` method currently requires a `&mut self` reference and is thus no longer available after starting the `run` method. To fix this, we could create an additional `Spawner` type that shares some kind of queue with the executor and allows task creation from within tasks themselves. The queue could be for example the `task_queue` directly or a separate queue that the executor checks in its run loop. - **Utilizing Threads**: We don't have support for threads yet, but we will add it in the next post. This will make it possible to launch multiple instances of the executor in different threads. The advantage of this approach is that the delay imposed by long running tasks can be reduced because other tasks can run concurrently. This approach also allows it to utilize multiple CPU cores. - **Load Balancing**: When adding threading support, it becomes important how to distribute the tasks between the executors to ensure that all CPU cores are utilized. A common technique for this is [_work stealing_]. diff --git a/blog/content/edition-2/posts/_index.fr.md b/blog/content/edition-2/posts/_index.fr.md new file mode 100644 index 00000000..c7079c40 --- /dev/null +++ b/blog/content/edition-2/posts/_index.fr.md @@ -0,0 +1,7 @@ ++++ +title = "Posts" +sort_by = "weight" +insert_anchor_links = "left" +render = false +page_template = "edition-2/page.html" ++++ diff --git a/blog/content/edition-2/posts/_index.ru.md b/blog/content/edition-2/posts/_index.ru.md new file mode 100644 index 00000000..c7079c40 --- /dev/null +++ b/blog/content/edition-2/posts/_index.ru.md @@ -0,0 +1,7 @@ ++++ +title = "Posts" +sort_by = "weight" +insert_anchor_links = "left" +render = false +page_template = "edition-2/page.html" ++++ diff --git a/blog/content/edition-2/posts/deprecated/04-unit-testing/index.md b/blog/content/edition-2/posts/deprecated/04-unit-testing/index.md index 81a80343..5c97cf68 100644 --- a/blog/content/edition-2/posts/deprecated/04-unit-testing/index.md +++ b/blog/content/edition-2/posts/deprecated/04-unit-testing/index.md @@ -17,6 +17,7 @@ This blog is openly developed on [GitHub]. If you have any problems or questions [GitHub]: https://github.com/phil-opp/blog_os [at the bottom]: #comments + [post branch]: https://github.com/phil-opp/blog_os/tree/post-04 diff --git a/blog/content/edition-2/posts/deprecated/05-integration-tests/index.md b/blog/content/edition-2/posts/deprecated/05-integration-tests/index.md index cf1c8b3c..900ec34d 100644 --- a/blog/content/edition-2/posts/deprecated/05-integration-tests/index.md +++ b/blog/content/edition-2/posts/deprecated/05-integration-tests/index.md @@ -17,6 +17,7 @@ This blog is openly developed on [GitHub]. If you have any problems or questions [GitHub]: https://github.com/phil-opp/blog_os [at the bottom]: #comments + [post branch]: https://github.com/phil-opp/blog_os/tree/post-05 diff --git a/blog/content/edition-2/posts/deprecated/10-advanced-paging/index.md b/blog/content/edition-2/posts/deprecated/10-advanced-paging/index.md index d2348543..70eac621 100644 --- a/blog/content/edition-2/posts/deprecated/10-advanced-paging/index.md +++ b/blog/content/edition-2/posts/deprecated/10-advanced-paging/index.md @@ -17,6 +17,7 @@ This blog is openly developed on [GitHub]. If you have any problems or questions [GitHub]: https://github.com/phil-opp/blog_os [at the bottom]: #comments + [post branch]: https://github.com/phil-opp/blog_os/tree/5c0fb63f33380fc8596d7166c2ebde03ef3d6726 ## Introduction diff --git a/blog/content/status-update/2019-12-02.md b/blog/content/status-update/2019-12-02.md index b366c0a9..5d7e4fca 100644 --- a/blog/content/status-update/2019-12-02.md +++ b/blog/content/status-update/2019-12-02.md @@ -15,7 +15,7 @@ We also have other news: We plan to add [Experimental Support for Community Tran ## `bootloader` -- [Change the way the kernel entry point is called to honor alignement ABI](https://github.com/rust-osdev/bootloader/pull/81) by [@GuillaumeDIDIER](https://github.com/GuillaumeDIDIER) (published as version 0.8.2) +- [Change the way the kernel entry point is called to honor alignment ABI](https://github.com/rust-osdev/bootloader/pull/81) by [@GuillaumeDIDIER](https://github.com/GuillaumeDIDIER) (published as version 0.8.2) - [Add support for Github Actions](https://github.com/rust-osdev/bootloader/pull/82) - [Remove unnecessary `extern C` on panic handler to fix not-ffi-safe warning](https://github.com/rust-osdev/bootloader/pull/85) by [@cmsd2](https://github.com/cmsd2) (published as version 0.8.3) diff --git a/blog/sass/css/edition-2/main.scss b/blog/sass/css/edition-2/main.scss new file mode 100644 index 00000000..f2ca3806 --- /dev/null +++ b/blog/sass/css/edition-2/main.scss @@ -0,0 +1,1061 @@ +/* + * CSS file for the second edition of os.phil-opp.com. + * + * Based on `poole`which was designed, built, and released under MIT license by @mdo. See + * https://github.com/poole/poole. + */ + +/* + * Contents + * + * Fonts + * Body resets + * Dark/Light Mode + * Custom type + * Messages + * Container + * Masthead + * Posts and pages + * Pagination + * Reverse layout + * Themes + */ + +/* Fonts */ + +@font-face { + font-family: "Iosevka"; + src: url("/fonts/iosevka-regular.woff2") format("woff2"), url("/fonts/iosevka-regular.woff") format("woff"); + font-weight: normal; + font-style: normal; + font-display: swap; +} + +/* + * Body resets + * + * Update the foundational and global aspects of the page. + */ + +* { + -webkit-box-sizing: border-box; + -moz-box-sizing: border-box; + box-sizing: border-box; +} + +html, +body { + margin: 0; + padding: 0; +} + +html { + font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; + line-height: 1.5; +} + +/* Dark/Light Mode */ + +@mixin set-colors-light { + --background-color: #fff; + --text-color: #515151; + --heading-color: #313131; + --heading-code-color: #a0565c; + --link-color: #268bd2; + --hr-color-top: #eee; + --hr-color-bottom: #fff; + --code-text-color: #bf616a; + --code-background-color: #f9f9f9; + --masthead-title-color: #505050; + --strong-color: #303030; + --masthead-subtitle: #c0c0c0; + --post-title-color: #228; +} + +@mixin set-colors-dark { + --background-color: #252525; + --text-color: #f5f5f5; + --heading-color: #eee; + --heading-code-color: #eee; + --link-color: #c59ff3; + --hr-color-top: #333; + --hr-color-bottom: #000; + --code-text-color: #eeeeee; + --code-background-color: #222222; + --masthead-title-color: #b6b6b6; + --strong-color: #c0c0c0; + --masthead-subtitle: #8f8f8f; + --post-title-color: #c8c8ff; +} + +body { + @include set-colors-light(); +} + +[data-theme="dark"] body { + @include set-colors-dark(); +} + +/* Styles for users who prefer dark mode at the OS level */ +@media (prefers-color-scheme: dark) { + /* defaults to dark theme */ + body { + @include set-colors-dark(); + } + /* Override dark mode with light mode styles if the user decides to swap */ + [data-theme="light"] body { + @include set-colors-light(); + } +} + +body { + color: var(--text-color); + background-color: var(--background-color); + -webkit-text-size-adjust: 100%; + -ms-text-size-adjust: 100%; +} + +/* No `:visited` state is required by default (browsers will use `a`) */ + +a { + color: var(--link-color); + text-decoration: none; +} + +/* `:focus` is linked to `:hover` for basic accessibility */ + +a:hover, +a:focus { + text-decoration: underline; +} + +/* Headings */ +h1, +h2, +h3, +h4, +h5, +h6 { + margin-bottom: 0.5rem; + font-weight: bold; + line-height: 1.25; + color: var(--heading-color); + text-rendering: optimizeLegibility; +} +h1 { + font-size: 2rem; +} +h2 { + margin-top: 1rem; + font-size: 1.5rem; +} +h3 { + margin-top: 1.5rem; + font-size: 1.25rem; +} +h4, +h5, +h6 { + margin-top: 1rem; + font-size: 1rem; +} + +/* Body text */ +p { + margin-top: 0; + margin-bottom: 1rem; +} + +strong { + color: var(--strong-color); +} + +/* Lists */ +ul, +ol, +dl { + margin-top: 0; + margin-bottom: 1rem; +} + +/* Nested lists */ +li ul, +li ol, +li dl { + margin-bottom: 0; +} + +li ul + p, +li ol + p, +li dl + p { + margin-top: 1rem; +} + +dt { + font-weight: bold; +} +dd { + margin-bottom: 0.5rem; +} + +/* Misc */ +hr { + position: relative; + margin: 1.5rem 0; + border: 0; + border-top: 1px solid var(--hr-color-top); + border-bottom: 1px solid var(--hr-color-bottom); +} + +abbr { + font-size: 90%; + font-weight: bold; + color: #555; + text-transform: uppercase; +} +abbr[title] { + cursor: help; + border-bottom: 1px dotted #e5e5e5; +} + +/* Code */ +code, +pre { + font-family: "Iosevka", monospace; +} +code { + padding: 0.25em 0.5em; + font-size: 85%; + color: var(--code-text-color); + background-color: var(--code-background-color); + border-radius: 3px; +} +pre { + display: block; + margin-top: 0; + margin-bottom: 1rem; + padding: 0.5rem; + font-size: 0.95rem; + line-height: 1.4; + white-space: pre; + overflow: auto; + word-wrap: normal; + background-color: var(--code-background-color); +} +pre code { + padding: 0; + font-size: 100%; + color: inherit; + background-color: transparent; +} +.highlight { + margin-bottom: 1rem; + border-radius: 4px; +} +.highlight pre { + margin-bottom: 0; +} + +/* Quotes */ +blockquote { + padding: 0.5rem 1rem; + margin: 0.8rem 0; + color: #7a7a7a; + border-left: 0.25rem solid #e5e5e5; +} +blockquote p:last-child { + margin-bottom: 0; +} +@media (min-width: 30rem) { + blockquote { + padding-right: 5rem; + padding-left: 1.25rem; + } +} + +img { + display: block; + margin: 0 0 1rem; + border-radius: 5px; + max-width: 100%; + color: grey; + font-style: italic; +} + +/* Tables */ +table { + margin-bottom: 1rem; + width: 100%; + border: 1px solid #e5e5e5; + border-collapse: collapse; +} +td, +th { + padding: 0.25rem 0.5rem; + border: 1px solid #e5e5e5; +} +tbody tr:nth-child(odd) td, +tbody tr:nth-child(odd) th { + background-color: var(--code-background-color); +} + +/* + * Custom type + * + * Extend paragraphs with `.lead` for larger introductory text. + */ + +.lead { + font-size: 1.25rem; + font-weight: 300; +} + +/* + * Messages + * + * Show alert messages to users. You may add it to single elements like a `

    `, + * or to a parent if there are multiple elements to show. + */ + +.message { + margin-bottom: 1rem; + padding: 1rem; + color: #717171; + background-color: var(--code-background-color); +} + +/* + * Container + * + * Center the page content. + */ + +.container { + max-width: 45rem; + padding-left: 1rem; + padding-right: 1rem; + margin-left: auto; + margin-right: auto; +} + +/* + * Masthead + * + * Super small header above the content for site name and short description. + */ + +.masthead { + padding-top: 1rem; + padding-bottom: 1rem; + margin-bottom: 1rem; +} +.masthead-title { + margin-top: 0; + margin-bottom: 0; + color: var(--masthead-title-color); +} +.masthead-title a { + color: var(--masthead-title-color); +} +.masthead small { + font-size: 75%; + font-weight: 400; + color: var(--masthead-subtitle); + letter-spacing: 0; +} + +/* + * Posts and pages + * + * Each post is wrapped in `.post` and is used on default and post layouts. Each + * page is wrapped in `.page` and is only used on the page layout. + */ + +.page { + margin-bottom: 4em; +} + +/* Blog post or page title */ +.page-title, +.post-title a { + color: var(--post-title-color); +} +.page-title, +.post-title { + margin-top: 0; +} + +/* Meta data line below post title */ +.post-date { + display: block; + margin-top: -0.5rem; + margin-bottom: 1rem; + color: #9a9a9a; +} + +/* Related posts */ +.related { + padding-top: 2rem; + padding-bottom: 2rem; + border-top: 1px solid #eee; +} +.related-posts { + padding-left: 0; + list-style: none; +} +.related-posts h3 { + margin-top: 0; +} +.related-posts li small { + font-size: 75%; + color: #999; +} +.related-posts li a:hover { + color: #268bd2; + text-decoration: none; +} +.related-posts li a:hover small { + color: inherit; +} + +/* + * Pagination + * + * Super lightweight (HTML-wise) blog pagination. `span`s are provide for when + * there are no more previous or next posts to show. + */ + +.pagination { + overflow: hidden; /* clearfix */ + margin-left: -1rem; + margin-right: -1rem; + font-family: "PT Sans", Helvetica, Arial, sans-serif; + color: #ccc; + text-align: center; +} + +/* Pagination items can be `span`s or `a`s */ +.pagination-item { + display: block; + padding: 1rem; + border: 1px solid #eee; +} +.pagination-item:first-child { + margin-bottom: -1px; +} + +/* Only provide a hover state for linked pagination items */ +a.pagination-item:hover { + background-color: #f5f5f5; +} + +@media (min-width: 30rem) { + .pagination { + margin: 3rem 0; + } + .pagination-item { + float: left; + width: 50%; + } + .pagination-item:first-child { + margin-bottom: 0; + border-top-left-radius: 4px; + border-bottom-left-radius: 4px; + } + .pagination-item:last-child { + margin-left: -1px; + border-top-right-radius: 4px; + border-bottom-right-radius: 4px; + } +} + +h1 code, +h2 code, +h3 code, +h4 code, +h5 code, +h6 code { + padding: 0; + color: var(--heading-code-color); + font-size: 90%; + background-color: inherit; +} + +.masthead-title { + font-size: 1.25rem; + display: inline; +} + +.masthead p { + font-size: 1.25rem; + display: inline; + margin: 0; + margin-left: 1rem; + padding: 0; + line-height: 1; +} + +.front-page-introduction { + margin-bottom: 2rem; +} + +.navigation { + float: right; +} + +.navigation img { + height: 1em; + vertical-align: baseline; + display: inline-block; + margin: 0; + padding: 0; + border-radius: 0; +} + +main img { + max-width: 100%; + margin: auto; +} + +.post { + margin-bottom: 2em; +} + +.post:last-child { + margin-bottom: 0em; +} + +.frontpage-section { + margin-bottom: 2rem; +} + +.posts { + padding: 1.5rem 1rem 0.5rem 1rem; + border-radius: 10px; + margin-bottom: 2rem; + margin-left: -0.5rem; + margin-right: -0.5rem; +} + +.posts.neutral { + border: 2px solid #999; +} + +.posts.subscribe { + border: 2px solid #aaa; +} + +.posts.edition-1 { + border: 2px solid #aaa; + background-color: #99ff0022; +} + +.posts.bare-bones { + border: 2px solid #66f; +} + +.posts.memory-management { + border: 2px solid #fc0; +} + +.posts.interrupts { + border: 2px solid #f66; +} + +.posts.multitasking { + border: 2px solid #556b2f; +} + +.posts hr { + margin: 2rem 0; +} + +.post-summary { + margin-bottom: 1rem; +} + +.post-summary p { + display: inline; +} + +.read-more { + margin-left: 5px; +} + +.no-translation { + margin-top: 0.3rem; + color: #999999; +} + +.post-category { + margin-right: 0.5rem; + text-transform: uppercase; + font-size: 0.8rem; + text-align: right; +} + +.post-category.bare-bones { + color: #55d; +} + +.post-category.memory-management { + color: #990; +} + +.post-category.interrupts { + color: #f33; +} + +.post-category.multitasking { + color: #556b2f; +} + +.post-footer-support { + margin-top: 2rem; +} + +.PageNavigation { + font-size: 0.9em; + display: table; + width: 100%; + overflow: hidden; +} + +.PageNavigation a { + display: table-cell; +} + +.PageNavigation .previous { + text-align: left; +} + +.PageNavigation .next { + text-align: right; +} + +footer.footer { + margin-top: 1rem; + margin-bottom: 1rem; + + .spaced { + margin-left: 0.5rem; + } +} + +.footnotes { + font-size: 85%; +} + +.footnotes li { + margin-bottom: 1rem; +} + +sup, +sub { + line-height: 0; +} + +a.anchorjs-link:hover { + text-decoration: none; +} + +#toc-aside { + display: none; +} + +#toc-inline summary { + margin-bottom: 0.2rem; +} + +aside#all-posts-link { + font-size: 90%; + margin-top: 0.5rem; +} + +@media (min-width: 80rem) { + #toc-inline { + display: none; + } + + #toc-aside { + display: block; + width: 12rem; + position: sticky; + float: left; + top: 3.5rem; + margin-top: -4rem; + margin-left: -15rem; + font-size: 90%; + line-height: 1.2; + } + + #toc-aside li > a, + #toc-aside h2 { + opacity: 0.5; + transition: opacity 0.5s; + } + + #toc-aside:hover li > a, + #toc-aside:hover h2 { + opacity: 1; + } + + #toc-aside li.active > a { + font-weight: bold; + } + + #toc-aside h2 { + font-size: 110%; + margin-bottom: 0.2rem; + } + + #toc-aside ol { + margin: 0 0 0.2rem 0; + padding: 0 0 0 1rem; + list-style: none; + } + + #toc-aside ol li a:before { + content: ""; + border-color: transparent #008eef; + border-style: solid; + border-width: 0.35em 0 0.35em 0.45em; + display: block; + height: 0; + width: 0; + left: -1em; + top: 0.9em; + position: relative; + } + + #toc-aside.coarse li ol { + display: none; + } + + aside.page-aside-right { + position: absolute; + min-width: 11rem; + max-width: 17rem; + top: 4rem; + margin-left: 45rem; + margin-right: 2rem; + font-size: 90%; + } + + aside.page-aside-right .block { + margin-bottom: 1.5rem; + } + + aside.page-aside-right h2 { + font-size: 110%; + margin-bottom: 0.2rem; + } + + aside.page-aside-right ul { + margin: 0 0 0.2rem 0; + padding: 0 0 0 1rem; + } + + aside.page-aside-right ul li { + margin-top: 0.5rem; + } + + #language-selector li { + margin-top: 0; + } + + aside#all-posts-link { + position: fixed; + top: 1.25rem; + margin-top: 0; + margin-left: -15rem; + } +} + +aside.page-aside-right time { + color: #9a9a9a; +} + +a code { + color: var(--link-color); +} + +a.zola-anchor { + opacity: 0; + position: absolute; + margin-left: -1.5em; + padding-right: 1em; + font-size: 0.6em; + vertical-align: baseline; + line-height: 2em; +} + +:hover > a.zola-anchor { + opacity: 1; + text-decoration: none; +} + +a.zola-anchor:hover { + text-decoration: none; +} + +div.note { + padding: 0.7rem 1rem; + margin: 1rem 0.2rem; + border: 2px solid #6ad46a; + border-radius: 5px; + background-color: #99ff991f; +} + +div.note p:last-child { + margin-bottom: 0; +} + +div.warning { + padding: 0.7rem 1rem; + margin: 1rem 0.2rem; + border: 2px solid orange; + border-radius: 5px; + background-color: #ffa50022; +} + +div.warning p:last-child { + margin-bottom: 0; +} + +div.warning h2 { + margin-top: 0rem; +} + +form.subscribe { + margin: 1rem; +} + +div.subscribe-fields { + display: flex; +} + +form.subscribe input { + padding: 0.5rem; + border: 1px solid #e5e5e5; +} + +form.subscribe input[type="email"] { + flex: 1; +} + +form.subscribe input[type="submit"] { + padding: 0.25rem 0.5rem; + cursor: pointer; +} + +/* Asides */ +aside.post_aside { + font-style: italic; + padding: 0rem 1rem 0rem; + margin: 0.8rem 0; + border-left: 0.1rem solid #e5e5e5; + border-right: 0.1rem solid #e5e5e5; +} + +details summary { + cursor: pointer; +} + +details summary h3, +details summary h4, +details summary h5, +details summary h6 { + display: inline; +} + +.gh-repo-box { + border: 1px solid #d1d5da; + border-radius: 3px; + padding: 16px; + margin-top: 0.5rem; + color: #586069; + font-size: 80%; +} + +.gh-repo-box .repo-link { + color: #0366d6; + font-weight: 600; + font-size: 120%; +} + +.gh-repo-box .subtitle { + margin-bottom: 16px; +} + +.gh-repo-box .stars-forks { + margin-bottom: 0; +} + +.gh-repo-box .stars-forks a { + color: #586069; +} + +.gh-repo-box .stars-forks a:hover { + color: #0366d6; + text-decoration: none; +} + +.gh-repo-box .stars-forks svg { + vertical-align: text-bottom; + fill: currentColor; +} + +.gh-repo-box .stars { + display: inline-block; +} + +.gh-repo-box .forks { + display: inline-block; + margin-left: 16px; +} + +.gh-repo-box .sponsor { + display: inline-block; + margin-left: 16px; +} + +.hidden { + display: none; +} + +.toc-comments-link { + margin-top: 0.5rem; +} + +h5 { + font-style: italic; + font-size: 0.9rem; +} +.gray { + color: gray; +} + +a strong { + color: #268bd2; +} + +.right-to-left { + direction: rtl; + font-family: Vazir; +} + +.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: 0.5rem; +} + +.giscus { + margin-top: 1.5rem; +} + +img { + background-color: white; +} + +/* Manual switch between dark and light mode */ + +.theme-switch { + margin-bottom: 1rem; + + @media (min-width: 80rem) { + position: fixed; + left: 2rem; + bottom: 2rem; + margin-bottom: 0rem; + } +} + +.light-switch { + @mixin light-switch-light { + // icon: https://icons.getbootstrap.com/icons/moon-fill/ (MIT licensed) + background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='%23004' class='bi bi-moon' viewBox='0 0 16 16'%3E%3Cpath fill-rule='evenodd' d='M14.53 10.53a7 7 0 0 1-9.058-9.058A7.003 7.003 0 0 0 8 15a7.002 7.002 0 0 0 6.53-4.47z'/%3E%3C/svg%3E"); + } + + @mixin light-switch-dark { + // icon: https://icons.getbootstrap.com/icons/brightness-high-fill/ (MIT licensed) + background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='%23ff9' class='bi bi-brightness-high-fill' viewBox='0 0 16 16'%3E%3Cpath d='M12 8a4 4 0 1 1-8 0 4 4 0 0 1 8 0zM8 0a.5.5 0 0 1 .5.5v2a.5.5 0 0 1-1 0v-2A.5.5 0 0 1 8 0zm0 13a.5.5 0 0 1 .5.5v2a.5.5 0 0 1-1 0v-2A.5.5 0 0 1 8 13zm8-5a.5.5 0 0 1-.5.5h-2a.5.5 0 0 1 0-1h2a.5.5 0 0 1 .5.5zM3 8a.5.5 0 0 1-.5.5h-2a.5.5 0 0 1 0-1h2A.5.5 0 0 1 3 8zm10.657-5.657a.5.5 0 0 1 0 .707l-1.414 1.415a.5.5 0 1 1-.707-.708l1.414-1.414a.5.5 0 0 1 .707 0zm-9.193 9.193a.5.5 0 0 1 0 .707L3.05 13.657a.5.5 0 0 1-.707-.707l1.414-1.414a.5.5 0 0 1 .707 0zm9.193 2.121a.5.5 0 0 1-.707 0l-1.414-1.414a.5.5 0 0 1 .707-.707l1.414 1.414a.5.5 0 0 1 0 .707zM4.464 4.465a.5.5 0 0 1-.707 0L2.343 3.05a.5.5 0 1 1 .707-.707l1.414 1.414a.5.5 0 0 1 0 .708z'/%3E%3C/svg%3E"); + } + + display: inline-block; + @include light-switch-light(); + + background-repeat: no-repeat; + width: 2rem; + height: 2rem; + cursor: pointer; + opacity: 0.6; + + &:hover { + transform: scale(1.3); + transition: 200ms ease-out; + opacity: 1; + } + + [data-theme="dark"] & { + @include light-switch-dark(); + } + + @media (prefers-color-scheme: dark) { + @include light-switch-dark(); + + [data-theme="light"] & { + @include light-switch-light(); + } + } +} + +/* Clear theme override and go back to system theme */ + +.light-switch-reset { + @mixin light-switch-reset-light { + // icon: https://icons.getbootstrap.com/icons/x-circle-fill/ (MIT licensed) + background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='%23666' class='bi bi-x-circle' viewBox='0 0 16 16'%3E%3Cpath d='M8 15A7 7 0 1 1 8 1a7 7 0 0 1 0 14zm0 1A8 8 0 1 0 8 0a8 8 0 0 0 0 16z'/%3E%3Cpath d='M4.646 4.646a.5.5 0 0 1 .708 0L8 7.293l2.646-2.647a.5.5 0 0 1 .708.708L8.707 8l2.647 2.646a.5.5 0 0 1-.708.708L8 8.707l-2.646 2.647a.5.5 0 0 1-.708-.708L7.293 8 4.646 5.354a.5.5 0 0 1 0-.708z'/%3E%3C/svg%3E"); + } + + @mixin light-switch-reset-dark { + // icon: https://icons.getbootstrap.com/icons/x-circle-fill/ (MIT licensed) + background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='%23999' class='bi bi-x-circle' viewBox='0 0 16 16'%3E%3Cpath d='M8 15A7 7 0 1 1 8 1a7 7 0 0 1 0 14zm0 1A8 8 0 1 0 8 0a8 8 0 0 0 0 16z'/%3E%3Cpath d='M4.646 4.646a.5.5 0 0 1 .708 0L8 7.293l2.646-2.647a.5.5 0 0 1 .708.708L8.707 8l2.647 2.646a.5.5 0 0 1-.708.708L8 8.707l-2.646 2.647a.5.5 0 0 1-.708-.708L7.293 8 4.646 5.354a.5.5 0 0 1 0-.708z'/%3E%3C/svg%3E"); + } + + @include light-switch-reset-light(); + vertical-align: bottom; + margin-left: 0.5rem; + background-repeat: no-repeat; + width: 2rem; + height: 2rem; + cursor: pointer; + opacity: 0.6; + + display: none; + [data-theme="light"] & { + display: inline-block; + } + [data-theme="dark"] & { + @include light-switch-reset-dark(); + display: inline-block; + } + + @media (min-width: 80rem) { + position: fixed; + left: 4.5rem; + bottom: 2rem; + } + + &:hover { + transform: scale(1.1); + transition: 200ms ease-out; + opacity: 1; + } +} diff --git a/blog/static/css/edition-2/main.css b/blog/static/css/edition-2/main.css deleted file mode 100644 index 32587de6..00000000 --- a/blog/static/css/edition-2/main.css +++ /dev/null @@ -1,464 +0,0 @@ -h1 code, h2 code, h3 code, h4 code, h5 code, h6 code { - padding: 0; - color: #a0565c; - font-size: 95%; - background-color: inherit; -} - -.masthead-title { - font-size: 1.25rem; - display: inline; -} - -.masthead p { - font-size: 1.25rem; - display: inline; - margin: 0; - margin-left: 1rem; - padding: 0; - line-height: 1; -} - -.front-page-introduction { - margin-bottom: 2rem; -} - -.navigation { - float: right; -} - -.navigation img { - height: 1em; - vertical-align: baseline; - display: inline-block; - margin: 0; - padding: 0; - border-radius: 0; -} - -main img { - max-width: 100%; - margin: auto; -} - -.post { - margin-bottom: 2em; -} - -.post:last-child { - margin-bottom: 0em; -} - -.frontpage-section { - margin-bottom: 2rem; -} - -.posts { - padding: 1.5rem 1rem 0.5rem 1rem; - border-radius: 10px; - margin-bottom: 2rem; - margin-left: -0.5rem; - margin-right: -0.5rem; -} - -.posts.neutral { - border: 2px solid #999; -} - -.posts.subscribe { - border: 2px solid #aaa; -} - -.posts.edition-1 { - border: 2px solid #aaa; - background-color: #99ff0022; -} - -.posts.bare-bones { - border: 2px solid #66f; -} - -.posts.memory-management { - border: 2px solid #fc0 -} - -.posts.interrupts { - border: 2px solid #f66; -} - -.posts.multitasking { - border: 2px solid #556b2f; -} - -.posts hr { - margin: 2rem 0; -} - -.post-summary { - margin-bottom: 1rem; -} - -.post-summary p { - display: inline; -} - -.read-more { - margin-left: 5px; -} - -.no-translation { - margin-top: .3rem; - color: #999999; -} - -.post-category { - margin-right: 0.5rem; - text-transform: uppercase; - font-size: 0.8rem; - text-align: right; -} - -.post-category.bare-bones { - color: #55d; -} - -.post-category.memory-management { - color: #990; -} - -.post-category.interrupts { - color: #f33; -} - -.post-category.multitasking { - color: #556b2f; -} - -.post-footer-support { - margin-top: 2rem; -} - -.PageNavigation { - font-size: 0.9em; - display: table; - width: 100%; - overflow: hidden; -} - -.PageNavigation a { - display: table-cell; -} - -.PageNavigation .previous { - text-align: left; -} - -.PageNavigation .next { - text-align: right; -} - -footer.footer { - margin-top: 1rem; - margin-bottom: 1rem; -} - -.footnotes { - font-size: 85%; -} - -.footnotes li { - margin-bottom: 1rem; -} - -sup, sub { - line-height: 0; -} - -a.anchorjs-link:hover { - text-decoration: none; -} - -#toc-aside { - display: none; -} - -#toc-inline summary { - margin-bottom: .2rem; -} - -aside#all-posts-link { - font-size: 90%; - margin-top: 0.5rem; -} - -@media (min-width: 80rem) { - #toc-inline { - display: none; - } - - #toc-aside { - display: block; - width: 12rem; - position: sticky; - float: left; - top: 3.5rem; - margin-top: -4rem; - margin-left: -15rem; - font-size: 90%; - line-height: 1.2; - } - - #toc-aside li > a, #toc-aside h2 { - opacity: .5; - transition: opacity .5s; - } - - #toc-aside:hover li > a, #toc-aside:hover h2 { - opacity: 1; - } - - #toc-aside li.active > a { - font-weight: bold; - } - - #toc-aside h2 { - font-size: 110%; - margin-bottom: .2rem; - } - - #toc-aside ol { - margin: 0 0 .2rem 0; - padding: 0 0 0 1rem; - list-style:none; - } - - #toc-aside ol li a:before { - content: ""; - border-color: transparent #008eef; - border-style: solid; - border-width: 0.35em 0 0.35em 0.45em; - display: block; - height: 0; - width: 0; - left: -1em; - top: 0.9em; - position: relative; - } - - #toc-aside.coarse li ol { - display: none; - } - - aside.page-aside-right { - position: absolute; - min-width: 11rem; - max-width: 17rem; - top: 4rem; - margin-left: 45rem; - margin-right: 2rem; - font-size: 90%; - } - - aside.page-aside-right .block { - margin-bottom: 1.5rem; - } - - aside.page-aside-right h2 { - font-size: 110%; - margin-bottom: .2rem; - } - - aside.page-aside-right ul { - margin: 0 0 .2rem 0; - padding: 0 0 0 1rem; - } - - aside.page-aside-right ul li { - margin-top: .5rem; - } - - #language-selector li { - margin-top: 0; - } - - aside#all-posts-link { - position: fixed; - top: 1.25rem; - margin-top: 0; - margin-left: -15rem; - } -} - -aside.page-aside-right time { - color: #9a9a9a; -} - -a code { - color: #268bd2; -} - -a.zola-anchor { - opacity: 0; - position: absolute; - margin-left: -1.5em; - padding-right: 1em; - font-size: 0.6em; - vertical-align: baseline; - line-height: 2em; -} - -:hover>a.zola-anchor { - opacity: 1; - text-decoration: none; -} - -a.zola-anchor:hover { - text-decoration: none; -} - -div.note { - padding: .7rem 1rem; - margin: 1rem .2rem; - border: 2px solid #6ad46a; - border-radius: 5px; - background-color: #99ff991f; -} - -div.note p:last-child { - margin-bottom: 0; -} - -div.warning { - padding: .7rem 1rem; - margin: 1rem .2rem; - border: 2px solid orange; - border-radius: 5px; - background-color: #ffa50022; -} - -div.warning p:last-child { - margin-bottom: 0; -} - -form.subscribe { - margin: 1rem; -} - -div.subscribe-fields { - display: flex; -} - -form.subscribe input { - padding: .5rem; - border: 1px solid #e5e5e5; -} - -form.subscribe input[type=email] { - flex: 1; -} - -form.subscribe input[type=submit] { - padding: .25rem .5rem; - cursor: pointer; -} - -/* Asides */ -aside.post_aside { - font-style: italic; - padding: 0rem 1rem 0rem; - margin: .8rem 0; - border-left: .1rem solid #e5e5e5; - border-right: .1rem solid #e5e5e5; -} - -details summary { - cursor: pointer; -} - -details summary h3, details summary h4, details summary h5, details summary h6 { - display: inline; -} - -.gh-repo-box { - border: 1px solid #d1d5da; - border-radius: 3px; - padding: 16px; - margin-top: 0.5rem; - color: #586069; - font-size: 80%; -} - -.gh-repo-box .repo-link { - color: #0366d6; - font-weight: 600; - font-size: 120%; -} - -.gh-repo-box .subtitle { - margin-bottom: 16px; -} - -.gh-repo-box .stars-forks { - margin-bottom: 0; -} - -.gh-repo-box .stars-forks a { - color: #586069; -} - -.gh-repo-box .stars-forks a:hover { - color: #0366d6; - text-decoration: none; -} - -.gh-repo-box .stars-forks svg { - vertical-align: text-bottom; - fill: currentColor; -} - -.gh-repo-box .stars { - display: inline-block; -} - -.gh-repo-box .forks { - display: inline-block; - margin-left: 16px; -} - -.gh-repo-box .sponsor { - display: inline-block; - margin-left: 16px; -} - -.hidden { - display: none; -} - -.toc-comments-link { - margin-top: .5rem; -} - -h5 { - font-style: italic; - font-size: 0.9rem; -} -.gray { - color: gray; -} - -a strong { - color: #268bd2; -} - -.right-to-left { - direction: rtl; - font-family: Vazir; -} - -.left-to-right, .right-to-left pre, .right-to-left table, .right-to-left[id="toc-aside"] { - direction: ltr; -} - -.status-update-list li { - margin-bottom: .5rem; -} diff --git a/blog/static/css/edition-2/poole.css b/blog/static/css/edition-2/poole.css deleted file mode 100644 index 0856c099..00000000 --- a/blog/static/css/edition-2/poole.css +++ /dev/null @@ -1,406 +0,0 @@ -/* - * ___ - * /\_ \ - * _____ ___ ___\//\ \ __ - * /\ '__`\ / __`\ / __`\\ \ \ /'__`\ - * \ \ \_\ \/\ \_\ \/\ \_\ \\_\ \_/\ __/ - * \ \ ,__/\ \____/\ \____//\____\ \____\ - * \ \ \/ \/___/ \/___/ \/____/\/____/ - * \ \_\ - * \/_/ - * - * Designed, built, and released under MIT license by @mdo. Learn more at - * https://github.com/poole/poole. - */ - - -/* - * Contents - * - * Body resets - * Custom type - * Messages - * Container - * Masthead - * Posts and pages - * Pagination - * Reverse layout - * Themes - */ - - -/* - * Body resets - * - * Update the foundational and global aspects of the page. - */ - -* { - -webkit-box-sizing: border-box; - -moz-box-sizing: border-box; - box-sizing: border-box; -} - -html, -body { - margin: 0; - padding: 0; -} - -html { - font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; - line-height: 1.5; -} - -body { - color: #515151; - background-color: #fff; - -webkit-text-size-adjust: 100%; - -ms-text-size-adjust: 100%; -} - -/* No `:visited` state is required by default (browsers will use `a`) */ -a { - color: #268bd2; - text-decoration: none; -} -/* `:focus` is linked to `:hover` for basic accessibility */ -a:hover, -a:focus { - text-decoration: underline; -} - -/* Headings */ -h1, h2, h3, h4, h5, h6 { - margin-bottom: .5rem; - font-weight: bold; - line-height: 1.25; - color: #313131; - text-rendering: optimizeLegibility; -} -h1 { - font-size: 2rem; -} -h2 { - margin-top: 1rem; - font-size: 1.5rem; -} -h3 { - margin-top: 1.5rem; - font-size: 1.25rem; -} -h4, h5, h6 { - margin-top: 1rem; - font-size: 1rem; -} - -/* Body text */ -p { - margin-top: 0; - margin-bottom: 1rem; -} - -strong { - color: #303030; -} - - -/* Lists */ -ul, ol, dl { - margin-top: 0; - margin-bottom: 1rem; -} - -/* Nested lists */ -li ul, li ol, li dl { - margin-bottom: 0; -} - -li ul + p, li ol + p, li dl + p { - margin-top: 1rem; -} - -dt { - font-weight: bold; -} -dd { - margin-bottom: .5rem; -} - -/* Misc */ -hr { - position: relative; - margin: 1.5rem 0; - border: 0; - border-top: 1px solid #eee; - border-bottom: 1px solid #fff; -} - -abbr { - font-size: 85%; - font-weight: bold; - color: #555; - text-transform: uppercase; -} -abbr[title] { - cursor: help; - border-bottom: 1px dotted #e5e5e5; -} - -/* Code */ -code, -pre { - font-family: Menlo, Monaco, Consolas, monospace -} -code { - padding: .25em .5em; - font-size: 85%; - color: #bf616a; - background-color: #f9f9f9; - border-radius: 3px; -} -pre { - display: block; - margin-top: 0; - margin-bottom: 1rem; - padding: .5rem; - font-size: .85rem; - line-height: 1.4; - white-space: pre; - overflow: auto; - word-wrap: normal; - background-color: #f9f9f9; -} -pre code { - padding: 0; - font-size: 100%; - color: inherit; - background-color: transparent; -} -.highlight { - margin-bottom: 1rem; - border-radius: 4px; -} -.highlight pre { - margin-bottom: 0; -} - -/* Quotes */ -blockquote { - padding: .5rem 1rem; - margin: .8rem 0; - color: #7a7a7a; - border-left: .25rem solid #e5e5e5; -} -blockquote p:last-child { - margin-bottom: 0; -} -@media (min-width: 30rem) { - blockquote { - padding-right: 5rem; - padding-left: 1.25rem; - } -} - -img { - display: block; - margin: 0 0 1rem; - border-radius: 5px; - max-width: 100%; - color: grey; - font-style: italic; -} - -/* Tables */ -table { - margin-bottom: 1rem; - width: 100%; - border: 1px solid #e5e5e5; - border-collapse: collapse; -} -td, -th { - padding: .25rem .5rem; - border: 1px solid #e5e5e5; -} -tbody tr:nth-child(odd) td, -tbody tr:nth-child(odd) th { - background-color: #f9f9f9; -} - - -/* - * Custom type - * - * Extend paragraphs with `.lead` for larger introductory text. - */ - -.lead { - font-size: 1.25rem; - font-weight: 300; -} - - -/* - * Messages - * - * Show alert messages to users. You may add it to single elements like a `

    `, - * or to a parent if there are multiple elements to show. - */ - -.message { - margin-bottom: 1rem; - padding: 1rem; - color: #717171; - background-color: #f9f9f9; -} - - -/* - * Container - * - * Center the page content. - */ - -.container { - max-width: 45rem; - padding-left: 1rem; - padding-right: 1rem; - margin-left: auto; - margin-right: auto; -} - - -/* - * Masthead - * - * Super small header above the content for site name and short description. - */ - -.masthead { - padding-top: 1rem; - padding-bottom: 1rem; - margin-bottom: 1rem; -} -.masthead-title { - margin-top: 0; - margin-bottom: 0; - color: #505050; -} -.masthead-title a { - color: #505050; -} -.masthead small { - font-size: 75%; - font-weight: 400; - color: #c0c0c0; - letter-spacing: 0; -} - - -/* - * Posts and pages - * - * Each post is wrapped in `.post` and is used on default and post layouts. Each - * page is wrapped in `.page` and is only used on the page layout. - */ - -.page { - margin-bottom: 4em; -} - -/* Blog post or page title */ -.page-title, -.post-title, -.post-title a { - color: #303030; -} -.page-title, -.post-title { - margin-top: 0; -} - -/* Meta data line below post title */ -.post-date { - display: block; - margin-top: -.5rem; - margin-bottom: 1rem; - color: #9a9a9a; -} - -/* Related posts */ -.related { - padding-top: 2rem; - padding-bottom: 2rem; - border-top: 1px solid #eee; -} -.related-posts { - padding-left: 0; - list-style: none; -} -.related-posts h3 { - margin-top: 0; -} -.related-posts li small { - font-size: 75%; - color: #999; -} -.related-posts li a:hover { - color: #268bd2; - text-decoration: none; -} -.related-posts li a:hover small { - color: inherit; -} - - -/* - * Pagination - * - * Super lightweight (HTML-wise) blog pagination. `span`s are provide for when - * there are no more previous or next posts to show. - */ - -.pagination { - overflow: hidden; /* clearfix */ - margin-left: -1rem; - margin-right: -1rem; - font-family: "PT Sans", Helvetica, Arial, sans-serif; - color: #ccc; - text-align: center; -} - -/* Pagination items can be `span`s or `a`s */ -.pagination-item { - display: block; - padding: 1rem; - border: 1px solid #eee; -} -.pagination-item:first-child { - margin-bottom: -1px; -} - -/* Only provide a hover state for linked pagination items */ -a.pagination-item:hover { - background-color: #f5f5f5; -} - -@media (min-width: 30rem) { - .pagination { - margin: 3rem 0; - } - .pagination-item { - float: left; - width: 50%; - } - .pagination-item:first-child { - margin-bottom: 0; - border-top-left-radius: 4px; - border-bottom-left-radius: 4px; - } - .pagination-item:last-child { - margin-left: -1px; - border-top-right-radius: 4px; - border-bottom-right-radius: 4px; - } -} diff --git a/blog/static/js/edition-2/main.js b/blog/static/js/edition-2/main.js index dfa1dd4d..f4cb57c2 100644 --- a/blog/static/js/edition-2/main.js +++ b/blog/static/js/edition-2/main.js @@ -1,17 +1,23 @@ -window.onload = function() { - var container = document.querySelector('#toc-aside'); - +window.onload = function () { + let container = document.querySelector('#toc-aside'); if (container != null) { resize_toc(container); toc_scroll_position(container); - window.onscroll = function() { toc_scroll_position(container) }; + window.onscroll = function () { toc_scroll_position(container) }; + } + + let theme = localStorage.getItem("theme"); + if (theme != null) { + setTimeout(() => { + set_giscus_theme(theme) + }, 500); } } function resize_toc(container) { - var containerHeight = container.clientHeight; + let containerHeight = container.clientHeight; - var resize = function() { + let resize = function () { if (containerHeight > document.documentElement.clientHeight - 100) { container.classList.add('coarse'); } else { @@ -20,8 +26,8 @@ function resize_toc(container) { }; resize(); - var resizeId; - window.onresize = function() { + let resizeId; + window.onresize = function () { clearTimeout(resizeId); resizeId = setTimeout(resize, 300); }; @@ -32,7 +38,6 @@ function toc_scroll_position(container) { // skip computation if ToC is not visible return; } - var items = container.querySelectorAll("li") // remove active class for all items for (item of container.querySelectorAll("li")) { @@ -40,15 +45,15 @@ function toc_scroll_position(container) { } // look for active item - var site_offset = document.documentElement.scrollTop; - var current_toc_item = null; + let site_offset = document.documentElement.scrollTop; + let current_toc_item = null; for (item of container.querySelectorAll("li")) { if (item.offsetParent === null) { // skip items that are not visible continue; } - var anchor = item.firstElementChild.getAttribute("href"); - var heading = document.querySelector(anchor); + let anchor = item.firstElementChild.getAttribute("href"); + let heading = document.querySelector(anchor); if (heading.offsetTop <= (site_offset + document.documentElement.clientHeight / 3)) { current_toc_item = item; } else { @@ -61,3 +66,35 @@ function toc_scroll_position(container) { current_toc_item.classList.add("active"); } } + +function toggle_lights() { + if (document.documentElement.getAttribute("data-theme") === "dark") { + set_theme("light") + } else if (document.documentElement.getAttribute("data-theme") === "light") { + set_theme("dark") + } else { + set_theme(window.matchMedia("(prefers-color-scheme: dark)").matches ? "light" : "dark") + } +} + +function set_theme(theme) { + document.documentElement.setAttribute("data-theme", theme) + set_giscus_theme(theme) + localStorage.setItem("theme", theme) +} + +function clear_theme_override() { + document.documentElement.removeAttribute("data-theme"); + set_giscus_theme("preferred_color_scheme") + localStorage.removeItem("theme") +} + +function set_giscus_theme(theme) { + let comment_form = document.querySelector("iframe.giscus-frame"); + if (comment_form != null) { + comment_form.contentWindow.postMessage({ + giscus: { setConfig: { theme: theme } } + }, "https://giscus.app") + } +} + diff --git a/blog/templates/edition-1/base.html b/blog/templates/edition-1/base.html index f2ee6390..c553b675 100644 --- a/blog/templates/edition-1/base.html +++ b/blog/templates/edition-1/base.html @@ -40,21 +40,7 @@ - - - + diff --git a/blog/templates/edition-2/base.html b/blog/templates/edition-2/base.html index 46301639..f5a74c95 100644 --- a/blog/templates/edition-2/base.html +++ b/blog/templates/edition-2/base.html @@ -6,17 +6,24 @@ + {% if current_url %} {% endif %} - + + {% block title %}{% endblock title %} (Second Edition) @@ -34,6 +41,11 @@ +

    +
    +
    +
    +
    {% block toc_aside %}{% endblock toc_aside %}
    {% block main %}{% endblock main %}
    @@ -44,27 +56,14 @@
    - - - + diff --git a/blog/templates/edition-2/extra.html b/blog/templates/edition-2/extra.html index c098e58c..f07c219f 100644 --- a/blog/templates/edition-2/extra.html +++ b/blog/templates/edition-2/extra.html @@ -16,7 +16,7 @@ {% block after_main %}
    -

    Comments

    - {{ macros::utterances() }} +

    Comments

    + {{ snippets::giscus(search_term=page.title ~ " (Extra Post)") }}
    {% endblock after_main %} diff --git a/blog/templates/edition-2/index.html b/blog/templates/edition-2/index.html index 6084311a..2c6246a3 100644 --- a/blog/templates/edition-2/index.html +++ b/blog/templates/edition-2/index.html @@ -72,14 +72,14 @@ {% if section.translations -%}

    Other Languages

    -
    {%- endif %} diff --git a/blog/templates/edition-2/macros.html b/blog/templates/edition-2/macros.html index 3db67187..66cd305e 100644 --- a/blog/templates/edition-2/macros.html +++ b/blog/templates/edition-2/macros.html @@ -17,7 +17,7 @@ {{ post.summary | safe }} {{ trans(key="readmore", lang=lang) | safe }} - {%- if lang and not_translated and lang != config.default_language -%} + {%- if lang and not_translated and lang != config.extra.default_language -%} diff --git a/blog/templates/edition-2/page.html b/blog/templates/edition-2/page.html index af9fe75e..f7000823 100644 --- a/blog/templates/edition-2/page.html +++ b/blog/templates/edition-2/page.html @@ -4,6 +4,7 @@ {% import "snippets.html" as snippets %} {% block title %}{{ page.title }} | {{ config.title }}{% endblock title %} + {% block header %} {% if lang != "en" -%} @@ -96,39 +97,39 @@

    {{ trans(key="comments", lang=lang) }}

    + {% if page.extra.comments_search_term %} + {% set search_term=page.extra.comments_search_term %} + {% elif page.lang != "en" %} + {% set translations = page.translations | filter(attribute="lang", value="en") %} + {% set original = translations.0 %} + {% set search_term=original.title ~ " (" ~ page.lang ~ ")" %} + {% else %} + {% set search_term=page.title %} + {% endif %} + {{ snippets::giscus(search_term=search_term) }} + {%- if page.lang != "en" %}

    {{ trans(key="comments_notice", lang=lang) }}

    {% endif %} - - {{ macros::utterances() }}
    {% endblock main %} diff --git a/blog/templates/rss.xml b/blog/templates/rss.xml index b4659b91..bed52436 100644 --- a/blog/templates/rss.xml +++ b/blog/templates/rss.xml @@ -4,7 +4,7 @@ {{ config.base_url | safe }} {{ config.description }} Zola - {{ config.default_language }} + {{ lang }} {{ last_updated | date(format="%a, %d %b %Y %H:%M:%S %z") }} {% for page in pages %} diff --git a/blog/templates/snippets.html b/blog/templates/snippets.html index e39465d7..4a9dde1f 100644 --- a/blog/templates/snippets.html +++ b/blog/templates/snippets.html @@ -9,3 +9,36 @@ Thank you!

    {% endmacro support %} + +{% macro giscus(search_term) %} + {% if search_term is number %} + {% set discussion_url = "https://github.com/phil-opp/blog_os/discussions/" ~ search_term %} + {% else %} + {% set search_term_encoded = `"` ~ search_term ~ `"` | urlencode %} + {% set discussion_url = `https://github.com/phil-opp/blog_os/discussions/categories/post-comments?discussions_q=` ~ search_term_encoded %} + {% endif %} + +

    + Do you have a problem, want to share feedback, or discuss further ideas? Feel free to leave a comment here! Please stick to English and follow Rust's code of conduct. This comment thread directly maps to a discussion on GitHub, so you can also comment there if you prefer. +

    + +
    + +