OS 开发备忘2

Rust OS 开发备忘2
OS 开发备忘2

实现堆内存

本地变量与栈内存

outer 方法调用 inner 方法的栈,从上至下。 本地变量

inner 方法执行完成后。 inner执行完成后

静态变量

静态变量存储在独立于堆栈的固定内存位置。

该内存位置在编译时由链接器分配,并在可执行文件中编码。

静态变量

动态内存

本地变量只生存于作用域内,静态变量全局可用、不能够回收、所有权不清晰。

两者都有一个固定的大小,所以无法存储会动态增长的变量。

为了解决这些问题,编程语言都会引入第三块内存区域: heap。

假设使用 allocate 和 deallocate 来分配和释放内存:

heap

在调用 deallocate 之后,内存变成如下图所示:

释放之后

所以相对于静态变量来说,z[1] 这块内存区域,我们可以复用。

但是 z[0] 和 z[2] 永远不释放,这就造成了内存泄漏。

其它错误

除了内存泄漏,但它并不会使我们的程序更易受到攻击,还有两种类型的错误会有更严重的后果:

  1. use-after-free: 在 deallocate 之后尝试使用这个变量,这种错误会导致无法预料的行为,经常被攻击者利用来执行任意代码。
  2. double-free: 释放两次,可能会释放掉一个在 deallocate 之后又重新分配的地址,也有可能导致 use-after-free 问题。

即使是最好的程序员,也无法永远不出错误地处理这些分配和释放流程,而且每年都会有新的错误, 比如可以在搜索引擎尝试搜索 use-after-free 2024,你可以把 2024 换成当年的年份。

为了解决这些问题,java 和 python 会使用垃圾回收机制,核心思想是,编码者无需手动写 allocate 和 deallocate,而是定时暂停、 扫描程序中未使用的堆变量,然后释放掉这些内存。这样,以上错误就不会再出现,缺点是定时扫描导致的性能开销,以及长时间的暂停。

rust 使用了不一样的解决方法:它使用所有权设计,来在编译时检查动态内存操作的正确性,于是rust消灭了垃圾回收,当然也就没有性能开销了。 另一个好处是,编码者仍然对内存拥有细粒度的控制,就像 C、C++,缺点就是你会有很多编译错误需要解决。

rust 中的分配

Rust 中最重要的类型之一是 Box,它其实是堆变量的封装,使用 Box::new 接收一个变量,执行 allocate 分配一个变量大小的区域,然后将变量 的值转移到堆中。要释放这块区域,Box 中还有 Drop 特质,它会调用 deallocate。

光有 Box 肯定不够,下面的代码块,z[1]的引用被分配给了x(这被称为 borrow),紧接着z会被释放,但是你可能在代码块之外调用x。

1let x = {
2    let z = Box::new([1,2,3]);
3    &z[1]
4}; // z goes out of scope and `deallocate` is called
5println!("{}", x);

于是所有权发挥作用了,你会遇到编译错误: img.png

borrow 一个变量和现实生活中的一样,你没有所有权,不能销毁它。rust 通过检查在一个变量销毁前,所有的 borrow 都结束,来确保不会发生 use-after-free。

rust 的这一切都是在编译期发生的,因此不会像在 C 中手写内存管理一样产生性能开销。

实现一个操作系统的堆内存

实现堆内存,需要如下几步:

  1. 创建一块内存区域
    1. 确定一块虚拟内存区域
    2. 将这块区域映射到物理 frame
  2. 创建一个 allocator

1.1 确定内存区域

一个开始地址,一个大小,就可以确定一块区域。开始地址可以随便写,只要不与程序中的其它内存区域冲突。

1pub const HEAP_START: usize = 0x_4444_4444_0000;
2pub const HEAP_SIZE: usize = 100 * 1024; // 100 KiB

1.2 映射到物理地址

1linked_list_allocator = "0.9.0"
 1// lib/allocator.rs
 2use alloc::alloc::GlobalAlloc;
 3use linked_list_allocator::LockedHeap;
 4use x86_64::{
 5    structures::paging::{
 6        FrameAllocator, Mapper, mapper::MapToError, Page, PageTableFlags, Size4KiB,
 7    },
 8    VirtAddr,
 9};
10
11pub const HEAP_START: usize = 0x_4444_4444_0000;
12pub const HEAP_SIZE: usize = 100 * 1024; // 100 KiB
13
14pub fn init_heap(
15    mapper: &mut impl Mapper<Size4KiB>,
16    frame_allocator: &mut impl FrameAllocator<Size4KiB>,
17) -> Result<(), MapToError<Size4KiB>> {
18    let page_range = {
19        let heap_start = VirtAddr::new(HEAP_START as u64);
20        let heap_end = heap_start + HEAP_SIZE - 1u64;
21        let heap_start_page = Page::containing_address(heap_start);
22        let heap_end_page = Page::containing_address(heap_end);
23        Page::range_inclusive(heap_start_page, heap_end_page)
24    };
25
26    for page in page_range {
27        let frame = frame_allocator
28            .allocate_frame()
29            .ok_or(MapToError::FrameAllocationFailed)?;
30        let flags = PageTableFlags::PRESENT | PageTableFlags::WRITABLE;
31        unsafe {
32            mapper.map_to(page, frame, flags, frame_allocator)?.flush()
33        };
34    }
35
36    unsafe {
37        ALLOCATOR.lock().init(HEAP_START, HEAP_SIZE);
38    }
39
40    Ok(())
41}
42
43#[global_allocator]
44static ALLOCATOR: LockedHeap = LockedHeap::empty();
 1// src/main.rs
 2...
 3extern crate alloc;
 4entry_point!(kernel_main);
 5
 6// 非测试情况,在程序崩溃的时候调用的函数
 7#[cfg(not(test))] // new attribute
 8#[panic_handler]
 9fn panic(info: &PanicInfo) -> ! {
10   ...
11}
12
13#[cfg(test)]
14#[panic_handler]
15fn panic(info: &PanicInfo) -> ! {
16    rust_os::test_panic_handler(info)
17}
18
19fn kernel_main(boot_info: &'static BootInfo) -> ! {
20    // region use
21    ...
22    // endregion use
23
24    println!("Hello World{}", "!");
25
26    // region 异常与中断
27    rust_os::init();
28    // endregion
29
30    // region 内存映射
31    let phys_mem_offset = VirtAddr::new(boot_info.physical_memory_offset);
32    let mut mapper = unsafe { memory::init(phys_mem_offset) };
33    let mut frame_allocator = unsafe {
34        BootInfoFrameAllocator::init(&boot_info.memory_map)
35    };
36    allocator::init_heap(&mut mapper, &mut frame_allocator)
37        .expect("heap initialization failed");
38    // endregion 内存映射
39
40    // region 测试堆内存使用
41    let heap_value = Box::new(41);
42    println!("heap_value at {:p}", heap_value);
43
44    // create a dynamically sized vector
45    let mut vec = Vec::new();
46    for i in 0..500 {
47        vec.push(i);
48    }
49    println!("vec at {:p}", vec.as_slice());
50
51    // create a reference counted vector -> will be freed when count reaches 0
52    let reference_counted = Rc::new(vec![1, 2, 3]);
53    let cloned_reference = reference_counted.clone();
54    println!("current reference count is {}", Rc::strong_count(&cloned_reference));
55    core::mem::drop(reference_counted);
56    println!("reference count is {} now", Rc::strong_count(&cloned_reference));
57    // endregion 测试堆内存使用
58
59    // region 单元测试入口
60    ...
61    // endregion 单元测试入口
62
63    println!("It did not crash!");
64    rust_os::hlt_loop();
65}

我们创建了一个Box,一个Vec,一个Rc包裹的变量。

测试堆内存使用

如此就实现了堆内存。注意vec分配的地址比box便宜了0x800,不是因为box值大小是0x800字节大,而是vec需要增加容量,发生了重分配。

比如vector的容量是32,我们尝试添加一个新元素,vector就分配一个新的64大小的数组,将之前的元素全部拷贝过来,这一步就会释放旧的内存。

内存 Allocator 手动实现

主要目标

  • allocate 返回可用的内存区域;
  • deallocate 释放指定的内存区域。

Bump Allocator

也叫 Stack Allocator。

缺点:有严重的限制,只能一次释放所有内存,释放完成后才能服用内存。 有点:很快

如下图,next永远指向可用内存的开头位置。 20240823-bump-allocator.png

Linked List Allocator

缺点:性能差,可能要遍历链表才能找到一块区域。

linked list allocator 每个链表中的节点都保存有当前内存区域大小,以及下一块未使用内存的地址。

Fixed-Size Block Allocator

  • 多链表,每个链表固定大小。
  • 不足大小向上取整

缺点:容易造成空间浪费。

20240823-fixed-size-allocator.png

Linux 中使用的 Fixed-Size Block Allocator 变体

Slab Allocator 和 Buddy Allocator

Pin 与自引用 struct

通过 allocate 分配在堆内存上的变量,有一个固定的地址,并且同时有一个指针类型例如 Box 指向它。

但是在自引用结构中(上述提到的状态机,会用一种自引用结构实现),这种

 1fn main() {
 2    // 创建一个名为 SelfReferential 的 struct,其中包含一个指针字段 self_ptr。
 3    let mut heap_value = Box::new(SelfReferential {
 4       // 首先,使用空指针初始化, 然后使用 Box::new 在堆上分配该 struct。
 5       self_ptr: 0 as *const _,
 6    });
 7    // 然后,我们获取到堆分配结构的内存地址,并将其存储在一个 ptr 变量中。
 8    let ptr = &*heap_value as *const SelfReferential;
 9    // 最后,我们将 ptr 变量赋值给 self_ptr 字段,使 struct 成为自引用 struct。
10    heap_value.self_ptr = ptr;
11    println!("heap value at: {:p}", heap_value);
12    println!("internal reference: {:p}", heap_value.self_ptr);
13
14   // 破坏自引用结构,将 heap_value 转移到了 stack 上,上面的 self_ptr 此时变成了悬空引用 
15   let stack_value = mem::replace(&mut *heap_value, SelfReferential {
16      self_ptr: 0 as *const _,
17   });
18   
19   // 下面的两个值不一样,结构被破坏
20   println!("value at: {:p}", &stack_value);
21   println!("internal reference: {:p}", stack_value.self_ptr);
22}
23
24struct SelfReferential {
25    self_ptr: *const Self,
26}
27// heap value at: 0x55eaf7ad09b0
28// internal reference: 0x55eaf7ad09b0
29// value at: 0x7ffccbf74a58
30// internal reference: 0x55eaf7ad09b0

原因在于 Box 返回了一个 &mut T 的引用指向堆变量,因此就可以用 mem::replace 或者 mem::swap 等方法作废堆变量。

Pin<Box> and Unpin

1use core::marker::PhantomPinned; // 标记类型,唯一作用是不实现 UnPin 特质
2
3struct SelfReferential {
4    self_ptr: *const Self,
5    _pin: PhantomPinned // 只要有一个字段 UnPin,整个 struct 就是 Unpin 的
6}

如下代码将会按预料报错。

 1use std::mem;
 2use std::marker::PhantomPinned;
 3
 4fn main() {
 5    let mut heap_value = Box::pin(SelfReferential {
 6        self_ptr: 0 as *const _,
 7        _pin: PhantomPinned,
 8    });
 9    let ptr = &*heap_value as *const SelfReferential;
10    heap_value.self_ptr = ptr; // 报错 trait `DerefMut` is required to modify through a dereference
11    println!("heap value at: {:p}", heap_value);
12    println!("internal reference: {:p}", heap_value.self_ptr);
13    
14    // break it
15    // 报错 trait `DerefMut` is required to modify through a dereference
16    let stack_value = mem::replace(&mut *heap_value, SelfReferential {
17        self_ptr: 0 as *const _,
18        _pin: PhantomPinned,
19    });
20    println!("value at: {:p}", &stack_value);
21    println!("internal reference: {:p}", stack_value.self_ptr);
22}
23
24struct SelfReferential {
25    self_ptr: *const Self,
26    _pin: PhantomPinned,
27}

现在编译器虽然修复了 self_ptr 的 move 问题,但是我们现在也无法初始化了。要想修复此问题,需要:

 1use std::mem;
 2use std::marker::PhantomPinned;
 3use std::pin::Pin;
 4
 5fn main() {
 6    let mut heap_value = Box::pin(SelfReferential {
 7        self_ptr: 0 as *const _,
 8        _pin: PhantomPinned,
 9    });
10    let ptr = &*heap_value as *const SelfReferential;
11    
12    // safe because modifying a field doesn't move the whole struct
13    unsafe {
14        let mut_ref = Pin::as_mut(&mut heap_value);
15        Pin::get_unchecked_mut(mut_ref).self_ptr = ptr;
16    }
17
18    println!("heap value at: {:p}", heap_value);
19    println!("internal reference: {:p}", heap_value.self_ptr);
20    
21    // break it
22    // 现在这段代码,只有这里报错了。这里是我们需要它报错,我们不希望将 heap 上的变量 move 到 stack 上。
23    let stack_value = mem::replace(&mut *heap_value, SelfReferential {
24        self_ptr: 0 as *const _,
25        _pin: PhantomPinned,
26    });
27    println!("value at: {:p}", &stack_value);
28    println!("internal reference: {:p}", stack_value.self_ptr);
29}
30
31struct SelfReferential {
32    self_ptr: *const Self,
33    _pin: PhantomPinned,
34}