x86_64
. We also found that the loader has already configured a hierarchy of page tables for our kernel, so the kernel works on virtual addresses. This increases security because unauthorized memory access causes a page fault instead of an arbitrary change in physical memory.bootloader
version 0.4.0 or higher and x86_64
version 0.5.2 or higher. You can update dependencies in Cargo.toml
: [dependencies] bootloader = "0.4.0" x86_64 = "0.5.2"
4 KiB
, we get access to the virtual address 4 KiB
, and not to the physical address where the table of the 4th level pages is stored. If we want to access the physical address of 4 KiB
, then we need to use a virtual address that is translated into it.28 KiB
region, because it will rest on the already occupied page at 1004 KiB
. Therefore, we will have to look further until we find a suitable large fragment, for example, with 1008 KiB
. The same problem of fragmentation arises as in segmented memory.1008 KiB
. Now we can no longer use any frame with a physical address between 1000 KiB
and 2008 KiB
, because it cannot be identically displayed.x86_64
, because the 48-bit address space is 256 TiB.8
translates the virtual page at 32 KiB
into the physical frame at 32 KiB
, thereby identically displaying the level 1 table itself. The figure shows this with a horizontal arrow.24 KiB
. This creates a temporary mapping of the virtual page at 0 KiB
address 0 KiB
with the physical frame of the level 2 page table, indicated by the dotted arrow.4 KiB
. This creates a temporary mapping of the virtual page at 36 KiB
with the physical frame of the level 4 page table, indicated by the dotted arrow.0 KiB
and a table of level 4 by writing to a page that starts at 33 KiB
.511
in the table of level 4, which is matched with the physical frame 4 KiB
, which is in the table itself.Virtual address for | Address structure ( octal ) |
---|---|
Page | 0o_SSSSSS_AAA_BBB_CCC_DDD_EEEE |
Entry in table level 1 | 0o_SSSSSS_RRR_AAA_BBB_CCC_DDDD |
Entry in table level 2 | 0o_SSSSSS_RRR_RRR_AAA_BBB_CCCC |
Entry in table level 3 | 0o_SSSSSS_RRR_RRR_RRR_AAA_BBBB |
Entry in table level 4 | 0o_SSSSSS_RRR_RRR_RRR_RRR_AAAA |
is a level 4 index,
is level 3,
is level 2, and DDD
is level 1 index for the displayed frame, EEEE
is its offset. RRR
- recursive write index. The index (three digits) is converted to an offset (four digits) by multiplying by 8 (the size of the page table entry). At this offset, the resulting address directly points to the corresponding page table entry.SSSS
are the bits of the sign bit extension, that is, they are all copies of bit 47. This is a special requirement for valid addresses in the x86_64 architecture, which we discussed in the previous article . // the virtual address whose corresponding page tables you want to access let addr: usize = […]; 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);
0o777
(511) recursively mapped. This is currently not the case, so the code will not work yet. See below for how to tell the loader to set a recursive mapping.x86_64
crate type RecursivePageTable
, which provides safe abstractions for various table operations. For example, the code below shows how to convert a virtual address to its corresponding physical address: // 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. let level_4_table_addr = […]; let level_4_table_ptr = level_4_table_addr as *mut PageTable; let recursive_page_table = unsafe { let level_4_table = &mut *level_4_table_ptr; RecursivePageTable::new(level_4_table).unwrap(); } /// Retrieve the physical address for the given virtual address let addr: u64 = […] 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()))
level_4_table_addr
calculated as in the first code example.bootloader
crate supports the two aforementioned approaches using cargo functions :map_physical_memory
function maps total physical memory somewhere in a virtual address space. Thus, the kernel gains access to all physical memory and can apply an approach with the display of full physical memory .recursive_page_table
function, the loader recursively displays the fourth-level page table entry. This allows the kernel to work according to the method described in the Recursive Page Tables section .map_physical_memory
: [dependencies] bootloader = { version = "0.4.0", features = ["map_physical_memory"]}
bootloader
defines a BootInfo structure with all the information passed to the kernel. The structure is still being finalized, so some failures are possible when upgrading to future versions that are incompatible with semver . Currently, the structure has two fields: memory_map
and physical_memory_offset
:memory_map
contains an overview of the available physical memory. It tells the kernel how much physical memory is available in the system and which areas of memory are reserved for devices, such as VGA. You can request a memory card from the BIOS or UEFI firmware, but only at the very beginning of the boot process. For this reason, it must be provided by the loader, because then the kernel will no longer be able to obtain this information. The memory card is useful to us later in this article.physical_memory_offset
reports the virtual starting address of the physical memory mapping. By adding this offset to the physical address, we get the corresponding virtual address. This gives access from the kernel to arbitrary physical memory.BootInfo
to the kernel as an argument &'static BootInfo
to the function _start
. Add it: // in src/main.rs use bootloader::BootInfo; #[cfg(not(test))] #[no_mangle] pub extern "C" fn _start(boot_info: &'static BootInfo) -> ! { // new argument […] }
_start
is called externally from the loader, the signature of the function is not checked. This means that we can allow it to take arbitrary arguments without compiling errors, but this will lead to a failure or cause undefined behavior in runtime.bootloader
provides a macro entry_point
. Rewrite our function using this macro: // in src/main.rs use bootloader::{BootInfo, entry_point}; entry_point!(kernel_main); #[cfg(not(test))] fn kernel_main(boot_info: &'static BootInfo) -> ! { […] }
extern "C"
or no_mangle
because the macro defines for us the real entry point of the lower level _start
. The function has kernel_main
now become a completely normal Rust function, so we can choose an arbitrary name for it. It is important that it is checked by type, so if you use the wrong signature, for example, by adding an argument or changing its type, a compilation error will occurmemory
: // in src/lib.rs pub mod memory;
src/memory.rs
.CR3
. Now we can continue working from this place: the function active_level_4_table
will return a link to the active fourth-level page table: // in src/memory.rs use x86_64::structures::paging::PageTable; /// Returns a mutable reference to the active level 4 table. /// /// This function is unsafe because the caller must guarantee that the /// complete physical memory is mapped to virtual memory at the passed /// `physical_memory_offset`. Also, this function must be only called once /// to avoid aliasing `&mut` references (which is undefined behavior). pub unsafe fn active_level_4_table(physical_memory_offset: u64) -> &'static mut PageTable { use x86_64::{registers::control::Cr3, VirtAddr}; let (level_4_table_frame, _) = Cr3::read(); let phys = level_4_table_frame.start_address(); let virt = VirtAddr::new(phys.as_u64() + physical_memory_offset); let page_table_ptr: *mut PageTable = virt.as_mut_ptr(); &mut *page_table_ptr // unsafe }
CR3
. Then we take its physical starting address and convert it to a virtual address by adding physical_memory_offset
. Finally, we translate the address into a raw pointer with a *mut PageTable
method as_mut_ptr
, and then unsafely create a link from it &mut PageTable
. We create a link &mut
instead &
, because later in the article we will modify these page tables.unsafe fn
as one big unsafe block. This increases the risk, because you can accidentally enter an unsafe operation in the previous lines. This also makes detection of unsafe operations much more difficult. An RFC has already been created to change this behavior. Rust. // in src/main.rs #[cfg(not(test))] fn kernel_main(boot_info: &'static BootInfo) -> ! { […] // initialize GDT, IDT, PICS use blog_os::memory::active_level_4_table; let l4_table = unsafe { active_level_4_table(boot_info.physical_memory_offset) }; for (i, entry) in l4_table.iter().enumerate() { if !entry.is_unused() { println!("L4 Entry {}: {:?}", i, entry); } } println!("It did not crash!"); blog_os::hlt_loop(); }
physical_memory_offset
transfer the corresponding field of structure BootInfo
. Then we use the function iter
to iterate over the page table entries and the combinator enumerate
to add an index i
to each element. We display only non-empty records, because all 512 records will not fit on the screen. // in the for loop in src/main.rs use x86_64::{structures::paging::PageTable, VirtAddr}; if !entry.is_unused() { println!("L4 Entry {}: {:?}", i, entry); // get the physical address from the entry and convert it let phys = entry.frame().unwrap().start_address(); let virt = phys.as_u64() + boot_info.physical_memory_offset; let ptr = VirtAddr::new(virt).as_mut_ptr(); let l3_table: &PageTable = unsafe { &*ptr }; // print non-empty entries of the level 3 table for (i, entry) in l3_table.iter().enumerate() { if !entry.is_unused() { println!(" L3 Entry {}: {:?}", i, entry); } } }
// in src/memory.rs use x86_64::{PhysAddr, VirtAddr}; /// Translates the given virtual address to the mapped physical address, or /// `None` if the address is not mapped. /// /// This function is unsafe because the caller must guarantee that the /// complete physical memory is mapped to virtual memory at the passed /// `physical_memory_offset`. pub unsafe fn translate_addr(addr: VirtAddr, physical_memory_offset: u64) -> Option<PhysAddr> { translate_addr_inner(addr, physical_memory_offset) }
translate_addr_inner
to limit the amount of unsafe code. As noted above, Rust regards the whole body unsafe fn
as a big insecure unit. By calling one safe function, we again make every operation explicit unsafe
. // in src/memory.rs /// Private function that is called by `translate_addr`. /// /// This function is safe to limit the scope of `unsafe` because Rust treats /// the whole body of unsafe functions as an unsafe block. This function must /// only be reachable through `unsafe fn` from outside of this module. fn translate_addr_inner(addr: VirtAddr, physical_memory_offset: u64) -> Option<PhysAddr> { use x86_64::structures::paging::page_table::FrameError; use x86_64::registers::control::Cr3; // read the active level 4 frame from the CR3 register let (level_4_table_frame, _) = Cr3::read(); let table_indexes = [ addr.p4_index(), addr.p3_index(), addr.p2_index(), addr.p1_index() ]; let mut frame = level_4_table_frame; // traverse the multi-level page table for &index in &table_indexes { // convert the frame into a page table reference let virt = frame.start_address().as_u64() + physical_memory_offset; let table_ptr: *const PageTable = VirtAddr::new(virt).as_ptr(); let table = unsafe {&*table_ptr}; // read the page table entry and update `frame` let entry = &table[index]; frame = match entry.frame() { Ok(frame) => frame, Err(FrameError::FrameNotPresent) => return None, Err(FrameError::HugeFrame) => panic!("huge pages not supported"), }; } // calculate the physical address by adding the page offset Some(frame.start_address() + u64::from(addr.page_offset())) }
active_level_4_table
we reread the fourth level frame from the register CR3
, because this simplifies the implementation of the prototype. Do not worry, we will soon improve the solution.VirtAddr
already provides methods for calculating indexes in four-level page tables. We store these indexes in a small array, because it allows you to go through all the tables using a loop for
. Outside the loop, remember the last visited frame to later calculate the physical address. frame
points to the frames of the page table during the iteration and the mapped frame after the last iteration, i.e. after passing the level 1 record.physical_memory_offset
to convert a frame into a link to a page table. Then we read the record of the current page table and use the function PageTableEntry::frame
to extract the matched frame. If the record is not associated with the frame, return None
. If the entry displays a huge 2 MiB or 1 GiB page, for now we’ll have a panic. // in src/main.rs #[cfg(not(test))] fn kernel_main(boot_info: &'static BootInfo) -> ! { […] // initialize GDT, IDT, PICS use blog_os::memory::translate_addr; use x86_64::VirtAddr; let addresses = [ // the identity-mapped vga buffer page 0xb8000, // some code page 0x20010a, // some stack page 0x57ac_001f_fe48, // virtual address mapped to physical address 0 boot_info.physical_memory_offset, ]; for &address in &addresses { let virt = VirtAddr::new(address); let phys = unsafe { translate_addr(virt, boot_info.physical_memory_offset) }; println!("{:?} -> {:?}", virt, phys); } println!("It did not crash!"); blog_os::hlt_loop(); }
0xb8000
converted to the same physical address. The code page and the stack page are converted to arbitrary physical addresses, which depend on how the loader created the initial mapping for our kernel. The mapping physical_memory_offset
must point to a physical address 0
, but fails, because the translation uses huge pages for efficiency. A future bootloader version can apply the same optimization for the kernel and stack pages.x86_64
provides an abstraction for it. It already supports huge pages and several other features besides translate_addr
, so we use it instead of adding support for large pages to our own implementation.Mapper
provides functions that work on the pages. For example, translate_page
to translate this page into a frame of the same size, as well as map_to
to create a new mapping in the table.MapperAllSizes
implies application Mapper
for all page sizes. In addition, it provides features that work with pages of different sizes, including translate_addr
or common translate
.x86_64
provides two types that implement traits: MappedPageTable
and RecursivePageTable
. The first requires that each frame of the page table be displayed somewhere (for example, with an offset). The second type can be used if the table of the fourth level is displayed recursively.physical_memory_offset
, so you can use the type MappedPageTable. To initialize it, create a new function init
in the module memory
: use x86_64::structures::paging::{PhysFrame, MapperAllSizes, MappedPageTable}; use x86_64::PhysAddr; /// Initialize a new MappedPageTable. /// /// This function is unsafe because the caller must guarantee that the /// complete physical memory is mapped to virtual memory at the passed /// `physical_memory_offset`. Also, this function must be only called once /// to avoid aliasing `&mut` references (which is undefined behavior). pub unsafe fn init(physical_memory_offset: u64) -> impl MapperAllSizes { let level_4_table = active_level_4_table(physical_memory_offset); let phys_to_virt = move |frame: PhysFrame| -> *mut PageTable { let phys = frame.start_address().as_u64(); let virt = VirtAddr::new(phys + physical_memory_offset); virt.as_mut_ptr() }; MappedPageTable::new(level_4_table, phys_to_virt) } // make private unsafe fn active_level_4_table(physical_memory_offset: u64) -> &'static mut PageTable {…}
MappedPageTable
from a function, because it is common for the type of closure. We will circumvent this problem with the help of the syntax impl Trait
. An additional advantage is that you can then switch the kernel to RecursivePageTable
without changing the function signature.MappedPageTable::new
expects two parameters: a modifiable reference to the table of the level 4 pages and a closure phys_to_virt
that converts the physical frame into a page table index *mut PageTable
. For the first parameter, we can reuse the function active_level_4_table
. For the second, we create a closure that uses physical_memory_offset
to perform the transformation.active_level_4_table
private function, because from now on it will only be called from init
.MapperAllSizes::translate_addr
instead of our own function memory::translate_addr
, you need to change just a few lines to kernel_main
: // in src/main.rs #[cfg(not(test))] fn kernel_main(boot_info: &'static BootInfo) -> ! { […] // initialize GDT, IDT, PICS // new: different imports use blog_os::memory; use x86_64::{structures::paging::MapperAllSizes, VirtAddr}; // new: initialize a mapper let mapper = unsafe { memory::init(boot_info.physical_memory_offset) }; let addresses = […]; // same as before for &address in &addresses { let virt = VirtAddr::new(address); // new: use the `mapper.translate_addr` method let phys = mapper.translate_addr(virt); println!("{:?} -> {:?}", virt, phys); } println!("It did not crash!"); blog_os::hlt_loop(); }
physical_memory_offset
converted to a physical address 0x0
. Using the broadcast function for the type MappedPageTable
, we eliminate the need to implement support for huge pages. We also have access to other page functions, such as map_to
that we will use in the next section. At this stage, we no longer need the function memory::translate_addr
, you can delete it if you want.map_to
from the trait Mapper
, so first consider this function. The documentation says that it requires four arguments: the page we want to display; frame to which the page should be matched; a set of flags for writing the page table and the frame allocator frame_allocator
. The frame allocator is necessary, since the mapping of this page may require the creation of additional tables that need unused frames as backup storage.create_example_mapping
create_example_mapping
that maps this page to the 0xb8000
physical frame of the VGA text buffer. We select this frame because it allows you to easily check whether the display was created correctly: we just need to write to the newly displayed page and see if it appears on the screen.create_example_mapping
looks like this: // in src/memory.rs use x86_64::structures::paging::{Page, Size4KiB, Mapper, FrameAllocator}; /// Creates an example mapping for the given page to frame `0xb8000`. pub fn create_example_mapping( page: Page, mapper: &mut impl Mapper<Size4KiB>, frame_allocator: &mut impl FrameAllocator<Size4KiB>, ) { use x86_64::structures::paging::PageTableFlags as Flags; let frame = PhysFrame::containing_address(PhysAddr::new(0xb8000)); let flags = Flags::PRESENT | Flags::WRITABLE; let map_to_result = unsafe { mapper.map_to(page, frame, flags, frame_allocator) }; map_to_result.expect("map_to failed").flush(); }
page
to be matched, the function expects an instance mapper
and frame_allocator
. The type mapper
implements the treyt Mapper<Size4KiB>
that the method provides map_to
. The common parameter is Size4KiB
necessary because the trait Mapper
is common to the trait PageSize
, working with both standard 4 KiB pages and huge 2 MiB and 1 GiB pages. We want to create only 4 KiB pages, so we can use Mapper<Size4KiB>
instead of the requirement MapperAllSizes
.PRESENT
, since it is required for all valid entries, and the flag WRITABLE
to make the displayed page writable. Callmap_to
unsafe: you can violate memory security with invalid arguments, so you have to use a block unsafe
. For a list of all possible flags, see the “Page Table Format” section of the previous article .map_to
may fail, so it returns Result
. Since this is just an example of code that should not be reliable, we simply use it expect
to panic in case of an error. If successful, the function returns a type MapperFlush
that provides an easy way to clear the newly displayed page from the dynamic translation buffer (TLB) using the method flush
. As well Result
, this type applies the [ #[must_use]
] attribute toissue a warning if we accidentally forget to use it .FrameAllocator
create_example_mapping
, you must first create FrameAllocator
. As noted above, the complexity of creating a new display depends on the virtual page that we want to display. In the simplest case, a level 1 table for the page already exists, and we only need to make one entry. In the most difficult case, the page is in the memory area for which level 3 has not yet been created, so you must first create page tables of level 3, 2 and 1.None
. We create this EmptyFrameAllocator
to test the display function: // 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 } }
0x1000
.0x1000
, and then display the contents of the memory: // in src/main.rs #[cfg(not(test))] fn kernel_main(boot_info: &'static BootInfo) -> ! { […] // initialize GDT, IDT, PICS use blog_os::memory; use x86_64::{structures::paging::Page, VirtAddr}; let mut mapper = unsafe { memory::init(boot_info.physical_memory_offset) }; let mut frame_allocator = memory::EmptyFrameAllocator; // map a previously unmapped page let page = Page::containing_address(VirtAddr::new(0x1000)); memory::create_example_mapping(page, &mut mapper, &mut frame_allocator); // write the string `New!` to the screen through the new mapping let page_ptr: *mut u64 = page.start_address().as_mut_ptr(); unsafe { page_ptr.offset(400).write_volatile(0x_f021_f077_f065_f04e)}; println!("It did not crash!"); blog_os::hlt_loop(); }
0x1000
by calling the function create_example_mapping
with the variable reference to the instances mapper
and frame_allocator
. This matches the page 0x1000
with the VGA text buffer frame, so we need to see what is written there on the screen.400
. We do not write to the top of the page because the top line of the VGA buffer directly shifts from the screen as follows println
. Write the value 0x_f021_f077_f065_f04e
that corresponds to the string “New!” On a white background. As we learned in the VGA Text Mode article , writing to the VGA buffer should be volatile, so we use the method write_volatile
.0x1000
, the inscription “New!” Appeared on the screen . So, we have successfully created a new mapping in the page tables.0x1000
. When we try to match a page for which a level 1 table does not exist yet, the function map_to
fails because it tries to allocate frames from EmptyFrameAllocator
to create new tables. We see that this happens when we try to display the page 0xdeadbeaf000
instead of 0x1000
: // in src/main.rs #[cfg(not(test))] fn kernel_main(boot_info: &'static BootInfo) -> ! { […] let page = Page::containing_address(VirtAddr::new(0xdeadbeaf000)); […] }
panicked at 'map_to failed: FrameAllocationFailed', /…/result.rs:999:5
FrameAllocator
. But how do you know which frames are free and how much physical memory is available? // 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() } }
frames
. alloc
Iterator::next
.BootInfoFrameAllocator
memory_map
, BootInfo
. « » , BIOS/UEFI. , .MemoryRegion
, , (, , . .) . , , BootInfoFrameAllocator
.BootInfoFrameAllocator
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.step_by(4096)); // create `PhysFrame` types from the start addresses let frames = frame_addresses.map(|addr| { PhysFrame::containing_address(PhysAddr::new(addr)) }); BootInfoFrameAllocator { frames } }
MemoryMap
:iter
MemoryRegion
. filter
. , , , (, ) , InUse
. , , Usable
- .map
range Rust .into_iter
, 4096- step_by
. 4096 (= 4 ) — , . , . flat_map
map
, Iterator<Item = u64>
Iterator<Item = Iterator<Item = u64>>
.PhysFrame
, Iterator<Item = PhysFrame>
. BootInfoFrameAllocator
.kernel_main
, BootInfoFrameAllocator
EmptyFrameAllocator
: // in src/main.rs #[cfg(not(test))] fn kernel_main(boot_info: &'static BootInfo) -> ! { […] let mut frame_allocator = memory::init_frame_allocator(&boot_info.memory_map); […] }
map_to
:frame_allocator
.create_example_mapping
— , . .bootloader
cargo. &BootInfo
.MappedPageTable
x86_64
. , FrameAllocator
, .Source: https://habr.com/ru/post/445618/
All Articles