From 841b9deea336b45ed5255cb541b6f505f94aede4 Mon Sep 17 00:00:00 2001 From: woodyZootopia Date: Sat, 15 May 2021 22:52:11 +0900 Subject: [PATCH 01/11] Add the translation file and translated a bit --- .../09-paging-implementation/index.ja.md | 1005 +++++++++++++++++ 1 file changed, 1005 insertions(+) create mode 100644 blog/content/edition-2/posts/09-paging-implementation/index.ja.md diff --git a/blog/content/edition-2/posts/09-paging-implementation/index.ja.md b/blog/content/edition-2/posts/09-paging-implementation/index.ja.md new file mode 100644 index 00000000..73925812 --- /dev/null +++ b/blog/content/edition-2/posts/09-paging-implementation/index.ja.md @@ -0,0 +1,1005 @@ ++++ +title = "ページングの実装" +weight = 9 +path = "ja/paging-implementation" +date = 2019-03-14 + +[extra] +chapter = "Memory Management" +translation_based_on_commit = "bf4f88107966c7ab1327c3cdc0ebfbd76bad5c5f" +translators = ["woodyZootopia"] ++++ + +この記事では私達のカーネルをページングに対応させる方法についてお伝えします。まず物理ページテーブルのフレームにカーネルがアクセスできるようにする様々な方法を示し、それらの利点と欠点について議論します。次にアドレス変換関数を、ついで新しいマッピングを作るための関数を実装します。 + + + +このブログの内容は [GitHub] 上で公開・開発されています。何か問題や質問などがあれば issue をたててください (訳注: リンクは原文(英語)のものになります)。また[こちら][at the bottom]にコメントを残すこともできます。この記事の完全なソースコードは[`post-09` ブランチ][post branch]にあります。 + +[GitHub]: https://github.com/phil-opp/blog_os +[at the bottom]: #comments +[post branch]: https://github.com/phil-opp/blog_os/tree/post-09 + + + +## 導入 + +[1つ前の記事][previous post]ではページングの概念を説明しました。セグメンテーションと比較することによってページングのメリットを示し、ページングとページテーブルの仕組みを説明し、そして`x86_64`における4層ページテーブルの設計を導入しました。ブートローダはすでにページテーブルの階層構造を設定してしまっているので、私達のカーネルは既に仮想アドレス上で動いているということを学びました。これにより、不正なメモリアクセスは、任意の物理メモリを書き換えてしまう代わりにページフォルト例外を発生させるので、安全性が向上しています。 + +[previous post]: @/edition-2/posts/08-paging-introduction/index.ja.md + +記事の最後で、[ページテーブルにカーネルからアクセスできない][end of previous post]という問題が起きていました。この問題は、ページテーブルは物理メモリ内に格納されている一方、私達のカーネルは既に仮想アドレス上で実行されているために発生します。この記事ではその続きとして、私達のカーネルからページテーブルのフレームにアクセスするための様々な方法を探ります。それぞれの方法の利点と欠点を議論し、カーネルに採用する手法を決めます。 + +[end of previous post]: @/edition-2/posts/08-paging-introduction/index.md#accessing-the-page-tables + +To implement the approach, we will need support from the bootloader, so we'll configure it first. Afterward, we will implement a function that traverses the page table hierarchy in order to translate virtual to physical addresses. Finally, we learn how to create new mappings in the page tables and how to find unused memory frames for creating new page tables. + +## Accessing Page Tables + +Accessing the page tables from our kernel is not as easy as it may seem. To understand the problem let's take a look at the example 4-level page table hierarchy of the previous post again: + +![An example 4-level page hierarchy with each page table shown in physical memory](../paging-introduction/x86_64-page-table-translation.svg) + +The important thing here is that each page entry stores the _physical_ address of the next table. This avoids the need to run a translation for these addresses too, which would be bad for performance and could easily cause endless translation loops. + +The problem for us is that we can't directly access physical addresses from our kernel since our kernel also runs on top of virtual addresses. For example, when we access address `4 KiB` we access the _virtual_ address `4 KiB`, not the _physical_ address `4 KiB` where the level 4 page table is stored. When we want to access the physical address `4 KiB`, we can only do so through some virtual address that maps to it. + +So in order to access page table frames, we need to map some virtual pages to them. There are different ways to create these mappings that all allow us to access arbitrary page table frames. + +### Identity Mapping + +A simple solution is to **identity map all page tables**: + +![A virtual and a physical address space with various virtual pages mapped to the physical frame with the same address](identity-mapped-page-tables.svg) + +In this example, we see various identity-mapped page table frames. This way the physical addresses of page tables are also valid virtual addresses so that we can easily access the page tables of all levels starting from the CR3 register. + +However, it clutters the virtual address space and makes it more difficult to find continuous memory regions of larger sizes. For example, imagine that we want to create a virtual memory region of size 1000 KiB in the above graphic, e.g. for [memory-mapping a file]. We can't start the region at `28 KiB` because it would collide with the already mapped page at `1004 KiB`. So we have to look further until we find a large enough unmapped area, for example at `1008 KiB`. This is a similar fragmentation problem as with [segmentation]. + +[memory-mapping a file]: https://en.wikipedia.org/wiki/Memory-mapped_file +[segmentation]: @/edition-2/posts/08-paging-introduction/index.md#fragmentation + +Equally, it makes it much more difficult to create new page tables, because we need to find physical frames whose corresponding pages aren't already in use. For example, let's assume that we reserved the _virtual_ 1000 KiB memory region starting at `1008 KiB` for our memory-mapped file. Now we can't use any frame with a _physical_ address between `1000 KiB` and `2008 KiB` anymore, because we can't identity map it. + +### Map at a Fixed Offset + +To avoid the problem of cluttering the virtual address space, we can **use a separate memory region for page table mappings**. So instead of identity mapping page table frames, we map them at a fixed offset in the virtual address space. For example, the offset could be 10 TiB: + +![The same figure as for the identity mapping, but each mapped virtual page is offset by 10 TiB.](page-tables-mapped-at-offset.svg) + +By using the virtual memory in the range `10TiB..(10TiB + physical memory size)` exclusively for page table mappings, we avoid the collision problems of the identity mapping. Reserving such a large region of the virtual address space is only possible if the virtual address space is much larger than the physical memory size. This isn't a problem on x86_64 since the 48-bit address space is 256 TiB large. + +This approach still has the disadvantage that we need to create a new mapping whenever we create a new page table. Also, it does not allow accessing page tables of other address spaces, which would be useful when creating a new process. + +### Map the Complete Physical Memory + +We can solve these problems by **mapping the complete physical memory** instead of only page table frames: + +![The same figure as for the offset mapping, but every physical frame has a mapping (at 10TiB + X) instead of only page table frames.](map-complete-physical-memory.svg) + +This approach allows our kernel to access arbitrary physical memory, including page table frames of other address spaces. The reserved virtual memory range has the same size as before, with the difference that it no longer contains unmapped pages. + +The disadvantage of this approach is that additional page tables are needed for storing the mapping of the physical memory. These page tables need to be stored somewhere, so they use up a part of physical memory, which can be a problem on devices with a small amount of memory. + +On x86_64, however, we can use [huge pages] with size 2MiB for the mapping, instead of the default 4KiB pages. This way, mapping 32 GiB of physical memory only requires 132 KiB for page tables since only one level 3 table and 32 level 2 tables are needed. Huge pages are also more cache efficient since they use fewer entries in the translation lookaside buffer (TLB). + +[huge pages]: https://en.wikipedia.org/wiki/Page_%28computer_memory%29#Multiple_page_sizes + +### Temporary Mapping + +For devices with very small amounts of physical memory, we could **map the page tables frames only temporarily** when we need to access them. To be able to create the temporary mappings we only need a single identity-mapped level 1 table: + +![A virtual and a physical address space with an identity mapped level 1 table, which maps its 0th entry to the level 2 table frame, thereby mapping that frame to page with address 0](temporarily-mapped-page-tables.svg) + +The level 1 table in this graphic controls the first 2 MiB of the virtual address space. This is because it is reachable by starting at the CR3 register and following the 0th entry in the level 4, level 3, and level 2 page tables. The entry with index `8` maps the virtual page at address `32 KiB` to the physical frame at address `32 KiB`, thereby identity mapping the level 1 table itself. The graphic shows this identity-mapping by the horizontal arrow at `32 KiB`. + +By writing to the identity-mapped level 1 table, our kernel can create up to 511 temporary mappings (512 minus the entry required for the identity mapping). In the above example, the kernel created two temporary mappings: + +- By mapping the 0th entry of the level 1 table to the frame with address `24 KiB`, it created a temporary mapping of the virtual page at `0 KiB` to the physical frame of the level 2 page table, indicated by the dashed arrow. +- By mapping the 9th entry of the level 1 table to the frame with address `4 KiB`, it created a temporary mapping of the virtual page at `36 KiB` to the physical frame of the level 4 page table, indicated by the dashed arrow. + +Now the kernel can access the level 2 page table by writing to page `0 KiB` and the level 4 page table by writing to page `36 KiB`. + +The process for accessing an arbitrary page table frame with temporary mappings would be: + +- Search for a free entry in the identity-mapped level 1 table. +- Map that entry to the physical frame of the page table that we want to access. +- Access the target frame through the virtual page that maps to the entry. +- Set the entry back to unused thereby removing the temporary mapping again. + +This approach reuses the same 512 virtual pages for creating the mappings and thus requires only 4KiB of physical memory. The drawback is that it is a bit cumbersome, especially since a new mapping might require modifications of multiple table levels, which means that we would need to repeat the above process multiple times. + +### Recursive Page Tables + +Another interesting approach, that requires no additional page tables at all, is to **map the page table recursively**. The idea behind this approach is to map some entry of the level 4 page table to the level 4 table itself. By doing this, we effectively reserve a part of the virtual address space and map all current and future page table frames to that space. + +Let's go through an example to understand how this all works: + +![An example 4-level page hierarchy with each page table shown in physical memory. Entry 511 of the level 4 page is mapped to frame 4KiB, the frame of the level 4 table itself.](recursive-page-table.png) + +The only difference to the [example at the beginning of this post] is the additional entry at index `511` in the level 4 table, which is mapped to physical frame `4 KiB`, the frame of the level 4 table itself. + +[example at the beginning of this post]: #accessing-page-tables + +By letting the CPU follow this entry on a translation, it doesn't reach a level 3 table, but the same level 4 table again. This is similar to a recursive function that calls itself, therefore this table is called a _recursive page table_. The important thing is that the CPU assumes that every entry in the level 4 table points to a level 3 table, so it now treats the level 4 table as a level 3 table. This works because tables of all levels have the exact same layout on x86_64. + +By following the recursive entry one or multiple times before we start the actual translation, we can effectively shorten the number of levels that the CPU traverses. For example, if we follow the recursive entry once and then proceed to the level 3 table, the CPU thinks that the level 3 table is a level 2 table. Going further, it treats the level 2 table as a level 1 table and the level 1 table as the mapped frame. This means that we can now read and write the level 1 page table because the CPU thinks that it is the mapped frame. The graphic below illustrates the 5 translation steps: + +![The above example 4-level page hierarchy with 5 arrows: "Step 0" from CR4 to level 4 table, "Step 1" from level 4 table to level 4 table, "Step 2" from level 4 table to level 3 table, "Step 3" from level 3 table to level 2 table, and "Step 4" from level 2 table to level 1 table.](recursive-page-table-access-level-1.png) + +Similarly, we can follow the recursive entry twice before starting the translation to reduce the number of traversed levels to two: + +![The same 4-level page hierarchy with the following 4 arrows: "Step 0" from CR4 to level 4 table, "Steps 1&2" from level 4 table to level 4 table, "Step 3" from level 4 table to level 3 table, and "Step 4" from level 3 table to level 2 table.](recursive-page-table-access-level-2.png) + +Let's go through it step by step: First, the CPU follows the recursive entry on the level 4 table and thinks that it reaches a level 3 table. Then it follows the recursive entry again and thinks that it reaches a level 2 table. But in reality, it is still on the level 4 table. When the CPU now follows a different entry, it lands on a level 3 table but thinks it is already on a level 1 table. So while the next entry points at a level 2 table, the CPU thinks that it points to the mapped frame, which allows us to read and write the level 2 table. + +Accessing the tables of levels 3 and 4 works in the same way. For accessing the level 3 table, we follow the recursive entry three times, tricking the CPU into thinking it is already on a level 1 table. Then we follow another entry and reach a level 3 table, which the CPU treats as a mapped frame. For accessing the level 4 table itself, we just follow the recursive entry four times until the CPU treats the level 4 table itself as the mapped frame (in blue in the graphic below). + +![The same 4-level page hierarchy with the following 3 arrows: "Step 0" from CR4 to level 4 table, "Steps 1,2,3" from level 4 table to level 4 table, and "Step 4" from level 4 table to level 3 table. In blue the alternative "Steps 1,2,3,4" arrow from level 4 table to level 4 table.](recursive-page-table-access-level-3.png) + +It might take some time to wrap your head around the concept, but it works quite well in practice. + +In the section below we explain how to construct virtual addresses for following the recursive entry one or multiple times. We will not use recursive paging for our implementation, so you don't need to read it to continue with the post. If it interests you, just click on _"Address Calculation"_ to expand it. + +--- + +
+

Address Calculation

+ +We saw that we can access tables of all levels by following the recursive entry once or multiple times before the actual translation. Since the indexes into the tables of the four levels are derived directly from the virtual address, we need to construct special virtual addresses for this technique. Remember, the page table indexes are derived from the address in the following way: + +![Bits 0–12 are the page offset, bits 12–21 the level 1 index, bits 21–30 the level 2 index, bits 30–39 the level 3 index, and bits 39–48 the level 4 index](../paging-introduction/x86_64-table-indices-from-address.svg) + +Let's assume that we want to access the level 1 page table that maps a specific page. As we learned above, this means that we have to follow the recursive entry one time before continuing with the level 4, level 3, and level 2 indexes. To do that we move each block of the address one block to the right and set the original level 4 index to the index of the recursive entry: + +![Bits 0–12 are the offset into the level 1 table frame, bits 12–21 the level 2 index, bits 21–30 the level 3 index, bits 30–39 the level 4 index, and bits 39–48 the index of the recursive entry](table-indices-from-address-recursive-level-1.svg) + +For accessing the level 2 table of that page, we move each index block two blocks to the right and set both the blocks of the original level 4 index and the original level 3 index to the index of the recursive entry: + +![Bits 0–12 are the offset into the level 2 table frame, bits 12–21 the level 3 index, bits 21–30 the level 4 index, and bits 30–39 and bits 39–48 are the index of the recursive entry](table-indices-from-address-recursive-level-2.svg) + +Accessing the level 3 table works by moving each block three blocks to the right and using the recursive index for the original level 4, level 3, and level 2 address blocks: + +![Bits 0–12 are the offset into the level 3 table frame, bits 12–21 the level 4 index, and bits 21–30, bits 30–39 and bits 39–48 are the index of the recursive entry](table-indices-from-address-recursive-level-3.svg) + +Finally, we can access the level 4 table by moving each block four blocks to the right and using the recursive index for all address blocks except for the offset: + +![Bits 0–12 are the offset into the level l table frame and bits 12–21, bits 21–30, bits 30–39 and bits 39–48 are the index of the recursive entry](table-indices-from-address-recursive-level-4.svg) + +We can now calculate virtual addresses for the page tables of all four levels. We can even calculate an address that points exactly to a specific page table entry by multiplying its index by 8, the size of a page table entry. + +The table below summarizes the address structure for accessing the different kinds of frames: + +Virtual Address for | Address Structure ([octal]) +------------------- | ------------------------------- +Page | `0o_SSSSSS_AAA_BBB_CCC_DDD_EEEE` +Level 1 Table Entry | `0o_SSSSSS_RRR_AAA_BBB_CCC_DDDD` +Level 2 Table Entry | `0o_SSSSSS_RRR_RRR_AAA_BBB_CCCC` +Level 3 Table Entry | `0o_SSSSSS_RRR_RRR_RRR_AAA_BBBB` +Level 4 Table Entry | `0o_SSSSSS_RRR_RRR_RRR_RRR_AAAA` + +[octal]: https://en.wikipedia.org/wiki/Octal + +Whereas `AAA` is the level 4 index, `BBB` the level 3 index, `CCC` the level 2 index, and `DDD` the level 1 index of the mapped frame, and `EEEE` the offset into it. `RRR` is the index of the recursive entry. When an index (three digits) is transformed to an offset (four digits), it is done by multiplying it by 8 (the size of a page table entry). With this offset, the resulting address directly points to the respective page table entry. + +`SSSSSS` are sign extension bits, which means that they are all copies of bit 47. This is a special requirement for valid addresses on the x86_64 architecture. We explained it in the [previous post][sign extension]. + +[sign extension]: @/edition-2/posts/08-paging-introduction/index.md#paging-on-x86-64 + +We use [octal] numbers for representing the addresses since each octal character represents three bits, which allows us to clearly separate the 9-bit indexes of the different page table levels. This isn't possible with the hexadecimal system where each character represents four bits. + +##### In Rust Code + +To construct such addresses in Rust code, you can use bitwise operations: + +```rust +// 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); +``` + +The above code assumes that the last level 4 entry with index `0o777` (511) is recursively mapped. This isn't the case currently, so the code won't work yet. See below on how to tell the bootloader to set up the recursive mapping. + +Alternatively to performing the bitwise operations by hand, you can use the [`RecursivePageTable`] type of the `x86_64` crate, which provides safe abstractions for various page table operations. For example, the code below shows how to translate a virtual address to its mapped physical address: + +[`RecursivePageTable`]: https://docs.rs/x86_64/0.13.2/x86_64/structures/paging/mapper/struct.RecursivePageTable.html + +```rust +// 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())) +``` + +Again, a valid recursive mapping is required for this code. With such a mapping, the missing `level_4_table_addr` can be calculated as in the first code example. + +
+ +--- + +Recursive Paging is an interesting technique that shows how powerful a single mapping in a page table can be. It is relatively easy to implement and only requires a minimal amount of setup (just a single recursive entry), so it's a good choice for first experiments with paging. + +However, it also has some disadvantages: + +- It occupies a large amount of virtual memory (512GiB). This isn't a big problem in the large 48-bit address space, but it might lead to suboptimal cache behavior. +- It only allows accessing the currently active address space easily. Accessing other address spaces is still possible by changing the recursive entry, but a temporary mapping is required for switching back. We described how to do this in the (outdated) [_Remap The Kernel_] post. +- It heavily relies on the page table format of x86 and might not work on other architectures. + +[_Remap The Kernel_]: https://os.phil-opp.com/remap-the-kernel/#overview + +## Bootloader Support + +All of these approaches require page table modifications for their setup. For example, mappings for the physical memory need to be created or an entry of the level 4 table needs to be mapped recursively. The problem is that we can't create these required mappings without an existing way to access the page tables. + +This means that we need the help of the bootloader, which creates the page tables that our kernel runs on. The bootloader has access to the page tables, so it can create any mappings that we need. In its current implementation, the `bootloader` crate has support for two of the above approaches, controlled through [cargo features]: + +[cargo features]: https://doc.rust-lang.org/cargo/reference/features.html#the-features-section + +- The `map_physical_memory` feature maps the complete physical memory somewhere into the virtual address space. Thus, the kernel can access all physical memory and can follow the [_Map the Complete Physical Memory_](#map-the-complete-physical-memory) approach. +- With the `recursive_page_table` feature, the bootloader maps an entry of the level 4 page table recursively. This allows the kernel to access the page tables as described in the [_Recursive Page Tables_](#recursive-page-tables) section. + +We choose the first approach for our kernel since it is simple, platform-independent, and more powerful (it also allows access to non-page-table-frames). To enable the required bootloader support, we add the `map_physical_memory` feature to our `bootloader` dependency: + +```toml +[dependencies] +bootloader = { version = "0.9.8", features = ["map_physical_memory"]} +``` + +With this feature enabled, the bootloader maps the complete physical memory to some unused virtual address range. To communicate the virtual address range to our kernel, the bootloader passes a _boot information_ structure. + +### Boot Information + +The `bootloader` crate defines a [`BootInfo`] struct that contains all the information it passes to our kernel. The struct is still in an early stage, so expect some breakage when updating to future [semver-incompatible] bootloader versions. With the `map_physical_memory` feature enabled, it currently has the two fields `memory_map` and `physical_memory_offset`: + +[`BootInfo`]: https://docs.rs/bootloader/0.9.3/bootloader/bootinfo/struct.BootInfo.html +[semver-incompatible]: https://doc.rust-lang.org/stable/cargo/reference/specifying-dependencies.html#caret-requirements + +- The `memory_map` field contains an overview of the available physical memory. This tells our kernel how much physical memory is available in the system and which memory regions are reserved for devices such as the VGA hardware. The memory map can be queried from the BIOS or UEFI firmware, but only very early in the boot process. For this reason, it must be provided by the bootloader because there is no way for the kernel to retrieve it later. We will need the memory map later in this post. +- The `physical_memory_offset` tells us the virtual start address of the physical memory mapping. By adding this offset to a physical address, we get the corresponding virtual address. This allows us to access arbitrary physical memory from our kernel. + +The bootloader passes the `BootInfo` struct to our kernel in the form of a `&'static BootInfo` argument to our `_start` function. We don't have this argument declared in our function yet, so let's add it: + +```rust +// in src/main.rs + +use bootloader::BootInfo; + +#[no_mangle] +pub extern "C" fn _start(boot_info: &'static BootInfo) -> ! { // new argument + […] +} +``` + +It wasn't a problem to leave off this argument before because the x86_64 calling convention passes the first argument in a CPU register. Thus, the argument is simply ignored when it isn't declared. However, it would be a problem if we accidentally used a wrong argument type, since the compiler doesn't know the correct type signature of our entry point function. + +### The `entry_point` Macro + +Since our `_start` function is called externally from the bootloader, no checking of our function signature occurs. This means that we could let it take arbitrary arguments without any compilation errors, but it would fail or cause undefined behavior at runtime. + +To make sure that the entry point function has always the correct signature that the bootloader expects, the `bootloader` crate provides an [`entry_point`] macro that provides a type-checked way to define a Rust function as the entry point. Let's rewrite our entry point function to use this macro: + +[`entry_point`]: https://docs.rs/bootloader/0.6.4/bootloader/macro.entry_point.html + +```rust +// in src/main.rs + +use bootloader::{BootInfo, entry_point}; + +entry_point!(kernel_main); + +fn kernel_main(boot_info: &'static BootInfo) -> ! { + […] +} +``` + +We no longer need to use `extern "C"` or `no_mangle` for our entry point, as the macro defines the real lower level `_start` entry point for us. The `kernel_main` function is now a completely normal Rust function, so we can choose an arbitrary name for it. The important thing is that it is type-checked so that a compilation error occurs when we use a wrong function signature, for example by adding an argument or changing the argument type. + +Let's perform the same change in our `lib.rs`: + +```rust +// in src/lib.rs + +#[cfg(test)] +use bootloader::{entry_point, BootInfo}; + +#[cfg(test)] +entry_point!(test_kernel_main); + +/// Entry point for `cargo test` +#[cfg(test)] +fn test_kernel_main(_boot_info: &'static BootInfo) -> ! { + // like before + init(); + test_main(); + hlt_loop(); +} +``` + +Since the entry point is only used in test mode, we add the `#[cfg(test)]` attribute to all items. We give our test entry point the distinct name `test_kernel_main` to avoid confusion with the `kernel_main` of our `main.rs`. We don't use the `BootInfo` parameter for now, so we prefix the parameter name with a `_` to silence the unused variable warning. + +## Implementation + +Now that we have access to physical memory, we can finally start to implement our page table code. First, we will take a look at the currently active page tables that our kernel runs on. In the second step, we will create a translation function that returns the physical address that a given virtual address is mapped to. As the last step, we will try to modify the page tables in order to create a new mapping. + +Before we begin, we create a new `memory` module for our code: + +```rust +// in src/lib.rs + +pub mod memory; +``` + +For the module we create an empty `src/memory.rs` file. + +### Accessing the Page Tables + +At the [end of the previous post], we tried to take a look at the page tables our kernel runs on, but failed since we couldn't access the physical frame that the `CR3` register points to. We're now able to continue from there by creating an `active_level_4_table` function that returns a reference to the active level 4 page table: + +[end of the previous post]: @/edition-2/posts/08-paging-introduction/index.md#accessing-the-page-tables + +```rust +// in src/memory.rs + +use x86_64::{ + structures::paging::PageTable, + VirtAddr, +}; + +/// 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: VirtAddr) + -> &'static mut PageTable +{ + use x86_64::registers::control::Cr3; + + let (level_4_table_frame, _) = Cr3::read(); + + let phys = level_4_table_frame.start_address(); + let virt = physical_memory_offset + phys.as_u64(); + let page_table_ptr: *mut PageTable = virt.as_mut_ptr(); + + &mut *page_table_ptr // unsafe +} +``` + +First, we read the physical frame of the active level 4 table from the `CR3` register. We then take its physical start address, convert it to an `u64`, and add it to `physical_memory_offset` to get the virtual address where the page table frame is mapped. Finally, we convert the virtual address to a `*mut PageTable` raw pointer through the `as_mut_ptr` method and then unsafely create a `&mut PageTable` reference from it. We create a `&mut` reference instead of a `&` reference because we will mutate the page tables later in this post. + +We don't need to use an unsafe block here because Rust treats the complete body of an `unsafe fn` like a large `unsafe` block. This makes our code more dangerous since we could accidentally introduce an unsafe operation in previous lines without noticing. It also makes it much more difficult to spot the unsafe operations. There is an [RFC](https://github.com/rust-lang/rfcs/pull/2585) to change this behavior. + +We can now use this function to print the entries of the level 4 table: + +```rust +// in src/main.rs + +fn kernel_main(boot_info: &'static BootInfo) -> ! { + use blog_os::memory::active_level_4_table; + use x86_64::VirtAddr; + + println!("Hello World{}", "!"); + blog_os::init(); + + let phys_mem_offset = VirtAddr::new(boot_info.physical_memory_offset); + let l4_table = unsafe { active_level_4_table(phys_mem_offset) }; + + for (i, entry) in l4_table.iter().enumerate() { + if !entry.is_unused() { + println!("L4 Entry {}: {:?}", i, entry); + } + } + + // as before + #[cfg(test)] + test_main(); + + println!("It did not crash!"); + blog_os::hlt_loop(); +} +``` + +First, we convert the `physical_memory_offset` of the `BootInfo` struct to a [`VirtAddr`] and pass it to the `active_level_4_table` function. We then use the `iter` function to iterate over the page table entries and the [`enumerate`] combinator to additionally add an index `i` to each element. We only print non-empty entries because all 512 entries wouldn't fit on the screen. + +[`VirtAddr`]: https://docs.rs/x86_64/0.13.2/x86_64/addr/struct.VirtAddr.html +[`enumerate`]: https://doc.rust-lang.org/core/iter/trait.Iterator.html#method.enumerate + +When we run it, we see the following output: + +![QEMU printing entry 0 (0x2000, PRESENT, WRITABLE, ACCESSED), entry 1 (0x894000, PRESENT, WRITABLE, ACCESSED, DIRTY), entry 31 (0x88e000, PRESENT, WRITABLE, ACCESSED, DIRTY), entry 175 (0x891000, PRESENT, WRITABLE, ACCESSED, DIRTY), and entry 504 (0x897000, PRESENT, WRITABLE, ACCESSED, DIRTY)](qemu-print-level-4-table.png) + +We see that there are various non-empty entries, which all map to different level 3 tables. There are so many regions because kernel code, kernel stack, the physical memory mapping, and the boot information all use separate memory areas. + +To traverse the page tables further and take a look at a level 3 table, we can take the mapped frame of an entry convert it to a virtual address again: + +```rust +// in the `for` loop in src/main.rs + +use x86_64::structures::paging::PageTable; + +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); + } + } +} +``` + +For looking at the level 2 and level 1 tables, we repeat that process for the level 3 and level 2 entries. As you can imagine, this gets very verbose quickly, so we don't show the full code here. + +Traversing the page tables manually is interesting because it helps to understand how the CPU performs the translation. However, most of the time we are only interested in the mapped physical address for a given virtual address, so let's create a function for that. + +### Translating Addresses + +For translating a virtual to a physical address, we have to traverse the four-level page table until we reach the mapped frame. Let's create a function that performs this translation: + +```rust +// in src/memory.rs + +use x86_64::PhysAddr; + +/// 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: VirtAddr) + -> Option +{ + translate_addr_inner(addr, physical_memory_offset) +} +``` + +We forward the function to a safe `translate_addr_inner` function to limit the scope of `unsafe`. As we noted above, Rust treats the complete body of an unsafe fn like a large unsafe block. By calling into a private safe function, we make each `unsafe` operation explicit again. + +The private inner function contains the real implementation: + +```rust +// 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: VirtAddr) + -> Option +{ + 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 = physical_memory_offset + frame.start_address().as_u64(); + let table_ptr: *const PageTable = 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())) +} +``` + +Instead of reusing our `active_level_4_table` function, we read the level 4 frame from the `CR3` register again. We do this because it simplifies this prototype implementation. Don't worry, we will create a better solution in a moment. + +The `VirtAddr` struct already provides methods to compute the indexes into the page tables of the four levels. We store these indexes in a small array because it allows us to traverse the page tables using a `for` loop. Outside of the loop, we remember the last visited `frame` to calculate the physical address later. The `frame` points to page table frames while iterating, and to the mapped frame after the last iteration, i.e. after following the level 1 entry. + +Inside the loop, we again use the `physical_memory_offset` to convert the frame into a page table reference. We then read the entry of the current page table and use the [`PageTableEntry::frame`] function to retrieve the mapped frame. If the entry is not mapped to a frame we return `None`. If the entry maps a huge 2MiB or 1GiB page we panic for now. + +[`PageTableEntry::frame`]: https://docs.rs/x86_64/0.13.2/x86_64/structures/paging/page_table/struct.PageTableEntry.html#method.frame + +Let's test our translation function by translating some addresses: + +```rust +// in src/main.rs + +fn kernel_main(boot_info: &'static BootInfo) -> ! { + // new import + use blog_os::memory::translate_addr; + + […] // hello world and blog_os::init + + let phys_mem_offset = VirtAddr::new(boot_info.physical_memory_offset); + + let addresses = [ + // the identity-mapped vga buffer page + 0xb8000, + // some code page + 0x201008, + // some stack page + 0x0100_0020_1a10, + // 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, phys_mem_offset) }; + println!("{:?} -> {:?}", virt, phys); + } + + […] // test_main(), "it did not crash" printing, and hlt_loop() +} +``` + +When we run it, we see the following output: + +![0xb8000 -> 0xb8000, 0x201008 -> 0x401008, 0x10000201a10 -> 0x279a10, "panicked at 'huge pages not supported'](qemu-translate-addr.png) + +As expected, the identity-mapped address `0xb8000` translates to the same physical address. The code page and the stack page translate to some arbitrary physical addresses, which depend on how the bootloader created the initial mapping for our kernel. It's worth noting that the last 12 bits always stay the same after translation, which makes sense because these bits are the [_page offset_] and not part of the translation. + +[_page offset_]: @/edition-2/posts/08-paging-introduction/index.md#paging-on-x86-64 + +Since each physical address can be accessed by adding the `physical_memory_offset`, the translation of the `physical_memory_offset` address itself should point to physical address `0`. However, the translation fails because the mapping uses huge pages for efficiency, which is not supported in our implementation yet. + +### Using `OffsetPageTable` + +Translating virtual to physical addresses is a common task in an OS kernel, therefore the `x86_64` crate provides an abstraction for it. The implementation already supports huge pages and several other page table functions apart from `translate_addr`, so we will use it in the following instead of adding huge page support to our own implementation. + +The base of the abstraction are two traits that define various page table mapping functions: + +- The [`Mapper`] trait is generic over the page size and provides functions that operate on pages. Examples are [`translate_page`], which translates a given page to a frame of the same size, and [`map_to`], which creates a new mapping in the page table. +- The [`Translate`] trait provides functions that work with multiple page sizes such as [`translate_addr`] or the general [`translate`]. + +[`Mapper`]: https://docs.rs/x86_64/0.13.2/x86_64/structures/paging/mapper/trait.Mapper.html +[`translate_page`]: https://docs.rs/x86_64/0.13.2/x86_64/structures/paging/mapper/trait.Mapper.html#tymethod.translate_page +[`map_to`]: https://docs.rs/x86_64/0.13.2/x86_64/structures/paging/mapper/trait.Mapper.html#method.map_to +[`Translate`]: https://docs.rs/x86_64/0.13.2/x86_64/structures/paging/mapper/trait.Translate.html +[`translate_addr`]: https://docs.rs/x86_64/0.13.2/x86_64/structures/paging/mapper/trait.Translate.html#method.translate_addr +[`translate`]: https://docs.rs/x86_64/0.13.2/x86_64/structures/paging/mapper/trait.Translate.html#tymethod.translate + +The traits only define the interface, they don't provide any implementation. The `x86_64` crate currently provides three types that implement the traits with different requirements. The [`OffsetPageTable`] type assumes that the complete physical memory is mapped to the virtual address space at some offset. The [`MappedPageTable`] is a bit more flexible: It only requires that each page table frame is mapped to the virtual address space at a calculable address. Finally, the [`RecursivePageTable`] type can be used to access page table frames through [recursive page tables](#recursive-page-tables). + +[`OffsetPageTable`]: https://docs.rs/x86_64/0.13.2/x86_64/structures/paging/mapper/struct.OffsetPageTable.html +[`MappedPageTable`]: https://docs.rs/x86_64/0.13.2/x86_64/structures/paging/mapper/struct.MappedPageTable.html +[`RecursivePageTable`]: https://docs.rs/x86_64/0.13.2/x86_64/structures/paging/mapper/struct.RecursivePageTable.html + +In our case, the bootloader maps the complete physical memory at a virtual address specfied by the `physical_memory_offset` variable, so we can use the `OffsetPageTable` type. To initialize it, we create a new `init` function in our `memory` module: + +```rust +use x86_64::structures::paging::OffsetPageTable; + +/// Initialize a new OffsetPageTable. +/// +/// 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: VirtAddr) -> OffsetPageTable<'static> { + let level_4_table = active_level_4_table(physical_memory_offset); + OffsetPageTable::new(level_4_table, physical_memory_offset) +} + +// make private +unsafe fn active_level_4_table(physical_memory_offset: VirtAddr) + -> &'static mut PageTable +{…} +``` + +The function takes the `physical_memory_offset` as an argument and returns a new `OffsetPageTable` instance with a `'static` lifetime. This means that the instance stays valid for the complete runtime of our kernel. In the function body, we first call the `active_level_4_table` function to retrieve a mutable reference to the level 4 page table. We then invoke the [`OffsetPageTable::new`] function with this reference. As the second parameter, the `new` function expects the virtual address at which the mapping of the physical memory starts, which is given in the `physical_memory_offset` variable. + +[`OffsetPageTable::new`]: https://docs.rs/x86_64/0.13.2/x86_64/structures/paging/mapper/struct.OffsetPageTable.html#method.new + +The `active_level_4_table` function should be only called from the `init` function from now on because it can easily lead to aliased mutable references when called multiple times, which can cause undefined behavior. For this reason, we make the function private by removing the `pub` specifier. + +We now can use the `Translate::translate_addr` method instead of our own `memory::translate_addr` function. We only need to change a few lines in our `kernel_main`: + +```rust +// in src/main.rs + +fn kernel_main(boot_info: &'static BootInfo) -> ! { + // new: different imports + use blog_os::memory; + use x86_64::{structures::paging::Translate, VirtAddr}; + + […] // hello world and blog_os::init + + let phys_mem_offset = VirtAddr::new(boot_info.physical_memory_offset); + // new: initialize a mapper + let mapper = unsafe { memory::init(phys_mem_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); + } + + […] // test_main(), "it did not crash" printing, and hlt_loop() +} +``` + +We need to import the `Translate` trait in order to use the [`translate_addr`] method it provides. + +When we run it now, we see the same translation results as before, with the difference that the huge page translation now also works: + +![0xb8000 -> 0xb8000, 0x201008 -> 0x401008, 0x10000201a10 -> 0x279a10, 0x18000000000 -> 0x0](qemu-mapper-translate-addr.png) + +As expected, the translations of `0xb8000` and the code and stack addresses stay the same as with our own translation function. Additionally, we now see that the virtual address `physical_memory_offset` is mapped to the physical address `0x0`. + +By using the translation function of the `MappedPageTable` type we can spare ourselves the work of implementing huge page support. We also have access to other page functions such as `map_to`, which we will use in the next section. + +At this point we no longer need our `memory::translate_addr` and `memory::translate_addr_inner` functions, so we can delete them. + +### Creating a new Mapping + +Until now we only looked at the page tables without modifying anything. Let's change that by creating a new mapping for a previously unmapped page. + +We will use the [`map_to`] function of the [`Mapper`] trait for our implementation, so let's take a look at that function first. The documentation tells us that it takes four arguments: the page that we want to map, the frame that the page should be mapped to, a set of flags for the page table entry, and a `frame_allocator`. The frame allocator is needed because mapping the given page might require creating additional page tables, which need unused frames as backing storage. + +[`map_to`]: https://docs.rs/x86_64/0.13.2/x86_64/structures/paging/trait.Mapper.html#tymethod.map_to +[`Mapper`]: https://docs.rs/x86_64/0.13.2/x86_64/structures/paging/trait.Mapper.html + +#### A `create_example_mapping` Function + +The first step of our implementation is to create a new `create_example_mapping` function that maps a given virtual page to `0xb8000`, the physical frame of the VGA text buffer. We choose that frame because it allows us to easily test if the mapping was created correctly: We just need to write to the newly mapped page and see whether we see the write appear on the screen. + +The `create_example_mapping` function looks like this: + +```rust +// in src/memory.rs + +use x86_64::{ + PhysAddr, + structures::paging::{Page, PhysFrame, Mapper, Size4KiB, FrameAllocator} +}; + +/// Creates an example mapping for the given page to frame `0xb8000`. +pub fn create_example_mapping( + page: Page, + mapper: &mut OffsetPageTable, + frame_allocator: &mut impl FrameAllocator, +) { + 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 { + // FIXME: this is not safe, we do it only for testing + mapper.map_to(page, frame, flags, frame_allocator) + }; + map_to_result.expect("map_to failed").flush(); +} +``` + +In addition to the `page` that should be mapped, the function expects a mutable reference to an `OffsetPageTable` instance and a `frame_allocator`. The `frame_allocator` parameter uses the [`impl Trait`][impl-trait-arg] syntax to be [generic] over all types that implement the [`FrameAllocator`] trait. The trait is generic over the [`PageSize`] trait to work with both standard 4KiB pages and huge 2MiB/1GiB pages. We only want to create a 4KiB mapping, so we set the generic parameter to `Size4KiB`. + +[impl-trait-arg]: https://doc.rust-lang.org/book/ch10-02-traits.html#traits-as-parameters +[generic]: https://doc.rust-lang.org/book/ch10-00-generics.html +[`FrameAllocator`]: https://docs.rs/x86_64/0.13.2/x86_64/structures/paging/trait.FrameAllocator.html +[`PageSize`]: https://docs.rs/x86_64/0.13.2/x86_64/structures/paging/page/trait.PageSize.html + +The [`map_to`] method is unsafe because the caller must ensure that the frame is not already in use. The reason for this is that mapping the same frame twice could result in undefined behavior, for example when two different `&mut` references point to the same physical memory location. In our case, we reuse the VGA text buffer frame, which is already mapped, so we break the required condition. However, the `create_example_mapping` function is only a temporary testing function and will be removed after this post, so it is ok. To remind us of the unsafety, we put a `FIXME` comment on the line. + +In addition to the `page` and the `unused_frame`, the `map_to` method takes a set of flags for the mapping and a reference to the `frame_allocator`, which will be explained in a moment. For the flags, we set the `PRESENT` flag because it is required for all valid entries and the `WRITABLE` flag to make the mapped page writable. For a list of all possible flags, see the [_Page Table Format_] section of the previous post. + +[_Page Table Format_]: @/edition-2/posts/08-paging-introduction/index.md#page-table-format + +The [`map_to`] function can fail, so it returns a [`Result`]. Since this is just some example code that does not need to be robust, we just use [`expect`] to panic when an error occurs. On success, the function returns a [`MapperFlush`] type that provides an easy way to flush the newly mapped page from the translation lookaside buffer (TLB) with its [`flush`] method. Like `Result`, the type uses the [`#[must_use]`][must_use] attribute to emit a warning when we accidentally forget to use it. + +[`Result`]: https://doc.rust-lang.org/core/result/enum.Result.html +[`expect`]: https://doc.rust-lang.org/core/result/enum.Result.html#method.expect +[`MapperFlush`]: https://docs.rs/x86_64/0.13.2/x86_64/structures/paging/mapper/struct.MapperFlush.html +[`flush`]: https://docs.rs/x86_64/0.13.2/x86_64/structures/paging/mapper/struct.MapperFlush.html#method.flush +[must_use]: https://doc.rust-lang.org/std/result/#results-must-be-used + +#### A dummy `FrameAllocator` + +To be able to call `create_example_mapping` we need to create a type that implements the `FrameAllocator` trait first. As noted above, the trait is responsible for allocating frames for new page table if they are needed by `map_to`. + +Let's start with the simple case and assume that we don't need to create new page tables. For this case, a frame allocator that always returns `None` suffices. We create such an `EmptyFrameAllocator` for testing our mapping function: + +```rust +// in src/memory.rs + +/// A FrameAllocator that always returns `None`. +pub struct EmptyFrameAllocator; + +unsafe impl FrameAllocator for EmptyFrameAllocator { + fn allocate_frame(&mut self) -> Option { + None + } +} +``` + +Implementing the `FrameAllocator` is unsafe because the implementer must guarantee that the allocator yields only unused frames. Otherwise undefined behavior might occur, for example when two virtual pages are mapped to the same physical frame. Our `EmptyFrameAllocator` only returns `None`, so this isn't a problem in this case. + +#### Choosing a Virtual Page + +We now have a simple frame allocator that we can pass to our `create_example_mapping` function. However, the allocator always returns `None`, so this will only work if no additional page table frames are needed for creating the mapping. To understand when additional page table frames are needed and when not, let's consider an example: + +![A virtual and a physical address space with a single mapped page and the page tables of all four levels](required-page-frames-example.svg) + +The graphic shows the virtual address space on the left, the physical address space on the right, and the page tables in between. The page tables are stored in physical memory frames, indicated by the dashed lines. The virtual address space contains a single mapped page at address `0x803fe00000`, marked in blue. To translate this page to its frame, the CPU walks the 4-level page table until it reaches the frame at address 36 KiB. + +Additionally, the graphic shows the physical frame of the VGA text buffer in red. Our goal is to map a previously unmapped virtual page to this frame using our `create_example_mapping` function. Since our `EmptyFrameAllocator` always returns `None`, we want to create the mapping so that no additional frames are needed from the allocator. This depends on the virtual page that we select for the mapping. + +The graphic shows two canditate pages in the virtual address space, both marked in yellow. One page is at address `0x803fdfd000`, which is 3 pages before the mapped page (in blue). While the level 4 and level 3 page table indices are the same as for the blue page, the level 2 and level 1 indices are different (see the [previous post][page-table-indices]). The different index into the level 2 table means that a different level 1 table is used for this page. Since this level 1 table does not exist yet, we would need to create it if we chose that page for our example mapping, which would require an additional unused physical frame. In contrast, the second candidate page at address `0x803fe02000` does not have this problem because it uses the same level 1 page table than the blue page. Thus, all required page tables already exist. + +[page-table-indices]: @/edition-2/posts/08-paging-introduction/index.md#paging-on-x86-64 + +In summary, the difficulty of creating a new mapping depends on the virtual page that we want to map. In the easiest case, the level 1 page table for the page already exists and we just need to write a single entry. In the most difficult case, the page is in a memory region for that no level 3 exists yet so that we need to create new level 3, level 2 and level 1 page tables first. + +For calling our `create_example_mapping` function with the `EmptyFrameAllocator`, we need to choose a page for that all page tables already exist. To find such a page, we can utilize the fact that the bootloader loads itself in the first megabyte of the virtual address space. This means that a valid level 1 table exists for all pages this region. Thus, we can choose any unused page in this memory region for our example mapping, such as the page at address `0`. Normally, this page should stay unused to guarantee that dereferencing a null pointer causes a page fault, so we know that the bootloader leaves it unmapped. + +#### Creating the Mapping + +We now have all the required parameters for calling our `create_example_mapping` function, so let's modify our `kernel_main` function to map the page at virtual address `0`. Since we map the page to the frame of the VGA text buffer, we should be able to write to the screen through it afterwards. The implementation looks like this: + +```rust +// in src/main.rs + +fn kernel_main(boot_info: &'static BootInfo) -> ! { + use blog_os::memory; + use x86_64::{structures::paging::Page, VirtAddr}; // new import + + […] // hello world and blog_os::init + + let phys_mem_offset = VirtAddr::new(boot_info.physical_memory_offset); + let mut mapper = unsafe { memory::init(phys_mem_offset) }; + let mut frame_allocator = memory::EmptyFrameAllocator; + + // map an unused page + let page = Page::containing_address(VirtAddr::new(0)); + 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)}; + + […] // test_main(), "it did not crash" printing, and hlt_loop() +} +``` + +We first create the mapping for the page at address `0` by calling our `create_example_mapping` function with a mutable reference to the `mapper` and the `frame_allocator` instances. This maps the page to the VGA text buffer frame, so we should see any write to it on the screen. + +Then we convert the page to a raw pointer and write a value to offset `400`. We don't write to the start of the page because the top line of the VGA buffer is directly shifted off the screen by the next `println`. We write the value `0x_f021_f077_f065_f04e`, which represents the string _"New!"_ on white background. As we learned [in the _“VGA Text Mode”_ post], writes to the VGA buffer should be volatile, so we use the [`write_volatile`] method. + +[in the _“VGA Text Mode”_ post]: @/edition-2/posts/03-vga-text-buffer/index.md#volatile +[`write_volatile`]: https://doc.rust-lang.org/std/primitive.pointer.html#method.write_volatile + +When we run it in QEMU, we see the following output: + +![QEMU printing "It did not crash!" with four completely white cells in the middle of the screen](qemu-new-mapping.png) + +The _"New!"_ on the screen is by our write to page `0`, which means that we successfully created a new mapping in the page tables. + +Creating that mapping only worked because the level 1 table responsible for the page at address `0` already exists. When we try to map a page for that no level 1 table exists yet, the `map_to` function fails because it tries to allocate frames from the `EmptyFrameAllocator` for creating new page tables. We can see that happen when we try to map page `0xdeadbeaf000` instead of `0`: + +```rust +// in src/main.rs + +fn kernel_main(boot_info: &'static BootInfo) -> ! { + […] + let page = Page::containing_address(VirtAddr::new(0xdeadbeaf000)); + […] +} +``` + +When we run it, a panic with the following error message occurs: + +``` +panicked at 'map_to failed: FrameAllocationFailed', /…/result.rs:999:5 +``` + +To map pages that don't have a level 1 page table yet we need to create a proper `FrameAllocator`. But how do we know which frames are unused and how much physical memory is available? + +### Allocating Frames + +In order to create new page tables, we need to create a proper frame allocator. For that we use the `memory_map` that is passed by the bootloader as part of the `BootInfo` struct: + +```rust +// in src/memory.rs + +use bootloader::bootinfo::MemoryMap; + +/// A FrameAllocator that returns usable frames from the bootloader's memory map. +pub struct BootInfoFrameAllocator { + memory_map: &'static MemoryMap, + next: usize, +} + +impl BootInfoFrameAllocator { + /// Create a FrameAllocator from the passed memory map. + /// + /// This function is unsafe because the caller must guarantee that the passed + /// memory map is valid. The main requirement is that all frames that are marked + /// as `USABLE` in it are really unused. + pub unsafe fn init(memory_map: &'static MemoryMap) -> Self { + BootInfoFrameAllocator { + memory_map, + next: 0, + } + } +} +``` + +The struct has two fields: A `'static` reference to the memory map passed by the bootloader and a `next` field that keeps track of number of the next frame that the allocator should return. + +As we explained in the [_Boot Information_](#boot-information) section, the memory map is provided by the BIOS/UEFI firmware. It can only be queried very early in the boot process, so the bootloader already calls the respective functions for us. The memory map consists of a list of [`MemoryRegion`] structs, which contain the start address, the length, and the type (e.g. unused, reserved, etc.) of each memory region. + +The `init` function initializes a `BootInfoFrameAllocator` with a given memory map. The `next` field is initialized with `0` and will be increased for every frame allocation to avoid returning the same frame twice. Since we don't know if the usable frames of the memory map were already used somewhere else, our `init` function must be `unsafe` to require additional guarantees from the caller. + +#### A `usable_frames` Method + +Before we implement the `FrameAllocator` trait, we add an auxiliary method that converts the memory map into an iterator of usable frames: + +```rust +// in src/memory.rs + +use bootloader::bootinfo::MemoryRegionType; + +impl BootInfoFrameAllocator { + /// Returns an iterator over the usable frames specified in the memory map. + fn usable_frames(&self) -> impl Iterator { + // get usable regions from memory map + let regions = self.memory_map.iter(); + let usable_regions = regions + .filter(|r| r.region_type == MemoryRegionType::Usable); + // map each region to its address range + let addr_ranges = usable_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 + frame_addresses.map(|addr| PhysFrame::containing_address(PhysAddr::new(addr))) + } +} +``` + +This function uses iterator combinator methods to transform the initial `MemoryMap` into an iterator of usable physical frames: + +- First, we call the `iter` method to convert the memory map to an iterator of [`MemoryRegion`]s. +- Then we use the [`filter`] method to skip any reserved or otherwise unavailable regions. The bootloader updates the memory map for all the mappings it creates, so frames that are used by our kernel (code, data or stack) or to store the boot information are already marked as `InUse` or similar. Thus we can be sure that `Usable` frames are not used somewhere else. +- Afterwards, we use the [`map`] combinator and Rust's [range syntax] to transform our iterator of memory regions to an iterator of address ranges. +- Next, we use [`flat_map`] to transform the address ranges into an iterator of frame start addresses, choosing every 4096th address using [`step_by`]. Since 4096 bytes (= 4 KiB) is the page size, we get the start address of each frame. The bootloader page aligns all usable memory areas so that we don't need any alignment or rounding code here. By using [`flat_map`] instead of `map`, we get an `Iterator` instead of an `Iterator>`. +- Finally, we convert the start addresses to `PhysFrame` types to construct the an `Iterator`. + +[`MemoryRegion`]: https://docs.rs/bootloader/0.6.4/bootloader/bootinfo/struct.MemoryRegion.html +[`filter`]: https://doc.rust-lang.org/core/iter/trait.Iterator.html#method.filter +[`map`]: https://doc.rust-lang.org/core/iter/trait.Iterator.html#method.map +[range syntax]: https://doc.rust-lang.org/core/ops/struct.Range.html +[`step_by`]: https://doc.rust-lang.org/core/iter/trait.Iterator.html#method.step_by +[`flat_map`]: https://doc.rust-lang.org/core/iter/trait.Iterator.html#method.flat_map + +The return type of the function uses the [`impl Trait`] feature. This way, we can specify that we return some type that implements the [`Iterator`] trait with item type `PhysFrame`, but don't need to name the concrete return type. This is important here because we _can't_ name the concrete type since it depends on unnamable closure types. + +[`impl Trait`]: https://doc.rust-lang.org/book/ch10-02-traits.html#returning-types-that-implement-traits +[`Iterator`]: https://doc.rust-lang.org/core/iter/trait.Iterator.html + +#### Implementing the `FrameAllocator` Trait + +Now we can implement the `FrameAllocator` trait: + +```rust +// in src/memory.rs + +unsafe impl FrameAllocator for BootInfoFrameAllocator { + fn allocate_frame(&mut self) -> Option { + let frame = self.usable_frames().nth(self.next); + self.next += 1; + frame + } +} +``` + +We first use the `usable_frames` method to get an iterator of usable frames from the memory map. Then, we use the [`Iterator::nth`] function to get the frame with index `self.next` (thereby skipping `(self.next - 1)` frames). Before returning that frame, we increase `self.next` by one so that we return the following frame on the next call. + +[`Iterator::nth`]: https://doc.rust-lang.org/core/iter/trait.Iterator.html#method.nth + +This implementation is not quite optimal since it recreates the `usable_frame` allocator on every allocation. It would be better to directly store the iterator as a struct field instead. Then we wouldn't need the `nth` method and could just call [`next`] on every allocation. The problem with this approach is that it's not possible to store an `impl Trait` type in a struct field currently. It might work someday when [_named existential types_] are fully implemented. + +[`next`]: https://doc.rust-lang.org/core/iter/trait.Iterator.html#tymethod.next +[_named existential types_]: https://github.com/rust-lang/rfcs/pull/2071 + +#### Using the `BootInfoFrameAllocator` + +We can now modify our `kernel_main` function to pass a `BootInfoFrameAllocator` instance instead of an `EmptyFrameAllocator`: + +```rust +// in src/main.rs + +fn kernel_main(boot_info: &'static BootInfo) -> ! { + use blog_os::memory::BootInfoFrameAllocator; + […] + let mut frame_allocator = unsafe { + BootInfoFrameAllocator::init(&boot_info.memory_map) + }; + […] +} +``` + +With the boot info frame allocator, the mapping succeeds and we see the black-on-white _"New!"_ on the screen again. Behind the scenes, the `map_to` method creates the missing page tables in the following way: + +- Allocate an unused frame from the passed `frame_allocator`. +- Zero the frame to create a new, empty page table. +- Map the entry of the higher level table to that frame. +- Continue with the next table level. + +While our `create_example_mapping` function is just some example code, we are now able to create new mappings for arbitrary pages. This will be essential for allocating memory or implementing multithreading in future posts. + +At this point, we should delete the `create_example_mapping` function again to avoid accidentally invoking undefined behavior, as explained [above](#a-create-example-mapping-function). + +## Summary + +In this post we learned about different techniques to access the physical frames of page tables, including identity mapping, mapping of the complete physical memory, temporary mapping, and recursive page tables. We chose to map the complete physical memory since it's simple, portable, and powerful. + +We can't map the physical memory from our kernel without page table access, so we needed support from the bootloader. The `bootloader` crate supports creating the required mapping through optional cargo features. It passes the required information to our kernel in the form of a `&BootInfo` argument to our entry point function. + +For our implementation, we first manually traversed the page tables to implement a translation function, and then used the `MappedPageTable` type of the `x86_64` crate. We also learned how to create new mappings in the page table and how to create the necessary `FrameAllocator` on top of the memory map passed by the bootloader. + +## What's next? + +The next post will create a heap memory region for our kernel, which will allow us to [allocate memory] and use various [collection types]. + +[allocate memory]: https://doc.rust-lang.org/alloc/boxed/struct.Box.html +[collection types]: https://doc.rust-lang.org/alloc/collections/index.html From 21516fe8212e5af46f09707d0dc530ff801f609d Mon Sep 17 00:00:00 2001 From: woodyZootopia Date: Sun, 6 Jun 2021 11:48:15 +0900 Subject: [PATCH 02/11] Add file and translate a bit --- .../09-paging-implementation/index.ja.md | 42 +++++++++++-------- 1 file changed, 24 insertions(+), 18 deletions(-) diff --git a/blog/content/edition-2/posts/09-paging-implementation/index.ja.md b/blog/content/edition-2/posts/09-paging-implementation/index.ja.md index 73925812..b1f1bef5 100644 --- a/blog/content/edition-2/posts/09-paging-implementation/index.ja.md +++ b/blog/content/edition-2/posts/09-paging-implementation/index.ja.md @@ -34,64 +34,70 @@ translators = ["woodyZootopia"] To implement the approach, we will need support from the bootloader, so we'll configure it first. Afterward, we will implement a function that traverses the page table hierarchy in order to translate virtual to physical addresses. Finally, we learn how to create new mappings in the page tables and how to find unused memory frames for creating new page tables. -## Accessing Page Tables +この方法を実装するには、ブートローダーからの補助が必要になるので、まずこれに設定を加えます。その後で、ページテーブルの階層構造を移動して、仮想アドレスを物理アドレスに変換する関数を実装します。最後に、ページテーブルに新しい対応関係を作る方法と、それを作るための未使用メモリを見つける方法を学びます。 -Accessing the page tables from our kernel is not as easy as it may seem. To understand the problem let's take a look at the example 4-level page table hierarchy of the previous post again: +## ページテーブルにアクセスする + +私達のカーネルからページテーブルにアクセスするのは案外難しいです。この問題を理解するために、前回の記事の4層ページテーブルをもう一度見てみましょう: ![An example 4-level page hierarchy with each page table shown in physical memory](../paging-introduction/x86_64-page-table-translation.svg) -The important thing here is that each page entry stores the _physical_ address of the next table. This avoids the need to run a translation for these addresses too, which would be bad for performance and could easily cause endless translation loops. +ここで需要なのは、それぞれのページテーブルのエントリは次のテーブルの**物理**アドレスであるということです。これにより、それらのアドレスに対しても変換することを避けられます。もしこの変換が行われたとしたら、性能的にも良くないですし、容易に変換の無限ループに陥りかねません。 -The problem for us is that we can't directly access physical addresses from our kernel since our kernel also runs on top of virtual addresses. For example, when we access address `4 KiB` we access the _virtual_ address `4 KiB`, not the _physical_ address `4 KiB` where the level 4 page table is stored. When we want to access the physical address `4 KiB`, we can only do so through some virtual address that maps to it. +問題は、私達のカーネル自体も仮想アドレスの上で動いているため、カーネルから直接物理アドレスにアクセスすることができないということです。例えば、アドレス`4KiB`にアクセスしたとき、私達は**仮想**アドレス`4KiB`にアクセスしているのであって、レベル4ページテーブルが格納されている**物理**アドレス`4KiB`にアクセスしているのではありません。物理アドレス`4KiB`にアクセスしたいなら、それに対応づけられている何らかの仮想アドレスを通じてのみ可能です。 -So in order to access page table frames, we need to map some virtual pages to them. There are different ways to create these mappings that all allow us to access arbitrary page table frames. +そのため、ページテーブルのフレームにアクセスするためには、どこかの仮想ページをそれに対応づけなければいけません。このような、任意のページテーブルのフレームにアクセスできるようにしてくれる対応付けを作る方法にはいくつかあります。 -### Identity Mapping +### 恒等対応 -A simple solution is to **identity map all page tables**: +シンプルな方法として、**すべてのページテーブルを恒等対応させる**ということが考えられるでしょう: ![A virtual and a physical address space with various virtual pages mapped to the physical frame with the same address](identity-mapped-page-tables.svg) -In this example, we see various identity-mapped page table frames. This way the physical addresses of page tables are also valid virtual addresses so that we can easily access the page tables of all levels starting from the CR3 register. +この例では、いくつかの恒等対応したページテーブルのフレームが見てとれます。こうすることで、ページテーブルの物理アドレスは仮想アドレスとしても正当になり、よってCR3レジスタから始めることで全てのレベルのページテーブルに簡単にアクセスできます。 -However, it clutters the virtual address space and makes it more difficult to find continuous memory regions of larger sizes. For example, imagine that we want to create a virtual memory region of size 1000 KiB in the above graphic, e.g. for [memory-mapping a file]. We can't start the region at `28 KiB` because it would collide with the already mapped page at `1004 KiB`. So we have to look further until we find a large enough unmapped area, for example at `1008 KiB`. This is a similar fragmentation problem as with [segmentation]. +しかし、この方法では仮想アドレス空間が散らかってしまい、より大きいサイズの連続したメモリを見つけることが難しくなります。例えば、上の図において、[ファイルをメモリにマップする][memory-mapping a file]ために1000KiBの大きさの仮想メモリ領域を作りたいとします。`28KiB`を始点として領域を作ろうとすると、`1004KiB`のところで既存のページと衝突してしまうのでうまくいきません。そのため、`1008KiB`のような、十分な広さで対応付けのない領域が見つかるまで更に探さないといけません。これは[セグメンテーション][segmentation]の時にみた断片化の問題に似ています。 [memory-mapping a file]: https://en.wikipedia.org/wiki/Memory-mapped_file [segmentation]: @/edition-2/posts/08-paging-introduction/index.md#fragmentation Equally, it makes it much more difficult to create new page tables, because we need to find physical frames whose corresponding pages aren't already in use. For example, let's assume that we reserved the _virtual_ 1000 KiB memory region starting at `1008 KiB` for our memory-mapped file. Now we can't use any frame with a _physical_ address between `1000 KiB` and `2008 KiB` anymore, because we can't identity map it. +同様に、新しいページテーブルを作ることもずっと難しくなります。なぜなら、対応するページがまだ使われていない物理フレームを見つけないといけないからです。例えば、メモリマップト (に対応づけられた) ファイルのために`1008KiB`から1000KiBにわたって仮想メモリを占有したとしましょう。すると、すると、物理アドレス`1000KiB`から`2008KiB`までのフレームは、もう恒等対応を作ることができないので、使用することができません。 -### Map at a Fixed Offset +### 固定オフセットの対応 To avoid the problem of cluttering the virtual address space, we can **use a separate memory region for page table mappings**. So instead of identity mapping page table frames, we map them at a fixed offset in the virtual address space. For example, the offset could be 10 TiB: +仮想アドレス空間を散らかしてしまうという問題を回避するために、**ページテーブルの対応づけのために別のメモリ領域を使う**ことができます。ページテーブルを恒等対応させる代わりに、仮想アドレス空間で一定のオフセットをおいて対応づけてみましょう。例えば、オフセットを10TiBにしてみましょう: ![The same figure as for the identity mapping, but each mapped virtual page is offset by 10 TiB.](page-tables-mapped-at-offset.svg) By using the virtual memory in the range `10TiB..(10TiB + physical memory size)` exclusively for page table mappings, we avoid the collision problems of the identity mapping. Reserving such a large region of the virtual address space is only possible if the virtual address space is much larger than the physical memory size. This isn't a problem on x86_64 since the 48-bit address space is 256 TiB large. +`10TiB`から`10TiB+物理メモリ全体の大きさ`の範囲の仮想メモリをページテーブルの対応付け専用に使うことで、恒等対応のときに存在していた衝突問題を回避しています。このように巨大な領域を仮想アドレス空間内に用意するのは、仮想アドレス空間が物理メモリの大きさより遥かに大きい場合にのみ可能です。x86_64については、(x86_64で用いられている)48bit(仮想)アドレス空間は256TiBもの大きさがあるので、これは問題ではありません。 -This approach still has the disadvantage that we need to create a new mapping whenever we create a new page table. Also, it does not allow accessing page tables of other address spaces, which would be useful when creating a new process. +この方法では、新しいページテーブルを作るたびに新しい対応付けを作る必要があるという欠点があります。また、他のアドレス空間のページテーブルにアクセスすることができると、新しいプロセスを作るときに便利なのですがこれも不可能です。 -### Map the Complete Physical Memory +### 物理メモリ全体を対応付ける -We can solve these problems by **mapping the complete physical memory** instead of only page table frames: +これらの問題はページテーブルのフレームだけと言わず**物理メモリ全体を対応付け**てしまえば解決します: ![The same figure as for the offset mapping, but every physical frame has a mapping (at 10TiB + X) instead of only page table frames.](map-complete-physical-memory.svg) -This approach allows our kernel to access arbitrary physical memory, including page table frames of other address spaces. The reserved virtual memory range has the same size as before, with the difference that it no longer contains unmapped pages. +この方法を使えば、私達のカーネルは他のアドレス空間を含め任意の物理メモリにアクセスできます。用意する仮想メモリの範囲は以前と同じであり、違うのは全てのページが対応付けられているということです。 -The disadvantage of this approach is that additional page tables are needed for storing the mapping of the physical memory. These page tables need to be stored somewhere, so they use up a part of physical memory, which can be a problem on devices with a small amount of memory. +この方法の欠点は、物理メモリへの対応付けを格納するために、追加でページテーブルが必要になるところです。これらのページテーブルもどこかに格納されなければならず、したがって物理メモリの一部を占有することになります。これはメモリの量が少ないデバイスにおいては問題となりえます。 -On x86_64, however, we can use [huge pages] with size 2MiB for the mapping, instead of the default 4KiB pages. This way, mapping 32 GiB of physical memory only requires 132 KiB for page tables since only one level 3 table and 32 level 2 tables are needed. Huge pages are also more cache efficient since they use fewer entries in the translation lookaside buffer (TLB). +しかし、x86_64においては、通常の4KiBサイズのページに代わって、大きさ2MiBの[huge pages]を対応付けに使うことができます。こうすれば、例えば32GiBの物理メモリを対応付けるのに、レベル3テーブル1個とレベル2テーブル32個があればいいので、たったの132KiBしか必要ではありません。huge pagesは、トランスレーション・ルックアサイド・バッファ (TLB) のエントリをあまり使わないので、キャッシュ的にも効率が良いです。 [huge pages]: https://en.wikipedia.org/wiki/Page_%28computer_memory%29#Multiple_page_sizes -### Temporary Mapping +### 一時的な対応関係 -For devices with very small amounts of physical memory, we could **map the page tables frames only temporarily** when we need to access them. To be able to create the temporary mappings we only need a single identity-mapped level 1 table: +物理メモリの量が非常に限られたデバイスについては、アクセスする必要があるときだけ**ページテーブルのフレームを一時的に対応づける**という方法が考えられます。そのような一時的な対応を作りたいときには、たった一つだけ恒等対応させられたレベル1テーブルがあれば良いです: ![A virtual and a physical address space with an identity mapped level 1 table, which maps its 0th entry to the level 2 table frame, thereby mapping that frame to page with address 0](temporarily-mapped-page-tables.svg) The level 1 table in this graphic controls the first 2 MiB of the virtual address space. This is because it is reachable by starting at the CR3 register and following the 0th entry in the level 4, level 3, and level 2 page tables. The entry with index `8` maps the virtual page at address `32 KiB` to the physical frame at address `32 KiB`, thereby identity mapping the level 1 table itself. The graphic shows this identity-mapping by the horizontal arrow at `32 KiB`. +この図におけるレベル1テーブルは仮想アドレス空間の最初の2MiBを制御しています。なぜなら、CR3レジスタから始めて、レベル4、3、2のページテーブルの0番目のエントリを辿ることで到達できるからです。 By writing to the identity-mapped level 1 table, our kernel can create up to 511 temporary mappings (512 minus the entry required for the identity mapping). In the above example, the kernel created two temporary mappings: From fcabba07511592b0987955c03ea2b9a3c668d9a8 Mon Sep 17 00:00:00 2001 From: woodyZootopia Date: Fri, 13 Aug 2021 11:57:33 +0900 Subject: [PATCH 03/11] rebase to current origin/main --- .../09-paging-implementation/index.ja.md | 44 +++++++++---------- 1 file changed, 22 insertions(+), 22 deletions(-) diff --git a/blog/content/edition-2/posts/09-paging-implementation/index.ja.md b/blog/content/edition-2/posts/09-paging-implementation/index.ja.md index b1f1bef5..ad05eae9 100644 --- a/blog/content/edition-2/posts/09-paging-implementation/index.ja.md +++ b/blog/content/edition-2/posts/09-paging-implementation/index.ja.md @@ -6,7 +6,7 @@ date = 2019-03-14 [extra] chapter = "Memory Management" -translation_based_on_commit = "bf4f88107966c7ab1327c3cdc0ebfbd76bad5c5f" +translation_based_on_commit = "27ab4518acbb132e327ed4f4f0508393e9d4d684" translators = ["woodyZootopia"] +++ @@ -227,7 +227,7 @@ The above code assumes that the last level 4 entry with index `0o777` (511) is r Alternatively to performing the bitwise operations by hand, you can use the [`RecursivePageTable`] type of the `x86_64` crate, which provides safe abstractions for various page table operations. For example, the code below shows how to translate a virtual address to its mapped physical address: -[`RecursivePageTable`]: https://docs.rs/x86_64/0.13.2/x86_64/structures/paging/mapper/struct.RecursivePageTable.html +[`RecursivePageTable`]: https://docs.rs/x86_64/0.14.2/x86_64/structures/paging/mapper/struct.RecursivePageTable.html ```rust // in src/memory.rs @@ -445,7 +445,7 @@ fn kernel_main(boot_info: &'static BootInfo) -> ! { First, we convert the `physical_memory_offset` of the `BootInfo` struct to a [`VirtAddr`] and pass it to the `active_level_4_table` function. We then use the `iter` function to iterate over the page table entries and the [`enumerate`] combinator to additionally add an index `i` to each element. We only print non-empty entries because all 512 entries wouldn't fit on the screen. -[`VirtAddr`]: https://docs.rs/x86_64/0.13.2/x86_64/addr/struct.VirtAddr.html +[`VirtAddr`]: https://docs.rs/x86_64/0.14.2/x86_64/addr/struct.VirtAddr.html [`enumerate`]: https://doc.rust-lang.org/core/iter/trait.Iterator.html#method.enumerate When we run it, we see the following output: @@ -558,7 +558,7 @@ The `VirtAddr` struct already provides methods to compute the indexes into the p Inside the loop, we again use the `physical_memory_offset` to convert the frame into a page table reference. We then read the entry of the current page table and use the [`PageTableEntry::frame`] function to retrieve the mapped frame. If the entry is not mapped to a frame we return `None`. If the entry maps a huge 2MiB or 1GiB page we panic for now. -[`PageTableEntry::frame`]: https://docs.rs/x86_64/0.13.2/x86_64/structures/paging/page_table/struct.PageTableEntry.html#method.frame +[`PageTableEntry::frame`]: https://docs.rs/x86_64/0.14.2/x86_64/structures/paging/page_table/struct.PageTableEntry.html#method.frame Let's test our translation function by translating some addresses: @@ -613,20 +613,20 @@ The base of the abstraction are two traits that define various page table mappin - The [`Mapper`] trait is generic over the page size and provides functions that operate on pages. Examples are [`translate_page`], which translates a given page to a frame of the same size, and [`map_to`], which creates a new mapping in the page table. - The [`Translate`] trait provides functions that work with multiple page sizes such as [`translate_addr`] or the general [`translate`]. -[`Mapper`]: https://docs.rs/x86_64/0.13.2/x86_64/structures/paging/mapper/trait.Mapper.html -[`translate_page`]: https://docs.rs/x86_64/0.13.2/x86_64/structures/paging/mapper/trait.Mapper.html#tymethod.translate_page -[`map_to`]: https://docs.rs/x86_64/0.13.2/x86_64/structures/paging/mapper/trait.Mapper.html#method.map_to -[`Translate`]: https://docs.rs/x86_64/0.13.2/x86_64/structures/paging/mapper/trait.Translate.html -[`translate_addr`]: https://docs.rs/x86_64/0.13.2/x86_64/structures/paging/mapper/trait.Translate.html#method.translate_addr -[`translate`]: https://docs.rs/x86_64/0.13.2/x86_64/structures/paging/mapper/trait.Translate.html#tymethod.translate +[`Mapper`]: https://docs.rs/x86_64/0.14.2/x86_64/structures/paging/mapper/trait.Mapper.html +[`translate_page`]: https://docs.rs/x86_64/0.14.2/x86_64/structures/paging/mapper/trait.Mapper.html#tymethod.translate_page +[`map_to`]: https://docs.rs/x86_64/0.14.2/x86_64/structures/paging/mapper/trait.Mapper.html#method.map_to +[`Translate`]: https://docs.rs/x86_64/0.14.2/x86_64/structures/paging/mapper/trait.Translate.html +[`translate_addr`]: https://docs.rs/x86_64/0.14.2/x86_64/structures/paging/mapper/trait.Translate.html#method.translate_addr +[`translate`]: https://docs.rs/x86_64/0.14.2/x86_64/structures/paging/mapper/trait.Translate.html#tymethod.translate The traits only define the interface, they don't provide any implementation. The `x86_64` crate currently provides three types that implement the traits with different requirements. The [`OffsetPageTable`] type assumes that the complete physical memory is mapped to the virtual address space at some offset. The [`MappedPageTable`] is a bit more flexible: It only requires that each page table frame is mapped to the virtual address space at a calculable address. Finally, the [`RecursivePageTable`] type can be used to access page table frames through [recursive page tables](#recursive-page-tables). -[`OffsetPageTable`]: https://docs.rs/x86_64/0.13.2/x86_64/structures/paging/mapper/struct.OffsetPageTable.html -[`MappedPageTable`]: https://docs.rs/x86_64/0.13.2/x86_64/structures/paging/mapper/struct.MappedPageTable.html -[`RecursivePageTable`]: https://docs.rs/x86_64/0.13.2/x86_64/structures/paging/mapper/struct.RecursivePageTable.html +[`OffsetPageTable`]: https://docs.rs/x86_64/0.14.2/x86_64/structures/paging/mapper/struct.OffsetPageTable.html +[`MappedPageTable`]: https://docs.rs/x86_64/0.14.2/x86_64/structures/paging/mapper/struct.MappedPageTable.html +[`RecursivePageTable`]: https://docs.rs/x86_64/0.14.2/x86_64/structures/paging/mapper/struct.RecursivePageTable.html -In our case, the bootloader maps the complete physical memory at a virtual address specfied by the `physical_memory_offset` variable, so we can use the `OffsetPageTable` type. To initialize it, we create a new `init` function in our `memory` module: +In our case, the bootloader maps the complete physical memory at a virtual address specified by the `physical_memory_offset` variable, so we can use the `OffsetPageTable` type. To initialize it, we create a new `init` function in our `memory` module: ```rust use x86_64::structures::paging::OffsetPageTable; @@ -650,7 +650,7 @@ unsafe fn active_level_4_table(physical_memory_offset: VirtAddr) The function takes the `physical_memory_offset` as an argument and returns a new `OffsetPageTable` instance with a `'static` lifetime. This means that the instance stays valid for the complete runtime of our kernel. In the function body, we first call the `active_level_4_table` function to retrieve a mutable reference to the level 4 page table. We then invoke the [`OffsetPageTable::new`] function with this reference. As the second parameter, the `new` function expects the virtual address at which the mapping of the physical memory starts, which is given in the `physical_memory_offset` variable. -[`OffsetPageTable::new`]: https://docs.rs/x86_64/0.13.2/x86_64/structures/paging/mapper/struct.OffsetPageTable.html#method.new +[`OffsetPageTable::new`]: https://docs.rs/x86_64/0.14.2/x86_64/structures/paging/mapper/struct.OffsetPageTable.html#method.new The `active_level_4_table` function should be only called from the `init` function from now on because it can easily lead to aliased mutable references when called multiple times, which can cause undefined behavior. For this reason, we make the function private by removing the `pub` specifier. @@ -701,8 +701,8 @@ Until now we only looked at the page tables without modifying anything. Let's ch We will use the [`map_to`] function of the [`Mapper`] trait for our implementation, so let's take a look at that function first. The documentation tells us that it takes four arguments: the page that we want to map, the frame that the page should be mapped to, a set of flags for the page table entry, and a `frame_allocator`. The frame allocator is needed because mapping the given page might require creating additional page tables, which need unused frames as backing storage. -[`map_to`]: https://docs.rs/x86_64/0.13.2/x86_64/structures/paging/trait.Mapper.html#tymethod.map_to -[`Mapper`]: https://docs.rs/x86_64/0.13.2/x86_64/structures/paging/trait.Mapper.html +[`map_to`]: https://docs.rs/x86_64/0.14.2/x86_64/structures/paging/trait.Mapper.html#tymethod.map_to +[`Mapper`]: https://docs.rs/x86_64/0.14.2/x86_64/structures/paging/trait.Mapper.html #### A `create_example_mapping` Function @@ -741,8 +741,8 @@ In addition to the `page` that should be mapped, the function expects a mutable [impl-trait-arg]: https://doc.rust-lang.org/book/ch10-02-traits.html#traits-as-parameters [generic]: https://doc.rust-lang.org/book/ch10-00-generics.html -[`FrameAllocator`]: https://docs.rs/x86_64/0.13.2/x86_64/structures/paging/trait.FrameAllocator.html -[`PageSize`]: https://docs.rs/x86_64/0.13.2/x86_64/structures/paging/page/trait.PageSize.html +[`FrameAllocator`]: https://docs.rs/x86_64/0.14.2/x86_64/structures/paging/trait.FrameAllocator.html +[`PageSize`]: https://docs.rs/x86_64/0.14.2/x86_64/structures/paging/page/trait.PageSize.html The [`map_to`] method is unsafe because the caller must ensure that the frame is not already in use. The reason for this is that mapping the same frame twice could result in undefined behavior, for example when two different `&mut` references point to the same physical memory location. In our case, we reuse the VGA text buffer frame, which is already mapped, so we break the required condition. However, the `create_example_mapping` function is only a temporary testing function and will be removed after this post, so it is ok. To remind us of the unsafety, we put a `FIXME` comment on the line. @@ -754,8 +754,8 @@ The [`map_to`] function can fail, so it returns a [`Result`]. Since this is just [`Result`]: https://doc.rust-lang.org/core/result/enum.Result.html [`expect`]: https://doc.rust-lang.org/core/result/enum.Result.html#method.expect -[`MapperFlush`]: https://docs.rs/x86_64/0.13.2/x86_64/structures/paging/mapper/struct.MapperFlush.html -[`flush`]: https://docs.rs/x86_64/0.13.2/x86_64/structures/paging/mapper/struct.MapperFlush.html#method.flush +[`MapperFlush`]: https://docs.rs/x86_64/0.14.2/x86_64/structures/paging/mapper/struct.MapperFlush.html +[`flush`]: https://docs.rs/x86_64/0.14.2/x86_64/structures/paging/mapper/struct.MapperFlush.html#method.flush [must_use]: https://doc.rust-lang.org/std/result/#results-must-be-used #### A dummy `FrameAllocator` @@ -789,7 +789,7 @@ The graphic shows the virtual address space on the left, the physical address sp Additionally, the graphic shows the physical frame of the VGA text buffer in red. Our goal is to map a previously unmapped virtual page to this frame using our `create_example_mapping` function. Since our `EmptyFrameAllocator` always returns `None`, we want to create the mapping so that no additional frames are needed from the allocator. This depends on the virtual page that we select for the mapping. -The graphic shows two canditate pages in the virtual address space, both marked in yellow. One page is at address `0x803fdfd000`, which is 3 pages before the mapped page (in blue). While the level 4 and level 3 page table indices are the same as for the blue page, the level 2 and level 1 indices are different (see the [previous post][page-table-indices]). The different index into the level 2 table means that a different level 1 table is used for this page. Since this level 1 table does not exist yet, we would need to create it if we chose that page for our example mapping, which would require an additional unused physical frame. In contrast, the second candidate page at address `0x803fe02000` does not have this problem because it uses the same level 1 page table than the blue page. Thus, all required page tables already exist. +The graphic shows two candidate pages in the virtual address space, both marked in yellow. One page is at address `0x803fdfd000`, which is 3 pages before the mapped page (in blue). While the level 4 and level 3 page table indices are the same as for the blue page, the level 2 and level 1 indices are different (see the [previous post][page-table-indices]). The different index into the level 2 table means that a different level 1 table is used for this page. Since this level 1 table does not exist yet, we would need to create it if we chose that page for our example mapping, which would require an additional unused physical frame. In contrast, the second candidate page at address `0x803fe02000` does not have this problem because it uses the same level 1 page table than the blue page. Thus, all required page tables already exist. [page-table-indices]: @/edition-2/posts/08-paging-introduction/index.md#paging-on-x86-64 From ed14ee779d432286d664a90a170f2f7cde30d174 Mon Sep 17 00:00:00 2001 From: woodyZootopia Date: Fri, 13 Aug 2021 13:33:55 +0900 Subject: [PATCH 04/11] =?UTF-8?q?=E7=BF=BB=E8=A8=B3=E3=82=92=E9=80=B2?= =?UTF-8?q?=E3=82=81=E3=81=9F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../09-paging-implementation/index.ja.md | 62 +++++++++---------- 1 file changed, 28 insertions(+), 34 deletions(-) diff --git a/blog/content/edition-2/posts/09-paging-implementation/index.ja.md b/blog/content/edition-2/posts/09-paging-implementation/index.ja.md index ad05eae9..58f62a57 100644 --- a/blog/content/edition-2/posts/09-paging-implementation/index.ja.md +++ b/blog/content/edition-2/posts/09-paging-implementation/index.ja.md @@ -32,8 +32,6 @@ translators = ["woodyZootopia"] [end of previous post]: @/edition-2/posts/08-paging-introduction/index.md#accessing-the-page-tables -To implement the approach, we will need support from the bootloader, so we'll configure it first. Afterward, we will implement a function that traverses the page table hierarchy in order to translate virtual to physical addresses. Finally, we learn how to create new mappings in the page tables and how to find unused memory frames for creating new page tables. - この方法を実装するには、ブートローダーからの補助が必要になるので、まずこれに設定を加えます。その後で、ページテーブルの階層構造を移動して、仮想アドレスを物理アドレスに変換する関数を実装します。最後に、ページテーブルに新しい対応関係を作る方法と、それを作るための未使用メモリを見つける方法を学びます。 ## ページテーブルにアクセスする @@ -42,7 +40,7 @@ To implement the approach, we will need support from the bootloader, so we'll co ![An example 4-level page hierarchy with each page table shown in physical memory](../paging-introduction/x86_64-page-table-translation.svg) -ここで需要なのは、それぞれのページテーブルのエントリは次のテーブルの**物理**アドレスであるということです。これにより、それらのアドレスに対しても変換することを避けられます。もしこの変換が行われたとしたら、性能的にも良くないですし、容易に変換の無限ループに陥りかねません。 +ここで重要なのは、それぞれのページテーブルのエントリは次のテーブルの**物理**アドレスであるということです。これにより、それらのアドレスに対しても変換することを避けられます。もしこの変換が行われたとしたら、性能的にも良くないですし、容易に変換の無限ループに陥りかねません。 問題は、私達のカーネル自体も仮想アドレスの上で動いているため、カーネルから直接物理アドレスにアクセスすることができないということです。例えば、アドレス`4KiB`にアクセスしたとき、私達は**仮想**アドレス`4KiB`にアクセスしているのであって、レベル4ページテーブルが格納されている**物理**アドレス`4KiB`にアクセスしているのではありません。物理アドレス`4KiB`にアクセスしたいなら、それに対応づけられている何らかの仮想アドレスを通じてのみ可能です。 @@ -56,23 +54,20 @@ To implement the approach, we will need support from the bootloader, so we'll co この例では、いくつかの恒等対応したページテーブルのフレームが見てとれます。こうすることで、ページテーブルの物理アドレスは仮想アドレスとしても正当になり、よってCR3レジスタから始めることで全てのレベルのページテーブルに簡単にアクセスできます。 -しかし、この方法では仮想アドレス空間が散らかってしまい、より大きいサイズの連続したメモリを見つけることが難しくなります。例えば、上の図において、[ファイルをメモリにマップする][memory-mapping a file]ために1000KiBの大きさの仮想メモリ領域を作りたいとします。`28KiB`を始点として領域を作ろうとすると、`1004KiB`のところで既存のページと衝突してしまうのでうまくいきません。そのため、`1008KiB`のような、十分な広さで対応付けのない領域が見つかるまで更に探さないといけません。これは[セグメンテーション][segmentation]の時にみた断片化の問題に似ています。 +しかし、この方法では仮想アドレス空間が散らかってしまい、より大きいサイズの連続したメモリを見つけることが難しくなります。例えば、上の図において、[ファイルをメモリにマップする][memory-mapping a file]ために1000KiBの大きさの仮想メモリ領域を作りたいとします。`28KiB`を始点として領域を作ろうとすると、`1004KiB`のところで既存のページと衝突してしまうのでうまくいきません。そのため、`1008KiB`のような、十分な広さで対応付けのない領域が見つかるまで更に探さないといけません。これは[セグメンテーション][segmentation]の時に見た断片化の問題に似ています。 [memory-mapping a file]: https://en.wikipedia.org/wiki/Memory-mapped_file [segmentation]: @/edition-2/posts/08-paging-introduction/index.md#fragmentation -Equally, it makes it much more difficult to create new page tables, because we need to find physical frames whose corresponding pages aren't already in use. For example, let's assume that we reserved the _virtual_ 1000 KiB memory region starting at `1008 KiB` for our memory-mapped file. Now we can't use any frame with a _physical_ address between `1000 KiB` and `2008 KiB` anymore, because we can't identity map it. 同様に、新しいページテーブルを作ることもずっと難しくなります。なぜなら、対応するページがまだ使われていない物理フレームを見つけないといけないからです。例えば、メモリマップト (に対応づけられた) ファイルのために`1008KiB`から1000KiBにわたって仮想メモリを占有したとしましょう。すると、すると、物理アドレス`1000KiB`から`2008KiB`までのフレームは、もう恒等対応を作ることができないので、使用することができません。 -### 固定オフセットの対応 +### 固定オフセットの対応づけ -To avoid the problem of cluttering the virtual address space, we can **use a separate memory region for page table mappings**. So instead of identity mapping page table frames, we map them at a fixed offset in the virtual address space. For example, the offset could be 10 TiB: 仮想アドレス空間を散らかしてしまうという問題を回避するために、**ページテーブルの対応づけのために別のメモリ領域を使う**ことができます。ページテーブルを恒等対応させる代わりに、仮想アドレス空間で一定のオフセットをおいて対応づけてみましょう。例えば、オフセットを10TiBにしてみましょう: ![The same figure as for the identity mapping, but each mapped virtual page is offset by 10 TiB.](page-tables-mapped-at-offset.svg) -By using the virtual memory in the range `10TiB..(10TiB + physical memory size)` exclusively for page table mappings, we avoid the collision problems of the identity mapping. Reserving such a large region of the virtual address space is only possible if the virtual address space is much larger than the physical memory size. This isn't a problem on x86_64 since the 48-bit address space is 256 TiB large. -`10TiB`から`10TiB+物理メモリ全体の大きさ`の範囲の仮想メモリをページテーブルの対応付け専用に使うことで、恒等対応のときに存在していた衝突問題を回避しています。このように巨大な領域を仮想アドレス空間内に用意するのは、仮想アドレス空間が物理メモリの大きさより遥かに大きい場合にのみ可能です。x86_64については、(x86_64で用いられている)48bit(仮想)アドレス空間は256TiBもの大きさがあるので、これは問題ではありません。 +`10TiB`から`10TiB+物理メモリ全体の大きさ`の範囲の仮想メモリをページテーブルの対応付け専用に使うことで、恒等対応のときに存在していた衝突問題を回避しています。このように巨大な領域を仮想アドレス空間内に用意するのは、仮想アドレス空間が物理メモリの大きさより遥かに大きい場合にのみ可能です。x86_64で用いられている48bit(仮想)アドレス空間は256TiBもの大きさがあるので、これは問題ではありません。 この方法では、新しいページテーブルを作るたびに新しい対応付けを作る必要があるという欠点があります。また、他のアドレス空間のページテーブルにアクセスすることができると、新しいプロセスを作るときに便利なのですがこれも不可能です。 @@ -96,61 +91,60 @@ By using the virtual memory in the range `10TiB..(10TiB + physical memory size)` ![A virtual and a physical address space with an identity mapped level 1 table, which maps its 0th entry to the level 2 table frame, thereby mapping that frame to page with address 0](temporarily-mapped-page-tables.svg) -The level 1 table in this graphic controls the first 2 MiB of the virtual address space. This is because it is reachable by starting at the CR3 register and following the 0th entry in the level 4, level 3, and level 2 page tables. The entry with index `8` maps the virtual page at address `32 KiB` to the physical frame at address `32 KiB`, thereby identity mapping the level 1 table itself. The graphic shows this identity-mapping by the horizontal arrow at `32 KiB`. -この図におけるレベル1テーブルは仮想アドレス空間の最初の2MiBを制御しています。なぜなら、CR3レジスタから始めて、レベル4、3、2のページテーブルの0番目のエントリを辿ることで到達できるからです。 +この図におけるレベル1テーブルは仮想アドレス空間の最初の2MiBを制御しています。なぜなら、このテーブルにはCR3レジスタから始めて、レベル4、3、2のページテーブルの0番目のエントリを辿ることで到達できるからです。8番目のエントリは、アドレス`32 KiB`の仮想アドレスページをアドレス`32 KiB`の物理アドレスページに対応付けるので、レベル1テーブル自体を恒等対応させています。この図ではその恒等対応を`32 KiB`のところの横向きの(茶色の)矢印で表しています。 -By writing to the identity-mapped level 1 table, our kernel can create up to 511 temporary mappings (512 minus the entry required for the identity mapping). In the above example, the kernel created two temporary mappings: +恒等対応させたレベル1テーブルに書き込むことによって、カーネルは最大511個の一時的な対応を作ることができます(512から、恒等対応に必要な1つを除く)。上の例では、カーネルは2つの一時的な対応を作りました: -- By mapping the 0th entry of the level 1 table to the frame with address `24 KiB`, it created a temporary mapping of the virtual page at `0 KiB` to the physical frame of the level 2 page table, indicated by the dashed arrow. -- By mapping the 9th entry of the level 1 table to the frame with address `4 KiB`, it created a temporary mapping of the virtual page at `36 KiB` to the physical frame of the level 4 page table, indicated by the dashed arrow. +- レベル1テーブルの0番目のエントリをアドレス`24 KiB`のフレームに対応付けることで、破線の矢印で示されているように`0 KiB`の仮想ページからレベル2ページテーブルの物理フレームへの一時的対応付けを行いました。 +- レベル1テーブルの9番目のエントリをアドレス`4 KiB`のフレームに対応付けることで、破線の矢印で示されているように`36 KiB`の仮想ページからレベル4ページテーブルの物理フレームへの一時的対応付を行いました。 -Now the kernel can access the level 2 page table by writing to page `0 KiB` and the level 4 page table by writing to page `36 KiB`. +これで、カーネルは`0 KiB`に書き込むことによってレベル2ページテーブルに、`36 KiB`に書き込むことによってレベル4ページテーブルにアクセスできるようになりました。 -The process for accessing an arbitrary page table frame with temporary mappings would be: +任意のページテーブルに一時的対応付けを用いてアクセスする手続きは以下のようになるでしょう: -- Search for a free entry in the identity-mapped level 1 table. -- Map that entry to the physical frame of the page table that we want to access. -- Access the target frame through the virtual page that maps to the entry. -- Set the entry back to unused thereby removing the temporary mapping again. +- 恒等対応しているレベル1テーブルのうち、使われていないエントリを探す。 +- そのエントリを私達のアクセスしたいページテーブルの物理フレームに対応付ける。 +- そのエントリに対応付けられている仮想ページを通じて、対象のフレームにアクセスする。 +- エントリを未使用に戻すことで、一時的対応付けを削除する。 -This approach reuses the same 512 virtual pages for creating the mappings and thus requires only 4KiB of physical memory. The drawback is that it is a bit cumbersome, especially since a new mapping might require modifications of multiple table levels, which means that we would need to repeat the above process multiple times. +この方法では、同じ512個の下層ページを対応付けを作成するために再利用するため、物理メモリは4KiBしか必要としません。欠点としては、やや面倒であるということが言えるでしょう。特に、新しい対応付けを作る際に複数のページテーブルの変更が必要になるかもしれず、上の手続きを複数回繰り返さなくてはならないかもしれません。 -### Recursive Page Tables +### 再帰的ページテーブル -Another interesting approach, that requires no additional page tables at all, is to **map the page table recursively**. The idea behind this approach is to map some entry of the level 4 page table to the level 4 table itself. By doing this, we effectively reserve a part of the virtual address space and map all current and future page table frames to that space. +他に興味深いアプローチとして、**再帰的にページテーブルを対応付ける**方法があり、この方法では追加のページテーブルは一切不要です。発想としては、レベル4ページテーブルのエントリのどれかをレベル4ページテーブル自体に対応付けるのです。こうすることにより、仮想アドレス空間の一部を予約しておき、現在及び将来のあらゆるページテーブルフレームをその空間に対応付けているのと同じことになります。 -Let's go through an example to understand how this all works: +これがうまく行く理由を説明するために、例を見てみましょう: ![An example 4-level page hierarchy with each page table shown in physical memory. Entry 511 of the level 4 page is mapped to frame 4KiB, the frame of the level 4 table itself.](recursive-page-table.png) -The only difference to the [example at the beginning of this post] is the additional entry at index `511` in the level 4 table, which is mapped to physical frame `4 KiB`, the frame of the level 4 table itself. +[この記事の最初での例][example at the beginning of this post]との唯一の違いは、レベル4テーブルの511番目に、物理フレーム`4 KiB`すなわちレベル4テーブル自体のフレームに対応付けられたエントリが追加されていることです。 -[example at the beginning of this post]: #accessing-page-tables +[example at the beginning of this post]: #peziteburuniakusesusuru -By letting the CPU follow this entry on a translation, it doesn't reach a level 3 table, but the same level 4 table again. This is similar to a recursive function that calls itself, therefore this table is called a _recursive page table_. The important thing is that the CPU assumes that every entry in the level 4 table points to a level 3 table, so it now treats the level 4 table as a level 3 table. This works because tables of all levels have the exact same layout on x86_64. +CPUにこのエントリを辿らせるようにすると、レベル3テーブルではなく、そのレベル4テーブルに再び到達します。これは再帰的関数(自らを呼び出す関数)に似ているので、**再帰的ページテーブル**と呼ばれます。レベル4テーブルのすべてのエントリはレベル3テーブルを指しているとCPUは思っているので、CPUはいまレベル4テーブルをレベル3テーブルとして扱っているということに注目してください。これがうまく行くのは、x86_64においてはすべてのレベルのテーブルが全く同じレイアウトを持っているためです。 -By following the recursive entry one or multiple times before we start the actual translation, we can effectively shorten the number of levels that the CPU traverses. For example, if we follow the recursive entry once and then proceed to the level 3 table, the CPU thinks that the level 3 table is a level 2 table. Going further, it treats the level 2 table as a level 1 table and the level 1 table as the mapped frame. This means that we can now read and write the level 1 page table because the CPU thinks that it is the mapped frame. The graphic below illustrates the 5 translation steps: +実際に変換を始める前に、この再帰エントリを1回以上たどることで、CPUのたどる階層の数を短くできます。例えば、一度再帰的エントリを辿ったあとでレベル3テーブルに進むと、CPUはレベル3テーブルをレベル2テーブルだと思い込みます。同様に、レベル2テーブルをレベル1テーブルだと、レベル1テーブルを対応付けられた(物理)フレームだと思います。CPUがこれを物理フレームだと思っているということは、レベル1ページテーブルを読み書きできるということを意味します。下の図はこの5回の変換ステップを示しています: ![The above example 4-level page hierarchy with 5 arrows: "Step 0" from CR4 to level 4 table, "Step 1" from level 4 table to level 4 table, "Step 2" from level 4 table to level 3 table, "Step 3" from level 3 table to level 2 table, and "Step 4" from level 2 table to level 1 table.](recursive-page-table-access-level-1.png) -Similarly, we can follow the recursive entry twice before starting the translation to reduce the number of traversed levels to two: +同様に、変換の前に再帰エントリを2回たどることで、階層移動の回数を2回に減らせます: ![The same 4-level page hierarchy with the following 4 arrows: "Step 0" from CR4 to level 4 table, "Steps 1&2" from level 4 table to level 4 table, "Step 3" from level 4 table to level 3 table, and "Step 4" from level 3 table to level 2 table.](recursive-page-table-access-level-2.png) -Let's go through it step by step: First, the CPU follows the recursive entry on the level 4 table and thinks that it reaches a level 3 table. Then it follows the recursive entry again and thinks that it reaches a level 2 table. But in reality, it is still on the level 4 table. When the CPU now follows a different entry, it lands on a level 3 table but thinks it is already on a level 1 table. So while the next entry points at a level 2 table, the CPU thinks that it points to the mapped frame, which allows us to read and write the level 2 table. +ステップごとにこれを見てみましょう:まず、CPUはレベル4テーブルの再帰エントリをたどり、レベル3テーブルに着いたと思い込みます。同じ再帰エントリを再びたどり、レベル2テーブルに着いたと考えます。しかし実際にはまだレベル4テーブルから動いていません。CPUが異なるエントリをたどると、レベル3テーブルに到着するのですが、CPUはレベル1にすでにいるのだと思っています。そのため、次のエントリはレベル2テーブルを指しているのですが、CPUは対応付けられた物理フレームを指していると思うので、私達はレベル2テーブルを読み書きできるというわけです。 -Accessing the tables of levels 3 and 4 works in the same way. For accessing the level 3 table, we follow the recursive entry three times, tricking the CPU into thinking it is already on a level 1 table. Then we follow another entry and reach a level 3 table, which the CPU treats as a mapped frame. For accessing the level 4 table itself, we just follow the recursive entry four times until the CPU treats the level 4 table itself as the mapped frame (in blue in the graphic below). +レベル3や4のテーブルにアクセスするのも同じやり方でできます。レベル3テーブルにアクセスするためには、再帰エントリを3回たどることでCPUを騙し、すでにレベル1テーブルにいると思い込ませます。そこで別のエントリをたどりレベル3テーブルに着くと、CPUはそれを対応付けられたフレームとして扱います。レベル4テーブル自体にアクセスするには、再帰エントリを4回辿ればCPUはそのレベル4テーブル自体を対応付けられたフレームとして扱ってくれるというわけです(下の青紫の矢印)。 ![The same 4-level page hierarchy with the following 3 arrows: "Step 0" from CR4 to level 4 table, "Steps 1,2,3" from level 4 table to level 4 table, and "Step 4" from level 4 table to level 3 table. In blue the alternative "Steps 1,2,3,4" arrow from level 4 table to level 4 table.](recursive-page-table-access-level-3.png) -It might take some time to wrap your head around the concept, but it works quite well in practice. +この概念を理解するのは難しいかもしれませんが、実際これは非常にうまく行くのです。 -In the section below we explain how to construct virtual addresses for following the recursive entry one or multiple times. We will not use recursive paging for our implementation, so you don't need to read it to continue with the post. If it interests you, just click on _"Address Calculation"_ to expand it. +下のセクションでは、再帰エントリをたどるための仮想アドレスを構成する方法について説明します。私達の(カーネルの)実装には再帰的ページングは使わないので、これを読まずに記事の続きを読み進めても構いません。もし興味がおありでしたら、下の「アドレス計算」をクリックして展開してください。 ---
-

Address Calculation

+

アドレス計算

We saw that we can access tables of all levels by following the recursive entry once or multiple times before the actual translation. Since the indexes into the tables of the four levels are derived directly from the virtual address, we need to construct special virtual addresses for this technique. Remember, the page table indexes are derived from the address in the following way: From 803d2e6c7a1966e55e17c6d1c3887459706eb9da Mon Sep 17 00:00:00 2001 From: woodyZootopia Date: Sun, 15 Aug 2021 14:55:34 +0900 Subject: [PATCH 05/11] Translate till the end --- .../09-paging-implementation/index.ja.md | 475 +++++++++--------- 1 file changed, 245 insertions(+), 230 deletions(-) diff --git a/blog/content/edition-2/posts/09-paging-implementation/index.ja.md b/blog/content/edition-2/posts/09-paging-implementation/index.ja.md index 58f62a57..34540189 100644 --- a/blog/content/edition-2/posts/09-paging-implementation/index.ja.md +++ b/blog/content/edition-2/posts/09-paging-implementation/index.ja.md @@ -10,7 +10,7 @@ translation_based_on_commit = "27ab4518acbb132e327ed4f4f0508393e9d4d684" translators = ["woodyZootopia"] +++ -この記事では私達のカーネルをページングに対応させる方法についてお伝えします。まず物理ページテーブルのフレームにカーネルがアクセスできるようにする様々な方法を示し、それらの利点と欠点について議論します。次にアドレス変換関数を、ついで新しいマッピングを作るための関数を実装します。 +この記事では私達のカーネルをページングに対応させる方法についてお伝えします。まずページテーブルの物理フレームにカーネルがアクセスできるようにする様々な方法を示し、それらの利点と欠点について議論します。次にアドレス変換関数を、ついで新しい対応付けを作るための関数を実装します。 @@ -24,6 +24,8 @@ translators = ["woodyZootopia"] ## 導入 +## 記事へのリンクを日本語のものに直す + [1つ前の記事][previous post]ではページングの概念を説明しました。セグメンテーションと比較することによってページングのメリットを示し、ページングとページテーブルの仕組みを説明し、そして`x86_64`における4層ページテーブルの設計を導入しました。ブートローダはすでにページテーブルの階層構造を設定してしまっているので、私達のカーネルは既に仮想アドレス上で動いているということを学びました。これにより、不正なメモリアクセスは、任意の物理メモリを書き換えてしまう代わりにページフォルト例外を発生させるので、安全性が向上しています。 [previous post]: @/edition-2/posts/08-paging-introduction/index.ja.md @@ -52,24 +54,24 @@ translators = ["woodyZootopia"] ![A virtual and a physical address space with various virtual pages mapped to the physical frame with the same address](identity-mapped-page-tables.svg) -この例では、いくつかの恒等対応したページテーブルのフレームが見てとれます。こうすることで、ページテーブルの物理アドレスは仮想アドレスとしても正当になり、よってCR3レジスタから始めることで全てのレベルのページテーブルに簡単にアクセスできます。 +この例では、いくつかの恒等対応したページテーブルのフレームが見てとれます。こうすることで、ページテーブルの物理アドレスは仮想アドレスと同じ値になり、よってCR3レジスタから始めることで全ての階層のページテーブルに簡単にアクセスできます。 -しかし、この方法では仮想アドレス空間が散らかってしまい、より大きいサイズの連続したメモリを見つけることが難しくなります。例えば、上の図において、[ファイルをメモリにマップする][memory-mapping a file]ために1000KiBの大きさの仮想メモリ領域を作りたいとします。`28KiB`を始点として領域を作ろうとすると、`1004KiB`のところで既存のページと衝突してしまうのでうまくいきません。そのため、`1008KiB`のような、十分な広さで対応付けのない領域が見つかるまで更に探さないといけません。これは[セグメンテーション][segmentation]の時に見た断片化の問題に似ています。 +しかし、この方法では仮想アドレス空間が散らかってしまい、大きいサイズの連続したメモリを見つけることが難しくなります。例えば、上の図において、[ファイルをメモリにマップする][memory-mapping a file]ために1000KiBの大きさの仮想メモリ領域を作りたいとします。`28KiB`を始点として領域を作ろうとすると、`1004KiB`のところで既存のページと衝突してしまうのでうまくいきません。そのため、`1008KiB`のような、十分な広さで対応付けのない領域が見つかるまで更に探さないといけません。これは[セグメンテーション][segmentation]の時に見た断片化の問題に似ています。 [memory-mapping a file]: https://en.wikipedia.org/wiki/Memory-mapped_file [segmentation]: @/edition-2/posts/08-paging-introduction/index.md#fragmentation -同様に、新しいページテーブルを作ることもずっと難しくなります。なぜなら、対応するページがまだ使われていない物理フレームを見つけないといけないからです。例えば、メモリマップト (に対応づけられた) ファイルのために`1008KiB`から1000KiBにわたって仮想メモリを占有したとしましょう。すると、すると、物理アドレス`1000KiB`から`2008KiB`までのフレームは、もう恒等対応を作ることができないので、使用することができません。 +同様に、新しいページテーブルを作ることもずっと難しくなります。なぜなら、対応するページがまだ使われていない物理フレームを見つけないといけないからです。例えば、メモリマップト (に対応づけられた) ファイルのために`1008KiB`から1000KiBにわたって仮想メモリを占有したとしましょう。すると、すると、物理アドレス`1000KiB`から`2008KiB`までのフレームは、もう恒等対応を作ることができないので使用することができません。 ### 固定オフセットの対応づけ -仮想アドレス空間を散らかしてしまうという問題を回避するために、**ページテーブルの対応づけのために別のメモリ領域を使う**ことができます。ページテーブルを恒等対応させる代わりに、仮想アドレス空間で一定のオフセットをおいて対応づけてみましょう。例えば、オフセットを10TiBにしてみましょう: +仮想アドレス空間を散らかしてしまうという問題を回避するために、**ページテーブルの対応づけのために別のメモリ領域を使う**ことができます。ページテーブルを恒等対応させる代わりに、仮想アドレス空間で一定の補正値 (オフセット) をおいて対応づけてみましょう。例えば、オフセットを10TiBにしてみましょう: ![The same figure as for the identity mapping, but each mapped virtual page is offset by 10 TiB.](page-tables-mapped-at-offset.svg) `10TiB`から`10TiB+物理メモリ全体の大きさ`の範囲の仮想メモリをページテーブルの対応付け専用に使うことで、恒等対応のときに存在していた衝突問題を回避しています。このように巨大な領域を仮想アドレス空間内に用意するのは、仮想アドレス空間が物理メモリの大きさより遥かに大きい場合にのみ可能です。x86_64で用いられている48bit(仮想)アドレス空間は256TiBもの大きさがあるので、これは問題ではありません。 -この方法では、新しいページテーブルを作るたびに新しい対応付けを作る必要があるという欠点があります。また、他のアドレス空間のページテーブルにアクセスすることができると、新しいプロセスを作るときに便利なのですがこれも不可能です。 +この方法では、新しいページテーブルを作るたびに新しい対応付けを作る必要があるという欠点があります。また、他のアドレス空間のページテーブルにアクセスすることができると新しいプロセスを作るときに便利なのですが、これも不可能です。 ### 物理メモリ全体を対応付ける @@ -81,7 +83,7 @@ translators = ["woodyZootopia"] この方法の欠点は、物理メモリへの対応付けを格納するために、追加でページテーブルが必要になるところです。これらのページテーブルもどこかに格納されなければならず、したがって物理メモリの一部を占有することになります。これはメモリの量が少ないデバイスにおいては問題となりえます。 -しかし、x86_64においては、通常の4KiBサイズのページに代わって、大きさ2MiBの[huge pages]を対応付けに使うことができます。こうすれば、例えば32GiBの物理メモリを対応付けるのに、レベル3テーブル1個とレベル2テーブル32個があればいいので、たったの132KiBしか必要ではありません。huge pagesは、トランスレーション・ルックアサイド・バッファ (TLB) のエントリをあまり使わないので、キャッシュ的にも効率が良いです。 +しかし、x86_64においては、通常の4KiBサイズのページに代わって、大きさ2MiBの[huge page][huge pages]を対応付けに使うことができます。こうすれば、例えば32GiBの物理メモリを対応付けるのに、レベル3テーブル1個とレベル2テーブル32個があればいいので、たったの132KiBしか必要ではありません。huge pagesは、トランスレーション・ルックアサイド・バッファ (TLB) のエントリをあまり使わないので、キャッシュ的にも効率が良いです。 [huge pages]: https://en.wikipedia.org/wiki/Page_%28computer_memory%29#Multiple_page_sizes @@ -123,7 +125,7 @@ translators = ["woodyZootopia"] CPUにこのエントリを辿らせるようにすると、レベル3テーブルではなく、そのレベル4テーブルに再び到達します。これは再帰的関数(自らを呼び出す関数)に似ているので、**再帰的ページテーブル**と呼ばれます。レベル4テーブルのすべてのエントリはレベル3テーブルを指しているとCPUは思っているので、CPUはいまレベル4テーブルをレベル3テーブルとして扱っているということに注目してください。これがうまく行くのは、x86_64においてはすべてのレベルのテーブルが全く同じレイアウトを持っているためです。 -実際に変換を始める前に、この再帰エントリを1回以上たどることで、CPUのたどる階層の数を短くできます。例えば、一度再帰的エントリを辿ったあとでレベル3テーブルに進むと、CPUはレベル3テーブルをレベル2テーブルだと思い込みます。同様に、レベル2テーブルをレベル1テーブルだと、レベル1テーブルを対応付けられた(物理)フレームだと思います。CPUがこれを物理フレームだと思っているということは、レベル1ページテーブルを読み書きできるということを意味します。下の図はこの5回の変換ステップを示しています: +実際に変換を始める前に、この再帰エントリを1回以上たどることで、CPUのたどる階層の数を短くできます。例えば、一度再帰エントリを辿ったあとでレベル3テーブルに進むと、CPUはレベル3テーブルをレベル2テーブルだと思い込みます。同様に、レベル2テーブルをレベル1テーブルだと、レベル1テーブルを対応付けられた(物理)フレームだと思います。CPUがこれを物理フレームだと思っているということは、レベル1ページテーブルを読み書きできるということを意味します。下の図はこの5回の変換ステップを示しています: ![The above example 4-level page hierarchy with 5 arrows: "Step 0" from CR4 to level 4 table, "Step 1" from level 4 table to level 4 table, "Step 2" from level 4 table to level 3 table, "Step 3" from level 3 table to level 2 table, and "Step 4" from level 2 table to level 1 table.](recursive-page-table-access-level-1.png) @@ -139,74 +141,75 @@ CPUにこのエントリを辿らせるようにすると、レベル3テーブ この概念を理解するのは難しいかもしれませんが、実際これは非常にうまく行くのです。 -下のセクションでは、再帰エントリをたどるための仮想アドレスを構成する方法について説明します。私達の(カーネルの)実装には再帰的ページングは使わないので、これを読まずに記事の続きを読み進めても構いません。もし興味がおありでしたら、下の「アドレス計算」をクリックして展開してください。 +下のセクションでは、再帰エントリをたどるための仮想アドレスを構成する方法について説明します。私達の(OSの)実装には再帰的ページングは使わないので、これを読まずに記事の続きを読み進めても構いません。もし興味がおありでしたら、下の「アドレス計算」をクリックして展開してください。 ---

アドレス計算

-We saw that we can access tables of all levels by following the recursive entry once or multiple times before the actual translation. Since the indexes into the tables of the four levels are derived directly from the virtual address, we need to construct special virtual addresses for this technique. Remember, the page table indexes are derived from the address in the following way: +実際の変換の前に再帰的移動を1回または複数回行うことですべての階層のテーブルにアクセスできるということを見てきました。4つのテーブルそれぞれのどのインデクスが使われるかは仮想アドレスから直接計算されていましたから、再帰エントリを使うためには特別な仮想アドレスを作り出す必要があります。ページテーブルのインデクスは仮想アドレスから以下のように計算されていたことを思い出してください: ![Bits 0–12 are the page offset, bits 12–21 the level 1 index, bits 21–30 the level 2 index, bits 30–39 the level 3 index, and bits 39–48 the level 4 index](../paging-introduction/x86_64-table-indices-from-address.svg) -Let's assume that we want to access the level 1 page table that maps a specific page. As we learned above, this means that we have to follow the recursive entry one time before continuing with the level 4, level 3, and level 2 indexes. To do that we move each block of the address one block to the right and set the original level 4 index to the index of the recursive entry: +あるページを対応付けているレベル1テーブルにアクセスしたいとします。上で学んだように、このためには再帰エントリを1度辿ってからレベル4,3,2のインデクスへと続けていく必要があります。これをするために、それぞれのアドレスブロックを一つ右にずらし、レベル4のインデクスがあったところに再帰エントリのインデクスをセットします: ![Bits 0–12 are the offset into the level 1 table frame, bits 12–21 the level 2 index, bits 21–30 the level 3 index, bits 30–39 the level 4 index, and bits 39–48 the index of the recursive entry](table-indices-from-address-recursive-level-1.svg) -For accessing the level 2 table of that page, we move each index block two blocks to the right and set both the blocks of the original level 4 index and the original level 3 index to the index of the recursive entry: +そのページのレベル2テーブルにアクセスしたい場合、それぞれのブロックを2つ右にずらし、レベル4と3のインデクスがあったところに再帰エントリのインデクスをセットします: ![Bits 0–12 are the offset into the level 2 table frame, bits 12–21 the level 3 index, bits 21–30 the level 4 index, and bits 30–39 and bits 39–48 are the index of the recursive entry](table-indices-from-address-recursive-level-2.svg) -Accessing the level 3 table works by moving each block three blocks to the right and using the recursive index for the original level 4, level 3, and level 2 address blocks: +レベル3テーブルにアクセスする場合、それぞれのブロックを3つ右にずらし、レベル4,3,2のインデクスがあったところに再帰インデクスを使います: ![Bits 0–12 are the offset into the level 3 table frame, bits 12–21 the level 4 index, and bits 21–30, bits 30–39 and bits 39–48 are the index of the recursive entry](table-indices-from-address-recursive-level-3.svg) -Finally, we can access the level 4 table by moving each block four blocks to the right and using the recursive index for all address blocks except for the offset: +最後に、レベル4テーブルにはそれぞれのブロックを4ブロックずらし、オフセットを除いてすべてのアドレスブロックに再帰インデクスを使うことでアクセスできます: ![Bits 0–12 are the offset into the level l table frame and bits 12–21, bits 21–30, bits 30–39 and bits 39–48 are the index of the recursive entry](table-indices-from-address-recursive-level-4.svg) We can now calculate virtual addresses for the page tables of all four levels. We can even calculate an address that points exactly to a specific page table entry by multiplying its index by 8, the size of a page table entry. +これで、4つの階層すべてのページテーブルの仮想アドレスを計算できます。 -The table below summarizes the address structure for accessing the different kinds of frames: +下の表は、それぞれの種類のフレームにアクセスするためのアドレス構造をまとめたものです: -Virtual Address for | Address Structure ([octal]) -------------------- | ------------------------------- -Page | `0o_SSSSSS_AAA_BBB_CCC_DDD_EEEE` -Level 1 Table Entry | `0o_SSSSSS_RRR_AAA_BBB_CCC_DDDD` -Level 2 Table Entry | `0o_SSSSSS_RRR_RRR_AAA_BBB_CCCC` -Level 3 Table Entry | `0o_SSSSSS_RRR_RRR_RRR_AAA_BBBB` -Level 4 Table Entry | `0o_SSSSSS_RRR_RRR_RRR_RRR_AAAA` +……の仮想アドレス | アドレス構造([8進][octal]) +------------------- | ------------------------------- +ページ | `0o_SSSSSS_AAA_BBB_CCC_DDD_EEEE` +レベル1テーブルエントリ | `0o_SSSSSS_RRR_AAA_BBB_CCC_DDDD` +レベル2テーブルエントリ | `0o_SSSSSS_RRR_RRR_AAA_BBB_CCCC` +レベル3テーブルエントリ | `0o_SSSSSS_RRR_RRR_RRR_AAA_BBBB` +レベル4テーブルエントリ | `0o_SSSSSS_RRR_RRR_RRR_RRR_AAAA` [octal]: https://en.wikipedia.org/wiki/Octal -Whereas `AAA` is the level 4 index, `BBB` the level 3 index, `CCC` the level 2 index, and `DDD` the level 1 index of the mapped frame, and `EEEE` the offset into it. `RRR` is the index of the recursive entry. When an index (three digits) is transformed to an offset (four digits), it is done by multiplying it by 8 (the size of a page table entry). With this offset, the resulting address directly points to the respective page table entry. +ただし、`AAA`がレベル4インデクス、`BBB`がレベル3インデクス、`CCC`がレベル2インデクス、`DDD`が対応付けられたフレームのレベル1インデクス、`EEE`がオフセットです。`RRR`が再帰エントリのインデクスです。インデクス(3ケタ)をオフセット(4ケタ)に変換するときは、8倍(ページテーブルエントリのサイズ倍)しています。 -`SSSSSS` are sign extension bits, which means that they are all copies of bit 47. This is a special requirement for valid addresses on the x86_64 architecture. We explained it in the [previous post][sign extension]. +`SSSSS`は符号拡張ビットです、すなわち47番目のビットのコピーです。これはx86_64におけるアドレスの特殊な要求の一つです。これは[前回の記事][sign extension]で説明しました。 [sign extension]: @/edition-2/posts/08-paging-introduction/index.md#paging-on-x86-64 -We use [octal] numbers for representing the addresses since each octal character represents three bits, which allows us to clearly separate the 9-bit indexes of the different page table levels. This isn't possible with the hexadecimal system where each character represents four bits. +[8進][octal]数を用いたのは、8進数の1文字が3ビットを表すため、9ビットからなるそれぞれのページテーブルをきれいに分けることができるためです。4ビットからなる16進ではこうはいきません。 -##### In Rust Code +##### Rustのコードでは…… -To construct such addresses in Rust code, you can use bitwise operations: +これらのアドレスをRustのコードで構成するには、ビット演算を用いるとよいです: ```rust -// 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 +let r = 0o777; // 再帰インデクス +let sign = 0o177777 << 48; // 符号拡張 -// 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 l4_idx = (addr >> 39) & 0o777; // レベル4インデクス +let l3_idx = (addr >> 30) & 0o777; // レベル3インデクス +let l2_idx = (addr >> 21) & 0o777; // レベル2インデクス +let l1_idx = (addr >> 12) & 0o777; // レベル1インデクス 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 = @@ -217,9 +220,10 @@ let level_1_table_addr = sign | (r << 39) | (l4_idx << 30) | (l3_idx << 21) | (l2_idx << 12); ``` -The above code assumes that the last level 4 entry with index `0o777` (511) is recursively mapped. This isn't the case currently, so the code won't work yet. See below on how to tell the bootloader to set up the recursive mapping. +上のコードは、レベル4エントリの最後(インデクス`0o777`すなわち511)が再帰対応していると仮定しています。この対応はまだ行っていないので、この仮定は正しくありません。以下でブートローダに再帰対応付けを設定させる方法を説明します。 Alternatively to performing the bitwise operations by hand, you can use the [`RecursivePageTable`] type of the `x86_64` crate, which provides safe abstractions for various page table operations. For example, the code below shows how to translate a virtual address to its mapped physical address: +ビット演算を自前で行う代わりに、`x86_64`クレートの[`RecursivePageTable`]型を使うこともできます。これは様々なページ操作の安全な抽象化を提供します。例えば、以下のコードは仮想アドレスを対応付けられた物理アドレスに変換する方法を示しています [`RecursivePageTable`]: https://docs.rs/x86_64/0.14.2/x86_64/structures/paging/mapper/struct.RecursivePageTable.html @@ -229,7 +233,7 @@ Alternatively to performing the bitwise operations by hand, you can use the [`Re use x86_64::structures::paging::{Mapper, Page, PageTable, RecursivePageTable}; use x86_64::{VirtAddr, PhysAddr}; -/// Creates a RecursivePageTable instance from the level 4 address. +/// レベル4アドレスからRecursivePageTableインスタンスをつくる let level_4_table_addr = […]; let level_4_table_ptr = level_4_table_addr as *mut PageTable; let recursive_page_table = unsafe { @@ -238,63 +242,65 @@ let recursive_page_table = unsafe { } -/// 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())) ``` -Again, a valid recursive mapping is required for this code. With such a mapping, the missing `level_4_table_addr` can be calculated as in the first code example. +繰り返しになりますが、このコード(が正しく実行される)には正しい再帰対応がなされていることが必要となります。この対応付けがあるなら、空欄になっている`level_4_table_addr`を最初のコード例と同じ値にすればよいです。
--- -Recursive Paging is an interesting technique that shows how powerful a single mapping in a page table can be. It is relatively easy to implement and only requires a minimal amount of setup (just a single recursive entry), so it's a good choice for first experiments with paging. +再帰的ページングは、ページテーブルのたった一つの対応付けがいかに強力に使えるかを示す興味深いテクニックです。比較的実装するのが簡単であり、ほとんど設定も必要でない(一つ再帰エントリを作るだけ)ので、ページングを使って最初に実装するのに格好の対象でしょう。 -However, it also has some disadvantages: +しかし、いくつか欠点もあります: -- It occupies a large amount of virtual memory (512GiB). This isn't a big problem in the large 48-bit address space, but it might lead to suboptimal cache behavior. -- It only allows accessing the currently active address space easily. Accessing other address spaces is still possible by changing the recursive entry, but a temporary mapping is required for switching back. We described how to do this in the (outdated) [_Remap The Kernel_] post. -- It heavily relies on the page table format of x86 and might not work on other architectures. +- 大量の仮想メモリ領域(512GiB)を占有してしまう。私達の使っている48bitアドレス空間は巨大なのでこのことはさしたる問題にはなりませんが、キャッシュの挙動が最適でなくなってしまうかもしれません。 +- 現在有効なアドレス空間にしか簡単にはアクセスできない。他のアドレス空間にアクセスするのは再帰エントリを変更することで可能ではあるものの、もとに戻すためには一時的対応付けが必要。これを行う方法については[カーネルをリマップする][_Remap The Kernel_](未訳、また旧版のため情報が古い)という記事を読んでください。 +- x86のページテーブルの方式に強く依存しており、他のアーキテクチャでは動作しないかもしれない。 [_Remap The Kernel_]: https://os.phil-opp.com/remap-the-kernel/#overview -## Bootloader Support +## ブートローダのサポート -All of these approaches require page table modifications for their setup. For example, mappings for the physical memory need to be created or an entry of the level 4 table needs to be mapped recursively. The problem is that we can't create these required mappings without an existing way to access the page tables. +これらのアプローチはすべて、準備のためにページテーブルに対する修正が必要になります。例えば、物理メモリへの対応付けを作ったり、レベル4テーブルのエントリを再帰的に対応付けたりなどです。問題は、これらの必要な対応付けを作るためには、すでにページテーブルにアクセスできるようになっていなければいけないということです。 -This means that we need the help of the bootloader, which creates the page tables that our kernel runs on. The bootloader has access to the page tables, so it can create any mappings that we need. In its current implementation, the `bootloader` crate has support for two of the above approaches, controlled through [cargo features]: +これが意味するのは、私達のカーネルが使うページテーブルを作っている、ブートローダの手助けが必要になるということです。ブートローダはページテーブルへのアクセスができますから、私達の必要とするどんな対応付けも作れます。`bootloader`クレートは上の2つのアプローチをどちらもサポートしており、現在の実装においては[cargoのfeatures][cargo features]を使ってこれらをコントロールします。 [cargo features]: https://doc.rust-lang.org/cargo/reference/features.html#the-features-section -- The `map_physical_memory` feature maps the complete physical memory somewhere into the virtual address space. Thus, the kernel can access all physical memory and can follow the [_Map the Complete Physical Memory_](#map-the-complete-physical-memory) approach. -- With the `recursive_page_table` feature, the bootloader maps an entry of the level 4 page table recursively. This allows the kernel to access the page tables as described in the [_Recursive Page Tables_](#recursive-page-tables) section. +- `map_physical_memory` featureを使うと、全物理メモリを仮想アドレス空間のどこかに対応付けます。そのため、カーネルはすべての物理メモリにアクセスでき、[上で述べた方法に従って物理メモリ全体を対応付ける](#wu-li-memoriquan-ti-wodui-ying-fu-keru)ことができます。 +- `recursive_page_table` featureでは、ブートローダはレベル4ページテーブルのエントリを再帰的に対応付けます。これによりカーネルは[再帰的ページテーブル](#zai-gui-de-peziteburu)で述べた方法に従ってページテーブルにアクセスすることができます。 + +私達のカーネルには、シンプルでプラットフォーム非依存でより強力である(ページテーブルのフレームでないメモリにもアクセスできる)1つ目の方法を取ることにします。必要なブートローダの機能 (feature) を有効化するために、`map_physical_memory` featureを`bootloader`のdependencyに追加します。 -We choose the first approach for our kernel since it is simple, platform-independent, and more powerful (it also allows access to non-page-table-frames). To enable the required bootloader support, we add the `map_physical_memory` feature to our `bootloader` dependency: ```toml [dependencies] bootloader = { version = "0.9.8", features = ["map_physical_memory"]} ``` -With this feature enabled, the bootloader maps the complete physical memory to some unused virtual address range. To communicate the virtual address range to our kernel, the bootloader passes a _boot information_ structure. +この機能を有効化すると、ブートローダは物理メモリの全体を、とある未使用の仮想アドレス空間に対応付けます。この仮想アドレスの範囲をカーネルに伝えるために、ブートローダは**boot information**構造体を渡します。 + ### Boot Information -The `bootloader` crate defines a [`BootInfo`] struct that contains all the information it passes to our kernel. The struct is still in an early stage, so expect some breakage when updating to future [semver-incompatible] bootloader versions. With the `map_physical_memory` feature enabled, it currently has the two fields `memory_map` and `physical_memory_offset`: +`bootloader`クレートは、カーネルに渡されるすべての情報を格納する[`BootInfo`]構造体を定義しています。この構造体はまだ開発の初期段階にあり、将来の[対応していないsemverの][semver-incompatible]ブートローダのバージョンに更新した際には、うまく動かなくなることが予想されます。`map_physical_memory` featureが有効化されているので、いまこれは`memory_map`と`physical_memory_offset`という2つのフィールドを持っています: [`BootInfo`]: https://docs.rs/bootloader/0.9.3/bootloader/bootinfo/struct.BootInfo.html [semver-incompatible]: https://doc.rust-lang.org/stable/cargo/reference/specifying-dependencies.html#caret-requirements -- The `memory_map` field contains an overview of the available physical memory. This tells our kernel how much physical memory is available in the system and which memory regions are reserved for devices such as the VGA hardware. The memory map can be queried from the BIOS or UEFI firmware, but only very early in the boot process. For this reason, it must be provided by the bootloader because there is no way for the kernel to retrieve it later. We will need the memory map later in this post. -- The `physical_memory_offset` tells us the virtual start address of the physical memory mapping. By adding this offset to a physical address, we get the corresponding virtual address. This allows us to access arbitrary physical memory from our kernel. +- `memory_map`フィールドは、利用可能な物理メモリの情報の概要を保持しています。システムの利用可能な物理メモリがどのくらいかや、どのメモリ領域がVGAハードウェアのようなデバイスのために予約されているかをカーネルに伝えます。これらのメモリ対応付けはBIOSやUEFIファームウェアからブートのごく初期に限り取得することが可能です。そのため、これらをカーネルが後で取得することはできないので、ブートローダによって提供する必要があるわけです。このメモリ対応付けは後で必要となります。 +- `physical_memory_offset`は、物理メモリの対応付けの始まっている仮想アドレスです。このオフセットを物理アドレスに追加することによって、対応する仮想アドレスを得られます。これによって、カーネルから任意の物理アドレスにアクセスできます。 -The bootloader passes the `BootInfo` struct to our kernel in the form of a `&'static BootInfo` argument to our `_start` function. We don't have this argument declared in our function yet, so let's add it: +ブートローダは`BootInfo`構造体を`_start`関数の`&'static BootInfo`引数という形でカーネルに渡します。この引数は私達の関数ではまだ宣言していなかったので追加します: ```rust // in src/main.rs @@ -302,18 +308,18 @@ The bootloader passes the `BootInfo` struct to our kernel in the form of a `&'st use bootloader::BootInfo; #[no_mangle] -pub extern "C" fn _start(boot_info: &'static BootInfo) -> ! { // new argument +pub extern "C" fn _start(boot_info: &'static BootInfo) -> ! { // 新しい引数 […] } ``` -It wasn't a problem to leave off this argument before because the x86_64 calling convention passes the first argument in a CPU register. Thus, the argument is simply ignored when it isn't declared. However, it would be a problem if we accidentally used a wrong argument type, since the compiler doesn't know the correct type signature of our entry point function. +今までこの引数を無視していましたが、x86_64の呼出し規約は最初の引数をCPUレジスタに渡していたため、これは問題ではありませんでした。つまり、引数が宣言されていなかったとき、単に無視されていたわけです。しかし、もし引数の型を間違えてしまうと、コンパイラが私達のエントリポイント関数の正しい型シグネチャがわからなくなってしまうので、それは問題です。 -### The `entry_point` Macro +### `entry_point`マクロ -Since our `_start` function is called externally from the bootloader, no checking of our function signature occurs. This means that we could let it take arbitrary arguments without any compilation errors, but it would fail or cause undefined behavior at runtime. +私達の`_start`関数はブートローダから外部呼び出しされるので、私達の関数のシグネチャに対する検査は行われません。これにより、コンパイルエラーなしにあらゆる引数を取ることを許してしまい、いざ実行時にエラーになったり未定義動作を起こしてしまいます。 -To make sure that the entry point function has always the correct signature that the bootloader expects, the `bootloader` crate provides an [`entry_point`] macro that provides a type-checked way to define a Rust function as the entry point. Let's rewrite our entry point function to use this macro: +私達のエントリポイント関数が常にブートローダの期待する正しいシグネチャを持っていることを保証するために、`bootloader`クレートは[`entry_point`]マクロによって、Rustの関数をエントリポイントとして型チェックしながら定義する方法を提供します。私達のエントリポイントをこのマクロを使って書き直してみましょう: [`entry_point`]: https://docs.rs/bootloader/0.6.4/bootloader/macro.entry_point.html @@ -329,9 +335,9 @@ fn kernel_main(boot_info: &'static BootInfo) -> ! { } ``` -We no longer need to use `extern "C"` or `no_mangle` for our entry point, as the macro defines the real lower level `_start` entry point for us. The `kernel_main` function is now a completely normal Rust function, so we can choose an arbitrary name for it. The important thing is that it is type-checked so that a compilation error occurs when we use a wrong function signature, for example by adding an argument or changing the argument type. +このマクロが本物の`_start`エントリポイントをより低レベルに定義してくれるので、`extern "C"`や`no_mangle`をエントリポイントに使う必要はもうありません。`kernel_main`関数は今や完全に普通のRustの関数なので、自由に名前をつけることができます。そして重要なのは、これは型チェックされているので、間違った関数シグネチャ(例えば引数を増やしたり引数の型を変えたり)にするとコンパイルエラーが発生するということです。 -Let's perform the same change in our `lib.rs`: +`lib.rs`に同じ変更を施しましょう: ```rust // in src/lib.rs @@ -342,23 +348,23 @@ use bootloader::{entry_point, BootInfo}; #[cfg(test)] entry_point!(test_kernel_main); -/// Entry point for `cargo test` +/// `cargo test`のエントリポイント #[cfg(test)] fn test_kernel_main(_boot_info: &'static BootInfo) -> ! { - // like before + // 前と同じ init(); test_main(); hlt_loop(); } ``` -Since the entry point is only used in test mode, we add the `#[cfg(test)]` attribute to all items. We give our test entry point the distinct name `test_kernel_main` to avoid confusion with the `kernel_main` of our `main.rs`. We don't use the `BootInfo` parameter for now, so we prefix the parameter name with a `_` to silence the unused variable warning. +こちらのエントリポイントはテストモードのときにのみ使用するので、`#[cfg(test)]`属性をすべての要素に付しています。`main.rs`の`kernel_main`関数と混同しないよう、`test_kernel_main`という別の名前をつけました。いまのところ`BootInfo`引数は使わないので、引数名の先頭に`_`をつけることでunused variable (未使用変数) 警告が出てくるのを防いでいます。 -## Implementation +## 実装 -Now that we have access to physical memory, we can finally start to implement our page table code. First, we will take a look at the currently active page tables that our kernel runs on. In the second step, we will create a translation function that returns the physical address that a given virtual address is mapped to. As the last step, we will try to modify the page tables in order to create a new mapping. +物理メモリへのアクセスができるようになったので、いよいよページテーブルのコードを実装できます。そのためにまず、現在有効な、私達のカーネルが使用しているページテーブルを見てみます。次に、与えられた仮想アドレスが対応付けられている物理アドレスを返す変換関数を作ります。最後に新しい対応付けを作るためにページテーブルを修正してみます。 -Before we begin, we create a new `memory` module for our code: +始める前に、`memory`モジュールを作ります: ```rust // in src/lib.rs @@ -366,11 +372,11 @@ Before we begin, we create a new `memory` module for our code: pub mod memory; ``` -For the module we create an empty `src/memory.rs` file. +このモジュールに合わせて`src/memory.rs`ファイルを作ります。 -### Accessing the Page Tables +### ページテーブルにアクセスする -At the [end of the previous post], we tried to take a look at the page tables our kernel runs on, but failed since we couldn't access the physical frame that the `CR3` register points to. We're now able to continue from there by creating an `active_level_4_table` function that returns a reference to the active level 4 page table: +[前の記事の最後]で、私達のカーネルの実行しているページテーブルを見てみようとしましたが、`CR3`レジスタの指す物理フレームにアクセスすることができなかったためそれはできませんでした。前回の続きとして、`active_level_4_table`という、現在有効 (アクティブ) なレベル4ページテーブルへの参照を返す関数を定義するところから始めましょう: [end of the previous post]: @/edition-2/posts/08-paging-introduction/index.md#accessing-the-page-tables @@ -382,12 +388,14 @@ use x86_64::{ VirtAddr, }; -/// Returns a mutable reference to the active level 4 table. +/// 有効なレベル4テーブルへの可変参照を返す。 /// -/// 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). +/// この関数はunsafeである:全物理メモリが、渡された +/// `physical_memory_offset`(だけずらしたうえ)で +/// 仮想メモリへと対応付けられていることを呼び出し元が +/// 保証しなければならない。また、`&mut`参照が複数の +/// 名称を持つこと (mutable aliasingといい、動作が未定義) +/// につながるため、この関数は一度しか呼び出してはならない。 pub unsafe fn active_level_4_table(physical_memory_offset: VirtAddr) -> &'static mut PageTable { @@ -403,11 +411,11 @@ pub unsafe fn active_level_4_table(physical_memory_offset: VirtAddr) } ``` -First, we read the physical frame of the active level 4 table from the `CR3` register. We then take its physical start address, convert it to an `u64`, and add it to `physical_memory_offset` to get the virtual address where the page table frame is mapped. Finally, we convert the virtual address to a `*mut PageTable` raw pointer through the `as_mut_ptr` method and then unsafely create a `&mut PageTable` reference from it. We create a `&mut` reference instead of a `&` reference because we will mutate the page tables later in this post. +まず、有効なレベル4テーブルの物理フレームを`CR3`レジスタから読みます。その開始物理アドレスを取り出し、`u64`に変換し、`physical_memory_offset`に足すことでそのページテーブルフレームに対応する仮想アドレスを得ます。最後に、`as_mut_ptr`メソッドを使ってこの仮想アドレスを`*mut PageTable`生ポインタに変換し、これから`&mut PageTable`参照を作ります(ここがunsafe)。`&`参照ではなく`&mut`参照にしているのは、後でこのページテーブルを変更するためです。 -We don't need to use an unsafe block here because Rust treats the complete body of an `unsafe fn` like a large `unsafe` block. This makes our code more dangerous since we could accidentally introduce an unsafe operation in previous lines without noticing. It also makes it much more difficult to spot the unsafe operations. There is an [RFC](https://github.com/rust-lang/rfcs/pull/2585) to change this behavior. +Rustは`unsafe fn`の中身全体を大きな`unsafe`ブロックであるかのように扱うので、ここでunsafeブロックを使う必要はありません。これでは、(unsafeを意図した)最後の行より前の行に間違ってunsafeな操作を書いても気づけないので、コードがより危険になります。また、どこがunsafeな操作であるのかを探すのも非常に難しくなります。そのため、この挙動を変更する[RFC](https://github.com/rust-lang/rfcs/pull/2585)が提案されています。 -We can now use this function to print the entries of the level 4 table: +この関数を使って、レベル4テーブルのエントリを出力してみましょう: ```rust // in src/main.rs @@ -437,34 +445,34 @@ fn kernel_main(boot_info: &'static BootInfo) -> ! { } ``` -First, we convert the `physical_memory_offset` of the `BootInfo` struct to a [`VirtAddr`] and pass it to the `active_level_4_table` function. We then use the `iter` function to iterate over the page table entries and the [`enumerate`] combinator to additionally add an index `i` to each element. We only print non-empty entries because all 512 entries wouldn't fit on the screen. +まず、`BootInfo`構造体の`physical_memory_offset`を[`VirtAddr`]に変換し、`active_level_4_table`関数に渡します。つぎに`iter`関数を使ってページテーブルの全エントリに対してfor文を回し、[`enumerate`]コンビネータをつかってそれぞれの要素にインデックス`i`を追加します。全512エントリを出力すると画面に収まらないので、 (から) でないエントリのみ出力します。 [`VirtAddr`]: https://docs.rs/x86_64/0.14.2/x86_64/addr/struct.VirtAddr.html [`enumerate`]: https://doc.rust-lang.org/core/iter/trait.Iterator.html#method.enumerate -When we run it, we see the following output: +実行すると、以下の出力を得ます: ![QEMU printing entry 0 (0x2000, PRESENT, WRITABLE, ACCESSED), entry 1 (0x894000, PRESENT, WRITABLE, ACCESSED, DIRTY), entry 31 (0x88e000, PRESENT, WRITABLE, ACCESSED, DIRTY), entry 175 (0x891000, PRESENT, WRITABLE, ACCESSED, DIRTY), and entry 504 (0x897000, PRESENT, WRITABLE, ACCESSED, DIRTY)](qemu-print-level-4-table.png) -We see that there are various non-empty entries, which all map to different level 3 tables. There are so many regions because kernel code, kernel stack, the physical memory mapping, and the boot information all use separate memory areas. +いくつかの空でないエントリがあり、いずれも異なるレベル3テーブルに対応づけられていることがわかります。このようにたくさんの領域があるのは、カーネルコード、カーネルスタック、物理メモリ対応、ブート情報が互いに離れたメモリ領域を使っているためです。 -To traverse the page tables further and take a look at a level 3 table, we can take the mapped frame of an entry convert it to a virtual address again: +ページテーブルを更に辿りレベル3テーブルを見るには、エントリに対応するフレームを取り出し再び仮想アドレスに変換すればよいです: ```rust -// in the `for` loop in src/main.rs +// src/main.rsのforループ内にて…… use x86_64::structures::paging::PageTable; 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 + // レベル3テーブルの空でないエントリを出力する for (i, entry) in l3_table.iter().enumerate() { if !entry.is_unused() { println!(" L3 Entry {}: {:?}", i, entry); @@ -473,25 +481,25 @@ if !entry.is_unused() { } ``` -For looking at the level 2 and level 1 tables, we repeat that process for the level 3 and level 2 entries. As you can imagine, this gets very verbose quickly, so we don't show the full code here. +レベル2やレベル1のテーブルも、同じ手続きをレベル3とレベル2のエントリに対して繰り返すことで見ることができます。お察しの通りそれを書くとかなり長くなるので、コードの全てはここには示しません。 -Traversing the page tables manually is interesting because it helps to understand how the CPU performs the translation. However, most of the time we are only interested in the mapped physical address for a given virtual address, so let's create a function for that. +ページテーブルを手作業で辿ると、CPUが変換を行う様子を理解できて面白いです。しかし、多くの場合は与えられた仮想アドレスに対応する物理アドレスにのみ興味があるので、そのための関数を作りましょう。 -### Translating Addresses +### アドレスの変換 -For translating a virtual to a physical address, we have to traverse the four-level page table until we reach the mapped frame. Let's create a function that performs this translation: +仮想アドレスを物理アドレスに変換するには、4層のページテーブルを辿って対応するフレームにたどり着けばよいです。この変換を行う関数を作りましょう: ```rust // in src/memory.rs use x86_64::PhysAddr; -/// Translates the given virtual address to the mapped physical address, or -/// `None` if the address is not mapped. +/// 与えられた仮想アドレスを対応する物理アドレスに変換し、 +/// そのアドレスが対応付けられていないなら`None`を返す。 /// -/// 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`. +/// この関数はunsafeである。なぜなら、呼び出し元は全物理メモリが与えられた +/// `physical_memory_offset`(だけずらした上)で対応付けられていることを +/// 保証しなくてはならないからである。 pub unsafe fn translate_addr(addr: VirtAddr, physical_memory_offset: VirtAddr) -> Option { @@ -499,25 +507,26 @@ pub unsafe fn translate_addr(addr: VirtAddr, physical_memory_offset: VirtAddr) } ``` -We forward the function to a safe `translate_addr_inner` function to limit the scope of `unsafe`. As we noted above, Rust treats the complete body of an unsafe fn like a large unsafe block. By calling into a private safe function, we make each `unsafe` operation explicit again. +`unsafe`の範囲を制限するために、この関数は、すぐにunsafeでない`translate_addr_inner`関数に制御を渡しています。先に述べたように、Rustはunsafeな関数の全体をunsafeブロックとして扱ってしまいます。呼び出した非公開の (プライベートな) unsafeでない関数の中にコードを書くことで、それぞれのunsafeな操作を明確にします。 -The private inner function contains the real implementation: +非公開な内部の関数に本当の実装を書いていきます: ```rust // in src/memory.rs -/// Private function that is called by `translate_addr`. +/// `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. +/// Rustはunsafeな関数の全体をunsafeブロックとして扱ってしまうので、 +/// unsafeの範囲を絞るためにこの関数はunsafeにしていない。 +/// この関数をモジュール外から呼び出すときは、 +/// unsafeな関数を通じてのみ呼び出すこと。 fn translate_addr_inner(addr: VirtAddr, physical_memory_offset: VirtAddr) -> Option { use x86_64::structures::paging::page_table::FrameError; use x86_64::registers::control::Cr3; - // read the active level 4 frame from the CR3 register + // 有効なレベル4フレームをCR3レジスタから読む let (level_4_table_frame, _) = Cr3::read(); let table_indexes = [ @@ -525,56 +534,57 @@ fn translate_addr_inner(addr: VirtAddr, physical_memory_offset: VirtAddr) ]; 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 = physical_memory_offset + frame.start_address().as_u64(); let table_ptr: *const PageTable = virt.as_ptr(); let table = unsafe {&*table_ptr}; - // read the page table entry and update `frame` + // ページテーブルエントリを読んで、`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"), + //huge pageはサポートしていません }; } - // calculate the physical address by adding the page offset + // ページオフセットを足すことで、目的の物理アドレスを計算する Some(frame.start_address() + u64::from(addr.page_offset())) } ``` -Instead of reusing our `active_level_4_table` function, we read the level 4 frame from the `CR3` register again. We do this because it simplifies this prototype implementation. Don't worry, we will create a better solution in a moment. +先程作った`active_level_4_table`関数を再利用せず、`CR3`レジスタからレベル4フレームを読み出すコードを再び書いています。これは簡単に試作するためであり、後でもっと良い方法で作り直すのでご心配なく。 -The `VirtAddr` struct already provides methods to compute the indexes into the page tables of the four levels. We store these indexes in a small array because it allows us to traverse the page tables using a `for` loop. Outside of the loop, we remember the last visited `frame` to calculate the physical address later. The `frame` points to page table frames while iterating, and to the mapped frame after the last iteration, i.e. after following the level 1 entry. +`Virtaddr`構造体には、インデクスから4つの階層のページテーブルを計算してくれるメソッドが備わっています。この4つのインデクスを配列に格納することで、これらを`for`ループを使って辿ります。`for`ループを抜けたら、最後に計算した`frame`を覚えているので、物理アドレスを計算できます。この`frame`は、forループの中ではページテーブルのフレームを指していて、最後のループのあと(すなわちレベル1エントリを辿ったあと)では対応する(物理)フレームを指しています。 -Inside the loop, we again use the `physical_memory_offset` to convert the frame into a page table reference. We then read the entry of the current page table and use the [`PageTableEntry::frame`] function to retrieve the mapped frame. If the entry is not mapped to a frame we return `None`. If the entry maps a huge 2MiB or 1GiB page we panic for now. +ループの中の話をします。前と同じように`physical_memory_offset`を使ってフレームをページテーブルの参照に変換します。次に、そのページテーブルのエントリを読み、[`PageTableEntry::frame`]関数を使って対応するフレームを取得します。もしエントリがフレームに対応付けられていなければ`None`を返します。もしエントリが2MiBや1GiBのhuge pageに対応付けられていたら、今のところはpanicすることにします。 [`PageTableEntry::frame`]: https://docs.rs/x86_64/0.14.2/x86_64/structures/paging/page_table/struct.PageTableEntry.html#method.frame -Let's test our translation function by translating some addresses: +いくつかのアドレスを変換して、この変換関数がうまく行くかテストしてみましょう: ```rust // in src/main.rs fn kernel_main(boot_info: &'static BootInfo) -> ! { - // new import + // 新しいインポート use blog_os::memory::translate_addr; - […] // hello world and blog_os::init + […] // hello world と blog_os::init let phys_mem_offset = VirtAddr::new(boot_info.physical_memory_offset); let addresses = [ - // the identity-mapped vga buffer page + // 恒等対応しているVGAバッファのページ 0xb8000, - // some code page + // コードページのどこか 0x201008, - // some stack page + // スタックページのどこか 0x0100_0020_1a10, - // virtual address mapped to physical address 0 + // 物理アドレス "0" に対応付けられている仮想アドレス boot_info.physical_memory_offset, ]; @@ -584,28 +594,28 @@ fn kernel_main(boot_info: &'static BootInfo) -> ! { println!("{:?} -> {:?}", virt, phys); } - […] // test_main(), "it did not crash" printing, and hlt_loop() + […] // test_main(), "it did not crash" の出力, および hlt_loop() } ``` -When we run it, we see the following output: +実行すると、以下の出力を得ます: ![0xb8000 -> 0xb8000, 0x201008 -> 0x401008, 0x10000201a10 -> 0x279a10, "panicked at 'huge pages not supported'](qemu-translate-addr.png) -As expected, the identity-mapped address `0xb8000` translates to the same physical address. The code page and the stack page translate to some arbitrary physical addresses, which depend on how the bootloader created the initial mapping for our kernel. It's worth noting that the last 12 bits always stay the same after translation, which makes sense because these bits are the [_page offset_] and not part of the translation. +期待したとおり、恒等対応しているアドレス`0xb8000`は同じ物理アドレスに変換されました。コードページとスタックページは物理アドレスのどこかしかに変換されていますが、その場所はブートローダが私達のカーネルのために最初に作った対応づけに依存します。また、下から12ビットは変換のあとも常に同じであるということも注目に値します:この部分は[ページオフセット][_page offset_]であり、変換には関わらないためです。 [_page offset_]: @/edition-2/posts/08-paging-introduction/index.md#paging-on-x86-64 -Since each physical address can be accessed by adding the `physical_memory_offset`, the translation of the `physical_memory_offset` address itself should point to physical address `0`. However, the translation fails because the mapping uses huge pages for efficiency, which is not supported in our implementation yet. +それぞれの物理アドレスは`physical_memory_offset`を足すことでアクセスできるわけですから、`physical_memory_offset`自体を(仮想アドレスとして)変換すると物理アドレス`0`を指すはずです。しかし、効率よく対応付けを行うためにここではhuge pageが使われており、これはまだサポートしていないので変換には失敗しています。 -### Using `OffsetPageTable` +### `OffsetPageTable`を使う -Translating virtual to physical addresses is a common task in an OS kernel, therefore the `x86_64` crate provides an abstraction for it. The implementation already supports huge pages and several other page table functions apart from `translate_addr`, so we will use it in the following instead of adding huge page support to our own implementation. +仮想アドレスから物理アドレスへの変換はOSのカーネルがよく行うことですから、`x86_64`クレートはそのための抽象化を提供しています。この実装はすでにhuge pageや`translate_addr`以外の様々な関数もサポートしているので、以下ではhuge pageのサポートを自前で実装する代わりにこれを使うことにします。 -The base of the abstraction are two traits that define various page table mapping functions: +この抽象化の基礎となっているのは、様々なページテーブル対応付け関数を定義している2つのトレイトです。 -- The [`Mapper`] trait is generic over the page size and provides functions that operate on pages. Examples are [`translate_page`], which translates a given page to a frame of the same size, and [`map_to`], which creates a new mapping in the page table. -- The [`Translate`] trait provides functions that work with multiple page sizes such as [`translate_addr`] or the general [`translate`]. +- [`Mapper`]トレイトはページサイズを型引数とする汎用型 (ジェネリクス) です。例えば、[`translate_page`]は与えられたページを同じサイズのフレームに変換し、[`map_to`]はページテーブルに新しい対応付けを作成します。 +- [`Translate`] トレイトは[`translate_addr`]や一般の[`translate`]のような、さまざまなページサイズに対して動くような関数を提供します。 [`Mapper`]: https://docs.rs/x86_64/0.14.2/x86_64/structures/paging/mapper/trait.Mapper.html [`translate_page`]: https://docs.rs/x86_64/0.14.2/x86_64/structures/paging/mapper/trait.Mapper.html#tymethod.translate_page @@ -614,95 +624,97 @@ The base of the abstraction are two traits that define various page table mappin [`translate_addr`]: https://docs.rs/x86_64/0.14.2/x86_64/structures/paging/mapper/trait.Translate.html#method.translate_addr [`translate`]: https://docs.rs/x86_64/0.14.2/x86_64/structures/paging/mapper/trait.Translate.html#tymethod.translate -The traits only define the interface, they don't provide any implementation. The `x86_64` crate currently provides three types that implement the traits with different requirements. The [`OffsetPageTable`] type assumes that the complete physical memory is mapped to the virtual address space at some offset. The [`MappedPageTable`] is a bit more flexible: It only requires that each page table frame is mapped to the virtual address space at a calculable address. Finally, the [`RecursivePageTable`] type can be used to access page table frames through [recursive page tables](#recursive-page-tables). +これらのトレイトはインターフェイスを定義しているだけであり、その実装は何一つ提供していません。`x86_64`クレートは現在、このトレイトを実装する型を異なる要件に合わせて3つ用意しています。[`OffsetPageTable`]型は、全物理メモリがあるオフセットで仮想アドレスに対応していることを前提とします。[`MappedPageTable`]はもう少し融通が効き、それぞれのページテーブルフレームが計算可能などこかの仮想アドレスに対応していることだけを前提とします。最後に[`RecursivePageTable`]型は、ページテーブルのフレームに[再帰的ページテーブル](#zai-gui-de-peziteburu)を使ってアクセスするときに使えます。 [`OffsetPageTable`]: https://docs.rs/x86_64/0.14.2/x86_64/structures/paging/mapper/struct.OffsetPageTable.html [`MappedPageTable`]: https://docs.rs/x86_64/0.14.2/x86_64/structures/paging/mapper/struct.MappedPageTable.html [`RecursivePageTable`]: https://docs.rs/x86_64/0.14.2/x86_64/structures/paging/mapper/struct.RecursivePageTable.html -In our case, the bootloader maps the complete physical memory at a virtual address specified by the `physical_memory_offset` variable, so we can use the `OffsetPageTable` type. To initialize it, we create a new `init` function in our `memory` module: +私達の場合、ブートローダは全物理メモリを`physical_memory_offset`変数で指定された仮想アドレスで物理メモリに対応付けているので、`OffsetPageTable`型を使えます。これを初期化するために、`memory`モジュールに新しく`init`関数を作りましょう: ```rust use x86_64::structures::paging::OffsetPageTable; -/// Initialize a new OffsetPageTable. +/// 新しいOffsetPageTableを初期化する。 /// -/// 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). +/// この関数はunsafeである:全物理メモリが、渡された +/// `physical_memory_offset`(だけずらしたうえ)で +/// 仮想メモリへと対応付けられていることを呼び出し元が +/// 保証しなければならない。また、`&mut`参照が複数の +/// 名称を持つこと (mutable aliasingといい、動作が未定義) +/// につながるため、この関数は一度しか呼び出してはならない。 pub unsafe fn init(physical_memory_offset: VirtAddr) -> OffsetPageTable<'static> { let level_4_table = active_level_4_table(physical_memory_offset); OffsetPageTable::new(level_4_table, physical_memory_offset) } -// make private +// これは非公開にする unsafe fn active_level_4_table(physical_memory_offset: VirtAddr) -> &'static mut PageTable {…} ``` -The function takes the `physical_memory_offset` as an argument and returns a new `OffsetPageTable` instance with a `'static` lifetime. This means that the instance stays valid for the complete runtime of our kernel. In the function body, we first call the `active_level_4_table` function to retrieve a mutable reference to the level 4 page table. We then invoke the [`OffsetPageTable::new`] function with this reference. As the second parameter, the `new` function expects the virtual address at which the mapping of the physical memory starts, which is given in the `physical_memory_offset` variable. +この関数は`physical_memory_offset`を引数としてとり、`'static`ライフタイムを持つ`OffsetPageTable`を作って返します。このライフタイムは、私達のカーネルが実行している間この実体 (インスタンス) はずっと有効であるという意味です。関数の中ではまず`active_level_4_table`関数を呼び出し、レベル4ページテーブルへの可変参照を取得します。次に[`OffsetPageTable::new`]関数をこの参照を使って呼び出します。この`new`関数の第二引数には、物理メモリの対応付けの始まる仮想アドレスが入ることになっています。つまり`physical_memory_offset`です。 [`OffsetPageTable::new`]: https://docs.rs/x86_64/0.14.2/x86_64/structures/paging/mapper/struct.OffsetPageTable.html#method.new -The `active_level_4_table` function should be only called from the `init` function from now on because it can easily lead to aliased mutable references when called multiple times, which can cause undefined behavior. For this reason, we make the function private by removing the `pub` specifier. +可変参照が複数の名称を持つと未定義動作を起こす可能性があるので、以降`active_level_4_table`関数は`init`関数から一度呼び出されることを除いては呼び出されてはなりません。そのため、`pub`指定子を外してこの関数を非公開にしています。 -We now can use the `Translate::translate_addr` method instead of our own `memory::translate_addr` function. We only need to change a few lines in our `kernel_main`: +これで、自前の`memory::translate_addr`関数の代わりに`Translate::translate_addr`メソッドを使うことができます。これには`kernel_main`を数行だけ書き換えればよいです: ```rust // in src/main.rs fn kernel_main(boot_info: &'static BootInfo) -> ! { - // new: different imports + // インポートが追加・変更されている use blog_os::memory; use x86_64::{structures::paging::Translate, VirtAddr}; - […] // hello world and blog_os::init + […] // hello worldとblog_os::init let phys_mem_offset = VirtAddr::new(boot_info.physical_memory_offset); - // new: initialize a mapper + // 追加:mapperを初期化 let mapper = unsafe { memory::init(phys_mem_offset) }; - let addresses = […]; // same as before + let addresses = […]; // 前と同じ for &address in &addresses { let virt = VirtAddr::new(address); - // new: use the `mapper.translate_addr` method + // 追加:`mapper.translate_addr`メソッドを使う let phys = mapper.translate_addr(virt); println!("{:?} -> {:?}", virt, phys); } - […] // test_main(), "it did not crash" printing, and hlt_loop() + […] // test_main(), "it did not crash" の出力, および hlt_loop() } ``` -We need to import the `Translate` trait in order to use the [`translate_addr`] method it provides. +[`translate_addr`]メソッドを使うために、それを提供している`Translate`トレイトをインポートする必要があります。 -When we run it now, we see the same translation results as before, with the difference that the huge page translation now also works: +これを実行すると、同じ変換結果が得られますが、今度はhuge pageの変換もうまく行っています: ![0xb8000 -> 0xb8000, 0x201008 -> 0x401008, 0x10000201a10 -> 0x279a10, 0x18000000000 -> 0x0](qemu-mapper-translate-addr.png) -As expected, the translations of `0xb8000` and the code and stack addresses stay the same as with our own translation function. Additionally, we now see that the virtual address `physical_memory_offset` is mapped to the physical address `0x0`. +想定通り、`0xb8000`やコード・スタックアドレスの変換結果は自前の変換関数と同じになっています。また、`physical_memory_offset`は物理アドレス`0x0`に対応付けられているのもわかります。 -By using the translation function of the `MappedPageTable` type we can spare ourselves the work of implementing huge page support. We also have access to other page functions such as `map_to`, which we will use in the next section. +`MappedPageTable`型の変換関数を使うことで、huge pageをサポートする手間が省けます。また`map_to`のような他のページング関数も利用でき、これは次のセクションで使います。 -At this point we no longer need our `memory::translate_addr` and `memory::translate_addr_inner` functions, so we can delete them. +この時点で、自作した`memory::translate_addr`関数や`memory::translate_addr_inner`関数はもう必要ではないので、削除して構いません。 -### Creating a new Mapping +### 新しい対応を作る -Until now we only looked at the page tables without modifying anything. Let's change that by creating a new mapping for a previously unmapped page. +これまでページテーブルを見てきましたが修正はしていませんでした。対応のなかったページに対応を作ることで、ページテーブルを修正してみましょう。 -We will use the [`map_to`] function of the [`Mapper`] trait for our implementation, so let's take a look at that function first. The documentation tells us that it takes four arguments: the page that we want to map, the frame that the page should be mapped to, a set of flags for the page table entry, and a `frame_allocator`. The frame allocator is needed because mapping the given page might require creating additional page tables, which need unused frames as backing storage. +これを実装するには[`Mapper`]トレイトの[`map_to`]関数を使うので、この関数について少し見てみましょう。ドキュメントによると四つ引数があります:対応に使うページ、ページを対応させるフレーム、ページテーブルエントリにつかうフラグの集合、そして`frame_allocator`です。フレームアロケータ (frame allocator) (フレームを割り当てる (アロケートする) 機能を持つ)が必要な理由は、与えられたページを対応付けるために追加でページテーブルを作成する必要があるかもしれず、これを格納するためには使われていないフレームが必要となるからです。 [`map_to`]: https://docs.rs/x86_64/0.14.2/x86_64/structures/paging/trait.Mapper.html#tymethod.map_to [`Mapper`]: https://docs.rs/x86_64/0.14.2/x86_64/structures/paging/trait.Mapper.html -#### A `create_example_mapping` Function +#### `create_example_mapping`関数 -The first step of our implementation is to create a new `create_example_mapping` function that maps a given virtual page to `0xb8000`, the physical frame of the VGA text buffer. We choose that frame because it allows us to easily test if the mapping was created correctly: We just need to write to the newly mapped page and see whether we see the write appear on the screen. +私達が実装していく最初のステップとして、`create_example_mapping`関数という、与えられた仮想ページを`0xb8000`すなわちVGAテキストバッファの物理フレームに対応付ける関数を作ってみましょう。このフレームを選んだ理由は、対応付けが正しくなされたかをテストするのが容易だからです:対応付けたページに書き込んで、それが画面に現れるか確認するだけでよいのですから。 -The `create_example_mapping` function looks like this: +`create_example_mapping`は以下のようになります: ```rust // in src/memory.rs @@ -712,7 +724,7 @@ use x86_64::{ structures::paging::{Page, PhysFrame, Mapper, Size4KiB, FrameAllocator} }; -/// Creates an example mapping for the given page to frame `0xb8000`. +/// 与えられたページをフレーム`0xb8000`に試しに対応付ける。 pub fn create_example_mapping( page: Page, mapper: &mut OffsetPageTable, @@ -724,27 +736,27 @@ pub fn create_example_mapping( let flags = Flags::PRESENT | Flags::WRITABLE; let map_to_result = unsafe { - // FIXME: this is not safe, we do it only for testing + // FIXME: unsafeであり、テストのためにのみ行う mapper.map_to(page, frame, flags, frame_allocator) }; map_to_result.expect("map_to failed").flush(); } ``` -In addition to the `page` that should be mapped, the function expects a mutable reference to an `OffsetPageTable` instance and a `frame_allocator`. The `frame_allocator` parameter uses the [`impl Trait`][impl-trait-arg] syntax to be [generic] over all types that implement the [`FrameAllocator`] trait. The trait is generic over the [`PageSize`] trait to work with both standard 4KiB pages and huge 2MiB/1GiB pages. We only want to create a 4KiB mapping, so we set the generic parameter to `Size4KiB`. +この関数は、対応付ける`page`に加え`OffsetPageTable`のインスタンスと`frame_allocator`への可変参照を引数に取ります。`frame_allocator`引数は[`impl Trait`][impl-trait-arg]構文により[`FrameAllocator`]トレイトを実装するあらゆる型の[汎用型][generic]になっています。`FrameAllocator`トレイトは[`PageSize`]トレイトを実装するなら(引数のサイズが)4KiBでも2MiBや1GiBのhuge pageでも大丈夫な汎用 (ジェネリック) トレイトです。私達は4KiBの対応付けのみを作りたいので、ジェネリック引数は`Size4KiB`にしています。 [impl-trait-arg]: https://doc.rust-lang.org/book/ch10-02-traits.html#traits-as-parameters [generic]: https://doc.rust-lang.org/book/ch10-00-generics.html [`FrameAllocator`]: https://docs.rs/x86_64/0.14.2/x86_64/structures/paging/trait.FrameAllocator.html [`PageSize`]: https://docs.rs/x86_64/0.14.2/x86_64/structures/paging/page/trait.PageSize.html -The [`map_to`] method is unsafe because the caller must ensure that the frame is not already in use. The reason for this is that mapping the same frame twice could result in undefined behavior, for example when two different `&mut` references point to the same physical memory location. In our case, we reuse the VGA text buffer frame, which is already mapped, so we break the required condition. However, the `create_example_mapping` function is only a temporary testing function and will be removed after this post, so it is ok. To remind us of the unsafety, we put a `FIXME` comment on the line. +[`map_to`]メソッドは、呼び出し元がフレームはまだ使われていないことを保証しないといけないので、unsafeです。なぜなら、同じフレームを二度対応付けると(例えば2つの異なる`&mut`参照が物理メモリの同じ場所を指すことで)未定義動作を起こす可能性があるからです。今回、VGAテキストバッファのフレームという、すでに対応付けられているフレームを再度使っているので、この要件を破ってしまっています。しかしながら、`create_example_mapping`関数は一時的なテスト関数でありこの記事のあとには取り除かれるので、大丈夫です。この危険性 (unsafety) のことを忘れないようにするために、その行に`FIXME` (要修正) コメントをつけておきます。 -In addition to the `page` and the `unused_frame`, the `map_to` method takes a set of flags for the mapping and a reference to the `frame_allocator`, which will be explained in a moment. For the flags, we set the `PRESENT` flag because it is required for all valid entries and the `WRITABLE` flag to make the mapped page writable. For a list of all possible flags, see the [_Page Table Format_] section of the previous post. +`map_to`関数が`page`と`unused_frame`に加えてフラグの集合と`frame_allocator`への参照を取りますが、これについてはすぐに説明します。フラグについては、`PRESENT`フラグという有効なエントリ全てに必須のフラグと、`WRITABLE`フラグという対応するページを書き込み可能にするフラグをセットしています。フラグの一覧については、前記事の[ページテーブルの形式][_Page Table Format_]を参照してください。 [_Page Table Format_]: @/edition-2/posts/08-paging-introduction/index.md#page-table-format -The [`map_to`] function can fail, so it returns a [`Result`]. Since this is just some example code that does not need to be robust, we just use [`expect`] to panic when an error occurs. On success, the function returns a [`MapperFlush`] type that provides an easy way to flush the newly mapped page from the translation lookaside buffer (TLB) with its [`flush`] method. Like `Result`, the type uses the [`#[must_use]`][must_use] attribute to emit a warning when we accidentally forget to use it. +[`map_to`]関数は失敗しうるので、[`Result`]を返します。これは失敗しても構わない単なるテストコードなので、エラーが起きたときは[`expect`]を使ってパニックしてしまうことにします。この関数は成功したとき[`MapperFlush`]型を返します。この型の[`flush`]メソッドを使うと、新しく対応させたページをトランスレーション・ルックアサイド・バッファ (TLB) から簡単にflushすることができます。この型は`Result`と同じく[`#[must_use]`][must_use]属性を使っており、使用し忘れると警告を出します。 [`Result`]: https://doc.rust-lang.org/core/result/enum.Result.html [`expect`]: https://doc.rust-lang.org/core/result/enum.Result.html#method.expect @@ -752,16 +764,16 @@ The [`map_to`] function can fail, so it returns a [`Result`]. Since this is just [`flush`]: https://docs.rs/x86_64/0.14.2/x86_64/structures/paging/mapper/struct.MapperFlush.html#method.flush [must_use]: https://doc.rust-lang.org/std/result/#results-must-be-used -#### A dummy `FrameAllocator` +#### ダミーの`FrameAllocator` -To be able to call `create_example_mapping` we need to create a type that implements the `FrameAllocator` trait first. As noted above, the trait is responsible for allocating frames for new page table if they are needed by `map_to`. +`create_example_mapping`関数を呼べるようにするためには、まず`FrameAllocator`トレイトを実装する型を作成する必要があります。上で述べたように、このトレイトは新しいページのためのフレームを`map_to`が必要としたときに割り当てる役割を持っています。 -Let's start with the simple case and assume that we don't need to create new page tables. For this case, a frame allocator that always returns `None` suffices. We create such an `EmptyFrameAllocator` for testing our mapping function: +単純なケースを考えましょう:新しいページテーブルを作る必要がないと仮定してしまいます。この場合、常に`None`を返すフレームアロケータで十分です。私達の対応付け関数をテストするために、そのような`EmptyFrameAllocator`を作ります。 ```rust // in src/memory.rs -/// A FrameAllocator that always returns `None`. +/// つねに`None`を返すFrameAllocator pub struct EmptyFrameAllocator; unsafe impl FrameAllocator for EmptyFrameAllocator { @@ -771,69 +783,70 @@ unsafe impl FrameAllocator for EmptyFrameAllocator { } ``` -Implementing the `FrameAllocator` is unsafe because the implementer must guarantee that the allocator yields only unused frames. Otherwise undefined behavior might occur, for example when two virtual pages are mapped to the same physical frame. Our `EmptyFrameAllocator` only returns `None`, so this isn't a problem in this case. +`FrameAllocator`を実装するのはunsafeです。なぜなら、実装する人は、作ったアロケータが未使用のフレームのみ取得することを保証しなければならないからです。さもなくば、例えば二つの仮想ページが同じ物理フレームに対応付けられたときに未定義動作が起こるかもしれません。この`EmptyFrameAllocator`は`None`しか返さないので、これは問題ではありません。 -#### Choosing a Virtual Page +#### 仮想ページを選ぶ -We now have a simple frame allocator that we can pass to our `create_example_mapping` function. However, the allocator always returns `None`, so this will only work if no additional page table frames are needed for creating the mapping. To understand when additional page table frames are needed and when not, let's consider an example: +`create_example_mapping`関数に渡すための単純なフレームアロケータを手に入れました。しかし、このアロケータは常に`None`を返すので、対応を作る際に追加のページテーブルフレームが必要でなかったときにのみうまく行きます。いつ追加のページテーブルフレームが必要でありいつそうでないのかを知るために、例をとって考えてみましょう: ![A virtual and a physical address space with a single mapped page and the page tables of all four levels](required-page-frames-example.svg) -The graphic shows the virtual address space on the left, the physical address space on the right, and the page tables in between. The page tables are stored in physical memory frames, indicated by the dashed lines. The virtual address space contains a single mapped page at address `0x803fe00000`, marked in blue. To translate this page to its frame, the CPU walks the 4-level page table until it reaches the frame at address 36 KiB. +この図の左は仮想アドレス空間を、右は物理アドレス空間を、真ん中はページテーブルを示します。このページテーブルが格納されている物理フレームが破線で示されています。仮想アドレス空間は一つの対応付けられたページをアドレス`0x803fe00000`に持っており、これは青色で示されています。このページをフレームに変換するために、CPUは4層のページテーブルを辿り、アドレス36KiBのフレームに到達します。 -Additionally, the graphic shows the physical frame of the VGA text buffer in red. Our goal is to map a previously unmapped virtual page to this frame using our `create_example_mapping` function. Since our `EmptyFrameAllocator` always returns `None`, we want to create the mapping so that no additional frames are needed from the allocator. This depends on the virtual page that we select for the mapping. +また、この図はVGAテキストバッファの物理フレームを赤色で示しています。私達の目的は、`create_example_mapping`関数を使ってまだ対応付けられていない仮想ページをこのフレームに対応付けることです。私達の`EmptyFrameAllocator`は常に`None`を返すので、アロケータからフレームを追加する必要がないように対応付けを作りたいです。これができるかは、私達が対応付けにどの仮想ページを使うかに依存します。 -The graphic shows two candidate pages in the virtual address space, both marked in yellow. One page is at address `0x803fdfd000`, which is 3 pages before the mapped page (in blue). While the level 4 and level 3 page table indices are the same as for the blue page, the level 2 and level 1 indices are different (see the [previous post][page-table-indices]). The different index into the level 2 table means that a different level 1 table is used for this page. Since this level 1 table does not exist yet, we would need to create it if we chose that page for our example mapping, which would require an additional unused physical frame. In contrast, the second candidate page at address `0x803fe02000` does not have this problem because it uses the same level 1 page table than the blue page. Thus, all required page tables already exist. + +この図の仮想アドレス空間には、2つの候補となるページを黄色で示しています。ページのうち一つはアドレス`0x803fe00000`で、これは(青で示された)対応付けられたページの3つ前です。レベル4と3のテーブルのインデクスは青いページと同じですが、レベル2と1のインデクスは違います([前の記事][page-table-indices]を参照)。レベル2テーブルのインデクスが違うということは、異なるレベル1テーブルが使われることを意味します。そんなレベル1テーブルは存在しないので、もしこちらを使っていたら、使われていない物理フレームを追加(でアロケート)する必要が出てきます。対して、2つ目のアドレス`0x803fe02000`にある候補のページは、青のページと同じレベル1ページテーブルを使うのでこの問題は発生しません。よって、必要となるすべてのページテーブルはすでに存在しています。 [page-table-indices]: @/edition-2/posts/08-paging-introduction/index.md#paging-on-x86-64 -In summary, the difficulty of creating a new mapping depends on the virtual page that we want to map. In the easiest case, the level 1 page table for the page already exists and we just need to write a single entry. In the most difficult case, the page is in a memory region for that no level 3 exists yet so that we need to create new level 3, level 2 and level 1 page tables first. +まとめると、新しい対応を作るときの難易度は、対応付けようとしている仮想ページに依存するということです。作ろうとしているページのレベル1ページテーブルがすでに存在すると最も簡単で、エントリを一つ書き込むだけです。ページがレベル3のテーブルすら存在しない領域にある場合が最も難しく、その場合まずレベル3,2,1のページテーブルを新しく作る必要があります。 -For calling our `create_example_mapping` function with the `EmptyFrameAllocator`, we need to choose a page for that all page tables already exist. To find such a page, we can utilize the fact that the bootloader loads itself in the first megabyte of the virtual address space. This means that a valid level 1 table exists for all pages this region. Thus, we can choose any unused page in this memory region for our example mapping, such as the page at address `0`. Normally, this page should stay unused to guarantee that dereferencing a null pointer causes a page fault, so we know that the bootloader leaves it unmapped. +`EmptyFrameAllocator`を使って`create_example_mapping`を呼び出すためには、すべての(階層の)ページテーブルがすでに存在しているページを選ぶ必要があります。そんなページを探すにあたっては、ブートローダが自分自身を仮想アドレス空間の最初の1メガバイトに読み込んでいるということを利用できます。つまり、この領域のすべてのページについて、レベル1テーブルがきちんと存在しているということです。したがって、試しに対応を作るときに、このメモリ領域のいずれかの未使用ページ、例えばアドレス`0`を使えばよいです。普通このページは、ヌルポインタの参照外しがページフォルトを引き起こすことを保証するために使用しないので、ブートローダもここを対応させてはいないはずです。 -#### Creating the Mapping +#### 対応を作る -We now have all the required parameters for calling our `create_example_mapping` function, so let's modify our `kernel_main` function to map the page at virtual address `0`. Since we map the page to the frame of the VGA text buffer, we should be able to write to the screen through it afterwards. The implementation looks like this: +というわけで、`create_example_mapping`関数を呼び出すために必要なすべての引数を手に入れたので、仮想アドレス`0`を対応付けるよう`kernel_main`関数を変更していきましょう。このページはVGAテキストバッファのフレームに対応付けているので、以後、画面に書き込むことができるはずです。実装は以下のようになります: ```rust // in src/main.rs fn kernel_main(boot_info: &'static BootInfo) -> ! { use blog_os::memory; - use x86_64::{structures::paging::Page, VirtAddr}; // new import + use x86_64::{structures::paging::Page, VirtAddr}; // 新しいインポート - […] // hello world and blog_os::init + […] // hello worldとblog_os::init let phys_mem_offset = VirtAddr::new(boot_info.physical_memory_offset); let mut mapper = unsafe { memory::init(phys_mem_offset) }; let mut frame_allocator = memory::EmptyFrameAllocator; - // map an unused page + // 未使用のページを対応付ける let page = Page::containing_address(VirtAddr::new(0)); memory::create_example_mapping(page, &mut mapper, &mut frame_allocator); - // write the string `New!` to the screen through the new mapping + // 新しい対応付けを使って、文字列`New!`を画面に書き出す let page_ptr: *mut u64 = page.start_address().as_mut_ptr(); unsafe { page_ptr.offset(400).write_volatile(0x_f021_f077_f065_f04e)}; - […] // test_main(), "it did not crash" printing, and hlt_loop() + […] // test_main(), "it did not crash" printing, および hlt_loop() } ``` -We first create the mapping for the page at address `0` by calling our `create_example_mapping` function with a mutable reference to the `mapper` and the `frame_allocator` instances. This maps the page to the VGA text buffer frame, so we should see any write to it on the screen. +まず、`mapper`と`frame_allocator`インスタンスの可変参照を渡して`create_example_mapping`を呼ぶことで、アドレス`0`のページに対応を作っています。これはVGAテキストバッファのフレームに対応付けているので、これに書き込んだものは何であれ画面に出てくるはずです。 -Then we convert the page to a raw pointer and write a value to offset `400`. We don't write to the start of the page because the top line of the VGA buffer is directly shifted off the screen by the next `println`. We write the value `0x_f021_f077_f065_f04e`, which represents the string _"New!"_ on white background. As we learned [in the _“VGA Text Mode”_ post], writes to the VGA buffer should be volatile, so we use the [`write_volatile`] method. +次にページを生ポインタに変更して、オフセット`400`に値を書き込みます。このページの最初に書き込むとVGAバッファの一番上の行になり、次のprintlnで即座に画面外に流れていってしまうのでそれはしません。値`0x_f021_f077_f065_f04e`は、白背景の"New!"という文字列を表します。[VGAテキストモードの記事][in the _“VGA Text Mode”_ post]で学んだように、VGAバッファへの書き込みはvolatileでなければならないので、[`write_volatile`]メソッドを使っています。 [in the _“VGA Text Mode”_ post]: @/edition-2/posts/03-vga-text-buffer/index.md#volatile [`write_volatile`]: https://doc.rust-lang.org/std/primitive.pointer.html#method.write_volatile -When we run it in QEMU, we see the following output: +QEMUで実行すると、以下の出力を得ます: ![QEMU printing "It did not crash!" with four completely white cells in the middle of the screen](qemu-new-mapping.png) -The _"New!"_ on the screen is by our write to page `0`, which means that we successfully created a new mapping in the page tables. +画面の"New!"はページ`0`への書き込みによるものなので、ページテーブルへの新しい対応付けの作成が成功したということを意味します。 -Creating that mapping only worked because the level 1 table responsible for the page at address `0` already exists. When we try to map a page for that no level 1 table exists yet, the `map_to` function fails because it tries to allocate frames from the `EmptyFrameAllocator` for creating new page tables. We can see that happen when we try to map page `0xdeadbeaf000` instead of `0`: +この対応付けが成功したのは、アドレス`0`を管轄するレベル1テーブルがすでに存在していたからに過ぎません。レベル1テーブルがまだ存在しないページを対応付けようとすると、`map_to`関数は新しいページテーブルを作るために`EmptyFrameAllocator`からフレームを割り当てようとしてエラーになります。`0`の代わりに`0xdeadbeaf000`を対応付けようとするとそれが発生するのが見られます。 ```rust // in src/main.rs @@ -845,35 +858,37 @@ fn kernel_main(boot_info: &'static BootInfo) -> ! { } ``` -When we run it, a panic with the following error message occurs: +これを実行すると、以下のエラーメッセージとともにパニックします: ``` panicked at 'map_to failed: FrameAllocationFailed', /…/result.rs:999:5 ``` -To map pages that don't have a level 1 page table yet we need to create a proper `FrameAllocator`. But how do we know which frames are unused and how much physical memory is available? +レベル1テーブルのまだ存在しないページを対応付けるためには、ちゃんとした`FrameAllocator`を作らないといけません。しかし、どのフレームが未使用で、どのフレームが利用可能かはどうすればわかるのでしょう? -### Allocating Frames +### フレームを割り当てる -In order to create new page tables, we need to create a proper frame allocator. For that we use the `memory_map` that is passed by the bootloader as part of the `BootInfo` struct: +新しいページテーブルを作成するためには、ちゃんとしたフレームアロケータを作る必要があります。このためには、ブートローダによって渡される`BootInfo`構造体の一部である`memory_map`を使います: ```rust // in src/memory.rs use bootloader::bootinfo::MemoryMap; -/// A FrameAllocator that returns usable frames from the bootloader's memory map. +/// ブートローダのメモリマップから、使用可能な +/// フレームを返すFrameAllocator pub struct BootInfoFrameAllocator { memory_map: &'static MemoryMap, next: usize, } impl BootInfoFrameAllocator { - /// Create a FrameAllocator from the passed memory map. + /// 渡されたメモリマップからFrameAllocatorを作る。 /// - /// This function is unsafe because the caller must guarantee that the passed - /// memory map is valid. The main requirement is that all frames that are marked - /// as `USABLE` in it are really unused. + /// この関数はunsafeである:呼び出し元は渡された + /// メモリマップが有効であることを保証しなければ + /// ならない。特に、`USABLE`なフレームは実際に + /// 未使用でなくてはならない。 pub unsafe fn init(memory_map: &'static MemoryMap) -> Self { BootInfoFrameAllocator { memory_map, @@ -883,15 +898,15 @@ impl BootInfoFrameAllocator { } ``` -The struct has two fields: A `'static` reference to the memory map passed by the bootloader and a `next` field that keeps track of number of the next frame that the allocator should return. +この構造体は2つのフィールドを持ちます。ブートローダによって渡されたメモリマップへの`'static`な参照と、アロケータが次に返すべきフレームの番号を覚えておくための`next`フィールドです。 -As we explained in the [_Boot Information_](#boot-information) section, the memory map is provided by the BIOS/UEFI firmware. It can only be queried very early in the boot process, so the bootloader already calls the respective functions for us. The memory map consists of a list of [`MemoryRegion`] structs, which contain the start address, the length, and the type (e.g. unused, reserved, etc.) of each memory region. +[_Boot Information_](#boot-information)節で説明したように、このメモリマップはBIOS/UEFIファームウェアから提供されます。これはブートプロセスのごく初期にのみ取得できるので、ブートローダがそのための関数を呼ぶようになっています。メモリマップは`MemoryRegion`構造体のリストからなり、この構造体はそれぞれのメモリ領域の開始アドレス、長さ、型(未使用、予約済み、など)を格納しています。 -The `init` function initializes a `BootInfoFrameAllocator` with a given memory map. The `next` field is initialized with `0` and will be increased for every frame allocation to avoid returning the same frame twice. Since we don't know if the usable frames of the memory map were already used somewhere else, our `init` function must be `unsafe` to require additional guarantees from the caller. +`init`関数は`BootInfoFrameAllocator`を与えられたメモリマップで初期化します。`next`フィールドは`0`で初期化し、フレームを割当てるたびに値を増やすことで同じフレームを二度返すことを防ぎます。メモリマップのusable (使用可能) なフレームが他の誰かに使われたりしていないかは知ることができないので、この`init`関数はそれを呼び出し元に追加で保証させるために`unsafe`でないといけません。 -#### A `usable_frames` Method +#### `usable_frames`メソッド -Before we implement the `FrameAllocator` trait, we add an auxiliary method that converts the memory map into an iterator of usable frames: +`FrameAllocator`トレイトを実装していく前に、渡されたメモリマップをusableなフレームのイテレータに変換する補助メソッドを追加します: ```rust // in src/memory.rs @@ -899,30 +914,30 @@ Before we implement the `FrameAllocator` trait, we add an auxiliary method that use bootloader::bootinfo::MemoryRegionType; impl BootInfoFrameAllocator { - /// Returns an iterator over the usable frames specified in the memory map. + /// メモリマップによって指定されたusableなフレームのイテレータを返す。 fn usable_frames(&self) -> impl Iterator { - // get usable regions from memory map + // メモリマップからusableな領域を得る let regions = self.memory_map.iter(); let usable_regions = regions .filter(|r| r.region_type == MemoryRegionType::Usable); - // map each region to its address range + // それぞれの領域をアドレス範囲にmapで変換する let addr_ranges = usable_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 + // 開始アドレスから`PhysFrame`型を作る frame_addresses.map(|addr| PhysFrame::containing_address(PhysAddr::new(addr))) } } ``` -This function uses iterator combinator methods to transform the initial `MemoryMap` into an iterator of usable physical frames: +この関数はイテレータのコンビネータメソッドを使って、最初に与えられる`MemoryMap`を使用可能な物理フレームのイテレータに変換します: -- First, we call the `iter` method to convert the memory map to an iterator of [`MemoryRegion`]s. -- Then we use the [`filter`] method to skip any reserved or otherwise unavailable regions. The bootloader updates the memory map for all the mappings it creates, so frames that are used by our kernel (code, data or stack) or to store the boot information are already marked as `InUse` or similar. Thus we can be sure that `Usable` frames are not used somewhere else. -- Afterwards, we use the [`map`] combinator and Rust's [range syntax] to transform our iterator of memory regions to an iterator of address ranges. -- Next, we use [`flat_map`] to transform the address ranges into an iterator of frame start addresses, choosing every 4096th address using [`step_by`]. Since 4096 bytes (= 4 KiB) is the page size, we get the start address of each frame. The bootloader page aligns all usable memory areas so that we don't need any alignment or rounding code here. By using [`flat_map`] instead of `map`, we get an `Iterator` instead of an `Iterator>`. -- Finally, we convert the start addresses to `PhysFrame` types to construct the an `Iterator`. +- まず`iter`メソッドを使ってメモリマップを[`MemoryRegion`]のイテレータに変える。 +- 次に[`filter`]メソッドを使って、予約済みなどの理由で使用不可能な領域を飛ばすようにする。ブートローダは作った対応付けに使ったメモリマップはきちんと更新するので、私達のカーネル(コード、データ、スタック)に使われているフレームやブート情報を格納するのに使われているフレームはすでに`InUse` (使用中) などでマークされています。そのため`Usable`なフレームは他の場所では使われていないはずとわかります。 +- つぎに、[`map`]コンビネータとRustの[range構文][range syntax]を使って、メモリ領域のイテレータからアドレス範囲のイテレータへと変換する。 +- つぎに、アドレス範囲から[`step_by`]で4096個ごとにアドレスを選び、[`flat_map`]を使うことでフレームの最初のアドレスのイテレータを得る。4096バイト(=4KiB)はページのサイズに等しいので、それぞれのフレームの開始地点のアドレスが得られます。ブートローダのページは使用可能なメモリ領域をすべてアラインするので、ここで改めてアラインや丸めを行う必要はありません。`map`ではなく[`flat_map`]を使うことで、`Iterator>`ではなく`Iterator`を得ています。 +- 最後に、開始アドレスの型を`PhysFrame`に変更することで`Iterator`を得ている。 [`MemoryRegion`]: https://docs.rs/bootloader/0.6.4/bootloader/bootinfo/struct.MemoryRegion.html [`filter`]: https://doc.rust-lang.org/core/iter/trait.Iterator.html#method.filter @@ -931,14 +946,14 @@ This function uses iterator combinator methods to transform the initial `MemoryM [`step_by`]: https://doc.rust-lang.org/core/iter/trait.Iterator.html#method.step_by [`flat_map`]: https://doc.rust-lang.org/core/iter/trait.Iterator.html#method.flat_map -The return type of the function uses the [`impl Trait`] feature. This way, we can specify that we return some type that implements the [`Iterator`] trait with item type `PhysFrame`, but don't need to name the concrete return type. This is important here because we _can't_ name the concrete type since it depends on unnamable closure types. +この関数の戻り型は[`impl Trait`]機能を用いています。こうすると、`PhysFrame`をitemの型として持つような[`Iterator`]トレイトを実装する何らかの型を返すのだと指定できます。これは重要です――なぜなら、戻り値の型は名前のつけられないクロージャ型に依存し、具体的な名前をつけるのが**不可能だ**からです。 [`impl Trait`]: https://doc.rust-lang.org/book/ch10-02-traits.html#returning-types-that-implement-traits [`Iterator`]: https://doc.rust-lang.org/core/iter/trait.Iterator.html -#### Implementing the `FrameAllocator` Trait +#### `FrameAllocator`トレイトを実装する -Now we can implement the `FrameAllocator` trait: +これで`FrameAllocator`トレイトを実装できます: ```rust // in src/memory.rs @@ -952,18 +967,18 @@ unsafe impl FrameAllocator for BootInfoFrameAllocator { } ``` -We first use the `usable_frames` method to get an iterator of usable frames from the memory map. Then, we use the [`Iterator::nth`] function to get the frame with index `self.next` (thereby skipping `(self.next - 1)` frames). Before returning that frame, we increase `self.next` by one so that we return the following frame on the next call. +まず`usable_frames`メソッドを使ってメモリマップからusableなフレームのイテレータを得ます。つぎに、[`Iterator::nth`]関数で`self.next`番目の(つまり`(self.next - 1)`だけ飛ばして)フレームを得ます。このフレームを返してリターンする前に、`self.next`を1だけ増やして次の呼び出しで1つ後のフレームが得られるようにします。 [`Iterator::nth`]: https://doc.rust-lang.org/core/iter/trait.Iterator.html#method.nth -This implementation is not quite optimal since it recreates the `usable_frame` allocator on every allocation. It would be better to directly store the iterator as a struct field instead. Then we wouldn't need the `nth` method and could just call [`next`] on every allocation. The problem with this approach is that it's not possible to store an `impl Trait` type in a struct field currently. It might work someday when [_named existential types_] are fully implemented. +この実装は割当てを行うごとに`usable_frames`アロケータを作り直しているので、あまり最適ではありません。イテレータを構造体のフィールドとして直接格納するほうが良いでしょう。すると`nth`メソッドを使う必要はなくなり、割り当てのたびに[`next`]を使えばいいだけです。このアプローチの問題は、今の所構造体のフィールドに`impl Trait`型(の変数)を格納することができないことです。いつの日か、[named existential type][_named existential types_]が完全に実装されたときにはこれが可能になるかもしれません。 [`next`]: https://doc.rust-lang.org/core/iter/trait.Iterator.html#tymethod.next [_named existential types_]: https://github.com/rust-lang/rfcs/pull/2071 -#### Using the `BootInfoFrameAllocator` +#### `BootInfoFrameAllocator`を使う -We can now modify our `kernel_main` function to pass a `BootInfoFrameAllocator` instance instead of an `EmptyFrameAllocator`: +`kernel_main`関数を修正して`EmptyFrameAllocator`のインスタンスの代わりに`BootInfoFrameAllocator`を渡しましょう: ```rust // in src/main.rs @@ -978,28 +993,28 @@ fn kernel_main(boot_info: &'static BootInfo) -> ! { } ``` -With the boot info frame allocator, the mapping succeeds and we see the black-on-white _"New!"_ on the screen again. Behind the scenes, the `map_to` method creates the missing page tables in the following way: +ブート情報を使うフレームアロケータのおかげで対応付けは成功し、白背景に黒文字の"New!"が再び画面に現れました。舞台裏では、`map_to`メソッドが不足しているページテーブルを以下のやり方で作っています: -- Allocate an unused frame from the passed `frame_allocator`. -- Zero the frame to create a new, empty page table. -- Map the entry of the higher level table to that frame. -- Continue with the next table level. +- 渡された`frame_allocator`を使って未使用のフレームを割り当てる。 +- フレームをゼロで埋め、新しい空のページテーブルを作る。 +- 上位のテーブルのエントリをそのフレームに対応付ける。 +- 次の層で同じことを続ける。 -While our `create_example_mapping` function is just some example code, we are now able to create new mappings for arbitrary pages. This will be essential for allocating memory or implementing multithreading in future posts. +`create_example_mapping`関数はただのお試しコードにすぎませんが、今や私達は任意のページに対応付けを作れるようになりました。これは、今後の記事で行うメモリ割り当てやマルチスレッディングにおいて不可欠です。 -At this point, we should delete the `create_example_mapping` function again to avoid accidentally invoking undefined behavior, as explained [above](#a-create-example-mapping-function). +[上](#create-example-mappingguan-shu)で説明したような未定義動作を誤って引き起こしてしまうことのないよう、この時点で`create_example_mapping`関数を再び取り除いておきましょう。 -## Summary +## まとめ -In this post we learned about different techniques to access the physical frames of page tables, including identity mapping, mapping of the complete physical memory, temporary mapping, and recursive page tables. We chose to map the complete physical memory since it's simple, portable, and powerful. +この記事ではページテーブルのある物理フレームにアクセスするための様々なテクニックを学びました。恒等対応、物理メモリ全体の対応付け、一時的な対応、再帰的ページテーブルなどです。このうち、シンプルでポータブル(アーキテクチャ非依存という意味)で強力な、物理メモリ全体の対応付けを選びました。 -We can't map the physical memory from our kernel without page table access, so we needed support from the bootloader. The `bootloader` crate supports creating the required mapping through optional cargo features. It passes the required information to our kernel in the form of a `&BootInfo` argument to our entry point function. +ページテーブルにアクセスできなければ物理メモリを対応付けられないので、ブートローダの補助が必要でした。`bootloader`クレートはcargoのfeaturesというオプションを通じて、必要となる対応付けの作成をサポートしています。さらに、必要となる情報をエントリポイント関数の`&BootInfo`引数という形で私達のカーネルに渡してくれます。 -For our implementation, we first manually traversed the page tables to implement a translation function, and then used the `MappedPageTable` type of the `x86_64` crate. We also learned how to create new mappings in the page table and how to create the necessary `FrameAllocator` on top of the memory map passed by the bootloader. +実装について。最初は手作業でページテーブルを辿ることで変換関数を実装し、そのあとで`x86_64`クレートの`MappedPageTable`型を使いました。また、ページテーブルに新しい対応を作る方法や、そのために必要な`FrameAllocator`をブートローダに渡されたメモリマップをラップすることで作る方法を学びました。 -## What's next? +## 次は? -The next post will create a heap memory region for our kernel, which will allow us to [allocate memory] and use various [collection types]. +次の記事では、私達のカーネルのためのヒープメモリ領域を作り、それによって[メモリの割り当て][allocate memory]や各種の[コレクション型][collection types]を使うことが可能になります。 [allocate memory]: https://doc.rust-lang.org/alloc/boxed/struct.Box.html [collection types]: https://doc.rust-lang.org/alloc/collections/index.html From 575547963d269ff53c44e9fb720f3b9655cc166f Mon Sep 17 00:00:00 2001 From: woodyZootopia Date: Sun, 15 Aug 2021 15:15:20 +0900 Subject: [PATCH 06/11] Fix links to Japanese articles --- .../09-paging-implementation/index.ja.md | 20 +++++++++---------- 1 file changed, 9 insertions(+), 11 deletions(-) diff --git a/blog/content/edition-2/posts/09-paging-implementation/index.ja.md b/blog/content/edition-2/posts/09-paging-implementation/index.ja.md index 34540189..fb70e770 100644 --- a/blog/content/edition-2/posts/09-paging-implementation/index.ja.md +++ b/blog/content/edition-2/posts/09-paging-implementation/index.ja.md @@ -24,15 +24,13 @@ translators = ["woodyZootopia"] ## 導入 -## 記事へのリンクを日本語のものに直す - [1つ前の記事][previous post]ではページングの概念を説明しました。セグメンテーションと比較することによってページングのメリットを示し、ページングとページテーブルの仕組みを説明し、そして`x86_64`における4層ページテーブルの設計を導入しました。ブートローダはすでにページテーブルの階層構造を設定してしまっているので、私達のカーネルは既に仮想アドレス上で動いているということを学びました。これにより、不正なメモリアクセスは、任意の物理メモリを書き換えてしまう代わりにページフォルト例外を発生させるので、安全性が向上しています。 [previous post]: @/edition-2/posts/08-paging-introduction/index.ja.md 記事の最後で、[ページテーブルにカーネルからアクセスできない][end of previous post]という問題が起きていました。この問題は、ページテーブルは物理メモリ内に格納されている一方、私達のカーネルは既に仮想アドレス上で実行されているために発生します。この記事ではその続きとして、私達のカーネルからページテーブルのフレームにアクセスするための様々な方法を探ります。それぞれの方法の利点と欠点を議論し、カーネルに採用する手法を決めます。 -[end of previous post]: @/edition-2/posts/08-paging-introduction/index.md#accessing-the-page-tables +[end of previous post]: @/edition-2/posts/08-paging-introduction/index.ja.md#peziteburuhenoakusesu この方法を実装するには、ブートローダーからの補助が必要になるので、まずこれに設定を加えます。その後で、ページテーブルの階層構造を移動して、仮想アドレスを物理アドレスに変換する関数を実装します。最後に、ページテーブルに新しい対応関係を作る方法と、それを作るための未使用メモリを見つける方法を学びます。 @@ -59,7 +57,7 @@ translators = ["woodyZootopia"] しかし、この方法では仮想アドレス空間が散らかってしまい、大きいサイズの連続したメモリを見つけることが難しくなります。例えば、上の図において、[ファイルをメモリにマップする][memory-mapping a file]ために1000KiBの大きさの仮想メモリ領域を作りたいとします。`28KiB`を始点として領域を作ろうとすると、`1004KiB`のところで既存のページと衝突してしまうのでうまくいきません。そのため、`1008KiB`のような、十分な広さで対応付けのない領域が見つかるまで更に探さないといけません。これは[セグメンテーション][segmentation]の時に見た断片化の問題に似ています。 [memory-mapping a file]: https://en.wikipedia.org/wiki/Memory-mapped_file -[segmentation]: @/edition-2/posts/08-paging-introduction/index.md#fragmentation +[segmentation]: @/edition-2/posts/08-paging-introduction/index.ja.md#duan-pian-hua-fragmentation 同様に、新しいページテーブルを作ることもずっと難しくなります。なぜなら、対応するページがまだ使われていない物理フレームを見つけないといけないからです。例えば、メモリマップト (に対応づけられた) ファイルのために`1008KiB`から1000KiBにわたって仮想メモリを占有したとしましょう。すると、すると、物理アドレス`1000KiB`から`2008KiB`までのフレームは、もう恒等対応を作ることができないので使用することができません。 @@ -187,7 +185,7 @@ We can now calculate virtual addresses for the page tables of all four levels. W `SSSSS`は符号拡張ビットです、すなわち47番目のビットのコピーです。これはx86_64におけるアドレスの特殊な要求の一つです。これは[前回の記事][sign extension]で説明しました。 -[sign extension]: @/edition-2/posts/08-paging-introduction/index.md#paging-on-x86-64 +[sign extension]: @/edition-2/posts/08-paging-introduction/index.ja.md#x86-64niokerupezingu [8進][octal]数を用いたのは、8進数の1文字が3ビットを表すため、9ビットからなるそれぞれのページテーブルをきれいに分けることができるためです。4ビットからなる16進ではこうはいきません。 @@ -376,9 +374,9 @@ pub mod memory; ### ページテーブルにアクセスする -[前の記事の最後]で、私達のカーネルの実行しているページテーブルを見てみようとしましたが、`CR3`レジスタの指す物理フレームにアクセスすることができなかったためそれはできませんでした。前回の続きとして、`active_level_4_table`という、現在有効 (アクティブ) なレベル4ページテーブルへの参照を返す関数を定義するところから始めましょう: +[前の記事の最後][end of the previous post]で、私達のカーネルの実行しているページテーブルを見てみようとしましたが、`CR3`レジスタの指す物理フレームにアクセスすることができなかったためそれはできませんでした。前回の続きとして、`active_level_4_table`という、現在有効 (アクティブ) なレベル4ページテーブルへの参照を返す関数を定義するところから始めましょう: -[end of the previous post]: @/edition-2/posts/08-paging-introduction/index.md#accessing-the-page-tables +[end of the previous post]: @/edition-2/posts/08-paging-introduction/index.ja.md#peziteburuhenoakusesu ```rust // in src/memory.rs @@ -604,7 +602,7 @@ fn kernel_main(boot_info: &'static BootInfo) -> ! { 期待したとおり、恒等対応しているアドレス`0xb8000`は同じ物理アドレスに変換されました。コードページとスタックページは物理アドレスのどこかしかに変換されていますが、その場所はブートローダが私達のカーネルのために最初に作った対応づけに依存します。また、下から12ビットは変換のあとも常に同じであるということも注目に値します:この部分は[ページオフセット][_page offset_]であり、変換には関わらないためです。 -[_page offset_]: @/edition-2/posts/08-paging-introduction/index.md#paging-on-x86-64 +[_page offset_]: @/edition-2/posts/08-paging-introduction/index.ja.md#x86-64niokerupezingu それぞれの物理アドレスは`physical_memory_offset`を足すことでアクセスできるわけですから、`physical_memory_offset`自体を(仮想アドレスとして)変換すると物理アドレス`0`を指すはずです。しかし、効率よく対応付けを行うためにここではhuge pageが使われており、これはまだサポートしていないので変換には失敗しています。 @@ -754,7 +752,7 @@ pub fn create_example_mapping( `map_to`関数が`page`と`unused_frame`に加えてフラグの集合と`frame_allocator`への参照を取りますが、これについてはすぐに説明します。フラグについては、`PRESENT`フラグという有効なエントリ全てに必須のフラグと、`WRITABLE`フラグという対応するページを書き込み可能にするフラグをセットしています。フラグの一覧については、前記事の[ページテーブルの形式][_Page Table Format_]を参照してください。 -[_Page Table Format_]: @/edition-2/posts/08-paging-introduction/index.md#page-table-format +[_Page Table Format_]: @/edition-2/posts/08-paging-introduction/index.ja.md#peziteburunoxing-shi [`map_to`]関数は失敗しうるので、[`Result`]を返します。これは失敗しても構わない単なるテストコードなので、エラーが起きたときは[`expect`]を使ってパニックしてしまうことにします。この関数は成功したとき[`MapperFlush`]型を返します。この型の[`flush`]メソッドを使うと、新しく対応させたページをトランスレーション・ルックアサイド・バッファ (TLB) から簡単にflushすることができます。この型は`Result`と同じく[`#[must_use]`][must_use]属性を使っており、使用し忘れると警告を出します。 @@ -798,7 +796,7 @@ unsafe impl FrameAllocator for EmptyFrameAllocator { この図の仮想アドレス空間には、2つの候補となるページを黄色で示しています。ページのうち一つはアドレス`0x803fe00000`で、これは(青で示された)対応付けられたページの3つ前です。レベル4と3のテーブルのインデクスは青いページと同じですが、レベル2と1のインデクスは違います([前の記事][page-table-indices]を参照)。レベル2テーブルのインデクスが違うということは、異なるレベル1テーブルが使われることを意味します。そんなレベル1テーブルは存在しないので、もしこちらを使っていたら、使われていない物理フレームを追加(でアロケート)する必要が出てきます。対して、2つ目のアドレス`0x803fe02000`にある候補のページは、青のページと同じレベル1ページテーブルを使うのでこの問題は発生しません。よって、必要となるすべてのページテーブルはすでに存在しています。 -[page-table-indices]: @/edition-2/posts/08-paging-introduction/index.md#paging-on-x86-64 +[page-table-indices]: @/edition-2/posts/08-paging-introduction/index.ja.md#x86-64niokerupezingu まとめると、新しい対応を作るときの難易度は、対応付けようとしている仮想ページに依存するということです。作ろうとしているページのレベル1ページテーブルがすでに存在すると最も簡単で、エントリを一つ書き込むだけです。ページがレベル3のテーブルすら存在しない領域にある場合が最も難しく、その場合まずレベル3,2,1のページテーブルを新しく作る必要があります。 @@ -837,7 +835,7 @@ fn kernel_main(boot_info: &'static BootInfo) -> ! { 次にページを生ポインタに変更して、オフセット`400`に値を書き込みます。このページの最初に書き込むとVGAバッファの一番上の行になり、次のprintlnで即座に画面外に流れていってしまうのでそれはしません。値`0x_f021_f077_f065_f04e`は、白背景の"New!"という文字列を表します。[VGAテキストモードの記事][in the _“VGA Text Mode”_ post]で学んだように、VGAバッファへの書き込みはvolatileでなければならないので、[`write_volatile`]メソッドを使っています。 -[in the _“VGA Text Mode”_ post]: @/edition-2/posts/03-vga-text-buffer/index.md#volatile +[in the _“VGA Text Mode”_ post]: @/edition-2/posts/03-vga-text-buffer/index.ja.md#volatile [`write_volatile`]: https://doc.rust-lang.org/std/primitive.pointer.html#method.write_volatile QEMUで実行すると、以下の出力を得ます: From 282ed23a99979e704ce6b1064c8a87b57df70640 Mon Sep 17 00:00:00 2001 From: woodyZootopia Date: Tue, 24 Aug 2021 12:03:52 +0900 Subject: [PATCH 07/11] =?UTF-8?q?=E7=BF=BB=E8=A8=B3=E3=82=92=E3=81=95?= =?UTF-8?q?=E3=82=89=E3=81=AB=E6=A0=A1=E6=AD=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../09-paging-implementation/index.ja.md | 102 +++++++++--------- 1 file changed, 50 insertions(+), 52 deletions(-) diff --git a/blog/content/edition-2/posts/09-paging-implementation/index.ja.md b/blog/content/edition-2/posts/09-paging-implementation/index.ja.md index fb70e770..791a8f4f 100644 --- a/blog/content/edition-2/posts/09-paging-implementation/index.ja.md +++ b/blog/content/edition-2/posts/09-paging-implementation/index.ja.md @@ -40,7 +40,7 @@ translators = ["woodyZootopia"] ![An example 4-level page hierarchy with each page table shown in physical memory](../paging-introduction/x86_64-page-table-translation.svg) -ここで重要なのは、それぞれのページテーブルのエントリは次のテーブルの**物理**アドレスであるということです。これにより、それらのアドレスに対しても変換することを避けられます。もしこの変換が行われたとしたら、性能的にも良くないですし、容易に変換の無限ループに陥りかねません。 +ここで重要なのは、それぞれのページテーブルのエントリは次のテーブルの**物理**アドレスであるということです。これにより、それらのアドレスに対しては変換せずにすみます。もしこの変換が行われたとしたら、性能的にも良くないですし、容易に変換の無限ループに陥りかねません。 問題は、私達のカーネル自体も仮想アドレスの上で動いているため、カーネルから直接物理アドレスにアクセスすることができないということです。例えば、アドレス`4KiB`にアクセスしたとき、私達は**仮想**アドレス`4KiB`にアクセスしているのであって、レベル4ページテーブルが格納されている**物理**アドレス`4KiB`にアクセスしているのではありません。物理アドレス`4KiB`にアクセスしたいなら、それに対応づけられている何らかの仮想アドレスを通じてのみ可能です。 @@ -81,7 +81,7 @@ translators = ["woodyZootopia"] この方法の欠点は、物理メモリへの対応付けを格納するために、追加でページテーブルが必要になるところです。これらのページテーブルもどこかに格納されなければならず、したがって物理メモリの一部を占有することになります。これはメモリの量が少ないデバイスにおいては問題となりえます。 -しかし、x86_64においては、通常の4KiBサイズのページに代わって、大きさ2MiBの[huge page][huge pages]を対応付けに使うことができます。こうすれば、例えば32GiBの物理メモリを対応付けるのに、レベル3テーブル1個とレベル2テーブル32個があればいいので、たったの132KiBしか必要ではありません。huge pagesは、トランスレーション・ルックアサイド・バッファ (TLB) のエントリをあまり使わないので、キャッシュ的にも効率が良いです。 +しかし、x86_64においては、通常の4KiBサイズのページに代わって、大きさ2MiBの[huge page][huge pages]を対応付けに使うことができます。こうすれば、例えば32GiBの物理メモリを対応付けるのにはレベル3テーブル1個とレベル2テーブル32個があればいいので、たったの132KiBしか必要ではありません。huge pagesは、トランスレーション・ルックアサイド・バッファ (TLB) のエントリをあまり使わないので、キャッシュ的にも効率が良いです。 [huge pages]: https://en.wikipedia.org/wiki/Page_%28computer_memory%29#Multiple_page_sizes @@ -91,12 +91,12 @@ translators = ["woodyZootopia"] ![A virtual and a physical address space with an identity mapped level 1 table, which maps its 0th entry to the level 2 table frame, thereby mapping that frame to page with address 0](temporarily-mapped-page-tables.svg) -この図におけるレベル1テーブルは仮想アドレス空間の最初の2MiBを制御しています。なぜなら、このテーブルにはCR3レジスタから始めて、レベル4、3、2のページテーブルの0番目のエントリを辿ることで到達できるからです。8番目のエントリは、アドレス`32 KiB`の仮想アドレスページをアドレス`32 KiB`の物理アドレスページに対応付けるので、レベル1テーブル自体を恒等対応させています。この図ではその恒等対応を`32 KiB`のところの横向きの(茶色の)矢印で表しています。 +この図におけるレベル1テーブルは仮想アドレス空間の最初の2MiBを制御しています。なぜなら、このテーブルにはCR3レジスタから始めて、レベル4、3、2のページテーブルの0番目のエントリを辿ることで到達できるからです。その8番目のエントリは、アドレス`32 KiB`の仮想アドレスページをアドレス`32 KiB`の物理アドレスページに対応付けるので、レベル1テーブル自体を恒等対応させています。この図ではその恒等対応を`32 KiB`のところの横向きの(茶色の)矢印で表しています。 恒等対応させたレベル1テーブルに書き込むことによって、カーネルは最大511個の一時的な対応を作ることができます(512から、恒等対応に必要な1つを除く)。上の例では、カーネルは2つの一時的な対応を作りました: - レベル1テーブルの0番目のエントリをアドレス`24 KiB`のフレームに対応付けることで、破線の矢印で示されているように`0 KiB`の仮想ページからレベル2ページテーブルの物理フレームへの一時的対応付けを行いました。 -- レベル1テーブルの9番目のエントリをアドレス`4 KiB`のフレームに対応付けることで、破線の矢印で示されているように`36 KiB`の仮想ページからレベル4ページテーブルの物理フレームへの一時的対応付を行いました。 +- レベル1テーブルの9番目のエントリをアドレス`4 KiB`のフレームに対応付けることで、破線の矢印で示されているように`36 KiB`の仮想ページからレベル4ページテーブルの物理フレームへの一時的対応付けを行いました。 これで、カーネルは`0 KiB`に書き込むことによってレベル2ページテーブルに、`36 KiB`に書き込むことによってレベル4ページテーブルにアクセスできるようになりました。 @@ -107,7 +107,7 @@ translators = ["woodyZootopia"] - そのエントリに対応付けられている仮想ページを通じて、対象のフレームにアクセスする。 - エントリを未使用に戻すことで、一時的対応付けを削除する。 -この方法では、同じ512個の下層ページを対応付けを作成するために再利用するため、物理メモリは4KiBしか必要としません。欠点としては、やや面倒であるということが言えるでしょう。特に、新しい対応付けを作る際に複数のページテーブルの変更が必要になるかもしれず、上の手続きを複数回繰り返さなくてはならないかもしれません。 +この方法では、同じ512個の仮想ページを対応付けを作成するために再利用するため、物理メモリは4KiBしか必要としません。欠点としては、やや面倒であるということが言えるでしょう。特に、新しい対応付けを作る際に複数のページテーブルの変更が必要になるかもしれず、上の手続きを複数回繰り返さなくてはならないかもしれません。 ### 再帰的ページテーブル @@ -121,7 +121,7 @@ translators = ["woodyZootopia"] [example at the beginning of this post]: #peziteburuniakusesusuru -CPUにこのエントリを辿らせるようにすると、レベル3テーブルではなく、そのレベル4テーブルに再び到達します。これは再帰的関数(自らを呼び出す関数)に似ているので、**再帰的ページテーブル**と呼ばれます。レベル4テーブルのすべてのエントリはレベル3テーブルを指しているとCPUは思っているので、CPUはいまレベル4テーブルをレベル3テーブルとして扱っているということに注目してください。これがうまく行くのは、x86_64においてはすべてのレベルのテーブルが全く同じレイアウトを持っているためです。 +CPUにこのエントリを辿らせるようにすると、レベル3テーブルではなく、そのレベル4テーブルに再び到達します。これは再帰関数(自らを呼び出す関数)に似ているので、**再帰的ページテーブル** (recursive page table) と呼ばれます。CPUはレベル4テーブルのすべてのエントリはレベル3テーブルを指していると思っているので、CPUはいまレベル4テーブルをレベル3テーブルとして扱っているということに注目してください。これがうまく行くのは、x86_64においてはすべてのレベルのテーブルが全く同じレイアウトを持っているためです。 実際に変換を始める前に、この再帰エントリを1回以上たどることで、CPUのたどる階層の数を短くできます。例えば、一度再帰エントリを辿ったあとでレベル3テーブルに進むと、CPUはレベル3テーブルをレベル2テーブルだと思い込みます。同様に、レベル2テーブルをレベル1テーブルだと、レベル1テーブルを対応付けられた(物理)フレームだと思います。CPUがこれを物理フレームだと思っているということは、レベル1ページテーブルを読み書きできるということを意味します。下の図はこの5回の変換ステップを示しています: @@ -166,8 +166,7 @@ CPUにこのエントリを辿らせるようにすると、レベル3テーブ ![Bits 0–12 are the offset into the level l table frame and bits 12–21, bits 21–30, bits 30–39 and bits 39–48 are the index of the recursive entry](table-indices-from-address-recursive-level-4.svg) -We can now calculate virtual addresses for the page tables of all four levels. We can even calculate an address that points exactly to a specific page table entry by multiplying its index by 8, the size of a page table entry. -これで、4つの階層すべてのページテーブルの仮想アドレスを計算できます。 +これで、4つの階層すべてのページテーブルの仮想アドレスを計算できます。また、インデクスをページテーブルエントリのサイズ倍、つまり8倍することによって、特定のページテーブルエントリを指すアドレスを計算できます。 下の表は、それぞれの種類のフレームにアクセスするためのアドレス構造をまとめたものです: @@ -183,7 +182,7 @@ We can now calculate virtual addresses for the page tables of all four levels. W ただし、`AAA`がレベル4インデクス、`BBB`がレベル3インデクス、`CCC`がレベル2インデクス、`DDD`が対応付けられたフレームのレベル1インデクス、`EEE`がオフセットです。`RRR`が再帰エントリのインデクスです。インデクス(3ケタ)をオフセット(4ケタ)に変換するときは、8倍(ページテーブルエントリのサイズ倍)しています。 -`SSSSS`は符号拡張ビットです、すなわち47番目のビットのコピーです。これはx86_64におけるアドレスの特殊な要求の一つです。これは[前回の記事][sign extension]で説明しました。 +`SSSSS`は符号拡張ビットで、すなわち47番目のビットのコピーです。これはx86_64におけるアドレスの特殊な要求の一つです。これは[前回の記事][sign extension]で説明しました。 [sign extension]: @/edition-2/posts/08-paging-introduction/index.ja.md#x86-64niokerupezingu @@ -220,8 +219,7 @@ let level_1_table_addr = 上のコードは、レベル4エントリの最後(インデクス`0o777`すなわち511)が再帰対応していると仮定しています。この対応はまだ行っていないので、この仮定は正しくありません。以下でブートローダに再帰対応付けを設定させる方法を説明します。 -Alternatively to performing the bitwise operations by hand, you can use the [`RecursivePageTable`] type of the `x86_64` crate, which provides safe abstractions for various page table operations. For example, the code below shows how to translate a virtual address to its mapped physical address: -ビット演算を自前で行う代わりに、`x86_64`クレートの[`RecursivePageTable`]型を使うこともできます。これは様々なページ操作の安全な抽象化を提供します。例えば、以下のコードは仮想アドレスを対応付けられた物理アドレスに変換する方法を示しています +ビット演算を自前で行う代わりに、`x86_64`クレートの[`RecursivePageTable`]型を使うこともできます。これは様々なページ操作の安全な抽象化を提供します。例えば、以下のコードは仮想アドレスを対応付けられた物理アドレスに変換する方法を示しています。 [`RecursivePageTable`]: https://docs.rs/x86_64/0.14.2/x86_64/structures/paging/mapper/struct.RecursivePageTable.html @@ -266,18 +264,18 @@ frame.map(|frame| frame.start_address() + u64::from(addr.page_offset())) [_Remap The Kernel_]: https://os.phil-opp.com/remap-the-kernel/#overview -## ブートローダのサポート +## ブートローダによる補助 これらのアプローチはすべて、準備のためにページテーブルに対する修正が必要になります。例えば、物理メモリへの対応付けを作ったり、レベル4テーブルのエントリを再帰的に対応付けたりなどです。問題は、これらの必要な対応付けを作るためには、すでにページテーブルにアクセスできるようになっていなければいけないということです。 -これが意味するのは、私達のカーネルが使うページテーブルを作っている、ブートローダの手助けが必要になるということです。ブートローダはページテーブルへのアクセスができますから、私達の必要とするどんな対応付けも作れます。`bootloader`クレートは上の2つのアプローチをどちらもサポートしており、現在の実装においては[cargoのfeatures][cargo features]を使ってこれらをコントロールします。 +これが意味するのは、私達のカーネルが使うページテーブルを作っている、ブートローダの手助けが必要になるということです。ブートローダはページテーブルにアクセスできますから、私達の必要とするどんな対応付けも作れます。`bootloader`クレートは上の2つのアプローチをどちらもサポートしており、現在の実装においては[cargoのfeatures][cargo features]を使ってこれらをコントロールします。 [cargo features]: https://doc.rust-lang.org/cargo/reference/features.html#the-features-section - `map_physical_memory` featureを使うと、全物理メモリを仮想アドレス空間のどこかに対応付けます。そのため、カーネルはすべての物理メモリにアクセスでき、[上で述べた方法に従って物理メモリ全体を対応付ける](#wu-li-memoriquan-ti-wodui-ying-fu-keru)ことができます。 - `recursive_page_table` featureでは、ブートローダはレベル4ページテーブルのエントリを再帰的に対応付けます。これによりカーネルは[再帰的ページテーブル](#zai-gui-de-peziteburu)で述べた方法に従ってページテーブルにアクセスすることができます。 -私達のカーネルには、シンプルでプラットフォーム非依存でより強力である(ページテーブルのフレームでないメモリにもアクセスできる)1つ目の方法を取ることにします。必要なブートローダの機能 (feature) を有効化するために、`map_physical_memory` featureを`bootloader`のdependencyに追加します。 +私達のカーネルには、シンプルでプラットフォーム非依存でより強力である(ページテーブルのフレームでないメモリにもアクセスできるので)1つ目の方法を採ることにします。必要なブートローダの機能 (feature) を有効化するために、`map_physical_memory` featureを`bootloader`のdependencyに追加します。 ```toml @@ -285,7 +283,7 @@ frame.map(|frame| frame.start_address() + u64::from(addr.page_offset())) bootloader = { version = "0.9.8", features = ["map_physical_memory"]} ``` -この機能を有効化すると、ブートローダは物理メモリの全体を、とある未使用の仮想アドレス空間に対応付けます。この仮想アドレスの範囲をカーネルに伝えるために、ブートローダは**boot information**構造体を渡します。 +この機能を有効化すると、ブートローダは物理メモリの全体を、ある未使用の仮想アドレス空間に対応付けます。この仮想アドレスの範囲をカーネルに伝えるために、ブートローダは**boot information**構造体を渡します。 ### Boot Information @@ -295,7 +293,7 @@ bootloader = { version = "0.9.8", features = ["map_physical_memory"]} [`BootInfo`]: https://docs.rs/bootloader/0.9.3/bootloader/bootinfo/struct.BootInfo.html [semver-incompatible]: https://doc.rust-lang.org/stable/cargo/reference/specifying-dependencies.html#caret-requirements -- `memory_map`フィールドは、利用可能な物理メモリの情報の概要を保持しています。システムの利用可能な物理メモリがどのくらいかや、どのメモリ領域がVGAハードウェアのようなデバイスのために予約されているかをカーネルに伝えます。これらのメモリ対応付けはBIOSやUEFIファームウェアからブートのごく初期に限り取得することが可能です。そのため、これらをカーネルが後で取得することはできないので、ブートローダによって提供する必要があるわけです。このメモリ対応付けは後で必要となります。 +- `memory_map`フィールドは、利用可能な物理メモリの情報の概要を保持しています。システムの利用可能な物理メモリがどのくらいかや、どのメモリ領域がVGAハードウェアのようなデバイスのために予約されているかをカーネルに伝えます。これらのメモリ対応付けはBIOSやUEFIファームウェアから取得できますが、それが可能なのはブートのごく初期に限られます。そのため、これらをカーネルが後で取得することはできないので、ブートローダによって提供する必要があるわけです。このメモリ対応付けは後で必要となります。 - `physical_memory_offset`は、物理メモリの対応付けの始まっている仮想アドレスです。このオフセットを物理アドレスに追加することによって、対応する仮想アドレスを得られます。これによって、カーネルから任意の物理アドレスにアクセスできます。 ブートローダは`BootInfo`構造体を`_start`関数の`&'static BootInfo`引数という形でカーネルに渡します。この引数は私達の関数ではまだ宣言していなかったので追加します: @@ -311,13 +309,13 @@ pub extern "C" fn _start(boot_info: &'static BootInfo) -> ! { // 新しい引数 } ``` -今までこの引数を無視していましたが、x86_64の呼出し規約は最初の引数をCPUレジスタに渡していたため、これは問題ではありませんでした。つまり、引数が宣言されていなかったとき、単に無視されていたわけです。しかし、もし引数の型を間違えてしまうと、コンパイラが私達のエントリポイント関数の正しい型シグネチャがわからなくなってしまうので、それは問題です。 +今までこの引数を無視していましたが、x86_64の呼出し規約は最初の引数をCPUレジスタに渡していたため、これは問題ではありませんでした。つまり、引数が宣言されていなかったとき、それが単に無視されていたわけです。しかし、もし引数の型を間違えてしまうと、コンパイラが私達のエントリポイント関数の正しい型シグネチャがわからなくなってしまうので問題です。 ### `entry_point`マクロ -私達の`_start`関数はブートローダから外部呼び出しされるので、私達の関数のシグネチャに対する検査は行われません。これにより、コンパイルエラーなしにあらゆる引数を取ることを許してしまい、いざ実行時にエラーになったり未定義動作を起こしてしまいます。 +私達の`_start`関数はブートローダから外部呼び出しされるので、私達の関数のシグネチャに対する検査は行われません。これにより、この関数はコンパイルエラーなしにあらゆる引数を取ることができるので、いざ実行時にエラーになったり未定義動作を起こしてしまいます。 -私達のエントリポイント関数が常にブートローダの期待する正しいシグネチャを持っていることを保証するために、`bootloader`クレートは[`entry_point`]マクロによって、Rustの関数をエントリポイントとして型チェックしながら定義する方法を提供します。私達のエントリポイントをこのマクロを使って書き直してみましょう: +私達のエントリポイント関数が常にブートローダの期待する正しいシグネチャを持っていることを保証するために、`bootloader`クレートは[`entry_point`]マクロによって、Rustの関数を型チェックしたうえでエントリポイントとして定義する方法を提供します。私達のエントリポイント関数をこのマクロを使って書き直してみましょう: [`entry_point`]: https://docs.rs/bootloader/0.6.4/bootloader/macro.entry_point.html @@ -333,7 +331,7 @@ fn kernel_main(boot_info: &'static BootInfo) -> ! { } ``` -このマクロが本物の`_start`エントリポイントをより低レベルに定義してくれるので、`extern "C"`や`no_mangle`をエントリポイントに使う必要はもうありません。`kernel_main`関数は今や完全に普通のRustの関数なので、自由に名前をつけることができます。そして重要なのは、これは型チェックされているので、間違った関数シグネチャ(例えば引数を増やしたり引数の型を変えたり)にするとコンパイルエラーが発生するということです。 +このマクロがより低レベルな本物の`_start`エントリポイントを定義してくれるので、`extern "C"`や`no_mangle`をエントリポイントに使う必要はもうありません。`kernel_main`関数は今や完全に普通のRustの関数なので、自由に名前をつけることができます。そして重要なのは、この関数は型チェックされているので、間違った関数シグネチャ(例えば引数を増やしたり引数の型を変えたり)にするとコンパイルエラーが発生するということです。 `lib.rs`に同じ変更を施しましょう: @@ -370,11 +368,11 @@ fn test_kernel_main(_boot_info: &'static BootInfo) -> ! { pub mod memory; ``` -このモジュールに合わせて`src/memory.rs`ファイルを作ります。 +また、このモジュールに対応するファイル`src/memory.rs`を作ります。 ### ページテーブルにアクセスする -[前の記事の最後][end of the previous post]で、私達のカーネルの実行しているページテーブルを見てみようとしましたが、`CR3`レジスタの指す物理フレームにアクセスすることができなかったためそれはできませんでした。前回の続きとして、`active_level_4_table`という、現在有効 (アクティブ) なレベル4ページテーブルへの参照を返す関数を定義するところから始めましょう: +[前の記事の最後][end of the previous post]で、私達のカーネルの実行しているページテーブルを見てみようとしましたが、`CR3`レジスタの指す物理フレームにアクセスすることができなかったためそれはできませんでした。この続きとして、`active_level_4_table`という、現在有効 (アクティブ) なレベル4ページテーブルへの参照を返す関数を定義するところから始めましょう: [end of the previous post]: @/edition-2/posts/08-paging-introduction/index.ja.md#peziteburuhenoakusesu @@ -443,7 +441,7 @@ fn kernel_main(boot_info: &'static BootInfo) -> ! { } ``` -まず、`BootInfo`構造体の`physical_memory_offset`を[`VirtAddr`]に変換し、`active_level_4_table`関数に渡します。つぎに`iter`関数を使ってページテーブルの全エントリに対してfor文を回し、[`enumerate`]コンビネータをつかってそれぞれの要素にインデックス`i`を追加します。全512エントリを出力すると画面に収まらないので、 (から) でないエントリのみ出力します。 +まず、`BootInfo`構造体の`physical_memory_offset`を[`VirtAddr`]に変換し、`active_level_4_table`関数に渡します。つぎに`iter`関数を使ってページテーブルのエントリをイテレートし、[`enumerate`]コンビネータをつかってそれぞれの要素にインデックス`i`を追加します。全512エントリを出力すると画面に収まらないので、 (から) でないエントリのみ出力します。 [`VirtAddr`]: https://docs.rs/x86_64/0.14.2/x86_64/addr/struct.VirtAddr.html [`enumerate`]: https://doc.rust-lang.org/core/iter/trait.Iterator.html#method.enumerate @@ -517,7 +515,7 @@ pub unsafe fn translate_addr(addr: VirtAddr, physical_memory_offset: VirtAddr) /// Rustはunsafeな関数の全体をunsafeブロックとして扱ってしまうので、 /// unsafeの範囲を絞るためにこの関数はunsafeにしていない。 /// この関数をモジュール外から呼び出すときは、 -/// unsafeな関数を通じてのみ呼び出すこと。 +/// unsafeな関数`translate_addr`を使って呼び出すこと。 fn translate_addr_inner(addr: VirtAddr, physical_memory_offset: VirtAddr) -> Option { @@ -556,7 +554,7 @@ fn translate_addr_inner(addr: VirtAddr, physical_memory_offset: VirtAddr) 先程作った`active_level_4_table`関数を再利用せず、`CR3`レジスタからレベル4フレームを読み出すコードを再び書いています。これは簡単に試作するためであり、後でもっと良い方法で作り直すのでご心配なく。 -`Virtaddr`構造体には、インデクスから4つの階層のページテーブルを計算してくれるメソッドが備わっています。この4つのインデクスを配列に格納することで、これらを`for`ループを使って辿ります。`for`ループを抜けたら、最後に計算した`frame`を覚えているので、物理アドレスを計算できます。この`frame`は、forループの中ではページテーブルのフレームを指していて、最後のループのあと(すなわちレベル1エントリを辿ったあと)では対応する(物理)フレームを指しています。 +`Virtaddr`構造体には、(仮想メモリの)インデクスから4つの階層のページテーブルを計算してくれるメソッドが備わっています。この4つのインデクスを配列に格納することで、これらを`for`ループを使って辿ります。`for`ループを抜けたら、最後に計算した`frame`を覚えているので、物理アドレスを計算できます。この`frame`は、forループの中ではページテーブルのフレームを指していて、最後のループのあと(すなわちレベル1エントリを辿ったあと)では対応する(物理)フレームを指しています。 ループの中の話をします。前と同じように`physical_memory_offset`を使ってフレームをページテーブルの参照に変換します。次に、そのページテーブルのエントリを読み、[`PageTableEntry::frame`]関数を使って対応するフレームを取得します。もしエントリがフレームに対応付けられていなければ`None`を返します。もしエントリが2MiBや1GiBのhuge pageに対応付けられていたら、今のところはpanicすることにします。 @@ -604,7 +602,7 @@ fn kernel_main(boot_info: &'static BootInfo) -> ! { [_page offset_]: @/edition-2/posts/08-paging-introduction/index.ja.md#x86-64niokerupezingu -それぞれの物理アドレスは`physical_memory_offset`を足すことでアクセスできるわけですから、`physical_memory_offset`自体を(仮想アドレスとして)変換すると物理アドレス`0`を指すはずです。しかし、効率よく対応付けを行うためにここではhuge pageが使われており、これはまだサポートしていないので変換には失敗しています。 +それぞれの物理アドレスは`physical_memory_offset`を足すことでアクセスできるわけですから、`physical_memory_offset`自体を変換すると物理アドレス`0`を指すはずです。しかし、効率よく対応付けを行うためにここではhuge pageが使われており、これはまだサポートしていないので変換には失敗しています。 ### `OffsetPageTable`を使う @@ -612,7 +610,7 @@ fn kernel_main(boot_info: &'static BootInfo) -> ! { この抽象化の基礎となっているのは、様々なページテーブル対応付け関数を定義している2つのトレイトです。 -- [`Mapper`]トレイトはページサイズを型引数とする汎用型 (ジェネリクス) です。例えば、[`translate_page`]は与えられたページを同じサイズのフレームに変換し、[`map_to`]はページテーブルに新しい対応付けを作成します。 +- [`Mapper`]トレイトはページサイズを型引数とする汎用型 (ジェネリクス) で、ページに対して操作を行う関数を提供します。例えば、[`translate_page`]は与えられたページを同じサイズのフレームに変換し、[`map_to`]はページテーブルに新しい対応付けを作成します。 - [`Translate`] トレイトは[`translate_addr`]や一般の[`translate`]のような、さまざまなページサイズに対して動くような関数を提供します。 [`Mapper`]: https://docs.rs/x86_64/0.14.2/x86_64/structures/paging/mapper/trait.Mapper.html @@ -622,13 +620,13 @@ fn kernel_main(boot_info: &'static BootInfo) -> ! { [`translate_addr`]: https://docs.rs/x86_64/0.14.2/x86_64/structures/paging/mapper/trait.Translate.html#method.translate_addr [`translate`]: https://docs.rs/x86_64/0.14.2/x86_64/structures/paging/mapper/trait.Translate.html#tymethod.translate -これらのトレイトはインターフェイスを定義しているだけであり、その実装は何一つ提供していません。`x86_64`クレートは現在、このトレイトを実装する型を異なる要件に合わせて3つ用意しています。[`OffsetPageTable`]型は、全物理メモリがあるオフセットで仮想アドレスに対応していることを前提とします。[`MappedPageTable`]はもう少し融通が効き、それぞれのページテーブルフレームが計算可能などこかの仮想アドレスに対応していることだけを前提とします。最後に[`RecursivePageTable`]型は、ページテーブルのフレームに[再帰的ページテーブル](#zai-gui-de-peziteburu)を使ってアクセスするときに使えます。 +これらのトレイトはインターフェイスを定義しているだけであり、その実装は何一つ提供していません。`x86_64`クレートは現在、このトレイトを実装する型を異なる要件に合わせて3つ用意しています。[`OffsetPageTable`]型は、全物理メモリがあるオフセットで仮想アドレスに対応していることを前提とします。[`MappedPageTable`]はもう少し融通が効き、それぞれのページテーブルフレームが(そのフレームから)計算可能な仮想アドレスに対応していることだけを前提とします。最後に[`RecursivePageTable`]型は、ページテーブルのフレームに[再帰的ページテーブル](#zai-gui-de-peziteburu)を使ってアクセスするときに使えます。 [`OffsetPageTable`]: https://docs.rs/x86_64/0.14.2/x86_64/structures/paging/mapper/struct.OffsetPageTable.html [`MappedPageTable`]: https://docs.rs/x86_64/0.14.2/x86_64/structures/paging/mapper/struct.MappedPageTable.html [`RecursivePageTable`]: https://docs.rs/x86_64/0.14.2/x86_64/structures/paging/mapper/struct.RecursivePageTable.html -私達の場合、ブートローダは全物理メモリを`physical_memory_offset`変数で指定された仮想アドレスで物理メモリに対応付けているので、`OffsetPageTable`型を使えます。これを初期化するために、`memory`モジュールに新しく`init`関数を作りましょう: +私達の場合、ブートローダは全物理メモリを`physical_memory_offset`変数で指定された仮想アドレスで物理メモリに対応付けているので、`OffsetPageTable`型が使えます。これを初期化するために、`memory`モジュールに新しく`init`関数を作りましょう: ```rust use x86_64::structures::paging::OffsetPageTable; @@ -656,7 +654,7 @@ unsafe fn active_level_4_table(physical_memory_offset: VirtAddr) [`OffsetPageTable::new`]: https://docs.rs/x86_64/0.14.2/x86_64/structures/paging/mapper/struct.OffsetPageTable.html#method.new -可変参照が複数の名称を持つと未定義動作を起こす可能性があるので、以降`active_level_4_table`関数は`init`関数から一度呼び出されることを除いては呼び出されてはなりません。そのため、`pub`指定子を外してこの関数を非公開にしています。 +可変参照が複数の名称を持つと未定義動作を起こす可能性があるので、今後`active_level_4_table`関数は`init`関数から一度呼び出されることを除いては呼び出されてはなりません。そのため、`pub`指定子を外してこの関数を非公開にしています。 これで、自前の`memory::translate_addr`関数の代わりに`Translate::translate_addr`メソッドを使うことができます。これには`kernel_main`を数行だけ書き換えればよいです: @@ -697,11 +695,11 @@ fn kernel_main(boot_info: &'static BootInfo) -> ! { `MappedPageTable`型の変換関数を使うことで、huge pageをサポートする手間が省けます。また`map_to`のような他のページング関数も利用でき、これは次のセクションで使います。 -この時点で、自作した`memory::translate_addr`関数や`memory::translate_addr_inner`関数はもう必要ではないので、削除して構いません。 +この時点で、自作した`memory::translate_addr`関数や`memory::translate_addr_inner`関数はもう必要ではないので削除して構いません。 ### 新しい対応を作る -これまでページテーブルを見てきましたが修正はしていませんでした。対応のなかったページに対応を作ることで、ページテーブルを修正してみましょう。 +これまでページテーブルを見てきましたが、それに対する変更は行っていませんでした。ページテーブルに対する変更として、対応のなかったページに対応を作ってみましょう。 これを実装するには[`Mapper`]トレイトの[`map_to`]関数を使うので、この関数について少し見てみましょう。ドキュメントによると四つ引数があります:対応に使うページ、ページを対応させるフレーム、ページテーブルエントリにつかうフラグの集合、そして`frame_allocator`です。フレームアロケータ (frame allocator) (フレームを割り当てる (アロケートする) 機能を持つ)が必要な理由は、与えられたページを対応付けるために追加でページテーブルを作成する必要があるかもしれず、これを格納するためには使われていないフレームが必要となるからです。 @@ -741,14 +739,14 @@ pub fn create_example_mapping( } ``` -この関数は、対応付ける`page`に加え`OffsetPageTable`のインスタンスと`frame_allocator`への可変参照を引数に取ります。`frame_allocator`引数は[`impl Trait`][impl-trait-arg]構文により[`FrameAllocator`]トレイトを実装するあらゆる型の[汎用型][generic]になっています。`FrameAllocator`トレイトは[`PageSize`]トレイトを実装するなら(引数のサイズが)4KiBでも2MiBや1GiBのhuge pageでも大丈夫な汎用 (ジェネリック) トレイトです。私達は4KiBの対応付けのみを作りたいので、ジェネリック引数は`Size4KiB`にしています。 +この関数は、対応付ける`page`に加え`OffsetPageTable`のインスタンスと`frame_allocator`への可変参照を引数に取ります。`frame_allocator`引数は[`impl Trait`][impl-trait-arg]構文により[`FrameAllocator`]トレイトを実装するあらゆる型の[汎用型][generic]になっています。`FrameAllocator`トレイトは[`PageSize`]トレイトを実装するなら(トレイト引数のサイズが)4KiBでも2MiBや1GiBのhuge pageでも構わない汎用 (ジェネリック) トレイトです。私達は4KiBの対応付けのみを作りたいので、ジェネリック引数は`Size4KiB`にしています。 [impl-trait-arg]: https://doc.rust-lang.org/book/ch10-02-traits.html#traits-as-parameters [generic]: https://doc.rust-lang.org/book/ch10-00-generics.html [`FrameAllocator`]: https://docs.rs/x86_64/0.14.2/x86_64/structures/paging/trait.FrameAllocator.html [`PageSize`]: https://docs.rs/x86_64/0.14.2/x86_64/structures/paging/page/trait.PageSize.html -[`map_to`]メソッドは、呼び出し元がフレームはまだ使われていないことを保証しないといけないので、unsafeです。なぜなら、同じフレームを二度対応付けると(例えば2つの異なる`&mut`参照が物理メモリの同じ場所を指すことで)未定義動作を起こす可能性があるからです。今回、VGAテキストバッファのフレームという、すでに対応付けられているフレームを再度使っているので、この要件を破ってしまっています。しかしながら、`create_example_mapping`関数は一時的なテスト関数でありこの記事のあとには取り除かれるので、大丈夫です。この危険性 (unsafety) のことを忘れないようにするために、その行に`FIXME` (要修正) コメントをつけておきます。 +[`map_to`]メソッドは、呼び出し元がフレームはまだ使われていないことを保証しないといけないので、unsafeです。なぜなら、同じフレームを二度対応付けると(例えば2つの異なる`&mut`参照が物理メモリの同じ場所を指すことで)未定義動作を起こす可能性があるからです。今回、VGAテキストバッファのフレームという、すでに対応付けられているフレームを再度使っているので、この要件を破ってしまっています。しかしながら、`create_example_mapping`関数は一時的なテスト関数であり、この記事のあとには取り除かれるので大丈夫です。この危険性のことを忘れないようにするために、その行に`FIXME` (`要修正`) コメントをつけておきます。 `map_to`関数が`page`と`unused_frame`に加えてフラグの集合と`frame_allocator`への参照を取りますが、これについてはすぐに説明します。フラグについては、`PRESENT`フラグという有効なエントリ全てに必須のフラグと、`WRITABLE`フラグという対応するページを書き込み可能にするフラグをセットしています。フラグの一覧については、前記事の[ページテーブルの形式][_Page Table Format_]を参照してください。 @@ -781,11 +779,11 @@ unsafe impl FrameAllocator for EmptyFrameAllocator { } ``` -`FrameAllocator`を実装するのはunsafeです。なぜなら、実装する人は、作ったアロケータが未使用のフレームのみ取得することを保証しなければならないからです。さもなくば、例えば二つの仮想ページが同じ物理フレームに対応付けられたときに未定義動作が起こるかもしれません。この`EmptyFrameAllocator`は`None`しか返さないので、これは問題ではありません。 +`FrameAllocator`を実装するのはunsafeです。なぜなら、実装する人は、実装したアロケータが未使用のフレームのみ取得することを保証しなければならないからです。さもなくば、例えば二つの仮想ページが同じ物理フレームに対応付けられたときに未定義動作が起こるかもしれません。この`EmptyFrameAllocator`は`None`しか返さないので、これは問題ではありません。 #### 仮想ページを選ぶ -`create_example_mapping`関数に渡すための単純なフレームアロケータを手に入れました。しかし、このアロケータは常に`None`を返すので、対応を作る際に追加のページテーブルフレームが必要でなかったときにのみうまく行きます。いつ追加のページテーブルフレームが必要でありいつそうでないのかを知るために、例をとって考えてみましょう: +`create_example_mapping`関数に渡すための単純なフレームアロケータを手に入れました。しかし、このアロケータは常に`None`を返すので、対応を作る際に追加のページテーブルフレームが必要でなかったときにのみうまく動作します。いつ追加のページテーブルフレームが必要でありいつそうでないのかを知るために、例をとって考えてみましょう: ![A virtual and a physical address space with a single mapped page and the page tables of all four levels](required-page-frames-example.svg) @@ -794,17 +792,17 @@ unsafe impl FrameAllocator for EmptyFrameAllocator { また、この図はVGAテキストバッファの物理フレームを赤色で示しています。私達の目的は、`create_example_mapping`関数を使ってまだ対応付けられていない仮想ページをこのフレームに対応付けることです。私達の`EmptyFrameAllocator`は常に`None`を返すので、アロケータからフレームを追加する必要がないように対応付けを作りたいです。これができるかは、私達が対応付けにどの仮想ページを使うかに依存します。 -この図の仮想アドレス空間には、2つの候補となるページを黄色で示しています。ページのうち一つはアドレス`0x803fe00000`で、これは(青で示された)対応付けられたページの3つ前です。レベル4と3のテーブルのインデクスは青いページと同じですが、レベル2と1のインデクスは違います([前の記事][page-table-indices]を参照)。レベル2テーブルのインデクスが違うということは、異なるレベル1テーブルが使われることを意味します。そんなレベル1テーブルは存在しないので、もしこちらを使っていたら、使われていない物理フレームを追加(でアロケート)する必要が出てきます。対して、2つ目のアドレス`0x803fe02000`にある候補のページは、青のページと同じレベル1ページテーブルを使うのでこの問題は発生しません。よって、必要となるすべてのページテーブルはすでに存在しています。 +この図の仮想アドレス空間には、2つの候補となるページを黄色で示しています。ページのうち一つはアドレス`0x803fe00000`で、これは(青で示された)対応付けられているページの3つ前です。レベル4と3のテーブルのインデクスは青いページと同じですが、レベル2と1のインデクスは違います([前の記事][page-table-indices]を参照)。レベル2テーブルのインデクスが違うということは、異なるレベル1テーブルが使われることを意味します。そんなレベル1テーブルは存在しないので、もしこちらを使っていたら、使われていない物理フレームを追加(でアロケート)する必要が出てきます。対して、2つ目のアドレス`0x803fe02000`にある候補のページは、青のページと同じレベル1ページテーブルを使うのでこの問題は発生しません。よって、必要となるすべてのページテーブルはすでに存在しています。 [page-table-indices]: @/edition-2/posts/08-paging-introduction/index.ja.md#x86-64niokerupezingu -まとめると、新しい対応を作るときの難易度は、対応付けようとしている仮想ページに依存するということです。作ろうとしているページのレベル1ページテーブルがすでに存在すると最も簡単で、エントリを一つ書き込むだけです。ページがレベル3のテーブルすら存在しない領域にある場合が最も難しく、その場合まずレベル3,2,1のページテーブルを新しく作る必要があります。 +まとめると、新しい対応を作るときの難易度は、対応付けようとしている仮想ページに依存するということです。作ろうとしているページのレベル1ページテーブルがすでに存在すると最も簡単で、エントリをそのページに一つ書き込むだけです。ページがレベル3のテーブルすら存在しない領域にある場合が最も難しく、その場合まずレベル3,2,1のページテーブルを新しく作る必要があります。 `EmptyFrameAllocator`を使って`create_example_mapping`を呼び出すためには、すべての(階層の)ページテーブルがすでに存在しているページを選ぶ必要があります。そんなページを探すにあたっては、ブートローダが自分自身を仮想アドレス空間の最初の1メガバイトに読み込んでいるということを利用できます。つまり、この領域のすべてのページについて、レベル1テーブルがきちんと存在しているということです。したがって、試しに対応を作るときに、このメモリ領域のいずれかの未使用ページ、例えばアドレス`0`を使えばよいです。普通このページは、ヌルポインタの参照外しがページフォルトを引き起こすことを保証するために使用しないので、ブートローダもここを対応させてはいないはずです。 #### 対応を作る -というわけで、`create_example_mapping`関数を呼び出すために必要なすべての引数を手に入れたので、仮想アドレス`0`を対応付けるよう`kernel_main`関数を変更していきましょう。このページはVGAテキストバッファのフレームに対応付けているので、以後、画面に書き込むことができるはずです。実装は以下のようになります: +というわけで、`create_example_mapping`関数を呼び出すために必要なすべての引数を手に入れたので、仮想アドレス`0`を対応付けるよう`kernel_main`関数を変更していきましょう。このページをVGAテキストバッファのフレームに対応付けると、以後、画面に書き込むことができるようになるはずです。実装は以下のようになります: ```rust // in src/main.rs @@ -833,7 +831,7 @@ fn kernel_main(boot_info: &'static BootInfo) -> ! { まず、`mapper`と`frame_allocator`インスタンスの可変参照を渡して`create_example_mapping`を呼ぶことで、アドレス`0`のページに対応を作っています。これはVGAテキストバッファのフレームに対応付けているので、これに書き込んだものは何であれ画面に出てくるはずです。 -次にページを生ポインタに変更して、オフセット`400`に値を書き込みます。このページの最初に書き込むとVGAバッファの一番上の行になり、次のprintlnで即座に画面外に流れていってしまうのでそれはしません。値`0x_f021_f077_f065_f04e`は、白背景の"New!"という文字列を表します。[VGAテキストモードの記事][in the _“VGA Text Mode”_ post]で学んだように、VGAバッファへの書き込みはvolatileでなければならないので、[`write_volatile`]メソッドを使っています。 +次にページを生ポインタに変更して、オフセット`400`に値を書き込みます。このページの最初に書き込むとVGAバッファの一番上の行になり、次のprintlnで即座に画面外に流れていってしまうので、それを避けています。値`0x_f021_f077_f065_f04e`は、白背景の"New!"という文字列を表します。[VGAテキストモードの記事][in the _“VGA Text Mode”_ post]で学んだように、VGAバッファへの書き込みはvolatileでなければならないので、[`write_volatile`]メソッドを使っています。 [in the _“VGA Text Mode”_ post]: @/edition-2/posts/03-vga-text-buffer/index.ja.md#volatile [`write_volatile`]: https://doc.rust-lang.org/std/primitive.pointer.html#method.write_volatile @@ -842,7 +840,7 @@ QEMUで実行すると、以下の出力を得ます: ![QEMU printing "It did not crash!" with four completely white cells in the middle of the screen](qemu-new-mapping.png) -画面の"New!"はページ`0`への書き込みによるものなので、ページテーブルへの新しい対応付けの作成が成功したということを意味します。 +画面の "New!" はページ`0`への書き込みによるものなので、ページテーブルへの新しい対応付けの作成が成功したということを意味します。 この対応付けが成功したのは、アドレス`0`を管轄するレベル1テーブルがすでに存在していたからに過ぎません。レベル1テーブルがまだ存在しないページを対応付けようとすると、`map_to`関数は新しいページテーブルを作るために`EmptyFrameAllocator`からフレームを割り当てようとしてエラーになります。`0`の代わりに`0xdeadbeaf000`を対応付けようとするとそれが発生するのが見られます。 @@ -862,7 +860,7 @@ fn kernel_main(boot_info: &'static BootInfo) -> ! { panicked at 'map_to failed: FrameAllocationFailed', /…/result.rs:999:5 ``` -レベル1テーブルのまだ存在しないページを対応付けるためには、ちゃんとした`FrameAllocator`を作らないといけません。しかし、どのフレームが未使用で、どのフレームが利用可能かはどうすればわかるのでしょう? +レベル1テーブルがまだ存在していないページを対応付けるためには、ちゃんとした`FrameAllocator`を作らないといけません。しかし、どのフレームが未使用で、どのフレームが利用可能かはどうすればわかるのでしょう? ### フレームを割り当てる @@ -898,9 +896,9 @@ impl BootInfoFrameAllocator { この構造体は2つのフィールドを持ちます。ブートローダによって渡されたメモリマップへの`'static`な参照と、アロケータが次に返すべきフレームの番号を覚えておくための`next`フィールドです。 -[_Boot Information_](#boot-information)節で説明したように、このメモリマップはBIOS/UEFIファームウェアから提供されます。これはブートプロセスのごく初期にのみ取得できるので、ブートローダがそのための関数を呼ぶようになっています。メモリマップは`MemoryRegion`構造体のリストからなり、この構造体はそれぞれのメモリ領域の開始アドレス、長さ、型(未使用、予約済み、など)を格納しています。 +[_Boot Information_](#boot-information)節で説明したように、このメモリマップはBIOS/UEFIファームウェアから提供されます。これはブートプロセスのごく初期にのみ取得できますが、ブートローダがそのための関数を既に呼んでくれています。メモリマップは`MemoryRegion`構造体のリストからなり、この構造体はそれぞれのメモリ領域の開始アドレス、長さ、型(未使用か、予約済みかなど)を格納しています。 -`init`関数は`BootInfoFrameAllocator`を与えられたメモリマップで初期化します。`next`フィールドは`0`で初期化し、フレームを割当てるたびに値を増やすことで同じフレームを二度返すことを防ぎます。メモリマップのusable (使用可能) なフレームが他の誰かに使われたりしていないかは知ることができないので、この`init`関数はそれを呼び出し元に追加で保証させるために`unsafe`でないといけません。 +`init`関数は`BootInfoFrameAllocator`を与えられたメモリマップで初期化します。`next`フィールドは`0`で初期化し、フレームを割当てるたびに値を増やすことで同じフレームを二度返すことを防ぎます。メモリマップのusable (使用可能) とされているフレームが他のどこかで使われたりしていないかは知ることができないので、この`init`関数はそれを呼び出し元に追加で保証させるために`unsafe`でないといけません。 #### `usable_frames`メソッド @@ -932,7 +930,7 @@ impl BootInfoFrameAllocator { この関数はイテレータのコンビネータメソッドを使って、最初に与えられる`MemoryMap`を使用可能な物理フレームのイテレータに変換します: - まず`iter`メソッドを使ってメモリマップを[`MemoryRegion`]のイテレータに変える。 -- 次に[`filter`]メソッドを使って、予約済みなどの理由で使用不可能な領域を飛ばすようにする。ブートローダは作った対応付けに使ったメモリマップはきちんと更新するので、私達のカーネル(コード、データ、スタック)に使われているフレームやブート情報を格納するのに使われているフレームはすでに`InUse` (使用中) などでマークされています。そのため`Usable`なフレームは他の場所では使われていないはずとわかります。 +- 次に[`filter`]メソッドを使って、予約済みなどの理由で使用不可能な領域を飛ばすようにする。ブートローダは作った対応付けに使ったメモリマップはきちんと更新するので、私達のカーネル(コード、データ、スタック)に使われているフレームやブート情報を格納するのに使われているフレームはすでに`InUse` (`使用中`) などでマークされています。そのため`Usable`なフレームは他の場所では使われていないはずとわかります。 - つぎに、[`map`]コンビネータとRustの[range構文][range syntax]を使って、メモリ領域のイテレータからアドレス範囲のイテレータへと変換する。 - つぎに、アドレス範囲から[`step_by`]で4096個ごとにアドレスを選び、[`flat_map`]を使うことでフレームの最初のアドレスのイテレータを得る。4096バイト(=4KiB)はページのサイズに等しいので、それぞれのフレームの開始地点のアドレスが得られます。ブートローダのページは使用可能なメモリ領域をすべてアラインするので、ここで改めてアラインや丸めを行う必要はありません。`map`ではなく[`flat_map`]を使うことで、`Iterator>`ではなく`Iterator`を得ています。 - 最後に、開始アドレスの型を`PhysFrame`に変更することで`Iterator`を得ている。 @@ -944,7 +942,7 @@ impl BootInfoFrameAllocator { [`step_by`]: https://doc.rust-lang.org/core/iter/trait.Iterator.html#method.step_by [`flat_map`]: https://doc.rust-lang.org/core/iter/trait.Iterator.html#method.flat_map -この関数の戻り型は[`impl Trait`]機能を用いています。こうすると、`PhysFrame`をitemの型として持つような[`Iterator`]トレイトを実装する何らかの型を返すのだと指定できます。これは重要です――なぜなら、戻り値の型は名前のつけられないクロージャ型に依存し、具体的な名前をつけるのが**不可能だ**からです。 +この関数の戻り型は[`impl Trait`]機能を用いています。こうすると、`PhysFrame`をitemの型として持つような[`Iterator`]トレイトを実装する何らかの型を返すのだと指定できます。これは重要です――なぜなら、戻り値の型は名前のつけられないクロージャ型に依存し、**具体的な名前をつけるのが不可能**だからです。 [`impl Trait`]: https://doc.rust-lang.org/book/ch10-02-traits.html#returning-types-that-implement-traits [`Iterator`]: https://doc.rust-lang.org/core/iter/trait.Iterator.html @@ -969,7 +967,7 @@ unsafe impl FrameAllocator for BootInfoFrameAllocator { [`Iterator::nth`]: https://doc.rust-lang.org/core/iter/trait.Iterator.html#method.nth -この実装は割当てを行うごとに`usable_frames`アロケータを作り直しているので、あまり最適ではありません。イテレータを構造体のフィールドとして直接格納するほうが良いでしょう。すると`nth`メソッドを使う必要はなくなり、割り当てのたびに[`next`]を使えばいいだけです。このアプローチの問題は、今の所構造体のフィールドに`impl Trait`型(の変数)を格納することができないことです。いつの日か、[named existential type][_named existential types_]が完全に実装されたときにはこれが可能になるかもしれません。 +この実装は割当てを行うごとに`usable_frames`アロケータを作り直しているので、最適とは言い難いです。イテレータを構造体のフィールドとして直接格納するほうが良いでしょう。すると`nth`メソッドを使う必要はなくなり、割り当てのたびに[`next`]を使えばいいだけです。このアプローチの問題は、今の所構造体のフィールドに`impl Trait`型(の変数)を格納することができないことです。いつの日か、[named existential type][_named existential types_]が完全に実装されたときにはこれが可能になるかもしれません。 [`next`]: https://doc.rust-lang.org/core/iter/trait.Iterator.html#tymethod.next [_named existential types_]: https://github.com/rust-lang/rfcs/pull/2071 @@ -993,8 +991,8 @@ fn kernel_main(boot_info: &'static BootInfo) -> ! { ブート情報を使うフレームアロケータのおかげで対応付けは成功し、白背景に黒文字の"New!"が再び画面に現れました。舞台裏では、`map_to`メソッドが不足しているページテーブルを以下のやり方で作っています: -- 渡された`frame_allocator`を使って未使用のフレームを割り当てる。 -- フレームをゼロで埋め、新しい空のページテーブルを作る。 +- 渡された`frame_allocator`を使って未使用のフレームを割り当ててもらう。 +- フレームをゼロで埋めることで、新しい空のページテーブルを作る。 - 上位のテーブルのエントリをそのフレームに対応付ける。 - 次の層で同じことを続ける。 @@ -1004,15 +1002,15 @@ fn kernel_main(boot_info: &'static BootInfo) -> ! { ## まとめ -この記事ではページテーブルのある物理フレームにアクセスするための様々なテクニックを学びました。恒等対応、物理メモリ全体の対応付け、一時的な対応、再帰的ページテーブルなどです。このうち、シンプルでポータブル(アーキテクチャ非依存という意味)で強力な、物理メモリ全体の対応付けを選びました。 +この記事ではページテーブルのある物理フレームにアクセスするための様々なテクニックを学びました。恒等対応、物理メモリ全体の対応付け、一時的な対応、再帰的ページテーブルなどです。このうち、シンプルでポータブル (アーキテクチャ非依存) で強力な、物理メモリ全体の対応付けを選びました。 ページテーブルにアクセスできなければ物理メモリを対応付けられないので、ブートローダの補助が必要でした。`bootloader`クレートはcargoのfeaturesというオプションを通じて、必要となる対応付けの作成をサポートしています。さらに、必要となる情報をエントリポイント関数の`&BootInfo`引数という形で私達のカーネルに渡してくれます。 -実装について。最初は手作業でページテーブルを辿ることで変換関数を実装し、そのあとで`x86_64`クレートの`MappedPageTable`型を使いました。また、ページテーブルに新しい対応を作る方法や、そのために必要な`FrameAllocator`をブートローダに渡されたメモリマップをラップすることで作る方法を学びました。 +実装について。最初はページテーブルを辿る変換関数を自分の手で実装し、そのあとで`x86_64`クレートの`MappedPageTable`型を使いました。また、ページテーブルに新しい対応を作る方法や、そのために必要な`FrameAllocator`をブートローダに渡されたメモリマップをラップすることで作る方法を学びました。 ## 次は? -次の記事では、私達のカーネルのためのヒープメモリ領域を作り、それによって[メモリの割り当て][allocate memory]や各種の[コレクション型][collection types]を使うことが可能になります。 +次の記事では、私達のカーネルのためのヒープメモリ領域を作り、それによって[メモリの割り当て][allocate memory]を行ったり各種の[コレクション型][collection types]を使うことが可能になります。 [allocate memory]: https://doc.rust-lang.org/alloc/boxed/struct.Box.html [collection types]: https://doc.rust-lang.org/alloc/collections/index.html From 0fe6d6aaec5af96ffa7a98725abd4ebf72cb10ff Mon Sep 17 00:00:00 2001 From: "Shu W. Nakamura" <30687489+woodyZootopia@users.noreply.github.com> Date: Sat, 11 Sep 2021 16:43:57 +0900 Subject: [PATCH 08/11] Apply suggestions from code review and Add garasubo as a co-translator Co-authored-by: garasubo --- .../posts/09-paging-implementation/index.ja.md | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/blog/content/edition-2/posts/09-paging-implementation/index.ja.md b/blog/content/edition-2/posts/09-paging-implementation/index.ja.md index 791a8f4f..3c2dd76f 100644 --- a/blog/content/edition-2/posts/09-paging-implementation/index.ja.md +++ b/blog/content/edition-2/posts/09-paging-implementation/index.ja.md @@ -7,7 +7,7 @@ date = 2019-03-14 [extra] chapter = "Memory Management" translation_based_on_commit = "27ab4518acbb132e327ed4f4f0508393e9d4d684" -translators = ["woodyZootopia"] +translators = ["woodyZootopia", "garasubo"] +++ この記事では私達のカーネルをページングに対応させる方法についてお伝えします。まずページテーブルの物理フレームにカーネルがアクセスできるようにする様々な方法を示し、それらの利点と欠点について議論します。次にアドレス変換関数を、ついで新しい対応付けを作るための関数を実装します。 @@ -24,7 +24,7 @@ translators = ["woodyZootopia"] ## 導入 -[1つ前の記事][previous post]ではページングの概念を説明しました。セグメンテーションと比較することによってページングのメリットを示し、ページングとページテーブルの仕組みを説明し、そして`x86_64`における4層ページテーブルの設計を導入しました。ブートローダはすでにページテーブルの階層構造を設定してしまっているので、私達のカーネルは既に仮想アドレス上で動いているということを学びました。これにより、不正なメモリアクセスは、任意の物理メモリを書き換えてしまう代わりにページフォルト例外を発生させるので、安全性が向上しています。 +[1つ前の記事][previous post]ではページングの概念を説明しました。セグメンテーションと比較することによってページングのメリットを示し、ページングとページテーブルの仕組みを説明し、そして`x86_64`における4層ページテーブルの設計を導入しました。ブートローダはすでにページテーブルの階層構造を設定してしまっているので、私達のカーネルは既に仮想アドレス上で動いているということを学びました。これにより、不正なメモリアクセスは、任意の物理メモリを書き換えてしまうのではなくページフォルト例外を発生させるので、安全性が向上しています。 [previous post]: @/edition-2/posts/08-paging-introduction/index.ja.md @@ -59,7 +59,7 @@ translators = ["woodyZootopia"] [memory-mapping a file]: https://en.wikipedia.org/wiki/Memory-mapped_file [segmentation]: @/edition-2/posts/08-paging-introduction/index.ja.md#duan-pian-hua-fragmentation -同様に、新しいページテーブルを作ることもずっと難しくなります。なぜなら、対応するページがまだ使われていない物理フレームを見つけないといけないからです。例えば、メモリマップト (に対応づけられた) ファイルのために`1008KiB`から1000KiBにわたって仮想メモリを占有したとしましょう。すると、すると、物理アドレス`1000KiB`から`2008KiB`までのフレームは、もう恒等対応を作ることができないので使用することができません。 +同様に、新しいページテーブルを作ることもずっと難しくなります。なぜなら、対応するページがまだ使われていない物理フレームを見つけないといけないからです。例えば、メモリマップト (に対応づけられた) ファイルのために`1008KiB`から1000KiBにわたって仮想メモリを占有したとしましょう。すると、物理アドレス`1000KiB`から`2008KiB`までのフレームは、もう恒等対応を作ることができないので使用することができません。 ### 固定オフセットの対応づけ @@ -121,7 +121,7 @@ translators = ["woodyZootopia"] [example at the beginning of this post]: #peziteburuniakusesusuru -CPUにこのエントリを辿らせるようにすると、レベル3テーブルではなく、そのレベル4テーブルに再び到達します。これは再帰関数(自らを呼び出す関数)に似ているので、**再帰的ページテーブル** (recursive page table) と呼ばれます。CPUはレベル4テーブルのすべてのエントリはレベル3テーブルを指していると思っているので、CPUはいまレベル4テーブルをレベル3テーブルとして扱っているということに注目してください。これがうまく行くのは、x86_64においてはすべてのレベルのテーブルが全く同じレイアウトを持っているためです。 +CPUにこのエントリを辿らせるようにすると、レベル3テーブルではなく、そのレベル4テーブルに再び到達します。これは再帰関数(自らを呼び出す関数)に似ているので、**再帰的 (recursive) ページテーブル**と呼ばれます。CPUはレベル4テーブルのすべてのエントリはレベル3テーブルを指していると思っているので、CPUはいまレベル4テーブルをレベル3テーブルとして扱っているということに注目してください。これがうまく行くのは、x86_64においてはすべてのレベルのテーブルが全く同じレイアウトを持っているためです。 実際に変換を始める前に、この再帰エントリを1回以上たどることで、CPUのたどる階層の数を短くできます。例えば、一度再帰エントリを辿ったあとでレベル3テーブルに進むと、CPUはレベル3テーブルをレベル2テーブルだと思い込みます。同様に、レベル2テーブルをレベル1テーブルだと、レベル1テーブルを対応付けられた(物理)フレームだと思います。CPUがこれを物理フレームだと思っているということは、レベル1ページテーブルを読み書きできるということを意味します。下の図はこの5回の変換ステップを示しています: @@ -217,7 +217,7 @@ let level_1_table_addr = sign | (r << 39) | (l4_idx << 30) | (l3_idx << 21) | (l2_idx << 12); ``` -上のコードは、レベル4エントリの最後(インデクス`0o777`すなわち511)が再帰対応していると仮定しています。この対応はまだ行っていないので、この仮定は正しくありません。以下でブートローダに再帰対応付けを設定させる方法を説明します。 +上のコードは、レベル4エントリの最後(インデクス`0o777`すなわち511)が再帰対応していると仮定しています。この仮定は正しくないので,このコードは動作しません。ブートローダに再帰対応付けを設定させる方法については後述します。 ビット演算を自前で行う代わりに、`x86_64`クレートの[`RecursivePageTable`]型を使うこともできます。これは様々なページ操作の安全な抽象化を提供します。例えば、以下のコードは仮想アドレスを対応付けられた物理アドレスに変換する方法を示しています。 @@ -248,7 +248,7 @@ let frame = recursive_page_table.translate_page(page); frame.map(|frame| frame.start_address() + u64::from(addr.page_offset())) ``` -繰り返しになりますが、このコード(が正しく実行される)には正しい再帰対応がなされていることが必要となります。この対応付けがあるなら、空欄になっている`level_4_table_addr`を最初のコード例と同じ値にすればよいです。 +繰り返しになりますが、このコード(が正しく実行される)には正しい再帰対応がなされていることが必要となります。この対応付けがあるのなら、空欄になっている`level_4_table_addr`は最初のコード例を使って計算すればよいです。
@@ -275,7 +275,7 @@ frame.map(|frame| frame.start_address() + u64::from(addr.page_offset())) - `map_physical_memory` featureを使うと、全物理メモリを仮想アドレス空間のどこかに対応付けます。そのため、カーネルはすべての物理メモリにアクセスでき、[上で述べた方法に従って物理メモリ全体を対応付ける](#wu-li-memoriquan-ti-wodui-ying-fu-keru)ことができます。 - `recursive_page_table` featureでは、ブートローダはレベル4ページテーブルのエントリを再帰的に対応付けます。これによりカーネルは[再帰的ページテーブル](#zai-gui-de-peziteburu)で述べた方法に従ってページテーブルにアクセスすることができます。 -私達のカーネルには、シンプルでプラットフォーム非依存でより強力である(ページテーブルのフレームでないメモリにもアクセスできるので)1つ目の方法を採ることにします。必要なブートローダの機能 (feature) を有効化するために、`map_physical_memory` featureを`bootloader`のdependencyに追加します。 +私達のカーネルには、シンプルでプラットフォーム非依存かつ(ページテーブルのフレームでないメモリにもアクセスできるので)より強力である1つ目の方法を採ることにします。必要なブートローダの機能 (feature) を有効化するために、`map_physical_memory` featureを`bootloader`のdependencyに追加します。 ```toml @@ -313,7 +313,7 @@ pub extern "C" fn _start(boot_info: &'static BootInfo) -> ! { // 新しい引数 ### `entry_point`マクロ -私達の`_start`関数はブートローダから外部呼び出しされるので、私達の関数のシグネチャに対する検査は行われません。これにより、この関数はコンパイルエラーなしにあらゆる引数を取ることができるので、いざ実行時にエラーになったり未定義動作を起こしてしまいます。 +私達の`_start`関数はブートローダから外部呼び出しされるので、私達の関数のシグネチャに対する検査は行われません。これにより、この関数はコンパイルエラーなしにあらゆる引数を取ることができるので、いざ実行時にエラーになったり未定義動作を起こしたりしてしまいます。 私達のエントリポイント関数が常にブートローダの期待する正しいシグネチャを持っていることを保証するために、`bootloader`クレートは[`entry_point`]マクロによって、Rustの関数を型チェックしたうえでエントリポイントとして定義する方法を提供します。私達のエントリポイント関数をこのマクロを使って書き直してみましょう: From 4472eed23c64c8680b347ad1deb7fd8ef617fb69 Mon Sep 17 00:00:00 2001 From: "Shu W. Nakamura" <30687489+woodyZootopia@users.noreply.github.com> Date: Tue, 14 Sep 2021 11:19:03 +0900 Subject: [PATCH 09/11] Apply suggestions from code review Co-authored-by: garasubo --- .../edition-2/posts/09-paging-implementation/index.ja.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/blog/content/edition-2/posts/09-paging-implementation/index.ja.md b/blog/content/edition-2/posts/09-paging-implementation/index.ja.md index 3c2dd76f..e7e82386 100644 --- a/blog/content/edition-2/posts/09-paging-implementation/index.ja.md +++ b/blog/content/edition-2/posts/09-paging-implementation/index.ja.md @@ -268,7 +268,7 @@ frame.map(|frame| frame.start_address() + u64::from(addr.page_offset())) これらのアプローチはすべて、準備のためにページテーブルに対する修正が必要になります。例えば、物理メモリへの対応付けを作ったり、レベル4テーブルのエントリを再帰的に対応付けたりなどです。問題は、これらの必要な対応付けを作るためには、すでにページテーブルにアクセスできるようになっていなければいけないということです。 -これが意味するのは、私達のカーネルが使うページテーブルを作っている、ブートローダの手助けが必要になるということです。ブートローダはページテーブルにアクセスできますから、私達の必要とするどんな対応付けも作れます。`bootloader`クレートは上の2つのアプローチをどちらもサポートしており、現在の実装においては[cargoのfeatures][cargo features]を使ってこれらをコントロールします。 +つまり、私達のカーネルが使うページテーブルを作っている、ブートローダの手助けが必要になるということです。ブートローダはページテーブルにアクセスできますから、私達の必要とするどんな対応付けも作れます。`bootloader`クレートは上の2つのアプローチをどちらもサポートしており、現在の実装においては[cargoのfeatures][cargo features]を使ってこれらをコントロールします。 [cargo features]: https://doc.rust-lang.org/cargo/reference/features.html#the-features-section @@ -479,7 +479,7 @@ if !entry.is_unused() { レベル2やレベル1のテーブルも、同じ手続きをレベル3とレベル2のエントリに対して繰り返すことで見ることができます。お察しの通りそれを書くとかなり長くなるので、コードの全てはここには示しません。 -ページテーブルを手作業で辿ると、CPUが変換を行う様子を理解できて面白いです。しかし、多くの場合は与えられた仮想アドレスに対応する物理アドレスにのみ興味があるので、そのための関数を作りましょう。 +ページテーブルを手作業で辿ると、CPUが変換を行う仕組みを理解できて面白いです。しかし、多くの場合は与えられた仮想アドレスに対応する物理アドレスにのみ興味があるので、そのための関数を作りましょう。 ### アドレスの変換 @@ -556,7 +556,7 @@ fn translate_addr_inner(addr: VirtAddr, physical_memory_offset: VirtAddr) `Virtaddr`構造体には、(仮想メモリの)インデクスから4つの階層のページテーブルを計算してくれるメソッドが備わっています。この4つのインデクスを配列に格納することで、これらを`for`ループを使って辿ります。`for`ループを抜けたら、最後に計算した`frame`を覚えているので、物理アドレスを計算できます。この`frame`は、forループの中ではページテーブルのフレームを指していて、最後のループのあと(すなわちレベル1エントリを辿ったあと)では対応する(物理)フレームを指しています。 -ループの中の話をします。前と同じように`physical_memory_offset`を使ってフレームをページテーブルの参照に変換します。次に、そのページテーブルのエントリを読み、[`PageTableEntry::frame`]関数を使って対応するフレームを取得します。もしエントリがフレームに対応付けられていなければ`None`を返します。もしエントリが2MiBや1GiBのhuge pageに対応付けられていたら、今のところはpanicすることにします。 +ループの中では、前と同じように`physical_memory_offset`を使ってフレームをページテーブルの参照に変換します。次に、そのページテーブルのエントリを読み、[`PageTableEntry::frame`]関数を使って対応するフレームを取得します。もしエントリがフレームに対応付けられていなければ`None`を返します。もしエントリが2MiBや1GiBのhuge pageに対応付けられていたら、今のところはpanicすることにします。 [`PageTableEntry::frame`]: https://docs.rs/x86_64/0.14.2/x86_64/structures/paging/page_table/struct.PageTableEntry.html#method.frame @@ -598,7 +598,7 @@ fn kernel_main(boot_info: &'static BootInfo) -> ! { ![0xb8000 -> 0xb8000, 0x201008 -> 0x401008, 0x10000201a10 -> 0x279a10, "panicked at 'huge pages not supported'](qemu-translate-addr.png) -期待したとおり、恒等対応しているアドレス`0xb8000`は同じ物理アドレスに変換されました。コードページとスタックページは物理アドレスのどこかしかに変換されていますが、その場所はブートローダが私達のカーネルのために最初に作った対応づけに依存します。また、下から12ビットは変換のあとも常に同じであるということも注目に値します:この部分は[ページオフセット][_page offset_]であり、変換には関わらないためです。 +期待したとおり、恒等対応しているアドレス`0xb8000`は同じ物理アドレスに変換されました。コードページとスタックページは物理アドレスのどこかしかに変換されていますが、その場所はブートローダがカーネルの初期対応づけをどのようにつくったかによります。また、下から12ビットは変換のあとも常に同じであるということも注目に値します:この部分は[ページオフセット][_page offset_]であり、変換には関わらないためです。 [_page offset_]: @/edition-2/posts/08-paging-introduction/index.ja.md#x86-64niokerupezingu From f57bded691593de26e1b0bdb8fd3e31346edc22e Mon Sep 17 00:00:00 2001 From: woodyZootopia Date: Thu, 7 Oct 2021 22:41:09 +0900 Subject: [PATCH 10/11] Fix for the advice given by @garasubo --- .../09-paging-implementation/index.ja.md | 216 +++++++++--------- 1 file changed, 108 insertions(+), 108 deletions(-) diff --git a/blog/content/edition-2/posts/09-paging-implementation/index.ja.md b/blog/content/edition-2/posts/09-paging-implementation/index.ja.md index e7e82386..fad6283c 100644 --- a/blog/content/edition-2/posts/09-paging-implementation/index.ja.md +++ b/blog/content/edition-2/posts/09-paging-implementation/index.ja.md @@ -10,7 +10,7 @@ translation_based_on_commit = "27ab4518acbb132e327ed4f4f0508393e9d4d684" translators = ["woodyZootopia", "garasubo"] +++ -この記事では私達のカーネルをページングに対応させる方法についてお伝えします。まずページテーブルの物理フレームにカーネルがアクセスできるようにする様々な方法を示し、それらの利点と欠点について議論します。次にアドレス変換関数を、ついで新しい対応付けを作るための関数を実装します。 +この記事では私達のカーネルをページングに対応 (マップ) させる方法についてお伝えします。まずページテーブルの物理フレームにカーネルがアクセスできるようにする様々な方法を示し、それらの利点と欠点について議論します。次にアドレス変換関数を、ついで新しい対応付け (マッピング) を作るための関数を実装します。 @@ -32,7 +32,7 @@ translators = ["woodyZootopia", "garasubo"] [end of previous post]: @/edition-2/posts/08-paging-introduction/index.ja.md#peziteburuhenoakusesu -この方法を実装するには、ブートローダーからの補助が必要になるので、まずこれに設定を加えます。その後で、ページテーブルの階層構造を移動して、仮想アドレスを物理アドレスに変換する関数を実装します。最後に、ページテーブルに新しい対応関係を作る方法と、それを作るための未使用メモリを見つける方法を学びます。 +この方法を実装するには、ブートローダーからの補助が必要になるので、まずこれに設定を加えます。その後で、ページテーブルの階層構造を移動して、仮想アドレスを物理アドレスに変換する関数を実装します。最後に、ページテーブルに新しいマッピングを作る方法と、それを作るための未使用メモリを見つける方法を学びます。 ## ページテーブルにアクセスする @@ -42,88 +42,88 @@ translators = ["woodyZootopia", "garasubo"] ここで重要なのは、それぞれのページテーブルのエントリは次のテーブルの**物理**アドレスであるということです。これにより、それらのアドレスに対しては変換せずにすみます。もしこの変換が行われたとしたら、性能的にも良くないですし、容易に変換の無限ループに陥りかねません。 -問題は、私達のカーネル自体も仮想アドレスの上で動いているため、カーネルから直接物理アドレスにアクセスすることができないということです。例えば、アドレス`4KiB`にアクセスしたとき、私達は**仮想**アドレス`4KiB`にアクセスしているのであって、レベル4ページテーブルが格納されている**物理**アドレス`4KiB`にアクセスしているのではありません。物理アドレス`4KiB`にアクセスしたいなら、それに対応づけられている何らかの仮想アドレスを通じてのみ可能です。 +問題は、私達のカーネル自体も仮想アドレスの上で動いているため、カーネルから直接物理アドレスにアクセスすることができないということです。例えば、アドレス`4KiB`にアクセスしたとき、私達は**仮想**アドレス`4KiB`にアクセスしているのであって、レベル4ページテーブルが格納されている**物理**アドレス`4KiB`にアクセスしているのではありません。物理アドレス`4KiB`にアクセスしたいなら、それにマップさせられている何らかの仮想アドレスを通じてのみ可能です。 -そのため、ページテーブルのフレームにアクセスするためには、どこかの仮想ページをそれに対応づけなければいけません。このような、任意のページテーブルのフレームにアクセスできるようにしてくれる対応付けを作る方法にはいくつかあります。 +そのため、ページテーブルのフレームにアクセスするためには、どこかの仮想ページをそれにマッピングしなければいけません。このような、任意のページテーブルのフレームにアクセスできるようにしてくれるマッピングを作る方法にはいくつかあります。 -### 恒等対応 +### 恒等マッピング -シンプルな方法として、**すべてのページテーブルを恒等対応させる**ということが考えられるでしょう: +シンプルな方法として、**すべてのページテーブルを恒等対応 (マップ) させる**ということが考えられるでしょう: ![A virtual and a physical address space with various virtual pages mapped to the physical frame with the same address](identity-mapped-page-tables.svg) -この例では、いくつかの恒等対応したページテーブルのフレームが見てとれます。こうすることで、ページテーブルの物理アドレスは仮想アドレスと同じ値になり、よってCR3レジスタから始めることで全ての階層のページテーブルに簡単にアクセスできます。 +この例では、恒等マップしたいくつかのページテーブルのフレームが見てとれます。こうすることで、ページテーブルの物理アドレスは仮想アドレスと同じ値になり、よってCR3レジスタから始めることで全ての階層のページテーブルに簡単にアクセスできます。 -しかし、この方法では仮想アドレス空間が散らかってしまい、大きいサイズの連続したメモリを見つけることが難しくなります。例えば、上の図において、[ファイルをメモリにマップする][memory-mapping a file]ために1000KiBの大きさの仮想メモリ領域を作りたいとします。`28KiB`を始点として領域を作ろうとすると、`1004KiB`のところで既存のページと衝突してしまうのでうまくいきません。そのため、`1008KiB`のような、十分な広さで対応付けのない領域が見つかるまで更に探さないといけません。これは[セグメンテーション][segmentation]の時に見た断片化の問題に似ています。 +しかし、この方法では仮想アドレス空間が散らかってしまい、大きいサイズの連続したメモリを見つけることが難しくなります。例えば、上の図において、[ファイルをメモリにマップする][memory-mapping a file]ために1000KiBの大きさの仮想メモリ領域を作りたいとします。`28KiB`を始点として領域を作ろうとすると、`1004KiB`のところで既存のページと衝突してしまうのでうまくいきません。そのため、`1008KiB`のような、十分な広さでマッピングのない領域が見つかるまで更に探さないといけません。これは[セグメンテーション][segmentation]の時に見た断片化の問題に似ています。 [memory-mapping a file]: https://en.wikipedia.org/wiki/Memory-mapped_file [segmentation]: @/edition-2/posts/08-paging-introduction/index.ja.md#duan-pian-hua-fragmentation -同様に、新しいページテーブルを作ることもずっと難しくなります。なぜなら、対応するページがまだ使われていない物理フレームを見つけないといけないからです。例えば、メモリマップト (に対応づけられた) ファイルのために`1008KiB`から1000KiBにわたって仮想メモリを占有したとしましょう。すると、物理アドレス`1000KiB`から`2008KiB`までのフレームは、もう恒等対応を作ることができないので使用することができません。 +同様に、新しいページテーブルを作ることもずっと難しくなります。なぜなら、対応するページがまだ使われていない物理フレームを見つけないといけないからです。例えば、メモリマップト (に対応づけられた) ファイルのために`1008KiB`から1000KiBにわたって仮想メモリを占有したとしましょう。すると、物理アドレス`1000KiB`から`2008KiB`までのフレームは、もう恒等マッピングを作ることができないので使用することができません。 -### 固定オフセットの対応づけ +### 固定オフセットのマッピング -仮想アドレス空間を散らかしてしまうという問題を回避するために、**ページテーブルの対応づけのために別のメモリ領域を使う**ことができます。ページテーブルを恒等対応させる代わりに、仮想アドレス空間で一定の補正値 (オフセット) をおいて対応づけてみましょう。例えば、オフセットを10TiBにしてみましょう: +仮想アドレス空間を散らかしてしまうという問題を回避するために、**ページテーブルのマッピングのために別のメモリ領域を使う**ことができます。ページテーブルを恒等マップさせる代わりに、仮想アドレス空間で一定の補正値 (オフセット) をおいてマッピングしてみましょう。例えば、オフセットを10TiBにしてみましょう: ![The same figure as for the identity mapping, but each mapped virtual page is offset by 10 TiB.](page-tables-mapped-at-offset.svg) -`10TiB`から`10TiB+物理メモリ全体の大きさ`の範囲の仮想メモリをページテーブルの対応付け専用に使うことで、恒等対応のときに存在していた衝突問題を回避しています。このように巨大な領域を仮想アドレス空間内に用意するのは、仮想アドレス空間が物理メモリの大きさより遥かに大きい場合にのみ可能です。x86_64で用いられている48bit(仮想)アドレス空間は256TiBもの大きさがあるので、これは問題ではありません。 +`10TiB`から`10TiB+物理メモリ全体の大きさ`の範囲の仮想メモリをページテーブルのマッピング専用に使うことで、恒等マップのときに存在していた衝突問題を回避しています。このように巨大な領域を仮想アドレス空間内に用意するのは、仮想アドレス空間が物理メモリの大きさより遥かに大きい場合にのみ可能です。x86_64で用いられている48bit(仮想)アドレス空間は256TiBもの大きさがあるので、これは問題ではありません。 -この方法では、新しいページテーブルを作るたびに新しい対応付けを作る必要があるという欠点があります。また、他のアドレス空間のページテーブルにアクセスすることができると新しいプロセスを作るときに便利なのですが、これも不可能です。 +この方法では、新しいページテーブルを作るたびに新しいマッピングを作る必要があるという欠点があります。また、他のアドレス空間のページテーブルにアクセスすることができると新しいプロセスを作るときに便利なのですが、これも不可能です。 -### 物理メモリ全体を対応付ける +### 物理メモリ全体をマップする -これらの問題はページテーブルのフレームだけと言わず**物理メモリ全体を対応付け**てしまえば解決します: +これらの問題はページテーブルのフレームだけと言わず**物理メモリ全体をマップして**しまえば解決します: ![The same figure as for the offset mapping, but every physical frame has a mapping (at 10TiB + X) instead of only page table frames.](map-complete-physical-memory.svg) -この方法を使えば、私達のカーネルは他のアドレス空間を含め任意の物理メモリにアクセスできます。用意する仮想メモリの範囲は以前と同じであり、違うのは全てのページが対応付けられているということです。 +この方法を使えば、私達のカーネルは他のアドレス空間を含め任意の物理メモリにアクセスできます。用意する仮想メモリの範囲は以前と同じであり、違うのは全てのページがマッピングされているということです。 -この方法の欠点は、物理メモリへの対応付けを格納するために、追加でページテーブルが必要になるところです。これらのページテーブルもどこかに格納されなければならず、したがって物理メモリの一部を占有することになります。これはメモリの量が少ないデバイスにおいては問題となりえます。 +この方法の欠点は、物理メモリへのマッピングを格納するために、追加でページテーブルが必要になるところです。これらのページテーブルもどこかに格納されなければならず、したがって物理メモリの一部を占有することになります。これはメモリの量が少ないデバイスにおいては問題となりえます。 -しかし、x86_64においては、通常の4KiBサイズのページに代わって、大きさ2MiBの[huge page][huge pages]を対応付けに使うことができます。こうすれば、例えば32GiBの物理メモリを対応付けるのにはレベル3テーブル1個とレベル2テーブル32個があればいいので、たったの132KiBしか必要ではありません。huge pagesは、トランスレーション・ルックアサイド・バッファ (TLB) のエントリをあまり使わないので、キャッシュ的にも効率が良いです。 +しかし、x86_64においては、通常の4KiBサイズのページに代わって、大きさ2MiBの[huge page][huge pages]をマッピングに使うことができます。こうすれば、例えば32GiBの物理メモリをマップするのにはレベル3テーブル1個とレベル2テーブル32個があればいいので、たったの132KiBしか必要ではありません。huge pagesは、トランスレーション・ルックアサイド・バッファ (TLB) のエントリをあまり使わないので、キャッシュ的にも効率が良いです。 [huge pages]: https://en.wikipedia.org/wiki/Page_%28computer_memory%29#Multiple_page_sizes -### 一時的な対応関係 +### 一時的な対応 (マッピング) -物理メモリの量が非常に限られたデバイスについては、アクセスする必要があるときだけ**ページテーブルのフレームを一時的に対応づける**という方法が考えられます。そのような一時的な対応を作りたいときには、たった一つだけ恒等対応させられたレベル1テーブルがあれば良いです: +物理メモリの量が非常に限られたデバイスについては、アクセスする必要があるときだけ**ページテーブルのフレームを一時的にマップする**という方法が考えられます。そのような一時的なマッピングを作りたいときには、たった一つだけ恒等マップさせられたレベル1テーブルがあれば良いです: ![A virtual and a physical address space with an identity mapped level 1 table, which maps its 0th entry to the level 2 table frame, thereby mapping that frame to page with address 0](temporarily-mapped-page-tables.svg) -この図におけるレベル1テーブルは仮想アドレス空間の最初の2MiBを制御しています。なぜなら、このテーブルにはCR3レジスタから始めて、レベル4、3、2のページテーブルの0番目のエントリを辿ることで到達できるからです。その8番目のエントリは、アドレス`32 KiB`の仮想アドレスページをアドレス`32 KiB`の物理アドレスページに対応付けるので、レベル1テーブル自体を恒等対応させています。この図ではその恒等対応を`32 KiB`のところの横向きの(茶色の)矢印で表しています。 +この図におけるレベル1テーブルは仮想アドレス空間の最初の2MiBを制御しています。なぜなら、このテーブルにはCR3レジスタから始めて、レベル4、3、2のページテーブルの0番目のエントリを辿ることで到達できるからです。その8番目のエントリは、アドレス`32 KiB`の仮想アドレスページをアドレス`32 KiB`の物理アドレスページにマップするので、レベル1テーブル自体を恒等マップしています。この図ではその恒等マッピングを`32 KiB`のところの横向きの(茶色の)矢印で表しています。 -恒等対応させたレベル1テーブルに書き込むことによって、カーネルは最大511個の一時的な対応を作ることができます(512から、恒等対応に必要な1つを除く)。上の例では、カーネルは2つの一時的な対応を作りました: +恒等マップさせたレベル1テーブルに書き込むことによって、カーネルは最大511個の一時的なマッピングを作ることができます(512から、恒等マッピングに必要な1つを除く)。上の例では、カーネルは2つの一時的なマッピングを作りました: -- レベル1テーブルの0番目のエントリをアドレス`24 KiB`のフレームに対応付けることで、破線の矢印で示されているように`0 KiB`の仮想ページからレベル2ページテーブルの物理フレームへの一時的対応付けを行いました。 -- レベル1テーブルの9番目のエントリをアドレス`4 KiB`のフレームに対応付けることで、破線の矢印で示されているように`36 KiB`の仮想ページからレベル4ページテーブルの物理フレームへの一時的対応付けを行いました。 +- レベル1テーブルの0番目のエントリをアドレス`24 KiB`のフレームにマップすることで、破線の矢印で示されているように`0 KiB`の仮想ページからレベル2ページテーブルの物理フレームへの一時的なマッピングを行いました。 +- レベル1テーブルの9番目のエントリをアドレス`4 KiB`のフレームにマップすることで、破線の矢印で示されているように`36 KiB`の仮想ページからレベル4ページテーブルの物理フレームへの一時的なマッピングを行いました。 これで、カーネルは`0 KiB`に書き込むことによってレベル2ページテーブルに、`36 KiB`に書き込むことによってレベル4ページテーブルにアクセスできるようになりました。 -任意のページテーブルに一時的対応付けを用いてアクセスする手続きは以下のようになるでしょう: +任意のページテーブルに一時的なマッピングを用いてアクセスする手続きは以下のようになるでしょう: -- 恒等対応しているレベル1テーブルのうち、使われていないエントリを探す。 -- そのエントリを私達のアクセスしたいページテーブルの物理フレームに対応付ける。 -- そのエントリに対応付けられている仮想ページを通じて、対象のフレームにアクセスする。 -- エントリを未使用に戻すことで、一時的対応付けを削除する。 +- 恒等マッピングしているレベル1テーブルのうち、使われていないエントリを探す。 +- そのエントリを私達のアクセスしたいページテーブルの物理フレームにマップする。 +- そのエントリにマップされている仮想ページを通じて、対象のフレームにアクセスする。 +- エントリを未使用に戻すことで、一時的なマッピングを削除する。 -この方法では、同じ512個の仮想ページを対応付けを作成するために再利用するため、物理メモリは4KiBしか必要としません。欠点としては、やや面倒であるということが言えるでしょう。特に、新しい対応付けを作る際に複数のページテーブルの変更が必要になるかもしれず、上の手続きを複数回繰り返さなくてはならないかもしれません。 +この方法では、同じ512個の仮想ページをマッピングを作成するために再利用するため、物理メモリは4KiBしか必要としません。欠点としては、やや面倒であるということが言えるでしょう。特に、新しいマッピングを作る際に複数のページテーブルの変更が必要になるかもしれず、上の手続きを複数回繰り返さなくてはならないかもしれません。 ### 再帰的ページテーブル -他に興味深いアプローチとして、**再帰的にページテーブルを対応付ける**方法があり、この方法では追加のページテーブルは一切不要です。発想としては、レベル4ページテーブルのエントリのどれかをレベル4ページテーブル自体に対応付けるのです。こうすることにより、仮想アドレス空間の一部を予約しておき、現在及び将来のあらゆるページテーブルフレームをその空間に対応付けているのと同じことになります。 +他に興味深いアプローチとして**再帰的にページテーブルをマップする**方法があり、この方法では追加のページテーブルは一切不要です。発想としては、レベル4ページテーブルのエントリのどれかをレベル4ページテーブル自体にマップするのです。こうすることにより、仮想アドレス空間の一部を予約しておき、現在及び将来のあらゆるページテーブルフレームをその空間にマップしているのと同じことになります。 これがうまく行く理由を説明するために、例を見てみましょう: ![An example 4-level page hierarchy with each page table shown in physical memory. Entry 511 of the level 4 page is mapped to frame 4KiB, the frame of the level 4 table itself.](recursive-page-table.png) -[この記事の最初での例][example at the beginning of this post]との唯一の違いは、レベル4テーブルの511番目に、物理フレーム`4 KiB`すなわちレベル4テーブル自体のフレームに対応付けられたエントリが追加されていることです。 +[この記事の最初での例][example at the beginning of this post]との唯一の違いは、レベル4テーブルの511番目に、物理フレーム`4 KiB`すなわちレベル4テーブル自体のフレームにマップされたエントリが追加されていることです。 [example at the beginning of this post]: #peziteburuniakusesusuru CPUにこのエントリを辿らせるようにすると、レベル3テーブルではなく、そのレベル4テーブルに再び到達します。これは再帰関数(自らを呼び出す関数)に似ているので、**再帰的 (recursive) ページテーブル**と呼ばれます。CPUはレベル4テーブルのすべてのエントリはレベル3テーブルを指していると思っているので、CPUはいまレベル4テーブルをレベル3テーブルとして扱っているということに注目してください。これがうまく行くのは、x86_64においてはすべてのレベルのテーブルが全く同じレイアウトを持っているためです。 -実際に変換を始める前に、この再帰エントリを1回以上たどることで、CPUのたどる階層の数を短くできます。例えば、一度再帰エントリを辿ったあとでレベル3テーブルに進むと、CPUはレベル3テーブルをレベル2テーブルだと思い込みます。同様に、レベル2テーブルをレベル1テーブルだと、レベル1テーブルを対応付けられた(物理)フレームだと思います。CPUがこれを物理フレームだと思っているということは、レベル1ページテーブルを読み書きできるということを意味します。下の図はこの5回の変換ステップを示しています: +実際に変換を始める前に、この再帰エントリを1回以上たどることで、CPUのたどる階層の数を短くできます。例えば、一度再帰エントリを辿ったあとでレベル3テーブルに進むと、CPUはレベル3テーブルをレベル2テーブルだと思い込みます。同様に、レベル2テーブルをレベル1テーブルだと、レベル1テーブルをマップされた(物理)フレームだと思います。CPUがこれを物理フレームだと思っているということは、レベル1ページテーブルを読み書きできるということを意味します。下の図はこの5回の変換ステップを示しています: ![The above example 4-level page hierarchy with 5 arrows: "Step 0" from CR4 to level 4 table, "Step 1" from level 4 table to level 4 table, "Step 2" from level 4 table to level 3 table, "Step 3" from level 3 table to level 2 table, and "Step 4" from level 2 table to level 1 table.](recursive-page-table-access-level-1.png) @@ -131,9 +131,9 @@ CPUにこのエントリを辿らせるようにすると、レベル3テーブ ![The same 4-level page hierarchy with the following 4 arrows: "Step 0" from CR4 to level 4 table, "Steps 1&2" from level 4 table to level 4 table, "Step 3" from level 4 table to level 3 table, and "Step 4" from level 3 table to level 2 table.](recursive-page-table-access-level-2.png) -ステップごとにこれを見てみましょう:まず、CPUはレベル4テーブルの再帰エントリをたどり、レベル3テーブルに着いたと思い込みます。同じ再帰エントリを再びたどり、レベル2テーブルに着いたと考えます。しかし実際にはまだレベル4テーブルから動いていません。CPUが異なるエントリをたどると、レベル3テーブルに到着するのですが、CPUはレベル1にすでにいるのだと思っています。そのため、次のエントリはレベル2テーブルを指しているのですが、CPUは対応付けられた物理フレームを指していると思うので、私達はレベル2テーブルを読み書きできるというわけです。 +ステップごとにこれを見てみましょう:まず、CPUはレベル4テーブルの再帰エントリをたどり、レベル3テーブルに着いたと思い込みます。同じ再帰エントリを再びたどり、レベル2テーブルに着いたと考えます。しかし実際にはまだレベル4テーブルから動いていません。CPUが異なるエントリをたどると、レベル3テーブルに到着するのですが、CPUはレベル1にすでにいるのだと思っています。そのため、次のエントリはレベル2テーブルを指しているのですが、CPUはマップされた物理フレームを指していると思うので、私達はレベル2テーブルを読み書きできるというわけです。 -レベル3や4のテーブルにアクセスするのも同じやり方でできます。レベル3テーブルにアクセスするためには、再帰エントリを3回たどることでCPUを騙し、すでにレベル1テーブルにいると思い込ませます。そこで別のエントリをたどりレベル3テーブルに着くと、CPUはそれを対応付けられたフレームとして扱います。レベル4テーブル自体にアクセスするには、再帰エントリを4回辿ればCPUはそのレベル4テーブル自体を対応付けられたフレームとして扱ってくれるというわけです(下の青紫の矢印)。 +レベル3や4のテーブルにアクセスするのも同じやり方でできます。レベル3テーブルにアクセスするためには、再帰エントリを3回たどることでCPUを騙し、すでにレベル1テーブルにいると思い込ませます。そこで別のエントリをたどりレベル3テーブルに着くと、CPUはそれをマップされたフレームとして扱います。レベル4テーブル自体にアクセスするには、再帰エントリを4回辿ればCPUはそのレベル4テーブル自体をマップされたフレームとして扱ってくれるというわけです(下の青紫の矢印)。 ![The same 4-level page hierarchy with the following 3 arrows: "Step 0" from CR4 to level 4 table, "Steps 1,2,3" from level 4 table to level 4 table, and "Step 4" from level 4 table to level 3 table. In blue the alternative "Steps 1,2,3,4" arrow from level 4 table to level 4 table.](recursive-page-table-access-level-3.png) @@ -146,27 +146,27 @@ CPUにこのエントリを辿らせるようにすると、レベル3テーブ

アドレス計算

-実際の変換の前に再帰的移動を1回または複数回行うことですべての階層のテーブルにアクセスできるということを見てきました。4つのテーブルそれぞれのどのインデクスが使われるかは仮想アドレスから直接計算されていましたから、再帰エントリを使うためには特別な仮想アドレスを作り出す必要があります。ページテーブルのインデクスは仮想アドレスから以下のように計算されていたことを思い出してください: +実際の変換の前に再帰的移動を1回または複数回行うことですべての階層のテーブルにアクセスできるということを見てきました。4つのテーブルそれぞれのどのインデックスが使われるかは仮想アドレスから直接計算されていましたから、再帰エントリを使うためには特別な仮想アドレスを作り出す必要があります。ページテーブルのインデックスは仮想アドレスから以下のように計算されていたことを思い出してください: ![Bits 0–12 are the page offset, bits 12–21 the level 1 index, bits 21–30 the level 2 index, bits 30–39 the level 3 index, and bits 39–48 the level 4 index](../paging-introduction/x86_64-table-indices-from-address.svg) -あるページを対応付けているレベル1テーブルにアクセスしたいとします。上で学んだように、このためには再帰エントリを1度辿ってからレベル4,3,2のインデクスへと続けていく必要があります。これをするために、それぞれのアドレスブロックを一つ右にずらし、レベル4のインデクスがあったところに再帰エントリのインデクスをセットします: +あるページをマップしているレベル1テーブルにアクセスしたいとします。上で学んだように、このためには再帰エントリを1度辿ってからレベル4,3,2のインデックスへと続けていく必要があります。これをするために、それぞれのアドレスブロックを一つ右にずらし、レベル4のインデックスがあったところに再帰エントリのインデックスをセットします: ![Bits 0–12 are the offset into the level 1 table frame, bits 12–21 the level 2 index, bits 21–30 the level 3 index, bits 30–39 the level 4 index, and bits 39–48 the index of the recursive entry](table-indices-from-address-recursive-level-1.svg) -そのページのレベル2テーブルにアクセスしたい場合、それぞれのブロックを2つ右にずらし、レベル4と3のインデクスがあったところに再帰エントリのインデクスをセットします: +そのページのレベル2テーブルにアクセスしたい場合、それぞれのブロックを2つ右にずらし、レベル4と3のインデックスがあったところに再帰エントリのインデックスをセットします: ![Bits 0–12 are the offset into the level 2 table frame, bits 12–21 the level 3 index, bits 21–30 the level 4 index, and bits 30–39 and bits 39–48 are the index of the recursive entry](table-indices-from-address-recursive-level-2.svg) -レベル3テーブルにアクセスする場合、それぞれのブロックを3つ右にずらし、レベル4,3,2のインデクスがあったところに再帰インデクスを使います: +レベル3テーブルにアクセスする場合、それぞれのブロックを3つ右にずらし、レベル4,3,2のインデックスがあったところに再帰インデックスを使います: ![Bits 0–12 are the offset into the level 3 table frame, bits 12–21 the level 4 index, and bits 21–30, bits 30–39 and bits 39–48 are the index of the recursive entry](table-indices-from-address-recursive-level-3.svg) -最後に、レベル4テーブルにはそれぞれのブロックを4ブロックずらし、オフセットを除いてすべてのアドレスブロックに再帰インデクスを使うことでアクセスできます: +最後に、レベル4テーブルにはそれぞれのブロックを4ブロックずらし、オフセットを除いてすべてのアドレスブロックに再帰インデックスを使うことでアクセスできます: ![Bits 0–12 are the offset into the level l table frame and bits 12–21, bits 21–30, bits 30–39 and bits 39–48 are the index of the recursive entry](table-indices-from-address-recursive-level-4.svg) -これで、4つの階層すべてのページテーブルの仮想アドレスを計算できます。また、インデクスをページテーブルエントリのサイズ倍、つまり8倍することによって、特定のページテーブルエントリを指すアドレスを計算できます。 +これで、4つの階層すべてのページテーブルの仮想アドレスを計算できます。また、インデックスをページテーブルエントリのサイズ倍、つまり8倍することによって、特定のページテーブルエントリを指すアドレスを計算できます。 下の表は、それぞれの種類のフレームにアクセスするためのアドレス構造をまとめたものです: @@ -180,7 +180,7 @@ CPUにこのエントリを辿らせるようにすると、レベル3テーブ [octal]: https://en.wikipedia.org/wiki/Octal -ただし、`AAA`がレベル4インデクス、`BBB`がレベル3インデクス、`CCC`がレベル2インデクス、`DDD`が対応付けられたフレームのレベル1インデクス、`EEE`がオフセットです。`RRR`が再帰エントリのインデクスです。インデクス(3ケタ)をオフセット(4ケタ)に変換するときは、8倍(ページテーブルエントリのサイズ倍)しています。 +ただし、`AAA`がレベル4インデックス、`BBB`がレベル3インデックス、`CCC`がレベル2インデックス、`DDD`がマップされたフレームのレベル1インデックス、`EEE`がオフセットです。`RRR`が再帰エントリのインデックスです。インデックス(3ケタ)をオフセット(4ケタ)に変換するときは、8倍(ページテーブルエントリのサイズ倍)しています。 `SSSSS`は符号拡張ビットで、すなわち47番目のビットのコピーです。これはx86_64におけるアドレスの特殊な要求の一つです。これは[前回の記事][sign extension]で説明しました。 @@ -196,14 +196,14 @@ CPUにこのエントリを辿らせるようにすると、レベル3テーブ // この仮想アドレスに対応するページテーブルにアクセスしたい let addr: usize = […]; -let r = 0o777; // 再帰インデクス +let r = 0o777; // 再帰インデックス let sign = 0o177777 << 48; // 符号拡張 -// 変換したいアドレスのページテーブルインデクスを取得する -let l4_idx = (addr >> 39) & 0o777; // レベル4インデクス -let l3_idx = (addr >> 30) & 0o777; // レベル3インデクス -let l2_idx = (addr >> 21) & 0o777; // レベル2インデクス -let l1_idx = (addr >> 12) & 0o777; // レベル1インデクス +// 変換したいアドレスのページテーブルインデックスを取得する +let l4_idx = (addr >> 39) & 0o777; // レベル4インデックス +let l3_idx = (addr >> 30) & 0o777; // レベル3インデックス +let l2_idx = (addr >> 21) & 0o777; // レベル2インデックス +let l1_idx = (addr >> 12) & 0o777; // レベル1インデックス let page_offset = addr & 0o7777; // テーブルアドレスを計算する @@ -217,9 +217,9 @@ let level_1_table_addr = sign | (r << 39) | (l4_idx << 30) | (l3_idx << 21) | (l2_idx << 12); ``` -上のコードは、レベル4エントリの最後(インデクス`0o777`すなわち511)が再帰対応していると仮定しています。この仮定は正しくないので,このコードは動作しません。ブートローダに再帰対応付けを設定させる方法については後述します。 +上のコードは、レベル4エントリの最後(インデックス`0o777`すなわち511)が再帰マッピングしていると仮定しています。この仮定は正しくないので,このコードは動作しません。ブートローダに再帰マッピングを設定させる方法については後述します。 -ビット演算を自前で行う代わりに、`x86_64`クレートの[`RecursivePageTable`]型を使うこともできます。これは様々なページ操作の安全な抽象化を提供します。例えば、以下のコードは仮想アドレスを対応付けられた物理アドレスに変換する方法を示しています。 +ビット演算を自前で行う代わりに、`x86_64`クレートの[`RecursivePageTable`]型を使うこともできます。これは様々なページ操作の安全な抽象化を提供します。例えば、以下のコードは仮想アドレスをマップされた物理アドレスに変換する方法を示しています。 [`RecursivePageTable`]: https://docs.rs/x86_64/0.14.2/x86_64/structures/paging/mapper/struct.RecursivePageTable.html @@ -248,32 +248,32 @@ let frame = recursive_page_table.translate_page(page); frame.map(|frame| frame.start_address() + u64::from(addr.page_offset())) ``` -繰り返しになりますが、このコード(が正しく実行される)には正しい再帰対応がなされていることが必要となります。この対応付けがあるのなら、空欄になっている`level_4_table_addr`は最初のコード例を使って計算すればよいです。 +繰り返しになりますが、このコード(が正しく実行される)には正しい再帰マッピングがなされていることが必要となります。そのようなマッピングがあるのなら、空欄になっている`level_4_table_addr`は最初のコード例を使って計算すればよいです。
--- -再帰的ページングは、ページテーブルのたった一つの対応付けがいかに強力に使えるかを示す興味深いテクニックです。比較的実装するのが簡単であり、ほとんど設定も必要でない(一つ再帰エントリを作るだけ)ので、ページングを使って最初に実装するのに格好の対象でしょう。 +再帰的ページングは、ページテーブルのたった一つのマッピングがいかに強力に使えるかを示す興味深いテクニックです。比較的実装するのが簡単であり、ほとんど設定も必要でない(一つ再帰エントリを作るだけ)ので、ページングを使って最初に実装するのに格好の対象でしょう。 しかし、いくつか欠点もあります: - 大量の仮想メモリ領域(512GiB)を占有してしまう。私達の使っている48bitアドレス空間は巨大なのでこのことはさしたる問題にはなりませんが、キャッシュの挙動が最適でなくなってしまうかもしれません。 -- 現在有効なアドレス空間にしか簡単にはアクセスできない。他のアドレス空間にアクセスするのは再帰エントリを変更することで可能ではあるものの、もとに戻すためには一時的対応付けが必要。これを行う方法については[カーネルをリマップする][_Remap The Kernel_](未訳、また旧版のため情報が古い)という記事を読んでください。 +- 現在有効なアドレス空間にしか簡単にはアクセスできない。他のアドレス空間にアクセスするのは再帰エントリを変更することで可能ではあるものの、もとに戻すためには一時的なマッピングが必要。これを行う方法については[カーネルをリマップする][_Remap The Kernel_](未訳、また旧版のため情報が古い)という記事を読んでください。 - x86のページテーブルの方式に強く依存しており、他のアーキテクチャでは動作しないかもしれない。 [_Remap The Kernel_]: https://os.phil-opp.com/remap-the-kernel/#overview ## ブートローダによる補助 -これらのアプローチはすべて、準備のためにページテーブルに対する修正が必要になります。例えば、物理メモリへの対応付けを作ったり、レベル4テーブルのエントリを再帰的に対応付けたりなどです。問題は、これらの必要な対応付けを作るためには、すでにページテーブルにアクセスできるようになっていなければいけないということです。 +これらのアプローチはすべて、準備のためにページテーブルに対する修正が必要になります。例えば、物理メモリへのマッピングを作ったり、レベル4テーブルのエントリを再帰的にマッピングしたりなどです。問題は、これらの必要なマッピングを作るためには、すでにページテーブルにアクセスできるようになっていなければいけないということです。 -つまり、私達のカーネルが使うページテーブルを作っている、ブートローダの手助けが必要になるということです。ブートローダはページテーブルにアクセスできますから、私達の必要とするどんな対応付けも作れます。`bootloader`クレートは上の2つのアプローチをどちらもサポートしており、現在の実装においては[cargoのfeatures][cargo features]を使ってこれらをコントロールします。 +つまり、私達のカーネルが使うページテーブルを作っている、ブートローダの手助けが必要になるということです。ブートローダはページテーブルにアクセスできますから、私達の必要とするどんなマッピングも作れます。`bootloader`クレートは上の2つのアプローチをどちらもサポートしており、現在の実装においては[cargoのfeatures][cargo features]を使ってこれらをコントロールします。 [cargo features]: https://doc.rust-lang.org/cargo/reference/features.html#the-features-section -- `map_physical_memory` featureを使うと、全物理メモリを仮想アドレス空間のどこかに対応付けます。そのため、カーネルはすべての物理メモリにアクセスでき、[上で述べた方法に従って物理メモリ全体を対応付ける](#wu-li-memoriquan-ti-wodui-ying-fu-keru)ことができます。 -- `recursive_page_table` featureでは、ブートローダはレベル4ページテーブルのエントリを再帰的に対応付けます。これによりカーネルは[再帰的ページテーブル](#zai-gui-de-peziteburu)で述べた方法に従ってページテーブルにアクセスすることができます。 +- `map_physical_memory` featureを使うと、全物理メモリを仮想アドレス空間のどこかにマッピングします。そのため、カーネルはすべての物理メモリにアクセスでき、[上で述べた方法に従って物理メモリ全体をマップする](#wu-li-memoriquan-ti-wodui-ying-fu-keru)ことができます。 +- `recursive_page_table` featureでは、ブートローダはレベル4ページテーブルのエントリを再帰的にマッピングします。これによりカーネルは[再帰的ページテーブル](#zai-gui-de-peziteburu)で述べた方法に従ってページテーブルにアクセスすることができます。 私達のカーネルには、シンプルでプラットフォーム非依存かつ(ページテーブルのフレームでないメモリにもアクセスできるので)より強力である1つ目の方法を採ることにします。必要なブートローダの機能 (feature) を有効化するために、`map_physical_memory` featureを`bootloader`のdependencyに追加します。 @@ -283,7 +283,7 @@ frame.map(|frame| frame.start_address() + u64::from(addr.page_offset())) bootloader = { version = "0.9.8", features = ["map_physical_memory"]} ``` -この機能を有効化すると、ブートローダは物理メモリの全体を、ある未使用の仮想アドレス空間に対応付けます。この仮想アドレスの範囲をカーネルに伝えるために、ブートローダは**boot information**構造体を渡します。 +この機能を有効化すると、ブートローダは物理メモリの全体を、ある未使用の仮想アドレス空間にマッピングします。この仮想アドレスの範囲をカーネルに伝えるために、ブートローダは**boot information**構造体を渡します。 ### Boot Information @@ -293,8 +293,8 @@ bootloader = { version = "0.9.8", features = ["map_physical_memory"]} [`BootInfo`]: https://docs.rs/bootloader/0.9.3/bootloader/bootinfo/struct.BootInfo.html [semver-incompatible]: https://doc.rust-lang.org/stable/cargo/reference/specifying-dependencies.html#caret-requirements -- `memory_map`フィールドは、利用可能な物理メモリの情報の概要を保持しています。システムの利用可能な物理メモリがどのくらいかや、どのメモリ領域がVGAハードウェアのようなデバイスのために予約されているかをカーネルに伝えます。これらのメモリ対応付けはBIOSやUEFIファームウェアから取得できますが、それが可能なのはブートのごく初期に限られます。そのため、これらをカーネルが後で取得することはできないので、ブートローダによって提供する必要があるわけです。このメモリ対応付けは後で必要となります。 -- `physical_memory_offset`は、物理メモリの対応付けの始まっている仮想アドレスです。このオフセットを物理アドレスに追加することによって、対応する仮想アドレスを得られます。これによって、カーネルから任意の物理アドレスにアクセスできます。 +- `memory_map`フィールドは、利用可能な物理メモリの情報の概要を保持しています。システムの利用可能な物理メモリがどのくらいかや、どのメモリ領域がVGAハードウェアのようなデバイスのために予約されているかをカーネルに伝えます。これらのメモリマッピングはBIOSやUEFIファームウェアから取得できますが、それが可能なのはブートのごく初期に限られます。そのため、これらをカーネルが後で取得することはできないので、ブートローダによって提供する必要があるわけです。このメモリマッピングは後で必要となります。 +- `physical_memory_offset`は、物理メモリのマッピングの始まっている仮想アドレスです。このオフセットを物理アドレスに追加することによって、対応する仮想アドレスを得られます。これによって、カーネルから任意の物理アドレスにアクセスできます。 ブートローダは`BootInfo`構造体を`_start`関数の`&'static BootInfo`引数という形でカーネルに渡します。この引数は私達の関数ではまだ宣言していなかったので追加します: @@ -358,7 +358,7 @@ fn test_kernel_main(_boot_info: &'static BootInfo) -> ! { ## 実装 -物理メモリへのアクセスができるようになったので、いよいよページテーブルのコードを実装できます。そのためにまず、現在有効な、私達のカーネルが使用しているページテーブルを見てみます。次に、与えられた仮想アドレスが対応付けられている物理アドレスを返す変換関数を作ります。最後に新しい対応付けを作るためにページテーブルを修正してみます。 +物理メモリへのアクセスができるようになったので、いよいよページテーブルのコードを実装できます。そのためにまず、現在有効な、私達のカーネルが使用しているページテーブルを見てみます。次に、与えられた仮想アドレスがマップされている物理アドレスを返す変換関数を作ります。最後に、新しいマッピングを作るためにページテーブルを修正してみます。 始める前に、`memory`モジュールを作ります: @@ -388,7 +388,7 @@ use x86_64::{ /// /// この関数はunsafeである:全物理メモリが、渡された /// `physical_memory_offset`(だけずらしたうえ)で -/// 仮想メモリへと対応付けられていることを呼び出し元が +/// 仮想メモリへとマップされていることを呼び出し元が /// 保証しなければならない。また、`&mut`参照が複数の /// 名称を持つこと (mutable aliasingといい、動作が未定義) /// につながるため、この関数は一度しか呼び出してはならない。 @@ -450,7 +450,7 @@ fn kernel_main(boot_info: &'static BootInfo) -> ! { ![QEMU printing entry 0 (0x2000, PRESENT, WRITABLE, ACCESSED), entry 1 (0x894000, PRESENT, WRITABLE, ACCESSED, DIRTY), entry 31 (0x88e000, PRESENT, WRITABLE, ACCESSED, DIRTY), entry 175 (0x891000, PRESENT, WRITABLE, ACCESSED, DIRTY), and entry 504 (0x897000, PRESENT, WRITABLE, ACCESSED, DIRTY)](qemu-print-level-4-table.png) -いくつかの空でないエントリがあり、いずれも異なるレベル3テーブルに対応づけられていることがわかります。このようにたくさんの領域があるのは、カーネルコード、カーネルスタック、物理メモリ対応、ブート情報が互いに離れたメモリ領域を使っているためです。 +いくつかの空でないエントリがあり、いずれも異なるレベル3テーブルにマップさせられていることがわかります。このようにたくさんの領域があるのは、カーネルコード、カーネルスタック、物理メモリマッピング、ブート情報が互いに離れたメモリ領域を使っているためです。 ページテーブルを更に辿りレベル3テーブルを見るには、エントリに対応するフレームを取り出し再び仮想アドレスに変換すればよいです: @@ -491,10 +491,10 @@ if !entry.is_unused() { use x86_64::PhysAddr; /// 与えられた仮想アドレスを対応する物理アドレスに変換し、 -/// そのアドレスが対応付けられていないなら`None`を返す。 +/// そのアドレスがマップされていないなら`None`を返す。 /// /// この関数はunsafeである。なぜなら、呼び出し元は全物理メモリが与えられた -/// `physical_memory_offset`(だけずらした上)で対応付けられていることを +/// `physical_memory_offset`(だけずらした上)でマップされていることを /// 保証しなくてはならないからである。 pub unsafe fn translate_addr(addr: VirtAddr, physical_memory_offset: VirtAddr) -> Option @@ -554,9 +554,9 @@ fn translate_addr_inner(addr: VirtAddr, physical_memory_offset: VirtAddr) 先程作った`active_level_4_table`関数を再利用せず、`CR3`レジスタからレベル4フレームを読み出すコードを再び書いています。これは簡単に試作するためであり、後でもっと良い方法で作り直すのでご心配なく。 -`Virtaddr`構造体には、(仮想メモリの)インデクスから4つの階層のページテーブルを計算してくれるメソッドが備わっています。この4つのインデクスを配列に格納することで、これらを`for`ループを使って辿ります。`for`ループを抜けたら、最後に計算した`frame`を覚えているので、物理アドレスを計算できます。この`frame`は、forループの中ではページテーブルのフレームを指していて、最後のループのあと(すなわちレベル1エントリを辿ったあと)では対応する(物理)フレームを指しています。 +`Virtaddr`構造体には、(仮想メモリの)インデックスから4つの階層のページテーブルを計算してくれるメソッドが備わっています。この4つのインデックスを配列に格納することで、これらを`for`ループを使って辿ります。`for`ループを抜けたら、最後に計算した`frame`を覚えているので、物理アドレスを計算できます。この`frame`は、forループの中ではページテーブルのフレームを指していて、最後のループのあと(すなわちレベル1エントリを辿ったあと)では対応する(物理)フレームを指しています。 -ループの中では、前と同じように`physical_memory_offset`を使ってフレームをページテーブルの参照に変換します。次に、そのページテーブルのエントリを読み、[`PageTableEntry::frame`]関数を使って対応するフレームを取得します。もしエントリがフレームに対応付けられていなければ`None`を返します。もしエントリが2MiBや1GiBのhuge pageに対応付けられていたら、今のところはpanicすることにします。 +ループの中では、前と同じように`physical_memory_offset`を使ってフレームをページテーブルの参照に変換します。次に、そのページテーブルのエントリを読み、[`PageTableEntry::frame`]関数を使って対応するフレームを取得します。もしエントリがフレームにマップされていなければ`None`を返します。もしエントリが2MiBや1GiBのhuge pageにマップされていたら、今のところはpanicすることにします。 [`PageTableEntry::frame`]: https://docs.rs/x86_64/0.14.2/x86_64/structures/paging/page_table/struct.PageTableEntry.html#method.frame @@ -580,7 +580,7 @@ fn kernel_main(boot_info: &'static BootInfo) -> ! { 0x201008, // スタックページのどこか 0x0100_0020_1a10, - // 物理アドレス "0" に対応付けられている仮想アドレス + // 物理アドレス "0" にマップされている仮想アドレス boot_info.physical_memory_offset, ]; @@ -598,19 +598,19 @@ fn kernel_main(boot_info: &'static BootInfo) -> ! { ![0xb8000 -> 0xb8000, 0x201008 -> 0x401008, 0x10000201a10 -> 0x279a10, "panicked at 'huge pages not supported'](qemu-translate-addr.png) -期待したとおり、恒等対応しているアドレス`0xb8000`は同じ物理アドレスに変換されました。コードページとスタックページは物理アドレスのどこかしかに変換されていますが、その場所はブートローダがカーネルの初期対応づけをどのようにつくったかによります。また、下から12ビットは変換のあとも常に同じであるということも注目に値します:この部分は[ページオフセット][_page offset_]であり、変換には関わらないためです。 +期待したとおり、恒等マップしているアドレス`0xb8000`は同じ物理アドレスに変換されました。コードページとスタックページは物理アドレスのどこかしかに変換されていますが、その場所はブートローダがカーネルの初期マッピングをどのようにつくったかによります。また、下から12ビットは変換のあとも常に同じであるということも注目に値します:この部分は[ページオフセット][_page offset_]であり、変換には関わらないためです。 [_page offset_]: @/edition-2/posts/08-paging-introduction/index.ja.md#x86-64niokerupezingu -それぞれの物理アドレスは`physical_memory_offset`を足すことでアクセスできるわけですから、`physical_memory_offset`自体を変換すると物理アドレス`0`を指すはずです。しかし、効率よく対応付けを行うためにここではhuge pageが使われており、これはまだサポートしていないので変換には失敗しています。 +それぞれの物理アドレスは`physical_memory_offset`を足すことでアクセスできるわけですから、`physical_memory_offset`自体を変換すると物理アドレス`0`を指すはずです。しかし、効率よくマッピングを行うためにここではhuge pageが使われており、これはまだサポートしていないので変換には失敗しています。 ### `OffsetPageTable`を使う 仮想アドレスから物理アドレスへの変換はOSのカーネルがよく行うことですから、`x86_64`クレートはそのための抽象化を提供しています。この実装はすでにhuge pageや`translate_addr`以外の様々な関数もサポートしているので、以下ではhuge pageのサポートを自前で実装する代わりにこれを使うことにします。 -この抽象化の基礎となっているのは、様々なページテーブル対応付け関数を定義している2つのトレイトです。 +この抽象化の基礎となっているのは、様々なページテーブルマッピング関数を定義している2つのトレイトです。 -- [`Mapper`]トレイトはページサイズを型引数とする汎用型 (ジェネリクス) で、ページに対して操作を行う関数を提供します。例えば、[`translate_page`]は与えられたページを同じサイズのフレームに変換し、[`map_to`]はページテーブルに新しい対応付けを作成します。 +- [`Mapper`]トレイトはページサイズを型引数とする汎用型 (ジェネリクス) で、ページに対して操作を行う関数を提供します。例えば、[`translate_page`]は与えられたページを同じサイズのフレームに変換し、[`map_to`]はページテーブルに新しいマッピングを作成します。 - [`Translate`] トレイトは[`translate_addr`]や一般の[`translate`]のような、さまざまなページサイズに対して動くような関数を提供します。 [`Mapper`]: https://docs.rs/x86_64/0.14.2/x86_64/structures/paging/mapper/trait.Mapper.html @@ -620,13 +620,13 @@ fn kernel_main(boot_info: &'static BootInfo) -> ! { [`translate_addr`]: https://docs.rs/x86_64/0.14.2/x86_64/structures/paging/mapper/trait.Translate.html#method.translate_addr [`translate`]: https://docs.rs/x86_64/0.14.2/x86_64/structures/paging/mapper/trait.Translate.html#tymethod.translate -これらのトレイトはインターフェイスを定義しているだけであり、その実装は何一つ提供していません。`x86_64`クレートは現在、このトレイトを実装する型を異なる要件に合わせて3つ用意しています。[`OffsetPageTable`]型は、全物理メモリがあるオフセットで仮想アドレスに対応していることを前提とします。[`MappedPageTable`]はもう少し融通が効き、それぞれのページテーブルフレームが(そのフレームから)計算可能な仮想アドレスに対応していることだけを前提とします。最後に[`RecursivePageTable`]型は、ページテーブルのフレームに[再帰的ページテーブル](#zai-gui-de-peziteburu)を使ってアクセスするときに使えます。 +これらのトレイトはインターフェイスを定義しているだけであり、その実装は何一つ提供していません。`x86_64`クレートは現在、このトレイトを実装する型を異なる要件に合わせて3つ用意しています。[`OffsetPageTable`]型は、全物理メモリがあるオフセットで仮想アドレスにマップしていることを前提とします。[`MappedPageTable`]はもう少し融通が効き、それぞれのページテーブルフレームが(そのフレームから)計算可能な仮想アドレスにマップしていることだけを前提とします。最後に[`RecursivePageTable`]型は、ページテーブルのフレームに[再帰的ページテーブル](#zai-gui-de-peziteburu)を使ってアクセスするときに使えます。 [`OffsetPageTable`]: https://docs.rs/x86_64/0.14.2/x86_64/structures/paging/mapper/struct.OffsetPageTable.html [`MappedPageTable`]: https://docs.rs/x86_64/0.14.2/x86_64/structures/paging/mapper/struct.MappedPageTable.html [`RecursivePageTable`]: https://docs.rs/x86_64/0.14.2/x86_64/structures/paging/mapper/struct.RecursivePageTable.html -私達の場合、ブートローダは全物理メモリを`physical_memory_offset`変数で指定された仮想アドレスで物理メモリに対応付けているので、`OffsetPageTable`型が使えます。これを初期化するために、`memory`モジュールに新しく`init`関数を作りましょう: +私達の場合、ブートローダは全物理メモリを`physical_memory_offset`変数で指定された仮想アドレスで物理メモリにマップしているので、`OffsetPageTable`型が使えます。これを初期化するために、`memory`モジュールに新しく`init`関数を作りましょう: ```rust use x86_64::structures::paging::OffsetPageTable; @@ -635,7 +635,7 @@ use x86_64::structures::paging::OffsetPageTable; /// /// この関数はunsafeである:全物理メモリが、渡された /// `physical_memory_offset`(だけずらしたうえ)で -/// 仮想メモリへと対応付けられていることを呼び出し元が +/// 仮想メモリへとマップされていることを呼び出し元が /// 保証しなければならない。また、`&mut`参照が複数の /// 名称を持つこと (mutable aliasingといい、動作が未定義) /// につながるため、この関数は一度しか呼び出してはならない。 @@ -650,7 +650,7 @@ unsafe fn active_level_4_table(physical_memory_offset: VirtAddr) {…} ``` -この関数は`physical_memory_offset`を引数としてとり、`'static`ライフタイムを持つ`OffsetPageTable`を作って返します。このライフタイムは、私達のカーネルが実行している間この実体 (インスタンス) はずっと有効であるという意味です。関数の中ではまず`active_level_4_table`関数を呼び出し、レベル4ページテーブルへの可変参照を取得します。次に[`OffsetPageTable::new`]関数をこの参照を使って呼び出します。この`new`関数の第二引数には、物理メモリの対応付けの始まる仮想アドレスが入ることになっています。つまり`physical_memory_offset`です。 +この関数は`physical_memory_offset`を引数としてとり、`'static`ライフタイムを持つ`OffsetPageTable`を作って返します。このライフタイムは、私達のカーネルが実行している間この実体 (インスタンス) はずっと有効であるという意味です。関数の中ではまず`active_level_4_table`関数を呼び出し、レベル4ページテーブルへの可変参照を取得します。次に[`OffsetPageTable::new`]関数をこの参照を使って呼び出します。この`new`関数の第二引数には、物理メモリのマッピングの始まる仮想アドレスが入ることになっています。つまり`physical_memory_offset`です。 [`OffsetPageTable::new`]: https://docs.rs/x86_64/0.14.2/x86_64/structures/paging/mapper/struct.OffsetPageTable.html#method.new @@ -691,24 +691,24 @@ fn kernel_main(boot_info: &'static BootInfo) -> ! { ![0xb8000 -> 0xb8000, 0x201008 -> 0x401008, 0x10000201a10 -> 0x279a10, 0x18000000000 -> 0x0](qemu-mapper-translate-addr.png) -想定通り、`0xb8000`やコード・スタックアドレスの変換結果は自前の変換関数と同じになっています。また、`physical_memory_offset`は物理アドレス`0x0`に対応付けられているのもわかります。 +想定通り、`0xb8000`やコード・スタックアドレスの変換結果は自前の変換関数と同じになっています。また、`physical_memory_offset`は物理アドレス`0x0`にマップされているのもわかります。 `MappedPageTable`型の変換関数を使うことで、huge pageをサポートする手間が省けます。また`map_to`のような他のページング関数も利用でき、これは次のセクションで使います。 この時点で、自作した`memory::translate_addr`関数や`memory::translate_addr_inner`関数はもう必要ではないので削除して構いません。 -### 新しい対応を作る +### 新しいマッピングを作る -これまでページテーブルを見てきましたが、それに対する変更は行っていませんでした。ページテーブルに対する変更として、対応のなかったページに対応を作ってみましょう。 +これまでページテーブルを見てきましたが、それに対する変更は行っていませんでした。ページテーブルに対する変更として、マッピングのなかったページにマッピングを作ってみましょう。 -これを実装するには[`Mapper`]トレイトの[`map_to`]関数を使うので、この関数について少し見てみましょう。ドキュメントによると四つ引数があります:対応に使うページ、ページを対応させるフレーム、ページテーブルエントリにつかうフラグの集合、そして`frame_allocator`です。フレームアロケータ (frame allocator) (フレームを割り当てる (アロケートする) 機能を持つ)が必要な理由は、与えられたページを対応付けるために追加でページテーブルを作成する必要があるかもしれず、これを格納するためには使われていないフレームが必要となるからです。 +これを実装するには[`Mapper`]トレイトの[`map_to`]関数を使うので、この関数について少し見てみましょう。ドキュメントによると四つ引数があります:マッピングに使うページ、ページをマップさせるフレーム、ページテーブルエントリにつかうフラグの集合、そして`frame_allocator`です。フレームアロケータ (frame allocator) (フレームを割り当てる (アロケートする) 機能を持つ)が必要な理由は、与えられたページをマップするために追加でページテーブルを作成する必要があるかもしれず、これを格納するためには使われていないフレームが必要となるからです。 [`map_to`]: https://docs.rs/x86_64/0.14.2/x86_64/structures/paging/trait.Mapper.html#tymethod.map_to [`Mapper`]: https://docs.rs/x86_64/0.14.2/x86_64/structures/paging/trait.Mapper.html #### `create_example_mapping`関数 -私達が実装していく最初のステップとして、`create_example_mapping`関数という、与えられた仮想ページを`0xb8000`すなわちVGAテキストバッファの物理フレームに対応付ける関数を作ってみましょう。このフレームを選んだ理由は、対応付けが正しくなされたかをテストするのが容易だからです:対応付けたページに書き込んで、それが画面に現れるか確認するだけでよいのですから。 +私達が実装していく最初のステップとして、`create_example_mapping`関数という、与えられた仮想ページを`0xb8000`すなわちVGAテキストバッファの物理フレームにマップする関数を作ってみましょう。このフレームを選んだ理由は、マッピングが正しくなされたかをテストするのが容易だからです:マッピングしたページに書き込んで、それが画面に現れるか確認するだけでよいのですから。 `create_example_mapping`は以下のようになります: @@ -720,7 +720,7 @@ use x86_64::{ structures::paging::{Page, PhysFrame, Mapper, Size4KiB, FrameAllocator} }; -/// 与えられたページをフレーム`0xb8000`に試しに対応付ける。 +/// 与えられたページをフレーム`0xb8000`に試しにマップする。 pub fn create_example_mapping( page: Page, mapper: &mut OffsetPageTable, @@ -739,20 +739,20 @@ pub fn create_example_mapping( } ``` -この関数は、対応付ける`page`に加え`OffsetPageTable`のインスタンスと`frame_allocator`への可変参照を引数に取ります。`frame_allocator`引数は[`impl Trait`][impl-trait-arg]構文により[`FrameAllocator`]トレイトを実装するあらゆる型の[汎用型][generic]になっています。`FrameAllocator`トレイトは[`PageSize`]トレイトを実装するなら(トレイト引数のサイズが)4KiBでも2MiBや1GiBのhuge pageでも構わない汎用 (ジェネリック) トレイトです。私達は4KiBの対応付けのみを作りたいので、ジェネリック引数は`Size4KiB`にしています。 +この関数は、マップする`page`に加え`OffsetPageTable`のインスタンスと`frame_allocator`への可変参照を引数に取ります。`frame_allocator`引数は[`impl Trait`][impl-trait-arg]構文により[`FrameAllocator`]トレイトを実装するあらゆる型の[汎用型][generic]になっています。`FrameAllocator`トレイトは[`PageSize`]トレイトを実装するなら(トレイト引数のサイズが)4KiBでも2MiBや1GiBのhuge pageでも構わない汎用 (ジェネリック) トレイトです。私達は4KiBのマッピングのみを作りたいので、ジェネリック引数は`Size4KiB`にしています。 [impl-trait-arg]: https://doc.rust-lang.org/book/ch10-02-traits.html#traits-as-parameters [generic]: https://doc.rust-lang.org/book/ch10-00-generics.html [`FrameAllocator`]: https://docs.rs/x86_64/0.14.2/x86_64/structures/paging/trait.FrameAllocator.html [`PageSize`]: https://docs.rs/x86_64/0.14.2/x86_64/structures/paging/page/trait.PageSize.html -[`map_to`]メソッドは、呼び出し元がフレームはまだ使われていないことを保証しないといけないので、unsafeです。なぜなら、同じフレームを二度対応付けると(例えば2つの異なる`&mut`参照が物理メモリの同じ場所を指すことで)未定義動作を起こす可能性があるからです。今回、VGAテキストバッファのフレームという、すでに対応付けられているフレームを再度使っているので、この要件を破ってしまっています。しかしながら、`create_example_mapping`関数は一時的なテスト関数であり、この記事のあとには取り除かれるので大丈夫です。この危険性のことを忘れないようにするために、その行に`FIXME` (`要修正`) コメントをつけておきます。 +[`map_to`]メソッドは、呼び出し元がフレームはまだ使われていないことを保証しないといけないので、unsafeです。なぜなら、同じフレームを二度マップすると(例えば2つの異なる`&mut`参照が物理メモリの同じ場所を指すことで)未定義動作を起こす可能性があるからです。今回、VGAテキストバッファのフレームという、すでにマップされているフレームを再度使っているので、この要件を破ってしまっています。しかしながら、`create_example_mapping`関数は一時的なテスト関数であり、この記事のあとには取り除かれるので大丈夫です。この危険性のことを忘れないようにするために、その行に`FIXME` (`要修正`) コメントをつけておきます。 `map_to`関数が`page`と`unused_frame`に加えてフラグの集合と`frame_allocator`への参照を取りますが、これについてはすぐに説明します。フラグについては、`PRESENT`フラグという有効なエントリ全てに必須のフラグと、`WRITABLE`フラグという対応するページを書き込み可能にするフラグをセットしています。フラグの一覧については、前記事の[ページテーブルの形式][_Page Table Format_]を参照してください。 [_Page Table Format_]: @/edition-2/posts/08-paging-introduction/index.ja.md#peziteburunoxing-shi -[`map_to`]関数は失敗しうるので、[`Result`]を返します。これは失敗しても構わない単なるテストコードなので、エラーが起きたときは[`expect`]を使ってパニックしてしまうことにします。この関数は成功したとき[`MapperFlush`]型を返します。この型の[`flush`]メソッドを使うと、新しく対応させたページをトランスレーション・ルックアサイド・バッファ (TLB) から簡単にflushすることができます。この型は`Result`と同じく[`#[must_use]`][must_use]属性を使っており、使用し忘れると警告を出します。 +[`map_to`]関数は失敗しうるので、[`Result`]を返します。これは失敗しても構わない単なるテストコードなので、エラーが起きたときは[`expect`]を使ってパニックしてしまうことにします。この関数は成功したとき[`MapperFlush`]型を返します。この型の[`flush`]メソッドを使うと、新しくマッピングしたページをトランスレーション・ルックアサイド・バッファ (TLB) から簡単にflushすることができます。この型は`Result`と同じく[`#[must_use]`][must_use]属性を使っており、使用し忘れると警告を出します。 [`Result`]: https://doc.rust-lang.org/core/result/enum.Result.html [`expect`]: https://doc.rust-lang.org/core/result/enum.Result.html#method.expect @@ -764,7 +764,7 @@ pub fn create_example_mapping( `create_example_mapping`関数を呼べるようにするためには、まず`FrameAllocator`トレイトを実装する型を作成する必要があります。上で述べたように、このトレイトは新しいページのためのフレームを`map_to`が必要としたときに割り当てる役割を持っています。 -単純なケースを考えましょう:新しいページテーブルを作る必要がないと仮定してしまいます。この場合、常に`None`を返すフレームアロケータで十分です。私達の対応付け関数をテストするために、そのような`EmptyFrameAllocator`を作ります。 +単純なケースを考えましょう:新しいページテーブルを作る必要がないと仮定してしまいます。この場合、常に`None`を返すフレームアロケータで十分です。私達のマッピング関数をテストするために、そのような`EmptyFrameAllocator`を作ります。 ```rust // in src/memory.rs @@ -779,30 +779,30 @@ unsafe impl FrameAllocator for EmptyFrameAllocator { } ``` -`FrameAllocator`を実装するのはunsafeです。なぜなら、実装する人は、実装したアロケータが未使用のフレームのみ取得することを保証しなければならないからです。さもなくば、例えば二つの仮想ページが同じ物理フレームに対応付けられたときに未定義動作が起こるかもしれません。この`EmptyFrameAllocator`は`None`しか返さないので、これは問題ではありません。 +`FrameAllocator`を実装するのはunsafeです。なぜなら、実装する人は、実装したアロケータが未使用のフレームのみ取得することを保証しなければならないからです。さもなくば、例えば二つの仮想ページが同じ物理フレームにマップされたときに未定義動作が起こるかもしれません。この`EmptyFrameAllocator`は`None`しか返さないので、これは問題ではありません。 #### 仮想ページを選ぶ -`create_example_mapping`関数に渡すための単純なフレームアロケータを手に入れました。しかし、このアロケータは常に`None`を返すので、対応を作る際に追加のページテーブルフレームが必要でなかったときにのみうまく動作します。いつ追加のページテーブルフレームが必要でありいつそうでないのかを知るために、例をとって考えてみましょう: +`create_example_mapping`関数に渡すための単純なフレームアロケータを手に入れました。しかし、このアロケータは常に`None`を返すので、マッピングを作る際に追加のページテーブルフレームが必要でなかったときにのみうまく動作します。いつ追加のページテーブルフレームが必要でありいつそうでないのかを知るために、例をとって考えてみましょう: ![A virtual and a physical address space with a single mapped page and the page tables of all four levels](required-page-frames-example.svg) -この図の左は仮想アドレス空間を、右は物理アドレス空間を、真ん中はページテーブルを示します。このページテーブルが格納されている物理フレームが破線で示されています。仮想アドレス空間は一つの対応付けられたページをアドレス`0x803fe00000`に持っており、これは青色で示されています。このページをフレームに変換するために、CPUは4層のページテーブルを辿り、アドレス36KiBのフレームに到達します。 +この図の左は仮想アドレス空間を、右は物理アドレス空間を、真ん中はページテーブルを示します。このページテーブルが格納されている物理フレームが破線で示されています。仮想アドレス空間は一つのマップされたページをアドレス`0x803fe00000`に持っており、これは青色で示されています。このページをフレームに変換するために、CPUは4層のページテーブルを辿り、アドレス36KiBのフレームに到達します。 -また、この図はVGAテキストバッファの物理フレームを赤色で示しています。私達の目的は、`create_example_mapping`関数を使ってまだ対応付けられていない仮想ページをこのフレームに対応付けることです。私達の`EmptyFrameAllocator`は常に`None`を返すので、アロケータからフレームを追加する必要がないように対応付けを作りたいです。これができるかは、私達が対応付けにどの仮想ページを使うかに依存します。 +また、この図はVGAテキストバッファの物理フレームを赤色で示しています。私達の目的は、`create_example_mapping`関数を使ってまだマップされていない仮想ページをこのフレームにマップすることです。私達の`EmptyFrameAllocator`は常に`None`を返すので、アロケータからフレームを追加する必要がないようにマッピングを作りたいです。これができるかは、私達がマッピングにどの仮想ページを使うかに依存します。 -この図の仮想アドレス空間には、2つの候補となるページを黄色で示しています。ページのうち一つはアドレス`0x803fe00000`で、これは(青で示された)対応付けられているページの3つ前です。レベル4と3のテーブルのインデクスは青いページと同じですが、レベル2と1のインデクスは違います([前の記事][page-table-indices]を参照)。レベル2テーブルのインデクスが違うということは、異なるレベル1テーブルが使われることを意味します。そんなレベル1テーブルは存在しないので、もしこちらを使っていたら、使われていない物理フレームを追加(でアロケート)する必要が出てきます。対して、2つ目のアドレス`0x803fe02000`にある候補のページは、青のページと同じレベル1ページテーブルを使うのでこの問題は発生しません。よって、必要となるすべてのページテーブルはすでに存在しています。 +この図の仮想アドレス空間には、2つの候補となるページを黄色で示しています。ページのうち一つはアドレス`0x803fe00000`で、これは(青で示された)マップされているページの3つ前です。レベル4と3のテーブルのインデックスは青いページと同じですが、レベル2と1のインデックスは違います([前の記事][page-table-indices]を参照)。レベル2テーブルのインデックスが違うということは、異なるレベル1テーブルが使われることを意味します。そんなレベル1テーブルは存在しないので、もしこちらを使っていたら、使われていない物理フレームを追加(でアロケート)する必要が出てきます。対して、2つ目のアドレス`0x803fe02000`にある候補のページは、青のページと同じレベル1ページテーブルを使うのでこの問題は発生しません。よって、必要となるすべてのページテーブルはすでに存在しています。 [page-table-indices]: @/edition-2/posts/08-paging-introduction/index.ja.md#x86-64niokerupezingu -まとめると、新しい対応を作るときの難易度は、対応付けようとしている仮想ページに依存するということです。作ろうとしているページのレベル1ページテーブルがすでに存在すると最も簡単で、エントリをそのページに一つ書き込むだけです。ページがレベル3のテーブルすら存在しない領域にある場合が最も難しく、その場合まずレベル3,2,1のページテーブルを新しく作る必要があります。 +まとめると、新しいマッピングを作るときの難易度は、マッピングしようとしている仮想ページに依存するということです。作ろうとしているページのレベル1ページテーブルがすでに存在すると最も簡単で、エントリをそのページに一つ書き込むだけです。ページがレベル3のテーブルすら存在しない領域にある場合が最も難しく、その場合まずレベル3,2,1のページテーブルを新しく作る必要があります。 -`EmptyFrameAllocator`を使って`create_example_mapping`を呼び出すためには、すべての(階層の)ページテーブルがすでに存在しているページを選ぶ必要があります。そんなページを探すにあたっては、ブートローダが自分自身を仮想アドレス空間の最初の1メガバイトに読み込んでいるということを利用できます。つまり、この領域のすべてのページについて、レベル1テーブルがきちんと存在しているということです。したがって、試しに対応を作るときに、このメモリ領域のいずれかの未使用ページ、例えばアドレス`0`を使えばよいです。普通このページは、ヌルポインタの参照外しがページフォルトを引き起こすことを保証するために使用しないので、ブートローダもここを対応させてはいないはずです。 +`EmptyFrameAllocator`を使って`create_example_mapping`を呼び出すためには、すべての(階層の)ページテーブルがすでに存在しているページを選ぶ必要があります。そんなページを探すにあたっては、ブートローダが自分自身を仮想アドレス空間の最初の1メガバイトに読み込んでいるということを利用できます。つまり、この領域のすべてのページについて、レベル1テーブルがきちんと存在しているということです。したがって、試しにマッピングを作るときに、このメモリ領域のいずれかの未使用ページ、例えばアドレス`0`を使えばよいです。普通このページは、ヌルポインタの参照外しがページフォルトを引き起こすことを保証するために使用しないので、ブートローダもここをマップさせてはいないはずです。 -#### 対応を作る +#### マッピングを作る -というわけで、`create_example_mapping`関数を呼び出すために必要なすべての引数を手に入れたので、仮想アドレス`0`を対応付けるよう`kernel_main`関数を変更していきましょう。このページをVGAテキストバッファのフレームに対応付けると、以後、画面に書き込むことができるようになるはずです。実装は以下のようになります: +というわけで、`create_example_mapping`関数を呼び出すために必要なすべての引数を手に入れたので、仮想アドレス`0`をマップするよう`kernel_main`関数を変更していきましょう。このページをVGAテキストバッファのフレームにマップすると、以後、画面に書き込むことができるようになるはずです。実装は以下のようになります: ```rust // in src/main.rs @@ -817,11 +817,11 @@ fn kernel_main(boot_info: &'static BootInfo) -> ! { let mut mapper = unsafe { memory::init(phys_mem_offset) }; let mut frame_allocator = memory::EmptyFrameAllocator; - // 未使用のページを対応付ける + // 未使用のページをマップする let page = Page::containing_address(VirtAddr::new(0)); memory::create_example_mapping(page, &mut mapper, &mut frame_allocator); - // 新しい対応付けを使って、文字列`New!`を画面に書き出す + // 新しいマッピングを使って、文字列`New!`を画面に書き出す let page_ptr: *mut u64 = page.start_address().as_mut_ptr(); unsafe { page_ptr.offset(400).write_volatile(0x_f021_f077_f065_f04e)}; @@ -829,7 +829,7 @@ fn kernel_main(boot_info: &'static BootInfo) -> ! { } ``` -まず、`mapper`と`frame_allocator`インスタンスの可変参照を渡して`create_example_mapping`を呼ぶことで、アドレス`0`のページに対応を作っています。これはVGAテキストバッファのフレームに対応付けているので、これに書き込んだものは何であれ画面に出てくるはずです。 +まず、`mapper`と`frame_allocator`インスタンスの可変参照を渡して`create_example_mapping`を呼ぶことで、アドレス`0`のページにマッピングを作っています。これはVGAテキストバッファのフレームにマップしているので、これに書き込んだものは何であれ画面に出てくるはずです。 次にページを生ポインタに変更して、オフセット`400`に値を書き込みます。このページの最初に書き込むとVGAバッファの一番上の行になり、次のprintlnで即座に画面外に流れていってしまうので、それを避けています。値`0x_f021_f077_f065_f04e`は、白背景の"New!"という文字列を表します。[VGAテキストモードの記事][in the _“VGA Text Mode”_ post]で学んだように、VGAバッファへの書き込みはvolatileでなければならないので、[`write_volatile`]メソッドを使っています。 @@ -840,9 +840,9 @@ QEMUで実行すると、以下の出力を得ます: ![QEMU printing "It did not crash!" with four completely white cells in the middle of the screen](qemu-new-mapping.png) -画面の "New!" はページ`0`への書き込みによるものなので、ページテーブルへの新しい対応付けの作成が成功したということを意味します。 +画面の "New!" はページ`0`への書き込みによるものなので、ページテーブルへの新しいマッピングの作成が成功したということを意味します。 -この対応付けが成功したのは、アドレス`0`を管轄するレベル1テーブルがすでに存在していたからに過ぎません。レベル1テーブルがまだ存在しないページを対応付けようとすると、`map_to`関数は新しいページテーブルを作るために`EmptyFrameAllocator`からフレームを割り当てようとしてエラーになります。`0`の代わりに`0xdeadbeaf000`を対応付けようとするとそれが発生するのが見られます。 +このマッピングが成功したのは、アドレス`0`を管轄するレベル1テーブルがすでに存在していたからに過ぎません。レベル1テーブルがまだ存在しないページをマッピングしようとすると、`map_to`関数は新しいページテーブルを作るために`EmptyFrameAllocator`からフレームを割り当てようとしてエラーになります。`0`の代わりに`0xdeadbeaf000`をマッピングしようとするとそれが発生するのが見られます。 ```rust // in src/main.rs @@ -860,7 +860,7 @@ fn kernel_main(boot_info: &'static BootInfo) -> ! { panicked at 'map_to failed: FrameAllocationFailed', /…/result.rs:999:5 ``` -レベル1テーブルがまだ存在していないページを対応付けるためには、ちゃんとした`FrameAllocator`を作らないといけません。しかし、どのフレームが未使用で、どのフレームが利用可能かはどうすればわかるのでしょう? +レベル1テーブルがまだ存在していないページをマップするためには、ちゃんとした`FrameAllocator`を作らないといけません。しかし、どのフレームが未使用で、どのフレームが利用可能かはどうすればわかるのでしょう? ### フレームを割り当てる @@ -930,7 +930,7 @@ impl BootInfoFrameAllocator { この関数はイテレータのコンビネータメソッドを使って、最初に与えられる`MemoryMap`を使用可能な物理フレームのイテレータに変換します: - まず`iter`メソッドを使ってメモリマップを[`MemoryRegion`]のイテレータに変える。 -- 次に[`filter`]メソッドを使って、予約済みなどの理由で使用不可能な領域を飛ばすようにする。ブートローダは作った対応付けに使ったメモリマップはきちんと更新するので、私達のカーネル(コード、データ、スタック)に使われているフレームやブート情報を格納するのに使われているフレームはすでに`InUse` (`使用中`) などでマークされています。そのため`Usable`なフレームは他の場所では使われていないはずとわかります。 +- 次に[`filter`]メソッドを使って、予約済みなどの理由で使用不可能な領域を飛ばすようにする。ブートローダは作ったマッピングに使ったメモリマップはきちんと更新するので、私達のカーネル(コード、データ、スタック)に使われているフレームやブート情報を格納するのに使われているフレームはすでに`InUse` (`使用中`) などでマークされています。そのため`Usable`なフレームは他の場所では使われていないはずとわかります。 - つぎに、[`map`]コンビネータとRustの[range構文][range syntax]を使って、メモリ領域のイテレータからアドレス範囲のイテレータへと変換する。 - つぎに、アドレス範囲から[`step_by`]で4096個ごとにアドレスを選び、[`flat_map`]を使うことでフレームの最初のアドレスのイテレータを得る。4096バイト(=4KiB)はページのサイズに等しいので、それぞれのフレームの開始地点のアドレスが得られます。ブートローダのページは使用可能なメモリ領域をすべてアラインするので、ここで改めてアラインや丸めを行う必要はありません。`map`ではなく[`flat_map`]を使うことで、`Iterator>`ではなく`Iterator`を得ています。 - 最後に、開始アドレスの型を`PhysFrame`に変更することで`Iterator`を得ている。 @@ -989,24 +989,24 @@ fn kernel_main(boot_info: &'static BootInfo) -> ! { } ``` -ブート情報を使うフレームアロケータのおかげで対応付けは成功し、白背景に黒文字の"New!"が再び画面に現れました。舞台裏では、`map_to`メソッドが不足しているページテーブルを以下のやり方で作っています: +ブート情報を使うフレームアロケータのおかげでマッピングは成功し、白背景に黒文字の"New!"が再び画面に現れました。舞台裏では、`map_to`メソッドが不足しているページテーブルを以下のやり方で作っています: - 渡された`frame_allocator`を使って未使用のフレームを割り当ててもらう。 - フレームをゼロで埋めることで、新しい空のページテーブルを作る。 -- 上位のテーブルのエントリをそのフレームに対応付ける。 +- 上位のテーブルのエントリをそのフレームにマップする。 - 次の層で同じことを続ける。 -`create_example_mapping`関数はただのお試しコードにすぎませんが、今や私達は任意のページに対応付けを作れるようになりました。これは、今後の記事で行うメモリ割り当てやマルチスレッディングにおいて不可欠です。 +`create_example_mapping`関数はただのお試しコードにすぎませんが、今や私達は任意のページにマッピングを作れるようになりました。これは、今後の記事で行うメモリ割り当てやマルチスレッディングにおいて不可欠です。 [上](#create-example-mappingguan-shu)で説明したような未定義動作を誤って引き起こしてしまうことのないよう、この時点で`create_example_mapping`関数を再び取り除いておきましょう。 ## まとめ -この記事ではページテーブルのある物理フレームにアクセスするための様々なテクニックを学びました。恒等対応、物理メモリ全体の対応付け、一時的な対応、再帰的ページテーブルなどです。このうち、シンプルでポータブル (アーキテクチャ非依存) で強力な、物理メモリ全体の対応付けを選びました。 +この記事ではページテーブルのある物理フレームにアクセスするための様々なテクニックを学びました。恒等マップ、物理メモリ全体のマッピング、一時的なマッピング、再帰的ページテーブルなどです。このうち、シンプルでポータブル (アーキテクチャ非依存) で強力な、物理メモリ全体のマッピングを選びました。 -ページテーブルにアクセスできなければ物理メモリを対応付けられないので、ブートローダの補助が必要でした。`bootloader`クレートはcargoのfeaturesというオプションを通じて、必要となる対応付けの作成をサポートしています。さらに、必要となる情報をエントリポイント関数の`&BootInfo`引数という形で私達のカーネルに渡してくれます。 +ページテーブルにアクセスできなければ物理メモリをマップされないので、ブートローダの補助が必要でした。`bootloader`クレートはcargoのfeaturesというオプションを通じて、必要となるマッピングの作成をサポートしています。さらに、必要となる情報をエントリポイント関数の`&BootInfo`引数という形で私達のカーネルに渡してくれます。 -実装について。最初はページテーブルを辿る変換関数を自分の手で実装し、そのあとで`x86_64`クレートの`MappedPageTable`型を使いました。また、ページテーブルに新しい対応を作る方法や、そのために必要な`FrameAllocator`をブートローダに渡されたメモリマップをラップすることで作る方法を学びました。 +実装についてですが、最初はページテーブルを辿る変換関数を自分の手で実装し、そのあとで`x86_64`クレートの`MappedPageTable`型を使いました。また、ページテーブルに新しいマッピングを作る方法や、そのために必要な`FrameAllocator`をブートローダに渡されたメモリマップをラップすることで作る方法を学びました。 ## 次は? From 893c1cf4472caf015189fcc6ec082f3bd4ab5deb Mon Sep 17 00:00:00 2001 From: "Shu W. Nakamura" <30687489+woodyZootopia@users.noreply.github.com> Date: Fri, 8 Oct 2021 12:19:07 +0900 Subject: [PATCH 11/11] Update blog/content/edition-2/posts/09-paging-implementation/index.ja.md --- .../edition-2/posts/09-paging-implementation/index.ja.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/blog/content/edition-2/posts/09-paging-implementation/index.ja.md b/blog/content/edition-2/posts/09-paging-implementation/index.ja.md index fad6283c..ff00a0bd 100644 --- a/blog/content/edition-2/posts/09-paging-implementation/index.ja.md +++ b/blog/content/edition-2/posts/09-paging-implementation/index.ja.md @@ -10,7 +10,7 @@ translation_based_on_commit = "27ab4518acbb132e327ed4f4f0508393e9d4d684" translators = ["woodyZootopia", "garasubo"] +++ -この記事では私達のカーネルをページングに対応 (マップ) させる方法についてお伝えします。まずページテーブルの物理フレームにカーネルがアクセスできるようにする様々な方法を示し、それらの利点と欠点について議論します。次にアドレス変換関数を、ついで新しい対応付け (マッピング) を作るための関数を実装します。 +この記事では私達のカーネルをページングに対応させる方法についてお伝えします。まずページテーブルの物理フレームにカーネルがアクセスできるようにする様々な方法を示し、それらの利点と欠点について議論します。次にアドレス変換関数を、ついで新しい対応付け (マッピング) を作るための関数を実装します。