计算机系统篇之虚拟内存(8):理解 glibc malloc 的工作原理(上)

Author: stormQ

Created: Wednesday, 18. November 2020 07:50PM

Last Modified: Wednesday, 25. November 2020 10:49PM



摘要

本文简单介绍了动态内存分配和 malloc 的基础知识,并通过示例初步研究了 glibc malloc 中 chunk 的内存布局,从而为更深入地探索其工作原理做好准备。

动态内存分配简介

为什么要使用动态内存分配?

程序使用动态内存分配的最重要原因是经常直到程序实际运行时,才知道某些数据结构的大小。

什么是动态内存分配器?

动态内存分配器(dynamic memory allocator)维护着一个进程的虚拟内存区域,称为堆(heap)。

动态内存分配器将堆看作一组不同大小的块(block)的集合来维护。每个块就是一个连续的虚拟内存片(chunk),要么是已分配的,要么是空闲的。

已分配的块显式地保留以供应用程序使用,空闲块可用来分配。空闲块保持空闲,直到它显示地被应用程序所分配。一个已分配的块保持已分配状态,直到它被释放。

动态内存分配器的类型?

根据负责释放已分配块的实体不同,动态内存分配器可以分为以下两种:

malloc 简介

C 语言中,malloc函数的实现因系统不同差异很大。本文以 glibc 2.31 版本为例,研究 glibc 中malloc的工作原理。

glibc 中的malloc用于分配堆内存。这里所说的“堆”是指广义上的堆,包括通过sbrk函数和mmap函数从操作系统申请的虚拟内存区域。

注:严格地讲,通过sbrk函数申请的虚拟内存区域称为堆,通过mmap函数申请的虚拟内存区域称为内存映射区域。


堆在进程虚拟地址空间中的分布

在查看堆在进程虚拟地址空间中的分布之前,我们先看下可执行目标文件中这些全局符号:_start_end__data_start_edata__bss_start 的意义。

首先,通过 gdb 启动可执行目标文件后,查看i files命令的输出(部分):

(gdb) i files
Symbols from "/home/test/vm/vm3_main".
Native process:
    Using the running image of child process 101968.
    While running this, GDB does not access memory from...
Local exec file:
    `/home/test/vm/vm3_main', file type elf64-x86-64.
    Entry point: 0x555555555180
    0x0000555555554318 - 0x0000555555554334 is .interp
    0x0000555555554338 - 0x0000555555554358 is .note.gnu.property
    0x0000555555554358 - 0x000055555555437c is .note.gnu.build-id
    0x000055555555437c - 0x000055555555439c is .note.ABI-tag
    0x00005555555543a0 - 0x00005555555543c4 is .gnu.hash
    0x00005555555543c8 - 0x0000555555554548 is .dynsym
    0x0000555555554548 - 0x0000555555554603 is .dynstr
    0x0000555555554604 - 0x0000555555554624 is .gnu.version
    0x0000555555554628 - 0x0000555555554648 is .gnu.version_r
    0x0000555555554648 - 0x0000555555554708 is .rela.dyn
    0x0000555555554708 - 0x00005555555547f8 is .rela.plt
    0x0000555555555000 - 0x000055555555501b is .init
    0x0000555555555020 - 0x00005555555550d0 is .plt
    0x00005555555550d0 - 0x00005555555550e0 is .plt.got
    0x00005555555550e0 - 0x0000555555555180 is .plt.sec
    0x0000555555555180 - 0x0000555555555455 is .text
    0x0000555555555458 - 0x0000555555555465 is .fini
    0x0000555555556000 - 0x0000555555556075 is .rodata
    0x0000555555556078 - 0x00005555555560bc is .eh_frame_hdr
    0x00005555555560c0 - 0x00005555555561c8 is .eh_frame
    0x0000555555557d70 - 0x0000555555557d78 is .init_array
    0x0000555555557d78 - 0x0000555555557d80 is .fini_array
    0x0000555555557d80 - 0x0000555555557f70 is .dynamic
    0x0000555555557f70 - 0x0000555555558000 is .got
    0x0000555555558000 - 0x0000555555558010 is .data
    0x0000555555558010 - 0x0000555555558018 is .bss
    0x00007ffff7fcf2a8 - 0x00007ffff7fcf2c8 is .note.gnu.property in /lib64/ld-linux-x86-64.so.2
# 省略...

接下来,分别打印上述全局符号的地址:

(gdb) p/x &_start
$1 = 0x555555555180
(gdb) p/x &_end
$2 = 0x555555558018
(gdb) p/x &__data_start
$3 = 0x555555558000
(gdb) p/x &_edata
$4 = 0x555555558010
(gdb) p/x &__bss_start
$5 = 0x555555558010

结合以上两个输出结果,我们可以得出以下结论:

可执行目标文件中的全局符号名称 该符号的地址的意义
_start 该符号的地址表示 .text section 的起始地址,即 .text section 中第一个字节的地址
__data_start 该符号的地址表示 .data section 的起始地址,即 .data section 中第一个字节的地址
_edata 该符号的地址表示 .data section 的结束地址,即 .data section 中最后一个字节后的第一个字节的地址
__bss_start 该符号的地址表示 .bss section 的起始地址,即 .bss section 中第一个字节的地址
_end 该符号的地址表示 .bss section 的结束地址,即 .bss section 中最后一个字节后的第一个字节的地址

最后,查看进程的内存映射情况:

(gdb) i proc mappings 
process 101968
Mapped address spaces:

          Start Addr           End Addr       Size     Offset objfile
      0x555555554000     0x555555555000     0x1000        0x0 /home/test/vm/vm3_main
      0x555555555000     0x555555556000     0x1000     0x1000 /home/test/vm/vm3_main
      0x555555556000     0x555555557000     0x1000     0x2000 /home/test/vm/vm3_main
      0x555555557000     0x555555558000     0x1000     0x2000 /home/test/vm/vm3_main
      0x555555558000     0x555555559000     0x1000     0x3000 /home/test/vm/vm3_main
      0x555555559000     0x55555557a000    0x21000        0x0 [heap]
      0x7ffff7dc4000     0x7ffff7de9000    0x25000        0x0 /usr/lib/x86_64-linux-gnu/libc-2.31.so
      0x7ffff7de9000     0x7ffff7f61000   0x178000    0x25000 /usr/lib/x86_64-linux-gnu/libc-2.31.so
      0x7ffff7f61000     0x7ffff7fab000    0x4a000   0x19d000 /usr/lib/x86_64-linux-gnu/libc-2.31.so
      0x7ffff7fab000     0x7ffff7fac000     0x1000   0x1e7000 /usr/lib/x86_64-linux-gnu/libc-2.31.so
      0x7ffff7fac000     0x7ffff7faf000     0x3000   0x1e7000 /usr/lib/x86_64-linux-gnu/libc-2.31.so
      0x7ffff7faf000     0x7ffff7fb2000     0x3000   0x1ea000 /usr/lib/x86_64-linux-gnu/libc-2.31.so
      0x7ffff7fb2000     0x7ffff7fb8000     0x6000        0x0 
      0x7ffff7fcb000     0x7ffff7fce000     0x3000        0x0 [vvar]
      0x7ffff7fce000     0x7ffff7fcf000     0x1000        0x0 [vdso]
      0x7ffff7fcf000     0x7ffff7fd0000     0x1000        0x0 /usr/lib/x86_64-linux-gnu/ld-2.31.so
      0x7ffff7fd0000     0x7ffff7ff3000    0x23000     0x1000 /usr/lib/x86_64-linux-gnu/ld-2.31.so
      0x7ffff7ff3000     0x7ffff7ffb000     0x8000    0x24000 /usr/lib/x86_64-linux-gnu/ld-2.31.so
      0x7ffff7ffb000     0x7ffff7ffc000     0x1000        0x0 /home/test/vm/a.txt
      0x7ffff7ffc000     0x7ffff7ffd000     0x1000    0x2c000 /usr/lib/x86_64-linux-gnu/ld-2.31.so
      0x7ffff7ffd000     0x7ffff7ffe000     0x1000    0x2d000 /usr/lib/x86_64-linux-gnu/ld-2.31.so
      0x7ffff7ffe000     0x7ffff7fff000     0x1000        0x0 
      0x7ffffffde000     0x7ffffffff000    0x21000        0x0 [stack]
  0xffffffffff600000 0xffffffffff601000     0x1000        0x0 [vsyscall]

上面结果中,objfile列中值为[heap]的虚拟内存区域就是我们经常所说的堆,其起始地址为 0x555555559000,大小为 0x21000 字节。

结合全局符号_end的地址的意义,我们可以发现:在 Linux 系统中,堆紧接在未初始化的数据区域(即.bss section)后开始,并向上生长(向更高的地址)。

需要注意的是,由于页对齐要求,堆实际上是从.bss section 所处页面的下一个页面开始的,而不是从.bss section 最后一个字节后的第一个字节开始的。


堆顶可以“顶”到哪

在 Linux 系统中,内核为每个进程维护着一个变量brk,它指向堆的顶部。我们可以通过sbrk函数来扩展和收缩堆。

sbrk函数原型(定义在#include <unistd.h>头文件中):

void *sbrk (intptr_t __delta);

注:

下面通过一个简单的程序来测试笔者机器上,一个进程的堆顶的实际上限。

源码,vm5_main.cpp:

#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>

int main(int argc, char *argv[])
{
  if (argc < 2)
  {
    printf("Usage, needed args: heap-size-bytes\n");
    return EXIT_FAILURE;
  }

  const auto size = atoll(argv[1]);
  auto old_top = sbrk(size);
  if ((void *)-1 == old_top)
  {
    perror("sbrk failed, reason");
  }
  else
  {
    printf("sbrk ok, size:%lld bytes\n", size);
  }
  return 0;
}

编译:

$ g++ -o vm5_main vm5_main.cpp -g

首先,观察此刻物理内存和交换空间的大小:

$ cat /proc/meminfo
MemTotal:        8033464 kB
MemFree:          189324 kB
MemAvailable:    3283572 kB
// 省略...
SwapTotal:       2097148 kB
SwapFree:        2074848 kB
// 省略...

从上面可以看出,在笔者机器上,物理内存和交换空间的大小之和为 10373746688 字节(10373746688 = (8033464 + 2097148) * 1024)。此刻,空闲的物理内存和空闲的交换空间大小之和为 2318512128 字节(2318512128 = (189324 + 2074848) * 1024)。

接下来,尝试运行以下命令:

$ ./vm5_main 10373746688
sbrk ok, size:10373746688 bytes
$ ./vm5_main 10373746689
sbrk failed, reason: Cannot allocate memory

从上面的结果可以看出,一个进程实际可以分配的堆大小不会超过物理内存和交换空间的大小之和。


初探 malloc 中已分配块和空闲块的内存布局

我们通过示例研究下malloc中已分配块和空闲块的内存布局是否如其注释所描述的那样,以 glibc 2.31 版本为例。

malloc.c中对已分配块的内存布局描述如下:

    An allocated chunk looks like this:


    chunk-> +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
        |             Size of previous chunk, if unallocated (P clear)  |
        +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
        |             Size of chunk, in bytes                     |A|M|P|
      mem-> +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
        |
             User data starts here...                          .
        .                                                               .
        .             (malloc_usable_size() bytes)                      .
        .                                                               |
nextchunk-> +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
        |
             (size of chunk, but used for application data)    |
        +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
        |
             Size of next chunk, in bytes                |A|0|1|
        +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+

malloc.c中对空闲块的内存布局描述如下:

    Free chunks are stored in circular doubly-linked lists, and look like this:

    chunk-> +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
        |             Size of previous chunk, if unallocated (P clear)  |
        +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
    `head:' |             Size of chunk, in bytes                     |A|0|P|
      mem-> +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
        |             Forward pointer to next chunk in list             |
        +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
        |             Back pointer to previous chunk in list            |
        +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
        |             Unused space (may be 0 bytes long)                .
        .                                                               .
        .                                                               |
nextchunk-> +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
    `foot:' |             Size of chunk, in bytes                           |
        +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
        |             Size of next chunk, in bytes                |A|0|0|
        +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+

研究过程:

step 1: 准备示例程序

源码,vm4_main.cpp:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

class HeapObject
{
public:
  explicit HeapObject(std::size_t size)
  : data_(nullptr), size_(0)
  {
    data_ = malloc(size);
    if (data_)
    {
      size_ = size;
      memset(data_, 1, size);
    }
  }

  ~HeapObject()
  {
    Free();
  }

  void Free()
  {
    free(data_);
    data_ = nullptr;
    size_ = 0;
  }

private:
  void *data_;
  std::size_t size_;
};

int main(int argc, char *argv[])
{
  HeapObject obj1(8);
  HeapObject obj2(4);
  HeapObject obj3(64);
  HeapObject obj4(4);

  obj2.Free();
  obj4.Free();
  obj3.Free();
  obj1.Free();

  HeapObject obj5(32);
  obj5.Free();
  return 0;
}

如何编译:

$ g++ -o vm4_main vm4_main.cpp -g

step 2: 分配一块大小为 8 字节的内存,并将该内存的起始地址赋给 obj1.data_

1)启动程序

$ gdb -q ./vm4_main
Reading symbols from ./vm4_main...
(gdb) start
Temporary breakpoint 1 at 0x11a9file vm4_main.cpp, line 37.
Starting program: /home/test/vm/vm4_main 

Temporary breakpoint 1main (argc=0, argv=0x0at vm4_main.cpp:37
37    {

2)在刚进入main函数后,查看进程的内存映射情况

(gdb) i proc mappings 
process 23479
Mapped address spaces:

          Start Addr           End Addr       Size     Offset objfile
      0x555555554000     0x555555555000     0x1000        0x0 /home/test/vm/vm4_main
      0x555555555000     0x555555556000     0x1000     0x1000 /home/test/vm/vm4_main
      0x555555556000     0x555555557000     0x1000     0x2000 /home/test/vm/vm4_main
      0x555555557000     0x555555558000     0x1000     0x2000 /home/test/vm/vm4_main
      0x555555558000     0x555555559000     0x1000     0x3000 /home/test/vm/vm4_main
      0x7ffff7dc4000     0x7ffff7de9000    0x25000        0x0 /usr/lib/x86_64-linux-gnu/libc-2.31.so
      0x7ffff7de9000     0x7ffff7f61000   0x178000    0x25000 /usr/lib/x86_64-linux-gnu/libc-2.31.so
      0x7ffff7f61000     0x7ffff7fab000    0x4a000   0x19d000 /usr/lib/x86_64-linux-gnu/libc-2.31.so
      0x7ffff7fab000     0x7ffff7fac000     0x1000   0x1e7000 /usr/lib/x86_64-linux-gnu/libc-2.31.so
      0x7ffff7fac000     0x7ffff7faf000     0x3000   0x1e7000 /usr/lib/x86_64-linux-gnu/libc-2.31.so
      0x7ffff7faf000     0x7ffff7fb2000     0x3000   0x1ea000 /usr/lib/x86_64-linux-gnu/libc-2.31.so
      0x7ffff7fb2000     0x7ffff7fb8000     0x6000        0x0 
      0x7ffff7fcb000     0x7ffff7fce000     0x3000        0x0 [vvar]
      0x7ffff7fce000     0x7ffff7fcf000     0x1000        0x0 [vdso]
      0x7ffff7fcf000     0x7ffff7fd0000     0x1000        0x0 /usr/lib/x86_64-linux-gnu/ld-2.31.so
      0x7ffff7fd0000     0x7ffff7ff3000    0x23000     0x1000 /usr/lib/x86_64-linux-gnu/ld-2.31.so
      0x7ffff7ff3000     0x7ffff7ffb000     0x8000    0x24000 /usr/lib/x86_64-linux-gnu/ld-2.31.so
      0x7ffff7ffc000     0x7ffff7ffd000     0x1000    0x2c000 /usr/lib/x86_64-linux-gnu/ld-2.31.so
      0x7ffff7ffd000     0x7ffff7ffe000     0x1000    0x2d000 /usr/lib/x86_64-linux-gnu/ld-2.31.so
      0x7ffff7ffe000     0x7ffff7fff000     0x1000        0x0 
      0x7ffffffde000     0x7ffffffff000    0x21000        0x0 [stack]
  0xffffffffff600000 0xffffffffff601000     0x1000        0x0 [vsyscall]

从上面结果中可以看出,在刚进入main函数后,进程还未创建堆这一虚拟内存区域。

3)继续执行

(gdb) n
38      HeapObject obj1(8);
(gdb) n
39      HeapObject obj2(4);
(gdb) p obj1.data_
$1 = (void *) 0x5555555592a0
(gdb) x/8bx obj1.data_
0x5555555592a0:    0x01    0x01    0x01    0x01    0x01    0x01    0x01    0x01

此时(即在执行完语句HeapObject obj1(8);后),malloc为应用程序在堆上分配了一块起始地址为 0x5555555592a0、大小为 8 字节的内存。并且,这块内存中的每个字节的值都被初始化为 0x01。

4)再次查看进程的内存映射情况

(gdb) i proc mappings 
process 23479
Mapped address spaces:

          Start Addr           End Addr       Size     Offset objfile
      0x555555554000     0x555555555000     0x1000        0x0 /home/test/vm/vm4_main
      0x555555555000     0x555555556000     0x1000     0x1000 /home/test/vm/vm4_main
      0x555555556000     0x555555557000     0x1000     0x2000 /home/test/vm/vm4_main
      0x555555557000     0x555555558000     0x1000     0x2000 /home/test/vm/vm4_main
      0x555555558000     0x555555559000     0x1000     0x3000 /home/test/vm/vm4_main
      0x555555559000     0x55555557a000    0x21000        0x0 [heap]
      0x7ffff7dc4000     0x7ffff7de9000    0x25000        0x0 /usr/lib/x86_64-linux-gnu/libc-2.31.so
// 省略...

从上面结果中可以看出,malloc创建了起始地址为 0x555555559000、初始大小为 0x21000 字节的堆。

5)查看 obj1.data_ 附近的内存数据

obj1.data_ 附近的内存数据:

(gdb) x/10gx 0x5555555592a0-8*4
0x555555559280:    0x0000000000000000  0x0000000000000000
0x555555559290:    0x0000000000000000  0x0000000000000021
0x5555555592a0:    0x0101010101010101  0x0000000000000000
0x5555555592b0:    0x0000000000000000  0x0000000000020d51
0x5555555592c0:    0x0000000000000000  0x0000000000000000

在分析obj1.data_附近的内存数据之前,我们先看下chunk以及相关的定义。

malloc.c中关于chunk的结构体定义为:

struct malloc_chunk {

  INTERNAL_SIZE_T      mchunk_prev_size;  /* Size of previous chunk (if free).  */
  INTERNAL_SIZE_T      mchunk_size;       /* Size in bytes, including overhead. */

  struct malloc_chunkfd;         /* double links -- used only if free. */
  struct malloc_chunkbk;

  /* Only used for large blocks: pointer to next larger size.  */
  struct malloc_chunkfd_nextsize; /* double links -- used only if free. */
  struct malloc_chunkbk_nextsize;
};

注:chunk就是malloc程序包管理的内存块(包括空闲块和已分配块),malloc函数为应用程序分配的内存(即用户数据)就内嵌在chunk中。

关于chunkP标志位的注释为:

The P (PREV_INUSE) bit, stored in the unused low-order bit of the
chunk size (which is always a multiple of two words), is an in-use
bit for the *previous* chunk.  If that bit is *clear*, then the
word before the current chunk size contains the previous chunk
sizeand can be used to find the front of the previous chunk.
The very first chunk allocated always has this bit set,
preventing access to non-existent (or non-owned) memory. If
prev_inuse is set for any given chunkthen you CANNOT determine
the size of the previous chunkand might even get a memory
addressing fault when trying to do so.

从上述注释中,我们可以得出:

判断上一个chunk是已分配块还是空闲块的宏定义为prev_inuse,如下:

/* size field is or'ed with PREV_INUSE when previous adjacent chunk in use */
#define PREV_INUSE 0x1

/* extract inuse bit of previous chunk */
#define prev_inuse(p)       ((p)->mchunk_size & PREV_INUSE)

从上述宏定义可以看出,prev_inuse(p)的求值结果为 1 表示上一个chunk是已分配块;求值结果为 0 表示上一个chunk是空闲块。

获取一个chunk大小的宏定义为chunksize,如下:

/* size field is or'ed with IS_MMAPPED if the chunk was obtained with mmap() */
#define IS_MMAPPED 0x2

/* size field is or'ed with NON_MAIN_ARENA if the chunk was obtained
   from a non-main arena.  This is only set immediately before handing
   the chunk to the user, if necessary.  */
#define NON_MAIN_ARENA 0x4

/*
   Bits to mask off when extracting size

   Note: IS_MMAPPED is intentionally not masked off from size field in
   macros for which mmapped chunks should never be seen. This should
   cause helpful core dumps to occur if it is tried by accident by
   people extending or adapting this malloc.
 */
#define SIZE_BITS (PREV_INUSE | IS_MMAPPED | NON_MAIN_ARENA)

/* Get size, ignoring use bits */
#define chunksize(p) (chunksize_nomask (p) & ~(SIZE_BITS))

从上述宏定义可以看出,malloc_chunk结构体中mchunk_size字段的最低三位用于携带额外信息,其余位表示整个chunk的大小(包括有效负载和chunk头部等开销,单位:Bytes)。

通过用户数据指针确定其所在chunk的起始位置的宏定义为mem2chunk,如下:

/* The corresponding word size.  */
#define SIZE_SZ (sizeof (INTERNAL_SIZE_T))

#define mem2chunk(mem) ((mchunkptr)((char*)(mem) - 2*SIZE_SZ))

从上述宏定义中,我们可以得出:用户数据所在chunk的起始位置距离用户数据的起始处为 2 个字的长度。在 32-bit 系统中,一个字占用 4 字节;在 64-bit 系统中,一个字占用 8 字节。

综合以上内容,我们可以推断:

因此,我们可以推断obj1.data_所在chunk(已分配块)的内存布局为:

起始地址 大小(字节) 意义
0x555555559290 8 用于存储 mchunk_prev_size 字段的值,无意义(由于上一个chunk是已分配块)
0x555555559298 8 用于存储 size 字段的值,包括该 chunk 的大小和“P”标志位等
0x5555555592a0 8 用于存储用户数据,即 obj1.data_ 所指向的对象
0x5555555592a8 8 暂时未知?

另外,我们可以推断:由于下一个chunkmchunk_size字段的值为 0x20d51,其中P标志位的值为 1。因此,该chunk的大小为 0x20d50 字节,其上一个chunk(即obj1.data_所在的chunk)是一个已分配块,与实际情况符合。由于这个chunk位于堆顶,在malloc中被称为Top Chunk

查看堆底附近的内存数据:

(gdb) x/10gx 0x555555559000
0x555555559000:    0x0000000000000000  0x0000000000000291
0x555555559010:    0x0000000000000000  0x0000000000000000
0x555555559020:    0x0000000000000000  0x0000000000000000
0x555555559030:    0x0000000000000000  0x0000000000000000
0x555555559040:    0x0000000000000000  0x0000000000000000

从上面的输出结果中可以推断,另一个已分配块位于堆底,该chunkmchunk_size字段的值为 0x291,其中P标志位的值为 1。因此,该chunk的大小为 0x290 字节。另外,由于该chunk是堆中的第一个已分配块,也就是说其上一个chunk是不存在的。所以,该chunkP标志位的值始终是 1,从而避免非法内存访问,符合P标志位注释所描述的。

上述 3 个chunk的大小之和为 0x21000(0x21000 = 0x290 + 0x20 + 0x20d50),正好是堆的大小。

step 3: 继续分配一块大小为 4 字节的内存,并将该内存的起始地址赋给 obj2.data_

1)继续执行

(gdb) n
40      HeapObject obj3(64);
(gdb) p obj2.data_
$6 = (void *) 0x5555555592c0
(gdb) x/8bx obj2.data_
0x5555555592c0:    0x01    0x01    0x01    0x01    0x00    0x00    0x00    0x00

此时(即在执行完语句HeapObject obj2(4);后),malloc为应用程序在堆上分配了一块起始地址为 0x5555555592c0、大小为 4 字节的内存。并且,这块内存中的每个字节的值都被初始化为 0x01。

2)查看 obj2.data_ 附近的内存数据

obj2.data_ 附近的内存数据:

(gdb) x/10gx 0x5555555592c0-8*6
0x555555559290:    0x0000000000000000  0x0000000000000021
0x5555555592a0:    0x0101010101010101  0x0000000000000000
0x5555555592b0:    0x0000000000000000  0x0000000000000021
0x5555555592c0:    0x0000000001010101  0x0000000000000000
0x5555555592d0:    0x0000000000000000  0x0000000000020d31

按照上述分析思路,我们可以推断obj2.data_所在chunk(已分配块)的内存布局为:

起始地址 大小(字节) 意义
0x5555555592b0 8 用于存储 mchunk_prev_size 字段的值,无意义(由于上一个 chunk 是已分配块)
0x5555555592b8 8 用于存储 size 字段的值,包括该 chunk 的大小和“P”标志位等
0x5555555592c0 4 用于存储用户数据,即 obj2.data_ 所指向的对象
0x5555555592c4 4 对齐填充数据(alignment padding bytes)
0x5555555592c8 8 暂时未知?

另外,我们可以发现,obj2.data_所在的chunk是从Top Chunk分割而来的。

step 4: 继续分配一块大小为 64 字节的内存,并将该内存的起始地址赋给 obj3.data_

1)继续执行

(gdb) n
41      HeapObject obj4(4);
(gdb) p obj3.data_
$7 = (void *) 0x5555555592e0
(gdb) x/64bx obj3.data_
0x5555555592e0:    0x01    0x01    0x01    0x01    0x01    0x01    0x01    0x01
0x5555555592e8:    0x01    0x01    0x01    0x01    0x01    0x01    0x01    0x01
0x5555555592f0:    0x01    0x01    0x01    0x01    0x01    0x01    0x01    0x01
0x5555555592f8:    0x01    0x01    0x01    0x01    0x01    0x01    0x01    0x01
0x555555559300:    0x01    0x01    0x01    0x01    0x01    0x01    0x01    0x01
0x555555559308:    0x01    0x01    0x01    0x01    0x01    0x01    0x01    0x01
0x555555559310:    0x01    0x01    0x01    0x01    0x01    0x01    0x01    0x01
0x555555559318:    0x01    0x01    0x01    0x01    0x01    0x01    0x01    0x01

此时(即在执行完语句HeapObject obj3(64);后),malloc为应用程序在堆上分配了一块起始地址为 0x5555555592e0、大小为 64 字节的内存。并且,这块内存中的每个字节的值都被初始化为 0x01。

2)查看 obj3.data_ 附近的内存数据

obj3.data_ 附近的内存数据:

(gdb) x/16gx 0x5555555592e0-8*6
0x5555555592b0:    0x0000000000000000  0x0000000000000021
0x5555555592c0:    0x0000000001010101  0x0000000000000000
0x5555555592d0:    0x0000000000000000  0x0000000000000051
0x5555555592e0:    0x0101010101010101  0x0101010101010101
0x5555555592f0:    0x0101010101010101  0x0101010101010101
0x555555559300:    0x0101010101010101  0x0101010101010101
0x555555559310:    0x0101010101010101  0x0101010101010101
0x555555559320:    0x0000000000000000  0x0000000000020ce1

按照上述分析思路,我们可以推断obj3.data_所在chunk(已分配块)的内存布局为:

起始地址 大小(字节) 意义
0x5555555592d0 8 用于存储 mchunk_prev_size 字段的值,无意义(由于上一个 chunk 是已分配块)
0x5555555592d8 8 用于存储 size 字段的值,包括该 chunk 的大小和“P”标志位等
0x5555555592e0 64 用于存储用户数据,即 obj3.data_ 所指向的对象

需要注意的是,obj3.data_所在的chunk除了头部以外,其余都是有效负载,即没有对齐填充数据。

另外,我们可以发现,obj3.data_所在的chunk也是从Top Chunk分割而来的。

step 5: 继续分配一块大小为 4 字节的内存,并将该内存的起始地址赋给 obj4.data_

1)继续执行

(gdb) n
43      obj2.Free();
(gdb) p obj4.data_
$8 = (void *) 0x555555559330
(gdb) x/8bx obj4.data_
0x555555559330:    0x01    0x01    0x01    0x01    0x00    0x00    0x00    0x00

此时(即在执行完语句HeapObject obj4(4);后),malloc为应用程序在堆上分配了一块起始地址为 0x555555559330、大小为 4 字节的内存。并且,这块内存中的每个字节的值都被初始化为 0x01。

2)查看 obj4.data_ 附近的内存数据

obj4.data_ 附近的内存数据:

(gdb) x/10gx 0x555555559330-8*6
0x555555559300:    0x0101010101010101  0x0101010101010101
0x555555559310:    0x0101010101010101  0x0101010101010101
0x555555559320:    0x0000000000000000  0x0000000000000021
0x555555559330:    0x0000000001010101  0x0000000000000000
0x555555559340:    0x0000000000000000  0x0000000000020cc1

按照上述分析思路,我们可以推断obj4.data_所在chunk(已分配块)的内存布局为:

起始地址 大小(字节) 意义
0x555555559320 8 用于存储 mchunk_prev_size 字段的值,无意义(由于上一个 chunk 是已分配块)
0x555555559328 8 用于存储 size 字段的值,包括该 chunk 的大小和“P”标志位等
0x555555559330 4 用于存储用户数据,即 obj4.data_ 所指向的对象
0x555555559334 4 对齐填充数据(alignment padding bytes)
0x555555559338 8 暂时未知?

另外,我们可以发现,obj4.data_所在的chunk也是从Top Chunk分割而来的。

step 6: 释放 obj2.data_ 所在的 chunk

1)继续执行

(gdbn
44      obj4.Free();

此时(即在执行完语句obj2.Free();后),应用程序显式地释放了obj2.data_所在的chunk

2)查看 obj2.data_ 附近的内存数据

obj2.data_ 附近的内存数据:

(gdb) x/10gx 0x5555555592c0-8*6
0x555555559290:    0x0000000000000000  0x0000000000000021
0x5555555592a0:    0x0101010101010101  0x0000000000000000
0x5555555592b0:    0x0000000000000000  0x0000000000000021
0x5555555592c0:    0x0000000000000000  0x0000555555559010
0x5555555592d0:    0x0000000000000000  0x0000000000000051

从上面的结果中可以看出:

我们可以推断obj2.data_所在chunk(空闲块)的内存布局为:

起始地址 大小(字节) 意义
0x5555555592b0 8 用于存储 mchunk_prev_size 字段的值,无意义(由于上一个 chunk 是已分配块)
0x5555555592b8 8 用于存储 size 字段的值,包括该 chunk 的大小和“P”标志位等
0x5555555592c0 8 用于存储 fd 字段的值,值为 0x0,表示该空闲块位于空闲块链表的末尾
0x5555555592c8 8 用于存储 bk 字段的值,值为 0x555555559010,指向空闲块链表中的上一个空闲块(chunk 的起始地址为 0x555555559010 - 2*SIZE_SZ)

查看堆底附近的内存数据:

(gdb) x/100gx 0x0000555555559000
0x555555559000:    0x0000000000000000  0x0000000000000291
0x555555559010:    0x0000000000000001  0x0000000000000000
0x555555559020:    0x0000000000000000  0x0000000000000000
0x555555559030:    0x0000000000000000  0x0000000000000000
0x555555559040:    0x0000000000000000  0x0000000000000000
0x555555559050:    0x0000000000000000  0x0000000000000000
0x555555559060:    0x0000000000000000  0x0000000000000000
0x555555559070:    0x0000000000000000  0x0000000000000000
0x555555559080:    0x0000000000000000  0x0000000000000000
0x555555559090:    0x00005555555592c0  0x0000000000000000
0x5555555590a0:    0x0000000000000000  0x0000000000000000
// 省略

从上面的结果中可以发现,在释放 obj2.data_ 所在的chunk后,第一个chunk有如下两个变化:1)起始地址为 0x555555559010 后面的 8 字节内容从 0x0 变成了 0x1;2)起始地址为 0x555555559090 后面的 8 字节内容从 0x0 变成了 0x05555555592c0,即指向了obj2.data_所在chunk(空闲块)的“用户数据”起始处。

step 7: 释放 obj4.data_ 所在的 chunk

1)继续执行

(gdbn
45      obj3.Free();

此时(即在执行完语句obj4.Free();后),应用程序显式地释放了obj4.data_所在的chunk

2)查看 obj4.data_ 附近的内存数据

obj4.data_ 附近的内存数据:

(gdb) x/10gx 0x555555559330-8*6
0x555555559300:    0x0101010101010101  0x0101010101010101
0x555555559310:    0x0101010101010101  0x0101010101010101
0x555555559320:    0x0000000000000000  0x0000000000000021
0x555555559330:    0x00005555555592c0  0x0000555555559010
0x555555559340:    0x0000000000000000  0x0000000000020cc1

从上面的结果中可以看出:

我们可以推断obj4.data_所在chunk(空闲块)的内存布局为:

起始地址 大小(字节) 意义
0x555555559320 8 用于存储 mchunk_prev_size 字段的值,无意义(由于上一个 chunk 是已分配块)
0x555555559328 8 用于存储 size 字段的值,包括该 chunk 的大小和“P”标志位等
0x555555559330 8 用于存储 fd 字段的值,值为 0x5555555592c0,指向空闲块链表中的下一个空闲块(chunk 的起始地址为 0x5555555592c0 - 2*SIZE_SZ,即已经释放了的 obj2.data_ 所在的 chunk
0x555555559338 8 用于存储 bk 字段的值,值为 0x555555559010,指向空闲块链表中的上一个空闲块(chunk 的起始地址为 0x555555559010 - 2*SIZE_SZ)

需要注意的是,从内存数据来看,已释放的obj4.data_obj2.data_所在的chunk(都是空闲块)属于同一个空闲块链表。

查看堆底附近的内存数据:

(gdb) x/100gx 0x0000555555559000
0x555555559000:    0x0000000000000000  0x0000000000000291
0x555555559010:    0x0000000000000002  0x0000000000000000
0x555555559020:    0x0000000000000000  0x0000000000000000
0x555555559030:    0x0000000000000000  0x0000000000000000
0x555555559040:    0x0000000000000000  0x0000000000000000
0x555555559050:    0x0000000000000000  0x0000000000000000
0x555555559060:    0x0000000000000000  0x0000000000000000
0x555555559070:    0x0000000000000000  0x0000000000000000
0x555555559080:    0x0000000000000000  0x0000000000000000
0x555555559090:    0x0000555555559330  0x0000000000000000
0x5555555590a0:    0x0000000000000000  0x0000000000000000
// 省略

从上面的结果中可以发现,在释放 obj4.data_ 所在的chunk后,第一个chunk有如下两个变化:1)起始地址为 0x555555559010 后面的 8 字节内容从 0x1 变成了 0x2;2)起始地址为 0x555555559090 后面的 8 字节内容从 0x5555555592c0 变成了 0x555555559330,即指向了obj4.data_所在chunk(空闲块)的“用户数据”起始处。

step 8: 释放 obj3.data_ 所在的 chunk

1)继续执行

(gdbn
46      obj1.Free();

此时(即在执行完语句obj3.Free();后),应用程序显式地释放了obj3.data_所在的chunk

2)查看 obj3.data_ 附近的内存数据

obj3.data_ 附近的内存数据:

(gdb) x/16gx 0x5555555592e0-8*6
0x5555555592b0:    0x0000000000000000  0x0000000000000021
0x5555555592c0:    0x0000000000000000  0x0000555555559010
0x5555555592d0:    0x0000000000000000  0x0000000000000051
0x5555555592e0:    0x0000000000000000  0x0000555555559010
0x5555555592f0:    0x0101010101010101  0x0101010101010101
0x555555559300:    0x0101010101010101  0x0101010101010101
0x555555559310:    0x0101010101010101  0x0101010101010101
0x555555559320:    0x0000000000000000  0x0000000000000021

从上面的结果中可以看出:

我们可以推断obj3.data_所在chunk(空闲块)的内存布局为:

起始地址 大小(字节) 意义
0x5555555592d0 8 用于存储 mchunk_prev_size 字段的值,实际值不符合预期结果
0x5555555592d8 8 用于存储 size 字段的值,包括该 chunk 的大小和“P”标志位等,“P”标志位的实际值不符合预期结果
0x5555555592e0 8 用于存储 fd 字段的值,值为 0x0,表示该空闲块位于空闲块链表的末尾
0x5555555592e8 8 用于存储 bk 字段的值,值为 0x555555559010,指向空闲块链表中的上一个空闲块(chunk 的起始地址为 0x555555559010 - 2*SIZE_SZ)
0x5555555592f0 48 未使用

需要注意的是,从内存数据来看,已释放的obj3.data_所在chunk(空闲块)属于另一个空闲块链表。

查看堆底附近的内存数据:

(gdb) x/100gx 0x0000555555559000
0x555555559000:    0x0000000000000000  0x0000000000000291
0x555555559010:    0x0001000000000002  0x0000000000000000
0x555555559020:    0x0000000000000000  0x0000000000000000
0x555555559030:    0x0000000000000000  0x0000000000000000
0x555555559040:    0x0000000000000000  0x0000000000000000
0x555555559050:    0x0000000000000000  0x0000000000000000
0x555555559060:    0x0000000000000000  0x0000000000000000
0x555555559070:    0x0000000000000000  0x0000000000000000
0x555555559080:    0x0000000000000000  0x0000000000000000
0x555555559090:    0x0000555555559330  0x0000000000000000
0x5555555590a0:    0x0000000000000000  0x00005555555592e0
0x5555555590b0:    0x0000000000000000  0x0000000000000000
// 省略

从上面的结果中可以发现,在释放 obj3.data_ 所在的chunk后,第一个chunk有如下两个变化:1)起始地址为 0x555555559010 后面的 8 字节内容从 0x2 变成了 0x0001000000000002;2)起始地址为 0x5555555590a8 后面的 8 字节内容从 0x0 变成了 0x5555555592e0,即指向了obj3.data_所在chunk(空闲块)的“用户数据”起始处。

step 9: 释放 obj1.data_ 所在的 chunk

1)继续执行

(gdb) n
48      HeapObject obj5(32);

此时(即在执行完语句obj1.Free();后),应用程序显式地释放obj1.data_所在的chunk

2)查看 obj1.data_ 附近的内存数据

obj1.data_ 附近的内存数据:

(gdb) x/10gx 0x5555555592a0-8*4
0x555555559280:    0x0000000000000000  0x0000000000000000
0x555555559290:    0x0000000000000000  0x0000000000000021
0x5555555592a0:    0x0000555555559330  0x0000555555559010
0x5555555592b0:    0x0000000000000000  0x0000000000000021
0x5555555592c0:    0x0000000000000000  0x0000555555559010

从上面的结果中可以看出:

我们可以推断obj1.data_所在chunk(空闲块)的内存布局为:

起始地址 大小(字节) 意义
0x555555559290 8 用于存储 mchunk_prev_size 字段的值,实际值不符合预期结果
0x555555559298 8 用于存储 size 字段的值,包括该 chunk 的大小和“P”标志位等,“P”标志位的实际值不符合预期结果
0x5555555592a0 8 用于存储 fd 字段的值,值为 0x555555559330,,指向空闲块链表中的下一个空闲块(chunk 的起始地址为 0x555555559330 - 2*SIZE_SZ,即已经释放了到 obj4.data_ 所在的 chunk
0x5555555592a8 8 用于存储 bk 字段的值,值为 0x555555559010,指向空闲块链表中的上一个空闲块(chunk 的起始地址为 0x555555559010 - 2*SIZE_SZ)

需要注意的是,从内存数据来看,已释放的obj1.data_obj4.data_obj2.data_所在的chunk(都是空闲块)属于同一个空闲块链表。并且,这 3 个空闲块的大小相等,都是 0x20 字节。

查看堆底附近的内存数据:

(gdb) x/100gx 0x0000555555559000
0x555555559000:    0x0000000000000000  0x0000000000000291
0x555555559010:    0x0001000000000003  0x0000000000000000
0x555555559020:    0x0000000000000000  0x0000000000000000
0x555555559030:    0x0000000000000000  0x0000000000000000
0x555555559040:    0x0000000000000000  0x0000000000000000
0x555555559050:    0x0000000000000000  0x0000000000000000
0x555555559060:    0x0000000000000000  0x0000000000000000
0x555555559070:    0x0000000000000000  0x0000000000000000
0x555555559080:    0x0000000000000000  0x0000000000000000
0x555555559090:    0x00005555555592a0  0x0000000000000000
0x5555555590a0:    0x0000000000000000  0x00005555555592e0
0x5555555590b0:    0x0000000000000000  0x0000000000000000
// 省略

从上面的结果中可以发现,在释放 obj1.data_ 所在的chunk后,第一个chunk有如下两个变化:1)起始地址为 0x555555559010 后面的 8 字节内容从 0x0001000000000002 变成了 0x0001000000000003;2)起始地址为 0x555555559090 后面的 8 字节内容从 0x555555559330变成了 0x5555555592a0,即指向了obj1.data_所在chunk(空闲块)的“用户数据”起始处。

step 10: 继续分配一块大小为 32 字节的内存,并将该内存的起始地址赋给 obj5.data_

1)继续执行

(gdb) n
49      obj5.Free();
(gdb) p obj5.data_
$9 = (void *) 0x555555559350
(gdb) x/4gx 0x555555559350
0x555555559350:    0x0101010101010101  0x0101010101010101
0x555555559360:    0x0101010101010101  0x0101010101010101

此时(即在执行完语句HeapObject obj5(32);后),malloc为应用程序在堆上分配了一块起始地址为 0x555555559350、大小为 32 字节的内存。并且,这块内存中的每个字节的值都被初始化为 0x01。

2)查看 obj5.data_ 附近的内存数据

obj5.data_ 附近的内存数据:

(gdb) x/10gx 0x555555559350-8*4
0x555555559330:    0x00005555555592c0  0x0000555555559010
0x555555559340:    0x0000000000000000  0x0000000000000031
0x555555559350:    0x0101010101010101  0x0101010101010101
0x555555559360:    0x0101010101010101  0x0101010101010101
0x555555559370:    0x0000000000000000  0x0000000000020c91

我们可以推断obj5.data_所在chunk(已分配块)的内存布局为:

起始地址 大小(字节) 意义
0x555555559340 8 用于存储 mchunk_prev_size 字段的值,实际值不符合预期结果
0x555555559348 8 用于存储 size 字段的值,包括该 chunk 的大小和“P”标志位等,“P”标志位的实际值不符合预期结果
0x555555559350 32 用于存储用户数据,即 obj5.data_ 所指向的对象

另外,我们可以发现,obj5.data_所在的chunk也是从Top Chunk分割而来的。

step 11: 释放 obj5.data_ 所在的 chunk

1)继续执行

(gdb) n
50      return 0;

此时(即在执行完语句obj5.Free();后),应用程序显式地释放obj5.data_所在的chunk

2)查看 obj5.data_ 附近的内存数据

obj5.data_ 附近的内存数据:

(gdb) x/10gx 0x555555559350-8*4
0x555555559330:    0x00005555555592c0  0x0000555555559010
0x555555559340:    0x0000000000000000  0x0000000000000031
0x555555559350:    0x0000000000000000  0x0000555555559010
0x555555559360:    0x0101010101010101  0x0101010101010101
0x555555559370:    0x0000000000000000  0x0000000000020c91

我们可以推断obj5.data_所在chunk(空闲块)的内存布局为:

起始地址 大小(字节) 意义
0x555555559340 8 用于存储 mchunk_prev_size 字段的值,实际值不符合预期结果
0x555555559348 8 用于存储 size 字段的值,包括该 chunk 的大小和“P”标志位等,“P”标志位的实际值不符合预期结果
0x555555559350 8 用于存储 fd 字段的值,值为 0x0,表示该空闲块位于空闲块链表的末尾
0x555555559358 8 用于存储 bk 字段的值,值为 0x555555559010,指向空闲块链表中的上一个空闲块(chunk 的起始地址为 0x555555559010 - 2*SIZE_SZ)
0x555555559360 16 未使用

需要注意的是,从内存数据来看,已释放的obj5.data_所在chunk(空闲块)属于第三个空闲块链表。

查看堆底附近的内存数据:

(gdb) x/100gx 0x0000555555559000
0x555555559000:    0x0000000000000000  0x0000000000000291
0x555555559010:    0x0001000000010003  0x0000000000000000
0x555555559020:    0x0000000000000000  0x0000000000000000
0x555555559030:    0x0000000000000000  0x0000000000000000
0x555555559040:    0x0000000000000000  0x0000000000000000
0x555555559050:    0x0000000000000000  0x0000000000000000
0x555555559060:    0x0000000000000000  0x0000000000000000
0x555555559070:    0x0000000000000000  0x0000000000000000
0x555555559080:    0x0000000000000000  0x0000000000000000
0x555555559090:    0x00005555555592a0  0x0000555555559350
0x5555555590a0:    0x0000000000000000  0x00005555555592e0
0x5555555590b0:    0x0000000000000000  0x0000000000000000
// 省略

从上面的结果中可以发现,在释放 obj5.data_ 所在的chunk后,第一个chunk有如下两个变化:1)起始地址为 0x555555559010 后面的 8 字节内容从 0x0001000000000003 变成了 0x0001000000010003;1)起始地址为 0x555555559098 后面的 8 字节内容从 0x0变成了 0x555555559350,即指向了obj5.data_所在chunk(空闲块)的“用户数据”起始处。

研究结论:

对于 glibc-2.31 版本且在 64-bit 系统上,malloc中已分配块的内存布局为(从低地址到高地址):

相对于 chunk 起始位置的偏移量 对应的 malloc_chunk 结构体中的字段名 字段大小(字节) 作用 备注
0 mchunk_prev_size 8 表示上一个 chunk 的大小 该字段仅在上一个 chunk 是一个空闲块时有效
8 mchunk_size 8 表示本 chunk 的大小
  • chunk 的大小包括有效负载(即用户数据)和 chunk 头部等开销
  • 该字段的最低位为“P”标志位,用于表示上一个 chunk 是否是已分配块(1,是;0,不是)。
  • 16 fd 8 表示用户数据的起始位置,即 malloc 等函数的返回值

    对于 glibc-2.31 版本且在 64-bit 系统上,malloc中空闲块的内存布局为(从低地址到高地址):

    相对于 chunk 起始位置的偏移量 对应的 malloc_chunk 结构体中的字段名 字段大小(字节) 作用 备注
    0 mchunk_prev_size 8 表示上一个 chunk 的大小 该字段仅在上一个 chunk 是一个空闲块时有效
    8 mchunk_size 8 表示本 chunk 的大小
  • chunk 的大小包括有效负载(即用户数据)和 chunk 头部等开销
  • 该字段的最低位为“P”标志位,用于表示上一个 chunk 是否是已分配块(1,是;0,不是)。
  • 16 fd 8 表示空闲块链表中的下一个 chunk
    24 bk 8 表示空闲块链表中的上一个 chunk
    32 fd_nextsize 大于等于 0 未使用

    遗留问题:

    下一篇我们结合malloc的实现源码来分析上述可执行目标文件vm4_main动态申请和释放堆内存的过程。


    References


    下一篇:计算机系统篇之虚拟内存(9):理解 glibc malloc 的工作原理(中)

    上一篇:计算机系统篇之虚拟内存(4):再探 mmap

    首页