OS1: kernel primitif di Rust untuk x86. Bagian 2. VGA, GDT, IDT

Bagian pertama


Artikel pertama belum punya waktu untuk menenangkan, tetapi saya memutuskan untuk tidak membuat Anda tertarik dan menulis sekuel.


Jadi, pada artikel sebelumnya kita berbicara tentang menautkan, memuat file kernel, dan inisialisasi primer. Saya memberikan beberapa tautan bermanfaat, memberi tahu bagaimana kernel yang dimuat terletak di memori, bagaimana alamat virtual dan fisik dibandingkan pada saat boot, dan bagaimana mengaktifkan dukungan untuk mekanisme halaman. Terakhir, kontrol diteruskan ke fungsi kmain dari kernel saya, yang ditulis dalam Rust. Saatnya untuk melanjutkan dan mencari tahu seberapa dalam lubang kelinci!


Pada bagian catatan ini, saya akan menjelaskan secara singkat konfigurasi Rust saya, secara umum saya akan berbicara tentang output informasi dalam VGA, dan secara rinci tentang pengaturan segmen dan interupsi . Saya meminta semua yang tertarik di bawah potongan, dan kami mulai.


Setup karat


Secara umum, tidak ada yang rumit dalam prosedur ini, untuk detail Anda dapat menghubungi blog Philippe . Namun, saya akan berhenti di beberapa titik.


Stable Rust masih tidak mendukung beberapa fitur yang diperlukan untuk pengembangan tingkat rendah, oleh karena itu, untuk menonaktifkan perpustakaan standar dan membangun di Bare Bones, kita perlu Rust malam. Hati-hati, sekali setelah memperbarui ke terbaru saya mendapat kompiler yang benar-benar tidak beroperasi dan harus memutar kembali ke yang stabil terdekat. Jika Anda yakin bahwa kompiler Anda berfungsi kemarin, tetapi diperbarui dan tidak berfungsi, jalankan perintah, gantilah tanggal yang Anda butuhkan


rustup override add nightly-YYYY-MM-DD 

Untuk detail mekanisme, Anda dapat menghubungi di sini .


Selanjutnya, konfigurasikan platform target yang akan kita tuju. Saya berdasarkan pada blog Philip Opperman, begitu banyak hal dalam bagian ini diambil darinya, dibongkar oleh tulang dan disesuaikan dengan kebutuhan saya. Philip mengembangkan untuk x64 di blog-nya, saya awalnya memilih x32, jadi target.json saya akan sedikit berbeda. Saya membawanya sepenuhnya


 { "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" } 

Bagian tersulit di sini adalah parameter " data-layout ". Dokumentasi LLVM memberi tahu kita bahwa ini adalah opsi tata letak data, dipisahkan oleh β€œ-”. Karakter β€œe” pertama bertanggung jawab untuk keindonesiaan - dalam kasus kami ini adalah little-endian, seperti yang dibutuhkan oleh platform. Karakter kedua adalah m, "distorsi". Bertanggung jawab atas nama karakter selama tata letak. Karena format output kami adalah ELF (lihat skrip pembuatan), kami memilih "m: e". Karakter ketiga adalah ukuran pointer dalam bit dan ABI (Application binary interface). Semuanya sederhana di sini, kami memiliki 32 bit, jadi kami dengan berani menempatkan "p: 32: 32". Selanjutnya adalah angka floating point. Kami melaporkan bahwa kami mendukung angka 64-bit sesuai dengan ABI 32 dengan perataan 64 - "f64: 32: 64", serta angka 80-bit dengan perataan secara default - "f80: 32". Elemen selanjutnya adalah bilangan bulat. Kami mulai dengan 8 bit dan pindah ke platform maksimum 32 bit - "n8: 16: 32". Yang terakhir adalah penjajaran tumpukan. Saya bahkan perlu integer 128 bit, jadi biarlah S128. Bagaimanapun, LLVM dapat dengan aman mengabaikan parameter ini, ini adalah preferensi kami.


Mengenai parameter yang tersisa, Anda bisa mengintip Philip, ia menjelaskan semuanya dengan baik.


Kami juga membutuhkan kargo-xbuild - alat yang memungkinkan Anda melakukan kompilasi silang-inti saat membangun di bawah platform target yang tidak dikenal.
Instal.


 cargo install cargo-xbuild 

Kami akan mengumpulkannya seperti ini.


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

Saya membutuhkan manifes untuk operasi Make yang benar, karena ini dimulai dari direktori root, dan kernel terletak di direktori kernel.


Dari fitur manifes, saya hanya dapat menyorot crate-type = ["staticlib"] , yang memberikan file yang dapat ditautkan ke output. Kami akan memberinya makan di LLD.


kmain dan pengaturan awal


Menurut konvensi Rust, jika kita membuat pustaka statis (atau file biner "flat"), root dari peti harus berisi file lib.rs, yang merupakan titik masuk. Di dalamnya, dengan bantuan atribut, fitur bahasa dikonfigurasikan, dan juga kmain berharga terletak.


Jadi, pada langkah pertama kita perlu menonaktifkan perpustakaan std. Ini dilakukan dengan makro.


 #![no_std] 

Dengan langkah sederhana ini, kami segera melupakan multithreading, memori dinamis, dan kesenangan lain dari perpustakaan standar. Selain itu, kami bahkan menghilangkan println !, makro, jadi kami harus menerapkannya sendiri. Saya akan memberi tahu Anda cara melakukannya lain kali.


Banyak tutorial di suatu tempat di tempat ini berakhir dengan output dari "Hello World" dan tanpa menjelaskan bagaimana hidup. Kami akan pergi ke arah lain. Pertama-tama, kita perlu mengatur kode dan segmen data untuk mode terproteksi, mengkonfigurasi VGA, mengkonfigurasi interupsi, yang akan kita lakukan.


 #![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 {} } 

Apa yang sedang terjadi di sini? Seperti yang saya katakan, kami mematikan perpustakaan standar. Kami juga akan mengumumkan dua modul yang sangat penting - debug (di mana kami akan menulis di layar) dan lengkungan (di mana semua sihir yang bergantung pada platform akan hidup). Saya menggunakan fitur Rust dengan konfigurasi untuk mendeklarasikan antarmuka yang sama dalam implementasi arsitektur yang berbeda dan menggunakannya secara maksimal. Di sini saya berhenti hanya pada x86 dan kemudian kita hanya membicarakannya.


Saya menyatakan penangan panik yang benar-benar primitif, yang dituntut oleh Rust. Maka akan dimungkinkan untuk memodifikasinya.


kmain menerima tiga argumen dan juga diekspor dalam notasi C tanpa distorsi nama sehingga linker dapat dengan benar mengaitkan fungsi dengan panggilan dari _loader, yang saya jelaskan di artikel sebelumnya. Argumen pertama adalah alamat tabel halaman PD, yang kedua adalah alamat fisik struktur GRUB, dari mana kita akan mendapatkan kartu memori, yang ketiga adalah angka ajaib. Di masa depan, saya ingin mengimplementasikan dukungan Multiboot 2 dan bootloader saya sendiri, jadi saya menggunakan angka ajaib untuk mengidentifikasi metode boot.


Panggilan kmain pertama adalah inisialisasi platform khusus. Kami masuk ke dalam. Fungsi arch_init terletak di file arch / i686 / mod.rs, bersifat publik, 32-bit x86-spesifik, dan terlihat seperti ini:


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

Seperti yang Anda lihat, untuk x86, output, segmentasi, interupsi, dan paging diinisialisasi secara berurutan. Mari kita mulai dengan VGA.


Inisialisasi VGA


Setiap tutorial menganggap itu tugas mereka untuk mencetak Hello World, sehingga Anda akan menemukan cara bekerja dengan VGA di mana-mana. Untuk alasan ini, saya akan pergi sesingkat mungkin, saya akan fokus hanya pada chip yang saya buat sendiri. Tentang penggunaan lazy_static, saya akan mengirim Anda ke blog Philippe dan tidak akan menjelaskan secara detail. const fn belum dirilis, jadi inisialisasi statis yang indah belum dapat dilakukan. Dan kami akan menambahkan kunci putar sehingga tidak menjadi berantakan.


 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) } }); } 

Seperti yang Anda ketahui, buffer layar terletak di alamat fisik 0xB8000 dan memiliki ukuran 80x25x2 byte (lebar dan tinggi layar, byte per karakter dan atribut: warna, flicker). Karena kami telah mengaktifkan memori virtual, mengakses alamat ini akan macet, jadi kami menambahkan 3 GB. Kami juga melakukan penunjuk pointer mentah, yang tidak aman - tapi kami tahu apa yang kami lakukan.
Dari hal-hal menarik dalam file ini, mungkin, hanya penerapan struktur Writer, yang memungkinkan tidak hanya untuk menampilkan karakter dalam satu baris, tetapi juga untuk menggulir, pergi ke setiap tempat di layar dan hal-hal sepele yang menyenangkan lainnya.


Penulis Vga
 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), } } } } 

Saat memutar, cukup salin bagian memori ukuran lebar layar ke belakang, dengan mengisi baris baru (ini adalah cara saya melakukan pembersihan). Panggilan outb sedikit lebih menarik - tidak lain daripada bekerja dengan port I / O tidak mungkin untuk memindahkan kursor. Namun, kami masih membutuhkan input / output melalui port, sehingga mereka dikirim dalam paket terpisah dan dibungkus dengan pembungkus yang aman. Di bawah spoiler di bawah ini adalah kode assembler. Untuk saat ini, cukup mengetahui bahwa:


  • Offset kursor absolut, bukan koordinat, ditampilkan.
  • Anda dapat output ke controller satu byte pada suatu waktu
  • Output dari satu byte terjadi dalam dua perintah - pertama kita menulis perintah ke controller, kemudian data.
  • Port untuk perintah adalah 0x3D4, port data adalah 0x3D5
  • Pertama, cetak byte bawah posisi dengan perintah 0x0F, lalu atas dengan perintah 0x0E

keluar

Perhatikan bekerja dengan variabel lulus pada stack. Karena tumpukan dimulai pada akhir ruang dan mengurangi penunjuk tumpukan saat memanggil fungsi, untuk mendapatkan parameter, titik kembali, dll., Anda perlu menambahkan ukuran argumen yang disejajarkan dengan penyelarasan tumpukan ke register ESP, dalam kasus kami 4 byte.


 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 

Pengaturan Segmen


Kami sampai pada yang paling membingungkan, tetapi pada saat yang sama topik paling sederhana. Seperti yang saya katakan di artikel sebelumnya, organisasi memori halaman dan segmen dicampur di kepala saya, saya memuat alamat tabel halaman ke dalam GDTR dan meraih kepala saya. Butuh beberapa bulan bagi saya untuk membaca materi itu dengan cukup, mencernanya dan dapat menyadarinya. Saya mungkin telah menjadi korban Assembler buku teks Peter Abel. Bahasa dan pemrograman untuk PC IBM ”(buku yang hebat!), Yang menjelaskan segmentasi untuk Intel 8086. Di masa-masa yang menyenangkan itu, kami memasukkan 16 bit teratas dari alamat dua puluh bit ke register segmen, dan itu adalah alamat dalam memori. Ternyata menjadi kekecewaan yang kejam karena mulai dengan i286 dalam mode terproteksi, semuanya benar-benar salah.


Jadi, teori telanjangnya adalah bahwa x86 mendukung model memori tersegmentasi, karena program yang lebih lama hanya dapat melampaui 640 KB, dan kemudian memori 1 MB.


Programmer harus memikirkan cara menempatkan kode yang dapat dieksekusi, cara menempatkan data, dan cara menjaga keamanannya. Munculnya organisasi halaman membuat organisasi tersegmentasi tidak perlu, tetapi tetap untuk tujuan kompatibilitas dan perlindungan (pemisahan hak istimewa untuk ruang kernel dan ruang pengguna), jadi tanpa itu tidak ada tempat. Beberapa instruksi prosesor dilarang ketika level privilege lebih lemah dari 0, dan akses antara segmen program dan kernel akan menyebabkan kesalahan segmentasi.


Mari kita lakukan lagi (semoga yang terakhir) tentang terjemahan alamat
Alamat baris [0x08: 0xFFFFFFFF] -> Verifikasi izin segmen 0x08 -> Alamat virtual [0xFFFFFFFF] -> Tabel halaman + TLB -> Alamat fisik [0xAAAAFFFF]


Segmen digunakan hanya di dalam prosesor, disimpan dalam register segmen khusus (CS, SS, DS, ES, FS, GS) dan digunakan secara eksklusif untuk memeriksa hak untuk mengeksekusi kode dan kontrol transfer. Itulah sebabnya Anda tidak bisa hanya mengambil dan memanggil fungsi kernel dari ruang pengguna. Segmen dengan deskriptor 0x18 (saya punya satu, Anda punya yang berbeda) memiliki hak level 3, dan segmen dengan deskriptor 0x08 memiliki hak level 0. Menurut konvensi x86, untuk melindungi terhadap akses tidak sah, segmen dengan sedikit hak istimewa tidak dapat secara langsung memanggil segmen dengan besar hak melalui jmp 0x08: [EAX], tetapi wajib menggunakan mekanisme lain, seperti jebakan, gerbang, interupsi.


Segmen dan jenisnya (kode, data, tangga, gerbang) harus dijelaskan dalam tabel deskriptor global GDT, alamat virtual, dan ukurannya yang dimuat ke dalam register GDTR. Saat beralih antar segmen (untuk kemudahan, saya berasumsi bahwa transisi langsung dimungkinkan), Anda harus menghubungi instruksi jmp 0x08: [EAX], di mana 0x08 adalah offset dari deskriptor yang valid pertama dalam byte dari awal tabel , dan EAX adalah register yang berisi alamat transisi. Offset (pemilih) akan dimuat ke dalam register CS, dan deskriptor yang sesuai akan dimuat ke dalam register bayangan prosesor. Setiap deskriptor adalah struktur 8 byte. Ini didokumentasikan dengan baik dan deskripsinya dapat ditemukan di OSDev dan di dokumentasi Intel (lihat artikel pertama).


Saya meringkas. Ketika kita menginisialisasi GDT dan menjalankan transisi jmp 0x08: [EAX], status prosesor adalah sebagai berikut:


  • GDTR berisi alamat GDT virtual
  • CS berisi nilai 0x08
  • Pegangan ke alamat [GDTR + 0x08] disalin ke register bayangan CS dari memori
  • Register EIP berisi alamat dari register EAX

Deskriptor nol harus selalu tidak diinisialisasi dan akses ke sana dilarang. Saya akan membahas deskriptor TSS dan artinya secara lebih rinci ketika kita membahas multithreading. Tabel GDT saya sekarang terlihat seperti ini:


 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), ]; 

Dan di sini adalah inisialisasi, yang saya bicarakan di atas. Alamat dan ukuran pemuatan GDT dilakukan melalui struktur terpisah, yang hanya berisi dua bidang. Alamat struktur ini diteruskan ke perintah lgdt. Di register segmen data, muat deskriptor berikut dengan offset 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 

Maka semuanya akan sedikit lebih mudah, tetapi tidak kalah menarik.


Gangguan


Sebenarnya, ini saatnya memberi kita kesempatan untuk berinteraksi dengan inti kita (setidaknya untuk melihat apa yang kita tekan pada keyboard). Untuk melakukan ini, Anda harus menginisialisasi pengontrol interupsi.


Penyimpangan liris tentang gaya kode.


Berkat upaya komunitas dan khususnya Philip Opperman, konvensi panggilan interupsi x86 telah ditambahkan ke Rust, yang memungkinkan Anda untuk menulis penangan interupsi yang menjalankan iret. Namun, saya secara sadar memutuskan untuk tidak pergi rute ini, karena saya memutuskan untuk memisahkan assembler dan Rust ke file yang berbeda, dan karena itu berfungsi. Ya, saya menggunakan memori tumpukan secara tidak masuk akal, saya menyadari hal ini, tetapi rasanya masih enak. Penangan interupsi saya ditulis dalam assembler dan melakukan tepat satu hal: mereka memanggil penangan interupsi yang hampir sama yang ditulis dalam Rust. Terimalah fakta ini dan bersikap sabar.


Secara umum, proses inisialisasi interupsi mirip dengan menginisialisasi GDT, tetapi lebih mudah dimengerti. Di sisi lain, Anda memerlukan banyak kode seragam. Para pengembang Redox OS membuat keputusan yang indah, menggunakan semua kesenangan bahasa, tetapi saya pergi "di dahi" dan memutuskan untuk mengizinkan duplikasi kode.


Menurut konvensi x86, kami memiliki interupsi, tetapi ada situasi luar biasa. Dalam konteks ini, pengaturan untuk kami praktis sama. Satu-satunya perbedaan adalah bahwa ketika pengecualian dilemparkan, tumpukan mungkin berisi informasi tambahan. Sebagai contoh, saya menggunakannya untuk menangani kekurangan halaman ketika bekerja dengan banyak (tetapi semuanya memiliki waktu). Kedua interupsi dan pengecualian diproses dari tabel yang sama, yang harus Anda dan saya isi. Hal ini juga diperlukan untuk memprogram PIC (Programmable Interrupt Controller). Ada juga APIC, tapi saya belum menemukan jawabannya.


Bekerja dengan PIC, saya tidak akan memberikan banyak komentar, karena ada banyak contoh di jaringan untuk bekerja dengannya. Saya akan mulai dengan penangan di assembler. Semuanya benar-benar identik, jadi saya akan menghapus kode untuk spoiler.


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 

Seperti yang Anda lihat, semua panggilan ke fungsi Rust dimulai dengan awalan "k" - untuk perbedaan dan kenyamanan. Penanganan pengecualian sama persis. Untuk fungsi assembler, awalan "e" dipilih, untuk Rust, "k". Penangan Kesalahan Halaman berbeda, tetapi tentang hal itu - dalam catatan tentang manajemen memori.


Pengecualian
 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 

Kami menyatakan assembler handler:


 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(); } 

Kami mendefinisikan penangan karat yang kami sebut di atas. Harap dicatat bahwa untuk mengganggu keyboard, saya cukup menampilkan kode yang diterima, yang saya dapatkan dari port 0x60 - ini adalah cara keyboard bekerja dalam mode paling sederhana. Di masa depan, ini berubah menjadi pengemudi yang penuh, saya harap. Setelah setiap interupsi, Anda perlu mengeluarkan sinyal pada akhir pemrosesan 0x20 ke controller, ini penting! Jika tidak, Anda tidak akan mendapatkan lebih banyak interupsi.


 #[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); } ... 

Inisialisasi IDT dan PIC. Tentang PIC dan remappingnya, saya menemukan sejumlah besar tutorial dengan berbagai tingkat detail, dimulai dengan OSDev dan berakhir dengan situs amatir. Karena prosedur pemrograman beroperasi dengan urutan operasi konstan dan perintah konstan, saya akan memberikan kode ini tanpa penjelasan lebih lanjut. , 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 

Kata penutup


, , . setup_pd, . , , , .


- GitLab .


Terima kasih atas perhatian anda!


UPD: 3

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


All Articles