动态内存之堆的分派(一) | 申博官网
登录
  • 欢迎进入申博官网!
  • 如果您觉得申博官网对你有帮助,那么赶紧使用Ctrl+D 收藏申博官网并分享出去吧
  • 这里是申博官方网!
  • 申博官网是菲律宾sunbet官网品牌平台!
  • 申博开户专业品牌平台!

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

申博_安全预警 申博 25次浏览 未收录 0个评论

申博网

申博网开户仅需短短几秒,全天24小时无休止无限制免费注册,申博官网欢迎您的光临!

,

这篇文章我们会向你引见内核是怎样增加对堆分派的支撑,起首我会引见动态内存,并展现了借用搜检器怎样防备罕见的分派破绽。然后,它完成Rust的基础分派接口,建立一个堆内存地区,并设置一个分派器crate。在这篇文章的末端,内置alloc crate的一切分派和网络范例将对我们的内核可用。别的,此文所引见的完整源代码可以post-10 分支中找到。

部份和静态变量

我们如今在内核中运用两种范例的变量:部份变量和静态变量,部份变量存储在挪用客栈中,而且仅在四周的函数返回之前才有效。静态变量存储在牢固的内存位置,而且在顺序的全部生命周期中一向有效。

部份变量

部份变量存储在挪用客栈中,该客栈是支撑推入和弹出操纵的客栈数据结构。在每一个函数项上,被挪用函数的参数、返回所在和部份变量由编译器推送:

上面的示例显现了外部函数挪用内部函数后的挪用客栈,我们看到挪用客栈包含外部优先的部份变量。在内部挪用中,参数1和函数的返回所在被推送。然后将掌握权转移到内部,从而推送其部份变量。

内部函数返回后,它的挪用客栈部份再次弹出,只要外部的部份变量保存:

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

可以看到,内部的部份变量只在函数返回之前有效。当我们运用太长的值时,比方当我们尝试返回对部份变量的援用时,Rust编译器会强迫实行这些生存期并激发破绽:

fn inner(i: usize) -> &'static u32 {
    let z = [1, 2, 3];
    &z[i]
}

虽然在此示例中返回援用毫无意义,但在某些状况下,我们愿望变量的寿命比函数更长。当我们已在内核中看到过这类状况,当时我们试图加载一个中断形貌符表,而且必须运用一个静态变量来延伸生存期。

静态变量

静态变量存储在与客栈离开的牢固内存位置。链接器在编译时分派了此存储位置,并在可实行文件中举行了编码。静态变量在顺序的完整运行时中都有效,因而它们具有“静态寿命”,而且一向可以从部份变量中举行援用:

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

当上面示例中的内部函数返回时,它的挪用客栈的一部份将被烧毁。静态变量位于一个零丁的内存局限中,这个内存局限永久不会被烧毁,因而在返回今后,&Z[1]援用依然有效。

除了“静态生存期”以外,静态变量另有一个有效的属性,即它们的位置在编译时是已知的,因而接见它不须要援用。我们在println宏中运用了这个属性:经由过程在内部运用静态Writer,不须要&mut Writer援用即可挪用该宏,这在我们无法接见任何其他变量的异常处置惩罚顺序中异常有效。

然则,静态变量的此属性带来一个症结的瑕玷:默许状况下,它们是只读的。 Rust强迫实行此操纵,由于假如两个线程同时修正一个静态变量,就会发作数据合作。修正静态变量的唯一要领是将其封装在互斥范例中,这可以确保在任何时刻点都只存在单个&mut援用。我们已为静态VGA缓冲区写入器运用了互斥锁。

动态内存

部份变量和静态变量一同已异常壮大,而且支撑大多数用例。但是,我们发明它们都有各自的局限性:

1. 部份变量只存在到四周函数或块的末端,这是由于它们位于挪用客栈上,在四周的函数返回后被烧毁。

2. 静态变量老是在顺序的完整运行时存在,所以当不再须要它们时,没有办法接纳和重用它们的内存。另外,它们的一切权语义不明确,而且可以从一切函数接见,因而当我们想要修正它们时,须要运用互斥锁来庇护它们。

部份变量和静态变量的另一个限定是它们的大小是牢固的。因而,它们不能存储一个在增加更多元素时动态增进的鸠合。有人发起在Rust中运用未调解大小的rvalue,这将许可动态调解部份变量的大小,然则它们只在某些特定的状况下起作用。

为了防止这些瑕玷,编程言语一般支撑第三个内存地区来存储称为堆的变量。堆经由过程两个名为分派和开释的函数在运行时支撑动态内存分派。它的工作体式格局以下:分派函数返回指定大小的余暇内存块,可用来存储变量。然后,经由过程挪用带有该变量援用的deallocate函数开释该变量。

让我们来看一个例子:

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

在此,内部函数运用堆内存而不是静态变量来存储z。它起首分派所需大小的内存块,然后返回* mut u32原始指针。然后,它运用ptr :: write要领将数组[1,2,3]写入个中。在末了一步中,它运用偏移量函数来盘算指向第i个元素的指针,然后将其返回。注重,为了简朴起见,我们在这个示例函数中省略了一些必须的强迫范例转换和不平安的块。

Azure AD 环境的特权提拔破绽剖析

在今年的DEF CON和Troopers 中,我演示了Azure AD中存在的一个漏洞,其中管理员或本地同步帐户可以通过向程序分配凭据来进行特权提升。后来我再分析这个漏洞时,发现该漏洞实际上不是由Microsoft修复的,并且仍然存在使用默认Office 365程序进行提权的方法。在文章中,我会解释原因和提权方法。 0x01 Applications and Service Principals 在Azure AD中,程序主体和服务主体之间是有区别的。程序是应用程序的配置,而服务主体是实际上可以在Azure目录中拥有特权的安全对象。这会造成

分派的内存一向存在,直到经由过程挪用deallocate显式开释它。因而,纵然内部返回并烧毁了挪用客栈的一部份,返回的指针依然有效。与静态内存比拟,运用堆内存的长处是开释内存后可以重用它,这是经由过程外部的deallocate挪用完成的。因而挪用今后,状况是如许的:

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

我们看到z [1]槽又空了,可以从新用于下一个分派挪用。然则,我们也看到z [0]和z [2]从未被开释,由于我们从未开释过它们。这类破绽称为内存走漏,一般会致使顺序内存斲丧过量。设想一下,当我们在循环中重复挪用inner时会发作什么,这能够看起来很蹩脚,然则动态分派能够会发作更多风险的破绽范例。

罕见破绽

除了不幸的但不会使顺序轻易遭到进击的内存走漏外,另有两种罕见的破绽范例,其效果更为严重:

当我们意外埠在挪用deallocate今后继承运用一个变量时,我们就有了所谓的“开释后可重用”破绽。如许的破绽会致使未定义的行动,而且进击者常常可以利用它来实行恣意代码。

当我们不小心开释了一个变量两次时,就有一个两重开释破绽。这是有题目的,由于它能够会开释在第一个deallocate挪用今后在统一所在分派的另一个分派。因而,它能够会致使再次运用“开释后可重用”破绽。

这些范例的破绽是尽人皆知的,因而人们可以希冀人们如今已学会了怎样防止它们。然则,没有,依然常常发明此类破绽,比方,Linux中近来的这类“先用后用”破绽许可恣意代码实行。这表明纵然是最好的顺序员也不一定老是可以正确处置惩罚庞杂项目中的动态内存。

为了防止这些题目,很多言语(比方Java或Python)都运用称为垃圾接纳的手艺自动治理动态内存。其头脑是顺序员从不手动挪用deallocate。相反,顺序会按期停息并扫描未运用的堆变量,然后自动开释这些堆变量。因而,上述破绽永久不会发作。瑕玷是通例扫描的机能斲丧和能够的长停息时刻。

Rust采用了一种差别的要领来处理这个题目:它运用了一个称为一切权的观点,这个观点可以在编译时搜检动态内存操纵的正确性。因而,不须要垃圾网络来防止上述破绽,这意味着没有机能斲丧。这类要领的另一个长处是,顺序员依然可以细粒度地掌握动态内存的运用,就像运用C或c++一样。

RustRust中的分派体式格局

与顺序员手动挪用分派和开释差别,Rust规范库供应了隐式挪用这些函数的笼统范例。最主要的范例是Box,它是对堆分派值的笼统。它供应了一个Box::new constructor函数,该函数接收一个值,运用该值的大小挪用分派,然后将该值移动到堆上新分派的槽中。为了再次开释堆内存,Box范例完成了Droptrait以在超出局限时挪用deallocate:

{
    let z = Box::new([1,2,3]);
    […]
} // z goes out of scope and `deallocate` is called

此形式有一个新鲜的称号:资本猎取是初始化或简称为RAII。它起源于c++,用于完成称为std :: unique_ptr的类似笼统范例。

单靠这类范例还足以防备一切的开释后可重用破绽,由于顺序员依然可以在Box超出局限和响应的堆内存槽开释后保存援用:

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

这就是Rust的一切权发挥作用的处所,它为每一个援用分派一个笼统的生存期,这是该援用有效的局限。在上面的示例中,x援用是从z数组中猎取的,因而在z超出局限后,它将变成无效。以下所示,你会看到Rust编译器确切激发了破绽:

error[E0597]: `z[_]` does not live long enough
 --> src/main.rs:4:9
  |
2 |     let x = {
  |         - borrow later stored here
3 |         let z = Box::new([1,2,3]);
4 |         &z[1]
  |         ^^^^^ borrowed value does not live long enough
5 |     }; // z goes out of scope and `deallocate` is called
  |     - `z[_]` dropped here while still borrowed

起首,该术语能够会有些杂沓。援用值称为借入值,由于它类似于现实生活中的借用:你可以暂时接见某个对象,但须要在某个时刻将其返回,而且不得损坏它。经由过程搜检一切借用在对象被烧毁之前是不是已完毕,Rust编译器可以保证不会发作无用后运用状况。

Rust的一切权不仅可以防备运用后运用的破绽,而且可以像Java或Python如许的垃圾网络言语供应完整的内存平安性。别的,它保证线程平安,因而比多线程代码中的那些言语更平安。最主要的是,一切这些搜检都在编译时举行,因而与C中的手写内存治理比拟,没有运行时的斲丧。

用例

如今我们晓得Rust中动态内存分派的基础知识,然则什么时刻应当运用它呢?没有动态内存分派的内核已走得很远了,那末为何如今须要它呢?

起首,动态内存分派老是会带来一些机能斲丧,由于我们须要为每一个分派在堆上找到一个余暇插槽。因而,一般最好运用部份变量,尤其是在机能敏感的内核代码中。然则,在某些状况下,动态内存分派是最好挑选。

作为基础划定规矩,具有动态生存期或可变大小的变量须要动态内存。动态生存期最主要的范例是Rc,它盘算对其包装值的援用,并在一切援用超出局限后将其开释。具有大小可变的范例的示例包含Vec,String和其他鸠合范例,这些鸠合范例在增加更多元素时会动态增进。这些范例的工作体式格局是在它们变满时分派大批内存,将一切元素复制过来,然后作废分派旧分派。

关于我们的内核,我们最须要的是鸠合范例,比方,在今后的文章中完成多使命处置惩罚时,用于存储运动使命列表。

分派器界面

完成堆分派器的第一步是在内置的alloc crate上增加一个依靠项。与core crate一样,它也是规范库的一个子集,别的还包含了分派和网络范例。为了增加对alloc的依靠,我们在lib中增加了以下内容:

// in src/lib.rs

extern crate alloc;

与一般的依靠关联相反,我们不须要修正Cargo.toml,原因是alloc crate与Rust编译器一同作为规范库的一部份供应,因而我们只须要启用它即可。这就是extern crate语句的作用,曾,一切依靠项都须要extern crate语句,如今该语句是可选的。

在#[no_std]中,alloc crate在默许状况下是禁用的,原因是它有分外的请求。如今尝试编译项目时,我们可以将这些需求视为破绽:

error: no global memory allocator found but one is required; link to std or add
       #[global_allocator] to a static item that implements the GlobalAlloc trait.

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

发作第一个破绽是由于alloc crate须要堆分派器,该堆分派器是供应分派和消除分派功用的对象。在Rust中,堆分派器由GlobalAlloc trait形貌,该trait在破绽音讯中提到。要设置crate的堆分派器,必须将#[global_allocator]属性应用于完成GlobalAlloctrait的静态变量。

发作第二个破绽是由于分派挪用能够失利,最罕见的状况是没有更多可用内存。我们的顺序必须可以对这类状况作出反应,这就是#[alloc_error_handler]函数的作用。

鄙人一篇文章中,我们将详细形貌这些函数的属性。

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


申博|网络安全巴士站声明:该文看法仅代表作者自己,与本平台无关。版权所有丨如未注明 , 均为原创丨本网站采用BY-NC-SA协议进行授权
转载请注明动态内存之堆的分派(一)
喜欢 (0)
[]
分享 (0)
发表我的评论
取消评论
表情 贴图 加粗 删除线 居中 斜体 签到

Hi,您需要填写昵称和邮箱!

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址