【问题标题】:GLIBC malloc implementation bookkeepingGLIBC malloc 实现记账
【发布时间】:2020-09-17 14:32:46
【问题描述】:

我试图了解 glibc 的 malloc 究竟是如何在我的 64 位机器上记账的。

根据文档,它在块之前存储实际大小(malloc 值加上簿记字节)。于是我从here中获取了以下代码:

int *a = (int *) malloc(4);
int *b = (int *) malloc(7);
int *c = (int *) malloc(1);
int *d = (int *) malloc(32);
int *e = (int *) malloc(4);

printf("0x%x\n", a);
printf("0x%x\n", b);
printf("0x%x\n", c);
printf("0x%x\n", d);
printf("0x%x\n", e);


printf("a[-1] = %d, a[-2] = %d\n", a[-1], a[-2]);
printf("b[-1] = %d, b[-2] = %d\n", b[-1], b[-2]);
printf("c[-1] = %d, c[-2] = %d\n", c[-1], c[-2]);
printf("d[-1] = %d, d[-2] = %d\n", d[-1], d[-2]);
printf("e[-1] = %d, e[-2] = %d\n", e[-1], e[-2]);  

产生:

0xfca042a0 
0xfca042c0
0xfca042e0
0xfca04300
0xfca04330

a[-1] = 0, a[-2] = 33 // letter[-2] is how much memory malloc has actually allocated
b[-1] = 0, b[-2] = 33
c[-1] = 0, c[-2] = 33
d[-1] = 0, d[-2] = 49
e[-1] = 0, e[-2] = 33

所以你可以看到前三个地址相隔 32 字节,这是有道理的,因为 malloc 分配的最小块是 32 或者更确切地说是4 * sizeof(void*)。但是,当我分配 32 字节时,下一个块是 48 字节而不是 64 字节,这是为什么呢?

如果 malloc 分配了 32 和 48 字节,为什么它分别打印 33 和 49?

【问题讨论】:

标签: c memory-management malloc glibc


【解决方案1】:

块的内部 glibc 表示是以下结构:

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_chunk* fd;                /* double links -- used only if free. */
    struct malloc_chunk* bk;        
    /* Only used for large blocks: pointer to next larger size.  */
    struct malloc_chunk* fd_nextsize;       /* double links -- used only if free. */
    struct malloc_chunk* bk_nextsize;
};

除了mchunk_prev_sizemchunk_size 之外的每个字段只有在块空闲时才会被填充。这两个字段就在用户可用缓冲区之前。 mchunk_prev_size 字段保存前一个块的大小(如果它是空闲的),而mchunk_size 字段保存块的实际大小(比请求的大小至少多 16 个字节)。

最小分配大小为 16(较小大小的请求仅四舍五入到 16),mchunk_prev_sizemchunk_size 始终需要额外的 16 个字节(每个 8 个字节)。此外,块总是与 16 字节边界对齐(例如,它们的十六进制地址总是以 0 结尾)。


所以现在你大概可以猜到第一个问题的答案了:

[...] malloc 分配的最小块是 32 或者更确切地说是4 * sizeof(void*)。但是,当我分配 32 字节时,下一个块是 48 字节而不是 64 字节,这是为什么呢?

嗯,是的,最小的块大小是 32,但增量实际上是 16。因此,您可以具有 16 的倍数且大于或等于 32 的任何大小。如果您要求大小在 17 到 32 之间,您将获得 48 个字节的块(其中 32 个可用于用户数据)。此外,最小malloc 分配大小与sizeof(void *) 没有太大关系,它实际上与sizeof(size_t) 更相关(正如您的链接还指出的那样)。

在您的示例中分配后堆的状态如下:

      +-----------+ 0xfca04290
      | prev size |
      |-----------|
      | size      |
a --> |-----------| 0xfca042a0
      | user data |
      |           |
      +-----------+ 0xfca042b8
      | prev size |
      |-----------|
      | size      |
b --> |-----------| 0xfca042c0
      | user data |
      |           |
      +-----------+ 0xfca042d0
      | prev size |
      |-----------|
      | size      |
c --> |-----------| 0xfca042e0
      | user data |
      |           |
      +-----------+ 0xfca042f0
      | prev size |
      |-----------|
      | size      |
d --> |-----------| 0xfca04300
      | user data |
      |           |
      |           |
      |           |
      +-----------+ 0xfca04320
      | prev size |
      |-----------|
      | size      |
e --> |-----------| 0xfca04330
      | user data |
      |           |
      +-----------+

现在,进入第二个问题:

如果 malloc 分配了 32 和 48 字节,为什么它分别打印 33 和 49?

由于每个块的大小必须是 16 字节的倍数,因此该大小的最低 4 位(一个十六进制数字)将保持未使用状态。 malloc 节省空间并使用这些空间来存储有关块的附加信息。最后 3 位实际上是 malloc 内部使用的标志。这在源代码 cmets 中也有解释:

chunk-> +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
        |             Size of previous chunk, if unallocated (P clear)  |
        +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
        |             Size of chunk, in bytes                     |A|M|P| <== flags
  mem-> +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
        |             User data starts here...                          |

那些标志位A|M|P 是:

  • A: 非主竞技场区块。
  • M:块是mmaped。
  • P:前一个块正在使用中(即不是空闲的)。

上面右边in the malloc source code可以找到更透彻的解释。

由于您的所有块仍在使用中,因此在大小字段中您会看到size | PREV_IN_USE。由于“先前使用” (P) 是最低有效位,这具有将大小值增加 1 的效果,因此您会看到 33 而不是 32


一些补充说明:

  • Don't cast the return value of malloc.

  • 如果你想检查一个块的大小,你应该使用size_t而不是int,像这样:

      void *a = malloc(32);
      size_t *ptr = a;
      size_t chunk_size = ptr[-1] & ~0x7; // Strip the A|M|P flags from the size.
    
  • 请记住,chunk_size 是块的内部大小,而不是可用的用户大小(即 32)。

  • 最后但同样重要的是:printf 指针的正确格式说明符是 %p,而不是 %x(它还应该已经包括前导 0x):

      printf("%p\n", a);
    

【讨论】:

  • 这个答案很准确!堆状态的绘制确实消除了我的一些误解
猜你喜欢
  • 2020-12-15
  • 1970-01-01
  • 2018-04-04
  • 1970-01-01
  • 2016-12-03
  • 2011-07-22
  • 2013-09-09
  • 2017-12-07
  • 1970-01-01
相关资源
最近更新 更多