1 poin oleh GN⁺ 2024-07-16 | 1 komentar | Bagikan ke WhatsApp
  • Menyusun alur bootloader minimal langkah demi langkah, dimulai dari boot sector 512 byte untuk menaikkan CPU x86_64 dari real mode 16-bit ke long mode 64-bit
  • Memastikan output terlebih dahulu lewat interrupt BIOS, lalu menghubungkan nasm, ld, objcopy, dan QEMU untuk memverifikasi apakah boot image benar-benar berjalan
  • Karena keterbatasan kapasitas boot sector, proses dibagi menjadi stage 1/stage 2, dan sebelum masuk ke protected mode, kode berikutnya dibaca dari disk dengan BIOS int 0x13
  • Mulai dari protected mode 32-bit, routine BIOS tidak bisa dipakai, sehingga diperlukan inisialisasi hardware seperti GDT, flat segmentation, dan output langsung ke buffer VGA
  • Untuk masuk ke long mode 64-bit, page table, PAE, EFER.LME, cr0.PG, dan GDT untuk 64-bit semuanya harus disetel dengan benar; setelah itu kode C freestanding dapat dipanggil seperti kernel

Lingkungan 16-bit yang dimulai dari BIOS

  • Setelah reset, CPU x86 berada dalam real mode, dan ukuran operand default adalah 16-bit
  • Real mode membentuk ruang alamat 20-bit dengan segmentation, dan dapat menangani memori hingga 1MB
  • Kode pertama yang dijalankan BIOS berada di boot sector pada disk
    • BIOS mencari disk yang sector pertamanya berakhir dengan magic number 0xaa55
    • Sector tersebut dimuat ke alamat memori 0x7c00
  • Ruang yang diberikan BIOS hanya 512 byte, sehingga kode ini berfokus pada peran bootstrap untuk memuat sisa bootloader
  • Routine BIOS hanya dapat digunakan selama tetap berada di real mode

Perlengkapan dan lingkungan build

Membuat boot sector dan memastikan eksekusinya

  • Boot sector pertama mencetak "Hello, world!" dengan routine BIOS, lalu berhenti dengan hlt dan loop
  • Untuk mencetak string, digunakan pemanggilan BIOS video services int 0x10 dengan ah = 0x0e
  • Makefile membuat object dengan nasm, menautkannya dengan linker script, lalu menghasilkan raw boot image melalui objcopy -O binary
  • make boot menjalankan image dengan QEMU
    • qemu-system-x86_64 -no-reboot -drive file=$<,format=raw,index=0,media=disk
  • Linker script diatur agar boot sector ditempatkan dengan basis 0x7c00
    • Origin boot_sector pada MEMORY adalah 0x7c00, dengan length 512
    • Section .bootsign menambahkan 0x55, 0xaa pada posisi 0x7c00 + 510
  • Offset dan magic number juga bisa ditangani langsung di dalam assembly boot sector, tetapi di sini linker script mengambil peran tersebut

Stage 1 membaca stage 2 dari disk

  • Stage 1 adalah kode boot sector yang dimuat BIOS, dan tujuannya adalah memuat stage 2 ke memori
  • Stage 2 berisi kode untuk berpindah dari real mode 16-bit ke protected mode 32-bit
  • Setelah masuk ke protected mode, routine BIOS tidak bisa digunakan, sehingga pembacaan sector disk harus selesai sebelum transisi
  • Akses disk menggunakan BIOS disk services int 0x13
    • ah = 0x42 adalah fungsi BIOS extended read
    • dl = 0x80 adalah nomor drive
    • Disk address packet berisi jumlah sector yang akan dibaca, alamat tujuan, dan sector awal
  • Contoh membaca 64 sector dengan READ_SECTORS_NUM equ 64
    • Karena boot sector adalah sector 0, stage 2 dibaca mulai dari sector 1
    • Alamat tujuannya adalah BOOT_LOAD_ADDR + SECTOR_SIZE, yaitu 0x7c00 + 512
  • Di dalam kode masih tersisa penanganan sementara yang menerima kasus ketika sector yang terbaca lebih sedikit dari yang diminta
  • Pada awalnya, stage 2 menyalin print_string untuk real mode, mencetak "Hello from stage 2", dan memastikan lompatan dari stage 1 tersambung dengan benar

Beralih ke protected mode 32-bit

  • Untuk masuk ke protected mode, pertama-tama harus didefinisikan Global Descriptor Table(GDT)
  • Dalam protected mode, segmentation pada dasarnya digunakan untuk proteksi memori
  • Pada long mode 64-bit diperlukan paging, tetapi untuk tahap sebelumnya, yaitu masuk ke protected mode, konfigurasi segmentation harus dilakukan lebih dahulu
  • Contoh GDT mengikuti flat model dari manual Intel
    • Memiliki code segment dan data segment
    • Kedua segment dipetakan ke seluruh linear address space
    • Karena ini hanya tahap perantara menuju long mode, digunakan model yang paling sederhana
  • GDT adalah struktur kontinu yang ditempatkan di memori
    • Di awal terdapat null descriptor untuk menangkap invalid translation
    • Setelah itu diikuti code segment descriptor dan data segment descriptor
  • Transisi dilakukan dengan urutan berikut
    • Menonaktifkan interrupt dengan cli
    • Memuat alamat dan panjang GDT ke GDTR dengan lgdt [gdt32_pseudo_descriptor]
    • Mengaktifkan protected mode dengan menyetel cr0.PE, yaitu bit 0 pada cr0
    • Mengosongkan instruction pipeline dan memperbarui cs ke code segment baru melalui far jump
  • Setelah masuk ke protected mode, nilai segment lama tidak lagi bermakna, sehingga ds, ss, es, fs, gs disetel ke data segment selector baru
  • Untuk mengaktifkan kembali interrupt setelah semua konfigurasi, diperlukan pekerjaan tambahan

Mencetak ke layar tanpa BIOS

  • Dalam protected mode, routine BIOS tidak lagi bisa dipanggil
  • Output string berubah menjadi metode menulis langsung ke VGA buffer
  • print_string32 menuliskan karakter dan byte warna ke alamat 0xb8000
    • Nilai warnanya adalah 0xf
    • Setiap cell karakter memakai 2 byte
  • Fungsi output ini sangat sederhana, sehingga pesan selalu ditampilkan di kiri atas layar

Page table untuk masuk ke long mode

  • IA-32e mode dalam dokumentasi Intel setara dengan long mode pada manual AMD64
  • Untuk beralih ke long mode, CPU harus berada dalam protected mode dan paging juga harus diaktifkan
  • Konsep paging merujuk pada Introduction to Paging dan OSTEP
  • Pada long mode dengan PAE aktif, digunakan page table 4 tingkat
  • build_page_table membuat page table 4 tingkat di alamat yang ditentukan
    • Ukuran page adalah 0x1000
    • Ukuran setiap page table adalah 0x1000
    • Jumlah entry adalah 512
    • Pertama, empat table diinisialisasi ke 0 sehingga semua entry berada dalam status not present
    • Entry pertama PML4 → PDP → PD → page table dihubungkan
    • Pada layer page table paling bawah, 512 entry disetel

GDT untuk 64-bit dan urutan transisi ke long mode

  • Meski paging menangani virtual address space dan manajemen hak akses, GDT tetap diperlukan dalam long mode
  • GDT untuk 64-bit juga mengikuti flat model, dan hampir sama dengan GDT untuk protected mode
  • Perbedaannya ada pada penyetelan bit yang terkait long mode
    • Menyetel flag 64-bit code segment pada code segment
    • Jika flag ini disetel, default operation size bit harus bernilai 0
  • Transisi ke long mode berlangsung dengan alur berikut
    • Membuat page table 4 tingkat di alamat 0x1000
    • Memasukkan alamat PML4 table ke cr3
    • Mengaktifkan PAE dengan menyetel bit 5 pada cr4
    • Membaca MSR 0xc0000080 dan menyetel EFER.LME, yaitu bit 8
    • Mengaktifkan paging dengan menyetel flag PG, yaitu bit 31 pada cr0
    • Memuat GDT untuk 64-bit dengan lgdt
    • Masuk ke mode 64-bit dengan far jump ke 64-bit code segment
  • Tepat setelah paging diaktifkan, statusnya adalah IA-32e compatibility mode, lalu CPU beralih ke mode 64-bit saat melompat ke GDT dengan flag segment 64-bit yang sudah disetel
  • Pesan keberhasilan ditampilkan di kiri atas layar melalui VGA buffer

Memanggil kode C freestanding

  • Setelah masuk hingga long mode 64-bit, kode C freestanding dapat dipanggil
  • kernel.c menghapus VGA buffer 0xb8000 dan mencetak "Hello from C"
  • start_long_mode di sisi assembly mencetak string untuk 64-bit, lalu mendeklarasikan extern _start_kernel dan memanggil _start_kernel
  • Linker script membagi area memori menjadi tiga bagian
    • boot_sector: 0x7c00, length 512
    • stage2: 0x7e00, length 512
    • kernel: 0x8000, length 0x10000
  • Section .text, .data, .rodata, .bss ditempatkan di area kernel
  • Makefile diubah agar membuild assembly dan C sekaligus
    • C compiler yang digunakan adalah gcc
    • CFLAGS utama adalah -std=c99 -ffreestanding -m64 -mno-red-zone -fno-builtin -nostdinc -Wall -Wextra
  • Seluruh kode contoh tersedia melalui tautan unduhan

1 komentar

 
GN⁺ 2024-07-16
Komentar Hacker News
  • Bisa masuk langsung ke long mode dengan kode yang jauh lebih sedikit tanpa melalui protected mode: https://wiki.osdev.org/Entering_Long_Mode_Directly
    Pernah ada bootloader untuk kernel 64-bit kecil yang dibuat dengan cara ini, dan bahkan dengan kode untuk membaca kernel dari disk serta mengatur mode VESA, semuanya masih muat longgar di boot sector. Tidak perlu loader tahap kedua.

    • Saya penasaran bagaimana semua itu bisa dimasukkan ke dalam 512 byte. Sepertinya tidak mungkin ada sistem berkas sungguhan yang memungkinkan kernel diletakkan di mana saja di disk seperti file biasa; menangani fragmentasi file saja rasanya sudah jauh melebihi 512 byte.
    • Pakai saja https://limine-bootloader.org/, itu jauh lebih sederhana. Tidak perlu menyentuh real mode, begitu juga untuk SMP; kernel otomatis dimuat dengan pemetaan higher-half, dan juga berjalan di aarch64 serta riscv64.
    • Benar, tetapi kalau ingin mendukung kontroler AHCI modern dan SATA sekaligus menyertakan tabel partisi, ruang bootloader makin berkurang sehingga perlu optimisasi. Dalam kasus ini, loader tidak bisa memakai seluruh 510 byte, melainkan jauh lebih sedikit; dan kalau harus mengisi entri partisi yang valid, byte di dalam tabel pun tidak bisa dipakai, sehingga makin rumit.
      Untuk memakai hard disk modern sungguhan, lebih baik melihat GPT daripada MBR. GPT bisa menangani disk besar di atas 2TB tanpa terbentur batas tabel partisi. UEFI menghilangkan masalah seperti ini dan memungkinkan tata letak disk yang layak tanpa banyak kesulitan.
      Untuk masuk ke mode 64-bit, protected mode tidak diperlukan. Namun sebaiknya jangan pakai BIOS. Itu berantakan dan hanya membuat pekerjaan lebih merepotkan.
      Lebih baik memakai UEFI lewat EDK2 atau GnuEFI; keduanya cukup mudah dan nyaman diimplementasikan. Memang perlu sedikit waktu untuk terbiasa dengan konsep awal UEFI, tetapi dengan melihat proyek contoh di GitHub, strukturnya mudah dipahami. EDK agak kurang enak dengan file .dec dan .inf, sedangkan GnuEFI mengharuskan kita membaca file header untuk menemukan fungsi, tetapi tetap jauh lebih baik daripada antarmuka BIOS yang spesifikasinya tidak jelas. Pada hardware nyata, kita bahkan tidak bisa berasumsi bahwa int 0x10, int 0x15, dan semacamnya tersedia dengan benar.
      Pada sistem UEFI, kita bisa mengasumsikan fondasi minimum yang stabil, dan fitur hardware maupun platform juga bisa dienumerasi secara sane. Selain itu, UEFI sudah cukup banyak mengonfigurasi platform, sehingga komponen OS loader tidak perlu melakukan banyak inisialisasi; cukup langsung memuat OS, driver, dan komponen yang sesuai dengan desain kernel. Ambil peta memori, akses sistem berkas EFI, lalu baca hal-hal yang diperlukan.
    • Saya tidak tahu hal seperti ini mungkin. Kalau tujuannya sekadar masuk ke long mode, sejak awal saya penasaran kenapa harus melalui protected mode.
  • 80286 memiliki Machine Status Word (MSW), sebuah register 16-bit, dan 80386 memperluasnya menjadi register 32-bit CR0. Kemudian long mode 64-bit menambahkan EFER MSR dan memperluas CR0 menjadi 64-bit, tetapi sampai hari ini hanya 11 bit yang dipakai di CR0 dan EFER pun hanya punya 8 bit aktif.
    Saya penasaran kenapa Intel/AMD sampai dua kali memilih mekanisme baru, alih-alih memakai bit kosong di register yang sudah ada: https://wiki.osdev.org/CPU_Registers_x86-64#CR0

    • Kemungkinan besar alasannya untuk membuat kompatibilitas mundur lebih kokoh. Perangkat lunak bisa saja mengasumsikan nilai bit yang dicadangkan atau menulis nilai ke sana. Penempatan bit pada register hardware seperti ini cukup arbitrer, dan hampir tidak ada biaya untuk memakai bit yang lebih tinggi.
    • Kalau dijawab dengan satu kata, mungkin birokrasi. Organisasi besar pada umumnya tidak terlalu pandai mengambil keputusan yang benar-benar bagus, dan banyak pilihan yang tidak masuk akal bisa muncul.
      Alasan mengapa CR8 muncul sementara CR1 dan CR5–CR7 masih dicadangkan juga tampaknya mirip.
  • Hal yang terlihat paling tidak perlu rumit dalam tulisan ini adalah Makefile dan skrip linker. NASM mendukung keluaran biner datar, tetapi sepertinya mereka menganggap memakai itu terlalu “hacky”.

    • Secara pribadi, saya merasa skrip linker jauh lebih mudah dibaca dan dipahami dibanding NASM datar, terutama ketika ada banyak file sumber.
    • Benar sekali. Nantinya Makefile dan skrip linker memang menjadi sumber masalah penting, tetapi kalau ingin membuat biner datar, ya buat saja biner datar. Tidak perlu dibesar-besarkan.
      Dulu di OS saya ada file bernama make.sh untuk mengejek hal ini. Sekarang saya memakai hal-hal keren seperti ‘format file’, jadi -fbin dan --oformat=binary tinggal kenangan singkat. Saya lama mencoba memisahkan file C data dan file C kode, men-dump-nya menjadi biner, lalu merakit monster dari sana, tetapi linking dan loading menjadi terlalu sulit. Lebih baik pakai ELF atau PE saja, dan memang sepertinya format-format itu dibuat untuk melakukan hal semacam itu.
  • Terlihat keren dan seperti latihan yang bagus, tetapi saya tidak tahu apakah ini berguna. Saya penasaran apakah ada UX seperti mainan Fisher-Price yang memungkinkan memeriksa atau mengubah konfigurasi saat berjalan.
    Booting adalah proses dari mode mini-me, single-user mode, recovery mode, menuju keadaan lepas landas.
    Saya sudah memakai Unix bersama produk Microsoft sejak era Xenix/DOS, kira-kira sudah 40 tahun. Saya bertanya-tanya sudah sejauh apa kemajuannya.
    Linux juga saya pakai sejak versi Swedia-nya, yaitu rilis pertama, dan saya juga pernah mencoba GNU 0.1.
    Saya minta maaf karena menyebut Xenix sebagai Unix. Xenix adalah produk me-too yang kacau, yang sejak segera setelah rilis hingga masa kemundurannya sudah ingin menjadi barang masa lalu.
    Microsoft rasanya bukan perusahaan yang merilis produk, melainkan perusahaan yang mengosongkan kotak pasir kucing ke atas pelanggannya. Contoh terbaru adalah Copilot dan 22H2.
    Kalau melihat bagaimana mobil F1, pensil, dan kalkulator saku berkembang, saya bertanya-tanya seberapa dekat kita dengan bentuk ideal yang bisa dipakai.
    Saya juga bertanya-tanya kenapa bootloader bukan static kernel mode. Dulu memang begitu, dan belum lama ini ada seseorang yang mengusulkan agar kembali begitu; saya pun setuju.

  • https://wiki.osdev.org/A20_Line

  • Terkejut karena semua langkah yang diperlukan untuk mengalihkan CPU ke mode yang benar tampak sama sekali tidak perlu. Sebagian besar terlihat seperti prosedur yang diperlukan karena kompatibilitas mundur
    Saya penasaran apakah Intel tidak bisa menyediakan flag atau instruksi untuk memulai langsung dalam mode yang benar sejak awal, atau apakah mereka tidak bisa menghapus kompatibilitas mundur
    Seingat saya ARM64 juga punya sebagian masalah serupa. Saya penasaran apakah ada CPU yang sejak awal dirancang sebagai 64-bit sehingga tidak memerlukan kompatibilitas mundur dan secara default masuk ke keadaan yang diinginkan. Mungkin itukah tujuan atau desain Itanium

    • X86S yang diusulkan Intel memang untuk tujuan seperti itu

      X86S is a legacy-reduced-OS ISA that removes outdated execution modes and operating system ISA.
      The presence of the X86S ISA is enumerated by a single, main CPUID feature LEGACY_REDUCED_ISA in CPUID 7.1.ECX[2] which implies all the ISA removals described in this document. A new, 64-bit “start-up” interprocessor interrupt (SIPI) has a separate CPUID feature flag.
      [0] https://cdrdv2.intel.com/v1/dl/getContent/776648 [hati-hati PDF]

    • Intel pernah mencobanya dengan 80376, tetapi tidak berhasil: https://en.wikipedia.org/wiki/Intel_80376
      Itanium, alias Itanic, juga begitu
      Kompatibilitas mundur justru alasan utama memilih x86, bukan ARM, MIPS, RISC-V, dan sebagainya. Sayangnya, sebagian orang di Intel dan AMD tampaknya tidak begitu memahami hal ini
    • UEFI sudah ada. Jika menaruh binary mirip Windows di folder pada partisi, itu akan dijalankan dalam lingkungan hosting mode 64-bit. Tentu saja ada banyak sekali bootloader yang menangani hal semacam ini untuk kita
    • Saya tidak tahu apa masalahnya di arm64
  • Proyek yang bagus. Kalau para pendukung UEFI di sini heran mengapa perlu repot-repot membuat cara bootloader baru, sepertinya mereka melewatkan alasan orang melakukan pekerjaan seperti ini
    Seperti yang ditulis penulis di bagian akhir, “kalau Anda mengikuti sampai sini, keren”—itu memang benar-benar keren

  • Rasanya UEFI sudah ada cukup lama. Sayang sekali BIOS tidak ikut dibuang bersamaan dengan long mode

    • BIOS sudah direncanakan untuk ditinggalkan. Pada motherboard baru, fungsinya pada dasarnya diemulasikan melalui UEFI dan tidak lagi diperluas
      Direncanakan untuk ditinggalkan bukan berarti sudah dihapus, melainkan tidak lagi diperbarui atau dikembangkan dengan tujuan akhirnya dihapus
  • Saya penasaran apakah prosedur boot ini juga bekerja di EFI/UEFI. Jika bekerja, saya juga penasaran apakah supervisor UEFI mengemulasikan perpindahan real mode, protected mode, dan long mode, atau melakukannya di perangkat keras sungguhan

    • Tidak. Firmware UEFI menyediakan lingkungan yang sama sekali berbeda dari lingkungan BIOS legacy, yaitu mode alamat riil, kepada bootloader UEFI. Firmware UEFI pada sistem modern langsung masuk ke 64-bit long mode, serta menyiapkan GDT dengan model memori datar dan paging identity mapping
      Saya menulis proses membuat bootloader UEFI untuk OS hobi di sini: https://0xc0ffee.netlify.app/osdev/05-bootloader-p1.html
  • Saya penasaran apakah ini lebih sederhana di ARM

    • Sederhana dalam arti tiap produsen board bisa melakukan sesukanya. Sederhana bagi produsen board, tetapi menjadi sangat rumit bagi semua pihak lainnya
    • Benar. Bootloader tetap rumit, tetapi pengaturan legacy yang diperlukan lebih sedikit. Namun jika menargetkan UEFI alih-alih BIOS, di x86 pun jadi jauh lebih sederhana
    • Saya tidak yakin dan tidak terlalu berharap. Saat ini saya sedang mendalami RISC-V, dan di sana tampaknya ada harapan