OS1: kernel primitif di Rust untuk x86

Saya memutuskan untuk menulis sebuah artikel, dan jika mungkin, kemudian serangkaian artikel untuk berbagi pengalaman penelitian independen saya baik dari perangkat B86 Bone x86 dan organisasi sistem operasi. Saat ini, hack saya bahkan tidak dapat disebut sistem operasi - ini adalah kernel kecil yang dapat melakukan booting dari Multiboot (GRUB), mengelola memori nyata dan virtual, dan juga melakukan beberapa fungsi yang tidak berguna dalam mode multitasking pada satu prosesor.


Selama pengembangan, saya tidak menetapkan diri saya tujuan untuk menulis Linux baru (meskipun, saya akui, saya memimpikannya sekitar 5 tahun yang lalu) atau membuat seseorang terkesan, jadi saya meminta Anda untuk tidak terlihat lagi terutama terkesan. Apa yang benar-benar ingin saya lakukan adalah mencari tahu bagaimana arsitektur i386 bekerja pada tingkat paling dasar, dan bagaimana tepatnya sistem operasi melakukan keajaiban mereka, dan menggali Rust yang hype.


Dalam catatan saya, saya akan mencoba untuk membagikan tidak hanya teks sumber (mereka dapat ditemukan di GitLab) dan teori telanjang (dapat ditemukan di banyak sumber), tetapi juga jalan yang saya tempuh untuk menemukan jawaban yang tidak jelas. Khususnya, dalam artikel ini saya akan berbicara tentang membangun file kernel, memuatnya, dan menginisialisasi itu .


Tujuan saya adalah menyusun informasi di kepala saya, serta membantu mereka yang mengikuti jalan yang sama. Saya mengerti bahwa materi dan blog yang serupa sudah ada di jaringan, tetapi untuk sampai pada situasi saya saat ini, saya harus mengumpulkan mereka untuk waktu yang lama. Semua sumber (dalam hal apa pun, yang saya ingat), saya akan bagikan sekarang.


Sastra dan Sumber


Tentu saja, saya mendapatkan sebagian besar dari sumber daya OSDev yang sangat baik, baik dari wiki dan dari forum. Kedua, saya akan memberi nama Philip Opperman dengan blognya - banyak informasi tentang sekelompok Rust dan besi.


Beberapa poin dimata-matai dalam kernel Linux, Minix bukan tanpa bantuan literatur khusus, seperti buku Tanenbaum " Sistem Operasi." Desain dan implementasi, "buku Robert Love" The Kernel Linux. Deskripsi proses pengembangan . " Pertanyaan sulit tentang pengorganisasian arsitektur x86 diselesaikan dengan menggunakan manual “ Manual Pengembang Perangkat Lunak Arsitektur Intel 64 dan IA-32 Volume 3 (3A, 3B, 3C & 3D): Panduan Pemrograman Sistem ”. Dalam memahami format binari, tata letak adalah panduan untuk ld, llvm, nm, nasm, make.
UPD Terima kasih kepada CoreTeamTech karena mengingatkan saya pada sistem Redox OS yang luar biasa. Saya tidak keluar dari sumbernya . Sayangnya, sistem GitLab resmi tidak tersedia dari IP Rusia, sehingga Anda dapat melihat GitHub .


Pendahuluan lain


Saya menyadari bahwa saya bukan programmer yang baik di Rust, apalagi, ini adalah proyek pertama saya dalam bahasa ini (bukan cara terbaik untuk mulai berkencan, kan?). Oleh karena itu, implementasinya mungkin kelihatan benar-benar salah bagi Anda - sebelumnya saya ingin meminta keringanan hukuman terhadap kode saya dan saya akan dengan senang hati memberikan komentar dan saran. Jika seorang pembaca yang terhormat dapat memberi tahu saya di mana dan bagaimana melanjutkan, saya juga akan sangat berterima kasih. Beberapa fragmen kode dapat disalin dari tutorial sebagaimana adanya dan sedikit dimodifikasi, tetapi saya akan mencoba memberikan penjelasan sejelas mungkin ke bagian tersebut sehingga Anda tidak memiliki pertanyaan yang sama seperti yang saya miliki ketika menguraikannya. Saya juga tidak berpura-pura menggunakan pendekatan yang tepat dalam desain, jadi jika manajer memori saya membuat Anda ingin menulis komentar yang marah, saya mengerti mengapa.


Toolkit


Jadi, saya akan mulai dengan menyelam ke alat pengembangan yang saya gunakan. Sebagai lingkungan, saya memilih editor VS Code yang bagus dan nyaman dengan plugins untuk Rust dan GDB debugger. VS Code kadang-kadang tidak begitu baik dengan RLS, terutama ketika mendefinisikan ulang di direktori tertentu, jadi setelah setiap pembaruan Rust setiap malam saya harus menginstal ulang RLS.


Rust dipilih karena beberapa alasan. Pertama, popularitasnya yang meningkat dan filosofi yang menyenangkan. Kedua, kemampuannya untuk bekerja dengan level rendah tetapi dengan kemungkinan lebih rendah "menembak dirinya sendiri di kaki". Ketiga, sebagai pecinta Jawa dan Maven, saya sangat kecanduan untuk membangun sistem dan manajemen ketergantungan, dan kargo sudah dibangun ke dalam bahasa toolchain. Keempat, saya hanya menginginkan sesuatu yang baru, bukan seperti C.


Untuk kode tingkat rendah, saya menggunakan NASM Saya merasa yakin dengan sintaks Intel, dan saya juga nyaman bekerja dengan arahannya. Saya sengaja meninggalkan insert assembler di Rust untuk secara eksplisit memisahkan pekerjaan dengan besi dan logika tingkat tinggi.
Membuat dan penghubung dari pasokan LLVM LLD (sebagai penghubung yang lebih cepat dan lebih baik) digunakan sebagai perakitan dan tata letak umum - ini masalah selera. Itu mungkin dilakukan dengan membuat skrip untuk kargo.


Qemu digunakan untuk meluncurkan - Saya suka kecepatannya, mode interaktif dan kemampuan untuk menghubungkan GDB. Untuk mem-boot dan segera memiliki semua informasi perangkat keras - tentu saja GRUB (Legacy lebih mudah untuk mengatur tajuk, jadi bawa).


Tautan dan tata letak


Cukup aneh, bagi saya itu ternyata menjadi salah satu topik yang paling sulit. Sangat sulit untuk menyadari setelah uji coba panjang dengan register segmen x86 bahwa segmen dan bagian tidak sama. Dalam pemrograman untuk lingkungan yang ada, tidak perlu memikirkan bagaimana menempatkan program dalam memori - untuk setiap platform dan format linker sudah memiliki resep yang sudah jadi, jadi tidak perlu menulis skrip linker.


Untuk bare iron, sebaliknya, perlu menunjukkan bagaimana menempatkan dan mengatasi kode program dalam memori. Di sini saya ingin menekankan bahwa kita berbicara tentang alamat linear (virtual) menggunakan mekanisme halaman. OS1 menggunakan mekanisme halaman, tetapi saya akan membahasnya secara terpisah di bagian artikel yang sesuai.


Logis, linier, virtual, fisik ...

Alamat logis, linier, virtual, fisik. Saya mematahkan kepala saya pada pertanyaan ini, jadi untuk detail saya ingin membahas artikel yang sangat baik ini


Untuk sistem operasi yang menggunakan paging, dalam lingkungan 32-bit, setiap tugas memiliki 4 GB ruang memori yang dapat dialamatkan, bahkan jika Anda memiliki 128 MB RAM yang diinstal. Ini terjadi hanya karena pengaturan memori halaman, tidak adanya halaman dalam memori utama ditangani sesuai.


Namun, pada kenyataannya, aplikasi biasanya tersedia sedikit kurang dari 4 GB. Ini karena OS harus menangani interupsi, panggilan sistem, yang berarti bahwa setidaknya penangan mereka harus berada dalam ruang alamat ini. Kita dihadapkan dengan pertanyaan: di mana tepatnya dalam 4 GB ini haruskah alamat kernel ditempatkan sehingga program dapat bekerja dengan benar?


Dalam dunia program modern, konsep seperti itu digunakan: setiap tugas percaya bahwa ia berkuasa atas prosesor dan merupakan satu-satunya program yang berjalan di komputer (pada tahap ini kita tidak berbicara tentang komunikasi antar proses). Jika Anda melihat persis bagaimana kompiler mengumpulkan program pada tahap penautan, ternyata mereka mulai dengan alamat linear nol atau mendekati nol. Ini berarti bahwa jika citra kernel menempati ruang memori mendekati nol, program yang dirakit dengan cara ini tidak dapat dijalankan, instruksi jmp apa pun dalam program akan menyebabkan entri ke dalam memori yang diproteksi kernel dan kesalahan perlindungan. Oleh karena itu, jika kita ingin menggunakan tidak hanya program-program yang ditulis sendiri di masa depan, masuk akal untuk memberikan aplikasi sebanyak mungkin memori mendekati nol, dan menempatkan gambar kernel lebih tinggi.


Konsep ini disebut Higher-half kernel (di sini saya merujuk Anda ke osdev.org, jika Anda menginginkan informasi terkait). Memori mana yang dipilih hanya bergantung pada selera Anda. 512 MB sudah cukup untuk seseorang, tetapi saya memutuskan untuk mengambil sendiri 1 GB, jadi kernel saya berada pada 3 GB + 1 MB (+ 1 MB diperlukan untuk mematuhi batas memori yang lebih rendah-lebih tinggi, GRUB memuat kami ke dalam memori fisik setelah 1 MB) .
Penting juga bagi kita untuk menentukan titik masuk ke file yang dapat dieksekusi. Untuk executable saya, ini akan menjadi fungsi _loader yang ditulis dalam assembler, yang akan saya bahas lebih detail di bagian selanjutnya.


Tentang titik masuk

Tahukah Anda bahwa Anda telah berbohong sepanjang hidup Anda tentang fakta bahwa main () adalah titik masuk ke program? Bahkan, main () adalah konvensi bahasa C dan bahasa yang dihasilkan olehnya. Jika Anda menggali, ternyata seperti berikut ini.


Pertama, setiap platform memiliki spesifikasi dan nama titik masuknya sendiri: untuk linux biasanya _start, untuk Windows itu adalah mainCRTStartup. Kedua, titik-titik ini dapat didefinisikan ulang, tetapi kemudian tidak akan berhasil menggunakan kesenangan libc. Ketiga, kompiler menyediakan titik entri ini secara default dan mereka ada di file crt0..crtN (CRT - C RunTime, N - jumlah argumen utama).


Sebenarnya, apa yang dilakukan oleh kompiler seperti gcc atau vc - mereka memilih skrip tautan khusus platform yang mendefinisikan titik entri standar, memilih file objek yang diinginkan dengan fungsi inisialisasi C yang siap pakai dan memanggil fungsi utama dan menghubungkan ke output dalam bentuk file format yang diinginkan dengan titik entri standar.


Jadi, untuk tujuan kita, titik masuk standar dan inisialisasi CRT harus dimatikan, karena kita tidak memiliki apa-apa selain besi kosong.


Apa lagi yang perlu Anda ketahui untuk menautkan? Bagaimana bagian data (.rodata, .data), variabel yang tidak diinisialisasi (.bss, umum) akan ditemukan, dan juga ingat bahwa GRUB memerlukan lokasi header multiboot dalam 8 KB pertama biner.


Jadi sekarang kita bisa menulis skrip linker!


ENTRY(_loader) OUTPUT_FORMAT(elf32-i386) SECTIONS { . = 0xC0100000; .text ALIGN(4K) : AT(ADDR(.text) - 0xC0000000) { *(.multiboot1) *(.multiboot2) *(.text) } .rodata ALIGN(4K) : AT(ADDR(.rodata) - 0xC0000000) { *(.rodata*) } .data ALIGN (4K) : AT(ADDR(.data) - 0xC0000000) { *(.data) } .bss : AT(ADDR(.bss) - 0xC0000000) { _sbss = .; *(COMMON) *(.bss) _ebss = .; } } 

Unduh setelah GRUB


Seperti disebutkan di atas, spesifikasi Multiboot mengharuskan header berada di 8 KB pertama dari gambar boot. Spesifikasi lengkapnya dapat dilihat di sini , tetapi saya hanya akan membahas detail yang menarik.


  • Penjajaran 32 bit (4 byte) harus dihormati
  • Harus ada angka ajaib 0x1BADB002
  • Penting untuk memberi tahu multibooter informasi apa yang ingin kami terima dan bagaimana menempatkan modul (dalam kasus saya, saya ingin modul kernel disejajarkan pada halaman 4 KB, dan juga mendapatkan kartu memori untuk menghemat waktu dan tenaga saya sendiri)
  • Berikan checksum (checksum + angka ajaib + bendera harus memberi nol)

 MB1_MODULEALIGN equ 1<<0 MB1_MEMINFO equ 1<<1 MB1_FLAGS equ MB1_MODULEALIGN | MB1_MEMINFO MB1_MAGIC equ 0x1BADB002 MB1_CHECKSUM equ -(MB1_MAGIC + MB1_FLAGS) section .multiboot1 align 4 dd MB1_MAGIC dd MB1_FLAGS dd MB1_CHECKSUM 

Setelah boot, Multiboot menjamin beberapa kondisi yang harus kita pertimbangkan.


  • Register EAX berisi angka ajaib 0x2BADB002, yang mengatakan bahwa unduhan berhasil
  • Register EBX berisi alamat fisik struktur dengan informasi tentang hasil pemuatan (kami akan membicarakannya nanti)
  • Prosesor dalam mode terproteksi, memori halaman dimatikan, register segmen dan tumpukan berada dalam keadaan tidak ditentukan (untuk kami), GRUB menggunakannya untuk kebutuhannya dan perlu didefinisikan ulang sesegera mungkin.

Hal pertama yang perlu kita lakukan adalah mengaktifkan paging, menyetel stack, dan akhirnya mentransfer kontrol ke kode Rust tingkat tinggi.
Saya tidak akan membahas secara terperinci tentang organisasi memori, Direktori Halaman dan Tabel Halaman, karena artikel-artikel yang bagus telah ditulis tentang hal ini ( salah satunya ). Hal utama yang ingin saya bagikan adalah halaman bukan segmen! Tolong jangan ulangi kesalahan saya dan jangan memuat alamat tabel halaman di GDTR! Untuk tabel halaman adalah CR3! Halaman dapat memiliki ukuran yang berbeda dalam arsitektur yang berbeda, untuk kesederhanaan pekerjaan (hanya memiliki satu halaman tabel), saya memilih ukuran 4 MB karena dimasukkannya PSE.


Jadi, kami ingin mengaktifkan memori halaman virtual. Untuk melakukan ini, kita perlu tabel halaman, dan alamat fisiknya, dimuat ke CR3. Pada saat yang sama, file biner kami ditautkan untuk berfungsi di ruang alamat virtual dengan offset 3 GB. Ini berarti bahwa semua alamat variabel dan label memiliki offset 3 GB. Tabel halaman hanyalah sebuah array di mana alamat halaman berisi alamat aslinya, disejajarkan dengan ukuran halaman, serta bendera akses dan status. Karena saya menggunakan halaman 4 MB, saya hanya perlu satu tabel halaman PD dengan 1024 entri:


 section .data align 0x1000 BootPageDirectory: dd 0x00000083 times (KERNEL_PAGE_NUMBER - 1) dd 0 dd 0x00000083 times (1024 - KERNEL_PAGE_NUMBER - 1) dd 0 

Apa yang ada di meja?


  1. Halaman pertama harus mengarah ke bagian kode saat ini (0-4 MB memori fisik), karena semua alamat dalam prosesor adalah fisik dan terjemahan ke virtual belum dilakukan. Tidak adanya deskriptor halaman ini akan menyebabkan crash langsung, karena prosesor tidak akan dapat mengambil instruksi berikutnya setelah menyalakan halaman. Bendera: bit 0 - tabel ada, bit 1 - halaman ditulis, bit 7 - ukuran halaman 4 MB. Setelah menyalakan halaman, catatan diatur ulang.
  2. Lewati hingga 3 GB - nol memastikan bahwa halaman tersebut tidak ada dalam memori
  3. Tanda 3 GB adalah inti kami dalam memori virtual, merujuk 0 pada memori fisik. Setelah menyalakan halaman, kami akan bekerja di sini. Bendera mirip dengan catatan pertama.
  4. Lewati hingga 4 GB.

Jadi, kami menyatakan tabel dan sekarang kami ingin memuat alamat fisiknya di CR3. Jangan lupa tentang offset alamat 3 GB pada tahap penautan. Mencoba memuat alamat seperti itu akan mengirim kami ke alamat sebenarnya sebesar 3 GB + offset variabel dan mengakibatkan kerusakan segera. Oleh karena itu, kami mengambil alamat BootPageDirectory dan mengurangi 3 GB darinya, letakkan di CR3. Kami mengaktifkan PSE di register CR4, aktifkan pekerjaan dengan halaman di register CR0:


  mov ecx, (BootPageDirectory - KERNEL_VIRTUAL_BASE) mov cr3, ecx mov ecx, cr4 or ecx, 0x00000010 mov cr4, ecx mov ecx, cr0 or ecx, 0x80000000 mov cr0, ecx 

Sejauh ini, semuanya berjalan dengan baik, tetapi segera setelah kami mengatur ulang halaman pertama untuk akhirnya pergi ke bagian atas 3 GB, semuanya akan runtuh, karena register EIP masih memiliki alamat fisik di wilayah megabyte pertama. Untuk memperbaikinya, kami melakukan manipulasi sederhana: beri tanda di tempat terdekat, muat alamatnya (sudah dengan offset 3 GB, ingat ini) dan lakukan lompatan tanpa syarat. Setelah itu, halaman yang tidak perlu dapat diatur ulang untuk aplikasi masa depan.


  lea ecx, [StartInHigherHalf] jmp ecx StartInHigherHalf: mov dword [BootPageDirectory], 0 invlpg [0] 

Sekarang semuanya tentang yang sangat kecil: inisialisasi tumpukan, lewati struktur GRUB dan assembler sudah cukup!


  mov esp, stack+STACKSIZE push eax push ebx lea ecx, [BootPageDirectory] push ecx call kmain hlt section .bss align 32 stack: resb STACKSIZE 

Apa yang perlu Anda ketahui tentang kode ini:


  1. Menurut konvensi-C panggilan (itu juga berlaku untuk Rust), variabel ditransfer ke fungsi melalui tumpukan dalam urutan terbalik. Semua variabel disejajarkan oleh 4 byte di x86.
  2. Tumpukan tumbuh dari ujung, sehingga penunjuk ke tumpukan harus mengarah ke ujung tumpukan (tambahkan STACKSIZE ke alamat). Ukuran tumpukan yang saya ambil adalah 16 KB, seharusnya cukup.
  3. Berikut ini ditransfer ke kernel: angka ajaib Multiboot, alamat fisik struktur bootloader (ada kartu memori berharga bagi kami), alamat virtual tabel halaman (di suatu tempat dalam ruang 3 GB)

Juga, jangan lupa untuk menyatakan bahwa kmain adalah eksternal dan _loader bersifat global.


Langkah selanjutnya


Dalam catatan berikut, saya akan berbicara tentang pengaturan register segmen, secara singkat membahas output informasi melalui buffer VGA, memberi tahu Anda bagaimana saya mengatur pekerjaan dengan interupsi, manajemen halaman, dan hal termanis - multitasking - Saya akan pergi untuk pencuci mulut.


Kode proyek lengkap tersedia di GitLab .


Terima kasih atas perhatian anda!


UPD2: Bagian 2
UPD2: Bagian 3

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


All Articles