mirror of
https://github.com/phil-opp/blog_os.git
synced 2025-12-16 22:37:49 +00:00
Translation of post-05 to Persian is done.
This commit is contained in:
472
blog/content/edition-2/posts/05-cpu-exceptions/index.fa.md
Normal file
472
blog/content/edition-2/posts/05-cpu-exceptions/index.fa.md
Normal file
@@ -0,0 +1,472 @@
|
||||
+++
|
||||
title = "استثناهای پردازنده"
|
||||
weight = 5
|
||||
path = "fa/cpu-exceptions"
|
||||
date = 2018-06-17
|
||||
|
||||
[extra]
|
||||
chapter = "Interrupts"
|
||||
# Please update this when updating the translation
|
||||
translation_based_on_commit = "a081faf3cced9aeb0521052ba91b74a1c408dcff"
|
||||
# GitHub usernames of the people that translated this post
|
||||
translators = ["hamidrezakp", "MHBahrampour"]
|
||||
rtl = true
|
||||
+++
|
||||
|
||||
استثناهای پردازنده در موقعیت های مختلف دارای خطا رخ می دهد ، به عنوان مثال هنگام دسترسی به آدرس حافظه نامعتبر یا تقسیم بر صفر. برای واکنش به آنها ، باید یک _جدول توصیف کننده وقفه_ تنظیم کنیم که توابع کنترل کننده را فراهم کند. در انتهای این پست ، هسته ما قادر به گرفتن [استثناهای breakpoint] و ادامه اجرای طبیعی پس از آن خواهد بود.
|
||||
|
||||
[استثناهای breakpoint]: https://wiki.osdev.org/Exceptions#Breakpoint
|
||||
|
||||
<!-- more -->
|
||||
|
||||
این بلاگ بصورت آزاد روی [گیتهاب] توسعه داده شده است. اگر مشکل یا سوالی دارید، لطفاً آنجا یک ایشو باز کنید. همچنین میتوانید [در زیر] این پست کامنت بگذارید. سورس کد کامل این پست را میتوانید در بِرَنچ [`post-05`][post branch] پیدا کنید.
|
||||
|
||||
[گیتهاب]: https://github.com/phil-opp/blog_os
|
||||
[در زیر]: #comments
|
||||
[post branch]: https://github.com/phil-opp/blog_os/tree/post-05
|
||||
|
||||
<!-- toc -->
|
||||
|
||||
## بررسی اجمالی
|
||||
یک استثنا نشان می دهد که مشکلی در دستورالعمل فعلی وجود دارد. به عنوان مثال ، اگر دستورالعمل فعلی بخواهد تقسیم بر 0 کند ، پردازنده یک استثنا صادر می کند. وقتی یک استثنا اتفاق می افتد ، پردازنده کار فعلی خود را رها کرده و بسته به نوع استثنا ، بلافاصله یک تابع خاص کنترل کننده استثنا را فراخوانی می کند.
|
||||
|
||||
در x86 حدود 20 نوع مختلف استثنا پردازنده وجود دارد. مهمترین آنها در زیر آمده اند:
|
||||
|
||||
- **خطای صفحه**: خطای صفحه در دسترسی غیرقانونی به حافظه رخ می دهد. به عنوان مثال ، اگر دستورالعمل فعلی بخواهد از یک صفحه نگاشت نشده بخواند یا بخواهد در یک صفحه فقط خواندنی بنویسد.
|
||||
- **کد نامعتبر**: این استثنا وقتی رخ می دهد که دستورالعمل فعلی نامعتبر است ، به عنوان مثال وقتی می خواهیم از [دستورالعمل های SSE] جدیدتر بر روی یک پردازنده قدیمی استفاده کنیم که آنها را پشتیبانی نمی کند.
|
||||
- **خطای محافظت عمومی**: این استثنا دارای بیشترین دامنه علل است. این مورد در انواع مختلف نقض دسترسی مانند تلاش برای اجرای یک دستورالعمل ممتاز در کد سطح کاربر یا نوشتن فیلدهای رزرو شده در ثبات های پیکربندی رخ می دهد.
|
||||
- **خطای دوگانه**: هنگامی که یک استثنا رخ می دهد ، پردازنده سعی می کند تابع کنترل کننده مربوطه را اجرا کند. اگر یک استثنا دیگر رخ دهد _هنگام فراخوانی تابع کنترل کننده استثنا_ ، پردازنده یک استثنای خطای دوگانه ایجاد می کند. این استثنا همچنین زمانی اتفاق می افتد که هیچ تابع کنترل کننده ای برای یک استثنا ثبت نشده باشد.
|
||||
- **خطای سهگانه**: اگر در حالی که پردازنده سعی می کند تابع کنترل کننده خطای دوگانه را فراخوانی کند استثنایی رخ دهد ، این یک خطای سهگانه است. ما نمی توانیم یک خطای سه گانه را بگیریم یا آن را کنترل کنیم. بیشتر پردازنده ها ریست کردن خود و راه اندازی مجدد سیستم عامل واکنش نشان می دهند.
|
||||
|
||||
[دستورالعمل های SSE]: https://en.wikipedia.org/wiki/Streaming_SIMD_Extensions
|
||||
|
||||
برای مشاهده لیست کامل استثناها ، [ویکی OSDev][exceptions] را بررسی کنید.
|
||||
|
||||
[exceptions]: https://wiki.osdev.org/Exceptions
|
||||
|
||||
### جدول توصیف کننده وقفه
|
||||
برای گرفتن و رسیدگی به استثناها ، باید اصطلاحاً _جدول توصیفگر وقفه_ (IDT) را تنظیم کنیم. در این جدول می توانیم برای هر استثنا پردازنده یک عملکرد تابع کننده مشخص کنیم. سخت افزار به طور مستقیم از این جدول استفاده می کند ، بنابراین باید از یک قالب از پیش تعریف شده پیروی کنیم. هر ورودی جدول باید ساختار 16 بایتی زیر را داشته باشد:
|
||||
|
||||
Type| Name | Description
|
||||
----|--------------------------|-----------------------------------
|
||||
u16 | Function Pointer [0:15] | The lower bits of the pointer to the handler function.
|
||||
u16 | GDT selector | Selector of a code segment in the [global descriptor table].
|
||||
u16 | Options | (see below)
|
||||
u16 | Function Pointer [16:31] | The middle bits of the pointer to the handler function.
|
||||
u32 | Function Pointer [32:63] | The remaining bits of the pointer to the handler function.
|
||||
u32 | Reserved |
|
||||
|
||||
[global descriptor table]: https://en.wikipedia.org/wiki/Global_Descriptor_Table
|
||||
|
||||
قسمت گزینه ها (Options) دارای قالب زیر است:
|
||||
|
||||
Bits | Name | Description
|
||||
------|-----------------------------------|-----------------------------------
|
||||
0-2 | Interrupt Stack Table Index | 0: Don't switch stacks, 1-7: Switch to the n-th stack in the Interrupt Stack Table when this handler is called.
|
||||
3-7 | Reserved |
|
||||
8 | 0: Interrupt Gate, 1: Trap Gate | If this bit is 0, interrupts are disabled when this handler is called.
|
||||
9-11 | must be one |
|
||||
12 | must be zero |
|
||||
13‑14 | Descriptor Privilege Level (DPL) | The minimal privilege level required for calling this handler.
|
||||
15 | Present |
|
||||
|
||||
هر استثنا دارای یک اندیس از پیش تعریف شده در IDT است. به عنوان مثال استثنا کد نامعتبر دارای اندیس 6 و استثنا خطای صفحه دارای اندیس 14 است. بنابراین ، سخت افزار می تواند به طور خودکار عنصر مربوطه را برای هر استثنا بارگذاری کند. [جدول استثناها][exceptions] در ویکی OSDev ، اندیس های IDT کلیه استثناها را در ستون “Vector nr.” نشان داده است.
|
||||
|
||||
هنگامی که یک استثنا رخ می دهد ، پردازنده تقریباً موارد زیر را انجام می دهد:
|
||||
|
||||
1. برخی از ثباتها را به پشته وارد میکند، از جمله اشاره گر دستورالعمل و ثبات [RFLAGS]. (بعداً در این پست از این مقادیر استفاده خواهیم کرد.)
|
||||
2. عنصر مربوط به آن (استثنا) را از جدول توصیف کننده وقفه (IDT) میخواند. به عنوان مثال ، پردازنده هنگام رخ دادن خطای صفحه ، عنصر چهاردهم را می خواند.
|
||||
3. وجود عنصر را بررسی میکند. اگر اینگونه نباشد یک خطای دوگانه ایجاد میکند.
|
||||
4. اگر عنصر یک گیت وقفه است (بیت 40 تنظیم نشده است) وقفه های سخت افزاری را غیرفعال میکند.
|
||||
5. انتخابگر مشخص شده [GDT] را در سگمنت CS بارگذاری میکند.
|
||||
6. به تابع کنترل کننده مشخص شده میرود.
|
||||
|
||||
[RFLAGS]: https://en.wikipedia.org/wiki/FLAGS_register
|
||||
[GDT]: https://en.wikipedia.org/wiki/Global_Descriptor_Table
|
||||
|
||||
در حال حاضر نگران مراحل 4 و 5 نباشید ، ما در مورد جدول توصیف کننده گلوبال و وقفه های سخت افزاری در پست های بعدی خواهیم آموخت.
|
||||
|
||||
## یک نوع IDT
|
||||
|
||||
به جای ایجاد نوع IDT خود ، از [ساختمان `InterruptDescriptorTable`] کرت `x86_64` استفاده خواهیم کرد که به این شکل است:
|
||||
|
||||
[ساختمان `InterruptDescriptorTable`]: https://docs.rs/x86_64/0.12.1/x86_64/structures/idt/struct.InterruptDescriptorTable.html
|
||||
|
||||
``` rust
|
||||
#[repr(C)]
|
||||
pub struct InterruptDescriptorTable {
|
||||
pub divide_by_zero: Entry<HandlerFunc>,
|
||||
pub debug: Entry<HandlerFunc>,
|
||||
pub non_maskable_interrupt: Entry<HandlerFunc>,
|
||||
pub breakpoint: Entry<HandlerFunc>,
|
||||
pub overflow: Entry<HandlerFunc>,
|
||||
pub bound_range_exceeded: Entry<HandlerFunc>,
|
||||
pub invalid_opcode: Entry<HandlerFunc>,
|
||||
pub device_not_available: Entry<HandlerFunc>,
|
||||
pub double_fault: Entry<HandlerFuncWithErrCode>,
|
||||
pub invalid_tss: Entry<HandlerFuncWithErrCode>,
|
||||
pub segment_not_present: Entry<HandlerFuncWithErrCode>,
|
||||
pub stack_segment_fault: Entry<HandlerFuncWithErrCode>,
|
||||
pub general_protection_fault: Entry<HandlerFuncWithErrCode>,
|
||||
pub page_fault: Entry<PageFaultHandlerFunc>,
|
||||
pub x87_floating_point: Entry<HandlerFunc>,
|
||||
pub alignment_check: Entry<HandlerFuncWithErrCode>,
|
||||
pub machine_check: Entry<HandlerFunc>,
|
||||
pub simd_floating_point: Entry<HandlerFunc>,
|
||||
pub virtualization: Entry<HandlerFunc>,
|
||||
pub security_exception: Entry<HandlerFuncWithErrCode>,
|
||||
// some fields omitted
|
||||
}
|
||||
```
|
||||
|
||||
فیلدها از نوع [`<idt::Entry<F`] هستند ، این ساختمانی است که فیلد های یک عنصر IDT را نشان می دهد (به جدول بالا مراجعه کنید). پارامتر نوع `F`، نوع تابع کنترل کننده مورد انتظار را تعریف می کند. می بینیم که برخی از عناصر به یک [`HandlerFunc`] و برخی دیگر به [`HandlerFuncWithErrCode`] نیاز دارند. خطای صفحه حتی نوع خاص خود را دارد: [`PageFaultHandlerFunc`].
|
||||
|
||||
[`<idt::Entry<F`]: https://docs.rs/x86_64/0.12.1/x86_64/structures/idt/struct.Entry.html
|
||||
[`HandlerFunc`]: https://docs.rs/x86_64/0.12.1/x86_64/structures/idt/type.HandlerFunc.html
|
||||
[`HandlerFuncWithErrCode`]: https://docs.rs/x86_64/0.12.1/x86_64/structures/idt/type.HandlerFuncWithErrCode.html
|
||||
[`PageFaultHandlerFunc`]: https://docs.rs/x86_64/0.12.1/x86_64/structures/idt/type.PageFaultHandlerFunc.html
|
||||
|
||||
بیایید ابتدا به نوع `HandlerFunc` نگاه کنیم:
|
||||
|
||||
```rust
|
||||
type HandlerFunc = extern "x86-interrupt" fn(_: &mut InterruptStackFrame);
|
||||
```
|
||||
|
||||
این یک [نوع مستعار(type alias)] برای نوع "`extern "x86-interrupt" fn` است. کلمه کلیدی `extern` تابعی را با یک [قرارداد فراخوانی خارجی] تعریف می کند و اغلب برای برقراری ارتباط با کد C استفاده می شود(`extern "C" fn`) . اما قرارداد فراخوانی `x86-interrupt` چیست؟
|
||||
|
||||
[نوع مستعار(type alias)]: https://doc.rust-lang.org/book/ch19-04-advanced-types.html#creating-type-synonyms-with-type-aliases
|
||||
[قرارداد فراخوانی خارجی]: https://doc.rust-lang.org/nomicon/ffi.html#foreign-calling-conventions
|
||||
|
||||
## قرارداد فراخوانی وقفه
|
||||
استثناها کاملاً شبیه فراخوانی توابع هستند: پردازنده به اولین دستورالعمل تابع فراخوانی شده می رود و آن را اجرا می کند. پس از آن پردازنده به آدرس بازگشت می پرد و اجرای تابع اصلی را ادامه می دهد.
|
||||
|
||||
با این وجود ، تفاوت عمده ای بین فراخوانی استثناها و توابع وجود دارد: یک فراخوانی تابع توسط یک کامپایلر که دستور "فراخوانی" در آن درج شده است ، انجام می شود ، در حالی که یک استثنا ممکن است در _هر_ دستورالعملی رخ دهد. برای درک عواقب این تفاوت ، باید فراخوانی توابع را با جزئیات بیشتری بررسی کنیم.
|
||||
|
||||
[قرارداد فراخوانی] جزئیات فراخوانی تابع را مشخص می کند. به عنوان مثال ، آنها مشخص میکنند که پارامترهای تابع کجا قرار می گیرند (به عنوان مثال در ثباتها یا بر روی پشته) و نحوه بازگشت نتایج. در x86_64 لینوکس ، قوانین زیر برای توابع C اعمال می شود (مشخص شده در [System V ABI]):
|
||||
|
||||
[قرارداد فراخوانی]: https://en.wikipedia.org/wiki/Calling_convention
|
||||
[System V ABI]: https://refspecs.linuxbase.org/elf/x86_64-abi-0.99.pdf
|
||||
|
||||
- شش آرگومان اول با نوع عدد صحیح در ثباتهای`rdi`, `rsi`, `rdx`, `rcx`, `r8`, `r9` منتقل می شوند
|
||||
- آرگومان های اضافی بر روی پشته منتقل می شوند
|
||||
- نتایج درون`rax` و `rdx` بر می گردند
|
||||
|
||||
توجه داشته باشید که راست از C ABI پیروی نمی کند (در واقع ، [هنوز حتی یک Rust ABI وجود ندارد][rust abi]) ، بنابراین این قوانین فقط برای توابع اعلام شده به عنوان `extern "C" fn` اعمال می شود.
|
||||
|
||||
[rust abi]: https://github.com/rust-lang/rfcs/issues/600
|
||||
|
||||
### ثبات های حفظ شده و تغییرشونده (Scratch)
|
||||
قرارداد فراخوانی، ثباتها را به دو دسته ثبات های _محفوظ شده_ و _تغییرشونده_ تقسیم می کند.
|
||||
|
||||
مقادیر ثباتهای _محفوظ شده_، در فراخوانی تابع باید بدون تغییر باقی بمانند. بنابراین یک تابع فراخوانی شده (_“callee”_) فقط در صورتی مجاز است این ثباتها را تغییر دهد، که مقادیر اصلی آنها را قبل از بازگشت، برگرداند. بنابراین به این ثباتها _"callee-saved"_ گفته می شود. یک الگوی عمومی این است که ثباتها در آغاز تابع بر روی پشته ذخیره شده و درست قبل از بازگشت از پشته برداشته شده و مقدار دهی شوند.
|
||||
|
||||
در مقابل، یک تابع فراخوانی شده مجاز است که بدون محدودیت ، ثباتهای _تغییرشونده_ را دوباره بنویسد. اگر فراخواننده (_"caller"_) بخواهد مقدار یک ثبات تغییرشونده را در یک فراخوانی تابع حفظ کند ، لازم است قبل از فراخوانی تابع (به عنوان مثال بوسیله اضافه و برداشتن از روی پشته) آن را پشتیبان گیری و بازیابی کند. بنابراین ثباتهای تغییرشونده _caller-saved_ هستند.
|
||||
|
||||
در x86_64 ، قرارداد فراخوانی C ثباتهای محفوظ شده و تغییرشونده زیر را مشخص می کند:
|
||||
|
||||
preserved registers | scratch registers
|
||||
---|---
|
||||
`rbp`, `rbx`, `rsp`, `r12`, `r13`, `r14`, `r15` | `rax`, `rcx`, `rdx`, `rsi`, `rdi`, `r8`, `r9`, `r10`, `r11`
|
||||
_callee-saved_ | _caller-saved_
|
||||
|
||||
کامپایلر این قوانین را می داند ، بنابراین کد را متناسب با آن تولید می کند. به عنوان مثال ، بیشتر توابع با `push rbp` شروع می شوند که پشتیبان گیری از`rbp` روی پشته است (زیرا این یک ثبات _caller-saved_).
|
||||
|
||||
### حفظ کلیه ثباتها
|
||||
برخلاف فراخوانی تابع ، استثناها می توانند در _هر_ دستورالعملی رخ دهند. در بیشتر موارد ، ما حتی در زمان کامپایل نمی دانیم که کد تولید شده استثنا ایجاد می کند یا نه. به عنوان مثال ، کامپایلر نمی تواند بداند که آیا یک دستورالعمل باعث سرریز شدن پشته یا خطای صفحه می شود.
|
||||
|
||||
از آنجا که نمی دانیم چه زمانی استثنا رخ میدهد ، نمی توانیم قبل از آن از هیچ ثباتی پشتیبان گیری کنیم. این بدان معناست که ما نمی توانیم از قرارداد فراخوانیای استفاده کنیم که متکی به ثباتهای caller-saved برای کنترل کننده های استثنا هست. در عوض ، به یک قرارداد فراخوانی نیاز داریم که _همه_ ثباتها را حفظ کند. قرارداد فراخوانی `x86-interrupt` چنین قرارداد فراخوانی است ، بنابراین تضمین می کند که تمام مقادیر ثباتها در هنگام بازگشت تابع به مقادیر اصلی خود بازگردند.
|
||||
|
||||
توجه داشته باشید که این بدان معنا نیست که همه ثباتها در ورود به تابع در پشته ذخیره می شوند. در عوض ، کامپایلر فقط از ثباتهایی که توسط تابع تغییر میکنند ، پشتیبان تهیه می کند. به این ترتیب ، کد بسیار کارآمدی برای توابع کوتاه که فقط از چند ثبات استفاده می کنند ، تولید می شود.
|
||||
|
||||
### قاب پشته وقفه (The Interrupt Stack Frame)
|
||||
در یک فراخوانی عادی تابع (با استفاده از دستورالعمل `call`) ، پردازنده قبل از پرش به تابع هدف ، آدرس بازگشت را در پشته ذخیره میکند. در هنگام بازگشت تابع (با استفاده از دستورالعمل `ret`) ، پردازنده این آدرس بازگشت را از پشته برمیدارد و به آن می پرد. بنابراین قاب پشته یک فراخوانی عادی تابع به این شکل است:
|
||||
|
||||

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

|
||||
|
||||
در کرت `x86_64` ، فریم پشته وقفه توسط ساختمان [`InterruptStackFrame`] نشان داده می شود. این ساختمان به عنوان `&mut` به کنترل کننده وقفه منتقل می شود و می تواند برای دریافت اطلاعات بیشتر در مورد علت استثنا استفاده شود. ساختمان بدون فیلد کد خطا است ، زیرا فقط برخی از استثناها کد خطا را پوش میکنند. این استثناها از نوع تابع جداگانه [`HandlerFuncWithErrCode`] استفاده میکنند ، که دارای یک آرگومان اضافی `error_code` است.
|
||||
|
||||
[`InterruptStackFrame`]: https://docs.rs/x86_64/0.12.1/x86_64/structures/idt/struct.InterruptStackFrame.html
|
||||
|
||||
### پشت صحنه
|
||||
قرارداد فراخوانی `x86-interrupt` یک انتزاع قدرتمند است که تقریباً تمام جزئیات پیچیده فرآیند مدیریت استثناها را پنهان می کند. با این حال ، گاهی اوقات مفید است که بدانیم پشت پرده چه اتفاقی می افتد. در اینجا یک مرور کوتاه از مواردی که قرارداد فراخوانی `x86-interrupt` انجام میدهد را میبینید:
|
||||
|
||||
- **دریافت آرگومان ها**: بیشتر قرارداد های فراخوانی انتظار دارند که آرگومان ها در ثباتها منتقل شوند. این برای کنترل کننده های استثنا امکان پذیر نیست ، زیرا ما نباید قبل از تهیه نسخه پشتیبان از مقادیر ثباتها ، آنها را بازنویسی کنیم. در عوض، قرارداد فراخوانی `x86-interrupt` آگاه است که آرگومان ها از قبل در مکان خاصی بر روی پشته قرار دارند.
|
||||
- **بازگشت با استفاده از `iretq`**: از آنجا که قاب پشته وقفه با قاب پشته صدا زدن توابع معمولی کاملاً متفاوت است، نمی توانیم از طریق دستورالعمل` ret` از توابع کنترل کننده برگردیم. در عوض، باید از دستور `iretq` استفاده شود.
|
||||
- **مدیریت کد خطا**: کد خطا که برای برخی استثناها به پشته اضافه می شود ، کارها را بسیار پیچیده تر می کند. تراز بندی پشته را تغییر می دهد (به قسمت بعدی مراجعه کنید) و باید قبل از بازگشت، از پشته خارج شود. قرارداد فراخوانی `x86-interrupt` تمام پیچیدگیها را برطرف می کند. با این حال، نمی داند کدام تابع کنترل کننده برای کدام استثنا استفاده می شود، بنابراین باید این اطلاعات را از تعداد آرگومان های تابع استخراج کند. این بدان معناست که برنامه نویس همچنان مسئول استفاده صحیح هر نوع تابع برای هر استثنا است. خوشبختانه نوع `InterruptDescriptorTable` که توسط کرت `x86_64` تعریف شده است، استفاده از انواع تابع صحیح را تضمین می کند.
|
||||
- **تراز کردن پشته**: برخی دستورالعملها (به ویژه دستورالعمل های SSE) وجود دارند که به یک تراز پشته 16 بایتی نیاز دارند. پردازنده این تراز را هر زمان که یک استثنا اتفاق می افتد تضمین می کند ، اما برای برخی از استثناها بعداً هنگامی که یک کد خطا را به پشته اضافه میکند، دوباره آن را از بین می برد. قرارداد فراخوانی `x86-interrupt` با تنظیم مجدد پشته در این حالت این مشکل را برطرف میکند.
|
||||
|
||||
اگر به جزئیات بیشتر علاقه مندید: ما همچنین یک سری پست داریم که مدیریت استثنا با استفاده از [توابع برهنه] را توضیح میدهند. ([در انتهای این پست][too-much-magic]).
|
||||
|
||||
[توابع برهنه]: https://github.com/rust-lang/rfcs/blob/master/text/1201-naked-fns.md
|
||||
[too-much-magic]: #khyly-jdwyy-bwd
|
||||
|
||||
## پیاده سازی
|
||||
اکنون که تئوری را فهمیدیم ، وقت آن رسیده است که استثناهای پردازنده را در هسته خود کنترل کنیم. ما با ایجاد یک ماژول جدید وقفهها در `src/interrupts.rs` شروع خواهیم کرد، که ابتدا یک تابع `init_idt` ایجاد می کند که یک `InterruptDescriptorTable` جدید ایجاد می کند:
|
||||
|
||||
``` rust
|
||||
// in src/lib.rs
|
||||
|
||||
pub mod interrupts;
|
||||
|
||||
// in src/interrupts.rs
|
||||
|
||||
use x86_64::structures::idt::InterruptDescriptorTable;
|
||||
|
||||
pub fn init_idt() {
|
||||
let mut idt = InterruptDescriptorTable::new();
|
||||
}
|
||||
```
|
||||
|
||||
اکنون می توانیم توابع کنترل کننده را اضافه کنیم. با اضافه کردن یک کنترل کننده برای [استثنا بریکپوینت] شروع می کنیم. استثنای بریکپوینت، استثنای عالی برای آزمایش مدیریت استثنا است. تنها هدف آن مکث موقت برنامه هنگام اجرای دستورالعمل بریکپوینت `int3` است.
|
||||
|
||||
[استثنا بریکپوینت]: https://wiki.osdev.org/Exceptions#Breakpoint
|
||||
|
||||
استثنا بریکپوینت معمولاً در دیباگرها به کار می رود: وقتی کاربر بریکپوینت را تعیین می کند ، دیباگر دستورالعمل مربوطه را با دستورالعمل `int3` بازنویسی می کند تا پردازنده هنگام رسیدن به آن خط، استثنای بریکپوینت را ایجاد کند. هنگامی که کاربر می خواهد برنامه را ادامه دهد، دیباگر دوباره دستورالعمل `int3` را با دستورالعمل اصلی جایگزین می کند و برنامه را ادامه می دهد. برای جزئیات بیشتر ، سری ["_دیباگرها چطور کار میکنند_"] را ببینید.
|
||||
|
||||
["_دیباگرها چطور کار میکنند_"]: https://eli.thegreenplace.net/2011/01/27/how-debuggers-work-part-2-breakpoints
|
||||
|
||||
برای موارد استفاده ما، نیازی به بازنویسی دستورالعمل نداریم. در عوض، فقط میخواهیم هنگام اجرای دستورالعمل بریکپوینت پیامی چاپ کنیم و سپس برنامه را ادامه دهیم. بنابراین بیایید یک تابع ساده `breakpoint_handler` ایجاد کنیم و آن را به IDT خود اضافه کنیم:
|
||||
|
||||
```rust
|
||||
// in src/interrupts.rs
|
||||
|
||||
use x86_64::structures::idt::{InterruptDescriptorTable, InterruptStackFrame};
|
||||
use crate::println;
|
||||
|
||||
pub fn init_idt() {
|
||||
let mut idt = InterruptDescriptorTable::new();
|
||||
idt.breakpoint.set_handler_fn(breakpoint_handler);
|
||||
}
|
||||
|
||||
extern "x86-interrupt" fn breakpoint_handler(
|
||||
stack_frame: &mut InterruptStackFrame)
|
||||
{
|
||||
println!("EXCEPTION: BREAKPOINT\n{:#?}", stack_frame);
|
||||
}
|
||||
```
|
||||
|
||||
کنترل کننده ما فقط یک پیام را خارج می کند و قاب پشته وقفه را زیبا چاپ می کند.
|
||||
|
||||
هنگامی که می خواهیم آن را کامپایل کنیم ، خطای زیر رخ می دهد:
|
||||
|
||||
```
|
||||
error[E0658]: x86-interrupt ABI is experimental and subject to change (see issue #40180)
|
||||
--> src/main.rs:53:1
|
||||
|
|
||||
53 | / extern "x86-interrupt" fn breakpoint_handler(stack_frame: &mut InterruptStackFrame) {
|
||||
54 | | println!("EXCEPTION: BREAKPOINT\n{:#?}", stack_frame);
|
||||
55 | | }
|
||||
| |_^
|
||||
|
|
||||
= help: add #![feature(abi_x86_interrupt)] to the crate attributes to enable
|
||||
```
|
||||
|
||||
این خطا به این دلیل رخ می دهد که قرارداد فراخوانی `x86-interrupt` هنوز ناپایدار است. به هر حال برای استفاده از آن ، باید صریحاً آن را با اضافه کردن `#![feature(abi_x86_interrupt)]` در بالای `lib.rs` فعال کنیم.
|
||||
|
||||
### بارگیری IDT
|
||||
برای اینکه پردازنده از جدول توصیف کننده وقفه جدید ما استفاده کند ، باید آن را با استفاده از دستورالعمل [`lidt`] بارگیری کنیم. ساختمان `InterruptDescriptorTable` از کرت ` x86_64` متد [`load`][InterruptDescriptorTable::load] را برای این کار فراهم می کند. بیایید سعی کنیم از آن استفاده کنیم:
|
||||
|
||||
[`lidt`]: https://www.felixcloutier.com/x86/lgdt:lidt
|
||||
[InterruptDescriptorTable::load]: https://docs.rs/x86_64/0.12.1/x86_64/structures/idt/struct.InterruptDescriptorTable.html#method.load
|
||||
|
||||
```rust
|
||||
// in src/interrupts.rs
|
||||
|
||||
pub fn init_idt() {
|
||||
let mut idt = InterruptDescriptorTable::new();
|
||||
idt.breakpoint.set_handler_fn(breakpoint_handler);
|
||||
idt.load();
|
||||
}
|
||||
```
|
||||
|
||||
اکنون هنگامی که می خواهیم آن را کامپایل کنیم ، خطای زیر رخ می دهد:
|
||||
|
||||
```
|
||||
error: `idt` does not live long enough
|
||||
--> src/interrupts/mod.rs:43:5
|
||||
|
|
||||
43 | idt.load();
|
||||
| ^^^ does not live long enough
|
||||
44 | }
|
||||
| - borrowed value only lives until here
|
||||
|
|
||||
= note: borrowed value must be valid for the static lifetime...
|
||||
```
|
||||
|
||||
پس متد `load` انتظار دریافت یک `static self'&` را دارد، این مرجعی است که برای تمام مدت زمان اجرای برنامه معتبر است. دلیل این امر این است که پردازنده در هر وقفه به این جدول دسترسی پیدا می کند تا زمانی که IDT دیگری بارگیری کنیم. بنابراین استفاده از طول عمر کوتاه تر از `static'` می تواند منجر به باگ های استفاده-بعد-از-آزادسازی شود.
|
||||
|
||||
در واقع ، این دقیقاً همان چیزی است که در اینجا اتفاق می افتد. `idt` ما روی پشته ایجاد می شود ، بنابراین فقط در داخل تابع `init` معتبر است. پس از آن حافظه پشته برای توابع دیگر مورد استفاده مجدد قرار می گیرد ، بنابراین پردازنده حافظه پشته تصادفی را به عنوان IDT تفسیر می کند. خوشبختانه ، متد `InterruptDescriptorTable::load` این نیاز به طول عمر را در تعریف تابع خود اجباری می کند، بنابراین کامپایلر راست قادر است از این مشکل احتمالی در زمان کامپایل جلوگیری کند.
|
||||
|
||||
برای رفع این مشکل، باید `idt` را در مکانی ذخیره کنیم که طول عمر `static'` داشته باشد. برای رسیدن به این هدف می توانیم IDT را با استفاده از [`Box`] بر روی حافظه Heap ایجاد کنیم و سپس آن را به یک مرجع `static'` تبدیل کنیم، اما ما در حال نوشتن هسته سیستم عامل هستیم و بنابراین هنوز Heap نداریم.
|
||||
|
||||
[`Box`]: https://doc.rust-lang.org/std/boxed/struct.Box.html
|
||||
|
||||
|
||||
به عنوان یک گزینه دیگر، می توانیم IDT را به صورت `static` ذخیره کنیم:
|
||||
```rust
|
||||
static IDT: InterruptDescriptorTable = InterruptDescriptorTable::new();
|
||||
|
||||
pub fn init_idt() {
|
||||
IDT.breakpoint.set_handler_fn(breakpoint_handler);
|
||||
IDT.load();
|
||||
}
|
||||
```
|
||||
|
||||
با این وجود، یک مشکل وجود دارد: استاتیکها تغییرناپذیر هستند، پس نمی توانیم ورودی بریکپوینت را از تابع `init` تغییر دهیم. می توانیم این مشکل را با استفاده از [`static mut`] حل کنیم:
|
||||
|
||||
[`static mut`]: https://doc.rust-lang.org/1.30.0/book/second-edition/ch19-01-unsafe-rust.html#accessing-or-modifying-a-mutable-static-variable
|
||||
|
||||
```rust
|
||||
static mut IDT: InterruptDescriptorTable = InterruptDescriptorTable::new();
|
||||
|
||||
pub fn init_idt() {
|
||||
unsafe {
|
||||
IDT.breakpoint.set_handler_fn(breakpoint_handler);
|
||||
IDT.load();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
در این روش بدون خطا کامپایل می شود اما مشکلات دیگری به همراه دارد. `static mut` بسیار مستعد Data Race هستند، بنابراین در هر دسترسی به یک [بلوک `unsafe`] نیاز داریم.
|
||||
|
||||
[بلوک `unsafe`]: https://doc.rust-lang.org/1.30.0/book/second-edition/ch19-01-unsafe-rust.html#unsafe-superpowers
|
||||
|
||||
#### Lazy Statics به نجات ما میآیند
|
||||
خوشبختانه ماکرو `lazy_static` وجود دارد. ماکرو به جای ارزیابی یک `static` در زمان کامپایل ، مقداردهی اولیه آن را هنگام اولین ارجاع به آن انجام می دهد. بنابراین، می توانیم تقریباً همه کاری را در بلوک مقداردهی اولیه انجام دهیم و حتی قادر به خواندن مقادیر زمان اجرا هستیم.
|
||||
|
||||
ما قبلاً کرت `lazy_static` را وارد کردیم وقتی [یک انتزاع برای بافر متن VGA ایجاد کردیم][vga text buffer lazy static]. بنابراین می توانیم مستقیماً از ماکرو `!lazy_static` برای ایجاد IDT استاتیک استفاده کنیم:
|
||||
|
||||
[vga text buffer lazy static]: @/edition-2/posts/03-vga-text-buffer/index.md#lazy-statics
|
||||
|
||||
```rust
|
||||
// in src/interrupts.rs
|
||||
|
||||
use lazy_static::lazy_static;
|
||||
|
||||
lazy_static! {
|
||||
static ref IDT: InterruptDescriptorTable = {
|
||||
let mut idt = InterruptDescriptorTable::new();
|
||||
idt.breakpoint.set_handler_fn(breakpoint_handler);
|
||||
idt
|
||||
};
|
||||
}
|
||||
|
||||
pub fn init_idt() {
|
||||
IDT.load();
|
||||
}
|
||||
```
|
||||
|
||||
توجه داشته باشید که چگونه این راه حل به هیچ بلوک `unsafe` نیاز ندارد. ماکرو `!lazy_static` از `unsafe` در پشت صحنه استفاده می کند ، اما در یک رابط امن به ما داده می شود.
|
||||
|
||||
### اجرای آن
|
||||
|
||||
آخرین مرحله برای کارکرد استثناها در هسته ما فراخوانی تابع `init_idt` از `main.rs` است. به جای فراخوانی مستقیم آن، یک تابع عمومی `init` را در `lib.rs` معرفی می کنیم:
|
||||
|
||||
```rust
|
||||
// in src/lib.rs
|
||||
|
||||
pub fn init() {
|
||||
interrupts::init_idt();
|
||||
}
|
||||
```
|
||||
|
||||
با استفاده از این تابع اکنون یک مکان اصلی برای روالهای اولیه داریم که می تواند بین توابع مختلف `start_` در `main.rs` ، `lib.rs` و تستهای یکپارچه به اشتراک گذاشته شود.
|
||||
|
||||
اکنون می توانیم تابع `start_` در `main.rs` را به روز کنیم تا `init` را فراخوانی کرده و سپس یک استثنا بریکپوینت ایجاد کند:
|
||||
|
||||
```rust
|
||||
// in src/main.rs
|
||||
|
||||
#[no_mangle]
|
||||
pub extern "C" fn _start() -> ! {
|
||||
println!("Hello World{}", "!");
|
||||
|
||||
blog_os::init(); // new
|
||||
|
||||
// invoke a breakpoint exception
|
||||
x86_64::instructions::interrupts::int3(); // new
|
||||
|
||||
// as before
|
||||
#[cfg(test)]
|
||||
test_main();
|
||||
|
||||
println!("It did not crash!");
|
||||
loop {}
|
||||
}
|
||||
```
|
||||
|
||||
اکنون هنگامی که آن را در QEMU اجرا می کنیم (با استفاده از `cargo run`) ، موارد زیر را مشاهده می کنیم:
|
||||
|
||||

|
||||
|
||||
کار می کند! پردازنده با موفقیت تابع کنترل کننده بریکپوینت ما را فراخوانی می کند ، که پیام را چاپ می کند و سپس به تابع `start_` برمی گردد ، جایی که پیام `!It did not crash` چاپ شده است.
|
||||
|
||||
می بینیم که قاب پشته وقفه، دستورالعمل و نشانگرهای پشته را در زمان وقوع استثنا به ما می گوید. این اطلاعات هنگام رفع اشکال استثناهای غیر منتظره بسیار مفید است.
|
||||
|
||||
### افزودن یک تست
|
||||
|
||||
بیایید یک تست ایجاد کنیم که از ادامه کار کد بالا اطمینان حاصل کند. ابتدا تابع `start_` را به روز می کنیم تا `init` را نیز فراخوانی کند:
|
||||
|
||||
```rust
|
||||
// in src/lib.rs
|
||||
|
||||
/// Entry point for `cargo test`
|
||||
#[cfg(test)]
|
||||
#[no_mangle]
|
||||
pub extern "C" fn _start() -> ! {
|
||||
init(); // new
|
||||
test_main();
|
||||
loop {}
|
||||
}
|
||||
```
|
||||
|
||||
بخاطر داشته باشید، این تابع `start_` هنگام اجرای`cargo test --lib` استفاده می شود، زیرا راست `lib.rs` را کاملاً مستقل از`main.rs` تست میکند. قبل از اجرای تستها باید برای راه اندازی IDT در اینجا `init` فراخوانی شود.
|
||||
|
||||
اکنون می توانیم یک تست `test_breakpoint_exception` ایجاد کنیم:
|
||||
|
||||
```rust
|
||||
// in src/interrupts.rs
|
||||
|
||||
#[test_case]
|
||||
fn test_breakpoint_exception() {
|
||||
// invoke a breakpoint exception
|
||||
x86_64::instructions::interrupts::int3();
|
||||
}
|
||||
```
|
||||
|
||||
این تست تابع `int3` را فراخوانی می کند تا یک استثنا بریکپوینت ایجاد کند. با بررسی اینکه اجرا پس از آن ادامه دارد ، تأیید می کنیم که کنترل کننده بریکپوینت ما به درستی کار می کند.
|
||||
|
||||
شما می توانید این تست جدید را با اجرای `cargo test` (همه تستها) یا` cargo test --lib` (فقط تست های `lib.rs` و ماژول های آن) امتحان کنید. باید موارد زیر را در خروجی مشاهده کنید:
|
||||
|
||||
```
|
||||
blog_os::interrupts::test_breakpoint_exception... [ok]
|
||||
```
|
||||
|
||||
## خیلی جادویی بود؟
|
||||
قرارداد فراخوانی `x86-interrupt` و نوع [`InterruptDescriptorTable`] روند مدیریت استثناها را نسبتاً سر راست و بدون درد ساختهاند. اگر این برای شما بسیار جادویی بود و دوست دارید تمام جزئیات مهم مدیریت استثنا را بیاموزید، برای شما هم مطالبی داریم: مجموعه ["مدیریت استثناها با توابع برهنه"] ما، نحوه مدیریت استثناها بدون قرارداد فراخوانی`x86-interrupt` را نشان می دهد و همچنین نوع IDT خاص خود را ایجاد می کند. از نظر تاریخی، این پستها مهمترین پستهای مدیریت استثناها قبل از وجود قرارداد فراخوانی `x86-interrupt` و کرت `x86_64` بودند. توجه داشته باشید که این پستها بر اساس [نسخه اول] این وبلاگ هستند و ممکن است قدیمی باشند.
|
||||
|
||||
["مدیریت استثناها با توابع برهنه"]: @/edition-1/extra/naked-exceptions/_index.md
|
||||
[`InterruptDescriptorTable`]: https://docs.rs/x86_64/0.12.1/x86_64/structures/idt/struct.InterruptDescriptorTable.html
|
||||
[نسخه اول]: @/edition-1/_index.md
|
||||
|
||||
## مرحله بعدی چیست؟
|
||||
ما اولین استثنای خود را با موفقیت گرفتیم و از آن بازگشتیم! گام بعدی اطمینان از این است که همه استثناها را می گیریم ، زیرا یک استثنا گرفته نشده باعث [خطای سهگانه] می شود که منجر به شروع مجدد سیستم می شود. پست بعدی توضیح می دهد که چگونه می توان با گرفتن صحیح [خطای دوگانه] از این امر جلوگیری کرد.
|
||||
|
||||
[خطای سهگانه]: https://wiki.osdev.org/Triple_Fault
|
||||
[خطای دوگانه]: https://wiki.osdev.org/Double_Fault#Double_Fault
|
||||
Reference in New Issue
Block a user