Compare commits
7 Commits
5217bc27b7
...
threads
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c9ceb8731c | ||
|
|
a02b4d7284 | ||
|
|
d3f19e1e92 | ||
|
|
4d21a7fc33 | ||
|
|
a90e673e87 | ||
|
|
c4816f0256 | ||
|
|
16356ca821 |
@@ -1,4 +1,4 @@
|
||||
name: Blog
|
||||
name: Build Site
|
||||
|
||||
on:
|
||||
push:
|
||||
@@ -20,7 +20,9 @@ jobs:
|
||||
- uses: actions/checkout@v1
|
||||
|
||||
- name: 'Download Zola'
|
||||
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
|
||||
run: curl -sL https://github.com/getzola/zola/releases/download/v0.9.0/zola-v0.9.0-x86_64-unknown-linux-gnu.tar.gz | tar zxv
|
||||
- name: "Install Python Tools"
|
||||
run: python -m pip install --upgrade pip setuptools wheel
|
||||
- name: 'Install Python Libraries'
|
||||
run: python -m pip install --user -r requirements.txt
|
||||
working-directory: "blog"
|
||||
@@ -46,11 +48,12 @@ jobs:
|
||||
- uses: actions/checkout@v1
|
||||
|
||||
- name: 'Download Zola'
|
||||
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
|
||||
run: curl -sL https://github.com/getzola/zola/releases/download/v0.9.0/zola-v0.9.0-x86_64-unknown-linux-gnu.tar.gz | tar zxv
|
||||
|
||||
- name: "Run zola check"
|
||||
run: ../zola check
|
||||
working-directory: "blog"
|
||||
continue-on-error: true
|
||||
|
||||
check_spelling:
|
||||
name: "Check Spelling"
|
||||
@@ -59,16 +62,16 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@v1
|
||||
|
||||
- name: Typo Check
|
||||
uses: crate-ci/typos@v1.1.9
|
||||
with:
|
||||
files: blog
|
||||
- run: curl -L https://git.io/misspell | bash
|
||||
name: "Install misspell"
|
||||
- run: bin/misspell -error blog/content
|
||||
name: "Check for common typos"
|
||||
|
||||
deploy_site:
|
||||
name: "Deploy Generated Site"
|
||||
runs-on: ubuntu-latest
|
||||
needs: [build_site, check_spelling]
|
||||
if: github.ref == 'refs/heads/main' && (github.event_name == 'push' || github.event_name == 'schedule')
|
||||
if: github.ref == 'refs/heads/master' && (github.event_name == 'push' || github.event_name == 'schedule')
|
||||
|
||||
steps:
|
||||
- name: "Download Generated Site"
|
||||
@@ -82,7 +85,7 @@ jobs:
|
||||
ssh-keyscan github.com >> ~/.ssh/known_hosts
|
||||
ssh-agent -a $SSH_AUTH_SOCK > /dev/null
|
||||
ssh-add - <<< "$deploy_key"
|
||||
echo "SSH_AUTH_SOCK=$SSH_AUTH_SOCK" >> $GITHUB_ENV
|
||||
echo ::set-env name=SSH_AUTH_SOCK::$SSH_AUTH_SOCK
|
||||
env:
|
||||
SSH_AUTH_SOCK: /tmp/ssh_agent.sock
|
||||
deploy_key: ${{ secrets.DEPLOY_SSH_KEY }}
|
||||
33
.github/workflows/scheduled-builds.yml
vendored
@@ -1,33 +0,0 @@
|
||||
name: Build code on schedule
|
||||
|
||||
on:
|
||||
schedule:
|
||||
- cron: '40 1 * * *' # every day at 1:40
|
||||
|
||||
jobs:
|
||||
trigger-build:
|
||||
name: Trigger Build
|
||||
strategy:
|
||||
matrix:
|
||||
branch: [
|
||||
post-01,
|
||||
post-02,
|
||||
post-03,
|
||||
post-04,
|
||||
post-05,
|
||||
post-06,
|
||||
post-07,
|
||||
post-08,
|
||||
post-09,
|
||||
post-10,
|
||||
post-11,
|
||||
post-12,
|
||||
]
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Invoke workflow
|
||||
uses: benc-uk/workflow-dispatch@v1.1
|
||||
with:
|
||||
workflow: Code
|
||||
token: ${{ secrets.SCHEDULED_BUILDS_TOKEN }}
|
||||
ref: ${{ matrix.branch }}
|
||||
@@ -1,6 +1,6 @@
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
https://www.apache.org/licenses/
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
@@ -174,3 +174,28 @@
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "{}"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright {yyyy} {name of copyright owner}
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
|
||||
15
README.md
@@ -10,7 +10,7 @@ The code for each post lives in a separate git branch. This makes it possible to
|
||||
|
||||
**The code for the latest post is available [here][latest-post].**
|
||||
|
||||
[latest-post]: https://github.com/phil-opp/blog_os/tree/post-12
|
||||
[latest-post]: https://github.com/phil-opp/blog_os/tree/post-11
|
||||
|
||||
You can find the branch for each post by following the `(source code)` link in the [post list](#posts) below. The branches are named `post-XX` where `XX` is the post number, for example `post-03` for the _VGA Text Mode_ post or `post-07` for the _Hardware Interrupts_ post. For build instructions, see the Readme of the respective branch.
|
||||
|
||||
@@ -59,17 +59,10 @@ The goal of this project is to provide step-by-step tutorials in individual blog
|
||||
- [Allocator Designs](https://os.phil-opp.com/allocator-designs/)
|
||||
([source code](https://github.com/phil-opp/blog_os/tree/post-11))
|
||||
|
||||
**Multitasking**:
|
||||
|
||||
- [Async/Await](https://os.phil-opp.com/async-await/)
|
||||
([source code](https://github.com/phil-opp/blog_os/tree/post-12))
|
||||
|
||||
## First Edition Posts
|
||||
|
||||
The current version of the blog is already the second edition. The first edition is outdated and no longer maintained, but might still be useful. The posts of the first edition are:
|
||||
|
||||
<details><summary><i>Click to expand</i></summary>
|
||||
|
||||
**Bare Bones:**
|
||||
|
||||
- [A Minimal x86 Kernel](https://os.phil-opp.com/multiboot-kernel.html)
|
||||
@@ -112,15 +105,13 @@ The current version of the blog is already the second edition. The first edition
|
||||
- [Returning from Exceptions](https://os.phil-opp.com/returning-from-exceptions.html)
|
||||
([source code](https://github.com/phil-opp/blog_os/tree/returning_from_exceptions))
|
||||
|
||||
</details>
|
||||
|
||||
## License
|
||||
|
||||
This project, with exception of the `blog/content` folder, is licensed under either of
|
||||
|
||||
- Apache License, Version 2.0 ([LICENSE-APACHE](LICENSE-APACHE) or
|
||||
https://www.apache.org/licenses/LICENSE-2.0)
|
||||
- MIT license ([LICENSE-MIT](LICENSE-MIT) or https://opensource.org/licenses/MIT)
|
||||
http://www.apache.org/licenses/LICENSE-2.0)
|
||||
- MIT license ([LICENSE-MIT](LICENSE-MIT) or http://opensource.org/licenses/MIT)
|
||||
|
||||
at your option.
|
||||
|
||||
|
||||
@@ -2,13 +2,12 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
import io
|
||||
import urllib
|
||||
import datetime
|
||||
from github import Github
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
g = Github()
|
||||
|
||||
one_month_ago = datetime.datetime.now() - datetime.timedelta(days=32)
|
||||
one_month_ago = datetime.now() - timedelta(days=32)
|
||||
|
||||
def filter_date(issue):
|
||||
return issue.closed_at > one_month_ago
|
||||
@@ -22,20 +21,20 @@ def format_number(number):
|
||||
with io.open("templates/auto/recent-updates.html", 'w', encoding='utf8') as recent_updates:
|
||||
recent_updates.truncate()
|
||||
|
||||
relnotes_issues = g.search_issues("is:merged", repo="phil-opp/blog_os", type="pr", label="relnotes")[:100]
|
||||
recent_relnotes_issues = list(filter(filter_date, relnotes_issues))
|
||||
relnotes_issues = g.search_issues("is:merged", repo="phil-opp/blog_os", type="pr", label="relnotes")[:10]
|
||||
recent_relnotes_issues = filter(filter_date, relnotes_issues)
|
||||
|
||||
if len(recent_relnotes_issues) == 0:
|
||||
recent_updates.write(u"No notable updates recently.")
|
||||
else:
|
||||
recent_updates.write(u"<ul>\n")
|
||||
|
||||
for pr in sorted(recent_relnotes_issues, key=lambda issue: issue.closed_at, reverse=True):
|
||||
for pr in recent_relnotes_issues:
|
||||
link = '<a href="' + pr.html_url + '">' + pr.title + "</a> "
|
||||
iso_date = pr.closed_at.isoformat()
|
||||
readable_date = pr.closed_at.strftime("%b %d")
|
||||
datetime_str = '<time datetime="' + iso_date + '">' + readable_date + '</time>'
|
||||
recent_updates.write(u" <li>" + link + datetime_str + "</li>\n")
|
||||
datetime = '<time datetime="' + iso_date + '">' + readable_date + '</time>'
|
||||
recent_updates.write(u" <li>" + link + datetime + "</li>\n")
|
||||
|
||||
recent_updates.write(u"</ul>")
|
||||
|
||||
@@ -48,42 +47,3 @@ with io.open("templates/auto/stars.html", 'w', encoding='utf8') as stars:
|
||||
with io.open("templates/auto/forks.html", 'w', encoding='utf8') as forks:
|
||||
forks.truncate()
|
||||
forks.write(format_number(repo.forks_count))
|
||||
|
||||
|
||||
# query "This week in Rust OSDev posts"
|
||||
|
||||
lines = []
|
||||
year = 2020
|
||||
month = 4
|
||||
while True:
|
||||
url = "https://rust-osdev.com/this-month/" + str(year) + "-" + str(month).zfill(2) + "/"
|
||||
try:
|
||||
urllib.request.urlopen(url)
|
||||
except urllib.error.HTTPError as e:
|
||||
break
|
||||
|
||||
month_str = datetime.date(1900, month, 1).strftime('%B')
|
||||
|
||||
link = '<a href="' + url + '">This Month in Rust OSDev (' + month_str + " " + str(year) + ")</a> "
|
||||
lines.append(u" <li><b>" + link + "</b></li>\n")
|
||||
|
||||
month = month + 1
|
||||
if month > 12:
|
||||
month = 1
|
||||
year = year + 1
|
||||
|
||||
lines.reverse()
|
||||
|
||||
with io.open("templates/auto/status-updates.html", 'w', encoding='utf8') as status_updates:
|
||||
status_updates.truncate()
|
||||
|
||||
for line in lines:
|
||||
status_updates.write(line)
|
||||
|
||||
with io.open("templates/auto/status-updates-truncated.html", 'w', encoding='utf8') as status_updates:
|
||||
status_updates.truncate()
|
||||
|
||||
for index, line in enumerate(lines):
|
||||
if index == 5:
|
||||
break
|
||||
status_updates.write(line)
|
||||
|
||||
163
blog/config.toml
@@ -1,168 +1,13 @@
|
||||
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."
|
||||
|
||||
generate_feed = true
|
||||
feed_filename = "rss.xml"
|
||||
compile_sass = true
|
||||
minify_html = false
|
||||
|
||||
ignored_content = ["*/README.md", "LICENSE-CC-BY-NC"]
|
||||
|
||||
[markdown]
|
||||
highlight_code = true
|
||||
highlight_theme = "visual-studio-dark"
|
||||
smart_punctuation = true
|
||||
generate_rss = true
|
||||
|
||||
[link_checker]
|
||||
skip_prefixes = [
|
||||
"https://crates.io/crates", # see https://github.com/rust-lang/crates.io/issues/788
|
||||
"https://www.amd.com/system/files/TechDocs/", # seems to have problems with PDFs
|
||||
"https://developer.apple.com/library/archive/qa/qa1118/_index.html", # results in a 401 (I don't know why)
|
||||
"https://github.com", # rate limiting often leads to "Error 429 Too Many Requests"
|
||||
"https://www.linkedin.com/", # seems to send invalid HTTP status codes
|
||||
]
|
||||
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
|
||||
]
|
||||
ignored_content = ["*/README.md"]
|
||||
|
||||
[extra]
|
||||
subtitle = "Philipp Oppermann's blog"
|
||||
author = { name = "Philipp Oppermann" }
|
||||
default_language = "en"
|
||||
languages = ["en", "zh-CN", "zh-TW", "fr", "ja", "fa", "ru", "ko"]
|
||||
|
||||
[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"
|
||||
comments_notice = "Please leave your comments in English if possible."
|
||||
readmore = "read more »"
|
||||
not_translated = "(This post is not translated yet.)"
|
||||
translated_content = "Translated Content:"
|
||||
translated_content_notice = "This is a community translation of the <strong><a href=\"_original.permalink_\">_original.title_</a></strong> post. It might be incomplete, outdated or contain errors. Please report any issues!"
|
||||
translated_by = "Translation by"
|
||||
word_separator = "and"
|
||||
|
||||
# Chinese (simplified)
|
||||
[languages.zh-CN]
|
||||
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-CN.translations]
|
||||
lang_name = "Chinese (simplified)"
|
||||
toc = "目录"
|
||||
all_posts = "« 所有文章"
|
||||
comments = "评论"
|
||||
comments_notice = "请尽可能使用英语评论。"
|
||||
readmore = "更多 »"
|
||||
not_translated = "(该文章还没有被翻译。)"
|
||||
translated_content = "翻译内容:"
|
||||
translated_content_notice = "这是对原文章 <strong><a href=\"_original.permalink_\">_original.title_</a></strong> 的社区中文翻译。它可能不完整,过时或者包含错误。可以在 <a href=\"https://github.com/phil-opp/blog_os/issues/961\">这个 Issue</a> 上评论和提问!"
|
||||
translated_by = "翻译者:"
|
||||
word_separator = "和"
|
||||
|
||||
# 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 = "目錄"
|
||||
all_posts = "« 所有文章"
|
||||
comments = "評論"
|
||||
comments_notice = "請儘可能使用英語評論。"
|
||||
readmore = "更多 »"
|
||||
not_translated = "(該文章還沒有被翻譯。)"
|
||||
translated_content = "翻譯內容:"
|
||||
translated_content_notice = "這是對原文章 <strong><a href=\"_original.permalink_\">_original.title_</a></strong> 的社區中文翻譯。它可能不完整,過時或者包含錯誤。可以在 <a href=\"https://github.com/phil-opp/blog_os/issues/961\">這個 Issue</a> 上評論和提問!"
|
||||
translated_by = "翻譯者:"
|
||||
word_separator = "和"
|
||||
|
||||
# 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 = "« すべての記事へ"
|
||||
comments = "コメント"
|
||||
comments_notice = "可能な限りコメントは英語で残すようにしてください。"
|
||||
readmore = "もっと読む »"
|
||||
not_translated = "(この記事はまだ翻訳されていません。)"
|
||||
translated_content = "この記事は翻訳されたものです:"
|
||||
translated_content_notice = "この記事は<strong><a href=\"_original.permalink_\">_original.title_</a></strong>をコミュニティの手により翻訳したものです。そのため、翻訳が完全・最新でなかったり、原文にない誤りを含んでいる可能性があります。問題があれば<a href=\"https://github.com/phil-opp/blog_os/issues/906\">このissue</a>上で報告してください!"
|
||||
translated_by = "翻訳者:"
|
||||
word_separator = "及び"
|
||||
|
||||
# Persian
|
||||
[languages.fa]
|
||||
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.fa.translations]
|
||||
lang_name = "Persian"
|
||||
toc = "فهرست مطالب"
|
||||
all_posts = "« همه پستها"
|
||||
comments = "نظرات"
|
||||
comments_notice = "لطفا نظرات خود را در صورت امکان به انگلیسی بنویسید."
|
||||
readmore = "ادامهمطلب»"
|
||||
not_translated = "(.این پست هنوز ترجمه نشده است)"
|
||||
translated_content = "محتوای ترجمه شده:"
|
||||
translated_content_notice = "این یک ترجمه از جامعه کاربران برای پست <strong><a href=\"_original.permalink_\">_original.title_</a></strong> است. ممکن است ناقص، منسوخ شده یا دارای خطا باشد. لطفا هر گونه مشکل را در <a href=\"https://github.com/phil-opp/blog_os/issues/908\">این ایشو</a> گزارش دهید!"
|
||||
translated_by = "ترجمه توسط"
|
||||
word_separator = "و"
|
||||
|
||||
# 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 = "Это перевод сообщества поста <strong><a href=\"_original.permalink_\">_original.title_</a></strong>. Он может быть неполным, устаревшим или содержать ошибки. Пожалуйста, сообщайте о любых проблемах!"
|
||||
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 <strong><a href=\"_original.permalink_\">_original.title_</a></strong>. Il peut être incomplet, obsolète ou contenir des erreurs. Veuillez signaler les quelconques problèmes !"
|
||||
translated_by = "Traduit par : "
|
||||
word_separator = "et"
|
||||
|
||||
# Korean
|
||||
[languages.ko]
|
||||
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.ko.translations]
|
||||
lang_name = "Korean"
|
||||
toc = "목차"
|
||||
all_posts = "« 모든 게시글"
|
||||
comments = "댓글"
|
||||
comments_notice = "댓글은 가능하면 영어로 작성해주세요."
|
||||
readmore = "더 읽기 »"
|
||||
not_translated = "(아직 번역이 완료되지 않은 게시글입니다)"
|
||||
translated_content = "번역된 내용 : "
|
||||
translated_content_notice = "이것은 커뮤니티 멤버가 <strong><a href=\"_original.permalink_\">_original.title_</a></strong> 포스트를 번역한 글입니다. 부족한 설명이나 오류, 혹은 시간이 지나 더 이상 유효하지 않은 정보를 발견하시면 제보해주세요!"
|
||||
translated_by = "번역한 사람 : "
|
||||
word_separator = "와"
|
||||
|
||||
@@ -4,13 +4,13 @@ This folder contains the content for the _"Writing an OS in Rust"_ blog.
|
||||
|
||||
## License
|
||||
|
||||
This folder is licensed under a Creative Commons Attribution-NonCommercial 4.0 International License, available in [LICENSE-CC-BY-NC](LICENSE-CC-BY-NC) or under <https://creativecommons.org/licenses/by-nc/4.0/>.
|
||||
This folder is licensed under a Creative Commons Attribution-NonCommercial 4.0 International License, available in [LICENSE-CC-BY-NC](LICENSE-CC-BY-NC) or under <http://creativecommons.org/licenses/by-nc/4.0/>.
|
||||
|
||||
All _code examples_ between markdown code blocks denoted by three backticks (<code>\`\`\`</code>) are additionally licensed under either of
|
||||
|
||||
- Apache License, Version 2.0 ([LICENSE-APACHE](../../LICENSE-APACHE) or
|
||||
https://www.apache.org/licenses/LICENSE-2.0)
|
||||
- MIT license ([LICENSE-MIT](../../LICENSE-MIT) or https://opensource.org/licenses/MIT)
|
||||
http://www.apache.org/licenses/LICENSE-2.0)
|
||||
- MIT license ([LICENSE-MIT](../../LICENSE-MIT) or http://opensource.org/licenses/MIT)
|
||||
|
||||
at your option.
|
||||
|
||||
|
||||
@@ -1,13 +0,0 @@
|
||||
+++
|
||||
template = "edition-2/index.html"
|
||||
+++
|
||||
|
||||
<h1 style="visibility: hidden; height: 0px; margin: 0px; padding: 0px;">نوشتن یک سیستم عامل با راست</h1>
|
||||
|
||||
<div class="front-page-introduction right-to-left">
|
||||
|
||||
این مجموعه بلاگ یک سیستم عامل کوچک در [زبان برنامه نویسی Rust](https://www.rust-lang.org/) ایجاد می کند. هر پست یک آموزش کوچک است و شامل تمام کدهای مورد نیاز است ، بنابراین اگر دوست دارید می توانید آن را دنبال کنید. کد منبع نیز در [مخزن گیتهاب](https://github.com/phil-opp/blog_os) مربوطه موجود است.
|
||||
|
||||
اخرین پست: <!-- latest-post -->
|
||||
|
||||
</div>
|
||||
@@ -1,13 +0,0 @@
|
||||
+++
|
||||
template = "edition-2/index.html"
|
||||
+++
|
||||
|
||||
<h1 style="visibility: hidden; height: 0px; margin: 0px; padding: 0px;">Écrire un OS en Rust</h1>
|
||||
|
||||
<div class="front-page-introduction">
|
||||
|
||||
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 : <!-- latest-post -->
|
||||
|
||||
</div>
|
||||
@@ -1,13 +0,0 @@
|
||||
+++
|
||||
template = "edition-2/index.html"
|
||||
+++
|
||||
|
||||
<h1 style="visibility: hidden; height: 0px; margin: 0px; padding: 0px;">RustでOSを書く</h1>
|
||||
|
||||
<div class="front-page-introduction">
|
||||
|
||||
このブログシリーズでは、ちょっとしたオペレーティングシステムを[Rustプログラミング言語](https://www.rust-lang.org/)を使って作ります。それぞれの記事が小さなチュートリアルになっており、必要なコードも全て記事内に記されているので、一つずつ読み進めて行けば理解できるでしょう。対応した[Githubリポジトリ](https://github.com/phil-opp/blog_os)でソースコードを見ることもできます。
|
||||
|
||||
最新記事: <!-- latest-post -->
|
||||
|
||||
</div>
|
||||
@@ -1,14 +0,0 @@
|
||||
+++
|
||||
template = "edition-2/index.html"
|
||||
+++
|
||||
|
||||
<h1 style="visibility: hidden; height: 0px; margin: 0px; padding: 0px;">Rust로 OS 구현하기</h1>
|
||||
|
||||
<div class="front-page-introduction">
|
||||
|
||||
이 블로그 시리즈는 [Rust 프로그래밍 언어](https://www.rust-lang.org/)로 작은 OS를 구현하는 것을 주제로 합니다.
|
||||
각 포스트는 구현에 필요한 소스 코드를 포함한 작은 튜토리얼 형식으로 구성되어 있습니다. 소스 코드는 이 블로그의 [Github 저장소](https://github.com/phil-opp/blog_os)에서도 확인하실 수 있습니다.
|
||||
|
||||
최신 포스트: <!-- latest-post -->
|
||||
|
||||
</div>
|
||||
@@ -1,13 +0,0 @@
|
||||
+++
|
||||
template = "edition-2/index.html"
|
||||
+++
|
||||
|
||||
<h1 style="visibility: hidden; height: 0px; margin: 0px; padding: 0px;">Writing an OS in Rust</h1>
|
||||
|
||||
<div class="front-page-introduction">
|
||||
|
||||
This blog series creates a small operating system in the [Rust programming language](https://www.rust-lang.org/). Each post is a small tutorial and includes all needed code, so you can follow along if you like. The source code is also available in the corresponding [Github repository](https://github.com/phil-opp/blog_os).
|
||||
|
||||
Latest post: <!-- latest-post -->
|
||||
|
||||
</div>
|
||||
@@ -1,13 +0,0 @@
|
||||
+++
|
||||
template = "edition-2/index.html"
|
||||
+++
|
||||
|
||||
<h1 style="visibility: hidden; height: 0px; margin: 0px; padding: 0px;">Собственная операционная система на Rust</h1>
|
||||
|
||||
<div class="front-page-introduction">
|
||||
|
||||
Этот блог посвящен написанию маленькой операционной системы на [языке программирования Rust](https://www.rust-lang.org/). Каждый пост — это маленькое руководство, включающее в себя весь необходимый код, — вы сможете следовать ему, если пожелаете. Исходный код также доступен в соотвестующем [репозитории на Github](https://github.com/phil-opp/blog_os).
|
||||
|
||||
Последний пост: <!-- latest-post -->
|
||||
|
||||
</div>
|
||||
@@ -1,13 +0,0 @@
|
||||
+++
|
||||
template = "edition-2/index.html"
|
||||
+++
|
||||
|
||||
<h1 style="visibility: hidden; height: 0px; margin: 0px; padding: 0px;">用Rust写一个操作系统</h1>
|
||||
|
||||
<div class="front-page-introduction">
|
||||
|
||||
这个博客系列用[Rust编程语言](https://www.rust-lang.org/)编写了一个小操作系统。每篇文章都是一个小教程,并且包含了所有代码,你可以跟着一起学习。源代码也放在了[Github 仓库](https://github.com/phil-opp/blog_os)。
|
||||
|
||||
最新文章: <!-- latest-post -->
|
||||
|
||||
</div>
|
||||
@@ -1,13 +0,0 @@
|
||||
+++
|
||||
template = "edition-2/index.html"
|
||||
+++
|
||||
|
||||
<h1 style="visibility: hidden; height: 0px; margin: 0px; padding: 0px;">Writing an OS in Rust</h1>
|
||||
|
||||
<div class="front-page-introduction">
|
||||
|
||||
This blog series creates a small operating system in the [Rust programming language](https://www.rust-lang.org/). Each post is a small tutorial and includes all needed code, so you can follow along if you like. The source code is also available in the corresponding [Github repository](https://github.com/phil-opp/blog_os).
|
||||
|
||||
Latest post: <!-- latest-post -->
|
||||
|
||||
</div>
|
||||
@@ -1,5 +0,0 @@
|
||||
+++
|
||||
title = "First Edition"
|
||||
template = "edition-1/index.html"
|
||||
aliases = ["first-edition/index.html"]
|
||||
+++
|
||||
@@ -1,7 +0,0 @@
|
||||
+++
|
||||
title = "Handling Exceptions using naked Functions"
|
||||
sort_by = "weight"
|
||||
template = "edition-1/handling-exceptions-with-naked-fns.html"
|
||||
insert_anchor_links = "left"
|
||||
aliases = ["first-edition/extra/naked-exceptions/index.html"]
|
||||
+++
|
||||
@@ -1,527 +0,0 @@
|
||||
+++
|
||||
title = " یک باینری مستقل Rust"
|
||||
weight = 1
|
||||
path = "fa/freestanding-rust-binary"
|
||||
date = 2018-02-10
|
||||
|
||||
[extra]
|
||||
chapter = "Bare Bones"
|
||||
# Please update this when updating the translation
|
||||
translation_based_on_commit = "80136cc0474ae8d2da04f391b5281cfcda068c1a"
|
||||
# GitHub usernames of the people that translated this post
|
||||
translators = ["hamidrezakp", "MHBahrampour"]
|
||||
rtl = true
|
||||
+++
|
||||
|
||||
اولین قدم برای نوشتن سیستمعامل، ساخت یک باینری راست (کلمه: Rust) هست که به کتابخانه استاندارد نیازمند نباشد. این باعث میشود تا بتوانیم کد راست را بدون سیستمعامل زیرین، بر روی سخت افزار [bare metal] اجرا کنیم.
|
||||
|
||||
[bare metal]: https://en.wikipedia.org/wiki/Bare_machine
|
||||
|
||||
<!-- more -->
|
||||
|
||||
این بلاگ بصورت آزاد بر روی [گیتهاب] توسعه داده شده. اگر مشکل یا سوالی دارید، لطفاً آنجا یک ایشو باز کنید. همچنین میتوانید [در زیر] این پست کامنت بگذارید. سورس کد کامل این پست را میتوانید در بِرَنچ [`post-01`][post branch] پیدا کنید.
|
||||
|
||||
[گیتهاب]: https://github.com/phil-opp/blog_os
|
||||
[در زیر]: #comments
|
||||
<!-- fix for zola anchor checker (target is in template): <a id="comments"> -->
|
||||
[post branch]: https://github.com/phil-opp/blog_os/tree/post-01
|
||||
|
||||
<!-- toc -->
|
||||
|
||||
## مقدمه
|
||||
برای نوشتن هسته سیستمعامل، ما به کدی نیاز داریم که به هیچ یک از ویژگیهای سیستمعامل نیازی نداشته باشد. یعنی نمیتوانیم از نخها (ترجمه: Threads)، فایلها، حافظه هیپ (کلمه: Heap)، شبکه، اعداد تصادفی، ورودی استاندارد، یا هر ویژگی دیگری که نیاز به انتزاعات سیستمعامل یا سختافزار خاصی داشته، استفاده کنیم. منطقی هم به نظر میرسد، چون ما سعی داریم سیستمعامل و درایورهای خودمان را بنویسیم.
|
||||
|
||||
نداشتن انتزاعات سیستمعامل به این معنی هست که نمیتوانیم از بخش زیادی از [کتابخانه استاندارد راست] استفاده کنیم، اما هنوز بسیاری از ویژگیهای راست هستند که میتوانیم از آنها استفاده کنیم. به عنوان مثال، میتوانیم از [iterator] ها، [closure] ها، [pattern matching]، [option]، [result]، [string formatting] و البته [سیستم ownership] استفاده کنیم. این ویژگیها به ما امکان نوشتن هسته به طور رسا، سطح بالا و بدون نگرانی درباره [رفتار تعریف نشده] و [امنیت حافظه] را میدهند.
|
||||
|
||||
[option]: https://doc.rust-lang.org/core/option/
|
||||
[result]:https://doc.rust-lang.org/core/result/
|
||||
[کتابخانه استاندارد راست]: 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]: https://doc.rust-lang.org/book/ch04-00-understanding-ownership.html
|
||||
[رفتار تعریف نشده]: https://www.nayuki.io/page/undefined-behavior-in-c-and-cplusplus-programs
|
||||
[امنیت حافظه]: https://tonyarcieri.com/it-s-time-for-a-memory-safety-intervention
|
||||
|
||||
برای ساختن یک هسته سیستمعامل به زبان راست، باید فایل اجراییای بسازیم که بتواند بدون سیستمعامل زیرین اجرا بشود. چنین فایل اجرایی، فایل اجرایی مستقل (ترجمه: freestanding) یا فایل اجرایی “bare-metal” نامیده میشود.
|
||||
|
||||
این پست قدمهای لازم برای ساخت یک باینری مستقل راست و اینکه چرا این قدمها نیاز هستند را توضیح میدهد. اگر علاقهایی به خواندن کل توضیحات ندارید، میتوانید **[به قسمت خلاصه مراجعه کنید](#summary)**.
|
||||
|
||||
## غیر فعال کردن کتابخانه استاندارد
|
||||
به طور پیشفرض تمام کِرِیتهای راست، از [کتابخانه استاندارد] استفاده میکنند(لینک به آن دارند)، که به سیستمعامل برای قابلیتهایی مثل نخها، فایلها یا شبکه وابستگی دارد. همچنین به کتابخانه استاندارد زبان سی، `libc` هم وابسطه هست که با سرویسهای سیستمعامل تعامل نزدیکی دارند. از آنجا که قصد داریم یک سیستمعامل بنویسیم، نمیتوانیم از هیچ کتابخانهایی که به سیستمعامل نیاز داشته باشد استفاده کنیم. بنابراین باید اضافه شدن خودکار کتابخانه استاندارد را از طریق [خاصیت `no_std`] غیر فعال کنیم.
|
||||
|
||||
[کتابخانه استاندارد]: https://doc.rust-lang.org/std/
|
||||
[خاصیت `no_std`]: https://doc.rust-lang.org/1.30.0/book/first-edition/using-rust-without-the-standard-library.html
|
||||
|
||||
|
||||
با ساخت یک اپلیکیشن جدید کارگو شروع میکنیم. سادهترین راه برای انجام این کار از طریق خط فرمان است:
|
||||
|
||||
```
|
||||
cargo new blog_os --bin --edition 2018
|
||||
```
|
||||
|
||||
نام پروژه را `blog_os` گذاشتم، اما شما میتوانید نام دلخواه خود را انتخاب کنید. پرچمِ (ترجمه: Flag) `bin--` مشخص میکند که ما میخواهیم یک فایل اجرایی ایجاد کنیم (به جای یک کتابخانه) و پرچمِ `edition 2018--` مشخص میکند که میخواهیم از [ویرایش 2018] زبان راست برای کریت خود استفاده کنیم. وقتی دستور را اجرا میکنیم، کارگو ساختار پوشههای زیر را برای ما ایجاد میکند:
|
||||
|
||||
[ویرایش 2018]: https://doc.rust-lang.org/nightly/edition-guide/rust-2018/index.html
|
||||
|
||||
```
|
||||
blog_os
|
||||
├── Cargo.toml
|
||||
└── src
|
||||
└── main.rs
|
||||
```
|
||||
|
||||
فایل `Cargo.toml` شامل تنظیمات کریت میباشد، به عنوان مثال نام کریت، نام نویسنده، شماره [نسخه سمنتیک] و وابستگیها. فایل `src/main.rs` شامل ماژول ریشه برای کریت ما و تابع `main` است. میتوانید کریت خود را با دستور `cargo build` کامپایل کنید و سپس باینری کامپایل شده `blog_os` را در زیرپوشه `target/debug` اجرا کنید.
|
||||
|
||||
[نسخه سمنتیک]: https://semver.org/
|
||||
|
||||
### خاصیت `no_std`
|
||||
|
||||
در حال حاظر کریت ما بطور ضمنی به کتابخانه استاندارد لینک دارد. بیایید تا سعی کنیم آن را با اضافه کردن [خاصیت `no_std`] غیر فعال کنیم:
|
||||
|
||||
```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` در [خروجی استاندارد] مینویسد، که یک توصیف کننده فایل (ترجمه: File Descriptor) خاص است که توسط سیستمعامل ارائه میشود.
|
||||
|
||||
[ماکروی `println`]: https://doc.rust-lang.org/std/macro.println.html
|
||||
[خروجی استاندارد]: https://en.wikipedia.org/wiki/Standard_streams#Standard_output_.28stdout.29
|
||||
|
||||
پس بیایید قسمت مروبط به چاپ را پاک کرده و این بار با یک تابع main خالی امتحان کنیم:
|
||||
|
||||
```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]` و یک _language item_ روبرو است.
|
||||
|
||||
## پیادهسازی پنیک (کلمه: Panic)
|
||||
|
||||
خاصیت `panic_handler` تابعی را تعریف میکند که کامپایلر باید در هنگام رخ دادن یک [پنیک] اجرا کند. کتابخانه استاندارد تابع مدیریت پنیک خود را ارائه میدهد، اما در یک محیط `no_std` ما باید خودمان آن را تعریف کنیم.
|
||||
|
||||
[پنیک]: 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 function) بوسیله نوع برگشتی `!` [نوع ”هرگز“] علامتگذاری شده است. فعلا کار زیادی نیست که بتوانیم در این تابع انجام دهیم، بنابراین فقط یک حلقه بینهایت مینویسیم.
|
||||
|
||||
[PanicInfo]: https://doc.rust-lang.org/nightly/core/panic/struct.PanicInfo.html
|
||||
[تابع واگرا]: https://doc.rust-lang.org/1.30.0/book/first-edition/functions.html#diverging-functions
|
||||
[نوع ”هرگز“]: https://doc.rust-lang.org/nightly/std/primitive.never.html
|
||||
|
||||
## آیتم زبان `eh_personality`
|
||||
|
||||
آیتمهای زبان، توابع و انواع خاصی هستند که برای استفاده درون کامپایلر ضروریاند. به عنوان مثال، تِرِیت [`Copy`]\(کلمه: Trait) یک آیتم زبان است که به کامپایلر میگوید کدام انواع دارای [_مفهوم کپی_][`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`] یک تابع را به عنوان تابعی که برای پیادهسازی [بازکردن پشته (Stack Unwinding)] استفاده شده، علامتگذاری میکند. راست بطور پیشفرض از _بازکردن_ (ترجمه: unwinding) برای اجرای نابودگرهای (ترجمه: Destructors) تمام متغیرهای زنده درون استک در مواقع [پنیک] استفاده میکند. این تضمین میکند که تمام حافظه استفاده شده آزاد میشود و به نخ اصلی اجازه میدهد پنیک را دریافت کرده و اجرا را ادامه دهد. باز کردن، یک فرآیند پیچیده است و به برخی از کتابخانههای خاص سیستمعامل (به عنوان مثال [libunwind] در لینوکس یا [مدیریت اکسپشن ساخت یافته] در ویندوز) نیاز دارد، بنابراین ما نمیخواهیم از آن برای سیستمعامل خود استفاده کنیم.
|
||||
|
||||
[آیتم زبان `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/en-us/windows/win32/debug/structured-exception-handling
|
||||
|
||||
### غیرفعال کردن Unwinding
|
||||
|
||||
موارد استفاده دیگری نیز وجود دارد که باز کردن نامطلوب است، بنابراین راست به جای آن گزینه [قطع در پنیک] را فراهم میکند. این امر تولید اطلاعات نمادها (ترجمه: Symbol) را از بین میبرد و بنابراین اندازه باینری را بطور قابل توجهی کاهش میدهد. چندین مکان وجود دارد که می توانیم باز کردن را غیرفعال کنیم. سادهترین راه این است که خطوط زیر را به `Cargo.toml` اضافه کنید:
|
||||
|
||||
```toml
|
||||
[profile.dev]
|
||||
panic = "abort"
|
||||
|
||||
[profile.release]
|
||||
panic = "abort"
|
||||
```
|
||||
|
||||
این استراتژی پنیک را برای دو پروفایل `dev` (در `cargo build` استفاده میشود) و پروفایل `release` (در ` cargo build --release` استفاده میشود) تنظیم میکند. اکنون آیتم زبان `eh_personality` نباید دیگر لازم باشد.
|
||||
|
||||
[قطع در پنیک]: https://github.com/rust-lang/rust/pull/32900
|
||||
|
||||
اکنون هر دو خطای فوق را برطرف کردیم. با این حال، اگر اکنون بخواهیم آن را کامپایل کنیم، خطای دیگری رخ میدهد:
|
||||
|
||||
```
|
||||
> cargo build
|
||||
error: requires `start` lang_item
|
||||
```
|
||||
|
||||
برنامه ما آیتم زبان `start` که نقطه ورود را مشخص میکند، را ندارد.
|
||||
|
||||
## خاصیت `start`
|
||||
|
||||
ممکن است تصور شود که تابع `main` اولین تابعی است که هنگام اجرای یک برنامه فراخوانی میشود. با این حال، بیشتر زبانها دارای [سیستم رانتایم] هستند که مسئول مواردی مانند جمع آوری زباله (به عنوان مثال در جاوا) یا نخهای نرمافزار (به عنوان مثال goroutines در Go) است. این رانتایم باید قبل از `main` فراخوانی شود، زیرا باید خود را مقداردهی اولیه و آماده کند.
|
||||
|
||||
[سیستم رانتایم]: https://en.wikipedia.org/wiki/Runtime_system
|
||||
|
||||
در یک باینری معمولی راست که از کتابخانه استاندارد استفاده میکند، اجرا در یک کتابخانه رانتایم C به نام `crt0` ("زمان اجرا صفر C") شروع میشود، که محیط را برای یک برنامه C تنظیم میکند. این شامل ایجاد یک پشته و قرار دادن آرگومانها در رجیسترهای مناسب است. سپس رانتایم C [ورودی رانتایم راست][rt::lang_start] را فراخوانی میکند، که با آیتم زبان `start` مشخص شده است. راست فقط یک رانتایم بسیار کوچک دارد، که مواظب برخی از کارهای کوچک مانند راهاندازی محافظهای سرریز پشته یا چاپ backtrace با پنیک میباشد. رانتایم در نهایت تابع `main` را فراخوانی میکند.
|
||||
|
||||
[rt::lang_start]: https://github.com/rust-lang/rust/blob/bb4d1491466d8239a7a5fd68bd605e3276e97afb/src/libstd/rt.rs#L32-L73
|
||||
|
||||
برنامه اجرایی مستقل ما به رانتایم و `crt0` دسترسی ندارد، بنابراین باید نقطه ورود را مشخص کنیم. پیادهسازی آیتم زبان `start` کمکی نخواهد کرد، زیرا همچنان به `crt0` نیاز دارد. در عوض، باید نقطه ورود `crt0` را مستقیماً بازنویسی کنیم.
|
||||
|
||||
### بازنویسی نقطه ورود
|
||||
|
||||
برای اینکه به کامپایلر راست بگوییم که نمیخواهیم از زنجیره نقطه ورودی عادی استفاده کنیم، ویژگی `#![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` بدون یک رانتایم اساسی که آن را صدا کند معنی ندارد. در عوض، ما در حال بازنویسی نقطه ورود سیستمعامل با تابع `start_` خود هستیم:
|
||||
|
||||
```rust
|
||||
#[no_mangle]
|
||||
pub extern "C" fn _start() -> ! {
|
||||
loop {}
|
||||
}
|
||||
```
|
||||
|
||||
با استفاده از ویژگی `[no_mangle]#` ما [name mangling] را غیرفعال می کنیم تا اطمینان حاصل کنیم که کامپایلر راست تابعی با نام `start_` را خروجی میدهد. بدون این ویژگی، کامپایلر برخی از نمادهای رمزنگاری شده `ZN3blog_os4_start7hb173fedf945531caE_` را تولید میکند تا به هر تابع یک نام منحصر به فرد بدهد. این ویژگی لازم است زیرا در مرحله بعدی باید نام تایع نقطه ورود را به لینکر (کلمه: linker) بگوییم.
|
||||
|
||||
ما همچنین باید تابع را به عنوان `"extern "C` علامتگذاری کنیم تا به کامپایلر بگوییم که باید از [قرارداد فراخوانی C] برای این تابع استفاده کند (به جای قرارداد مشخص نشده فراخوانی راست). دلیل نامگذاری تابع `start_` این است که این نام نقطه پیشفرض ورودی برای اکثر سیستمها است.
|
||||
|
||||
[name mangling]: https://en.wikipedia.org/wiki/Name_mangling
|
||||
[قرارداد فراخوانی C]: https://en.wikipedia.org/wiki/Calling_convention
|
||||
|
||||
نوع بازگشت `!` به این معنی است که تایع واگرا است، یعنی اجازه بازگشت ندارد. این مورد لازم است زیرا نقطه ورود توسط هیچ تابعی فراخوانی نمیشود، بلکه مستقیماً توسط سیستمعامل یا بوتلودر فراخوانی میشود. بنابراین به جای بازگشت، نقطه ورود باید به عنوان مثال [فراخوان سیستمی `exit`] از سیستمعامل را فراخوانی کند. در مورد ما، خاموش کردن دستگاه میتواند اقدامی منطقی باشد، زیرا در صورت بازگشت باینری مستقل دیگر کاری برای انجام دادن وجود ندارد. در حال حاضر، ما این نیاز را با حلقههای بیپایان انجام میدهیم.
|
||||
|
||||
[فراخوان سیستمی `exit`]: https://en.wikipedia.org/wiki/Exit_(system_call)
|
||||
|
||||
حالا وقتی `cargo build` را اجرا میکنیم، با یک خطای _لینکر_ زشت مواجه میشویم.
|
||||
|
||||
## خطاهای لینکر (Linker)
|
||||
|
||||
لینکر برنامهای است که کد تولید شده را ترکیب کرده و یک فایل اجرایی میسازد. از آنجا که فرمت اجرایی بین لینوکس، ویندوز و macOS متفاوت است، هر سیستم لینکر خود را دارد که خطای متفاوتی ایجاد میکند. علت اصلی خطاها یکسان است: پیکربندی پیشفرض لینکر فرض میکند که برنامه ما به رانتایم C وابسته است، که این طور نیست.
|
||||
|
||||
برای حل خطاها، باید به لینکر بگوییم که نباید رانتایم C را اضافه کند. ما میتوانیم این کار را با اضافه کردن مجموعهای از آرگمانها به لینکر یا با ساختن یک هدف (ترجمه: Target) bare metal انجام دهیم.
|
||||
|
||||
### بیلد کردن برای یک هدف bare metal
|
||||
|
||||
راست به طور پیشفرض سعی در ایجاد یک اجرایی دارد که بتواند در محیط سیستم فعلی شما اجرا شود. به عنوان مثال، اگر از ویندوز در `x86_64` استفاده میکنید، راست سعی در ایجاد یک `exe.` اجرایی ویندوز دارد که از دستورالعملهای `x86_64` استفاده میکند. به این محیط سیستم "میزبان" شما گفته میشود.
|
||||
|
||||
راست برای توصیف محیطهای مختلف، از رشتهای به نام [_target triple_]\(سهگانه هدف) استفاده میکند. با اجرای `rustc --version --verbose` میتوانید target triple را برای سیستم میزبان خود مشاهده کنید:
|
||||
|
||||
[_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` است. میبینیم که سهگانه میزبان `x86_64-unknown-linux-gnu` است که شامل معماری پردازنده (`x86_64`)، فروشنده (`ناشناخته`)، سیستمعامل (` linux`) و [ABI] (`gnu`) است.
|
||||
|
||||
[ABI]: https://en.wikipedia.org/wiki/Application_binary_interface
|
||||
|
||||
با کامپایل کردن برای سهگانه میزبانمان، کامپایلر راست و لینکر فرض میکنند که یک سیستمعامل زیرین مانند Linux یا Windows وجود دارد که به طور پیشفرض از رانتایم C استفاده میکند، که باعث خطاهای لینکر میشود. بنابراین برای جلوگیری از خطاهای لینکر، میتوانیم برای محیطی متفاوت و بدون سیستمعامل زیرین کامپایل کنیم.
|
||||
|
||||
یک مثال برای چنین محیطِ bare metal ی، سهگانه هدف `thumbv7em-none-eabihf` است، که یک سیستم [تعبیه شده][ARM] را توصیف میکند. جزئیات مهم نیستند، مهم این است که سهگانه هدف فاقد سیستمعامل زیرین باشد، که با `none` در سهگانه هدف نشان داده میشود. برای این که بتوانیم برای این هدف کامپایل کنیم، باید آن را به rustup اضافه کنیم:
|
||||
|
||||
[تعبیه شده]: https://en.wikipedia.org/wiki/Embedded_system
|
||||
[ARM]: https://en.wikipedia.org/wiki/ARM_architecture
|
||||
|
||||
```
|
||||
rustup target add thumbv7em-none-eabihf
|
||||
```
|
||||
|
||||
با این کار نسخهای از کتابخانه استاندارد (و core) برای سیستم بارگیری میشود. اکنون میتوانیم برای این هدف اجرایی مستقل خود را بسازیم:
|
||||
|
||||
```
|
||||
cargo build --target thumbv7em-none-eabihf
|
||||
```
|
||||
|
||||
با استفاده از یک آرگومان `target--`، ما اجرایی خود را برای یک سیستم هدف bare metal [کراس کامپایل] میکنیم. از آنجا که سیستم هدف فاقد سیستمعامل است، لینکر سعی نمیکند رانتایم C را به آن پیوند دهد و بیلد ما بدون هیچ گونه خطای لینکر با موفقیت انجام میشود.
|
||||
|
||||
[کراس کامپایل]: https://en.wikipedia.org/wiki/Cross_compiler
|
||||
|
||||
این روشی است که ما برای ساخت هسته سیستمعامل خود استفاده خواهیم کرد. به جای `thumbv7em-none-eabihf`، ما از یک [هدف سفارشی] استفاده خواهیم کرد که یک محیط `x86_64` bare metal را توصیف میکند. جزئیات در پست بعدی توضیح داده خواهد شد.
|
||||
|
||||
[هدف سفارشی]: https://doc.rust-lang.org/rustc/targets/custom.html
|
||||
|
||||
### آرگومانهای لینکر
|
||||
|
||||
به جای کامپایل کردن برای یک سیستم bare metal، میتوان خطاهای لینکر را با استفاده از مجموعه خاصی از آرگومانها به لینکر حل کرد. این روشی نیست که ما برای هسته خود استفاده کنیم، بنابراین این بخش اختیاری است و فقط برای کامل بودن ارائه میشود. برای نشان دادن محتوای اختیاری، روی _"آرگومانهای لینکر"_ در زیر کلیک کنید.
|
||||
|
||||
<details>
|
||||
|
||||
<summary>آرگومانهای لینکر</summary>
|
||||
|
||||
در این بخش، ما در مورد خطاهای لینکر که در لینوکس، ویندوز و macOS رخ میدهد بحث میکنیم و نحوه حل آنها را با استفاده از آرگومانهای اضافی به لینکر توضیح میدهیم. توجه داشته باشید که فرمت اجرایی و لینکر بین سیستمعاملها متفاوت است، بنابراین برای هر سیستم مجموعهای متفاوت از آرگومانها مورد نیاز است.
|
||||
|
||||
#### لینوکس
|
||||
|
||||
در لینوکس خطای لینکر زیر رخ میدهد (کوتاه شده):
|
||||
|
||||
```
|
||||
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 است که به آن `start_` نیز گفته میشود. به برخی از نمادهای کتابخانه استاندارد C یعنی `libc` نیاز دارد که به دلیل ویژگی`no_std` آنها را نداریم، بنابراین لینکر نمیتواند این مراجع را پیدا کند. برای حل این مسئله، با استفاده از پرچم `nostartfiles-` میتوانیم به لینکر بگوییم که نباید روال راهاندازی C را لینک دهد.
|
||||
|
||||
یکی از راههای عبور صفات لینکر از طریق cargo، دستور `cargo rustc` است. این دستور دقیقاً مانند `cargo build` رفتار میکند، اما اجازه میدهد گزینهها را به `rustc`، کامپایلر اصلی راست انتقال دهید. `rustc` دارای پرچم`C link-arg-` است که آرگومان را به لینکر منتقل میکند. با ترکیب همه اینها، دستور بیلد جدید ما به این شکل است:
|
||||
|
||||
```
|
||||
cargo rustc -- -C link-arg=-nostartfiles
|
||||
```
|
||||
|
||||
اکنون کریت ما بصورت اجرایی مستقل در لینوکس ساخته میشود!
|
||||
|
||||
لازم نیست که صریحاً نام تابع نقطه ورود را مشخص کنیم، زیرا لینکر به طور پیشفرض به دنبال تابعی با نام `start_` میگردد.
|
||||
|
||||
#### ویندوز
|
||||
|
||||
در ویندوز، یک خطای لینکر متفاوت رخ میدهد (کوتاه شده):
|
||||
|
||||
```
|
||||
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-subsystem]. برای زیر سیستم `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
|
||||
```
|
||||
|
||||
از متفاوت بودن فرمت آرگومان، به وضوح میفهمیم که لینکر ویندوز یک برنامه کاملاً متفاوت از لینکر 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-subsystems] مختلف استفاده کنند. برای برنامههای عادی، بسته به نام نقطه ورود استنباط می شوند: اگر نقطه ورود `main` نامگذاری شود، از زیر سیستم `CONSOLE` و اگر نقطه ورود `WinMain` نامگذاری شود، از زیر سیستم `WINDOWS` استفاده میشود. از آنجا که تابع `start_` ما نام دیگری دارد، باید زیر سیستم را صریحاً مشخص کنیم:
|
||||
|
||||
```
|
||||
cargo rustc -- -C link-args="/ENTRY:_start /SUBSYSTEM:console"
|
||||
```
|
||||
|
||||
ما در اینجا از زیر سیستم `CONSOLE` استفاده میکنیم، اما زیر سیستم `WINDOWS` نیز کار خواهد کرد. به جای اینکه چند بار از `C link-arg-` استفاده کنیم، از`C link-args-` استفاده میکنیم که لیستی از آرگومانها به صورت جدا شده با فاصله را دریافت میکند.
|
||||
|
||||
با استفاده از این دستور، اجرایی ما باید با موفقیت بر روی ویندوز ساخته شود.
|
||||
|
||||
#### 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 […]
|
||||
```
|
||||
|
||||
سیستمعامل مک [رسماً باینریهایی را که بطور استاتیک با هم پیوند دارند پشتیبانی نمیکند] و بطور پیشفرض به برنامههایی برای پیوند دادن کتابخانه `libSystem` نیاز دارد. برای تغییر این حالت و پیوند دادن یک باینری استاتیک، پرچم `static-` را به لینکر ارسال میکنیم:
|
||||
|
||||
[باینریهایی را که بطور استاتیک با هم پیوند دارند پشتیبانی نمیکند]: 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") پیوند دارند. این همان خطایی است که در لینوکس داشتیم و با افزودن آرگومان لینکر `nostartfiles-` نیز قابل حل است:
|
||||
|
||||
```
|
||||
cargo rustc -- -C link-args="-e __start -static -nostartfiles"
|
||||
```
|
||||
|
||||
اکنون برنامه ما باید با موفقیت بر روی macOS ساخته شود.
|
||||
|
||||
#### متحد کردن دستورات Build
|
||||
|
||||
در حال حاضر بسته به سیستمعامل میزبان، دستورات ساخت متفاوتی داریم که ایده آل نیست. برای جلوگیری از این، میتوانیم فایلی با نام `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` ساده قابل بیلد باشد.
|
||||
|
||||
#### آیا شما باید این کار را انجام دهید؟
|
||||
|
||||
اگرچه ساخت یک اجرایی مستقل برای لینوکس، ویندوز و macOS امکان پذیر است، اما احتمالاً ایده خوبی نیست. چرا که اجرایی ما هنوز انتظار موارد مختلفی را دارد، به عنوان مثال با فراخوانی تابع `start_` یک پشته مقداردهی اولیه شده است. بدون رانتایم C، ممکن است برخی از این الزامات برآورده نشود، که ممکن است باعث شکست برنامه ما شود، به عنوان مثال از طریق `segmentation fault`.
|
||||
|
||||
اگر می خواهید یک باینری کوچک ایجاد کنید که بر روی سیستمعامل موجود اجرا شود، اضافه کردن `libc` و تنظیم ویژگی `[start]#` همانطور که [اینجا](https://doc.rust-lang.org/1.16.0/book/no-stdlib.html) شرح داده شده است، احتمالاً ایده بهتری است.
|
||||
|
||||
</details>
|
||||
|
||||
## خلاصه {#summary}
|
||||
|
||||
یک باینری مستقل مینیمال راست مانند این است:
|
||||
|
||||
`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 <author@example.com>"]
|
||||
|
||||
# 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
|
||||
```
|
||||
|
||||
برای ساخت این باینری، ما باید برای یک هدف bare metal مانند `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"
|
||||
```
|
||||
|
||||
توجه داشته باشید که این فقط یک نمونه حداقلی از باینری مستقل راست است. این باینری انتظار چیزهای مختلفی را دارد، به عنوان مثال با فراخوانی تابع `start_` یک پشته مقداردهی اولیه میشود. **بنابراین برای هر گونه استفاده واقعی از چنین باینری، مراحل بیشتری لازم است**.
|
||||
|
||||
## بعدی چیست؟
|
||||
|
||||
[پست بعدی] مراحل مورد نیاز برای تبدیل باینری مستقل به حداقل هسته سیستمعامل را توضیح میدهد. که شامل ایجاد یک هدف سفارشی، ترکیب اجرایی ما با بوتلودر و یادگیری نحوه چاپ چیزی در صفحه است.
|
||||
|
||||
[پست بعدی]: @/edition-2/posts/02-minimal-rust-kernel/index.fa.md
|
||||
@@ -1,524 +0,0 @@
|
||||
+++
|
||||
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 = ["AlexandreMarcq", "alaincao"]
|
||||
+++
|
||||
|
||||
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
|
||||
|
||||
<!-- more -->
|
||||
|
||||
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
|
||||
<!-- fix for zola anchor checker (target is in template): <a id="comments"> -->
|
||||
[post branch]: https://github.com/phil-opp/blog_os/tree/post-01
|
||||
|
||||
<!-- toc -->
|
||||
|
||||
## 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 compiler 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.
|
||||
|
||||
<details>
|
||||
|
||||
<summary>Arguments du Linker</summary>
|
||||
|
||||
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, certaines 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.
|
||||
|
||||
</details>
|
||||
|
||||
## 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 <author@example.com>"]
|
||||
|
||||
# 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
|
||||
@@ -1,532 +0,0 @@
|
||||
+++
|
||||
title = "フリースタンディングな Rust バイナリ"
|
||||
weight = 1
|
||||
path = "ja/freestanding-rust-binary"
|
||||
date = 2018-02-10
|
||||
|
||||
[extra]
|
||||
chapter = "Bare Bones"
|
||||
# Please update this when updating the translation
|
||||
translation_based_on_commit = "6f1f87215892c2be12c6973a6f753c9a25c34b7e"
|
||||
# GitHub usernames of the people that translated this post
|
||||
translators = ["JohnTitor"]
|
||||
+++
|
||||
|
||||
私達自身のオペレーティングシステム(以下、OS)カーネルを作っていく最初のステップは標準ライブラリとリンクしない Rust の実行可能ファイルをつくることです。これにより、基盤となる OS がない[ベアメタル][bare metal]上で Rust のコードを実行することができるようになります。
|
||||
|
||||
[bare metal]: https://en.wikipedia.org/wiki/Bare_machine
|
||||
|
||||
<!-- more -->
|
||||
|
||||
このブログの内容は [GitHub] 上で公開・開発されています。何か問題や質問などがあれば issue をたててください (訳注: リンクは原文(英語)のものになります)。また[こちら][comments]にコメントを残すこともできます。この記事の完全なソースコードは[`post-01` ブランチ][post branch]にあります。
|
||||
|
||||
[GitHub]: https://github.com/phil-opp/blog_os
|
||||
[comments]: #comments
|
||||
<!-- fix for zola anchor checker (target is in template): <a id="comments"> -->
|
||||
[post branch]: https://github.com/phil-opp/blog_os/tree/post-01
|
||||
|
||||
<!-- toc -->
|
||||
|
||||
## 導入
|
||||
|
||||
OS カーネルを書くためには、いかなる OS の機能にも依存しないコードが必要となります。つまり、スレッドやヒープメモリ、ネットワーク、乱数、標準出力、その他 OS による抽象化や特定のハードウェアを必要とする機能は使えません。私達は自分自身で OS とそのドライバを書こうとしているので、これは理にかなっています。
|
||||
|
||||
これは [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 で OS カーネルを書くには、基盤となる OS なしで動く実行環境をつくる必要があります。そのような実行環境はフリースタンディング環境やベアメタルのように呼ばれます。
|
||||
|
||||
この記事では、フリースタンディングな Rust のバイナリをつくるために必要なステップを紹介し、なぜそれが必要なのかを説明します。もし最小限の説明だけを読みたいのであれば **[概要](#summary)** まで飛ばしてください。
|
||||
|
||||
## 標準ライブラリの無効化
|
||||
|
||||
デフォルトでは、全ての Rust クレートは[標準ライブラリ][standard library]にリンクされています。標準ライブラリはスレッドやファイル、ネットワークのような OS の機能に依存しています。また OS と密接な関係にある C の標準ライブラリ(`libc`)にも依存しています。私達の目的は OS を書くことなので、 OS 依存のライブラリを使うことはできません。そのため、 [`no_std` attribute] を使って標準ライブラリが自動的にリンクされるのを無効にします。
|
||||
|
||||
[standard library]: https://doc.rust-lang.org/std/
|
||||
[`no_std` attribute]: https://doc.rust-lang.org/1.30.0/book/first-edition/using-rust-without-the-standard-library.html
|
||||
|
||||
新しい Cargo プロジェクトをつくるところから始めましょう。もっとも簡単なやり方はコマンドラインで以下を実行することです。
|
||||
|
||||
```bash
|
||||
cargo new blog_os --bin --edition 2018
|
||||
```
|
||||
|
||||
プロジェクト名を `blog_os` としましたが、もちろんお好きな名前をつけていただいても大丈夫です。`--bin`フラグは実行可能バイナリを作成することを、 `--edition 2018` は[2018エディション][2018 edition]を使用することを明示的に指定します。コマンドを実行すると、 Cargoは以下のようなディレクトリ構造を作成します:
|
||||
|
||||
[2018 edition]: https://doc.rust-lang.org/nightly/edition-guide/rust-2018/index.html
|
||||
|
||||
```bash
|
||||
blog_os
|
||||
├── Cargo.toml
|
||||
└── src
|
||||
└── main.rs
|
||||
```
|
||||
|
||||
`Cargo.toml` にはクレートの名前や作者名、[セマンティックバージョニング][semantic version]に基づくバージョンナンバーや依存関係などが書かれています。`src/main.rs` には私達のクレートのルートモジュールと `main` 関数が含まれています。`cargo build` コマンドでこのクレートをコンパイルして、 `target/debug` ディレクトリの中にあるコンパイルされた `blog_os` バイナリを実行することができます。
|
||||
|
||||
[semantic version]: https://semver.org/
|
||||
|
||||
### `no_std` Attribute
|
||||
|
||||
今のところ私達のクレートは暗黙のうちに標準ライブラリをリンクしています。[`no_std` attribute]を追加してこれを無効にしてみましょう:
|
||||
|
||||
```rust
|
||||
// main.rs
|
||||
|
||||
#![no_std]
|
||||
|
||||
fn main() {
|
||||
println!("Hello, world!");
|
||||
}
|
||||
```
|
||||
|
||||
(`cargo build` を実行して)ビルドしようとすると、次のようなエラーが発生します:
|
||||
|
||||
```bash
|
||||
error: cannot find macro `println!` in this scope
|
||||
--> src/main.rs:4:5
|
||||
|
|
||||
4 | println!("Hello, world!");
|
||||
| ^^^^^^^
|
||||
```
|
||||
|
||||
これは [`println` マクロ][`println` macro]が標準ライブラリに含まれているためです。`no_std` で標準ライブラリを無効にしたので、何かをプリントすることはできなくなりました。`println` は標準出力に書き込むのでこれは理にかなっています。[標準出力][standard output]は OS によって提供される特別なファイル記述子であるためです。
|
||||
|
||||
[`println` macro]: https://doc.rust-lang.org/std/macro.println.html
|
||||
[standard output]: https://en.wikipedia.org/wiki/Standard_streams#Standard_output_.28stdout.29
|
||||
|
||||
では、 `println` を削除し `main` 関数を空にしてもう一度ビルドしてみましょう:
|
||||
|
||||
```rust
|
||||
// main.rs
|
||||
|
||||
#![no_std]
|
||||
|
||||
fn main() {}
|
||||
```
|
||||
|
||||
```bash
|
||||
> cargo build
|
||||
error: `#[panic_handler]` function required, but not found
|
||||
error: language item required, but not found: `eh_personality`
|
||||
```
|
||||
|
||||
この状態では `#[panic_handler]` 関数と `language item` がないというエラーが発生します。
|
||||
|
||||
## Panic の実装
|
||||
|
||||
`panic_handler` attribute は[パニック]が発生したときにコンパイラが呼び出す関数を定義します。標準ライブラリには独自のパニックハンドラー関数がありますが、 `no_std` 環境では私達の手でそれを実装する必要があります:
|
||||
|
||||
[パニック]: https://doc.rust-lang.org/stable/book/ch09-01-unrecoverable-errors-with-panic.html
|
||||
|
||||
```rust
|
||||
// in main.rs
|
||||
|
||||
use core::panic::PanicInfo;
|
||||
|
||||
/// この関数はパニック時に呼ばれる
|
||||
#[panic_handler]
|
||||
fn panic(_info: &PanicInfo) -> ! {
|
||||
loop {}
|
||||
}
|
||||
```
|
||||
|
||||
[`PanicInfo` パラメータ]には、パニックが発生したファイルと行、およびオプションでパニックメッセージが含まれます。この関数は戻り値を取るべきではないので、]"never" 型(`!`)][“never” type]を返すことで[発散する関数][diverging function]となります。今のところこの関数でできることは多くないので、無限にループするだけです。
|
||||
|
||||
[`PanicInfo` パラメータ]: https://doc.rust-lang.org/nightly/core/panic/struct.PanicInfo.html
|
||||
[diverging function]: 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` Language Item
|
||||
|
||||
language item はコンパイラによって内部的に必要とされる特別な関数や型です。例えば、[`Copy`] トレイトはどの型が[コピーセマンティクス][`Copy`]を持っているかをコンパイラに伝える language item です。[実装][copy code]を見てみると、 language item として定義されている特別な `#[lang = "copy"]` attribute を持っていることが分かります。
|
||||
|
||||
[`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
|
||||
|
||||
独自に language item を実装することもできますが、これは最終手段として行われるべきでしょう。というのも、language item は非常に不安定な実装であり型検査も行われないからです(なので、コンパイラは関数が正しい引数の型を取っているかさえ検査しません)。幸い、上記の language item のエラーを修正するためのより安定した方法があります。
|
||||
|
||||
[`eh_personality` language item] は[スタックアンワインド][stack unwinding] を実装するための関数を定義します。デフォルトでは、パニックが起きた場合には Rust はアンワインドを使用してすべてのスタックにある変数のデストラクタを実行します。これにより、使用されている全てのメモリが確実に解放され、親スレッドはパニックを検知して実行を継続できます。しかしアンワインドは複雑であり、いくつかの OS 特有のライブラリ(例えば、Linux では [libunwind] 、Windows では[構造化例外][structured exception handling])を必要とするので、私達の OS には使いたくありません。
|
||||
|
||||
[`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/en-us/windows/win32/debug/structured-exception-handling
|
||||
|
||||
### アンワインドの無効化
|
||||
|
||||
他にもアンワインドが望ましくないユースケースがあります。そのため、Rust には代わりに[パニックで中止する][abort on panic]オプションがあります。これにより、アンワインドのシンボル情報の生成が無効になり、バイナリサイズが大幅に削減されます。アンワインドを無効にする方法は複数あります。もっとも簡単な方法は、`Cargo.toml` に次の行を追加することです:
|
||||
|
||||
```toml
|
||||
[profile.dev]
|
||||
panic = "abort"
|
||||
|
||||
[profile.release]
|
||||
panic = "abort"
|
||||
```
|
||||
|
||||
これは dev プロファイル(`cargo build` に使用される)と release プロファイル(`cargo build --release` に使用される)の両方でパニックで中止するようにするための設定です。これで `eh_personality` language item が不要になりました。
|
||||
|
||||
[abort on panic]: https://github.com/rust-lang/rust/pull/32900
|
||||
|
||||
これで上の2つのエラーを修正しました。しかし、コンパイルしようとすると別のエラーが発生します:
|
||||
|
||||
```bash
|
||||
> cargo build
|
||||
error: requires `start` lang_item
|
||||
```
|
||||
|
||||
私達のプログラムにはエントリポイントを定義する `start` language item がありません。
|
||||
|
||||
## `start` attribute
|
||||
|
||||
`main` 関数はプログラムを実行したときに最初に呼び出される関数であると思うかもしれません。しかし、ほとんどの言語には[ランタイムシステム][runtime system]があり、これはガベージコレクション(Java など)やソフトウェアスレッド(Go のゴルーチン)などを処理します。ランタイムは自身を初期化する必要があるため、`main` 関数の前に呼び出す必要があります。これにはスタック領域の作成と正しいレジスタへの引数の配置が含まれます。
|
||||
|
||||
[runtime system]: https://en.wikipedia.org/wiki/Runtime_system
|
||||
|
||||
標準ライブラリをリンクする一般的な Rust バイナリでは、`crt0` ("C runtime zero")と呼ばれる C のランタイムライブラリで実行が開始され、C アプリケーションの環境が設定されます。その後 C ランタイムは、`start` language item で定義されている [Rust ランタイムのエントリポイント][rt::lang_start]を呼び出します。Rust にはごくわずかなランタイムしかありません。これは、スタックオーバーフローを防ぐ設定やパニック時のバックトレースの表示など、いくつかの小さな処理を行います。最後に、ランタイムは `main` 関数を呼び出します。
|
||||
|
||||
[rt::lang_start]: https://github.com/rust-lang/rust/blob/bb4d1491466d8239a7a5fd68bd605e3276e97afb/src/libstd/rt.rs#L32-L73
|
||||
|
||||
私達のフリースタンディングな実行可能ファイルは今のところ Rust ランタイムと `crt0` へアクセスできません。なので、私達は自身でエントリポイントを定義する必要があります。`start` language item を実装することは `crt0` を必要とするのでここではできません。代わりに `crt0` エントリポイントを直接上書きしなければなりません。
|
||||
|
||||
### エントリポイントの上書き
|
||||
|
||||
Rust コンパイラに通常のエントリポイントを使いたくないことを伝えるために、`#![no_main]` attribute を追加します。
|
||||
|
||||
```rust
|
||||
#![no_std]
|
||||
#![no_main]
|
||||
|
||||
use core::panic::PanicInfo;
|
||||
|
||||
/// This function is called on panic.
|
||||
#[panic_handler]
|
||||
fn panic(_info: &PanicInfo) -> ! {
|
||||
loop {}
|
||||
}
|
||||
```
|
||||
|
||||
`main` 関数を削除したことに気付いたかもしれません。`main` 関数を呼び出す基盤となるランタイムなしには置いていても意味がないからです。代わりに、OS のエントリポイントを独自の `_start` 関数で上書きしていきます:
|
||||
|
||||
```rust
|
||||
#[no_mangle]
|
||||
pub extern "C" fn _start() -> ! {
|
||||
loop {}
|
||||
}
|
||||
```
|
||||
|
||||
Rust コンパイラが `_start` という名前の関数を実際に出力するように、`#[no_mangle]` attributeを用いて[名前修飾][name mangling]を無効にします。この attribute がないと、コンパイラはすべての関数にユニークな名前をつけるために、 `_ZN3blog_os4_start7hb173fedf945531caE` のようなシンボルを生成します。次のステップでエントリポイントとなる関数の名前をリンカに伝えるため、この属性が必要となります。
|
||||
|
||||
また、(指定されていない Rust の呼び出し規約の代わりに)この関数に [C の呼び出し規約][C calling convention]を使用するようコンパイラに伝えるために、関数を `extern "C"` として定義する必要があります。`_start`という名前をつける理由は、これがほとんどのシステムのデフォルトのエントリポイント名だからです。
|
||||
|
||||
[name mangling]: https://en.wikipedia.org/wiki/Name_mangling
|
||||
[C calling convention]: https://en.wikipedia.org/wiki/Calling_convention
|
||||
|
||||
戻り値の型である `!` は関数が発散している、つまり値を返すことができないことを意味しています。エントリポイントはどの関数からも呼び出されず、OS またはブートローダから直接呼び出されるので、これは必須です。なので、値を返す代わりに、エントリポイントは例えば OS の [`exit` システムコール][`exit` system call]を呼び出します。今回はフリースタンディングなバイナリが返されたときマシンをシャットダウンするようにすると良いでしょう。今のところ、私達は無限ループを起こすことで要件を満たします。
|
||||
|
||||
[`exit` system call]: https://en.wikipedia.org/wiki/Exit_(system_call)
|
||||
|
||||
`cargo build` を実行すると、見づらいリンカエラーが発生します。
|
||||
|
||||
## リンカエラー
|
||||
|
||||
リンカは、生成されたコードを実行可能ファイルに紐付けるプログラムです。実行可能ファイルの形式は Linux や Windows、macOS でそれぞれ異なるため、各システムにはそれぞれ異なるエラーを発生させる独自のリンカがあります。エラーの根本的な原因は同じです。リンカのデフォルト設定では、プログラムが C ランタイムに依存していると仮定していますが、実際にはしていません。
|
||||
|
||||
エラーを回避するためにはリンカに C ランタイムに依存しないことを伝える必要があります。これはリンカに一連の引数を渡すか、ベアメタルターゲット用にビルドすることで可能となります。
|
||||
|
||||
### ベアメタルターゲット用にビルドする
|
||||
|
||||
デフォルトでは、Rust は現在のシステム環境に合った実行可能ファイルをビルドしようとします。例えば、`x86_64` で Windows を使用している場合、Rust は `x86_64` 用の `.exe` Windows 実行可能ファイルをビルドしようとします。このような環境は「ホスト」システムと呼ばれます。
|
||||
|
||||
様々な環境を表現するために、Rust は [_target triple_] という文字列を使います。`rustc --version --verbose` を実行すると、ホストシステムの target triple を確認できます:
|
||||
|
||||
[_target triple_]: https://clang.llvm.org/docs/CrossCompilation.html#target-triple
|
||||
|
||||
```bash
|
||||
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` です。これには CPU アーキテクチャ(`x86_64`)、ベンダー(`unknown`)、OS(`Linux`)、そして [ABI] (`gnu`)が含まれています。
|
||||
|
||||
[ABI]: https://en.wikipedia.org/wiki/Application_binary_interface
|
||||
|
||||
ホストの triple 用にコンパイルすることで、Rust コンパイラとリンカは、デフォルトで C ランタイムを使用する Linux や Windows のような基盤となる OS があると想定し、それによってリンカエラーが発生します。なのでリンカエラーを回避するために、基盤となる OS を使用せずに異なる環境用にコンパイルします。
|
||||
|
||||
このようなベアメタル環境の例としては、`thumbv7em-none-eabihf` target triple があります。これは、[組込みシステム][embedded]を表しています。詳細は省きますが、重要なのは `none` という文字列からわかるように、 この target triple に基盤となる OS がないことです。このターゲット用にコンパイルできるようにするには、 rustup にこれを追加する必要があります:
|
||||
|
||||
[embedded]: https://en.wikipedia.org/wiki/Embedded_system
|
||||
|
||||
```bash
|
||||
rustup target add thumbv7em-none-eabihf
|
||||
```
|
||||
|
||||
これにより、この target triple 用の標準(およびコア)ライブラリのコピーがダウンロードされます。これで、このターゲット用にフリースタンディングな実行可能ファイルをビルドできます:
|
||||
|
||||
```bash
|
||||
cargo build --target thumbv7em-none-eabihf
|
||||
```
|
||||
|
||||
`--target` 引数を渡すことで、ベアメタルターゲット用に実行可能ファイルを[クロスコンパイル][cross compile]します。このターゲットシステムには OS がないため、リンカは C ランタイムをリンクしようとせず、ビルドはリンカエラーなしで成功します。
|
||||
|
||||
[cross compile]: https://en.wikipedia.org/wiki/Cross_compiler
|
||||
|
||||
これが私達の OS カーネルを書くために使うアプローチです。`thumbv7em-none-eabihf` の代わりに、`x86_64` のベアメタル環境を表す[カスタムターゲット][custom target]を使用することもできます。詳細は次のセクションで説明します。
|
||||
|
||||
[custom target]: https://doc.rust-lang.org/rustc/targets/custom.html
|
||||
|
||||
### リンカへの引数
|
||||
|
||||
ベアメタルターゲット用にコンパイルする代わりに、特定の引数のセットをリンカにわたすことでリンカエラーを回避することもできます。これは私達がカーネルに使用するアプローチではありません。したがって、このセクションはオプションであり、選択肢を増やすために書かれています。表示するには以下の「リンカへの引数」をクリックしてください。
|
||||
|
||||
<details>
|
||||
|
||||
<summary>リンカへの引数</summary>
|
||||
|
||||
このセクションでは、Linux、Windows、および macOS で発生するリンカエラーについてと、リンカに追加の引数を渡すことによってそれらを解決する方法を説明します。実行可能ファイルの形式とリンカは OS によって異なるため、システムごとに異なる引数のセットが必要です。
|
||||
|
||||
#### 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 ランタイムの起動ルーチンがリンカに含まれていることです。これは `_start` とも呼ばれます。`no_std` attribute により、C 標準ライブラリ `libc` のいくつかのシンボルが必要となります。なので、リンカはこれらの参照を解決できません。これを解決するために、リンカに `-nostartfiles` フラグを渡して、C の起動ルーチンをリンクしないようにします。
|
||||
|
||||
Cargo を通してリンカの attribute を渡す方法の一つに、`cargo rustc` コマンドがあります。このコマンドは `cargo build` と全く同じように動作しますが、基本となる Rust コンパイラである `rustc` にオプションを渡すことができます。`rustc` にはリンカに引数を渡す `-C link-arg` フラグがあります。新しいビルドコマンドは次のようになります:
|
||||
|
||||
```bash
|
||||
cargo rustc -- -C link-arg=-nostartfiles
|
||||
```
|
||||
|
||||
これで crate を 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
|
||||
|
||||
```bash
|
||||
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 での実行可能ファイルが異なる [subsystems][windows-subsystems] を使用することができるために発生します。通常のプログラムでは、エントリポイント名に基づいて推定されます。エントリポイントが `main` という名前の場合は `CONSOLE` サブシステムが使用され、エントリポイント名が `WinMain` である場合には `WINDOWS` サブシステムが使用されます。`_start` 関数は別の名前を持っているので、サブシステムを明示的に指定する必要があります:
|
||||
|
||||
This error occurs because Windows executables can use different [subsystems][windows-subsystems]. For normal programs they are inferred depending on the entry point name: If the entry point is named `main`, the `CONSOLE` subsystem is used, and if the entry point is named `WinMain`, the `WINDOWS` subsystem is used. Since our `_start` function has a different name, we need to specify the subsystem explicitly:
|
||||
|
||||
```bash
|
||||
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` というリンカ引数を渡します:
|
||||
|
||||
```bash
|
||||
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 は[正式には静的にリンクされたバイナリをサポートしておらず][does not officially support statically linked binaries]、プログラムはデフォルトで `libSystem` ライブラリにリンクされる必要があります。これを無効にして静的バイナリをリンクするには、`-static` フラグをリンカに渡します:
|
||||
|
||||
[does not officially support statically linked binaries]: https://developer.apple.com/library/archive/qa/qa1118/_index.html
|
||||
|
||||
```bash
|
||||
cargo rustc -- -C link-args="-e __start -static"
|
||||
```
|
||||
|
||||
これでもまだ十分ではありません、3つ目のリンカエラーが発生します:
|
||||
|
||||
```
|
||||
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` というリンカ引数を追加することで解決できます:
|
||||
|
||||
```bash
|
||||
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` についての詳細は[公式のドキュメント][official documentation]を確認してください。
|
||||
|
||||
[official documentation]: https://doc.rust-lang.org/cargo/reference/config.html
|
||||
|
||||
これで私達のプログラムは3つすべてのプラットフォーム上で、シンプルに `cargo build` のみでビルドすることができるようになります。
|
||||
|
||||
#### 私達はこれをすべきですか?
|
||||
|
||||
これらの手順で Linux、Windows および macOS 用の独立した実行可能ファイルをビルドすることはできますが、おそらく良い方法ではありません。その理由は、例えば `_start` 関数が呼ばれたときにスタックが初期化されるなど、まだ色々なことを前提としているからです。C ランタイムがなければ、これらの要件のうちいくつかが満たされない可能性があり、セグメンテーション違反(segfault)などによってプログラムが失敗する可能性があります。
|
||||
|
||||
もし既存の OS 上で動作する最小限のバイナリを作成したいなら、`libc` を使って `#[start]` attribute を[ここ][no-stdlib]で説明するとおりに設定するのが良いでしょう。
|
||||
|
||||
[no-stdlib]: https://doc.rust-lang.org/1.16.0/book/no-stdlib.html
|
||||
|
||||
</details>
|
||||
|
||||
## 概要 {#summary}
|
||||
|
||||
最小限の独立した Rust バイナリは次のようになります:
|
||||
|
||||
`src/main.rs`:
|
||||
|
||||
```rust
|
||||
#![no_std] // Rust の標準ライブラリにリンクしない
|
||||
#![no_main] // 全ての Rust レベルのエントリポイントを無効にする
|
||||
|
||||
use core::panic::PanicInfo;
|
||||
|
||||
#[no_mangle] // この関数の名前修飾をしない
|
||||
pub extern "C" fn _start() -> ! {
|
||||
// リンカはデフォルトで `_start` という名前の関数を探すので、
|
||||
// この関数がエントリポイントとなる
|
||||
loop {}
|
||||
}
|
||||
|
||||
/// この関数はパニック時に呼ばれる
|
||||
#[panic_handler]
|
||||
fn panic(_info: &PanicInfo) -> ! {
|
||||
loop {}
|
||||
}
|
||||
```
|
||||
|
||||
`Cargo.toml`:
|
||||
|
||||
```toml
|
||||
[package]
|
||||
name = "crate_name"
|
||||
version = "0.1.0"
|
||||
authors = ["Author Name <author@example.com>"]
|
||||
|
||||
# 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` のようなベアメタルターゲット用にコンパイルする必要があります:
|
||||
|
||||
```bash
|
||||
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]では、この独立したバイナリを最小限の OS カーネルにするために必要なステップを説明しています。カスタムターゲットの作成、実行可能ファイルとブートローダの組み合わせ、画面に何か文字を表示する方法について説明しています。
|
||||
|
||||
[next post]: @/edition-2/posts/02-minimal-rust-kernel/index.ja.md
|
||||
@@ -1,547 +0,0 @@
|
||||
+++
|
||||
title = "Rust로 'Freestanding 실행파일' 만들기"
|
||||
weight = 1
|
||||
path = "ko/freestanding-rust-binary"
|
||||
date = 2018-02-10
|
||||
|
||||
[extra]
|
||||
chapter = "Bare Bones"
|
||||
# Please update this when updating the translation
|
||||
translation_based_on_commit = "c1af4e31b14e562826029999b9ab1dce86396b93"
|
||||
# GitHub usernames of the people that translated this post
|
||||
translators = ["JOE1994"]
|
||||
+++
|
||||
|
||||
운영체제 커널을 만드는 첫 단계는 표준 라이브러리(standard library)를 링크하지 않는 Rust 실행파일을 만드는 것입니다.
|
||||
이러한 실행파일은 운영체제가 없는 [bare metal] 시스템에서 동작할 수 있습니다.
|
||||
|
||||
[bare metal]: https://en.wikipedia.org/wiki/Bare_machine
|
||||
|
||||
<!-- more -->
|
||||
|
||||
이 블로그는 [GitHub 저장소][GitHub]에서 오픈 소스로 개발되고 있으니, 문제나 문의사항이 있다면 저장소의 'Issue' 기능을 이용해 제보해주세요. [페이지 맨 아래][at the bottom]에 댓글을 남기실 수도 있습니다. 이 포스트와 관련된 모든 소스 코드는 저장소의 [`post-01 브랜치`][post branch]에서 확인하실 수 있습니다.
|
||||
|
||||
[GitHub]: https://github.com/phil-opp/blog_os
|
||||
[at the bottom]: #comments
|
||||
<!-- fix for zola anchor checker (target is in template): <a id="comments"> -->
|
||||
[post branch]: https://github.com/phil-opp/blog_os/tree/post-01
|
||||
|
||||
<!-- toc -->
|
||||
|
||||
## 소개
|
||||
운영체제 커널을 만드려면 운영체제에 의존하지 않는 코드가 필요합니다. 자세히 설명하자면, 스레드, 파일, 동적 메모리, 네트워크, 난수 생성기, 표준 출력 및 기타 운영체제의 추상화 또는 특정 하드웨어의 기능을 필요로 하는 것들은 전부 사용할 수 없다는 뜻입니다. 우리는 스스로 운영체제 및 드라이버를 직접 구현하려는 상황이니 어찌 보면 당연한 조건입니다.
|
||||
|
||||
운영체제에 의존하지 않으려면 [Rust 표준 라이브러리][Rust standard library]의 많은 부분을 사용할 수 없습니다.
|
||||
그래도 우리가 이용할 수 있는 Rust 언어 자체의 기능들은 많이 남아 있습니다. 예를 들어 [반복자][iterators], [클로저][closures], [패턴 매칭][pattern matching], [option] / [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로 운영체제 커널을 작성하려면, 운영체제 없이도 실행가능한 실행파일이 필요합니다. 이러한 실행파일은
|
||||
보통 "freestanding 실행파일" 혹은 "bare-metal 실행파일" 이라고 불립니다.
|
||||
|
||||
이 포스트에서는 "freestanding 실행 파일" 을 만드는 데 필요한 것들을 여러 단계로 나누고, 각 단계가 왜 필요한지에 대해 설명해드립니다. 중간 과정은 생략하고 그저 최소한의 예제 코드만 확인하고 싶으시면 **[요약 섹션으로 넘어가시면 됩니다](#summary)**.
|
||||
|
||||
## Rust 표준 라이브러리 링크 해제하기
|
||||
모든 Rust 프로그램들은 Rust 표준 라이브러리를 링크하는데, 이 라이브러리는 스레드, 파일, 네트워킹 등의 기능을 제공하기 위해 운영체제에 의존합니다. Rust 표준 라이브러리는 또한 C 표준 라이브러리인 `libc`에도 의존합니다 (`libc`는 운영체제의 여러 기능들을 이용합니다).
|
||||
우리가 운영체제를 직접 구현하기 위해서는 운영체제를 이용하는 라이브러리들은 사용할 수 없습니다. 그렇기에 우선 [`no_std` 속성][`no_std` attribute]을 이용해 자동으로 Rust 표준 라이브러리가 링크되는 것을 막아야 합니다.
|
||||
|
||||
[standard library]: https://doc.rust-lang.org/std/
|
||||
[`no_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` 인자는 우리가 cargo에게 실행 파일 (라이브러리와 대조됨)을 만들겠다고 알려주고, `--edition 2018` 인자는 cargo에게 우리가 [Rust 2018 에디션][2018 edition]을 사용할 것이라고 알려줍니다.
|
||||
위 명령어를 실행하고 나면, cargo가 아래와 같은 크레이트 디렉토리를 만들어줍니다.
|
||||
|
||||
[2018 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` 명령어를 통해 크레이트를 빌드하면 `target/debug` 디렉토리에 `blog_os` 실행파일이 생성됩니다.
|
||||
|
||||
[semantic version]: https://semver.org/
|
||||
|
||||
### `no_std` 속성
|
||||
|
||||
현재 우리가 만든 크레이트는 암시적으로 Rust 표준 라이브러리를 링크합니다. 아래와 같이 [`no_std` 속성]을 이용해 더 이상 표준 라이브러리가 링크되지 않게 해줍니다.
|
||||
|
||||
```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` 매크로][`println` macro]를 제공하는 Rust 표준 라이브러리를 우리의 크레이트에 링크하지 않게 되었기 때문입니다.
|
||||
`println`은 [표준 입출력][standard output] (운영체제가 제공하는 특별한 파일 서술자)으로 데이터를 쓰기 때문에, 우리는 이제 `println`을 이용해 메세지를 출력할 수 없습니다.
|
||||
|
||||
[`println` 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]` 함수와 _language item_ 을 필요로 함을 확인할 수 있습니다.
|
||||
|
||||
## 패닉 (Panic) 시 호출되는 함수 구현하기
|
||||
|
||||
컴파일러는 [패닉][panic]이 일어날 경우 `panic_handler` 속성이 적용된 함수가 호출되도록 합니다. 표준 라이브러리는 패닉 시 호출되는 함수가 제공되지만, `no_std` 환경에서는 우리가 패닉 시 호출될 함수를 직접 설정해야 합니다.
|
||||
|
||||
[panic]: https://doc.rust-lang.org/stable/book/ch09-01-unrecoverable-errors-with-panic.html
|
||||
|
||||
```rust
|
||||
// in main.rs
|
||||
|
||||
use core::panic::PanicInfo;
|
||||
|
||||
/// 패닉이 일어날 경우, 이 함수가 호출됩니다.
|
||||
#[panic_handler]
|
||||
fn panic(_info: &PanicInfo) -> ! {
|
||||
loop {}
|
||||
}
|
||||
```
|
||||
|
||||
[`PanicInfo` 인자][PanicInfo]는 패닉이 일어난 파일명, 패닉이 파일 내 몇 번째 줄에서 일어났는지, 그리고 패닉시 전달된 메세지에 대한 정보를 가진 구조체입니다.
|
||||
위 `panic` 함수는 절대로 반환하지 않기에 ["never" 타입][“never” type] `!`을 반환하도록 적어 컴파일러에게 이 함수가 [반환 함수][diverging function]임을 알립니다.
|
||||
당장 이 함수에서 우리가 하고자 하는 일은 없기에 그저 함수가 반환하지 않도록 무한루프를 넣어줍니다.
|
||||
|
||||
[PanicInfo]: https://doc.rust-lang.org/nightly/core/panic/struct.PanicInfo.html
|
||||
[diverging function]: 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` Language Item
|
||||
|
||||
Language item은 컴파일러가 내부적으로 요구하는 특별한 함수 및 타입들을 가리킵니다. 예를 들어 [`Copy`] 트레잇은 어떤 타입들이 [_copy semantics_][`Copy`] 를 가지는지 컴파일러에게 알려주는 language item 입니다.
|
||||
[`Copy` 트레잇이 구현된 코드][copy code]에 있는 `#[lang = "copy"]` 속성을 통해 이 트레잇이 language item으로 선언되어 있음을 확인할 수 있습니다.
|
||||
|
||||
[`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
|
||||
|
||||
임의로 구현한 language item을 사용할 수는 있지만, 위험할 수도 있기에 주의해야 합니다.
|
||||
그 이유는 language item의 구현 코드는 매우 자주 변경되어 불안정하며, language item에 대해서 컴파일러가 타입 체크 조차 하지 않습니다 (예시: language item 함수의 인자 타입이 정확한지 조차 체크하지 않습니다).
|
||||
임의로 구현한 language item을 이용하는 것보다 더 안정적으로 위의 language item 오류를 고칠 방법이 있습니다.
|
||||
|
||||
[`eh_personality` language item]은 [스택 되감기 (stack unwinding)][stack unwinding]을 구현하는 함수를 가리킵니다. 기본적으로 Rust는 [패닉][panic]이 일어났을 때 스택 되감기를 통해 스택에 살아있는 각 변수의 소멸자를 호출합니다. 이를 통해 자식 스레드에서 사용 중이던 모든 메모리 리소스가 반환되고, 부모 스레드가 패닉에 대처한 후 계속 실행될 수 있게 합니다. 스택 되감기는 복잡한 과정으로 이루어지며 운영체제마다 특정한 라이브러리를 필요로 하기에 (예: Linux는 [libunwind], Windows는 [structured exception handling]), 우리가 구현할 운영체제에서는 이 기능을 사용하지 않을 것입니다.
|
||||
|
||||
[`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/en-us/windows/win32/debug/structured-exception-handling
|
||||
|
||||
### 스택 되감기를 해제하는 방법
|
||||
|
||||
스택 되감기가 불필요한 상황들이 여럿 있기에, Rust 언어는 [패닉 시 실행 종료][abort on panic] 할 수 있는 선택지를 제공합니다. 이는 스택 되감기에 필요한 심볼 정보 생성을 막아주어 실행 파일의 크기 자체도 많이 줄어들게 됩니다. 스택 되감기를 해제하는 방법은 여러가지 있지만, 가장 쉬운 방법은 `Cargo.toml` 파일에 아래의 코드를 추가하는 것입니다.
|
||||
|
||||
```toml
|
||||
[profile.dev]
|
||||
panic = "abort"
|
||||
|
||||
[profile.release]
|
||||
panic = "abort"
|
||||
```
|
||||
|
||||
위의 코드를 통해 `dev` 빌드 (`cargo build` 실행)와 `release` 빌드 (`cargo build --release` 실행) 에서 모두 패닉 시 실행이 종료되도록 설정되었습니다.
|
||||
이제 더 이상 컴파일러가 `eh_personality` language item을 필요로 하지 않습니다.
|
||||
|
||||
[abort on panic]: https://github.com/rust-lang/rust/pull/32900
|
||||
|
||||
위에서 본 오류들을 고쳤지만, 크레이트를 빌드하려고 하면 새로운 오류가 뜰 것입니다:
|
||||
|
||||
```
|
||||
> cargo build
|
||||
error: requires `start` lang_item
|
||||
```
|
||||
|
||||
우리의 프로그램에는 프로그램 실행 시 최초 실행 시작 지점을 지정해주는 `start` language item이 필요합니다.
|
||||
|
||||
## `start` 속성
|
||||
|
||||
혹자는 프로그램 실행 시 언제나 `main` 함수가 가장 먼저 호출된다고 생각할지도 모릅니다. 대부분의 프로그래밍 언어들은 [런타임 시스템][runtime system]을 가지고 있는데, 이는 가비지 컬렉션 (예시: Java) 혹은 소프트웨어 스레드 (예시: GoLang의 goroutine) 등의 기능을 담당합니다.
|
||||
이러한 런타임 시스템은 프로그램 실행 이전에 초기화 되어야 하기에 `main` 함수 호출 이전에 먼저 호출됩니다.
|
||||
|
||||
[runtime system]: https://en.wikipedia.org/wiki/Runtime_system
|
||||
|
||||
러스트 표준 라이브러리를 링크하는 전형적인 러스트 실행 파일의 경우, 프로그램 실행 시 C 런타임 라이브러리인 `crt0` (“C runtime zero”) 에서 실행이 시작됩니다. `crt0`는 C 프로그램의 환경을 설정하고 초기화하는 런타임 시스템으로, 스택을 만들고 프로그램에 주어진 인자들을 적절한 레지스터에 배치합니다. `crt0`가 작업을 마친 후 `start` language item으로 지정된 [Rust 런타임의 실행 시작 함수][rt::lang_start]를 호출합니다.
|
||||
Rust는 최소한의 런타임 시스템을 가지며, 주요 기능은 스택 오버플로우 가드를 초기화하고 패닉 시 역추적 (backtrace) 정보를 출력하는 것입니다. Rust 런타임의 초기화 작업이 끝난 후에야 `main` 함수가 호출됩니다.
|
||||
|
||||
[rt::lang_start]: https://github.com/rust-lang/rust/blob/bb4d1491466d8239a7a5fd68bd605e3276e97afb/src/libstd/rt.rs#L32-L73
|
||||
|
||||
우리의 "freestanding 실행 파일" 은 Rust 런타임이나 `crt0`에 접근할 수 없기에, 우리가 직접 프로그램 실행 시작 지점을 지정해야 합니다.
|
||||
`crt0`가 `start` language item을 호출해주는 방식으로 동작하기에, `start` language item을 구현하고 지정하는 것만으로는 문제를 해결할 수 없습니다.
|
||||
대신 우리가 직접 `crt0`의 시작 지점을 대체할 새로운 실행 시작 지점을 제공해야 합니다.
|
||||
|
||||
### 실행 시작 지점 덮어쓰기
|
||||
`#![no_main]` 속성을 이용해 Rust 컴파일러에게 우리가 일반적인 실행 시작 호출 단계를 이용하지 않겠다고 선언합니다.
|
||||
|
||||
```rust
|
||||
#![no_std]
|
||||
#![no_main]
|
||||
|
||||
use core::panic::PanicInfo;
|
||||
|
||||
/// 패닉이 일어날 경우, 이 함수가 호출됩니다.
|
||||
#[panic_handler]
|
||||
fn panic(_info: &PanicInfo) -> ! {
|
||||
loop {}
|
||||
}
|
||||
```
|
||||
|
||||
`main` 함수가 사라진 것을 눈치채셨나요? `main` 함수를 호출해주는 런타임 시스템이 없는 이상 `main` 함수의 존재도 더 이상 의미가 없습니다.
|
||||
우리는 운영체제가 호출하는 프로그램 실행 시작 지점 대신 우리의 새로운 `_start` 함수를 실행 시작 지점으로 대체할 것입니다.
|
||||
|
||||
```rust
|
||||
#[no_mangle]
|
||||
pub extern "C" fn _start() -> ! {
|
||||
loop {}
|
||||
}
|
||||
```
|
||||
|
||||
`#[no_mangle]` 속성을 통해 [name mangling]을 해제하여 Rust 컴파일러가 `_start` 라는 이름 그대로 함수를 만들도록 합니다. 이 속성이 없다면, 컴파일러가 각 함수의 이름을 고유하게 만드는 과정에서 이 함수의 실제 이름을 `_ZN3blog_os4_start7hb173fedf945531caE` 라는 이상한 이름으로 바꿔 생성합니다. 우리가 원하는 실제 시작 지점 함수의 이름을 정확히 알고 있어야 링커 (linker)에도 그 이름을 정확히 전달할 수 있기에 (후속 단계에서 진행) `#[no_mangle]` 속성이 필요합니다.
|
||||
|
||||
또한 우리는 이 함수에 `extern "C"`라는 표시를 추가하여 이 함수가 Rust 함수 호출 규약 대신에 [C 함수 호출 규약][C calling convention]을 사용하도록 합니다. 함수의 이름을 `_start`로 지정한 이유는 그저 런타임 시스템들의 실행 시작 함수 이름이 대부분 `_start`이기 때문입니다.
|
||||
|
||||
[name mangling]: https://en.wikipedia.org/wiki/Name_mangling
|
||||
[C calling convention]: https://en.wikipedia.org/wiki/Calling_convention
|
||||
|
||||
`!` 반환 타입은 이 함수가 발산 함수라는 것을 의미합니다. 시작 지점 함수는 오직 운영체제나 부트로더에 의해서만 직접 호출됩니다. 따라서 시작 지점 함수는 반환하는 대신 운영체제의 [`exit` 시스템콜][`exit` system call]을 이용해 종료됩니다. 우리의 "freestanding 실행 파일" 은 실행 종료 후 더 이상 실행할 작업이 없기에, 시작 지점 함수가 작업을 마친 후 기기를 종료하는 것이 합리적입니다. 여기서는 일단 `!` 타입의 조건을 만족시키기 위해 무한루프를 넣어 줍니다.
|
||||
|
||||
[`exit` system call]: https://en.wikipedia.org/wiki/Exit_(system_call)
|
||||
|
||||
다시 `cargo build`를 실행하면, 끔찍한 _링커_ 오류를 마주하게 됩니다.
|
||||
|
||||
## 링커 오류
|
||||
|
||||
링커는 컴파일러가 생성한 코드들을 묶어 실행파일로 만드는 프로그램입니다. 실행 파일 형식은 Linux, Windows, macOS 마다 전부 다르기에 각 운영체제는 자신만의 링커가 있고 링커마다 다른 오류 메세지를 출력할 것입니다.
|
||||
오류가 나는 근본적인 원인은 모두 동일한데, 링커는 주어진 프로그램이 C 런타임 시스템을 이용할 것이라고 가정하는 반면 우리의 크레이트는 그렇지 않기 때문입니다.
|
||||
|
||||
이 링커 오류를 해결하려면 링커에게 C 런타임을 링크하지 말라고 알려줘야 합니다. 두 가지 방법이 있는데, 하나는 링커에 특정 인자들을 주는 것이고, 또다른 하나는 크레이트 컴파일 대상 기기를 bare metal 기기로 설정하는 것입니다.
|
||||
|
||||
### Bare Metal 시스템을 목표로 빌드하기
|
||||
|
||||
기본적으로 Rust는 당신의 현재 시스템 환경에서 실행할 수 있는 실행파일을 생성하고자 합니다. 예를 들어 Windows `x86_64` 사용자의 경우, Rust는 `x86_64` 명령어 셋을 사용하는 `.exe` 확장자 실행파일을 생성합니다. 사용자의 기본 시스템 환경을 "호스트" 시스템이라고 부릅니다.
|
||||
|
||||
여러 다른 시스템 환경들을 표현하기 위해 Rust는 [_target triple_]이라는 문자열을 이용합니다. 현재 호스트 시스템의 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 시스템에서 얻은 것입니다. 호스트 target triple이 `x86_64-unknown-linux-gnu`으로 나오는데, 이는 CPU 아키텍쳐 정보 (`x86_64`)와 하드웨어 판매자 (`unknown`), 운영체제 (`linux`) 그리고 [응용 프로그램 이진 인터페이스 (ABI)][ABI] (`gnu`) 정보를 모두 담고 있습니다.
|
||||
|
||||
[ABI]: https://en.wikipedia.org/wiki/Application_binary_interface
|
||||
|
||||
우리의 호스트 시스템 triple을 위해 컴파일하는 경우, Rust 컴파일러와 링커는 Linux나 Windows와 같은 운영체제가 있다고 가정하고 또한 운영체제가 C 런타임 시스템을 사용할 것이라고 가정하기 때문에 링커 오류 메세지가 출력된 것입니다. 이런 링커 오류를 피하려면 운영체제가 없는 시스템 환경에서 코드가 구동하는 것을 목표로 컴파일해야 합니다.
|
||||
|
||||
운영체제가 없는 bare metal 시스템 환경의 한 예시로 `thumbv7em-none-eabihf` target triple이 있습니다 (이는 [임베디드][embedded] [ARM] 시스템을 가리킵니다). Target triple의 `none`은 시스템에 운영체제가 동작하지 않음을 의미하며, 이 target triple의 나머지 부분의 의미는 아직 모르셔도 괜찮습니다. 이 시스템 환경에서 구동 가능하도록 컴파일하려면 rustup에서 해당 시스템 환경을 추가해야 합니다.
|
||||
|
||||
[embedded]: https://en.wikipedia.org/wiki/Embedded_system
|
||||
[ARM]: https://en.wikipedia.org/wiki/ARM_architecture
|
||||
|
||||
```
|
||||
rustup target add thumbv7em-none-eabihf
|
||||
```
|
||||
|
||||
위 명령어를 실행하면 해당 시스템을 위한 Rust 표준 라이브러리 및 코어 라이브러리를 설치합니다. 이제 해당 target triple을 목표로 하는 freestanding 실행파일을 만들 수 있습니다.
|
||||
|
||||
```
|
||||
cargo build --target thumbv7em-none-eabihf
|
||||
```
|
||||
|
||||
`--target` 인자를 통해 우리가 해당 bare metal 시스템을 목표로 [크로스 컴파일][cross compile]할 것이라는 것을 cargo에게 알려줍니다. 목표 시스템 환경에 운영체제가 없는 것을 링커도 알기 때문에 C 런타임을 링크하려고 시도하지 않으며 이제는 링커 에러 없이 빌드가 성공할 것입니다.
|
||||
|
||||
[cross compile]: https://en.wikipedia.org/wiki/Cross_compiler
|
||||
|
||||
우리는 이 방법을 이용하여 우리의 운영체제 커널을 빌드해나갈 것입니다. 위에서 보인 `thumbv7em-none-eabihf` 시스템 환경 대신 bare metal `x86_64` 시스템 환경을 묘사하는 [커스텀 시스템 환경][custom target]을 설정하여 빌드할 것입니다. 더 자세한 내용은 다음 포스트에서 더 설명하겠습니다.
|
||||
|
||||
[custom target]: https://doc.rust-lang.org/rustc/targets/custom.html
|
||||
|
||||
### 링커 인자
|
||||
|
||||
Bare metal 시스템을 목표로 컴파일하는 대신, 링커에게 특정 인자들을 추가로 주어 링커 오류를 해결하는 방법도 있습니다.
|
||||
이 방법은 앞으로 우리가 작성해나갈 커널 코드를 빌드할 때는 사용하지 않을 것이지만, 더 알고싶어 하실 분들을 위해서 이 섹션을 준비했습니다.
|
||||
아래의 _"링커 인자"_ 텍스트를 눌러 이 섹션의 내용을 확인하세요.
|
||||
|
||||
<details>
|
||||
|
||||
<summary>링커 인자</summary>
|
||||
|
||||
이 섹션에서는 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 런타임의 실행 시작 루틴을 링크하는데, 이 루틴 역시 `_start`라는 이름을 가집니다. 이 `_start` 루틴은 C 표준 라이브러리 (`libc`)가 포함하는 여러 symbol들을 필요로 하지만, 우리는 `no_std` 속성을 이용해 크레이트에서 `libc`를 링크하지 않기 때문에 링커가 몇몇 symbol들의 출처를 찾지 못하여 위와 같은 링커 오류 메세지가 출력되는 것입니다. 이 문제를 해결하려면, 링커에게 `--nostartfiles` 인자를 전달하여 더 이상 링커가 C 런타임의 실행 시작 루틴을 링크하지 않도록 해야 합니다.
|
||||
|
||||
링커에 인자를 전달하는 한 방법은 `cargo rustc` 명령어를 이용하는 것입니다. 이 명령어는 `cargo build`와 유사하게 동작하나, `rustc`(Rust 컴파일러)에 직접 인자를 전달할 수 있게 해줍니다. `rustc`는 `-C link-arg` 인자를 통해 링커에게 인자를 전달할 수 있게 해줍니다. 우리가 이용할 새로운 빌드 명령어는 아래와 같습니다:
|
||||
|
||||
```
|
||||
cargo rustc -- -C link-arg=-nostartfiles
|
||||
```
|
||||
|
||||
이제 우리의 크레이트가 성공적으로 빌드되고 Linux에서 동작하는 freestanding 실행파일이 생성됩니다!
|
||||
|
||||
우리는 위의 빌드 명령어에서 실행 시작 함수의 이름을 명시적으로 전달하지 않았는데, 그 이유는 링커가 기본적으로 `_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에서는 기본 실행 시작 지점의 이름이 [사용 중인 서브시스템(subsystem)에 따라 다릅니다][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
|
||||
```
|
||||
|
||||
Linux에서와는 다른 인자 형식을 통해 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는 [공식적으로는 정적으로 링크된 실행파일을 지원하지 않으며][does not officially support statically linked binaries], 기본적으로 모든 프로그램이 `libSystem` 라이브러리를 링크하도록 요구합니다. 이러한 기본 요구사항을 무시하고 정적으로 링크된 실행 파일을 만드려면 링커에게 `-static` 인자를 주어야 합니다:
|
||||
|
||||
[does not officially support statically linked binaries]: 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`가 실행될 때마다 자동적으로 `rustc`에 인자로 전달됩니다. `.cargo/config.toml`에 대한 더 자세한 정보는 [공식 안내 문서](https://doc.rust-lang.org/cargo/reference/config.html)를 통해 확인해주세요.
|
||||
|
||||
이제 `cargo build` 명령어 만으로 세 가지 플랫폼 어디에서도 우리의 프로그램을 성공적으로 빌드할 수 있습니다.
|
||||
|
||||
#### 이렇게 하는 것이 괜찮나요?
|
||||
|
||||
Linux, Windows 또는 macOS 위에서 동작하는 freestanding 실행파일을 빌드하는 것이 가능하긴 해도 좋은 방법은 아닙니다. 운영체제가 갖춰진 환경을 목표로 빌드를 한다면, 실행 파일 동작 시 다른 많은 조건들이 런타임에 의해 제공될 것이라는 가정 하에 빌드가 이뤄지기 때문입니다 (예: 실행 파일이 `_start` 함수가 호출되는 시점에 이미 스택이 초기화되어있을 것이라고 간주하고 작동합니다). C 런타임 없이는 실행 파일이 필요로 하는 조건들이 갖춰지지 않아 결국 세그멘테이션 오류가 나는 등 프로그램이 제대로 실행되지 못할 수 있습니다.
|
||||
|
||||
이미 존재하는 운영체제 위에서 동작하는 최소한의 실행 파일을 만들고 싶다면, `libc`를 링크하고 [이 곳의 설명](https://doc.rust-lang.org/1.16.0/book/no-stdlib.html)에 따라 `#[start]` 속성을 설정하는 것이 더 좋은 방법일 것입니다.
|
||||
|
||||
</details>
|
||||
|
||||
## 요약 {#summary}
|
||||
|
||||
아래와 같은 최소한의 코드로 "freestanding" Rust 실행파일을 만들 수 있습니다:
|
||||
|
||||
`src/main.rs`:
|
||||
|
||||
```rust
|
||||
#![no_std] // Rust 표준 라이브러리를 링크하지 않도록 합니다
|
||||
#![no_main] // Rust 언어에서 사용하는 실행 시작 지점 (main 함수)을 사용하지 않습니다
|
||||
|
||||
use core::panic::PanicInfo;
|
||||
|
||||
#[no_mangle] // 이 함수의 이름을 mangle하지 않습니다
|
||||
pub extern "C" fn _start() -> ! {
|
||||
// 링커는 기본적으로 '_start' 라는 이름을 가진 함수를 실행 시작 지점으로 삼기에,
|
||||
// 이 함수는 실행 시작 지점이 됩니다
|
||||
loop {}
|
||||
}
|
||||
|
||||
/// 패닉이 일어날 경우, 이 함수가 호출됩니다.
|
||||
#[panic_handler]
|
||||
fn panic(_info: &PanicInfo) -> ! {
|
||||
loop {}
|
||||
}
|
||||
```
|
||||
|
||||
`Cargo.toml`:
|
||||
|
||||
```toml
|
||||
[package]
|
||||
name = "crate_name"
|
||||
version = "0.1.0"
|
||||
authors = ["Author Name <author@example.com>"]
|
||||
|
||||
# `cargo build` 실행 시 이용되는 빌드 설정
|
||||
[profile.dev]
|
||||
panic = "abort" # 패닉 시 스택 되감기를 하지 않고 바로 프로그램 종료
|
||||
|
||||
# `cargo build --release` 실행 시 이용되는 빌드 설정
|
||||
[profile.release]
|
||||
panic = "abort" # 패닉 시 스택 되감기를 하지 않고 바로 프로그램 종료
|
||||
```
|
||||
|
||||
이 실행 파일을 빌드하려면, `thumbv7em-none-eabihf`와 같은 bare metal 시스템 환경을 목표로 컴파일해야 합니다:
|
||||
|
||||
```
|
||||
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"
|
||||
```
|
||||
|
||||
주의할 것은 이것이 정말 최소한의 freestanding Rust 실행 파일이라는 것입니다. 실행 파일은 여러 가지 조건들을 가정하는데, 그 예로 실행파일 동작 시 `_start` 함수가 호출될 때 스택이 초기화되어 있을 것을 가정합니다. **이 freestanding 실행 파일을 이용해 실제로 유용한 작업을 처리하려면 아직 더 많은 코드 구현이 필요합니다**.
|
||||
|
||||
## 다음 단계는 무엇일까요?
|
||||
|
||||
[다음 포스트][next post]에서는 우리의 freestanding 실행 파일을 최소한의 기능을 갖춘 운영체제 커널로 만드는 과정을 단게별로 설명할 것입니다.
|
||||
예시로 커스텀 시스템 환경을 설정하는 방법, 우리의 실행 파일을 부트로더와 합치는 방법, 그리고 화면에 메세지를 출력하는 방법 등에 대해 다루겠습니다.
|
||||
|
||||
[next post]: @/edition-2/posts/02-minimal-rust-kernel/index.md
|
||||
@@ -1,523 +0,0 @@
|
||||
+++
|
||||
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
|
||||
|
||||
<!-- more -->
|
||||
|
||||
Этот блог открыто разрабатывается на [GitHub]. Если у вас возникли какие-либо проблемы или вопросы, пожалуйста, создайте _issue_. Также вы можете оставлять комментарии [в конце страницы][at the bottom]. Полный исходный код для этого поста вы можете найти в репозитории в ветке [`post-01`][post branch].
|
||||
|
||||
[GitHub]: https://github.com/phil-opp/blog_os
|
||||
[at the bottom]: #comments
|
||||
<!-- fix for zola anchor checker (target is in template): <a id="comments"> -->
|
||||
[post branch]: https://github.com/phil-opp/blog_os/tree/post-01
|
||||
|
||||
<!-- toc -->
|
||||
|
||||
## Введение
|
||||
Для того, чтобы написать ядро операционной системы, нужен код, который не зависит от операционной системы и ее возможностей. Это означает, что нельзя использовать потоки, файлы, [кучу][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/ru-ru/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
|
||||
|
||||
### Аргументы компоновщика
|
||||
|
||||
Вместо компиляции под голое железо, ошибки компоновщика можно исправить, передав ему определенный набор аргументов. Мы не будем использовать этот подход для нашего ядра, поэтому данный раздел является необязательным и приводится только для полноты картины. Щелкните на _"Аргументы компоновщика"_ ниже, чтобы показать необязательное содержание.
|
||||
|
||||
<details>
|
||||
|
||||
<summary>Аргументы компоновщика</summary>
|
||||
|
||||
В этом разделе мы рассмотрим ошибки компоновщика, возникающие в 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), вероятно, будет идеей получше.
|
||||
|
||||
</details>
|
||||
|
||||
## Итоги {#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 <author@example.com>"]
|
||||
|
||||
# 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
|
||||
@@ -1,300 +0,0 @@
|
||||
+++
|
||||
title = "独立式可执行程序"
|
||||
weight = 1
|
||||
path = "zh-CN/freestanding-rust-binary"
|
||||
date = 2018-02-10
|
||||
|
||||
[extra]
|
||||
# Please update this when updating the translation
|
||||
translation_based_on_commit = "bd6fbcb1c36705b2c474d7fcee387bfea1210851"
|
||||
# GitHub usernames of the people that translated this post
|
||||
translators = ["luojia65", "Rustin-Liu", "TheBegining"]
|
||||
+++
|
||||
|
||||
创建一个不链接标准库的 Rust 可执行文件,将是我们迈出的第一步。无需底层操作系统的支撑,这样才能在**裸机**([bare metal])上运行 Rust 代码。
|
||||
|
||||
[bare metal]: https://en.wikipedia.org/wiki/Bare_machine
|
||||
|
||||
<!-- more -->
|
||||
|
||||
此博客在 [GitHub] 上公开开发. 如果您有任何问题或疑问,请在此处打开一个 issue。 您也可以在[底部][at the bottom]发表评论. 这篇文章的完整源代码可以在 [`post-01`] [post branch] 分支中找到。
|
||||
|
||||
[GitHub]: https://github.com/phil-opp/blog_os
|
||||
[at the bottom]: #comments
|
||||
<!-- fix for zola anchor checker (target is in template): <a id="comments"> -->
|
||||
[post branch]: https://github.com/phil-opp/blog_os/tree/post-01
|
||||
|
||||
<!-- toc -->
|
||||
|
||||
## 简介
|
||||
|
||||
要编写一个操作系统内核,我们需要编写不依赖任何操作系统特性的代码。这意味着我们不能使用线程、文件、堆内存、网络、随机数、标准输出,或其它任何需要操作系统抽象和特定硬件的特性;因为我们正在编写自己的操作系统和硬件驱动。
|
||||
|
||||
实现这一点,意味着我们不能使用 [Rust标准库](https://doc.rust-lang.org/std/)的大部分;但还有很多 Rust 特性是我们依然可以使用的。比如说,我们可以使用[迭代器](https://doc.rust-lang.org/book/ch13-02-iterators.html)、[闭包](https://doc.rust-lang.org/book/ch13-01-closures.html)、[模式匹配](https://doc.rust-lang.org/book/ch06-00-enums.html)、[Option](https://doc.rust-lang.org/core/option/)、[Result](https://doc.rust-lang.org/core/result/index.html)、[字符串格式化](https://doc.rust-lang.org/core/macro.write.html),当然还有[所有权系统](https://doc.rust-lang.org/book/ch04-00-understanding-ownership.html)。这些功能让我们能够编写表达性强、高层抽象的操作系统,而无需关心[未定义行为](https://www.nayuki.io/page/undefined-behavior-in-c-and-cplusplus-programs)和[内存安全](https://tonyarcieri.com/it-s-time-for-a-memory-safety-intervention)。
|
||||
|
||||
为了用 Rust 编写一个操作系统内核,我们需要创建一个独立于操作系统的可执行程序。这样的可执行程序常被称作**独立式可执行程序**(freestanding executable)或**裸机程序**(bare-metal executable)。
|
||||
|
||||
在这篇文章里,我们将逐步地创建一个独立式可执行程序,并且详细解释为什么每个步骤都是必须的。如果读者只对最终的代码感兴趣,可以跳转到本篇文章的小结部分。
|
||||
|
||||
## 禁用标准库
|
||||
|
||||
在默认情况下,所有的 Rust **包**(crate)都会链接**标准库**([standard library](https://doc.rust-lang.org/std/)),而标准库依赖于操作系统功能,如线程、文件系统、网络。标准库还与 **Rust 的 C 语言标准库实现库**(libc)相关联,它也是和操作系统紧密交互的。既然我们的计划是编写自己的操作系统,我们就需要不使用任何与操作系统相关的库——因此我们必须禁用**标准库自动引用**(automatic inclusion)。使用 [no_std 属性](https://doc.rust-lang.org/book/first-edition/using-rust-without-the-standard-library.html)可以实现这一点。
|
||||
|
||||
我们可以从创建一个新的 cargo 项目开始。最简单的办法是使用下面的命令:
|
||||
|
||||
```bash
|
||||
> cargo new blog_os
|
||||
```
|
||||
|
||||
在这里我把项目命名为 `blog_os`,当然读者也可以选择自己的项目名称。这里,cargo 默认为我们添加了`--bin` 选项,说明我们将要创建一个可执行文件(而不是一个库);cargo还为我们添加了`--edition 2018` 标签,指明项目的包要使用 Rust 的 **2018 版次**([2018 edition])。当我们执行这行指令的时候,cargo 为我们创建的目录结构如下:
|
||||
|
||||
[2018 edition]: https://doc.rust-lang.org/nightly/edition-guide/rust-2018/index.html
|
||||
|
||||
```
|
||||
blog_os
|
||||
├── Cargo.toml
|
||||
└── src
|
||||
└── main.rs
|
||||
```
|
||||
|
||||
在这里,`Cargo.toml` 文件包含了包的**配置**(configuration),比如包的名称、作者、[semver版本](https://semver.org/) 和项目依赖项;`src/main.rs` 文件包含包的**根模块**(root module)和 main 函数。我们可以使用 `cargo build` 来编译这个包,然后在 `target/debug` 文件夹内找到编译好的 `blog_os` 二进制文件。
|
||||
|
||||
### no_std 属性
|
||||
|
||||
现在我们的包依然隐式地与标准库链接。为了禁用这种链接,我们可以尝试添加 [no_std 属性](https://doc.rust-lang.org/book/first-edition/using-rust-without-the-standard-library.html):
|
||||
|
||||
```rust
|
||||
// main.rs
|
||||
|
||||
#![no_std]
|
||||
|
||||
fn main() {
|
||||
println!("Hello, world!");
|
||||
}
|
||||
```
|
||||
|
||||
看起来很顺利。当我们使用 `cargo build` 来编译的时候,却出现了下面的错误:
|
||||
|
||||
```rust
|
||||
error: cannot find macro `println!` in this scope
|
||||
--> src\main.rs:4:5
|
||||
|
|
||||
4 | println!("Hello, world!");
|
||||
| ^^^^^^^
|
||||
```
|
||||
|
||||
出现这个错误的原因是:[println! 宏](https://doc.rust-lang.org/std/macro.println.html)是标准库的一部分,而我们的项目不再依赖于标准库。我们选择不再打印字符串。这也很好理解,因为 `println!` 将会向**标准输出**([standard output](https://en.wikipedia.org/wiki/Standard_streams#Standard_output_.28stdout.29))打印字符,它依赖于特殊的文件描述符,而这是由操作系统提供的特性。
|
||||
|
||||
所以我们可以移除这行代码,使用一个空的 main 函数再次尝试编译:
|
||||
|
||||
```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]` 函数和一个**语言项**(language item)。
|
||||
|
||||
## 实现 panic 处理函数
|
||||
|
||||
`panic_handler` 属性定义了一个函数,它会在一个 panic 发生时被调用。标准库中提供了自己的 panic 处理函数,但在 `no_std` 环境中,我们需要定义一个自己的 panic 处理函数:
|
||||
|
||||
```rust
|
||||
// in main.rs
|
||||
|
||||
use core::panic::PanicInfo;
|
||||
|
||||
/// 这个函数将在 panic 时被调用
|
||||
#[panic_handler]
|
||||
fn panic(_info: &PanicInfo) -> ! {
|
||||
loop {}
|
||||
}
|
||||
```
|
||||
|
||||
类型为 [PanicInfo](https://doc.rust-lang.org/nightly/core/panic/struct.PanicInfo.html) 的参数包含了 panic 发生的文件名、代码行数和可选的错误信息。这个函数从不返回,所以他被标记为**发散函数**([diverging function])。发散函数的返回类型称作 **Never 类型**(["never" type](https://doc.rust-lang.org/nightly/std/primitive.never.html)),记为`!`。对这个函数,我们目前能做的很少,所以我们只需编写一个无限循环 `loop {}`。
|
||||
|
||||
[diverging function]: https://doc.rust-lang.org/1.30.0/book/first-edition/functions.html#diverging-functions
|
||||
|
||||
## eh_personality 语言项
|
||||
|
||||
语言项是一些编译器需求的特殊函数或类型。举例来说,Rust 的 [Copy](https://doc.rust-lang.org/nightly/core/marker/trait.Copy.html) trait 是一个这样的语言项,告诉编译器哪些类型需要遵循**复制语义**([copy semantics](https://doc.rust-lang.org/nightly/core/marker/trait.Copy.html))——当我们查找 `Copy` trait 的[实现](https://github.com/rust-lang/rust/blob/485397e49a02a3b7ff77c17e4a3f16c653925cb3/src/libcore/marker.rs#L296-L299)时,我们会发现,一个特殊的 `#[lang = "copy"]` 属性将它定义为了一个语言项,达到与编译器联系的目的。
|
||||
|
||||
我们可以自己实现语言项,但这是下下策:目前来看,语言项是高度不稳定的语言细节实现,它们不会经过编译期类型检查(所以编译器甚至不确保它们的参数类型是否正确)。幸运的是,我们有更稳定的方式,来修复上面的语言项错误。
|
||||
|
||||
`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)),通常需要依赖于操作系统的库;所以我们不在自己编写的操作系统中使用它。
|
||||
|
||||
### 禁用栈展开
|
||||
|
||||
在其它一些情况下,栈展开并不是迫切需求的功能;因此,Rust 提供了**在 panic 时中止**([abort on panic](https://github.com/rust-lang/rust/pull/32900))的选项。这个选项能禁用栈展开相关的标志信息生成,也因此能缩小生成的二进制程序的长度。有许多方式能打开这个选项,最简单的方式是把下面的几行设置代码加入我们的 `Cargo.toml`:
|
||||
|
||||
```toml
|
||||
[profile.dev]
|
||||
panic = "abort"
|
||||
|
||||
[profile.release]
|
||||
panic = "abort"
|
||||
```
|
||||
|
||||
这些选项能将 **dev 配置**(dev profile)和 **release 配置**(release profile)的 panic 策略设为 `abort`。`dev` 配置适用于 `cargo build`,而 `release` 配置适用于 `cargo build --release`。现在编译器应该不再要求我们提供 `eh_personality` 语言项实现。
|
||||
|
||||
现在我们已经修复了出现的两个错误,可以开始编译了。然而,尝试编译运行后,一个新的错误出现了:
|
||||
|
||||
```bash
|
||||
> cargo build
|
||||
error: requires `start` lang_item
|
||||
```
|
||||
|
||||
## start 语言项
|
||||
|
||||
这里,我们的程序遗失了 `start` 语言项,它将定义一个程序的**入口点**(entry point)。
|
||||
|
||||
我们通常会认为,当运行一个程序时,首先被调用的是 `main` 函数。但是,大多数语言都拥有一个**运行时系统**([runtime system](https://en.wikipedia.org/wiki/Runtime_system)),它通常为**垃圾回收**(garbage collection)或**绿色线程**(software threads,或 green threads)服务,如 Java 的 GC 或 Go 语言的协程(goroutine);这个运行时系统需要在 main 函数前启动,因为它需要让程序初始化。
|
||||
|
||||
在一个典型的使用标准库的 Rust 程序中,程序运行是从一个名为 `crt0` 的运行时库开始的。`crt0` 意为 C runtime zero,它能建立一个适合运行 C 语言程序的环境,这包含了栈的创建和可执行程序参数的传入。在这之后,这个运行时库会调用 [Rust 的运行时入口点](https://github.com/rust-lang/rust/blob/bb4d1491466d8239a7a5fd68bd605e3276e97afb/src/libstd/rt.rs#L32-L73),这个入口点被称作 **start语言项**("start" language item)。Rust 只拥有一个极小的运行时,它被设计为拥有较少的功能,如爆栈检测和打印**堆栈轨迹**(stack trace)。这之后,这个运行时将会调用 main 函数。
|
||||
|
||||
我们的独立式可执行程序并不能访问 Rust 运行时或 `crt0` 库,所以我们需要定义自己的入口点。只实现一个 `start` 语言项并不能帮助我们,因为这之后程序依然要求 `crt0` 库。所以,我们要做的是,直接重写整个 `crt0` 库和它定义的入口点。
|
||||
|
||||
### 重写入口点
|
||||
|
||||
要告诉 Rust 编译器我们不使用预定义的入口点,我们可以添加 `#![no_main]` 属性。
|
||||
|
||||
```rust
|
||||
#![no_std]
|
||||
#![no_main]
|
||||
|
||||
use core::panic::PanicInfo;
|
||||
|
||||
/// 这个函数将在 panic 时被调用
|
||||
#[panic_handler]
|
||||
fn panic(_info: &PanicInfo) -> ! {
|
||||
loop {}
|
||||
}
|
||||
```
|
||||
|
||||
读者也许会注意到,我们移除了 `main` 函数。原因很显然,既然没有底层运行时调用它,`main` 函数也失去了存在的必要性。为了重写操作系统的入口点,我们转而编写一个 `_start` 函数:
|
||||
|
||||
```rust
|
||||
#[no_mangle]
|
||||
pub extern "C" fn _start() -> ! {
|
||||
loop {}
|
||||
}
|
||||
```
|
||||
|
||||
我们使用 `no_mangle` 标记这个函数,来对它禁用**名称重整**([name mangling](https://en.wikipedia.org/wiki/Name_mangling))——这确保 Rust 编译器输出一个名为 `_start` 的函数;否则,编译器可能最终生成名为 `_ZN3blog_os4_start7hb173fedf945531caE` 的函数,无法让链接器正确辨别。
|
||||
|
||||
我们还将函数标记为 `extern "C"`,告诉编译器这个函数应当使用 [C 语言的调用约定](https://en.wikipedia.org/wiki/Calling_convention),而不是 Rust 语言的调用约定。函数名为 `_start` ,是因为大多数系统默认使用这个名字作为入口点名称。
|
||||
|
||||
与前文的 `panic` 函数类似,这个函数的返回值类型为`!`——它定义了一个发散函数,或者说一个不允许返回的函数。这一点很重要,因为这个入口点不会被任何函数调用,但将直接被操作系统或**引导程序**(bootloader)调用。所以作为函数返回的替代,这个入口点应该去调用,比如操作系统提供的 **exit 系统调用**(["exit" system call](https://en.wikipedia.org/wiki/Exit_(system_call)))函数。在我们编写操作系统的情况下,关机应该是一个合适的选择,因为**当一个独立式可执行程序返回时,不会留下任何需要做的事情**(there is nothing to do if a freestanding binary returns)。现在来看,我们可以添加一个无限循环,来满足对返回值类型的需求。
|
||||
|
||||
如果我们现在编译这段程序,会出来一大段不太好看的**链接器错误**(linker error)。
|
||||
|
||||
## 链接器错误
|
||||
|
||||
**链接器**(linker)是一个程序,它将生成的目标文件组合为一个可执行文件。不同的操作系统如 Windows、macOS、Linux,规定了不同的可执行文件格式,因此也各有自己的链接器,抛出不同的错误;但这些错误的根本原因还是相同的:链接器的默认配置假定程序依赖于C语言的运行时环境,但我们的程序并不依赖于它。
|
||||
|
||||
为了解决这个错误,我们需要告诉链接器,它不应该包含(include)C 语言运行环境。我们可以选择提供特定的**链接器参数**(linker argument),也可以选择编译为**裸机目标**(bare metal target)。
|
||||
|
||||
### 编译为裸机目标
|
||||
|
||||
在默认情况下,Rust 尝试适配当前的系统环境,编译可执行程序。举个例子,如果你使用 `x86_64` 平台的 Windows 系统,Rust 将尝试编译一个扩展名为 `.exe` 的 Windows 可执行程序,并使用 `x86_64` 指令集。这个环境又被称作为你的**宿主系统**("host" system)。
|
||||
|
||||
为了描述不同的环境,Rust 使用一个称为**目标三元组**(target triple)的字符串。要查看当前系统的目标三元组,我们可以运行 `rustc --version --verbose`:
|
||||
|
||||
```
|
||||
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`,它包含了 CPU 架构 `x86_64` 、供应商 `unknown` 、操作系统 `linux` 和[二进制接口](https://en.wikipedia.org/wiki/Application_binary_interface) `gnu`。
|
||||
|
||||
Rust 编译器尝试为当前系统的三元组编译,并假定底层有一个类似于 Windows 或 Linux 的操作系统提供C语言运行环境——然而这将导致链接器错误。所以,为了避免这个错误,我们可以另选一个底层没有操作系统的运行环境。
|
||||
|
||||
这样的运行环境被称作裸机环境,例如目标三元组 `thumbv7em-none-eabihf` 描述了一个 ARM **嵌入式系统**([embedded system](https://en.wikipedia.org/wiki/Embedded_system))。我们暂时不需要了解它的细节,只需要知道这个环境底层没有操作系统——这是由三元组中的 `none` 描述的。要为这个目标编译,我们需要使用 rustup 添加它:
|
||||
|
||||
```
|
||||
rustup target add thumbv7em-none-eabihf
|
||||
```
|
||||
|
||||
这行命令将为目标下载一个标准库和 core 库。这之后,我们就能为这个目标构建独立式可执行程序了:
|
||||
|
||||
```
|
||||
cargo build --target thumbv7em-none-eabihf
|
||||
```
|
||||
|
||||
我们传递了 `--target` 参数,来为裸机目标系统**交叉编译**([cross compile](https://en.wikipedia.org/wiki/Cross_compiler))我们的程序。我们的目标并不包括操作系统,所以链接器不会试着链接 C 语言运行环境,因此构建过程成功会完成,不会产生链接器错误。
|
||||
|
||||
我们将使用这个方法编写自己的操作系统内核。我们不会编译到 `thumbv7em-none-eabihf`,而是使用描述 `x86_64` 环境的**自定义目标**([custom target](https://doc.rust-lang.org/rustc/targets/custom.html))。在下一篇文章中,我们将详细描述一些相关的细节。
|
||||
|
||||
### 链接器参数
|
||||
|
||||
我们也可以选择不编译到裸机系统,因为传递特定的参数也能解决链接器错误问题。虽然我们不会在后面使用到这个方法,为了教程的完整性,我们也撰写了专门的短文章,来提供这个途径的解决方案。
|
||||
|
||||
## 小结
|
||||
|
||||
一个用 Rust 编写的最小化的独立式可执行程序应该长这样:
|
||||
|
||||
`src/main.rs`:
|
||||
|
||||
```rust
|
||||
#![no_std] // 不链接 Rust 标准库
|
||||
#![no_main] // 禁用所有 Rust 层级的入口点
|
||||
|
||||
use core::panic::PanicInfo;
|
||||
|
||||
#[no_mangle] // 不重整函数名
|
||||
pub extern "C" fn _start() -> ! {
|
||||
// 因为编译器会寻找一个名为 `_start` 的函数,所以这个函数就是入口点
|
||||
// 默认命名为 `_start`
|
||||
loop {}
|
||||
}
|
||||
|
||||
/// 这个函数将在 panic 时被调用
|
||||
#[panic_handler]
|
||||
fn panic(_info: &PanicInfo) -> ! {
|
||||
loop {}
|
||||
}
|
||||
```
|
||||
|
||||
`Cargo.toml`:
|
||||
|
||||
```toml
|
||||
[package]
|
||||
name = "crate_name"
|
||||
version = "0.1.0"
|
||||
authors = ["Author Name <author@example.com>"]
|
||||
|
||||
# 使用 `cargo build` 编译时需要的配置
|
||||
[profile.dev]
|
||||
panic = "abort" # 禁用panic时栈展开
|
||||
|
||||
# 使用 `cargo build --release` 编译时需要的配置
|
||||
[profile.release]
|
||||
panic = "abort" # 禁用 panic 时栈展开
|
||||
```
|
||||
|
||||
选用任意一个裸机目标来编译。比如对 `thumbv7em-none-eabihf`,我们使用以下命令:
|
||||
|
||||
```bash
|
||||
cargo build --target thumbv7em-none-eabihf
|
||||
```
|
||||
|
||||
要注意的是,现在我们的代码只是一个 Rust 编写的独立式可执行程序的一个例子。运行这个二进制程序还需要很多准备,比如在 `_start` 函数之前需要一个已经预加载完毕的栈。所以为了真正运行这样的程序,我们还有很多事情需要做。
|
||||
|
||||
## 下篇预览
|
||||
|
||||
下一篇文章要做的事情基于我们这篇文章的成果,它将详细讲述编写一个最小的操作系统内核需要的步骤:如何配置特定的编译目标,如何将可执行程序与引导程序拼接,以及如何把一些特定的字符串打印到屏幕上。
|
||||
|
||||
[next post]: @/edition-2/posts/02-minimal-rust-kernel/index.md
|
||||
@@ -1,520 +0,0 @@
|
||||
+++
|
||||
title = "獨立的 Rust 二進制檔"
|
||||
weight = 1
|
||||
path = "zh-TW/freestanding-rust-binary"
|
||||
date = 2018-02-10
|
||||
|
||||
[extra]
|
||||
# Please update this when updating the translation
|
||||
translation_based_on_commit = "24d04e0e39a3395ecdce795bab0963cb6afe1bfd"
|
||||
# GitHub usernames of the people that translated this post
|
||||
translators = ["wusyong"]
|
||||
+++
|
||||
|
||||
建立我們自己的作業系統核心的第一步是建立一個不連結標準函式庫的 Rust 執行檔,這使得無需基礎作業系統即可在[裸機][bare metal]上執行 Rust 程式碼。
|
||||
|
||||
[bare metal]: https://en.wikipedia.org/wiki/Bare_machine
|
||||
|
||||
<!-- more -->
|
||||
|
||||
此網誌在 [GitHub] 上公開開發,如果您有任何問題或疑問,請在那開一個 issue,您也可以在[下面][at the bottom]發表評論,這篇文章的完整開源程式碼可以在 [`post-01`][post branch] 分支中找到。
|
||||
|
||||
[GitHub]: https://github.com/phil-opp/blog_os
|
||||
[at the bottom]: #comments
|
||||
<!-- fix for zola anchor checker (target is in template): <a id="comments"> -->
|
||||
[post branch]: https://github.com/phil-opp/blog_os/tree/post-01
|
||||
|
||||
<!-- toc -->
|
||||
|
||||
## 介紹
|
||||
要編寫作業系統核心,我們需要不依賴於任何作業系統功能的程式碼。這代表我們不能使用執行緒、檔案系統、堆記憶體、網路、隨機數、標準輸出或任何其他需要作業系統抽象或特定硬體的功能。這也是理所當然的,因為我們正在嘗試寫出自己的 OS 和我們的驅動程式。
|
||||
|
||||
這意味著我們不能使用大多數的 [Rust 標準函式庫][Rust standard library],但是我們還是可以使用 _很多_ Rust 的功能。比如說我們可以使用[疊代器][iterators]、[閉包][closures]、[模式配對][pattern matching]、[option] 和 [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 中建立 OS 核心,我們需要建立一個無須底層作業系統即可運行的執行檔,這類的執行檔通常稱為「獨立式(freestanding)」或「裸機(bare-metal)」的執行檔。
|
||||
|
||||
這篇文章描述了建立一個獨立的 Rust 執行檔的必要步驟,並解釋為什麼需要這些步驟。如果您只對簡單的範例感興趣,可以直接跳到 **[總結](#summary)**。
|
||||
|
||||
## 停用標準函式庫
|
||||
|
||||
Rust 所有的 crate 在預設情況下都會連結[標準函式庫][standard library],而標準函式庫會依賴作業系統的功能,像式執行緒、檔案系統或是網路。它也會依賴 C 語言的標準函式庫 `libc`,因為其與作業系統緊密相關。既然我們的計劃是編寫自己的作業系統,我們就得用到 [`no_std` 屬性][`no_std` attribute]來停止標準函式庫的自動引用(automatic inclusion)。
|
||||
|
||||
[standard library]: https://doc.rust-lang.org/std/
|
||||
[`no_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` 選項指明我們的 crate 想使用 Rust [2018 版本][2018 edition]。當我們執行這行指令的時候,cargo 會為我們建立以下目錄結構:
|
||||
|
||||
[2018 edition]: https://doc.rust-lang.org/nightly/edition-guide/rust-2018/index.html
|
||||
|
||||
```
|
||||
blog_os
|
||||
├── Cargo.toml
|
||||
└── src
|
||||
└── main.rs
|
||||
```
|
||||
|
||||
`Cargo.toml` 包含 crate 的設置,像是 crate 的名稱、作者、[語意化版本][semantic version]以及依賴套件。`src/main.rs` 檔案則包含 crate 的根模組(root module)以及我們的 `main` 函式。您可以用 `cargo build` 編譯您的 crate 然後在 `target/debug` 目錄下運行編譯過後的 `blog_os` 執行檔。
|
||||
|
||||
[semantic version]: https://semver.org/lang/zh-TW/
|
||||
|
||||
### no_std 屬性
|
||||
|
||||
現在我們的 crate 背後依然有和標準函式庫連結。讓我們加上 [`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` macro]是標準函式庫的一部份,而我們不再包含它,所以我們無法再輸出東西來。這也是理所當然因為 `println` 會寫到[標準輸出][standard output],而這是一個由作業系統提供的特殊檔案描述符。
|
||||
|
||||
[`println` macro]: https://doc.rust-lang.org/std/macro.println.html
|
||||
[standard output]: https://en.wikipedia.org/wiki/Standard_streams#Standard_output_.28stdout.29
|
||||
|
||||
所以讓我們移除這行程式碼,然後用空的 main 函式再試一次:
|
||||
|
||||
```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]` 函式以及 _language item_。
|
||||
|
||||
## 實作 panic 處理函式
|
||||
|
||||
`panic_handler` 屬性定義了當 [panic] 發生時編譯器需要呼叫的函式。在標準函式庫中有自己的 panic 處理函式,但在 `no_std` 的環境中我們得定義我們自己的:
|
||||
|
||||
[panic]: https://doc.rust-lang.org/stable/book/ch09-01-unrecoverable-errors-with-panic.html
|
||||
|
||||
```rust
|
||||
// main.rs
|
||||
|
||||
use core::panic::PanicInfo;
|
||||
|
||||
/// 此函式會在 panic 時呼叫。
|
||||
#[panic_handler]
|
||||
fn panic(_info: &PanicInfo) -> ! {
|
||||
loop {}
|
||||
}
|
||||
```
|
||||
|
||||
[`PanicInfo` parameter][PanicInfo] 包含 panic 發生時的檔案、行數以及可選的錯誤訊息。這個函式不會返回,所以它被標記為[發散函式][diverging function],只會返回[“never” 型態][“never” type] `!`。現在我們什麼事可以做,所以我們只需寫一個無限迴圈。
|
||||
|
||||
[PanicInfo]: https://doc.rust-lang.org/nightly/core/panic/struct.PanicInfo.html
|
||||
[diverging function]: 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 Language Item
|
||||
|
||||
Language item 是一些編譯器需求的特殊函式或類型。舉例來說,Rust 的 [`Copy`] trait 就是一個 language item,告訴編譯器哪些類型擁有[_複製的語意_][`Copy`]。當我們搜尋 `Copy` trait 的[實作][copy code]時,我們會發現一個特殊的 `#[lang = "copy"]` 屬性將它定義為一個 language item。
|
||||
|
||||
我們可以自己實現 language item,但這只應是最後的手段。因為 language item 屬於非常不穩定的實作細節,而且不會做類型檢查(所以編譯器甚至不會確保它們的參數類型是否正確)。幸運的是,我們有更穩定的方式來修復上面關於 language item 的錯誤。
|
||||
|
||||
`eh_personality` language item 標記的函式將被用於實作[堆疊回溯][stack unwinding]。在預設情況下當 panic 發生時,Rust 會使用堆疊回溯來執行所有存在堆疊上變數的解構子(destructor)。這確保所有使用的記憶體都被釋放,並讓 parent thread 獲取 panic 資訊並繼續運行。但是堆疊回溯是一個複雜的過程,通常會需要一些 OS 的函式庫如 Linux 的 [libunwind] 或 Windows 的 [structured exception handling]。所以我們並不希望在我們的作業系統中使用它。
|
||||
|
||||
[stack unwinding]: https://www.bogotobogo.com/cplusplus/stackunwinding.php
|
||||
[libunwind]: https://www.nongnu.org/libunwind/
|
||||
[structured exception handling]: https://docs.microsoft.com/en-us/windows/win32/debug/structured-exception-handling
|
||||
|
||||
### 停用回溯
|
||||
|
||||
在某些狀況下回溯可能並不是我們要的功能,因此 Rust 提供了[在 panic 時中止][abort on panic]的選項。這個選項能停用回溯標誌訊息的產生,也因此能縮小生成的二進制檔案大小。我們能用許多方式開啟這個選項,而最簡單的方式就是把以下幾行設置加入我們的 `Cargo.toml`:
|
||||
|
||||
```toml
|
||||
[profile.dev]
|
||||
panic = "abort"
|
||||
|
||||
[profile.release]
|
||||
panic = "abort"
|
||||
```
|
||||
|
||||
這些選項能將 `dev` 設置(用於 `cargo build`)和 `release` 設置(用於 `cargo build --release`)的 panic 策略設為 `abort`。現在編譯器不會再要求我們提供 `eh_personality` language item。
|
||||
|
||||
[abort on panic]: https://github.com/rust-lang/rust/pull/32900
|
||||
|
||||
現在我們已經修復了上面的錯誤,但是如果我們嘗試編譯的話,又會出現一個新的錯誤:
|
||||
|
||||
```
|
||||
> cargo build
|
||||
error: requires `start` lang_item
|
||||
```
|
||||
|
||||
我們的程式缺少 `start` 這個用來定義入口點(entry point)的 language item。
|
||||
|
||||
## `start` 屬性
|
||||
|
||||
我們通常會認為執行一個程式時,首先被呼叫的是 `main` 函式。但是大多數語言都擁有一個[執行時系統][runtime system],它通常負責垃圾回收(garbage collection)像是 Java 或軟體執行緒(software threads)像是 Go 的 goroutines。這個執行時系統需要在 main 函式前啟動,因為它需要讓先進行初始化。
|
||||
|
||||
[runtime system]: https://en.wikipedia.org/wiki/Runtime_system
|
||||
|
||||
在一個典型使用標準函式庫的 Rust 程式中,程式運行是從一個名為 `crt0`(“C runtime zero”)的執行時函式庫開始的,它會設置 C 程式的執行環境。這包含建立堆疊和可執行程式參數的傳入。在這之後,這個執行時函式庫會呼叫 [Rust 的執行時入口點][rt::lang_start],而此處就是由 `start` language item 標記。 Rust 只有一個非常小的執行時系統,負責處理一些小事情,像是堆疊溢位或是印出 panic 時回溯的訊息。再來執行時系統最終才會呼叫 main 函式。
|
||||
|
||||
[rt::lang_start]: https://github.com/rust-lang/rust/blob/bb4d1491466d8239a7a5fd68bd605e3276e97afb/src/libstd/rt.rs#L32-L73
|
||||
|
||||
我們的獨立式可執行檔並沒有辦法存取 Rust 執行時系統或 `crt0`,所以我們需要定義自己的入口點。實作 `start` language item 並沒有用,因為這樣還是會需要 `crt0`。所以我們要做的是直接覆寫 `crt0` 的入口點。
|
||||
|
||||
### 重寫入口點
|
||||
|
||||
為了告訴 Rust 編譯器我們不要使用一般的入口點呼叫順序,我們先加上 `#![no_main]` 屬性。
|
||||
|
||||
```rust
|
||||
#![no_std]
|
||||
#![no_main]
|
||||
|
||||
use core::panic::PanicInfo;
|
||||
|
||||
/// 此函式會在 panic 時呼叫。
|
||||
#[panic_handler]
|
||||
fn panic(_info: &PanicInfo) -> ! {
|
||||
loop {}
|
||||
}
|
||||
```
|
||||
|
||||
您可能會注意到我們移除了 `main` 函式,原因是因為既然沒有了底層的執行時系統呼叫,那麼 `main` 也沒必要存在。我們要重寫作業系統的入口點,定義為 `_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
|
||||
|
||||
`!` 返回型態代表這個函式是發散函式,它不允許返回。這是必要的因為入口點不會被任何函式呼叫,只會直接由作業系統或啟動程式(bootloader)執行。所以取代返回值的是入口點需要執行作業系統的 [`exit` 系統呼叫][`exit` system call]。在我們的例子中,關閉機器似乎是個理想的動作,因為獨立的二進制檔案返回後也沒什麼事可做。現在我們先寫一個無窮迴圈來滿足需求。
|
||||
|
||||
[`exit` system call]: https://en.wikipedia.org/wiki/Exit_(system_call)
|
||||
|
||||
當我們現在運行 `cargo build` 的話會看到很醜的 _連結器_ 錯誤。
|
||||
|
||||
## 連結器錯誤
|
||||
|
||||
連結器是用來將產生的程式碼結合起來成為執行檔的程式。因為 Linux、Windows 和 macOS 之間的執行檔格式都不同,每個系統都會有自己的連結器錯誤。不過造成錯誤的原因通常都差不多:連結器預設的設定會認為我們的程式依賴於 C 的執行時系統,但我們並沒有。
|
||||
|
||||
為了解決這個錯誤,我們需要告訴連結器它不需要包含 C 的執行時系統。我們可以選擇提供特定的連結器參數設定,或是選擇編譯為裸機目標。
|
||||
|
||||
### 編譯為裸機目標
|
||||
|
||||
Rust 在預設情況下會嘗試編譯出符合你目前系統環境的可執行檔。舉例來說,如果你正在 `x86_64` 上使用 Windows,那麼 Rust 就會嘗試編譯出 `.exe`,一個使用 `x86_64` 指令集的 Windows 執行檔。這樣的環境稱之為主機系統(host system)。
|
||||
|
||||
為了描述不同環境,Rust 使用 [_target triple_] 的字串。要查看目前系統的 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` 的 target triple 為 `x86_64-unknown-linux-gnu`,分別代表 CPU 架構 (`x86_64`)、供應商 (`unknown`) 以及作業系統 (`linux`) 和 [ABI] (`gnu`)。
|
||||
|
||||
[ABI]: https://en.wikipedia.org/wiki/Application_binary_interface
|
||||
|
||||
在依據主機的 triple 編譯時,Rust 編譯器和連結器理所當然地會認為預設是底層的作業系統並使用 C 執行時系統,這便是造成錯誤的原因。要避免這項錯誤,我們可以選擇編譯出沒有底層作業系統的不同環境。
|
||||
|
||||
其中一個裸機環境的例子是 `thumbv7em-none-eabihf` target triple,它描述了[嵌入式][embedded] [ARM] 系統。其中的細節目前並不重要,我們現在只需要知道沒有底層作業系統的 target triple 是用 `none` 描述的。想要編譯這樣的目標的話,我們需要將它新增至 rustup:
|
||||
|
||||
[embedded]: https://en.wikipedia.org/wiki/Embedded_system
|
||||
[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 執行時系統並成功建立,不會產生任何連結器錯誤。
|
||||
|
||||
[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
|
||||
|
||||
### 連結器引數
|
||||
|
||||
除了編譯裸機系統為目標以外,我們也可以傳入特定的引數組合給連結器來解決錯誤。這不會是我們到時候用在我們核心的方法,所以以下的內容不是必需的,只是用來補齊資訊。點選下面的 _「連結器引數」_ 來顯示額外資訊。
|
||||
|
||||
<details>
|
||||
|
||||
<summary>連結器引數</summary>
|
||||
|
||||
在這部份我們將討論 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 的執行時系統,而且剛好也叫做 `_start`。它需要一些 C 標準函式庫 `libc` 提供的符號,但我們用 `no_std` 來停用它了,所以連結器無法找出引用來源。我們可以用 `-nostartfiles` 來告訴連結器一開始不必連結 C 的執行時系統。
|
||||
|
||||
要傳入的其中一個方法是透過 cargo 的 `cargo rustc` 命令,此命令行為和 `cargo build` 一樣,不過允許傳入一些選項到 Rust 底層的編譯器 `rustc`。`rustc` 有 `-C link-arg` 的選項會繼續將引數傳到連結器,這樣一來我們的指令會長得像這樣:
|
||||
|
||||
```
|
||||
cargo rustc -- -C link-arg=-nostartfiles
|
||||
```
|
||||
|
||||
現在我們的 crate 便能產生出 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
|
||||
|
||||
以下是 Linux 上會出現的(簡化過)連結器錯誤:
|
||||
|
||||
```
|
||||
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 [官方並不支援靜態連結執行檔][does not officially support statically linked binaries]且要求程式預設要連結到 `libSystem` 函式庫。要覆蓋這個設定並連結靜態執行檔,我們傳入 `-static` 給連結器:
|
||||
|
||||
[does not officially support statically linked binaries]: 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
|
||||
# in .cargo/config
|
||||
|
||||
[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` 的資訊請參考[官方文件][official documentation](https://doc.rust-lang.org/cargo/reference/config.html)。
|
||||
|
||||
這樣一來我們就能同時在三個平台只用 `cargo build` 來建立了。
|
||||
|
||||
#### 你該這麼作嗎?
|
||||
|
||||
雖然我們可以在 Linux、Windows 和 macOS 上建立獨立執行檔,不過這可能不是好主意。我們目前會需要這樣做的原因是因為我們的執行檔仍然需要仰賴一些事情,像是當 `_start` 函式呼叫時堆疊已經初始化完畢。少了 C 執行時系統,有些要求可能會無法達成,造成我們的程式失效,像是 segmentation fault。
|
||||
|
||||
如果你想要建立一個運行在已存作業系統上的最小執行檔,改用 `libc` 然後如這邊[所述](https://doc.rust-lang.org/1.16.0/book/no-stdlib.html)設置 `#[start]` 屬性可能會是更好的做法。
|
||||
|
||||
</details>
|
||||
|
||||
## 總結 {#summary}
|
||||
|
||||
一個最小的 Rust 獨立執行檔會看起來像這樣:
|
||||
|
||||
`src/main.rs`:
|
||||
|
||||
```rust
|
||||
#![no_std] // 不連結標準函式庫
|
||||
#![no_main] // 停用 Rust 層級的入口點
|
||||
|
||||
use core::panic::PanicInfo;
|
||||
|
||||
#[no_mangle] // 不修飾函式名稱
|
||||
pub extern "C" fn _start() -> ! {
|
||||
// 因為連結器預設會尋找 `_start` 函式名稱
|
||||
// 所以這個函式就是入口點
|
||||
loop {}
|
||||
}
|
||||
|
||||
/// 此函式會在 panic 時呼叫
|
||||
#[panic_handler]
|
||||
fn panic(_info: &PanicInfo) -> ! {
|
||||
loop {}
|
||||
}
|
||||
```
|
||||
|
||||
`Cargo.toml`:
|
||||
|
||||
```toml
|
||||
[package]
|
||||
name = "crate_name"
|
||||
version = "0.1.0"
|
||||
authors = ["Author Name <author@example.com>"]
|
||||
|
||||
# `cargo build` 時需要的設置
|
||||
[profile.dev]
|
||||
panic = "abort" # 停用 panic 時堆疊回溯
|
||||
|
||||
# `cargo build --release` 時需要的設置
|
||||
[profile.release]
|
||||
panic = "abort" # 停用 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] 將會講解如何將我們的獨立執行檔轉成最小的作業系統核心。這包含建立自訂目標、用啟動程式組合我們的執行檔,還有學習如何輸出一些東西到螢幕上。
|
||||
|
||||
[next post]: @/edition-2/posts/02-minimal-rust-kernel/index.md
|
||||
@@ -1,6 +0,0 @@
|
||||
+++
|
||||
title = "Extra Posts for Minimal Rust Kernel"
|
||||
sort_by = "weight"
|
||||
insert_anchor_links = "left"
|
||||
render = false
|
||||
+++
|
||||
@@ -1,29 +0,0 @@
|
||||
+++
|
||||
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
|
||||
|
||||
<!-- more -->
|
||||
|
||||

|
||||
|
||||
На рисунке показан стековый фрейм функции с `n` локальных переменных. При входе в функцию указатель стека корректируется, чтобы освободить место в стеке для адреса возврата и локальных переменных.
|
||||
|
||||
Красная зона определяется как 128 байт ниже скорректированного указателя стека. Функция может использовать эту зону для временных данных, которые не нужны при всех вызовах функции. Таким образом, в некоторых случаях (например, в небольших листовых функциях) можно обойтись без двух инструкций для корректировки указателя стека.
|
||||
|
||||
Однако такая оптимизация приводит к огромным проблемам при работе с исключениями или аппаратными прерываниями. Предположим, что во время использования функцией красной зоны происходит исключение:
|
||||
|
||||

|
||||
|
||||
Процессор и обработчик исключений перезаписывают данные в красной зоне. Но эти данные все еще нужны прерванной функции. Поэтому функция не будет работать правильно, когда мы вернемся из обработчика исключений. Это может привести к странным ошибкам, на отладку которых [уйдут недели][take weeks to debug].
|
||||
|
||||
[take weeks to debug]: https://forum.osdev.org/viewtopic.php?t=21720
|
||||
|
||||
Чтобы избежать подобных ошибок при реализации обработки исключений в будущем, мы отключим красную зону с самого начала. Это достигается путем добавления строки `"disable-redzone": true` в наш целевой конфигурационный файл.
|
||||
@@ -1,44 +0,0 @@
|
||||
+++
|
||||
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
|
||||
|
||||
<!-- more -->
|
||||
|
||||
- [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"
|
||||
```
|
||||
@@ -1,504 +0,0 @@
|
||||
+++
|
||||
title = "یک هسته مینیمال با Rust"
|
||||
weight = 2
|
||||
path = "fa/minimal-rust-kernel"
|
||||
date = 2018-02-10
|
||||
|
||||
[extra]
|
||||
chapter = "Bare Bones"
|
||||
# Please update this when updating the translation
|
||||
translation_based_on_commit = "7212ffaa8383122b1eb07fe1854814f99d2e1af4"
|
||||
# GitHub usernames of the people that translated this post
|
||||
translators = ["hamidrezakp", "MHBahrampour"]
|
||||
rtl = true
|
||||
+++
|
||||
|
||||
در این پست ما برای معماری x86 یک هسته مینیمال ۶۴ بیتی به زبان راست میسازیم. با استفاده از باینری مستقل Rust از پست قبل، یک دیسک ایمیج قابل بوت میسازیم، که متنی را در صفحه چاپ کند.
|
||||
|
||||
[باینری مستقل Rust]: @/edition-2/posts/01-freestanding-rust-binary/index.md
|
||||
|
||||
<!-- more -->
|
||||
|
||||
این بلاگ بصورت آزاد روی [گیتهاب] توسعه داده شده است. اگر شما مشکل یا سوالی دارید، لطفاً آنجا یک ایشو باز کنید. شما همچنین میتوانید [در زیر] این پست کامنت بگذارید. منبع کد کامل این پست را میتوانید در بِرَنچ [`post-02`][post branch] پیدا کنید.
|
||||
|
||||
[گیتهاب]: https://github.com/phil-opp/blog_os
|
||||
[در زیر]: #comments
|
||||
<!-- fix for zola anchor checker (target is in template): <a id="comments"> -->
|
||||
[post branch]: https://github.com/phil-opp/blog_os/tree/post-02
|
||||
|
||||
<!-- toc -->
|
||||
|
||||
## فرآیند بوت شدن
|
||||
وقتی یک رایانه را روشن میکنید، شروع به اجرای کد فِرْموِر (کلمه: firmware) ذخیره شده در [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، دو استاندارد فِرْموِر (کلمه: firmware) وجود دارد: «سامانهٔ ورودی/خروجیِ پایه» (**[BIOS]**) و استاندارد جدیدتر «رابط فِرْموِر توسعه یافته یکپارچه» (**[UEFI]**). استاندارد BIOS قدیمی و منسوخ است، اما ساده است و از دهه ۱۹۸۰ تاکنون در هر دستگاه x86 کاملاً پشتیبانی میشود. در مقابل، UEFI مدرنتر است و ویژگیهای بسیار بیشتری دارد، اما راه اندازی آن پیچیدهتر است (حداقل به نظر من).
|
||||
|
||||
[BIOS]: https://en.wikipedia.org/wiki/BIOS
|
||||
[UEFI]: https://en.wikipedia.org/wiki/Unified_Extensible_Firmware_Interface
|
||||
|
||||
در حال حاضر، ما فقط پشتیبانی BIOS را ارائه میدهیم، اما پشتیبانی از UEFI نیز برنامهریزی شده است. اگر میخواهید در این زمینه به ما کمک کنید، [ایشو گیتهاب](https://github.com/phil-opp/blog_os/issues/349) را بررسی کنید.
|
||||
|
||||
### بوت شدن BIOS
|
||||
|
||||
تقریباً همه سیستمهای x86 از بوت شدن BIOS پشتیبانی میکنند، از جمله سیستمهای جدیدترِ مبتنی بر UEFI که از BIOS شبیهسازی شده استفاده میکنند. این عالی است، زیرا شما میتوانید از منطق بوت یکسانی در تمام سیستمهای قرنهای گذشته استفاده کنید. اما این سازگاری گسترده در عین حال بزرگترین نقطه ضعف راهاندازی BIOS است، زیرا این بدان معناست که پردازنده قبل از بوت شدن در یک حالت سازگاری 16 بیتی به نام [real mode] قرار داده میشود تا بوتلودرهای قدیمی از دهه 1980 همچنان کار کنند.
|
||||
|
||||
اما بیایید از ابتدا شروع کنیم:
|
||||
|
||||
وقتی یک رایانه را روشن میکنید، BIOS را از حافظه فلش مخصوصی که روی مادربرد قرار دارد بارگذاری میکند. BIOS روالهای خودآزمایی و مقداردهی اولیه سخت افزار را اجرا می کند، سپس به دنبال دیسکهای قابل بوت میگردد. اگر یکی را پیدا کند، کنترل به _بوتلودرِ_ آن منتقل میشود، که یک قسمت ۵۱۲ بایتی از کد اجرایی است و در ابتدای دیسک ذخیره شده است. بیشتر بوتلودرها از ۵۱۲ بایت بزرگتر هستند، بنابراین بوتلودرها معمولاً به یک قسمت کوچک ابتدایی تقسیم میشوند که در ۵۱۲ بایت جای میگیرد و قسمت دوم که متعاقباً توسط قسمت اول بارگذاری میشود.
|
||||
|
||||
بوتلودر باید محل ایمیج هسته را بر روی دیسک تعیین کرده و آن را در حافظه بارگذاری کند. همچنین ابتدا باید CPU را از [real mode] (ترجمه: حالت واقعی) 16 بیتی به [protected mode] (ترجمه: حالت محافظت شده) 32 بیتی و سپس به [long mode] (ترجمه: حالت طولانی) 64 بیتی سوییچ کند، جایی که ثباتهای 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
|
||||
|
||||
اگر علاقهمند به ساخت بوتلودر هستید: با ما همراه باشید، مجموعهای از پستها در این زمینه از قبل برنامهریزی شده است! <!-- , check out our “_[Writing a Bootloader]_” posts, where we explain in detail how a bootloader is built. -->
|
||||
|
||||
#### استاندارد بوت چندگانه
|
||||
|
||||
برای جلوگیری از این که هر سیستم عاملی بوتلودر خود را پیادهسازی کند، که فقط با یک سیستم عامل سازگار است، [بنیاد نرم افزار آزاد] در سال 1995 یک استاندارد بوتلودر آزاد به نام [Multiboot] ایجاد کرد. این استاندارد یک رابط بین بوتلودر و سیستم عامل را تعریف میکند، به طوری که هر بوتلودر سازگار با Multiboot میتواند هر سیستم عامل سازگار با Multiboot را بارگذاری کند. پیادهسازی مرجع [GNU GRUB] است که محبوبترین بوتلودر برای سیستمهای لینوکس است.
|
||||
|
||||
[بنیاد نرم افزار آزاد]: 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 header] را در ابتدای فایل هسته اضافه کنید. با این کار بوت کردن سیستم عامل در GRUB بسیار آسان خواهد شد. با این حال، GRUB و استاندارد Multiboot نیز دارای برخی مشکلات هستند:
|
||||
|
||||
[Multiboot header]: https://www.gnu.org/software/grub/manual/multiboot/multiboot.html#OS-image-format
|
||||
|
||||
- آنها فقط از حالت محافظت شده 32 بیتی پشتیبانی میکنند. این بدان معناست که شما برای تغییر به حالت طولانی 64 بیتی هنوز باید پیکربندی CPU را انجام دهید.
|
||||
- آنها برای ساده سازی بوتلودر طراحی شدهاند نه برای ساده سازی هسته. به عنوان مثال، هسته باید با [اندازه صفحه پیش فرض تنظیم شده] پیوند داده شود، زیرا GRUB در غیر اینصورت نمیتواند هدر Multiboot را پیدا کند. مثال دیگر این است که [اطلاعات بوت]، که به هسته منتقل میشوند، به جای ارائه انتزاعات تمیز و واضح، شامل ساختارها با وابستگی زیاد به معماری هستند.
|
||||
- هر دو استاندارد GRUB و Multiboot بصورت ناقص مستند شدهاند.
|
||||
- برای ایجاد یک ایمیج دیسکِ قابل بوت از فایل هسته، GRUB باید روی سیستم میزبان نصب شود. این امر باعث دشوارتر شدنِ توسعه در ویندوز یا Mac میشود.
|
||||
|
||||
[اندازه صفحه پیش فرض تنظیم شده]: https://wiki.osdev.org/Multiboot#Multiboot_2
|
||||
[اطلاعات بوت]: https://www.gnu.org/software/grub/manual/multiboot/multiboot.html#Boot-information-format
|
||||
|
||||
به دلیل این اشکالات ما تصمیم گرفتیم از GRUB یا استاندارد Multiboot استفاده نکنیم. با این حال، ما قصد داریم پشتیبانی Multiboot را به ابزار [bootimage] خود اضافه کنیم، به طوری که امکان بارگذاری هسته شما بر روی یک سیستم با بوتلودر GRUB نیز وجود داشته باشد. اگر علاقهمند به نوشتن هسته سازگار با Multiboot هستید، [نسخه اول] مجموعه پستهای این وبلاگ را بررسی کنید.
|
||||
|
||||
[نسخه اول]: @/edition-1/_index.md
|
||||
|
||||
### UEFI
|
||||
|
||||
(ما در حال حاضر پشتیبانی UEFI را ارائه نمیدهیم، اما خیلی دوست داریم این کار را انجام دهیم! اگر میخواهید کمک کنید، لطفاً در [ایشو گیتهاب](https://github.com/phil-opp/blog_os/issues/349) به ما بگویید.)
|
||||
|
||||
## یک هسته مینیمال
|
||||
|
||||
اکنون که تقریباً میدانیم چگونه یک کامپیوتر بوت میشود، وقت آن است که هسته مینیمال خودمان را ایجاد کنیم. هدف ما ایجاد دیسک ایمیجی میباشد که “!Hello World” را هنگام بوت شدن چاپ کند. برای این منظور از [باینری مستقل Rust] که در پست قبل دیدید استفاده میکنیم.
|
||||
|
||||
همانطور که ممکن است به یاد داشته باشید، باینری مستقل را از طریق `cargo` ایجاد کردیم، اما با توجه به سیستم عامل، به نامهای ورودی و پرچمهای کامپایل مختلف نیاز داشتیم. به این دلیل که `cargo` به طور پیش فرض برای سیستم میزبان بیلد میکند، بطور مثال سیستمی که از آن برای نوشتن هسته استفاده میکنید. این چیزی نیست که ما برای هسته خود بخواهیم، زیرا منطقی نیست که هسته سیستم عاملمان را روی یک سیستم عامل دیگر اجرا کنیم. در عوض، ما میخواهیم هسته را برای یک _سیستم هدف_ کاملاً مشخص کامپایل کنیم.
|
||||
|
||||
### نصب 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 را نصب کنیم.
|
||||
|
||||
برای مدیریت نصبهای Rust من به شدت [rustup] را توصیه میکنم. به شما این امکان را میدهد که کامپایلرهای شبانه، بتا و پایدار را در کنار هم نصب کنید و بروزرسانی آنها را آسان میکند. با rustup شما میتوانید از یک کامپایلر شبانه برای دایرکتوری جاری استفاده کنید، کافیست دستور `rustup override set nightly` را اجرا کنید. همچنین میتوانید فایلی به نام `rust-toolchain` را با محتوای `nightly` در دایرکتوری ریشه پروژه اضافه کنید. با اجرای `rustc --version` میتوانید چک کنید که نسخه شبانه را دارید یا نه. شماره نسخه باید در پایان شامل `nightly-` باشد.
|
||||
|
||||
[rustup]: https://www.rustup.rs/
|
||||
|
||||
کامپایلر شبانه به ما امکان میدهد با استفاده از به اصطلاح _feature flags_ در بالای فایل، از ویژگیهای مختلف آزمایشی استفاده کنیم. به عنوان مثال، میتوانیم [`asm!` macro] آزمایشی را برای اجرای دستورات اسمبلیِ اینلاین (تلفظ: inline) با اضافه کردن `[feature(asm)]!#` به بالای فایل `main.rs` فعال کنیم. توجه داشته باشید که این ویژگیهای آزمایشی، کاملاً ناپایدار هستند، به این معنی که نسخههای آتی Rust ممکن است بدون هشدار قبلی آنها را تغییر داده یا حذف کند. به همین دلیل ما فقط در صورت لزوم از آنها استفاده خواهیم کرد.
|
||||
|
||||
[`asm!` macro]: https://doc.rust-lang.org/unstable-book/library-features/asm.html
|
||||
|
||||
### مشخصات هدف
|
||||
|
||||
کارگو (کلمه: cargo) سیستمهای هدف مختلف را از طریق `target--` پشتیبانی میکند. سیستم هدف توسط یک به اصطلاح _[target triple]_ (ترجمه: هدف سه گانه) توصیف شده است، که معماری CPU، فروشنده، سیستم عامل، و [ABI] را شامل میشود. برای مثال، هدف سه گانه `x86_64-unknown-linux-gnu` یک سیستم را توصیف میکند که دارای سیپییو `x86_64`، بدون فروشنده مشخص و یک سیستم عامل لینوکس با GNU ABI است. Rust از [هدفهای سه گانه مختلفی][platform-support] پشتیبانی میکند، شامل `arm-linux-androideabi` برای اندروید یا [`wasm32-unknown-unknown` برای وباسمبلی](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-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`. نوع سوم فیلدها نحوه ساخت crate (تلفظ: کرِیت) را تعریف میکنند. مثلا، فیلد `pre-link-args` آرگومانهای منتقل شده به [لینکر] را مشخص میکند.
|
||||
|
||||
[`data-layout`]: https://llvm.org/docs/LangRef.html#data-layout
|
||||
[لینکر]: 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
|
||||
}
|
||||
```
|
||||
|
||||
توجه داشته باشید که ما OS را در `llvm-target` و همچنین فیلد `os` را به `none` تغییر دادیم، زیرا ما هسته را روی یک bare metal اجرا میکنیم.
|
||||
|
||||
همچنین موارد زیر که مربوط به ساخت (ترجمه: build-related) هستند را اضافه میکنیم:
|
||||
|
||||
```json
|
||||
"linker-flavor": "ld.lld",
|
||||
"linker": "rust-lld",
|
||||
```
|
||||
|
||||
به جای استفاده از لینکر پیش فرض پلتفرم (که ممکن است از اهداف لینوکس پشتیبانی نکند)، ما از لینکر کراس پلتفرم [LLD] استفاده میکنیم که برای پیوند دادن هسته ما با Rust ارائه میشود.
|
||||
|
||||
[LLD]: https://lld.llvm.org/
|
||||
|
||||
```json
|
||||
"panic-strategy": "abort",
|
||||
```
|
||||
|
||||
این تنظیم مشخص میکند که هدف از [stack unwinding] درهنگام panic پشتیبانی نمیکند، بنابراین به جای آن خود برنامه باید مستقیماً متوقف شود. این همان اثر است که آپشن `panic = "abort"` در فایل Cargo.toml دارد، پس میتوانیم آن را از فایل Cargo.toml حذف کنیم.(توجه داشته باشید که این آپشنِ هدف همچنین زمانی اعمال میشود که ما کتابخانه `هسته` را مجددا در ادامه همین پست کامپایل میکنیم. بنابراین حتماً این گزینه را اضافه کنید، حتی اگر ترجیح می دهید گزینه Cargo.toml را حفظ کنید.)
|
||||
|
||||
[stack unwinding]: https://www.bogotobogo.com/cplusplus/stackunwinding.php
|
||||
|
||||
```json
|
||||
"disable-redzone": true,
|
||||
```
|
||||
|
||||
ما در حال نوشتن یک هسته هستیم، بنابراین بالاخره باید وقفهها را مدیریت کنیم. برای انجام ایمن آن، باید بهینهسازی اشارهگر پشتهای خاصی به نام _“red zone”_ (ترجمه: منطقه قرمز) را غیرفعال کنیم، زیرا در غیر این صورت باعث خراب شدن پشته میشود. برای اطلاعات بیشتر، به پست جداگانه ما در مورد [غیرفعال کردن منطقه قرمز] مراجعه کنید.
|
||||
|
||||
[غیرفعال کردن منطقه قرمز]: @/edition-2/posts/02-minimal-rust-kernel/disable-red-zone/index.md
|
||||
|
||||
```json
|
||||
"features": "-mmx,-sse,+soft-float",
|
||||
```
|
||||
|
||||
فیلد `features` ویژگیهای هدف را فعال/غیرفعال میکند. ما ویژگیهای `mmx` و `sse` را با گذاشتن یک منفی در ابتدای آنها غیرفعال کردیم و ویژگی `soft-float` را با اضافه کردن یک مثبت به ابتدای آن فعال کردیم. توجه داشته باشید که بین پرچمهای مختلف نباید فاصلهای وجود داشته باشد، در غیر این صورت LLVM قادر به تفسیر رشته ویژگیها نیست.
|
||||
|
||||
ویژگیهای `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 این است که عملیاتهای مُمَیزِ شناور (ترجمه: floating point) در `x86_64` به طور پیش فرض به ثباتهای SIMD نیاز دارد. برای حل این مشکل، ویژگی `soft-float` را اضافه میکنیم، که از طریق عملکردهای نرمافزاری مبتنی بر اعداد صحیح عادی، تمام عملیات مُمَیزِ شناور را شبیهسازی میکند.
|
||||
|
||||
For more information, see our post on [disabling SIMD](@/edition-2/posts/02-minimal-rust-kernel/disable-simd/index.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"
|
||||
}
|
||||
```
|
||||
|
||||
### ساخت هسته
|
||||
|
||||
عملیات کامپایل کردن برای هدف جدید ما از قراردادهای لینوکس استفاده خواهد کرد (کاملاً مطمئن نیستم که چرا، تصور میکنم این فقط پیش فرض LLVM باشد). این بدان معنی است که ما به یک نقطه ورود به نام `start_` نیاز داریم همانطور که در [پست قبلی] توضیح داده شد:
|
||||
|
||||
[پست قبلی]: @/edition-2/posts/01-freestanding-rust-binary/index.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`] را پیدا نمیکند. این کتابخانه شامل انواع اساسی Rust مانند `Result` ، `Option` و iterators است، و به طور ضمنی به همه کریتهای `no_std` لینک است.
|
||||
|
||||
[کتابخانه `core`]: https://doc.rust-lang.org/nightly/core/index.html
|
||||
|
||||
مشکل این است که کتابخانه core همراه با کامپایلر Rust به عنوان یک کتابخانه _precompiled_ (ترجمه: از پیش کامپایل شده) توزیع میشود. بنابراین فقط برای میزبانهای سهگانه پشتیبانی شده مجاز است (مثلا، `x86_64-unknown-linux-gnu`) اما برای هدف سفارشی ما صدق نمیکند. اگر میخواهیم برای سیستمهای هدف دیگر کدی را کامپایل کنیم، ابتدا باید `core` را برای این اهداف دوباره کامپایل کنیم.
|
||||
|
||||
#### آپشن `build-std`
|
||||
|
||||
اینجاست که [ویژگی `build-std`] کارگو وارد میشود. این امکان را میدهد تا بجای استفاده از نسخههای از پیش کامپایل شده با نصب Rust، بتوانیم `core` و کریت سایر کتابخانههای استاندارد را در صورت نیاز دوباره کامپایل کنیم. این ویژگی بسیار جدید بوده و هنوز تکمیل نشده است، بنابراین بعنوان «ناپایدار» علامت گذاری شده و فقط در [نسخه شبانه کامپایلر Rust] در دسترس میباشد.
|
||||
|
||||
[ویژگی `build-std`]: https://doc.rust-lang.org/nightly/cargo/reference/unstable.html#build-std
|
||||
[نسخه شبانه کامپایلر Rust]: #installing-rust-nightly
|
||||
|
||||
برای استفاده از این ویژگی، ما نیاز داریم تا یک فایل [پیکربندی کارگو] در `cargo/config.toml.` با محتوای زیر بسازیم:
|
||||
|
||||
```toml
|
||||
# in .cargo/config.toml
|
||||
|
||||
[unstable]
|
||||
build-std = ["core", "compiler_builtins"]
|
||||
```
|
||||
|
||||
این به کارگو میگوید که باید `core` و کتابخانه `compiler_builtins` را دوباره کامپایل کند. مورد دوم لازم است زیرا یک وابستگی از `core` است. به منظور کامپایل مجدد این کتابخانهها، کارگو نیاز به دسترسی به کد منبع Rust دارد که میتوانیم آن را با `rustup component add rust-src` نصب کنیم.
|
||||
|
||||
<div class="note">
|
||||
|
||||
**یادداشت:** کلید پیکربندی `unstable.build-std` به نسخهای جدیدتر از نسخه 2020-07-15 شبانه Rust نیاز دارد.
|
||||
|
||||
</div>
|
||||
|
||||
پس از تنظیم کلید پیکربندی `unstable.build-std` و نصب مولفه `rust-src`، میتوانیم مجددا دستور بیلد (کلمه: build) را اجرا کنیم.
|
||||
|
||||
```
|
||||
> 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` که دو بلوک حافظه را با یکدیگر مقایسه میکند. اگرچه ما در حال حاضر به هیچ یک از این توابع برای کامپایل هسته خود نیازی نداریم، اما به محض افزودن کدهای بیشتر به آن، این توابع مورد نیاز خواهند بود (برای مثال، هنگام کپی کردن یک ساختمان).
|
||||
|
||||
از آنجا که نمیتوانیم به کتابخانه C سیستم عامل لینک دهیم، به روشی جایگزین برای ارائه این توابع به کامپایلر نیاز داریم. یک رویکرد ممکن برای این کار میتواند پیادهسازی توابع `memset` و غیره و اعمال صفت `[no_mangle]#` (برای جلوگیری از تغییر نام خودکار در هنگام کامپایل کردن) بر روی آنها اعمال باشد. با این حال، این خطرناک است زیرا کوچکترین اشتباهی در اجرای این توابع میتواند منجر به یک رفتار تعریف نشده شود. به عنوان مثال، ممکن است هنگام پیادهسازی `memcpy` با استفاده از حلقه `for` یک بازگشت بیپایان داشته باشید زیرا حلقههای `for` به طور ضمنی مِتُد تریتِ (کلمه: trait) [`IntoIterator::into_iter`] را فراخوانی میکنند، که ممکن است دوباره `memcpy` را فراخوانی کند. بنابراین بهتر است به جای آن از پیاده سازیهای تست شده موجود، مجدداً استفاده کنید.
|
||||
|
||||
[`IntoIterator::into_iter`]: https://doc.rust-lang.org/stable/core/iter/trait.IntoIterator.html#tymethod.into_iter
|
||||
|
||||
خوشبختانه کریت `compiler_builtins` از قبل شامل پیاده سازی تمام توابع مورد نیازمان است، آنها فقط به طور پیش فرض غیرفعال هستند تا با پیاده سازی های کتابخانه C تداخلی نداشته باشند. ما میتوانیم آنها را با تنظیم پرچم [`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"]
|
||||
```
|
||||
پشتیبانی برای ویژگی `compiler-builtins-mem` [به تازگی اضافه شده](https://github.com/rust-lang/rust/pull/77284)، پس حداقل به نسخه شبانه `2020-09-30` نیاز دارید.
|
||||
|
||||
در پشت صحنه، این پرچم [ویژگی `mem`] از کریت `compiler_builtins` را فعال میکند. اثرش این است که صفت `[no_mangle]#` بر روی [پیادهسازی `memcpy` و بقیه موارد] از کریت اعمال میشود، که آنها در دسترس لینکر قرار میدهد. شایان ذکر است که این توابع در حال حاضر [بهینه نشدهاند]، بنابراین ممکن است عملکرد آنها در بهترین حالت نباشد، اما حداقل صحیح هستند. برای `x86_64` ، یک pull request باز برای [بهینه سازی این توابع با استفاده از دستورالعملهای خاص اسمبلی][memcpy rep movsb] وجود دارد.
|
||||
|
||||
[ویژگی `mem`]: https://github.com/rust-lang/compiler-builtins/blob/eff506cd49b637f1ab5931625a33cef7e91fbbf6/Cargo.toml#L51-L52
|
||||
[پیادهسازی `memcpy` و بقیه موارد]: (https://github.com/rust-lang/compiler-builtins/blob/eff506cd49b637f1ab5931625a33cef7e91fbbf6/src/mem.rs#L12-L69)
|
||||
[بهینه نشدهاند]: https://github.com/rust-lang/compiler-builtins/issues/339
|
||||
[memcpy rep movsb]: https://github.com/rust-lang/compiler-builtins/pull/365
|
||||
|
||||
با این تغییر، هسته ما برای همه توابع مورد نیاز کامپایلر، پیاده سازی معتبری دارد، بنابراین حتی اگر کد ما پیچیدهتر باشد نیز باز کامپایل میشود.
|
||||
|
||||
#### تنظیم یک هدف پیش فرض
|
||||
|
||||
برای اینکه نیاز نباشد در هر فراخوانی `cargo build` پارامتر `target--` را وارد کنیم، میتوانیم هدف پیشفرض را بازنویسی کنیم. برای این کار، ما کد زیر را به [پیکربندی کارگو] در فایل `cargo/config.toml.` اضافه میکنیم:
|
||||
|
||||
[پیکربندی کارگو]: https://doc.rust-lang.org/cargo/reference/config.html
|
||||
|
||||
```toml
|
||||
# in .cargo/config.toml
|
||||
|
||||
[build]
|
||||
target = "x86_64-blog_os.json"
|
||||
```
|
||||
|
||||
این به `cargo` میگوید در صورتی که صریحاً از `target--` استفاده نکردیم، از هدف ما یعنی `x86_64-blog_os.json` استفاده کند. در واقع اکنون میتوانیم هسته خود را با یک `cargo build` ساده بسازیم. برای اطلاعات بیشتر در مورد گزینههای پیکربندی کارگو، [اسناد رسمی][پیکربندی کارگو] را بررسی کنید.
|
||||
|
||||
اکنون میتوانیم هسته را برای یک هدف bare metal با یک `cargo build` ساده بسازیم. با این حال، نقطه ورود `start_` ما، که توسط بوت لودر فراخوانی میشود، هنوز خالی است. وقت آن است که از طریق آن، چیزی را در خروجی نمایش دهیم.
|
||||
|
||||
### چاپ روی صفحه
|
||||
|
||||
سادهترین راه برای چاپ متن در صفحه در این مرحله [بافر متن VGA] است. این یک منطقه خاص حافظه است که به سخت افزار VGA نگاشت (مَپ) شده و حاوی مطالب نمایش داده شده روی صفحه است. به طور معمول از 25 خط تشکیل شده است که هر کدام شامل 80 سلول کاراکتر هستند. هر سلول کاراکتر یک کاراکتر ASCII را با برخی از رنگهای پیش زمینه و پس زمینه نشان میدهد. خروجی صفحه به این شکل است:
|
||||
|
||||
[بافر متن VGA]: https://en.wikipedia.org/wiki/VGA-compatible_text_mode
|
||||
|
||||

|
||||
|
||||
ما در پست بعدی، جایی که اولین درایور کوچک را برای آن مینویسیم، در مورد قالب دقیق بافر متن 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) میریزیم. سپس روی بایتهای [رشته بایت][byte string] [استاتیک][static] `HELLO` [پیمایش][iterate] میکنیم. ما از متد [`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
|
||||
[اشارهگر خام]: 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 را خاموش نمیکند. فقط به شما این امکان را میدهد که [پنج کار اضافی] انجام دهید.
|
||||
|
||||
[`unsafe`]: https://doc.rust-lang.org/stable/book/ch19-01-unsafe-rust.html
|
||||
[پنج کار اضافی]: https://doc.rust-lang.org/stable/book/ch19-01-unsafe-rust.html#unsafe-superpowers
|
||||
|
||||
می خواهم تأکید کنم که **این روشی نیست که ما بخواهیم در Rust کارها را از طریق آن پبش ببریم!** به هم ریختگی هنگام کار با اشارهگرهای خام در داخل بلوکهای ناامن بسیار محتمل و ساده است، به عنوان مثال، اگر مواظب نباشیم به راحتی میتوانیم فراتر از انتهای بافر بنویسیم.
|
||||
|
||||
بنابراین ما میخواهیم تا آنجا که ممکن است استفاده از `unsafe` را به حداقل برسانیم. Rust با ایجاد انتزاعهای ایمن به ما توانایی انجام این کار را میدهد. به عنوان مثال، ما میتوانیم یک نوع بافر VGA ایجاد کنیم که تمام کدهای ناامن را در خود قرار داده و اطمینان حاصل کند که انجام هرگونه اشتباه از خارج از این انتزاع _غیرممکن_ است. به این ترتیب، ما فقط به حداقل مقادیر ناامن نیاز خواهیم داشت و میتوان اطمینان داشت که [ایمنی حافظه] را نقض نمیکنیم. در پست بعدی چنین انتزاع ایمن بافر VGA را ایجاد خواهیم کرد.
|
||||
|
||||
[ایمنی حافظه]: 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"
|
||||
```
|
||||
|
||||
افزودن بوتلودر به عنوان وابستگی برای ایجاد یک دیسک ایمیج قابل بوت کافی نیست. مشکل این است که ما باید هسته خود را با بوت لودر پیوند دهیم، اما کارگو از [اسکریپت های بعد از بیلد] پشتیبانی نمیکند.
|
||||
|
||||
[اسکریپت های بعد از بیلد]: https://github.com/rust-lang/cargo/issues/545
|
||||
|
||||
برای حل این مشکل، ما ابزاری به نام `bootimage` ایجاد کردیم که ابتدا هسته و بوت لودر را کامپایل میکند و سپس آنها را به یکدیگر پیوند میدهد تا یک ایمیج دیسک قابل بوت ایجاد کند. برای نصب ابزار، دستور زیر را در ترمینال خود اجرا کنید:
|
||||
|
||||
```
|
||||
cargo install bootimage
|
||||
```
|
||||
|
||||
برای اجرای `bootimage` و ساختن بوتلودر، شما باید `llvm-tools-preview` که یک مولفه rustup میباشد را نصب داشته باشید. شما میتوانید این کار را با اجرای دستور `rustup component add llvm-tools-preview` انجام دهید.
|
||||
|
||||
پس از نصب `bootimage` و اضافه کردن مولفه `llvm-tools-preview`، ما میتوانیم یک دیسک ایمیج قابل بوت را با اجرای این دستور ایجاد کنیم:
|
||||
|
||||
```
|
||||
> cargo bootimage
|
||||
```
|
||||
|
||||
میبینیم که این ابزار، هسته ما را با استفاده از `cargo build` دوباره کامپایل میکند، بنابراین به طور خودکار هر تغییری که ایجاد میکنید را دربر میگیرد. پس از آن بوتلودر را کامپایل میکند که ممکن است مدتی طول بکشد. مانند تمام کریتهای وابسته ، فقط یک بار بیلد میشود و سپس کش (کلمه: cache) میشود، بنابراین بیلدهای بعدی بسیار سریعتر خواهد بود. سرانجام، `bootimage`، بوتلودر و هسته شما را با یک دیسک ایمیج قابل بوت ترکیب میکند.
|
||||
|
||||
پس از اجرای این دستور، شما باید یک دیسک ایمیج قابل بوت به نام `bootimage-blog_os.bin` در مسیر `target/x86_64-blog_os/debug` ببینید. شما میتوانید آن را در یک ماشین مجازی بوت کنید یا آن را در یک درایو USB کپی کرده و روی یک سخت افزار واقعی بوت کنید. (توجه داشته باشید که این یک ایمیج CD نیست، بنابراین رایت کردن آن روی CD بیفایده است چرا که ایمیج CD دارای قالب متفاوتی است).
|
||||
|
||||
#### چگونه کار می کند؟
|
||||
|
||||
ابزار `bootimage` مراحل زیر را در پشت صحنه انجام می دهد:
|
||||
|
||||
- کرنل ما را به یک فایل [ELF] کامپایل میکند.
|
||||
- وابستگی بوتلودر را به عنوان یک اجرایی مستقل (ترجمه: standalone executable) کامپایل میکند.
|
||||
- بایتهای فایل ELF هسته را به بوتلودر پیوند میدهد.
|
||||
|
||||
[ELF]: https://en.wikipedia.org/wiki/Executable_and_Linkable_Format
|
||||
[rust-osdev/bootloader]: https://github.com/rust-osdev/bootloader
|
||||
|
||||
وقتی بوت شد، بوتلودر فایل ضمیمه شده ELF را خوانده و تجزیه میکند. سپس بخشهای (ترجمه: segments) برنامه را به آدرسهای مجازی در جداول صفحه نگاشت (مپ) میکند، بخش `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]
|
||||
```
|
||||
|
||||
این یک پنجره جداگانه با این شکل باز میکند:
|
||||
|
||||

|
||||
|
||||
میبینیم که “!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` را برای کارگو تنظیم کنیم:
|
||||
|
||||
```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` فراخوانی شود مشخص میکند. دستور پس از بیلد موفقیت آمیز با مسیر فایل اجرایی که به عنوان اولین آرگومان داده شده، اجرا میشود. برای جزئیات بیشتر به [اسناد کارگو][پیکربندی کارگو] مراجعه کنید.
|
||||
|
||||
دستور `bootimage runner` بصورت مشخص طراحی شده تا بعنوان یک `runner` قابل اجرا مورد استفاده قرار بگیرد. فایل اجرایی داده شده را به بوتلودر پروژه پیوند داده و سپس QEMU را اجرا میکند. برای جزئیات بیشتر و گزینههای پیکربندی احتمالی، به [توضیحات `bootimage`] مراجعه کنید.
|
||||
|
||||
[توضیحات `bootimage`]: https://github.com/rust-osdev/bootimage
|
||||
|
||||
اکنون میتوانیم از `cargo run` برای کامپایل هسته خود و راه اندازی آن در QEMU استفاده کنیم.
|
||||
|
||||
## مرحله بعد چیست؟
|
||||
|
||||
در پست بعدی، ما بافر متن VGA را با جزئیات بیشتری بررسی خواهیم کرد و یک رابط امن برای آن مینویسیم. همچنین پشتیبانی از ماکرو `println` را نیز اضافه خواهیم کرد.
|
||||
@@ -1,500 +0,0 @@
|
||||
+++
|
||||
title = "Rustでつくる最小のカーネル"
|
||||
weight = 2
|
||||
path = "ja/minimal-rust-kernel"
|
||||
date = 2018-02-10
|
||||
|
||||
[extra]
|
||||
chapter = "Bare Bones"
|
||||
# Please update this when updating the translation
|
||||
translation_based_on_commit = "7212ffaa8383122b1eb07fe1854814f99d2e1af4"
|
||||
# GitHub usernames of the people that translated this post
|
||||
translators = ["woodyZootopia", "JohnTitor"]
|
||||
+++
|
||||
|
||||
この記事では、Rustで最小限の64bitカーネルを作ります。前の記事で作った[フリースタンディングなRustバイナリ][freestanding Rust binary]を下敷きにして、何かを画面に出力する、ブータブルディスクイメージを作ります。
|
||||
|
||||
[freestanding Rust binary]: @/edition-2/posts/01-freestanding-rust-binary/index.ja.md
|
||||
|
||||
<!-- more -->
|
||||
|
||||
このブログの内容は [GitHub] 上で公開・開発されています。何か問題や質問などがあれば issue をたててください (訳注: リンクは原文(英語)のものになります)。また[こちら][at the bottom]にコメントを残すこともできます。この記事の完全なソースコードは[`post-02` ブランチ][post branch]にあります。
|
||||
|
||||
[GitHub]: https://github.com/phil-opp/blog_os
|
||||
[at the bottom]: #comments
|
||||
<!-- fix for zola anchor checker (target is in template): <a id="comments"> -->
|
||||
[post branch]: https://github.com/phil-opp/blog_os/tree/post-02
|
||||
|
||||
<!-- toc -->
|
||||
|
||||
## <ruby>起動<rp> (</rp><rt>Boot</rt><rp>) </rp></ruby>のプロセス {#the-boot-process}
|
||||
コンピュータを起動すると、マザーボードの [ROM] に保存されたファームウェアのコードを実行し始めます。このコードは、[<ruby>起動時の自己テスト<rp> (</rp><rt>power-on self test</rt><rp>) </rp></ruby>][power-on self-test]を実行し、使用可能なRAMを検出し、CPUとハードウェアを<ruby>事前初期化<rp> (</rp><rt>pre-initialize</rt><rp>) </rp></ruby>します。その後、<ruby>ブータブル<rp> (</rp><rt>bootable</rt><rp>) </rp></ruby>ディスクを探し、オペレーティングシステムのカーネルを<ruby>起動<rp> (</rp><rt>boot</rt><rp>) </rp></ruby>します。
|
||||
|
||||
[ROM]: https://ja.wikipedia.org/wiki/Read_only_memory
|
||||
[power-on self-test]: https://ja.wikipedia.org/wiki/Power_On_Self_Test
|
||||
|
||||
x86には2つのファームウェアの標準規格があります:"Basic Input/Output System" (**[BIOS]**) と、より新しい "Unified Extensible Firmware Interface" (**[UEFI]**) です。BIOS規格は古く時代遅れですが、シンプルでありすべてのx86のマシンで1980年代からよくサポートされています。対して、UEFIはより現代的でずっと多くの機能を持っていますが、セットアップが複雑です(少なくとも私はそう思います)。
|
||||
|
||||
[BIOS]: https://ja.wikipedia.org/wiki/Basic_Input/Output_System
|
||||
[UEFI]: https://ja.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による起動の最大の欠点でもあるのです。というのもこれは、1980年代の化石のようなブートローダーを動かすために、CPUが[<ruby>リアルモード<rp> (</rp><rt>real mode</rt><rp>) </rp></ruby>][real mode]と呼ばれる16bit互換モードにされてしまうということを意味しているからです。
|
||||
|
||||
まあ順を追って見ていくこととしましょう。
|
||||
|
||||
コンピュータは起動時にマザーボードにある特殊なフラッシュメモリからBIOSを読み込みます。BIOSは自己テストとハードウェアの初期化ルーチンを実行し、ブータブルディスクを探します。ディスクが見つかると、 **<ruby>ブートローダー<rp> (</rp><rt>bootloader</rt><rp>) </rp></ruby>** と呼ばれる、その先頭512バイトに保存された実行可能コードへと操作権が移ります。多くのブートローダーのサイズは512バイトより大きいため、通常は512バイトに収まる小さな最初のステージと、その最初のステージによって読み込まれる第2ステージに分けられています。
|
||||
|
||||
ブートローダーはディスク内のカーネルイメージの場所を特定し、メモリに読み込まなければなりません。また、CPUを16bitの[リアルモード][real mode]から32bitの[<ruby>プロテクトモード<rp> (</rp><rt>protected mode</rt><rp>) </rp></ruby>][protected mode]へ、そして64bitの[<ruby>ロングモード<rp> (</rp><rt>long mode</rt><rp>) </rp></ruby>][long mode]――64bitレジスタとすべてのメインメモリが利用可能になります――へと変更しなければなりません。3つ目の仕事は、特定の情報(例えばメモリーマップなどです)をBIOSから聞き出し、OSのカーネルに渡すことです。
|
||||
|
||||
[real mode]: https://ja.wikipedia.org/wiki/リアルモード
|
||||
[protected mode]: https://ja.wikipedia.org/wiki/プロテクトモード
|
||||
[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標準規格
|
||||
すべてのオペレーティングシステムが、自身にのみ対応しているブートローダーを実装するということを避けるために、1995年に[フリーソフトウェア財団][Free Software Foundation]が[Multiboot]というブートローダーの公開標準規格を策定しています。この標準規格では、ブートローダーとオペレーティングシステムのインターフェースが定義されており、Multibootに準拠したブートローダーであれば、同じくそれに準拠したすべてのオペレーティングシステムが読み込めるようになっています。そのリファレンス実装として、Linuxシステムで一番人気のブートローダーである[GNU GRUB]があります。
|
||||
|
||||
[Free Software Foundation]: https://ja.wikipedia.org/wiki/フリーソフトウェア財団
|
||||
[Multiboot]: https://wiki.osdev.org/Multiboot
|
||||
[GNU GRUB]: https://ja.wikipedia.org/wiki/GNU_GRUB
|
||||
|
||||
カーネルをMultibootに準拠させるには、カーネルファイルの先頭にいわゆる[Multiboot header]を挿入するだけで済みます。このおかげで、OSをGRUBで起動するのはとても簡単です。しかし、GRUBとMultiboot標準規格にはいくつか問題もあります:
|
||||
|
||||
[Multiboot header]: https://www.gnu.org/software/grub/manual/multiboot/multiboot.html#OS-image-format
|
||||
|
||||
- これらは32bitプロテクトモードしかサポートしていません。そのため、64bitロングモードに変更するためのCPUの設定は依然行う必要があります。
|
||||
- これらは、カーネルではなくブートローダーがシンプルになるように設計されています。例えば、カーネルは[通常とは異なるデフォルトページサイズ][adjusted default page size]でリンクされる必要があり、そうしないとGRUBはMultiboot headerを見つけることができません。他にも、カーネルに渡される[<ruby>ブート情報<rp> (</rp><rt>boot information</rt><rp>) </rp></ruby>][boot information]は、クリーンな抽象化を与えてくれず、アーキテクチャ依存の構造を多く含んでいます。
|
||||
- GRUBもMultiboot標準規格もドキュメントが充実していません。
|
||||
- カーネルファイルからブータブルディスクイメージを作るには、ホストシステムにGRUBがインストールされている必要があります。これにより、MacとWindows上での開発は比較的難しくなっています。
|
||||
|
||||
[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標準規格を使わないことに決めました。しかし、あなたのカーネルをGRUBシステム上で読み込めるように、私達の[bootimage]ツールにMultibootのサポートを追加することも計画しています。Multiboot準拠なカーネルを書きたい場合は、このブログシリーズの[第1版][first edition]をご覧ください。
|
||||
|
||||
[first edition]: @/edition-1/_index.md
|
||||
|
||||
### UEFI
|
||||
|
||||
(今の所UEFIのサポートは提供していませんが、ぜひともしたいと思っています!お手伝いいただける場合は、 [GitHub issue](https://github.com/phil-opp/blog_os/issues/349)で教えてください。)
|
||||
|
||||
## 最小のカーネル
|
||||
どのようにコンピュータが起動するのかについてざっくりと理解できたので、自前で最小のカーネルを書いてみましょう。目標は、起動したら画面に"Hello, World!"と出力するようなディスクイメージを作ることです。というわけで、前の記事の[<ruby>独立した<rp> (</rp><rt>freestanding</rt><rp>) </rp></ruby>Rustバイナリ][freestanding Rust binary]をもとにして作っていきます。
|
||||
|
||||
覚えていますか、この独立したバイナリは`cargo`を使ってビルドしましたが、オペレーティングシステムに依って異なるエントリポイント名とコンパイルフラグが必要なのでした。これは`cargo`は標準では **ホストシステム**(あなたの使っているシステム)向けにビルドするためです。例えばWindows上で走るカーネルというのはあまり意味がなく、私達の望む動作ではありません。代わりに、明確に定義された **ターゲットシステム** 向けにコンパイルできると理想的です。
|
||||
|
||||
### 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`とあるはずです。
|
||||
|
||||
[rustup]: https://www.rustup.rs/
|
||||
|
||||
nightlyコンパイラでは、いわゆる**feature flag**をファイルの先頭につけることで、いろいろな実験的機能を使うことを選択できます。例えば、`#![feature(asm)]`を`main.rs`の先頭につけることで、インラインアセンブリのための実験的な[`asm!`マクロ][`asm!` macro]を有効化することができます。ただし、これらの実験的機能は全くもって<ruby>不安定<rp> (</rp><rt>unstable</rt><rp>) </rp></ruby>であり、将来のRustバージョンにおいては事前の警告なく変更されたり取り除かれたりする可能性があることに注意してください。このため、絶対に必要なときにのみこれらを使うことにします。
|
||||
|
||||
[`asm!` macro]: https://doc.rust-lang.org/unstable-book/library-features/asm.html
|
||||
|
||||
### ターゲットの仕様
|
||||
Cargoは`--target`パラメータを使ってさまざまなターゲットをサポートします。ターゲットはいわゆる[target <ruby>triple<rp> (</rp><rt>3つ組</rt><rp>) </rp></ruby>][target triple]によって表されます。これはCPUアーキテクチャ、製造元、オペレーティングシステム、そして[ABI]を表します。例えば、`x86_64-unknown-linux-gnu`というtarget tripleは、`x86_64`のCPU、製造元不明、GNU ABIのLinuxオペレーティングシステム向けのシステムを表します。Rustは[多くのtarget triple][platform-support]をサポートしており、その中にはAndroidのための`arm-linux-androideabi`や[WebAssemblyのための`wasm32-unknown-unknown`](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
|
||||
|
||||
しかしながら、私達のターゲットシステムには、いくつか特殊な設定パラメータが必要になります(例えば、その下ではOSが走っていない、など)。なので、[既存のtarget triple][platform-support]はどれも当てはまりません。ありがたいことに、RustではJSONファイルを使って[独自のターゲット][custom-targets]を定義できます。例えば、`x86_64-unknown-linux-gnu`というターゲットを表すJSONファイルはこんな感じです。
|
||||
|
||||
```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`]フィールドは種々の整数、浮動小数点数、ポインタ型の大きさを定義しています。次に、`target-pointer-width`のような、条件付きコンパイルに用いられるフィールドがあります。第3の種類のフィールドはクレートがどのようにビルドされるべきかを定義します。例えば、`pre-link-args`フィールドは[<ruby>リンカ<rp> (</rp><rt>linker</rt><rp>) </rp></ruby>][linker]に渡される引数を指定しています。
|
||||
|
||||
[`data-layout`]: https://llvm.org/docs/LangRef.html#data-layout
|
||||
[linker]: https://ja.wikipedia.org/wiki/リンケージエディタ
|
||||
|
||||
私達のカーネルも`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
|
||||
}
|
||||
```
|
||||
|
||||
<ruby>ベアメタル<rp> (</rp><rt>bare metal</rt><rp>) </rp></ruby>環境で実行するので、`llvm-target`のOSを変え、`os`フィールドを`none`にしたことに注目してください。
|
||||
|
||||
以下の、ビルドに関係する項目を追加します。
|
||||
|
||||
```json
|
||||
"linker-flavor": "ld.lld",
|
||||
"linker": "rust-lld",
|
||||
```
|
||||
|
||||
私達のカーネルをリンクするのに、プラットフォーム標準の(Linuxターゲットをサポートしていないかもしれない)リンカではなく、Rustに付属しているクロスプラットフォームの[LLD]リンカを使用します。
|
||||
|
||||
[LLD]: https://lld.llvm.org/
|
||||
|
||||
```json
|
||||
"panic-strategy": "abort",
|
||||
```
|
||||
|
||||
この設定は、ターゲットがパニック時の[stack unwinding]をサポートしていないので、プログラムは代わりに直接<ruby>中断<rp> (</rp><rt>abort</rt><rp>) </rp></ruby>しなければならないということを指定しています。これは、Cargo.tomlに`panic = "abort"`という設定を書くのに等しいですから、後者の設定を消しても構いません(このターゲット設定は、Cargo.tomlの設定と異なり、このあと行う`core`ライブラリの再コンパイルにも適用されます。ですので、Cargo.tomlに設定する方が好みだったとしても、この設定を追加するようにしてください)。
|
||||
|
||||
[stack unwinding]: https://www.bogotobogo.com/cplusplus/stackunwinding.php
|
||||
|
||||
```json
|
||||
"disable-redzone": true,
|
||||
```
|
||||
|
||||
カーネルを書いている以上、ある時点で<ruby>割り込み<rp> (</rp><rt>interrupt</rt><rp>) </rp></ruby>を処理しなければならなくなるでしょう。これを安全に行うために、 **"red zone"** と呼ばれる、ある種のスタックポインタ最適化を無効化する必要があります。こうしないと、スタックの<ruby>破損<rp> (</rp><rt>corruption</rt><rp>) </rp></ruby>を引き起こしてしまう恐れがあるためです。より詳しくは、[red zoneの無効化][disabling the red zone]という別記事をご覧ください。
|
||||
|
||||
[disabling the red zone]: @/edition-2/posts/02-minimal-rust-kernel/disable-red-zone/index.md
|
||||
|
||||
```json
|
||||
"features": "-mmx,-sse,+soft-float",
|
||||
```
|
||||
|
||||
`features`フィールドは、ターゲットの<ruby>機能<rp> (</rp><rt>features</rt><rp>) </rp></ruby>を有効化/無効化します。マイナスを前につけることで`mmx`と`sse`という機能を無効化し、プラスを前につけることで`soft-float`という機能を有効化しています。それぞれのフラグの間にスペースは入れてはならず、もしそうするとLLVMが機能文字列の解釈に失敗してしまうことに注意してください。
|
||||
|
||||
`mmx`と`sse`という機能は、[Single Instruction Multiple Data (SIMD)]命令をサポートするかを決定します。この命令は、しばしばプログラムを著しく速くしてくれます。しかし、大きなSIMDレジスタをOSカーネルで使うことは性能上の問題に繋がります。 その理由は、カーネルは、割り込まれたプログラムを再開する前に、すべてのレジスタを元に戻さないといけないためです。これは、カーネルがSIMDの状態のすべてを、システムコールやハードウェア割り込みがあるたびにメインメモリに保存しないといけないということを意味します。SIMDの状態情報はとても巨大(512〜1600 bytes)で、割り込みは非常に頻繁に起こるかもしれないので、保存・復元の操作がこのように追加されるのは性能にかなりの悪影響を及ぼします。これを避けるために、(カーネルの上で走っているアプリケーションではなく!)カーネル上でSIMDを無効化するのです。
|
||||
|
||||
[Single Instruction Multiple Data (SIMD)]: https://ja.wikipedia.org/wiki/SIMD
|
||||
|
||||
SIMDを無効化することによる問題に、`x86_64`における浮動小数点演算は標準ではSIMDレジスタを必要とするということがあります。この問題を解決するため、`soft-float`機能を追加します。これは、すべての浮動小数点演算を通常の整数に基づいたソフトウェア上の関数を使ってエミュレートするというものです。
|
||||
|
||||
より詳しくは、[SIMDを無効化する](@/edition-2/posts/02-minimal-rust-kernel/disable-simd/index.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のデフォルトであるというだけではないでしょうか)。つまり、[前の記事][previous post]で説明したように`_start`という名前のエントリポイントが要るということです。
|
||||
|
||||
[previous post]: @/edition-2/posts/01-freestanding-rust-binary/index.ja.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 {}
|
||||
}
|
||||
```
|
||||
|
||||
ホストOSが何であるかにかかわらず、エントリポイントは`_start`という名前でなければならないことに注意してください。
|
||||
|
||||
これで、私達の新しいターゲットのためのカーネルを、JSONファイル名を`--target`として渡すことでビルドできるようになりました。
|
||||
|
||||
```
|
||||
> cargo build --target x86_64-blog_os.json
|
||||
|
||||
error[E0463]: can't find crate for `core`
|
||||
```
|
||||
|
||||
失敗しましたね!エラーはRustコンパイラが[`core`ライブラリ][`core` library]を見つけられなくなったと言っています。このライブラリは、`Result` や `Option`、イテレータのような基本的なRustの型を持っており、暗黙のうちにすべての`no_std`なクレートにリンクされています。
|
||||
|
||||
[`core` library]: https://doc.rust-lang.org/nightly/core/index.html
|
||||
|
||||
問題は、coreライブラリはRustコンパイラと一緒に<ruby>コンパイル済み<rp> (</rp><rt>precompiled</rt><rp>) </rp></ruby>ライブラリとして配布されているということです。そのため、これは、私達独自のターゲットではなく、サポートされているhost triple(例えば `x86_64-unknown-linux-gnu`)でのみ使えるのです。他のターゲットのためにコードをコンパイルしたいときには、`core`をそれらのターゲットに向けて再コンパイルする必要があります。
|
||||
|
||||
#### `build-std`オプション
|
||||
|
||||
ここでcargoの[`build-std`機能][`build-std` feature]の出番です。これを使うと`core`やその他の標準ライブラリクレートについて、Rustインストール時に一緒についてくるコンパイル済みバージョンを使う代わりに、必要に応じて再コンパイルすることができます。これはとても新しくまだ完成していないので、<ruby>不安定<rp> (</rp><rt>unstable</rt><rp>) </rp></ruby>機能とされており、[nightly Rustコンパイラ][nightly Rust compilers]でのみ利用可能です。
|
||||
|
||||
[`build-std` feature]: https://doc.rust-lang.org/nightly/cargo/reference/unstable.html#build-std
|
||||
[nightly Rust compilers]: #installing-rust-nightly
|
||||
|
||||
この機能を使うためには、[cargoの設定][cargo configuration]ファイルを`.cargo/config.toml`に作り、次の内容を書きましょう。
|
||||
|
||||
```toml
|
||||
# in .cargo/config.toml
|
||||
|
||||
[unstable]
|
||||
build-std = ["core", "compiler_builtins"]
|
||||
```
|
||||
|
||||
これはcargoに`core`と`compiler_builtins`ライブラリを再コンパイルするよう命令します。後者が必要なのは`core`がこれに依存しているためです。 これらのライブラリを再コンパイルするためには、cargoがRustのソースコードにアクセスできる必要があります。これは`rustup component add rust-src`でインストールできます。
|
||||
|
||||
<div class="note">
|
||||
|
||||
**注意:** `unstable.build-std`設定キーを使うには、少なくとも2020-07-15以降のRust nightlyが必要です。
|
||||
|
||||
</div>
|
||||
|
||||
`unstable.build-std`設定キーをセットし、`rust-src`コンポーネントをインストールしたら、ビルドコマンドをもう一度実行しましょう。
|
||||
|
||||
```
|
||||
> 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`を私達のカスタムターゲット向けに再コンパイルしているということがわかります。
|
||||
|
||||
#### メモリ関係の<ruby>組み込み関数<rp> (</rp><rt>intrinsics</rt><rp>) </rp></ruby>
|
||||
|
||||
Rustコンパイラは、すべてのシステムにおいて、特定の組み込み関数が利用可能であるということを前提にしています。それらの関数の多くは、私達がちょうど再コンパイルした`compiler_builtins`クレートによって提供されています。しかしながら、通常システムのCライブラリによって提供されているので標準では有効化されていない、メモリ関係の関数がいくつかあります。それらの関数には、メモリブロック内のすべてのバイトを与えられた値にセットする`memset`、メモリーブロックを他のブロックへとコピーする`memcpy`、2つのメモリーブロックを比較する`memcmp`などがあります。これらの関数はどれも、現在の段階で我々のカーネルをコンパイルするのに必要というわけではありませんが、コードを追加していくとすぐに必要になるでしょう(たとえば、構造体をコピーする、など)。
|
||||
|
||||
オペレーティングシステムのCライブラリにリンクすることはできませんので、これらの関数をコンパイラに与えてやる別の方法が必要になります。このための方法として考えられるものの一つが、自前で`memset`を実装し、(コンパイル中の自動リネームを防ぐため)`#[no_mangle]`アトリビュートをこれらに適用することでしょう。しかし、こうすると、これらの関数の実装のちょっとしたミスが未定義動作に繋がりうるため危険です。たとえば、`for`ループを使って`memcpy`を実装すると無限再帰を起こしてしまうかもしれません。なぜなら、`for`ループは暗黙のうちに[`IntoIterator::into_iter`]トレイトメソッドを呼び出しており、これが`memcpy`を再び呼び出しているかもしれないためです。なので、代わりに既存のよくテストされた実装を再利用するのが良いでしょう。
|
||||
|
||||
[`IntoIterator::into_iter`]: https://doc.rust-lang.org/stable/core/iter/trait.IntoIterator.html#tymethod.into_iter
|
||||
|
||||
ありがたいことに、`compiler_builtins`クレートにはこれらの必要な関数すべての実装が含まれており、標準ではCライブラリの実装と競合しないように無効化されているだけなのです。これはcargoの[`build-std-features`]フラグを`["compiler-builtins-mem"]`に設定することで有効化できます。`build-std`フラグと同じように、このフラグはコマンドラインで`-Z`フラグとして渡すこともできれば、`.cargo/config.toml`ファイルの`unstable`テーブルで設定することもできます。ビルド時は常にこのフラグをセットしたいので、設定ファイルを使う方が良いでしょう:
|
||||
|
||||
[`build-std-features`]: https://doc.rust-lang.org/nightly/cargo/reference/unstable.html#build-std-features
|
||||
|
||||
```toml
|
||||
# in .cargo/config.toml
|
||||
|
||||
[unstable]
|
||||
build-std-features = ["compiler-builtins-mem"]
|
||||
```
|
||||
|
||||
(`compiler-builtins-mem`機能のサポートが追加されたのは[つい最近](https://github.com/rust-lang/rust/pull/77284)なので、`2019-09-30`以降のRust nightlyが必要です。)
|
||||
|
||||
このとき、裏で`compiler_builtins`クレートの[`mem`機能][`mem` feature]が有効化されています。これにより、このクレートの[`memcpy`などの実装][`memcpy` etc. implementations]に`#[no_mangle]`アトリビュートが適用され、リンカがこれらを利用できるようになっています。
|
||||
|
||||
[`mem` feature]: https://github.com/rust-lang/compiler-builtins/blob/eff506cd49b637f1ab5931625a33cef7e91fbbf6/Cargo.toml#L51-L52
|
||||
[`memcpy` etc. implementations]: (https://github.com/rust-lang/compiler-builtins/blob/eff506cd49b637f1ab5931625a33cef7e91fbbf6/src/mem.rs#L12-L69)
|
||||
|
||||
この変更をもって、私達のカーネルはコンパイラに必要とされているすべての関数の有効な実装を手に入れたので、コードがもっと複雑になっても変わらずコンパイルできるでしょう。
|
||||
|
||||
#### 標準のターゲットをセットする
|
||||
|
||||
`cargo build`を呼び出すたびに`--target`パラメータを渡すのを避けるために、デフォルトのターゲットを書き換えることができます。これをするには、以下を`.cargo/config.toml`の[cargo設定][cargo configuration]ファイルに付け加えます:
|
||||
|
||||
[cargo configuration]: https://doc.rust-lang.org/cargo/reference/config.html
|
||||
|
||||
```toml
|
||||
# in .cargo/config.toml
|
||||
|
||||
[build]
|
||||
target = "x86_64-blog_os.json"
|
||||
```
|
||||
|
||||
これは、明示的に`--target`引数が渡されていないときは、`x86_64-blog_os.json`ターゲットを使うように`cargo`に命令します。つまり、私達はカーネルをシンプルな`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
|
||||
|
||||

|
||||
|
||||
次の記事では、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]にキャストします。次に[<ruby>静的<rp> (</rp><rt>static</rt><rp>) </rp></ruby>][static]な`HELLO`という[バイト列][byte string]変数の要素に対し[イテレート][iterate]します。[`enumerate`]メソッドを使うことで、`for` ループの実行回数を表す変数 `i` も取得します。ループの内部では、[`offset`]メソッドを使って文字列のバイトと対応する色のバイト(`0xb`は明るいシアン色)を書き込んでいます。
|
||||
|
||||
[iterate]: https://doc.rust-jp.rs/book-ja/ch13-02-iterators.html
|
||||
[static]: https://doc.rust-jp.rs/book-ja/ch10-03-lifetime-syntax.html#静的ライフタイム
|
||||
[`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-jp.rs/book-ja/ch19-01-unsafe-rust.html#生ポインタを参照外しする
|
||||
[`offset`]: https://doc.rust-lang.org/std/primitive.pointer.html#method.offset
|
||||
|
||||
すべてのメモリへの書き込み処理のコードを、[<ruby>`unsafe`<rp> (</rp><rt>安全でない</rt><rp>) </rp></ruby>][`unsafe`]ブロックが囲んでいることに注意してください。この理由は、私達の作った生ポインタが正しいものであることをRustコンパイラが証明できないためです。生ポインタはどんな場所でも指しうるので、データの破損につながるかもしれません。これらの操作を`unsafe`ブロックに入れることで、私達はこれが正しいことを確信しているとコンパイラに伝えているのです。ただし、`unsafe`ブロックはRustの安全性チェックを消すわけではなく、[追加で5つのことができるようになる][five additional things]だけということに注意してください。
|
||||
|
||||
<div class="note">
|
||||
|
||||
**訳注:** 翻訳時点(2020-10-20)では、リンク先のThe Rust book日本語版には「追加でできるようになること」は4つしか書かれていません。
|
||||
|
||||
</div>
|
||||
|
||||
[`unsafe`]: https://doc.rust-jp.rs/book-ja/ch19-01-unsafe-rust.html
|
||||
[five additional things]: https://doc.rust-jp.rs/book-ja/ch19-01-unsafe-rust.html#unsafeの強大な力superpower
|
||||
|
||||
強調しておきたいのですが、 **このような機能はRustでプログラミングするときに使いたいものではありません!** unsafeブロック内で生ポインタを扱うと非常にしくじりやすいです。たとえば、注意不足でバッファの終端のさらに奥に書き込みを行ってしまったりするかもしれません。
|
||||
|
||||
ですので、`unsafe`の使用は最小限にしたいです。これをするために、Rustでは安全な<ruby>abstraction<rp> (</rp><rt>抽象化されたもの</rt><rp>) </rp></ruby>を作ることができます。たとえば、VGAバッファ型を作り、この中にすべてのunsafeな操作をカプセル化し、外側からの誤った操作が**不可能**であることを保証できるでしょう。こうすれば、`unsafe`の量を最小限にでき、[メモリ安全性][memory safety]を侵していないことを確かにできます。そのような安全なVGAバッファの abstraction を次の記事で作ります。
|
||||
|
||||
[memory safety]: https://ja.wikipedia.org/wiki/メモリ安全性
|
||||
|
||||
## カーネルを実行する
|
||||
|
||||
では、目で見て分かる処理を行う実行可能ファイルを手に入れたので、実行してみましょう。まず、コンパイルした私達のカーネルを、ブートローダーとリンクすることによってブータブルディスクイメージにする必要があります。そして、そのディスクイメージを、[QEMU]バーチャルマシン内や、USBメモリを使って実際のハードウェア上で実行できます。
|
||||
|
||||
### ブートイメージを作る
|
||||
|
||||
コンパイルされた私達のカーネルをブータブルディスクイメージに変えるには、ブートローダーとリンクする必要があります。[起動のプロセスのセクション][section about booting]で学んだように、ブートローダーはCPUを初期化しカーネルをロードする役割があります。
|
||||
|
||||
[section about booting]: #the-boot-process
|
||||
|
||||
自前のブートローダーを書くと、それだけで1つのプロジェクトになってしまうので、代わりに[`bootloader`]クレートを使いましょう。このクレートは、Cに依存せず、Rustとインラインアセンブリだけで基本的なBIOSブートローダーを実装しています。私達のカーネルを起動するためにこれを依存関係に追加する必要があります:
|
||||
|
||||
[`bootloader`]: https://crates.io/crates/bootloader
|
||||
|
||||
```toml
|
||||
# in Cargo.toml
|
||||
|
||||
[dependencies]
|
||||
bootloader = "0.9.8"
|
||||
```
|
||||
|
||||
bootloaderを依存として加えることだけでブータブルディスクイメージが実際に作れるわけではなく、私達のカーネルをコンパイル後にブートローダーにリンクする必要があります。問題は、cargoが[<ruby>ビルド後<rp> (</rp><rt>post-build</rt><rp>) </rp></ruby>にスクリプトを走らせる機能][post-build scripts]を持っていないことです。
|
||||
|
||||
[post-build scripts]: https://github.com/rust-lang/cargo/issues/545
|
||||
|
||||
この問題を解決するため、私達は`bootimage`というツールを作りました。これは、まずカーネルとブートローダーをコンパイルし、そしてこれらをリンクしてブータブルディスクイメージを作ります。このツールをインストールするには、以下のコマンドをターミナルで実行してください:
|
||||
|
||||
```
|
||||
cargo install bootimage
|
||||
```
|
||||
|
||||
`bootimage`を実行しブートローダをビルドするには、`llvm-tools-preview`というrustupコンポーネントをインストールする必要があります。これは`rustup component add llvm-tools-preview`と実行することでできます。
|
||||
|
||||
`bootimage`をインストールし、`llvm-tools-preview`を追加したら、以下のように実行することでブータブルディスクイメージを作れます:
|
||||
|
||||
```
|
||||
> cargo bootimage
|
||||
```
|
||||
|
||||
このツールが私達のカーネルを`cargo build`を使って再コンパイルしていることがわかります。そのため、あなたの行った変更を自動で検知してくれます。その後、bootloaderをビルドします。これには少し時間がかかるかもしれません。他の依存クレートと同じように、ビルドは一度しか行われず、その都度キャッシュされるので、以降のビルドはもっと早くなります。最終的に、`bootimage`はbootloaderとあなたのカーネルを合体させ、ブータブルディスクイメージにします。
|
||||
|
||||
このコマンドを実行したら、`target/x86_64-blog_os/debug`ディレクトリ内に`bootimage-blog_os.bin`という名前のブータブルディスクイメージがあるはずです。これをバーチャルマシン内で起動してもいいですし、実際のハードウェア上で起動するためにUSBメモリにコピーしてもいいでしょう(ただし、これはCDイメージではありません。CDイメージは異なるフォーマットを持つので、これをCDに焼いてもうまくいきません)。
|
||||
|
||||
#### どういう仕組みなの?
|
||||
`bootimage`ツールは、裏で以下のステップを行っています:
|
||||
|
||||
- 私達のカーネルを[ELF]ファイルにコンパイルする。
|
||||
- 依存であるbootloaderをスタンドアロンの実行ファイルとしてコンパイルする。
|
||||
- カーネルのELFファイルのバイト列をブートローダーにリンクする。
|
||||
|
||||
[ELF]: https://ja.wikipedia.org/wiki/Executable_and_Linkable_Format
|
||||
[rust-osdev/bootloader]: https://github.com/rust-osdev/bootloader
|
||||
|
||||
起動時、ブートローダーは追加されたELFファイルを読み、解釈します。次にプログラム部を<ruby>ページテーブル<rp> (</rp><rt>page table</rt><rp>) </rp></ruby>の<ruby>仮想アドレス<rp> (</rp><rt>virtual address</rt><rp>) </rp></ruby>にマップし、`.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]
|
||||
```
|
||||
|
||||
これにより、以下のような見た目の別のウィンドウが開きます:
|
||||
|
||||

|
||||
|
||||
私達の書いた"Hello World!"が画面に見えますね。
|
||||
|
||||
### 実際のマシン
|
||||
|
||||
USBメモリにこれを書き込んで実際のマシン上で起動することも可能です:
|
||||
|
||||
```
|
||||
> dd if=target/x86_64-blog_os/debug/bootimage-blog_os.bin of=/dev/sdX && sync
|
||||
```
|
||||
|
||||
`sdX`はあなたのUSBメモリのデバイス名です。そのデバイス上のすべてのデータが上書きされてしまうので、 **正しいデバイス名を選んでいるのかよく確認してください** 。
|
||||
|
||||
イメージをUSBメモリに書き込んだあとは、そこから起動することによって実際のハードウェア上で走らせることができます。特殊なブートメニューを使ったり、BIOS設定で起動時の優先順位を変え、USBメモリから起動することを選択する必要があるでしょう。ただし、`bootloader`クレートはUEFIをサポートしていないので、UEFIマシン上ではうまく動作しないということに注意してください。
|
||||
|
||||
### `cargo run`を使う
|
||||
|
||||
QEMU上でより簡単に私達のカーネルを走らせるために、cargoの`runner`設定が使えます。
|
||||
|
||||
```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`ターゲットもその1つです。`runner`キーは`cargo run`のときに呼ばれるコマンドを指定しています。このコマンドは、ビルドが成功した後に、実行可能ファイルのパスを第一引数として実行されます。詳しくは、[cargoのドキュメント][cargo configuration]を読んでください。
|
||||
|
||||
`bootimage runner`コマンドは、`runner`キーとして実行するために設計されています。このコマンドは、与えられた実行ファイルをプロジェクトの依存するbootloaderとリンクして、QEMUを立ち上げます。より詳しく知りたいときや、設定オプションについては[`bootimage`のReadme][Readme of `bootimage`]を読んでください。
|
||||
|
||||
[Readme of `bootimage`]: https://github.com/rust-osdev/bootimage
|
||||
|
||||
これで、`cargo run`を使ってカーネルをコンパイルしQEMU内で起動することができます。
|
||||
|
||||
## 次は?
|
||||
|
||||
次の記事では、VGAテキストバッファをより詳しく学び、そのための安全なインターフェースを書きます。また、`println`マクロのサポートも行います。
|
||||
@@ -1,502 +0,0 @@
|
||||
+++
|
||||
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
|
||||
|
||||
<!-- more -->
|
||||
Этот блог открыто разрабатывается на [GitHub]. Если у вас возникли какие-либо проблемы или вопросы, пожалуйста, создайте _issue_. Также вы можете оставлять комментарии [в конце страницы][at the bottom]. Полный исходный код для этого поста вы можете найти в репозитории в ветке [`post-02`][post branch].
|
||||
|
||||
|
||||
[GitHub]: https://github.com/phil-opp/blog_os
|
||||
[at the bottom]: #comments
|
||||
<!-- fix for zola anchor checker (target is in template): <a id="comments"> -->
|
||||
[post branch]: https://github.com/phil-opp/blog_os/tree/post-02
|
||||
|
||||
<!-- toc -->
|
||||
|
||||
## Последовательность процессов запуска {#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
|
||||
|
||||
Если вы заинтересованы в создании собственного загрузчика: Оставайтесь с нами, набор постов на эту тему уже запланирован! <!-- , check out our “_[Writing a Bootloader]_” posts, where we explain in detail how a bootloader is built. -->
|
||||
|
||||
#### Стандарт 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`.
|
||||
|
||||
<div class="note">
|
||||
|
||||
**Note:** Ключ конфигурации `unstable.build-std` требует как минимум Rust nightly от 2020-07-15.
|
||||
|
||||
</div>
|
||||
|
||||
```
|
||||
> 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
|
||||
|
||||

|
||||
|
||||
Точную разметку буфера 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]
|
||||
```
|
||||
|
||||
Откроется отдельное окно, которое выглядит следующим образом:
|
||||
|
||||

|
||||
|
||||
Мы видим, что наш "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`.
|
||||
@@ -1,390 +0,0 @@
|
||||
+++
|
||||
title = "最小化内核"
|
||||
weight = 2
|
||||
path = "zh-CN/minimal-rust-kernel"
|
||||
date = 2018-02-10
|
||||
|
||||
[extra]
|
||||
# Please update this when updating the translation
|
||||
translation_based_on_commit = "bd6fbcb1c36705b2c474d7fcee387bfea1210851"
|
||||
# GitHub usernames of the people that translated this post
|
||||
translators = ["luojia65", "Rustin-Liu"]
|
||||
+++
|
||||
|
||||
在这篇文章中,我们将基于 **x86架构**(the x86 architecture),使用 Rust 语言,编写一个最小化的 64 位内核。我们将从上一章中构建的独立式可执行程序开始,构建自己的内核;它将向显示器打印字符串,并能被打包为一个能够引导启动的**磁盘映像**(disk image)。
|
||||
|
||||
[freestanding Rust binary]: @/edition-2/posts/01-freestanding-rust-binary/index.md
|
||||
|
||||
<!-- more -->
|
||||
|
||||
This blog is openly developed on [GitHub]. If you have any problems or questions, please open an issue there. You can also leave comments [at the bottom]. The complete source code for this post can be found in the [`post-02`][post branch] branch.
|
||||
|
||||
[GitHub]: https://github.com/phil-opp/blog_os
|
||||
[at the bottom]: #comments
|
||||
<!-- fix for zola anchor checker (target is in template): <a id="comments"> -->
|
||||
[post branch]: https://github.com/phil-opp/blog_os/tree/post-02
|
||||
|
||||
<!-- toc -->
|
||||
|
||||
## 引导启动
|
||||
|
||||
当我们启动电脑时,主板 [ROM](https://en.wikipedia.org/wiki/Read-only_memory)内存储的**固件**(firmware)将会运行:它将负责电脑的**加电自检**([power-on self test](https://en.wikipedia.org/wiki/Power-on_self-test)),**可用内存**(available RAM)的检测,以及 CPU 和其它硬件的预加载。这之后,它将寻找一个**可引导的存储介质**(bootable disk),并开始引导启动其中的**内核**(kernel)。
|
||||
|
||||
x86 架构支持两种固件标准: **BIOS**([Basic Input/Output System](https://en.wikipedia.org/wiki/BIOS))和 **UEFI**([Unified Extensible Firmware Interface](https://en.wikipedia.org/wiki/Unified_Extensible_Firmware_Interface))。其中,BIOS 标准显得陈旧而过时,但实现简单,并为 1980 年代后的所有 x86 设备所支持;相反地,UEFI 更现代化,功能也更全面,但开发和构建更复杂(至少从我的角度看是如此)。
|
||||
|
||||
在这篇文章中,我们暂时只提供 BIOS 固件的引导启动方式。
|
||||
|
||||
### BIOS 启动
|
||||
|
||||
几乎所有的 x86 硬件系统都支持 BIOS 启动,这也包含新型的、基于 UEFI、用**模拟 BIOS**(emulated BIOS)的方式向后兼容的硬件系统。这可以说是一件好事情,因为无论是上世纪还是现在的硬件系统,你都只需编写同样的引导启动逻辑;但这种兼容性有时也是 BIOS 引导启动最大的缺点,因为这意味着在系统启动前,你的 CPU 必须先进入一个 16 位系统兼容的**实模式**([real mode](https://en.wikipedia.org/wiki/Real_mode)),这样 1980 年代古老的引导固件才能够继续使用。
|
||||
|
||||
让我们从头开始,理解一遍 BIOS 启动的过程。
|
||||
|
||||
当电脑启动时,主板上特殊的闪存中存储的 BIOS 固件将被加载。BIOS 固件将会加电自检、初始化硬件,然后它将寻找一个可引导的存储介质。如果找到了,那电脑的控制权将被转交给**引导程序**(bootloader):一段存储在存储介质的开头的、512字节长度的程序片段。大多数的引导程序长度都大于512字节——所以通常情况下,引导程序都被切分为一段优先启动、长度不超过512字节、存储在介质开头的**第一阶段引导程序**(first stage bootloader),和一段随后由其加载的、长度可能较长、存储在其它位置的**第二阶段引导程序**(second stage bootloader)。
|
||||
|
||||
引导程序必须决定内核的位置,并将内核加载到内存。引导程序还需要将 CPU 从 16 位的实模式,先切换到 32 位的**保护模式**([protected mode](https://en.wikipedia.org/wiki/Protected_mode)),最终切换到 64 位的**长模式**([long mode](https://en.wikipedia.org/wiki/Long_mode)):此时,所有的 64 位寄存器和整个**主内存**(main memory)才能被访问。引导程序的第三个作用,是从 BIOS 查询特定的信息,并将其传递到内核;如查询和传递**内存映射表**(memory map)。
|
||||
|
||||
编写一个引导程序并不是一个简单的任务,因为这需要使用汇编语言,而且必须经过许多意图并不明显的步骤——比如,把一些**魔术数字**(magic number)写入某个寄存器。因此,我们不会讲解如何编写自己的引导程序,而是推荐 [bootimage 工具](https://github.com/rust-osdev/bootimage)——它能够自动并且方便地为你的内核准备一个引导程序。
|
||||
|
||||
### Multiboot 标准
|
||||
|
||||
每个操作系统都实现自己的引导程序,而这只对单个操作系统有效。为了避免这样的僵局,1995 年,**自由软件基金会**([Free Software Foundation](https://en.wikipedia.org/wiki/Free_Software_Foundation))颁布了一个开源的引导程序标准——[Multiboot](https://wiki.osdev.org/Multiboot)。这个标准定义了引导程序和操作系统间的统一接口,所以任何适配 Multiboot 的引导程序,都能用来加载任何同样适配了 Multiboot 的操作系统。[GNU GRUB](https://en.wikipedia.org/wiki/GNU_GRUB) 是一个可供参考的 Multiboot 实现,它也是最热门的Linux系统引导程序之一。
|
||||
|
||||
要编写一款适配 Multiboot 的内核,我们只需要在内核文件开头,插入被称作 **Multiboot头**([Multiboot header](https://www.gnu.org/software/grub/manual/multiboot/multiboot.html#OS-image-format))的数据片段。这让 GRUB 很容易引导任何操作系统,但是,GRUB 和 Multiboot 标准也有一些可预知的问题:
|
||||
|
||||
1. 它们只支持 32 位的保护模式。这意味着,在引导之后,你依然需要配置你的 CPU,让它切换到 64 位的长模式;
|
||||
2. 它们被设计为精简引导程序,而不是精简内核。举个例子,内核需要以调整过的**默认页长度**([default page size](https://wiki.osdev.org/Multiboot#Multiboot_2))被链接,否则 GRUB 将无法找到内核的 Multiboot 头。另一个例子是**引导信息**([boot information](https://www.gnu.org/software/grub/manual/multiboot/multiboot.html#Boot-information-format)),这个包含着大量与架构有关的数据,会在引导启动时,被直接传到操作系统,而不会经过一层清晰的抽象;
|
||||
3. GRUB 和 Multiboot 标准并没有被详细地解释,阅读相关文档需要一定经验;
|
||||
4. 为了创建一个能够被引导的磁盘映像,我们在开发时必须安装 GRUB:这加大了基于 Windows 或 macOS 开发内核的难度。
|
||||
|
||||
出于这些考虑,我们决定不使用 GRUB 或者 Multiboot 标准。然而,Multiboot 支持功能也在 bootimage 工具的开发计划之中,所以从原理上讲,如果选用 bootimage 工具,在未来使用 GRUB 引导你的系统内核是可能的。
|
||||
|
||||
## 最小化内核
|
||||
|
||||
现在我们已经明白电脑是如何启动的,那也是时候编写我们自己的内核了。我们的小目标是,创建一个内核的磁盘映像,它能够在启动时,向屏幕输出一行“Hello World!”;我们的工作将基于上一章构建的独立式可执行程序。
|
||||
|
||||
如果读者还有印象的话,在上一章,我们使用 `cargo` 构建了一个独立的二进制程序;但这个程序依然基于特定的操作系统平台:因平台而异,我们需要定义不同名称的函数,且使用不同的编译指令。这是因为在默认情况下,`cargo` 会为特定的**宿主系统**(host system)构建源码,比如为你正在运行的系统构建源码。这并不是我们想要的,因为我们的内核不应该基于另一个操作系统——我们想要编写的,就是这个操作系统。确切地说,我们想要的是,编译为一个特定的**目标系统**(target system)。
|
||||
|
||||
## 安装 Nightly Rust
|
||||
|
||||
Rust 语言有三个**发行频道**(release channel),分别是 stable、beta 和 nightly。《Rust 程序设计语言》中对这三个频道的区别解释得很详细,可以前往[这里](https://doc.rust-lang.org/book/appendix-07-nightly-rust.html)看一看。为了搭建一个操作系统,我们需要一些只有 nightly 会提供的实验性功能,所以我们需要安装一个 nightly 版本的 Rust。
|
||||
|
||||
要管理安装好的 Rust,我强烈建议使用 [rustup](https://www.rustup.rs/):它允许你同时安装 nightly、beta 和 stable 版本的编译器,而且让更新 Rust 变得容易。你可以输入 `rustup override add nightly` 来选择在当前目录使用 nightly 版本的 Rust。或者,你也可以在项目根目录添加一个名称为 `rust-toolchain`、内容为 `nightly` 的文件。要检查你是否已经安装了一个 nightly,你可以运行 `rustc --version`:返回的版本号末尾应该包含`-nightly`。
|
||||
|
||||
Nightly 版本的编译器允许我们在源码的开头插入**特性标签**(feature flag),来自由选择并使用大量实验性的功能。举个例子,要使用实验性的[内联汇编(asm!宏)][asm feature],我们可以在 `main.rs` 的顶部添加 `#![feature(asm)]`。要注意的是,这样的实验性功能**不稳定**(unstable),意味着未来的 Rust 版本可能会修改或移除这些功能,而不会有预先的警告过渡。因此我们只有在绝对必要的时候,才应该使用这些特性。
|
||||
|
||||
[asm feature]: https://doc.rust-lang.org/unstable-book/library-features/asm.html
|
||||
|
||||
### 目标配置清单
|
||||
|
||||
通过 `--target` 参数,`cargo` 支持不同的目标系统。这个目标系统可以使用一个**目标三元组**([target triple](https://clang.llvm.org/docs/CrossCompilation.html#target-triple))来描述,它描述了 CPU 架构、平台供应者、操作系统和**应用程序二进制接口**([Application Binary Interface, ABI](https://stackoverflow.com/a/2456882))。比方说,目标三元组` x86_64-unknown-linux-gnu` 描述一个基于 `x86_64` 架构 CPU 的、没有明确的平台供应者的 linux 系统,它遵循 GNU 风格的 ABI。Rust 支持[许多不同的目标三元组](https://forge.rust-lang.org/release/platform-support.html),包括安卓系统对应的 `arm-linux-androideabi` 和 [WebAssembly使用的wasm32-unknown-unknown](https://www.hellorust.com/setup/wasm-target/)。
|
||||
|
||||
为了编写我们的目标系统,并且鉴于我们需要做一些特殊的配置(比如没有依赖的底层操作系统),[已经支持的目标三元组](https://forge.rust-lang.org/release/platform-support.html)都不能满足我们的要求。幸运的是,只需使用一个 JSON 文件,Rust 便允许我们定义自己的目标系统;这个文件常被称作**目标配置清单**(target specification)。比如,一个描述 `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
|
||||
}
|
||||
```
|
||||
|
||||
一个配置清单中包含多个**配置项**(field)。大多数的配置项都是 LLVM 需求的,它们将配置为特定平台生成的代码。打个比方,`data-layout` 配置项定义了不同的整数、浮点数、指针类型的长度;另外,还有一些 Rust 用作条件编译的配置项,如 `target-pointer-width`。还有一些类型的配置项,定义了这个包该如何被编译,例如,`pre-link-args` 配置项指定了应该向**链接器**([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
|
||||
}
|
||||
```
|
||||
|
||||
需要注意的是,因为我们要在**裸机**(bare metal)上运行内核,我们已经修改了 `llvm-target` 的内容,并将 `os` 配置项的值改为 `none`。
|
||||
|
||||
我们还需要添加下面与编译相关的配置项:
|
||||
|
||||
```json
|
||||
"linker-flavor": "ld.lld",
|
||||
"linker": "rust-lld",
|
||||
```
|
||||
|
||||
在这里,我们不使用平台默认提供的链接器,因为它可能不支持 Linux 目标系统。为了链接我们的内核,我们使用跨平台的 **LLD链接器**([LLD linker](https://lld.llvm.org/)),它是和 Rust 一起打包发布的。
|
||||
|
||||
```json
|
||||
"panic-strategy": "abort",
|
||||
```
|
||||
|
||||
这个配置项的意思是,我们的编译目标不支持 panic 时的**栈展开**([stack unwinding](https://www.bogotobogo.com/cplusplus/stackunwinding.php)),所以我们选择直接**在 panic 时中止**(abort on panic)。这和在 `Cargo.toml` 文件中添加 `panic = "abort"` 选项的作用是相同的,所以我们可以不在这里的配置清单中填写这一项。
|
||||
|
||||
```json
|
||||
"disable-redzone": true,
|
||||
```
|
||||
|
||||
我们正在编写一个内核,所以我们应该同时处理中断。要安全地实现这一点,我们必须禁用一个与**红区**(redzone)有关的栈指针优化:因为此时,这个优化可能会导致栈被破坏。我们撰写了一篇专门的短文,来更详细地解释红区及与其相关的优化。
|
||||
|
||||
```json
|
||||
"features": "-mmx,-sse,+soft-float",
|
||||
```
|
||||
|
||||
`features` 配置项被用来启用或禁用某个目标 **CPU 特征**(CPU feature)。通过在它们前面添加`-`号,我们将 `mmx` 和 `sse` 特征禁用;添加前缀`+`号,我们启用了 `soft-float` 特征。
|
||||
|
||||
`mmx` 和 `sse` 特征决定了是否支持**单指令多数据流**([Single Instruction Multiple Data,SIMD](https://en.wikipedia.org/wiki/SIMD))相关指令,这些指令常常能显著地提高程序层面的性能。然而,在内核中使用庞大的 SIMD 寄存器,可能会造成较大的性能影响:因为每次程序中断时,内核不得不储存整个庞大的 SIMD 寄存器以备恢复——这意味着,对每个硬件中断或系统调用,完整的 SIMD 状态必须存到主存中。由于 SIMD 状态可能相当大(512~1600 个字节),而中断可能时常发生,这些额外的存储与恢复操作可能显著地影响效率。为解决这个问题,我们对内核禁用 SIMD(但这不意味着禁用内核之上的应用程序的 SIMD 支持)。
|
||||
|
||||
禁用 SIMD 产生的一个问题是,`x86_64` 架构的浮点数指针运算默认依赖于 SIMD 寄存器。我们的解决方法是,启用 `soft-float` 特征,它将使用基于整数的软件功能,模拟浮点数指针运算。
|
||||
|
||||
为了让读者的印象更清晰,我们撰写了一篇关于禁用 SIMD 的短文。
|
||||
|
||||
现在,我们将各个配置项整合在一起。我们的目标配置清单应该长这样:
|
||||
|
||||
```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`:
|
||||
|
||||
```rust
|
||||
// src/main.rs
|
||||
|
||||
#![no_std] // 不链接 Rust 标准库
|
||||
#![no_main] // 禁用所有 Rust 层级的入口点
|
||||
|
||||
use core::panic::PanicInfo;
|
||||
|
||||
/// 这个函数将在 panic 时被调用
|
||||
#[panic_handler]
|
||||
fn panic(_info: &PanicInfo) -> ! {
|
||||
loop {}
|
||||
}
|
||||
|
||||
#[no_mangle] // 不重整函数名
|
||||
pub extern "C" fn _start() -> ! {
|
||||
// 因为编译器会寻找一个名为 `_start` 的函数,所以这个函数就是入口点
|
||||
// 默认命名为 `_start`
|
||||
loop {}
|
||||
}
|
||||
```
|
||||
|
||||
注意的是,无论你开发使用的是哪类操作系统,你都需要将入口点命名为 `_start`。前一篇文章中编写的 Windows 系统和 macOS 对应的入口点不应该被保留。
|
||||
|
||||
通过把 JSON 文件名传入 `--target` 选项,我们现在可以开始编译我们的内核。让我们试试看:
|
||||
|
||||
```
|
||||
> cargo build --target x86_64-blog_os.json
|
||||
|
||||
error[E0463]: can't find crate for `core`
|
||||
(或者是下面的错误)
|
||||
error[E0463]: can't find crate for `compiler_builtins`
|
||||
```
|
||||
|
||||
哇哦,编译失败了!输出的错误告诉我们,Rust 编译器找不到 `core` 或者 `compiler_builtins` 包;而所有 `no_std` 上下文都隐式地链接到这两个包。[`core` 包](https://doc.rust-lang.org/nightly/core/index.html)包含基础的 Rust 类型,如` Result`、`Option` 和迭代器等;[`compiler_builtins` 包](https://github.com/rust-lang-nursery/compiler-builtins)提供 LLVM 需要的许多底层操作,比如 `memcpy`。
|
||||
|
||||
通常状况下,`core` 库以**预编译库**(precompiled library)的形式与 Rust 编译器一同发布——这时,`core` 库只对支持的宿主系统有效,而我们自定义的目标系统无效。如果我们想为其它系统编译代码,我们需要为这些系统重新编译整个 `core` 库。
|
||||
|
||||
### Cargo xbuild
|
||||
|
||||
这就是为什么我们需要 [cargo xbuild 工具](https://github.com/rust-osdev/cargo-xbuild)。这个工具封装了 `cargo build`;但不同的是,它将自动交叉编译 `core` 库和一些**编译器内建库**(compiler built-in libraries)。我们可以用下面的命令安装它:
|
||||
|
||||
```bash
|
||||
cargo install cargo-xbuild
|
||||
```
|
||||
|
||||
这个工具依赖于Rust的源代码;我们可以使用 `rustup component add rust-src` 来安装源代码。
|
||||
|
||||
现在我们可以使用 `xbuild` 代替 `build` 重新编译:
|
||||
|
||||
```bash
|
||||
> cargo xbuild --target x86_64-blog_os.json
|
||||
Compiling core v0.0.0 (/…/rust/src/libcore)
|
||||
Compiling compiler_builtins v0.1.5
|
||||
Compiling rustc-std-workspace-core v1.0.0 (/…/rust/src/tools/rustc-std-workspace-core)
|
||||
Compiling alloc v0.0.0 (/tmp/xargo.PB7fj9KZJhAI)
|
||||
Finished release [optimized + debuginfo] target(s) in 45.18s
|
||||
Compiling blog_os v0.1.0 (file:///…/blog_os)
|
||||
Finished dev [unoptimized + debuginfo] target(s) in 0.29 secs
|
||||
```
|
||||
|
||||
我们能看到,`cargo xbuild` 为我们自定义的目标交叉编译了 `core`、`compiler_builtin` 和 `alloc` 三个部件。这些部件使用了大量的**不稳定特性**(unstable features),所以只能在[nightly 版本的 Rust 编译器][installing rust nightly]中工作。这之后,`cargo xbuild` 成功地编译了我们的 `blog_os` 包。
|
||||
|
||||
[installing rust nightly]: #an-zhuang-nightly-rust
|
||||
|
||||
现在我们可以为裸机编译内核了;但是,我们提供给引导程序的入口点 `_start` 函数还是空的。我们可以添加一些东西进去,不过我们可以先做一些优化工作。
|
||||
|
||||
### 设置默认目标
|
||||
|
||||
为了避免每次使用`cargo xbuild`时传递`--target`参数,我们可以覆写默认的编译目标。我们创建一个名为`.cargo/config`的[cargo配置文件](https://doc.rust-lang.org/cargo/reference/config.html),添加下面的内容:
|
||||
|
||||
```toml
|
||||
# in .cargo/config
|
||||
|
||||
[build]
|
||||
target = "x86_64-blog_os.json"
|
||||
```
|
||||
|
||||
这里的配置告诉 `cargo` 在没有显式声明目标的情况下,使用我们提供的 `x86_64-blog_os.json` 作为目标配置。这意味着保存后,我们可以直接使用:
|
||||
|
||||
```
|
||||
cargo xbuild
|
||||
```
|
||||
|
||||
来编译我们的内核。[官方提供的一份文档](https://doc.rust-lang.org/cargo/reference/config.html)中有对 cargo 配置文件更详细的说明。
|
||||
|
||||
### 向屏幕打印字符
|
||||
|
||||
要做到这一步,最简单的方式是写入 **VGA 字符缓冲区**([VGA text buffer](https://en.wikipedia.org/wiki/VGA-compatible_text_mode)):这是一段映射到 VGA 硬件的特殊内存片段,包含着显示在屏幕上的内容。通常情况下,它能够存储 25 行、80 列共 2000 个**字符单元**(character cell);每个字符单元能够显示一个 ASCII 字符,也能设置这个字符的**前景色**(foreground color)和**背景色**(background color)。输出到屏幕的字符大概长这样:
|
||||
|
||||

|
||||
|
||||
我们将在下篇文章中详细讨论 VGA 字符缓冲区的内存布局;目前我们只需要知道,这段缓冲区的地址是 `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 {}
|
||||
}
|
||||
```
|
||||
|
||||
在这段代码中,我们预先定义了一个**字节字符串**(byte string)类型的**静态变量**(static variable),名为 `HELLO`。我们首先将整数 `0xb8000` **转换**(cast)为一个**裸指针**([raw pointer])。这之后,我们迭代 `HELLO` 的每个字节,使用 [enumerate](https://doc.rust-lang.org/core/iter/trait.Iterator.html#method.enumerate) 获得一个额外的序号变量 `i`。在 `for` 语句的循环体中,我们使用 [offset](https://doc.rust-lang.org/std/primitive.pointer.html#method.offset) 偏移裸指针,解引用它,来将字符串的每个字节和对应的颜色字节——`0xb` 代表淡青色——写入内存位置。
|
||||
|
||||
[raw pointer]: https://doc.rust-lang.org/1.30.0/book/second-edition/ch19-01-unsafe-rust.html#dereferencing-a-raw-pointer
|
||||
|
||||
要注意的是,所有的裸指针内存操作都被一个 **unsafe 语句块**([unsafe block](https://doc.rust-lang.org/stable/book/second-edition/ch19-01-unsafe-rust.html))包围。这是因为,此时编译器不能确保我们创建的裸指针是有效的;一个裸指针可能指向任何一个你内存位置;直接解引用并写入它,也许会损坏正常的数据。使用 `unsafe` 语句块时,程序员其实在告诉编译器,自己保证语句块内的操作是有效的。事实上,`unsafe` 语句块并不会关闭 Rust 的安全检查机制;它允许你多做的事情[只有四件][unsafe superpowers]。
|
||||
|
||||
[unsafe superpowers]: https://doc.rust-lang.org/1.30.0/book/second-edition/ch19-01-unsafe-rust.html#unsafe-superpowers
|
||||
|
||||
使用 `unsafe` 语句块要求程序员有足够的自信,所以必须强调的一点是,**肆意使用 unsafe 语句块并不是 Rust 编程的一贯方式**。在缺乏足够经验的前提下,直接在 `unsafe` 语句块内操作裸指针,非常容易把事情弄得很糟糕;比如,在不注意的情况下,我们很可能会意外地操作缓冲区以外的内存。
|
||||
|
||||
在这样的前提下,我们希望最小化 `unsafe ` 语句块的使用。使用 Rust 语言,我们能够将不安全操作将包装为一个安全的抽象模块。举个例子,我们可以创建一个 VGA 缓冲区类型,把所有的不安全语句封装起来,来确保从类型外部操作时,无法写出不安全的代码:通过这种方式,我们只需要最少的 `unsafe` 语句块来确保我们不破坏**内存安全**([memory safety](https://en.wikipedia.org/wiki/Memory_safety))。在下一篇文章中,我们将会创建这样的 VGA 缓冲区封装。
|
||||
|
||||
## 启动内核
|
||||
|
||||
既然我们已经有了一个能够打印字符的可执行程序,是时候把它运行起来试试看了。首先,我们将编译完毕的内核与引导程序链接,来创建一个引导映像;这之后,我们可以在 QEMU 虚拟机中运行它,或者通过 U 盘在真机上运行。
|
||||
|
||||
### 创建引导映像
|
||||
|
||||
要将可执行程序转换为**可引导的映像**(bootable disk image),我们需要把它和引导程序链接。这里,引导程序将负责初始化 CPU 并加载我们的内核。
|
||||
|
||||
编写引导程序并不容易,所以我们不编写自己的引导程序,而是使用已有的 [bootloader](https://crates.io/crates/bootloader) 包;无需依赖于 C 语言,这个包基于 Rust 代码和内联汇编,实现了一个五脏俱全的 BIOS 引导程序。为了用它启动我们的内核,我们需要将它添加为一个依赖项,在 `Cargo.toml` 中添加下面的代码:
|
||||
|
||||
```toml
|
||||
# in Cargo.toml
|
||||
|
||||
[dependencies]
|
||||
bootloader = "0.9.3"
|
||||
```
|
||||
|
||||
只添加引导程序为依赖项,并不足以创建一个可引导的磁盘映像;我们还需要内核编译完成之后,将内核和引导程序组合在一起。然而,截至目前,原生的 cargo 并不支持在编译完成后添加其它步骤(详见[这个 issue](https://github.com/rust-lang/cargo/issues/545))。
|
||||
|
||||
为了解决这个问题,我们建议使用 `bootimage` 工具——它将会在内核编译完毕后,将它和引导程序组合在一起,最终创建一个能够引导的磁盘映像。我们可以使用下面的命令来安装这款工具:
|
||||
|
||||
```bash
|
||||
cargo install bootimage
|
||||
```
|
||||
|
||||
为了运行 `bootimage` 以及编译引导程序,我们需要安装 rustup 模块 `llvm-tools-preview`——我们可以使用 `rustup component add llvm-tools-preview` 来安装这个工具。
|
||||
|
||||
成功安装 `bootimage` 后,创建一个可引导的磁盘映像就变得相当容易。我们来输入下面的命令:
|
||||
|
||||
```bash
|
||||
> cargo bootimage
|
||||
```
|
||||
|
||||
可以看到的是,`bootimage` 工具开始使用 `cargo xbuild` 编译你的内核,所以它将增量编译我们修改后的源码。在这之后,它会编译内核的引导程序,这可能将花费一定的时间;但和所有其它依赖包相似的是,在首次编译后,产生的二进制文件将被缓存下来——这将显著地加速后续的编译过程。最终,`bootimage` 将把内核和引导程序组合为一个可引导的磁盘映像。
|
||||
|
||||
运行这行命令之后,我们应该能在 `target/x86_64-blog_os/debug` 目录内找到我们的映像文件 `bootimage-blog_os.bin`。我们可以在虚拟机内启动它,也可以刻录到 U 盘上以便在真机上启动。(需要注意的是,因为文件格式不同,这里的 bin 文件并不是一个光驱映像,所以将它刻录到光盘不会起作用。)
|
||||
|
||||
事实上,在这行命令背后,`bootimage` 工具执行了三个步骤:
|
||||
|
||||
1. 编译我们的内核为一个 **ELF**([Executable and Linkable Format](https://en.wikipedia.org/wiki/Executable_and_Linkable_Format))文件;
|
||||
2. 编译引导程序为独立的可执行文件;
|
||||
3. 将内核 ELF 文件**按字节拼接**(append by bytes)到引导程序的末端。
|
||||
|
||||
当机器启动时,引导程序将会读取并解析拼接在其后的 ELF 文件。这之后,它将把程序片段映射到**分页表**(page table)中的**虚拟地址**(virtual address),清零 **BSS段**(BSS segment),还将创建一个栈。最终它将读取**入口点地址**(entry point address)——我们程序中 `_start` 函数的位置——并跳转到这个位置。
|
||||
|
||||
### 在 QEMU 中启动内核
|
||||
|
||||
现在我们可以在虚拟机中启动内核了。为了在[ QEMU](https://www.qemu.org/) 中启动内核,我们使用下面的命令:
|
||||
|
||||
```bash
|
||||
> qemu-system-x86_64 -drive format=raw,file=bootimage-blog_os.bin
|
||||
```
|
||||
|
||||

|
||||
|
||||
我们可以看到,屏幕窗口已经显示出 “Hello World!” 字符串。祝贺你!
|
||||
|
||||
### 在真机上运行内核
|
||||
|
||||
我们也可以使用 dd 工具把内核写入 U 盘,以便在真机上启动。可以输入下面的命令:
|
||||
|
||||
```bash
|
||||
> dd if=target/x86_64-blog_os/debug/bootimage-blog_os.bin of=/dev/sdX && sync
|
||||
```
|
||||
|
||||
在这里,`sdX` 是U盘的**设备名**([device name](https://en.wikipedia.org/wiki/Device_file))。请注意,**在选择设备名的时候一定要极其小心,因为目标设备上已有的数据将全部被擦除**。
|
||||
|
||||
写入到 U 盘之后,你可以在真机上通过引导启动你的系统。视情况而定,你可能需要在 BIOS 中打开特殊的启动菜单,或者调整启动顺序。需要注意的是,`bootloader` 包暂时不支持 UEFI,所以我们并不能在 UEFI 机器上启动。
|
||||
|
||||
### 使用 `cargo run`
|
||||
|
||||
要让在 QEMU 中运行内核更轻松,我们可以设置在 cargo 配置文件中设置 `runner` 配置项:
|
||||
|
||||
```toml
|
||||
# in .cargo/config
|
||||
|
||||
[target.'cfg(target_os = "none")']
|
||||
runner = "bootimage runner"
|
||||
```
|
||||
|
||||
在这里,`target.'cfg(target_os = "none")'` 筛选了三元组中宿主系统设置为 `"none"` 的所有编译目标——这将包含我们的 `x86_64-blog_os.json` 目标。另外,`runner` 的值规定了运行 `cargo run` 使用的命令;这个命令将在成功编译后执行,而且会传递可执行文件的路径为第一个参数。[官方提供的 cargo 文档](https://doc.rust-lang.org/cargo/reference/config.html)讲述了更多的细节。
|
||||
|
||||
命令 `bootimage runner` 由 `bootimage` 包提供,参数格式经过特殊设计,可以用于 `runner` 命令。它将给定的可执行文件与项目的引导程序依赖项链接,然后在 QEMU 中启动它。`bootimage` 包的 [README文档](https://github.com/rust-osdev/bootimage) 提供了更多细节和可以传入的配置参数。
|
||||
|
||||
现在我们可以使用 `cargo xrun` 来编译内核并在 QEMU 中启动了。和 `xbuild` 类似,`xrun` 子命令将在调用 cargo 命令前编译内核所需的包。这个子命令也由 `cargo-xbuild` 工具提供,所以你不需要安装额外的工具。
|
||||
|
||||
## 下篇预告
|
||||
|
||||
在下篇文章中,我们将细致地探索 VGA 字符缓冲区,并包装它为一个安全的接口。我们还将基于它实现 `println!` 宏。
|
||||
@@ -1,703 +0,0 @@
|
||||
+++
|
||||
title = "حالت متن VGA"
|
||||
weight = 3
|
||||
path = "fa/vga-text-mode"
|
||||
date = 2018-02-26
|
||||
|
||||
[extra]
|
||||
chapter = "Bare Bones"
|
||||
# Please update this when updating the translation
|
||||
translation_based_on_commit = "fb8b03e82d9805473fed16e8795a78a020a6b537"
|
||||
# GitHub usernames of the people that translated this post
|
||||
translators = ["hamidrezakp", "MHBahrampour"]
|
||||
rtl = true
|
||||
+++
|
||||
|
||||
[حالت متن VGA] یک روش ساده برای چاپ متن روی صفحه است. در این پست ، با قرار دادن همه موارد غیر ایمنی در یک ماژول جداگانه ، رابطی ایجاد می کنیم که استفاده از آن را ایمن و ساده می کند. همچنین پشتیبانی از [ماکروی فرمتبندی] راست را پیاده سازی میکنیم.
|
||||
|
||||
[حالت متن VGA]: https://en.wikipedia.org/wiki/VGA-compatible_text_mode
|
||||
[ماکروی فرمتبندی]: https://doc.rust-lang.org/std/fmt/#related-macros
|
||||
|
||||
<!-- more -->
|
||||
|
||||
این بلاگ بصورت آزاد بر روی [گیتهاب] توسعه داده شده. اگر مشکل یا سوالی دارید، لطفا آنجا یک ایشو باز کنید. همچنین میتوانید [در زیر] این پست کامنت بگذارید. سورس کد کامل این پست را می توانید در شاخه [`post-01`][post branch] پیدا کنید.
|
||||
|
||||
[گیتهاب]: https://github.com/phil-opp/blog_os
|
||||
[در زیر]: #comments
|
||||
<!-- fix for zola anchor checker (target is in template): <a id="comments"> -->
|
||||
[post branch]: https://github.com/phil-opp/blog_os/tree/post-03
|
||||
|
||||
<!-- toc -->
|
||||
|
||||
## بافر متن VGA
|
||||
برای چاپ یک کاراکتر روی صفحه در حالت متن VGA ، باید آن را در بافر متن سخت افزار VGA بنویسید. بافر متن VGA یک آرایه دو بعدی است که به طور معمول 25 ردیف و 80 ستون دارد که مستقیماً به صفحه نمایش داده(رندر) می شود. هر خانه آرایه یک کاراکتر صفحه نمایش را از طریق قالب زیر توصیف می کند:
|
||||
|
||||
Bit(s) | Value
|
||||
------ | ----------------
|
||||
0-7 | ASCII code point
|
||||
8-11 | Foreground color
|
||||
12-14 | Background color
|
||||
15 | Blink
|
||||
|
||||
اولین بایت کاراکتری در [کدگذاری ASCII] را نشان می دهد که باید چاپ شود. اگر بخواهیم دقیق باشیم ، دقیقاً ASCII نیست ، بلکه مجموعه ای از کاراکترها به نام [_کد صفحه 437_] با برخی کاراکتر های اضافی و تغییرات جزئی است. برای سادگی ، ما در این پست آنرا یک کاراکتر ASCII می نامیم.
|
||||
|
||||
[کدگذاری ASCII]: https://en.wikipedia.org/wiki/ASCII
|
||||
[_کد صفحه 437_]: https://en.wikipedia.org/wiki/Code_page_437
|
||||
|
||||
بایت دوم نحوه نمایش کاراکتر را مشخص می کند. چهار بیت اول رنگ پیش زمینه را مشخص می کند ، سه بیت بعدی رنگ پس زمینه و بیت آخر اینکه کاراکتر باید چشمک بزند یا نه. رنگ های زیر موجود است:
|
||||
|
||||
Number | Color | Number + Bright Bit | Bright Color
|
||||
------ | ---------- | ------------------- | -------------
|
||||
0x0 | Black | 0x8 | Dark Gray
|
||||
0x1 | Blue | 0x9 | Light Blue
|
||||
0x2 | Green | 0xa | Light Green
|
||||
0x3 | Cyan | 0xb | Light Cyan
|
||||
0x4 | Red | 0xc | Light Red
|
||||
0x5 | Magenta | 0xd | Pink
|
||||
0x6 | Brown | 0xe | Yellow
|
||||
0x7 | Light Gray | 0xf | White
|
||||
|
||||
بیت 4، بیت روشنایی است ، که به عنوان مثال آبی به آبی روشن تبدیل میکند. برای رنگ پس زمینه ، این بیت به عنوان بیت چشمک مورد استفاده قرار می گیرد.
|
||||
|
||||
بافر متن VGA از طریق [ورودی/خروجی حافظهنگاشتی] به آدرس`0xb8000` قابل دسترسی است. این بدان معنی است که خواندن و نوشتن در آن آدرس به RAM دسترسی ندارد ، بلکه مستقیماً دسترسی به بافر متن در سخت افزار VGA دارد. این بدان معنی است که می توانیم آن را از طریق عملیات حافظه عادی در آن آدرس بخوانیم و بنویسیم.
|
||||
|
||||
[ورودی/خروجی حافظهنگاشتی]: https://en.wikipedia.org/wiki/Memory-mapped_I/O
|
||||
|
||||
توجه داشته باشید که ممکن است سخت افزار حافظهنگاشتی شده از تمام عملیات معمول RAM پشتیبانی نکند. به عنوان مثال ، یک دستگاه ممکن است فقط خواندن بایتی را پشتیبانی کرده و با خواندن `u64` یک مقدار زباله را برگرداند. خوشبختانه بافر متن [از خواندن و نوشتن عادی پشتیبانی می کند] ، بنابراین مجبور نیستیم با آن به روش خاصی برخورد کنیم.
|
||||
|
||||
[از خواندن و نوشتن عادی پشتیبانی می کند]: https://web.stanford.edu/class/cs140/projects/pintos/specs/freevga/vga/vgamem.htm#manip
|
||||
|
||||
## یک ماژول راست
|
||||
اکنون که از نحوه کار بافر VGA مطلع شدیم ، می توانیم یک ماژول Rust برای مدیریت چاپ ایجاد کنیم:
|
||||
|
||||
```rust
|
||||
// in src/main.rs
|
||||
mod vga_buffer;
|
||||
```
|
||||
|
||||
برای محتوای این ماژول ما یک فایل جدید `src/vga_buffer.rs` ایجاد می کنیم. همه کدهای زیر وارد ماژول جدید ما می شوند (مگر اینکه طور دیگری مشخص شده باشد).
|
||||
|
||||
### رنگ ها
|
||||
اول ، ما رنگ های مختلف را با استفاده از یک enum نشان می دهیم:
|
||||
|
||||
```rust
|
||||
// in src/vga_buffer.rs
|
||||
|
||||
#[allow(dead_code)]
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
#[repr(u8)]
|
||||
pub enum Color {
|
||||
Black = 0,
|
||||
Blue = 1,
|
||||
Green = 2,
|
||||
Cyan = 3,
|
||||
Red = 4,
|
||||
Magenta = 5,
|
||||
Brown = 6,
|
||||
LightGray = 7,
|
||||
DarkGray = 8,
|
||||
LightBlue = 9,
|
||||
LightGreen = 10,
|
||||
LightCyan = 11,
|
||||
LightRed = 12,
|
||||
Pink = 13,
|
||||
Yellow = 14,
|
||||
White = 15,
|
||||
}
|
||||
```
|
||||
ما در اینجا از [enum مانند C] برای مشخص کردن صریح عدد برای هر رنگ استفاده می کنیم. به دلیل ویژگی `repr(u8)` هر نوع enum به عنوان یک `u8` ذخیره می شود. در واقع 4 بیت کافی است ، اما Rust نوع `u4` ندارد.
|
||||
|
||||
[enum مانند C]: https://doc.rust-lang.org/rust-by-example/custom_types/enum/c_like.html
|
||||
|
||||
به طور معمول کامپایلر برای هر نوع استفاده نشده اخطار می دهد. با استفاده از ویژگی `#[allow(dead_code)]` این هشدارها را برای enum `Color` غیرفعال می کنیم.
|
||||
|
||||
توسط [deriving] کردن تریتهای [`Copy`], [`Clone`], [`Debug`], [`PartialEq`], و [`Eq`] ما [مفهوم کپی] را برای نوع فعال کرده و آن را قابل پرینت کردن میکنیم.
|
||||
|
||||
[deriving]: https://doc.rust-lang.org/rust-by-example/trait/derive.html
|
||||
[`Copy`]: https://doc.rust-lang.org/nightly/core/marker/trait.Copy.html
|
||||
[`Clone`]: https://doc.rust-lang.org/nightly/core/clone/trait.Clone.html
|
||||
[`Debug`]: https://doc.rust-lang.org/nightly/core/fmt/trait.Debug.html
|
||||
[`PartialEq`]: https://doc.rust-lang.org/nightly/core/cmp/trait.PartialEq.html
|
||||
[`Eq`]: https://doc.rust-lang.org/nightly/core/cmp/trait.Eq.html
|
||||
[مفهوم کپی]: https://doc.rust-lang.org/1.30.0/book/first-edition/ownership.html#copy-types
|
||||
|
||||
برای نشان دادن یک کد کامل رنگ که رنگ پیش زمینه و پس زمینه را مشخص می کند ، یک [نوع جدید] بر روی `u8` ایجاد می کنیم:
|
||||
|
||||
[نوع جدید]: https://doc.rust-lang.org/rust-by-example/generics/new_types.html
|
||||
|
||||
```rust
|
||||
// in src/vga_buffer.rs
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
#[repr(transparent)]
|
||||
struct ColorCode(u8);
|
||||
|
||||
impl ColorCode {
|
||||
fn new(foreground: Color, background: Color) -> ColorCode {
|
||||
ColorCode((background as u8) << 4 | (foreground as u8))
|
||||
}
|
||||
}
|
||||
```
|
||||
ساختمان `ColorCode` شامل بایت کامل رنگ است که شامل رنگ پیش زمینه و پس زمینه است. مانند قبل ، ویژگی های `Copy` و` Debug` را برای آن derive می کنیم. برای اطمینان از اینکه `ColorCode` دقیقاً ساختار داده مشابه `u8` دارد ، از ویژگی [`repr(transparent)`] استفاده می کنیم.
|
||||
|
||||
[`repr(transparent)`]: https://doc.rust-lang.org/nomicon/other-reprs.html#reprtransparent
|
||||
|
||||
### بافر متن
|
||||
اکنون می توانیم ساختمانهایی را برای نمایش یک کاراکتر صفحه و بافر متن اضافه کنیم:
|
||||
|
||||
```rust
|
||||
// in src/vga_buffer.rs
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
#[repr(C)]
|
||||
struct ScreenChar {
|
||||
ascii_character: u8,
|
||||
color_code: ColorCode,
|
||||
}
|
||||
|
||||
const BUFFER_HEIGHT: usize = 25;
|
||||
const BUFFER_WIDTH: usize = 80;
|
||||
|
||||
#[repr(transparent)]
|
||||
struct Buffer {
|
||||
chars: [[ScreenChar; BUFFER_WIDTH]; BUFFER_HEIGHT],
|
||||
}
|
||||
```
|
||||
از آنجا که ترتیب فیلدهای ساختمانهای پیش فرض در Rust تعریف نشده است ، به ویژگی[`repr(C)`] نیاز داریم. این تضمین می کند که فیلد های ساختمان دقیقاً مانند یک ساختمان C ترسیم شده اند و بنابراین ترتیب درست را تضمین می کند. برای ساختمان `Buffer` ، ما دوباره از [`repr(transparent)`] استفاده می کنیم تا اطمینان حاصل شود که نحوه قرارگیری در حافظه دقیقا همان یک فیلد است.
|
||||
|
||||
[`repr(C)`]: https://doc.rust-lang.org/nightly/nomicon/other-reprs.html#reprc
|
||||
|
||||
برای نوشتن در صفحه ، اکنون یک نوع نویسنده ایجاد می کنیم:
|
||||
|
||||
```rust
|
||||
// in src/vga_buffer.rs
|
||||
|
||||
pub struct Writer {
|
||||
column_position: usize,
|
||||
color_code: ColorCode,
|
||||
buffer: &'static mut Buffer,
|
||||
}
|
||||
```
|
||||
نویسنده همیشه در آخرین خط مینویسد و وقتی خط پر است (یا در `\n`) ، سطرها را به سمت بالا شیفت می دهد. فیلد `column_position` موقعیت فعلی در ردیف آخر را نگهداری می کند. رنگهای پیش زمینه و پس زمینه فعلی توسط `color_code` مشخص شده و یک ارجاع (رفرنس) به بافر VGA در `buffer` ذخیره می شود. توجه داشته باشید که ما در اینجا به [طول عمر مشخصی] نیاز داریم تا به کامپایلر بگوییم تا چه مدت این ارجاع معتبر است. ظول عمر [`'static`] مشخص می کند که ارجاع برای کل مدت زمان اجرای برنامه معتبر باشد (که برای بافر متن VGA درست است).
|
||||
|
||||
[طول عمر مشخصی]: https://doc.rust-lang.org/book/ch10-03-lifetime-syntax.html#lifetime-annotation-syntax
|
||||
[`'static`]: https://doc.rust-lang.org/book/ch10-03-lifetime-syntax.html#the-static-lifetime
|
||||
|
||||
### چاپ کردن
|
||||
اکنون می توانیم از `Writer` برای تغییر کاراکترهای بافر استفاده کنیم. ابتدا یک متد برای نوشتن یک بایت ASCII ایجاد می کنیم:
|
||||
|
||||
```rust
|
||||
// in src/vga_buffer.rs
|
||||
|
||||
impl Writer {
|
||||
pub fn write_byte(&mut self, byte: u8) {
|
||||
match byte {
|
||||
b'\n' => self.new_line(),
|
||||
byte => {
|
||||
if self.column_position >= BUFFER_WIDTH {
|
||||
self.new_line();
|
||||
}
|
||||
|
||||
let row = BUFFER_HEIGHT - 1;
|
||||
let col = self.column_position;
|
||||
|
||||
let color_code = self.color_code;
|
||||
self.buffer.chars[row][col] = ScreenChar {
|
||||
ascii_character: byte,
|
||||
color_code,
|
||||
};
|
||||
self.column_position += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn new_line(&mut self) {/* TODO */}
|
||||
}
|
||||
```
|
||||
اگر بایت، بایتِ [خط جدید] `\n` باشد، نویسنده چیزی چاپ نمی کند. در عوض متد `new_line` را فراخوانی می کند که بعداً آن را پیادهسازی خواهیم کرد. بایت های دیگر در حالت دوم match روی صفحه چاپ می شوند.
|
||||
|
||||
[خط جدید]: https://en.wikipedia.org/wiki/Newline
|
||||
|
||||
هنگام چاپ بایت ، نویسنده بررسی می کند که آیا خط فعلی پر است یا نه. در صورت پُر بودن، برای نوشتن در خط ، باید متد `new_line` صدا زده شود. سپس یک `ScreenChar` جدید در بافر در موقعیت فعلی می نویسد. سرانجام ، موقعیت ستون فعلی یکی افزایش مییابد.
|
||||
|
||||
برای چاپ کل رشته ها، می توانیم آنها را به بایت تبدیل کرده و یکی یکی چاپ کنیم:
|
||||
|
||||
```rust
|
||||
// in src/vga_buffer.rs
|
||||
|
||||
impl Writer {
|
||||
pub fn write_string(&mut self, s: &str) {
|
||||
for byte in s.bytes() {
|
||||
match byte {
|
||||
// printable ASCII byte or newline
|
||||
0x20..=0x7e | b'\n' => self.write_byte(byte),
|
||||
// not part of printable ASCII range
|
||||
_ => self.write_byte(0xfe),
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
بافر متن VGA فقط از ASCII و بایت های اضافی [کد صفحه 437] پشتیبانی می کند. رشته های راست به طور پیش فرض [UTF-8] هستند ، بنابراین ممکن است حاوی بایت هایی باشند که توسط بافر متن VGA پشتیبانی نمی شوند. ما از یک match برای تفکیک بایت های قابل چاپ ASCII (یک خط جدید یا هر چیز دیگری بین یک کاراکتر فاصله و یک کاراکتر`~`) و بایت های غیر قابل چاپ استفاده می کنیم. برای بایت های غیر قابل چاپ ، یک کاراکتر `■` چاپ می کنیم که دارای کد شانزدهای (hex) `0xfe` بر روی سخت افزار VGA است.
|
||||
|
||||
[کد صفحه 437]: https://en.wikipedia.org/wiki/Code_page_437
|
||||
[UTF-8]: https://www.fileformat.info/info/unicode/utf8.htm
|
||||
|
||||
#### امتحاناش کنید!
|
||||
برای نوشتن چند کاراکتر بر روی صفحه ، می توانید یک تابع موقتی ایجاد کنید:
|
||||
|
||||
```rust
|
||||
// in src/vga_buffer.rs
|
||||
|
||||
pub fn print_something() {
|
||||
let mut writer = Writer {
|
||||
column_position: 0,
|
||||
color_code: ColorCode::new(Color::Yellow, Color::Black),
|
||||
buffer: unsafe { &mut *(0xb8000 as *mut Buffer) },
|
||||
};
|
||||
|
||||
writer.write_byte(b'H');
|
||||
writer.write_string("ello ");
|
||||
writer.write_string("Wörld!");
|
||||
}
|
||||
```
|
||||
ابتدا یک Writer جدید ایجاد می کند که به بافر VGA در `0xb8000` اشاره دارد. سینتکس این ممکن است کمی عجیب به نظر برسد: اول ، ما عدد صحیح `0xb8000` را به عنوان [اشاره گر خام] قابل تغییر در نظر می گیریم. سپس با dereferencing کردن آن (از طریق "*") و بلافاصله ارجاع مجدد (از طریق `&mut`) آن را به یک مرجع قابل تغییر تبدیل می کنیم. این تبدیل به یک [بلوک `غیرایمن`] احتیاج دارد ، زیرا کامپایلر نمی تواند صحت اشارهگر خام را تضمین کند.
|
||||
|
||||
[اشاره گر خام]: https://doc.rust-lang.org/book/ch19-01-unsafe-rust.html#dereferencing-a-raw-pointer
|
||||
[بلوک `غیرایمن`]: https://doc.rust-lang.org/book/ch19-01-unsafe-rust.html
|
||||
|
||||
سپس بایت `b'H'` را روی آن می نویسد. پیشوند `b` یک [بایت لیترال] ایجاد می کند ، که بیانگر یک کاراکتر ASCII است. با نوشتن رشته های `"ello "` و `"Wörld!"` ، ما متد `write_string` و واکنش به کاراکترهای غیر قابل چاپ را آزمایش می کنیم. برای دیدن خروجی ، باید تابع `print_something` را از تابع `_start` فراخوانی کنیم:
|
||||
|
||||
```rust
|
||||
// in src/main.rs
|
||||
|
||||
#[no_mangle]
|
||||
pub extern "C" fn _start() -> ! {
|
||||
vga_buffer::print_something();
|
||||
|
||||
loop {}
|
||||
}
|
||||
```
|
||||
|
||||
اکنون هنگامی که ما پروژه را اجرا می کنیم ، باید یک `Hello W■■rld!` در گوشه سمت چپ _پایین_ صفحه به رنگ زرد چاپ شود:
|
||||
|
||||
[بایت لیترال]: https://doc.rust-lang.org/reference/tokens.html#byte-literals
|
||||
|
||||

|
||||
|
||||
توجه داشته باشید که `ö` به عنوان دو کاراکتر `■` چاپ شده است. به این دلیل که `ö` با دو بایت در [UTF-8] نمایش داده می شود ، که هر دو در محدوده قابل چاپ ASCII قرار نمی گیرند. در حقیقت ، این یک ویژگی اساسی UTF-8 است: هر بایت از مقادیر چند بایتی هرگز ASCII معتبر نیستند.
|
||||
|
||||
### فرّار
|
||||
ما الان دیدیم که پیام ما به درستی چاپ شده است. با این حال ، ممکن است با کامپایلرهای آینده Rust که به صورت تهاجمی تری(aggressively) بهینه می شوند ، کار نکند.
|
||||
|
||||
مشکل این است که ما فقط به `Buffer` می نویسیم و هرگز از آن نمیخوانیم. کامپایلر نمی داند که ما واقعاً به حافظه بافر VGA (به جای RAM معمولی) دسترسی پیدا می کنیم و در مورد اثر جانبی آن یعنی نمایش برخی کاراکتر ها روی صفحه چیزی نمی داند. بنابراین ممکن است تصمیم بگیرد که این نوشتن ها غیرضروری هستند و می تواند آن را حذف کند. برای جلوگیری از این بهینه سازی اشتباه ، باید این نوشتن ها را به عنوان _[فرّار]_ مشخص کنیم. این به کامپایلر می گوید که نوشتن عوارض جانبی دارد و نباید بهینه شود.
|
||||
|
||||
[فرّار]: https://en.wikipedia.org/wiki/Volatile_(computer_programming)
|
||||
|
||||
به منظور استفاده از نوشتن های فرار برای بافر VGA ، ما از کتابخانه [volatile][volatile crate] استفاده می کنیم. این _crate_ (بسته ها در جهان Rust اینطور نامیده میشوند) نوع `Volatile` را که یک نوع wrapper هست با متد های `read` و `write` فراهم می کند. این متد ها به طور داخلی از توابع [read_volatile] و [write_volatile] کتابخانه اصلی استفاده می کنند و بنابراین تضمین می کنند که خواندن/ نوشتن با بهینه شدن حذف نمیشوند.
|
||||
|
||||
[volatile crate]: https://docs.rs/volatile
|
||||
[read_volatile]: https://doc.rust-lang.org/nightly/core/ptr/fn.read_volatile.html
|
||||
[write_volatile]: https://doc.rust-lang.org/nightly/core/ptr/fn.write_volatile.html
|
||||
|
||||
ما می توانیم وابستگی به کرت (crate) `volatile` را بوسیله اضافه کردن آن به بخش `dependencies` (وابستگی های) `Cargo.toml` اضافه کنیم:
|
||||
|
||||
```toml
|
||||
# in Cargo.toml
|
||||
|
||||
[dependencies]
|
||||
volatile = "0.2.6"
|
||||
```
|
||||
|
||||
`0.2.6` شماره نسخه [معنایی] است. برای اطلاعات بیشتر ، به راهنمای [تعیین وابستگی ها] مستندات کارگو (cargo) مراجعه کنید.
|
||||
|
||||
[معنایی]: https://semver.org/
|
||||
[تعیین وابستگی ها]: https://doc.crates.io/specifying-dependencies.html
|
||||
|
||||
بیایید از آن برای نوشتن فرار در بافر VGA استفاده کنیم. نوع `Buffer` خود را به صورت زیر بروزرسانی می کنیم:
|
||||
|
||||
```rust
|
||||
// in src/vga_buffer.rs
|
||||
|
||||
use volatile::Volatile;
|
||||
|
||||
struct Buffer {
|
||||
chars: [[Volatile<ScreenChar>; BUFFER_WIDTH]; BUFFER_HEIGHT],
|
||||
}
|
||||
```
|
||||
به جای `ScreenChar` ، ما اکنون از `Volatile<ScreenChar>` استفاده می کنیم. (نوع `Volatile`، [generic] است و می تواند (تقریباً) هر نوع را در خود قرار دهد). این اطمینان می دهد که ما به طور تصادفی نمی توانیم از طریق نوشتن "عادی" در آن بنویسیم. در عوض ، اکنون باید از متد `write` استفاده کنیم.
|
||||
|
||||
[generic]: https://doc.rust-lang.org/book/ch10-01-syntax.html
|
||||
|
||||
این بدان معنی است که ما باید متد `Writer::write_byte` خود را به روز کنیم:
|
||||
|
||||
```rust
|
||||
// in src/vga_buffer.rs
|
||||
|
||||
impl Writer {
|
||||
pub fn write_byte(&mut self, byte: u8) {
|
||||
match byte {
|
||||
b'\n' => self.new_line(),
|
||||
byte => {
|
||||
...
|
||||
|
||||
self.buffer.chars[row][col].write(ScreenChar {
|
||||
ascii_character: byte,
|
||||
color_code,
|
||||
});
|
||||
...
|
||||
}
|
||||
}
|
||||
}
|
||||
...
|
||||
}
|
||||
```
|
||||
|
||||
به جای انتساب عادی با استفاده از `=` ، اکنون ما از متد `write` استفاده می کنیم. این تضمین می کند که کامپایلر هرگز این نوشتن را بهینه نخواهد کرد.
|
||||
|
||||
### ماکروهای قالببندی
|
||||
خوب است که از ماکروهای قالب بندی Rust نیز پشتیبانی کنید. به این ترتیب ، می توانیم انواع مختلفی مانند عدد صحیح یا شناور را به راحتی چاپ کنیم. برای پشتیبانی از آنها ، باید تریت [`core::fmt::Write`] را پیاده سازی کنیم. تنها متد مورد نیاز این تریت ،`write_str` است که کاملاً شبیه به متد `write_str` ما است ، فقط با نوع بازگشت `fmt::Result`:
|
||||
|
||||
[`core::fmt::Write`]: https://doc.rust-lang.org/nightly/core/fmt/trait.Write.html
|
||||
|
||||
```rust
|
||||
// in src/vga_buffer.rs
|
||||
|
||||
use core::fmt;
|
||||
|
||||
impl fmt::Write for Writer {
|
||||
fn write_str(&mut self, s: &str) -> fmt::Result {
|
||||
self.write_string(s);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
```
|
||||
`Ok(())` فقط نتیجه `Ok` حاوی نوع `()` است.
|
||||
|
||||
اکنون ما می توانیم از ماکروهای قالب بندی داخلی راست یعنی `write!`/`writeln!` استفاده کنیم:
|
||||
|
||||
```rust
|
||||
// in src/vga_buffer.rs
|
||||
|
||||
pub fn print_something() {
|
||||
use core::fmt::Write;
|
||||
let mut writer = Writer {
|
||||
column_position: 0,
|
||||
color_code: ColorCode::new(Color::Yellow, Color::Black),
|
||||
buffer: unsafe { &mut *(0xb8000 as *mut Buffer) },
|
||||
};
|
||||
|
||||
writer.write_byte(b'H');
|
||||
writer.write_string("ello! ");
|
||||
write!(writer, "The numbers are {} and {}", 42, 1.0/3.0).unwrap();
|
||||
}
|
||||
```
|
||||
|
||||
حالا شما باید یک `Hello! The numbers are 42 and 0.3333333333333333` در پایین صفحه ببینید. فراخوانی `write!` یک `Result` را برمی گرداند که در صورت عدم استفاده باعث هشدار می شود ، بنابراین ما تابع [`unwrap`] را روی آن فراخوانی می کنیم که در صورت بروز خطا پنیک می کند. این در مورد ما مشکلی ندارد ، زیرا نوشتن در بافر VGA هرگز شکست نمیخورد.
|
||||
|
||||
[`unwrap`]: https://doc.rust-lang.org/core/result/enum.Result.html#method.unwrap
|
||||
|
||||
### خطوط جدید
|
||||
در حال حاضر ، ما از خطوط جدید و کاراکتر هایی که دیگر در خط نمی گنجند چشم پوشی می کنیم. درعوض ما می خواهیم هر کاراکتر را یک خط به بالا منتقل کنیم (خط بالا حذف می شود) و دوباره از ابتدای آخرین خط شروع کنیم. برای انجام این کار ، ما یک پیاده سازی برای متد `new_line` در `Writer` اضافه می کنیم:
|
||||
|
||||
```rust
|
||||
// in src/vga_buffer.rs
|
||||
|
||||
impl Writer {
|
||||
fn new_line(&mut self) {
|
||||
for row in 1..BUFFER_HEIGHT {
|
||||
for col in 0..BUFFER_WIDTH {
|
||||
let character = self.buffer.chars[row][col].read();
|
||||
self.buffer.chars[row - 1][col].write(character);
|
||||
}
|
||||
}
|
||||
self.clear_row(BUFFER_HEIGHT - 1);
|
||||
self.column_position = 0;
|
||||
}
|
||||
|
||||
fn clear_row(&mut self, row: usize) {/* TODO */}
|
||||
}
|
||||
```
|
||||
ما تمام کاراکترهای صفحه را پیمایش می کنیم و هر کاراکتر را یک ردیف به بالا شیفت می دهیم. توجه داشته باشید که علامت گذاری دامنه (`..`) فاقد مقدار حد بالا است. ما همچنین سطر 0 را حذف می کنیم (اول محدوده از "1" شروع می شود) زیرا این سطر است که از صفحه به بیرون شیفت می شود.
|
||||
|
||||
برای تکمیل کد `newline` ، متد `clear_row` را اضافه می کنیم:
|
||||
|
||||
```rust
|
||||
// in src/vga_buffer.rs
|
||||
|
||||
impl Writer {
|
||||
fn clear_row(&mut self, row: usize) {
|
||||
let blank = ScreenChar {
|
||||
ascii_character: b' ',
|
||||
color_code: self.color_code,
|
||||
};
|
||||
for col in 0..BUFFER_WIDTH {
|
||||
self.buffer.chars[row][col].write(blank);
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
این متد با جایگزینی تمام کاراکترها با یک کاراکتر فاصله ، یک سطر را پاک می کند.
|
||||
|
||||
## یک رابط گلوبال
|
||||
برای فراهم کردن یک نویسنده گلوبال که بتواند به عنوان رابط از سایر ماژول ها بدون حمل نمونه `Writer` در اطراف استفاده شود ، سعی می کنیم یک `WRITER` ثابت ایجاد کنیم:
|
||||
|
||||
```rust
|
||||
// in src/vga_buffer.rs
|
||||
|
||||
pub static WRITER: Writer = Writer {
|
||||
column_position: 0,
|
||||
color_code: ColorCode::new(Color::Yellow, Color::Black),
|
||||
buffer: unsafe { &mut *(0xb8000 as *mut Buffer) },
|
||||
};
|
||||
```
|
||||
|
||||
با این حال ، اگر سعی کنیم اکنون آن را کامپایل کنیم ، خطاهای زیر رخ می دهد:
|
||||
|
||||
```
|
||||
error[E0015]: calls in statics are limited to constant functions, tuple structs and tuple variants
|
||||
--> src/vga_buffer.rs:7:17
|
||||
|
|
||||
7 | color_code: ColorCode::new(Color::Yellow, Color::Black),
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
error[E0396]: raw pointers cannot be dereferenced in statics
|
||||
--> src/vga_buffer.rs:8:22
|
||||
|
|
||||
8 | buffer: unsafe { &mut *(0xb8000 as *mut Buffer) },
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ dereference of raw pointer in constant
|
||||
|
||||
error[E0017]: references in statics may only refer to immutable values
|
||||
--> src/vga_buffer.rs:8:22
|
||||
|
|
||||
8 | buffer: unsafe { &mut *(0xb8000 as *mut Buffer) },
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ statics require immutable values
|
||||
|
||||
error[E0017]: references in statics may only refer to immutable values
|
||||
--> src/vga_buffer.rs:8:13
|
||||
|
|
||||
8 | buffer: unsafe { &mut *(0xb8000 as *mut Buffer) },
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ statics require immutable values
|
||||
```
|
||||
|
||||
برای فهمیدن آنچه در اینجا اتفاق می افتد ، باید بدانیم که ثابت ها(Statics) در زمان کامپایل مقداردهی اولیه می شوند ، برخلاف متغیرهای عادی که در زمان اجرا مقداردهی اولیه می شوند. مولفهای(component) از کامپایلر Rust که چنین عبارات مقداردهی اولیه را ارزیابی می کند ، “[const evaluator]” نامیده می شود. عملکرد آن هنوز محدود است ، اما کارهای گسترده ای برای گسترش آن در حال انجام است ، به عنوان مثال در “[Allow panicking in constants]” RFC.
|
||||
|
||||
[const evaluator]: https://rustc-dev-guide.rust-lang.org/const-eval.html
|
||||
[Allow panicking in constants]: https://github.com/rust-lang/rfcs/pull/2345
|
||||
|
||||
مسئله در مورد `ColorCode::new` با استفاده از توابع [`const` functions] قابل حل است ، اما مشکل اساسی اینجاست که Rust's const evaluator قادر به تبدیل اشارهگرهای خام به رفرنس در زمان کامپایل نیست. شاید روزی جواب دهد ، اما تا آن زمان ، ما باید راه حل دیگری پیدا کنیم.
|
||||
|
||||
[`const` functions]: https://doc.rust-lang.org/reference/const_eval.html#const-functions
|
||||
|
||||
### استاتیکهای تنبل (Lazy Statics)
|
||||
یکبار مقداردهی اولیه استاتیکها با توابع غیر ثابت یک مشکل رایج در راست است. خوشبختانه ، در حال حاضر راه حل خوبی در کرتی به نام [lazy_static] وجود دارد. این کرت ماکرو `lazy_static!` را فراهم می کند که یک `استاتیک` را با تنبلی مقداردهی اولیه می کند. به جای محاسبه مقدار آن در زمان کامپایل ، `استاتیک` به تنبلی هنگام اولین دسترسی به آن، خود را مقداردهی اولیه میکند. بنابراین ، مقداردهی اولیه در زمان اجرا اتفاق می افتد تا کد مقدار دهی اولیه پیچیده و دلخواه امکان پذیر باشد.
|
||||
|
||||
[lazy_static]: https://docs.rs/lazy_static/1.0.1/lazy_static/
|
||||
|
||||
بیایید کرت `lazy_static` را به پروژه خود اضافه کنیم:
|
||||
|
||||
```toml
|
||||
# in Cargo.toml
|
||||
|
||||
[dependencies.lazy_static]
|
||||
version = "1.0"
|
||||
features = ["spin_no_std"]
|
||||
```
|
||||
|
||||
ما به ویژگی `spin_no_std` نیاز داریم ، زیرا به کتابخانه استاندارد پیوند نمی دهیم.
|
||||
|
||||
با استفاده از `lazy_static` ، می توانیم WRITER ثابت خود را بدون مشکل تعریف کنیم:
|
||||
|
||||
```rust
|
||||
// in src/vga_buffer.rs
|
||||
|
||||
use lazy_static::lazy_static;
|
||||
|
||||
lazy_static! {
|
||||
pub static ref WRITER: Writer = Writer {
|
||||
column_position: 0,
|
||||
color_code: ColorCode::new(Color::Yellow, Color::Black),
|
||||
buffer: unsafe { &mut *(0xb8000 as *mut Buffer) },
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
با این حال ، این `WRITER` بسیار بی فایده است زیرا غیر قابل تغییر است. این بدان معنی است که ما نمی توانیم چیزی در آن بنویسیم (از آنجا که همه متد های نوشتن `&mut self` را در ورودی میگیرند). یک راه حل ممکن استفاده از [استاتیک قابل تغییر] است. اما پس از آن هر خواندن و نوشتن آن ناامن (unsafe) است زیرا می تواند به راحتی باعث data race و سایر موارد بد باشد. استفاده از `static mut` بسیار نهی شده است ، حتی پیشنهادهایی برای [حذف آن][remove static mut] وجود داشت. اما گزینه های دیگر چیست؟ ما می توانیم سعی کنیم از یک استاتیک تغییرناپذیر با نوع سلول مانند [RefCell] یا حتی [UnsafeCell] استفاده کنیم که [تغییر پذیری داخلی] را فراهم می کند. اما این انواع [Sync] نیستند (با دلیل کافی) ، بنابراین نمی توانیم از آنها در استاتیک استفاده کنیم.
|
||||
|
||||
[استاتیک قابل تغییر]: https://doc.rust-lang.org/book/ch19-01-unsafe-rust.html#accessing-or-modifying-a-mutable-static-variable
|
||||
[remove static mut]: https://internals.rust-lang.org/t/pre-rfc-remove-static-mut/1437
|
||||
[RefCell]: https://doc.rust-lang.org/book/ch15-05-interior-mutability.html#keeping-track-of-borrows-at-runtime-with-refcellt
|
||||
[UnsafeCell]: https://doc.rust-lang.org/nightly/core/cell/struct.UnsafeCell.html
|
||||
[تغییر پذیری داخلی]: https://doc.rust-lang.org/book/ch15-05-interior-mutability.html
|
||||
[Sync]: https://doc.rust-lang.org/nightly/core/marker/trait.Sync.html
|
||||
|
||||
### Spinlocks
|
||||
برای دستیابی به قابلیت تغییرپذیری داخلی همزمان (synchronized) ، کاربران کتابخانه استاندارد می توانند از [Mutex] استفاده کنند. هنگامی که منبع از قبل قفل شده است ، با مسدود کردن رشته ها ، امکان انحصار متقابل را فراهم می کند. اما هسته اصلی ما هیچ پشتیبانی از مسدود کردن یا حتی مفهومی از نخ ها ندارد ، بنابراین ما هم نمی توانیم از آن استفاده کنیم. با این وجود یک نوع کاملاً پایهای از mutex در علوم کامپیوتر وجود دارد که به هیچ ویژگی سیستم عاملی نیاز ندارد: [spinlock]. به جای مسدود کردن ، نخ ها سعی می کنند آن را بارها و بارها در یک حلقه قفل کنند و بنابراین زمان پردازنده را می سوزانند تا دوباره mutex آزاد شود.
|
||||
|
||||
[Mutex]: https://doc.rust-lang.org/nightly/std/sync/struct.Mutex.html
|
||||
[spinlock]: https://en.wikipedia.org/wiki/Spinlock
|
||||
|
||||
برای استفاده از spinning mutex ، می توانیم [کرت spin] را به عنوان یک وابستگی اضافه کنیم:
|
||||
|
||||
[کرت spin]: https://crates.io/crates/spin
|
||||
|
||||
```toml
|
||||
# in Cargo.toml
|
||||
[dependencies]
|
||||
spin = "0.5.2"
|
||||
```
|
||||
|
||||
سپس می توانیم از spinning Mutex برای افزودن [تغییر پذیری داخلی] امن به `WRITER` استاتیک خود استفاده کنیم:
|
||||
|
||||
```rust
|
||||
// in src/vga_buffer.rs
|
||||
|
||||
use spin::Mutex;
|
||||
...
|
||||
lazy_static! {
|
||||
pub static ref WRITER: Mutex<Writer> = Mutex::new(Writer {
|
||||
column_position: 0,
|
||||
color_code: ColorCode::new(Color::Yellow, Color::Black),
|
||||
buffer: unsafe { &mut *(0xb8000 as *mut Buffer) },
|
||||
});
|
||||
}
|
||||
```
|
||||
اکنون می توانیم تابع `print_something` را حذف کرده و مستقیماً از تابع`_start` خود چاپ کنیم:
|
||||
|
||||
```rust
|
||||
// in src/main.rs
|
||||
#[no_mangle]
|
||||
pub extern "C" fn _start() -> ! {
|
||||
use core::fmt::Write;
|
||||
vga_buffer::WRITER.lock().write_str("Hello again").unwrap();
|
||||
write!(vga_buffer::WRITER.lock(), ", some numbers: {} {}", 42, 1.337).unwrap();
|
||||
|
||||
loop {}
|
||||
}
|
||||
```
|
||||
برای اینکه بتوانیم از توابع آن استفاده کنیم ، باید تریت `fmt::Write` را وارد کنیم.
|
||||
|
||||
### ایمنی
|
||||
توجه داشته باشید که ما فقط یک بلوک ناامن در کد خود داریم که برای ایجاد رفرنس `Buffer` با اشاره به `0xb8000` لازم است. پس از آن ، تمام عملیات ایمن هستند. Rust به طور پیش فرض از بررسی مرزها در دسترسی به آرایه استفاده می کند ، بنابراین نمی توانیم به طور اتفاقی خارج از بافر بنویسیم. بنابراین ، ما شرایط مورد نیاز را در سیستم نوع انجام میدهیم و قادر به ایجاد یک رابط ایمن به خارج هستیم.
|
||||
|
||||
### یک ماکروی println
|
||||
اکنون که یک نویسنده گلوبال داریم ، می توانیم یک ماکرو `println` اضافه کنیم که می تواند از هر کجا در کد استفاده شود. [سینتکس ماکروی] راست کمی عجیب است ، بنابراین ما سعی نمی کنیم ماکرو را از ابتدا بنویسیم. در عوض به سورس [ماکروی `println!`] در کتابخانه استاندارد نگاه می کنیم:
|
||||
|
||||
[سینتکس ماکروی]: https://doc.rust-lang.org/nightly/book/ch19-06-macros.html#declarative-macros-with-macro_rules-for-general-metaprogramming
|
||||
[ماکروی `println!`]: https://doc.rust-lang.org/nightly/std/macro.println!.html
|
||||
|
||||
```rust
|
||||
#[macro_export]
|
||||
macro_rules! println {
|
||||
() => (print!("\n"));
|
||||
($($arg:tt)*) => (print!("{}\n", format_args!($($arg)*)));
|
||||
}
|
||||
```
|
||||
|
||||
ماکروها از طریق یک یا چند قانون تعریف می شوند که شبیه بازوهای `match` هستند. ماکرو `println` دارای دو قانون است: اولین قانون برای فراخوانی های بدون آرگمان است (به عنوان مثال: `println!()`) ، که به `print!("\n")` گسترش می یابد، بنابراین فقط یک خط جدید را چاپ می کند. قانون دوم برای فراخوانی هایی با پارامترهایی مانند `println!("Hello")` یا `println!("Number: {}", 4)` است. همچنین با فراخوانی کل آرگومان ها و یک خط جدید `\n` اضافی در انتها ، به فراخوانی ماکرو `print!` گسترش می یابد.
|
||||
|
||||
ویژگی `#[macro_export]` ماکرو را برای کل کرت (نه فقط ماژولی که تعریف شده است) و کرت های خارجی در دسترس قرار می دهد. همچنین ماکرو را در ریشه کرت قرار می دهد ، به این معنی که ما باید ماکرو را به جای `std::macros::println` از طریق `use std::println` وارد کنیم.
|
||||
|
||||
[ماکرو `print!`] به این صورت تعریف می شود:
|
||||
|
||||
[ماکرو `print!`]: https://doc.rust-lang.org/nightly/std/macro.print!.html
|
||||
|
||||
```rust
|
||||
#[macro_export]
|
||||
macro_rules! print {
|
||||
($($arg:tt)*) => ($crate::io::_print(format_args!($($arg)*)));
|
||||
}
|
||||
```
|
||||
|
||||
ماکرو به فراخوانی [تابع `_print`] در ماژول `io` گسترش می یابد. [متغیر `$crate`] تضمین می کند که ماکرو هنگام گسترش در `std` در زمان استفاده در کرت های دیگر، در خارج از کرت `std` نیز کار می کند.
|
||||
|
||||
[ماکرو `format_args`] از آرگمان های داده شده یک نوع [fmt::Arguments] را می سازد که به `_print` ارسال می شود. [تابع `_print`] از کتابخانه استاندارد،`print_to` را فراخوانی می کند ، که بسیار پیچیده است زیرا از دستگاه های مختلف `Stdout` پشتیبانی می کند. ما به این پیچیدگی احتیاج نداریم زیرا فقط می خواهیم در بافر VGA چاپ کنیم.
|
||||
|
||||
[تابع `_print`]: https://github.com/rust-lang/rust/blob/29f5c699b11a6a148f097f82eaa05202f8799bbc/src/libstd/io/stdio.rs#L698
|
||||
[متغیر `$crate`]: https://doc.rust-lang.org/1.30.0/book/first-edition/macros.html#the-variable-crate
|
||||
[ماکرو `format_args`]: https://doc.rust-lang.org/nightly/std/macro.format_args.html
|
||||
[fmt::Arguments]: https://doc.rust-lang.org/nightly/core/fmt/struct.Arguments.html
|
||||
|
||||
برای چاپ در بافر VGA ، ما فقط ماکروهای `println!` و `print!` را کپی می کنیم ، اما آنها را اصلاح می کنیم تا از تابع `_print` خود استفاده کنیم:
|
||||
|
||||
```rust
|
||||
// in src/vga_buffer.rs
|
||||
|
||||
#[macro_export]
|
||||
macro_rules! print {
|
||||
($($arg:tt)*) => ($crate::vga_buffer::_print(format_args!($($arg)*)));
|
||||
}
|
||||
|
||||
#[macro_export]
|
||||
macro_rules! println {
|
||||
() => ($crate::print!("\n"));
|
||||
($($arg:tt)*) => ($crate::print!("{}\n", format_args!($($arg)*)));
|
||||
}
|
||||
|
||||
#[doc(hidden)]
|
||||
pub fn _print(args: fmt::Arguments) {
|
||||
use core::fmt::Write;
|
||||
WRITER.lock().write_fmt(args).unwrap();
|
||||
}
|
||||
```
|
||||
|
||||
چیزی که ما از تعریف اصلی `println` تغییر دادیم این است که فراخوانی ماکرو `print!` را با پیشوند `$crate` انجام می دهیم. این تضمین می کند که اگر فقط می خواهیم از `println` استفاده کنیم ، نیازی به وارد کردن ماکرو `print!` هم نداشته باشیم.
|
||||
|
||||
مانند کتابخانه استاندارد ، ویژگی `#[macro_export]` را به هر دو ماکرو اضافه می کنیم تا در همه جای کرت ما در دسترس باشند. توجه داشته باشید که این ماکروها را در فضای نام ریشه کرت قرار می دهد ، بنابراین وارد کردن آنها از طریق `use crate::vga_buffer::println` کار نمی کند. در عوض ، ما باید `use crate::println` را استفاده کنیم.
|
||||
|
||||
تابع `_print` نویسنده (`WRITER`) استاتیک ما را قفل می کند و متد`write_fmt` را روی آن فراخوانی می کند. این متد از تریت `Write` است ، ما باید این تریت را وارد کنیم. اگر چاپ موفقیت آمیز نباشد ، `unwrap()` اضافی در انتها باعث پنیک میشود. اما از آنجا که ما همیشه `Ok` را در `write_str` برمی گردانیم ، این اتفاق نمی افتد.
|
||||
|
||||
از آنجا که ماکروها باید بتوانند از خارج از ماژول، `_print` را فراخوانی کنند، تابع باید عمومی (public) باشد. با این حال ، از آنجا که این جزئیات پیاده سازی را خصوصی (private) در نظر می گیریم، [ویژگی `doc(hidden)`] را اضافه می کنیم تا از مستندات تولید شده پنهان شود.
|
||||
|
||||
[ویژگی `doc(hidden)`]: https://doc.rust-lang.org/nightly/rustdoc/the-doc-attribute.html#dochidden
|
||||
|
||||
### Hello World توسط `println`
|
||||
اکنون می توانیم از `println` در تابع `_start` استفاده کنیم:
|
||||
|
||||
```rust
|
||||
// in src/main.rs
|
||||
|
||||
#[no_mangle]
|
||||
pub extern "C" fn _start() {
|
||||
println!("Hello World{}", "!");
|
||||
|
||||
loop {}
|
||||
}
|
||||
```
|
||||
|
||||
توجه داشته باشید که ما مجبور نیستیم ماکرو را در تابع اصلی وارد کنیم ، زیرا در حال حاضر در فضای نام ریشه موجود است.
|
||||
|
||||
همانطور که انتظار می رفت ، اکنون یک _“Hello World!”_ روی صفحه مشاهده می کنیم:
|
||||
|
||||

|
||||
|
||||
### چاپ پیام های پنیک
|
||||
|
||||
اکنون که ماکرو `println` را داریم ، می توانیم از آن در تابع پنیک برای چاپ پیام و مکان پنیک استفاده کنیم:
|
||||
|
||||
```rust
|
||||
// in main.rs
|
||||
|
||||
/// This function is called on panic.
|
||||
#[panic_handler]
|
||||
fn panic(info: &PanicInfo) -> ! {
|
||||
println!("{}", info);
|
||||
loop {}
|
||||
}
|
||||
```
|
||||
|
||||
اکنون وقتی که `panic!("Some panic message");` را در تابع `_start` خود اضافه میکنیم ، خروجی زیر را می گیریم:
|
||||
|
||||

|
||||
|
||||
بنابراین ما نه تنها میدانیم که یک پنیک رخ داده است ، بلکه پیام پنیک و اینکه در کجای کد رخ داده است را نیز میدانیم.
|
||||
|
||||
## خلاصه
|
||||
در این پست با ساختار بافر متن VGA و نحوه نوشتن آن از طریق نگاشت حافظه در آدرس `0xb8000` آشنا شدیم. ما یک ماژول راست ایجاد کردیم که عدم امنیت نوشتن را در این بافر نگاشت حافظه شده را محصور می کند و یک رابط امن و راحت به خارج ارائه می دهد.
|
||||
|
||||
همچنین دیدیم که به لطف کارگو ، اضافه کردن وابستگی به کتابخانه های دیگران چقدر آسان است. دو وابستگی که اضافه کردیم ، `lazy_static` و`spin` ، در توسعه سیستم عامل بسیار مفید هستند و ما در پست های بعدی از آنها در مکان های بیشتری استفاده خواهیم کرد.
|
||||
|
||||
## بعدی چیست؟
|
||||
در پست بعدی نحوه راه اندازی چارچوب تست واحد (Unit Test) راست توضیح داده شده است. سپس از این پست چند تست واحد اساسی برای ماژول بافر VGA ایجاد خواهیم کرد.
|
||||
@@ -1,715 +0,0 @@
|
||||
+++
|
||||
title = "VGAテキストモード"
|
||||
weight = 3
|
||||
path = "ja/vga-text-mode"
|
||||
date = 2018-02-26
|
||||
|
||||
[extra]
|
||||
chapter = "Bare Bones"
|
||||
# Please update this when updating the translation
|
||||
translation_based_on_commit = "bd6fbcb1c36705b2c474d7fcee387bfea1210851"
|
||||
# GitHub usernames of the people that translated this post
|
||||
translators = ["woodyZootopia", "JohnTitor"]
|
||||
+++
|
||||
|
||||
[VGAテキストモード][VGA text mode]は画面にテキストを出力するシンプルな方法です。この記事では、すべてのunsafeな要素を別のモジュールにカプセル化することで、それを安全かつシンプルに扱えるようにするインターフェースを作ります。また、Rustの[フォーマッティングマクロ][formatting macros]のサポートも実装します。
|
||||
|
||||
[VGA text mode]: https://en.wikipedia.org/wiki/VGA-compatible_text_mode
|
||||
[formatting macros]: https://doc.rust-lang.org/std/fmt/#related-macros
|
||||
|
||||
<!-- more -->
|
||||
|
||||
このブログの内容は [GitHub] 上で公開・開発されています。何か問題や質問などがあれば issue をたててください (訳注: リンクは原文(英語)のものになります)。また[こちら][at the bottom]にコメントを残すこともできます。この記事の完全なソースコードは[`post-03` ブランチ][post branch]にあります。
|
||||
|
||||
[GitHub]: https://github.com/phil-opp/blog_os
|
||||
[at the bottom]: #comments
|
||||
<!-- fix for zola anchor checker (target is in template): <a id="comments"> -->
|
||||
[post branch]: https://github.com/phil-opp/blog_os/tree/post-03
|
||||
|
||||
<!-- toc -->
|
||||
|
||||
## VGAテキストバッファ
|
||||
VGAテキストモードにおいて、文字を画面に出力するには、VGAハードウェアのテキストバッファにそれを書き込まないといけません。VGAテキストバッファは、普通25行と80列からなる2次元配列で、画面に直接書き出されます。それぞれの配列の要素は画面上の一つの文字を以下の形式で表現しています:
|
||||
|
||||
ビット | 値
|
||||
------ | ----------------
|
||||
0-7 | ASCII コードポイント
|
||||
8-11 | フォアグラウンド(前景)色
|
||||
12-14 | バックグラウンド(背景)色
|
||||
15 | 点滅
|
||||
|
||||
最初の1バイトは、出力されるべき文字を[ASCIIエンコーディング][ASCII encoding]で表します。正確に言うと、完全にASCIIではなく、[コードページ437][_code page 437_]という、いくつか文字が追加され、軽微な修正のなされたものです。簡単のため、この記事ではASCII文字と呼ぶことにします。
|
||||
|
||||
[ASCII encoding]: https://ja.wikipedia.org/wiki/ASCII
|
||||
[_code page 437_]: https://ja.wikipedia.org/wiki/コードページ437
|
||||
|
||||
2つ目のバイトはその文字がどのように出力されるのかを定義します。最初の4ビットが前景色(訳注:文字自体の色)を、次の3ビットが背景色を、最後のビットがその文字が点滅するのかを決めます。以下の色を使うことができます:
|
||||
|
||||
数字 | 色 | 数字 + Bright Bit | <ruby>Bright<rp> (</rp><rt>明るい</rt><rp>) </rp></ruby> 色
|
||||
------ | ---------- | ------------------- | -------------
|
||||
0x0 | 黒 | 0x8 | 暗いグレー
|
||||
0x1 | 青 | 0x9 | 明るい青
|
||||
0x2 | 緑 | 0xa | 明るい緑
|
||||
0x3 | シアン | 0xb | 明るいシアン
|
||||
0x4 | 赤 | 0xc | 明るい赤
|
||||
0x5 | マゼンタ | 0xd | ピンク
|
||||
0x6 | 茶色 | 0xe | 黄色
|
||||
0x7 | 明るいグレー| 0xf | 白
|
||||
|
||||
4ビット目は **bright bit** で、これは(1になっているとき)たとえば青を明るい青に変えます。背景色については、このビットは点滅ビットとして再利用されています。
|
||||
|
||||
VGAテキストバッファはアドレス`0xb8000`に[<ruby>memory-mapped<rp> (</rp><rt>メモリマップされた</rt><rp>) </rp></ruby> I/O][memory-mapped I/O]を通じてアクセスできます。これは、このアドレスへの読み書きをしても、RAMではなく直接VGAハードウェアのテキストバッファにアクセスするということを意味します。つまり、このアドレスに対する通常のメモリ操作を通じて、テキストバッファを読み書きできるのです。
|
||||
|
||||
[memory-mapped I/O]: https://ja.wikipedia.org/wiki/メモリマップドI/O
|
||||
|
||||
メモリマップされたハードウェアは通常のRAM操作すべてをサポートしてはいないかもしれないということに注意してください。たとえば、デバイスはバイトずつの読み取りしかサポートしておらず、`u64`が読まれるとゴミデータを返すかもしれません。ありがたいことに、テキストバッファを特別なやり方で取り扱う必要がないよう、テキストバッファは[通常の読み書きをサポートしています][supports normal reads and writes]。
|
||||
|
||||
[supports normal reads and writes]: https://web.stanford.edu/class/cs140/projects/pintos/specs/freevga/vga/vgamem.htm#manip
|
||||
|
||||
## Rustのモジュール
|
||||
VGAバッファが動く仕組みを学んだので、さっそく画面出力を扱うRustのモジュールを作っていきます。
|
||||
|
||||
```rust
|
||||
// in src/main.rs
|
||||
mod vga_buffer;
|
||||
```
|
||||
|
||||
このモジュールの中身のために、新しい`src/vga_buffer.rs`というファイルを作ります。このファイル以下のコードは、(そうならないよう指定されない限り)すべてこの新しいモジュールの中に入ります。
|
||||
|
||||
### 色
|
||||
まず、様々な色をenumを使って表しましょう:
|
||||
|
||||
```rust
|
||||
// in src/vga_buffer.rs
|
||||
|
||||
#[allow(dead_code)]
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
#[repr(u8)]
|
||||
pub enum Color {
|
||||
Black = 0,
|
||||
Blue = 1,
|
||||
Green = 2,
|
||||
Cyan = 3,
|
||||
Red = 4,
|
||||
Magenta = 5,
|
||||
Brown = 6,
|
||||
LightGray = 7,
|
||||
DarkGray = 8,
|
||||
LightBlue = 9,
|
||||
LightGreen = 10,
|
||||
LightCyan = 11,
|
||||
LightRed = 12,
|
||||
Pink = 13,
|
||||
Yellow = 14,
|
||||
White = 15,
|
||||
}
|
||||
```
|
||||
ここでは、それぞれの色の数を指定するのに[C言語ライクなenum][C-like enum]を使っています。`repr(u8)`属性のため、それぞれのenumのヴァリアントは`u8`として格納されています。実際には4ビットでも十分なのですが、Rustには`u4`型はありませんので。
|
||||
|
||||
[C-like enum]: https://doc.rust-jp.rs/rust-by-example-ja/custom_types/enum/c_like.html
|
||||
|
||||
通常、コンパイラは使われていないヴァリアントそれぞれに対して警告を発します。`#[allow(dead_code)]`属性を使うことで`Color` enumに対するそれらの警告を消すことができます。
|
||||
|
||||
[`Copy`]、[`Clone`]、[`Debug`]、[`PartialEq`]、および [`Eq`]を[derive][deriving]することによって、この型の[コピーセマンティクス][copy semantics]を有効化し、この型を出力することと比較することを可能にします。
|
||||
|
||||
[deriving]: https://doc.rust-jp.rs/rust-by-example-ja/trait/derive.html
|
||||
[`Copy`]: https://doc.rust-lang.org/nightly/core/marker/trait.Copy.html
|
||||
[`Clone`]: https://doc.rust-lang.org/nightly/core/clone/trait.Clone.html
|
||||
[`Debug`]: https://doc.rust-lang.org/nightly/core/fmt/trait.Debug.html
|
||||
[`PartialEq`]: https://doc.rust-lang.org/nightly/core/cmp/trait.PartialEq.html
|
||||
[`Eq`]: https://doc.rust-lang.org/nightly/core/cmp/trait.Eq.html
|
||||
[copy semantics]: https://doc.rust-jp.rs/book-ja/appendix-03-derivable-traits.html#値を複製するcloneとcopy
|
||||
|
||||
|
||||
前景と背景の色を指定する完全なカラーコードを表現するために、`u8`の上に[ニュータイプ][newtype]を作ります。
|
||||
|
||||
[newtype]: https://doc.rust-jp.rs/rust-by-example-ja/generics/new_types.html
|
||||
|
||||
```rust
|
||||
// in src/vga_buffer.rs
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
#[repr(transparent)]
|
||||
struct ColorCode(u8);
|
||||
|
||||
impl ColorCode {
|
||||
fn new(foreground: Color, background: Color) -> ColorCode {
|
||||
ColorCode((background as u8) << 4 | (foreground as u8))
|
||||
}
|
||||
}
|
||||
```
|
||||
`ColorCode`構造体は前景色と背景色を持つので、完全なカラーコードを持ちます。前と同じように、`Copy`と`Debug`トレイトをこれにderiveします。`ColorCode`が`u8`と全く同じデータ構造を持つようにするために、[`repr(transparent)`]属性(訳注:翻訳当時、リンク先未訳)を使います。
|
||||
|
||||
[`repr(transparent)`]: https://doc.rust-lang.org/nomicon/other-reprs.html#reprtransparent
|
||||
|
||||
### テキストバッファ
|
||||
次に、画面上の文字とテキストバッファをそれぞれ表す構造体を追加していきます。
|
||||
|
||||
```rust
|
||||
// in src/vga_buffer.rs
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
#[repr(C)]
|
||||
struct ScreenChar {
|
||||
ascii_character: u8,
|
||||
color_code: ColorCode,
|
||||
}
|
||||
|
||||
const BUFFER_HEIGHT: usize = 25;
|
||||
const BUFFER_WIDTH: usize = 80;
|
||||
|
||||
#[repr(transparent)]
|
||||
struct Buffer {
|
||||
chars: [[ScreenChar; BUFFER_WIDTH]; BUFFER_HEIGHT],
|
||||
}
|
||||
```
|
||||
Rustにおいて、デフォルトの構造体におけるフィールドの並べ方は未定義なので、[`repr(C)`]属性が必要になります。これは、構造体のフィールドがCの構造体と全く同じように並べられることを保証してくれるので、フィールドの並べ方が正しいと保証してくれるのです。`Buffer`構造体については、[`repr(transparent)`]をもう一度使うことで、その唯一のフィールドと同じメモリレイアウトを持つようにしています。
|
||||
|
||||
[`repr(C)`]: https://doc.rust-jp.rs/rust-nomicon-ja/other-reprs.html#reprc
|
||||
|
||||
実際に画面に書き出すため、writer型を作ります。
|
||||
|
||||
```rust
|
||||
// in src/vga_buffer.rs
|
||||
|
||||
pub struct Writer {
|
||||
column_position: usize,
|
||||
color_code: ColorCode,
|
||||
buffer: &'static mut Buffer,
|
||||
}
|
||||
```
|
||||
writerは常に最後の行に書き、行が一杯になったとき(もしくは`\n`を受け取った時)は1行上に送ります。`column_position`フィールドは、最後の行における現在の位置を持ちます。現在の前景および背景色は`color_code`によって指定されており、VGAバッファへの参照は`buffer`に格納されています。ここで、コンパイラにどのくらいの間参照が有効であるのかを教えるために[明示的なライフタイム][explicit lifetime]が必要になることに注意してください。[`'static`]ライフタイムは、その参照がプログラムの実行中ずっと有効であることを指定しています(これはVGAバッファについて正しいです)。
|
||||
|
||||
[explicit lifetime]: https://doc.rust-jp.rs/book-ja/ch10-03-lifetime-syntax.html#ライフタイム注釈記法
|
||||
[`'static`]: https://doc.rust-jp.rs/book-ja/ch10-03-lifetime-syntax.html#静的ライフタイム
|
||||
|
||||
### 出力する
|
||||
では`Writer`を使ってバッファの文字を変更しましょう。まず一つのASCII文字を書くメソッドを作ります:
|
||||
|
||||
```rust
|
||||
// in src/vga_buffer.rs
|
||||
|
||||
impl Writer {
|
||||
pub fn write_byte(&mut self, byte: u8) {
|
||||
match byte {
|
||||
b'\n' => self.new_line(),
|
||||
byte => {
|
||||
if self.column_position >= BUFFER_WIDTH {
|
||||
self.new_line();
|
||||
}
|
||||
|
||||
let row = BUFFER_HEIGHT - 1;
|
||||
let col = self.column_position;
|
||||
|
||||
let color_code = self.color_code;
|
||||
self.buffer.chars[row][col] = ScreenChar {
|
||||
ascii_character: byte,
|
||||
color_code,
|
||||
};
|
||||
self.column_position += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn new_line(&mut self) {/* TODO */}
|
||||
}
|
||||
```
|
||||
(引数の)バイトが[改行コード][newline]のバイトすなわち`\n`の場合は、writerは何も出力しません。代わりに、あとで実装する`new_line`メソッドを呼びます。他のバイトは、2つ目のマッチケースにおいて画面に出力されます。
|
||||
|
||||
[newline]: https://ja.wikipedia.org/wiki/%E6%94%B9%E8%A1%8C%E3%82%B3%E3%83%BC%E3%83%89
|
||||
|
||||
バイトを出力する時、writerは現在の行がいっぱいかをチェックします。その場合、行を折り返すために先に`new_line`の呼び出しが必要です。その後で現在の場所のバッファに新しい`ScreenChar`を書き込みます。最後に、現在の<ruby>列の位置<rp> (</rp><rt>column position</rt><rp>) </rp></ruby>を進めます。
|
||||
|
||||
文字列全体を出力するには、バイト列に変換しひとつひとつ出力すればよいです:
|
||||
|
||||
```rust
|
||||
// in src/vga_buffer.rs
|
||||
|
||||
impl Writer {
|
||||
pub fn write_string(&mut self, s: &str) {
|
||||
for byte in s.bytes() {
|
||||
match byte {
|
||||
// 出力可能なASCIIバイトか、改行コード
|
||||
0x20..=0x7e | b'\n' => self.write_byte(byte),
|
||||
// 出力可能なASCIIバイトではない
|
||||
_ => self.write_byte(0xfe),
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
VGAテキストバッファはASCIIおよび[コードページ437][code page 437]にある追加のバイトのみをサポートしています。Rustの文字列はデフォルトでは[UTF-8]なのでVGAテキストバッファにはサポートされていないバイトを含んでいる可能性があります。matchを使って出力可能なASCIIバイト(改行コードか、空白文字から`~`文字の間のすべての文字)と出力不可能なバイトを分けています。出力不可能なバイトについては、文字`■`を出力します(これはVGAハードウェアにおいて16進コード`0xfe`を持っています)。
|
||||
|
||||
[code page 437]: https://ja.wikipedia.org/wiki/コードページ437
|
||||
[UTF-8]: https://www.fileformat.info/info/unicode/utf8.htm
|
||||
|
||||
#### やってみよう!
|
||||
適当な文字を画面に書き出すために、一時的に使う関数を作ってみましょう。
|
||||
|
||||
```rust
|
||||
// in src/vga_buffer.rs
|
||||
|
||||
pub fn print_something() {
|
||||
let mut writer = Writer {
|
||||
column_position: 0,
|
||||
color_code: ColorCode::new(Color::Yellow, Color::Black),
|
||||
buffer: unsafe { &mut *(0xb8000 as *mut Buffer) },
|
||||
};
|
||||
|
||||
writer.write_byte(b'H');
|
||||
writer.write_string("ello ");
|
||||
writer.write_string("Wörld!");
|
||||
}
|
||||
```
|
||||
この関数はまず、VGAバッファの`0xb8000`を指す新しいwriterを作ります。このための構文はやや奇妙に思われるかもしれません:まず、整数`0xb8000`を可変な[生ポインタ][raw pointer]にキャストします。次にこれを(`*`を使って)参照外しすることで可変な参照に変え、即座にそれを(`&mut`を使って)再び借用します。コンパイラはこの生ポインタが有効であることを保証できないので、この変換には[`unsafe`ブロック][`unsafe` block]が必要となります。
|
||||
|
||||
[raw pointer]: https://doc.rust-jp.rs/book-ja/ch19-01-unsafe-rust.html#生ポインタを参照外しする
|
||||
[`unsafe` block]: https://doc.rust-jp.rs/book-ja/ch19-01-unsafe-rust.html
|
||||
|
||||
つぎに、この関数はそれにバイト`b'H'`を書きます。`b`というプレフィックスは、ASCII文字を表す[バイトリテラル][byte literal]を作ります。文字列`"ello "`と`"Wörld!"`を書くことで、私達の`write_string`関数と出力不可能な文字の処理をテストできます。出力を見るためには、`print_something`関数を`_start`関数から呼び出さなければなりません:
|
||||
|
||||
```rust
|
||||
// in src/main.rs
|
||||
|
||||
#[no_mangle]
|
||||
pub extern "C" fn _start() -> ! {
|
||||
vga_buffer::print_something();
|
||||
|
||||
loop {}
|
||||
}
|
||||
```
|
||||
|
||||
ここで、私達のプロジェクトを実行したら、`Hello W■■rld!`が画面の左 **下** に黄色で出力されるはずです。
|
||||
|
||||
[byte literal]: https://doc.rust-lang.org/reference/tokens.html#byte-literals
|
||||
|
||||

|
||||
|
||||
`ö`は2つの`■`という文字として出力されていることに注目してください。これは、`ö`は[UTF-8]において2つのバイトで表され、それらはどちらも出力可能なASCIIの範囲に収まっていないためです。実は、これはUTF-8の基本的な特性です:マルチバイト値のそれぞれのバイトは、絶対に有効なASCIIではないのです。
|
||||
|
||||
### Volatile
|
||||
メッセージが正しく出力されるのを確認できました。しかし、より強力に最適化をする将来のRustコンパイラでは、これはうまく行かないかもしれません。
|
||||
|
||||
問題なのは、私達は`Buffer`に書き込むけれども、それから読み取ることはないということです。コンパイラは私達が実際には(通常のRAMの代わりに)VGAバッファメモリにアクセスしていることを知らないので、文字が画面に出力されるという副作用も全く知りません。なので、それらの書き込みは不要で省略可能と判断するかもしれません。この誤った最適化を回避するためには、それらの書き込みを **[volatile]** であると指定する必要があります。これは、この書き込みには副作用があり、最適化により取り除かれるべきではないとコンパイラに命令します。
|
||||
|
||||
[volatile]: https://en.wikipedia.org/wiki/Volatile_(computer_programming)
|
||||
|
||||
VGAバッファへのvolatileな書き込みをするために、[volatile][volatile crate]ライブラリを使います。この **クレート**(Rustではパッケージのことをこう呼びます)は、`read`と`write`というメソッドを持つ`Volatile`というラッパー型を提供します。これらのメソッドは、内部的にcoreライブラリの[read_volatile]と[write_volatile]関数を使い、読み込み・書き込みが最適化により取り除かれないことを保証します。
|
||||
|
||||
[volatile crate]: https://docs.rs/volatile
|
||||
[read_volatile]: https://doc.rust-lang.org/nightly/core/ptr/fn.read_volatile.html
|
||||
[write_volatile]: https://doc.rust-lang.org/nightly/core/ptr/fn.write_volatile.html
|
||||
|
||||
`Cargo.toml`の`dependencies`セクションに`volatile`クレートを追加することで、このクレートへの依存関係を追加できます。
|
||||
|
||||
```toml
|
||||
# in Cargo.toml
|
||||
|
||||
[dependencies]
|
||||
volatile = "0.2.6"
|
||||
```
|
||||
|
||||
`0.2.6`は[セマンティック][semantic]バージョン番号です。詳しくは、cargoドキュメントの[依存関係の指定][Specifying Dependencies]を見てください。
|
||||
|
||||
[semantic]: https://semver.org/lang/ja/
|
||||
[Specifying Dependencies]: https://doc.crates.io/specifying-dependencies.html
|
||||
|
||||
これを使って、VGAバッファへの書き込みをvolatileにしてみましょう。`Buffer`型を以下のように変更します:
|
||||
|
||||
```rust
|
||||
// in src/vga_buffer.rs
|
||||
|
||||
use volatile::Volatile;
|
||||
|
||||
struct Buffer {
|
||||
chars: [[Volatile<ScreenChar>; BUFFER_WIDTH]; BUFFER_HEIGHT],
|
||||
}
|
||||
```
|
||||
`ScreenChar`の代わりに、`Volatile<ScreenChar>`を使っています(`Volatile`型は[ジェネリック][generic]であり(ほぼ)すべての型をラップできます)。これにより、間違って「普通の」書き込みをこれに対して行わないようにできます。これからは、代わりに`write`メソッドを使わなければいけません。
|
||||
|
||||
[generic]: https://doc.rust-lang.org/book/ch10-01-syntax.html
|
||||
|
||||
つまり、`Writer::write_byte`メソッドを更新しなければいけません:
|
||||
|
||||
```rust
|
||||
// in src/vga_buffer.rs
|
||||
|
||||
impl Writer {
|
||||
pub fn write_byte(&mut self, byte: u8) {
|
||||
match byte {
|
||||
b'\n' => self.new_line(),
|
||||
byte => {
|
||||
...
|
||||
|
||||
self.buffer.chars[row][col].write(ScreenChar {
|
||||
ascii_character: byte,
|
||||
color_code,
|
||||
});
|
||||
...
|
||||
}
|
||||
}
|
||||
}
|
||||
...
|
||||
}
|
||||
```
|
||||
|
||||
`=`を使った通常の代入の代わりに`write`メソッドを使っています。これにより、コンパイラがこの書き込みを最適化して取り除いてしまわないことが保証されます。
|
||||
|
||||
### フォーマットマクロ
|
||||
Rustの<ruby>フォーマットマクロ<rp> (</rp><rt>formatting macro</rt><rp>) </rp></ruby>もサポートすると良さそうです。そうすると、整数や浮動小数点数といった様々な型を簡単に出力できます。それらをサポートするためには、[`core::fmt::Write`]トレイトを実装する必要があります。このトレイトに必要なメソッドは`write_str`だけです。これは私達の`write_string`によく似ており、戻り値の型が`fmt::Result`であるだけです:
|
||||
|
||||
[`core::fmt::Write`]: https://doc.rust-lang.org/nightly/core/fmt/trait.Write.html
|
||||
|
||||
```rust
|
||||
// in src/vga_buffer.rs
|
||||
|
||||
use core::fmt;
|
||||
|
||||
impl fmt::Write for Writer {
|
||||
fn write_str(&mut self, s: &str) -> fmt::Result {
|
||||
self.write_string(s);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
```
|
||||
`Ok(())`は、`()`型を持つ`Ok`、というだけです。
|
||||
|
||||
Rustの組み込みの`write!`/`writeln!`フォーマットマクロが使えるようになりました。
|
||||
|
||||
```rust
|
||||
// in src/vga_buffer.rs
|
||||
|
||||
pub fn print_something() {
|
||||
use core::fmt::Write;
|
||||
let mut writer = Writer {
|
||||
column_position: 0,
|
||||
color_code: ColorCode::new(Color::Yellow, Color::Black),
|
||||
buffer: unsafe { &mut *(0xb8000 as *mut Buffer) },
|
||||
};
|
||||
|
||||
writer.write_byte(b'H');
|
||||
writer.write_string("ello! ");
|
||||
write!(writer, "The numbers are {} and {}", 42, 1.0/3.0).unwrap();
|
||||
}
|
||||
```
|
||||
|
||||
このようにすると、画面の下端に`Hello! The numbers are 42 and 0.3333333333333333`が見えるはずです。`write!`の呼び出しは`Result`を返し、これは放置されると警告を出すので、[`unwrap`]関数(エラーの際パニックします)をこれに呼び出しています。VGAバッファへの書き込みは絶対に失敗しないので、この場合これは問題ではありません。
|
||||
|
||||
[`unwrap`]: https://doc.rust-lang.org/core/result/enum.Result.html#method.unwrap
|
||||
|
||||
### 改行
|
||||
現在、改行や、行に収まらない文字は無視しています。その代わりに、すべての文字を一行上に持っていき(一番上の行は消去されます)、前の行の最初から始めるようにしたいです。これをするために、`Writer`の`new_line`というメソッドの実装を追加します。
|
||||
|
||||
```rust
|
||||
// in src/vga_buffer.rs
|
||||
|
||||
impl Writer {
|
||||
fn new_line(&mut self) {
|
||||
for row in 1..BUFFER_HEIGHT {
|
||||
for col in 0..BUFFER_WIDTH {
|
||||
let character = self.buffer.chars[row][col].read();
|
||||
self.buffer.chars[row - 1][col].write(character);
|
||||
}
|
||||
}
|
||||
self.clear_row(BUFFER_HEIGHT - 1);
|
||||
self.column_position = 0;
|
||||
}
|
||||
|
||||
fn clear_row(&mut self, row: usize) {/* TODO */}
|
||||
}
|
||||
```
|
||||
すべての画面の文字をイテレートし、それぞれの文字を一行上に動かします。範囲記法 (`..`) は上端を含まないことに注意してください。また、0行目はシフトしたら画面から除かれるので、この行についても省いています(最初の範囲は`1`から始まっています)。
|
||||
|
||||
newlineのプログラムを完成させるには、`clear_row`メソッドを追加すればよいです:
|
||||
|
||||
```rust
|
||||
// in src/vga_buffer.rs
|
||||
|
||||
impl Writer {
|
||||
fn clear_row(&mut self, row: usize) {
|
||||
let blank = ScreenChar {
|
||||
ascii_character: b' ',
|
||||
color_code: self.color_code,
|
||||
};
|
||||
for col in 0..BUFFER_WIDTH {
|
||||
self.buffer.chars[row][col].write(blank);
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
このメソッドはすべての文字を空白文字で書き換えることによって行をクリアしてくれます。
|
||||
|
||||
## <ruby>大域的<rp> (</rp><rt>global</rt><rp>) </rp></ruby>なインターフェース
|
||||
`Writer`のインスタンスを動かさずとも他のモジュールからインターフェースとして使える、大域的なwriterを提供するために、<ruby>静的<rp> (</rp><rt>static</rt><rp>) </rp></ruby>な`WRITER`を作りましょう:
|
||||
|
||||
```rust
|
||||
// in src/vga_buffer.rs
|
||||
|
||||
pub static WRITER: Writer = Writer {
|
||||
column_position: 0,
|
||||
color_code: ColorCode::new(Color::Yellow, Color::Black),
|
||||
buffer: unsafe { &mut *(0xb8000 as *mut Buffer) },
|
||||
};
|
||||
```
|
||||
|
||||
しかし、これをコンパイルしようとすると、次のエラーが起こります:
|
||||
|
||||
```
|
||||
error[E0015]: calls in statics are limited to constant functions, tuple structs and tuple variants
|
||||
(エラー[E0015]: static内における呼び出しは、定数関数、タプル構造体、タプルヴァリアントに限定されています)
|
||||
--> src/vga_buffer.rs:7:17
|
||||
|
|
||||
7 | color_code: ColorCode::new(Color::Yellow, Color::Black),
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
error[E0396]: raw pointers cannot be dereferenced in statics
|
||||
(エラー[E0396]: 生ポインタはstatic内では参照外しできません)
|
||||
--> src/vga_buffer.rs:8:22
|
||||
|
|
||||
8 | buffer: unsafe { &mut *(0xb8000 as *mut Buffer) },
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ dereference of raw pointer in constant
|
||||
| (定数内での生ポインタの参照外し)
|
||||
|
||||
error[E0017]: references in statics may only refer to immutable values
|
||||
(エラー[E0017]: static内における参照が参照してよいのは不変変数だけです)
|
||||
--> src/vga_buffer.rs:8:22
|
||||
|
|
||||
8 | buffer: unsafe { &mut *(0xb8000 as *mut Buffer) },
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ statics require immutable values
|
||||
| (staticは不変変数を必要とします)
|
||||
|
||||
error[E0017]: references in statics may only refer to immutable values
|
||||
(エラー[E0017]: static内における参照が参照してよいのは不変変数だけです)
|
||||
--> src/vga_buffer.rs:8:13
|
||||
|
|
||||
8 | buffer: unsafe { &mut *(0xb8000 as *mut Buffer) },
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ statics require immutable values
|
||||
| (staticは不変変数を必要とします)
|
||||
```
|
||||
|
||||
何が起こっているかを理解するには、実行時に初期化される通常の変数とは対照的に、静的変数はコンパイル時に初期化されるということを知らないといけません。この初期化表現を評価するRustコンパイラのコンポーネントを"[const evaluator]"といいます。この機能はまだ限定的ですが、「[定数内でpanicできるようにする][Allow panicking in constants]」RFCのように、この機能を拡張する作業が現在も進行しています。
|
||||
|
||||
[const evaluator]: https://rustc-dev-guide.rust-lang.org/const-eval.html
|
||||
[Allow panicking in constants]: https://github.com/rust-lang/rfcs/pull/2345
|
||||
|
||||
`ColorCode::new`に関する問題は[`const`関数][`const` functions]を使って解決できるかもしれませんが、ここでの根本的な問題は、Rustのconst evaluatorがコンパイル時に生ポインタを参照へと変えることができないということです。いつかうまく行くようになるのかもしれませんが、その時までは、別の方法を行わなければなりません。
|
||||
|
||||
[`const` functions]: https://doc.rust-lang.org/reference/const_eval.html#const-functions
|
||||
|
||||
### <ruby>怠けた<rp> (</rp><rt>Lazy</rt><rp>) </rp></ruby>静的変数
|
||||
定数でない関数で一度だけ静的変数を初期化したい、というのはRustにおいてよくある問題です。嬉しいことに、[lazy_static]というクレートにすでに良い解決方法が存在します。このクレートは、初期化が後回しにされる`static`を定義する`lazy_static!`マクロを提供します。その値をコンパイル時に計算する代わりに、この`static`は最初にアクセスされたときに初めて初期化します。したがって、初期化は実行時に起こるので、どんなに複雑な初期化プログラムも可能ということです。
|
||||
|
||||
<div class="note">
|
||||
|
||||
**訳注:** lazyは、普通「遅延(評価)」などと訳されます。「怠けているので、アクセスされるギリギリまで評価されない」という英語のイメージを伝えたかったので上のように訳してみました。
|
||||
|
||||
</div>
|
||||
|
||||
[lazy_static]: https://docs.rs/lazy_static/1.0.1/lazy_static/
|
||||
|
||||
私達のプロジェクトに`lazy_static`クレートを追加しましょう:
|
||||
|
||||
```toml
|
||||
# in Cargo.toml
|
||||
|
||||
[dependencies.lazy_static]
|
||||
version = "1.0"
|
||||
features = ["spin_no_std"]
|
||||
```
|
||||
|
||||
標準ライブラリをリンクしないので、`spin_no_std`機能が必要です。
|
||||
|
||||
`lazy_static`を使えば、静的な`WRITER`が問題なく定義できます:
|
||||
|
||||
```rust
|
||||
// in src/vga_buffer.rs
|
||||
|
||||
use lazy_static::lazy_static;
|
||||
|
||||
lazy_static! {
|
||||
pub static ref WRITER: Writer = Writer {
|
||||
column_position: 0,
|
||||
color_code: ColorCode::new(Color::Yellow, Color::Black),
|
||||
buffer: unsafe { &mut *(0xb8000 as *mut Buffer) },
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
しかし、この`WRITER`は<ruby>不変<rp> (</rp><rt>immutable</rt><rp>) </rp></ruby>なので、全く使い物になりません。なぜならこれは、この`WRITER`に何も書き込めないということを意味するからです(私達のすべての書き込みメソッドは`&mut self`を取るからです)。ひとつの解決策には、[<ruby>可変<rp> (</rp><rt>mutable</rt><rp>) </rp></ruby>で静的な変数][mutable static]を使うということがあります。しかし、そうすると、あらゆる読み書きが容易にデータ競合やその他の良くないことを引き起こしてしまうので、それらがすべてunsafeになってしまいます。`static mut`を使うことも、[それを削除しようという提案][remove static mut]すらあることを考えると、できる限り避けたいです。しかし他に方法はあるのでしょうか?不変静的変数を[RefCell]や、果ては[UnsafeCell]のような、[<ruby>内部可変性<rp> (</rp><rt>interior mutability</rt><rp>) </rp></ruby>][interior mutability]を提供するcell型と一緒に使うという事も考えられます。しかし、それらの型は(ちゃんとした理由があって)[Sync]ではないので、静的変数で使うことはできません。
|
||||
|
||||
[mutable static]: https://doc.rust-jp.rs/book-ja/ch19-01-unsafe-rust.html#可変で静的な変数にアクセスしたり変更する
|
||||
[remove static mut]: https://internals.rust-lang.org/t/pre-rfc-remove-static-mut/1437
|
||||
[RefCell]: https://doc.rust-jp.rs/book-ja/ch15-05-interior-mutability.html#refcelltで実行時に借用を追いかける
|
||||
[UnsafeCell]: https://doc.rust-lang.org/nightly/core/cell/struct.UnsafeCell.html
|
||||
[interior mutability]: https://doc.rust-jp.rs/book-ja/ch15-05-interior-mutability.html
|
||||
[Sync]: https://doc.rust-lang.org/nightly/core/marker/trait.Sync.html
|
||||
|
||||
### スピンロック
|
||||
同期された内部可変性を得るためには、標準ライブラリを使えるなら[Mutex]を使うことができます。これは、リソースがすでにロックされていた場合、スレッドをブロックすることにより相互排他性を提供します。しかし、私達の初歩的なカーネルにはブロックの機能はもちろんのこと、スレッドの概念すらないので、これも使うことはできません。しかし、コンピュータサイエンスの世界には、OSを必要としない非常に単純なmutexが存在するのです:それが[<ruby>スピンロック<rp> (</rp><rt>spinlock</rt><rp>) </rp></ruby>][spinlock]です。スピンロックを使うと、ブロックする代わりに、スレッドは単純にリソースを何度も何度もロックしようとすることで、mutexが開放されるまでの間CPU時間を使い尽くします。
|
||||
|
||||
[Mutex]: https://doc.rust-lang.org/nightly/std/sync/struct.Mutex.html
|
||||
[spinlock]: https://ja.wikipedia.org/wiki/スピンロック
|
||||
|
||||
スピンロックによるmutexを使うには、[spinクレート][spin crate]への依存を追加すればよいです:
|
||||
|
||||
[spin crate]: https://crates.io/crates/spin
|
||||
|
||||
```toml
|
||||
# in Cargo.toml
|
||||
[dependencies]
|
||||
spin = "0.5.2"
|
||||
```
|
||||
|
||||
すると、スピンを使ったMutexを使うことができ、静的な`WRITER`に安全な[内部可変性][interior mutability]を追加できます。
|
||||
|
||||
```rust
|
||||
// in src/vga_buffer.rs
|
||||
|
||||
use spin::Mutex;
|
||||
...
|
||||
lazy_static! {
|
||||
pub static ref WRITER: Mutex<Writer> = Mutex::new(Writer {
|
||||
column_position: 0,
|
||||
color_code: ColorCode::new(Color::Yellow, Color::Black),
|
||||
buffer: unsafe { &mut *(0xb8000 as *mut Buffer) },
|
||||
});
|
||||
}
|
||||
```
|
||||
`print_something`関数を消して、`_start`関数から直接出力しましょう:
|
||||
|
||||
```rust
|
||||
// in src/main.rs
|
||||
#[no_mangle]
|
||||
pub extern "C" fn _start() -> ! {
|
||||
use core::fmt::Write;
|
||||
vga_buffer::WRITER.lock().write_str("Hello again").unwrap();
|
||||
write!(vga_buffer::WRITER.lock(), ", some numbers: {} {}", 42, 1.337).unwrap();
|
||||
|
||||
loop {}
|
||||
}
|
||||
```
|
||||
`fmt::Write`トレイトの関数を使うためには、このトレイトをインポートする必要があります。
|
||||
|
||||
### 安全性
|
||||
コードにはunsafeブロックが一つ(`0xb8000`を指す参照`Buffer`を作るために必要なもの)しかないことに注目してください。その後は、すべての命令が<ruby>安全<rp> (</rp><rt>safe</rt><rp>) </rp></ruby>です。Rustは配列アクセスにはデフォルトで境界チェックを行うので、間違ってバッファの外に書き込んでしまうことはありえません。よって、必要とされる条件を型システムにすべて組み込んだので、安全なインターフェースを外部に提供できます。
|
||||
|
||||
### printlnマクロ
|
||||
大域的なwriterを手に入れたので、プログラムのどこでも使える`println`マクロを追加できます。Rustの[マクロの構文][macro syntax]はすこしややこしいので、一からマクロを書くことはしません。代わりに、標準ライブラリで[`println!`マクロ][`println!` macro]のソースを見てみます:
|
||||
|
||||
[macro syntax]: https://doc.rust-lang.org/nightly/book/ch19-06-macros.html#declarative-macros-with-macro_rules-for-general-metaprogramming
|
||||
[`println!` macro]: https://doc.rust-lang.org/nightly/std/macro.println!.html
|
||||
|
||||
```rust
|
||||
#[macro_export]
|
||||
macro_rules! println {
|
||||
() => (print!("\n"));
|
||||
($($arg:tt)*) => (print!("{}\n", format_args!($($arg)*)));
|
||||
}
|
||||
```
|
||||
|
||||
マクロは1つ以上のルールを使って定義されます(`match`アームと似ていますね)。`println`には2つのルールがあります:1つ目は引数なし呼び出し(例えば `println!()`)のためのもので、これは`print!("\n")`に展開され、よってただ改行を出力するだけになります。2つ目のルールはパラメータ付きの呼び出し(例えば`println!("Hello")`や `println!("Number: {}", 4)`)のためのものです。これも`print!`マクロの呼び出しへと展開され、すべての引数に加え、改行`\n`を最後に追加して渡します。
|
||||
|
||||
`#[macro_export]`属性はマクロを(その定義されたモジュールだけではなく)クレート全体および外部クレートで使えるようにします。また、これはマクロをクレートルートに置くため、`std::macros::println`の代わりに`use std::println`を使ってマクロをインポートしないといけないということを意味します。
|
||||
|
||||
[`print!`マクロ][`print!` macro]は以下のように定義されています:
|
||||
|
||||
[`print!` macro]: https://doc.rust-lang.org/nightly/std/macro.print!.html
|
||||
|
||||
```rust
|
||||
#[macro_export]
|
||||
macro_rules! print {
|
||||
($($arg:tt)*) => ($crate::io::_print(format_args!($($arg)*)));
|
||||
}
|
||||
```
|
||||
|
||||
このマクロは`io`モジュール内の[`_print`関数][`_print` function]の呼び出しへと展開しています。[`$crate`という変数][`$crate` variable]は、他のクレートで使われた際、`std`へと展開することによって、マクロが`std`クレートの外側で使われたとしてもうまく動くようにしてくれます。
|
||||
|
||||
[`format_args`マクロ][`format_args` macro]が与えられた引数から[fmt::Arguments]型を作り、これが`_print`へと渡されています。libstdの[`_print`関数]は`print_to`を呼び出すのですが、これは様々な`Stdout`デバイスをサポートいているためかなり煩雑です。ここではただVGAバッファに出力したいだけなので、そのような煩雑な実装は必要ありません。
|
||||
|
||||
[`_print` function]: https://github.com/rust-lang/rust/blob/29f5c699b11a6a148f097f82eaa05202f8799bbc/src/libstd/io/stdio.rs#L698
|
||||
[`$crate` variable]: https://doc.rust-lang.org/1.30.0/book/first-edition/macros.html#the-variable-crate
|
||||
[`format_args` macro]: https://doc.rust-lang.org/nightly/std/macro.format_args.html
|
||||
[fmt::Arguments]: https://doc.rust-lang.org/nightly/core/fmt/struct.Arguments.html
|
||||
|
||||
VGAバッファに出力するには、`println!`マクロと`print!`マクロをコピーし、独自の`_print`関数を使うように修正してやればいいです:
|
||||
|
||||
```rust
|
||||
// in src/vga_buffer.rs
|
||||
|
||||
#[macro_export]
|
||||
macro_rules! print {
|
||||
($($arg:tt)*) => ($crate::vga_buffer::_print(format_args!($($arg)*)));
|
||||
}
|
||||
|
||||
#[macro_export]
|
||||
macro_rules! println {
|
||||
() => ($crate::print!("\n"));
|
||||
($($arg:tt)*) => ($crate::print!("{}\n", format_args!($($arg)*)));
|
||||
}
|
||||
|
||||
#[doc(hidden)]
|
||||
pub fn _print(args: fmt::Arguments) {
|
||||
use core::fmt::Write;
|
||||
WRITER.lock().write_fmt(args).unwrap();
|
||||
}
|
||||
```
|
||||
|
||||
元の`println`の定義と異なり、`print!`マクロの呼び出しにも`$crate`をつけるようにしています。これにより、`println`だけを使いたいと思ったら`print!`マクロもインポートしなくていいようになります。
|
||||
|
||||
標準ライブラリのように、`#[macro_export]`属性を両方のマクロに与え、クレートのどこでも使えるようにします。このようにすると、マクロはクレートの名前空間のルートに置かれるので、`use crate::vga_buffer::println`としてインポートするとうまく行かないことに注意してください。代わりに、 `use crate::println`としなければいけません。
|
||||
|
||||
`_print`関数は静的な`WRITER`をロックし、その`write_fmt`メソッドを呼び出します。このメソッドは`Write`トレイトのものなので、このトレイトもインポートしないといけません。最後に追加した`unwrap()`は、画面出力がうまく行かなかったときパニックします。しかし、`write_str`は常に`Ok`を返すようにしているので、これは起きないはずです。
|
||||
|
||||
マクロは`_print`をモジュールの外側から呼び出せる必要があるので、この関数は<ruby>公開<rp> (</rp><rt>public</rt><rp>) </rp></ruby>されていなければなりません。しかし、これは<ruby>非公開<rp> (</rp><rt>private</rt><rp>) </rp></ruby>の実装の詳細であると考え、[`doc(hidden)`属性][`doc(hidden)` attribute]をつけることで、生成されたドキュメントから隠すようにします。
|
||||
|
||||
[`doc(hidden)` attribute]: https://doc.rust-lang.org/nightly/rustdoc/the-doc-attribute.html#dochidden
|
||||
|
||||
### `println`を使ってHello World
|
||||
こうすることで、`_start`関数で`println`を使えるようになります:
|
||||
|
||||
```rust
|
||||
// in src/main.rs
|
||||
|
||||
#[no_mangle]
|
||||
pub extern "C" fn _start() {
|
||||
println!("Hello World{}", "!");
|
||||
|
||||
loop {}
|
||||
}
|
||||
```
|
||||
|
||||
マクロはすでに名前空間のルートにいるので、main関数内でマクロをインポートしなくても良いということに注意してください。
|
||||
|
||||
期待通り、画面に Hello World! と出ています:
|
||||
|
||||

|
||||
|
||||
### パニックメッセージを出力する
|
||||
|
||||
`println`マクロを手に入れたので、これを私達のパニック関数で使って、パニックメッセージとパニックの場所を出力させることができます:
|
||||
|
||||
```rust
|
||||
// in main.rs
|
||||
|
||||
/// この関数はパニック時に呼ばれる。
|
||||
#[panic_handler]
|
||||
fn panic(info: &PanicInfo) -> ! {
|
||||
println!("{}", info);
|
||||
loop {}
|
||||
}
|
||||
```
|
||||
`panic!("Some panic message");`という文を`_start`関数に書くと、次の出力を得ます:
|
||||
|
||||

|
||||
|
||||
つまり、パニックが起こったということだけでなく、パニックメッセージとそれがコードのどこで起こったかまで知ることができます。
|
||||
|
||||
## まとめ
|
||||
この記事では、VGAテキストバッファの構造と、どのようにすれば`0xb8000`番地におけるメモリマッピングを通じてそれに書き込みを行えるかを学びました。このメモリマップされたバッファへの書き込みというunsafeな操作をカプセル化し、安全で便利なインターフェースを外部に提供するRustモジュールを作りました。
|
||||
|
||||
また、cargoのおかげでサードパーティのライブラリへの依存関係を簡単に追加できることも分かりました。`lazy_static`と`spin`という2つの依存先は、OS開発においてとても便利であり、今後の記事においても使っていきます。
|
||||
|
||||
## 次は?
|
||||
次の記事ではRustに組み込まれている単体テストフレームワークをセットアップする方法を説明します。その後、この記事のVGAバッファモジュールに対する基本的な単体テストを作ります。
|
||||
@@ -1,647 +0,0 @@
|
||||
+++
|
||||
title = "VGA 字符模式"
|
||||
weight = 3
|
||||
path = "zh-CN/vga-text-mode"
|
||||
date = 2018-02-26
|
||||
|
||||
[extra]
|
||||
# Please update this when updating the translation
|
||||
translation_based_on_commit = "bd6fbcb1c36705b2c474d7fcee387bfea1210851"
|
||||
# GitHub usernames of the people that translated this post
|
||||
translators = ["luojia65", "Rustin-Liu"]
|
||||
+++
|
||||
|
||||
**VGA 字符模式**([VGA text mode])是打印字符到屏幕的一种简单方式。在这篇文章中,为了包装这个模式为一个安全而简单的接口,我们将包装 unsafe 代码到独立的模块。我们还将实现对 Rust 语言**格式化宏**([formatting macros])的支持。
|
||||
|
||||
[VGA text mode]: https://en.wikipedia.org/wiki/VGA-compatible_text_mode
|
||||
[formatting macros]: https://doc.rust-lang.org/std/fmt/#related-macros
|
||||
|
||||
<!-- more -->
|
||||
|
||||
This blog is openly developed on [GitHub]. If you have any problems or questions, please open an issue there. You can also leave comments [at the bottom]. The complete source code for this post can be found in the [`post-03`][post branch] branch.
|
||||
|
||||
[GitHub]: https://github.com/phil-opp/blog_os
|
||||
[at the bottom]: #comments
|
||||
<!-- fix for zola anchor checker (target is in template): <a id="comments"> -->
|
||||
[post branch]: https://github.com/phil-opp/blog_os/tree/post-03
|
||||
|
||||
<!-- toc -->
|
||||
|
||||
## VGA 字符缓冲区
|
||||
|
||||
为了在 VGA 字符模式中向屏幕打印字符,我们必须将它写入硬件提供的 **VGA 字符缓冲区**(VGA text buffer)。通常状况下,VGA 字符缓冲区是一个 25 行、80 列的二维数组,它的内容将被实时渲染到屏幕。这个数组的元素被称作**字符单元**(character cell),它使用下面的格式描述一个屏幕上的字符:
|
||||
|
||||
| Bit(s) | Value |
|
||||
|-----|----------------|
|
||||
| 0-7 | ASCII code point |
|
||||
| 8-11 | Foreground color |
|
||||
| 12-14 | Background color |
|
||||
| 15 | Blink |
|
||||
|
||||
其中,**前景色**(foreground color)和**背景色**(background color)取值范围如下:
|
||||
|
||||
| Number | Color | Number + Bright Bit | Bright Color |
|
||||
|-----|----------|------|--------|
|
||||
| 0x0 | Black | 0x8 | Dark Gray |
|
||||
| 0x1 | Blue | 0x9 | Light Blue |
|
||||
| 0x2 | Green | 0xa | Light Green |
|
||||
| 0x3 | Cyan | 0xb | Light Cyan |
|
||||
| 0x4 | Red | 0xc | Light Red |
|
||||
| 0x5 | Magenta | 0xd | Pink |
|
||||
| 0x6 | Brown | 0xe | Yellow |
|
||||
| 0x7 | Light Gray | 0xf | White |
|
||||
|
||||
每个颜色的第四位称为**加亮位**(bright bit)。
|
||||
|
||||
要修改 VGA 字符缓冲区,我们可以通过**存储器映射输入输出**([memory-mapped I/O](https://en.wikipedia.org/wiki/Memory-mapped_I/O))的方式,读取或写入地址 `0xb8000`;这意味着,我们可以像操作普通的内存区域一样操作这个地址。
|
||||
|
||||
需要注意的是,一些硬件虽然映射到存储器,但可能不会完全支持所有的内存操作:可能会有一些设备支持按 `u8` 字节读取,但在读取 `u64` 时返回无效的数据。幸运的是,字符缓冲区都[支持标准的读写操作](https://web.stanford.edu/class/cs140/projects/pintos/specs/freevga/vga/vgamem.htm#manip),所以我们不需要用特殊的标准对待它。
|
||||
|
||||
## 包装到 Rust 模块
|
||||
|
||||
既然我们已经知道 VGA 文字缓冲区如何工作,也是时候创建一个 Rust 模块来处理文字打印了。我们输入这样的代码:
|
||||
|
||||
```rust
|
||||
// in src/main.rs
|
||||
mod vga_buffer;
|
||||
```
|
||||
|
||||
这行代码定义了一个 Rust 模块,它的内容应当保存在 `src/vga_buffer.rs` 文件中。使用 **2018 版次**(2018 edition)的 Rust 时,我们可以把模块的**子模块**(submodule)文件直接保存到 `src/vga_buffer/` 文件夹下,与 `vga_buffer.rs` 文件共存,而无需创建一个 `mod.rs` 文件。
|
||||
|
||||
我们的模块暂时不需要添加子模块,所以我们将它创建为 `src/vga_buffer.rs` 文件。除非另有说明,本文中的代码都保存到这个文件中。
|
||||
|
||||
### 颜色
|
||||
|
||||
首先,我们使用 Rust 的**枚举**(enum)表示一种颜色:
|
||||
|
||||
```rust
|
||||
// in src/vga_buffer.rs
|
||||
|
||||
#[allow(dead_code)]
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
#[repr(u8)]
|
||||
pub enum Color {
|
||||
Black = 0,
|
||||
Blue = 1,
|
||||
Green = 2,
|
||||
Cyan = 3,
|
||||
Red = 4,
|
||||
Magenta = 5,
|
||||
Brown = 6,
|
||||
LightGray = 7,
|
||||
DarkGray = 8,
|
||||
LightBlue = 9,
|
||||
LightGreen = 10,
|
||||
LightCyan = 11,
|
||||
LightRed = 12,
|
||||
Pink = 13,
|
||||
Yellow = 14,
|
||||
White = 15,
|
||||
}
|
||||
```
|
||||
|
||||
我们使用**类似于 C 语言的枚举**(C-like enum),为每个颜色明确指定一个数字。在这里,每个用 `repr(u8)` 注记标注的枚举类型,都会以一个 `u8` 的形式存储——事实上 4 个二进制位就足够了,但 Rust 语言并不提供 `u4` 类型。
|
||||
|
||||
通常来说,编译器会对每个未使用的变量发出**警告**(warning);使用 `#[allow(dead_code)]`,我们可以对 `Color` 枚举类型禁用这个警告。
|
||||
|
||||
我们还**生成**([derive])了 [`Copy`](https://doc.rust-lang.org/nightly/core/marker/trait.Copy.html)、[`Clone`](https://doc.rust-lang.org/nightly/core/clone/trait.Clone.html)、[`Debug`](https://doc.rust-lang.org/nightly/core/fmt/trait.Debug.html)、[`PartialEq`](https://doc.rust-lang.org/nightly/core/cmp/trait.PartialEq.html) 和 [`Eq`](https://doc.rust-lang.org/nightly/core/cmp/trait.Eq.html) 这几个 trait:这让我们的类型遵循**复制语义**([copy semantics]),也让它可以被比较、被调试和打印。
|
||||
|
||||
[derive]: https://doc.rust-lang.org/rust-by-example/trait/derive.html
|
||||
[copy semantics]: https://doc.rust-lang.org/1.30.0/book/first-edition/ownership.html#copy-types
|
||||
|
||||
为了描述包含前景色和背景色的、完整的**颜色代码**(color code),我们基于 `u8` 创建一个新类型:
|
||||
|
||||
```rust
|
||||
// in src/vga_buffer.rs
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
#[repr(transparent)]
|
||||
struct ColorCode(u8);
|
||||
|
||||
impl ColorCode {
|
||||
fn new(foreground: Color, background: Color) -> ColorCode {
|
||||
ColorCode((background as u8) << 4 | (foreground as u8))
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
这里,`ColorCode` 类型包装了一个完整的颜色代码字节,它包含前景色和背景色信息。和 `Color` 类型类似,我们为它生成 `Copy` 和 `Debug` 等一系列 trait。为了确保 `ColorCode` 和 `u8` 有完全相同的内存布局,我们添加 [repr(transparent) 标记](https://doc.rust-lang.org/nomicon/other-reprs.html#reprtransparent)。
|
||||
|
||||
### 字符缓冲区
|
||||
|
||||
现在,我们可以添加更多的结构体,来描述屏幕上的字符和整个字符缓冲区:
|
||||
|
||||
```rust
|
||||
// in src/vga_buffer.rs
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
#[repr(C)]
|
||||
struct ScreenChar {
|
||||
ascii_character: u8,
|
||||
color_code: ColorCode,
|
||||
}
|
||||
|
||||
const BUFFER_HEIGHT: usize = 25;
|
||||
const BUFFER_WIDTH: usize = 80;
|
||||
|
||||
#[repr(transparent)]
|
||||
struct Buffer {
|
||||
chars: [[ScreenChar; BUFFER_WIDTH]; BUFFER_HEIGHT],
|
||||
}
|
||||
```
|
||||
|
||||
在内存布局层面,Rust 并不保证按顺序布局成员变量。因此,我们需要使用 `#[repr(C)]` 标记结构体;这将按 C 语言约定的顺序布局它的成员变量,让我们能正确地映射内存片段。对 `Buffer` 类型,我们再次使用 `repr(transparent)`,来确保类型和它的单个成员有相同的内存布局。
|
||||
|
||||
为了输出字符到屏幕,我们来创建一个 `Writer` 类型:
|
||||
|
||||
```rust
|
||||
// in src/vga_buffer.rs
|
||||
|
||||
pub struct Writer {
|
||||
column_position: usize,
|
||||
color_code: ColorCode,
|
||||
buffer: &'static mut Buffer,
|
||||
}
|
||||
```
|
||||
|
||||
我们将让这个 `Writer` 类型将字符写入屏幕的最后一行,并在一行写满或接收到换行符 `\n` 的时候,将所有的字符向上位移一行。`column_position` 变量将跟踪光标在最后一行的位置。当前字符的前景和背景色将由 `color_code` 变量指定;另外,我们存入一个 VGA 字符缓冲区的可变借用到`buffer`变量中。需要注意的是,这里我们对借用使用**显式生命周期**([explicit lifetime](https://doc.rust-lang.org/book/ch10-03-lifetime-syntax.html#lifetime-annotation-syntax)),告诉编译器这个借用在何时有效:我们使用** `'static` 生命周期 **(['static lifetime](https://doc.rust-lang.org/book/ch10-03-lifetime-syntax.html#the-static-lifetime)),意味着这个借用应该在整个程序的运行期间有效;这对一个全局有效的 VGA 字符缓冲区来说,是非常合理的。
|
||||
|
||||
### 打印字符
|
||||
|
||||
现在我们可以使用 `Writer` 类型来更改缓冲区内的字符了。首先,为了写入一个 ASCII 码字节,我们创建这样的函数:
|
||||
|
||||
```rust
|
||||
// in src/vga_buffer.rs
|
||||
|
||||
impl Writer {
|
||||
pub fn write_byte(&mut self, byte: u8) {
|
||||
match byte {
|
||||
b'\n' => self.new_line(),
|
||||
byte => {
|
||||
if self.column_position >= BUFFER_WIDTH {
|
||||
self.new_line();
|
||||
}
|
||||
|
||||
let row = BUFFER_HEIGHT - 1;
|
||||
let col = self.column_position;
|
||||
|
||||
let color_code = self.color_code;
|
||||
self.buffer.chars[row][col] = ScreenChar {
|
||||
ascii_character: byte,
|
||||
color_code,
|
||||
};
|
||||
self.column_position += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn new_line(&mut self) {/* TODO */}
|
||||
}
|
||||
```
|
||||
|
||||
如果这个字节是一个**换行符**([line feed](https://en.wikipedia.org/wiki/Newline))字节 `\n`,我们的 `Writer` 不应该打印新字符,相反,它将调用我们稍后会实现的 `new_line` 方法;其它的字节应该将在 `match` 语句的第二个分支中被打印到屏幕上。
|
||||
|
||||
当打印字节时,`Writer` 将检查当前行是否已满。如果已满,它将首先调用 `new_line` 方法来将这一行字向上提升,再将一个新的 `ScreenChar` 写入到缓冲区,最终将当前的光标位置前进一位。
|
||||
|
||||
要打印整个字符串,我们把它转换为字节并依次输出:
|
||||
|
||||
```rust
|
||||
// in src/vga_buffer.rs
|
||||
|
||||
impl Writer {
|
||||
pub fn write_string(&mut self, s: &str) {
|
||||
for byte in s.bytes() {
|
||||
match byte {
|
||||
// 可以是能打印的 ASCII 码字节,也可以是换行符
|
||||
0x20..=0x7e | b'\n' => self.write_byte(byte),
|
||||
// 不包含在上述范围之内的字节
|
||||
_ => self.write_byte(0xfe),
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
VGA 字符缓冲区只支持 ASCII 码字节和**代码页 437**([Code page 437](https://en.wikipedia.org/wiki/Code_page_437))定义的字节。Rust 语言的字符串默认编码为 [UTF-8](https://www.fileformat.info/info/unicode/utf8.htm),也因此可能包含一些 VGA 字符缓冲区不支持的字节:我们使用 `match` 语句,来区别可打印的 ASCII 码或换行字节,和其它不可打印的字节。对每个不可打印的字节,我们打印一个 `■` 符号;这个符号在 VGA 硬件中被编码为十六进制的 `0xfe`。
|
||||
|
||||
我们可以亲自试一试已经编写的代码。为了这样做,我们可以临时编写一个函数:
|
||||
|
||||
```rust
|
||||
// in src/vga_buffer.rs
|
||||
|
||||
pub fn print_something() {
|
||||
let mut writer = Writer {
|
||||
column_position: 0,
|
||||
color_code: ColorCode::new(Color::Yellow, Color::Black),
|
||||
buffer: unsafe { &mut *(0xb8000 as *mut Buffer) },
|
||||
};
|
||||
|
||||
writer.write_byte(b'H');
|
||||
writer.write_string("ello ");
|
||||
writer.write_string("Wörld!");
|
||||
}
|
||||
```
|
||||
|
||||
这个函数首先创建一个指向 `0xb8000` 地址VGA缓冲区的 `Writer`。实现这一点,我们需要编写的代码可能看起来有点奇怪:首先,我们把整数 `0xb8000` 强制转换为一个可变的**裸指针**([raw pointer](https://doc.rust-lang.org/book/ch19-01-unsafe-rust.html#dereferencing-a-raw-pointer));之后,通过运算符`*`,我们将这个裸指针解引用;最后,我们再通过 `&mut`,再次获得它的可变借用。这些转换需要 **`unsafe` 语句块**([unsafe block](https://doc.rust-lang.org/book/ch19-01-unsafe-rust.html)),因为编译器并不能保证这个裸指针是有效的。
|
||||
|
||||
然后它将字节 `b'H'` 写入缓冲区内. 前缀 `b` 创建了一个字节常量([byte literal](https://doc.rust-lang.org/reference/tokens.html#byte-literals)),表示单个 ASCII 码字符;通过尝试写入 `"ello "` 和 `"Wörld!"`,我们可以测试 `write_string` 方法和其后对无法打印字符的处理逻辑。为了观察输出,我们需要在 `_start` 函数中调用 `print_something` 方法:
|
||||
|
||||
```rust
|
||||
// in src/main.rs
|
||||
#[no_mangle]
|
||||
pub extern "C" fn _start() -> ! {
|
||||
vga_buffer::print_something();
|
||||
loop {}
|
||||
}
|
||||
```
|
||||
|
||||
编译运行后,黄色的 `Hello W■■rld!` 字符串将会被打印在屏幕的左下角:
|
||||
|
||||

|
||||
|
||||
需要注意的是,`ö` 字符被打印为两个 `■` 字符。这是因为在 [UTF-8](https://www.fileformat.info/info/unicode/utf8.htm) 编码下,字符 `ö` 是由两个字节表述的——而这两个字节并不处在可打印的 ASCII 码字节范围之内。事实上,这是 UTF-8 编码的基本特点之一:**如果一个字符占用多个字节,那么每个组成它的独立字节都不是有效的 ASCII 码字节**(the individual bytes of multi-byte values are never valid ASCII)。
|
||||
|
||||
### 易失操作
|
||||
|
||||
我们刚才看到,自己想要输出的信息被正确地打印到屏幕上。然而,未来 Rust 编译器更暴力的优化可能让这段代码不按预期工作。
|
||||
|
||||
产生问题的原因在于,我们只向 `Buffer` 写入,却不再从它读出数据。此时,编译器不知道我们事实上已经在操作 VGA 缓冲区内存,而不是在操作普通的 RAM——因此也不知道产生的**副效应**(side effect),即会有几个字符显示在屏幕上。这时,编译器也许会认为这些写入操作都没有必要,甚至会选择忽略这些操作!所以,为了避免这些并不正确的优化,这些写入操作应当被指定为[易失操作](https://en.wikipedia.org/wiki/Volatile_(computer_programming))。这将告诉编译器,这些写入可能会产生副效应,不应该被优化掉。
|
||||
|
||||
为了在我们的 VGA 缓冲区中使用易失的写入操作,我们使用 [volatile](https://docs.rs/volatile) 库。这个**包**(crate)提供一个名为 `Volatile` 的**包装类型**(wrapping type)和它的 `read`、`write` 方法;这些方法包装了 `core::ptr` 内的 [read_volatile](https://doc.rust-lang.org/nightly/core/ptr/fn.read_volatile.html) 和 [write_volatile](https://doc.rust-lang.org/nightly/core/ptr/fn.write_volatile.html) 函数,从而保证读操作或写操作不会被编译器优化。
|
||||
|
||||
要添加 `volatile` 包为项目的**依赖项**(dependency),我们可以在 `Cargo.toml` 文件的 `dependencies` 中添加下面的代码:
|
||||
|
||||
```toml
|
||||
# in Cargo.toml
|
||||
|
||||
[dependencies]
|
||||
volatile = "0.2.6"
|
||||
```
|
||||
|
||||
`0.2.6` 表示一个**语义版本号**([semantic version number](https://semver.org/)),在 cargo 文档的[《指定依赖项》章节](https://doc.rust-lang.org/cargo/reference/specifying-dependencies.html)可以找到与它相关的使用指南。
|
||||
|
||||
现在,我们使用它来完成 VGA 缓冲区的 volatile 写入操作。我们将 `Buffer` 类型的定义修改为下列代码:
|
||||
|
||||
```rust
|
||||
// in src/vga_buffer.rs
|
||||
|
||||
use volatile::Volatile;
|
||||
|
||||
struct Buffer {
|
||||
chars: [[Volatile<ScreenChar>; BUFFER_WIDTH]; BUFFER_HEIGHT],
|
||||
}
|
||||
```
|
||||
|
||||
在这里,我们不使用 `ScreenChar` ,而选择使用 `Volatile<ScreenChar>` ——在这里,`Volatile` 类型是一个**泛型**([generic](https://doc.rust-lang.org/book/ch10-01-syntax.html)),可以包装几乎所有的类型——这确保了我们不会通过普通的写入操作,意外地向它写入数据;我们转而使用提供的 `write` 方法。
|
||||
|
||||
这意味着,我们必须要修改我们的 `Writer::write_byte` 方法:
|
||||
|
||||
```rust
|
||||
// in src/vga_buffer.rs
|
||||
|
||||
impl Writer {
|
||||
pub fn write_byte(&mut self, byte: u8) {
|
||||
match byte {
|
||||
b'\n' => self.new_line(),
|
||||
byte => {
|
||||
...
|
||||
|
||||
self.buffer.chars[row][col].write(ScreenChar {
|
||||
ascii_character: byte,
|
||||
color_code: color_code,
|
||||
});
|
||||
...
|
||||
}
|
||||
}
|
||||
}
|
||||
...
|
||||
}
|
||||
```
|
||||
|
||||
正如代码所示,我们不再使用普通的 `=` 赋值,而使用了 `write` 方法:这能确保编译器不再优化这个写入操作。
|
||||
|
||||
### 格式化宏
|
||||
|
||||
支持 Rust 提供的**格式化宏**(formatting macros)也是一个很好的思路。通过这种途径,我们可以轻松地打印不同类型的变量,如整数或浮点数。为了支持它们,我们需要实现 [`core::fmt::Write`](https://doc.rust-lang.org/nightly/core/fmt/trait.Write.html) trait;要实现它,唯一需要提供的方法是 `write_str`,它和我们先前编写的 `write_string` 方法差别不大,只是返回值类型变成了 `fmt::Result`:
|
||||
|
||||
```rust
|
||||
// in src/vga_buffer.rs
|
||||
|
||||
use core::fmt;
|
||||
|
||||
impl fmt::Write for Writer {
|
||||
fn write_str(&mut self, s: &str) -> fmt::Result {
|
||||
self.write_string(s);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
这里,`Ok(())` 属于 `Result` 枚举类型中的 `Ok`,包含一个值为 `()` 的变量。
|
||||
|
||||
现在我们就可以使用 Rust 内置的格式化宏 `write!` 和 `writeln!` 了:
|
||||
|
||||
```rust
|
||||
// in src/vga_buffer.rs
|
||||
|
||||
pub fn print_something() {
|
||||
use core::fmt::Write;
|
||||
let mut writer = Writer {
|
||||
column_position: 0,
|
||||
color_code: ColorCode::new(Color::Yellow, Color::Black),
|
||||
buffer: unsafe { &mut *(0xb8000 as *mut Buffer) },
|
||||
};
|
||||
|
||||
writer.write_byte(b'H');
|
||||
writer.write_string("ello! ");
|
||||
write!(writer, "The numbers are {} and {}", 42, 1.0/3.0).unwrap();
|
||||
}
|
||||
```
|
||||
|
||||
现在,你应该在屏幕下端看到一串 `Hello! The numbers are 42 and 0.3333333333333333`。`write!` 宏返回的 `Result` 类型必须被使用,所以我们调用它的 [`unwrap`](https://doc.rust-lang.org/core/result/enum.Result.html#method.unwrap) 方法,它将在错误发生时 panic。这里的情况下应该不会发生这样的问题,因为写入 VGA 字符缓冲区并没有可能失败。
|
||||
|
||||
### 换行
|
||||
|
||||
在之前的代码中,我们忽略了换行符,因此没有处理超出一行字符的情况。当换行时,我们想要把每个字符向上移动一行——此时最顶上的一行将被删除——然后在最后一行的起始位置继续打印。要做到这一点,我们要为 `Writer` 实现一个新的 `new_line` 方法:
|
||||
|
||||
```rust
|
||||
// in src/vga_buffer.rs
|
||||
|
||||
impl Writer {
|
||||
fn new_line(&mut self) {
|
||||
for row in 1..BUFFER_HEIGHT {
|
||||
for col in 0..BUFFER_WIDTH {
|
||||
let character = self.buffer.chars[row][col].read();
|
||||
self.buffer.chars[row - 1][col].write(character);
|
||||
}
|
||||
}
|
||||
self.clear_row(BUFFER_HEIGHT - 1);
|
||||
self.column_position = 0;
|
||||
}
|
||||
|
||||
fn clear_row(&mut self, row: usize) {/* TODO */}
|
||||
}
|
||||
```
|
||||
|
||||
我们遍历每个屏幕上的字符,把每个字符移动到它上方一行的相应位置。这里,`..` 符号是**区间标号**(range notation)的一种;它表示左闭右开的区间,因此不包含它的上界。在外层的枚举中,我们从第 1 行开始,省略了对第 0 行的枚举过程——因为这一行应该被移出屏幕,即它将被下一行的字符覆写。
|
||||
|
||||
所以我们实现的 `clear_row` 方法代码如下:
|
||||
|
||||
```rust
|
||||
// in src/vga_buffer.rs
|
||||
|
||||
impl Writer {
|
||||
fn clear_row(&mut self, row: usize) {
|
||||
let blank = ScreenChar {
|
||||
ascii_character: b' ',
|
||||
color_code: self.color_code,
|
||||
};
|
||||
for col in 0..BUFFER_WIDTH {
|
||||
self.buffer.chars[row][col].write(blank);
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
通过向对应的缓冲区写入空格字符,这个方法能清空一整行的字符位置。
|
||||
|
||||
## 全局接口
|
||||
|
||||
编写其它模块时,我们希望无需随时拥有 `Writer` 实例,便能使用它的方法。我们尝试创建一个静态的 `WRITER` 变量:
|
||||
|
||||
```rust
|
||||
// in src/vga_buffer.rs
|
||||
|
||||
pub static WRITER: Writer = Writer {
|
||||
column_position: 0,
|
||||
color_code: ColorCode::new(Color::Yellow, Color::Black),
|
||||
buffer: unsafe { &mut *(0xb8000 as *mut Buffer) },
|
||||
};
|
||||
```
|
||||
|
||||
我们尝试编译这些代码,却发生了下面的编译错误:
|
||||
|
||||
```
|
||||
error[E0015]: calls in statics are limited to constant functions, tuple structs and tuple variants
|
||||
--> src/vga_buffer.rs:7:17
|
||||
|
|
||||
7 | color_code: ColorCode::new(Color::Yellow, Color::Black),
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
error[E0396]: raw pointers cannot be dereferenced in statics
|
||||
--> src/vga_buffer.rs:8:22
|
||||
|
|
||||
8 | buffer: unsafe { &mut *(0xb8000 as *mut Buffer) },
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ dereference of raw pointer in constant
|
||||
|
||||
error[E0017]: references in statics may only refer to immutable values
|
||||
--> src/vga_buffer.rs:8:22
|
||||
|
|
||||
8 | buffer: unsafe { &mut *(0xb8000 as *mut Buffer) },
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ statics require immutable values
|
||||
|
||||
error[E0017]: references in statics may only refer to immutable values
|
||||
--> src/vga_buffer.rs:8:13
|
||||
|
|
||||
8 | buffer: unsafe { &mut *(0xb8000 as *mut Buffer) },
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ statics require 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/reference/const_eval.html#const-functions))解决,但常量求值器还存在不完善之处,它还不能在编译时直接转换裸指针到变量的引用——也许未来这段代码能够工作,但在那之前,我们需要寻找另外的解决方案。
|
||||
|
||||
### 延迟初始化
|
||||
|
||||
使用非常函数初始化静态变量是 Rust 程序员普遍遇到的问题。幸运的是,有一个叫做 [lazy_static](https://docs.rs/lazy_static/1.0.1/lazy_static/) 的包提供了一个很棒的解决方案:它提供了名为 `lazy_static!` 的宏,定义了一个**延迟初始化**(lazily initialized)的静态变量;这个变量的值将在第一次使用时计算,而非在编译时计算。这时,变量的初始化过程将在运行时执行,任意的初始化代码——无论简单或复杂——都是能够使用的。
|
||||
|
||||
现在,我们将 `lazy_static` 包导入到我们的项目:
|
||||
|
||||
```toml
|
||||
# in Cargo.toml
|
||||
|
||||
[dependencies.lazy_static]
|
||||
version = "1.0"
|
||||
features = ["spin_no_std"]
|
||||
```
|
||||
|
||||
在这里,由于程序不连接标准库,我们需要启用 `spin_no_std` 特性。
|
||||
|
||||
使用 `lazy_static` 我们就可以定义一个不出问题的 `WRITER` 变量:
|
||||
|
||||
```rust
|
||||
// in src/vga_buffer.rs
|
||||
|
||||
use lazy_static::lazy_static;
|
||||
|
||||
lazy_static! {
|
||||
pub static ref WRITER: Writer = Writer {
|
||||
column_position: 0,
|
||||
color_code: ColorCode::new(Color::Yellow, Color::Black),
|
||||
buffer: unsafe { &mut *(0xb8000 as *mut Buffer) },
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
然而,这个 `WRITER` 可能没有什么用途,因为它目前还是**不可变变量**(immutable variable):这意味着我们无法向它写入数据,因为所有与写入数据相关的方法都需要实例的可变引用 `&mut self`。一种解决方案是使用**可变静态**([mutable static](https://doc.rust-lang.org/book/ch19-01-unsafe-rust.html#accessing-or-modifying-a-mutable-static-variable))的变量,但所有对它的读写操作都被规定为不安全的(unsafe)操作,因为这很容易导致数据竞争或发生其它不好的事情——使用 `static mut` 极其不被赞成,甚至有一些提案认为[应该将它删除](https://internals.rust-lang.org/t/pre-rfc-remove-static-mut/1437)。也有其它的替代方案,比如可以尝试使用比如 [RefCell](https://doc.rust-lang.org/book/ch15-05-interior-mutability.html#keeping-track-of-borrows-at-runtime-with-refcellt) 或甚至 [UnsafeCell](https://doc.rust-lang.org/nightly/core/cell/struct.UnsafeCell.html) 等类型提供的**内部可变性**([interior mutability](https://doc.rust-lang.org/book/ch15-05-interior-mutability.html));但这些类型都被设计为非同步类型,即不满足 [Sync](https://doc.rust-lang.org/nightly/core/marker/trait.Sync.html) 约束,所以我们不能在静态变量中使用它们。
|
||||
|
||||
### 自旋锁
|
||||
|
||||
要定义同步的内部可变性,我们往往使用标准库提供的互斥锁类 [Mutex](https://doc.rust-lang.org/nightly/std/sync/struct.Mutex.html),它通过提供当资源被占用时将线程**阻塞**(block)的**互斥条件**(mutual exclusion)实现这一点;但我们初步的内核代码还没有线程和阻塞的概念,我们将不能使用这个类。不过,我们还有一种较为基础的互斥锁实现方式——**自旋锁**([spinlock](https://en.wikipedia.org/wiki/Spinlock))。自旋锁并不会调用阻塞逻辑,而是在一个小的无限循环中反复尝试获得这个锁,也因此会一直占用 CPU 时间,直到互斥锁被它的占用者释放。
|
||||
|
||||
为了使用自旋的互斥锁,我们添加 [spin包](https://crates.io/crates/spin) 到项目的依赖项列表:
|
||||
|
||||
```toml
|
||||
# in Cargo.toml
|
||||
[dependencies]
|
||||
spin = "0.4.9"
|
||||
```
|
||||
|
||||
现在,我们能够使用自旋的互斥锁,为我们的 `WRITER` 类实现安全的[内部可变性](https://doc.rust-lang.org/book/ch15-05-interior-mutability.html):
|
||||
|
||||
```rust
|
||||
// in src/vga_buffer.rs
|
||||
|
||||
use spin::Mutex;
|
||||
...
|
||||
lazy_static! {
|
||||
pub static ref WRITER: Mutex<Writer> = Mutex::new(Writer {
|
||||
column_position: 0,
|
||||
color_code: ColorCode::new(Color::Yellow, Color::Black),
|
||||
buffer: unsafe { &mut *(0xb8000 as *mut Buffer) },
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
现在我们可以删除 `print_something` 函数,尝试直接在 `_start` 函数中打印字符:
|
||||
|
||||
```rust
|
||||
// in src/main.rs
|
||||
#[no_mangle]
|
||||
pub extern "C" fn _start() -> ! {
|
||||
use core::fmt::Write;
|
||||
vga_buffer::WRITER.lock().write_str("Hello again").unwrap();
|
||||
write!(vga_buffer::WRITER.lock(), ", some numbers: {} {}", 42, 1.337).unwrap();
|
||||
|
||||
loop {}
|
||||
}
|
||||
```
|
||||
|
||||
在这里,我们需要导入名为 `fmt::Write` 的 trait,来使用实现它的类的相应方法。
|
||||
|
||||
### 安全性
|
||||
|
||||
经过上面的努力后,我们现在的代码只剩一个 unsafe 语句块,它用于创建一个指向 `0xb8000` 地址的 `Buffer` 类型引用;在这步之后,所有的操作都是安全的。Rust 将为每个数组访问检查边界,所以我们不会在不经意间越界到缓冲区之外。因此,我们把需要的条件编码到 Rust 的类型系统,这之后,我们为外界提供的接口就符合内存安全原则了。
|
||||
|
||||
### `println!` 宏
|
||||
|
||||
现在我们有了一个全局的 `Writer` 实例,我们就可以基于它实现 `println!` 宏,这样它就能被任意地方的代码使用了。Rust 提供的[宏定义语法](https://doc.rust-lang.org/nightly/book/ch19-06-macros.html#declarative-macros-with-macro_rules-for-general-metaprogramming)需要时间理解,所以我们将不从零开始编写这个宏。我们先看看标准库中 [`println!` 宏的实现源码](https://doc.rust-lang.org/nightly/std/macro.println!.html):
|
||||
|
||||
```rust
|
||||
#[macro_export]
|
||||
macro_rules! println {
|
||||
() => (print!("\n"));
|
||||
($($arg:tt)*) => (print!("{}\n", format_args!($($arg)*)));
|
||||
}
|
||||
```
|
||||
|
||||
宏是通过一个或多个**规则**(rule)定义的,这就像 `match` 语句的多个分支。`println!` 宏有两个规则:第一个规则不要求传入参数——就比如 `println!()` ——它将被扩展为 `print!("\n")`,因此只会打印一个新行;第二个要求传入参数——好比 `println!("Rust 能够编写操作系统")` 或 `println!("我学习 Rust 已经{}年了", 3)`——它将使用 `print!` 宏扩展,传入它需求的所有参数,并在输出的字符串最后加入一个换行符 `\n`。
|
||||
|
||||
这里,`#[macro_export]` 属性让整个包(crate)和基于它的包都能访问这个宏,而不仅限于定义它的模块(module)。它还将把宏置于包的根模块(crate root)下,这意味着比如我们需要通过 `use std::println` 来导入这个宏,而不是通过 `std::macros::println`。
|
||||
|
||||
[`print!` 宏](https://doc.rust-lang.org/nightly/std/macro.print!.html)是这样定义的:
|
||||
|
||||
```
|
||||
#[macro_export]
|
||||
macro_rules! print {
|
||||
($($arg:tt)*) => ($crate::io::_print(format_args!($($arg)*)));
|
||||
}
|
||||
```
|
||||
|
||||
这个宏将扩展为一个对 `io` 模块中 [`_print` 函数](https://github.com/rust-lang/rust/blob/29f5c699b11a6a148f097f82eaa05202f8799bbc/src/libstd/io/stdio.rs#L698)的调用。[`$crate` 变量](https://doc.rust-lang.org/1.30.0/book/first-edition/macros.html#the-variable-crate)将在 `std` 包之外被解析为 `std` 包,保证整个宏在 `std` 包之外也可以使用。
|
||||
|
||||
[`format_args!` 宏](https://doc.rust-lang.org/nightly/std/macro.format_args.html)将传入的参数搭建为一个 [fmt::Arguments](https://doc.rust-lang.org/nightly/core/fmt/struct.Arguments.html) 类型,这个类型将被传入 `_print` 函数。`std` 包中的 [`_print` 函数](https://github.com/rust-lang/rust/blob/29f5c699b11a6a148f097f82eaa05202f8799bbc/src/libstd/io/stdio.rs#L698)将调用复杂的私有函数 `print_to`,来处理对不同 `Stdout` 设备的支持。我们不需要编写这样的复杂函数,因为我们只需要打印到 VGA 字符缓冲区。
|
||||
|
||||
要打印到字符缓冲区,我们把 `println!` 和 `print!` 两个宏复制过来,但修改部分代码,让这些宏使用我们定义的 `_print` 函数:
|
||||
|
||||
```rust
|
||||
// in src/vga_buffer.rs
|
||||
|
||||
#[macro_export]
|
||||
macro_rules! print {
|
||||
($($arg:tt)*) => ($crate::vga_buffer::_print(format_args!($($arg)*)));
|
||||
}
|
||||
|
||||
#[macro_export]
|
||||
macro_rules! println {
|
||||
() => ($crate::print!("\n"));
|
||||
($($arg:tt)*) => ($crate::print!("{}\n", format_args!($($arg)*)));
|
||||
}
|
||||
|
||||
#[doc(hidden)]
|
||||
pub fn _print(args: fmt::Arguments) {
|
||||
use core::fmt::Write;
|
||||
WRITER.lock().write_fmt(args).unwrap();
|
||||
}
|
||||
```
|
||||
|
||||
我们首先修改了 `println!` 宏,在每个使用的 `print!` 宏前面添加了 `$crate` 变量。这样我们在只需要使用 `println!` 时,不必也编写代码导入 `print!` 宏。
|
||||
|
||||
就像标准库做的那样,我们为两个宏都添加了 `#[macro_export]` 属性,这样在包的其它地方也可以使用它们。需要注意的是,这将占用包的**根命名空间**(root namespace),所以我们不能通过 `use crate::vga_buffer::println` 来导入它们;我们应该使用 `use crate::println`。
|
||||
|
||||
另外,`_print` 函数将占有静态变量 `WRITER` 的锁,并调用它的 `write_fmt` 方法。这个方法是从名为 `Write` 的 trait 中获得的,所以我们需要导入这个 trait。额外的 `unwrap()` 函数将在打印不成功的时候 panic;但既然我们的 `write_str` 总是返回 `Ok`,这种情况不应该发生。
|
||||
|
||||
如果这个宏将能在模块外访问,它们也应当能访问 `_print` 函数,因此这个函数必须是公有的(public)。然而,考虑到这是一个私有的实现细节,我们添加一个 [`doc(hidden)` 属性](https://doc.rust-lang.org/nightly/rustdoc/the-doc-attribute.html#dochidden),防止它在生成的文档中出现。
|
||||
|
||||
### 使用 `println!` 的 Hello World
|
||||
|
||||
现在,我们可以在 `_start` 里使用 `println!` 了:
|
||||
|
||||
```rust
|
||||
// in src/main.rs
|
||||
|
||||
#[no_mangle]
|
||||
pub extern "C" fn _start() {
|
||||
println!("Hello World{}", "!");
|
||||
|
||||
loop {}
|
||||
}
|
||||
```
|
||||
|
||||
要注意的是,我们在入口函数中不需要导入这个宏——因为它已经被置于包的根命名空间了。
|
||||
|
||||
运行这段代码,和我们预料的一样,一个 *“Hello World!”* 字符串被打印到了屏幕上:
|
||||
|
||||

|
||||
|
||||
### 打印 panic 信息
|
||||
|
||||
既然我们已经有了 `println!` 宏,我们可以在 panic 处理函数中,使用它打印 panic 信息和 panic 产生的位置:
|
||||
|
||||
```rust
|
||||
// in main.rs
|
||||
|
||||
/// 这个函数将在 panic 发生时被调用
|
||||
#[panic_handler]
|
||||
fn panic(info: &PanicInfo) -> ! {
|
||||
println!("{}", info);
|
||||
loop {}
|
||||
}
|
||||
```
|
||||
|
||||
当我们在 `_start` 函数中插入一行 `panic!("Some panic message");` 后,我们得到了这样的输出:
|
||||
|
||||

|
||||
|
||||
所以,现在我们不仅能知道 panic 已经发生,还能够知道 panic 信息和产生 panic 的代码。
|
||||
|
||||
## 小结
|
||||
|
||||
这篇文章中,我们学习了 VGA 字符缓冲区的结构,以及如何在 `0xb8000` 的内存映射地址访问它。我们将所有的不安全操作包装为一个 Rust 模块,以便在外界安全地访问它。
|
||||
|
||||
我们也发现了——感谢便于使用的 cargo——在 Rust 中使用第三方提供的包是及其容易的。我们添加的两个依赖项,`lazy_static` 和 `spin`,都在操作系统开发中及其有用;我们将在未来的文章中多次使用它们。
|
||||
|
||||
## 下篇预告
|
||||
|
||||
下一篇文章中,我们将会讲述如何配置 Rust 内置的单元测试框架。我们还将为本文编写的 VGA 缓冲区模块添加基础的单元测试项目。
|
||||
@@ -1,932 +0,0 @@
|
||||
+++
|
||||
title = "内核测试"
|
||||
weight = 4
|
||||
path = "zh-CN/testing"
|
||||
date = 2019-04-27
|
||||
|
||||
[extra]
|
||||
# Please update this when updating the translation
|
||||
translation_based_on_commit = "bd6fbcb1c36705b2c474d7fcee387bfea1210851"
|
||||
# GitHub usernames of the people that translated this post
|
||||
translators = ["luojia65", "Rustin-Liu"]
|
||||
+++
|
||||
|
||||
本文主要讲述了在`no_std`环境下进行单元测试和集成测试的方法。我们将通过Rust的自定义测试框架来在我们的内核中执行一些测试函数。为了将结果反馈到QEMU上,我们需要使用QEMU的一些其他的功能以及`bootimage`工具。
|
||||
|
||||
<!-- more -->
|
||||
|
||||
这个系列的blog在[GitHub]上开放开发,如果你有任何问题,请在这里开一个issue来讨论。当然你也可以在[底部]留言。你可以在[这里][post branch]找到这篇文章的完整源码。
|
||||
|
||||
[GitHub]: https://github.com/phil-opp/blog_os
|
||||
[at the bottom]: #comments
|
||||
<!-- fix for zola anchor checker (target is in template): <a id="comments"> -->
|
||||
[post branch]: https://github.com/phil-opp/blog_os/tree/post-04
|
||||
|
||||
<!-- toc -->
|
||||
|
||||
## 阅读要求
|
||||
|
||||
这篇文章替换了此前的(现在已经过时了) [_单元测试(Unit Testing)_] 和 [_集成测试(Integration Tests)_] 两篇文章。这里我将假定你是在2019-04-27日后阅读的[_最小Rust内核_]一文。总而言之,本文要求你已经有一个[设置默认目标]的 `.cargo/config` 文件且[定义了一个runner可执行文件]。
|
||||
|
||||
[_单元测试(Unit Testing)_]: @/edition-2/posts/deprecated/04-unit-testing/index.md
|
||||
[_集成测试(Integration Tests)_]: @/edition-2/posts/deprecated/05-integration-tests/index.md
|
||||
[_最小Rust内核_]: @/edition-2/posts/02-minimal-rust-kernel/index.md
|
||||
[设置默认目标]: @/edition-2/posts/02-minimal-rust-kernel/index.md#set-a-default-target
|
||||
[定义了一个runner可执行文件]: @/edition-2/posts/02-minimal-rust-kernel/index.md#using-cargo-run
|
||||
|
||||
## Rust中的测试
|
||||
|
||||
Rust有一个**内置的测试框架**([built-in test framework]):无需任何设置就可以进行单元测试,只需要创建一个通过assert来检查结果的函数并在函数的头部加上`#[test]`属性即可。然后`cargo test`会自动找到并执行你的crate中的所有测试函数。
|
||||
|
||||
[built-in test framework]: https://doc.rust-lang.org/book/second-edition/ch11-00-testing.html
|
||||
|
||||
不幸的是,对于一个`no_std`的应用,比如我们的内核,这有点点复杂。现在的问题是,Rust的测试框架会隐式的调用内置的[`test`]库,但是这个库依赖于标准库。这也就是说我们的 `#[no_std]`内核无法使用默认的测试框架。
|
||||
|
||||
[`test`]: https://doc.rust-lang.org/test/index.html
|
||||
|
||||
当我们试图在我们的项目中执行`cargo xtest`时,我们可以看到如下信息:
|
||||
|
||||
```
|
||||
> cargo xtest
|
||||
Compiling blog_os v0.1.0 (/…/blog_os)
|
||||
error[E0463]: can't find crate for `test`
|
||||
```
|
||||
|
||||
由于`test`crate依赖于标准库,所以它在我们的裸机目标上并不可用。虽然将`test`crate移植到一个 `#[no_std]` 上下文环境中是[可能的][utest],但是这样做是高度不稳定的并且还会需要一些特殊的hacks,例如重定义 `panic` 宏。
|
||||
|
||||
[utest]: https://github.com/japaric/utest
|
||||
|
||||
### 自定义测试框架
|
||||
|
||||
幸运的是,Rust支持通过使用不稳定的**自定义测试框架**([`custom_test_frameworks`]) 功能来替换默认的测试框架。该功能不需要额外的库,因此在 `#[no_std]`环境中它也可以工作。它的工作原理是收集所有标注了 `#[test_case]`属性的函数,然后将这个测试函数的列表作为参数传递给用户指定的runner函数。因此,它实现了对测试过程的最大控制。
|
||||
|
||||
[`custom_test_frameworks`]: https://doc.rust-lang.org/unstable-book/language-features/custom-test-frameworks.html
|
||||
|
||||
与默认的测试框架相比,它的缺点是有一些高级功能诸如 [`should_panic` tests]都不可用了。相对的,如果需要这些功能,我们需要自己来实现。当然,这点对我们来说是好事,因为我们的环境非常特殊,在这个环境里,这些高级功能的默认实现无论如何都是无法工作的,举个例子, `#[should_panic]`属性依赖于堆栈展开来捕获内核panic,而我的内核早已将其禁用了。
|
||||
|
||||
[`should_panic` tests]: https://doc.rust-lang.org/book/ch11-01-writing-tests.html#checking-for-panics-with-should_panic
|
||||
|
||||
要为我们的内核实现自定义测试框架,我们需要将如下代码添加到我们的`main.rs`中去:
|
||||
|
||||
```rust
|
||||
// in src/main.rs
|
||||
|
||||
#![feature(custom_test_frameworks)]
|
||||
#![test_runner(crate::test_runner)]
|
||||
|
||||
#[cfg(test)]
|
||||
fn test_runner(tests: &[&dyn Fn()]) {
|
||||
println!("Running {} tests", tests.len());
|
||||
for test in tests {
|
||||
test();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
我们的runner会打印一个简短的debug信息然后调用列表中的每个测试函数。参数类型 `&[&dyn Fn()]` 是[_Fn()_] trait的 [_trait object_] 引用的一个 [_slice_]。它基本上可以被看做一个可以像函数一样被调用的类型的引用列表。由于这个函数在不进行测试的时候没有什么用,这里我们使用 `#[cfg(test)]`属性保证它只会出现在测试中。
|
||||
|
||||
[_slice_]: https://doc.rust-lang.org/std/primitive.slice.html
|
||||
[_trait object_]: https://doc.rust-lang.org/1.30.0/book/first-edition/trait-objects.html
|
||||
[_Fn()_]: https://doc.rust-lang.org/std/ops/trait.Fn.html
|
||||
|
||||
现在当我们运行 `cargo xtest` ,我们可以发现运行成功了。然而,我们看到的仍然是"Hello World"而不是我们的 `test_runner`传递来的信息。这是由于我们的入口点仍然是 `_start` 函数——自定义测试框架会生成一个`main`函数来调用`test_runner`,但是由于我们使用了 `#[no_main]`并提供了我们自己的入口点,所以这个`main`函数就被忽略了。
|
||||
|
||||
为了修复这个问题,我们需要通过 `reexport_test_harness_main`属性来将生成的函数的名称更改为与`main`不同的名称。然后我们可以在我们的`_start`函数里调用这个重命名的函数:
|
||||
|
||||
```rust
|
||||
// in src/main.rs
|
||||
|
||||
#![reexport_test_harness_main = "test_main"]
|
||||
|
||||
#[no_mangle]
|
||||
pub extern "C" fn _start() -> ! {
|
||||
println!("Hello World{}", "!");
|
||||
|
||||
#[cfg(test)]
|
||||
test_main();
|
||||
|
||||
loop {}
|
||||
}
|
||||
```
|
||||
|
||||
我们将测试框架的入口函数的名字设置为`test_main`,并在我们的 `_start`入口点里调用它。通过使用**条件编译**([conditional compilation]),我们能够只在上下文环境为测试(test)时调用`test_main`,因为该函数将不在非测试上下文中生成。
|
||||
|
||||
[ conditional compilation ]: https://doc.rust-lang.org/1.30.0/book/first-edition/conditional-compilation.html
|
||||
|
||||
现在当我们执行 `cargo xtest`时,我们可以看到我们的`test_runner`将"Running 0 tests"信息显示在屏幕上了。我们可以创建第一个测试函数了:
|
||||
|
||||
```rust
|
||||
// in src/main.rs
|
||||
|
||||
#[test_case]
|
||||
fn trivial_assertion() {
|
||||
print!("trivial assertion... ");
|
||||
assert_eq!(1, 1);
|
||||
println!("[ok]");
|
||||
}
|
||||
```
|
||||
|
||||
现在,当我们运行 `cargo xtest`时,我们可以看到如下输出:
|
||||
|
||||
![QEMU printing "Hello World!", "Running 1 tests", and "trivial assertion... [ok]"](https://os.phil-opp.com/testing/qemu-test-runner-output.png)
|
||||
|
||||
传递给 `test_runner`函数的`tests`切片里包含了一个 `trivial_assertion` 函数的引用,从屏幕上输出的 `trivial assertion... [ok]`信息可见,我们的测试已被调用并且顺利通过。
|
||||
|
||||
在执行完tests后, `test_runner`会将结果返回给 `test_main`函数,而这个函数又返回到 `_start`入口点函数——这样我们就进入了一个死循环,因为入口点函数是不允许返回的。这将导致一个问题:我们希望`cargo xtest`在所有的测试运行完毕后,才返回并退出。
|
||||
|
||||
## 退出QEMU
|
||||
|
||||
现在我们在`_start`函数结束后进入了一个死循环,所以每次执行完`cargo xtest`后我们都需要手动去关闭QEMU;但是我们还想在没有用户交互的脚本环境下执行 `cargo xtest`。解决这个问题的最佳方式,是实现一个合适的方法来关闭我们的操作系统——不幸的是,这个方式实现起来相对有些复杂,因为这要求我们实现对[APM]或[ACPI]电源管理标准的支持。
|
||||
|
||||
[APM]: https://wiki.osdev.org/APM
|
||||
[ACPI]: https://wiki.osdev.org/ACPI
|
||||
|
||||
幸运的是,还有一个绕开这些问题的办法:QEMU支持一种名为 `isa-debug-exit`的特殊设备,它提供了一种从客户系统(guest system)里退出QEMU的简单方式。为了使用这个设备,我们需要向QEMU传递一个`-device`参数。当然,我们也可以通过将 `package.metadata.bootimage.test-args` 配置关键字添加到我们的`Cargo.toml`来达到目的:
|
||||
|
||||
```toml
|
||||
# in Cargo.toml
|
||||
|
||||
[package.metadata.bootimage]
|
||||
test-args = ["-device", "isa-debug-exit,iobase=0xf4,iosize=0x04"]
|
||||
```
|
||||
|
||||
`bootimage runner` 会在QEMU的默认测试命令后添加`test-args` 参数。(对于`cargo xrun`命令,这个参数会被忽略。)
|
||||
|
||||
在传递设备名 (`isa-debug-exit`)的同时,我们还传递了两个参数,`iobase` 和 `iosize` 。这两个参数指定了一个_I/O 端口_,我们的内核将通过它来访问设备。
|
||||
|
||||
### I/O 端口
|
||||
在x86平台上,CPU和外围硬件通信通常有两种方式,**内存映射I/O**和**端口映射I/O**。之前,我们已经使用内存映射的方式,通过内存地址`0xb8000`访问了[VGA文本缓冲区]。该地址并没有映射到RAM,而是映射到了VGA设备的一部分内存上。
|
||||
|
||||
[VGA text buffer]: @/edition-2/posts/03-vga-text-buffer/index.md
|
||||
|
||||
与内存映射不同,端口映射I/O使用独立的I/O总线来进行通信。每个外围设备都有一个或数个端口号。CPU采用了特殊的`in`和`out`指令来和端口通信,这些指令要求一个端口号和一个字节的数据作为参数(有些这种指令的变体也允许发送`u16`或是`u32`长度的数据)。
|
||||
|
||||
`isa-debug-exit`设备使用的就是端口映射I/O。其中, `iobase` 参数指定了设备对应的端口地址(在x86中,`0xf4`是一个[通常未被使用的端口][list of x86 I/O ports]),而`iosize`则指定了端口的大小(`0x04`代表4字节)。
|
||||
|
||||
[list of x86 I/O ports]: https://wiki.osdev.org/I/O_Ports#The_list
|
||||
|
||||
### 使用退出(Exit)设备
|
||||
|
||||
`isa-debug-exit`设备的功能非常简单。当一个 `value`写入`iobase`指定的端口时,它会导致QEMU以**退出状态**([exit status])`(value << 1) | 1`退出。也就是说,当我们向端口写入`0`时,QEMU将以退出状态`(0 << 1) | 1 = 1`退出,而当我们向端口写入`1`时,它将以退出状态`(1 << 1) | 1 = 3`退出。
|
||||
|
||||
[exit status]: https://en.wikipedia.org/wiki/Exit_status
|
||||
|
||||
这里我们使用 [`x86_64`] crate提供的抽象,而不是手动调用`in`或`out`指令。为了添加对该crate的依赖,我们可以将其添加到我们的 `Cargo.toml`中的 `dependencies` 小节中去:
|
||||
|
||||
|
||||
[`x86_64`]: https://docs.rs/x86_64/0.14.2/x86_64/
|
||||
|
||||
```toml
|
||||
# in Cargo.toml
|
||||
|
||||
[dependencies]
|
||||
x86_64 = "0.14.2"
|
||||
```
|
||||
|
||||
现在我们可以使用crate中提供的[`Port`] 类型来创建一个`exit_qemu` 函数了:
|
||||
|
||||
[`Port`]: https://docs.rs/x86_64/0.14.2/x86_64/instructions/port/struct.Port.html
|
||||
|
||||
```rust
|
||||
// in src/main.rs
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
#[repr(u32)]
|
||||
pub enum QemuExitCode {
|
||||
Success = 0x10,
|
||||
Failed = 0x11,
|
||||
}
|
||||
|
||||
pub fn exit_qemu(exit_code: QemuExitCode) {
|
||||
use x86_64::instructions::port::Port;
|
||||
|
||||
unsafe {
|
||||
let mut port = Port::new(0xf4);
|
||||
port.write(exit_code as u32);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
该函数在`0xf4`处创建了一个新的端口,该端口同时也是 `isa-debug-exit` 设备的 `iobase` 。然后它会向端口写入传递的退出代码。这里我们使用`u32`来传递数据,因为我们之前已经将 `isa-debug-exit`设备的 `iosize` 指定为4字节了。上述两个操作都是`unsafe`的,因为I/O端口的写入操作通常会导致一些不可预知的行为。
|
||||
|
||||
为了指定退出状态,我们创建了一个 `QemuExitCode`枚举。思路大体上是,如果所有的测试均成功,就以成功退出码退出;否则就以失败退出码退出。这个枚举类型被标记为 `#[repr(u32)]`,代表每个变量都是一个`u32`的整数类型。我们使用退出代码`0x10`代表成功,`0x11`代表失败。 实际的退出代码并不重要,只要它们不与QEMU的默认退出代码冲突即可。 例如,使用退出代码0表示成功可能并不是一个好主意,因为它在转换后就变成了`(0 << 1) | 1 = 1` ,而`1`是QEMU运行失败时的默认退出代码。 这样,我们就无法将QEMU错误与成功的测试运行区分开来了。
|
||||
|
||||
现在我们来更新`test_runner`的代码,让程序在运行所有测试完毕后退出QEMU:
|
||||
|
||||
```rust
|
||||
fn test_runner(tests: &[&dyn Fn()]) {
|
||||
println!("Running {} tests", tests.len());
|
||||
for test in tests {
|
||||
test();
|
||||
}
|
||||
/// new
|
||||
exit_qemu(QemuExitCode::Success);
|
||||
}
|
||||
```
|
||||
|
||||
当我们现在运行`cargo xtest`时,QEMU会在测试运行后立刻退出。现在的问题是,即使我们传递了表示成功(`Success`)的退出代码, `cargo test`依然会将所有的测试都视为失败:
|
||||
|
||||
```
|
||||
> cargo xtest
|
||||
Finished dev [unoptimized + debuginfo] target(s) in 0.03s
|
||||
Running target/x86_64-blog_os/debug/deps/blog_os-5804fc7d2dd4c9be
|
||||
Building bootloader
|
||||
Compiling bootloader v0.5.3 (/home/philipp/Documents/bootloader)
|
||||
Finished release [optimized + debuginfo] target(s) in 1.07s
|
||||
Running: `qemu-system-x86_64 -drive format=raw,file=/…/target/x86_64-blog_os/debug/
|
||||
deps/bootimage-blog_os-5804fc7d2dd4c9be.bin -device isa-debug-exit,iobase=0xf4,
|
||||
iosize=0x04`
|
||||
error: test failed, to rerun pass '--bin blog_os'
|
||||
```
|
||||
|
||||
这里的问题在于,`cargo test`会将所有非`0`的错误码都视为测试失败。
|
||||
|
||||
### 成功退出(Exit)代码
|
||||
|
||||
为了解决这个问题, `bootimage`提供了一个 `test-success-exit-code`配置项,可以将指定的退出代码映射到退出代码`0`:
|
||||
|
||||
```toml
|
||||
[package.metadata.bootimage]
|
||||
test-args = […]
|
||||
test-success-exit-code = 33 # (0x10 << 1) | 1
|
||||
```
|
||||
|
||||
有了这个配置,`bootimage`就会将我们的成功退出码映射到退出码0;这样一来, `cargo xtest`就能正确的识别出测试成功的情况,而不会将其视为测试失败。
|
||||
|
||||
我们的测试runner现在会在正确报告测试结果后自动关闭QEMU。我们可以看到QEMU的窗口只会显示很短的时间——我们不容易看清测试的结果。如果测试结果会打印在控制台上而不是QEMU里,让我们能在QEMU退出后仍然能看到测试结果就好了。
|
||||
|
||||
## 打印到控制台
|
||||
|
||||
要在控制台上查看测试输出,我们需要以某种方式将数据从内核发送到宿主系统。 有多种方法可以实现这一点,例如通过TCP网络接口来发送数据。但是,设置网络堆栈是一项很复杂的任务——这里我们选择更简单的解决方案。
|
||||
|
||||
### 串口
|
||||
|
||||
发送数据的一个简单的方式是通过[串行端口],这是一个现代电脑中已经不存在的旧标准接口(译者注:玩过单片机的同学应该知道,其实译者上大学的时候有些同学的笔记本电脑还有串口的,没有串口的同学在烧录单片机程序的时候也都会需要usb转串口线,一般是51,像stm32有st-link,这个另说,不过其实也可以用串口来下载)。串口非常易于编程,QEMU可以将通过串口发送的数据重定向到宿主机的标准输出或是文件中。
|
||||
|
||||
[串行端口]: https://en.wikipedia.org/wiki/Serial_port
|
||||
|
||||
用来实现串行接口的芯片被称为 [UARTs]。在x86上,有[很多UART模型],但是幸运的是,它们之间仅有的那些不同之处都是我们用不到的高级功能。目前通用的UARTs都会兼容[16550 UART],所以我们在我们测试框架里采用该模型。
|
||||
|
||||
[UARTs]: https://en.wikipedia.org/wiki/Universal_asynchronous_receiver-transmitter
|
||||
[很多UART模型]: https://en.wikipedia.org/wiki/Universal_asynchronous_receiver-transmitter#UART_models
|
||||
[16550 UART]: https://en.wikipedia.org/wiki/16550_UART
|
||||
|
||||
我们使用[`uart_16550`] crate来初始化UART,并通过串口来发送数据。为了将该crate添加为依赖,我们将我们的`Cargo.toml`和`main.rs`修改为如下:
|
||||
|
||||
[`uart_16550`]: https://docs.rs/uart_16550
|
||||
|
||||
```toml
|
||||
# in Cargo.toml
|
||||
|
||||
[dependencies]
|
||||
uart_16550 = "0.2.0"
|
||||
```
|
||||
|
||||
`uart_16550` crate包含了一个代表UART寄存器的`SerialPort`结构体,但是我们仍然需要自己来创建一个相应的实例。我们使用以下内容来创建一个新的串口模块`serial`:
|
||||
|
||||
```rust
|
||||
// in src/main.rs
|
||||
|
||||
mod serial;
|
||||
```
|
||||
|
||||
```rust
|
||||
// in src/serial.rs
|
||||
|
||||
use uart_16550::SerialPort;
|
||||
use spin::Mutex;
|
||||
use lazy_static::lazy_static;
|
||||
|
||||
lazy_static! {
|
||||
pub static ref SERIAL1: Mutex<SerialPort> = {
|
||||
let mut serial_port = unsafe { SerialPort::new(0x3F8) };
|
||||
serial_port.init();
|
||||
Mutex::new(serial_port)
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
就像[VGA文本缓冲区][vga lazy-static]一样,我们使用 `lazy_static` 和一个自旋锁来创建一个 `static` writer实例。通过使用 `lazy_static` ,我们可以保证`init`方法只会在该示例第一次被使用使被调用。
|
||||
|
||||
和 `isa-debug-exit`设备一样,UART也是用过I/O端口进行编程的。由于UART相对来讲更加复杂,它使用多个I/O端口来对不同的设备寄存器进行编程。不安全的`SerialPort::new`函数需要UART的第一个I/O端口的地址作为参数,从该地址中可以计算出所有所需端口的地址。我们传递的端口地址为`0x3F8` ,该地址是第一个串行接口的标准端口号。
|
||||
|
||||
[vga lazy-static]: @/edition-2/posts/03-vga-text-buffer/index.md#lazy-statics
|
||||
|
||||
为了使串口更加易用,我们添加了 `serial_print!` 和 `serial_println!`宏:
|
||||
|
||||
```rust
|
||||
#[doc(hidden)]
|
||||
pub fn _print(args: ::core::fmt::Arguments) {
|
||||
use core::fmt::Write;
|
||||
SERIAL1.lock().write_fmt(args).expect("Printing to serial failed");
|
||||
}
|
||||
|
||||
/// Prints to the host through the serial interface.
|
||||
#[macro_export]
|
||||
macro_rules! serial_print {
|
||||
($($arg:tt)*) => {
|
||||
$crate::serial::_print(format_args!($($arg)*));
|
||||
};
|
||||
}
|
||||
|
||||
/// Prints to the host through the serial interface, appending a newline.
|
||||
#[macro_export]
|
||||
macro_rules! serial_println {
|
||||
() => ($crate::serial_print!("\n"));
|
||||
($fmt:expr) => ($crate::serial_print!(concat!($fmt, "\n")));
|
||||
($fmt:expr, $($arg:tt)*) => ($crate::serial_print!(
|
||||
concat!($fmt, "\n"), $($arg)*));
|
||||
}
|
||||
```
|
||||
|
||||
该实现和我们此前的`print`和`println`宏的实现非常类似。 由于`SerialPort`类型已经实现了`fmt::Write` trait,所以我们不需要提供我们自己的实现了。
|
||||
|
||||
[`fmt::Write`]: https://doc.rust-lang.org/nightly/core/fmt/trait.Write.html
|
||||
|
||||
现在我们可以从测试代码里向串行接口打印而不是向VGA文本缓冲区打印了:
|
||||
|
||||
```rust
|
||||
// in src/main.rs
|
||||
|
||||
#[cfg(test)]
|
||||
fn test_runner(tests: &[&dyn Fn()]) {
|
||||
serial_println!("Running {} tests", tests.len());
|
||||
[…]
|
||||
}
|
||||
|
||||
#[test_case]
|
||||
fn trivial_assertion() {
|
||||
serial_print!("trivial assertion... ");
|
||||
assert_eq!(1, 1);
|
||||
serial_println!("[ok]");
|
||||
}
|
||||
```
|
||||
|
||||
注意,由于我们使用了 `#[macro_export]` 属性, `serial_println`宏直接位于根命名空间下——所以通过`use crate::serial::serial_println` 来导入该宏是不起作用的。
|
||||
|
||||
### QEMU参数
|
||||
|
||||
为了查看QEMU的串行输出,我们需要使用`-serial`参数将输出重定向到stdout:
|
||||
|
||||
```toml
|
||||
# in Cargo.toml
|
||||
|
||||
[package.metadata.bootimage]
|
||||
test-args = [
|
||||
"-device", "isa-debug-exit,iobase=0xf4,iosize=0x04", "-serial", "stdio"
|
||||
]
|
||||
```
|
||||
|
||||
现在,当我们运行 `cargo xtest`时,我们可以直接在控制台里看到测试输出了:
|
||||
|
||||
```
|
||||
> cargo xtest
|
||||
Finished dev [unoptimized + debuginfo] target(s) in 0.02s
|
||||
Running target/x86_64-blog_os/debug/deps/blog_os-7b7c37b4ad62551a
|
||||
Building bootloader
|
||||
Finished release [optimized + debuginfo] target(s) in 0.02s
|
||||
Running: `qemu-system-x86_64 -drive format=raw,file=/…/target/x86_64-blog_os/debug/
|
||||
deps/bootimage-blog_os-7b7c37b4ad62551a.bin -device
|
||||
isa-debug-exit,iobase=0xf4,iosize=0x04 -serial stdio`
|
||||
Running 1 tests
|
||||
trivial assertion... [ok]
|
||||
```
|
||||
|
||||
然而,当测试失败时,我们仍然会在QEMU内看到输出结果,因为我们的panic handler还是用了`println`。为了模拟这个过程,我们将我们的 `trivial_assertion` test中的断言(assertion)修改为 `assert_eq!(0, 1)`:
|
||||
|
||||

|
||||
|
||||
可以看到,panic信息被打印到了VGA缓冲区里,而测试输出则被打印到串口上了。panic信息非常有用,所以我们希望能够在控制台中来查看它。
|
||||
|
||||
### 在panic时打印一个错误信息
|
||||
|
||||
为了在panic时使用错误信息来退出QEMU,我们可以使用**条件编译**([conditional compilation])在测试模式下使用(与非测试模式下)不同的panic处理方式:
|
||||
|
||||
[conditional compilation]: https://doc.rust-lang.org/1.30.0/book/first-edition/conditional-compilation.html
|
||||
|
||||
```rust
|
||||
// our existing panic handler
|
||||
#[cfg(not(test))] // new attribute
|
||||
#[panic_handler]
|
||||
fn panic(info: &PanicInfo) -> ! {
|
||||
println!("{}", info);
|
||||
loop {}
|
||||
}
|
||||
|
||||
// our panic handler in test mode
|
||||
#[cfg(test)]
|
||||
#[panic_handler]
|
||||
fn panic(info: &PanicInfo) -> ! {
|
||||
serial_println!("[failed]\n");
|
||||
serial_println!("Error: {}\n", info);
|
||||
exit_qemu(QemuExitCode::Failed);
|
||||
loop {}
|
||||
}
|
||||
```
|
||||
|
||||
在我们的测试panic处理中,我们用 `serial_println`来代替`println` 并使用失败代码来退出QEMU。注意,在`exit_qemu`调用后,我们仍然需要一个无限循环的`loop`因为编译器并不知道 `isa-debug-exit`设备会导致程序退出。
|
||||
|
||||
现在,即使在测试失败的情况下QEMU仍然会存在,并会将一些有用的错误信息打印到控制台:
|
||||
|
||||
```
|
||||
> cargo xtest
|
||||
Finished dev [unoptimized + debuginfo] target(s) in 0.02s
|
||||
Running target/x86_64-blog_os/debug/deps/blog_os-7b7c37b4ad62551a
|
||||
Building bootloader
|
||||
Finished release [optimized + debuginfo] target(s) in 0.02s
|
||||
Running: `qemu-system-x86_64 -drive format=raw,file=/…/target/x86_64-blog_os/debug/
|
||||
deps/bootimage-blog_os-7b7c37b4ad62551a.bin -device
|
||||
isa-debug-exit,iobase=0xf4,iosize=0x04 -serial stdio`
|
||||
Running 1 tests
|
||||
trivial assertion... [failed]
|
||||
|
||||
Error: panicked at 'assertion failed: `(left == right)`
|
||||
left: `0`,
|
||||
right: `1`', src/main.rs:65:5
|
||||
```
|
||||
|
||||
由于现在所有的测试都将输出到控制台上,我们不再需要让QEMU窗口弹出一小会儿了——我们完全可以把窗口藏起来。
|
||||
|
||||
### 隐藏 QEMU
|
||||
|
||||
由于我们使用`isa-debug-exit`设备和串行端口来报告完整的测试结果,所以我们不再需要QMEU的窗口了。我们可以通过向QEMU传递 `-display none`参数来将其隐藏:
|
||||
|
||||
```toml
|
||||
# in Cargo.toml
|
||||
|
||||
[package.metadata.bootimage]
|
||||
test-args = [
|
||||
"-device", "isa-debug-exit,iobase=0xf4,iosize=0x04", "-serial", "stdio",
|
||||
"-display", "none"
|
||||
]
|
||||
```
|
||||
|
||||
现在QEMU完全在后台运行且没有任何窗口会被打开。这不仅不那么烦人,还允许我们的测试框架在没有图形界面的环境里,诸如CI服务器或是[SSH]连接里运行。
|
||||
|
||||
[SSH]: https://en.wikipedia.org/wiki/Secure_Shell
|
||||
|
||||
### 超时
|
||||
|
||||
由于 `cargo xtest` 会等待test runner退出,如果一个测试永远不返回那么它就会一直阻塞test runner。幸运的是,在实际应用中这并不是一个大问题,因为无限循环通常是很容易避免的。在我们的这个例子里,无限循环会发生在以下几种不同的情况中:
|
||||
|
||||
|
||||
- bootloader加载内核失败,导致系统不停重启;
|
||||
- BIOS/UEFI固件加载bootloader失败,同样会导致无限重启;
|
||||
- CPU在某些函数结束时进入一个`loop {}`语句,例如因为QEMU的exit设备无法正常工作而导致死循环;
|
||||
- 硬件触发了系统重置,例如未捕获CPU异常时(后续的文章将会详细解释)。
|
||||
|
||||
由于无限循环可能会在各种情况中发生,因此, `bootimage` 工具默认为每个可执行测试设置了一个长度为5分钟的超时时间。如果测试未在此时间内完成,则将其标记为失败,并向控制台输出"Timed Out(超时)"错误。这个功能确保了那些卡在无限循环里的测试不会一直阻塞`cargo xtest`。
|
||||
|
||||
你可以将`loop {}`语句添加到 `trivial_assertion`测试中来进行尝试。当你运行 `cargo xtest`时,你可以发现该测试会在五分钟后被标记为超时。超时持续的时间可以通过Cargo.toml中的`test-timeout`来进行[配置][bootimage config]:
|
||||
|
||||
[bootimage config]: https://github.com/rust-osdev/bootimage#configuration
|
||||
|
||||
```toml
|
||||
# in Cargo.toml
|
||||
|
||||
[package.metadata.bootimage]
|
||||
test-timeout = 300 # (in seconds)
|
||||
```
|
||||
|
||||
如果你不想为了观察`trivial_assertion` 测试超时等待5分钟之久,你可以暂时降低将上述值。
|
||||
|
||||
此后,我们不再需要 `trivial_assertion` 测试,所以我们可以将其删除。
|
||||
|
||||
## 测试VGA缓冲区
|
||||
|
||||
现在我们已经有了一个可以工作的测试框架了,我们可以为我们的VGA缓冲区实现创建一些测试。首先,我们创建了一个非常简单的测试来验证 `println`是否正常运行而不会panic:
|
||||
|
||||
```rust
|
||||
// in src/vga_buffer.rs
|
||||
|
||||
#[cfg(test)]
|
||||
use crate::{serial_print, serial_println};
|
||||
|
||||
#[test_case]
|
||||
fn test_println_simple() {
|
||||
serial_print!("test_println... ");
|
||||
println!("test_println_simple output");
|
||||
serial_println!("[ok]");
|
||||
}
|
||||
```
|
||||
|
||||
这个测试所做的仅仅是将一些内容打印到VGA缓冲区。如果它正常结束并且没有panic,也就意味着`println`调用也没有panic。由于我们只需要将 `serial_println` 导入到测试模式里,所以我们添加了 `cfg(test)` 属性(attribute)来避免正常模式下 `cargo xbuild`会出现的未使用导入警告(unused import warning)。
|
||||
|
||||
为了确保即使打印很多行且有些行超出屏幕的情况下也没有panic发生,我们可以创建另一个测试:
|
||||
|
||||
```rust
|
||||
// in src/vga_buffer.rs
|
||||
|
||||
#[test_case]
|
||||
fn test_println_many() {
|
||||
serial_print!("test_println_many... ");
|
||||
for _ in 0..200 {
|
||||
println!("test_println_many output");
|
||||
}
|
||||
serial_println!("[ok]");
|
||||
}
|
||||
```
|
||||
|
||||
我们还可以创建另一个测试函数,来验证打印的几行字符是否真的出现在了屏幕上:
|
||||
|
||||
```rust
|
||||
// in src/vga_buffer.rs
|
||||
|
||||
#[test_case]
|
||||
fn test_println_output() {
|
||||
serial_print!("test_println_output... ");
|
||||
|
||||
let s = "Some test string that fits on a single line";
|
||||
println!("{}", s);
|
||||
for (i, c) in s.chars().enumerate() {
|
||||
let screen_char = WRITER.lock().buffer.chars[BUFFER_HEIGHT - 2][i].read();
|
||||
assert_eq!(char::from(screen_char.ascii_character), c);
|
||||
}
|
||||
|
||||
serial_println!("[ok]");
|
||||
}
|
||||
```
|
||||
|
||||
该函数定义了一个测试字符串,并通过 `println`将其输出,然后遍历静态 `WRITER`也就是vga字符缓冲区的屏幕字符。由于`println`在将字符串打印到屏幕上最后一行后会立刻附加一个新行(即输出完后有一个换行符),所以这个字符串应该会出现在第 `BUFFER_HEIGHT - 2`行。
|
||||
|
||||
通过使用[`enumerate`] ,我们统计了变量`i`的迭代次数,然后用它来加载对应于`c`的屏幕字符。 通过比较屏幕字符的`ascii_character`和`c` ,我们可以确保字符串的每个字符确实出现在vga文本缓冲区中。
|
||||
|
||||
[`enumerate`]: https://doc.rust-lang.org/core/iter/trait.Iterator.html#method.enumerate
|
||||
|
||||
如你所想,我们可以创建更多的测试函数:例如一个用来测试当打印一个很长的且包装正确的行时是否会发生panic的函数,或是一个用于测试换行符、不可打印字符、非unicode字符是否能被正确处理的函数。
|
||||
|
||||
在这篇文章的剩余部分,我们还会解释如何创建一个_集成测试_以测试不同组建之间的交互。
|
||||
|
||||
|
||||
## 集成测试
|
||||
|
||||
在Rust中,**集成测试**([integration tests])的约定是将其放到项目根目录中的`tests`目录下(即`src`的同级目录)。无论是默认测试框架还是自定义测试框架都将自动获取并执行该目录下所有的测试。
|
||||
|
||||
[integration tests]: https://doc.rust-lang.org/book/ch11-03-test-organization.html#integration-tests
|
||||
|
||||
所有的集成测试都是它们自己的可执行文件,并且与我们的`main.rs`完全独立。这也就意味着每个测试都需要定义它们自己的函数入口点。让我们创建一个名为`basic_boot`的例子来看看集成测试的工作细节吧:
|
||||
|
||||
```rust
|
||||
// in tests/basic_boot.rs
|
||||
|
||||
#![no_std]
|
||||
#![no_main]
|
||||
#![feature(custom_test_frameworks)]
|
||||
#![test_runner(crate::test_runner)]
|
||||
#![reexport_test_harness_main = "test_main"]
|
||||
|
||||
use core::panic::PanicInfo;
|
||||
|
||||
#[no_mangle] // don't mangle the name of this function
|
||||
pub extern "C" fn _start() -> ! {
|
||||
test_main();
|
||||
|
||||
loop {}
|
||||
}
|
||||
|
||||
fn test_runner(tests: &[&dyn Fn()]) {
|
||||
unimplemented!();
|
||||
}
|
||||
|
||||
#[panic_handler]
|
||||
fn panic(info: &PanicInfo) -> ! {
|
||||
loop {}
|
||||
}
|
||||
```
|
||||
|
||||
由于集成测试都是单独的可执行文件,所以我们需要再次提供所有的crate属性(`no_std`, `no_main`, `test_runner`, 等等)。我们还需要创建一个新的入口点函数`_start`,用于调用测试入口函数`test_main`。我们不需要任何的`cfg(test)` attributes(属性),因为集成测试的二进制文件在非测试模式下根本不会被编译构建。
|
||||
|
||||
这里我们采用[`unimplemented`]宏,充当`test_runner`暂未实现的占位符;添加简单的`loop {}`循环,作为`panic`处理器的内容。理想情况下,我们希望能向我们在`main.rs`里所做的一样使用`serial_println`宏和`exit_qemu`函数来实现这个函数。但问题是,由于这些测试的构建和我们的`main.rs`的可执行文件是完全独立的,我们没有办法使用这些函数。
|
||||
|
||||
[`unimplemented`]: https://doc.rust-lang.org/core/macro.unimplemented.html
|
||||
|
||||
如果现阶段你运行`cargo xtest`,你将进入一个无限循环,因为目前panic的处理就是进入无限循环。你需要使用快捷键`Ctrl+c`,才可以退出QEMU。
|
||||
|
||||
### 创建一个库
|
||||
为了让这些函数能在我们的集成测试中使用,我们需要从我们的`main.rs`中分割出一个库,这个库应当可以被其他的crate和集成测试可执行文件使用。为了达成这个目的,我们创建了一个新文件,`src/lib.rs`:
|
||||
|
||||
```rust
|
||||
// src/lib.rs
|
||||
|
||||
#![no_std]
|
||||
```
|
||||
|
||||
和`main.rs`一样,`lib.rs`也是一个可以被cargo自动识别的特殊文件。该库是一个独立的编译单元,所以我们需要再次指定`#![no_std]` 属性。
|
||||
|
||||
为了让我们的库可以和`cargo xtest`一起协同工作,我们还需要移动以下测试函数和属性:
|
||||
|
||||
```rust
|
||||
// in src/lib.rs
|
||||
|
||||
#![cfg_attr(test, no_main)]
|
||||
#![feature(custom_test_frameworks)]
|
||||
#![test_runner(crate::test_runner)]
|
||||
#![reexport_test_harness_main = "test_main"]
|
||||
|
||||
use core::panic::PanicInfo;
|
||||
|
||||
pub fn test_runner(tests: &[&dyn Fn()]) {
|
||||
serial_println!("Running {} tests", tests.len());
|
||||
for test in tests {
|
||||
test();
|
||||
}
|
||||
exit_qemu(QemuExitCode::Success);
|
||||
}
|
||||
|
||||
pub fn test_panic_handler(info: &PanicInfo) -> ! {
|
||||
serial_println!("[failed]\n");
|
||||
serial_println!("Error: {}\n", info);
|
||||
exit_qemu(QemuExitCode::Failed);
|
||||
loop {}
|
||||
}
|
||||
|
||||
/// Entry point for `cargo xtest`
|
||||
#[cfg(test)]
|
||||
#[no_mangle]
|
||||
pub extern "C" fn _start() -> ! {
|
||||
test_main();
|
||||
loop {}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[panic_handler]
|
||||
fn panic(info: &PanicInfo) -> ! {
|
||||
test_panic_handler(info)
|
||||
}
|
||||
```
|
||||
|
||||
为了能在可执行文件和集成测试中使用`test_runner`,我们不对其应用`cfg(test)` attribute(属性),并将其设置为public。同时,我们还将panic的处理程序分解为public函数`test_panic_handler`,这样一来它也可以用于可执行文件了。
|
||||
|
||||
由于我们的`lib.rs`是独立于`main.rs`进行测试的,因此当该库实在测试模式下编译时我们需要添加一个`_start`入口点和一个panic处理程序。通过使用[`cfg_attr`] ,我们可以在这种情况下有条件地启用`no_main` 属性。
|
||||
|
||||
[`cfg_attr`]: https://doc.rust-lang.org/reference/conditional-compilation.html#the-cfg_attr-attribute
|
||||
|
||||
我们还将`QemuExitCode`枚举和`exit_qemu`函数从main.rs移动过来,并将其设置为公有函数:
|
||||
|
||||
```rust
|
||||
// in src/lib.rs
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
#[repr(u32)]
|
||||
pub enum QemuExitCode {
|
||||
Success = 0x10,
|
||||
Failed = 0x11,
|
||||
}
|
||||
|
||||
pub fn exit_qemu(exit_code: QemuExitCode) {
|
||||
use x86_64::instructions::port::Port;
|
||||
|
||||
unsafe {
|
||||
let mut port = Port::new(0xf4);
|
||||
port.write(exit_code as u32);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
现在,可执行文件和集成测试都可以从库中导入这些函数,而不需要实现自己的定义。为了使`println` 和 `serial_println`可用,我们将以下的模块声明代码也移动到`lib.rs`中:
|
||||
|
||||
```rust
|
||||
// in src/lib.rs
|
||||
|
||||
pub mod serial;
|
||||
pub mod vga_buffer;
|
||||
```
|
||||
|
||||
我们将这些模块设置为public(公有),这样一来我们在库的外部也一样能使用它们了。由于这两者都用了该模块内的`_print`函数,所以这也是让`println` 和 `serial_println`宏可用的必要条件。
|
||||
|
||||
现在我们修改我们的`main.rs`代码来使用该库:
|
||||
|
||||
```rust
|
||||
// src/main.rs
|
||||
|
||||
#![no_std]
|
||||
#![no_main]
|
||||
#![feature(custom_test_frameworks)]
|
||||
#![test_runner(blog_os::test_runner)]
|
||||
#![reexport_test_harness_main = "test_main"]
|
||||
|
||||
use core::panic::PanicInfo;
|
||||
use blog_os::println;
|
||||
|
||||
#[no_mangle]
|
||||
pub extern "C" fn _start() -> ! {
|
||||
println!("Hello World{}", "!");
|
||||
|
||||
#[cfg(test)]
|
||||
test_main();
|
||||
|
||||
loop {}
|
||||
}
|
||||
|
||||
/// This function is called on panic.
|
||||
#[cfg(not(test))]
|
||||
#[panic_handler]
|
||||
fn panic(info: &PanicInfo) -> ! {
|
||||
println!("{}", info);
|
||||
loop {}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[panic_handler]
|
||||
fn panic(info: &PanicInfo) -> ! {
|
||||
blog_os::test_panic_handler(info)
|
||||
}
|
||||
```
|
||||
|
||||
可以看到,这个库用起来就像一个普通的外部crate。它的调用方法与其它crate无异;在我们的这个例子中,位置可能为`blog_os`。上述代码使用了`test_runner` attribute中的`blog_os::test_runner`函数和`cfg(test)`的panic处理中的`blog_os::test_panic_handler`函数。它还导入了`println`宏,这样一来,我们可以在我们的`_start` 和 `panic`中使用它了。
|
||||
|
||||
与此同时,`cargo xrun` 和 `cargo xtest`可以再次正常工作了。当然了,`cargo xtest`仍然会进入无限循环(你可以通过`ctrl+c`来退出)。接下来让我们在我们的集成测试中通过所需要的库函数来修复这个问题吧。
|
||||
|
||||
### 完成集成测试
|
||||
|
||||
就像我们的`src/main.rs`,我们的`tests/basic_boot.rs`可执行文件同样可以从我们的新库中导入类型。这也就意味着我们可以导入缺失的组件来完成我们的测试。
|
||||
|
||||
```rust
|
||||
// in tests/basic_boot.rs
|
||||
|
||||
#![test_runner(blog_os::test_runner)]
|
||||
|
||||
#[panic_handler]
|
||||
fn panic(info: &PanicInfo) -> ! {
|
||||
blog_os::test_panic_handler(info)
|
||||
}
|
||||
```
|
||||
|
||||
这里我们使用我们的库中的`test_runner`函数,而不是重新实现一个test runner。至于panic处理,调用`blog_os::test_panic_handler`函数即可,就像我们之前在我们的`main.rs`里面做的一样。
|
||||
|
||||
现在,`cargo xtest`又可以正常退出了。当你运行该命令时,你会发现它为我们的`lib.rs`, `main.rs`, 和 `basic_boot.rs`分别构建并运行了测试。其中,对于 `main.rs` 和 `basic_boot`的集成测试,它会报告"Running 0 tests"(正在运行0个测试),因为这些文件里面没有任何用 `#[test_case]`标注的函数。
|
||||
|
||||
现在我们可以在`basic_boot.rs`中添加测试了。举个例子,我们可以测试`println`是否能够正常工作而不panic,就像我们之前在vga缓冲区测试中做的那样:
|
||||
|
||||
```rust
|
||||
// in tests/basic_boot.rs
|
||||
|
||||
use blog_os::{println, serial_print, serial_println};
|
||||
|
||||
#[test_case]
|
||||
fn test_println() {
|
||||
serial_print!("test_println... ");
|
||||
println!("test_println output");
|
||||
serial_println!("[ok]");
|
||||
}
|
||||
```
|
||||
|
||||
现在当我们运行`cargo xtest`时,我们可以看到它会寻找并执行这些测试函数。
|
||||
|
||||
由于该测试和vga缓冲区测试中的一个几乎完全相同,所以目前它看起来似乎没什么用。然而,在将来,我们的`main.rs`和`lib.rs`中的`_start`函数的内容会不断增长,并且在运行`test_main`之前需要调用一系列的初始化进程,所以这两个测试将会运行在完全不同的环境中(译者注:也就是说虽然现在看起来差不多,但是在将来该测试和vga buffer中的测试会很不一样,有必要单独拿出来,这两者并没有重复)。
|
||||
|
||||
通过在`basic_boot`环境里不掉用任何初始化例程的`_start`中测试`println`函数,我们可以确保`println`在启动(boot)后可以正常工作。这一点非常重要,因为我们有很多部分依赖于`println`,例如打印panic信息。
|
||||
|
||||
### 未来的测试
|
||||
|
||||
集成测试的强大之处在于,它们可以被看成是完全独立的可执行文件;这也给了它们完全控制环境的能力,使得他们能够测试代码和CPU或是其他硬件的交互是否正确。
|
||||
|
||||
我们的`basic_boot`测试正是集成测试的一个非常简单的例子。在将来,我们的内核的功能会变得更多,和硬件交互的方式也会变得多种多样。通过添加集成测试,我们可以保证这些交互按预期工作(并一直保持工作)。下面是一些对于未来的测试的设想:
|
||||
|
||||
- **CPU异常**:当代码执行无效操作(例如除以零)时,CPU就会抛出异常。内核会为这些异常注册处理函数。集成测试可以验证在CPU异常时是否调用了正确的异常处理程序,或者在可解析的异常之后程序是否能正确执行;
|
||||
- **页表**:页表定义了哪些内存区域是有效且可访问的。通过修改页表,可以重新分配新的内存区域,例如,当你启动一个软件的时候。我们可以在集成测试中调整`_start`函数中的一些页表项,并确认这些改动是否会对`#[test_case]`的函数产生影响;
|
||||
- **用户空间程序**:用户空间程序是只能访问有限的系统资源的程序。例如,他们无法访问内核数据结构或是其他应用程序的内存。集成测试可以启动执行禁止操作的用户空间程序验证认内核是否会将这些操作全都阻止。
|
||||
|
||||
可以想象,还有更多的测试可以进行。通过添加各种各样的测试,我们确保在为我们的内核添加新功能或是重构代码时,不会意外地破坏他们。这一点在我们的内核变得更大和更复杂的时候显得尤为重要。
|
||||
|
||||
### 那些应该Panic的测试
|
||||
|
||||
标准库的测试框架支持允许构造失败测试的[`#[should_panic]` attribute][should_panic]。这个功能对于验证传递无效参数时函数是否会失败非常有用。不幸的是,这个属性需要标准库的支持,因此,在`#[no_std]`环境下无法使用。
|
||||
|
||||
[should_panic]: https://doc.rust-lang.org/rust-by-example/testing/unit_testing.html#testing-panics
|
||||
|
||||
尽管我们不能在我们的内核中使用`#[should_panic]` 属性,但是通过创建一个集成测试我们可以达到类似的效果——该集成测试可以从panic处理程序中返回一个成功错误代码。接下来让我一起来创建一个如上所述名为`should_panic`的测试吧:
|
||||
|
||||
```rust
|
||||
// in tests/should_panic.rs
|
||||
|
||||
#![no_std]
|
||||
#![no_main]
|
||||
|
||||
use core::panic::PanicInfo;
|
||||
use blog_os::{QemuExitCode, exit_qemu, serial_println};
|
||||
|
||||
#[panic_handler]
|
||||
fn panic(_info: &PanicInfo) -> ! {
|
||||
serial_println!("[ok]");
|
||||
exit_qemu(QemuExitCode::Success);
|
||||
loop {}
|
||||
}
|
||||
```
|
||||
|
||||
这个测试还没有完成,因为它尚未定义`_start`函数或是其他自定义的test runner attributes。让我们来补充缺少的内容吧:
|
||||
|
||||
|
||||
```rust
|
||||
// in tests/should_panic.rs
|
||||
|
||||
#![feature(custom_test_frameworks)]
|
||||
#![test_runner(test_runner)]
|
||||
#![reexport_test_harness_main = "test_main"]
|
||||
|
||||
#[no_mangle]
|
||||
pub extern "C" fn _start() -> ! {
|
||||
test_main();
|
||||
|
||||
loop {}
|
||||
}
|
||||
|
||||
pub fn test_runner(tests: &[&dyn Fn()]) {
|
||||
serial_println!("Running {} tests", tests.len());
|
||||
for test in tests {
|
||||
test();
|
||||
serial_println!("[test did not panic]");
|
||||
exit_qemu(QemuExitCode::Failed);
|
||||
}
|
||||
exit_qemu(QemuExitCode::Success);
|
||||
}
|
||||
```
|
||||
|
||||
这个测试定义了自己的`test_runner`函数,而不是复用`lib.rs`中的`test_runner`,该函数会在测试没有panic而是正常退出时返回一个错误退出代码(因为这里我们希望测试会panic)。如果没有定义测试函数,runner就会以一个成功错误代码退出。由于这个runner总是在执行完单个的测试后就退出,因此定义超过一个`#[test_case]`的函数都是没有意义的。
|
||||
|
||||
现在我们来创建一个应该失败的测试:
|
||||
|
||||
```rust
|
||||
// in tests/should_panic.rs
|
||||
|
||||
use blog_os::serial_print;
|
||||
|
||||
#[test_case]
|
||||
fn should_fail() {
|
||||
serial_print!("should_fail... ");
|
||||
assert_eq!(0, 1);
|
||||
}
|
||||
```
|
||||
|
||||
该测试用 `assert_eq`来断言(assert)`0`和`1`是否相等。毫无疑问,这当然会失败(`0`当然不等于`1`),所以我们的测试就会像我们想要的那样panic。
|
||||
|
||||
当我们通过`cargo xtest --test should_panic`运行该测试时,我们会发现成功了因为该测试如我们预期的那样panic了。当我们将断言部分(即`assert_eq!(0, 1);`)注释掉后,我们就会发现测试失败并返回了_"test did not panic"_的信息。
|
||||
|
||||
这种方法的缺点是它只使用于单个的测试函数。对于多个`#[test_case]`函数,它只会执行第一个函数因为程序无法在panic处理被调用后继续执行。我目前没有想到解决这个问题的方法,如果你有任何想法,请务必告诉我!
|
||||
|
||||
### 无约束测试
|
||||
|
||||
对于那些只有单个测试函数的集成测试而言(例如我们的`should_panic`测试),其实并不需要test runner。对于这种情况,我们可以完全禁用test runner,直接在`_start`函数中直接运行我们的测试。
|
||||
|
||||
这里的关键就是在`Cargo.toml`中为测试禁用 `harness` flag,这个标志(flag)定义了是否将test runner用于集成测试中。如果该标志位被设置为`false`,那么默认的test runner和自定义的test runner功能都将被禁用,这样一来该测试就可以像一个普通的可执行程序一样运行了。
|
||||
|
||||
现在让我们为我们的`should_panic`测试禁用`harness` flag吧:
|
||||
|
||||
```toml
|
||||
# in Cargo.toml
|
||||
|
||||
[[test]]
|
||||
name = "should_panic"
|
||||
harness = false
|
||||
```
|
||||
|
||||
现在我们通过移除test runner相关的代码,大大简化了我们的`should_panic`测试。结果看起来如下:
|
||||
|
||||
```rust
|
||||
// in tests/should_panic.rs
|
||||
|
||||
#![no_std]
|
||||
#![no_main]
|
||||
|
||||
use core::panic::PanicInfo;
|
||||
use blog_os::{QemuExitCode, exit_qemu, serial_println};
|
||||
|
||||
#[no_mangle]
|
||||
pub extern "C" fn _start() -> ! {
|
||||
should_fail();
|
||||
serial_println!("[test did not panic]");
|
||||
exit_qemu(QemuExitCode::Failed);
|
||||
loop{}
|
||||
}
|
||||
|
||||
fn should_fail() {
|
||||
serial_print!("should_fail... ");
|
||||
assert_eq!(0, 1);
|
||||
}
|
||||
|
||||
#[panic_handler]
|
||||
fn panic(_info: &PanicInfo) -> ! {
|
||||
serial_println!("[ok]");
|
||||
exit_qemu(QemuExitCode::Success);
|
||||
loop {}
|
||||
}
|
||||
```
|
||||
|
||||
现在我们可以通过我们的`_start`函数来直接调用`should_fail`函数了,如果返回则返回一个失败退出代码并退出。现在当我们执行`cargo xtest --test should_panic`时,我们可以发现测试的行为和之前完全一样。
|
||||
|
||||
除了创建`should_panic`测试,禁用`harness` attribute对复杂集成测试也很有用,例如,当单个测试函数会产生一些边际效应需要通过特定的顺序执行时。
|
||||
|
||||
## 总结
|
||||
|
||||
测试是一种非常有用的技术,它能确保特定的部件拥有我们期望的行为。即使它们不能显示是否有bug,它们仍然是用来寻找bug的利器,尤其是用来避免回归。
|
||||
|
||||
本文讲述了如何为我们的Rust kernel创建一个测试框架。我们使用Rust的自定义框架功能为我们的裸机环境实现了一个简单的`#[test_case]` attribute支持。通过使用QEMU的`isa-debug-exit`设备,我们的test runner可以在运行测试后退出QEMU并报告测试状态。我们还为串行端口实现了一个简单的驱动,使得错误信息可以被打印到控制台而不是VGA buffer中。
|
||||
|
||||
在为我们的`println`宏创建了一些测试后,我们在本文的后半部分还探索了集成测试。我们了解到它们位于`tests`目录中,并被视为完全独立的可执行文件。为了使他们能够使用`exit_qemu` 函数和 `serial_println` 宏,我们将大部分代码移动到一个库里,使其能够被导入到所有可执行文件和集成测试中。由于集成测试在各自独立的环境中运行,所以能够测试与硬件的交互或是创建应该panic的测试。
|
||||
|
||||
我们现在有了一个在QEMU内部真是环境中运行的测试框架。在未来的文章里,我们会创建更多的测试,从而让我们的内核在变得更复杂的同时保持可维护性。
|
||||
|
||||
## 下期预告
|
||||
|
||||
在下一篇文章中,我们将会探索_CPU异常_。这些异常将在一些非法事件发生时由CPU抛出,例如抛出除以零或是访问没有映射的内存页(通常也被称为`page fault`即缺页异常)。能够捕获和检查这些异常,对将来的调试来说是非常重要的。异常处理与键盘支持所需的硬件中断处理十分相似。
|
||||
@@ -1,473 +0,0 @@
|
||||
+++
|
||||
title = "استثناهای پردازنده"
|
||||
weight = 5
|
||||
path = "fa/cpu-exceptions"
|
||||
date = 2018-06-17
|
||||
|
||||
[extra]
|
||||
chapter = "Interrupts"
|
||||
# Please update this when updating the translation
|
||||
translation_based_on_commit = "a081faf3cced9aeb0521052ba91b74a1c408dcff"
|
||||
# GitHub usernames of the people that translated this post
|
||||
translators = ["hamidrezakp", "MHBahrampour"]
|
||||
rtl = true
|
||||
+++
|
||||
|
||||
استثناهای پردازنده در موقعیت های مختلف دارای خطا رخ می دهد ، به عنوان مثال هنگام دسترسی به آدرس حافظه نامعتبر یا تقسیم بر صفر. برای واکنش به آنها ، باید یک _جدول توصیف کننده وقفه_ تنظیم کنیم که توابع کنترل کننده را فراهم کند. در انتهای این پست ، هسته ما قادر به گرفتن [استثناهای breakpoint] و ادامه اجرای طبیعی پس از آن خواهد بود.
|
||||
|
||||
[استثناهای breakpoint]: https://wiki.osdev.org/Exceptions#Breakpoint
|
||||
|
||||
<!-- more -->
|
||||
|
||||
این بلاگ بصورت آزاد روی [گیتهاب] توسعه داده شده است. اگر مشکل یا سوالی دارید، لطفاً آنجا یک ایشو باز کنید. همچنین میتوانید [در زیر] این پست کامنت بگذارید. سورس کد کامل این پست را میتوانید در بِرَنچ [`post-05`][post branch] پیدا کنید.
|
||||
|
||||
[گیتهاب]: https://github.com/phil-opp/blog_os
|
||||
[در زیر]: #comments
|
||||
<!-- fix for zola anchor checker (target is in template): <a id="comments"> -->
|
||||
[post branch]: https://github.com/phil-opp/blog_os/tree/post-05
|
||||
|
||||
<!-- toc -->
|
||||
|
||||
## بررسی اجمالی
|
||||
یک استثنا نشان می دهد که مشکلی در دستورالعمل فعلی وجود دارد. به عنوان مثال ، اگر دستورالعمل فعلی بخواهد تقسیم بر 0 کند ، پردازنده یک استثنا صادر می کند. وقتی یک استثنا اتفاق می افتد ، پردازنده کار فعلی خود را رها کرده و بسته به نوع استثنا ، بلافاصله یک تابع خاص کنترل کننده استثنا را فراخوانی می کند.
|
||||
|
||||
در x86 حدود 20 نوع مختلف استثنا پردازنده وجود دارد. مهمترین آنها در زیر آمده اند:
|
||||
|
||||
- **خطای صفحه**: خطای صفحه در دسترسی غیرقانونی به حافظه رخ می دهد. به عنوان مثال ، اگر دستورالعمل فعلی بخواهد از یک صفحه نگاشت نشده بخواند یا بخواهد در یک صفحه فقط خواندنی بنویسد.
|
||||
- **کد نامعتبر**: این استثنا وقتی رخ می دهد که دستورالعمل فعلی نامعتبر است ، به عنوان مثال وقتی می خواهیم از [دستورالعمل های SSE] جدیدتر بر روی یک پردازنده قدیمی استفاده کنیم که آنها را پشتیبانی نمی کند.
|
||||
- **خطای محافظت عمومی**: این استثنا دارای بیشترین دامنه علل است. این مورد در انواع مختلف نقض دسترسی مانند تلاش برای اجرای یک دستورالعمل ممتاز در کد سطح کاربر یا نوشتن فیلدهای رزرو شده در ثبات های پیکربندی رخ می دهد.
|
||||
- **خطای دوگانه**: هنگامی که یک استثنا رخ می دهد ، پردازنده سعی می کند تابع کنترل کننده مربوطه را اجرا کند. اگر یک استثنا دیگر رخ دهد _هنگام فراخوانی تابع کنترل کننده استثنا_ ، پردازنده یک استثنای خطای دوگانه ایجاد می کند. این استثنا همچنین زمانی اتفاق می افتد که هیچ تابع کنترل کننده ای برای یک استثنا ثبت نشده باشد.
|
||||
- **خطای سهگانه**: اگر در حالی که پردازنده سعی می کند تابع کنترل کننده خطای دوگانه را فراخوانی کند استثنایی رخ دهد ، این یک خطای سهگانه است. ما نمی توانیم یک خطای سه گانه را بگیریم یا آن را کنترل کنیم. بیشتر پردازنده ها ریست کردن خود و راه اندازی مجدد سیستم عامل واکنش نشان می دهند.
|
||||
|
||||
[دستورالعمل های SSE]: https://en.wikipedia.org/wiki/Streaming_SIMD_Extensions
|
||||
|
||||
برای مشاهده لیست کامل استثناها ، [ویکی OSDev][exceptions] را بررسی کنید.
|
||||
|
||||
[exceptions]: https://wiki.osdev.org/Exceptions
|
||||
|
||||
### جدول توصیف کننده وقفه
|
||||
برای گرفتن و رسیدگی به استثناها ، باید اصطلاحاً _جدول توصیفگر وقفه_ (IDT) را تنظیم کنیم. در این جدول می توانیم برای هر استثنا پردازنده یک عملکرد تابع کننده مشخص کنیم. سخت افزار به طور مستقیم از این جدول استفاده می کند ، بنابراین باید از یک قالب از پیش تعریف شده پیروی کنیم. هر ورودی جدول باید ساختار 16 بایتی زیر را داشته باشد:
|
||||
|
||||
Type| Name | Description
|
||||
----|--------------------------|-----------------------------------
|
||||
u16 | Function Pointer [0:15] | The lower bits of the pointer to the handler function.
|
||||
u16 | GDT selector | Selector of a code segment in the [global descriptor table].
|
||||
u16 | Options | (see below)
|
||||
u16 | Function Pointer [16:31] | The middle bits of the pointer to the handler function.
|
||||
u32 | Function Pointer [32:63] | The remaining bits of the pointer to the handler function.
|
||||
u32 | Reserved |
|
||||
|
||||
[global descriptor table]: https://en.wikipedia.org/wiki/Global_Descriptor_Table
|
||||
|
||||
قسمت گزینه ها (Options) دارای قالب زیر است:
|
||||
|
||||
Bits | Name | Description
|
||||
------|-----------------------------------|-----------------------------------
|
||||
0-2 | Interrupt Stack Table Index | 0: Don't switch stacks, 1-7: Switch to the n-th stack in the Interrupt Stack Table when this handler is called.
|
||||
3-7 | Reserved |
|
||||
8 | 0: Interrupt Gate, 1: Trap Gate | If this bit is 0, interrupts are disabled when this handler is called.
|
||||
9-11 | must be one |
|
||||
12 | must be zero |
|
||||
13‑14 | Descriptor Privilege Level (DPL) | The minimal privilege level required for calling this handler.
|
||||
15 | Present |
|
||||
|
||||
هر استثنا دارای یک اندیس از پیش تعریف شده در IDT است. به عنوان مثال استثنا کد نامعتبر دارای اندیس 6 و استثنا خطای صفحه دارای اندیس 14 است. بنابراین ، سخت افزار می تواند به طور خودکار عنصر مربوطه را برای هر استثنا بارگذاری کند. [جدول استثناها][exceptions] در ویکی OSDev ، اندیس های IDT کلیه استثناها را در ستون “Vector nr.” نشان داده است.
|
||||
|
||||
هنگامی که یک استثنا رخ می دهد ، پردازنده تقریباً موارد زیر را انجام می دهد:
|
||||
|
||||
1. برخی از ثباتها را به پشته وارد میکند، از جمله اشاره گر دستورالعمل و ثبات [RFLAGS]. (بعداً در این پست از این مقادیر استفاده خواهیم کرد.)
|
||||
2. عنصر مربوط به آن (استثنا) را از جدول توصیف کننده وقفه (IDT) میخواند. به عنوان مثال ، پردازنده هنگام رخ دادن خطای صفحه ، عنصر چهاردهم را می خواند.
|
||||
3. وجود عنصر را بررسی میکند. اگر اینگونه نباشد یک خطای دوگانه ایجاد میکند.
|
||||
4. اگر عنصر یک گیت وقفه است (بیت 40 تنظیم نشده است) وقفه های سخت افزاری را غیرفعال میکند.
|
||||
5. انتخابگر مشخص شده [GDT] را در سگمنت CS بارگذاری میکند.
|
||||
6. به تابع کنترل کننده مشخص شده میرود.
|
||||
|
||||
[RFLAGS]: https://en.wikipedia.org/wiki/FLAGS_register
|
||||
[GDT]: https://en.wikipedia.org/wiki/Global_Descriptor_Table
|
||||
|
||||
در حال حاضر نگران مراحل 4 و 5 نباشید ، ما در مورد جدول توصیف کننده گلوبال و وقفه های سخت افزاری در پست های بعدی خواهیم آموخت.
|
||||
|
||||
## یک نوع IDT
|
||||
|
||||
به جای ایجاد نوع IDT خود ، از [ساختمان `InterruptDescriptorTable`] کرت `x86_64` استفاده خواهیم کرد که به این شکل است:
|
||||
|
||||
[ساختمان `InterruptDescriptorTable`]: https://docs.rs/x86_64/0.14.2/x86_64/structures/idt/struct.InterruptDescriptorTable.html
|
||||
|
||||
``` rust
|
||||
#[repr(C)]
|
||||
pub struct InterruptDescriptorTable {
|
||||
pub divide_by_zero: Entry<HandlerFunc>,
|
||||
pub debug: Entry<HandlerFunc>,
|
||||
pub non_maskable_interrupt: Entry<HandlerFunc>,
|
||||
pub breakpoint: Entry<HandlerFunc>,
|
||||
pub overflow: Entry<HandlerFunc>,
|
||||
pub bound_range_exceeded: Entry<HandlerFunc>,
|
||||
pub invalid_opcode: Entry<HandlerFunc>,
|
||||
pub device_not_available: Entry<HandlerFunc>,
|
||||
pub double_fault: Entry<HandlerFuncWithErrCode>,
|
||||
pub invalid_tss: Entry<HandlerFuncWithErrCode>,
|
||||
pub segment_not_present: Entry<HandlerFuncWithErrCode>,
|
||||
pub stack_segment_fault: Entry<HandlerFuncWithErrCode>,
|
||||
pub general_protection_fault: Entry<HandlerFuncWithErrCode>,
|
||||
pub page_fault: Entry<PageFaultHandlerFunc>,
|
||||
pub x87_floating_point: Entry<HandlerFunc>,
|
||||
pub alignment_check: Entry<HandlerFuncWithErrCode>,
|
||||
pub machine_check: Entry<HandlerFunc>,
|
||||
pub simd_floating_point: Entry<HandlerFunc>,
|
||||
pub virtualization: Entry<HandlerFunc>,
|
||||
pub security_exception: Entry<HandlerFuncWithErrCode>,
|
||||
// some fields omitted
|
||||
}
|
||||
```
|
||||
|
||||
فیلدها از نوع [`<idt::Entry<F`] هستند ، این ساختمانی است که فیلد های یک عنصر IDT را نشان می دهد (به جدول بالا مراجعه کنید). پارامتر نوع `F`، نوع تابع کنترل کننده مورد انتظار را تعریف می کند. می بینیم که برخی از عناصر به یک [`HandlerFunc`] و برخی دیگر به [`HandlerFuncWithErrCode`] نیاز دارند. خطای صفحه حتی نوع خاص خود را دارد: [`PageFaultHandlerFunc`].
|
||||
|
||||
[`<idt::Entry<F`]: https://docs.rs/x86_64/0.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);
|
||||
```
|
||||
|
||||
این یک [نوع مستعار(type alias)] برای نوع "`extern "x86-interrupt" fn` است. کلمه کلیدی `extern` تابعی را با یک [قرارداد فراخوانی خارجی] تعریف می کند و اغلب برای برقراری ارتباط با کد C استفاده می شود(`extern "C" fn`) . اما قرارداد فراخوانی `x86-interrupt` چیست؟
|
||||
|
||||
[نوع مستعار(type alias)]: https://doc.rust-lang.org/book/ch19-04-advanced-types.html#creating-type-synonyms-with-type-aliases
|
||||
[قرارداد فراخوانی خارجی]: https://doc.rust-lang.org/nomicon/ffi.html#foreign-calling-conventions
|
||||
|
||||
## قرارداد فراخوانی وقفه
|
||||
استثناها کاملاً شبیه فراخوانی توابع هستند: پردازنده به اولین دستورالعمل تابع فراخوانی شده می رود و آن را اجرا می کند. پس از آن پردازنده به آدرس بازگشت می پرد و اجرای تابع اصلی را ادامه می دهد.
|
||||
|
||||
با این وجود ، تفاوت عمده ای بین فراخوانی استثناها و توابع وجود دارد: یک فراخوانی تابع توسط یک کامپایلر که دستور "فراخوانی" در آن درج شده است ، انجام می شود ، در حالی که یک استثنا ممکن است در _هر_ دستورالعملی رخ دهد. برای درک عواقب این تفاوت ، باید فراخوانی توابع را با جزئیات بیشتری بررسی کنیم.
|
||||
|
||||
[قرارداد فراخوانی] جزئیات فراخوانی تابع را مشخص می کند. به عنوان مثال ، آنها مشخص میکنند که پارامترهای تابع کجا قرار می گیرند (به عنوان مثال در ثباتها یا بر روی پشته) و نحوه بازگشت نتایج. در x86_64 لینوکس ، قوانین زیر برای توابع C اعمال می شود (مشخص شده در [System V ABI]):
|
||||
|
||||
[قرارداد فراخوانی]: https://en.wikipedia.org/wiki/Calling_convention
|
||||
[System V ABI]: https://refspecs.linuxbase.org/elf/x86_64-abi-0.99.pdf
|
||||
|
||||
- شش آرگومان اول با نوع عدد صحیح در ثباتهای`rdi`, `rsi`, `rdx`, `rcx`, `r8`, `r9` منتقل می شوند
|
||||
- آرگومان های اضافی بر روی پشته منتقل می شوند
|
||||
- نتایج درون`rax` و `rdx` بر می گردند
|
||||
|
||||
توجه داشته باشید که راست از C ABI پیروی نمی کند (در واقع ، [هنوز حتی یک Rust ABI وجود ندارد][rust abi]) ، بنابراین این قوانین فقط برای توابع اعلام شده به عنوان `extern "C" fn` اعمال می شود.
|
||||
|
||||
[rust abi]: https://github.com/rust-lang/rfcs/issues/600
|
||||
|
||||
### ثبات های حفظ شده و تغییرشونده (Scratch)
|
||||
قرارداد فراخوانی، ثباتها را به دو دسته ثبات های _محفوظ شده_ و _تغییرشونده_ تقسیم می کند.
|
||||
|
||||
مقادیر ثباتهای _محفوظ شده_، در فراخوانی تابع باید بدون تغییر باقی بمانند. بنابراین یک تابع فراخوانی شده (_“callee”_) فقط در صورتی مجاز است این ثباتها را تغییر دهد، که مقادیر اصلی آنها را قبل از بازگشت، برگرداند. بنابراین به این ثباتها _"callee-saved"_ گفته می شود. یک الگوی عمومی این است که ثباتها در آغاز تابع بر روی پشته ذخیره شده و درست قبل از بازگشت از پشته برداشته شده و مقدار دهی شوند.
|
||||
|
||||
در مقابل، یک تابع فراخوانی شده مجاز است که بدون محدودیت ، ثباتهای _تغییرشونده_ را دوباره بنویسد. اگر فراخواننده (_"caller"_) بخواهد مقدار یک ثبات تغییرشونده را در یک فراخوانی تابع حفظ کند ، لازم است قبل از فراخوانی تابع (به عنوان مثال بوسیله اضافه و برداشتن از روی پشته) آن را پشتیبان گیری و بازیابی کند. بنابراین ثباتهای تغییرشونده _caller-saved_ هستند.
|
||||
|
||||
در x86_64 ، قرارداد فراخوانی C ثباتهای محفوظ شده و تغییرشونده زیر را مشخص می کند:
|
||||
|
||||
preserved registers | scratch registers
|
||||
---|---
|
||||
`rbp`, `rbx`, `rsp`, `r12`, `r13`, `r14`, `r15` | `rax`, `rcx`, `rdx`, `rsi`, `rdi`, `r8`, `r9`, `r10`, `r11`
|
||||
_callee-saved_ | _caller-saved_
|
||||
|
||||
کامپایلر این قوانین را می داند ، بنابراین کد را متناسب با آن تولید می کند. به عنوان مثال ، بیشتر توابع با `push rbp` شروع می شوند که پشتیبان گیری از`rbp` روی پشته است (زیرا این یک ثبات _caller-saved_).
|
||||
|
||||
### حفظ کلیه ثباتها
|
||||
برخلاف فراخوانی تابع ، استثناها می توانند در _هر_ دستورالعملی رخ دهند. در بیشتر موارد ، ما حتی در زمان کامپایل نمی دانیم که کد تولید شده استثنا ایجاد می کند یا نه. به عنوان مثال ، کامپایلر نمی تواند بداند که آیا یک دستورالعمل باعث سرریز شدن پشته یا خطای صفحه می شود.
|
||||
|
||||
از آنجا که نمی دانیم چه زمانی استثنا رخ میدهد ، نمی توانیم قبل از آن از هیچ ثباتی پشتیبان گیری کنیم. این بدان معناست که ما نمی توانیم از قرارداد فراخوانیای استفاده کنیم که متکی به ثباتهای caller-saved برای کنترل کننده های استثنا هست. در عوض ، به یک قرارداد فراخوانی نیاز داریم که _همه_ ثباتها را حفظ کند. قرارداد فراخوانی `x86-interrupt` چنین قرارداد فراخوانی است ، بنابراین تضمین می کند که تمام مقادیر ثباتها در هنگام بازگشت تابع به مقادیر اصلی خود بازگردند.
|
||||
|
||||
توجه داشته باشید که این بدان معنا نیست که همه ثباتها در ورود به تابع در پشته ذخیره می شوند. در عوض ، کامپایلر فقط از ثباتهایی که توسط تابع تغییر میکنند ، پشتیبان تهیه می کند. به این ترتیب ، کد بسیار کارآمدی برای توابع کوتاه که فقط از چند ثبات استفاده می کنند ، تولید می شود.
|
||||
|
||||
### قاب پشته وقفه (The Interrupt Stack Frame)
|
||||
در یک فراخوانی عادی تابع (با استفاده از دستورالعمل `call`) ، پردازنده قبل از پرش به تابع هدف ، آدرس بازگشت را در پشته ذخیره میکند. در هنگام بازگشت تابع (با استفاده از دستورالعمل `ret`) ، پردازنده این آدرس بازگشت را از پشته برمیدارد و به آن می پرد. بنابراین قاب پشته یک فراخوانی عادی تابع به این شکل است:
|
||||
|
||||

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

|
||||
|
||||
در کرت `x86_64` ، فریم پشته وقفه توسط ساختمان [`InterruptStackFrame`] نشان داده می شود. این ساختمان به عنوان `&mut` به کنترل کننده وقفه منتقل می شود و می تواند برای دریافت اطلاعات بیشتر در مورد علت استثنا استفاده شود. ساختمان بدون فیلد کد خطا است ، زیرا فقط برخی از استثناها کد خطا را پوش میکنند. این استثناها از نوع تابع جداگانه [`HandlerFuncWithErrCode`] استفاده میکنند ، که دارای یک آرگومان اضافی `error_code` است.
|
||||
|
||||
[`InterruptStackFrame`]: https://docs.rs/x86_64/0.14.2/x86_64/structures/idt/struct.InterruptStackFrame.html
|
||||
|
||||
### پشت صحنه
|
||||
قرارداد فراخوانی `x86-interrupt` یک انتزاع قدرتمند است که تقریباً تمام جزئیات پیچیده فرآیند مدیریت استثناها را پنهان می کند. با این حال ، گاهی اوقات مفید است که بدانیم پشت پرده چه اتفاقی می افتد. در اینجا یک مرور کوتاه از مواردی که قرارداد فراخوانی `x86-interrupt` انجام میدهد را میبینید:
|
||||
|
||||
- **دریافت آرگومان ها**: بیشتر قرارداد های فراخوانی انتظار دارند که آرگومان ها در ثباتها منتقل شوند. این برای کنترل کننده های استثنا امکان پذیر نیست ، زیرا ما نباید قبل از تهیه نسخه پشتیبان از مقادیر ثباتها ، آنها را بازنویسی کنیم. در عوض، قرارداد فراخوانی `x86-interrupt` آگاه است که آرگومان ها از قبل در مکان خاصی بر روی پشته قرار دارند.
|
||||
- **بازگشت با استفاده از `iretq`**: از آنجا که قاب پشته وقفه با قاب پشته صدا زدن توابع معمولی کاملاً متفاوت است، نمی توانیم از طریق دستورالعمل` ret` از توابع کنترل کننده برگردیم. در عوض، باید از دستور `iretq` استفاده شود.
|
||||
- **مدیریت کد خطا**: کد خطا که برای برخی استثناها به پشته اضافه می شود ، کارها را بسیار پیچیده تر می کند. تراز بندی پشته را تغییر می دهد (به قسمت بعدی مراجعه کنید) و باید قبل از بازگشت، از پشته خارج شود. قرارداد فراخوانی `x86-interrupt` تمام پیچیدگیها را برطرف می کند. با این حال، نمی داند کدام تابع کنترل کننده برای کدام استثنا استفاده می شود، بنابراین باید این اطلاعات را از تعداد آرگومان های تابع استخراج کند. این بدان معناست که برنامه نویس همچنان مسئول استفاده صحیح هر نوع تابع برای هر استثنا است. خوشبختانه نوع `InterruptDescriptorTable` که توسط کرت `x86_64` تعریف شده است، استفاده از انواع تابع صحیح را تضمین می کند.
|
||||
- **تراز کردن پشته**: برخی دستورالعملها (به ویژه دستورالعمل های SSE) وجود دارند که به یک تراز پشته 16 بایتی نیاز دارند. پردازنده این تراز را هر زمان که یک استثنا اتفاق می افتد تضمین می کند ، اما برای برخی از استثناها بعداً هنگامی که یک کد خطا را به پشته اضافه میکند، دوباره آن را از بین می برد. قرارداد فراخوانی `x86-interrupt` با تنظیم مجدد پشته در این حالت این مشکل را برطرف میکند.
|
||||
|
||||
اگر به جزئیات بیشتر علاقه مندید: ما همچنین یک سری پست داریم که مدیریت استثنا با استفاده از [توابع برهنه] را توضیح میدهند. ([در انتهای این پست][too-much-magic]).
|
||||
|
||||
[توابع برهنه]: https://github.com/rust-lang/rfcs/blob/master/text/1201-naked-fns.md
|
||||
[too-much-magic]: #khyly-jdwyy-bwd
|
||||
|
||||
## پیاده سازی
|
||||
اکنون که تئوری را فهمیدیم ، وقت آن رسیده است که استثناهای پردازنده را در هسته خود کنترل کنیم. ما با ایجاد یک ماژول جدید وقفهها در `src/interrupts.rs` شروع خواهیم کرد، که ابتدا یک تابع `init_idt` ایجاد می کند که یک `InterruptDescriptorTable` جدید ایجاد می کند:
|
||||
|
||||
``` rust
|
||||
// in src/lib.rs
|
||||
|
||||
pub mod interrupts;
|
||||
|
||||
// in src/interrupts.rs
|
||||
|
||||
use x86_64::structures::idt::InterruptDescriptorTable;
|
||||
|
||||
pub fn init_idt() {
|
||||
let mut idt = InterruptDescriptorTable::new();
|
||||
}
|
||||
```
|
||||
|
||||
اکنون می توانیم توابع کنترل کننده را اضافه کنیم. با اضافه کردن یک کنترل کننده برای [استثنا بریکپوینت] شروع می کنیم. استثنای بریکپوینت، استثنای عالی برای آزمایش مدیریت استثنا است. تنها هدف آن مکث موقت برنامه هنگام اجرای دستورالعمل بریکپوینت `int3` است.
|
||||
|
||||
[استثنا بریکپوینت]: https://wiki.osdev.org/Exceptions#Breakpoint
|
||||
|
||||
استثنا بریکپوینت معمولاً در دیباگرها به کار می رود: وقتی کاربر بریکپوینت را تعیین می کند ، دیباگر دستورالعمل مربوطه را با دستورالعمل `int3` بازنویسی می کند تا پردازنده هنگام رسیدن به آن خط، استثنای بریکپوینت را ایجاد کند. هنگامی که کاربر می خواهد برنامه را ادامه دهد، دیباگر دوباره دستورالعمل `int3` را با دستورالعمل اصلی جایگزین می کند و برنامه را ادامه می دهد. برای جزئیات بیشتر ، سری ["_دیباگرها چطور کار میکنند_"] را ببینید.
|
||||
|
||||
["_دیباگرها چطور کار میکنند_"]: https://eli.thegreenplace.net/2011/01/27/how-debuggers-work-part-2-breakpoints
|
||||
|
||||
برای موارد استفاده ما، نیازی به بازنویسی دستورالعمل نداریم. در عوض، فقط میخواهیم هنگام اجرای دستورالعمل بریکپوینت پیامی چاپ کنیم و سپس برنامه را ادامه دهیم. بنابراین بیایید یک تابع ساده `breakpoint_handler` ایجاد کنیم و آن را به IDT خود اضافه کنیم:
|
||||
|
||||
```rust
|
||||
// in src/interrupts.rs
|
||||
|
||||
use x86_64::structures::idt::{InterruptDescriptorTable, InterruptStackFrame};
|
||||
use crate::println;
|
||||
|
||||
pub fn init_idt() {
|
||||
let mut idt = InterruptDescriptorTable::new();
|
||||
idt.breakpoint.set_handler_fn(breakpoint_handler);
|
||||
}
|
||||
|
||||
extern "x86-interrupt" fn breakpoint_handler(
|
||||
stack_frame: 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` هنوز ناپایدار است. به هر حال برای استفاده از آن ، باید صریحاً آن را با اضافه کردن `#![feature(abi_x86_interrupt)]` در بالای `lib.rs` فعال کنیم.
|
||||
|
||||
### بارگیری IDT
|
||||
برای اینکه پردازنده از جدول توصیف کننده وقفه جدید ما استفاده کند ، باید آن را با استفاده از دستورالعمل [`lidt`] بارگیری کنیم. ساختمان `InterruptDescriptorTable` از کرت ` x86_64` متد [`load`][InterruptDescriptorTable::load] را برای این کار فراهم می کند. بیایید سعی کنیم از آن استفاده کنیم:
|
||||
|
||||
[`lidt`]: https://www.felixcloutier.com/x86/lgdt:lidt
|
||||
[InterruptDescriptorTable::load]: https://docs.rs/x86_64/0.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` انتظار دریافت یک `static self'&` را دارد، این مرجعی است که برای تمام مدت زمان اجرای برنامه معتبر است. دلیل این امر این است که پردازنده در هر وقفه به این جدول دسترسی پیدا می کند تا زمانی که IDT دیگری بارگیری کنیم. بنابراین استفاده از طول عمر کوتاه تر از `static'` می تواند منجر به باگ های استفاده-بعد-از-آزادسازی شود.
|
||||
|
||||
در واقع ، این دقیقاً همان چیزی است که در اینجا اتفاق می افتد. `idt` ما روی پشته ایجاد می شود ، بنابراین فقط در داخل تابع `init` معتبر است. پس از آن حافظه پشته برای توابع دیگر مورد استفاده مجدد قرار می گیرد ، بنابراین پردازنده حافظه پشته تصادفی را به عنوان IDT تفسیر می کند. خوشبختانه ، متد `InterruptDescriptorTable::load` این نیاز به طول عمر را در تعریف تابع خود اجباری می کند، بنابراین کامپایلر راست قادر است از این مشکل احتمالی در زمان کامپایل جلوگیری کند.
|
||||
|
||||
برای رفع این مشکل، باید `idt` را در مکانی ذخیره کنیم که طول عمر `static'` داشته باشد. برای رسیدن به این هدف می توانیم IDT را با استفاده از [`Box`] بر روی حافظه Heap ایجاد کنیم و سپس آن را به یک مرجع `static'` تبدیل کنیم، اما ما در حال نوشتن هسته سیستم عامل هستیم و بنابراین هنوز Heap نداریم.
|
||||
|
||||
[`Box`]: https://doc.rust-lang.org/std/boxed/struct.Box.html
|
||||
|
||||
|
||||
به عنوان یک گزینه دیگر، می توانیم IDT را به صورت `static` ذخیره کنیم:
|
||||
```rust
|
||||
static IDT: InterruptDescriptorTable = InterruptDescriptorTable::new();
|
||||
|
||||
pub fn init_idt() {
|
||||
IDT.breakpoint.set_handler_fn(breakpoint_handler);
|
||||
IDT.load();
|
||||
}
|
||||
```
|
||||
|
||||
با این وجود، یک مشکل وجود دارد: استاتیکها تغییرناپذیر هستند، پس نمی توانیم ورودی بریکپوینت را از تابع `init` تغییر دهیم. می توانیم این مشکل را با استفاده از [`static mut`] حل کنیم:
|
||||
|
||||
[`static mut`]: https://doc.rust-lang.org/1.30.0/book/second-edition/ch19-01-unsafe-rust.html#accessing-or-modifying-a-mutable-static-variable
|
||||
|
||||
```rust
|
||||
static mut IDT: InterruptDescriptorTable = InterruptDescriptorTable::new();
|
||||
|
||||
pub fn init_idt() {
|
||||
unsafe {
|
||||
IDT.breakpoint.set_handler_fn(breakpoint_handler);
|
||||
IDT.load();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
در این روش بدون خطا کامپایل می شود اما مشکلات دیگری به همراه دارد. `static mut` بسیار مستعد Data Race هستند، بنابراین در هر دسترسی به یک [بلوک `unsafe`] نیاز داریم.
|
||||
|
||||
[بلوک `unsafe`]: https://doc.rust-lang.org/1.30.0/book/second-edition/ch19-01-unsafe-rust.html#unsafe-superpowers
|
||||
|
||||
#### Lazy Statics به نجات ما میآیند
|
||||
خوشبختانه ماکرو `lazy_static` وجود دارد. ماکرو به جای ارزیابی یک `static` در زمان کامپایل ، مقداردهی اولیه آن را هنگام اولین ارجاع به آن انجام می دهد. بنابراین، می توانیم تقریباً همه کاری را در بلوک مقداردهی اولیه انجام دهیم و حتی قادر به خواندن مقادیر زمان اجرا هستیم.
|
||||
|
||||
ما قبلاً کرت `lazy_static` را وارد کردیم وقتی [یک انتزاع برای بافر متن VGA ایجاد کردیم][vga text buffer lazy static]. بنابراین می توانیم مستقیماً از ماکرو `!lazy_static` برای ایجاد IDT استاتیک استفاده کنیم:
|
||||
|
||||
[vga text buffer lazy static]: @/edition-2/posts/03-vga-text-buffer/index.md#lazy-statics
|
||||
|
||||
```rust
|
||||
// in src/interrupts.rs
|
||||
|
||||
use lazy_static::lazy_static;
|
||||
|
||||
lazy_static! {
|
||||
static ref IDT: InterruptDescriptorTable = {
|
||||
let mut idt = InterruptDescriptorTable::new();
|
||||
idt.breakpoint.set_handler_fn(breakpoint_handler);
|
||||
idt
|
||||
};
|
||||
}
|
||||
|
||||
pub fn init_idt() {
|
||||
IDT.load();
|
||||
}
|
||||
```
|
||||
|
||||
توجه داشته باشید که چگونه این راه حل به هیچ بلوک `unsafe` نیاز ندارد. ماکرو `!lazy_static` از `unsafe` در پشت صحنه استفاده می کند ، اما در یک رابط امن به ما داده می شود.
|
||||
|
||||
### اجرای آن
|
||||
|
||||
آخرین مرحله برای کارکرد استثناها در هسته ما فراخوانی تابع `init_idt` از `main.rs` است. به جای فراخوانی مستقیم آن، یک تابع عمومی `init` را در `lib.rs` معرفی می کنیم:
|
||||
|
||||
```rust
|
||||
// in src/lib.rs
|
||||
|
||||
pub fn init() {
|
||||
interrupts::init_idt();
|
||||
}
|
||||
```
|
||||
|
||||
با استفاده از این تابع اکنون یک مکان اصلی برای روالهای اولیه داریم که می تواند بین توابع مختلف `start_` در `main.rs` ، `lib.rs` و تستهای یکپارچه به اشتراک گذاشته شود.
|
||||
|
||||
اکنون می توانیم تابع `start_` در `main.rs` را به روز کنیم تا `init` را فراخوانی کرده و سپس یک استثنا بریکپوینت ایجاد کند:
|
||||
|
||||
```rust
|
||||
// in src/main.rs
|
||||
|
||||
#[no_mangle]
|
||||
pub extern "C" fn _start() -> ! {
|
||||
println!("Hello World{}", "!");
|
||||
|
||||
blog_os::init(); // new
|
||||
|
||||
// invoke a breakpoint exception
|
||||
x86_64::instructions::interrupts::int3(); // new
|
||||
|
||||
// as before
|
||||
#[cfg(test)]
|
||||
test_main();
|
||||
|
||||
println!("It did not crash!");
|
||||
loop {}
|
||||
}
|
||||
```
|
||||
|
||||
اکنون هنگامی که آن را در QEMU اجرا می کنیم (با استفاده از `cargo run`) ، موارد زیر را مشاهده می کنیم:
|
||||
|
||||

|
||||
|
||||
کار می کند! پردازنده با موفقیت تابع کنترل کننده بریکپوینت ما را فراخوانی می کند ، که پیام را چاپ می کند و سپس به تابع `start_` برمی گردد ، جایی که پیام `!It did not crash` چاپ شده است.
|
||||
|
||||
می بینیم که قاب پشته وقفه، دستورالعمل و نشانگرهای پشته را در زمان وقوع استثنا به ما می گوید. این اطلاعات هنگام رفع اشکال استثناهای غیر منتظره بسیار مفید است.
|
||||
|
||||
### افزودن یک تست
|
||||
|
||||
بیایید یک تست ایجاد کنیم که از ادامه کار کد بالا اطمینان حاصل کند. ابتدا تابع `start_` را به روز می کنیم تا `init` را نیز فراخوانی کند:
|
||||
|
||||
```rust
|
||||
// in src/lib.rs
|
||||
|
||||
/// Entry point for `cargo test`
|
||||
#[cfg(test)]
|
||||
#[no_mangle]
|
||||
pub extern "C" fn _start() -> ! {
|
||||
init(); // new
|
||||
test_main();
|
||||
loop {}
|
||||
}
|
||||
```
|
||||
|
||||
بخاطر داشته باشید، این تابع `start_` هنگام اجرای`cargo test --lib` استفاده می شود، زیرا راست `lib.rs` را کاملاً مستقل از`main.rs` تست میکند. قبل از اجرای تستها باید برای راه اندازی IDT در اینجا `init` فراخوانی شود.
|
||||
|
||||
اکنون می توانیم یک تست `test_breakpoint_exception` ایجاد کنیم:
|
||||
|
||||
```rust
|
||||
// in src/interrupts.rs
|
||||
|
||||
#[test_case]
|
||||
fn test_breakpoint_exception() {
|
||||
// invoke a breakpoint exception
|
||||
x86_64::instructions::interrupts::int3();
|
||||
}
|
||||
```
|
||||
|
||||
این تست تابع `int3` را فراخوانی می کند تا یک استثنا بریکپوینت ایجاد کند. با بررسی اینکه اجرا پس از آن ادامه دارد ، تأیید می کنیم که کنترل کننده بریکپوینت ما به درستی کار می کند.
|
||||
|
||||
شما می توانید این تست جدید را با اجرای `cargo test` (همه تستها) یا` cargo test --lib` (فقط تست های `lib.rs` و ماژول های آن) امتحان کنید. باید موارد زیر را در خروجی مشاهده کنید:
|
||||
|
||||
```
|
||||
blog_os::interrupts::test_breakpoint_exception... [ok]
|
||||
```
|
||||
|
||||
## خیلی جادویی بود؟
|
||||
قرارداد فراخوانی `x86-interrupt` و نوع [`InterruptDescriptorTable`] روند مدیریت استثناها را نسبتاً سر راست و بدون درد ساختهاند. اگر این برای شما بسیار جادویی بود و دوست دارید تمام جزئیات مهم مدیریت استثنا را بیاموزید، برای شما هم مطالبی داریم: مجموعه ["مدیریت استثناها با توابع برهنه"] ما، نحوه مدیریت استثناها بدون قرارداد فراخوانی`x86-interrupt` را نشان می دهد و همچنین نوع IDT خاص خود را ایجاد می کند. از نظر تاریخی، این پستها مهمترین پستهای مدیریت استثناها قبل از وجود قرارداد فراخوانی `x86-interrupt` و کرت `x86_64` بودند. توجه داشته باشید که این پستها بر اساس [نسخه اول] این وبلاگ هستند و ممکن است قدیمی باشند.
|
||||
|
||||
["مدیریت استثناها با توابع برهنه"]: @/edition-1/extra/naked-exceptions/_index.md
|
||||
[`InterruptDescriptorTable`]: https://docs.rs/x86_64/0.14.2/x86_64/structures/idt/struct.InterruptDescriptorTable.html
|
||||
[نسخه اول]: @/edition-1/_index.md
|
||||
|
||||
## مرحله بعدی چیست؟
|
||||
ما اولین استثنای خود را با موفقیت گرفتیم و از آن بازگشتیم! گام بعدی اطمینان از این است که همه استثناها را می گیریم ، زیرا یک استثنا گرفته نشده باعث [خطای سهگانه] می شود که منجر به شروع مجدد سیستم می شود. پست بعدی توضیح می دهد که چگونه می توان با گرفتن صحیح [خطای دوگانه] از این امر جلوگیری کرد.
|
||||
|
||||
[خطای سهگانه]: https://wiki.osdev.org/Triple_Fault
|
||||
[خطای دوگانه]: https://wiki.osdev.org/Double_Fault#Double_Fault
|
||||
@@ -1,471 +0,0 @@
|
||||
+++
|
||||
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例外は、例えば無効なメモリアドレスにアクセスしたときやゼロ除算したときなど、様々なミスによって発生します。それらに対処するために、ハンドラ関数を提供する **<ruby>割り込み記述子表<rp> (</rp><rt>interrupt descriptor table</rt><rp>) </rp></ruby>** を設定しなくてはなりません。この記事を読み終わる頃には、私達のカーネルは[ブレークポイント例外][breakpoint exceptions]を捕捉し、その後通常の実行を継続できるようになっているでしょう。
|
||||
|
||||
[breakpoint exceptions]: https://wiki.osdev.org/Exceptions#Breakpoint
|
||||
|
||||
<!-- more -->
|
||||
|
||||
このブログの内容は [GitHub] 上で公開・開発されています。何か問題や質問などがあれば issue をたててください (訳注: リンクは原文(英語)のものになります)。また[こちら][at the bottom]にコメントを残すこともできます。この記事の完全なソースコードは[`post-05` ブランチ][post branch]にあります。
|
||||
|
||||
[GitHub]: https://github.com/phil-opp/blog_os
|
||||
[at the bottom]: #comments
|
||||
<!-- fix for zola anchor checker (target is in template): <a id="comments"> -->
|
||||
[post branch]: https://github.com/phil-opp/blog_os/tree/post-05
|
||||
|
||||
<!-- toc -->
|
||||
|
||||
## 概要
|
||||
例外とは、今実行している命令はなにかおかしいぞ、ということを示すものです。例えば、現在の命令がゼロ除算を実行しようとしているとき、CPUは例外を発します。例外が起こると、CPUは現在行われている作業に割り込み、例外の種類に従って、即座に特定の例外ハンドラ関数を呼びます。
|
||||
|
||||
x86には20種類のCPU例外があります。中でも重要なものは:
|
||||
|
||||
- **<ruby>ページフォルト<rp> (</rp><rt>Page Fault</rt><rp>) </rp></ruby>**: ページフォルトは不正なメモリアクセスの際に発生します。例えば、現在の命令がマップされていないページから読み込もうとしたり、読み込み専用のページに書き込もうとしたときに生じます。
|
||||
- **<ruby>無効な<rp> (</rp><rt>Invalid</rt><rp>) </rp></ruby><ruby>命令コード<rp> (</rp><rt>Opcode</rt><rp>) </rp></ruby>**: この例外は現在の命令が無効であるときに発生します。例えば、[SSE命令][SSE instructions]という新しい命令をサポートしていない旧式のCPU上でこれを実行しようとしたときに生じます。
|
||||
- **<ruby>一般保護違反<rp> (</rp><rt>General Protection Fault</rt><rp>) </rp></ruby>**: これは、例外の中でも、最もいろいろな理由で発生しうるものです。ユーザーレベルのコードで<ruby>特権命令<rp> (</rp><rt>privileged instruction</rt><rp>) </rp></ruby>を実行しようとしたときや、設定レジスタの保護領域に書き込もうとしたときなど、様々な種類のアクセス違反によって生じます。
|
||||
- **<ruby>ダブルフォルト<rp> (</rp><rt>Double Fault</rt><rp>) </rp></ruby>**: 何らかの例外が起こったとき、CPUは対応するハンドラ関数を呼び出そうとします。 この例外ハンドラを **呼び出している間に** 別の例外が起こった場合、CPUはダブルフォルト例外を出します。この例外はまた、ある例外に対してハンドラ関数が登録されていないときにも起こります。
|
||||
- **<ruby>トリプルフォルト<rp> (</rp><rt>Triple Fault</rt><rp>) </rp></ruby>**: 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 | <ruby>記述子の特権レベル<rp> (</rp><rt>Descriptor Privilege Level</rt><rp>) </rp></ruby> (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<HandlerFunc>,
|
||||
pub debug: Entry<HandlerFunc>,
|
||||
pub non_maskable_interrupt: Entry<HandlerFunc>,
|
||||
pub breakpoint: Entry<HandlerFunc>,
|
||||
pub overflow: Entry<HandlerFunc>,
|
||||
pub bound_range_exceeded: Entry<HandlerFunc>,
|
||||
pub invalid_opcode: Entry<HandlerFunc>,
|
||||
pub device_not_available: Entry<HandlerFunc>,
|
||||
pub double_fault: Entry<HandlerFuncWithErrCode>,
|
||||
pub invalid_tss: Entry<HandlerFuncWithErrCode>,
|
||||
pub segment_not_present: Entry<HandlerFuncWithErrCode>,
|
||||
pub stack_segment_fault: Entry<HandlerFuncWithErrCode>,
|
||||
pub general_protection_fault: Entry<HandlerFuncWithErrCode>,
|
||||
pub page_fault: Entry<PageFaultHandlerFunc>,
|
||||
pub x87_floating_point: Entry<HandlerFunc>,
|
||||
pub alignment_check: Entry<HandlerFuncWithErrCode>,
|
||||
pub machine_check: Entry<HandlerFunc>,
|
||||
pub simd_floating_point: Entry<HandlerFunc>,
|
||||
pub virtualization: Entry<HandlerFunc>,
|
||||
pub security_exception: Entry<HandlerFuncWithErrCode>,
|
||||
// いくつかのフィールドは省略している
|
||||
}
|
||||
```
|
||||
|
||||
この構造体のフィールドは[`idt::Entry<F>`]という型を持っています。これはIDTのエントリのフィールド(上の表を見てください)を表す構造体です。型パラメータ`F`は、期待されるハンドラ関数の型を表します。エントリの中には、[`HandlerFunc`]型を要求するものや、[`HandlerFuncWithErrCode`]型を要求するものがあることがわかります。ページフォルトに至っては、[`PageFaultHandlerFunc`]という自分専用の型を要求していますね。
|
||||
|
||||
[`idt::Entry<F>`]: 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種類に分けています:<ruby>preserved<rp> (</rp><rt>保存</rt><rp>) </rp></ruby>レジスタと<ruby>scratch<rp> (</rp><rt>下書き</rt><rp>) </rp></ruby>レジスタです。
|
||||
|
||||
preservedレジスタの値は関数呼び出しの前後で変化してはいけません。ですので、呼び出された関数(訳注:callの受け身で"callee"と呼ばれます)は、リターンする前にその値をもとに戻す場合に限り、その値を上書きできます。そのため、これらのレジスタは<ruby>callee-saved<rp> (</rp><rt>呼び出し先によって保存される</rt><rp>) </rp></ruby>と呼ばれます。よくとられる方法は、関数の最初でそのレジスタをスタックに保存し、リターンする直前にその値をもとに戻すことです。
|
||||
|
||||
それとは対照的に、呼び出された関数はscratchレジスタを何の制限もなく上書きすることができます。呼び出し元の関数がscratchレジスタの値を関数呼び出しの前後で保存したいなら、関数呼び出しの前に自分で(スタックにプッシュするなどして)バックアップしておいて、もとに戻す必要があります。なので、scratchレジスタは<ruby>caller-saved<rp> (</rp><rt>呼び出し元によって保存される</rt><rp>) </rp></ruby>です。
|
||||
|
||||
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はこのリターンアドレスをポップし、そこにジャンプします。そのため、通常の関数呼び出しの際のスタックフレームは以下のようになっています:
|
||||
|
||||

|
||||
|
||||
しかし、例外と割り込みハンドラについては、リターンアドレスをプッシュするだけではだめです。なぜなら、割り込みハンドラはしばしば(スタックポインタや、CPUフラグなどが)異なる状況で実行されるからです。ですので、代わりに、CPUは割り込みが起こると以下の手順を実行します。
|
||||
|
||||
1. **スタックポインタをアラインする**: 割り込みはあらゆる命令において発生しうるので、スタックポインタもあらゆる値を取る可能性があります。しかし、CPU命令のうちいくつか(例えばSSE命令の一部など)はスタックポインタが16バイトの倍数になっていることを要求するので、そうなるようにCPUは割り込みの直後にスタックポインタを<ruby>揃え<rp> (</rp><rt>アラインし</rt><rp>) </rp></ruby>ます。
|
||||
2. (場合によっては)**スタックを変更する**: スタックの変更は、例えばCPU例外がユーザーモードのプログラムで起こった場合のような、CPUの特権レベルを変更するときに起こります。いわゆる<ruby>割り込みスタック表<rp> (</rp><rt>Interrupt Stack Table</rt><rp>) </rp></ruby>を使うことで、特定の割り込みに対しスタックを変更するよう設定することも可能です。割り込みスタック表については次の記事で説明します。
|
||||
3. **古いスタックポインタをプッシュする**: CPUは、割り込みが発生した際の(アラインされる前の)スタックポインタレジスタ(`rsp`)とスタックセグメントレジスタ(`ss`)の値をプッシュします。これにより、割り込みハンドラからリターンしてきたときにもとのスタックポインタを復元することが可能になります。
|
||||
4. **`RFLAGS`レジスタをプッシュして更新する**: [`RFLAGS`]レジスタは状態や制御のための様々なビットを保持しています。割り込みに入るとき、CPUはビットのうちいくつかを変更し古い値をプッシュしておきます。
|
||||
5. **命令ポインタをプッシュする**: 割り込みハンドラ関数にジャンプする前に、CPUは命令ポインタ(`rip`)とコードセグメント(`cs`)をプッシュします。これは通常の関数呼び出しにおける戻り値のプッシュに対応します。
|
||||
6. (例外によっては)**エラーコードをプッシュする**: ページフォルトのような特定の例外の場合、CPUはエラーコードをプッシュします。これは、例外の原因を説明するものです。
|
||||
7. **割り込みハンドラを呼び出す**: CPUは割り込みハンドラ関数のアドレスと<ruby>セグメント記述子<rp> (</rp><rt>segment descriptor</rt><rp>) </rp></ruby>をIDTの対応するフィールドから読み出します。そして、この値を`rip`と`cs`レジスタに書き出してから、ハンドラを呼び出します。
|
||||
|
||||
[`RFLAGS`]: https://en.wikipedia.org/wiki/FLAGS_register
|
||||
|
||||
ですので、割り込み時のスタックフレーム (interrupt stack frame) は以下のようになります:
|
||||
|
||||

|
||||
|
||||
`x86_64`クレートにおいては、割り込み時のスタックフレームは[`InterruptStackFrame`]構造体によって表現されます。これは割り込みハンドラに`&mut`として渡されるため、これを使うことで例外の原因に関して追加で情報を手に入れることができます。例外のすべてがエラーコードをプッシュするわけではないので、この構造体にはエラーコードのためのフィールドはありません。これらの例外は[`HandlerFuncWithErrCode`]という別の関数型を使いますが、これらは追加で`error_code`引数を持ちます。
|
||||
|
||||
[`InterruptStackFrame`]: https://docs.rs/x86_64/0.14.2/x86_64/structures/idt/struct.InterruptStackFrame.html
|
||||
|
||||
### 舞台裏では何が
|
||||
`x86-interrupt`呼び出し規約は、この例外<ruby>処理<rp> (</rp><rt>ハンドル</rt><rp>) </rp></ruby>プロセスのややこしいところをほぼ全て隠蔽してくれる、強力な抽象化です。しかし、その後ろで何が起こっているのかを知っておいたほうが良いこともあるでしょう。以下に、`x86-interrupt`呼び出し規約がやってくれることを簡単なリストにして示しました。
|
||||
|
||||
- **引数を取得する**: 多くの呼び出し規約においては、引数はレジスタを使って渡されることを想定しています。例外ハンドラにおいては、スタックにバックアップする前にレジスタの値を上書きしてはいけないので、これは不可能です。その代わり、`x86-interrupt`呼び出し規約は、引数が既に特定のオフセットでスタック上にあることを認識しています。
|
||||
- **`iretq`を使ってリターンする**: 割り込み時のスタックフレームは通常の関数呼び出しのスタックフレームとは全く異なるため、通常の `ret` 命令を使ってハンドラ関数から戻ることはできません。その代わりに、`iretq` 命令を使う必要があります。
|
||||
- **エラーコードを処理する**: いくつかの例外の場合、エラーコードがプッシュされるのですが、これが状況をより複雑にします。エラーコードはスタックのアラインメントを変更し(次の箇条を参照)、リターンする前にスタックからポップされる必要があるのです。`x86-interrupt`呼び出し規約は、このややこしい仕組みをすべて処理してくれます。しかし、どのハンドラ関数がどの例外に使われているかは呼び出し規約側にはわからないので、関数の引数の数からその情報を推測する必要があります。つまり、プログラマはやはりそれぞれの例外に対して正しい関数型を使う責任があるということです。幸いにも、`x86_64`クレートで定義されている`InterruptDescriptorTable`型が、正しい関数型が確実に使われるようにしてくれます。
|
||||
- **スタックをアラインする**: 一部の命令(特にSSE命令)には、16バイトのスタックアラインメントを必要とするものがあります。CPUは例外が発生したときには必ずこのようにスタックが<ruby>整列<rp> (</rp><rt>アライン</rt><rp>) </rp></ruby>されることを保証しますが、例外の中には、エラーコードをプッシュして再びスタックの整列を壊してしまうものもあります。この場合、`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`より短いライフタイムの場合、<ruby>use-after-free<rp> (</rp><rt>メモリ解放後にアクセス</rt><rp>) </rp></ruby>バグが発生する可能性があります。
|
||||
|
||||
実際、これはまさにここで起こっていることです。私達の`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は<ruby>不変<rp> (</rp><rt>イミュータブル</rt><rp>) </rp></ruby>なので、`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内でこれを実行すると、以下のようになります
|
||||
|
||||

|
||||
|
||||
うまくいきました!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
|
||||
@@ -1,570 +0,0 @@
|
||||
+++
|
||||
title = "خطاهای دوگانه"
|
||||
weight = 6
|
||||
path = "fa/double-fault-exceptions"
|
||||
date = 2018-06-18
|
||||
|
||||
[extra]
|
||||
chapter = "Interrupts"
|
||||
# Please update this when updating the translation
|
||||
translation_based_on_commit = "3ac829171218156c07ce9a27186fee58e3a5521e"
|
||||
# GitHub usernames of the people that translated this post
|
||||
translators = ["hamidrezakp", "MHBahrampour"]
|
||||
rtl = true
|
||||
+++
|
||||
|
||||
این پست به طور دقیق جزئیات استثنای خطای دوگانه (ترجمه: double fault exception) را بررسی میکند، این استثنا هنگامی رخ میدهد که CPU نتواند یک کنترل کننده استثنا را فراخوانی کند. با کنترل این استثنا، از بروز _خطاهای سه گانه_ (ترجمه: triple faults) کشنده که باعث ریست (کلمه: reset) شدن سیستم میشوند، جلوگیری میکنیم. برای جلوگیری از خطاهای سه گانه در همه موارد، ما همچنین یک _Interrupt Stack Table_ را تنظیم کردهایم تا خطاهای دوگانه را روی یک پشته هسته جداگانه بگیرد.
|
||||
|
||||
<!-- more -->
|
||||
|
||||
این بلاگ بصورت آزاد روی [گیتهاب] توسعه داده شده است. اگر شما مشکل یا سوالی دارید، لطفاً آنجا یک ایشو باز کنید. شما همچنین میتوانید [در زیر] این پست کامنت بگذارید. منبع کد کامل این پست را میتوانید در بِرَنچ [`post-06`][post branch] پیدا کنید.
|
||||
|
||||
[گیتهاب]: https://github.com/phil-opp/blog_os
|
||||
[در زیر]: #comments
|
||||
<!-- fix for zola anchor checker (target is in template): <a id="comments"> -->
|
||||
[post branch]: https://github.com/phil-opp/blog_os/tree/post-06
|
||||
|
||||
<!-- toc -->
|
||||
|
||||
## خطای دوگانه چیست؟
|
||||
|
||||
به عبارت ساده، خطای دوگانه یک استثنای به خصوص است و هنگامی رخ میدهد که CPU نتواند یک کنترل کننده استثنا را فراخوانی کند. به عنوان مثال، این اتفاق هنگامی رخ میدهد که یک page fault (ترجمه: خطای صفحه) رخ دهد اما هیچ کنترل کننده خطایی در [جدول توصیف کننده وقفه][IDT] (ترجمه: Interrupt Descriptor Table) ثبت نشده باشد. بنابراین به نوعی شبیه بلاکهای همه گیر در زبانهای برنامهنویسی با استثناها میباشد، به عنوان مثال `catch(...)` در ++C یا `catch(Exception e)` در جاوا و #C.
|
||||
|
||||
[IDT]: @/edition-2/posts/05-cpu-exceptions/index.md#the-interrupt-descriptor-table
|
||||
|
||||
خطای دوگانه مانند یک استثنای عادی رفتار میکند. دارای شماره وکتور (کلمه: vector) `8` است و ما میتوانیم یک تابع طبیعی کنترل کننده برای آن در IDT تعریف کنیم. تهیه یک کنترل کننده خطای دوگانه بسیار مهم است، زیرا اگر یک خطای دوگانه کنترل نشود، یک خطای کشنده سه گانه رخ میدهد. خطاهای سه گانه قابل کشف نیستند و اکثر سخت افزارها با تنظیم مجدد سیستم واکنش نشان میدهند.
|
||||
|
||||
### راهاندازی یک خطای دوگانه
|
||||
|
||||
بیایید یک خطای دوگانه را با راهاندازی (ترجمه: triggering) یک استثنا برای آن ایجاد کنیم، ما هنوز یک تابع کنترل کننده تعریف نکردهایم:
|
||||
|
||||
```rust
|
||||
// in src/main.rs
|
||||
|
||||
#[no_mangle]
|
||||
pub extern "C" fn _start() -> ! {
|
||||
println!("Hello World{}", "!");
|
||||
|
||||
blog_os::init();
|
||||
|
||||
// trigger a page fault
|
||||
unsafe {
|
||||
*(0xdeadbeef as *mut u64) = 42;
|
||||
};
|
||||
|
||||
// as before
|
||||
#[cfg(test)]
|
||||
test_main();
|
||||
|
||||
println!("It did not crash!");
|
||||
loop {}
|
||||
}
|
||||
```
|
||||
|
||||
برای نوشتن در آدرس نامعتبر `0xdeadbeef` از` unsafe` استفاده میکنیم. آدرس مجازی در جداول صفحه به آدرس فیزیکی مپ نمیشود، بنابراین خطای صفحه رخ میدهد. ما یک کنترل کننده خطای صفحه در [IDT] خود ثبت نکردهایم، بنابراین یک خطای دوگانه رخ میدهد.
|
||||
|
||||
حال وقتی هسته را اجرا میکنیم، میبینیم که وارد یک حلقه بوت بیپایان میشود. دلایل حلقه بوت به شرح زیر است:
|
||||
|
||||
۱. سیپییو سعی به نوشتن در `0xdeadbeef` دارد، که باعث خطای صفحه میشود.
|
||||
۲. سیپییو به ورودی مربوطه در IDT نگاه میکند و میبیند که هیچ تابع کنترل کنندهای مشخص نشده است. بنابراین، نمیتواند کنترل کننده خطای صفحه را فراخوانی کند و یک خطای دوگانه رخ میدهد.
|
||||
۳. سیپییو ورودی IDT کنترل کننده خطای دو گانه را بررسی میکند، اما این ورودی هم تابع کنترل کنندهای را مشخص نمیکند. بنابراین، یک خطای _سهگانه_ رخ میدهد.
|
||||
۴. خطای سه گانه کشنده است. QEMU مانند اکثر سخت افزارهای واقعی به آن واکنش نشان داده دستور ریست شدن سیستم را صادر میکند.
|
||||
|
||||
بنابراین برای جلوگیری از این خطای سهگانه، باید یک تابع کنترل کننده برای خطاهای صفحه یا یک کنترل کننده خطای دوگانه ارائه دهیم. ما میخواهیم در همه موارد از خطاهای سه گانه جلوگیری کنیم، بنابراین بیایید با یک کنترل کننده خطای دوگانه شروع کنیم که برای همه انواع استثنا بدون کنترل فراخوانی میشود.
|
||||
|
||||
## کنترل کننده خطای دوگانه
|
||||
|
||||
خطای دوگانه یک استثنا عادی با کد خطا است، بنابراین میتوانیم یک تابع کنترل کننده مشابه کنترل کننده نقطه شکست (ترجمه: breakpoint) تعیین کنیم:
|
||||
|
||||
```rust
|
||||
// in src/interrupts.rs
|
||||
|
||||
lazy_static! {
|
||||
static ref IDT: InterruptDescriptorTable = {
|
||||
let mut idt = InterruptDescriptorTable::new();
|
||||
idt.breakpoint.set_handler_fn(breakpoint_handler);
|
||||
idt.double_fault.set_handler_fn(double_fault_handler); // new
|
||||
idt
|
||||
};
|
||||
}
|
||||
|
||||
// new
|
||||
extern "x86-interrupt" fn double_fault_handler(
|
||||
stack_frame: InterruptStackFrame, _error_code: u64) -> !
|
||||
{
|
||||
panic!("EXCEPTION: DOUBLE FAULT\n{:#?}", stack_frame);
|
||||
}
|
||||
```
|
||||
|
||||
کنترل کننده ما یک پیام خطای کوتاه چاپ میکند و قاب پشته استثنا را تخلیه میکند. کد خطای کنترل کننده خطای دوگانه همیشه صفر است، بنابراین دلیلی برای چاپ آن وجود ندارد. یک تفاوت در کنترل کننده نقطه شکست این است که کنترل کننده خطای دوگانه [_diverging_] \(ترجمه: واگرا) است. چون معماری `x86_64` امکان بازگشت از یک استثنا خطای دوگانه را ندارد.
|
||||
|
||||
[_diverging_]: https://doc.rust-lang.org/stable/rust-by-example/fn/diverging.html
|
||||
|
||||
حال وقتی هسته را اجرا میکنیم، باید ببینیم که کنترل کننده خطای دوگانه فراخوانی میشود:
|
||||
|
||||

|
||||
|
||||
کار کرد! آنچه این بار اتفاق میافتد بصورت زیر است:
|
||||
|
||||
۱. سیپییو سعی به نوشتن در `0xdeadbeef` دارد، که باعث خطای صفحه میشود.
|
||||
۲. مانند قبل، سیپییو به ورودی مربوطه در IDT نگاه میکند و میبیند که هیچ تابع کنترل کنندهای مشخص نشده است. بنابراین، یک خطای دوگانه رخ میدهد.
|
||||
۳. سیپییو به کنترل کننده خطای دوگانه - که اکنون وجود دارد - میرود.
|
||||
|
||||
خطای سه گانه (و حلقه بوت) دیگر رخ نمیدهد، زیرا اکنون CPU میتواند کنترل کننده خطای دوگانه را فراخوانی کند.
|
||||
|
||||
این کاملاً ساده بود! پس چرا ما برای این موضوع به یک پست کامل نیاز داریم؟ خب، ما اکنون قادر به ردیابی _اکثر_ خطاهای دوگانه هستیم، اما مواردی وجود دارد که رویکرد فعلی ما کافی نیست.
|
||||
|
||||
## علل رخ داد خطای دوگانه
|
||||
|
||||
قبل از بررسی موارد خاص، باید علل دقیق خطاهای دوگانه را بدانیم. در بالا، ما از یک تعریف کاملا مبهم استفاده کردیم:
|
||||
|
||||
> خطای دوگانه یک استثنای به خصوص است و هنگامی رخ میدهد که CPU نتواند یک کنترل کننده استثنا را فراخوانی کند.
|
||||
|
||||
عبارت _“fails to invoke”_ دقیقا چه معنایی دارد؟ کنترل کننده وجود ندارد؟ کنترل کننده [خارج شده][swapped out] \(منظور این است که آیا صفحه مربوط به کنترل کننده از حافظه خارج شده)؟ و اگر کنترل کننده خودش باعث رخ دادن یک استثناها شود، چه اتفاقی رخ میدهد؟
|
||||
|
||||
[swapped out]: http://pages.cs.wisc.edu/~remzi/OSTEP/vm-beyondphys.pdf
|
||||
|
||||
به عنوان مثال، چه اتفاقی میافتد اگر:
|
||||
|
||||
۱. یک استثنای نقطه شکست رخ میدهد، آیا تابع کنترل کننده مربوطه خارج شده است؟
|
||||
۲. یک خطای صفحه رخ میدهد، آیا کنترل کننده خطای صفحه خارج شده است؟
|
||||
۳. کنترل کنندهی «تقسیم بر صفر» باعث رخ دادن یک استثنای نقطه شکست میشود، آیا کنترل کننده نقطه شکست خارج شده است؟
|
||||
۴. هسته ما پشته خود را سرریز میکند و آیا _صفحه محافظ_ (ترجمه: guard page) ضربه میخورد؟
|
||||
|
||||
خوشبختانه، کتابچه راهنمای AMD64 ([PDF][AMD64 manual]) یک تعریف دقیق دارد (در بخش 8.2.9). مطابق آن، "یک استثنای خطای دوگانه _میتواند_ زمانی اتفاق بیفتد که یک استثنا دوم هنگام کار با یک کنترل کننده استثنا قبلی (اول) رخ دهد". _"می تواند"_ مهم است: فقط ترکیبی بسیار خاص از استثناها منجر به خطای دوگانه میشود. این ترکیبات عبارتند از:
|
||||
|
||||
استثنای اول | استثنای دوم
|
||||
----------------|-----------------
|
||||
[Divide-by-zero],<br>[Invalid TSS],<br>[Segment Not Present],<br>[Stack-Segment Fault],<br>[General Protection Fault] | [Invalid TSS],<br>[Segment Not Present],<br>[Stack-Segment Fault],<br>[General Protection Fault]
|
||||
[Page Fault] | [Page Fault],<br>[Invalid TSS],<br>[Segment Not Present],<br>[Stack-Segment Fault],<br>[General Protection Fault]
|
||||
|
||||
[Divide-by-zero]: https://wiki.osdev.org/Exceptions#Divide-by-zero_Error
|
||||
[Invalid TSS]: https://wiki.osdev.org/Exceptions#Invalid_TSS
|
||||
[Segment Not Present]: https://wiki.osdev.org/Exceptions#Segment_Not_Present
|
||||
[Stack-Segment Fault]: https://wiki.osdev.org/Exceptions#Stack-Segment_Fault
|
||||
[General Protection Fault]: https://wiki.osdev.org/Exceptions#General_Protection_Fault
|
||||
[Page Fault]: https://wiki.osdev.org/Exceptions#Page_Fault
|
||||
|
||||
|
||||
[AMD64 manual]: https://www.amd.com/system/files/TechDocs/24593.pdf
|
||||
|
||||
بنابراین به عنوان مثال، یک خطای تقسیم بر صفر (ترجمه: Divide-by-zero) و به دنبال آن خطای صفحه (ترجمه: Page Fault)، خوب است (کنترل کننده خطای صفحه فراخوانی میشود)، اما خطای تقسیم بر صفر و به دنبال آن یک خطای محافظت عمومی (ترجمه: General Protection) منجر به خطای دوگانه می شود.
|
||||
|
||||
با کمک این جدول میتوانیم به سه مورد اول از سوالهای بالا پاسخ دهیم:
|
||||
|
||||
۱. اگر یک استثنای نقطه شکست اتفاق بیفتد و تابع مربوط به کنترل کننده آن خارج شده باشد، یک _خطای صفحه_ رخ میدهد و _کنترل کننده خطای صفحه_ فراخوانی میشود.
|
||||
۲. اگر خطای صفحه رخ دهد و کنترل کننده خطای صفحه خارج شده باشد، یک _خطای دوگانه_ رخ میدهد و _کنترل کننده خطای دوگانه_ فراخوانی میشود.
|
||||
۳. اگر یک کنترل کننده تقسیم بر صفر باعث استثنای نقطه شکست شود، CPU سعی میکند تا کنترل کننده نقطه شکست را فراخوانی کند. اگر کنترل کننده نقطه شکست خارج شده باشد، یک _خطای صفحه_ رخ میدهد و _کنترل کننده خطای صفحه_ فراخوانی میشود.
|
||||
|
||||
در حقیقت، حتی موارد استثنا بدون تابع کنترل کننده در IDT نیز از این طرح پیروی میکند: وقتی استثنا رخ میدهد، CPU سعی میکند ورودی IDT مربوطه را بخواند. از آنجا که ورودی 0 است، که یک ورودی IDT معتبر نیست، یک _خطای محافظت کلی_ رخ میدهد. ما یک تابع کنترل کننده برای خطای محافظت عمومی نیز تعریف نکردیم، بنابراین یک خطای محافظت عمومی دیگر رخ میدهد. طبق جدول، این منجر به یک خطای دوگانه میشود.
|
||||
|
||||
### سرریزِ پشتهی هسته
|
||||
|
||||
بیایید به سوال چهارم نگاه کنیم:
|
||||
|
||||
> چه اتفاقی میافتد اگر هسته ما پشته خود را سرریز کند و صفحه محافظ ضربه بخورد؟
|
||||
|
||||
یک صفحه محافظ یک صفحه حافظه ویژه در پایین پشته است که امکان تشخیصِ سرریز پشته را فراهم میکند. صفحه به هیچ قاب فیزیکی مپ نشده است، بنابراین دسترسی به آن باعث خطای صفحه میشود به جای اینکه بی صدا حافظه دیگر را خراب کند. بوتلودر یک صفحه محافظ برای پشته هسته ما تنظیم میکند، بنابراین سرریز پشته باعث _خطای صفحه_ میشود.
|
||||
|
||||
هنگامی که خطای صفحه رخ میدهد، پردازنده به دنبال کنترل کننده خطای صفحه در IDT است و سعی میکند تا [قاب پشته وقفه][interrupt stack frame] را به پشته پوش میکند. با این حال، اشارهگر پشته فعلی هنوز به صفحه محافظی اشاره میکند که موجود نیست. بنابراین، خطای صفحه دوم رخ میدهد، که باعث خطای دوگانه میشود (مطابق جدول فوق).
|
||||
|
||||
[interrupt stack frame]: @/edition-2/posts/05-cpu-exceptions/index.md#the-interrupt-stack-frame
|
||||
|
||||
بنابراین حالا پردازنده سعی میکند _کنترل کننده خطای دوگانه_ را فراخوانی کند. با این حال، هنگام رخ دادن خطای دوگانه پردازنده سعی میکند تا قاب پشته استثنا را نیز پوش کند. اشارهگر پشته هنوز به سمت صفحه محافظ است، بنابراین یک خطای صفحه _سوم_ رخ میهد که باعث یک _خطای سهگانه_ و راه اندازی مجدد سیستم میشود. بنابراین کنترل کننده خطای دوگانه فعلی ما نمیتواند از خطای سهگانه در این مورد جلوگیری کند.
|
||||
|
||||
بیایید خودمان امتحان کنیم! ما میتوانیم با فراخوانی تابعی که به طور بیوقفه بازگشت مییابد، به راحتی سرریز پشته هسته را تحریک کنیم (باعث رخ دادن یک سرریز پشته هسته شویم):
|
||||
|
||||
```rust
|
||||
// in src/main.rs
|
||||
|
||||
#[no_mangle] // don't mangle the name of this function
|
||||
pub extern "C" fn _start() -> ! {
|
||||
println!("Hello World{}", "!");
|
||||
|
||||
blog_os::init();
|
||||
|
||||
fn stack_overflow() {
|
||||
stack_overflow(); // for each recursion, the return address is pushed
|
||||
}
|
||||
|
||||
// trigger a stack overflow
|
||||
stack_overflow();
|
||||
|
||||
[…] // test_main(), println(…), and loop {}
|
||||
}
|
||||
```
|
||||
|
||||
وقتی این کد را در QEMU امتحان میکنیم، میبینیم که سیستم دوباره وارد یک حلقه بوت میشود.
|
||||
|
||||
بنابراین چگونه میتوانیم از بروز این مشکل جلوگیری کنیم؟ ما نمیتوانیم پوش کردن قاب پشته استثنا را حذف کنیم، زیرا پردازنده خود این کار را انجام میدهد. بنابراین ما باید به نحوی اطمینان حاصل کنیم که وقتی یک استثنای خطای دوگانه رخ میدهد، پشته همیشه معتبر است. خوشبختانه، معماری x86_64 راه حلی برای این مشکل دارد.
|
||||
|
||||
## تعویض پشتهها
|
||||
|
||||
معماری x86_64 قادر است در صورت وقوع یک استثنا به یک پشته از پیش تعریف شده و شناخته شده تعویض شود. این تعویض در سطح سخت افزاری اتفاق میافتد، بنابراین میتوان آن را قبل از اینکه پردازنده قاب پشته استثنا را پوش کند، انجام داد.
|
||||
|
||||
مکانیزم تعویض به عنوان _Interrupt Stack Table_ (IST) پیادهسازی میشود. IST جدولی است با 7 اشارهگر برای دسته های معروف. در شبه کد شبیه Rust:
|
||||
|
||||
```rust
|
||||
struct InterruptStackTable {
|
||||
stack_pointers: [Option<StackPointer>; 7],
|
||||
}
|
||||
```
|
||||
|
||||
برای هر کنترل کننده استثنا، میتوانیم یک پشته از IST از طریق فیلد `stack_pointers` مربوط به [IDT entry] انتخاب کنیم. به عنوان مثال، ما میتوانیم از اولین پشته در IST برای کنترل کننده خطای دوگانه استفاده کنیم. هرگاه خطای دوگانه رخ دهد، پردازنده به طور خودکار به این پشته تغییر میکند. این تعویض قبل از پوش کردن هر چیزی اتفاق میافتد، بنابراین از خطای سهگانه جلوگیری میکند.
|
||||
|
||||
[IDT entry]: @/edition-2/posts/05-cpu-exceptions/index.md#the-interrupt-descriptor-table
|
||||
|
||||
### IST و TSS
|
||||
|
||||
جدول پشته وقفه (ترجمه: Interrupt Stack Table: IST) بخشی از یک ساختار قدیمی است که به آن _[سگمنت وضعیت پروسه]_ \(Task State Segment: TSS) گفته میشود. TSS برای نگهداری اطلاعات مختلف (به عنوان مثال وضعیت ثبات پردازنده) در مورد یک پروسه در حالت 32 بیتی استفاده میشد و به عنوان مثال برای [تعویض سختافزاری context] \(ترجمه: hardware context switching) استفاده میشد. با این حال، تعویض سختافزاری context دیگر در حالت 64 بیتی پشتیبانی نمیشود و قالب TSS کاملاً تغییر کرده است.
|
||||
|
||||
[سگمنت وضعیت پروسه]: https://en.wikipedia.org/wiki/Task_state_segment
|
||||
[تعویض سختافزاری context]: https://wiki.osdev.org/Context_Switching#Hardware_Context_Switching
|
||||
|
||||
در x86_64، دیگر TSS هیچ اطلاعات خاصی برای پرسهها ندارد. در عوض، دو جدول پشته را در خود جای داده است (IST یکی از آنهاست). تنها فیلد مشترک بین TSS 32-bit و TSS 64-bit اشارهگر به [بیتمپ مجوزهای پورت I/O] است.
|
||||
|
||||
[بیتمپ مجوزهای پورت I/O]: https://en.wikipedia.org/wiki/Task_state_segment#I.2FO_port_permissions
|
||||
|
||||
فرمت TSS 64-bit مانند زیر است:
|
||||
|
||||
فیلد | نوع
|
||||
------ | ----------------
|
||||
<span style="opacity: 0.5">(reserved)</span> | `u32`
|
||||
Privilege Stack Table | `[u64; 3]`
|
||||
<span style="opacity: 0.5">(reserved)</span> | `u64`
|
||||
Interrupt Stack Table | `[u64; 7]`
|
||||
<span style="opacity: 0.5">(reserved)</span> | `u64`
|
||||
<span style="opacity: 0.5">(reserved)</span> | `u16`
|
||||
I/O Map Base Address | `u16`
|
||||
|
||||
وقتی سطح ممتاز تغییر میکند، پردازنده از _Privilege Stack Table_ استفاده میکند. به عنوان مثال، اگر یک استثنا در حالی که CPU در حالت کاربر است (سطح ممتاز 3) رخ دهد، CPU معمولاً قبل از فراخوانی کنترل کننده استثنا، به حالت هسته تغییر میکند (سطح امتیاز 0). در این حالت، CPU به پشته صفرم در جدول پشته ممتاز تغییر وضعیت می دهد (از آنجا که 0، سطح ممتاز هدف است). ما هنوز هیچ برنامه حالت کاربر نداریم، بنابراین اکنون این جدول را نادیده میگیریم.
|
||||
|
||||
### ایجاد یک TSS
|
||||
|
||||
بیایید یک TSS جدید ایجاد کنیم که شامل یک پشته خطای دوگانه جداگانه در جدول پشته وقفه خود باشد. برای این منظور ما به یک ساختار TSS نیاز داریم. خوشبختانه کریت `x86_64` از قبل حاوی [ساختار `TaskStateSegment`] است که میتوانیم از آن استفاده کنیم.
|
||||
|
||||
[ساختار `TaskStateSegment`]: https://docs.rs/x86_64/0.14.2/x86_64/structures/tss/struct.TaskStateSegment.html
|
||||
|
||||
ما TSS را در یک ماژول جدید به نام `gdt` ایجاد میکنیم (نام این ماژول بعداً برایتان معنا پیدا میکند):
|
||||
|
||||
```rust
|
||||
// in src/lib.rs
|
||||
|
||||
pub mod gdt;
|
||||
|
||||
// in src/gdt.rs
|
||||
|
||||
use x86_64::VirtAddr;
|
||||
use x86_64::structures::tss::TaskStateSegment;
|
||||
use lazy_static::lazy_static;
|
||||
|
||||
pub const DOUBLE_FAULT_IST_INDEX: u16 = 0;
|
||||
|
||||
lazy_static! {
|
||||
static ref TSS: TaskStateSegment = {
|
||||
let mut tss = TaskStateSegment::new();
|
||||
tss.interrupt_stack_table[DOUBLE_FAULT_IST_INDEX as usize] = {
|
||||
const STACK_SIZE: usize = 4096 * 5;
|
||||
static mut STACK: [u8; STACK_SIZE] = [0; STACK_SIZE];
|
||||
|
||||
let stack_start = VirtAddr::from_ptr(unsafe { &STACK });
|
||||
let stack_end = stack_start + STACK_SIZE;
|
||||
stack_end
|
||||
};
|
||||
tss
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
ما از `lazy_static` استفاده میکنیم زیرا ارزیابی کننده ثابت راست هنوز آنقدر توانمند نیست که بتواند این مقداردهی اولیه را در زمان کامپایل انجام دهد. ما تعریف میکنیم که ورودی صفرم IST پشته خطای دوگانه است (هر اندیس دیگری از IST نیز قابل استفاده است). سپس آدرس بالای یک پشته خطای دوگانه را در ورودی صفرم مینویسیم. ما آدرس بالایی را مینویسیم زیرا پشتههای x86 به سمت پایین رشد میکنند، یعنی از آدرسهای بالا به آدرسهای پایین میآیند.
|
||||
|
||||
ما هنوز مدیریت حافظه را پیاده سازی نکردهایم، بنابراین روش مناسبی برای اختصاص پشته جدید نداریم. در عوض، فعلاً از یک آرایه `static mut` به عنوان حافظه پشته استفاده میکنیم. `unsafe` لازم است زیرا هنگام دسترسی به استاتیکهای تغییرپذیر (ترجمه: mutable)، کامپایلر نمیتواند عدم وجود رقابت بین داده ها را تضمین کند. مهم است که یک `static mut` باشد و نه یک استاتیک تغییرناپذیر (ترجمه: immutable)، زیرا در غیر این صورت bootloader آن را به یک صفحه فقط خواندنی نگاشت میکند. ما در پست بعدی این را با یک تخصیص پشته مناسب جایگزین خواهیم کرد، سپس `unsafe` دیگر در اینجا مورد نیاز نخواهد بود.
|
||||
|
||||
توجه داشته باشید که این پشته خطای دوگانه فاقد صفحه محافظ در برابر سرریز پشته است. یعنی ما نباید هیچ کاری که اضافه شدن ایتمی در پشته شود را انجام دهیم زیرا سرریز پشته ممکن است حافظه زیر پشته را خراب کند.
|
||||
|
||||
#### بارگذاری TSS
|
||||
|
||||
اکنون که TSS جدیدی ایجاد کردیم، به روشی نیاز داریم که به CPU بگوییم باید از آن استفاده کند. متأسفانه این کمی دشوار است، زیرا TSS به دلایل تاریخی از سیستم سگمنتبندی (ترجمه: segmentation) استفاده میکند. به جای بارگذاری مستقیم جدول، باید توصیفگر سگمنت جدیدی را به [جدول توصیفگر سراسری] \(Global Descriptor Table: GDT) اضافه کنیم. سپس میتوانیم TSS خود را با فراخوانی [دستور `ltr`] با اندیس GDT مربوطه بارگذاری کنیم. (دلیل اینکه نام ماژول را `gdt` گذاشتیم نیز همین بود).
|
||||
|
||||
[جدول توصیفگر سراسری]: https://web.archive.org/web/20190217233448/https://www.flingos.co.uk/docs/reference/Global-Descriptor-Table/
|
||||
[دستور `ltr`]: https://www.felixcloutier.com/x86/ltr
|
||||
|
||||
### جدول توصیفگر سراسری
|
||||
|
||||
جدول توصیفگر سراسری (GDT) یک یادگاری است که قبل از اینکه صفحهبندی به صورت استاندارد تبدیل شود، برای [تقسیمبندی حافظه] استفاده میشد. این مورد همچنان در حالت 64 بیتی برای موارد مختلف مانند پیکربندی هسته/کاربر یا بارگذاری TSS مورد نیاز است.
|
||||
|
||||
[تقسیمبندی حافظه]: https://en.wikipedia.org/wiki/X86_memory_segmentation
|
||||
|
||||
جدول توصیفگر سراسری، ساختاری است که شامل _بخشهای_ برنامه است. قبل از اینکه صفحهبندی به استاندارد تبدیل شود، از آن در معماریهای قدیمی استفاده میشد تا برنامه ها را از یکدیگر جدا کند. برای کسب اطلاعات بیشتر در مورد سگمنتبندی، فصل مربوط به این موضوع در [کتاب “Three Easy Pieces”] را مطالعه کنید. در حالی که سگمنتبندی در حالت 64 بیتی دیگر پشتیبانی نمیشود، GDT هنوز وجود دارد. بیشتر برای دو چیز استفاده میشود: جابجایی بین فضای هسته و فضای کاربر، و بارگذاری ساختار TSS.
|
||||
|
||||
[کتاب “Three Easy Pieces”]: http://pages.cs.wisc.edu/~remzi/OSTEP/
|
||||
|
||||
#### ایجاد یک GDT
|
||||
|
||||
بیایید یک `GDT` استاتیک ایجاد کنیم که شامل یک بخش برای TSS استاتیک ما باشد:
|
||||
|
||||
```rust
|
||||
// in src/gdt.rs
|
||||
|
||||
use x86_64::structures::gdt::{GlobalDescriptorTable, Descriptor};
|
||||
|
||||
lazy_static! {
|
||||
static ref GDT: GlobalDescriptorTable = {
|
||||
let mut gdt = GlobalDescriptorTable::new();
|
||||
gdt.add_entry(Descriptor::kernel_code_segment());
|
||||
gdt.add_entry(Descriptor::tss_segment(&TSS));
|
||||
gdt
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
ما دوباره از `lazy_static` استفاده میکنیم، زیرا ارزیابی کننده ثابت راست هنوز آنقدر توانمند نیست. ما یک GDT جدید با یک کد سگمنت و یک بخش TSS ایجاد میکنیم.
|
||||
|
||||
#### بارگذاری GDT
|
||||
|
||||
برای بارگذاری GDT، یک تابع جدید `gdt::init` ایجاد میکنیم که آن را از تابع `init` فراخوانی میکنیم:
|
||||
|
||||
```rust
|
||||
// in src/gdt.rs
|
||||
|
||||
pub fn init() {
|
||||
GDT.load();
|
||||
}
|
||||
|
||||
// in src/lib.rs
|
||||
|
||||
pub fn init() {
|
||||
gdt::init();
|
||||
interrupts::init_idt();
|
||||
}
|
||||
```
|
||||
|
||||
اکنون GDT ما بارگذاری شده است (از آنجا که تابع `start_`، تابع `init` را فراخوانی میکند)، اما هنوز حلقه بوت را هنگامِ سرریز پشته مشاهده میکنیم.
|
||||
|
||||
### مراحل پایانی
|
||||
|
||||
مشکل این است که سگمنتهای GDT هنوز فعال نیستند زیرا سگمنت و ثباتهای TSS هنوز حاوی مقادیر GDT قدیمی هستند. ما همچنین باید ورودی خطای دوگانه IDT را اصلاح کنیم تا از پشته جدید استفاده کند.
|
||||
|
||||
به طور خلاصه، باید موارد زیر را انجام دهیم:
|
||||
|
||||
۱. **بارگذاری مجدد ثبات کد سگمنت**: ما GDT خود را تغییر دادیم، بنابراین باید `cs`، ثبات کد سگمنت را بارگذاری مجدد کنیم. این مورد الزامی است زیرا انتخابگر سگمنت قدیمی میتواند اکنون توصیفگر دیگری از GDT را نشان دهد (به عنوان مثال توصیف کننده TSS).
|
||||
۲. **بارگذاری TSS**: ما یک GDT بارگذاری کردیم که شامل یک انتخابگر TSS است، اما هنوز باید به CPU بگوییم که باید از آن TSS استفاده کند.
|
||||
۳. **بروزرسانی ورودی IDT**: به محض اینکه TSS بارگذاری شد، CPU به یک جدول پشته وقفه معتبر (IST) دسترسی دارد. سپس میتوانیم به CPU بگوییم که باید با تغییر در ورودی IDT خطای دوگانه از پشته خطای دوگانه جدید استفاده کند.
|
||||
|
||||
برای دو مرحله اول، ما نیاز به دسترسی به متغیرهای` code_selector` و `tss_selector` در تابع `gdt::init` داریم. میتوانیم با تبدیل آنها به بخشی از استاتیک از طریق ساختار جدید `Selectors` به این هدف برسیم:
|
||||
|
||||
```rust
|
||||
// in src/gdt.rs
|
||||
|
||||
use x86_64::structures::gdt::SegmentSelector;
|
||||
|
||||
lazy_static! {
|
||||
static ref GDT: (GlobalDescriptorTable, Selectors) = {
|
||||
let mut gdt = GlobalDescriptorTable::new();
|
||||
let code_selector = gdt.add_entry(Descriptor::kernel_code_segment());
|
||||
let tss_selector = gdt.add_entry(Descriptor::tss_segment(&TSS));
|
||||
(gdt, Selectors { code_selector, tss_selector })
|
||||
};
|
||||
}
|
||||
|
||||
struct Selectors {
|
||||
code_selector: SegmentSelector,
|
||||
tss_selector: SegmentSelector,
|
||||
}
|
||||
```
|
||||
|
||||
اکنون میتوانیم با استفاده از انتخابگرها، ثبات بخش `cs` را بارگذاری مجدد کرده و `TSS` را بارگذاری کنیم:
|
||||
|
||||
```rust
|
||||
// in src/gdt.rs
|
||||
|
||||
pub fn init() {
|
||||
use x86_64::instructions::segmentation::set_cs;
|
||||
use x86_64::instructions::tables::load_tss;
|
||||
|
||||
GDT.0.load();
|
||||
unsafe {
|
||||
set_cs(GDT.1.code_selector);
|
||||
load_tss(GDT.1.tss_selector);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
ما با استفاده از [`set_cs`] ثبات کد سگمنت را بارگذاری مجدد میکنیم و برای بارگذاری TSS با از [`load_tss`] استفاده میکنیم. توابع به عنوان `unsafe` علامت گذاری شدهاند، بنابراین برای فراخوانی آنها به یک بلوک `unsafe` نیاز داریم. چون ممکن است با بارگذاری انتخابگرهای نامعتبر، ایمنی حافظه از بین برود.
|
||||
|
||||
[`set_cs`]: https://docs.rs/x86_64/0.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 تنظیم کنیم:
|
||||
|
||||
```rust
|
||||
// in src/interrupts.rs
|
||||
|
||||
use crate::gdt;
|
||||
|
||||
lazy_static! {
|
||||
static ref IDT: InterruptDescriptorTable = {
|
||||
let mut idt = InterruptDescriptorTable::new();
|
||||
idt.breakpoint.set_handler_fn(breakpoint_handler);
|
||||
unsafe {
|
||||
idt.double_fault.set_handler_fn(double_fault_handler)
|
||||
.set_stack_index(gdt::DOUBLE_FAULT_IST_INDEX); // new
|
||||
}
|
||||
|
||||
idt
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
روش `set_stack_index` ایمن نیست زیرا فراخوان (ترجمه: caller) باید اطمینان حاصل کند که اندیس استفاده شده معتبر است و قبلاً برای استثنای دیگری استفاده نشده است.
|
||||
|
||||
همین! اکنون CPU باید هر زمان که خطای دوگانه رخ داد، به پشته خطای دوگانه برود. بنابراین، ما میتوانیم _همه_ خطاهای دوگانه، از جمله سرریزهای پشته هسته را بگیریم:
|
||||
|
||||

|
||||
|
||||
از این به بعد هرگز نباید شاهد خطای سهگانه باشیم! برای اطمینان از اینکه موارد بالا را به طور تصادفی نقض نمیکنیم، باید یک تست برای این کار اضافه کنیم.
|
||||
|
||||
## تست سرریز پشته
|
||||
|
||||
برای آزمایش ماژول `gdt` جدید و اطمینان از اینکه مدیر خطای دوگانه به درستی هنگام سرریز پشته فراخوانی شده است، میتوانیم یک تست یکپارچه اضافه کنیم. ایده این است که یک خطای دوگانه در تابع تست ایجاد کنید و تأیید کنید که مدیر خطای دوگانه فراخوانی میشود.
|
||||
|
||||
بیایید با یک طرح مینیمال شروع کنیم:
|
||||
|
||||
|
||||
```rust
|
||||
// in tests/stack_overflow.rs
|
||||
|
||||
#![no_std]
|
||||
#![no_main]
|
||||
|
||||
use core::panic::PanicInfo;
|
||||
|
||||
#[no_mangle]
|
||||
pub extern "C" fn _start() -> ! {
|
||||
unimplemented!();
|
||||
}
|
||||
|
||||
#[panic_handler]
|
||||
fn panic(info: &PanicInfo) -> ! {
|
||||
blog_os::test_panic_handler(info)
|
||||
}
|
||||
```
|
||||
|
||||
مانند تست `panic_handler`، تست [بدون یک test harness] اجرا خواهد شد. زیرا پس از یک خطای دوگانه نمیتوانیم اجرا را ادامه دهیم، بنابراین بیش از یک تست منطقی نیست. برای غیرفعال کردن test harness برای این تست، موارد زیر را به `Cargo.toml` اضافه میکنیم:
|
||||
|
||||
```toml
|
||||
# in Cargo.toml
|
||||
|
||||
[[test]]
|
||||
name = "stack_overflow"
|
||||
harness = false
|
||||
```
|
||||
|
||||
[بدون یک test harness]: @/edition-2/posts/04-testing/index.md#no-harness-tests
|
||||
|
||||
حال باید `cargo test --test stack_overflow` بصورت موفقیتآمیز کامپایل شود. البته این تست با شکست مواجه میشود، زیرا ماکروی `unimplemented` پنیک میکند.
|
||||
|
||||
### پیادهسازی `start_`
|
||||
|
||||
پیادهسازی تابع `start_` مانند این است:
|
||||
|
||||
```rust
|
||||
// in tests/stack_overflow.rs
|
||||
|
||||
use blog_os::serial_print;
|
||||
|
||||
#[no_mangle]
|
||||
pub extern "C" fn _start() -> ! {
|
||||
serial_print!("stack_overflow::stack_overflow...\t");
|
||||
|
||||
blog_os::gdt::init();
|
||||
init_test_idt();
|
||||
|
||||
// trigger a stack overflow
|
||||
stack_overflow();
|
||||
|
||||
panic!("Execution continued after stack overflow");
|
||||
}
|
||||
|
||||
#[allow(unconditional_recursion)]
|
||||
fn stack_overflow() {
|
||||
stack_overflow(); // for each recursion, the return address is pushed
|
||||
volatile::Volatile::new(0).read(); // prevent tail recursion optimizations
|
||||
}
|
||||
```
|
||||
|
||||
برای راهاندازی یک GDT جدید، تابع `gdt::init` را فراخوانی میکنیم. به جای فراخوانی تابع `interrupts::init_idt`، تابع `init_test_idt` را فراخوانی میکنیم که بزودی توضیح داده میشود. زیرا ما میخواهیم یک مدیر خطای دوگانه سفارشی ثبت کنیم که به جای پنیک کردن، دستور `exit_qemu(QemuExitCode::Success)` را انجام میدهد.
|
||||
|
||||
تابع `stack_overflow` تقریباً مشابه تابع موجود در `main.rs` است. تنها تفاوت این است که برای جلوگیری از بهینهسازی کامپایلر موسوم به [_tail call elimination_]، در پایان تابع، یک خواندنِ [فرارِ] \(ترجمه: volatile) اضافه به وسیله نوع [`Volatile`] انجام میدهیم. از جمله، این بهینهسازی به کامپایلر اجازه میدهد تابعی را که آخرین عبارت آن فراخوانی تابع بازگشتی است، به یک حلقه طبیعی تبدیل کند. بنابراین، هیچ قاب پشته اضافی برای فراخوانی تابع ایجاد نمیشود، پس استفاده از پشته ثابت میماند.
|
||||
|
||||
[volatile]: https://en.wikipedia.org/wiki/Volatile_(computer_programming)
|
||||
[`Volatile`]: https://docs.rs/volatile/0.2.6/volatile/struct.Volatile.html
|
||||
[_tail call elimination_]: https://en.wikipedia.org/wiki/Tail_call
|
||||
|
||||
با این حال، در مورد ما، ما میخواهیم که سرریز پشته اتفاق بیفتد، بنابراین در انتهای تابع یک دستور خواندن فرار ساختگی اضافه میکنیم، که کامپایلر مجاز به حذف آن نیست. بنابراین، تابع دیگر _tail recursive_ نیست و از تبدیل به یک حلقه جلوگیری میشود. ما همچنین صفت `allow(unconditional_recursion)` را اضافه میکنیم تا اخطار کامپایلر را در مورد تکرار بیوقفه تابع خاموش نگه دارد.
|
||||
|
||||
### تست IDT
|
||||
|
||||
همانطور که در بالا ذکر شد، این تست به IDT مخصوص خود با یک مدیر خطای دوگانه سفارشی نیاز دارد. پیادهسازی به این شکل است:
|
||||
|
||||
```rust
|
||||
// in tests/stack_overflow.rs
|
||||
|
||||
use lazy_static::lazy_static;
|
||||
use x86_64::structures::idt::InterruptDescriptorTable;
|
||||
|
||||
lazy_static! {
|
||||
static ref TEST_IDT: InterruptDescriptorTable = {
|
||||
let mut idt = InterruptDescriptorTable::new();
|
||||
unsafe {
|
||||
idt.double_fault
|
||||
.set_handler_fn(test_double_fault_handler)
|
||||
.set_stack_index(blog_os::gdt::DOUBLE_FAULT_IST_INDEX);
|
||||
}
|
||||
|
||||
idt
|
||||
};
|
||||
}
|
||||
|
||||
pub fn init_test_idt() {
|
||||
TEST_IDT.load();
|
||||
}
|
||||
```
|
||||
|
||||
پیادهسازی بسیار شبیه IDT طبیعی ما در `interrupts.rs` است. مانند IDT عادی، برای مدیر خطای دوگانه به منظور جابجایی به پشتهای جداگانه، یک اندیس پشته را در IST تنظیم میکنیم. تابع `init_test_idt` با استفاده از روش `load`، آیدیتی را بر روی پردازنده بارگذاری میکند.
|
||||
|
||||
### مدیر خطای دوگانه
|
||||
|
||||
تنها قسمت جامانده، مدیر خطای دوگانه است که به این شکل پیادهسازی میشود:
|
||||
|
||||
```rust
|
||||
// in tests/stack_overflow.rs
|
||||
|
||||
use blog_os::{exit_qemu, QemuExitCode, serial_println};
|
||||
use x86_64::structures::idt::InterruptStackFrame;
|
||||
|
||||
extern "x86-interrupt" fn test_double_fault_handler(
|
||||
_stack_frame: InterruptStackFrame,
|
||||
_error_code: u64,
|
||||
) -> ! {
|
||||
serial_println!("[ok]");
|
||||
exit_qemu(QemuExitCode::Success);
|
||||
loop {}
|
||||
}
|
||||
```
|
||||
|
||||
هنگامی که مدیر خطای دوگانه فراخوانی میشود، از QEMU با یک کد خروج موفقیتآمیز خارج میشویم، که تست را بعنوان «قبول شده» علامتگذاری میداند. از آنجا که تستهای یکپارچه اجراییهای کاملاً مجزایی هستند، باید صفت `[feature(abi_x86_interrupt)]!#` را در بالای فایل تست تنظیم کنیم.
|
||||
|
||||
اکنون میتوانیم تست را از طریق `cargo test --test stack_overflow` (یا `cargo test` برای اجرای همه تستها) انجام دهیم. همانطور که انتظار میرفت، خروجی `stack_overflow... [ok ]` را در کنسول مشاهده میکنیم. خط `set_stack_index` را کامنت کنید: این امر باعث میشود تست از کار بیفتد.
|
||||
|
||||
## خلاصه
|
||||
|
||||
در این پست یاد گرفتیم که خطای دوگانه چیست و در چه شرایطی رخ میدهد. ما یک مدیر خطای دوگانه پایه اضافه کردیم که پیام خطا را چاپ میکند و یک تست یکپارچه برای آن اضافه کردیم.
|
||||
|
||||
ما همچنین تعویض پشته پشتیبانی شده سختافزاری را در استثناهای خطای دوگانه فعال کردیم تا در سرریز پشته نیز کار کند. در حین پیادهسازی آن، ما با سگمنت وضعیت پروسه (TSS)، جدول پشته وقفه (IST) و جدول توصیف کننده سراسری (GDT) آشنا شدیم، که برای سگمنتبندی در معماریهای قدیمی استفاده میشد.
|
||||
|
||||
## بعدی چیست؟
|
||||
|
||||
پست بعدی نحوه مدیریت وقفههای دستگاههای خارجی مانند تایمر، صفحه کلید یا کنترل کنندههای شبکه را توضیح میدهد. این وقفههای سختافزاری بسیار شبیه به استثناها هستند، به عنوان مثال آنها هم از طریق IDT ارسال میشوند. با این حال، برخلاف استثناها، مستقیماً روی پردازنده رخ نمیدهند. در عوض، یک _interrupt controller_ این وقفهها را جمع کرده و بسته به اولویت، آنها را به CPU میفرستد. در بخش بعدی، مدیر وقفه [Intel 8259] \("PIC") را بررسی خواهیم کرد و نحوه پیادهسازی پشتیبانی صفحه کلید را یاد خواهیم گرفت.
|
||||
|
||||
[Intel 8259]: https://en.wikipedia.org/wiki/Intel_8259
|
||||
@@ -1,554 +0,0 @@
|
||||
+++
|
||||
title = "Double Faults"
|
||||
weight = 6
|
||||
path = "ja/double-fault-exceptions"
|
||||
date = 2018-06-18
|
||||
|
||||
[extra]
|
||||
chapter = "Interrupts"
|
||||
# Please update this when updating the translation
|
||||
translation_based_on_commit = "27ac0e1acc36f640d7045b427da2ed65b945756b"
|
||||
# GitHub usernames of the people that translated this post
|
||||
translators = ["garasubo"]
|
||||
+++
|
||||
|
||||
この記事ではCPUが例外ハンドラの呼び出しに失敗したときに起きる、ダブルフォルト例外について詳細に見ていきます。この例外を処理することによって、システムリセットを起こす重大な**トリプルフォルト**を避けることができます。あらゆる場合においてトリプルフォルトを防ぐために、ダブルフォルトを異なるカーネルスタック上でキャッチするための**割り込みスタックテーブル**をセットアップしていきます。
|
||||
|
||||
<!-- more -->
|
||||
|
||||
このブログの内容は [GitHub] 上で公開・開発されています。何か問題や質問などがあれば issue をたててください(訳注: リンクは原文(英語)のものになります)。また[こちら][at the bottom]にコメントを残すこともできます。この記事の完全なソースコードは[`post-06` ブランチ][post branch]にあります。
|
||||
|
||||
[GitHub]: https://github.com/phil-opp/blog_os
|
||||
[at the bottom]: #comments
|
||||
<!-- fix for zola anchor checker (target is in template): <a id="comments"> -->
|
||||
[post branch]: https://github.com/phil-opp/blog_os/tree/post-06
|
||||
|
||||
<!-- toc -->
|
||||
|
||||
## ダブルフォルトとは
|
||||
簡単に言うとダブルフォルトとはCPUが例外ハンドラを呼び出すことに失敗したときに起きる特別な例外です。例えば、ページフォルトが起きたが、ページフォルトハンドラが[割り込みディスクリプタテーブル][IDT](IDT: Interrupt Descriptor Table)に登録されていないときに発生します。つまり、C++での`catch(...)`や、JavaやC#の`catch(Exception e)`ような、例外のあるプログラミング言語のcatch-allブロックのようなものです。
|
||||
|
||||
[IDT]: @/edition-2/posts/05-cpu-exceptions/index.ja.md#ge-riip-miji-shu-zi-biao
|
||||
ダブルフォルトは通常の例外のように振る舞います。ベクター番号`8`を持ち、IDTに通常のハンドラ関数として定義できます。ダブルフォルトがうまく処理されないと、より重大な例外である**トリプルフォルト**が起きてしまうため、ダブルフォルトハンドラを設定することはとても重要です。トリプルフォルトはキャッチできず、ほとんどのハードウェアはシステムリセットを起こします。
|
||||
|
||||
### ダブルフォルトを起こす
|
||||
ハンドラ関数を定義していない例外を発生させることでダブルフォルトを起こしてみましょう。
|
||||
|
||||
```rust
|
||||
// in src/main.rs
|
||||
|
||||
#[no_mangle]
|
||||
pub extern "C" fn _start() -> ! {
|
||||
println!("Hello World{}", "!");
|
||||
|
||||
blog_os::init();
|
||||
|
||||
// ページフォルトを起こす
|
||||
unsafe {
|
||||
*(0xdeadbeef as *mut u64) = 42;
|
||||
};
|
||||
|
||||
// 前回同様
|
||||
#[cfg(test)]
|
||||
test_main();
|
||||
|
||||
println!("It did not crash!");
|
||||
loop {}
|
||||
}
|
||||
```
|
||||
|
||||
不正なアドレスである`0xdeadbeef`に書き込みを行うため`unsafe`を使います。この仮想アドレスはページテーブル上で物理アドレスにマップされていないため、ページフォルトが発生します。私達の[IDT]にはページフォルトが登録されていないため、ダブルフォルトが発生します。
|
||||
|
||||
今、私達のカーネルを起動すると、ブートループが発生します。この理由は以下の通りです:
|
||||
|
||||
1. CPUが`0xdeadbeef`に書き込みを試みページフォルトを起こします。
|
||||
2. CPUはIDTに対応するエントリを探しに行き、ハンドラ関数が指定されていないことを発見します。結果、ページフォルトハンドラが呼び出せず、ダブルフォルトが発生します。
|
||||
3. CPUはダブルフォルトハンドラのIDTエントリを見にいきますが、このエントリもハンドラ関数を指定していません。結果、**トリプルフォルト**が発生します。
|
||||
4. トリプルフォルトは重大なエラーなので、QEMUはほとんどの実際のハードウェアと同様にシステムリセットを行います。
|
||||
|
||||
このトリプルフォルトを防ぐためには、ページフォルトかダブルフォルトのハンドラ関数を定義しないといけません。私達はすべての場合におけるトリプルフォルトを防ぎたいので、適切に処理できなかったすべての例外において呼び出されることになるダブルフォルトハンドラを定義するところからはじめましょう。
|
||||
|
||||
## ダブルフォルトハンドラ
|
||||
ダブルフォルトは通常のエラーコードのある例外なので、ブレークポイントハンドラと同じようにハンドラ関数を指定することができます。
|
||||
|
||||
```rust
|
||||
// in src/interrupts.rs
|
||||
|
||||
lazy_static! {
|
||||
static ref IDT: InterruptDescriptorTable = {
|
||||
let mut idt = InterruptDescriptorTable::new();
|
||||
idt.breakpoint.set_handler_fn(breakpoint_handler);
|
||||
idt.double_fault.set_handler_fn(double_fault_handler); // new
|
||||
idt
|
||||
};
|
||||
}
|
||||
|
||||
// new
|
||||
extern "x86-interrupt" fn double_fault_handler(
|
||||
stack_frame: InterruptStackFrame, _error_code: u64) -> !
|
||||
{
|
||||
panic!("EXCEPTION: DOUBLE FAULT\n{:#?}", stack_frame);
|
||||
}
|
||||
```
|
||||
|
||||
私達のハンドラは短いエラーメッセージを出力して、例外スタックフレームをダンプします。ダブルフォルトハンドラのエラーコードは常に`0`なので、出力する必要はないでしょう。ブレークポイントハンドラとの違いの一つは、ダブルフォルトハンドラは[発散する](diverging)(訳注: 翻訳当時、リンク先未訳)ということです。`x86_64`アーキテクチャではダブルフォルト例外から復帰することができないためです。
|
||||
|
||||
[発散する]: https://doc.rust-jp.rs/rust-by-example-ja/fn/diverging.html
|
||||
|
||||
ここで私達のカーネルを起動すると、ダブルフォルトハンドラが呼び出されていることがわかることでしょう。
|
||||
|
||||

|
||||
|
||||
動きました!ここで何が起きているかというと、
|
||||
|
||||
1. CPUが`0xdeadbeef`に書き込みを試みページフォルトを起こします。
|
||||
2. 以前と同様に、CPUはIDT中の対応するエントリを見にいきますが、ハンドラ関数が定義されていないことを発見し、結果、ダブルフォルトが起きます。
|
||||
3. 今回はダブルフォルトハンドラが指定されているので、CPUはそれを適切に呼び出せます。
|
||||
|
||||
CPUはダブルフォルトハンドラを呼べるようになったので、トリプルフォルト(とブートループ)はもう起こりません。
|
||||
|
||||
ここまでは簡単です。ではなぜこの例外のために丸々一つの記事を用意したのでしょうか?実は、私達は**ほとんどの**ダブルフォルトをキャッチすることはできますが、このアプローチでは十分でないケースがいくつか存在するのです。
|
||||
|
||||
## ダブルフォルトの原因
|
||||
特別なケースを見ていく前に、ダブルフォルトの正確な原因を知る必要があります。ここまで、私達はとてもあいまいな定義を使ってきました。
|
||||
|
||||
> ダブルフォルトとはCPUが例外ハンドラを呼び出すことに失敗したときに起きる特別な例外です。
|
||||
|
||||
**「呼び出すことに失敗する」** とは正確には何を意味するのでしょうか?ハンドラが存在しない?ハンドラが[スワップアウト]された?また、ハンドラ自身が例外を発生させたらどうなるのでしょうか?
|
||||
|
||||
[スワップアウト]: http://pages.cs.wisc.edu/~remzi/OSTEP/vm-beyondphys.pdf
|
||||
|
||||
例えば以下のようなことが起こるとどうなるでしょう?
|
||||
|
||||
1. ブレークポイント例外が発生したが、対応するハンドラがスワップアウトされていたら?
|
||||
2. ページフォルトが発生したが、ページフォルトハンドラがスワップアウトされていたら?
|
||||
3. ゼロ除算ハンドラがブレークポイント例外を起こしたが、ブレークポイントハンドラがスワップアウトされていたら?
|
||||
4. カーネルがスタックをオーバーフローさせて**ガードページ**にヒットしたら?
|
||||
|
||||
幸いにもAMD64のマニュアル([PDF][AMD64 manual])には正確な定義が書かれています(8.2.9章)。それによると「ダブルフォルト例外は直前の(一度目の)例外ハンドラの処理中に二度目の例外が発生したとき**起きうる** (can occur)」と書かれています。**起きうる**というのが重要で、とても特別な例外の組み合わせでのみダブルフォルトとなります。この組み合わせは以下のようになっています。
|
||||
|
||||
最初の例外 | 二度目の例外
|
||||
----------------|-----------------
|
||||
[ゼロ除算],<br>[無効TSS],<br>[セグメント不在],<br>[スタックセグメントフォルト],<br>[一般保護違反] | [無効TSS],<br>[セグメント不在],<br>[スタックセグメントフォルト],<br>[一般保護違反]
|
||||
[ページフォルト] | [ページフォルト],<br>[無効TSS],<br>[セグメント不在],<br>[スタックセグメントフォルト],<br>[一般保護違反]
|
||||
|
||||
[ゼロ除算]: https://wiki.osdev.org/Exceptions#Divide-by-zero_Error
|
||||
[無効TSS]: https://wiki.osdev.org/Exceptions#Invalid_TSS
|
||||
[セグメント不在]: https://wiki.osdev.org/Exceptions#Segment_Not_Present
|
||||
[スタックセグメントフォルト]: https://wiki.osdev.org/Exceptions#Stack-Segment_Fault
|
||||
[一般保護違反]: https://wiki.osdev.org/Exceptions#General_Protection_Fault
|
||||
[ページフォルト]: https://wiki.osdev.org/Exceptions#Page_Fault
|
||||
|
||||
|
||||
[AMD64 manual]: https://www.amd.com/system/files/TechDocs/24593.pdf
|
||||
|
||||
例えばページフォルトに続いてゼロ除算例外が起きた場合は問題ありません(ページフォルトハンドラが呼び出される)が、一般保護違反に続いてゼロ除算例外が起きた場合はダブルフォルトが発生します。
|
||||
|
||||
この表を見れば、先程の質問のうち最初の3つに答えることができます:
|
||||
|
||||
1. ブレークポイント例外が発生して、対応するハンドラ関数がスワップアウトされている場合、**ページフォルト**が発生して**ページフォルトハンドラ**が呼び出される
|
||||
2. ページフォルトが発生してページフォルトハンドラがスワップアウトされている場合、**ダブルフォルト**が発生して**ダブルフォルトハンドラ**が呼び出されます。
|
||||
3. ゼロ除算ハンドラがブレークポイント例外を発生させた場合、CPUはブレークポイントハンドラを呼び出そうとします。もしブレークポイントハンドラがスワップアウトされている場合、**ページフォルト**が発生して**ページフォルトハンドラ**が呼び出されます。
|
||||
|
||||
実際、IDTにハンドラ関数が指定されていない例外のケースでもこの体系に従っています。つまり、例外が発生したとき、CPUは対応するIDTエントリを読み込みにいきます。このエントリは0であり正しいIDTエントリではないので、**一般保護違反**が発生します。私達は一般保護違反のハンドラも定義していないので、新たな一般保護違反が発生します。表によるとこれはダブルフォルトを起こします。
|
||||
|
||||
### カーネルスタックオーバーフロー
|
||||
4つ目の質問を見てみましょう。
|
||||
|
||||
> カーネルがスタックをオーバーフローさせてガードページにヒットしたら?
|
||||
|
||||
ガードページはスタックの底にある特別なメモリページで、これによってスタックオーバーフローを検出することができます。このページはどの物理メモリにもマップされていないので、アクセスすることで警告なく他のメモリを破壊する代わりにページフォルトが発生します。ブートローダーはカーネルスタックのためにガードページをセットアップするので、スタックオーバーフローは**ページフォルト**を発生させることになります。
|
||||
|
||||
ページフォルトが起きるとCPUはIDT内のページフォルトハンドラを探しにいき、[割り込みスタックフレーム](訳注: 翻訳当時、リンク先未訳)をスタック上にプッシュしようと試みます。しかし、このスタックポインタは存在しないガードページを指しています。結果、二度目のページフォルトが発生して、ダブルフォルトが起きます(上の表によれば)。
|
||||
|
||||
[割り込みスタックフレーム]: @/edition-2/posts/05-cpu-exceptions/index.md#the-interrupt-stack-frame
|
||||
|
||||
そして、ここでCPUは**ダブルフォルトハンドラ**を呼びにいきます。しかし、ダブルフォルト例外においてもCPUは例外スタックフレームをプッシュしようと試みます。スタックポインタはまだガードページを指しているので、**三度目の**ページフォルトが起きて、**トリプルフォルト**を発生させシステムは再起動します。そのため、私達の今のダブルフォルトハンドラではこの場合でのトリプルフォルトを避けることができません。
|
||||
|
||||
実際にやってみましょう。カーネルスタックオーバーフローは無限に再帰する関数を呼び出すことによって簡単に引き起こせます:
|
||||
|
||||
```rust
|
||||
// in src/main.rs
|
||||
|
||||
#[no_mangle] // この関数の名前修飾をしない
|
||||
pub extern "C" fn _start() -> ! {
|
||||
println!("Hello World{}", "!");
|
||||
|
||||
blog_os::init();
|
||||
|
||||
fn stack_overflow() {
|
||||
stack_overflow(); // 再帰呼び出しのために、リターンアドレスがプッシュされる
|
||||
}
|
||||
|
||||
// スタックオーバーフローを起こす
|
||||
stack_overflow();
|
||||
|
||||
[…] // test_main(), println(…), and loop {}
|
||||
}
|
||||
```
|
||||
|
||||
これをQEMUで試すと、再びブートループに入るのがわかります。
|
||||
|
||||
では、私達はどうすればこの問題を避けられるでしょうか?例外スタックフレームをプッシュすることは、CPU自身が行ってしまうので、省略できません。つまりどうにかしてダブルフォルト例外が発生したときスタックが常に正常であることを保証する必要があります。幸いにもx86_64アーキテクチャにはこの問題の解決策があります。
|
||||
|
||||
## スタックを切り替える
|
||||
x86_64アーキテクチャは例外発生時に予め定義されている既知の正常なスタックに切り替えることができます。この切り替えはハードウェアレベルで発生するので、CPUが例外スタックフレームをプッシュする前に行うことができます。
|
||||
|
||||
切り替えの仕組みは**割り込みスタックテーブル**(IST: Interrupt Stack Table)として実装されています。ISTは7つの既知の正常なポインタのテーブルです。Rust風の疑似コードで表すとこのようになります:
|
||||
|
||||
```rust
|
||||
struct InterruptStackTable {
|
||||
stack_pointers: [Option<StackPointer>; 7],
|
||||
}
|
||||
```
|
||||
|
||||
各例外ハンドラに対して、私達は対応する[IDTエントリ](訳注: 翻訳当時、リンク先未訳)の`stack_pointers`フィールドを通してISTからスタックを選ぶことができます。例えば、IST中の最初のスタックをダブルフォルトハンドラのために使うことができます。そうすると、CPUがダブルフォルトが発生したときは必ず、このスタックに自動的に切り替えを行います。この切り替えは何かがプッシュされる前に起きるので、トリプルフォルトを防ぐことになります。
|
||||
|
||||
[IDTエントリ]: @/edition-2/posts/05-cpu-exceptions/index.md#the-interrupt-descriptor-table
|
||||
|
||||
### ISTとTSS
|
||||
割り込みスタックテーブル(IST)は **[タスクステートセグメント]**(TSS)というレガシーな構造体の一部です。TSSはかつては様々な32ビットモードでのタスクに関する情報(例:プロセッサのレジスタの状態)を保持していて、例えば[ハードウェアコンテキストスイッチング]に使われていました。しかし、ハードウェアコンテキストスイッチングは64ビットモードではサポートされなくなり、TSSのフォーマットは完全に変わりました。
|
||||
|
||||
[タスクステートセグメント]: https://ja.wikipedia.org/wiki/Task_state_segment
|
||||
[ハードウェアコンテキストスイッチング]: https://wiki.osdev.org/Context_Switching#Hardware_Context_Switching
|
||||
|
||||
x86_64ではTSSはタスク固有の情報は全く持たなくなりました。代わりに、2つのスタックテーブル(ISTがその1つ)を持つようになりました。唯一32ビットと64ビットのTSSで共通のフィールドは[I/Oポート許可ビットマップ]へのポインタのみです。
|
||||
|
||||
[I/Oポート許可ビットマップ]: https://ja.wikipedia.org/wiki/Task_state_segment#I/O許可ビットマップ
|
||||
|
||||
64ビットのTSSは下記のようなフォーマットです:
|
||||
|
||||
フィールド | 型
|
||||
------ | ----------------
|
||||
<span style="opacity: 0.5">(予約済み)</span> | `u32`
|
||||
特権スタックテーブル | `[u64; 3]`
|
||||
<span style="opacity: 0.5">(予約済み)</span> | `u64`
|
||||
割り込みスタックテーブル | `[u64; 7]`
|
||||
<span style="opacity: 0.5">(予約済み)</span> | `u64`
|
||||
<span style="opacity: 0.5">(予約済み)</span> | `u16`
|
||||
I/Oマップベースアドレス | `u16`
|
||||
|
||||
**特権スタックテーブル**は特権レベルが変わった際にCPUが使用します。例えば、CPUがユーザーモード(特権レベル3)の時に例外が発生した場合、CPUは通常例外ハンドラを呼び出す前にカーネルモード(特権レベル0)に切り替わります。この場合、CPUは特権レベルスタックテーブルの0番目のスタックに切り替わります。ユーザーモードについてはまだ実装してないため、このテープルはとりあえず無視しておきましょう。
|
||||
|
||||
### TSSをつくる
|
||||
割り込みスタックテーブルにダブルフォルト用のスタックを含めた新しいTSSをつくってみましょう。そのためにはTSS構造体が必要です。幸いにも、すでに`x86_64`クレートに[`TaskStateSegment`構造体]が含まれているので、これを使っていきます。
|
||||
|
||||
[`TaskStateSegment`構造体]: https://docs.rs/x86_64/0.14.2/x86_64/structures/tss/struct.TaskStateSegment.html
|
||||
|
||||
新しい`gdt`モジュール内でTSSをつくります(名前の意味は後でわかるでしょう):
|
||||
|
||||
```rust
|
||||
// in src/lib.rs
|
||||
|
||||
pub mod gdt;
|
||||
|
||||
// in src/gdt.rs
|
||||
|
||||
use x86_64::VirtAddr;
|
||||
use x86_64::structures::tss::TaskStateSegment;
|
||||
use lazy_static::lazy_static;
|
||||
|
||||
pub const DOUBLE_FAULT_IST_INDEX: u16 = 0;
|
||||
|
||||
lazy_static! {
|
||||
static ref TSS: TaskStateSegment = {
|
||||
let mut tss = TaskStateSegment::new();
|
||||
tss.interrupt_stack_table[DOUBLE_FAULT_IST_INDEX as usize] = {
|
||||
const STACK_SIZE: usize = 4096 * 5;
|
||||
static mut STACK: [u8; STACK_SIZE] = [0; STACK_SIZE];
|
||||
|
||||
let stack_start = VirtAddr::from_ptr(unsafe { &STACK });
|
||||
let stack_end = stack_start + STACK_SIZE;
|
||||
stack_end
|
||||
};
|
||||
tss
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
Rustの定数評価機はこの初期化をコンパイル時に行うことがまだできないので`lazy_static`を使います。ここでは0番目のISTエントリをダブルフォルト用のスタックとして定義します(他のISTのインデックスでも動くでしょう)。そして、ダブルフォルト用スタックの先頭アドレスを0番目のエントリに書き込みます。先頭アドレスを書き込むのはx86のスタックは下、つまり高いアドレスから低いアドレスに向かって伸びていくからです。
|
||||
|
||||
私達はまだメモリ管理を実装していません。そのため、新しいスタックを確保する適切な方法がありません。その代わり今回は、スタックのストレージとして`static mut`な配列を使います。コンパイラが変更可能な静的変数がアクセスされるとき競合がないことを保証できないため`unsafe`が必要となります。これが不変の`static`ではなく`static mut`であることは重要です。そうでなければブートローダーはこれをリードオンリーのページにマップしてしまうからです。私達は後の記事でこの部分を適切なスタック確保処理に置き換えます。そうすればこの部分での`unsafe`は必要なくなります。
|
||||
|
||||
ちなみに、このダブルフォルトスタックはスタックオーバーフローに対する保護をするガードページを持ちません。つまり、スタックオーバーフローがスタックより下のメモリを破壊するかもしれないので、私達はダブルフォルトハンドラ内でスタックを多用すべきではないということです。
|
||||
|
||||
#### TSSを読み込む
|
||||
新しいTSSをつくったので、CPUにそれを使うように教える方法が必要です。残念ながら、これはちょっと面倒くさいです。なぜならTSSは(歴史的な理由で)セグメンテーションシステムを使うためです。テーブルを直接読み込むのではなく、新しいセグメントディスクリプタを[グローバルディスクリプタテーブル](GDT: Global Descriptor Table)に追加する必要があります。そうすると各自のGDTインデックスで[`ltr`命令]を呼び出すことで私達のTSSを読み込むことができます。
|
||||
|
||||
[グローバルディスクリプタテーブル]: https://web.archive.org/web/20190217233448/https://www.flingos.co.uk/docs/reference/Global-Descriptor-Table/
|
||||
[`ltr`命令]: https://www.felixcloutier.com/x86/ltr
|
||||
|
||||
### グローバルディスクリプタテーブル
|
||||
グローバルディスクリプタテーブル(GDT)はページングがデファクトスタンダードになる以前は、[メモリセグメンテーション]のため使われていた古い仕組みです。カーネル・ユーザーモードの設定やTSSの読み込みなど、様々なことを行うために64ビットモードでも未だに必要です。
|
||||
|
||||
[メモリセグメンテーション]: https://ja.wikipedia.org/wiki/セグメント方式
|
||||
|
||||
GDTはプログラムの**セグメント**を含む構造です。ページングが標準になる以前に、プログラム同士を独立させるためにより古いアーキテクチャで使われていました。セグメンテーションに関するより詳しい情報は無料の[「Three Easy Pieces」]という本の同じ名前の章を見てください。セグメンテーションは64ビットモードではもうサポートされていませんが、GDTはまだ存在しています。GDTはカーネル空間とユーザー空間の切り替えと、TSS構造体の読み込みという主に2つのことに使われています。
|
||||
|
||||
[「Three Easy Pieces」]: http://pages.cs.wisc.edu/~remzi/OSTEP/
|
||||
|
||||
#### GDTをつくる
|
||||
`TSS`の静的変数のセグメントを含む静的`GDT`をつくりましょう:
|
||||
|
||||
```rust
|
||||
// in src/gdt.rs
|
||||
|
||||
use x86_64::structures::gdt::{GlobalDescriptorTable, Descriptor};
|
||||
|
||||
lazy_static! {
|
||||
static ref GDT: GlobalDescriptorTable = {
|
||||
let mut gdt = GlobalDescriptorTable::new();
|
||||
gdt.add_entry(Descriptor::kernel_code_segment());
|
||||
gdt.add_entry(Descriptor::tss_segment(&TSS));
|
||||
gdt
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
先に紹介したコードと同様に、再び`lazy_static`を使います。コードセグメントとTSSセグメントを持つ新しいGDTを作成します。
|
||||
|
||||
#### GDTを読み込む
|
||||
|
||||
GDTを読み込むために新しく`gdt::init`関数をつくり、これを`init`関数から呼び出します:
|
||||
|
||||
```rust
|
||||
// in src/gdt.rs
|
||||
|
||||
pub fn init() {
|
||||
GDT.load();
|
||||
}
|
||||
|
||||
// in src/lib.rs
|
||||
|
||||
pub fn init() {
|
||||
gdt::init();
|
||||
interrupts::init_idt();
|
||||
}
|
||||
```
|
||||
|
||||
これでGDTが読み込まれます(`_start`関数は`init`を呼び出すため)が、これではまだスタックオーバーフローでブートループが起きてしまいます。
|
||||
|
||||
### 最後のステップ
|
||||
|
||||
問題はGDTセグメントとTSSレジスタが古いGDTからの値を含んでいるため、GDTセグメントがまだ有効になっていないことです。ダブルフォルト用のIDTエントリが新しいスタックを使うように変更する必要もあります。
|
||||
|
||||
まとめると、私達は次のようなことをする必要があります:
|
||||
|
||||
1. **コードセグメントレジスタを再読み込みする**:GDTを変更したので、コードセグメントレジスタ`cs`を再読み込みする必要があります。これは、古いセグメントセレクタが異なるGDTディスクリプタ(例:TSSディスクリプタ)を指す可能性があるためです。
|
||||
2. **TSSをロードする**:TSSセレクタを含むGDTをロードしましたが、CPUにこのTSSを使うよう教えてあげる必要があります。
|
||||
3. **IDTエントリを更新する**:TSSがロードされると同時に、CPUは正常な割り込みスタックテーブル(IST)へアクセスできるようになります。そうしたら、ダブルフォルトIDTエントリを変更することで、CPUに新しいダブルフォルトスタックを使うよう教えてあげることができます。
|
||||
|
||||
最初の2つのステップのために、私達は`gdt::init`関数の中で`code_selector`と`tss_selector`変数にアクセスする必要があります。これは、その変数たちを新しい`Selectors`構造体を使い静的変数にすることで実装できます:
|
||||
|
||||
```rust
|
||||
// in src/gdt.rs
|
||||
|
||||
use x86_64::structures::gdt::SegmentSelector;
|
||||
|
||||
lazy_static! {
|
||||
static ref GDT: (GlobalDescriptorTable, Selectors) = {
|
||||
let mut gdt = GlobalDescriptorTable::new();
|
||||
let code_selector = gdt.add_entry(Descriptor::kernel_code_segment());
|
||||
let tss_selector = gdt.add_entry(Descriptor::tss_segment(&TSS));
|
||||
(gdt, Selectors { code_selector, tss_selector })
|
||||
};
|
||||
}
|
||||
|
||||
struct Selectors {
|
||||
code_selector: SegmentSelector,
|
||||
tss_selector: SegmentSelector,
|
||||
}
|
||||
```
|
||||
|
||||
これで私達は`cs`セグメントレジスタを再読み込みし`TSS`を読み込むためにセレクタを使うことができます:
|
||||
|
||||
```rust
|
||||
// in src/gdt.rs
|
||||
|
||||
pub fn init() {
|
||||
use x86_64::instructions::segmentation::set_cs;
|
||||
use x86_64::instructions::tables::load_tss;
|
||||
|
||||
GDT.0.load();
|
||||
unsafe {
|
||||
set_cs(GDT.1.code_selector);
|
||||
load_tss(GDT.1.tss_selector);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
[`set_cs`]を使ってコードセグメントレジスタを再読み込みして、[`load_tss`]を使ってTSSを読み込んでいます。これらの関数は`unsafe`とマークされているので、呼び出すには`unsafe`ブロックが必要です。`unsafe`なのは、不正なセレクタを読み込むことでメモリ安全性を壊す可能性があるからです。
|
||||
|
||||
[`set_cs`]: https://docs.rs/x86_64/0.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内のダブルフォルトハンドラにスタックインデックスをセットすることができます:
|
||||
|
||||
```rust
|
||||
// in src/interrupts.rs
|
||||
|
||||
use crate::gdt;
|
||||
|
||||
lazy_static! {
|
||||
static ref IDT: InterruptDescriptorTable = {
|
||||
let mut idt = InterruptDescriptorTable::new();
|
||||
idt.breakpoint.set_handler_fn(breakpoint_handler);
|
||||
unsafe {
|
||||
idt.double_fault.set_handler_fn(double_fault_handler)
|
||||
.set_stack_index(gdt::DOUBLE_FAULT_IST_INDEX); // new
|
||||
}
|
||||
|
||||
idt
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
`set_stack_index`メソッドは呼び出し側が、使われているインデックスが正しいものであり、かつ他の例外で使われていないかを確かめる必要があるため、`unsafe`です。
|
||||
|
||||
これで全部です。CPUはダブルフォルトが発生したら常にダブルフォルトスタックに切り替えるでしょう。よって、私達はカーネルスタックオーバーフローを含む**すべての**ダブルフォルトをキャッチすることができます。
|
||||
|
||||

|
||||
|
||||
これからはトリプルフォルトを見ることは二度とないでしょう。これらダブルフォルトのための実装を誤って壊していないことを保証するために、テストを追加しましょう。
|
||||
|
||||
## スタックオーバーフローテスト
|
||||
|
||||
新しい`gdt`モジュールをテストしダブルフォルトハンドラがスタックオーバーフローで正しく呼ばれることを保証するために、結合テストを追加します。ここでの考えは、テスト関数内でダブルフォルトを引き起こしダブルフォルトハンドラが呼び出されていることを確かめる、というものです。
|
||||
|
||||
最小の骨組みから始めましょう:
|
||||
|
||||
```rust
|
||||
// in tests/stack_overflow.rs
|
||||
|
||||
#![no_std]
|
||||
#![no_main]
|
||||
|
||||
use core::panic::PanicInfo;
|
||||
|
||||
#[no_mangle]
|
||||
pub extern "C" fn _start() -> ! {
|
||||
unimplemented!();
|
||||
}
|
||||
|
||||
#[panic_handler]
|
||||
fn panic(info: &PanicInfo) -> ! {
|
||||
blog_os::test_panic_handler(info)
|
||||
}
|
||||
```
|
||||
|
||||
`panic_handler`のテストと同様、テストは[テストハーネスなし]で実行されます。理由は私達はダブルフォルト後に実行を続けることができず、2つ以上のテストは意味をなさないためです。テストハーネスを無効にするために、以下を`Cargo.toml`に追加します:
|
||||
|
||||
```toml
|
||||
# in Cargo.toml
|
||||
|
||||
[[test]]
|
||||
name = "stack_overflow"
|
||||
harness = false
|
||||
```
|
||||
|
||||
[テストハーネスなし]: @/edition-2/posts/04-testing/index.ja.md#hanesu-harness-nonaitesuto
|
||||
|
||||
これで`cargo test --test stack_overflow`でのコンパイルは成功するでしょう。`unimplemented`マクロがパニックを起こすため、テストはもちろん失敗します。
|
||||
|
||||
### `_start`を実装する
|
||||
|
||||
`_start`関数の実装はこのようになります:
|
||||
|
||||
```rust
|
||||
// in tests/stack_overflow.rs
|
||||
|
||||
use blog_os::serial_print;
|
||||
|
||||
#[no_mangle]
|
||||
pub extern "C" fn _start() -> ! {
|
||||
serial_print!("stack_overflow::stack_overflow...\t");
|
||||
|
||||
blog_os::gdt::init();
|
||||
init_test_idt();
|
||||
|
||||
// スタックオーバーフローを起こす
|
||||
stack_overflow();
|
||||
|
||||
panic!("Execution continued after stack overflow");
|
||||
}
|
||||
|
||||
#[allow(unconditional_recursion)]
|
||||
fn stack_overflow() {
|
||||
stack_overflow(); // 再帰のたびにリターンアドレスがプッシュされる
|
||||
volatile::Volatile::new(0).read(); // 末尾最適化を防ぐ
|
||||
}
|
||||
```
|
||||
|
||||
新しいGDTを初期化するために`gdt::init`関数を呼びます。そして`interrupts::init_idt`関数を呼び出す代わりに、すぐ後で説明する`init_test_idt`関数を呼びます。なぜなら、私達はパニックの代わりに`exit_qemu(QemuExitCode::Success)`を実行するカスタムしたダブルフォルトハンドラを登録したいためです。
|
||||
|
||||
`stack_overflow`関数は`main.rs`の中にある関数とほとんど同じです。唯一の違いは[**末尾呼び出し最適化**]と呼ばれるコンパイラの最適化を防ぐために[`Volatile`]タイプを使って関数の末尾で追加の[volatile]読み込みを行っていることです。この最適化の特徴として、コンパイラが、最後の文が再帰関数呼び出しである関数を通常のループに変換できるようになる、というものがあります。その結果として、追加のスタックフレームが関数呼び出しではつくられず、スタックの使用量が変わらないままとなります。
|
||||
|
||||
[volatile]: https://en.wikipedia.org/wiki/Volatile_(computer_programming)
|
||||
[`Volatile`]: https://docs.rs/volatile/0.2.6/volatile/struct.Volatile.html
|
||||
[**末尾呼び出し最適化**]: https://ja.wikipedia.org/wiki/末尾再帰#末尾呼出し最適化
|
||||
|
||||
しかし、ここではスタックオーバーフローを起こしたいので、コンパイラに削除されない、ダミーのvolatile読み込み文を関数の末尾に追加します。その結果、関数は**末尾再帰**ではなくなり、ループへの変換は防がれます。更に関数が無限に再帰することに対するコンパイラの警告をなくすために`allow(unconditional_recursion)`属性を追加します。
|
||||
|
||||
### IDTのテスト
|
||||
|
||||
上で述べたように、テストはカスタムしたダブルフォルトハンドラを含む専用のIDTが必要です。実装はこのようになります:
|
||||
|
||||
```rust
|
||||
// in tests/stack_overflow.rs
|
||||
|
||||
use lazy_static::lazy_static;
|
||||
use x86_64::structures::idt::InterruptDescriptorTable;
|
||||
|
||||
lazy_static! {
|
||||
static ref TEST_IDT: InterruptDescriptorTable = {
|
||||
let mut idt = InterruptDescriptorTable::new();
|
||||
unsafe {
|
||||
idt.double_fault
|
||||
.set_handler_fn(test_double_fault_handler)
|
||||
.set_stack_index(blog_os::gdt::DOUBLE_FAULT_IST_INDEX);
|
||||
}
|
||||
|
||||
idt
|
||||
};
|
||||
}
|
||||
|
||||
pub fn init_test_idt() {
|
||||
TEST_IDT.load();
|
||||
}
|
||||
```
|
||||
|
||||
実装は`interrupts.rs`内の通常のIDTと非常に似ています。通常のIDT同様、分離されたスタックに切り替えるようダブルフォルトハンドラ用のISTにスタックインデックスをセットします。`init_test_idt`関数は`load`メソッドによりCPU上にIDTを読み込みます。
|
||||
|
||||
### ダブルフォルトハンドラ
|
||||
|
||||
唯一欠けているのはダブルフォルトハンドラです。このようになります:
|
||||
|
||||
```rust
|
||||
// in tests/stack_overflow.rs
|
||||
|
||||
use blog_os::{exit_qemu, QemuExitCode, serial_println};
|
||||
use x86_64::structures::idt::InterruptStackFrame;
|
||||
|
||||
extern "x86-interrupt" fn test_double_fault_handler(
|
||||
_stack_frame: InterruptStackFrame,
|
||||
_error_code: u64,
|
||||
) -> ! {
|
||||
serial_println!("[ok]");
|
||||
exit_qemu(QemuExitCode::Success);
|
||||
loop {}
|
||||
}
|
||||
```
|
||||
|
||||
ダブルフォルトハンドラが呼ばれるとき、私達はQEMUを正常な終了コードで終了し、テストを成功とマークします。結合テストは完全に分けられた実行ファイルなので、私達はテストファイルの先頭で`#![feature(abi_x86_interrupt)]`属性を再びセットする必要があります。
|
||||
|
||||
これで私達は`cargo test --test stack_overflow`(もしくは全部のテストを走らせるよう`cargo test`)でテストを走らせることができます。期待していたとおり、`stack_overflow... [ok]`とコンソールに出力されるのがわかります。`set_stack_index`の行をコメントアウトすると、テストは失敗するでしょう。
|
||||
|
||||
## まとめ
|
||||
この記事では私達はダブルフォルトが何であるかとどういう条件下で発生するかを学びました。エラーメッセージを出力する基本的なダブルフォルトハンドラと、そのための結合テストを追加しました。
|
||||
|
||||
また、私達はスタックオーバーフロー下でも動くよう、ダブルフォルト発生時にハードウェアがサポートするスタック切り替えを行うようにしました。実装していく中で、古いアーキテクチャでのセグメンテーションで使われていたタスクステートセグメント(TSS)、割り込みスタックテーブル(IST)、グローバルディスクリプタテーブル(GDT)についても学びました。
|
||||
|
||||
## 次は?
|
||||
次の記事ではタイマーやキーボード、ネットワークコントローラのような、外部デバイスからの割り込みをどのように処理するかを説明します。これらのハードウェア割り込みは例外によく似ています。例えば、これらもIDTからディスパッチされます。しかしながら、例外とは違い、それらはCPU上で直接発生するものではありません。代わりに、**割り込みコントローラ**がこれらの割り込みを集めて、優先度によってそれらをCPUに送ります。次回、私達は[Intel 8259](PIC)割り込みコントローラを調べ、どのようにキーボードのサポートを実装するかを学びます。
|
||||
|
||||
[Intel 8259]: https://ja.wikipedia.org/wiki/Intel_8259
|
||||
@@ -1,739 +0,0 @@
|
||||
+++
|
||||
title = "وقفههای سختافزاری"
|
||||
weight = 7
|
||||
path = "fa/hardware-interrupts"
|
||||
date = 2018-10-22
|
||||
|
||||
[extra]
|
||||
chapter = "Interrupts"
|
||||
# Please update this when updating the translation
|
||||
translation_based_on_commit = "b6ff79ac3290ea92c86763d49cc6c0ff4fb0ea30"
|
||||
# GitHub usernames of the people that translated this post
|
||||
translators = ["hamidrezakp", "MHBahrampour"]
|
||||
rtl = true
|
||||
+++
|
||||
|
||||
در این پست ما کنترل کننده قابل برنامه ریزی وقفه را تنظیم می کنیم تا وقفه های سخت افزاری را به درستی به پردازنده منتقل کند. برای مدیریت این وقفهها ، موارد جدیدی به جدول توصیف کننده وقفه اضافه می کنیم ، دقیقاً مانند کارهایی که برای کنترل کننده های استثنا انجام دادیم. ما یاد خواهیم گرفت که چگونه وقفه های متناوب تایمر را گرفته و چگونه از صفحه کلید ورودی بگیریم.
|
||||
|
||||
<!-- more -->
|
||||
|
||||
این بلاگ بصورت آزاد بر روی [گیتهاب] توسعه داده شده. اگر مشکل یا سوالی دارید، لطفاً آنجا یک ایشو باز کنید. همچنین میتوانید [در زیر] این پست کامنت بگذارید. سورس کد کامل این پست را میتوانید در بِرَنچ [`post-07`][post branch] پیدا کنید.
|
||||
|
||||
[گیتهاب]: https://github.com/phil-opp/blog_os
|
||||
[در زیر]: #comments
|
||||
<!-- fix for zola anchor checker (target is in template): <a id="comments"> -->
|
||||
[post branch]: https://github.com/phil-opp/blog_os/tree/post-07
|
||||
|
||||
<!-- toc -->
|
||||
|
||||
## مقدمه
|
||||
|
||||
وقفهها راهی برای اطلاع به پردازنده از دستگاه های سخت افزاری متصل ارائه می دهند. بنابراین به جای اینکه پردازنده به طور دورهای صفحه کلید را برای کاراکترهای جدید بررسی کند(فرآیندی به نام [_polling_]) ، صفحه کلید میتواند هسته را برای هر فشردن کلید مطلع کند. این بسیار کارآمدتر است زیرا هسته فقط زمانی که اتفاقی افتاده است باید عمل کند. همچنین زمان واکنش سریع تری را فراهم می کند ، زیرا هسته می تواند بلافاصله و نه تنها در پول(کلمه: poll) بعدی واکنش نشان دهد.
|
||||
|
||||
[_polling_]: https://en.wikipedia.org/wiki/Polling_(computer_science)
|
||||
|
||||
اتصال مستقیم تمام دستگاه های سخت افزاری به پردازنده امکان پذیر نیست. در عوض ، یک _کنترل کننده وقفه_ جداگانه ، وقفهها را از همه دستگاهها جمع کرده و سپس پردازنده را مطلع می کند:
|
||||
|
||||
```
|
||||
____________ _____
|
||||
Timer ------------> | | | |
|
||||
Keyboard ---------> | Interrupt |---------> | CPU |
|
||||
Other Hardware ---> | Controller | |_____|
|
||||
Etc. -------------> |____________|
|
||||
|
||||
```
|
||||
|
||||
بیشتر کنترل کننده های وقفه قابل برنامه ریزی هستند ، به این معنی که آنها از اولویت های مختلف برای وقفهها پشتیبانی می کنند. به عنوان مثال ، این اجازه را می دهند تا به وقفه های تایمر اولویت بیشتری نسبت به وقفه های صفحه کلید داد تا از زمان بندی دقیق اطمینان حاصل شود.
|
||||
|
||||
بر خلاف استثناها ، وقفه های سخت افزاری _به صورت نا هم زمان_ اتفاق می افتند. این بدان معنی است که آنها کاملاً از کد اجرا شده مستقل هستند و در هر زمان ممکن است رخ دهند. بنابراین ما ناگهان شکلی از همروندی در هسته خود با تمام اشکالات احتمالی مرتبط با همروندی داریم. مدل مالکیت دقیق راست در اینجا به ما کمک می کند زیرا مانع حالت تغییر پذیری گلوبال است(mutable global state). با این حال، همچنان احتمال بن بست وجود دارد، همانطور که بعداً در این پست خواهیم دید.
|
||||
|
||||
## The 8259 PIC
|
||||
|
||||
[Intel 8259] یک کنترل کننده وقفه قابل برنامه ریزی (PIC) است که در سال 1976 معرفی شد. مدت طولانی است که با [APIC] جدید جایگزین شده است ، اما رابط آن هنوز به دلایل سازگاری در سیستم های فعلی پشتیبانی می شود. 8259 PIC به طور قابل ملاحظه ای آسان تر از APIC است ، بنابراین ما قبل از مهاجرت و استفاده از APIC در آینده، از آن برای معرفی وقفه استفاده خواهیم کرد.
|
||||
|
||||
[APIC]: https://en.wikipedia.org/wiki/Intel_APIC_Architecture
|
||||
|
||||
8259 دارای 8 خط وقفه و چندین خط برای برقراری ارتباط با پردازنده است. سیستم های معمولی در آن زمان به دو نمونه از 8259 PIC مجهز بودند ، یکی اصلی و دیگری PIC ثانویه که به یکی از خطوط وقفه اولیه متصل است:
|
||||
|
||||
[Intel 8259]: https://en.wikipedia.org/wiki/Intel_8259
|
||||
|
||||
```
|
||||
____________ ____________
|
||||
Real Time Clock --> | | Timer -------------> | |
|
||||
ACPI -------------> | | Keyboard-----------> | | _____
|
||||
Available --------> | Secondary |----------------------> | Primary | | |
|
||||
Available --------> | Interrupt | Serial Port 2 -----> | Interrupt |---> | CPU |
|
||||
Mouse ------------> | Controller | Serial Port 1 -----> | Controller | |_____|
|
||||
Co-Processor -----> | | Parallel Port 2/3 -> | |
|
||||
Primary ATA ------> | | Floppy disk -------> | |
|
||||
Secondary ATA ----> |____________| Parallel Port 1----> |____________|
|
||||
|
||||
```
|
||||
|
||||
این نمودار نحوه اتصال معمول خطوط وقفه را نشان می دهد. می بینیم که بیشتر 15 خط دارای یک نگاشت ثابت هستند ، به عنوان مثال خط 4 PIC ثانویه به ماوس اختصاص داده شده است.
|
||||
|
||||
هر کنترل کننده را می توان از طریق دو [پورت ورودی/خروجی] ، یک پورت "فرمان" و یک پورت "داده" پیکربندی کرد. برای کنترل کننده اصلی ، این پورتها `0x20` (فرمان) و`0x21` (داده) هستند. برای کنترل کننده ثانویه آنها `0xa0` (فرمان) و `0xa1` (داده) هستند. برای اطلاعات بیشتر در مورد نحوه پیکربندی PIC ها ، به [مقالهای در osdev.org] مراجعه کنید.
|
||||
|
||||
[پورت ورودی/خروجی]: @/edition-2/posts/04-testing/index.md#i-o-ports
|
||||
[مقالهای در osdev.org]: https://wiki.osdev.org/8259_PIC
|
||||
|
||||
### پیاده سازی
|
||||
|
||||
پیکربندی پیش فرض PIC ها قابل استفاده نیست، زیرا اعداد بردار وقفه را در محدوده 15-0 به پردازنده می فرستد. این اعداد در حال حاضر توسط استثناهای پردازنده اشغال شدهاند ، به عنوان مثال شماره 8 مربوط به یک خطای دوگانه است. برای رفع این مشکل همپوشانی، باید وقفه های PIC را به اعداد دیگری تغییر دهیم. دامنه واقعی مهم نیست به شرطی که با استثناها همپوشانی نداشته باشد ، اما معمولاً محدوده 47-32 انتخاب می شود، زیرا اینها اولین شماره های آزاد پس از 32 اسلات استثنا هستند.
|
||||
|
||||
پیکربندی با نوشتن مقادیر ویژه در پورت های فرمان و داده PIC ها اتفاق می افتد. خوشبختانه قبلا کرتای به نام [`pic8259`] وجود دارد، بنابراین نیازی نیست که توالی راه اندازی اولیه را خودمان بنویسیم. در صورت علاقهمند بودن به چگونگی عملکرد آن، [کد منبع آن][pic crate source] را بررسی کنید، نسبتاً کوچک و دارای مستند خوبی است.
|
||||
|
||||
[pic crate source]: https://docs.rs/crate/pic8259/0.10.1/source/src/lib.rs
|
||||
|
||||
برای افزودن کرت به عنوان وابستگی ، موارد زیر را به پروژه خود اضافه می کنیم:
|
||||
|
||||
[`pic8259`]: https://docs.rs/pic8259/0.10.1/pic8259/
|
||||
|
||||
```toml
|
||||
# in Cargo.toml
|
||||
|
||||
[dependencies]
|
||||
pic8259 = "0.10.1"
|
||||
```
|
||||
|
||||
انتزاع اصلی ارائه شده توسط کرت، ساختمان [`ChainedPics`] است که نمایانگر طرح اولیه/ثانویه PIC است که در بالا دیدیم. برای استفاده به روش زیر طراحی شده است:
|
||||
|
||||
[`ChainedPics`]: https://docs.rs/pic8259/0.10.1/pic8259/struct.ChainedPics.html
|
||||
|
||||
```rust
|
||||
// in src/interrupts.rs
|
||||
|
||||
use pic8259::ChainedPics;
|
||||
use spin;
|
||||
|
||||
pub const PIC_1_OFFSET: u8 = 32;
|
||||
pub const PIC_2_OFFSET: u8 = PIC_1_OFFSET + 8;
|
||||
|
||||
pub static PICS: spin::Mutex<ChainedPics> =
|
||||
spin::Mutex::new(unsafe { ChainedPics::new(PIC_1_OFFSET, PIC_2_OFFSET) });
|
||||
```
|
||||
|
||||
همانطور که در بالا اشاره کردیم، افست PIC ها را در محدوده 47-32 تنظیم می کنیم. با بسته بندی ساختمان `ChainedPics` در `Mutex` می توانیم دسترسی قابل تغییر و ایمن (از طریق [متد lock][spin mutex lock]) به آن داشته باشیم، که در مرحله بعدی به آن نیاز داریم. تابع `ChainedPics::new` ناامن است زیرا افست اشتباه ممکن است باعث رفتار نامشخص شود.
|
||||
|
||||
[spin mutex lock]: https://docs.rs/spin/0.5.2/spin/struct.Mutex.html#method.lock
|
||||
|
||||
اکنون می توانیم 8259 PIC را در تابع `init` خود مقدار دهی اولیه کنیم:
|
||||
|
||||
```rust
|
||||
// in src/lib.rs
|
||||
|
||||
pub fn init() {
|
||||
gdt::init();
|
||||
interrupts::init_idt();
|
||||
unsafe { interrupts::PICS.lock().initialize() }; // new
|
||||
}
|
||||
```
|
||||
|
||||
ما از تابع [`initialize`] برای انجام مقداردهی اولیه PIC استفاده می کنیم. مانند تابع `ChainedPics::new`، این تابع نیز ایمن نیست زیرا در صورت عدم پیکربندی صحیح PIC می تواند باعث رفتار نامشخص شود.
|
||||
|
||||
[`initialize`]: https://docs.rs/pic8259/0.10.1/pic8259/struct.ChainedPics.html#method.initialize
|
||||
|
||||
اگر همه چیز خوب پیش برود ، باید هنگام اجرای `cargo run` پیام "It did not crash" را ببینیم.
|
||||
|
||||
## فعالسازی وقفهها
|
||||
|
||||
تاکنون هیچ اتفاقی نیفتاده است زیرا وقفهها همچنان در تنظیمات پردازنده غیرفعال هستند. این بدان معناست که پردازنده به هیچ وجه به کنترل کننده وقفه گوش نمی دهد، بنابراین هیچ وقفه ای نمی تواند به پردازنده برسد. بیایید این را تغییر دهیم:
|
||||
|
||||
```rust
|
||||
// in src/lib.rs
|
||||
|
||||
pub fn init() {
|
||||
gdt::init();
|
||||
interrupts::init_idt();
|
||||
unsafe { interrupts::PICS.lock().initialize() };
|
||||
x86_64::instructions::interrupts::enable(); // new
|
||||
}
|
||||
```
|
||||
|
||||
تابع `interrupts::enable` از کرت `x86_64` دستورالعمل خاص `sti` را اجرا می کند (“set interrupts”) تا وقفه های خارجی را فعال کند. اکنون وقتی `cargo run` را امتحان می کنیم ، می بینیم که یک خطای دوگانه رخ میدهد:
|
||||
|
||||

|
||||
|
||||
دلیل این خطای دوگانه این است که تایمر سخت افزاری (به طور دقیق تر [Intel 8253]) به طور پیش فرض فعال است، بنابراین به محض فعال کردن وقفهها ، شروع به دریافت وقفه های تایمر می کنیم. از آنجا که هنوز یک تابع کنترل کننده برای آن تعریف نکردهایم ، کنترل کننده خطای دوگانه فراخوانی می شود.
|
||||
|
||||
[Intel 8253]: https://en.wikipedia.org/wiki/Intel_8253
|
||||
|
||||
## مدیریت وقفههای تایمر
|
||||
|
||||
همانطور که در شکل [بالا](#the-8259-pic) می بینیم، تایمر از خط 0 از PIC اصلی استفاده می کند. این به این معنی است که به صورت وقفه 32 (0 + افست 32) به پردازنده می رسد. به جای هارد-کد(Hardcode) کردن 32، آن را در یک اینام(enum) به نام `InterruptIndex` ذخیره می کنیم:
|
||||
|
||||
```rust
|
||||
// in src/interrupts.rs
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
#[repr(u8)]
|
||||
pub enum InterruptIndex {
|
||||
Timer = PIC_1_OFFSET,
|
||||
}
|
||||
|
||||
impl InterruptIndex {
|
||||
fn as_u8(self) -> u8 {
|
||||
self as u8
|
||||
}
|
||||
|
||||
fn as_usize(self) -> usize {
|
||||
usize::from(self.as_u8())
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
اینام یک [اینام C مانند] است بنابراین ما می توانیم ایندکس را برای هر نوع به طور مستقیم مشخص کنیم. ویژگی `repr(u8)` مشخص می کند که هر نوع به عنوان `u8` نشان داده می شود. در آینده انواع بیشتری برای وقفه های دیگر اضافه خواهیم کرد.
|
||||
|
||||
[اینام C مانند]: https://doc.rust-lang.org/reference/items/enumerations.html#custom-discriminant-values-for-fieldless-enumerations
|
||||
|
||||
اکنون می توانیم یک تابع کنترل کننده برای وقفه تایمر اضافه کنیم:
|
||||
|
||||
```rust
|
||||
// in src/interrupts.rs
|
||||
|
||||
use crate::print;
|
||||
|
||||
lazy_static! {
|
||||
static ref IDT: InterruptDescriptorTable = {
|
||||
let mut idt = InterruptDescriptorTable::new();
|
||||
idt.breakpoint.set_handler_fn(breakpoint_handler);
|
||||
[…]
|
||||
idt[InterruptIndex::Timer.as_usize()]
|
||||
.set_handler_fn(timer_interrupt_handler); // new
|
||||
|
||||
idt
|
||||
};
|
||||
}
|
||||
|
||||
extern "x86-interrupt" fn timer_interrupt_handler(
|
||||
_stack_frame: InterruptStackFrame)
|
||||
{
|
||||
print!(".");
|
||||
}
|
||||
```
|
||||
|
||||
`timer_interrupt_handler` ما دارای امضای مشابه کنترل کننده های استثنای ما است ، زیرا پردازنده به طور یکسان به استثناها و وقفه های خارجی واکنش نشان می دهد (تنها تفاوت این است که برخی از استثناها کد خطا را در پشته ذخیره میکنند). ساختمان [`InterruptDescriptorTable`] تریت [`IndexMut`] را پیاده سازی می کند، بنابراین می توانیم از طریق سینتکس ایندکسدهی آرایه، به ایتم های جداگانه دسترسی پیدا کنیم.
|
||||
|
||||
[`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
|
||||
|
||||
در کنترل کننده وقفه تایمر، یک نقطه را روی صفحه چاپ می کنیم. همانطور که وقفه تایمر به صورت دوره ای اتفاق می افتد ، انتظار داریم که در هر تیک تایمر یک نقطه ظاهر شود. با این حال، هنگامی که آن را اجرا می کنیم می بینیم که فقط یک نقطه چاپ می شود:
|
||||
|
||||

|
||||
|
||||
### پایان وقفه
|
||||
|
||||
دلیل این امر این است که PIC انتظار دارد یک سیگنال صریح "پایان وقفه" (EOI) از کنترل کننده وقفه ما دریافت کند. این سیگنال به PIC می گوید که وقفه پردازش شده و سیستم آماده دریافت وقفه بعدی است. بنابراین PIC فکر می کند ما هنوز مشغول پردازش وقفه تایمر اول هستیم و قبل از ارسال سیگنال بعدی با صبر و حوصله منتظر سیگنال EOI از ما هست.
|
||||
|
||||
برای ارسال EOI ، ما دوباره از ساختمان ثابت `PICS` خود استفاده می کنیم:
|
||||
|
||||
```rust
|
||||
// in src/interrupts.rs
|
||||
|
||||
extern "x86-interrupt" fn timer_interrupt_handler(
|
||||
_stack_frame: InterruptStackFrame)
|
||||
{
|
||||
print!(".");
|
||||
|
||||
unsafe {
|
||||
PICS.lock()
|
||||
.notify_end_of_interrupt(InterruptIndex::Timer.as_u8());
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
`notify_end_of_interrupt` تشخیص میدهد که PIC اصلی یا ثانویه وقفه را ارسال کرده است و سپس از پورت های `command` و `data` برای ارسال سیگنال EOI به PIC های مربوطه استفاده می کند. اگر PIC ثانویه وقفه را ارسال کرد ، هر دو PIC باید مطلع شوند زیرا PIC ثانویه به یک خط ورودی از PIC اصلی متصل است.
|
||||
|
||||
ما باید مراقب باشیم که از شماره بردار وقفه صحیح استفاده کنیم، در غیر این صورت می توانیم به طور تصادفی یک وقفه مهم ارسال نشده را حذف کنیم یا باعث هنگ سیستم خود شویم. این دلیل آن است که تابع ناامن است.
|
||||
|
||||
اکنون هنگامی که `cargo run` را اجرا می کنیم، نقاطی را می بینیم که به صورت دوره ای روی صفحه ظاهر می شوند:
|
||||
|
||||

|
||||
|
||||
### پیکربندی تایمر
|
||||
|
||||
تایمر سخت افزاری که ما از آن استفاده می کنیم ، _Programmable Interval Timer_ یا به اختصار PIT نامیده می شود. همانطور که از نام آن مشخص است ، می توان فاصله بین دو وقفه را پیکربندی کرد. ما در اینجا به جزئیات نمی پردازیم زیرا به زودی به [تایمر APIC] سوییچ خواهیم کرد، اما ویکی OSDev مقاله مفصلی درباره [پیکربندی PIT] دارد.
|
||||
|
||||
[تایمر APIC]: https://wiki.osdev.org/APIC_timer
|
||||
[پیکربندی PIT]: https://wiki.osdev.org/Programmable_Interval_Timer
|
||||
|
||||
## بنبست ها
|
||||
|
||||
اکنون نوعی همروندی در هسته خود داریم: وقفه های تایمر به صورت ناهمزمان اتفاق می افتند ، بنابراین می توانند تابع `start_` را در هر زمان قطع کنند. خوشبختانه سیستم مالکیت راست از بسیاری از مشکلات مربوط به همروندی در زمان کامپایل جلوگیری می کند. یک استثنا قابل توجه بنبست است. درصورتی که نخ(Thread) بخواهد قفلی را بدست آورد که هرگز آزاد نخواهد شد، بنبست به وجود می آید. بنابراین نخ به طور نامحدود هنگ میکند.
|
||||
|
||||
ما می توانیم در هسته خود بنبست ایجاد کنیم. اگر به یاد داشته باشید، ماکرو `println` ما تابع `vga_buffer::_print` را فراخوانی می کند، که با استفاده از spinlock یک [`WRITER` گلوبال را قفل میکند][vga spinlock].
|
||||
|
||||
[vga spinlock]: @/edition-2/posts/03-vga-text-buffer/index.md#spinlocks
|
||||
|
||||
```rust
|
||||
// in src/vga_buffer.rs
|
||||
|
||||
[…]
|
||||
|
||||
#[doc(hidden)]
|
||||
pub fn _print(args: fmt::Arguments) {
|
||||
use core::fmt::Write;
|
||||
WRITER.lock().write_fmt(args).unwrap();
|
||||
}
|
||||
```
|
||||
|
||||
`WRITER` را قفل می کند، `write_fmt` را روی آن فراخوانی می کند و در انتهای تابع به طور ضمنی قفل آن را باز می کند. حال تصور کنید که در حالی که `WRITER` قفل شده است وقفه رخ دهد و کنترل کننده وقفه نیز سعی کند چیزی را چاپ کند:
|
||||
|
||||
Timestep | _start | interrupt_handler
|
||||
---------|------|------------------
|
||||
0 | calls `println!` |
|
||||
1 | `print` locks `WRITER` |
|
||||
2 | | **interrupt occurs**, handler begins to run
|
||||
3 | | calls `println!` |
|
||||
4 | | `print` tries to lock `WRITER` (already locked)
|
||||
5 | | `print` tries to lock `WRITER` (already locked)
|
||||
… | | …
|
||||
_never_ | _unlock `WRITER`_ |
|
||||
|
||||
`WRITER` قفل شده است ، بنابراین کنترل کننده وقفه منتظر می ماند تا آزاد شود. اما این هرگز اتفاق نمی افتد ، زیرا تابع `start_` فقط پس از بازگشت کنترل کننده وقفه ادامه می یابد. بنابراین کل سیستم هنگ است.
|
||||
|
||||
### ایجاد بنبست
|
||||
|
||||
ما می توانیم با چاپ چیزی در حلقه در انتهای تابع `start_` خود ، به راحتی چنین بنبستای در هسته خود ایجاد کنیم:
|
||||
|
||||
```rust
|
||||
// in src/main.rs
|
||||
|
||||
#[no_mangle]
|
||||
pub extern "C" fn _start() -> ! {
|
||||
[…]
|
||||
loop {
|
||||
use blog_os::print;
|
||||
print!("-"); // new
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
وقتی آن را در QEMU اجرا می کنیم ، خروجی به حالت زیر دریافت میکنیم:
|
||||
|
||||

|
||||
|
||||
می بینیم که فقط تعداد محدودی خط فاصله ، تا زمانی که وقفه تایمر اول اتفاق بیفتد، چاپ می شود. سپس سیستم هنگ میکند زیرا تایمر هنگام تلاش برای چاپ یک نقطه باعث بنبست میشود. به همین دلیل است که در خروجی فوق هیچ نقطهای مشاهده نمیکنیم.
|
||||
|
||||
تعداد واقعی خط فاصله بین هر اجرا متفاوت است زیرا وقفه تایمر به صورت غیر همزمان انجام می شود. این عدم قطعیت، اشکال زدایی اشکالات مربوط به همروندی را بسیار دشوار می کند.
|
||||
|
||||
### رفع بنبست
|
||||
|
||||
برای جلوگیری از این بنبست ، تا زمانی که `Mutex` قفل شده باشد، می توانیم وقفهها را غیرفعال کنیم:
|
||||
|
||||
```rust
|
||||
// in src/vga_buffer.rs
|
||||
|
||||
/// Prints the given formatted string to the VGA text buffer
|
||||
/// through the global `WRITER` instance.
|
||||
#[doc(hidden)]
|
||||
pub fn _print(args: fmt::Arguments) {
|
||||
use core::fmt::Write;
|
||||
use x86_64::instructions::interrupts; // new
|
||||
|
||||
interrupts::without_interrupts(|| { // new
|
||||
WRITER.lock().write_fmt(args).unwrap();
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
تابع [`without_interrupts`] یک [کلوژر] را گرفته و آن را در یک محیط بدون وقفه اجرا می کند. ما از آن استفاده می کنیم تا اطمینان حاصل کنیم که تا زمانی که `Mutex` قفل شده است ، هیچ وقفه ای رخ نمی دهد. اکنون هنگامی که هسته را اجرا می کنیم ، می بینیم که آن بدون هنگ کردن به کار خود ادامه می دهد. (ما هنوز هیچ نقطه ای را مشاهده نمی کنیم ، اما این به این دلیل است که سرعت حرکت آنها بسیار سریع است. سعی کنید سرعت چاپ را کم کنید، مثلاً با قرار دادن `for _ in 0..10000 {}` در داخل حلقه.)
|
||||
|
||||
[`without_interrupts`]: https://docs.rs/x86_64/0.14.2/x86_64/instructions/interrupts/fn.without_interrupts.html
|
||||
[کلوژر]: https://doc.rust-lang.org/book/second-edition/ch13-01-closures.html
|
||||
|
||||
ما می توانیم همین تغییر را در تابع چاپ سریال نیز اعمال کنیم تا اطمینان حاصل کنیم که هیچ بنبستی در آن رخ نمی دهد:
|
||||
|
||||
```rust
|
||||
// in src/serial.rs
|
||||
|
||||
#[doc(hidden)]
|
||||
pub fn _print(args: ::core::fmt::Arguments) {
|
||||
use core::fmt::Write;
|
||||
use x86_64::instructions::interrupts; // new
|
||||
|
||||
interrupts::without_interrupts(|| { // new
|
||||
SERIAL1
|
||||
.lock()
|
||||
.write_fmt(args)
|
||||
.expect("Printing to serial failed");
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
توجه داشته باشید که غیرفعال کردن وقفهها نباید یک راه حل کلی باشد. مشکل این است که بدترین حالت تأخیر در وقفه را افزایش می دهد ، یعنی زمانی که سیستم به وقفه واکنش نشان می دهد. بنابراین وقفهها باید فقط برای مدت زمان کوتاه غیرفعال شوند.
|
||||
|
||||
## رفع وضعیت رقابتی
|
||||
|
||||
اگر `cargo test` را اجرا کنید ، ممکن است ببینید تست `test_println_output` با شکست مواجه میشود:
|
||||
|
||||
```
|
||||
> cargo test --lib
|
||||
[…]
|
||||
Running 4 tests
|
||||
test_breakpoint_exception...[ok]
|
||||
test_println... [ok]
|
||||
test_println_many... [ok]
|
||||
test_println_output... [failed]
|
||||
|
||||
Error: panicked at 'assertion failed: `(left == right)`
|
||||
left: `'.'`,
|
||||
right: `'S'`', src/vga_buffer.rs:205:9
|
||||
```
|
||||
|
||||
دلیل آن وجود یک _وضعیت رقابتی_ بین تست و کنترل کننده تایمر ماست. اگر به یاد داشته باشید ، تست به این شکل است:
|
||||
|
||||
```rust
|
||||
// in src/vga_buffer.rs
|
||||
|
||||
#[test_case]
|
||||
fn test_println_output() {
|
||||
let s = "Some test string that fits on a single line";
|
||||
println!("{}", s);
|
||||
for (i, c) in s.chars().enumerate() {
|
||||
let screen_char = WRITER.lock().buffer.chars[BUFFER_HEIGHT - 2][i].read();
|
||||
assert_eq!(char::from(screen_char.ascii_character), c);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
این تست یک رشته را در بافر VGA چاپ می کند و سپس با پیمایش دستی روی آرایه `buffer_chars` خروجی را بررسی می کند. وضعیت رقابتی رخ می دهد زیرا ممکن است کنترل کننده وقفه تایمر بین `println` و خواندن کاراکتر های صفحه اجرا شود. توجه داشته باشید که این یک رقابت داده(Data race) خطرناک نیست، که Rust در زمان کامپایل کاملاً از آن جلوگیری کند. برای جزئیات به [_Rustonomicon_][nomicon-races] مراجعه کنید.
|
||||
|
||||
[nomicon-races]: https://doc.rust-lang.org/nomicon/races.html
|
||||
|
||||
برای رفع این مشکل ، باید `WRITER` را برای مدت زمان کامل تست قفل نگه داریم ، به این ترتیب که کنترل کننده تایمر نمی تواند `.` را روی صفحه نمایش در میان کار تست بنویسد. تست اصلاح شده به این شکل است:
|
||||
|
||||
```rust
|
||||
// in src/vga_buffer.rs
|
||||
|
||||
#[test_case]
|
||||
fn test_println_output() {
|
||||
use core::fmt::Write;
|
||||
use x86_64::instructions::interrupts;
|
||||
|
||||
let s = "Some test string that fits on a single line";
|
||||
interrupts::without_interrupts(|| {
|
||||
let mut writer = WRITER.lock();
|
||||
writeln!(writer, "\n{}", s).expect("writeln failed");
|
||||
for (i, c) in s.chars().enumerate() {
|
||||
let screen_char = writer.buffer.chars[BUFFER_HEIGHT - 2][i].read();
|
||||
assert_eq!(char::from(screen_char.ascii_character), c);
|
||||
}
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
ما تغییرات زیر را انجام دادیم:
|
||||
|
||||
- ما با استفاده صریح از متد `()lock` ، نویسنده را برای کل تست قفل می کنیم. به جای `println` ، از ماکرو [`writeln`] استفاده می کنیم که امکان چاپ بر روی نویسنده قبلاً قفل شده را فراهم می کند.
|
||||
- برای جلوگیری از یک بنبست دیگر ، وقفهها را برای مدت زمان تست غیرفعال می کنیم. در غیر این صورت ممکن است تست در حالی که نویسنده هنوز قفل است قطع شود.
|
||||
- از آنجا که کنترل کننده وقفه تایمر هنوز می تواند قبل از تست اجرا شود ، قبل از چاپ رشته `s` یک خط جدید `n\` اضافی چاپ می کنیم. به این ترتیب ، اگر که کنترل کننده تایمر تعدادی کاراکتر `.` را در خط فعلی چاپ کرده باشد، از شکست تست جلوگیری می کنیم.
|
||||
|
||||
[`writeln`]: https://doc.rust-lang.org/core/macro.writeln.html
|
||||
|
||||
اکنون با تغییرات فوق ، `cargo test` دوباره با قطعیت موفق می شود.
|
||||
|
||||
این یک وضعیت رقابتی بسیار بی خطر بود که فقط باعث شکست تست میشد. همانطور که می توانید تصور کنید، اشکال زدایی سایر وضعیتهای رقابتی به دلیل ماهیت غیر قطعی بودن آنها بسیار دشوارتر است. خوشبختانه، راست مانع از رقابت دادهها می شود ، که جدیترین نوع وضعیت رقابتی است ، زیرا می تواند باعث انواع رفتارهای تعریف نشده ، از جمله کرش کردن سیستم و خراب شدن آرام و بی صدای حافظه شود.
|
||||
|
||||
## دستورالعمل `hlt`
|
||||
|
||||
تاکنون از یک حلقه خالی ساده در پایان توابع `start_` و` panic` استفاده می کردیم. این باعث می شود پردازنده به طور بی وقفه بچرخد و بنابراین مطابق انتظار عمل می کند. اما بسیار ناکارآمد است، زیرا پردازنده همچنان با سرعت کامل کار می کند حتی اگر کاری برای انجام نداشته باشد. هنگامی که هسته را اجرا می کنید می توانید این مشکل را در مدیر وظیفه خود مشاهده کنید: فرایند QEMU در کل مدت زمان نیاز به تقریباً 100٪ پردازنده دارد.
|
||||
|
||||
کاری که واقعاً می خواهیم انجام دهیم این است که پردازنده را تا رسیدن وقفه بعدی متوقف کنیم. این اجازه می دهد پردازنده وارد حالت خواب شود که در آن انرژی بسیار کمتری مصرف می کند. [دستورالعمل `hlt`] دقیقاً همین کار را می کند. بیایید از این دستورالعمل برای ایجاد یک حلقه بی پایان با مصرف انرژی پایین استفاده کنیم:
|
||||
|
||||
[دستورالعمل `hlt`]: https://en.wikipedia.org/wiki/HLT_(x86_instruction)
|
||||
|
||||
```rust
|
||||
// in src/lib.rs
|
||||
|
||||
pub fn hlt_loop() -> ! {
|
||||
loop {
|
||||
x86_64::instructions::hlt();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
تابع `instructions::hlt` فقط یک [پوشش نازک] بر روی دستورالعمل اسمبلی است. این بی خطر است زیرا به هیچ وجه نمی تواند ایمنی حافظه را به خطر بیندازد.
|
||||
|
||||
[پوشش نازک]: https://github.com/rust-osdev/x86_64/blob/5e8e218381c5205f5777cb50da3ecac5d7e3b1ab/src/instructions/mod.rs#L16-L22
|
||||
|
||||
اکنون می توانیم از این `hlt_loop` به جای حلقه های بی پایان در توابع` start_` و `panic` استفاده کنیم:
|
||||
|
||||
```rust
|
||||
// in src/main.rs
|
||||
|
||||
#[no_mangle]
|
||||
pub extern "C" fn _start() -> ! {
|
||||
[…]
|
||||
|
||||
println!("It did not crash!");
|
||||
blog_os::hlt_loop(); // new
|
||||
}
|
||||
|
||||
|
||||
#[cfg(not(test))]
|
||||
#[panic_handler]
|
||||
fn panic(info: &PanicInfo) -> ! {
|
||||
println!("{}", info);
|
||||
blog_os::hlt_loop(); // new
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
بیایید `lib.rs` را نیز به روز کنیم:
|
||||
|
||||
```rust
|
||||
// in src/lib.rs
|
||||
|
||||
/// Entry point for `cargo test`
|
||||
#[cfg(test)]
|
||||
#[no_mangle]
|
||||
pub extern "C" fn _start() -> ! {
|
||||
init();
|
||||
test_main();
|
||||
hlt_loop(); // new
|
||||
}
|
||||
|
||||
pub fn test_panic_handler(info: &PanicInfo) -> ! {
|
||||
serial_println!("[failed]\n");
|
||||
serial_println!("Error: {}\n", info);
|
||||
exit_qemu(QemuExitCode::Failed);
|
||||
hlt_loop(); // new
|
||||
}
|
||||
```
|
||||
|
||||
اکنون وقتی هسته خود را در QEMU اجرا می کنیم ، شاهد استفاده بسیار کمتری از پردازنده هستیم.
|
||||
|
||||
## ورودی صفحه کلید
|
||||
|
||||
اکنون که قادر به مدیریت وقفه های دستگاه های خارجی هستیم ، سرانجام قادر به پشتیبانی از ورودی صفحه کلید هستیم. این به ما امکان می دهد برای اولین بار با هسته خود تعامل داشته باشیم.
|
||||
|
||||
<aside class="post_aside">
|
||||
|
||||
توجه داشته باشید که ما فقط نحوه مدیریت صفحه کلیدهای [PS/2] را توضیح می دهیم ، نه صفحه کلیدهای USB. هر چند که، مادربورد صفحه کلیدهای USB را به عنوان دستگاه های PS/2 برای پشتیبانی از نرم افزارهای قدیمی تقلید میکند ، بنابراین تا زمانی که هسته ما از USB پشتیبانی نمیکند می توانیم با اطمینان از صفحه کلیدهای USB چشم پوشی کنیم.
|
||||
|
||||
</aside>
|
||||
|
||||
[PS/2]: https://en.wikipedia.org/wiki/PS/2_port
|
||||
|
||||
مانند تایمر سخت افزاری ، کنترل کننده صفحه کلید نیز به طور پیش فرض از قبل فعال شده است. بنابراین با فشار دادن یک کلید ، کنترل کننده صفحه کلید وقفه را به PIC ارسال می کند و آن را به پردازنده منتقل می کند. پردازنده به دنبال یک تابع کنترل کننده در IDT میگردد ، اما ایتم مربوطه خالی است. بنابراین یک خطای دوگانه رخ می دهد.
|
||||
|
||||
پس بیایید یک تایع کنترل کننده برای وقفه صفحه کلید اضافه کنیم. این کاملاً مشابه نحوه تعریف کنترل کننده برای وقفه تایمر است ، فقط از یک شماره وقفه متفاوت استفاده می کند:
|
||||
|
||||
```rust
|
||||
// in src/interrupts.rs
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
#[repr(u8)]
|
||||
pub enum InterruptIndex {
|
||||
Timer = PIC_1_OFFSET,
|
||||
Keyboard, // new
|
||||
}
|
||||
|
||||
lazy_static! {
|
||||
static ref IDT: InterruptDescriptorTable = {
|
||||
let mut idt = InterruptDescriptorTable::new();
|
||||
idt.breakpoint.set_handler_fn(breakpoint_handler);
|
||||
[…]
|
||||
// new
|
||||
idt[InterruptIndex::Keyboard.as_usize()]
|
||||
.set_handler_fn(keyboard_interrupt_handler);
|
||||
|
||||
idt
|
||||
};
|
||||
}
|
||||
|
||||
extern "x86-interrupt" fn keyboard_interrupt_handler(
|
||||
_stack_frame: InterruptStackFrame)
|
||||
{
|
||||
print!("k");
|
||||
|
||||
unsafe {
|
||||
PICS.lock()
|
||||
.notify_end_of_interrupt(InterruptIndex::Keyboard.as_u8());
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
همانطور که در شکل [بالا](#the-8259-pic) مشاهده می کنیم ، صفحه کلید از خط 1 در PIC اصلی استفاده می کند. این به این معنی است که به صورت وقفه 33 (1 + افست 32) به پردازنده می رسد. ما این ایندکس را به عنوان یک نوع جدید `Keyboard` به اینام `InterruptIndex` اضافه می کنیم. نیازی نیست که مقدار را صریحاً مشخص کنیم ، زیرا این مقدار به طور پیش فرض برابر مقدار قبلی بعلاوه یک که 33 نیز می باشد ، هست. در کنترل کننده وقفه ، ما یک `k` چاپ می کنیم و سیگنال پایان وقفه را به کنترل کننده وقفه می فرستیم.
|
||||
|
||||
اکنون می بینیم که وقتی کلید را فشار می دهیم `k` بر روی صفحه ظاهر می شود. با این حال ، این فقط برای اولین کلیدی که فشار می دهیم کار می کند ، حتی اگر به فشار دادن کلیدها ادامه دهیم ، دیگر `k` بر روی صفحه نمایش ظاهر نمی شود. این امر به این دلیل است که کنترل کننده صفحه کلید تا زمانی که اصطلاحاً _scancode_ را نخوانیم ، وقفه دیگری ارسال نمی کند.
|
||||
|
||||
### خواندن اسکنکد ها
|
||||
|
||||
برای اینکه بفهمیم _کدام_ کلید فشار داده شده است ، باید کنترل کننده صفحه کلید را جستجو کنیم. ما این کار را با خواندن از پورت داده کنترل کننده PS/2 ، که [پورت ورودی/خروجی] با شماره `0x60` است ، انجام می دهیم:
|
||||
|
||||
[پورت ورودی/خروجی]: @/edition-2/posts/04-testing/index.md#i-o-ports
|
||||
|
||||
```rust
|
||||
// in src/interrupts.rs
|
||||
|
||||
extern "x86-interrupt" fn keyboard_interrupt_handler(
|
||||
_stack_frame: InterruptStackFrame)
|
||||
{
|
||||
use x86_64::instructions::port::Port;
|
||||
|
||||
let mut port = Port::new(0x60);
|
||||
let scancode: u8 = unsafe { port.read() };
|
||||
print!("{}", scancode);
|
||||
|
||||
unsafe {
|
||||
PICS.lock()
|
||||
.notify_end_of_interrupt(InterruptIndex::Keyboard.as_u8());
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
ما برای خواندن یک بایت از پورت داده صفحه کلید از نوع [`Port`] کرت `x86_64` استفاده میکنیم. این بایت [_اسکن کد_] نامیده می شود و عددی است که کلید فشرده شده / رها شده را نشان می دهد. ما هنوز کاری با اسکن کد انجام نمی دهیم ، فقط آن را روی صفحه چاپ می کنیم:
|
||||
|
||||
[`Port`]: https://docs.rs/x86_64/0.14.2/x86_64/instructions/port/struct.Port.html
|
||||
[_اسکن کد_]: https://en.wikipedia.org/wiki/Scancode
|
||||
|
||||

|
||||
|
||||
تصویر بالا نشان می دهد که من آرام آرام "123" را تایپ می کنم. می بینیم که کلیدهای مجاور دارای اسکن کد مجاور هستند و فشار دادن یک کلید دارای اسکن کد متفاوت با رها کردن آن است. اما چگونه اسکنکدها را دقیقاً به کار اصلی آن کلید ترجمه کنیم؟
|
||||
|
||||
### تفسیر اسکنکد ها
|
||||
سه استاندارد مختلف برای نگاشت بین اسکن کدها و کلیدها وجود دارد ، اصطلاحاً _مجموعه های اسکن کد_. هر سه به صفحه کلید رایانه های اولیه IBM برمی گردند: [IBM XT] ، [IBM 3270 PC] و [IBM AT]. خوشبختانه رایانه های بعدی روند تعریف مجموعه های جدید اسکن کد را ادامه ندادند ، بلکه مجموعه های موجود را تقلید و آنها را گسترش دادند. امروزه بیشتر صفحه کلیدها را می توان به گونه ای پیکربندی کرد که از هر کدام از سه مجموعه تقلید کند.
|
||||
|
||||
[IBM XT]: https://en.wikipedia.org/wiki/IBM_Personal_Computer_XT
|
||||
[IBM 3270 PC]: https://en.wikipedia.org/wiki/IBM_3270_PC
|
||||
[IBM AT]: https://en.wikipedia.org/wiki/IBM_Personal_Computer/AT
|
||||
|
||||
به طور پیش فرض ، صفحه کلیدهای PS/2 مجموعه شماره 1 ("XT") را تقلید می کنند. در این مجموعه ، 7 بیت پایین بایت اسکنکد، کلید را تعریف می کند و مهمترین بیت فشردن ("0") یا رها کردن ("1") را مشخص می کند. کلیدهایی که در صفحه کلید اصلی [IBM XT] وجود نداشتند ، مانند کلید enter روی کیپد ، دو اسکن کد به طور متوالی ایجاد می کنند: یک بایت فرار(escape) `0xe0` و سپس یک بایت نمایانگر کلید. برای مشاهده لیست تمام اسکنکدهای مجموعه 1 و کلیدهای مربوط به آنها ، [ویکی OSDev][scancode set 1] را مشاهده کنید.
|
||||
|
||||
[scancode set 1]: https://wiki.osdev.org/Keyboard#Scan_Code_Set_1
|
||||
|
||||
برای ترجمه اسکن کدها به کلیدها ، می توانیم از عبارت match استفاده کنیم:
|
||||
|
||||
```rust
|
||||
// in src/interrupts.rs
|
||||
|
||||
extern "x86-interrupt" fn keyboard_interrupt_handler(
|
||||
_stack_frame: InterruptStackFrame)
|
||||
{
|
||||
use x86_64::instructions::port::Port;
|
||||
|
||||
let mut port = Port::new(0x60);
|
||||
let scancode: u8 = unsafe { port.read() };
|
||||
|
||||
// new
|
||||
let key = match scancode {
|
||||
0x02 => Some('1'),
|
||||
0x03 => Some('2'),
|
||||
0x04 => Some('3'),
|
||||
0x05 => Some('4'),
|
||||
0x06 => Some('5'),
|
||||
0x07 => Some('6'),
|
||||
0x08 => Some('7'),
|
||||
0x09 => Some('8'),
|
||||
0x0a => Some('9'),
|
||||
0x0b => Some('0'),
|
||||
_ => None,
|
||||
};
|
||||
if let Some(key) = key {
|
||||
print!("{}", key);
|
||||
}
|
||||
|
||||
unsafe {
|
||||
PICS.lock()
|
||||
.notify_end_of_interrupt(InterruptIndex::Keyboard.as_u8());
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
کد بالا فشردن کلیدهای عددی 9-0 را ترجمه کرده و کلیه کلیدهای دیگر را نادیده می گیرد. از عبارت [match] برای اختصاص یک کاراکتر یا `None` به هر اسکن کد استفاده می کند. سپس با استفاده از [`if let`] اپشن `key` را از بین می برد. با استفاده از همان نام متغیر `key` در الگو که یک روش معمول برای از بین بردن انواع`Option` در راست است تعریف قبلی را [سایه می زنیم].
|
||||
|
||||
[match]: https://doc.rust-lang.org/book/ch06-02-match.html
|
||||
[`if let`]: https://doc.rust-lang.org/book/ch18-01-all-the-places-for-patterns.html#conditional-if-let-expressions
|
||||
[سایه می زنیم]: https://doc.rust-lang.org/book/ch03-01-variables-and-mutability.html#shadowing
|
||||
|
||||
اکنون می توانیم اعداد را بنویسیم:
|
||||
|
||||

|
||||
|
||||
ترجمه کلیدهای دیگر نیز به همین روش کار می کند. خوشبختانه کرت ای با نام [`pc-keyboard`] برای ترجمه اسکنکد مجموعه های اسکنکد 1 و 2 وجود دارد ، بنابراین لازم نیست که خودمان این را پیاده سازی کنیم. برای استفاده از کرت ، آن را به `Cargo.toml` اضافه کرده و در`lib.rs` خود وارد می کنیم:
|
||||
|
||||
[`pc-keyboard`]: https://docs.rs/pc-keyboard/0.5.0/pc_keyboard/
|
||||
|
||||
```toml
|
||||
# in Cargo.toml
|
||||
|
||||
[dependencies]
|
||||
pc-keyboard = "0.5.0"
|
||||
```
|
||||
|
||||
اکنون میتوانیم از این کرت برای باز نویسی `keyboard_interrupt_handler` استفاده کنیم:
|
||||
|
||||
```rust
|
||||
// in/src/interrupts.rs
|
||||
|
||||
extern "x86-interrupt" fn keyboard_interrupt_handler(
|
||||
_stack_frame: InterruptStackFrame)
|
||||
{
|
||||
use pc_keyboard::{layouts, DecodedKey, HandleControl, Keyboard, ScancodeSet1};
|
||||
use spin::Mutex;
|
||||
use x86_64::instructions::port::Port;
|
||||
|
||||
lazy_static! {
|
||||
static ref KEYBOARD: Mutex<Keyboard<layouts::Us104Key, ScancodeSet1>> =
|
||||
Mutex::new(Keyboard::new(layouts::Us104Key, ScancodeSet1,
|
||||
HandleControl::Ignore)
|
||||
);
|
||||
}
|
||||
|
||||
let mut keyboard = KEYBOARD.lock();
|
||||
let mut port = Port::new(0x60);
|
||||
|
||||
let scancode: u8 = unsafe { port.read() };
|
||||
if let Ok(Some(key_event)) = keyboard.add_byte(scancode) {
|
||||
if let Some(key) = keyboard.process_keyevent(key_event) {
|
||||
match key {
|
||||
DecodedKey::Unicode(character) => print!("{}", character),
|
||||
DecodedKey::RawKey(key) => print!("{:?}", key),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
unsafe {
|
||||
PICS.lock()
|
||||
.notify_end_of_interrupt(InterruptIndex::Keyboard.as_u8());
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
ما از ماکرو `lazy_static` برای ایجاد یک شی ثابت [`Keyboard`] محافظت شده توسط Mutex استفاده می کنیم. `Keyboard` را با طرح صفحه کلید ایالات متحده و مجموعه اسکن کد 1 مقداردهی می کنیم. پارامتر [`HandleControl`] اجازه می دهد تا `ctrl+[a-z]` را به کاراکتر های `U+0001` تا `U+001A` نگاشت کنیم. ما نمی خواهیم چنین کاری انجام دهیم ، بنابراین از گزینه `Ignore` برای برخورد با `ctrl` مانند کلیدهای عادی استفاده می کنیم.
|
||||
|
||||
[`HandleControl`]: https://docs.rs/pc-keyboard/0.5.0/pc_keyboard/enum.HandleControl.html
|
||||
|
||||
در هر وقفه ، Mutex را قفل می کنیم ، اسکن کد را از کنترل کننده صفحه کلید می خوانیم و آن را به متد [`add_byte`] منتقل می کنیم ، که اسکن کد را به یک `<Option<KeyEvent` ترجمه می کند. [`KeyEvent`] حاوی كلیدی است كه باعث رویداد شده و آیا این یک رویداد فشردن یا رها کردن بوده است.
|
||||
|
||||
[`Keyboard`]: https://docs.rs/pc-keyboard/0.5.0/pc_keyboard/struct.Keyboard.html
|
||||
[`add_byte`]: https://docs.rs/pc-keyboard/0.5.0/pc_keyboard/struct.Keyboard.html#method.add_byte
|
||||
[`KeyEvent`]: https://docs.rs/pc-keyboard/0.5.0/pc_keyboard/struct.KeyEvent.html
|
||||
|
||||
برای تفسیر این رویداد کلید ، آن را به متد [`process_keyevent`] منتقل می کنیم ، که در صورت امکان رویداد کلید را به یک کاراکتر ترجمه می کند. به عنوان مثال ، بسته به فشردن کلید shift ، یک رویداد فشردن کلید `A` را به یک حرف کوچک `a` یا یک حرف بزرگ `A` ترجمه می کند.
|
||||
|
||||
[`process_keyevent`]: https://docs.rs/pc-keyboard/0.5.0/pc_keyboard/struct.Keyboard.html#method.process_keyevent
|
||||
|
||||
با استفاده از این کنترل کننده وقفه اصلاح شده اکنون می توانیم متن بنویسیم:
|
||||
|
||||

|
||||
|
||||
### پیکربندی صفحه کلید
|
||||
|
||||
امکان پیکربندی برخی از جنبه های صفحه کلید PS/2 وجود دارد، به عنوان مثال از کدام اسکن کد استفاده کند. ما در اینجا به آن نمی پردازیم زیرا این پست به اندازه کافی طولانی شده است ، اما ویکی OSDev دارای نمای کلی [دستورات پیکربندی] ممکن است.
|
||||
|
||||
[دستورات پیکربندی]: https://wiki.osdev.org/PS/2_Keyboard#Commands
|
||||
|
||||
## خلاصه
|
||||
|
||||
در این پست نحوه فعال سازی و مدیریت وقفه های خارجی توضیح داده شد. ما در مورد 8259 PIC و طرح اولیه/ثانویه آن ، نگاشت دوباره اعداد وقفه و سیگنال "پایان وقفه" اطلاعات کسب کردیم. ما کنترل کنندههایی را برای تایمر سخت افزاری و صفحه کلید پیاده سازی کردیم و درباره دستورات `hlt` اطلاعاتی کسب کردیم که پردازنده را تا وقفه بعدی متوقف می کند.
|
||||
|
||||
اکنون ما قادر به تعامل با هسته خود هستیم و برخی از عناصر اساسی برای ایجاد یک پوسته(Shell) کوچک یا بازی های ساده را داریم.
|
||||
|
||||
## مرحله بعدی چیست؟
|
||||
|
||||
وقفه های تایمر برای یک سیستم عامل ضروری است، زیرا راهی برای قطع دوره ای روند اجرا و کنترل مجدد هسته فراهم می کند. سپس هسته می تواند به فرایند دیگری سوییچ کند و این توهم را ایجاد کند که چندین فرآیند به طور موازی اجرا می شوند.
|
||||
|
||||
اما قبل از اینکه بتوانیم فرایندها یا نخها را ایجاد کنیم ، به روشی برای اختصاص حافظه برای آنها نیاز داریم. در پست های بعدی، مدیریت حافظه مورد بررسی قرار می گیرد تا این عنصر اساسی ایجاد شود.
|
||||
@@ -1,421 +0,0 @@
|
||||
+++
|
||||
title = "مقدمهای بر صفحهبندی"
|
||||
weight = 8
|
||||
path = "fa/paging-introduction"
|
||||
date = 2019-01-14
|
||||
|
||||
[extra]
|
||||
chapter = "Memory Management"
|
||||
# Please update this when updating the translation
|
||||
translation_based_on_commit = "f692c5b377460e872bca2d3fcec787f4a0d1ec9b"
|
||||
# GitHub usernames of the people that translated this post
|
||||
translators = ["hamidrezakp", "MHBahrampour"]
|
||||
rtl = true
|
||||
+++
|
||||
|
||||
در این پست _صفحهبندی_، یک طرح مدیریت حافظه بسیار رایج که ما نیز برای سیستمعامل خود استفاده خواهیم کرد، معرفی میشود. این پست توضیح میدهد که چرا ایزوله سازی حافظه مورد نیاز است، قطعهبندی چگونه کار میکند، _حافظه مجازی_ چیست و چگونه صفحهبندی مشکلات تقسیم حافظه را حل می کند. همچنین طرح جدولهای صفحه چند سطحی را در معماری x86_64 بررسی میکند.
|
||||
|
||||
<!-- more -->
|
||||
|
||||
این بلاگ بصورت آزاد روی [گیتهاب] توسعه داده شده است. اگر شما مشکل یا سوالی دارید، لطفاً آنجا یک ایشو باز کنید. شما همچنین میتوانید [در زیر] این پست کامنت بگذارید. منبع کد کامل این پست را میتوانید در بِرَنچ [`post-08`][post branch] پیدا کنید.
|
||||
|
||||
[گیتهاب]: https://github.com/phil-opp/blog_os
|
||||
[در زیر]: #comments
|
||||
<!-- fix for zola anchor checker (target is in template): <a id="comments"> -->
|
||||
[post branch]: https://github.com/phil-opp/blog_os/tree/post-08
|
||||
|
||||
<!-- toc -->
|
||||
|
||||
## محافظت از حافظه
|
||||
|
||||
یکی از وظایف اصلی یک سیستمعامل جداسازی (ایزوله کردن) برنامهها از یکدیگر است. به عنوان مثال، مرورگر وب شما نباید در کار ویرایشگر متن تداخلی ایجاد کند. برای دستیابی به این هدف، سیستمعاملها از قابلیتی سختافزاری استفاده کرده تا اطمینان حاصل کنند که حافظه مربوط به یک پروسه، توسط پروسهای دیگر غیر قابل دسترس است. رویکردهای مختلفی وجود دارد که به سختافزار و پیادهسازی سیستم عامل بستگی دارد.
|
||||
|
||||
به عنوان مثال، برخی از پردازندههای ARM Cortex-M (برای سیستمهای تعبیه شده استفاده میشوند) دارای یک [_واحد محافظت از حافظه_] (Memory Protection Unit: MPU) هستند، که به شما این امکان را میدهد که تعداد کمی از ناحیه حافظه (مانند 8) را با مجوزهای دسترسی متفاوت تعریف کنید (به عنوان مثال عدم دسترسی، فقط خواندنی، خواندنی-نوشتنی). در هر دسترسی به حافظه، MPU اطمینان حاصل میکند که آدرس در ناحیهای با مجوزهای دسترسی صحیح قرار دارد و در غیر اینصورت یک استثنا ایجاد میکند. با تغییر ناحیه و مجوزهای دسترسی در هر تعویض پروسه (ترجمه: process switch)، سیستمعامل میتواند اطمینان حاصل کند که هر پروسه فقط به حافظه خود دسترسی پیدا میکند و بنابراین پروسهها را ایزوله میکند.
|
||||
|
||||
[_واحد محافظت از حافظه_]: https://developer.arm.com/docs/ddi0337/e/memory-protection-unit/about-the-mpu
|
||||
|
||||
در x86، سختافزار از دو روش مختلف برای محافظت از حافظه پشتیبانی میکند: [قطعهبندی] و [صفحهبندی].
|
||||
|
||||
[قطعهبندی]: https://en.wikipedia.org/wiki/X86_memory_segmentation
|
||||
[صفحهبندی]: https://en.wikipedia.org/wiki/Virtual_memory#Paged_virtual_memory
|
||||
|
||||
## قطعهبندی
|
||||
|
||||
قطعهبندی قبلاً در سال 1978 برای افزایش میزان حافظهی آدرس پذیر معرفی شده بود. وضعیت در آن زمان این بود که پردازندهها فقط از آدرسهای 16 بیتی استفاده میکردند که باعث کاهش حافظه آدرس پذیر به 64KiB میشد. برای دسترسی بیشتر از این 64KiB، ثباتهای قطعهی اضافی معرفی شدند که هر کدام حاوی یک offset هستند. پردازنده به طور خودکار این آفست را بر روی هر دسترسی به حافظه اضافه میکند، بنابراین حداکثر ۱ مگابایت حافظه قابل دسترسی است.
|
||||
|
||||
بسته به نوع دسترسی به حافظه، ثبات قطعه به طور خودکار توسط پردازنده انتخاب میشود: برای دستورالعملهای واکشی (ترجمه: fetching)، از کد `CS` و برای عملیاتهای پشته (push/pop) پشته قطعه `SS` استفاده میشود. سایر دستورالعملها ازقطعهی داده `DS` یا قطعهی اضافه `ES` استفاده میکنند. بعدها دو ثبات قطعهی اضافی `FS` و `GS` اضافه شدند که میتوانند آزادانه مورد استفاده قرار گیرند.
|
||||
|
||||
در نسخه اول قطعهبندی، ثباتهای قطعه مستقیماً شامل آفست بودند و هیچ كنترل دسترسی انجام نمیشد. بعدها با معرفی [_حالت محافظت شده_] این مورد تغییر کرد. هنگامی که پردازنده در این حالت اجرا میشود، توصیف کنندگان قطعه شامل یک فهرست در یک [_جدول توصیفکننده_] محلی یا سراسری هستند - که علاوه بر آدرس آفست - اندازه و مجوزهای دسترسی را نیز در خود دارد. با بارگذاری جدولهای توصیفکننده سراسری/محلی برای هر فرآیند که دسترسی حافظه را به ناحیه حافظه خود فرآیند محدود میکند، سیستمعامل میتواند فرایندها را از یکدیگر جدا کند.
|
||||
|
||||
[_حالت محافظت شده_]: https://en.wikipedia.org/wiki/X86_memory_segmentation#Protected_mode
|
||||
[_جدول توصیفکننده_]: https://en.wikipedia.org/wiki/Global_Descriptor_Table
|
||||
|
||||
با اصلاح آدرسهای حافظه قبل از دسترسی واقعی، قطعهبندی از تکنیکی استفاده کرده است که اکنون تقریباً در همه جا استفاده می شود: _حافظه مجازی_.
|
||||
|
||||
### حافظه مجازی
|
||||
|
||||
ایده پشت حافظه مجازی این است که آدرسهای حافظه را از دستگاه ذخیرهسازی فیزیکی زیرین، دور کنید. به جای دسترسی مستقیم به دستگاه ذخیرهسازی، ابتدا مرحله ترجمه انجام میشود. برای قطعهبندی، مرحله ترجمه، افزودن آدرس آفست قطعهی فعال است. تصور کنید یک برنامه به آدرس حافظه `0x1234000` در قطعهای با آفست` 0x1111000` دسترسی پیدا کند: آدرسی که واقعاً قابل دسترسی است `0x2345000` است.
|
||||
|
||||
برای تمایز بین دو نوع آدرس، به آدرسهای قبل از ترجمه _مجازی_ و به آدرسهای بعد از ترجمه _فیزیکی_ گفته میشود. یک تفاوت مهم بین این دو نوع آدرس این است که آدرسهای فیزیکی منحصربهفرد هستند و همیشه به همان مکان حافظه متمایز اشاره دارند. از طرف دیگر آدرسهای مجازی به تابع ترجمه بستگی دارد. کاملاً ممکن است که دو آدرس مجازی مختلف به همان آدرس فیزیکی اشاره داشته باشند. همچنین، آدرسهای مجازی یکسان میتوانند هنگام استفاده از توابع ترجمه مختلف، به آدرسهای فیزیکی مختلفی مراجعه کنند.
|
||||
|
||||
برای مثال هنگامی که میخواهید یک برنامه را دو بار بصورت موازی اجرا کنید، این خاصیت مفید است.
|
||||
|
||||

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

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

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

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

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

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

|
||||
|
||||
صفحه 0 در اولین بایت منطقه `000_10` قرار میگیرد، بنابراین از اولین ورودی جدول صفحه سطح 2 استفاده میکند. این ورودی به جدول صفحه 1 سطح T1 اشاره دارد که مشخص می کند صفحه `0` به قاب `0` اشاره میکند.
|
||||
|
||||
صفحات `000_000_1` ،`050_000_1` و `100_000_1` همگی در منطقه صدم `000_10` بایت قرار میگیرند، بنابراین آنها از ورودی صدم در جدول صفحه سطح 2 استفاده میکنند. این ورودی در جدول سطح 1 صفحه T2 متفاوت است که سه صفحه را با قابهای `100`، `150` و `200` نگاشت میکند. توجه داشته باشید که آدرس صفحه در جدولهای سطح 1 شامل آفست منطقه نیست، به عنوان مثال، ورودی صفحه `050_000_1` فقط `50` است.
|
||||
|
||||
ما هنوز 100 ورودی خالی در جدول سطح 2 داریم، اما بسیار کمتر از یک میلیون ورودی خالیِ قبل است. دلیل این پسانداز این است که نیازی به ایجاد جدولهای صفحه سطح 1 برای ناحیه حافظه نگاشت نشده بین `000_10` و `000_000_1` نداریم.
|
||||
|
||||
قاعده جدولهای صفحه دو سطحی را میتوان به سه، چهار یا بیشتر سطح گسترش داد. سپس ثبات جدول صفحه به جدول بالاترین سطح اشاره میکند، که به جدول سطح پایین بعدی اشاره میکند، که به سطح پایین بعدی اشاره میکند و این روال ادامه پیدا میکند. جدول صفحه سطح 1 سپس به قاب نگاشته شده اشاره میکند. این قاعده را به صورت کلی، جدول صفحات _چند سطحی_ \(ترجمه: multilevel) یا _سلسله مراتبی_ \(ترجمه: hierarchical) مینامند.
|
||||
|
||||
اکنون که از نحوه کار جدولهای صفحهبندی و صفحههای چند سطحی مطلع شدیم، میتوانیم به نحوه پیادهسازی در معماری x86_64 توجه کنیم (در ادامه فرض میکنیم CPU در حالت 64 بیتی کار میکند).
|
||||
|
||||
## صفحهبندی در x86_64
|
||||
|
||||
معماری x86_64 از جدول صفحه 4 سطحی و اندازه صفحه 4KiB استفاده میکند. هر جدول صفحه، مستقل از سطح، دارای اندازه ثابت 512 ورودی است. اندازه هر ورودی 8 بایت است، پس بزرگی هر جدول 8B * 512 = 4KiB است و بنابراین دقیقاً در یک صفحه قرار میگیرد.
|
||||
|
||||
اندیس جدول صفحه برای سطح مستقیماً از آدرس مجازی مشتق میشود:
|
||||
|
||||

|
||||
|
||||
میبینیم که هر اندیس جدول از 9 بیت تشکیل شده است، که منطقی است زیرا هر جدول دارای 512 = 9^2 ورودی است. کمترین 12 بیت در صفحه 4KiB آفست هستند (2^12 بایت = 4 کیلوبایت). بیت های 48 تا 64 کنار گذاشته میشوند، به این معنی که x86_64 در واقع 64 بیتی نیست زیرا فقط از آدرس های 48 بیتی پشتیبانی میکند.
|
||||
|
||||
[جدول صفحه 5 سطحی]: https://en.wikipedia.org/wiki/Intel_5-level_paging
|
||||
|
||||
حتی اگر بیتهای 48 تا 64 کنار گذاشتهشوند، نمیتوان آنها را روی مقادیر دلخواه تنظیم کرد. در عوض، همه بیتهای این محدوده باید کپی از بیت 47 باشند تا آدرسها منحصربهفرد باشند و extension های آینده مانند [جدول صفحه 5 سطحی] را ممکن کنند. این _sign-extension_ نامیده میشود زیرا بسیار شبیه به [extension علامت در مکمل دو] است. وقتی آدرس به درستی امضا نشده باشد، CPU یک استثنا را ارائه میدهد.
|
||||
|
||||
[extension علامت در مکمل دو]: https://en.wikipedia.org/wiki/Two's_complement#Sign_extension
|
||||
|
||||
شایان ذکر است که پردازندههای اخیر "Ice Lake" اینتل به صورت اختیاری از [جدولهای صفحه 5 سطحی] پشتیبانی میکنند تا آدرسهای مجازی را از 48 بیتی به 57 بیتی گسترش دهند. با توجه به اینکه بهینهسازی هسته ما برای یک CPU خاص در این مرحله منطقی نیست، ما در این پست فقط با جدولهای صفحه 4 سطحیِ استاندارد کار خواهیم کرد.
|
||||
|
||||
[جدولهای صفحه 5 سطحی]: https://en.wikipedia.org/wiki/Intel_5-level_paging
|
||||
|
||||
### مثالی از ترجمه
|
||||
|
||||
بیایید مثالی بزنیم تا با جزئیات بفهمیم که روند ترجمه چگونه کار میکند:
|
||||
|
||||

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

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

|
||||
|
||||
مجوزهای صفحه در جدول سطح 1، مجوز "r" است، که به معنای فقط خواندن است. سختافزار این مجوزها را اعمال میکند و اگر بخواهیم در آن صفحه بنویسیم یک استثنا را ایجاد میکند. مجوزها در صفحات سطح بالاتر مجوزهای احتمالی را در سطح پایین محدود میکنند، بنابراین اگر ورودی سطح 3 را فقط برای خواندن تنظیم کنیم، صفحههایی که از این ورودی استفاده میکنند نیز قابل نوشتن نیستند، حتی اگر سطوح پایینتر مجوزهای خواندن/نوشتن را مشخص کرده باشند.
|
||||
|
||||
توجه به این نکته مهم است که اگرچه این مثال فقط از یک نمونه از هر جدول استفاده میکند، به طور معمول از هر سطح در هر فضای آدرس چندین نمونه وجود دارد. در حالت حداکثری، موارد زیر وجود دارد:
|
||||
|
||||
- یک جدول سطح 4،
|
||||
- 512 جدول سطح 3 (زیرا جدول سطح 4 دارای 512 ورودی است)،
|
||||
- 512 * 512 جدول سطح 2 (زیرا هر 512 جدولِ سطح 3 دارای 512 ورودی است)، و
|
||||
- 512 * 512 * 512 جدول سطح 1 (512 ورودی برای هر جدول سطح 2).
|
||||
|
||||
### قالب جدول صفحه
|
||||
|
||||
جدولهای صفحه در معماری x86_64 اساساً آرایهای از 512 ورودی است. در سینتکس (کلمه: syntax) راست:
|
||||
|
||||
```rust
|
||||
#[repr(align(4096))]
|
||||
pub struct PageTable {
|
||||
entries: [PageTableEntry; 512],
|
||||
}
|
||||
```
|
||||
|
||||
همانطور که با ویژگی `repr` نشان داده شده است، جدولهای صفحه باید صفحه تراز شوند، یعنی در یک مرز 4KiB تراز شوند. این نیاز تضمین میکند که یک جدول صفحه همیشه یک صفحه کامل را پر میکند و به بهینهسازی اجازه میدهد که ورودیها را بسیار جمع و جور کند.
|
||||
|
||||
هر ورودی 8 بایت (64 بیت) اندازه دارد و دارای قالب زیر است:
|
||||
|
||||
Bit(s) | Name | Meaning
|
||||
------ | ---- | -------
|
||||
0 | present | the page is currently in memory
|
||||
1 | writable | it's allowed to write to this page
|
||||
2 | user accessible | if not set, only kernel mode code can access this page
|
||||
3 | write through caching | writes go directly to memory
|
||||
4 | disable cache | no cache is used for this page
|
||||
5 | accessed | the CPU sets this bit when this page is used
|
||||
6 | dirty | the CPU sets this bit when a write to this page occurs
|
||||
7 | huge page/null | must be 0 in P1 and P4, creates a 1GiB page in P3, creates a 2MiB page in P2
|
||||
8 | global | page isn't flushed from caches on address space switch (PGE bit of CR4 register must be set)
|
||||
9-11 | available | can be used freely by the OS
|
||||
12-51 | physical address | the page aligned 52bit physical address of the frame or the next page table
|
||||
52-62 | available | can be used freely by the OS
|
||||
63 | no execute | forbid executing code on this page (the NXE bit in the EFER register must be set)
|
||||
|
||||
میبینیم که فقط بیتهای 12–51 برای ذخیره آدرس قاب فیزیکی استفاده میشود، بیتهای باقیمانده به عنوان پرچم استفاده میشوند یا توسط سیستمعامل میتوانند آزادانه استفاده شوند. این امکان وجود دارد زیرا ما همیشه به یک آدرس تراز شده 4096 بایت، یا به یک جدول صفحه تراز شده با صفحه یا به شروع یک قاب نگاشت شده، اشاره میکنیم. این بدان معناست که بیتهای 0–11 همیشه صفر هستند، بنابراین دلیلی برای ذخیره این بیتها وجود ندارد زیرا سختافزار میتواند آنها را قبل از استفاده از آدرس صفر کند. این مورد در بیتهای 52-63 نیز صدق میکند، زیرا معماری x86_64 فقط از آدرسهای فیزیکی 52 بیتی پشتیبانی میکند (همانطور که فقط از آدرسهای مجازی 48 بیتی پشتیبانی میکند).
|
||||
|
||||
بیایید نگاهی دقیقتر به پرچمهای موجود بیندازیم:
|
||||
|
||||
- پرچم `present` صفحات نگاشت شده را از صفحات نگاشته نشده متمایز میکند. وقتی حافظه اصلی پر شود میتوان از آن برای تعویض موقت صفحات روی دیسک استفاده کرد. وقتی متعاقباً به صفحه دسترسی پیدا شد، یک استثنای ویژه به نام _page fault_ اتفاق میافتد که سیستمعامل میتواند با بارگیری مجدد صفحه از دست رفته از دیسک و سپس ادامه برنامه، به آن واکنش نشان دهد.
|
||||
- پرچمهای `writable` و `no execute` به ترتیب کنترل میکنند که آیا محتوای صفحه، «قابل نوشتن» یا «حاوی دستورالعملهای اجرایی بودن» هستند.
|
||||
- پرچم های `accessed` و `dirty` به طور خودکار هنگام پردازش یا نوشتن روی صفحه توسط CPU تنظیم میشوند. این اطلاعات میتواند توسط سیستمعامل مورد استفاده قرار گیرد. به عنوان مثال برای تصمیمگیری در مورد تعویض صفحهها یا تغییر محتوای صفحه از آخرین ذخیره روی دیسک.
|
||||
- پرچمهای `write through caching` و `disable cache` امکان کنترل حافظه پنهان برای هر صفحه را به صورت جداگانه فراهم میکند.
|
||||
- پرچم `user accessible` یک صفحه را در دسترس کد فضای کاربر قرار میدهد، در غیر اینصورت فقط وقتی CPU در حالت هسته است، قابل دسترسی است. از این ویژگی میتواند برای سریعتر کردن [فراخوانیهای سیستم] با نگه داشتن نگاشت هسته در حین اجرای برنامه فضای کاربر مورد استفاده قرار گیرد. با این وجود، آسیبپذیری [Spectre] میتواند به برنامههای فضای کاربر اجازه دهد این صفحات را بخوانند.
|
||||
- پرچم `global` به سختافزار سیگنال میدهد که یک صفحه در تمام فضاهای آدرس موجود است و بنابراین نیازی به حذف شدن از حافظه پنهان ترجمه نیست (به بخش TLB زیر مراجعه کنید) در تعویضهای فضای آدرس. این پرچم معمولاً همراه با یک پرچم پاک شده `user accessible` برای نگاشت کد هسته در تمام فضاهای آدرس استفاده میشود.
|
||||
- پرچم `large page` با اجازه دادن به ورودی جدولهای صفحه سطح 2 یا سطح 3، اجازه ایجاد صفحاتی با اندازه بزرگتر را میدهد تا مستقیماً به یک قاب نگاشت شده اشاره کنند. با استفاده از این بیت، اندازه صفحه با ضریب 512 افزایش مییابد برای هر یک از 2MiB = 512 * 4KiB ورودیهای سطح 2 یا 1GiB = 512 * 2MiB برای ورودیهای سطح 3. مزیت استفاده از صفحات بزرگتر این است که به خطوط حافظه پنهان ترجمه کمتر و جدولهای صفحه کمتر نیاز است.
|
||||
|
||||
[فراخوانیهای سیستم]: https://en.wikipedia.org/wiki/System_call
|
||||
[Spectre]: https://en.wikipedia.org/wiki/Spectre_(security_vulnerability)
|
||||
|
||||
کریت `x86_64` انواع مختلفی را برای [جدولهای صفحه] و [ورودیهای] آنها فراهم میکند، بنابراین نیازی نیست که خودمان این ساختارها را ایجاد کنیم.
|
||||
|
||||
[جدولهای صفحه]: https://docs.rs/x86_64/0.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 ذخیره میکند. و این به ما اجازه میدهد تا از ترجمه کردن مجدد ترجمههایی که در حافظه پنهان قرار دارند خودداری کنیم.
|
||||
|
||||
برخلاف سایر حافظههای پنهان پردازنده، 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.14.2/x86_64/instructions/tlb/index.html
|
||||
|
||||
مهم است که به یاد داشته باشید که TLB را روی هر جدول صفحه فلاش کنید، زیرا در غیر اینصورت پردازنده ممکن است از ترجمه قدیمی استفاده کند، که میتواند منجر به باگهای غیرقطعی شود که اشکالزدایی آن بسیار سخت است.
|
||||
|
||||
## پیادهسازی
|
||||
|
||||
چیزی که ما هنوز به آن اشاره نکردیم: **هسته ما از قبل با صفحهبندی اجرا میشود**. بوتلودری که در پست ["یک هسته مینیمال با Rust"] اضافه کردیم، قبلاً یک سلسله مراتب صفحهبندی 4 سطح را تنظیم کرده است که هر صفحه از هسته ما را در یک قاب فیزیکی نگاشت میکند. بوتلودر این کار را انجام میدهد زیرا صفحهبندی در حالت 64 بیتی در x86_64 اجباری است.
|
||||
|
||||
["یک هسته مینیمال با Rust"]: @/edition-2/posts/02-minimal-rust-kernel/index.fa.md#skht-dyskh-ymyj
|
||||
|
||||
این بدان معناست که هر آدرس حافظهای که در هسته خود استفاده کردیم یک آدرس مجازی بود. دسترسی به بافر VGA در آدرس `0xb8000` فقط به این دلیل کار کرد که بوتلودر آن صفحه حافظه را نگاشت یکتا (ترجمه: identity mapped) کرد، یعنی صفحه مجازی `0xb8000` را با فریم فیزیکی `0xb8000` نگاشت کرده است.
|
||||
|
||||
صفحهبندی باعث میشود که هسته ما نسبتاً ایمن باشد، زیرا هر دسترسی به حافظه که از مرز خارج شود باعث ایجاد استثنای خطای صفحه، به جای نوشتن روی حافظه فیزیکی تصادفی میشود. بوتلودر حتی مجوزهای دسترسی صحیح را برای هر صفحه تنظیم کرده است، به این معنی که فقط صفحات حاوی کد قابل اجرا هستند و فقط صفحات داده قابل نوشتن هستند.
|
||||
|
||||
### خطاهای صفحه
|
||||
|
||||
بیایید سعی کنیم با دسترسی به برخی از حافظههای خارج از هسته، باعث ایجاد خطای صفحه شویم. ابتدا، یک کنترلکننده خطای صفحه ایجاد میکنیم و آن را در IDT ثبت میکنیم، بهطوری که به جای یک [خطای دوگانه] یک استثنای خطای صفحه مشاهده میکنیم:
|
||||
|
||||
[خطای دوگانه]: @/edition-2/posts/06-double-faults/index.fa.md
|
||||
|
||||
```rust
|
||||
// in src/interrupts.rs
|
||||
|
||||
lazy_static! {
|
||||
static ref IDT: InterruptDescriptorTable = {
|
||||
let mut idt = InterruptDescriptorTable::new();
|
||||
|
||||
[…]
|
||||
|
||||
idt.page_fault.set_handler_fn(page_fault_handler); // new
|
||||
|
||||
idt
|
||||
};
|
||||
}
|
||||
|
||||
use x86_64::structures::idt::PageFaultErrorCode;
|
||||
use crate::hlt_loop;
|
||||
|
||||
extern "x86-interrupt" fn page_fault_handler(
|
||||
stack_frame: InterruptStackFrame,
|
||||
error_code: PageFaultErrorCode,
|
||||
) {
|
||||
use x86_64::registers::control::Cr2;
|
||||
|
||||
println!("EXCEPTION: PAGE FAULT");
|
||||
println!("Accessed Address: {:?}", Cr2::read());
|
||||
println!("Error Code: {:?}", error_code);
|
||||
println!("{:#?}", stack_frame);
|
||||
hlt_loop();
|
||||
}
|
||||
```
|
||||
|
||||
ثبات [`CR2`] بهطور خودکار توسط CPU روی خطای صفحه تنظیم میشود و حاوی آدرس مجازی قابل دسترسی است که باعث رخ دادن خطای صفحه شده است. ما برای خواندن و چاپ آن از تابع [`Cr2::read`] کریت ` x86_64` استفاده میکنیم. نوع [`PageFaultErrorCode`] اطلاعات بیشتری در مورد نوع دسترسی به حافظهای که باعث خطای صفحه شده است، فراهم می کند، به عنوان مثال این امر به دلیل خواندن یا نوشتن بوده است. به همین دلیل ما آن را نیز چاپ میکنیم. بدون رفع خطای صفحه نمیتوانیم به اجرا ادامه دهیم، بنابراین در انتها یک [hlt_loop] اضافه میکنیم.
|
||||
|
||||
[`CR2`]: https://en.wikipedia.org/wiki/Control_register#CR2
|
||||
[`Cr2::read`]: https://docs.rs/x86_64/0.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();
|
||||
|
||||
// new
|
||||
let ptr = 0xdeadbeaf as *mut u32;
|
||||
unsafe { *ptr = 42; }
|
||||
|
||||
// as before
|
||||
#[cfg(test)]
|
||||
test_main();
|
||||
|
||||
println!("It did not crash!");
|
||||
blog_os::hlt_loop();
|
||||
}
|
||||
```
|
||||
|
||||
هنگامی که آن را اجرا میکنیم، میبینیم که کنترلکننده خطای صفحه ما صدا زده میشود:
|
||||
|
||||

|
||||
|
||||
ثبات `CR2` در واقع حاوی` 0xdeadbeaf` هست، آدرسی که سعی کردیم به آن دسترسی پیدا کنیم. کد خطا از طریق [`CAUSED_BY_WRITE`] به ما میگوید که خطا هنگام تلاش برای انجام یک عملیات نوشتن رخ داده است. حتی از طریق [بیتهایی که تنظیم _نشدهاند_][`PageFaultErrorCode`] اطلاعات بیشتری به ما میدهد. به عنوان مثال، عدم تنظیم پرچم `PROTECTION_VIOLATION` به این معنی است که خطای صفحه رخ داده است زیرا صفحه هدف وجود ندارد.
|
||||
|
||||
[`CAUSED_BY_WRITE`]: https://docs.rs/x86_64/0.14.2/x86_64/structures/idt/struct.PageFaultErrorCode.html#associatedconstant.CAUSED_BY_WRITE
|
||||
|
||||
میبینیم که اشارهگر دستورالعمل فعلی `0x2031b2` میباشد، بنابراین میدانیم که این آدرس به یک صفحه کد اشاره دارد. صفحات کد توسط بوتلودر بصورت فقط خواندنی نگاشت میشوند، بنابراین خواندن از این آدرس امکانپذیر است اما نوشتن باعث خطای صفحه میشود. میتوانید این کار را با تغییر اشارهگر `0xdeadbeaf` به `0x2031b2` امتحان کنید:
|
||||
|
||||
```rust
|
||||
// Note: The actual address might be different for you. Use the address that
|
||||
// your page fault handler reports.
|
||||
let ptr = 0x2031b2 as *mut u32;
|
||||
|
||||
// read from a code page
|
||||
unsafe { let x = *ptr; }
|
||||
println!("read worked");
|
||||
|
||||
// write to a code page
|
||||
unsafe { *ptr = 42; }
|
||||
println!("write worked");
|
||||
```
|
||||
|
||||
با کامنت کردن خط آخر، میبینیم که دسترسی خواندن کار میکند، اما دسترسی نوشتن باعث خطای صفحه میشود:
|
||||
|
||||

|
||||
|
||||
میبینیم که پیام _"read worked"_ چاپ شده است، که نشان میدهد عملیات خواندن هیچ خطایی ایجاد نکرده است. با این حال، به جای پیام _"write worked"_ خطای صفحه رخ میدهد. این بار پرچم [`PROTECTION_VIOLATION`] علاوه بر پرچم [`CAUSED_BY_WRITE`] تنظیم شده است، که نشاندهنده وجود صفحه است، اما عملیات روی آن مجاز نیست. در این حالت نوشتن در صفحه مجاز نیست زیرا صفحات کد به صورت فقط خواندنی نگاشت میشوند.
|
||||
|
||||
[`PROTECTION_VIOLATION`]: https://docs.rs/x86_64/0.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(…), and hlt_loop()
|
||||
}
|
||||
```
|
||||
|
||||
تابع [`Cr3::read`] از ` x86_64` جدول صفحه سطح 4 که در حال حاضر فعال است را از ثبات `CR3` برمیگرداند. یک تاپل (کلمه: tuple) از نوع [`PhysFrame`] و [`Cr3Flags`] برمیگرداند. ما فقط به قاب علاقهمَندیم، بنابراین عنصر دوم تاپل را نادیده میگیریم.
|
||||
|
||||
[`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 که در حال حاضر فعال است در آدرس `0x100` در حافظه _فیزیکی_ ذخیره میشود، همانطور که توسط نوع بستهبندی [`PhysAddr`] نشان داده شده است. حال سوال این است: چگونه میتوانیم از هسته خود به این جدول دسترسی پیدا کنیم؟
|
||||
|
||||
[`PhysAddr`]: https://docs.rs/x86_64/0.14.2/x86_64/addr/struct.PhysAddr.html
|
||||
|
||||
دسترسی مستقیم به حافظه فیزیکی در هنگام فعال بودن صفحهبندی امکان پذیر نیست، زیرا برنامهها به راحتی میتوانند محافظت از حافظه (ترجمه: memory protection) را دور بزنند و در غیر اینصورت به حافظه سایر برنامهها دسترسی پیدا میکنند. بنابراین تنها راه دسترسی به جدول از طریق برخی از صفحههای مجازی است که به قاب فیزیکی در آدرس`0x1000` نگاشت شده. این مشکل ایجاد نگاشت برای قابهای جدول صفحه یک مشکل کلی است، زیرا هسته به طور مرتب به جدولهای صفحه دسترسی دارد، به عنوان مثال هنگام اختصاص پشته برای یک نخِ (ترجمه: thread) جدید.
|
||||
|
||||
راه حلهای این مشکل در پست بعدی با جزئیات توضیح داده شده است.
|
||||
|
||||
## خلاصه
|
||||
|
||||
این پست دو روش حفاظت از حافظه را ارائه میدهد: تقسیمبندی و صفحهبندی. در حالی که اولی از ناحیه حافظه با اندازه متغیر استفاده میکند و از تکهتکه شدن خارجی رنج میبرد، دومی از صفحات با اندازه ثابت استفاده میکند و امکان کنترل دقیقتر مجوزهای دسترسی را فراهم میکند.
|
||||
|
||||
صفحهبندی اطلاعات نگاشت صفحات موجود در جدولهای صفحه با یک یا چند سطح را ذخیره میکند. معماری x86_64 از جدولهای صفحه با 4 سطح و اندازه صفحه 4KiB استفاده میکند. سختافزار بهطور خودکار جدولهای صفحه را مرور میکند و ترجمههای حاصل را در TLB ذخیره میکند. این بافر به طور شفاف بهروز نمیشود و باید به صورت دستی با تغییر جدول صفحه، فلاش شود.
|
||||
|
||||
ما فهمیدیم که هسته ما در حال حاضر در بالای صفحهبندی اجرا میشود و دسترسی غیرقانونی حافظه باعث استثناهای خطای صفحه میشود. ما سعی کردیم به جدولهای صفحه فعلی دسترسی پیدا کنیم، اما قادر به انجام این کار نبودیم زیرا ثبات CR3 یک آدرس فیزیکی را ذخیره میکند که ما نمیتوانیم مستقیماً از هسته به آن دسترسی داشته باشیم.
|
||||
|
||||
## بعدی چیست؟
|
||||
|
||||
در پست بعدی نحوه پیادهسازی پشتیبانی برای صفحهبندی در هسته توضیح داده شده است. که روشهای مختلفی برای دسترسی به حافظه فیزیکی از هسته ارائه میدهد، که دسترسی به جدولهای صفحهای که هسته در آن اجرا میشود را امکانپذیر میکند. در این مرحله ما میتوانیم توابع را برای ترجمه آدرسهای مجازی به فیزیکی و ایجاد نگاشتهای جدید در جدولهای صفحه پیادهسازی کنیم.
|
||||
@@ -1,428 +0,0 @@
|
||||
+++
|
||||
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"]
|
||||
+++
|
||||
|
||||
この記事では**ページング**を紹介します。これは、私達のオペレーティングシステムにも使う、とても一般的なメモリ管理方式です。なぜメモリの分離が必要なのか、**セグメンテーション**がどういう仕組みなのか、**仮想メモリ**とは何なのか、ページングがいかにしてメモリ<ruby>断片化<rp> (</rp><rt>フラグメンテーション</rt><rp>) </rp></ruby>の問題を解決するのかを説明します。また、x86_64アーキテクチャにおける、マルチレベルページテーブルのレイアウトについても説明します。
|
||||
|
||||
<!-- more -->
|
||||
|
||||
このブログの内容は [GitHub] 上で公開・開発されています。何か問題や質問などがあれば issue をたててください(訳注: リンクは原文(英語)のものになります)。また[こちら][at the bottom]にコメントを残すこともできます。この記事の完全なソースコードは[`post-08` ブランチ][post branch]にあります。
|
||||
|
||||
[GitHub]: https://github.com/phil-opp/blog_os
|
||||
[at the bottom]: #comments
|
||||
<!-- fix for zola anchor checker (target is in template): <a id="comments"> -->
|
||||
[post branch]: https://github.com/phil-opp/blog_os/tree/post-08
|
||||
|
||||
<!-- toc -->
|
||||
|
||||
## メモリの保護
|
||||
|
||||
オペレーティングシステムの主な役割の一つに、プログラムを互いに分離するということがあります。例えば、ウェブブラウザがテキストエディタに干渉してはいけません。この目的を達成するために、オペレーティングシステムはハードウェアの機能を利用して、あるプロセスのメモリ領域に他のプロセスがアクセスできないようにします。ハードウェアや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によって選ばれます。命令の<ruby>引き出し<rp> (</rp><rt>フェッチ</rt><rp>) </rp></ruby>にはコードセグメント`CS`が使用され、スタック操作(プッシュ・ポップ)にはスタックセグメント`SS`が使用されます。その他の命令では、データセグメント`DS`やエクストラセグメント`ES`が使用されます。その後、自由に使用できる`FS`と`GS`というセグメントレジスタも追加されました。
|
||||
|
||||
セグメンテーションの初期バージョンでは、セグメントレジスタは直接オフセットを格納しており、アクセス制御は行われていませんでした。これは後に[<ruby>プロテクトモード<rp> (</rp><rt>protected mode</rt><rp>) </rp></ruby>][_protected mode_]が導入されたことで変更されました。CPUがこのモードで動作している時、セグメント<ruby>記述子<rp> (</rp><rt>ディスクリプタ</rt><rp>) </rp></ruby>は<ruby>局所<rp> (</rp><rt>ローカル</rt><rp>) </rp></ruby>または<ruby>大域<rp> (</rp><rt>グローバル</rt><rp>) </rp>[**</ruby><ruby>記述子表<rp> (</rp><rt>ディスクリプタテーブル</rt><rp>) </rp></ruby>**][_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つ並行して実行するという状況が挙げられます。
|
||||
|
||||

|
||||
|
||||
同じプログラムを2つ実行していますが、別の変換関数が使われています。1つ目の<ruby>実体<rp> (</rp><rt>インスタンス</rt><rp>) </rp></ruby>ではセグメントのオフセットが100なので、0から150の仮想アドレスは100から250に変換されます。2つ目のインスタンスではオフセットが300なので、0から150の仮想アドレスが300から450に変換されます。これにより、プログラムが互いに干渉することなく同じコード、同じ仮想アドレスを使うことができます。
|
||||
|
||||
もう一つの利点は、プログラムが全く異なる仮想アドレスを使っていたとしても、物理メモリ上の任意の場所に置けるということです。したがって、OSはプログラムを再コンパイルすることなく利用可能なメモリをフルに活用できます。
|
||||
|
||||
### <ruby>断片化<rp> (</rp><rt>fragmentation</rt><rp>) </rp></ruby>
|
||||
|
||||
物理アドレスと仮想アドレスを分けることにより、セグメンテーションは非常に強力なものとなっています。しかし、これにより断片化という問題が発生します。例として、上で見たプログラムの3つ目を実行したいとしましょう:
|
||||
|
||||

|
||||
|
||||
開放されているメモリは十分にあるにも関わらず、プログラムのインスタンスを重ねることなく物理メモリに対応づけることはできません。ここで必要なのは **連続した** メモリであり、開放されているメモリが小さな塊であっては使えないためです。
|
||||
|
||||
この断片化に対処する方法の一つは、実行を一時停止し、メモリの使用されている部分を寄せ集めて、変換関数を更新し、実行を再開することでしょう:
|
||||
|
||||

|
||||
|
||||
これで、プログラムの3つ目のインスタンスを開始するのに十分な連続したスペースができました。
|
||||
|
||||
このデフラグメンテーションという処理の欠点は、大量のメモリをコピーしなければならず、パフォーマンスを低下させてしまうことです。また、メモリが断片化しすぎる前に定期的に実行しないといけません。そうすると、プログラムが時々一時停止して反応がなくなるので、性能が予測不可能になってしまいます。
|
||||
|
||||
ほとんどのシステムでセグメンテーションが用いられなくなった理由の一つに、この断片化の問題があります。実際、x86の64ビットモードでは、セグメンテーションはもはやサポートされていません。代わりに **ページング** が使用されており、これにより断片化の問題は完全に回避されます。
|
||||
|
||||
## ページング
|
||||
|
||||
ページングの考え方は、仮想メモリ空間と物理メモリ空間の両方を、サイズの固定された小さなブロックに分割するというものです。仮想メモリ空間のブロックは **ページ** と呼ばれ、物理アドレス空間のブロックは **フレーム** と呼ばれます。各ページはフレームに独立してマッピングできるので、大きなメモリ領域を連続していない物理フレームに分割することが可能です。
|
||||
|
||||
この方法の利点は、上のメモリ空間断片化の状況をもう一度、セグメンテーションの代わりにページングを使って見てみれば明らかになります:
|
||||
|
||||

|
||||
|
||||
この例では、ページサイズは50バイトなので、それぞれのメモリ領域が3つのページに分割されます。それぞれのページは個別にフレームに対応付けられるので、連続した仮想メモリ領域を非連続な物理フレームへと対応付けられるのです。これにより、デフラグを事前に実行することなく、3つ目のプログラムのインスタンスを開始できるようになります。
|
||||
|
||||
### 隠された断片化
|
||||
|
||||
少ない数の可変なサイズのメモリ領域を使っていたセグメンテーションと比べると、ページングでは大量の小さい固定サイズのメモリ領域を使います。すべてのフレームが同じ大きさなので、「小さすぎて使えないフレーム」などというものは存在せず、したがって断片化も起きません。
|
||||
|
||||
というより、**目に見える** 断片化は起きていない、という方が正しいでしょう。**<ruby>内部<rp> (</rp><rt>internal</rt><rp>) </rp></ruby>断片化**と呼ばれる、目に見えない断片化は依然として起こっています。内部断片化は、すべてのメモリ領域がページサイズの整数倍ぴったりにはならないために生じます。例えば、上の例でサイズが101のプログラムを考えてみてください:この場合でもサイズ50のページが3つ必要で、必要な量より49バイト多く占有します。これらの2種類の断片化を区別するため、セグメンテーションを使うときに起きる断片化は **<ruby>外部<rp> (</rp><rt>external</rt><rp>) </rp></ruby>断片化** と呼ばれます。
|
||||
|
||||
内部断片化が起こるのは残念なことですが、セグメンテーションで発生していた外部断片化よりも優れていることが多いです。確かにメモリ領域は無駄にしますが、デフラグメンテーションをする必要がなく、また断片化の量も予想できるからです(平均するとメモリ領域ごとにページの半分)。
|
||||
|
||||
### ページテーブル
|
||||
|
||||
最大で何百万ものページがそれぞれ独立にフレームに対応付けられることを見てきました。この対応付けの情報はどこかに保存されなければなりません。セグメンテーションでは、有効なメモリ領域ごとに個別のセグメントセレクタを使っていましたが、ページングではレジスタよりも遥かに多くのページが使われるので、これは不可能です。代わりにページングでは **ページテーブル** と呼ばれる<ruby>表<rp> (</rp><rt>テーブル</rt><rp>) </rp></ruby>構造を使って対応付の情報を保存します。
|
||||
|
||||
上の例では、ページテーブルは以下のようになります:
|
||||
|
||||

|
||||
|
||||
それぞれのプログラムのインスタンスが独自のページテーブルを持っているのが分かります。現在有効なテーブルへのポインタは、特殊なCPUのレジスタに格納されます。`x86`においては、このレジスタは`CR3`と呼ばれています。それぞれのプログラムのインスタンスを実行する前に、正しいページテーブルを指すポインタをこのレジスタにロードするのはOSの役割です。
|
||||
|
||||
それぞれのメモリアクセスにおいて、CPUはテーブルへのポインタをレジスタから読み出し、テーブル内のアクセスされたページから対応するフレームを見つけ出します。これは完全にハードウェア内で行われ、実行しているプログラムからはこの動作は見えません。変換プロセスを高速化するために、多くのCPUアーキテクチャは前回の変換の結果を覚えておく専用のキャッシュを持っています。
|
||||
|
||||
アーキテクチャによっては、ページテーブルのエントリは"Flags"フィールドにあるアクセス権限のような属性も保持できます。上の例では、"r/w"フラグがあることにより、このページは読み書きのどちらも可能だということを示しています。
|
||||
|
||||
### <ruby>複数層<rp> (</rp><rt>Multilevel</rt><rp>) </rp></ruby>ページテーブル
|
||||
|
||||
上で見たシンプルなページテーブルは、アドレス空間が大きくなってくると問題が発生します:メモリが無駄になるのです。たとえば、`0`, `1_000_000`, `1_000_050` および `1_000_100`(3ケタごとの区切りとして`_`を用いています)の4つの仮想ページを使うプログラムを考えてみましょう。
|
||||
|
||||

|
||||
|
||||
このプログラムはたった4つしか物理フレームを必要としていないのに、テーブルには100万以上ものエントリが存在してしまっています。空のエントリを省略した場合、変換プロセスにおいてCPUが正しいエントリに直接ジャンプできなくなってしまうので、それはできません(たとえば、4つめのページが4つめのエントリを使っていることが保証されなくなってしまいます)。
|
||||
|
||||
この無駄になるメモリを減らせる、 **2層ページテーブル** を使ってみましょう。発想としては、それぞれのアドレス領域に異なるページテーブルを使うというものです。**レベル2** ページテーブルと呼ばれる追加のページテーブルは、アドレス領域と(レベル1の)ページテーブルのあいだの対応を格納します。
|
||||
|
||||
これを理解するには、例を見るのが一番です。それぞれのレベル1テーブルは大きさ`10_000`の領域に対応するとします。すると、以下のテーブルが上のマッピングの例に対応するものとなります:
|
||||
|
||||

|
||||
|
||||
ページ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のテーブルは対応するフレームを指します。この原理は一般に **<ruby>複数層<rp> (</rp><rt>multilevel</rt><rp>) </rp></ruby>** ページテーブルや、 **<ruby>階層型<rp> (</rp><rt>hierarchical</rt><rp>) </rp></ruby>** ページテーブルと呼ばれます。
|
||||
|
||||
ページングと複数層ページテーブルの仕組みが理解できたので、x86_64アーキテクチャにおいてどのようにページングが実装されているのかについて見ていきましょう(以下では、CPUは64ビットモードで動いているとします)。
|
||||
|
||||
## x86_64におけるページング
|
||||
|
||||
x86_64アーキテクチャは4層ページテーブルを使っており、ページサイズは4KiBです。それぞれのページテーブルは、層によらず512のエントリを持っています。それぞれのエントリの大きさは8バイトなので、それぞれのテーブルは512 * 8B = 4KiBであり、よってぴったり1ページに収まります。
|
||||
|
||||
(各)レベルのページテーブルインデックスは、仮想アドレスから直接求められます:
|
||||
|
||||

|
||||
|
||||
それぞれのテーブルインデックスは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]によく似ているので、 **<ruby>符号<rp> (</rp><rt>sign</rt><rp>) </rp></ruby><ruby>拡張<rp> (</rp><rt>extension</rt><rp>) </rp></ruby>** とよばれています。アドレスが適切に符号拡張されていない場合、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
|
||||
|
||||
### 変換の例
|
||||
|
||||
この変換の仕組みをより詳細に理解するために、例を挙げて見てみましょう。
|
||||
|
||||

|
||||
|
||||
現在有効なレベル4ページテーブルの物理アドレス、つまりレベル4ページテーブルの「<ruby>根<rp> (</rp><rt>root</rt><rp>) </rp></ruby>」は`CR3`レジスタに格納されています。それぞれのページテーブルエントリは、次のレベルのテーブルの物理フレームを指しています。そして、レベル1のテーブルは対応するフレームを指しています。なお、ページテーブル内のアドレスは全て仮想ではなく物理アドレスであることに注意してください。さもなければ、CPUは(変換プロセス中に)それらのアドレスも変換しなくてはならず、無限再帰に陥ってしまうかもしれないからです。
|
||||
|
||||
上のページテーブル階層構造は、最終的に(青色の)2つのページへの対応を行っています。ページテーブルのインデックスから、これらの2つのページの仮想アドレスは`0x803FE7F000`と`0x803FE00000`であると推論できます。プログラムがアドレス`0x803FE7F5CE`から読み込もうとしたときに何が起こるかを見てみましょう。まず、アドレスを2進数に変換し、アドレスのページテーブルインデックスとページオフセットが何であるかを決定します:
|
||||
|
||||

|
||||
|
||||
これらのインデックス情報をもとにページテーブル階層構造を移動して、このアドレスに対応するフレームを決定します:
|
||||
|
||||
- まず、`CR3`レジスタからレベル4テーブルのアドレスを読み出します。
|
||||
- レベル4のインデックスは1なので、このテーブルの1つ目のインデックスを見ます。すると、レベル3テーブルはアドレス16KiBに格納されていると分かります。
|
||||
- レベル3テーブルをそのアドレスから読み出し、インデックス0のエントリを見ると、レベル2テーブルは24KiBにあると教えてくれます。
|
||||
- レベル2のインデックスは511なので、このページの最後のエントリを見て、レベル1テーブルのアドレスを見つけます。
|
||||
- レベル1テーブルの127番目のエントリを読むことで、ついに対象のページは12KiB(16進数では0x3000)のフレームに対応づけられていると分かります。
|
||||
- 最後のステップは、ページオフセットをフレームアドレスに足して、物理アドレスを得ることです。0x3000 + 0x5ce = 0x35ce
|
||||
|
||||

|
||||
|
||||
レベル1テーブルにあるこのページの権限は`r`であり、これは読み込み専用という意味です。これらのような権限に対する侵害はハードウェアによって保護されており、このページに書き込もうとした場合は例外が投げられます。より高いレベルのページにおける権限は、下のレベルにおいて可能な権限を制限します。たとえばレベル3エントリを読み込み専用にした場合、下のレベルで読み書きを許可したとしても、このエントリを使うページはすべて書き込み不可になります。
|
||||
|
||||
この例ではそれぞれのテーブルの<ruby>実体<rp> (</rp><rt>インスタンス</rt><rp>) </rp></ruby>を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バイト単位のページに<ruby>揃え<rp> (</rp><rt>アライン</rt><rp>) </rp></ruby>られたアドレス(ページテーブルか、対応づけられたフレームの先頭)を指しているからです。これは、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は完全に透明ではなく、ページテーブルの内容が変わったときに変換内容を更新したり取り除いたりしてくれません(訳注:キャッシュが<ruby>透明<rp> (</rp><rt>transparent</rt><rp>) </rp></ruby>であるとは、利用者がキャッシュの存在を意識する必要がないという意味)。つまり、カーネルがページテーブルを変更したときは、カーネル自らTLBを更新しないといけないということです。これを行うために、[`invlpg`]("invalidate page"、ページを無効化の意)という特別なCPU命令があります。これは指定されたページの変換をTLBから取り除き、次のアクセスの際に再び読み込まれるようにします。また、TLBは`CR3`レジスタを再設定することでもflushできます。`CR3`レジスタの再設定は、アドレス空間が変更されたという状況を模擬するのです。`x86_64`クレートの[`tlb`モジュール][`tlb` module]が、両方のやり方のRust関数を提供しています。
|
||||
|
||||
<div class="note">
|
||||
|
||||
**訳注:** flushは「(溜まった水を)どっと流す」「(トイレなどを)水で洗い流す」という意味の言葉です。そのためコンピュータサイエンスにおいて「キャッシュなどに溜められていたデータを(場合によっては適切な出力先に書き込みながら)削除する」という意味を持つようになりました。ここではどこかに出力しているわけではないので、「初期化」と同じような意味と考えて差し支えないでしょう。
|
||||
|
||||
</div>
|
||||
|
||||
[`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]ではなくページフォルト例外が得られるように、ページフォルト<ruby>処理関数<rp> (</rp><rt>ハンドラ</rt><rp>) </rp></ruby>を作って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();
|
||||
}
|
||||
```
|
||||
|
||||
これを実行すると、ページフォルトハンドラが呼びだされたのを見ることができます:
|
||||
|
||||

|
||||
|
||||
`CR2`レジスタは確かに私達がアクセスしようとしていたアドレスである`0xdeadbeaf`を格納しています。エラーコードが[`CAUSED_BY_WRITE`]なので、この<ruby>障害<rp> (</rp><rt>フォルト</rt><rp>) </rp></ruby>は<ruby>write<rp> (</rp><rt>書き込み</rt><rp>) </rp></ruby>操作の実行中に発生したのだと分かります。更に、[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行をコメントアウトすると、読み込みアクセスだけになるので実行は成功しますが、そうしなかった場合ページフォルトが発生します:
|
||||
|
||||

|
||||
|
||||
"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レジスタに格納されている物理アドレスはカーネルから直接アクセスできないものであるため、それはできませんでした。
|
||||
|
||||
## 次は?
|
||||
|
||||
次の記事では、私達のカーネルをページングに対応させる方法について説明します。私達のカーネルから物理メモリにアクセスする幾つかの方法を示すので、これらを用いれば私達のカーネルが動作しているページテーブルにアクセスできます。そうすると、仮想アドレスを物理アドレスに変換する関数を実装でき、ページテーブルに新しい対応づけを作れるようになります。
|
||||
|
Before Width: | Height: | Size: 22 KiB |
|
Before Width: | Height: | Size: 17 KiB |
|
Before Width: | Height: | Size: 18 KiB |
|
Before Width: | Height: | Size: 5.9 KiB |
|
Before Width: | Height: | Size: 7.6 KiB |
|
Before Width: | Height: | Size: 9.5 KiB |
|
Before Width: | Height: | Size: 6.8 KiB |
|
Before Width: | Height: | Size: 35 KiB |
@@ -1 +0,0 @@
|
||||
<mxfile host="Electron" modified="2020-03-20T10:35:53.357Z" agent="Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) draw.io/12.6.5 Chrome/80.0.3987.86 Electron/8.0.0 Safari/537.36" etag="GYNOQZMQxo_zCMLLT_wk" version="12.6.5" type="device"><diagram id="zeHyu60-730WxqfhUfuk" name="Page-1">7Vldb9sgFP01fmzk7ySPbdKt0zqpayate6Q2sVGJsTBukv76QQw22Plam6ZWNVeq4HC5wLk395jE8iaL1VcK8vQHiSG2XDteWd7Ucl1v7PP/AlhXgBu6FZBQFFeQ0wAz9AIlaEu0RDEsDENGCGYoN8GIZBmMmIEBSsnSNJsTbK6agwR2gFkEcBf9jWKWVugosBv8BqIkVSs7thxZAGUsgSIFMVlqkHdteRNKCKtai9UEYsGd4qWa92XHaL0xCjN2zITZC/aTcgZ+4zF5CFfpzfDevZBengEu5YHlZtlaMUBJmcVQOLEt72qZIgZnOYjE6JKHnGMpW2Dec3hTuoOUwdXOfTr16XnWQLKAjK65iZwwknzJhAlld9mwrxhNNeIVBmS8k9pvQwlvSFb+gSG3fwzVOdYTirweUuT2iyK/hxT5/aIo6CFFYb8oCk9M0RxhPCGY0M1cbz6HYRRxvGCUPEFtxN48JyK1ZwV++BlIdXsmCaNPQWrPRGTcIZW/qGYRf+Xm6M8ScrRNMj88M5k0GctIBlv0SghglGS8G3GyIMevBJWIvxhfyoEFimOxzNbQmcE9QSxaRcPvhqIu1meJhbqfaMH4DtePBNCYo98EZ7TMGW/fgIzzRPfkv3M4/981z4cmt/W7k0bucAu34btxe8StBGbxpbjeiQzFoChQZBIGV4g9yPQT7T+iPQhkb7rShqZr1cn47h/0jjZLdJtpm56aV20Oxp2bZCsA/ACkpBE8Iq0YoAlk+wy3R1QLWbAlZAqjEAOGns39boujXOGOoIzpamMkTO1WeaiOKSfpV9KWn1bi1SKm/FQsdPzwwIO1ZpYLg2L3doNWfgf23l21zIeGNW9UyzfpXdP/hozv3jLzskhFxtQV/mBlVxUbwznbV68pLNALeNw4Eukr6eNegysrmHIEg0eIr0D0lGyKlCG84lEmd6RADBHhnVZJVq962xqvVy94jUNZcrvZ5NQT4kMypi3hT8Xfvsomv+uRR7DqdNQ/cHsKy846eGEPvDAcmslykg+MY/i8cEeDkTfWntB0SObzArJW2p0m0bp3dU22foHiqcc61b45fLxQHXGtP1aonNcIlfNhQjU6Vqe8/0J1tFC11xnuF6rOi1twDqnqfk+Tk/x1StUWjfNIVUsfDynVvczKM0uVf0iq3MAUFVWM3ipVvplTzsAdv12deLf5paMyb34u8q7/Ag==</diagram></mxfile>
|
||||
|
Before Width: | Height: | Size: 8.3 KiB |
|
Before Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 12 KiB |
@@ -1,7 +0,0 @@
|
||||
+++
|
||||
title = "Posts"
|
||||
sort_by = "weight"
|
||||
insert_anchor_links = "left"
|
||||
render = false
|
||||
page_template = "edition-2/page.html"
|
||||
+++
|
||||
@@ -1,7 +0,0 @@
|
||||
+++
|
||||
title = "Posts"
|
||||
sort_by = "weight"
|
||||
insert_anchor_links = "left"
|
||||
render = false
|
||||
page_template = "edition-2/page.html"
|
||||
+++
|
||||
@@ -1,7 +0,0 @@
|
||||
+++
|
||||
title = "Posts"
|
||||
sort_by = "weight"
|
||||
insert_anchor_links = "left"
|
||||
render = false
|
||||
page_template = "edition-2/page.html"
|
||||
+++
|
||||
@@ -1,7 +0,0 @@
|
||||
+++
|
||||
title = "Posts"
|
||||
sort_by = "weight"
|
||||
insert_anchor_links = "left"
|
||||
render = false
|
||||
page_template = "edition-2/page.html"
|
||||
+++
|
||||
@@ -1,7 +0,0 @@
|
||||
+++
|
||||
title = "Posts"
|
||||
sort_by = "weight"
|
||||
insert_anchor_links = "left"
|
||||
render = false
|
||||
page_template = "edition-2/page.html"
|
||||
+++
|
||||
@@ -1,7 +0,0 @@
|
||||
+++
|
||||
title = "Posts"
|
||||
sort_by = "weight"
|
||||
insert_anchor_links = "left"
|
||||
render = false
|
||||
page_template = "edition-2/page.html"
|
||||
+++
|
||||
@@ -1,7 +0,0 @@
|
||||
+++
|
||||
title = "Posts"
|
||||
sort_by = "weight"
|
||||
insert_anchor_links = "left"
|
||||
render = false
|
||||
page_template = "edition-2/page.html"
|
||||
+++
|
||||
4
blog/content/first-edition/_index.md
Normal file
@@ -0,0 +1,4 @@
|
||||
+++
|
||||
title = "First Edition"
|
||||
template = "first-edition/index.html"
|
||||
+++
|
||||
@@ -4,7 +4,7 @@ weight = 1
|
||||
path = "catching-exceptions"
|
||||
aliases = ["catching-exceptions.html"]
|
||||
date = 2016-05-28
|
||||
template = "edition-1/page.html"
|
||||
template = "first-edition/page.html"
|
||||
[extra]
|
||||
updated = "2016-06-25"
|
||||
+++
|
||||
@@ -20,8 +20,8 @@ As always, the complete source code is on [GitHub]. Please file [issues] for any
|
||||
|
||||
> **Note**: This post describes how to handle exceptions using naked functions (see [“Handling Exceptions with Naked Functions”] for an overview). Our new way of handling exceptions can be found in the [“Handling Exceptions”] post.
|
||||
|
||||
[“Handling Exceptions with Naked Functions”]: @/edition-1/extra/naked-exceptions/_index.md
|
||||
[“Handling Exceptions”]: @/edition-1/posts/09-handling-exceptions/index.md
|
||||
[“Handling Exceptions with Naked Functions”]: @/first-edition/extra/naked-exceptions/_index.md
|
||||
[“Handling Exceptions”]: @/first-edition/posts/09-handling-exceptions/index.md
|
||||
|
||||
## Exceptions
|
||||
An exception signals that something is wrong with the current instruction. For example, the CPU issues an exception if the current instruction tries to divide by 0. When an exception occurs, the CPU interrupts its current work and immediately calls a specific exception handler function, depending on the exception type.
|
||||
@@ -35,7 +35,7 @@ We've already seen several types of exceptions in our kernel:
|
||||
|
||||
For the full list of exceptions check out the [OSDev wiki][exceptions].
|
||||
|
||||
[exceptions]: https://wiki.osdev.org/Exceptions
|
||||
[exceptions]: http://wiki.osdev.org/Exceptions
|
||||
|
||||
### The Interrupt Descriptor Table
|
||||
In order to catch and handle exceptions, we have to set up a so-called _Interrupt Descriptor Table_ (IDT). In this table we can specify a handler function for each CPU exception. The hardware uses this table directly, so we need to follow a predefined format. Each entry must have the following 16-byte structure:
|
||||
@@ -245,7 +245,7 @@ It needs to be a function with a defined [calling convention], as it called dire
|
||||
|
||||
It is important that the function is [diverging], i.e. it must never return. The reason is that the hardware doesn't _call_ the handler functions, it just _jumps_ to them after pushing some values to the stack. So our stack might look different:
|
||||
|
||||
[diverging]: https://doc.rust-lang.org/rust-by-example/fn/diverging.html
|
||||
[diverging]: https://doc.rust-lang.org/book/functions.html#diverging-functions
|
||||
|
||||

|
||||
|
||||
@@ -311,8 +311,8 @@ u64 | Offset | Virtual start address of the table.
|
||||
|
||||
This structure is already contained [in the x86_64 crate], so we don't need to create it ourselves. The same is true for the [lidt function]. So we just need to put the pieces together to create a `load` method:
|
||||
|
||||
[in the x86_64 crate]: https://docs.rs/x86_64/0.1.0/x86_64/instructions/tables/struct.DescriptorTablePointer.html
|
||||
[lidt function]: https://docs.rs/x86_64/0.1.0/x86_64/instructions/tables/fn.lidt.html
|
||||
[in the x86_64 crate]: http://docs.rs/x86_64/0.1.0/x86_64/instructions/tables/struct.DescriptorTablePointer.html
|
||||
[lidt function]: http://docs.rs/x86_64/0.1.0/x86_64/instructions/tables/fn.lidt.html
|
||||
|
||||
```rust
|
||||
impl Idt {
|
||||
@@ -422,7 +422,7 @@ extern "C" fn divide_by_zero_handler() -> ! {
|
||||
```
|
||||
We register a single handler function for a [divide by zero error] \(index 0). Like the name says, this exception occurs when dividing a number by 0. Thus we have an easy way to test our new exception handler.
|
||||
|
||||
[divide by zero error]: https://wiki.osdev.org/Exceptions#Divide-by-zero_Error
|
||||
[divide by zero error]: http://wiki.osdev.org/Exceptions#Divide-by-zero_Error
|
||||
|
||||
However, it doesn't work this way:
|
||||
|
||||
|
Before Width: | Height: | Size: 9.7 KiB After Width: | Height: | Size: 9.7 KiB |
|
Before Width: | Height: | Size: 14 KiB After Width: | Height: | Size: 14 KiB |
|
Before Width: | Height: | Size: 14 KiB After Width: | Height: | Size: 14 KiB |
@@ -4,7 +4,7 @@ weight = 2
|
||||
path = "better-exception-messages"
|
||||
aliases = ["better-exception-messages.html"]
|
||||
date = 2016-08-03
|
||||
template = "edition-1/page.html"
|
||||
template = "first-edition/page.html"
|
||||
[extra]
|
||||
updated = "2016-11-01"
|
||||
+++
|
||||
@@ -21,8 +21,8 @@ As always, the complete source code is on [GitHub]. Please file [issues] for any
|
||||
|
||||
> **Note**: This post describes how to handle exceptions using naked functions (see [“Handling Exceptions with Naked Functions”] for an overview). Our new way of handling exceptions can be found in the [“Handling Exceptions”] post.
|
||||
|
||||
[“Handling Exceptions with Naked Functions”]: @/edition-1/extra/naked-exceptions/_index.md
|
||||
[“Handling Exceptions”]: @/edition-1/posts/09-handling-exceptions/index.md
|
||||
[“Handling Exceptions with Naked Functions”]: @/first-edition/extra/naked-exceptions/_index.md
|
||||
[“Handling Exceptions”]: @/first-edition/posts/09-handling-exceptions/index.md
|
||||
|
||||
## Exceptions in Detail
|
||||
An exception signals that something is wrong with the currently-executed instruction. Whenever an exception occurs, the CPU interrupts its current work and starts an internal exception routine.
|
||||
@@ -394,7 +394,7 @@ The base pointer is initialized directly from the stack pointer (`rsp`) after pu
|
||||
The reason is that our exception handler is defined as `extern "C" function`, which specifies that it's using the C [calling convention]. On x86_64 Linux, the C calling convention is specified by the System V AMD64 ABI ([PDF][system v abi]). Section 3.2.2 defines the following:
|
||||
|
||||
[calling convention]: https://en.wikipedia.org/wiki/X86_calling_conventions
|
||||
[system v abi]: https://web.archive.org/web/20160801075139/https://www.x86-64.org/documentation/abi.pdf
|
||||
[system v abi]: http://web.archive.org/web/20160801075139/http://www.x86-64.org/documentation/abi.pdf
|
||||
|
||||
> The end of the input argument area shall be aligned on a 16 byte boundary. In other words, the value (%rsp + 8) is always a multiple of 16 when control is transferred to the function entry point.
|
||||
|
||||
@@ -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-privileged mode.
|
||||
- The `USER_MODE` flag is set when the fault occurred in non-priviledged 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.
|
||||
|
||||
|
Before Width: | Height: | Size: 17 KiB After Width: | Height: | Size: 17 KiB |
|
Before Width: | Height: | Size: 18 KiB After Width: | Height: | Size: 18 KiB |
|
Before Width: | Height: | Size: 17 KiB After Width: | Height: | Size: 17 KiB |
|
Before Width: | Height: | Size: 17 KiB After Width: | Height: | Size: 17 KiB |
|
Before Width: | Height: | Size: 14 KiB After Width: | Height: | Size: 14 KiB |
|
Before Width: | Height: | Size: 8.3 KiB After Width: | Height: | Size: 8.3 KiB |
@@ -4,7 +4,7 @@ weight = 3
|
||||
path = "returning-from-exceptions"
|
||||
aliases = ["returning-from-exceptions.html"]
|
||||
date = 2016-09-21
|
||||
template = "edition-1/page.html"
|
||||
template = "first-edition/page.html"
|
||||
[extra]
|
||||
updated = "2016-11-01"
|
||||
+++
|
||||
@@ -21,8 +21,8 @@ As always, the complete source code is on [GitHub]. Please file [issues] for any
|
||||
|
||||
> **Note**: This post describes how to handle exceptions using naked functions (see [“Handling Exceptions with Naked Functions”] for an overview). Our new way of handling exceptions can be found in the [“Handling Exceptions”] post.
|
||||
|
||||
[“Handling Exceptions with Naked Functions”]: @/edition-1/extra/naked-exceptions/_index.md
|
||||
[“Handling Exceptions”]: @/edition-1/posts/09-handling-exceptions/index.md
|
||||
[“Handling Exceptions with Naked Functions”]: @/first-edition/extra/naked-exceptions/_index.md
|
||||
[“Handling Exceptions”]: @/first-edition/posts/09-handling-exceptions/index.md
|
||||
|
||||
## Introduction
|
||||
Most exceptions are fatal and can't be resolved. For example, we can't return from a divide-by-zero exception in a reasonable way. However, there are some exceptions that we can resolve:
|
||||
@@ -38,11 +38,11 @@ The breakpoint exception is the perfect exception to test our upcoming return-fr
|
||||
|
||||
The breakpoint exception is commonly used in debuggers: When the user sets a breakpoint, the debugger overwrites the corresponding instruction with the `int3` instruction so that the CPU throws the breakpoint exception when it reaches that line. When the user wants to continue the program, the debugger replaces the `int3` instruction with the original instruction again and continues the program. For more details, see the [How debuggers work] series.
|
||||
|
||||
[How debuggers work]: https://eli.thegreenplace.net/2011/01/27/how-debuggers-work-part-2-breakpoints
|
||||
[How debuggers work]: http://eli.thegreenplace.net/2011/01/27/how-debuggers-work-part-2-breakpoints
|
||||
|
||||
For our use case, we don't need to overwrite any instructions (it wouldn't even be possible since we [set the page table flags] to read-only). Instead, we just want to print a message when the breakpoint instruction is executed and then continue the program.
|
||||
|
||||
[set the page table flags]: @/edition-1/posts/07-remap-the-kernel/index.md#using-the-correct-flags
|
||||
[set the page table flags]: @/first-edition/posts/07-remap-the-kernel/index.md#using-the-correct-flags
|
||||
|
||||
### Catching Breakpoints
|
||||
Let's start by defining a handler function for the breakpoint exception:
|
||||
@@ -126,9 +126,9 @@ Instead of pushing a return address, the CPU pushes the stack and instruction po
|
||||
So we can't use a normal `ret` instruction, since it expects a different stack frame layout. Instead, there is a special instruction for returning from exceptions: `iretq`.
|
||||
|
||||
### The `iretq` Instruction
|
||||
The `iretq` instruction is the one and only way to return from exceptions and is specifically designed for this purpose. The AMD64 instruction manual ([PDF][amd-manual]) even demands that `iretq` “_must_ be used to terminate the exception or interrupt handler associated with the exception”.
|
||||
The `iretq` instruction is the one and only way to return from exceptions and is specifically designed for this purpose. The AMD64 manual ([PDF][amd-manual]) even demands that `iretq` “_must_ be used to terminate the exception or interrupt handler associated with the exception”.
|
||||
|
||||
[amd-manual]: https://www.amd.com/system/files/TechDocs/24594.pdf
|
||||
[amd-manual]: https://support.amd.com/TechDocs/24594.pdf
|
||||
|
||||
IRETQ restores `rip`, `cs`, `rflags`, `rsp`, and `ss` from the values saved on the stack and thus continues the interrupted program. The instruction does not handle the optional error code, so it must be popped from the stack before.
|
||||
|
||||
@@ -144,7 +144,7 @@ EXCEPTION: BREAKPOINT at 0x110970
|
||||
|
||||
So let's disassemble the instruction at `0x110970` and its predecessor:
|
||||
|
||||
```bash
|
||||
```shell
|
||||
> objdump -d build/kernel-x86_64.bin | grep -B1 "110970:"
|
||||
11096f: cc int3
|
||||
110970: 48 c7 01 2a 00 00 00 movq $0x2a,(%rcx)
|
||||
@@ -216,7 +216,7 @@ Instead of the expected _“It did not crash”_ message after the breakpoint ex
|
||||
### Debugging
|
||||
Let's debug it using GDB. For that we execute `make debug` in one terminal (which starts QEMU with the `-s -S` flags) and then `make gdb` (which starts and connects GDB) in a second terminal. For more information about GDB debugging, check out our [Set Up GDB] guide.
|
||||
|
||||
[Set Up GDB]: @/edition-1/extra/set-up-gdb/index.md
|
||||
[Set Up GDB]: @/first-edition/extra/set-up-gdb/index.md
|
||||
|
||||
First we want to check if our `iretq` was successful. Therefore we set a breakpoint on the `println!("It did not crash line!")` statement in `src/lib.rs`. Let's assume that it's on line 61:
|
||||
|
||||
@@ -250,7 +250,7 @@ However, there is a major difference between exceptions and function calls: A fu
|
||||
[Calling conventions] specify the details of a function call. For example, they specify where function parameters are placed (e.g. in registers or on the stack) and how results are returned. On x86_64 Linux, the following rules apply for C functions (specified in the [System V ABI]):
|
||||
|
||||
[Calling conventions]: https://en.wikipedia.org/wiki/Calling_convention
|
||||
[System V ABI]: https://refspecs.linuxbase.org/elf/gabi41.pdf
|
||||
[System V ABI]: http://refspecs.linuxbase.org/elf/gabi41.pdf
|
||||
|
||||
- the first six integer arguments are passed in registers `rdi`, `rsi`, `rdx`, `rcx`, `r8`, `r9`
|
||||
- additional arguments are passed on the stack
|
||||
@@ -304,7 +304,7 @@ Unfortunately, Rust does not support such a calling convention. It was [proposed
|
||||
|
||||
[interrupt calling conventions]: https://github.com/rust-lang/rfcs/pull/1275
|
||||
[Naked functions]: https://github.com/rust-lang/rfcs/blob/master/text/1201-naked-fns.md
|
||||
[naked fn post]: @/edition-1/extra/naked-exceptions/02-better-exception-messages/index.md#naked-functions
|
||||
[naked fn post]: @/first-edition/extra/naked-exceptions/02-better-exception-messages/index.md#naked-functions
|
||||
|
||||
### A naked wrapper function
|
||||
|
||||
@@ -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 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.
|
||||
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.
|
||||
|
||||
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_:
|
||||
|
||||
@@ -518,7 +518,7 @@ The `llvm-target` field specifies the target triple that is passed to LLVM. We w
|
||||
|
||||
The other fields are used for conditional compilation. This allows crate authors to use `cfg` variables to write special code for depending on the OS or the architecture. There isn't any up-to-date documentation about these fields but the [corresponding source code][target specification] is quite readable.
|
||||
|
||||
[data layout]: https://llvm.org/docs/LangRef.html#data-layout
|
||||
[data layout]: http://llvm.org/docs/LangRef.html#data-layout
|
||||
[target specification]: https://github.com/rust-lang/rust/blob/c772948b687488a087356cb91432425662e034b9/src/librustc_back/target/mod.rs#L194-L214
|
||||
|
||||
#### Disabling MMX and SSE
|
||||
@@ -574,7 +574,7 @@ It doesn't compile anymore. The error tells us that the Rust compiler no longer
|
||||
The [core library] is implicitly linked to all `no_std` crates and contains things such as `Result`, `Option`, and iterators. We've used that library without problems since [the very beginning], so why is it no longer available?
|
||||
|
||||
[core library]: https://doc.rust-lang.org/nightly/core/index.html
|
||||
[the very beginning]: @/edition-1/posts/03-set-up-rust/index.md
|
||||
[the very beginning]: @/first-edition/posts/03-set-up-rust/index.md
|
||||
|
||||
The problem is that the core library is distributed together with the Rust compiler as a _precompiled_ library. So it is only valid for the host triple, which is `x86_64-unknown-linux-gnu` in our case. If we want to compile code for other targets, we need to recompile `core` for these targets first.
|
||||
|
||||
@@ -695,7 +695,7 @@ So now our return-from-exception logic works without problems in _most_ cases. H
|
||||
## The Red Zone
|
||||
The [red zone] is an optimization of the [System V ABI] that allows functions to temporary use the 128 bytes below its stack frame without adjusting the stack pointer:
|
||||
|
||||
[red zone]: https://eli.thegreenplace.net/2011/09/06/stack-frame-layout-on-x86-64#the-red-zone
|
||||
[red zone]: http://eli.thegreenplace.net/2011/09/06/stack-frame-layout-on-x86-64#the-red-zone
|
||||
|
||||

|
||||
|
||||
@@ -709,7 +709,7 @@ However, this optimization leads to huge problems with exceptions. Let's assume
|
||||
|
||||
The CPU and the exception handler overwrite the data in red zone. But this data is still needed by the interrupted function. So the function won't work correctly anymore when we return from the exception handler. It might fail or cause another exception, but it could also lead to strange bugs that [take weeks to debug].
|
||||
|
||||
[take weeks to debug]: https://forum.osdev.org/viewtopic.php?t=21720
|
||||
[take weeks to debug]: http://forum.osdev.org/viewtopic.php?t=21720
|
||||
|
||||
### Adjusting our Exception Handler?
|
||||
The problem is that the [System V ABI] demands that the red zone _“shall not be modified by signal or interrupt handlers.”_ Our current exception handlers do not respect this. We could try to fix it by subtracting 128 from the stack pointer before pushing anything:
|
||||
|
Before Width: | Height: | Size: 17 KiB After Width: | Height: | Size: 17 KiB |
|
Before Width: | Height: | Size: 16 KiB After Width: | Height: | Size: 16 KiB |
|
Before Width: | Height: | Size: 18 KiB After Width: | Height: | Size: 18 KiB |
|
Before Width: | Height: | Size: 16 KiB After Width: | Height: | Size: 16 KiB |
|
Before Width: | Height: | Size: 6.2 KiB After Width: | Height: | Size: 6.2 KiB |
|
Before Width: | Height: | Size: 4.2 KiB After Width: | Height: | Size: 4.2 KiB |
|
Before Width: | Height: | Size: 2.2 KiB After Width: | Height: | Size: 2.2 KiB |
|
Before Width: | Height: | Size: 6.7 KiB After Width: | Height: | Size: 6.7 KiB |
@@ -0,0 +1,6 @@
|
||||
+++
|
||||
title = "Handling Exceptions using naked Functions"
|
||||
sort_by = "weight"
|
||||
template = "first-edition/handling-exceptions-with-naked-fns.html"
|
||||
insert_anchor_links = "left"
|
||||
+++
|
||||
|
Before Width: | Height: | Size: 68 KiB After Width: | Height: | Size: 68 KiB |
@@ -32,7 +32,7 @@ Remote 'g' packet reply is too long: [a very long number]
|
||||
```
|
||||
This issue is known [since 2012][gdb issue patch] but it is still not fixed. Maybe we find the reason in the [issue thread][gdb issue thread]:
|
||||
|
||||
[gdb issue patch]: https://web.archive.org/web/20190114181420/https://www.cygwin.com/ml/gdb-patches/2012-03/msg00116.html
|
||||
[gdb issue patch]: http://www.cygwin.com/ml/gdb-patches/2012-03/msg00116.html
|
||||
[gdb issue thread]: https://sourceware.org/bugzilla/show_bug.cgi?id=13984#c11
|
||||
|
||||
> from my (limited) experience, unless you ping the gdb-patches list weekly, this patch is more likely to remain forgotten :-)
|
||||
@@ -73,5 +73,5 @@ After connecting to QEMU, you can use various gdb commands to control execution
|
||||
|
||||
Of course there are many more commands. Feel free to send a PR if you think this list is missing something important. For a more complete GDB overview, check out [Beej's Quick Guide][bggdb] or the [website for Harvard's CS161 course][CS161].
|
||||
|
||||
[bggdb]: https://beej.us/guide/bggdb/
|
||||
[CS161]: https://www.eecs.harvard.edu/~cs161/resources/gdb.html
|
||||
[bggdb]: http://beej.us/guide/bggdb/
|
||||
[CS161]: http://www.eecs.harvard.edu/~cs161/resources/gdb.html
|
||||
@@ -4,12 +4,12 @@ weight = 1
|
||||
path = "multiboot-kernel"
|
||||
aliases = ["multiboot-kernel.html", "/2015/08/18/multiboot-kernel/", "/rust-os/multiboot-kernel.html"]
|
||||
date = 2015-08-18
|
||||
template = "edition-1/page.html"
|
||||
template = "first-edition/page.html"
|
||||
+++
|
||||
|
||||
This post explains how to create a minimal x86 operating system kernel using the Multiboot standard. In fact, it will just boot and print `OK` to the screen. In subsequent blog posts we will extend it using the [Rust] programming language.
|
||||
|
||||
[Rust]: https://www.rust-lang.org/
|
||||
[Rust]: http://www.rust-lang.org/
|
||||
|
||||
<!-- more -->
|
||||
|
||||
@@ -28,19 +28,19 @@ When you turn on a computer, it loads the [BIOS] from some special flash memory.
|
||||
|
||||
[BIOS]: https://en.wikipedia.org/wiki/BIOS
|
||||
[protected mode]: https://en.wikipedia.org/wiki/Protected_mode
|
||||
[real mode]: https://wiki.osdev.org/Real_Mode
|
||||
[real mode]: http://wiki.osdev.org/Real_Mode
|
||||
|
||||
We won't write a bootloader because that would be a complex project on its own (if you really want to do it, check out [_Rolling Your Own Bootloader_]). Instead we will use one of the [many well-tested bootloaders][bootloader comparison] out there to boot our kernel from a CD-ROM. But which one?
|
||||
|
||||
[_Rolling Your Own Bootloader_]: https://wiki.osdev.org/Rolling_Your_Own_Bootloader
|
||||
[_Rolling Your Own Bootloader_]: http://wiki.osdev.org/Rolling_Your_Own_Bootloader
|
||||
[bootloader comparison]: https://en.wikipedia.org/wiki/Comparison_of_boot_loaders
|
||||
|
||||
## Multiboot
|
||||
Fortunately there is a bootloader standard: the [Multiboot Specification][multiboot]. Our kernel just needs to indicate that it supports Multiboot and every Multiboot-compliant bootloader can boot it. We will use the Multiboot 2 specification ([PDF][Multiboot 2]) together with the well-known [GRUB 2] bootloader.
|
||||
|
||||
[multiboot]: https://en.wikipedia.org/wiki/Multiboot_Specification
|
||||
[multiboot 2]: https://nongnu.askapache.com/grub/phcoder/multiboot.pdf
|
||||
[grub 2]: https://wiki.osdev.org/GRUB_2
|
||||
[multiboot 2]: http://nongnu.askapache.com/grub/phcoder/multiboot.pdf
|
||||
[grub 2]: http://wiki.osdev.org/GRUB_2
|
||||
|
||||
To indicate our Multiboot 2 support to the bootloader, our kernel must start with a _Multiboot Header_, which has the following format:
|
||||
|
||||
@@ -130,7 +130,7 @@ Through assembling, viewing and disassembling we can see the CPU [Opcodes] in ac
|
||||
To boot our executable later through GRUB, it should be an [ELF] executable. So we want `nasm` to create ELF [object files] instead of plain binaries. To do that, we simply pass the `‑f elf64` argument to it.
|
||||
|
||||
[ELF]: https://en.wikipedia.org/wiki/Executable_and_Linkable_Format
|
||||
[object files]: https://wiki.osdev.org/Object_Files
|
||||
[object files]: http://wiki.osdev.org/Object_Files
|
||||
|
||||
To create the ELF _executable_, we need to [link] the object files together. We use a custom [linker script] named `linker.ld`:
|
||||
|
||||
@@ -187,7 +187,7 @@ Idx Name Size VMA LMA File off Algn
|
||||
```
|
||||
_Note_: The `ld` and `objdump` commands are platform specific. If you're _not_ working on x86_64 architecture, you will need to [cross compile binutils]. Then use `x86_64‑elf‑ld` and `x86_64‑elf‑objdump` instead of `ld` and `objdump`.
|
||||
|
||||
[cross compile binutils]: @/edition-1/extra/cross-compile-binutils.md
|
||||
[cross compile binutils]: @/first-edition/extra/cross-compile-binutils.md
|
||||
|
||||
## Creating the ISO
|
||||
All PC BIOSes know how to boot from a CD-ROM, so we want to create a bootable CD-ROM image, containing our kernel and the GRUB bootloader's files, in a single file called an [ISO](https://en.wikipedia.org/wiki/ISO_image). Make the following directory structure and copy the `kernel.bin` to the right place:
|
||||
@@ -315,7 +315,7 @@ Now we can invoke `make` and all updated assembly files are compiled and linked.
|
||||
|
||||
In the [next post] we will create a page table and do some CPU configuration to switch to the 64-bit [long mode].
|
||||
|
||||
[next post]: @/edition-1/posts/02-entering-longmode/index.md
|
||||
[next post]: @/first-edition/posts/02-entering-longmode/index.md
|
||||
[long mode]: https://en.wikipedia.org/wiki/Long_mode
|
||||
|
||||
## Footnotes
|
||||
|
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 11 KiB |
|
Before Width: | Height: | Size: 27 KiB After Width: | Height: | Size: 27 KiB |
@@ -4,15 +4,15 @@ weight = 2
|
||||
path = "entering-longmode"
|
||||
aliases = ["entering-longmode.html", "/2015/08/25/entering-longmode/", "/rust-os/entering-longmode.html"]
|
||||
date = 2015-08-25
|
||||
template = "edition-1/page.html"
|
||||
template = "first-edition/page.html"
|
||||
[extra]
|
||||
updated = "2015-10-29"
|
||||
+++
|
||||
|
||||
In the [previous post] we created a minimal multiboot kernel. It just prints `OK` and hangs. The goal is to extend it and call 64-bit [Rust] code. But the CPU is currently in [protected mode] and allows only 32-bit instructions and up to 4GiB memory. So we need to set up _Paging_ and switch to the 64-bit [long mode] first.
|
||||
|
||||
[previous post]: @/edition-1/posts/01-multiboot-kernel/index.md
|
||||
[Rust]: https://www.rust-lang.org/
|
||||
[previous post]: @/first-edition/posts/01-multiboot-kernel/index.md
|
||||
[Rust]: http://www.rust-lang.org/
|
||||
[protected mode]: https://en.wikipedia.org/wiki/Protected_mode
|
||||
[long mode]: https://en.wikipedia.org/wiki/Long_mode
|
||||
|
||||
@@ -39,7 +39,7 @@ error:
|
||||
At address `0xb8000` begins the so-called [VGA text buffer]. It's an array of screen characters that are displayed by the graphics card. A [future post] will cover the VGA buffer in detail and create a Rust interface to it. But for now, manual bit-fiddling is the easiest option.
|
||||
|
||||
[VGA text buffer]: https://en.wikipedia.org/wiki/VGA-compatible_text_mode
|
||||
[future post]: @/edition-1/posts/04-printing-to-screen/index.md
|
||||
[future post]: @/first-edition/posts/04-printing-to-screen/index.md
|
||||
|
||||
A screen character consists of a 8 bit color code and a 8 bit [ASCII] character. We used the color code `4f` for all characters, which means white text on red background. `0x52` is an ASCII `R`, `0x45` is an `E`, `0x3a` is a `:`, and `0x20` is a space. The second space is overwritten by the given ASCII byte. Finally the CPU is stopped with the `hlt` instruction.
|
||||
|
||||
@@ -47,7 +47,7 @@ A screen character consists of a 8 bit color code and a 8 bit [ASCII] character.
|
||||
|
||||
Now we can add some check _functions_. A function is just a normal label with an `ret` (return) instruction at the end. The `call` instruction can be used to call it. Unlike the `jmp` instruction that just jumps to a memory address, the `call` instruction will push a return address to the stack (and the `ret` will jump to this address). But we don't have a stack yet. The [stack pointer] in the esp register could point to some important data or even invalid memory. So we need to update it and point it to some valid stack memory.
|
||||
|
||||
[stack pointer]: https://stackoverflow.com/a/1464052/866447
|
||||
[stack pointer]: http://stackoverflow.com/a/1464052/866447
|
||||
|
||||
### Creating a Stack
|
||||
To create stack memory we reserve some bytes at the end of our `boot.asm`:
|
||||
@@ -96,14 +96,14 @@ We use the `cmp` instruction to compare the value in `eax` to the magic value. I
|
||||
|
||||
In `no_multiboot`, we use the `jmp` (“jump”) instruction to jump to our error function. We could just as well use the `call` instruction, which additionally pushes the return address. But the return address is not needed because `error` never returns. To pass `0` as error code to the `error` function, we move it into `al` before the jump (`error` will read it from there).
|
||||
|
||||
[Multiboot specification]: https://nongnu.askapache.com/grub/phcoder/multiboot.pdf
|
||||
[Multiboot specification]: http://nongnu.askapache.com/grub/phcoder/multiboot.pdf
|
||||
[FLAGS register]: https://en.wikipedia.org/wiki/FLAGS_register
|
||||
|
||||
### CPUID check
|
||||
[CPUID] is a CPU instruction that can be used to get various information about the CPU. But not every processor supports it. CPUID detection is quite laborious, so we just copy a detection function from the [OSDev wiki][CPUID detection]:
|
||||
|
||||
[CPUID]: https://wiki.osdev.org/CPUID
|
||||
[CPUID detection]: https://wiki.osdev.org/Setting_Up_Long_Mode#Detection_of_CPUID
|
||||
[CPUID]: http://wiki.osdev.org/CPUID
|
||||
[CPUID detection]: http://wiki.osdev.org/Setting_Up_Long_Mode#Detection_of_CPUID
|
||||
|
||||
```nasm
|
||||
check_cpuid:
|
||||
@@ -151,7 +151,7 @@ Don't worry, you don't need to understand the details.
|
||||
### Long Mode check
|
||||
Now we can use CPUID to detect whether long mode can be used. I use code from [OSDev][long mode detection] again:
|
||||
|
||||
[long mode detection]: https://wiki.osdev.org/Setting_Up_Long_Mode#x86_or_x86-64
|
||||
[long mode detection]: http://wiki.osdev.org/Setting_Up_Long_Mode#x86_or_x86-64
|
||||
|
||||
```nasm
|
||||
check_long_mode:
|
||||
@@ -400,7 +400,7 @@ Bit(s) | Name | Meaning
|
||||
54 | 32-bit | must be 0 for 64-bit segments
|
||||
55-63 | ignored | ignored in 64-bit mode
|
||||
|
||||
[ring level]: https://wiki.osdev.org/Security#Rings
|
||||
[ring level]: http://wiki.osdev.org/Security#Rings
|
||||
|
||||
We need one code segment, a data segment is not necessary in 64-bit mode. Code segments have the following bits set: _descriptor type_, _present_, _executable_ and the _64-bit_ flag. Translated to assembly the long mode GDT looks like this:
|
||||
|
||||
@@ -451,7 +451,7 @@ gdt64:
|
||||
```
|
||||
We can't just use a normal label here, since we need the table _offset_. We calculate this offset using the current address `$` and set the label to this value using [equ]. Now we can use `gdt64.code` instead of 8 and this label will still work if we modify the GDT.
|
||||
|
||||
[equ]: https://www.nasm.us/doc/nasmdoc3.html#section-3.2.4
|
||||
[equ]: http://www.nasm.us/doc/nasmdoc3.html#section-3.2.4
|
||||
|
||||
In order to finally enter the true 64-bit mode, we need to load `cs` with `gdt64.code`. But we can't do it through `mov`. The only way to reload the code selector is a _far jump_ or a _far return_. These instructions work like a normal jump/return but change the code selector. We use a far jump to a long mode label:
|
||||
|
||||
@@ -492,8 +492,8 @@ _Congratulations_! You have successfully wrestled through this CPU configuration
|
||||
#### One Last Thing
|
||||
Above, we reloaded the code segment register `cs` with the new GDT offset. However, the data segment registers `ss`, `ds`, `es`, `fs`, and `gs` still contain the data segment offsets of the old GDT. This isn't necessarily bad, since they're ignored by almost all instructions in 64-bit mode. However, there are a few instructions that expect a valid data segment descriptor _or the null descriptor_ in those registers. An example is the the [iretq] instruction that we'll need in the [_Returning from Exceptions_] post.
|
||||
|
||||
[iretq]: @/edition-1/extra/naked-exceptions/03-returning-from-exceptions/index.md#the-iretq-instruction
|
||||
[_Returning from Exceptions_]: @/edition-1/extra/naked-exceptions/03-returning-from-exceptions/index.md
|
||||
[iretq]: @/first-edition/extra/naked-exceptions/03-returning-from-exceptions/index.md#the-iretq-instruction
|
||||
[_Returning from Exceptions_]: @/first-edition/extra/naked-exceptions/03-returning-from-exceptions/index.md
|
||||
|
||||
To avoid future problems, we reload all data segment registers with null:
|
||||
|
||||
@@ -515,7 +515,7 @@ long_mode_start:
|
||||
It's time to finally leave assembly behind and switch to [Rust]. Rust is a systems language without garbage collections that guarantees memory safety. Through a real type system and many abstractions it feels like a high-level language but can still be low-level enough for OS development. The [next post] describes the Rust setup.
|
||||
|
||||
[Rust]: https://www.rust-lang.org/
|
||||
[next post]: @/edition-1/posts/03-set-up-rust/index.md
|
||||
[next post]: @/first-edition/posts/03-set-up-rust/index.md
|
||||
|
||||
## Footnotes
|
||||
[^hardware_lookup]: In the x86 architecture, the page tables are _hardware walked_, so the CPU will look at the table on its own when it needs a translation. Other architectures, for example MIPS, just throw an exception and let the OS translate the virtual address.
|
||||
@@ -4,15 +4,15 @@ weight = 3
|
||||
path = "set-up-rust"
|
||||
aliases = ["set-up-rust.html", "setup-rust.html", "/2015/09/02/setup-rust/", "/rust-os/setup-rust.html"]
|
||||
date = 2015-09-02
|
||||
template = "edition-1/page.html"
|
||||
template = "first-edition/page.html"
|
||||
[extra]
|
||||
updated = "2017-04-12"
|
||||
+++
|
||||
|
||||
In the previous posts we created a [minimal Multiboot kernel][multiboot post] and [switched to Long Mode][long mode post]. Now we can finally switch to [Rust] code. Rust is a high-level language without runtime. It allows us to not link the standard library and write bare metal code. Unfortunately the setup is not quite hassle-free yet.
|
||||
|
||||
[multiboot post]: @/edition-1/posts/01-multiboot-kernel/index.md
|
||||
[long mode post]: @/edition-1/posts/02-entering-longmode/index.md
|
||||
[multiboot post]: @/first-edition/posts/01-multiboot-kernel/index.md
|
||||
[long mode post]: @/first-edition/posts/02-entering-longmode/index.md
|
||||
[Rust]: https://www.rust-lang.org/
|
||||
|
||||
<!-- more -->
|
||||
@@ -36,7 +36,7 @@ The code from this post (and all following) is [automatically tested](https://tr
|
||||
## Creating a Cargo project
|
||||
[Cargo] is Rust's excellent package manager. Normally you would call `cargo new` when you want to create a new project folder. We can't use it because our folder already exists, so we need to do it manually. Fortunately we only need to add a cargo configuration file named `Cargo.toml`:
|
||||
|
||||
[Cargo]: https://doc.crates.io/guide.html
|
||||
[Cargo]: http://doc.crates.io/guide.html
|
||||
|
||||
```toml
|
||||
[package]
|
||||
@@ -49,7 +49,7 @@ crate-type = ["staticlib"]
|
||||
```
|
||||
The `package` section contains required project metadata such as the [semantic crate version]. The `lib` section specifies that we want to build a static library, i.e. a library that contains all of its dependencies. This is required to link the Rust project with our kernel.
|
||||
|
||||
[semantic crate version]: https://doc.rust-lang.org/cargo/reference/manifest.html#the-package-section
|
||||
[semantic crate version]: http://doc.crates.io/manifest.html#the-package-section
|
||||
|
||||
Now we place our root source file in `src/lib.rs`:
|
||||
|
||||
@@ -92,7 +92,7 @@ Let's define some properties of our target system:
|
||||
- **No SSE**: Our target might not have [SSE] support. Even if it does, we probably don't want to use SSE instructions in our kernel, because it makes interrupt handling much slower. We will explain this in detail in the [“Handling Exceptions”] post.
|
||||
- **No hardware floats**: The `x86_64` architecture uses SSE instructions for floating point operations, which we don't want to use (see the previous point). So we also need to avoid hardware floating point operations in our kernel. Instead, we will use _soft floats_, which are basically software functions that emulate floating point operations using normal integers.
|
||||
|
||||
[“Handling Exceptions”]: @/edition-1/posts/09-handling-exceptions/index.md
|
||||
[“Handling Exceptions”]: @/first-edition/posts/09-handling-exceptions/index.md
|
||||
|
||||
### Target Specifications
|
||||
Rust allows us to define [custom targets] through a JSON configuration file. A minimal target specification equal to `x86_64-unknown-linux-gnu` (the default 64-bit Linux target) looks like this:
|
||||
@@ -114,7 +114,7 @@ Rust allows us to define [custom targets] through a JSON configuration file. A m
|
||||
|
||||
The `llvm-target` field specifies the target triple that is passed to LLVM. [Target triples] are a naming convention that define the CPU architecture (e.g., `x86_64` or `arm`), the vendor (e.g., `apple` or `unknown`), the operating system (e.g., `windows` or `linux`), and the [ABI] \(e.g., `gnu` or `msvc`). For example, the target triple for 64-bit Linux is `x86_64-unknown-linux-gnu` and for 32-bit Windows the target triple is `i686-pc-windows-msvc`.
|
||||
|
||||
[Target triples]: https://llvm.org/docs/LangRef.html#target-triple
|
||||
[Target triples]: http://llvm.org/docs/LangRef.html#target-triple
|
||||
[ABI]: https://en.wikipedia.org/wiki/Application_binary_interface
|
||||
|
||||
The `data-layout` field is also passed to LLVM and specifies how data should be laid out in memory. It consists of various specifications separated by a `-` character. For example, the `e` means little endian and `S128` specifies that the stack should be 128 bits (= 16 byte) aligned. The format is described in detail in the [LLVM documentation][data layout] but there shouldn't be a reason to change this string.
|
||||
@@ -126,7 +126,7 @@ The `linker-flavor` field was recently introduced in [#40018] with the intention
|
||||
|
||||
The other fields are used for conditional compilation. This allows crate authors to use `cfg` variables to write special code for depending on the OS or the architecture. There isn't any up-to-date documentation about these fields but the [corresponding source code][target specification] is quite readable.
|
||||
|
||||
[data layout]: https://llvm.org/docs/LangRef.html#data-layout
|
||||
[data layout]: http://llvm.org/docs/LangRef.html#data-layout
|
||||
[target specification]: https://github.com/rust-lang/rust/blob/c772948b687488a087356cb91432425662e034b9/src/librustc_back/target/mod.rs#L194-L214
|
||||
|
||||
### A Kernel Target Specification
|
||||
@@ -152,8 +152,8 @@ As `llvm-target` we use `x86_64-unknown-none`, which defines the `x86_64` archit
|
||||
#### The Red Zone
|
||||
The [red zone] is an optimization of the [System V ABI] that allows functions to temporary use the 128 bytes below its stack frame without adjusting the stack pointer:
|
||||
|
||||
[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
|
||||
[red zone]: http://eli.thegreenplace.net/2011/09/06/stack-frame-layout-on-x86-64#the-red-zone
|
||||
[System V ABI]: http://wiki.osdev.org/System_V_ABI
|
||||
|
||||

|
||||
|
||||
@@ -167,7 +167,7 @@ However, this optimization leads to huge problems with exceptions or hardware in
|
||||
|
||||
The CPU and the exception handler overwrite the data in red zone. But this data is still needed by the interrupted function. So the function won't work correctly anymore when we return from the exception handler. This might lead to strange bugs that [take weeks to debug].
|
||||
|
||||
[take weeks to debug]: https://forum.osdev.org/viewtopic.php?t=21720
|
||||
[take weeks to debug]: http://forum.osdev.org/viewtopic.php?t=21720
|
||||
|
||||
To avoid such bugs when we implement exception handling in the future, we disable the red zone right from the beginning. This is achieved by adding the `"disable-redzone": true` line to our target configuration file.
|
||||
|
||||
@@ -408,7 +408,7 @@ So the linker can't find a function named `_Unwind_Resume` that is referenced e.
|
||||
|
||||
By default, the destructors of all stack variables are run when a `panic` occurs. This is called _unwinding_ and allows parent threads to [recover from panics]. However, it requires a platform specific gcc library, which isn't available in our kernel.
|
||||
|
||||
[recover from panics]: https://www.howtobuildsoftware.com/index.php/how-do/fFH/rust-recovering-from-panic-in-another-thread
|
||||
[recover from panics]: https://doc.rust-lang.org/book/concurrency.html#panics
|
||||
|
||||
Fortunately, Rust allows us to disable unwinding for our target. For that we add the following line to our `x86_64-blog_os.json` file:
|
||||
|
||||
@@ -479,17 +479,17 @@ Some notes:
|
||||
- `buffer_ptr` is a [raw pointer] that points to the center of the VGA text buffer
|
||||
- Rust doesn't know the VGA buffer and thus can't guarantee that writing to the `buffer_ptr` is safe (it could point to important data). So we need to tell Rust that we know what we are doing by using an [unsafe block].
|
||||
|
||||
[byte string]: https://doc.rust-lang.org/reference/tokens.html#characters-and-strings
|
||||
[byte string]: https://doc.rust-lang.org/reference.html#characters-and-strings
|
||||
[enumerate]: https://doc.rust-lang.org/nightly/core/iter/trait.Iterator.html#method.enumerate
|
||||
[unsafe block]: https://doc.rust-lang.org/book/unsafe.html
|
||||
|
||||
### Stack Overflows
|
||||
Since we still use the small 64 byte [stack from the last post], we must be careful not to [overflow] it. Normally, Rust tries to avoid stack overflows through _guard pages_: The page below the stack isn't mapped and such a stack overflow triggers a page fault (instead of silently overwriting random memory). But we can't unmap the page below our stack right now since we currently use only a single big page. Fortunately the stack is located just above the page tables. So some important page table entry would probably get overwritten on stack overflow and then a page fault occurs, too.
|
||||
|
||||
[stack from the last post]: @/edition-1/posts/02-entering-longmode/index.md#creating-a-stack
|
||||
[stack from the last post]: @/first-edition/posts/02-entering-longmode/index.md#creating-a-stack
|
||||
[overflow]: https://en.wikipedia.org/wiki/Stack_overflow
|
||||
|
||||
## What's next?
|
||||
Until now we write magic bits to some memory location when we want to print something to screen. In the [next post] we create a abstraction for the VGA text buffer that allows us to print strings in different colors and provides a simple interface.
|
||||
|
||||
[next post]: @/edition-1/posts/04-printing-to-screen/index.md
|
||||
[next post]: @/first-edition/posts/04-printing-to-screen/index.md
|
||||
|
Before Width: | Height: | Size: 6.2 KiB After Width: | Height: | Size: 6.2 KiB |