Sistem operasi pada Rust. Memori Halaman: Tingkat Lanjut

Artikel ini menjelaskan bagaimana kernel sistem operasi dapat mengakses frame memori fisik. Kami akan mempelajari fungsi untuk mengubah alamat virtual menjadi alamat fisik. Kami juga akan mencari cara membuat pemetaan baru di tabel halaman.

Blog ini diposting di GitHub . Jika Anda memiliki pertanyaan atau masalah, buka tiket terkait di sana. Semua sumber untuk artikel ada di sini .

Pendahuluan


Dari artikel terakhir, kami belajar tentang prinsip-prinsip memori paging dan bagaimana tabel halaman empat tingkat pada x86_64 bekerja. Kami juga menemukan bahwa loader sudah mengatur hierarki tabel halaman untuk kernel kami, jadi kernel tersebut berjalan pada alamat virtual. Ini meningkatkan keamanan, tetapi muncul masalah: bagaimana cara mengakses alamat fisik nyata yang disimpan dalam entri tabel halaman atau CR3 ?

Di bagian pertama artikel, kita akan membahas masalah dan berbagai pendekatan untuk menyelesaikannya. Kemudian kami menerapkan fungsi yang menyelinap melalui hierarki tabel halaman untuk mengubah alamat virtual menjadi yang fisik. Akhirnya, pelajari cara membuat pemetaan baru dalam tabel halaman dan menemukan bingkai memori yang tidak digunakan untuk membuat tabel baru.

Pembaruan Ketergantungan


Untuk bekerja, Anda perlu x86_64 versi 0.4.0 atau yang lebih baru. Perbarui ketergantungan dalam Cargo.toml kami:

 [dependencies] x86_64 = "0.4.0" # or later 

Akses ke tabel halaman


Mengakses tabel halaman dari kernel tidak semudah kelihatannya. Untuk memahami masalah ini, lihat hierarki tabel empat tingkat dari artikel sebelumnya:



Yang penting adalah bahwa setiap entri halaman menyimpan alamat fisik dari tabel berikut. Ini menghindari terjemahan dari alamat-alamat ini, yang mengurangi kinerja dan dengan mudah mengarah ke loop tanpa akhir.

Masalahnya adalah kita tidak dapat secara langsung mengakses alamat fisik dari kernel, karena ia juga bekerja pada alamat virtual. Sebagai contoh, ketika kita pergi ke alamat 4 KiB , kita mendapatkan akses ke alamat virtual 4 KiB , dan bukan ke alamat fisik di mana tabel halaman tingkat 4 disimpan. Jika kita ingin mengakses alamat fisik 4 KiB , maka kita perlu menggunakan beberapa alamat virtual, yang diterjemahkan ke dalamnya.

Oleh karena itu, untuk mengakses frame dari tabel halaman, Anda perlu memetakan beberapa halaman virtual ke frame ini. Ada berbagai cara untuk membuat pemetaan seperti itu.

1. Solusi sederhana adalah tampilan identik dari semua tabel halaman .



Dalam contoh ini, kita melihat tampilan bingkai yang identik. Alamat fisik tabel halaman pada saat yang sama adalah alamat virtual yang valid, sehingga kita dapat dengan mudah mengakses tabel halaman dari semua level, dimulai dengan register CR3.

Namun, pendekatan ini mengacaukan ruang alamat virtual dan membuatnya sulit untuk menemukan area memori bebas yang bersebelahan. Katakanlah kita ingin membuat area memori virtual 1000 KiB pada gambar di atas, misalnya, untuk menampilkan file dalam memori . Kami tidak dapat memulai dengan wilayah 28 KiB , karena terletak pada halaman yang sudah diduduki pada 1004 KiB . Oleh karena itu, Anda harus melihat lebih jauh hingga kami menemukan fragmen besar yang cocok, misalnya, dengan 1008 KiB . Ada masalah fragmentasi yang sama seperti pada memori tersegmentasi.

Selain itu, pembuatan tabel halaman baru jauh lebih rumit, karena kita perlu menemukan bingkai fisik yang halaman yang sesuai belum digunakan. Misalnya, untuk file kami, kami memesan area seluas 1000 KiB memori virtual , mulai dari alamat 1008 KiB . Sekarang kita tidak dapat lagi menggunakan bingkai apa pun dengan alamat fisik antara 1000 KiB dan 2008 KiB , karena tidak dapat ditampilkan secara identik.

2. Pilihan lain adalah untuk menyiarkan tabel halaman hanya sementara ketika Anda perlu mengaksesnya. Untuk perbandingan sementara, tampilan identik hanya dari tabel level pertama diperlukan:



Dalam gambar ini, tabel level 1 mengelola 2 MiB pertama dari ruang alamat virtual. Hal ini dimungkinkan karena akses dilakukan dari register CR3 melalui entri nol pada tabel level 4, 3 dan 2. Catatan dengan indeks 8 menerjemahkan halaman virtual pada 32 KiB menjadi kerangka fisik pada 32 KiB , dengan demikian mengidentifikasi tabel level 1 itu sendiri. Pada gambar ini ditunjukkan oleh panah horizontal.

Dengan menulis ke tabel level 1 yang dipetakan secara identik, kernel kami dapat membuat hingga 511 perbandingan waktu (512 dikurangi catatan yang diperlukan untuk pemetaan identitas). Dalam contoh di atas, kernel cocok dengan catatan nol dari tabel level 1 dengan frame pada 24 KiB . Ini menciptakan pemetaan sementara dari halaman virtual pada 0 KiB ke bingkai fisik tabel level halaman 2 yang ditunjukkan oleh panah bertitik. Sekarang kernel dapat mengakses tabel level 2 dengan menulis ke halaman yang dimulai pada 0 KiB .

Dengan demikian, akses ke bingkai tabel halaman yang sewenang-wenang dengan pemetaan sementara terdiri dari tindakan berikut:

  • Temukan entri gratis di tabel level 1 yang ditampilkan secara identik.
  • Petakan entri ini ke bingkai fisik tabel halaman yang ingin kita akses.
  • Akses bingkai ini melalui halaman virtual yang terkait dengan entri.
  • Atur rekaman kembali ke yang tidak digunakan, sehingga menghapus pemetaan sementara.

Dengan pendekatan ini, ruang alamat virtual tetap bersih, karena 512 halaman virtual yang sama terus digunakan. Kerugiannya adalah beberapa ketidaknyamanan, terutama karena perbandingan baru mungkin memerlukan perubahan beberapa tingkat tabel, yaitu, kita perlu mengulangi proses yang dijelaskan beberapa kali.

3. Meskipun kedua pendekatan di atas bekerja, ada metode ketiga: tabel halaman rekursif . Ini menggabungkan keuntungan dari kedua pendekatan: itu terus-menerus membandingkan semua frame dari tabel halaman, tanpa memerlukan perbandingan sementara, dan juga menjaga halaman yang berdekatan berdampingan, menghindari fragmentasi ruang alamat virtual. Ini adalah metode yang akan kita gunakan.

Tabel Halaman Rekursif


Idenya adalah untuk menerjemahkan beberapa catatan dari tabel tingkat keempat ke dalamnya. Dengan demikian, kami benar-benar memesan bagian dari ruang alamat virtual dan memetakan semua frame tabel saat ini dan masa depan ke ruang ini.

Mari kita lihat contoh untuk memahami bagaimana semua ini bekerja:



Satu-satunya perbedaan dari contoh di awal artikel adalah catatan tambahan dengan indeks 511 di tabel level 4, yang dipetakan ke bingkai fisik 4 KiB , yang terletak di tabel ini sendiri.

Saat CPU mencatat, ini tidak merujuk ke tabel level 3, tetapi lagi-lagi merujuk ke tabel level 4. Ini mirip dengan fungsi rekursif yang memanggil dirinya sendiri. Adalah penting bahwa prosesor mengasumsikan bahwa setiap record di tabel level 4 menunjuk ke tabel level 3, jadi sekarang ia memperlakukan tabel level 4 sebagai tabel level 3. Ini berfungsi karena tabel semua level di x86_64 memiliki struktur yang sama.

Dengan mengikuti rekaman rekursif satu atau lebih kali sebelum memulai konversi yang sebenarnya, kita dapat secara efektif mengurangi jumlah level yang dilalui prosesor. Sebagai contoh, jika kita mengikuti catatan rekursif sekali, dan kemudian pergi ke tabel level 3, prosesor berpikir bahwa tabel level 3 adalah tabel level 2. Selanjutnya, dia menganggap tabel level 2 sebagai tabel level 1, dan tabel level 1 dipetakan bingkai dalam memori fisik. Ini berarti bahwa kita sekarang dapat membaca dan menulis ke tabel level 1 halaman karena prosesor berpikir ini adalah bingkai yang dipetakan. Gambar di bawah ini menunjukkan lima langkah terjemahan ini:



Demikian pula, kita dapat mengikuti entri rekursif dua kali sebelum memulai konversi untuk mengurangi jumlah level yang diteruskan menjadi dua:



Mari kita menjalani prosedur ini langkah demi langkah. Pertama, CPU mengikuti entri rekursif di tabel level 4 dan berpikir bahwa ia telah mencapai tabel level 3. Kemudian ia mengikuti catatan rekursif lagi dan berpikir bahwa ia telah mencapai level 2. Tetapi pada kenyataannya itu masih di level 4. Kemudian CPU pergi ke alamat baru. dan masuk ke tabel level 3, tetapi berpikir itu sudah ada di tabel level 1. Akhirnya, pada titik entri berikutnya di tabel level 2, prosesor berpikir telah mengakses frame memori fisik. Ini memungkinkan kita untuk membaca dan menulis ke tabel level 2.

Tabel level 3 dan 4 juga dapat diakses. Untuk mengakses tabel level 3 kita mengikuti catatan rekursif tiga kali: prosesor berpikir bahwa itu sudah ada di tabel level 1, dan pada langkah berikutnya kita mencapai level 3, yang CPU anggap sebagai bingkai yang dipetakan. Untuk mengakses tabel level 4 itu sendiri, kita cukup mengikuti catatan rekursif empat kali sampai prosesor memproses tabel level 4 itu sendiri sebagai bingkai yang dipetakan (berwarna biru pada gambar di bawah).



Konsep ini sulit dipahami pada awalnya, tetapi dalam praktiknya ia bekerja dengan cukup baik.

Perhitungan Alamat


Jadi, kita dapat mengakses tabel dari semua level dengan mengikuti rekaman rekursif satu atau lebih kali. Karena indeks dalam tabel empat level diturunkan langsung dari alamat virtual, alamat virtual khusus harus dibuat untuk metode ini. Seperti yang kita ingat, indeks tabel halaman diekstraksi dari alamat sebagai berikut:



Misalkan kita ingin mengakses tabel level 1 yang menampilkan halaman tertentu. Seperti yang kita pelajari di atas, Anda harus melalui catatan rekursif sekali, dan kemudian melalui indeks level 4, 3 dan 2. Untuk melakukan ini, kami memindahkan semua blok alamat satu blok ke kanan dan mengatur indeks catatan rekursif ke tempat indeks awal level 4:



Untuk mengakses tabel level 2 halaman ini, kami memindahkan semua blok indeks dua blok ke kanan dan mengatur indeks rekursif ke tempat kedua blok sumber: level 4 dan level 3:



Untuk mengakses tabel level 3, kami melakukan hal yang sama, kami hanya menggeser ke kanan sudah tiga blok alamat.



Akhirnya, untuk mengakses tabel level 4, pindahkan semuanya empat blok ke kanan.



Sekarang Anda dapat menghitung alamat virtual untuk tabel halaman dari keempat level. Kita bahkan dapat menghitung alamat yang secara tepat menunjuk ke entri tabel halaman tertentu dengan mengalikan indeksnya dengan 8, ukuran entri tabel halaman.

Tabel di bawah ini menunjukkan struktur alamat untuk mengakses berbagai jenis bingkai:

Alamat virtual untukStruktur alamat ( oktal )
Halaman0o_SSSSSS_AAA_BBB_CCC_DDD_EEEE
Entri di tabel level 10o_SSSSSS_RRR_AAA_BBB_CCC_DDDD
Entri dalam tabel level 20o_SSSSSS_RRR_RRR_AAA_BBB_CCCC
Entri dalam tabel level 30o_SSSSSS_RRR_RRR_RRR_AAA_BBBB
Entri di tabel level 40o_SSSSSS_RRR_RRR_RRR_RRR_AAAA

Di sini adalah indeks level 4, adalah level 3, adalah level 2, dan DDD adalah indeks level 1 untuk frame yang ditampilkan, EEEE adalah offset-nya. RRR adalah indeks dari catatan rekursif. Indeks (tiga digit) dikonversi menjadi offset (empat digit) dengan mengalikannya dengan 8 (ukuran entri tabel halaman). Dengan offset ini, alamat yang dihasilkan langsung menunjuk ke entri tabel halaman yang sesuai.

SSSS adalah bit ekspansi dari digit yang ditandatangani, yaitu, semuanya adalah salinan dari bit 47. Ini adalah persyaratan khusus untuk alamat yang valid dalam arsitektur x86_64, yang kami bahas dalam artikel sebelumnya .

Alamatnya oktal , karena setiap karakter oktal mewakili tiga bit, yang memungkinkan Anda untuk memisahkan indeks tabel 9-bit pada level yang berbeda. Ini tidak mungkin dalam sistem heksadesimal, di mana setiap karakter mewakili empat bit.

Implementasi


Setelah semua teori ini, kita akhirnya bisa melanjutkan implementasi. Dengan mudah, loader tidak hanya menghasilkan tabel halaman, tetapi juga tampilan rekursif pada catatan terakhir tabel level 4. Loader melakukan ini karena jika tidak akan ada masalah ayam atau telur: kita perlu mengakses tabel level 4 untuk membuat peta rekursif tetapi kami tidak dapat mengaksesnya tanpa tampilan apa pun.

Kami sudah menggunakan pemetaan rekursif ini di akhir artikel sebelumnya untuk mengakses tabel level 4 melalui alamat hard-code 0xffff_ffff_ffff_f000 . Jika kita mengonversi alamat ini menjadi oktal dan membandingkannya dengan tabel di atas, kita akan melihat bahwa alamat tersebut persis sesuai dengan struktur catatan di tabel level 4 dengan RRR = 0o777 , AAAA = 0 dan bit ekstensi tanda 1 :

  struktur: 0o_SSSSSS_RRR_RRR_RRR_RRR_AAAA
 alamat: 0o_177777_777_777_777_777_0000 

Berkat pengetahuan tabel rekursif, sekarang kita dapat membuat alamat virtual untuk mengakses semua tabel aktif. Dan buatlah fungsi siaran.

Terjemahan Alamat


Sebagai langkah pertama, buat fungsi yang mengubah alamat virtual menjadi alamat fisik, melewati hierarki tabel halaman:

 // in src/lib.rs pub mod memory; 

 // in src/memory.rs use x86_64::PhysAddr; use x86_64::structures::paging::PageTable; /// Returns the physical address for the given virtual address, or `None` if the /// virtual address is not mapped. pub fn translate_addr(addr: usize) -> Option<PhysAddr> { // introduce variables for the recursive index and the sign extension bits // TODO: Don't hardcode these values let r = 0o777; // recursive index let sign = 0o177777 << 48; // sign extension // retrieve the page table indices of the address that we want to translate let l4_idx = (addr >> 39) & 0o777; // level 4 index let l3_idx = (addr >> 30) & 0o777; // level 3 index let l2_idx = (addr >> 21) & 0o777; // level 2 index let l1_idx = (addr >> 12) & 0o777; // level 1 index let page_offset = addr & 0o7777; // calculate the table addresses let level_4_table_addr = sign | (r << 39) | (r << 30) | (r << 21) | (r << 12); let level_3_table_addr = sign | (r << 39) | (r << 30) | (r << 21) | (l4_idx << 12); let level_2_table_addr = sign | (r << 39) | (r << 30) | (l4_idx << 21) | (l3_idx << 12); let level_1_table_addr = sign | (r << 39) | (l4_idx << 30) | (l3_idx << 21) | (l2_idx << 12); // check that level 4 entry is mapped let level_4_table = unsafe { &*(level_4_table_addr as *const PageTable) }; if level_4_table[l4_idx].addr().is_null() { return None; } // check that level 3 entry is mapped let level_3_table = unsafe { &*(level_3_table_addr as *const PageTable) }; if level_3_table[l3_idx].addr().is_null() { return None; } // check that level 2 entry is mapped let level_2_table = unsafe { &*(level_2_table_addr as *const PageTable) }; if level_2_table[l2_idx].addr().is_null() { return None; } // check that level 1 entry is mapped and retrieve physical address from it let level_1_table = unsafe { &*(level_1_table_addr as *const PageTable) }; let phys_addr = level_1_table[l1_idx].addr(); if phys_addr.is_null() { return None; } Some(phys_addr + page_offset) } 

Pertama, kami memperkenalkan variabel untuk indeks rekursif (511 = 0o777 ) dan bit ekstensi tanda (masing-masing adalah 1). Kemudian kami menghitung indeks tabel halaman dan offset melalui operasi bitwise, seperti yang ditunjukkan dalam ilustrasi:



Langkah selanjutnya adalah menghitung alamat virtual dari empat tabel halaman, seperti yang dijelaskan pada bagian sebelumnya. Selanjutnya, dalam fungsinya, kami mengonversi masing-masing alamat ini ke tautan PageTable . Ini adalah operasi yang tidak aman karena kompiler tidak dapat mengetahui bahwa alamat ini valid.

Setelah menghitung alamat, kami menggunakan operator pengindeksan untuk melihat catatan di tabel level 4. Jika catatan ini nol, maka tidak ada tabel level 3 untuk catatan level 4. Ini berarti bahwa addr tidak dipetakan ke memori fisik apa pun. Jadi kami mengembalikan None . Kalau tidak, kita tahu bahwa tabel level 3 ada. Kemudian kami ulangi prosedurnya, seperti pada tingkat sebelumnya.

Setelah memeriksa tiga halaman dari level yang lebih tinggi, kita akhirnya bisa membaca catatan dari tabel level 1, yang memberi tahu kita kerangka fisik yang dengannya peta dipetakan. Sebagai langkah terakhir, tambahkan offset halaman ke dalamnya - dan kembalikan alamatnya.

Jika kami tahu pasti bahwa alamat itu dipetakan, kami dapat langsung mengakses tabel level 1 tanpa melihat halaman-halaman dari level yang lebih tinggi. Tetapi karena kita tidak mengetahuinya, pertama-tama kita perlu memeriksa apakah tabel level 1 ada, jika tidak fungsi kita akan mengembalikan kesalahan kegagalan halaman untuk alamat yang tidak cocok.

Coba


Mari kita coba menggunakan fungsi terjemahan untuk alamat virtual di fungsi _start kami:

 // in src/main.rs #[cfg(not(test))] #[no_mangle] pub extern "C" fn _start() -> ! { […] // initialize GDT, IDT, PICS use blog_os::memory::translate_addr; // the identity-mapped vga buffer page println!("0xb8000 -> {:?}", translate_addr(0xb8000)); // some code page println!("0x20010a -> {:?}", translate_addr(0x20010a)); // some stack page println!("0x57ac001ffe48 -> {:?}", translate_addr(0x57ac001ffe48)); println!("It did not crash!"); blog_os::hlt_loop(); } 


Setelah memulai, kami melihat hasil berikut:



Seperti yang diharapkan, alamat 0xb8000 yang terkait dengan pengenal diterjemahkan ke alamat fisik yang sama. Halaman kode dan halaman stack dikonversi ke beberapa alamat fisik yang berubah-ubah, yang tergantung pada bagaimana loader membuat pemetaan awal untuk kernel kami.

RecursivePageTable


x86_64 menyediakan jenis RecursivePageTable yang mengimplementasikan abstraksi aman untuk berbagai operasi tabel halaman. Dengan menggunakan tipe ini, Anda dapat mengimplementasikan fungsi translate_addr secara lebih ringkas:

 // in src/memory.rs use x86_64::structures::paging::{Mapper, Page, PageTable, RecursivePageTable}; use x86_64::{VirtAddr, PhysAddr}; /// Creates a RecursivePageTable instance from the level 4 address. /// /// This function is unsafe because it can break memory safety if an invalid /// address is passed. pub unsafe fn init(level_4_table_addr: usize) -> RecursivePageTable<'static> { let level_4_table_ptr = level_4_table_addr as *mut PageTable; let level_4_table = &mut *level_4_table_ptr; RecursivePageTable::new(level_4_table).unwrap() } /// Returns the physical address for the given virtual address, or `None` if /// the virtual address is not mapped. pub fn translate_addr(addr: u64, recursive_page_table: &RecursivePageTable) -> Option<PhysAddr> { let addr = VirtAddr::new(addr); let page: Page = Page::containing_address(addr); // perform the translation let frame = recursive_page_table.translate_page(page); frame.map(|frame| frame.start_address() + u64::from(addr.page_offset())) } 

Jenis RecursivePageTable sepenuhnya merangkum merangkak tabel halaman tidak aman, sehingga kode unsafe dalam fungsi translate_addr tidak lagi diperlukan. Fungsi init tetap tidak aman karena harus menjamin kebenaran level_4_table_addr diteruskan.

Fungsi _start kami harus diperbarui untuk masuk kembali fungsi sebagai berikut:

 // in src/main.rs #[cfg(not(test))] #[no_mangle] pub extern "C" fn _start() -> ! { […] // initialize GDT, IDT, PICS use blog_os::memory::{self, translate_addr}; const LEVEL_4_TABLE_ADDR: usize = 0o_177777_777_777_777_777_0000; let recursive_page_table = unsafe { memory::init(LEVEL_4_TABLE_ADDR) }; // the identity-mapped vga buffer page println!("0xb8000 -> {:?}", translate_addr(0xb8000, &recursive_page_table)); // some code page println!("0x20010a -> {:?}", translate_addr(0x20010a, &recursive_page_table)); // some stack page println!("0x57ac001ffe48 -> {:?}", translate_addr(0x57ac001ffe48, &recursive_page_table)); println!("It did not crash!"); blog_os::hlt_loop(); } 

Sekarang, alih-alih meneruskan LEVEL_4_TABLE_ADDR untuk translate_addr dan mengakses tabel halaman melalui pointer mentah yang tidak aman, kami meneruskan referensi ke tipe RecursivePageTable . Dengan demikian, kami sekarang memiliki abstraksi yang aman dan semantik kepemilikan yang jelas. Ini memastikan bahwa kami tidak akan dapat secara tidak sengaja mengubah tabel halaman dalam akses bersama, karena mengubahnya memerlukan kepemilikan eksklusif RecursivePageTable .

Fungsi ini memberikan hasil yang sama dengan fungsi terjemahan asli yang ditulis secara manual.

Membuat fitur yang tidak aman lebih aman


memory::initadalah fungsi yang tidak aman: memerlukan blok untuk memanggilnya unsafe, karena penelepon harus menjamin bahwa persyaratan tertentu dipenuhi. Dalam kasus kami, persyaratannya adalah bahwa alamat yang ditransmisikan secara tepat dipetakan ke kerangka fisik dari tabel halaman level 4. Seluruh tubuh dari fungsi yang tidak aman ditempatkan

di blok unsafesehingga semua jenis operasi dilakukan tanpa membuat blok tambahan unsafe. Karena itu, kita tidak perlu blok yang tidak aman untuk dereferencing level_4_table_ptr:

 pub unsafe fn init(level_4_table_addr: usize) -> RecursivePageTable<'static> { let level_4_table_ptr = level_4_table_addr as *mut PageTable; let level_4_table = &mut *level_4_table_ptr; // <- this operation is unsafe RecursivePageTable::new(level_4_table).unwrap() } 

Masalahnya adalah kita tidak segera melihat bagian mana yang tidak aman. Misalnya, tanpa melihat definisi suatu fungsi, RecursivePageTable::new kita tidak dapat mengatakan apakah itu aman atau tidak. Jadi sangat mudah untuk secara tidak sengaja melewatkan beberapa kode yang tidak aman.

Untuk menghindari masalah ini, Anda dapat menambahkan fungsi bawaan yang aman:

 // in src/memory.rs pub unsafe fn init(level_4_table_addr: usize) -> RecursivePageTable<'static> { /// Rust currently treats the whole body of unsafe functions as an unsafe /// block, which makes it difficult to see which operations are unsafe. To /// limit the scope of unsafe we use a safe inner function. fn init_inner(level_4_table_addr: usize) -> RecursivePageTable<'static> { let level_4_table_ptr = level_4_table_addr as *mut PageTable; let level_4_table = unsafe { &mut *level_4_table_ptr }; RecursivePageTable::new(level_4_table).unwrap() } init_inner(level_4_table_addr) } 

Sekarang blok unsafediperlukan lagi untuk dereferencing level_4_table_ptr, dan kami segera melihat bahwa ini adalah satu-satunya operasi yang tidak aman. Rust saat ini memiliki RFC terbuka untuk mengubah properti yang gagal fungsi tidak aman ini.

Buat pemetaan baru


Saat kami membaca tabel halaman dan membuat fungsi konversi, langkah selanjutnya adalah membuat pemetaan baru dalam hierarki tabel halaman.

Kompleksitas operasi ini tergantung pada halaman virtual yang ingin kami tampilkan. Dalam kasus paling sederhana, tabel halaman level 1 sudah ada untuk halaman ini, dan kita hanya perlu membuat satu entri. Dalam kasus yang paling sulit, halaman tersebut berada di area memori yang level 3-nya belum ada, jadi pertama-tama Anda perlu membuat tabel baru level 3, level 2 dan level 1.

Mari kita mulai dengan case sederhana ketika Anda tidak perlu membuat tabel baru. Pemuat dimuat ke megabyte pertama ruang alamat virtual, jadi kami tahu bahwa untuk wilayah ini ada tabel level 1. yang valid. Sebagai contoh kami, kami dapat memilih halaman yang tidak digunakan di area memori ini, misalnya, halaman di alamat 0x1000. Kami menggunakan 0xb8000bingkai buffer teks VGA sebagai bingkai yang diinginkan . Sangat mudah untuk memeriksa cara kerja terjemahan alamat kami.

Kami mengimplementasikannya dalam fungsi baru create_mapingdalam modul memory:

 // in src/memory.rs use x86_64::structures::paging::{FrameAllocator, PhysFrame, Size4KiB}; pub fn create_example_mapping( recursive_page_table: &mut RecursivePageTable, frame_allocator: &mut impl FrameAllocator<Size4KiB>, ) { use x86_64::structures::paging::PageTableFlags as Flags; let page: Page = Page::containing_address(VirtAddr::new(0x1000)); let frame = PhysFrame::containing_address(PhysAddr::new(0xb8000)); let flags = Flags::PRESENT | Flags::WRITABLE; let map_to_result = unsafe { recursive_page_table.map_to(page, frame, flags, frame_allocator) }; map_to_result.expect("map_to failed").flush(); } 

Fungsi menerima referensi yang bisa berubah ke RecursivePageTable(itu akan mengubahnya) dan FrameAllocator, yang dijelaskan di bawah ini. Kemudian menerapkan fungsi map_todalam baki Mapperuntuk memetakan halaman di alamat 0x1000dengan bingkai fisik di alamat 0xb8000. Fungsi ini tidak aman, karena dimungkinkan untuk melanggar keamanan memori dengan argumen yang tidak valid.

Selain argumen pagedan frame, fungsi map_tomembutuhkan dua argumen lagi. Argumen ketiga adalah set bendera untuk tabel halaman. Kami menetapkan bendera yang PRESENTdiperlukan untuk semua entri yang valid dan bendera WRITABLEuntuk kemampuan menulis.

Argumen keempat haruslah beberapa struktur yang mengimplementasikan sifat tersebut FrameAllocator. Argumen ini diperlukan oleh metode.map_tokarena membuat tabel halaman baru mungkin memerlukan bingkai yang tidak digunakan. Implementasi membutuhkan sifat argumen Size4KiB, seperti jenis Pagedan PhysFrameyang universal yang untuk sifat tersebut PageSize, bekerja dengan standar 4 halaman KIB dan dengan halaman yang sangat besar 2 MiB / 1 GiB.

Fungsi ini map_tomungkin gagal, jadi itu kembali Result. Karena ini hanyalah contoh kode yang seharusnya tidak dapat diandalkan, kami cukup menggunakannya expectdengan panik ketika terjadi kesalahan. Jika berhasil, fungsi mengembalikan jenis MapperFlushyang menyediakan cara mudah untuk menghapus halaman yang baru-baru ini cocok dari metode buffer terjemahan terjemahan (TLB) flush. SepertiResult, jenisnya menggunakan atribut #[must_use]dan mengeluarkan peringatan jika kita secara tidak sengaja lupa untuk menerapkannya.

Karena kita tahu bahwa alamat 0x1000itu tidak memerlukan tabel halaman baru, itu FrameAllocatorselalu dapat kembali None. Untuk menguji fungsi, buat ini EmptyFrameAllocator:

 // in src/memory.rs /// A FrameAllocator that always returns `None`. pub struct EmptyFrameAllocator; impl FrameAllocator<Size4KiB> for EmptyFrameAllocator { fn allocate_frame(&mut self) -> Option<PhysFrame> { None } } 

(Jika metode 'kesalahan allocate_framebukan anggota sifat FrameAllocator' muncul , Anda perlu meningkatkan x86_64ke versi 0.4.0.)

Sekarang kita dapat menguji fungsi terjemahan baru:

 // in src/main.rs #[cfg(not(test))] #[no_mangle] pub extern "C" fn _start() -> ! { […] // initialize GDT, IDT, PICS use blog_os::memory::{create_example_mapping, EmptyFrameAllocator}; const LEVEL_4_TABLE_ADDR: usize = 0o_177777_777_777_777_777_0000; let mut recursive_page_table = unsafe { memory::init(LEVEL_4_TABLE_ADDR) }; create_example_mapping(&mut recursive_page_table, &mut EmptyFrameAllocator); unsafe { (0x1900 as *mut u64).write_volatile(0xf021f077f065f04e)}; println!("It did not crash!"); blog_os::hlt_loop(); } 

Pertama, kami membuat pemetaan untuk halaman di alamat 0x1000, memanggil fungsi create_example_mappingdengan tautan yang dapat diubah ke instance RecursivePageTable. Ini menerjemahkan halaman 0x1000menjadi buffer teks VGA, jadi kita akan melihat beberapa hasil di layar.

Lalu kami menulis nilai di halaman ini 0xf021f077f065f04e, yang sesuai dengan baris "Baru!" pada latar belakang putih. Hanya saja, tidak perlu menuliskan nilai ini segera ke atas halaman 0x1000, karena baris teratas akan bergerak berikutnya dari layar println, dan menuliskannya di offset 0x900yang terletak kira-kira di tengah layar. Seperti yang kita ketahui dari artikel "Mode Teks VGA" , menulis ke buffer VGA harus volatile, jadi kami menggunakan metode ini write_volatile.

Ketika kami menjalankannya di QEMU, kami melihat ini:



Tulisan di layar.

Kode berfungsi karena sudah ada tabel level 1 untuk menampilkan halaman 0x1000. Jika kami mencoba menerjemahkan halaman yang tabelnya belum ada, fungsinya map_toakan menghasilkan kesalahan, karena ia akan mencoba memilih bingkai dari untuk membuat tabel halaman baru EmptyFrameAllocator. Kami akan melihat ini jika kami mencoba menerjemahkan halaman 0xdeadbeaf000alih-alih 0x1000:

 // in src/memory.rs pub fn create_example_mapping(…) { […] let page: Page = Page::containing_address(VirtAddr::new(0xdeadbeaf000)); […] } // in src/main.rs #[no_mangle] pub extern "C" fn _start() -> ! { […] unsafe { (0xdeadbeaf900 as *mut u64).write_volatile(0xf021f077f065f04e)}; […] } 

Saat memulai, panik dimulai dengan pesan kesalahan berikut:

 panik di 'map_to gagal: FrameAllocationFailed', /.../result.rs:999haps 

Untuk menampilkan halaman yang belum memiliki tabel level 1 halaman, Anda harus membuat yang benar FrameAllocator. Tapi bagaimana Anda tahu frame mana yang gratis dan berapa banyak memori fisik yang tersedia?

Informasi boot


Komputer yang berbeda memiliki jumlah memori fisik yang berbeda dan area yang berbeda disediakan oleh perangkat seperti VGA berbeda. Hanya firmware BIOS atau UEFI yang tahu persis area memori mana yang dapat digunakan dan mana yang dicadangkan. Kedua standar firmware menyediakan fungsi untuk mendapatkan kartu alokasi memori, tetapi hanya dapat dipanggil pada awal pengunduhan. Karena itu, bootloader kami telah meminta informasi ini (dan lainnya) dari BIOS.

Untuk meneruskan informasi ke kernel OS, loader sebagai argumen saat memanggil fungsi _startmemberikan tautan ke struktur informasi boot. Tambahkan argumen ini ke fungsi kami:

 // in src/main.rs use bootloader::bootinfo::BootInfo; #[cfg(not(test))] #[no_mangle] pub extern "C" fn _start(boot_info: &'static BootInfo) -> ! { // new argument […] } 

Struktur ini BootInfomasih difinalisasi, jadi jangan kaget ketika crash ketika memutakhirkan ke versi bootloader masa depan yang tidak kompatibel dengan semver . Dia saat ini memiliki tiga bidang p4_table_addr, memory_mapdan package:

  • Bidang ini p4_table_addrberisi alamat virtual rekursif dari tabel halaman level 4. Berkat ini, Anda tidak perlu mendaftarkan alamat tersebut dengan keras 0o_177777_777_777_777_777_0000.
  • Bidang memory_mapini paling menarik, karena berisi daftar semua area memori dan jenisnya (tidak digunakan, dicadangkan, atau yang lain).
  • Field packageadalah fungsi saat ini untuk mengaitkan data tambahan dengan loader. Implementasinya belum selesai, jadi kita bisa mengabaikannya untuk saat ini.

Sebelum menggunakan bidang memory_mapuntuk membuat yang benar FrameAllocator, kami ingin menjamin jenis argumen yang benar boot_info.

Makro entry_point


Karena _startini disebut secara eksternal, tanda tangan dari fungsi tidak dicentang. Ini berarti bahwa argumen arbitrer tidak akan menyebabkan kesalahan kompilasi, tetapi dapat menyebabkan crash atau perilaku runtime yang tidak ditentukan.

Untuk memverifikasi tanda tangan, kotak bootloaderuntuk mendefinisikan fungsi Karat sebagai titik masuk menggunakan makro entry_pointdengan tipe yang divalidasi. Kami menulis ulang fungsi kami untuk makro ini:

 // in src/main.rs use bootloader::{bootinfo::BootInfo, entry_point}; entry_point!(kernel_main); #[cfg(not(test))] fn kernel_main(boot_info: &'static BootInfo) -> ! { […] // initialize GDT, IDT, PICS let mut recursive_page_table = unsafe { memory::init(boot_info.p4_table_addr as usize) }; […] // create and test example mapping println!("It did not crash!"); blog_os::hlt_loop(); } 

Untuk titik masuk, Anda tidak perlu lagi menggunakan extern "C"atau no_mangle, karena makro menetapkan titik masuk nyata tingkat rendah _start. Fungsi kernel_mainsekarang telah menjadi fungsi Rust yang benar-benar normal, sehingga kita dapat memilih nama arbitrer untuk itu. Penting bahwa itu sudah diketik, sehingga kesalahan kompilasi terjadi jika Anda mengubah tanda tangan fungsi, misalnya, dengan menambahkan argumen atau mengubah tipenya.

Perhatikan bahwa sekarang kami mengirim ke memory::initalamat hard-coded, tetapi boot_info.p4_table_addr. Dengan demikian, kode ini akan berfungsi bahkan jika versi bootloader mendatang memilih entri lain di tabel tabel level 4 halaman untuk tampilan rekursif.

Pemilihan Bingkai


Sekarang, berkat informasi dari BIOS, kami memiliki akses ke kartu alokasi memori, sehingga Anda dapat membuat distributor bingkai normal. Mari kita mulai dengan kerangka umum:

 // in src/memory.rs pub struct BootInfoFrameAllocator<I> where I: Iterator<Item = PhysFrame> { frames: I, } impl<I> FrameAllocator<Size4KiB> for BootInfoFrameAllocator<I> where I: Iterator<Item = PhysFrame> { fn allocate_frame(&mut self) -> Option<PhysFrame> { self.frames.next() } } 

Bidang ini framesdiinisialisasi oleh iterator bingkai sewenang-wenang . Ini memungkinkan Anda untuk mendelegasikan panggilan allocke metode Iterator :: next .

Inisialisasi BootInfoFrameAllocatorterjadi pada fungsi baru init_frame_allocator:

 // in src/memory.rs use bootloader::bootinfo::{MemoryMap, MemoryRegionType}; /// Create a FrameAllocator from the passed memory map pub fn init_frame_allocator( memory_map: &'static MemoryMap, ) -> BootInfoFrameAllocator<impl Iterator<Item = PhysFrame>> { // get usable regions from memory map let regions = memory_map .iter() .filter(|r| r.region_type == MemoryRegionType::Usable); // map each region to its address range let addr_ranges = regions.map(|r| r.range.start_addr()..r.range.end_addr()); // transform to an iterator of frame start addresses let frame_addresses = addr_ranges.flat_map(|r| r.into_iter().step_by(4096)); // create `PhysFrame` types from the start addresses let frames = frame_addresses.map(|addr| { PhysFrame::containing_address(PhysAddr::new(addr)) }); BootInfoFrameAllocator { frames } } 

Fungsi ini, menggunakan kombinator, mengubah peta alokasi memori asli menjadi iterator dari frame fisik yang digunakan:

  • iter MemoryRegion . filter , . , , (, ) , InUse . , , - .
  • map range Rust .
  • Langkah ketiga adalah yang paling sulit: kami mengubah setiap rentang menjadi iterator menggunakan metode into_iter, dan kemudian memilih setiap alamat ke-4096 step_by. Karena ukuran halaman adalah 4096 byte (4 KiB), kami mendapatkan alamat awal setiap frame. Laman pemuat meluruskan semua area memori yang digunakan, jadi kami tidak memerlukan kode pelurusan atau pembulatan. Mengganti mapdengan flat_map, kita Iterator<Item = u64>malah mendapatkan Iterator<Item = Iterator<Item = u64>>.
  • Pada tahap akhir, kami akan mengonversi alamat mulai menjadi tipe PhysFrameuntuk membangun yang diperlukan Iterator<Item = PhysFrame>. Kemudian gunakan iterator ini untuk membuat dan mengembalikan yang baru BootInfoFrameAllocator.

Sekarang kita dapat mengubah fungsi kita kernel_mainsehingga melewati instance BootInfoFrameAllocatorsebagai gantinya EmptyFrameAllocator:

 // in src/main.rs #[cfg(not(test))] fn kernel_main(boot_info: &'static BootInfo) -> ! { […] // initialize GDT, IDT, PICS use x86_64::structures::paging::{PageTable, RecursivePageTable}; let mut recursive_page_table = unsafe { memory::init(boot_info.p4_table_addr as usize) }; // new let mut frame_allocator = memory::init_frame_allocator(&boot_info.memory_map); blog_os::memory::create_mapping(&mut recursive_page_table, &mut frame_allocator); unsafe { (0xdeadbeaf900 as *mut u64).write_volatile(0xf021f077f065f04e)}; println!("It did not crash!"); blog_os::hlt_loop(); } 

Sekarang terjemahan alamat berhasil - dan kita kembali melihat pesan hitam putih β€œBaru!” Di layar .Di belakang layar, metode ini map_tomembuat tabel halaman yang hilang sebagai berikut:

  • Mengekstrak bingkai yang tidak digunakan dari frame_allocator.
  • Cocok dengan entri tabel tingkat atas dengan bingkai ini. Bingkai sekarang dapat diakses melalui tabel halaman rekursif.
  • Nol bingkai untuk membuat tabel halaman baru yang kosong.
  • Pergi ke tabel level berikutnya.

Meskipun fungsi kami create_mapinghanyalah sebuah contoh, kami sekarang dapat membuat pemetaan baru untuk halaman sewenang-wenang. Ini sangat berguna ketika mengalokasikan memori dan mengimplementasikan multithreading di artikel mendatang.

Ringkasan


Dalam artikel ini, Anda belajar cara menggunakan tabel rekursif level 4 untuk menerjemahkan semua frame ke alamat virtual yang dapat dihitung. Kami menggunakan metode ini untuk mengimplementasikan fungsi terjemahan alamat dan membuat pemetaan baru dalam tabel halaman.

Kami melihat bahwa membuat pemetaan baru membutuhkan bingkai yang tidak digunakan untuk tabel baru. Frame distributor seperti itu dapat diimplementasikan berdasarkan informasi dari BIOS yang diberikan oleh bootloader ke kernel kami.

Apa selanjutnya


Pada artikel selanjutnya, kita akan membuat area memori heap untuk kernel kita, yang akan memungkinkan kita untuk mengalokasikan memori dan menggunakan berbagai jenis koleksi .

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


All Articles