From a1bb75850d823c9f94f4035e7af9ff8ed5a021a4 Mon Sep 17 00:00:00 2001 From: "Hamid R. K. Pishghadam" Date: Thu, 12 Nov 2020 16:10:55 +0330 Subject: [PATCH 1/6] Adding persian support --- blog/config.toml | 1 + blog/content/second-edition/posts/_index.fa.md | 7 +++++++ 2 files changed, 8 insertions(+) create mode 100644 blog/content/second-edition/posts/_index.fa.md diff --git a/blog/config.toml b/blog/config.toml index ddf9a677..4bb63fb3 100644 --- a/blog/config.toml +++ b/blog/config.toml @@ -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"] diff --git a/blog/content/second-edition/posts/_index.fa.md b/blog/content/second-edition/posts/_index.fa.md new file mode 100644 index 00000000..d9da15d1 --- /dev/null +++ b/blog/content/second-edition/posts/_index.fa.md @@ -0,0 +1,7 @@ ++++ +title = "Posts" +sort_by = "weight" +insert_anchor_links = "left" +render = false +page_template = "second-edition/page.html" ++++ From c39b1e2b3cfb89c4ade35663d23a3c2721860d16 Mon Sep 17 00:00:00 2001 From: "Hamid R. K. Pishghadam" Date: Thu, 12 Nov 2020 16:14:32 +0330 Subject: [PATCH 2/6] Post 01 translation done --- .../01-freestanding-rust-binary/index.fa.md | 526 ++++++++++++++++++ 1 file changed, 526 insertions(+) create mode 100644 blog/content/second-edition/posts/01-freestanding-rust-binary/index.fa.md diff --git a/blog/content/second-edition/posts/01-freestanding-rust-binary/index.fa.md b/blog/content/second-edition/posts/01-freestanding-rust-binary/index.fa.md new file mode 100644 index 00000000..b4e766a7 --- /dev/null +++ b/blog/content/second-edition/posts/01-freestanding-rust-binary/index.fa.md @@ -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 + + + +این بلاگ بصورت آزاد بر روی [گیت‌هاب] توسعه داده شده. اگر مشکل یا سوالی دارید، لطفاً آن‌جا یک ایشو باز کنید. همچنین می‌توانید [در زیر] این پست کامنت بگذارید. سورس کد کامل این پست را می‌توانید در بِرَنچ [`post-01`][post branch] پیدا کنید. + +[گیت‌هاب]: https://github.com/phil-opp/blog_os +[در زیر]: #comments +[post branch]: https://github.com/phil-opp/blog_os/tree/post-01 + + + +## مقدمه +برای نوشتن هسته سیستم‌عامل، ما به کدی نیاز داریم که به هیچ یک از ویژگی‌های سیستم‌عامل نیازی نداشته باشد. یعنی نمی‌توانیم از نخ‌ها (ترجمه: 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، می‌توان خطاهای لینکر را با استفاده از مجموعه خاصی از آرگومان‌ها به لینکر حل کرد. این روشی نیست که ما برای هسته خود استفاده کنیم، بنابراین این بخش اختیاری است و فقط برای کامل بودن ارائه می‌شود. برای نشان دادن محتوای اختیاری، روی _"آرگومان‌های لینکر"_ در زیر کلیک کنید. + +
+ +آرگومان‌های لینکر + +در این بخش، ما در مورد خطاهای لینکر که در لینوکس، ویندوز و 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) شرح داده شده است، احتمالاً ایده بهتری است. + +
+ +## خلاصه + +یک باینری مستقل مینیمال راست مانند این است: + +`src/main.rs`: + +```rust +#![no_std] // don't link the Rust standard library +#![no_main] // disable all Rust-level entry points + +use core::panic::PanicInfo; + +#[no_mangle] // don't mangle the name of this function +pub extern "C" fn _start() -> ! { + // this function is the entry point, since the linker looks for a function + // named `_start` by default + loop {} +} + +/// This function is called on panic. +#[panic_handler] +fn panic(_info: &PanicInfo) -> ! { + loop {} +} +``` + +`Cargo.toml`: + +```toml +[package] +name = "crate_name" +version = "0.1.0" +authors = ["Author Name "] + +# the profile used for `cargo build` +[profile.dev] +panic = "abort" # disable stack unwinding on panic + +# the profile used for `cargo build --release` +[profile.release] +panic = "abort" # disable stack unwinding on panic +``` + +برای ساخت این باینری، ما باید برای یک هدف 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 From 1ba70ef3981fb72499cb13a5f459733f61f084f0 Mon Sep 17 00:00:00 2001 From: "Hamid R. K. Pishghadam" Date: Thu, 12 Nov 2020 16:15:00 +0330 Subject: [PATCH 3/6] Post 02 translation done --- .../posts/02-minimal-rust-kernel/index.fa.md | 503 ++++++++++++++++++ 1 file changed, 503 insertions(+) create mode 100644 blog/content/second-edition/posts/02-minimal-rust-kernel/index.fa.md diff --git a/blog/content/second-edition/posts/02-minimal-rust-kernel/index.fa.md b/blog/content/second-edition/posts/02-minimal-rust-kernel/index.fa.md new file mode 100644 index 00000000..63d98352 --- /dev/null +++ b/blog/content/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 + + + +این بلاگ بصورت آزاد روی [گیت‌هاب] توسعه داده شده است. اگر شما مشکل یا سوالی دارید، لطفاً آن‌جا یک ایشو باز کنید. شما همچنین می‌توانید [در زیر] این پست کامنت بگذارید. منبع کد کامل این پست را می‌توانید در بِرَنچ [`post-02`][post branch] پیدا کنید. + +[گیت‌هاب]: https://github.com/phil-opp/blog_os +[در زیر]: #comments +[post branch]: https://github.com/phil-opp/blog_os/tree/post-02 + + + +## فرآیند بوت شدن +وقتی یک رایانه را روشن می‌کنید، شروع به اجرای کد فِرْم‌وِر (کلمه: 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 + +اگر علاقه‌مند به ساخت بوت‌لودر هستید: با ما همراه باشید‌، مجموعه‌ای از پست‌ها در این زمینه از قبل برنامه‌ریزی شده است! + +#### استاندارد بوت چندگانه + +برای جلوگیری از این که هر سیستم عاملی بوت‌لودر خود را پیاده‌سازی کند، که فقط با یک سیستم عامل سازگار است، [بنیاد نرم افزار آزاد] در سال 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` نصب کنیم. + +
+ +**یادداشت:** کلید پیکربندی `unstable.build-std` به نسخه‌‌ای جدیدتر از نسخه 2020-07-15 شبانه Rust نیاز دارد. + +
+ +پس از تنظیم کلید پیکربندی `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 + +![screen output for common ASCII characters](https://upload.wikimedia.org/wikipedia/commons/f/f8/Codepage-437.png) + +ما در پست بعدی، جایی که اولین درایور کوچک را برای آن می‌نویسیم، در مورد قالب دقیق بافر متن VGA بحث خواهیم کرد. برای چاپ “!Hello World”‌، فقط باید بدانیم که بافر در آدرس `0xb8000` قرار دارد و هر سلول کاراکتر از یک بایت ASCII و یک بایت رنگ تشکیل شده است. + +پیاده‌سازی مشابه این است: + +```rust +static HELLO: &[u8] = b"Hello World!"; + +#[no_mangle] +pub extern "C" fn _start() -> ! { + let vga_buffer = 0xb8000 as *mut u8; + + for (i, &byte) in HELLO.iter().enumerate() { + unsafe { + *vga_buffer.offset(i as isize * 2) = byte; + *vga_buffer.offset(i as isize * 2 + 1) = 0xb; + } + } + + loop {} +} +``` + +ابتدا عدد صحیح `0xb8000` را در یک اشاره‌گر خام (ترجمه: raw pointer) می‌ریزیم. سپس روی بایت‌های [رشته بایت][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] +``` + +این یک پنجره جداگانه با این شکل باز می‌کند: + +![QEMU showing "Hello World!"](qemu.png) + +می‌بینیم که “!Hello World” بر روی صفحه قابل مشاهده است. + +### ماشین واقعی + +همچنین می‌توانید آن را بر روی یک درایو USB رایت و بر روی یک دستگاه واقعی بوت کنید: + +``` +> dd if=target/x86_64-blog_os/debug/bootimage-blog_os.bin of=/dev/sdX && sync +``` + +در اینجا `sdX` نام دستگاه USB شماست. **مراقب باشید** که نام دستگاه را به درستی انتخاب کنید، زیرا همه داده‌های موجود در آن دستگاه بازنویسی می‌شوند. + +پس از رایت کردن ایمیج در USB، می‌توانید با بوت کردن، آن را بر روی سخت افزار واقعی اجرا کنید. برای راه اندازی از طریق USB احتمالاً باید از یک منوی بوت ویژه استفاده کنید یا ترتیب بوت را در پیکربندی BIOS تغییر دهید. توجه داشته باشید که این در حال حاضر برای دستگاه‌های UEFI کار نمی‌کند، زیرا کریت `bootloader` هنوز پشتیبانی UEFI را ندارد. + +### استفاده از `cargo run` + +برای سهولت اجرای هسته در QEMU، می‌توانیم کلید پیکربندی `runner` را برای کارگو تنظیم کنیم: + +```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` را نیز اضافه خواهیم کرد. From 6b0f9290ea09d230b41102dea234bcd45ca54b64 Mon Sep 17 00:00:00 2001 From: "Hamid R. K. Pishghadam" Date: Thu, 12 Nov 2020 16:15:36 +0330 Subject: [PATCH 4/6] Post 03 translation done --- .../posts/03-vga-text-buffer/index.fa.md | 702 ++++++++++++++++++ 1 file changed, 702 insertions(+) create mode 100644 blog/content/second-edition/posts/03-vga-text-buffer/index.fa.md diff --git a/blog/content/second-edition/posts/03-vga-text-buffer/index.fa.md b/blog/content/second-edition/posts/03-vga-text-buffer/index.fa.md new file mode 100644 index 00000000..68a004bd --- /dev/null +++ b/blog/content/second-edition/posts/03-vga-text-buffer/index.fa.md @@ -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 + + + +این بلاگ بصورت آزاد بر روی [گیت‌هاب] توسعه داده شده. اگر مشکل یا سوالی دارید، لطفا آنجا یک ایشو باز کنید. همچنین می‌توانید [در زیر] این پست کامنت بگذارید. سورس کد کامل این پست را می توانید در شاخه [`post-01`][post branch] پیدا کنید. + +[گیت‌هاب]: https://github.com/phil-opp/blog_os +[در زیر]: #comments +[post branch]: https://github.com/phil-opp/blog_os/tree/post-03 + + + +## بافر متن 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 + +![QEMU output with a yellow `Hello W■■rld!` in the lower left corner](vga-hello.png) + +توجه داشته باشید که `ö` به عنوان دو کاراکتر `■` چاپ شده است. به این دلیل که `ö` با دو بایت در [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; BUFFER_WIDTH]; BUFFER_HEIGHT], +} +``` +به جای `ScreenChar` ، ما اکنون از `Volatile` استفاده می کنیم. (نوع `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 = 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!”_ روی صفحه مشاهده می کنیم: + +![QEMU printing “Hello World!”](vga-hello-world.png) + +### چاپ پیام های پنیک + +اکنون که ماکرو `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` خود اضافه میکنیم ، خروجی زیر را می گیریم: + +![QEMU printing “panicked at 'Some panic message', src/main.rs:28:5](vga-panic.png) + +بنابراین ما نه تنها می‌دانیم که یک پنیک رخ داده است ، بلکه پیام پنیک و اینکه در کجای کد رخ داده است را نیز می‌دانیم. + +## خلاصه +در این پست با ساختار بافر متن VGA و نحوه نوشتن آن از طریق نگاشت حافظه در آدرس `0xb8000` آشنا شدیم. ما یک ماژول راست ایجاد کردیم که عدم امنیت نوشتن را در این بافر نگاشت حافظه شده را محصور می کند و یک رابط امن و راحت به خارج ارائه می دهد. + +همچنین دیدیم که به لطف کارگو ، اضافه کردن وابستگی به کتابخانه های دیگران چقدر آسان است. دو وابستگی که اضافه کردیم ، `lazy_static` و`spin` ، در توسعه سیستم عامل بسیار مفید هستند و ما در پست های بعدی از آنها در مکان های بیشتری استفاده خواهیم کرد. + +## بعدی چیست؟ +در پست بعدی نحوه راه اندازی چارچوب تست واحد (Unit Test) راست توضیح داده شده است. سپس از این پست چند تست واحد اساسی برای ماژول بافر VGA ایجاد خواهیم کرد. From a6be039bdb1db62908c84df771520928c3117a30 Mon Sep 17 00:00:00 2001 From: "Hamid R. K. Pishghadam" Date: Thu, 12 Nov 2020 16:15:55 +0330 Subject: [PATCH 5/6] Post 04 translation done --- .../posts/04-testing/index.fa.md | 1025 +++++++++++++++++ 1 file changed, 1025 insertions(+) create mode 100644 blog/content/second-edition/posts/04-testing/index.fa.md diff --git a/blog/content/second-edition/posts/04-testing/index.fa.md b/blog/content/second-edition/posts/04-testing/index.fa.md new file mode 100644 index 00000000..1084e40d --- /dev/null +++ b/blog/content/second-edition/posts/04-testing/index.fa.md @@ -0,0 +1,1025 @@ ++++ +title = "تست کردن" +weight = 4 +path = "fa/testing" +date = 2019-04-27 + +[extra] +chapter = "Bare Bones" +# Please update this when updating the translation +translation_based_on_commit = "d007af4811469b974f7abb988dd9c9d1373b55f0" +# GitHub usernames of the people that translated this post +translators = ["hamidrezakp", "MHBahrampour"] +rtl = true ++++ + +این پست به بررسی تست‌های واحد (ترجمه: unit) و یکپارچه (ترجمه: integration) در فایل‌های اجرایی ‌`no_std` می‌پردازد. ما از پشتیبانی Rust برای فریم‌ورک تست‌های سفارشی استفاده می‌کنیم تا توابع تست را درون کرنل‌مان اجرا کنیم. برای گزارش کردن نتایج خارج از QEMU، از ویژگی‌های مختلف QEMU و ابزار `bootimage` استفاده می‌کنیم. + + + +این بلاگ بصورت آزاد روی [گیت‌هاب] توسعه داده شده است. اگر شما مشکل یا سوالی دارید، لطفاً آن‌جا یک ایشو باز کنید. شما همچنین می‌توانید [در زیر] این پست کامنت بگذارید. منبع کد کامل این پست را می‌توانید در بِرَنچ [`post-04`][post branch] پیدا کنید. + +[گیت‌هاب]: https://github.com/phil-opp/blog_os +[در زیر]: #comments +[post branch]: https://github.com/phil-opp/blog_os/tree/post-04 + + + +## نیازمندی‌ها + +این پست جایگزین (حالا منسوخ شده) پست‌های [_Unit Testing_] و [_Integration Tests_] می‌شود. فرض بر این است که شما پست [_یک کرنل مینیمال با Rust_] را پس از 27-09-2019 دنبال کرده‌اید. اساساً نیاز است که شما یک فایل `.cargo/config.toml` داشته باشید که [یک هدف پیشفرض مشخص می‌کند] و [یک اجرا کننده قابل اجرا تعریف می‌کند]. + +[_Unit Testing_]: @/second-edition/posts/deprecated/04-unit-testing/index.md +[_Integration Tests_]: @/second-edition/posts/deprecated/05-integration-tests/index.md +[_یک کرنل مینیمال با Rust_]: @/second-edition/posts/02-minimal-rust-kernel/index.md +[یک هدف پیشفرض مشخص می‌کند]: @/second-edition/posts/02-minimal-rust-kernel/index.md#set-a-default-target +[یک اجرا کننده قابل اجرا تعریف می‌کند]: @/second-edition/posts/02-minimal-rust-kernel/index.md#using-cargo-run + +## تست کردن در Rust + +زبان Rust یک [فریم‌ورک تست توکار] دارد که قادر به اجرای تست‌های واحد بدون نیاز به تنظیم هر چیزی است. فقط کافی است تابعی ایجاد کنید که برخی نتایج را از طریق اَسرشن‌ها (کلمه: assertions) بررسی کند و صفت `#[test]` را به هدر تابع (ترجمه: function header) اضافه کنید. سپس `cargo test` به طور خودکار تمام تابع‌های تست کریت شما را پیدا و اجرا می‌کند. + +[فریم‌ورک تست توکار]: https://doc.rust-lang.org/book/second-edition/ch11-00-testing.html + +متأسفانه برای برنامه‌های `no_std` مانند هسته ما کمی پیچیده‌تر است. مسئله این است که فریم‌ورک تست Rust به طور ضمنی از کتابخانه [`test`] داخلی استفاده می‌کند که به کتابخانه استاندارد وابسته‌ است. این بدان معناست که ما نمی‌توانیم از فریم‌ورک تست پیشفرض برای هسته `#[no_std]` خود استفاده کنیم. + +[`test`]: https://doc.rust-lang.org/test/index.html + +وقتی می‌خواهیم `cargo test` را در پروژه خود اجرا کنیم، چنین چیزی می‌بینیم: + +``` +> cargo test + Compiling blog_os v0.1.0 (/…/blog_os) +error[E0463]: can't find crate for `test` +``` + +از آن‌جایی که کریت `test` به کتابخانه استاندارد وابسته است، برای هدف bare metal ما در دسترس نیست. در حالی که استفاده از کریت `test` در یک `#[no_std]` [امکان پذیر است][utest]، اما بسیار ناپایدار بوده و به برخی هک‌ها مانند تعریف مجدد ماکرو `panic` نیاز دارد. + +[utest]: https://github.com/japaric/utest + +### فریم‌ورک تست سفارشی + +خوشبختانه، Rust از جایگزین کردن فریم‌ورک تست پیشفرض از طریق ویژگی [`custom_test_frameworks`] ناپایدار پشتیبانی می‌کند. این ویژگی به کتابخانه خارجی احتیاج ندارد و بنابراین در محیط‌های `#[no_std]` نیز کار می‌کند. این کار با جمع آوری تمام توابع دارای صفت `#[test_case]` و سپس فراخوانی یک تابع اجرا کننده مشخص شده توسط کاربر و با لیست تست‌ها به عنوان آرگومان کار می‌کند. بنابراین حداکثر کنترل فرآیند تست را به ما می‌دهد. + +[`custom_test_frameworks`]: https://doc.rust-lang.org/unstable-book/language-features/custom-test-frameworks.html + +نقطه ضعف آن در مقایسه با فریم‌ورک تست پیشفرض این است که بسیاری از ویژگی‌های پیشرفته مانند [تست‌های `should_panic`] در دسترس نیست. در عوض، تهیه این ویژگی‌ها در صورت نیاز به پیاده‌سازی ما بستگی دارد. این برای ما ایده آل است، زیرا ما یک محیط اجرای بسیار ویژه داریم که پیاده سازی پیشفرض چنین ویژگی‌های پیشرفته‌ای احتمالاً کارساز نخواهد بود. به عنوان مثال‌، صفت `#[should_panic]` متکی به stack unwinding برای گرفتن پنیک‌ها (کلمه: panics) است، که ما آن را برای هسته خود غیرفعال کردیم. + +[تست‌های `should_panic`]: https://doc.rust-lang.org/book/ch11-01-writing-tests.html#checking-for-panics-with-should_panic + +برای اجرای یک فریم‌ورک تست سفارشی برای هسته خود، موارد زیر را به `main.rs` اضافه می‌کنیم: + +```rust +// in src/main.rs + +#![feature(custom_test_frameworks)] +#![test_runner(crate::test_runner)] + +#[cfg(test)] +fn test_runner(tests: &[&dyn Fn()]) { + println!("Running {} tests", tests.len()); + for test in tests { + test(); + } +} +``` + +اجرا کننده ما فقط یک پیام کوتاه اشکال زدایی را چاپ می‌کند و سپس هر تابع تست درون لیست را فراخوانی می‌کند. نوع آرگومان `&[&dyn Fn()]` یک [_slice_] از [_trait object_] است که آن هم ارجاعی از تِرِیت (کلمه: trait) [_Fn()_] می‌باشد. در اصل لیستی از ارجاع به انواع است که می‌توان آن‌ها را مانند یک تابع صدا زد. از آن‌جایی که این تابع برای اجراهایی که تست نباشند بی فایده است، از ویژگی `#[cfg(test)]` استفاده می‌کنیم تا آن را فقط برای تست کردن در اضافه کنیم. + +[_slice_]: https://doc.rust-lang.org/std/primitive.slice.html +[_trait object_]: https://doc.rust-lang.org/1.30.0/book/first-edition/trait-objects.html +[_Fn()_]: https://doc.rust-lang.org/std/ops/trait.Fn.html + +حال وقتی که `cargo test` را اجرا می‌کنیم، می‌بینیم که الان موفقیت آمیز است (اگر اینطور نیست یادداشت زیر را بخوانید). اگرچه، همچنان “Hello World” را به جای پیام `test_runner` می‌بینیم. دلیلش این است که تابع `_start` هنوز بعنوان نقطه شروع استفاده می‌شود. ویژگی فریم‌ورک تست سفارشی، یک تابع `main` ایجاد می‌کند که `test_runner` را صدا می‌زند، اما این تابع نادیده گرفته می‌شود چرا که ما از ویژگی `#[no_main]` استفاده می‌کنیم و نقطه شروع خودمان را ایجاد کردیم. + +
+ +**یادداشت:** درحال حاضر یک باگ در کارگو وجود دارد که در برخی موارد وقتی از `cargo test` استفاده می‌کنیم ما را به سمت خطای “duplicate lang item” می‌برد. زمانی رخ می‌دهد که شما `panic = "abort"` را برای یک پروفایل در `Cargo.toml` تنظیم کرده‌اید. سعی کنید آن را حذف کنید، سپس `cargo test` باید به درستی کار کند. برای اطلاعات بیشتر [ایشوی کارگو](https://github.com/rust-lang/cargo/issues/7359) را ببینید. + +
+ +برای حل کردن این مشکل، ما ابتدا نیاز داریم که نام تابع تولید شده را از طریق صفت `reexport_test_harness_main` به چیزی غیر از `main` تغییر دهیم. سپس می‌توانیم تابع تغییر نام داده شده را از تابع `_start` صدا بزنیم: + +```rust +// in src/main.rs + +#![reexport_test_harness_main = "test_main"] + +#[no_mangle] +pub extern "C" fn _start() -> ! { + println!("Hello World{}", "!"); + + #[cfg(test)] + test_main(); + + loop {} +} +``` + +ما نام فریم‌ورک تست تابع شروع را `test_main` گذاشتیم و آن را درون `_start` صدا زدیم. از [conditional compilation] برای اضافه کردن فراخوانی `test_main` فقط در زمینه‌های تست استفاده می‌کنیم زیرا تابع روی یک اجرای عادی تولید نشده است. + +زمانی که `cargo test` را اجرا می‌کنیم، می‌بینیم که پیام "Running 0 tests" از `test_runner` روی صفحه نمایش داده می‌شود. حال ما آماده‌ایم تا اولین تابع تست را بسازیم: + +```rust +// in src/main.rs + +#[test_case] +fn trivial_assertion() { + print!("trivial assertion... "); + assert_eq!(1, 1); + println!("[ok]"); +} +``` + +حال وقتی `cargo test` را اجرا می‌کنیم، خروجی زیر را می‌بینیم: + +![QEMU printing "Hello World!", "Running 1 tests", and "trivial assertion... [ok]"](qemu-test-runner-output.png) + +حالا بخش `tests` ارسال شده به تابع `test_runner` شامل یک ارجاع به تابع `trivial_assertion` است. از خروجی `trivial assertion... [ok]` روی صفحه می‌فهمیم که تست مورد نظر فراخوانی شده و موفقیت آمیز بوده است. + +پس از اجرای تست‌ها، `test_runner` به تابع `test_main` برمی‌گردد، که به نوبه خود به تابع `_start` برمی‌گردد. در انتهای `_start`، یک حلقه بی‌پایان ایجاد می‌کنیم زیرا تابع شروع اجازه برگردادن چیزی را ندارد (یعنی بدون خروجی است). این یک مشکل است، زیرا می‌خواهیم `cargo test` پس از اجرای تمام تست‌ها به کار خود پایان دهد. + +## خروج از QEMU + +در حال حاضر ما یک حلقه بی‌پایان در انتهای تابع `"_start"` داریم و باید QEMU را به صورت دستی در هر مرحله از `cargo test` ببندیم. این جای تأسف دارد زیرا ما همچنین می‌خواهیم `cargo test` را در اسکریپت‌ها بدون تعامل کاربر اجرا کنیم. یک راه حل خوب می‌تواند اجرای یک روش مناسب برای خاموش کردن سیستم عامل باشد. متأسفانه این کار نسبتاً پیچیده است، زیرا نیاز به پشتیبانی از استاندارد [APM] یا [ACPI] مدیریت توان دارد. + +[APM]: https://wiki.osdev.org/APM +[ACPI]: https://wiki.osdev.org/ACPI + +خوشبختانه، یک دریچه فرار وجود دارد: QEMU از یک دستگاه خاص `isa-debug-exit` پشتیبانی می‌کند، که راهی آسان برای خروج از سیستم QEMU از سیستم مهمان فراهم می‌کند. برای فعال کردن آن، باید یک آرگومان `-device` را به QEMU منتقل کنیم. ما می‌توانیم این کار را با اضافه کردن کلید پیکربندی `pack.metadata.bootimage.test-args` در` Cargo.toml` انجام دهیم: + +```toml +# in Cargo.toml + +[package.metadata.bootimage] +test-args = ["-device", "isa-debug-exit,iobase=0xf4,iosize=0x04"] +``` + +`bootimage runner` برای کلیه تست‌های اجرایی ` test-args` را به دستور پیش فرض QEMU اضافه می کند. برای یک `cargo run` عادی، آرگومان‌ها نادیده گرفته می‌شوند. + +همراه با نام دستگاه (`isa-debug-exit`)، دو پارامتر `iobase` و `iosize` را عبور می‌دهیم که _پورت I/O_ را مشخص می‌کند و هسته از طریق آن می‌تواند به دستگاه دسترسی داشته باشد. + +### پورت‌های I/O + +برای برقراری ارتباط بین پردازنده و سخت افزار جانبی در x86، دو رویکرد مختلف وجود دارد،**memory-mapped I/O** و **port-mapped I/O**. ما قبلاً برای دسترسی به [بافر متن VGA] از طریق آدرس حافظه `0xb8000` از memory-mapped I/O استفاده کرده‌ایم. این آدرس به RAM مپ (ترسیم) نشده است، بلکه به برخی از حافظه‌های دستگاه VGA مپ شده است. + +[بافر متن VGA]: @/second-edition/posts/03-vga-text-buffer/index.md + +در مقابل، port-mapped I/O از یک گذرگاه I/O جداگانه برای ارتباط استفاده می‌کند. هر قسمت جانبی متصل دارای یک یا چند شماره پورت است. برای برقراری ارتباط با چنین پورت I/O، دستورالعمل‌های CPU خاصی وجود دارد که `in` و `out` نامیده می‌شوند، که یک عدد پورت و یک بایت داده را می‌گیرند (همچنین این دستورات تغییراتی دارند که اجازه می دهد یک `u16` یا `u32` ارسال کنید). + +دستگاه‌های `isa-debug-exit` از port-mapped I/O استفاده می‌کنند. پارامتر `iobase` مشخص می‌کند که دستگاه باید در کدام آدرس پورت قرار بگیرد (`0xf4` یک پورت [معمولاً استفاده نشده][list of x86 I/O ports] در گذرگاه IO x86 است) و `iosize` اندازه پورت را مشخص می‌کند (`0x04` یعنی چهار بایت). + +[list of x86 I/O ports]: https://wiki.osdev.org/I/O_Ports#The_list + +### استفاده از دستگاه خروج + +عملکرد دستگاه `isa-debug-exit` بسیار ساده است. وقتی یک مقدار به پورت I/O مشخص شده توسط `iobase` نوشته می‌شود، باعث می شود QEMU با [exit status] خارج شود `(value << 1) | 1`. بنابراین هنگامی که ما `0` را در پورت می‌نویسیم، QEMU با وضعیت خروج `(0 << 1) | 1 = 1` خارج می‌شود و وقتی که ما `1` را در پورت می‌نویسیم با وضعیت خروج `(1 << 1) | 1 = 3` از آن خارج می شود. + +[exit status]: https://en.wikipedia.org/wiki/Exit_status + +به جای فراخوانی دستی دستورالعمل های اسمبلی `in` و `out`، ما از انتزاعات ارائه شده توسط کریت [`x86_64`] استفاده می‌کنیم. برای افزودن یک وابستگی به آن کریت، آن را به بخش `dependencies` در `Cargo.toml` اضافه می‌کنیم: + +[`x86_64`]: https://docs.rs/x86_64/0.12.1/x86_64/ + +```toml +# in Cargo.toml + +[dependencies] +x86_64 = "0.12.1" +``` + +اکنون می‌توانیم از نوع [`Port`] ارائه شده توسط کریت برای ایجاد عملکرد `exit_qemu` استفاده کنیم: + +[`Port`]: https://docs.rs/x86_64/0.12.1/x86_64/instructions/port/struct.Port.html + +```rust +// in src/main.rs + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[repr(u32)] +pub enum QemuExitCode { + Success = 0x10, + Failed = 0x11, +} + +pub fn exit_qemu(exit_code: QemuExitCode) { + use x86_64::instructions::port::Port; + + unsafe { + let mut port = Port::new(0xf4); + port.write(exit_code as u32); + } +} +``` + +این تابع یک [`Port`] جدید در `0xf4` ایجاد می‌کند، که `iobase` دستگاه `isa-debug-exit` است. سپس کد خروجی عبور داده شده را در پورت می‌نویسد. ما از `u32` استفاده می‌کنیم زیرا `iosize` دستگاه `isa-debug-exit` را به عنوان 4 بایت مشخص کردیم. هر دو عملیات ایمن نیستند، زیرا نوشتن در یک پورت I/O می‌تواند منجر به رفتار خودسرانه شود. + +برای تعیین وضعیت خروج، یک ای‌نام (کلمه: enum) `QemuExitCode` ایجاد می کنیم. ایده این است که اگر همه تست‌ها موفقیت آمیز بود، با کد خروج موفقیت (ترجمه: success exit code) خارج شود و در غیر این صورت با کد خروج شکست (ترجمه: failure exit code) خارج شود. enum به عنوان `#[repr(u32)]` علامت گذاری شده است تا هر نوع را با یک عدد صحیح `u32` نشان دهد. برای موفقیت از کد خروجی `0x10` و برای شکست از `0x11` استفاده می‌کنیم. کدهای خروجی واقعی چندان هم مهم نیستند، به شرطی که با کدهای خروجی پیش فرض QEMU مغایرت نداشته باشند. به عنوان مثال، استفاده از کد خروجی `0` برای موفقیت ایده خوبی نیست زیرا پس از تغییر شکل تبدیل به `(0 << 1) | 1 = 1` می‌شود، که کد خروجی پیش فرض است برای زمانی که QEMU نمی‌تواند اجرا شود. بنابراین ما نمی‌توانیم خطای QEMU را از یک تست موفقیت آمیز تشخیص دهیم. + +اکنون می توانیم `test_runner` خود را به روز کنیم تا پس از اتمام تست‌ها از QEMU خارج شویم: + +```rust +fn test_runner(tests: &[&dyn Fn()]) { + println!("Running {} tests", tests.len()); + for test in tests { + test(); + } + /// new + exit_qemu(QemuExitCode::Success); +} +``` + +حال وقتی `cargo test` را اجرا می‌کنیم، می‌بینیم که QEMU پس از اجرای تست‌ها بلافاصله بسته می‌شود. مشکل این است که `cargo test` تست را به عنوان شکست تفسیر می‌کند حتی اگر کد خروج `Success` را عبور دهیم: + +``` +> cargo test + Finished dev [unoptimized + debuginfo] target(s) in 0.03s + Running target/x86_64-blog_os/debug/deps/blog_os-5804fc7d2dd4c9be +Building bootloader + Compiling bootloader v0.5.3 (/home/philipp/Documents/bootloader) + Finished release [optimized + debuginfo] target(s) in 1.07s +Running: `qemu-system-x86_64 -drive format=raw,file=/…/target/x86_64-blog_os/debug/ + deps/bootimage-blog_os-5804fc7d2dd4c9be.bin -device isa-debug-exit,iobase=0xf4, + iosize=0x04` +error: test failed, to rerun pass '--bin blog_os' +``` + +مسئله این است که `cargo test` همه کدهای خطا به غیر از `0` را به عنوان شکست در نظر می‌گیرد. + +### کد خروج موفقیت + +برای کار در این مورد، `bootimage` یک کلید پیکربندی `test-success-exit-code` ارائه می‌دهد که یک کد خروجی مشخص را به کد خروجی `0` مپ می‌کند: + +```toml +[package.metadata.bootimage] +test-args = […] +test-success-exit-code = 33 # (0x10 << 1) | 1 +``` + +با استفاده از این پیکربندی، `bootimage` کد خروج موفقیت ما را به کد خروج 0 مپ می‌کند، به طوری که `cargo test` به درستی مورد موفقیت را تشخیص می‌دهد و تست را شکست خورده به حساب نمی‌آورد. + +اجرا کننده تست ما اکنون به طور خودکار QEMU را می‌بندد و نتایج تست را به درستی گزارش می‌کند. ما همچنان می‌بینیم که پنجره QEMU برای مدت بسیار کوتاهی باز است، اما این مدت بسیار کوتاه برای خواندن نتایج کافی نیست. جالب می‌شود اگر بتوانیم نتایج تست را به جای QEMU در کنسول چاپ کنیم، بنابراین پس از خروج از QEMU هنوز می‌توانیم آنها را ببینیم. + +## چاپ کردن در کنسول + +برای دیدن خروجی تست روی کنسول، باید داده‌ها را از هسته خود به نحوی به سیستم میزبان ارسال کنیم. روش‌های مختلفی برای دستیابی به این هدف وجود دارد، به عنوان مثال با ارسال داده‌ها از طریق رابط شبکه TCP. با این حال، تنظیم پشته شبکه یک کار کاملا پیچیده است، بنابراین ما به جای آن راه حل ساده‌تری را انتخاب خواهیم کرد. + +### پورت سریال + +یک راه ساده برای ارسال داده‌ها استفاده از [پورت سریال] است، یک استاندارد رابط قدیمی که دیگر در رایانه‌های مدرن یافت نمی‌شود. پیاده‌سازی آن آسان است و QEMU می‌تواند بایت‌های ارسالی از طریق سریال را به خروجی استاندارد میزبان یا یک فایل هدایت کند. + +[پورت سریال]: https://en.wikipedia.org/wiki/Serial_port + +تراشه‌های پیاده سازی یک رابط سریال [UART] نامیده می‌شوند. در x86 [مدلهای UART زیادی] وجود دارد، اما خوشبختانه تنها تفاوت آنها ویژگی‌های پیشرفته‌ای است که نیازی به آن‌ها نداریم. UART هایِ رایج امروزه همه با [16550 UART] سازگار هستند، بنابراین ما از آن مدل برای فریم‌ورک تست خود استفاده خواهیم کرد. + +[UARTs]: https://en.wikipedia.org/wiki/Universal_asynchronous_receiver-transmitter +[مدلهای UART زیادی]: https://en.wikipedia.org/wiki/Universal_asynchronous_receiver-transmitter#UART_models +[16550 UART]: https://en.wikipedia.org/wiki/16550_UART + +ما از کریت [`uart_16550`] برای شروع اولیه UART و ارسال داده‌ها از طریق پورت سریال استفاده خواهیم کرد. برای افزودن آن به عنوان یک وابستگی، ما `Cargo.toml` و `main.rs` خود را به روز می‌کنیم: + +[`uart_16550`]: https://docs.rs/uart_16550 + +```toml +# in Cargo.toml + +[dependencies] +uart_16550 = "0.2.0" +``` + +کریت `uart_16550` حاوی ساختار `SerialPort` است که نمایانگر ثبات‌های UART است، اما ما هنوز هم باید نمونه‌ای از آن را خودمان بسازیم. برای آن ما یک ماژول `‌serial` جدید با محتوای زیر ایجاد می‌کنیم: + +```rust +// in src/main.rs + +mod serial; +``` + +```rust +// in src/serial.rs + +use uart_16550::SerialPort; +use spin::Mutex; +use lazy_static::lazy_static; + +lazy_static! { + pub static ref SERIAL1: Mutex = { + let mut serial_port = unsafe { SerialPort::new(0x3F8) }; + serial_port.init(); + Mutex::new(serial_port) + }; +} +``` + +مانند [بافر متن VGA] [vga lazy-static]، ما از `lazy_static` و یک spinlock برای ایجاد یک نمونه نویسنده `static` استفاده می‌کنیم. با استفاده از `lazy_static` می‌توان اطمینان حاصل کرد که متد `init` در اولین استفاده دقیقاً یک بار فراخوانی می‌شود. + +مانند دستگاه `isa-debug-exit`، UART با استفاده از پورت I/O برنامه نویسی می‌شود. از آنجا که UART پیچیده‌تر است، از چندین پورت I/O برای برنامه نویسی رجیسترهای مختلف دستگاه استفاده می‌کند. تابع ناامن `SerialPort::new` انتظار دارد که آدرس اولین پورت I/O از UART به عنوان آرگومان باشد، که از آن می‌تواند آدرس تمام پورت‌های مورد نیاز را محاسبه کند. ما در حال عبور دادنِ آدرس پورت `0x3F8` هستیم که شماره پورت استاندارد برای اولین رابط سریال است. + +[vga lazy-static]: @/second-edition/posts/03-vga-text-buffer/index.md#lazy-statics + +برای اینکه پورت سریال به راحتی قابل استفاده باشد، ماکروهای `serial_print!` و `serial_println!` را اضافه می‌کنیم: + +```rust +#[doc(hidden)] +pub fn _print(args: ::core::fmt::Arguments) { + use core::fmt::Write; + SERIAL1.lock().write_fmt(args).expect("Printing to serial failed"); +} + +/// Prints to the host through the serial interface. +#[macro_export] +macro_rules! serial_print { + ($($arg:tt)*) => { + $crate::serial::_print(format_args!($($arg)*)); + }; +} + +/// Prints to the host through the serial interface, appending a newline. +#[macro_export] +macro_rules! serial_println { + () => ($crate::serial_print!("\n")); + ($fmt:expr) => ($crate::serial_print!(concat!($fmt, "\n"))); + ($fmt:expr, $($arg:tt)*) => ($crate::serial_print!( + concat!($fmt, "\n"), $($arg)*)); +} +``` + +پیاده سازی بسیار شبیه به پیاده سازی ماکروهای `print` و` println` است. از آنجا که نوع `SerialPort` تِرِیت [`fmt::Write`] را پیاده سازی می‌کند، نیازی نیست این پیاده سازی را خودمان انجام دهیم. + +[`fmt::Write`]: https://doc.rust-lang.org/nightly/core/fmt/trait.Write.html + +اکنون می‌توانیم به جای بافر متن VGA در کد تست خود، روی رابط سریال چاپ کنیم: + +```rust +// in src/main.rs + +#[cfg(test)] +fn test_runner(tests: &[&dyn Fn()]) { + serial_println!("Running {} tests", tests.len()); + […] +} + +#[test_case] +fn trivial_assertion() { + serial_print!("trivial assertion... "); + assert_eq!(1, 1); + serial_println!("[ok]"); +} +``` + +توجه داشته باشید که ماکرو `serial_println` مستقیماً در زیر فضای نام (ترجمه: namespace) ریشه قرار می‌گیرد زیرا ما از صفت `#[macro_export]` استفاده کردیم، بنابراین وارد کردن آن از طریق `use crate::serial::serial_println` کار نمی کند. + +### آرگومان‌‌های QEMU + +برای دیدن خروجی سریال از QEMU، باید از آرگومان `-serial` برای هدایت خروجی به stdout (خروجی استاندارد) استفاده کنیم: + +```toml +# in Cargo.toml + +[package.metadata.bootimage] +test-args = [ + "-device", "isa-debug-exit,iobase=0xf4,iosize=0x04", "-serial", "stdio" +] +``` + +حالا وقتی `cargo test` را اجرا می‌کنیم، خروجی تست را مستقیماً در کنسول مشاهده خواهیم گرد: + +``` +> cargo test + Finished dev [unoptimized + debuginfo] target(s) in 0.02s + Running target/x86_64-blog_os/debug/deps/blog_os-7b7c37b4ad62551a +Building bootloader + Finished release [optimized + debuginfo] target(s) in 0.02s +Running: `qemu-system-x86_64 -drive format=raw,file=/…/target/x86_64-blog_os/debug/ + deps/bootimage-blog_os-7b7c37b4ad62551a.bin -device + isa-debug-exit,iobase=0xf4,iosize=0x04 -serial stdio` +Running 1 tests +trivial assertion... [ok] +``` + +با این حال، هنگامی که یک تست ناموفق بود، ما همچنان خروجی را داخل QEMU مشاهده می‌کنیم، زیرا panic handler هنوز از `println` استفاده می‌کند. برای شبیه‌سازی این، می‌توانیم assertion درون تست `trivial_assertion` را به `assert_eq!(0, 1)` تغییر دهیم: + +![QEMU printing "Hello World!" and "panicked at 'assertion failed: `(left == right)` + left: `0`, right: `1`', src/main.rs:55:5](qemu-failed-test.png) + +می‌بینیم که پیام panic (تلفظ: پَنیک) هنوز در بافر VGA چاپ می‌شود، در حالی که خروجی‌ تست دیگر (منظور تستی می‌باشد که پنیک نکند) در پورت سریال چاپ می‌شود. پیام پنیک کاملاً مفید است، بنابراین دیدن آن در کنسول نیز مفید خواهد بود. + +### چاپ کردن پیام خطا هنگام پنیک کردن + +برای خروج از QEMU با یک پیام خطا هنگامی که پنیک رخ می‌دهد، می‌توانیم از [conditional compilation] برای استفاده از یک panic handler متفاوت در حالت تست استفاده کنیم: + +[conditional compilation]: https://doc.rust-lang.org/1.30.0/book/first-edition/conditional-compilation.html + +```rust +// our existing panic handler +#[cfg(not(test))] // new attribute +#[panic_handler] +fn panic(info: &PanicInfo) -> ! { + println!("{}", info); + loop {} +} + +// our panic handler in test mode +#[cfg(test)] +#[panic_handler] +fn panic(info: &PanicInfo) -> ! { + serial_println!("[failed]\n"); + serial_println!("Error: {}\n", info); + exit_qemu(QemuExitCode::Failed); + loop {} +} +``` + +برای panic handler تستِ خودمان، از `serial_println` به جای `println` استفاده می‌کنیم و سپس با کد خروج خطا از QEMU خارج می‌شویم. توجه داشته باشید که بعد از فراخوانی `exit_qemu` هنوز به یک حلقه بی‌پایان نیاز داریم زیرا کامپایلر نمی‌داند که دستگاه `isa-debug-exit` باعث خروج برنامه می‌شود. + +اکنون QEMU برای تست‌های ناموفق نیز خارج شده و یک پیام خطای مفید روی کنسول چاپ می کند: + +``` +> cargo test + Finished dev [unoptimized + debuginfo] target(s) in 0.02s + Running target/x86_64-blog_os/debug/deps/blog_os-7b7c37b4ad62551a +Building bootloader + Finished release [optimized + debuginfo] target(s) in 0.02s +Running: `qemu-system-x86_64 -drive format=raw,file=/…/target/x86_64-blog_os/debug/ + deps/bootimage-blog_os-7b7c37b4ad62551a.bin -device + isa-debug-exit,iobase=0xf4,iosize=0x04 -serial stdio` +Running 1 tests +trivial assertion... [failed] + +Error: panicked at 'assertion failed: `(left == right)` + left: `0`, + right: `1`', src/main.rs:65:5 +``` + +از آن‌جایی که اکنون همه خروجی‌های تست را در کنسول مشاهده می‌کنیم، دیگر نیازی به پنجره QEMU نداریم که برای مدت کوتاهی ظاهر می‌شود. بنابراین می‌توانیم آن را کاملا پنهان کنیم. + +### پنهان کردن QEMU + +از آنجا که ما نتایج کامل تست را با استفاده از دستگاه `isa-debug-exit` و پورت سریال گزارش می‌کنیم، دیگر نیازی به پنجره QEMU نداریم. ما می‌توانیم آن را با عبور دادن آرگومان `-display none` به QEMU پنهان کنیم: + +```toml +# in Cargo.toml + +[package.metadata.bootimage] +test-args = [ + "-device", "isa-debug-exit,iobase=0xf4,iosize=0x04", "-serial", "stdio", + "-display", "none" +] +``` + +اکنون QEMU کاملا در پس زمینه اجرا می‌شود و دیگر هیچ پنجره‌ای باز نمی‌شود. این نه تنها کمتر آزار دهنده است، بلکه به فریم‌ورک تست ما این امکان را می‌دهد که در محیط‌های بدون رابط کاربری گرافیکی مانند سرویس‌های CI یا کانکشن‌های [SSH] اجرا شود. + +[SSH]: https://en.wikipedia.org/wiki/Secure_Shell + +### Timeouts + +از آنجا که `cargo test` منتظر می‌ماند تا test runner (ترجمه: اجرا کننده تست) پایان یابد، تستی که هرگز به اتمام نمی‌رسد (چه موفق، چه ناموفق) می‌تواند برای همیشه اجرا کننده تست را مسدود کند. این جای تأسف دارد، اما در عمل مشکل بزرگی نیست زیرا اجتناب از حلقه‌های بی‌پایان به طور معمول آسان است. با این حال، در مورد ما، حلقه‌های بی‌پایان می‌توانند در موقعیت‌های مختلف رخ دهند: + +- بوت لودر موفق به بارگیری هسته نمی‌شود، در نتیجه سیستم به طور بی‌وقفه راه اندازی مجدد شود. +- فریم‌ورک BIOS/UEFI قادر به بارگیری بوت لودر نمی‌شود، در نتیجه باز هم باعث راه‌اندازی مجدد بی‌پایان می‌شود. +- وقتی که CPU در انتهای برخی از توابع ما وارد یک `loop {}` (حلقه بی‌پایان) می‌شود، به عنوان مثال به دلیل اینکه دستگاه خروج QEMU به درستی کار نمی‌کند. +- یا وقتی که سخت افزار باعث ریست شدن سیستم می‌شود، به عنوان مثال وقتی یک استثنای پردازنده (ترجمه: CPU exception) گیر نمی‌افتد (در پست بعدی توضیح داده شده است). + +از آنجا که حلقه های بی‌پایان در بسیاری از شرایط ممکن است رخ دهد، به طور پیش فرض ابزار `bootimage` برای هر تست ۵ دقیقه زمان تعیین می‌کند. اگر تست در این زمان به پایان نرسد، به عنوان ناموفق علامت گذاری شده و خطای "Timed Out" در کنسول چاپ می شود. این ویژگی تضمین می‌کند که تست‌هایی که در یک حلقه بی‌پایان گیر کرده‌اند، `cargo test` را برای همیشه مسدود نمی‌کنند. + +خودتان می‌توانید با افزودن عبارت `loop {}` در تست `trivial_assertion` آن را امتحان کنید. هنگامی که `cargo test` را اجرا می‌کنید، می‌بینید که این تست پس از ۵ دقیقه به پایان رسیده است. مدت زمان مهلت از طریق یک کلید `test-timeout` در Cargo.toml [قابل پیکربندی][bootimage config] است: + +[bootimage config]: https://github.com/rust-osdev/bootimage#configuration + +```toml +# in Cargo.toml + +[package.metadata.bootimage] +test-timeout = 300 # (in seconds) +``` + +اگر نمی‌خواهید ۵ دقیقه منتظر بمانید تا تست `trivial_assertion` تمام شود، می‌توانید به طور موقت مقدار فوق را کاهش دهید. + +### اضافه کردن چاپ خودکار + +تست `trivial_assertion` در حال حاضر باید اطلاعات وضعیت خود را با استفاده از `serial_print!`/`serial_println!` چاپ کند: + +```rust +#[test_case] +fn trivial_assertion() { + serial_print!("trivial assertion... "); + assert_eq!(1, 1); + serial_println!("[ok]"); +} +``` + +افزودن دستی این دستورات چاپی برای هر تستی که می‌نویسیم دست و پا گیر است، بنابراین بیایید `test_runner` خود را به روز کنیم تا به صورت خودکار این پیام‌ها را چاپ کنیم. برای انجام این کار، ما باید یک تریت جدید به نام `Testable` ایجاد کنیم: + +```rust +// in src/main.rs + +pub trait Testable { + fn run(&self) -> (); +} +``` + +این ترفند اکنون پیاده سازی این تریت برای همه انواع `T` است که [`Fn()` trait] را پیاده سازی می‌کنند: + +[`Fn()` trait]: https://doc.rust-lang.org/stable/core/ops/trait.Fn.html + +```rust +// in src/main.rs + +impl Testable for T +where + T: Fn(), +{ + fn run(&self) { + serial_print!("{}...\t", core::any::type_name::()); + self(); + serial_println!("[ok]"); + } +} +``` + +ما با اولین چاپِ نام تابع از طریق تابعِ [`any::type_name`]، تابع `run` را پیاده سازی می کنیم. این تابع مستقیماً در کامپایلر پیاده سازی شده و یک رشته توضیح از هر نوع را برمی‌گرداند. برای توابع، نوع آنها نامشان است، بنابراین این دقیقاً همان چیزی است که ما در این مورد می‌خواهیم. کاراکتر `\t` [کاراکتر tab] است، که مقداری ترازبندی‌ به پیام‌های `[ok]` اضافه می‌کند. + +[`any::type_name`]: https://doc.rust-lang.org/stable/core/any/fn.type_name.html +[کاراکتر tab]: https://en.wikipedia.org/wiki/Tab_key#Tab_characters + +پس از چاپ نام تابع، ما از طریق `self ()` تابع تست را فراخوانی می‌کنیم. این فقط به این دلیل کار می‌کند که ما نیاز داریم که `self` تریت `Fn()` را پیاده سازی کند. بعد از بازگشت تابع تست، ما `[ok]` را چاپ می‌کنیم تا نشان دهد که تابع پنیک نکرده است. + +آخرین مرحله به روزرسانی `test_runner` برای استفاده از تریت جدید` Testable` است: + +```rust +// in src/main.rs + +#[cfg(test)] +pub fn test_runner(tests: &[&dyn Testable]) { + serial_println!("Running {} tests", tests.len()); + for test in tests { + test.run(); // new + } + exit_qemu(QemuExitCode::Success); +} +``` + +تنها دو تغییر رخ داده، نوع آرگومان `tests` از `&[&dyn Fn()]` به `&[&dyn Testable]` است و ما اکنون `test.run()` را به جای `test()` فراخوانی می‌کنیم. + +اکنون می‌توانیم عبارات چاپ را از تست `trivial_assertion` حذف کنیم، زیرا آن‌ها اکنون به طور خودکار چاپ می‌شوند: + +```rust +// in src/main.rs + +#[test_case] +fn trivial_assertion() { + assert_eq!(1, 1); +} +``` + +خروجی `cargo test` اکنون به این شکل است: + +``` +Running 1 tests +blog_os::trivial_assertion... [ok] +``` + +نام تابع اکنون مسیر کامل به تابع را شامل می‌شود، که زمانی مفید است که توابع تست در ماژول‌های مختلف نام یکسانی دارند. در غیر اینصورت خروجی همانند قبل است، اما دیگر نیازی نیست که به صورت دستی دستورات چاپ را به تست‌های خود اضافه کنیم. + +## تست کردن بافر VGA + +اکنون که یک فریم‌ورک تستِ کارا داریم، می‌توانیم چند تست برای اجرای بافر VGA خود ایجاد کنیم. ابتدا، ما یک تست بسیار ساده برای تأیید اینکه `println` بدون پنیک کردن کار می‌کند ایجاد می‌کنیم: + + +```rust +// in src/vga_buffer.rs + +#[test_case] +fn test_println_simple() { + println!("test_println_simple output"); +} +``` + +این تست فقط چیزی را در بافر VGA چاپ می کند. اگر بدون پنیک تمام شود، به این معنی است که فراخوانی `println` نیز پنیک نکرده است. + +برای اطمینان از این‌ که پنیک ایجاد نمی‌شود حتی اگر خطوط زیادی چاپ شده و خطوط از صفحه خارج شوند، می‌توانیم آزمایش دیگری ایجاد کنیم: + +```rust +// in src/vga_buffer.rs + +#[test_case] +fn test_println_many() { + for _ in 0..200 { + println!("test_println_many output"); + } +} +``` + +همچنین می‌توانیم تابع تستی ایجاد کنیم تا تأیید کنیم که خطوط چاپ شده واقعاً روی صفحه ظاهر می شوند: + +```rust +// in src/vga_buffer.rs + +#[test_case] +fn test_println_output() { + let s = "Some test string that fits on a single line"; + println!("{}", s); + for (i, c) in s.chars().enumerate() { + let screen_char = WRITER.lock().buffer.chars[BUFFER_HEIGHT - 2][i].read(); + assert_eq!(char::from(screen_char.ascii_character), c); + } +} +``` + +این تابع یک رشته آزمایشی را تعریف می‌کند، آن را با استفاده از `println` چاپ می‌کند و سپس بر روی کاراکترهای صفحه از ` WRITER` ثابت تکرار (iterate) می‌کند، که نشان دهنده بافر متن vga است. از آنجا که `println` در آخرین خط صفحه چاپ می‌شود و سپس بلافاصله یک خط جدید اضافه می‌کند، رشته باید در خط` BUFFER_HEIGHT - 2` ظاهر شود. + +با استفاده از [`enumerate`]، تعداد تکرارها را در متغیر `i` حساب می‌کنیم، سپس از آن‌ها برای بارگذاری کاراکتر صفحه مربوط به `c` استفاده می‌کنیم. با مقایسه `ascii_character` از کاراکتر صفحه با `c`، اطمینان حاصل می‌کنیم که هر کاراکتر از این رشته واقعاً در بافر متن vga ظاهر می‌شود. + +[`enumerate`]: https://doc.rust-lang.org/core/iter/trait.Iterator.html#method.enumerate + +همانطور که می‌توانید تصور کنید، ما می‌توانیم توابع تست بیشتری ایجاد کنیم، به عنوان مثال تابعی که تست می‌کند هنگام چاپ خطوط طولانی پنیک ایجاد نمی‌شود و به درستی بسته‌بندی می‌شوند. یا تابعی برای تست این که خطوط جدید، کاراکترهای غیرقابل چاپ (ترجمه: non-printable) و کاراکترهای non-unicode به درستی اداره می‌شوند. + +برای بقیه این پست، ما نحوه ایجاد _integration tests_ را برای تست تعامل اجزای مختلف با هم توضیح خواهیم داد. + +## تست‌های یکپارچه + +قرارداد [تست‌های یکپارچه] در Rust این است که آن‌ها را در یک دایرکتوری `tests` در ریشه پروژه قرار دهید (یعنی در کنار فهرست `src`). فریم‌ورک تست پیش فرض و فریم‌ورک‌های تست سفارشی به طور خودکار تمام تست‌های موجود در آن فهرست را انتخاب و اجرا می‌کنند. + +[تست‌های یکپارچه]: https://doc.rust-lang.org/book/ch11-03-test-organization.html#integration-tests + +همه تست‌های یکپارچه، فایل اجرایی خاص خودشان هستند و کاملاً از `main.rs` جدا هستند. این بدان معناست که هر تست باید تابع نقطه شروع خود را مشخص کند. بیایید یک نمونه تست یکپارچه به نام `basic_boot` ایجاد کنیم تا با جزئیات ببینیم که چگونه کار می‌کند: + +```rust +// in tests/basic_boot.rs + +#![no_std] +#![no_main] +#![feature(custom_test_frameworks)] +#![test_runner(crate::test_runner)] +#![reexport_test_harness_main = "test_main"] + +use core::panic::PanicInfo; + +#[no_mangle] // don't mangle the name of this function +pub extern "C" fn _start() -> ! { + test_main(); + + loop {} +} + +fn test_runner(tests: &[&dyn Fn()]) { + unimplemented!(); +} + +#[panic_handler] +fn panic(info: &PanicInfo) -> ! { + loop {} +} +``` + +از آن‌جا که تست‌های یکپارچه فایل‌های اجرایی جداگانه‌ای هستند، ما باید تمام صفت‌های کریت (`no_std`، `no_main`، `test_runner` و غیره) را دوباره تهیه کنیم. ما همچنین باید یک تابع شروع جدید `_start` ایجاد کنیم که تابع نقطه شروع تست `test_main` را فراخوانی می‌کند. ما به هیچ یک از ویژگی‌های `cfg (test)` نیازی نداریم زیرا اجرایی‌های تست یکپارچه هرگز در حالت غیر تست ساخته نمی‌شوند. + +ما از ماکرو [ʻunimplemented] استفاده می‌کنیم که همیشه به عنوان یک مکان نگهدار برای تابع `test_runner` پنیک می‌کند و فقط در حلقه رسیدگی کننده `panic` فعلاً `loop` می‌زند. در حالت ایده آل، ما می‌خواهیم این توابع را دقیقاً همانطور که در `main.rs` خود با استفاده از ماکرو` serial_println` و تابع `exit_qemu` پیاده سازی کردیم، پیاده سازی کنیم. مشکل این است که ما به این توابع دسترسی نداریم زیرا تست‌ها کاملاً جدا از اجرایی `main.rs` ساخته شده‌اند. + +[`unimplemented`]: https://doc.rust-lang.org/core/macro.unimplemented.html + +اگر در این مرحله `cargo test` را انجام دهید، یک حلقه بی‌پایان خواهید گرفت زیرا رسیدگی کننده پنیک دارای حلقه بی‌پایان است. برای خروج از QEMU باید از میانبر صفحه کلید `Ctrl + c` استفاده کنید. + +### ساخت یک کتابخانه + +برای در دسترس قرار دادن توابع مورد نیاز در تست یکپارچه، باید یک کتابخانه را از `main.rs` جدا کنیم، کتابخانه‌ای که می‌تواند توسط کریت‌های دیگر و تست‌های یکپارچه مورد استفاده قرار بگیرد. برای این کار، یک فایل جدید `src/lib.rs` ایجاد می‌کنیم: + +```rust +// src/lib.rs + +#![no_std] + +``` + +مانند `main.rs` ،`lib.rs` یک فایل خاص است که به طور خودکار توسط کارگو شناسایی می‌شود. کتابخانه یک واحد تلفیقی جداگانه است، بنابراین باید ویژگی `#![no_std]` را دوباره مشخص کنیم. + +برای اینکه کتابخانه‌مان با `cargo test` کار کند، باید توابع و صفت‌های تست را نیز اضافه کنیم: +To make our library work with `cargo test`, we need to also add the test functions and attributes: + +```rust +// in src/lib.rs + +#![cfg_attr(test, no_main)] +#![feature(custom_test_frameworks)] +#![test_runner(crate::test_runner)] +#![reexport_test_harness_main = "test_main"] + +use core::panic::PanicInfo; + +pub trait Testable { + fn run(&self) -> (); +} + +impl Testable for T +where + T: Fn(), +{ + fn run(&self) { + serial_print!("{}...\t", core::any::type_name::()); + self(); + serial_println!("[ok]"); + } +} + +pub fn test_runner(tests: &[&dyn Testable]) { + serial_println!("Running {} tests", tests.len()); + for test in tests { + test.run(); + } + exit_qemu(QemuExitCode::Success); +} + +pub fn test_panic_handler(info: &PanicInfo) -> ! { + serial_println!("[failed]\n"); + serial_println!("Error: {}\n", info); + exit_qemu(QemuExitCode::Failed); + loop {} +} + +/// Entry point for `cargo test` +#[cfg(test)] +#[no_mangle] +pub extern "C" fn _start() -> ! { + test_main(); + loop {} +} + +#[cfg(test)] +#[panic_handler] +fn panic(info: &PanicInfo) -> ! { + test_panic_handler(info) +} +``` + +برای اینکه `test_runner` را در دسترس ‌تست‌های یکپارچه و فایل‌های اجرایی قرار دهیم، صفت `cfg(test)` را روی آن اعمال نمی‌کنیم و عمومی نمی‌کنیم. ما همچنین پیاده سازی رسیدگی کننده پنیک خود را به یک تابع عمومی `test_panic_handler` تبدیل می‌کنیم، به طوری که برای اجرایی‌ها نیز در دسترس باشد. + +از آن‌جا که `lib.rs` به طور مستقل از` main.rs` ما تست می‌شود، هنگام کامپایل کتابخانه در حالت تست، باید یک نقطه شروع `_start` و یک رسیدگی کننده پنیک اضافه کنیم. با استفاده از صفت کریت [`cfg_attr`]، در این حالت ویژگی`no_main` را به طور مشروط فعال می‌کنیم. + +[`cfg_attr`]: https://doc.rust-lang.org/reference/conditional-compilation.html#the-cfg_attr-attribute + +ما همچنین ای‌نام `QemuExitCode` و تابع `exit_qemu` را عمومی می‌کنیم: + +```rust +// in src/lib.rs + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[repr(u32)] +pub enum QemuExitCode { + Success = 0x10, + Failed = 0x11, +} + +pub fn exit_qemu(exit_code: QemuExitCode) { + use x86_64::instructions::port::Port; + + unsafe { + let mut port = Port::new(0xf4); + port.write(exit_code as u32); + } +} +``` + +اکنون فایل‌های اجرایی و تست‌های یکپارچه می‌توانند این توابع را از کتابخانه وارد کنند و نیازی به تعریف پیاده سازی‌های خود ندارند. برای در دسترس قرار دادن `println` و `serial_println`، اعلان ماژول‌ها را نیز منتقل می‌کنیم: + +```rust +// in src/lib.rs + +pub mod serial; +pub mod vga_buffer; +``` + +ما ماژول‌ها را عمومی می‌کنیم تا از خارج از کتابخانه قابل استفاده باشند. این امر همچنین برای استفاده از ماکروهای `println` و `serial_println` مورد نیاز است، زیرا آنها از توابع `_print` ماژول‌ها استفاده می‌کنند. + + +اکنون می توانیم `main.rs` خود را برای استفاده از کتابخانه به روز کنیم: + +```rust +// src/main.rs + +#![no_std] +#![no_main] +#![feature(custom_test_frameworks)] +#![test_runner(blog_os::test_runner)] +#![reexport_test_harness_main = "test_main"] + +use core::panic::PanicInfo; +use blog_os::println; + +#[no_mangle] +pub extern "C" fn _start() -> ! { + println!("Hello World{}", "!"); + + #[cfg(test)] + test_main(); + + loop {} +} + +/// This function is called on panic. +#[cfg(not(test))] +#[panic_handler] +fn panic(info: &PanicInfo) -> ! { + println!("{}", info); + loop {} +} + +#[cfg(test)] +#[panic_handler] +fn panic(info: &PanicInfo) -> ! { + blog_os::test_panic_handler(info) +} +``` + +کتابخانه مانند یک کریت خارجی معمولی قابل استفاده است. و مانند کریت (که در مورد ما کریت `blog_os` است) فراخوانی می‌شود. کد فوق از تابع `blog_os :: test_runner` در صفت `test_runner` و تابع `blog_os :: test_panic_handler` در رسیدگی کننده پنیک `cfg(test)` استفاده می‌کند. همچنین ماکرو `println` را وارد می‌کند تا در اختیار توابع `_start` و `panic` قرار گیرد. + +در این مرحله، `cargo run` و `cargo test` باید دوباره کار کنند. البته، `cargo test` هنوز هم در یک حلقه بی‌پایان گیر می‌کند (با `ctrl + c` می‌توانید خارج شوید). بیایید با استفاده از توابع مورد نیاز کتابخانه در تست یکپارچه این مشکل را برطرف کنیم. + +### تمام کردن تست یکپارچه + +مانند `src/main.rs`، اجرایی` test/basic_boot.rs` می‌تواند انواع مختلفی را از کتابخانه جدید ما وارد کند. که این امکان را به ما می‌دهد تا اجزای گمشده را برای تکمیل آزمایش وارد کنیم. + +```rust +// in tests/basic_boot.rs + +#![test_runner(blog_os::test_runner)] + +#[panic_handler] +fn panic(info: &PanicInfo) -> ! { + blog_os::test_panic_handler(info) +} +``` + +ما به جای پیاده سازی مجدد اجرا کننده تست، از تابع `test_runner` در کتابخانه خود استفاده می‌کنیم. برای رسیدگی کننده `panic`، ما تابع `blog_os::test_panic_handler` را مانند آن‌چه در `main.rs` انجام دادیم، فراخوانی می‌کنیم. + +اکنون `cargo test` مجدداً به طور معمول وجود دارد. وقتی آن را اجرا می‌کنید ، می‌بینید که تست‌های `lib.rs`، `main.rs` و `basic_boot.rs` ما را به طور جداگانه و یکی پس از دیگری ایجاد و اجرا می‌کند. برای تست‌های یکپارچه `main.rs` و `basic_boot`، متن "Running 0 tests" را نشان می‌دهد زیرا این فایل‌ها هیچ تابعی با حاشیه نویسی `#[test_case]` ندارد. + +اکنون می‌توانیم تست‌ها را به `basic_boot.rs` خود اضافه کنیم. به عنوان مثال، ما می‌توانیم آزمایش کنیم که `println` بدون پنیک کار می‌کند، مانند آنچه در تست‌های بافر vga انجام دادیم: + +```rust +// in tests/basic_boot.rs + +use blog_os::println; + +#[test_case] +fn test_println() { + println!("test_println output"); +} +``` + +حال وقتی `cargo test` را اجرا می‌کنیم، می‌بینیم که این تابع تست را پیدا و اجرا می‌کند. + +این تست ممکن است در حال حاضر کمی بی‌فایده به نظر برسد، زیرا تقریباً مشابه یکی از تست‌های بافر VGA است. با این حال، در آینده ممکن است توابع `_start` ما از `main.rs` و `lib.rs` رشد کرده و روال‌های اولیه مختلفی را قبل از اجرای تابع `test_main` فراخوانی کنند، به طوری که این دو تست در محیط‌های بسیار مختلف اجرا می‌شوند. + +### تست‌های آینده + +قدرت تست‌های یکپارچه این است که با آن‌ها به عنوان اجرایی کاملاً جداگانه برخورد می‌شود. این امر به آن‌ها اجازه کنترل کامل بر محیط را می‌دهد، و امکان تست کردن این که کد به درستی با CPU یا دستگاه‌های سخت‌افزاری ارتباط دارد را به ما می‌دهد. + +تست `basic_boot` ما یک مثال بسیار ساده برای تست یکپارچه است. در آینده، هسته ما ویژگی‌های بسیار بیشتری پیدا می‌کند و از راه‌های مختلف با سخت افزار ارتباط برقرار می‌کند. با افزودن تست های یکپارچه، می‌توانیم اطمینان حاصل کنیم که این تعاملات مطابق انتظار کار می‌کنند (و به کار خود ادامه می‌دهند). برخی از ایده‌ها برای تست‌های احتمالی در آینده عبارتند از: + +- **استثنائات CPU**: هنگامی که این کد عملیات نامعتبری را انجام می‌دهد (به عنوان مثال تقسیم بر صفر)، CPU یک استثنا را ارائه می‌دهد. هسته می‌تواند توابع رسیدگی کننده را برای چنین مواردی ثبت کند. یک تست یکپارچه می‌تواند تأیید کند که در صورت بروز استثنا پردازنده ، رسیدگی کننده استثنای صحیح فراخوانی می‌شود یا اجرای آن پس از استثناهای قابل حل به درستی ادامه دارد. + +- **جدول‌های صفحه**: جدول‌های صفحه مشخص می‌کند که کدام مناطق حافظه معتبر و قابل دسترسی هستند. با اصلاح جدول‌های صفحه، می‌توان مناطق حافظه جدیدی را اختصاص داد، به عنوان مثال هنگام راه‌اندازی برنامه‌ها. یک تست یکپارچه می‌تواند برخی از تغییرات جدول‌های صفحه را در تابع `_start` انجام دهد و سپس تأیید کند که این تغییرات در تابع‌های `# [test_case]` اثرات مطلوبی دارند. + +- **برنامه‌های فضای کاربر**: برنامه‌های فضای کاربر برنامه‌هایی با دسترسی محدود به منابع سیستم هستند. به عنوان مثال، آنها به ساختار داده‌های هسته یا حافظه برنامه‌های دیگر دسترسی ندارند. یک تست یکپارچه می‌تواند برنامه‌های فضای کاربر را که عملیات‌های ممنوعه را انجام می‌دهند راه‌اندازی کرده و بررسی کند هسته از همه آن‌ها جلوگیری می‌کند. + +همانطور که می‌توانید تصور کنید، تست‌های بیشتری امکان پذیر است. با افزودن چنین تست‌هایی، می‌توانیم اطمینان حاصل کنیم که وقتی ویژگی‌های جدیدی به هسته خود اضافه می‌کنیم یا کد خود را دوباره می‌سازیم، آن‌ها را به طور تصادفی خراب نمی‌کنیم. این امر به ویژه هنگامی مهم‌تر می‌شود که هسته ما بزرگتر و پیچیده‌تر شود. + +### تست‌هایی که باید پنیک کنند + +فریم‌ورک تست کتابخانه استاندارد از [صفت `#[should_panic]`][should_panic] پشتیبانی می‌کند که اجازه می‌دهد تست‌هایی را بسازد که باید ناموفق شوند (باید پنیک کنند). این مفید است، به عنوان مثال برای تأیید پنیک کردن یک تابع هنگام عبور دادن یک آرگومان نامعتبر به آن. متأسفانه این ویژگی در کریت‌های `#[no_std]` پشتیبانی نمی‌شود زیرا به پشتیبانی از کتابخانه استاندارد نیاز دارد. + +[should_panic]: https://doc.rust-lang.org/rust-by-example/testing/unit_testing.html#testing-panics + +اگرچه نمی‌توانیم از صفت `#[should_panic]` در هسته خود استفاده کنیم، اما می‌توانیم با ایجاد یک تست یکپارچه که با کد خطای موفقیت آمیز از رسیدگی کننده پنیک خارج می‌شود، رفتار مشابهی داشته باشیم. بیایید شروع به ایجاد چنین تستی با نام `should_panic` کنیم: + +```rust +// in tests/should_panic.rs + +#![no_std] +#![no_main] + +use core::panic::PanicInfo; +use blog_os::{QemuExitCode, exit_qemu, serial_println}; + +#[panic_handler] +fn panic(_info: &PanicInfo) -> ! { + serial_println!("[ok]"); + exit_qemu(QemuExitCode::Success); + loop {} +} +``` + +این تست هنوز ناقص است زیرا هنوز تابع `_start` یا هیچ یک از صفت‌های اجرا کننده تست سفارشی را مشخص نکرده. بیایید قسمت‌های گمشده را اضافه کنیم: + +```rust +// in tests/should_panic.rs + +#![feature(custom_test_frameworks)] +#![test_runner(test_runner)] +#![reexport_test_harness_main = "test_main"] + +#[no_mangle] +pub extern "C" fn _start() -> ! { + test_main(); + + loop {} +} + +pub fn test_runner(tests: &[&dyn Fn()]) { + serial_println!("Running {} tests", tests.len()); + for test in tests { + test(); + serial_println!("[test did not panic]"); + exit_qemu(QemuExitCode::Failed); + } + exit_qemu(QemuExitCode::Success); +} +``` + +به جای استفاده مجدد از `test_runner` از `lib.rs`، تست تابع `test_runner` خود را تعریف می‌کند که هنگام بازگشت یک تست بدون پنیک با یک کد خروج خطا خارج می‌شود (ما می‌خواهیم تست‌هایمان پنیک داشته باشند). اگر هیچ تابع تستی تعریف نشده باشد، اجرا کننده با کد خطای موفقیت خارج می‌شود. از آن‌جا که اجرا کننده همیشه پس از اجرای یک تست خارج می‌شود، منطقی نیست که بیش از یک تابع `#[test_case]` تعریف شود. + +اکنون می‌توانیم یک تست ایجاد کنیم که باید شکست بخورد: + +```rust +// in tests/should_panic.rs + +use blog_os::serial_print; + +#[test_case] +fn should_fail() { + serial_print!("should_panic::should_fail...\t"); + assert_eq!(0, 1); +} +``` + +این تست با استفاده از `assert_eq` ادعا (ترجمه: assert) می‌کند که `0` و `1` برابر هستند. این البته ناموفق است، به طوری که تست ما مطابق دلخواه پنیک می‌کند. توجه داشته باشید که ما باید نام تابع را با استفاده از `serial_print!` در اینجا چاپ دستی کنیم زیرا از تریت `Testable` استفاده نمی‌کنیم. + +هنگامی که ما تست را از طریق `cargo test --test should_panic` انجام دهیم، می‌بینیم که موفقیت آمیز است زیرا تست مطابق انتظار پنیک کرد. وقتی ادعا را کامنت کنیم و تست را دوباره اجرا کنیم، می‌بینیم که با پیام _"test did not panic"_ با شکست مواجه می‌شود. + +یک اشکال قابل توجه در این روش این است که این روش فقط برای یک تابع تست کار می‌کند. با چندین تابع `#[test_case]`، فقط اولین تابع اجرا می‌شود زیرا پس این‌که رسیدگی کننده پنیک فراخوانی شد، اجرا تمام می‌شود. من در حال حاضر راه خوبی برای حل این مشکل نمی‌دانم، بنابراین اگر ایده‌ای دارید به من اطلاع دهید! + +### تست های بدون مهار + +برای تست‌های یکپارچه که فقط یک تابع تست دارند (مانند تست `should_panic` ما)، اجرا کننده تست مورد نیاز نیست. برای مواردی از این دست، ما می‌توانیم اجرا کننده تست را به طور کامل غیرفعال کنیم و تست خود را مستقیماً در تابع `_start` اجرا کنیم. + +کلید این کار غیرفعال کردن پرچم `harness` برای تست در` Cargo.toml` است، که مشخص می‌کند آیا از یک اجرا کننده تست برای تست یکپارچه استفاده می‌شود. وقتی روی `false` تنظیم شود، هر دو اجرا ککنده تست پیش فرض و سفارشی غیرفعال می‌شوند، بنابراین با تست مانند یک اجرای معمولی رفتار می‌شود. + +بیایید پرچم `harness` را برای تست `should_panic` خود غیرفعال کنیم: + +```toml +# in Cargo.toml + +[[test]] +name = "should_panic" +harness = false +``` + +اکنون ما با حذف کد مربوط به آاجرا کننده تست، تست `should_panic` خود را بسیار ساده کردیم. نتیجه به این شکل است: + +```rust +// in tests/should_panic.rs + +#![no_std] +#![no_main] + +use core::panic::PanicInfo; +use blog_os::{exit_qemu, serial_print, serial_println, QemuExitCode}; + +#[no_mangle] +pub extern "C" fn _start() -> ! { + should_fail(); + serial_println!("[test did not panic]"); + exit_qemu(QemuExitCode::Failed); + loop{} +} + +fn should_fail() { + serial_print!("should_panic::should_fail...\t"); + assert_eq!(0, 1); +} + +#[panic_handler] +fn panic(_info: &PanicInfo) -> ! { + serial_println!("[ok]"); + exit_qemu(QemuExitCode::Success); + loop {} +} +``` + +اکنون تابع `should_fail` را مستقیماً از تابع `_start` خود فراخوانی می‌کنیم و در صورت بازگشت با کد خروج شکست خارج می‌شویم. اکنون وقتی `cargo test --test should_panic` را اجرا می‌کنیم، می‌بینیم که تست دقیقاً مانند قبل عمل می‌کند. + +غیر از ایجاد تست‌های `should_panic`، غیرفعال کردن صفت `harness` همچنین می‌تواند برای تست‌های یکپارچه پیچیده مفید باشد، به عنوان مثال هنگامی که تابع‌های منفرد دارای عوارض جانبی هستند و باید به ترتیب مشخصی اجرا شوند. + +## خلاصه + +تست کردن یک تکنیک بسیار مفید است تا اطمینان حاصل شود که اجزای خاصی رفتار مطلوبی دارند. حتی اگر آن‌ها نتوانند فقدان اشکالات را نشان دهند، آن‌ها هنوز هم یک ابزار مفید برای یافتن آن‌ها و به ویژه برای جلوگیری از دوباره کاری و پسرفت هستند. + +در این پست نحوه تنظیم فریم‌ورک تست برای هسته Rust ما توضیح داده شده است. ما از ویژگی فریم‌ورک تست سفارشی Rust برای پیاده سازی پشتیبانی از یک صفت ساده `#[test_case]` در محیط bare-metal خود استفاده کردیم. با استفاده از دستگاه `isa-debug-exit` شبیه‌ساز ماشین و مجازی‌ساز QEMU، اجرا کننده تست ما می‌تواند پس از اجرای تست‌ها از QEMU خارج شده و وضعیت تست را گزارش دهد. برای چاپ پیام‌های خطا به جای بافر VGA در کنسول، یک درایور اساسی برای پورت سریال ایجاد کردیم. + +پس از ایجاد چند تست برای ماکرو `println`، در نیمه دوم پست به بررسی تست‌های یکپارچه پرداختیم. ما فهمیدیم که آن‌ها در دایرکتوری `tests` قرار می‌گیرند و به عنوان اجرایی کاملاً مستقل با آن‌ها رفتار می‌شود. برای دسترسی دادن به آن‌ها به تابع `exit_qemu` و ماکرو `serial_println`، بیشتر کدهای خود را به یک کتابخانه منتقل کردیم که می‌تواند توسط همه اجراها و تست‌های یکپارچه وارد (import) شود. از آن‌جا که تست‌های یکپارچه در محیط جداگانه خود اجرا می‌شوند، آن‌ها تست تعاملاتی با سخت‌افزار یا ایجاد تست‌هایی که باید پنیک کنند را امکان پذیر می کنند. + +اکنون یک فریم‌ورک تست داریم که در یک محیط واقع گرایانه در داخل QEMU اجرا می‌شود. با ایجاد تست‌های بیشتر در پست‌های بعدی، می‌توانیم هسته خود را هنگامی که پیچیده‌تر شود، نگهداری کنیم. + +## مرحله بعدی چیست؟ + +در پست بعدی، ما _استثنائات CPU_ را بررسی خواهیم کرد. این موارد استثنایی توسط CPU در صورت بروز هرگونه اتفاق غیرقانونی، مانند تقسیم بر صفر یا دسترسی به صفحه حافظه مپ نشده (اصطلاحاً "خطای صفحه")، رخ می‌دهد. امکان کشف و بررسی این موارد استثنایی برای رفع اشکال در خطاهای آینده بسیار مهم است. رسیدگی به استثناها نیز بسیار شبیه رسیدگی به وقفه‌های سخت‌افزاری است، که برای پشتیبانی صفحه کلید مورد نیاز است. From 165a83efae306d0e0f13380c077d7444259e76bc Mon Sep 17 00:00:00 2001 From: "Hamid R. K. Pishghadam" Date: Fri, 13 Nov 2020 02:04:51 +0330 Subject: [PATCH 6/6] adding persian language name --- blog/config.toml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/blog/config.toml b/blog/config.toml index 4bb63fb3..2fe016b4 100644 --- a/blog/config.toml +++ b/blog/config.toml @@ -44,3 +44,6 @@ lang_name = "Chinese (traditional)" [translations.ja] lang_name = "Japanese" + +[translations.fa] +lang_name = "Persian"