OS1: نواة بدائية على Rust لـ x86. الجزء 2. VGA ، GDT ، IDT

الجزء الاول


المقال الأول لم يتح له الوقت حتى يبرد ، لكنني قررت ألا أبقيك مفتونًا وكتابة تكملة.


لذلك ، في المقال السابق تحدثنا عن الربط ، وتحميل ملف kernel ، والتهيئة الأولية. لقد قدمت بعض الارتباطات المفيدة ، وأخبرت كيف توجد النواة المحملة في الذاكرة ، وكيف تتم مقارنة العناوين الافتراضية والمادية في وقت التمهيد ، وكيفية تمكين الدعم لآلية الصفحة. أخيرًا ، انتقل التحكم إلى وظيفة kmain الخاصة بنواة بلدي ، المكتوبة في Rust. حان الوقت للمضي قدما ومعرفة مدى عمق حفرة الأرنب!


في هذا الجزء من الملاحظات ، سأصف بإيجاز تهيئة Rust الخاصة بي ، وبشكل عام ، سأتحدث عن إخراج المعلومات في VGA ، وبالتفصيل عن إعداد المقاطع والمقاطعات . أطلب كل المهتمين تحت الخفض ، ونحن نبدأ.


الإعداد الصدأ


بشكل عام ، لا يوجد شيء معقد بشكل خاص في هذا الإجراء ، للحصول على تفاصيل يمكنك الاتصال بمدونة Philippe . ومع ذلك ، سوف أتوقف عند بعض النقاط.


لا يزال Stable Rust لا يدعم بعض الميزات الضرورية للتطوير المنخفض المستوى ، وبالتالي ، لتعطيل المكتبة القياسية والبناء على Bare Bones ، نحتاج إلى Rust ليلا. كن حذرًا ، بعد الترقية إلى الأحدث ، حصلت على مترجم غير نشط تمامًا واضطررت إلى العودة إلى أقرب مستقر. إذا كنت متأكدًا من أن برنامج التحويل البرمجي الخاص بك كان يعمل بالأمس ، ولكن تم تحديثه ولا يعمل ، فقم بتشغيل الأمر ، واستبدل التاريخ الذي تحتاجه


rustup override add nightly-YYYY-MM-DD 

للحصول على تفاصيل الآلية ، يمكنك الاتصال هنا .


بعد ذلك ، قم بتكوين النظام الأساسي الهدف الذي سنذهب إليه. لقد استندت إلى مدونة Philip Opperman ، حيث تم نقل الكثير من الأشياء في هذا القسم منه ، وتفكيكها بواسطة العظام وتكييفها مع احتياجاتي. يعمل Philip على تطوير الإصدار x64 في مدونته ، وقد اخترت في الأصل الإصدار x32 ، لذلك فإن هدف بلدي سيكون مختلفًا قليلاً. أحملها تماما


 { "llvm-target": "i686-unknown-none", "data-layout": "em:ep:32:32-f64:32:64-f80:32-n8:16:32-S128", "arch": "x86", "target-endian": "little", "target-pointer-width": "32", "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 أن هذه هي خيارات تخطيط البيانات ، مفصولة بـ "-". الشخصية الأولى "e" هي المسؤولة عن الهنود - في حالتنا ، إنها صغيرة جداً ، كما تتطلب المنصة. الشخصية الثانية هي م ، "تشويه". مسؤول عن أسماء الشخصيات أثناء التخطيط. نظرًا لأن تنسيق الإخراج سيكون ELF (انظر النص البرمجي للبناء) ، فإننا نختار "m: e". الحرف الثالث هو حجم المؤشر بالبت و ABI (واجهة التطبيق الثنائية). كل شيء بسيط هنا ، لدينا 32 بت ، لذلك وضعنا بجرأة "p: 32: 32". يصل المقبل هي أرقام النقطة العائمة. نحن نبلغ عن دعمنا للأرقام 64 بت وفقًا لـ ABI 32 مع المحاذاة 64 - "f64: 32: 64" ، بالإضافة إلى أرقام 80 بت مع المحاذاة بشكل افتراضي - "f80: 32". العنصر التالي هو الأعداد الصحيحة. نبدأ بـ 8 بت وانتقل إلى المنصة بحد أقصى 32 بت - "n8: 16: 32". الأخير هو محاذاة المكدس. أحتاج حتى 128 بت أعداد صحيحة ، لذلك فليكن S128. في أي حال ، يمكن LLVM تجاهل هذه المعلمة بأمان ، وهذا هو تفضيلنا.


فيما يتعلق بالمعلمات المتبقية ، يمكنك إلقاء نظرة خاطفة على Philip ، يوضح كل شيء جيدًا.


نحتاج أيضًا إلى cargo-xbuild - وهي أداة تتيح لك إمكانية التحويل البرمجي للصدأ عند إنشاء منصة هدف غير مألوفة.
تعيين.


 cargo install cargo-xbuild 

سنقوم بجمعها مثل هذا.


 cargo xbuild -Z unstable-options --manifest-path=kernel/Cargo.toml --target kernel/targets/$(ARCH).json --out-dir=build/lib 

كنت بحاجة إلى بيان للتشغيل الصحيح لـ Make ، لأنه يبدأ من الدليل الجذر ، وتقع kernel في دليل kernel.


من ميزات البيان ، يمكنني تمييز فقط crate-type = ["staticlib"] ، والذي يعطي ملفًا مرتبطًا بالإخراج . سنطعمه في LLD.


كمين والإعداد الأولي


وفقًا لاتفاقيات Rust ، إذا أنشأنا مكتبة ثابتة (أو ملف ثنائي "مسطح") ، يجب أن يحتوي جذر الصندوق على ملف lib.rs ، وهو نقطة الدخول. في ذلك ، بمساعدة السمات ، يتم تكوين ميزات اللغة ، وكذلك يوجد كمن العزيزة.


لذلك ، في الخطوة الأولى سنحتاج إلى تعطيل مكتبة الأمراض المنقولة جنسياً. يتم ذلك باستخدام ماكرو.


 #![no_std] 

مع هذه الخطوة البسيطة ، ننسى على الفور التعددية والذاكرة الديناميكية والمسرات الأخرى للمكتبة القياسية. علاوة على ذلك ، نحن حتى نحرم أنفسنا من println! سأخبرك عن كيفية القيام بذلك في المرة القادمة.


تنتهي العديد من البرامج التعليمية في مكان ما في هذا المكان بإخراج "Hello World" وبدون شرح كيفية العيش. سنذهب في الاتجاه الآخر. بادئ ذي بدء ، نحن بحاجة إلى تعيين مقاطع التعليمات البرمجية والبيانات لوضع المحمية ، وتكوين VGA ، وتكوين المقاطعات ، والتي سنفعلها.


 #![no_std] #[macro_use] pub mod debug; #[cfg(target_arch = "x86")] #[path = "arch/i686/mod.rs"] pub mod arch; #[no_mangle] extern "C" fn kmain(pd: usize, mb_pointer: usize, mb_magic: usize) { arch::arch_init(pd); ...... } #[panic_handler] fn panic(_info: &PanicInfo) -> ! { println!("{}", _info); loop {} } 

ما الذي يحدث هنا؟ كما قلت ، نحن نغلق المكتبة القياسية. سنعلن أيضًا عن وحدتين هامتين للغاية - التصحيح (الذي سنكتب فيه على الشاشة) والقوس (الذي سيعيش فيه كل السحر المعتمد على النظام الأساسي). يمكنني استخدام ميزة Rust مع التكوينات لإعلان نفس الواجهات في تطبيقات معمارية مختلفة واستخدامها على أكمل وجه. هنا أتوقف فقط عن x86 ثم نتحدث عنه فقط.


أعلنت معالج الذعر بدائية تماما ، الأمر الذي يتطلب الصدأ. ثم سيكون من الممكن تعديله.


يقبل kmain ثلاث وسيطات ويتم تصديره أيضًا بالترميز C دون تشويه الاسم بحيث يمكن للرابط ربط الوظيفة بشكل صحيح بالمكالمة الواردة من _loader ، والتي وصفتها في المقالة السابقة. الوسيطة الأولى هي عنوان جدول صفحات PD ، والثاني هو العنوان الفعلي لهيكل GRUB ، حيث سنحصل على بطاقة الذاكرة ، والثالث هو الرقم السحري. في المستقبل ، أود تطبيق كل من دعم Multiboot 2 ومحمل الإقلاع الخاص بي ، لذلك أستخدم رقمًا سحريًا لتحديد طريقة التمهيد.


استدعاء kmain الأول هو التهيئة الخاصة بالنظام الأساسي. نحن نذهب الى الداخل. توجد وظيفة arch_init في ملف arch / i686 / mod.rs ، وهي عامة ومحددة 32 بت x 86 ، وتبدو كما يلي:


 pub fn arch_init(pd: usize) { unsafe { vga::VGA_WRITER.lock().init(); gdt::setup_gdt(); idt::init_idt(); paging::setup_pd(pd); } } 

كما ترون ، بالنسبة للإصدار x86 ، تتم تهيئة الإخراج ، التجزئة ، المقاطعات ، الترحيل بالترتيب. لنبدأ مع VGA.


تهيئة VGA


يعتبر كل برنامج تعليمي من واجبهم طباعة Hello World ، لذلك ستجد كيفية التعامل مع VGA في كل مكان. لهذا السبب ، سأذهب لفترة قصيرة قدر الإمكان ، وسأركز فقط على الرقائق التي صنعتها بنفسي. حول استخدام lazy_static ، سأرسل لك مدونة فيليب ولن أشرح بالتفصيل. لم يتم إطلاق const fn بعد ، لذلك لا يمكن إجراء التهيئة السكونية الجميلة. وسنضيف قفلًا دائريًا حتى لا يتحول إلى حالة من الفوضى.


 use lazy_static::lazy_static; use spin::Mutex; lazy_static! { pub static ref VGA_WRITER : Mutex<Writer> = Mutex::new(Writer { cursor_position: 0, vga_color: ColorCode::new(Color::LightGray, Color::Black), buffer: unsafe { &mut *(0xC00B8000 as *mut VgaBuffer) } }); } 

كما تعلم ، يوجد المخزن المؤقت للشاشة في العنوان الفعلي 0xB8000 ويبلغ حجمه 80 × 25 × 2 بايت (عرض وارتفاع الشاشة ، بايت لكل حرف وسمات: الألوان ، وميض). نظرًا لأننا قمنا بالفعل بتمكين الذاكرة الظاهرية ، فسيتم تعطيل الوصول إلى هذا العنوان ، لذلك نضيف 3 غيغابايت. نحن نفضّل أيضًا مؤشرًا خامًا ، وهو أمر غير آمن - لكننا نعرف ما نقوم به.
من بين الأشياء المثيرة للاهتمام في هذا الملف ، ربما فقط تطبيق بنية Writer ، الذي لا يسمح فقط بعرض الأحرف على التوالي ، ولكن أيضًا التمرير ، والانتقال إلى أي مكان على الشاشة وغيرها من الأشياء الصغيرة الرائعة.


فغا الكاتب
 pub struct Writer { cursor_position: usize, vga_color: ColorCode, buffer: &'static mut VgaBuffer, } impl Writer { pub fn init(&mut self) { let vga_color = self.vga_color; for y in 0..(VGA_HEIGHT - 1) { for x in 0..VGA_WIDTH { self.buffer.chars[y * VGA_WIDTH + x] = ScreenChar { ascii_character: b' ', color_code: vga_color, } } } self.set_cursor_abs(0); } fn set_cursor_abs(&mut self, position: usize) { unsafe { outb(0x3D4, 0x0F); outb(0x3D5, (position & 0xFF) as u8); outb(0x3D4, 0x0E); outb(0x3D4, ((position >> 8) & 0xFF) as u8); } self.cursor_position = position; } pub fn set_cursor(&mut self, x: usize, y: usize) { self.set_cursor_abs(y * VGA_WIDTH + x); } pub fn move_cursor(&mut self, offset: usize) { self.cursor_position = self.cursor_position + offset; self.set_cursor_abs(self.cursor_position); } pub fn get_x(&mut self) -> u8 { (self.cursor_position % VGA_WIDTH) as u8 } pub fn get_y(&mut self) -> u8 { (self.cursor_position / VGA_WIDTH) as u8 } pub fn scroll(&mut self) { for y in 0..(VGA_HEIGHT - 1) { for x in 0..VGA_WIDTH { self.buffer.chars[y * VGA_WIDTH + x] = self.buffer.chars[(y + 1) * VGA_WIDTH + x] } } for x in 0..VGA_WIDTH { let color_code = self.vga_color; self.buffer.chars[(VGA_HEIGHT - 1) * VGA_WIDTH + x] = ScreenChar { ascii_character: b' ', color_code } } } pub fn ln(&mut self) { let next_line = self.get_y() as usize + 1; if next_line >= VGA_HEIGHT { self.scroll(); self.set_cursor(0, VGA_HEIGHT - 1); } else { self.set_cursor(0, next_line) } } pub fn write_byte_at_xy(&mut self, byte: u8, color: ColorCode, x: usize, y: usize) { self.buffer.chars[y * VGA_WIDTH + x] = ScreenChar { ascii_character: byte, color_code: color } } pub fn write_byte_at_pos(&mut self, byte: u8, color: ColorCode, position: usize) { self.buffer.chars[position] = ScreenChar { ascii_character: byte, color_code: color } } pub fn write_byte(&mut self, byte: u8) { if self.cursor_position >= VGA_WIDTH * VGA_HEIGHT { self.scroll(); self.set_cursor(0, VGA_HEIGHT - 1); } self.write_byte_at_pos(byte, self.vga_color, self.cursor_position); self.move_cursor(1); } pub fn write_string(&mut self, s: &str) { for byte in s.bytes() { match byte { 0x20...0xFF => self.write_byte(byte), b'\n' => self.ln(), _ => self.write_byte(0xfe), } } } } 

عند اللف ، ما عليك سوى نسخ أقسام من الذاكرة بحجم عرض الشاشة للخلف ، وملء الفراغات بخط جديد (هكذا أفعل التنظيف). تعد المكالمات الخارجية أكثر إثارة للاهتمام - لا يمكن نقل المؤشر بأي شكل من الأشكال من العمل مع منافذ الإدخال / الإخراج. ومع ذلك ، ما زلنا بحاجة إلى الإدخال / الإخراج عبر المنافذ ، لذلك تم تسليمها في حزمة منفصلة وملفوفة في أغلفة آمنة. تحت المفسد أدناه هو رمز المجمع. في الوقت الحالي ، يكفي أن نعرف ما يلي:


  • يتم عرض إزاحة المؤشر المطلق وليس الإحداثي.
  • يمكنك الإخراج إلى وحدة تحكم بايت واحد في وقت واحد
  • يحدث إخراج بايت واحد في أمرين - أولاً نكتب الأمر إلى وحدة التحكم ، ثم البيانات.
  • منفذ الأوامر هو 0x3D4 ، ومنفذ البيانات هو 0x3D5
  • أولاً ، اطبع البايت السفلي من الموضع بالأمر 0x0F ، ثم الجزء العلوي بالأمر 0x0E

out.asm

إيلاء الاهتمام للعمل مع المتغيرات التي تم تمريرها على المكدس. منذ أن يبدأ المكدس في نهاية المساحة ويقلل من مؤشر المكدس عند استدعاء الوظيفة ، وللحصول على المعلمات ، ونقطة الإرجاع ، وما إلى ذلك ، تحتاج إلى إضافة حجم الوسيطة المحاذاة مع محاذاة المكدس إلى سجل ESP ، في الحالة 4 بايت لدينا.


 global writeb global writew global writed section .text writeb: push ebp mov ebp, esp mov edx, [ebp + 8] ;port in stack: 8 = 4 (push ebp) + 4 (parameter port length is 2 bytes but stack aligned 4 bytes) mov eax, [ebp + 8 + 4] ;value in stack - 8 = see ^, 4 = 1 byte value aligned 4 bytes out dx, al ;write byte by port number an dx - value in al mov esp, ebp pop ebp ret writew: push ebp mov ebp, esp mov edx, [ebp + 8] ;port in stack: 8 = 4 (push ebp) + 4 (parameter port length is 2 bytes but stack aligned 4 bytes) mov eax, [ebp + 8 + 4] ;value in stack - 8 = see ^, 4 = 1 word value aligned 4 bytes out dx, ax ;write word by port number an dx - value in ax mov esp, ebp pop ebp ret writed: push ebp mov ebp, esp mov edx, [ebp + 8] ;port in stack: 8 = 4 (push ebp) + 4 (parameter port length is 2 bytes but stack aligned 4 bytes) mov eax, [ebp + 8 + 4] ;value in stack - 8 = see ^, 4 = 1 double word value aligned 4 bytes out dx, eax ;write double word by port number an dx - value in eax mov esp, ebp pop ebp ret 

إعداد القطاع


وصلنا إلى أكثر المحير ، ولكن في نفس الوقت أبسط الموضوع. كما قلت في مقال سابق ، كانت الصفحة وتنظيم مقطع الذاكرة مختلطين في رأسي ، لقد قمت بتحميل عنوان جدول الصفحة في GDTR وأمسك برأسي. استغرق الأمر مني عدة أشهر لقراءة المادة بما فيه الكفاية ، وهضمها ، وتكون قادرة على تحقيق ذلك. ربما وقعت ضحية لمؤلف كتاب بيتر أبيل. اللغة والبرمجة للكمبيوتر IBM "(كتاب رائع!) ، والذي يصف تجزئة Intel 8086. في تلك الأوقات الممتعة ، قمنا بتحميل 16 بت العلوي من عنوان العشرين بت في سجل المقطع ، وكان هذا هو العنوان في الذاكرة. اتضح أن هناك خيبة أمل قاسية أنه بدءًا من i286 في الوضع المحمي ، كل شيء خاطئ تمامًا.


لذا ، فإن النظرية المجردة هي أن x86 يدعم طراز الذاكرة المقسمة ، حيث يمكن للبرامج القديمة الخروج فقط بعد 640 كيلو بايت ، ثم 1 ميغابايت من الذاكرة.


كان على المبرمجين التفكير في كيفية وضع التعليمات البرمجية القابلة للتنفيذ ، وكيفية وضع البيانات ، وكيفية الحفاظ على سلامتهم. جعل ظهور تنظيم الصفحة المنظمة المقسمة غير ضروري ، لكنه بقي لغرض التوافق والحماية (فصل الامتيازات الخاصة بمساحة kernel-space ومساحة المستخدم) ، لذلك بدونها لا يوجد مكان. يتم حظر بعض إرشادات المعالج عندما يكون مستوى الامتياز أضعف من 0 ، وسيؤدي الوصول بين مقاطع البرنامج و kernel إلى حدوث خطأ تجزئة.


دعونا نفعل ذلك مرة أخرى (نأمل في الأخير) حول ترجمة العنوان
عنوان السطر [0x08: 0xFFFFFFFF] -> تحقق من أذونات القطعة 0x08 -> العنوان الظاهري [0xFFFFFFFF] -> جدول الصفحة + TLB -> العنوان الفعلي [0xAAAAFFFF]


يتم استخدام قطعة فقط داخل المعالج ، ويتم تخزينها في سجل قطعة خاصة (CS ، SS ، DS ، ES ، FS ، GS) ويستخدم حصرا لفحص حقوق تنفيذ التعليمات البرمجية والتحكم في النقل. هذا هو السبب في أنه لا يمكنك فقط التقاط واستدعاء وظيفة kernel من مساحة المستخدم. إن مقطعًا يحتوي على واصف 0x18 (لدي واحد ، لديك مقطع آخر) لديه حقوق من المستوى 3 ، ولقطعة ذات واصف 0x08 لها حقوق المستوى 0. وفقًا للاتفاقية x86 ، للحماية من الوصول غير المصرح به ، لا يمكن للقطعة ذات الامتيازات الأقل الاتصال مباشرة بقطعة ذات امتيازات كبيرة الحقوق عبر jmp 0x08: [EAX] ، ولكنها ملزمة باستخدام آليات أخرى ، مثل الفخاخ والبوابات والقاطعات.


يجب وصف القطاعات وأنواعها (الكود ، البيانات ، السلالم ، البوابات) في جدول الواصف العالمي GDT ، العنوان الافتراضي وحجمه الذي تم تحميله في سجل GDTR. عند التبديل بين القطاعات (للبساطة ، أفترض أن الانتقال المباشر ممكن) ، يجب عليك استدعاء jmp 0x08: [EAX] ، حيث يكون 0x08 هو إزاحة أول واصف صالح بالبايت من بداية الجدول ، ويكون EAX هو السجل الذي يحتوي على عنوان النقل. سيتم تحميل الإزاحة (المحدد) في سجل CS ، وسيتم تحميل الواصف المقابل في سجل الظل للمعالج. كل واصف هو بنية 8 بايت. تم توثيقه جيدًا ويمكن العثور على وصفه على كل من OSDev ووثائق Intel (راجع المقال الأول).


أنا ألخص. عندما نهيئ GDT وننفذ jmp 0x08: انتقال [EAX] ، ستكون حالة المعالج كما يلي:


  • يحتوي GDTR على عنوان GDT افتراضي
  • يحتوي CS على القيمة 0x08
  • تم نسخ مؤشر إلى العنوان [GDTR + 0x08] إلى سجل الظل CS من الذاكرة
  • يحتوي سجل EIP على العنوان من سجل EAX

يجب أن يكون واصف الصفر دائمًا غير مهيأ ويحظر الوصول إليه. سوف أتطرق إلى واصف TSS ومعناه بمزيد من التفصيل عندما نناقش تعدد مؤشرات الترابط. يبدو جدول GDT الآن كما يلي:


 extern { fn load_gdt(base: *const GdtEntry, limit: u16); } pub unsafe fn setup_gdt() { GDT[5].set_offset((&super::tss::TSS) as *const _ as u32); GDT[5].set_limit(core::mem::size_of::<super::tss::Tss>() as u32); let gdt_ptr: *const GdtEntry = GDT.as_ptr(); let limit = (GDT.len() * core::mem::size_of::<GdtEntry>() - 1) as u16; load_gdt(gdt_ptr, limit); } static mut GDT: [GdtEntry; 7] = [ //null descriptor - cannot access GdtEntry::new(0, 0, 0, 0), //kernel code GdtEntry::new(0, 0xFFFFFFFF, GDT_A_PRESENT | GDT_A_RING_0 | GDT_A_SYSTEM | GDT_A_EXECUTABLE | GDT_A_PRIVILEGE, GDT_F_PAGE_SIZE | GDT_F_PROTECTED_MODE), //kernel data GdtEntry::new(0, 0xFFFFFFFF, GDT_A_PRESENT | GDT_A_RING_0 | GDT_A_SYSTEM | GDT_A_PRIVILEGE, GDT_F_PAGE_SIZE | GDT_F_PROTECTED_MODE), //user code GdtEntry::new(0, 0xFFFFFFFF, GDT_A_PRESENT | GDT_A_RING_3 | GDT_A_SYSTEM | GDT_A_EXECUTABLE | GDT_A_PRIVILEGE, GDT_F_PAGE_SIZE | GDT_F_PROTECTED_MODE), //user data GdtEntry::new(0, 0xFFFFFFFF, GDT_A_PRESENT | GDT_A_RING_3 | GDT_A_SYSTEM | GDT_A_PRIVILEGE, GDT_F_PAGE_SIZE | GDT_F_PROTECTED_MODE), //TSS - for interrupt handling in multithreading GdtEntry::new(0, 0, GDT_A_PRESENT | GDT_A_RING_3 | GDT_A_TSS_AVAIL, 0), GdtEntry::new(0, 0, 0, 0), ]; 

وهنا التهيئة ، التي تحدثت عنها كثيرا أعلاه. يتم تحميل عنوان GDT وحجمه من خلال هيكل منفصل ، يحتوي على حقلين فقط. يتم تمرير الأمر lgdt عنوان هذه البنية. في سجلات قطعة البيانات ، قم بتحميل الواصف التالي مع إزاحة 0x10.


 global load_gdt section .text gdtr dw 0 ; For limit storage dd 0 ; For base storage load_gdt: mov eax, [esp + 4] mov [gdtr + 2], eax mov ax, [esp + 8] mov [gdtr], ax lgdt [gdtr] jmp 0x08:.reload_CS .reload_CS: mov ax, 0x10 ; 0x10 points at the new data selector mov ds, ax mov es, ax mov fs, ax mov gs, ax mov ss, ax mov ax, 0x28 ltr ax ret 

ثم سيكون كل شيء أسهل قليلاً ، ولكن ليس أقل إثارة للاهتمام.


المقاطعات


في الواقع ، لقد حان الوقت لمنحنا الفرصة للتفاعل مع جوهرنا (على الأقل لمعرفة ما نضغط عليه على لوحة المفاتيح). للقيام بذلك ، يجب تهيئة وحدة تحكم المقاطعة.


الانحدار الغنائي حول أسلوب الكود.


بفضل الجهود التي بذلها المجتمع وتحديداً Philip Opperman ، تمت إضافة اتفاقية استدعاء المقاطعة x86 إلى Rust ، والتي تتيح لك كتابة معالجات المقاطعة التي تنفذ تنفيذًا. ومع ذلك ، فقد قررت بوعي عدم السير في هذا الطريق ، حيث قررت فصل المجمّع والصدأ في ملفات مختلفة ، وبالتالي الوظائف. نعم ، أنا أستخدم ذاكرة مكدسة بشكل غير معقول ، وأنا على علم بذلك ، لكنها لا تزال تنكه. تتم كتابة معالجات المقاطعة الخاصة بي في المجمّع ويقومون بشيء واحد: يسمون معالجات المقاطعة نفسها المكتوبة بلغة Rust تقريبًا. يرجى قبول هذه الحقيقة وتكون متسامح.


بشكل عام ، تشبه عملية تهيئة المقاطعات عملية تهيئة GDT ، ولكنها أسهل في الفهم. من ناحية أخرى ، تحتاج إلى الكثير من الكود الموحد. يتخذ مطورو Redox OS قرارًا جميلًا ، مستخدمين جميع مسرات اللغة ، لكنني ذهبت "وجهاً لوجه" وقررت السماح بتكرار الكود.


وفقًا للاتفاقية x86 ، لدينا انقطاع ، ولكن هناك حالات استثنائية. في هذا السياق ، الإعدادات الخاصة بنا هي نفسها عمليًا. الاختلاف الوحيد هو أنه عند طرح استثناء ، قد تحتوي الحزمة على معلومات إضافية. على سبيل المثال ، أستخدمها للتعامل مع عدم وجود صفحة عند العمل مع مجموعة (ولكن كل شيء له وقته). تتم معالجة كل المقاطعات والاستثناءات من نفس الجدول ، والتي أنت وأنا بحاجة إلى ملء. من الضروري أيضًا برمجة PIC (وحدة تحكم المقاطعة القابلة للبرمجة). هناك أيضا APIC ، لكنني لم أحسب حتى الآن.


حول العمل مع الموافقة المسبقة عن علم ، لن أقدم الكثير من التعليقات ، حيث توجد أمثلة كثيرة على الشبكة حول العمل معها. سأبدأ مع معالجات في المجمع. كلها متطابقة تمامًا ، لذلك سأزيل رمز المفسد.


IRQ
 global irq0 global irq1 ...... global irq14 global irq15 extern kirq0 extern kirq1 ...... extern kirq14 extern kirq15 section .text irq0: pusha call kirq0 popa iret irq1: pusha call kirq1 popa iret ...... irq14: pusha call kirq14 popa iret irq15: pusha call kirq15 popa iret 

كما ترون ، تبدأ جميع المكالمات إلى وظائف Rust بالبادئة "k" - للتمييز والراحة. معالجة الاستثناء هو نفسه تمامًا. بالنسبة لوظائف المجمّع ، يتم تحديد البادئة "e" ، بالنسبة إلى Rust ، "k". يختلف معالج أخطاء الصفحة ، ولكن عن ذلك - في الملاحظات حول إدارة الذاكرة.


استثناءات
 global e0_zero_divide global e1_debug ...... global eE_page_fault ...... global e14_virtualization global e1E_security extern k0_zero_divide extern k1_debug ...... extern kE_page_fault ...... extern k14_virtualization extern k1E_security section .text e0_zero_divide: pushad call k0_zero_divide popad iret e1_debug: pushad call k1_debug popad iret ...... eE_page_fault: pushad mov eax, [esp + 32] push eax mov eax, cr2 push eax call kE_page_fault pop eax pop eax popad add esp, 4 iret ...... e14_virtualization: pushad call k14_virtualization popad iret e1E_security: pushad call k1E_security popad iret 

نعلن معالجات المجمع:


 extern { fn load_idt(base: *const IdtEntry, limit: u16); fn e0_zero_divide(); fn e1_debug(); ...... fn e14_virtualization(); fn e1E_security(); fn irq0(); fn irq1(); ...... fn irq14(); fn irq15(); } 

نحدد معالجات الصدأ التي نسميها أعلاه. يرجى ملاحظة أنه لمقاطعة لوحة المفاتيح ، أنا ببساطة أعرض الكود الذي تم استلامه ، والذي أحصل عليه من المنفذ 0x60 - هكذا تعمل لوحة المفاتيح في أبسط وضع. آمل أن يتحول هذا في المستقبل إلى سائق متكامل. بعد كل مقاطعة ، تحتاج إلى إخراج إشارة إلى نهاية معالجة 0x20 ، وهذا هو المهم! خلاف ذلك ، لن تحصل على المزيد من المقاطعات.


 #[no_mangle] pub unsafe extern fn kirq0() { // println!("IRQ 0"); outb(0x20, 0x20); } #[no_mangle] pub unsafe extern fn kirq1() { let ch: char = inb(0x60) as char; crate::arch::vga::VGA_WRITER.force_unlock(); println!("IRQ 1 {}", ch); outb(0x20, 0x20); } #[no_mangle] pub unsafe extern fn kirq2() { println!("IRQ 2"); outb(0x20, 0x20); } ... 

تهيئة IDT و PIC. حول الموافقة المسبقة عن علم وإعادة تعيينها ، وجدت عددًا كبيرًا من البرامج التعليمية بدرجات متفاوتة من التفاصيل ، بدءًا من OSDev وتنتهي بمواقع الهواة. نظرًا لأن إجراء البرمجة يعمل بسلسلة مستمرة من العمليات والأوامر الثابتة ، سأقدم هذا الكود دون مزيد من التوضيح. , 0x20-0x2F , 0x20 0x28, 16 IDT.


 unsafe fn setup_pic(pic1: u8, pic2: u8) { // Start initialization outb(PIC1, 0x11); outb(PIC2, 0x11); // Set offsets outb(PIC1 + 1, pic1); /* remap */ outb(PIC2 + 1, pic2); /* pics */ // Set up cascade outb(PIC1 + 1, 4); /* IRQ2 -> connection to slave */ outb(PIC2 + 1, 2); // Set up interrupt mode (1 is 8086/88 mode, 2 is auto EOI) outb(PIC1 + 1, 1); outb(PIC2 + 1, 1); // Unmask interrupts outb(PIC1 + 1, 0); outb(PIC2 + 1, 0); // Ack waiting outb(PIC1, 0x20); outb(PIC2, 0x20); } pub unsafe fn init_idt() { IDT[0x0].set_func(e0_zero_divide); IDT[0x1].set_func(e1_debug); ...... IDT[0x14].set_func(e14_virtualization); IDT[0x1E].set_func(e1E_security); IDT[0x20].set_func(irq0); IDT[0x21].set_func(irq1); ...... IDT[0x2E].set_func(irq14); IDT[0x2F].set_func(irq15); setup_pic(0x20, 0x28); let idt_ptr: *const IdtEntry = IDT.as_ptr(); let limit = (IDT.len() * core::mem::size_of::<IdtEntry>() - 1) as u16; load_idt(idt_ptr, limit); } 

IDTR GDTR — . STI — — , , ASCII- -.


 global load_idt section .text idtr dw 0 ; For limit storage dd 0 ; For base storage load_idt: mov eax, [esp + 4] mov [idtr + 2], eax mov ax, [esp + 8] mov [idtr], ax lidt [idtr] sti ret 

خاتمة


, , . setup_pd, . , , , .


- GitLab .


شكرا لاهتمامكم!


UPD: 3

Source: https://habr.com/ru/post/ar445584/


All Articles