Ya, Linux menggunakan paging sehingga semua alamat selalu virtual. (Untuk mengakses memori pada alamat fisik yang diketahui, Linux menyimpan semua memori fisik 1:1 yang dipetakan ke rentang ruang alamat virtual kernel, sehingga Linux dapat dengan mudah mengindeks ke dalam "array" tersebut menggunakan alamat fisik sebagai offset. Komplikasi modulo untuk 32 -bit kernel pada sistem dengan lebih banyak RAM fisik daripada ruang alamat kernel.)
Ruang alamat linier ini terdiri dari halaman, dibagi menjadi empat segmen
Tidak, Linux menggunakan model memori datar. Basis dan batas untuk keempat deskriptor segmen tersebut adalah 0 dan -1 (tidak terbatas). yaitu semuanya tumpang tindih sepenuhnya, mencakup seluruh ruang alamat linier virtual 32-bit.
Jadi bagian merah terdiri dari dua segmen
__KERNEL_CS
dan__KERNEL_DS
Tidak, di sinilah Anda salah. register segmen x86 bukan digunakan untuk segmentasi; itu adalah bagasi lama x86 yang hanya digunakan untuk mode CPU dan pemilihan tingkat hak istimewa di x86-64 . Alih-alih menambahkan mekanisme baru untuk itu dan menghapus segmen sepenuhnya untuk mode panjang, AMD hanya mengebiri segmentasi dalam mode panjang (basis tetap pada 0 seperti semua orang yang menggunakan mode 32-bit) dan tetap menggunakan segmen hanya untuk keperluan konfigurasi mesin yang tidak sangat menarik kecuali Anda benar-benar menulis kode yang beralih ke mode 32-bit atau apa pun.
(Kecuali Anda dapat menyetel basis bukan nol untuk FS dan/atau GS, dan Linux melakukannya untuk penyimpanan lokal-thread. Tapi ini tidak ada hubungannya dengan bagaimana copy_from_user()
dilaksanakan, atau apapun. Itu hanya harus memeriksa nilai penunjuk itu, tidak dengan referensi ke segmen mana pun atau CPL / RPL dari deskriptor segmen.)
Dalam mode lama 32-bit, dimungkinkan untuk menulis kernel yang menggunakan model memori tersegmentasi, tetapi tidak ada OS arus utama yang benar-benar melakukannya. Namun, beberapa orang berharap itu menjadi sesuatu, mis. lihat jawaban ini meratapi x86-64 membuat OS bergaya Multics menjadi tidak mungkin. Tapi ini tidak cara kerja Linux.
Linux adalah https://wiki.osdev.org/Higher_Half_Kernel, di mana penunjuk kernel memiliki satu rentang nilai (bagian merah) dan alamat ruang pengguna berada di bagian hijau. Kernel dapat melakukan dereferensi sederhana alamat ruang pengguna jika tabel halaman ruang pengguna yang tepat dipetakan, tidak perlu menerjemahkannya atau melakukan apa pun dengan segmen; inilah artinya memiliki model memori datar . (Kernel dapat menggunakan entri tabel-halaman "pengguna", tetapi tidak dan sebaliknya). Khusus untuk x86-64, lihat https://www.kernel.org/doc/Documentation/x86/x86_64/mm.txt untuk peta memori sebenarnya.
Satu-satunya alasan keempat entri GDT tersebut harus dipisahkan adalah karena alasan tingkat hak istimewa, dan bahwa deskriptor data vs. segmen kode memiliki format yang berbeda. (Entri GDT berisi lebih dari sekadar basis/batas; itu adalah bagian-bagian yang harus berbeda. Lihat https://wiki.osdev.org/Global_Descriptor_Table)
Dan terutama https://wiki.osdev.org/Segmentation#Notes_Regarding_C yang menjelaskan bagaimana dan mengapa GDT biasanya digunakan oleh OS "normal" untuk membuat model memori datar, dengan sepasang kode dan deskriptor data untuk setiap tingkat hak istimewa .
Untuk kernel Linux 32-bit, hanya gs
mendapatkan basis non-nol untuk penyimpanan thread-local (jadi mode pengalamatan seperti [gs: 0x10]
akan mengakses alamat linier yang bergantung pada utas yang mengeksekusinya). Atau dalam kernel 64-bit (dan ruang pengguna 64-bit), Linux menggunakan fs
. (Karena x86-64 membuat GS spesial dengan swapgs
instruksi, dimaksudkan untuk digunakan dengan syscall
agar kernel menemukan tumpukan kernel.)
Tapi bagaimanapun, basis bukan nol untuk FS atau GS bukan dari entri GDT, mereka disetel dengan wrgsbase
petunjuk. (Atau pada CPU yang tidak mendukungnya, dengan penulisan ke MSR).
tapi apakah flag itu, yaitu
0xc09b
,0xa09b
dan seterusnya ? Saya cenderung percaya bahwa mereka adalah pemilih segmen
Tidak, pemilih segmen adalah indeks ke dalam GDT. Kernel mendefinisikan GDT sebagai larik C, menggunakan sintaks penginisialisasi yang ditunjuk seperti [GDT_ENTRY_KERNEL32_CS] = initializer_for_that_selector
.
(Sebenarnya 2 bit rendah dari pemilih, yaitu nilai register segmen, adalah tingkat hak istimewa saat ini. Jadi GDT_ENTRY_DEFAULT_USER_CS
seharusnya `__USER_CS>> 2.)
mov ds, eax
memicu perangkat keras untuk mengindeks GDT, bukan menelusurinya secara linear untuk mencocokkan data di memori!
format data GDT:
Anda sedang melihat kode sumber Linux x86-64, jadi kernel akan berada dalam mode lama, bukan mode terlindungi. Kami tahu karena ada entri terpisah untuk USER_CS
dan USER32_CS
. Deskriptor segmen kode 32-bit akan memiliki L
sedikit dibersihkan. Deskripsi segmen CS saat ini adalah yang menempatkan CPU x86-64 ke mode compat 32-bit vs. mode panjang 64-bit. Untuk memasuki ruang pengguna 32-bit, sebuah iret
atau sysret
akan menyetel CS:RIP ke pemilih segmen mode pengguna 32-bit.
Saya berpikir Anda juga dapat memiliki CPU dalam mode compat 16-bit (seperti mode compat bukan mode nyata, tetapi ukuran operan dan ukuran alamat default adalah 16). Namun, Linux tidak melakukan ini.
Pokoknya, seperti yang dijelaskan di https://wiki.osdev.org/Global_Descriptor_Table dan Segmentasi,
Setiap deskriptor segmen berisi informasi berikut:
- Alamat dasar segmen
- Ukuran operasi default di segmen (16-bit/32-bit)
- Tingkat hak istimewa deskriptor (Dering 0 -> Dering 3)
- Perincian (Batas segmen dalam satuan byte/4 kb)
- Batas segmen (Pengimbangan hukum maksimum dalam segmen)
- Keberadaan segmen (Apakah ada atau tidak)
- Jenis deskriptor (0 =sistem; 1 =kode/data)
- Jenis segmen (Kode/Data/Baca/Tulis/Diakses/Sesuai/Tidak Sesuai/Perluas/Perluas-Ke Bawah)
Ini adalah bit tambahan. Saya tidak terlalu tertarik dengan bit yang mana karena saya (rasanya saya) memahami gambaran tingkat tinggi tentang tujuan entri GDT yang berbeda dan apa fungsinya, tanpa membahas detail tentang bagaimana sebenarnya itu dikodekan.
Tetapi jika Anda memeriksa manual x86 atau osdev wiki, dan definisi untuk makro init tersebut, Anda akan menemukan bahwa makro ini menghasilkan entri GDT dengan L
bit diatur untuk segmen kode 64-bit, dibersihkan untuk segmen kode 32-bit. Dan jelas jenis (kode vs. data) dan tingkat hak istimewa berbeda.
Penafian
Saya memposting jawaban ini untuk membersihkan topik ini dari kesalahpahaman (seperti yang ditunjukkan oleh @PeterCordes).
Paging
Manajemen memori di Linux (x86 protected mode) menggunakan paging untuk memetakan alamat fisik ke flat tervirtualisasi ruang alamat linear, dari 0x00000000
ke 0xFFFFFFFF
(pada 32-bit), dikenal sebagai model memori datar . Linux, bersama dengan MMU (Memory Management Unit) CPU, akan memelihara setiap alamat virtual dan logis yang dipetakan 1:1 ke alamat fisik yang sesuai. Memori fisik biasanya dibagi menjadi 4KiB halaman, untuk memudahkan pengelolaan memori.
Alamat virtual kernel dapat berdekatan kernel logis alamat langsung dipetakan ke halaman fisik yang berdekatan; alamat virtual kernel lainnya sepenuhnya alamat virtual yang dipetakan dalam halaman fisik tidak bersebelahan yang digunakan untuk alokasi buffer besar (melebihi area bersebelahan pada sistem memori kecil) dan/atau memori PAE (hanya 32-bit). Port MMIO (Memory-Mapping I/O) juga dipetakan menggunakan alamat virtual kernel.
Setiap alamat dereferensi harus menjadi alamat virtual. Entah itu alamat logis atau sepenuhnya virtual, RAM fisik dan port MMIO dipetakan di ruang alamat virtual sebelum digunakan.
Kernel memperoleh sepotong memori virtual menggunakan kmalloc()
, ditunjukkan oleh alamat virtual, tetapi yang lebih penting, juga alamat logika kernel, artinya memiliki pemetaan langsung ke bersebelahan halaman fisik (jadi cocok untuk DMA). Di sisi lain, vmalloc()
rutin akan mengembalikan sebagian sepenuhnya memori virtual, ditunjukkan oleh alamat virtual, tetapi hanya bersebelahan pada ruang alamat virtual dan dipetakan ke halaman fisik yang tidak bersebelahan.
Alamat logis kernel menggunakan pemetaan tetap antara ruang alamat fisik dan virtual. Ini berarti daerah yang hampir bersebelahan secara alami juga bersebelahan secara fisik. Tidak demikian halnya dengan alamat virtual sepenuhnya, yang mengarah ke halaman fisik yang tidak bersebelahan.
alamat virtual pengguna - tidak seperti alamat logis kernel - tidak menggunakan pemetaan tetap antara alamat virtual dan fisik, proses userland memanfaatkan MMU sepenuhnya:
- Hanya bagian memori fisik yang digunakan yang dipetakan;
- Memori tidak bersebelahan;
- Memori dapat ditukar;
- Memori dapat dipindahkan;
Lebih jelasnya, halaman memori fisik 4KiB dipetakan ke alamat virtual di tabel halaman OS, masing-masing pemetaan dikenal sebagai PTE (Page Table Entry). MMU CPU kemudian akan menyimpan cache dari setiap PTE yang baru saja digunakan dari tabel halaman OS. Area caching ini dikenal sebagai TLB (Translation Lookaside Buffer). cr3
register digunakan untuk menemukan tabel halaman OS.
Setiap kali alamat virtual perlu diterjemahkan menjadi alamat fisik, TLB akan dicari. Jika ditemukan kecocokan (TLB hit ), alamat fisik dikembalikan dan diakses. Namun, jika tidak ada kecocokan (TLB miss ), pengendali miss TLB akan mencari tabel halaman untuk melihat apakah ada pemetaan (halaman berjalan ). Jika ada, itu ditulis kembali ke TLB dan instruksi yang salah dimulai kembali, terjemahan selanjutnya ini akan menemukan TLB hit dan akses memori akan berlanjut. Ini dikenal sebagai di bawah umur kesalahan halaman.
Terkadang, OS mungkin perlu menambah ukuran RAM fisik dengan memindahkan halaman ke dalam hard disk. Jika alamat virtual menyelesaikan ke halaman yang dipetakan di hard disk, halaman tersebut perlu dimuat di RAM fisik sebelum diakses. Ini dikenal sebagai jurusan kesalahan halaman. Penanganan kesalahan halaman OS kemudian perlu menemukan halaman kosong di memori.
Proses translasi bisa gagal jika tidak ada pemetaan yang tersedia untuk alamat virtual, artinya alamat virtual tidak valid. Ini dikenal sebagai tidak valid pengecualian kesalahan halaman, dan segfault akan dikeluarkan untuk proses oleh penangan kesalahan halaman OS.
Segmentasi memori
Mode nyata
Mode nyata masih menggunakan ruang alamat memori tersegmentasi 20-bit, dengan 1MiB memori yang dapat dialamatkan (0x00000 - 0xFFFFF
) dan akses perangkat lunak langsung tanpa batas ke semua memori yang dapat dialamatkan, alamat bus, port PMIO (I/O yang Dipetakan Port) dan perangkat keras periferal. Mode nyata memberikan tidak ada perlindungan memori , tidak ada tingkat hak istimewa dan tidak ada alamat virtual. Biasanya, register segmen berisi nilai pemilih segmen, dan operan memori adalah nilai offset relatif terhadap basis segmen.
Untuk mengatasi segmentasi (kompiler C biasanya hanya mendukung model memori datar), kompiler C menggunakan far
tidak resmi jenis pointer untuk mewakili alamat fisik dengan segment:offset
notasi alamat logis. Misalnya, alamat logis 0x5555:0x0005
, setelah menghitung 0x5555 * 16 + 0x0005
menghasilkan alamat fisik 20-bit 0x55555
, dapat digunakan dalam penunjuk jauh seperti yang ditunjukkan di bawah ini:
char far *ptr; /* declare a far pointer */
ptr = (char far *)0x55555; /* initialize a far pointer */
Sampai hari ini, sebagian besar CPU x86 modern masih memulai dalam mode nyata untuk kompatibilitas mundur dan beralih ke mode terlindungi setelahnya.
Mode terlindungi
Dalam mode terproteksi, dengan model memori datar , segmentasi tidak digunakan . Keempat segmen tersebut, yaitu __KERNEL_CS
, __KERNEL_DS
, __USER_CS
, __USER_DS
semua memiliki alamat dasar yang disetel ke 0. Segmen ini hanyalah bagasi lama dari model x86 sebelumnya di mana manajemen memori tersegmentasi digunakan. Dalam mode terproteksi, karena semua alamat dasar segmen disetel ke 0, alamat logis setara ke alamat linier.
Mode terlindungi dengan model memori datar berarti tidak ada segmentasi. Satu-satunya pengecualian di mana segmen memiliki alamat dasarnya yang disetel ke nilai selain 0 adalah ketika penyimpanan lokal thread terlibat.
FS
(danGS
pada 64-bit) register segmen digunakan untuk tujuan ini.
Namun, register segmen seperti SS
(register segmen tumpukan), DS
(register segmen data) atau CS
(register segmen kode) masih ada dan digunakan untuk menyimpan pemilih segmen 16-bit , yang berisi indeks untuk menyegmentasikan deskriptor di LDT dan GDT (Tabel Deskriptor Lokal &Global).
Setiap instruksi yang menyentuh memori secara implisit menggunakan register segmen. Bergantung pada konteksnya, register segmen tertentu digunakan. Misalnya, JMP
instruksi menggunakan CS
sedangkan PUSH
menggunakan SS
. Pemilih dapat dimuat ke dalam register dengan instruksi seperti MOV
, satu-satunya pengecualian adalah CS
register yang hanya dimodifikasi oleh instruksi yang memengaruhi aliran eksekusi , seperti CALL
atau JMP
.
CS
register sangat berguna karena melacak CPL (Tingkat Privilege Saat Ini) di pemilih segmennya, sehingga menghemat tingkat hak istimewa untuk segmen saat ini. Nilai CPL 2 bit ini adalah selalu setara dengan tingkat hak istimewa CPU saat ini.
Perlindungan memori
Paging
Tingkat hak istimewa CPU, juga dikenal sebagai bit mode atau cincin perlindungan , dari 0 hingga 3, membatasi beberapa instruksi yang dapat menumbangkan mekanisme perlindungan atau menyebabkan kekacauan jika diizinkan dalam mode pengguna, sehingga dicadangkan ke kernel. Upaya untuk menjalankannya di luar ring 0 menyebabkan perlindungan umum pengecualian kesalahan, skenario yang sama ketika terjadi kesalahan akses segmen yang tidak valid (hak istimewa, jenis, batas, hak baca/tulis). Demikian pula, setiap akses ke memori dan perangkat MMIO dibatasi berdasarkan tingkat hak istimewa dan setiap upaya untuk mengakses halaman yang dilindungi tanpa tingkat hak istimewa yang diperlukan akan menyebabkan pengecualian kesalahan halaman.
Bit mode akan dialihkan secara otomatis dari mode pengguna ke mode supervisor setiap kali ada permintaan interupsi (IRQ), baik perangkat lunak (mis. syscall ) atau perangkat keras, terjadi.
Pada sistem 32-bit, hanya memori 4GiB yang dapat ditangani secara efektif, dan memori dibagi dalam bentuk 3GiB/1GiB. Linux (dengan paging diaktifkan) menggunakan skema perlindungan yang dikenal sebagai kernel separuh atas di mana ruang pengalamatan datar dibagi menjadi dua rentang alamat virtual:
-
Alamat dalam rentang
0xC0000000 - 0xFFFFFFFF
adalah alamat virtual kernel (area merah). Kisaran 896MiB0xC0000000 - 0xF7FFFFFF
langsung memetakan alamat logis kernel 1:1 dengan alamat fisik kernel ke memori rendah yang bersebelahan halaman (menggunakan__pa()
dan__va()
makro). Rentang 128MiB yang tersisa0xF8000000 - 0xFFFFFFFF
kemudian digunakan untuk memetakan alamat virtual untuk alokasi buffer besar, port MMIO (Memory-Mapped I/O), dan/atau memori PAE ke memori tinggi yang tidak bersebelahan halaman (menggunakanioremap()
daniounmap()
). -
Alamat dalam rentang
0x00000000 - 0xBFFFFFFF
adalah alamat virtual pengguna (area hijau), tempat kode userland, data, dan perpustakaan berada. Pemetaan dapat berada di halaman memori rendah dan memori tinggi yang tidak bersebelahan.
Memori tinggi hanya ada pada sistem 32-bit. Semua memori dialokasikan dengan
kmalloc()
memiliki logis alamat virtual (dengan pemetaan fisik langsung); memori yang dialokasikan olehvmalloc()
memiliki sepenuhnya alamat virtual (tetapi tidak ada pemetaan fisik langsung). Sistem 64-bit memiliki kemampuan pengalamatan yang sangat besar sehingga tidak memerlukan memori tinggi, karena setiap halaman RAM fisik dapat ditangani secara efektif.
Batas alamat antara bagian atas supervisor dan bagian bawah userland dikenal sebagai TASK_SIZE_MAX
di kernel Linux. Kernel akan memeriksa bahwa setiap alamat virtual yang diakses dari setiap proses userland berada di bawah batas tersebut, seperti terlihat pada kode di bawah ini:
static int fault_in_kernel_space(unsigned long address)
{
/*
* On 64-bit systems, the vsyscall page is at an address above
* TASK_SIZE_MAX, but is not considered part of the kernel
* address space.
*/
if (IS_ENABLED(CONFIG_X86_64) && is_vsyscall_vaddr(address))
return false;
return address >= TASK_SIZE_MAX;
}
Jika proses userland mencoba mengakses alamat memori yang lebih tinggi dari TASK_SIZE_MAX
, do_kern_addr_fault()
rutin akan memanggil __bad_area_nosemaphore()
rutin, akhirnya menandakan tugas yang salah dengan SIGSEGV
(menggunakan get_current()
untuk mendapatkan task_struct
):
/*
* To avoid leaking information about the kernel page table
* layout, pretend that user-mode accesses to kernel addresses
* are always protection faults.
*/
if (address >= TASK_SIZE_MAX)
error_code |= X86_PF_PROT;
force_sig_fault(SIGSEGV, si_code, (void __user *)address, tsk); /* Kill the process */
Laman juga memiliki sedikit hak istimewa, yang dikenal sebagai U ser/Supervisor, digunakan untuk SMAP (Supervisor Mode Access Prevention) selain tanda R ead/Write flag yang digunakan SMEP (Supervisor Mode Execution Prevention).
Segmentasi
Arsitektur lama yang menggunakan segmentasi biasanya melakukan verifikasi akses segmen menggunakan bit hak istimewa GDT untuk setiap segmen yang diminta. Bit hak istimewa dari segmen yang diminta, dikenal sebagai DPL (Descriptor Privilege Level), dibandingkan dengan CPL segmen saat ini, memastikan bahwa CPL <= DPL
. Jika benar, akses memori kemudian diizinkan ke segmen yang diminta.