Bagaimana kernel mengetahui alamat basis memori fisik?
Saya mencoba untuk memahami 2 masalah yang terkait erat.
Kode kernel yang menjalankan pasca-bootloader dan sebelum mengaktifkan MMU beroperasi di memori virtual yang dipetakan fisik / identitas. Bagaimana kode ini dibuat portabel antara CPU yang berbeda mungkin memiliki DRAM dalam rentang alamat fisik yang berbeda?
Agar kernel dapat mengelola tabel halaman, kernel memerlukan kesadaran tentang sumber daya memori fisik yang tersedia, termasuk alamat dasar memori fisik dan memori fisik yang tersedia, sehingga kernel tidak menetapkan alamat fisik yang berada di luar jangkauan DRAM.
Saya membayangkan ini agak bergantung pada implementasi, tetapi referensi tentang bagaimana arsitektur yang berbeda menangani masalah ini akan dihargai. Beberapa ide yang saya miliki sejauh ini:
Rentang DRAM alamat fisik, atau setidaknya alamat dasar, dimasukkan pada waktu kompilasi kernel. Ini menyiratkan bahwa kompilasi ulang diperlukan untuk CPU yang berbeda bahkan dengan ISA yang sama. Ini terinspirasi oleh jawaban ini di sini , yang, jika saya memahaminya dengan benar, menjelaskan solusi yang sama untuk alamat basis kernel. Karena alamat dasar diketahui pada waktu kompilasi, kode kernel mereferensikan alamat literal daripada offset dari alamat dasar DRAM / kernel.
Informasi DRAM dibaca dan dipelajari dari pohon perangkat dengan peta memori fisik lainnya. Ini kesan saya setidaknya untuk Xilinx Zynq SoC, berdasarkan posting forum seperti ini . Meskipun solusi ini menawarkan lebih banyak fleksibilitas dan memungkinkan kita untuk hanya mengkompilasi ulang boot loader daripada seluruh kernel ke port CPU, itu membuat saya bertanya-tanya bagaimana mesin pribadi X86 saya dapat mendeteksi pada saat berjalan berapa banyak DRAM yang telah saya instal. Kode untuk mengelola tabel halaman hanya mereferensikan offset dari alamat dasar DRAM dan bersifat portabel tanpa kompilasi ulang di seluruh CPU dengan rentang alamat fisik DRAM yang berbeda.
Jawaban
Seluruh DIMM memori fisik yang tersedia saat boot mungkin tidak dan biasanya tidak dipetakan ke satu rentang ruang alamat memori fisik yang berdekatan, sehingga tidak ada "alamat dasar". Saat hard reset, setelah firmware CPU selesai dijalankan, firmware platform, yang biasanya merupakan BIOS atau UEFI lawas, dijalankan. Motherboard tertentu hanya kompatibel dengan kumpulan koleksi CPU terbatas yang biasanya memiliki metode yang sama untuk menemukan memori fisik termasuk DIMM dan perangkat memori firmware platform. Implementasi firmware platform menggunakan metode ini untuk membuat tabel entri deskripsi memori di mana setiap entri menjelaskan rentang alamat memori fisik. Untuk informasi lebih lanjut tentang seperti apa prosesor ini, lihat: Bagaimana BIOS menginisialisasi DRAM? . Tabel ini disimpan di alamat di memori utama (DIMM) yang diketahui dicadangkan untuk tujuan ini dan seharusnya didukung oleh memori sebenarnya (sistem dapat di-boot tanpa DIMM).
Kebanyakan implementasi x86 PC BIOS sejak pertengahan 90-an menawarkan fungsi real-mode INT 15h E820h
(15h adalah nomor interupsi dan E820h adalah argumen yang dilewatkan dalam AX
register). Ini adalah fungsi BIOS khusus vendor yang pertama kali diperkenalkan di PhoenixBIOS v4.0 (1992-1994, saya tidak dapat mengetahui tahun persisnya) dan kemudian diadopsi oleh vendor BIOS lainnya. Antarmuka ini diperluas oleh spesifikasi ACPI 1.0 yang dirilis pada tahun 1996 dan revisi selanjutnya dari ACPI yang didukung PhoenixBIOS. Antarmuka UEFI yang sesuai adalah GetMemoryMap()
, yang merupakan layanan waktu boot UEFI (artinya hanya dapat dipanggil pada waktu boot seperti yang ditentukan dalam spesifikasi UEFI). Kernel dapat menggunakan salah satu antarmuka ini untuk mendapatkan peta alamat yang mendeskripsikan memori pada semua node NUMA. Metode lain (lama) pada platform x86 dibahas di Mendeteksi Memori (x86) . Baik spesifikasi ACPI dimulai dengan versi? dan spesifikasi UEFI dimulai dengan versi? mendukung DRAM DIMM dan tipe rentang memori NVDIMM.
Pertimbangkan misalnya bagaimana kernel Linux yang kompatibel dengan ACPI menentukan kisaran alamat fisik apa yang tersedia (yaitu, didukung oleh memori aktual) dan dapat digunakan (yaitu, gratis) pada platform BIOS yang mendukung ACPI x86. Firmware BIOS memuat bootloader dari perangkat penyimpanan yang dapat di-boot yang ditentukan ke lokasi memori yang didedikasikan untuk tujuan ini. Setelah firmware selesai dieksekusi, ia melompat ke bootloader yang akan menemukan image kernel pada media penyimpanan, memuatnya ke memori, dan mentransfer kontrol ke kernel. Bootloader itu sendiri perlu mengetahui peta memori saat ini dan mengalokasikan beberapa memori untuk operasinya. Ia mencoba untuk mendapatkan peta memori dengan memanggil E820h
fungsi tersebut dan jika tidak didukung, ia akan menggunakan antarmuka BIOS PC yang lebih lama. The protokol boot kernel mendefinisikan yang rentang memori dapat digunakan oleh bootloader dan yang rentang memori harus dibiarkan tersedia untuk kernel.
Bootloader itu sendiri tidak mengubah peta memori atau menyediakan peta ke kernel. Sebaliknya, saat kernel mulai dijalankan, kernel akan memanggil E820h
fungsi dan meneruskan pointer 20-bit (masuk ES:DI
) ke buffer yang diketahui kernel sebagai gratis pada platform x86 sesuai dengan protokol boot. Setiap panggilan mengembalikan deskriptor rentang memori yang ukurannya setidaknya 20 byte. Untuk informasi lebih lanjut, lihat versi terbaru spesifikasi ACPI. Sebagian besar implementasi BIOS mendukung ACPI.
Dengan asumsi kernel Linux dengan parameter boot upstream-default, Anda dapat menggunakan perintah dmesg | grep 'BIOS-provided\|e820'
untuk melihat tabel deskriptor rentang memori yang ditampilkan. Di sistem saya, ini terlihat seperti ini:
[ 0.000000] BIOS-provided physical RAM map:
[ 0.000000] BIOS-e820: [mem 0x0000000000000000-0x00000000000917ff] usable
[ 0.000000] BIOS-e820: [mem 0x0000000000091800-0x000000000009ffff] reserved
[ 0.000000] BIOS-e820: [mem 0x00000000000e0000-0x00000000000fffff] reserved
[ 0.000000] BIOS-e820: [mem 0x0000000000100000-0x00000000d2982fff] usable
[ 0.000000] BIOS-e820: [mem 0x00000000d2983000-0x00000000d2989fff] ACPI NVS
[ 0.000000] BIOS-e820: [mem 0x00000000d298a000-0x00000000d2db9fff] usable
[ 0.000000] BIOS-e820: [mem 0x00000000d2dba000-0x00000000d323cfff] reserved
[ 0.000000] BIOS-e820: [mem 0x00000000d323d000-0x00000000d7eeafff] usable
[ 0.000000] BIOS-e820: [mem 0x00000000d7eeb000-0x00000000d7ffffff] reserved
[ 0.000000] BIOS-e820: [mem 0x00000000d8000000-0x00000000d875ffff] usable
[ 0.000000] BIOS-e820: [mem 0x00000000d8760000-0x00000000d87fffff] reserved
[ 0.000000] BIOS-e820: [mem 0x00000000d8800000-0x00000000d8fadfff] usable
[ 0.000000] BIOS-e820: [mem 0x00000000d8fae000-0x00000000d8ffffff] ACPI data
[ 0.000000] BIOS-e820: [mem 0x00000000d9000000-0x00000000da718fff] usable
[ 0.000000] BIOS-e820: [mem 0x00000000da719000-0x00000000da7fffff] ACPI NVS
[ 0.000000] BIOS-e820: [mem 0x00000000da800000-0x00000000dbe11fff] usable
[ 0.000000] BIOS-e820: [mem 0x00000000dbe12000-0x00000000dbffffff] reserved
[ 0.000000] BIOS-e820: [mem 0x00000000dd000000-0x00000000df1fffff] reserved
[ 0.000000] BIOS-e820: [mem 0x00000000f8000000-0x00000000fbffffff] reserved
[ 0.000000] BIOS-e820: [mem 0x00000000fec00000-0x00000000fec00fff] reserved
[ 0.000000] BIOS-e820: [mem 0x00000000fed00000-0x00000000fed03fff] reserved
[ 0.000000] BIOS-e820: [mem 0x00000000fed1c000-0x00000000fed1ffff] reserved
[ 0.000000] BIOS-e820: [mem 0x00000000fee00000-0x00000000fee00fff] reserved
[ 0.000000] BIOS-e820: [mem 0x00000000ff000000-0x00000000ffffffff] reserved
[ 0.000000] BIOS-e820: [mem 0x0000000100000000-0x000000041edfffff] usable
[ 0.002320] e820: update [mem 0x00000000-0x00000fff] usable ==> reserved
[ 0.002321] e820: remove [mem 0x000a0000-0x000fffff] usable
[ 0.002937] e820: update [mem 0xdd000000-0xffffffff] usable ==> reserved
[ 0.169287] e820: reserve RAM buffer [mem 0x00091800-0x0009ffff]
[ 0.169288] e820: reserve RAM buffer [mem 0xd2983000-0xd3ffffff]
[ 0.169289] e820: reserve RAM buffer [mem 0xd2dba000-0xd3ffffff]
[ 0.169289] e820: reserve RAM buffer [mem 0xd7eeb000-0xd7ffffff]
[ 0.169289] e820: reserve RAM buffer [mem 0xd8760000-0xdbffffff]
[ 0.169290] e820: reserve RAM buffer [mem 0xd8fae000-0xdbffffff]
[ 0.169291] e820: reserve RAM buffer [mem 0xda719000-0xdbffffff]
[ 0.169291] e820: reserve RAM buffer [mem 0xdbe12000-0xdbffffff]
[ 0.169292] e820: reserve RAM buffer [mem 0x41ee00000-0x41fffffff]
Rentang memori yang dimulai dengan "BIOS-e820" dijelaskan dalam tabel itu. Baris pertama dengan jelas memberi tahu Anda sumber informasi ini. Format pasti dari informasi ini bergantung pada versi kernel Linux. Bagaimanapun, Anda akan melihat rentang dan tipe di setiap entri. Baris yang diawali dengan "e820" (tanpa bagian "BIOS-") adalah perubahan yang dibuat oleh kernel itu sendiri pada tabel. Pelaksanaan E820h
mungkin buggy atau mungkin ada tumpang tindih antara rentang yang diperoleh di entri yang berbeda. Kernel melakukan pemeriksaan dan perubahan yang diperlukan. Rentang yang ditandai sebagai "dapat digunakan" sebagian besar gratis untuk digunakan oleh kernel dengan pengecualian yang dibahas dalam spesifikasi ACPI dan yang diketahui oleh kernel. Sebagian besar implementasi BIOS PC menghasilkan paling banyak 128 deskriptor rentang memori. Versi lama dari kernel Linux hanya dapat menangani hingga 128 rentang memori, jadi entri apa pun yang dikembalikan di E820h
luar 128 akan diabaikan. Dimulai dengan versi?, Batasan ini dilonggarkan. Untuk informasi lebih lanjut, lihat rangkaian patch kernel berjudul "x86 boot: pass entri peta memori E820 lebih dari 128 melalui daftar data setup yang terhubung."
Rentang tipe usable
dan ACPI data
. Rentang jenis reserved
didukung oleh DRAM DIMM atau dipotong untuk MMIO oleh CPU atau firmware platform. Rentang jenis ACPI NVS
didukung oleh memori firmware. Semua rentang lainnya tidak didukung oleh memori sebenarnya sejauh yang dapat diketahui firmware. Perhatikan bahwa firmware dapat memilih untuk tidak memetakan semua DIMM DRAM atau NVDIMM yang diinstal. Ini dapat terjadi jika konfigurasi memori fisik tidak didukung sebagaimana mestinya atau jika firmware tidak dapat memperoleh informasi dari DIMM yang diinstal karena masalah di DIMM.
Anda dapat menghitung berapa banyak memori DRAM DIMM dan NVDIMM yang diinstal yang disediakan oleh firmware ke kernel. Di sistem saya, saya telah menginstal 16 GB DRAM DIMM. Jadi, kecuali beberapa DIMM tidak diinstal dengan benar, tidak berfungsi dengan benar, ada bug di firmware, atau tidak didukung oleh platform atau prosesor, seharusnya ada sedikit kurang dari 16 GB yang tersedia untuk kernel.
Semua usable
rentang berjumlah hingga 0x3FA42B800 byte. Perhatikan bahwa alamat terakhir rentang bersifat inklusif, artinya alamat tersebut menunjuk ke lokasi byte yang merupakan bagian dari rentang. Jumlah total DIMM yang dipasang secara fisik adalah 16 GB atau 0x400000000 byte. Jadi jumlah total memori terpasang yang tidak tersedia untuk kernel adalah 0x400000000 - 0x3FA42B800 atau sekitar 92 MB dari total 16 GB. Memori ini diambil oleh beberapa reserved
rentang dan semua ACPI data
rentang. Jika lokasi tertentu di DRAM DIMM atau NVDIMM ditentukan oleh firmware platform sebagai tidak dapat diandalkan, lokasi tersebut juga akan dipotong sebagai reserved
.
Perhatikan bahwa kisaran 0x000a0000-0x000fffff tidak dijelaskan dalam E820
peta memori sesuai spesifikasi ACPI. Ini adalah area memori atas 640KB-1MB. Kernel mencetak pesan yang mengatakan telah menghapus kisaran ini dari area memori yang dapat digunakan untuk menjaga kompatibilitas dengan sistem kuno.
Pada titik ini, memori yang akan digunakan sebagai MMIO untuk sebagian besar perangkat PCIe belum dialokasikan. Prosesor saya mendukung ruang alamat fisik 39-bit, yang berarti bahwa alamat antara 0 hingga 2 ^ 39 tersedia untuk pemetaan. Sejauh ini hanya sebagian besar 16,5 GB terbawah dari ruang ini yang telah dipetakan ke sesuatu. Perhatikan bahwa masih ada celah yang belum dipetakan dalam kisaran ini. Kernel dapat menggunakan celah ini (beberapa 100 MB) dan sisa ruang alamat fisik (sekitar 495,5 GB) untuk mengalokasikan rentang alamat untuk perangkat IO. Kernel akhirnya akan menemukan perangkat PCIe dan untuk setiap perangkat, kernel akan mencoba memuat driver yang kompatibel jika tersedia. Driver kemudian menentukan berapa banyak memori yang dibutuhkan perangkat dan batasan apa pun pada alamat memori yang diberlakukan oleh perangkat dan meminta dari kernel untuk mengalokasikan memori untuk perangkat dan mengkonfigurasinya sebagai memori MMIO yang dimiliki oleh perangkat. Anda dapat melihat peta memori terakhir menggunakan perintah sudo cat /proc/iomem
.
Ada situasi di mana Anda ingin mengubah secara manual jenis memori dari rentang memori yang ada (misalnya, untuk pengujian), membuat rentang baru (misalnya, untuk meniru memori persisten pada DRAM atau jika firmware tidak dapat menemukan semua memori yang tersedia untuk alasan apa pun), kurangi jumlah memori yang dapat digunakan oleh kernel (misalnya, untuk mencegah hypervisor logam kosong untuk menggunakan memori di luar batas dan membuat sisanya tersedia untuk tamu), atau bahkan menimpa seluruh tabel yang dikembalikan dari E820h
. The mem
dan memmap
paramters kernel dapat digunakan untuk tujuan tersebut. Ketika satu atau lebih dari parameter ini ditentukan dengan nilai yang valid, kernel akan membaca peta memori yang disediakan BIOS terlebih dahulu dan membuat perubahan yang sesuai. Kernel akan mencetak peta memori akhir sebagai "peta RAM fisik yang ditentukan pengguna." di buffer cincin pesan kernel. Anda dapat melihat pesan ini dengan dmesg | grep user:
(setiap baris rentang memori dimulai dengan "pengguna:"). Pesan ini akan dicetak setelah pesan "BIOS-e820".
Pada platform x86 yang di-boot dengan firmware UEFI yang mendukung Modul Dukungan Kompatibilitas (lihat spesifikasi CSM untuk informasi lebih lanjut, yang terpisah dari UEFI), E820h
antarmuka mode-nyata yang lama didukung dan kernel Linux secara default masih menggunakannya. Jika kernerl berjalan pada platform x86 dengan UEFI yang tidak mendukung CSM, E820h
antarmuka mungkin tidak menyediakan semua atau semua rentang memori. Mungkin perlu menggunakan add_efi_memmap
parameter kernel pada platform semacam itu. Contohnya dapat ditemukan di UEFI Memory V E820 Memory . Ketika satu atau lebih rentang memori disediakan dari GetMemoryMap()
, kernel menggabungkan rentang ini dengan rentang dari E820h
antarmuka. Peta memori yang dihasilkan dapat dilihat menggunakan dmesg | grep 'efi:'
parameter kernel terkait UEFI lainnya yang mempengaruhi peta memori tersebut efi_fake_mem
.
Spesifikasi ACPI (Bagian 6.3) menyediakan mekanisme pemberitahuan untuk menginformasikan kernel ketika perangkat IO atau DIMM telah dimasukkan atau dihapus dari sistem dalam status S mana pun. (Saya tidak tahu apakah ada motherboad yang mendukung penghapusan DIMM dalam status-S mana pun. Ini biasanya hanya mungkin di negara bagian G3 dan mungkin S4 dan / atau S5) Ketika peristiwa seperti itu terjadi, baik kernel atau firmware membuat perubahan pada peta memori yang sesuai. Perubahan ini tercermin dalam sudo cat /proc/iomem
.
pengalamatan pc-relative mengacu pada teknik pemrograman dimana program anda dapat beroperasi di alamat manapun. Sejak register relokasi (mis. Segmen) telah berlalu, kebanyakan pemrograman relatif-pc dilakukan secara eksplisit. Berikut adalah contoh dalam jenis kode mesin generik:
.text
entry:
call reloc /* call is pc relative */
reloc:
pop %r0 /* r0 now contains physical address of reloc */
sub $reloc, %r0, %r14 /* r14 contains difference between link address of reloc */ /* At this point, r14 is a relocation register. A virtual address + r14 == the corresponding physical address. */ add $proot, %r14, %r0 /* physical address of page table root */
add $entry, %r14, %r1 /* entry is where we were loaded into ram */ test $0xfff, %r1 /* someone is being funny and not page aligning us */
jnz bad_alignment
or $0x7, %r1 /* put mythical page protection bits in r1 */ mov $1024, %r2 /* number of pages in r2 */
loop:
store %r1, (%r0) /* store a page table entry */
add $0x1000, %r1 /* setup next one 4096 bytes farther */ add $4, %r0 /* point to next page table entry */
sub $1, r2 /* are we done? */ cmp %0, r2 jne loop /* nope, setup next entry */ add $proot, %r14, %r0
loadsysreg %r0, page_table_base_register
mov $1, %r0 mov $v_entry, %r1
loadsysreg %r0, page_table_enabled
jmp %r1
v_entry:
/* now we are virtually addressed */
call main
1: jmp 1b /* main shouldn't return. */
.data
.align 12 /* 4096 byte pages */
proot:
.zero 4096
.text
Mesin mistis ini sangat sederhana, dengan satu tabel halaman datar, dan kernel terhubung ke alamat 0, tetapi dapat dijalankan dari mana saja dalam 4M pertama (1024 * 4096). Mesin sungguhan hanyalah versi yang lebih detail dari ini. Secara umum, Anda bahkan tidak dapat mempercayai bahasa sistem seperti C
sampai Anda memiliki pengaturan ruang alamat awal. Setelah itu, kode di dalamnya dapat membuat tabel halaman yang jauh lebih rumit, dan database kueri seperti pohon perangkat, atau bahkan monstrositas seperti apic / uefi untuk informasi lebih lanjut tentang tata letak ram, dll.
Dalam arsitektur tabel halaman yang dipetakan ke depan di mana node interior berada dalam format yang kompatibel sebagai node daun (misalnya x86-classic), Anda dapat menggunakan tabel halaman singe secara rekursif untuk memungkinkan alamat link yang lebih fleksibel. Misalnya, jika Anda mengarahkan entri terakhir dalam proot (yaitu proot [1023]) kembali ke proot, maka Anda dapat menautkan OS Anda di 0xffffc000, dan kode ini akan berfungsi (setelah diterjemahkan ke x86).