Unsafe operations in unsafe fn require an unsafe block since Rust 2024

This commit is contained in:
Philipp Oppermann
2025-03-27 15:39:55 +01:00
parent 9753695744
commit c0fc0bed9e
5 changed files with 55 additions and 33 deletions

View File

@@ -403,14 +403,12 @@ pub unsafe fn active_level_4_table(physical_memory_offset: VirtAddr)
let virt = physical_memory_offset + phys.as_u64();
let page_table_ptr: *mut PageTable = virt.as_mut_ptr();
&mut *page_table_ptr // unsafe
unsafe { &mut *page_table_ptr }
}
```
まず、有効なレベル4テーブルの物理フレームを`CR3`レジスタから読みます。その開始物理アドレスを取り出し、`u64`に変換し、`physical_memory_offset`に足すことでそのページテーブルフレームに対応する仮想アドレスを得ます。最後に、`as_mut_ptr`メソッドを使ってこの仮想アドレスを`*mut PageTable`生ポインタに変換し、これから`&mut PageTable`参照を作りますここがunsafe`&`参照ではなく`&mut`参照にしているのは、後でこのページテーブルを変更するためです。
Rustは`unsafe fn`の中身全体を大きな`unsafe`ブロックであるかのように扱うので、ここでunsafeブロックを使う必要はありません。これでは、unsafeを意図した最後の行より前の行に間違ってunsafeな操作を書いても気づけないので、コードがより危険になります。また、どこがunsafeな操作であるのかを探すのも非常に難しくなります。そのため、この挙動を変更する[RFC](https://github.com/rust-lang/rfcs/pull/2585)が提案されています。
この関数を使って、レベル4テーブルのエントリを出力してみましょう
```rust
@@ -640,9 +638,11 @@ use x86_64::structures::paging::OffsetPageTable;
/// 名称を持つこと (mutable aliasingといい、動作が未定義)
/// につながるため、この関数は一度しか呼び出してはならない。
pub unsafe fn init(physical_memory_offset: VirtAddr) -> OffsetPageTable<'static> {
unsafe {
let level_4_table = active_level_4_table(physical_memory_offset);
OffsetPageTable::new(level_4_table, physical_memory_offset)
}
}
// これは非公開にする
unsafe fn active_level_4_table(physical_memory_offset: VirtAddr)

View File

@@ -399,14 +399,12 @@ pub unsafe fn active_level_4_table(physical_memory_offset: VirtAddr)
let virt = physical_memory_offset + phys.as_u64();
let page_table_ptr: *mut PageTable = virt.as_mut_ptr();
&mut *page_table_ptr // unsafe
unsafe { mut *page_table_ptr }
}
```
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 a `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 unsafe operations in between safe 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
@@ -632,9 +630,11 @@ use x86_64::structures::paging::OffsetPageTable;
/// `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> {
unsafe {
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)

View File

@@ -409,14 +409,12 @@ pub unsafe fn active_level_4_table(physical_memory_offset: VirtAddr)
let virt = physical_memory_offset + phys.as_u64();
let page_table_ptr: *mut PageTable = virt.as_mut_ptr();
&mut *page_table_ptr // unsafe
unsafe { &mut *page_table_ptr }
}
```
首先,我们从`CR3`寄存器中读取活动的4级表的物理帧。然后我们取其物理起始地址将其转换为`u64`,并将其添加到`physical_memory_offset`中,得到页表框架映射的虚拟地址。最后,我们通过`as_mut_ptr`方法将虚拟地址转换为`*mut PageTable`原始指针,然后不安全地从它创建一个`&mut PageTable`引用。我们创建一个`&mut`引用,而不是`&`引用,因为我们将在本篇文章的后面对页表进行突变。
我们不需要在这里使用不安全块因为Rust把一个 `不安全 fn` 的完整主体当作一个大的 `不安全`块。这使得我们的代码更加危险,因为我们可能会在不知不觉中在前几行引入不安全操作。这也使得在安全操作之间发现不安全操作的难度大大增加。有一个[RFC](https://github.com/rust-lang/rfcs/pull/2585)可以改变这种行为。
现在我们可以用这个函数来打印第4级表格的条目。
```rust
@@ -638,9 +636,11 @@ use x86_64::structures::paging::OffsetPageTable;
/// 传递的`physical_memory_offset`处被映射到虚拟内存。另
/// 外,这个函数必须只被调用一次,以避免别名"&mut "引用(这是未定义的行为)。
pub unsafe fn init(physical_memory_offset: VirtAddr) -> OffsetPageTable<'static> {
unsafe {
let level_4_table = active_level_4_table(physical_memory_offset);
OffsetPageTable::new(level_4_table, physical_memory_offset)
}
}
// 私下进行
unsafe fn active_level_4_table(physical_memory_offset: VirtAddr)

View File

@@ -538,8 +538,10 @@ impl LinkedListAllocator {
/// 有効でヒープが未使用であることを保証しなければならないからである。
/// このメソッドは一度しか呼ばれてはならない。
pub unsafe fn init(&mut self, heap_start: usize, heap_size: usize) {
unsafe {
self.add_free_region(heap_start, heap_size);
}
}
/// 与えられたメモリ領域をリストの先頭に追加する。
unsafe fn add_free_region(&mut self, addr: usize, size: usize) {
@@ -581,10 +583,12 @@ impl LinkedListAllocator {
let mut node = ListNode::new(size);
node.next = self.head.next.take();
let node_ptr = addr as *mut ListNode;
unsafe {
node_ptr.write(node);
self.head.next = Some(&mut *node_ptr)
}
}
}
```
このメソッドはメモリ領域のアドレスと大きさを引数として取り、リストの先頭にそれを追加します。まず、与えられた領域が`ListNode`を格納するのに必要なサイズとアラインメントを満たしていることを確認します。次に、ノードを作成し、それを以下のようなステップでリストに追加します:
@@ -715,8 +719,10 @@ unsafe impl GlobalAlloc for Locked<LinkedListAllocator> {
let alloc_end = alloc_start.checked_add(size).expect("overflow");
let excess_size = region.end_addr() - alloc_end;
if excess_size > 0 {
unsafe {
allocator.add_free_region(alloc_end, excess_size);
}
}
alloc_start as *mut u8
} else {
ptr::null_mut()
@@ -727,7 +733,7 @@ unsafe impl GlobalAlloc for Locked<LinkedListAllocator> {
// レイアウト調整を行う
let (size, _) = LinkedListAllocator::size_align(layout);
self.lock().add_free_region(ptr as usize, size)
unsafe { self.lock().add_free_region(ptr as usize, size) }
}
}
```
@@ -959,9 +965,11 @@ impl FixedSizeBlockAllocator {
/// ヒープが未使用であることを保証しなければならないからである。
/// このメソッドは一度しか呼ばれてはならない。
pub unsafe fn init(&mut self, heap_start: usize, heap_size: usize) {
unsafe {
self.fallback_allocator.init(heap_start, heap_size);
}
}
}
```
`new`関数がするのは、`list_heads`配列を空のノードで初期化し、`fallback_allocator`として[`empty`]で空の連結リストアロケータを作ることだけです。`EMPTY`定数が必要なのは、Rustコンパイラに配列を定数値で初期化したいのだと伝えるためです。配列を直接`[None; BLOCK_SIZES.len()]`で初期化するとうまくいきません──なぜなら、そうするとコンパイラは`Option<&'static mut ListNode>``Copy`トレイトを実装していることを要求するようになるのですが、そうはなっていないからです。これは現在のRustコンパイラの制約であり、将来解決するかもしれません。
@@ -1112,15 +1120,19 @@ unsafe fn dealloc(&self, ptr: *mut u8, layout: Layout) {
assert!(mem::size_of::<ListNode>() <= BLOCK_SIZES[index]);
assert!(mem::align_of::<ListNode>() <= BLOCK_SIZES[index]);
let new_node_ptr = ptr as *mut ListNode;
unsafe {
new_node_ptr.write(new_node);
allocator.list_heads[index] = Some(&mut *new_node_ptr);
}
}
None => {
let ptr = NonNull::new(ptr).unwrap();
unsafe {
allocator.fallback_allocator.deallocate(ptr, layout);
}
}
}
}
```
`alloc`と同じように、まず`lock`メソッドを使ってアロケータの可変参照を得て、`list_index`関数で与えられた`Layout`に対応するブロックリストを得ます。インデックスが`None`なら、`BLOCK_SIZES`にはサイズの合うブロックサイズがなかった、つまりこの割り当てが代替アロケータによって行われたことを意味します。従って、代替アロケータの[`deallocate`][`Heap::deallocate`]を使ってメモリを解放します。このメソッドは`*mut u8`ではなく[`NonNull`]を受け取るので、先にポインタを変換しておく必要があります(ここの`unwrap`はポインタがヌル値だったときのみ失敗するのですが、コンパイラが`dealloc`を呼ぶときにはそれは決して起きないはずです)。

View File

@@ -537,8 +537,10 @@ impl LinkedListAllocator {
/// heap bounds are valid and that the heap is unused. This method must be
/// called only once.
pub unsafe fn init(&mut self, heap_start: usize, heap_size: usize) {
unsafe {
self.add_free_region(heap_start, heap_size);
}
}
/// Adds the given memory region to the front of the list.
unsafe fn add_free_region(&mut self, addr: usize, size: usize) {
@@ -580,10 +582,12 @@ impl LinkedListAllocator {
let mut node = ListNode::new(size);
node.next = self.head.next.take();
let node_ptr = addr as *mut ListNode;
unsafe {
node_ptr.write(node);
self.head.next = Some(&mut *node_ptr)
}
}
}
```
The method takes the address and size of a memory region as an argument and adds it to the front of the list. First, it ensures that the given region has the necessary size and alignment for storing a `ListNode`. Then it creates the node and inserts it into the list through the following steps:
@@ -714,8 +718,10 @@ unsafe impl GlobalAlloc for Locked<LinkedListAllocator> {
let alloc_end = alloc_start.checked_add(size).expect("overflow");
let excess_size = region.end_addr() - alloc_end;
if excess_size > 0 {
unsafe {
allocator.add_free_region(alloc_end, excess_size);
}
}
alloc_start as *mut u8
} else {
ptr::null_mut()
@@ -726,7 +732,7 @@ unsafe impl GlobalAlloc for Locked<LinkedListAllocator> {
// perform layout adjustments
let (size, _) = LinkedListAllocator::size_align(layout);
self.lock().add_free_region(ptr as usize, size)
unsafe { self.lock().add_free_region(ptr as usize, size) }
}
}
```
@@ -958,7 +964,7 @@ impl FixedSizeBlockAllocator {
/// heap bounds are valid and that the heap is unused. This method must be
/// called only once.
pub unsafe fn init(&mut self, heap_start: usize, heap_size: usize) {
self.fallback_allocator.init(heap_start, heap_size);
unsafe { self.fallback_allocator.init(heap_start, heap_size); }
}
}
```
@@ -1111,15 +1117,19 @@ unsafe fn dealloc(&self, ptr: *mut u8, layout: Layout) {
assert!(mem::size_of::<ListNode>() <= BLOCK_SIZES[index]);
assert!(mem::align_of::<ListNode>() <= BLOCK_SIZES[index]);
let new_node_ptr = ptr as *mut ListNode;
unsafe {
new_node_ptr.write(new_node);
allocator.list_heads[index] = Some(&mut *new_node_ptr);
}
}
None => {
let ptr = NonNull::new(ptr).unwrap();
unsafe {
allocator.fallback_allocator.deallocate(ptr, layout);
}
}
}
}
```
Like in `alloc`, we first use the `lock` method to get a mutable allocator reference and then the `list_index` function to get the block list corresponding to the given `Layout`. If the index is `None`, no fitting block size exists in `BLOCK_SIZES`, which indicates that the allocation was created by the fallback allocator. Therefore, we use its [`deallocate`][`Heap::deallocate`] to free the memory again. The method expects a [`NonNull`] instead of a `*mut u8`, so we need to convert the pointer first. (The `unwrap` call only fails when the pointer is null, which should never happen when the compiler calls `dealloc`.)