mirror of
https://github.com/phil-opp/blog_os.git
synced 2025-12-16 14:27:49 +00:00
Merge pull request #878 from hamidrezakp/persian-translation
Adding Persian translation for posts of `Bare Bone` Chapter
This commit is contained in:
@@ -11,6 +11,7 @@ languages = [
|
||||
{ code = "zh-CN" }, # Chinese (simplified)
|
||||
{ code = "zh-TW" }, # Chinese (traditional)
|
||||
{ code = "ja" }, # Japanese
|
||||
{ code = "fa" }, # Persian
|
||||
]
|
||||
|
||||
ignored_content = ["*/README.md"]
|
||||
@@ -43,3 +44,6 @@ lang_name = "Chinese (traditional)"
|
||||
|
||||
[translations.ja]
|
||||
lang_name = "Japanese"
|
||||
|
||||
[translations.fa]
|
||||
lang_name = "Persian"
|
||||
|
||||
@@ -0,0 +1,526 @@
|
||||
+++
|
||||
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
|
||||
[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/de-de/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>
|
||||
|
||||
## خلاصه
|
||||
|
||||
یک باینری مستقل مینیمال راست مانند این است:
|
||||
|
||||
`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_` یک پشته مقداردهی اولیه میشود. **بنابراین برای هر گونه استفاده واقعی از چنین باینری، مراحل بیشتری لازم است**.
|
||||
|
||||
## بعدی چیست؟
|
||||
|
||||
[پست بعدی] مراحل مورد نیاز برای تبدیل باینری مستقل به حداقل هسته سیستمعامل را توضیح میدهد. که شامل ایجاد یک هدف سفارشی، ترکیب اجرایی ما با بوتلودر و یادگیری نحوه چاپ چیزی در صفحه است.
|
||||
|
||||
[پست بعدی]: @/second-edition/posts/02-minimal-rust-kernel/index.fa.md
|
||||
@@ -0,0 +1,503 @@
|
||||
+++
|
||||
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]: @/second-edition/posts/01-freestanding-rust-binary/index.md
|
||||
|
||||
<!-- more -->
|
||||
|
||||
این بلاگ بصورت آزاد روی [گیتهاب] توسعه داده شده است. اگر شما مشکل یا سوالی دارید، لطفاً آنجا یک ایشو باز کنید. شما همچنین میتوانید [در زیر] این پست کامنت بگذارید. منبع کد کامل این پست را میتوانید در بِرَنچ [`post-02`][post branch] پیدا کنید.
|
||||
|
||||
[گیتهاب]: https://github.com/phil-opp/blog_os
|
||||
[در زیر]: #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 هستید، [نسخه اول] مجموعه پستهای این وبلاگ را بررسی کنید.
|
||||
|
||||
[نسخه اول]: @/first-edition/_index.md
|
||||
|
||||
### UEFI
|
||||
|
||||
(ما در حال حاضر پشتیبانی UEFI را ارائه نمیدهیم، اما خیلی دوست داریم این کار را انجام دهیم! اگر میخواهید کمک کنید، لطفاً در [ایشو گیتهاب](https://github.com/phil-opp/blog_os/issues/349) به ما بگویید.)
|
||||
|
||||
## یک هسته مینیمال
|
||||
|
||||
اکنون که تقریباً میدانیم چگونه یک کامپیوتر بوت میشود، وقت آن است که هسته مینیمال خودمان را ایجاد کنیم. هدف ما ایجاد دیسک ایمیجی میباشد که “!Hello World” را هنگام بوت شدن چاپ کند. برای این منظور از [باینری مستقل Rust] که در پست قبل دیدید استفاده میکنیم.
|
||||
|
||||
همانطور که ممکن است به یاد داشته باشید، باینری مستقل را از طریق `cargo` ایجاد کردیم، اما با توجه به سیستم عامل، به نامهای ورودی و پرچمهای کامپایل مختلف نیاز داشتیم. به این دلیل که `cargo` به طور پیش فرض برای سیستم میزبان بیلد میکند، بطور مثال سیستمی که از آن برای نوشتن هسته استفاده میکنید. این چیزی نیست که ما برای هسته خود بخواهیم، زیرا منطقی نیست که هسته سیستم عاملمان را روی یک سیستم عامل دیگر اجرا کنیم. در عوض، ما میخواهیم هسته را برای یک _سیستم هدف_ کاملاً مشخص کامپایل کنیم.
|
||||
|
||||
### نصب 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”_ (ترجمه: منطقه قرمز) را غیرفعال کنیم، زیرا در غیر این صورت باعث خراب شدن پشته میشود. برای اطلاعات بیشتر، به پست جداگانه ما در مورد [غیرفعال کردن منطقه قرمز] مراجعه کنید.
|
||||
|
||||
[غیرفعال کردن منطقه قرمز]: @/second-edition/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](@/second-edition/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_` نیاز داریم همانطور که در [پست قبلی] توضیح داده شد:
|
||||
|
||||
[پست قبلی]: @/second-edition/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` را نیز اضافه خواهیم کرد.
|
||||
702
blog/content/second-edition/posts/03-vga-text-buffer/index.fa.md
Normal file
702
blog/content/second-edition/posts/03-vga-text-buffer/index.fa.md
Normal file
@@ -0,0 +1,702 @@
|
||||
+++
|
||||
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
|
||||
[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/unstable-book/language-features/const-fn.html
|
||||
|
||||
### استاتیکهای تنبل (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 ایجاد خواهیم کرد.
|
||||
1025
blog/content/second-edition/posts/04-testing/index.fa.md
Normal file
1025
blog/content/second-edition/posts/04-testing/index.fa.md
Normal file
File diff suppressed because it is too large
Load Diff
7
blog/content/second-edition/posts/_index.fa.md
Normal file
7
blog/content/second-edition/posts/_index.fa.md
Normal file
@@ -0,0 +1,7 @@
|
||||
+++
|
||||
title = "Posts"
|
||||
sort_by = "weight"
|
||||
insert_anchor_links = "left"
|
||||
render = false
|
||||
page_template = "second-edition/page.html"
|
||||
+++
|
||||
Reference in New Issue
Block a user