计算机系统篇之虚拟内存(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),要么是已分配的,要么是空闲的。
已分配的块显式地保留以供应用程序使用,空闲块可用来分配。空闲块保持空闲,直到它显示地被应用程序所分配。一个已分配的块保持已分配状态,直到它被释放。
动态内存分配器的类型?
根据负责释放已分配块的实体不同,动态内存分配器可以分为以下两种:
显式分配器(explicit allocator)
要求应用程序显式地释放任何已分配的块。比如:C 标准库提供一种叫做 malloc 程序包的显式分配器。C 程序通过调用malloc
函数来分配一个块,并通过调用free
函数来释放一个块。
隐式分配器(explicit allocator)或垃圾回收(garbage collector)
要求分配器检测一个已分配块何时不再被应用程序所使用,那么就释放这个块。比如:Lisp、ML、Java 等高级语言依赖于垃圾回收来释放已分配的块。
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);
注:
参数__delta
,表示将堆扩展或收缩多少字节。在 64-bit 系统上,该参数是一个占用八字节的有符号整型。
__delta
< 0 时,表示将堆收缩abs(__delta)
字节。
__delta
= 0 时,表示获取当前堆顶。即sbrk
函数直接返回变量brk
的当前值。
__delta
> 0 时,表示将堆扩展__delta
字节。
返回值,如果sbrk
函数执行成功,则返回brk
的旧值;否则,返回 -1,并将全局变量errno
设置为ENOMEM
(值为 12)。
下面通过一个简单的程序来测试笔者机器上,一个进程的堆顶的实际上限。
源码,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
中已分配块和空闲块的内存布局是否如其注释所描述的那样,以 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 0x11a9: file vm4_main.cpp, line 37.
Starting program: /home/test/vm/vm4_main
Temporary breakpoint 1, main (argc=0, argv=0x0) at 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_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;
};
注:chunk
就是malloc
程序包管理的内存块(包括空闲块和已分配块),malloc
函数为应用程序分配的内存(即用户数据)就内嵌在chunk
中。
关于chunk
中P
标志位的注释为:
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
size, and 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 chunk, then you CANNOT determine
the size of the previous chunk, and might even get a memory
addressing fault when trying to do so.
从上述注释中,我们可以得出:
P
标志位位于malloc_chunk
结构体中mchunk_size
字段的最低位,表示上一个chunk
是已分配块还是空闲块。这里,上一个
的含义为:在内存中紧挨着的上一个。如果下文不加特别说明,所谓的上一个
都是该含义。
P
标志位的值为 1 时,表示上一个chunk
是已分配块。但不能通过本chunk
中的mchunk_prev_size
字段确定上一个chunk
的起始位置。
P
标志位的值为 0 时,表示上一个chunk
是空闲块。同时,可以通过本chunk
中的mchunk_prev_size
字段确定上一个chunk
的起始位置。从而,在释放本chunk
后,可以在常数时间内完成与上一个chunk
的合并。
malloc
函数分配的第一个chunk
(位于堆底)中P
标志位的值总是 1,从而避免非法内存访问。
判断上一个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 字节。
综合以上内容,我们可以推断:
地址 0x555555559290 后面 16 字节的内容分别对应malloc_chunk
结构体中mchunk_prev_size
和mchunk_size
字段的值,即这 16 个字节是该chunk
的头部。
由于该chunk
中mchunk_size
字段的值为 0x21。因此,该chunk
的大小为 0x20 字节,上一个chunk
是已分配块。令人疑惑的是,目前我们只从堆上申请了一块内存,但为什么已分配块有 2 个?(这里先作为遗留问题)
因此,我们可以推断obj1.data_
所在chunk
(已分配块)的内存布局为:
起始地址 | 大小(字节) | 意义 |
---|---|---|
0x555555559290 | 8 | 用于存储 mchunk_prev_size 字段的值,无意义(由于上一个chunk 是已分配块) |
0x555555559298 | 8 | 用于存储 size 字段的值,包括该 chunk 的大小和“P”标志位等 |
0x5555555592a0 | 8 | 用于存储用户数据,即 obj1.data_ 所指向的对象 |
0x5555555592a8 | 8 | 暂时未知? |
另外,我们可以推断:由于下一个chunk
中mchunk_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
从上面的输出结果中可以推断,另一个已分配块位于堆底,该chunk
中mchunk_size
字段的值为 0x291,其中P
标志位的值为 1。因此,该chunk
的大小为 0x290 字节。另外,由于该chunk
是堆中的第一个已分配块,也就是说其上一个chunk
是不存在的。所以,该chunk
的P
标志位的值始终是 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)继续执行
(gdb) n
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_
所指向对象的数据全部被清零了。
obj2.data_
所在的chunk
现在是一个空闲块,并且mchunk_size
字段中P
标志位的值仍为 1,表示上一个chunk
是一个已分配块,与实际情况符合。
对于下一个chunk
而言,obj2.data_
所在的chunk
是一个空闲块。因此,下一个chunk
里面mchunk_size
字段中P
标志位的值应该为 0。但实际上,下一个chunk
里面mchunk_size
字段为 0x51,即P
标志位的值是 1,不符合预期结果。
另外,下一个chunk
里面mchunk_prev_size
字段的值应该为 0x20,从而表示上一个chunk
(是一个空闲块)的大小。但实际上,该字段的值是 0x0,也不符合预期结果。
我们可以推断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)继续执行
(gdb) n
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
现在是一个空闲块,并且mchunk_size
字段中P
标志位的值仍为 1,表示上一个chunk
是一个已分配块,与实际情况符合。
对于下一个chunk
而言,obj4.data_
所在的chunk
是一个空闲块。因此,下一个chunk
里面mchunk_size
字段中P
标志位的值应该为 0。但实际上,下一个chunk
里面mchunk_size
字段为 0x20cc1,即P
标志位的值是 1,不符合预期结果。
我们可以推断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)继续执行
(gdb) n
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
现在是一个空闲块,并且mchunk_size
字段中P
标志位的值仍为 1,表示上一个chunk
(即obj4.data_
所在的chunk
,已经被释放了)是一个已分配块,不符合实际情况。
对于下一个chunk
而言,obj3.data_
所在的chunk
是一个空闲块。因此,下一个chunk
里面mchunk_size
字段中P
标志位的值应该为 0。但实际上,下一个chunk
里面mchunk_size
字段为 0x21,即P
标志位的值是 1,不符合预期结果。
另外,下一个chunk
里面mchunk_prev_size
字段的值应该为 0x50,从而表示上一个chunk
(是一个空闲块)的大小。但实际上,该字段的值是 0x0,也不符合预期结果。
我们可以推断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
现在是一个空闲块,并且mchunk_size
字段中P
标志位的值仍为 1。
对于下一个chunk
而言,obj1.data_
所在的chunk
是一个空闲块。因此,下一个chunk
里面mchunk_size
字段中P
标志位的值应该为 0。但实际上,下一个chunk
里面mchunk_size
字段为 0x21,即P
标志位的值是 1,不符合预期结果。
另外,下一个chunk
里面mchunk_prev_size
字段的值应该为 0x20,从而表示上一个chunk
(是一个空闲块)的大小。但实际上,该字段的值是 0x0,也不符合预期结果。
我们可以推断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 的大小 | |
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 的大小 | |
16 | fd | 8 | 表示空闲块链表中的下一个 chunk | 无 |
24 | bk | 8 | 表示空闲块链表中的上一个 chunk | 无 |
32 | fd_nextsize | 大于等于 0 | 未使用 | 无 |
遗留问题:
第一个chunk
的来龙去脉?
为什么在调用free()
函数后,被释放的chunk
从内存数据来看仍是一个已分配块?
空闲块是如何被划分到不同的空闲块链表中来维护的?
malloc_chunk
结构体中mchunk_size
字段里面的A
标志位是如何运用的?(见下一篇)
malloc_chunk
结构体中mchunk_size
字段里面的M
标志位是如何运用的?
malloc_chunk
结构体中fd_nextsize
和bk_nextsize
字段在large blocks
中是如何运用的?
下一篇我们结合malloc
的实现源码来分析上述可执行目标文件vm4_main
动态申请和释放堆内存的过程。