欢迎访问Sunbet官网(www.sunbet.us),Allbet欧博官网(www.ALLbetgame.us)!

首页Sunbet_安全防护正文

动态内存之堆的分派(二)_申博官方网站

b9e08c31ae1faa592019-11-1277Web安全

申博官方网站

申博官方网站是许多年来广受网民好评的娱乐网站。申博官方网站提供了游戏娱乐、交友互动、合理投资等等让客户流连忘返的娱乐方式。站内惊喜多多,随机厚礼等你拿!申博开户,存款简单,一秒入账。申博官方网是专程为您提供合理投资的一款网站平台。我们的目标是让没位加入我们的客户开心、放心的享受娱乐。还在犹豫什么?赶紧加入吧!

,

动态内存之堆的分派(一)

AllGlobalAlloctrait

GlobalAlloctrait定义了堆分派器必须供应的功用,该属性很特别,因为顺序员险些从不直接运用它。相反,当运用分派和鸠合范例的alloc时,编译器会自动地将恰当的挪用插进去到trait要领中。

因为我们须要为一切的分派器范例完成该属性,因而有必要细致研究其声明:

pub unsafe trait GlobalAlloc {
    unsafe fn alloc(&self, layout: Layout) -> *mut u8;
    unsafe fn dealloc(&self, ptr: *mut u8, layout: Layout);
    unsafe fn alloc_zeroed(&self, layout: Layout) -> *mut u8 { ... }
    unsafe fn realloc(
        &self,
        ptr: *mut u8,
        layout: Layout,
        new_size: usize
    ) -> *mut u8 { ... }
}

它定义了两个必须的要领alloc和dealloc,它们对应于我们在本例中运用的分派和deallocate函数:

alloc要领将规划实例作为参数,该实例形貌分派的内存应具有的所需大小和对齐体式格局。它返回原始指针,指向分派的内存块的第一个字节。alloc要领不是显式的毛病值,而是返回一个空指针来示意分派毛病。这有点不习惯,但它的长处是包装现有的体系分派器很轻易,因为它们运用雷同的商定。

dealloc要领是对应的,担任再次开释内存块。它吸收两个参数,一个是alloc返回的指针,另一个是用于分派的规划。

该trait还定义了两个要领alloc_zeroed和realloc,它们都有默许的完成:

alloc_zeroed要领等效于挪用alloc,然后将分派的内存块设置为零,这正是供应的默许完成所实行的。假如能够的话,分派器完成能够运用更有效的自定义完成来掩盖默许完成。

realloc要领许可增添或削减分派,默许完成分派一个具有所需大小的新内存块,并复制先前分派的一切内容。一样,分派器完成能够能够供应此要领的更有效完成,比方,假如能够的话,在恰当的位置增添/削减分派。

要注重的一件事是,trait自身和一切trait要领都被声明为不平安的:

将该trait声明为不平安的缘由是,顺序员必须保证分派器范例的trait完成是准确的。比方,alloc要领绝不能返回已在其他处所运用的内存块,因为这将致使未定义的行动。

类似地,要领不平安的缘由是挪用者在挪用要领时必须确保种种不变量,比方,传递给alloc的规划指定一个非零大小。这在实践中并不真正相干,因为编译器一般直接挪用这些要领,以确保满足需求。

DummyAllocator

如今我们知道了分派器范例应当供应什么,我们能够建立一个简朴的假造分派器。为此,我们建立了一个新的分派器模块:

// in src/lib.rs
pub mod allocator;

我们的假造分派器实行相对最小值来完成特性,而且老是在挪用alloc时返回一个毛病。看起来像如许:

// in src/allocator.rs
use alloc::alloc::{GlobalAlloc, Layout};
use core::ptr::null_mut;
pub struct Dummy;
unsafe impl GlobalAlloc for Dummy {
    unsafe fn alloc(&self, _layout: Layout) -> *mut u8 {
        null_mut()
    }
    unsafe fn dealloc(&self, _ptr: *mut u8, _layout: Layout) {
        panic!("dealloc should be never called")
    }
}

该组织不须要任何字段,因而我们将其建立为零尺寸范例。如上所述,我们老是从alloc返回空指针,这对应于分派破绽。因为分派器从不返回任何内存,所以对dealloc的挪用应当不会发作。因而,我们只是对dealloc要领觉得惊愕。 alloc_zeroed和realloc要领具有默许完成,因而我们无需为其供应完成。

如今我们有了一个简朴的分派器,然则我们依然必须通知Rust编译器它应当运用这个分派器,这就是#[global_allocator]属性发挥作用的处所。

#[global_allocator]属性

#[global_allocator]属性通知Rust编译器应当运用哪一个分派器实例作为全局堆分派器。该属性仅适用于完成GlobalAlloctrait的静态对象。让我们将Dummy分派器的一个实例注册为全局分派器:

// in src/lib.rs
#[global_allocator]
static ALLOCATOR: allocator::Dummy = allocator::Dummy;

因为Dummy分派器是零大小的范例,因而我们不须要在初始化表达式中指定任何字段。注重,#[global_allocator]模块不能在子模块中运用,因而我们须要将其放入lib.rs中。

如今,当我们尝试编译它时,第一个破绽应当消逝了。让我们修复第二个破绽:

error: `#[alloc_error_handler]` function required, but not found

[alloc_error_handler]属性

正如我们在议论GlobalAlloc特性时所相识的,alloc函数能够经由过程返回空指针来发出分派毛病的信号。题目是:Rust运转时应当怎样应对如许的分派失利?这就是#[alloc_error_handler]属性的作用。它指定在分派毛病发作时挪用的函数,类似于在发作紧要状况时挪用紧要处置惩罚顺序的体式格局。

让我们增加如许的函数来修复编译破绽:

// in src/lib.rs
#![feature(alloc_error_handler)] // at the top of the file
#[alloc_error_handler]
fn alloc_error_handler(layout: alloc::alloc::Layout) -> ! {
    panic!("allocation error: {:?}", layout)
}

alloc_error_handler函数依然不稳定,因而我们须要一个函数来启用它。该函数吸收一个参数:发作分派失利时传递给alloc的规划实例。我们没法处理该毛病,因而我们只对包含规划实例的音讯觉得惊愕。

运用此函数,应当修复编译毛病。如今我们能够运用分派范例和鸠合范例的alloc,比方我们能够运用一个框来分派堆上的一个值:

// in src/main.rs
extern crate alloc;
use alloc::boxed::Box;
fn kernel_main(boot_info: &'static BootInfo) -> ! {
    // […] print "Hello World!", call `init`, create `mapper` and `frame_allocator`
    let x = Box::new(41);
    // […] call `test_main` in test mode
    println!("It did not crash!");
    blog_os::hlt_loop();
}

请注重,我们也须要在main.rs中指定extern crate alloc语句。这是必须的,因为lib.rs和main.rs部分被视为零丁的crate。然则,我们不须要建立另一个#[global_allocator]静态变量,因为全局分派器适用于项目中的一切crate。现实上,在另一个crate中指定其他分派器将会发生破绽。

当我们运转上面的代码时,我们看到我们的alloc_error_handler函数被挪用:

动态内存之堆的分派(二)_申博官方网站  Web安全 第1张

挪用毛病处置惩罚顺序是因为Box::new函数隐式地挪用全局分派器的alloc函数,我们的假造分派器老是返回一个空指针,所以每次分派都邑失利。要处理这个题目,我们须要建立一个现实返回可用内存的分派器。

建立内核堆

在建立适宜的分派器之前,我们起首须要建立一个堆内存地区,分派器能够从中分派内存。为此,我们须要为堆地区定义一个假造内存局限,然后将该地区映照到物理帧。

第一步是为堆定义一个假造内存地区。我们能够挑选任何我们喜好的假造地点局限,只需它还没有效于其他内存地区即可。让我们将其定义为从地点0x_4444_4444_0000最先的内存,以便稍后能够轻松辨认堆指针:

// in src/allocator.rs
pub const HEAP_START: usize = 0x_4444_4444_0000;
pub const HEAP_SIZE: usize = 100 * 1024; // 100 KiB

我们如今将堆大小设置为100 KiB,假如我们在将来须要更多的空间,我们能够简朴地增添它。

假如我们如今尝试运用这个堆地区,则会涌现页面毛病,因为假造内存地区还没有映照到物理内存。为相识决这个题目,我们建立了一个init_heap函数,它运用我们在“分页完成”中引见的Mapper API来映照堆页面:

// in src/allocator.rs
use x86_64::{
    structures::paging::{
        mapper::MapToError, FrameAllocator, Mapper, Page, PageTableFlags, Size4KiB,
    },
    VirtAddr,
};
pub fn init_heap(
    mapper: &mut impl Mapper<Size4KiB>,
    frame_allocator: &mut impl FrameAllocator<Size4KiB>,
) -> Result<(), MapToError> {
    let page_range = {
        let heap_start = VirtAddr::new(HEAP_START as u64);
        let heap_end = heap_start + HEAP_SIZE - 1u64;
        let heap_start_page = Page::containing_address(heap_start);
        let heap_end_page = Page::containing_address(heap_end);
        Page::range_inclusive(heap_start_page, heap_end_page)
    };
    for page in page_range {
        let frame = frame_allocator
            .allocate_frame()
            .ok_or(MapToError::FrameAllocationFailed)?;
        let flags = PageTableFlags::PRESENT | PageTableFlags::WRITABLE;
        unsafe { mapper.map_to(page, frame, flags, frame_allocator)?.flush() };
    }
    Ok(())
}

该函数接收对Mapper和FrameAllocator实例的可变援用,经由过程运用Size4KiB作为泛型参数,这两个实例都被限制为4KiB页。该函数的返回值是一个效果,单元范例()作为胜利变量,MapToError作为毛病变量,这是Mapper::map_to要领返回的毛病范例。在这里重用毛病范例是有意义的,因为map_to要领是这个函数中毛病的主要泉源。

完成能够分为两部分:

建立页面局限:要建立我们要映照的页面局限,我们将HEAP_START指针转换为VirtAddr范例。然后,我们经由过程增加HEAP_SIZE从中计算出堆完毕地点。我们须要一个包含性的边境(堆末了一个字节的地点),因而我们减去1。接下来,我们运用containing_address函数将地点转换为页面范例。末了,我们运用page:: range_inclusion函数建立从最先到完毕的页面局限。

映照页面:第二步是映照我们方才建立的页面局限的一切页面。为此,我们运用for轮回遍历该局限内的页面。关于每一个页面,我们实行以下操纵:

我们运用FrameAllocator :: allocate_frame要领分派页面应映照到的物理框架。当没有更多的帧时,此要领返回None。我们经由过程运用Option :: ok_or要领将其映照到MapToError :: FrameAllocationFailed破绽来处置惩罚这类状况,然后运用问号运算符以在涌现破绽的状况下尽早返回。

利用 Z3 SMT 约束求解破解弱随机数算法

0x00 基础介绍 这篇文章主要是介绍Z3,它是麻省理工学院的一款SMT求解器,将讨论SMT求解器的一些典型用法,展示如何使用Z3的Python绑定生成约束,并提供使用此技术进行WebApp安全测试的示例。 SMT求解器应用范围非常广泛,适用于从预测genomic regulatory logic到designing microfluidic systems 和 validating cloud network configurations。从本质上讲,SMT求解器提供了一种正式的方法来探索和推理复杂的相互依赖的系统。 SMT求解器的一个重要应用是形式验证,例如,软件的符

我们为页面设置了必须的PRESENT标志和WRITABLE标志。运用这些标志,许可读取和写入接见,这关于堆内存是有意义的。

我们运用不平安的Mapper :: map_to要领在运动页面表中建立映照,该要领能够会失利,因而我们再次运用问号运算符将破绽转发给挪用方。胜利后,该要领将返回一个MapperFlush实例,我们能够运用该实例运用flush要领来更新转换后备缓冲区。

末了一步是从我们的kernel_main挪用此函数:

// in src/main.rs
fn kernel_main(boot_info: &'static BootInfo) -> ! {
    use blog_os::allocator; // new import
    use blog_os::memory::{self, BootInfoFrameAllocator};
    println!("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 = unsafe {
        BootInfoFrameAllocator::init(&boot_info.memory_map)
    };
    // new
    allocator::init_heap(&mut mapper, &mut frame_allocator)
        .expect("heap initialization failed");
    let x = Box::new(41);
    // […] call `test_main` in test mode
    println!("It did not crash!");
    blog_os::hlt_loop();
}

我们在这里显现上下文的完全功用,唯一的新行是blog_os :: allocator导入和对allocator :: init_heap函数的挪用。假如init_heap函数返回破绽,我们就会运用Result::expect要领,因为现在还没有适宜的要领来处置惩罚这个毛病。

如今,我们有一个预备运用的映照堆内存地区。 Box :: new挪用依然运用我们旧的Dummy分派器,因而运转它时,你依然会看到“内存不足”破绽。让我们经由过程运用恰当的分派器来处理此题目。

运用分派器crate

因为完成分派器有些庞杂,因而我们起首运用外部分派器crate。鄙人一篇文章中,我们将进修怎样完成本身的分派器。

no_std运用顺序的一个简朴分派器crate是linked_list_allocator crate,它的称号来自以下现实:它运用链接列表数据组织来跟踪开释的内存地区,下一篇文章将对这类要领举行更细致的诠释。

要运用crate,我们起首须要在Cargo.toml中增加对它的依靠:

# in Cargo.toml
[dependencies]
linked_list_allocator = "0.6.4"

然后,我们能够用crate供应的分派器替代假造分派器:

// in src/lib.rs
use linked_list_allocator::LockedHeap;
#[global_allocator]
static ALLOCATOR: LockedHeap = LockedHeap::empty();

该组织被命名为LockedHeap,因为它运用spin :: 互斥锁举行同步。这是必须的,因为多个线程能够同时接见ALLOCATOR静态对象。一如平常,在运用互斥锁时,我们须要注重不要不测致使死锁。这意味着我们不该当在中断处置惩罚顺序中实行任何分派,因为它们能够在恣意时候运转,而且能够会中断正在举行的分派。

仅将LockedHeap设置为全局分派器是不够的。缘由是我们运用了空的组织函数,该函数建立了一个没有任何后备内存的分派器。就像我们的假造分派器一样,它老是在分派时返回破绽。为相识决这个题目,我们须要在建立堆以后初始化分派器:

// in src/allocator.rs
pub fn init_heap(
    mapper: &mut impl Mapper<Size4KiB>,
    frame_allocator: &mut impl FrameAllocator<Size4KiB>,
) -> Result<(), MapToError> {
    // […] map all heap pages to physical frames
    // new
    unsafe {
        super::ALLOCATOR.lock().init(HEAP_START, HEAP_SIZE);
    }
    Ok(())
}

我们运用LockedHeap :: lock要领猎取对包装后的Heap实例的排他性援用,然后在该实例上以堆边境作为参数挪用init要领。主要的是在映照堆页面以后初始化堆,因为init函数已尝试写入堆内存。

初始化堆以后,我们如今能够运用内置alloc crate的一切分派和网络范例,而不会涌现破绽:

// in src/main.rs
use alloc::{boxed::Box, vec, vec::Vec, rc::Rc};
fn kernel_main(boot_info: &'static BootInfo) -> ! {
    // […] initialize interrupts, mapper, frame_allocator, heap
    // allocate a number on the heap
    let heap_value = Box::new(41);
    println!("heap_value at {:p}", heap_value);
    // create a dynamically sized vector
    let mut vec = Vec::new();
    for i in 0..500 {
        vec.push(i);
    }
    println!("vec at {:p}", vec.as_slice());
    // create a reference counted vector -> will be freed when count reaches 0
    let reference_counted = Rc::new(vec![1, 2, 3]);
    let cloned_reference = reference_counted.clone();
    println!("current reference count is {}", Rc::strong_count(&cloned_reference));
    core::mem::drop(reference_counted);
    println!("reference count is {} now", Rc::strong_count(&cloned_reference));
    // […] call `test_main` in test context
    println!("It did not crash!");
    blog_os::hlt_loop();
}

此代码示例显现Box,Vec和Rc范例的某些用法。关于Box和Vec范例,我们运用{:p}花样申明符来打印基础堆指针。为了展现Rc,我们建立一个援用计数的堆值,并在删除实例之前和以后运用Rc::strong_count函数来打印当前援用计数(运用core::mem::drop)。运转它时,我们看到以下内容:

动态内存之堆的分派(二)_申博官方网站  Web安全 第2张

正如预期的那样,我们看到Box和Vec值存在于堆中,如以0x_4444_4444开首的指针所示。援用计数值的行动也与预期的一样,在克隆挪用以后,援用计数为2,在删除一个实例以后,再次为1。

向量从偏移量0x800最先的缘由不是装crate的值大0x800字节,而是向量须要增添其容量时发作的从新分派。比方,当向量的容量为32且我们尝试增加下一个元素时,该向量在背景分派一个容量为64的新后备数组,并复制一切元素,然后开释旧的分派。

固然,在alloc crate中有更多的分派和网络范例,我们如今能够在我们的内核中运用,包含:

1. 线程平安援用计数的指针Arc;

2. 具有的字符串范例String和format!宏;

3. LinkedList;

4. 可增进的环形缓冲区VecDeque;

5. BinaryHeap优先级行列;

6. BTreeMap和BTreeSet。

当我们要完成线程列表,调理行列或支撑异步/守候时,这些范例将变得异常有效。

增加测试

为了确保我们不会不测损坏新的分派代码,我们应当为其增加集成测试。我们起首建立一个新的tests / heap_allocation.rs文件,其内容以下:

// in tests/heap_allocation.rs
#![no_std]
#![no_main]
#![feature(custom_test_frameworks)]
#![test_runner(blog_os::test_runner)]
#![reexport_test_harness_main = "test_main"]
extern crate alloc;
use bootloader::{entry_point, BootInfo};
use core::panic::PanicInfo;
entry_point!(main);
fn main(boot_info: &'static BootInfo) -> ! {
    unimplemented!();
}
#[panic_handler]
fn panic(info: &PanicInfo) -> ! {
    blog_os::test_panic_handler(info)
}

我们重用lib.rs中的test_runner和test_panic_handler函数。因为我们要测试分派,因而我们经由过程extern crate alloc语句启用了alloc crate。

主函数的完成以下:

// in tests/heap_allocation.rs
fn main(boot_info: &'static BootInfo) -> ! {
    use blog_os::allocator;
    use blog_os::memory::{self, BootInfoFrameAllocator};
    use x86_64::VirtAddr;
    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 = unsafe {
        BootInfoFrameAllocator::init(&boot_info.memory_map)
    };
    allocator::init_heap(&mut mapper, &mut frame_allocator)
        .expect("heap initialization failed");
    test_main();
    loop {}
}

它与main.rs中的kernel_main函数异常类似,不同之处在于我们不挪用println,不包含任何示例分派以及无条件挪用test_main。

如今我们预备增加一些测试用例,起首,我们增加一个测试,该测试运用Box实行简朴分派并搜检分派的值,以确保基础分派有效:

// in tests/heap_allocation.rs
use blog_os::{serial_print, serial_println};
use alloc::boxed::Box;
#[test_case]
fn simple_allocation() {
    serial_print!("simple_allocation... ");
    let heap_value = Box::new(41);
    assert_eq!(*heap_value, 41);
    serial_println!("[ok]");
}

最主要的是,这个测试能够考证有无发作分派毛病。

接下来,我们迭代地构建一个大的向量,来测试因为从新分派发生的大的分派和多个分派:

// in tests/heap_allocation.rs
use alloc::vec::Vec;
#[test_case]
fn large_vec() {
    serial_print!("large_vec... ");
    let n = 1000;
    let mut vec = Vec::new();
    for i in 0..n {
        vec.push(i);
    }
    assert_eq!(vec.iter().sum::<u64>(), (n - 1) * n / 2);
    serial_println!("[ok]");
}

我们经由过程与第n个部分和的公式举行比较来考证和,这使我们确信所分派的值都是准确的。

作为第三个测试,我们顺次建立10000个分派:

// in tests/heap_allocation.rs
#[test_case]
fn many_boxes() {
    serial_print!("many_boxes... ");
    for i in 0..10_000 {
        let x = Box::new(i);
        assert_eq!(*x, i);
    }
    serial_println!("[ok]");
}

此测试可确保分派器将开释的内存从新用于后续分派,不然分派器将耗尽内存。这似乎是一个显著的分派器需求,然则有些分派器设想并不如许做,如缓冲分派器的设想,我将鄙人一篇文章中诠释。

让我们运转新的集成测试:

> cargo xtest --test heap_allocation
[…]
Running 3 tests
simple_allocation... [ok]
large_vec... [ok]
many_boxes... [ok]

三个测试都胜利了!你还能够挪用cargo xtest(不带–test参数)来运转一切单元测试和集成测试。

总结

这篇文章引见了动态内存,并诠释了为何以及在那里须要动态内存。我们相识了Rust的借用搜检器怎样防备罕见破绽,并相识了Rust的分派API的事情体式格局。

在运用假造分派器建立了Rust分派器接口的最小完成以后,我们为内核建立了一个恰当的堆内存地区。为此,我们为堆定义了一个假造地点局限,然后运用上一篇文章中的Mapper和FrameAllocator将局限的一切页面映照到物理帧。

末了,我们增加了对linked_list_allocatorcrate的依靠性,以向内核增加恰当的分派器。有了这个分派器,我们就能够运用Box,Vec以及alloc crate中的其他分派和网络范例。

虽然我们已在本文中引见了堆分派支撑,然则我们将大部分事情留给了linked_list_allocatorcrate。鄙人一篇文章,我将细致引见怎样从头最先完成分派器,诠释我会引见多种能够的分派器设想,展现怎样完成它们的简朴版本,并申明其优缺点。

本文翻译自:https://os.phil-opp.com/heap-allocation/

网友评论