Chromium 文档翻译:PartitionAlloc Design

This document describes PartitionAlloc at a high level. For documentation about its implementation, see the comments in partition_alloc.h.

本文档从较高的层次上描述了最新的 Google Chrome 浏览器(M89)中所使用的 PartitionAlloc 内存分配器的设计。如果要了解其实现的细节,可以查看 partition_alloc.h 头文件里的注释。

概述 (Overview)

PartitionAlloc is a memory allocator optimized for security, low allocation latency (when called appropriately), and good space efficiency (when called appropriately). This document aims to help you understand how PartitionAlloc works so that you can use it effectively.

PartitionAlloc 是一个针对安全性、分配时延以及空间分配效率进行优化的内存分配器。本文档旨在帮助您了解 PartitionAlloc 的工作方式,有助于您高效地使用它。

分区和桶 (Partitions And Buckets)

A partition is a heap that contains certain object types, objects of certain sizes, or objects of a certain lifetime (as the caller prefers). Callers can create as many partitions as they need. Each partition is separate and protected from any other partitions.

一个分区 (partition) 是一个包含某些特定的类型、大小、生命周期的对象的堆,如调用者所期望的那样。调用者可以根据需要创建任意数量的分区。每个分区都是独立的,不受其他分区的影响(保护)。

Each partition holds multiple buckets. A bucket is a region in a partition that contains similar-sized objects.

每个分区都包含多个存储桶 (buckets)。每个桶是分区中包含相似大小的对象的区域。

PartitionAlloc aligns each object allocation with the closest bucket size. For example, if a partition has 3 buckets for 64 bytes, 256 bytes, and 1024 bytes, then PartitionAlloc will satisfy an allocation request for 128 bytes by rounding it up to 256 bytes and allocating from the second bucket.

PartitionAlloc 内存分配器将每个对象的内存分配与最接近其大小的存储桶对齐。例如,如果一个分区有三个存储桶,分别存储大小为 64 Bytes, 256 Bytes, 和 1024 Bytes 的对象,则 PartitionAlloc 对于一个 128 Bytes 的分配请求的处理方式是找到能满足其大小要求的最小的桶 ,即从第二个(256 Bytes)存储桶中进行分配。

性能 (Performance)

The current implementation is optimized for the main thread use-case. For example, PartitionAlloc doesn’t have threaded caches.

当前实现针对主线程用例进行了优化。例如,PartitionAlloc 没有线程缓存。

PartitionAlloc is designed to be extremely fast in its fast paths. The fast paths of allocation and deallocation require just 2 (reasonably predictable) branches. The number of operations in the fast paths is minimal, leading to the possibility of inlining.

PartitionAlloc 的快速路径被设计得非常快。分配和重新分配的快速路径仅需要 2 个(合理可预测的)分支。快速路径中的操作数量很少,相关代码更可能被内联。

For an example of how to use partitions to get good performance and good safety, see Blink’s usage, as described in wtf/allocator/

有关如何使用分区来获取高性能和高安全性的示例,请参见 Blink 的使用说明,如 wtf/allocator/ 中所述。

Large allocations (> kMaxBucketed == 960KB) are realized by direct memory mmapping. This size makes sense because 960KB = 0xF0000. The next larger bucket size is 1MB = 0x100000 which is greater than 1/2 the available space in a SuperPage meaning it would not be possible to pack even 2 sequential allocations in a SuperPage.

较大(> kMaxBucketed == 960KB)的内存分配操作被实现为内存映射。这个 size 是有意义的,因为 960KB = 0xF0000。下一个更大的存储桶大小被设置为 1MB = 0x100000,它大于 SuperPage 中可用空间的 1/2,这意味着及时在 SuperPage 中也不能打包两个连续的内存分配。

PartitionRoot<internal::ThreadSafe>::Alloc() acquires a lock for thread safety. (The current implementation uses a spin lock on the assumption that thread contention will be rare in its callers. The original caller was Blink, where this is generally true. Spin locks also have the benefit of simplicity.)

PartitionRoot<internal::ThreadSafe>::Alloc()  需要获取一个用于保证线程安全的锁。(当前的实现中使用了自旋锁,是出于线程间竞争在其调用方很少发生的假设。PartitionAlloc 分配器的原始的调用者是 Blink,因此这个假设通常情况下是成里的。自旋锁还具有简单易用的优点。)

Callers can get thread-unsafe performance using a PartitionRoot<internal::NotThreadSafe>::Alloc() or otherwise using PartitionAlloc<internal::NotThreadSafe>. Callers can also arrange for low contention, such as by using a dedicated partition for single-threaded, latency-critical allocations.

调用者也可以使用 PartitionRoot<internal::NotThreadSafe>::Alloc()  或者 PartitionAlloc<internal::NotThreadSafe> 来使用线程不安全的分配器以提升性能。调用方还选择使用争用率低的分配,例如使用专用分区进行单线程的、延迟敏感的分配操作。

Because PartitionAlloc guarantees that address space regions used for one partition are never reused for other partitions, partitions can eat a large amount of virtual address space (even if not of actual memory).

由于 PartitionAlloc 分配器保证一个分区所使用的地址空间区域永远不会再被用于其他的分区,因此分区会占用大量的虚拟地址空间(即使没有 map 到实际的物理内存)。

Mixing various random objects in the same partition will generally lead to lower efficiency. For good performance, group similar objects into the same partition.


安全性 (Security)

Security is one of the most important goals of PartitionAlloc.

安全性是 PartitionAlloc 的最重要目标之一。

PartitionAlloc guarantees that different partitions exist in different regions of the process’ address space. When the caller has freed all objects contained in a page in a partition, PartitionAlloc returns the physical memory to the operating system, but continues to reserve the region of address space. PartitionAlloc will only reuse an address space region for the same partition.

PartitionAlloc 保证不同的分区分布在进程地址空间的不同区域。当调用者释放了某个分区页面中的所有对象,PartitionAlloc会将相应的物理内存归还给操作系统,但仍保留地址空间区域。Partition 将仅对同一分区重用地址空间区域。(即某一个分区的地址空间区域不会更改)。

PartitionAlloc also guarantees that:

  • Linear overflows cannot corrupt into the partition. (There is a guard page at the beginning of each partition.)
  • Linear overflows cannot corrupt out of the partition. (There is a guard page at the end of each partition.)
  • Linear overflow or underflow cannot corrupt the allocation metadata. PartitionAlloc records metadata in a dedicated region out-of-line (not adjacent to objects).
  • Objects of different sizes will likely be allocated in different buckets, and hence at different addresses. One page can contain only similar-sized objects.
  • Dereference of a freelist pointer should fault.
  • Partial pointer overwrite of freelist pointer should fault.
  • Large allocations have guard pages at the beginning and end.

PartitionAlloc 还能够保证:

  • 线性上溢不会破坏当前分区。(每个分区的头部都有一个保护页)
  • 线性上溢不会破坏其他分区。(每个分区的尾部都有一个保护页)
  • 线性上溢或者下溢不会破坏分配元数据。PartitionAlloc 将分配元数据离线记录在专用区域中(不与对象相邻)。
  • 不同大小的对象可能会被分配到不同的存储桶中,并因此被分配在不同的地址。一个物理内存页只能包含大小相似的对象。
  • 取消引用一个 FreeList 的指针将会出错。(避免内存泄漏)
  • FreeList 的指针被部分覆盖将会导致错误。
  • 较大的内存分配在开始和结束都有保护页。

内存地址对齐 (Alignment)

PartitionAlloc doesn’t have explicit support for a posix_memalign() type call, however it provides some guarantees on the alignment of returned pointers.

PartitionAlloc 没有显式地支持 posix_memalign() 类型的调用,但是它能为返回的内存地址的对齐提供一些保障。

All pointers are aligned on the smallest allocation granularity, namely sizeof(void*). Additionally, for power-of-two sized allocations, the behavior depends on the compilation flags:

  • With DCHECK_IS_ON(), returned pointers are never guaranteed to be aligned on more than 16 bytes.
  • Otherwise, the returned pointer is guaranteed to be aligned on min(allocation_size, system page size).

所有的指针都会被按照最小的分配粒度(即 sizeof(void *))对齐。此外,对于大小为 2 的幂次的内存分配,其行为取决于编译标记:

  • 使用 DCHECK_IS_ON(),绝不能保证返回的指针对齐的字节数超过 16 个;
  • 否则返回的指针必须保证与  min(allocation_size, system page size) 对齐.

See the tests in for more details.

更多的详细信息请参见 中的单元测试用例。

ASLR 地址空间随机化

ASLR,全称为 Address Space Layout Randomization,地址空间布局随机化。该技术在 kernel 2.6.12 中被引入到 Linux 系统,它将进程的某些内存空间地址进行随机化来增大入侵者预测目的地址的难度,从而降低进程被成功入侵的风险。当前 Linux、Windows 等主流操作系统都已经采用该项技术。

Linux 平台上 ASLR 分为 0,1,2 三级,用户可以通过内核参数 randomize_va_space 进行等级控制,不同级别的含义如下:

  • 0 = 关
  • 1 = 半随机;共享库、栈、mmap() 以及 VDSO 将被随机化
  • 2 = 全随机;除了 1 中所述,还会随机化 heap

注:系统默认开启 2 全随机模式,PIE 会影响 heap 的随机化。

通过读写 /proc/sys/kernel/randomize_va_space 内核文件可以查看或者修改 ASLR 等级:

开启 ASLR,在每次程序运行时的时候,装载的可执行文件和共享库都会被映射到虚拟地址空间的不同地址处;而关掉 ASLR,则可以保证每次运行时都会被映射到虚拟地址空间的相同地址处。

关掉 ASLR,可以保证在可执行程序和共享库不发生变更的情况下,每次执行时的进程地址空间映射表的一致。我们可通过运行时某动态符号(不知其名)的地址,减去其所在的共享库在地址映射表中起始地址,算出它相对于共享库数据段中的偏移,然后借助 objdump、readelf, nm 等工具查看对应 ELF 文件中全局符号的相对偏移,就可以反推出该符号的名字。这种调试手段对于那些被 strip 掉符号表的程序而言非常有效。

当使用  gdb 调试一个程序时,GDB 会自动关掉 ASLR。可以通过以下命令将它打开:


Linux 下的动态链接/装载器 (,*)  支持通过环境变量来改变程序运行时的动态链接行为,灵活使用会给调试工作带来很多方便。

  • LD_BIND_NOT 标记;如果设定,则会执行延迟绑定,即决议 (resolve) 某个符号之后,不更新全局偏移表 (GOT) 和过程链接表 (PLT)
  • LD_BIND_NOW 标记;如果设定,则会指定立即绑定,即程序启动后决议所有的符号,并更新 GOT 和 PLT
  • LD_DEBUG 输出动态链接器的 debug 信息,设为 all 将会输出所有的调试信息,设为 help 将会输出帮助信息
  • LD_DEBUG_PATH 指定 LD_DEBUG 输出的文件路径;如果未指定,默认输出到标准输出
  • LD_DYNAMIC_WEAK 允许弱符号 (weak symbols) 被外部符号覆盖 (旧版 glibc 的行为)
  • LD_HWCAP_MASK 设置硬件、平台兼容性的掩码
  • LD_LIBRARY_PATH 一个用冒号分隔的列表,用来指定运行时动态库的搜索路径
  • LD_PRELOAD 一个用空格分隔的列表,用来指定程序在运行时首先被装载的共享库。可利用这个特性来覆盖其他共享库中定义的函数。对于设定了强制位 (setuid/setgid) 的可执行程序,只有在标准搜索路径里面并且也被设定强制位 (setuid) 的共享库才能被装载。
  • LD_ORIGIN_PATH 指向可执行文件的系统路径
  • LD_PROFILE 指定要分析的共享库
  • LD_PROFILE_OUTPUT 指定共享库分析结果的输出文件;如果未指定,默认输出到标准输出
  • LD_SHOW_AUXV 显示从内核传递的辅助数组
  • LD_TRACE_LOADED_OBJECTS 标记;如果设定,将会列出依赖性而不是实际运行(如 ldd)
  • LD_WARN 标记;如果设定,将会警告未定义符号
  • LD_VERBOSE 标记;如果设置,则在查询有关程序的信息时输出有关程序的符号版本控制信息

注:以上环境变量只支持 glibc 2.2 及以上版本。

通过 man 手册可以查看有关动态链接器的更详细的信息: