源码在

1
C:\Program Files (x86)\Windows Kits\10\Source\10.0.26100.0\ucrt\heap

malloc.cpp文件非常简洁:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
//
// malloc.cpp
//
// Copyright (c) Microsoft Corporation. All rights reserved.
//
// Implementation of malloc().
//
#include <corecrt_internal.h>
#include <malloc.h>



// Allocates a block of memory of size 'size' bytes in the heap. If allocation
// fails, nullptr is returned.
//
// This function supports patching and therefore must be marked noinline.
// Both _malloc_dbg and _malloc_base must also be marked noinline
// to prevent identical COMDAT folding from substituting calls to malloc
// with either other function or vice versa.
extern "C" _CRT_HYBRIDPATCHABLE __declspec(noinline) _CRTRESTRICT void* __cdecl malloc(size_t const size)
{
#ifdef _DEBUG
return _malloc_dbg(size, _NORMAL_BLOCK, nullptr, 0);
#else
return _malloc_base(size);
#endif
}

可以看到就是调试debug模式与其他。

先看看release模式:进入到_malloc_base

mallocbase也很简洁:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
//
// malloc_base.cpp
//
// Copyright (c) Microsoft Corporation. All rights reserved.
//
// Implementation of _malloc_base(). This is defined in a different source file
// from the malloc() function to allow malloc() to be replaced by the user.
//
#include <corecrt_internal.h>
#include <malloc.h>
#include <new.h>



// This function implements the logic of malloc(). It is called directly by the
// malloc() function in the Release CRT and is called by the debug heap in the
// Debug CRT.
//
// This function must be marked noinline, otherwise malloc and
// _malloc_base will have identical COMDATs, and the linker will fold
// them when calling one from the CRT. This is necessary because malloc
// needs to support users patching in custom implementations.
extern "C" __declspec(noinline) _CRTRESTRICT void* __cdecl _malloc_base(size_t const size)
{
// Ensure that the requested size is not too large:
_VALIDATE_RETURN_NOEXC(_HEAP_MAXREQ >= size, ENOMEM, nullptr);

// Ensure we request an allocation of at least one byte:
size_t const actual_size = size == 0 ? 1 : size;

for (;;)
{
void* const block = HeapAlloc(__acrt_heap, 0, actual_size);
if (block)
return block;

// Otherwise, see if we need to call the new handler, and if so call it.
// If the new handler fails, just return nullptr:
if (_query_new_mode() == 0 || !_callnewh(actual_size))
{
errno = ENOMEM;
return nullptr;
}

// The new handler was successful; try to allocate again...
}
}

感觉注释已经讲得很清楚了

分解一下看看,第一行代码是

1
_VALIDATE_RETURN_NOEXC(_HEAP_MAXREQ >= size, ENOMEM, nullptr);

这是一个宏,用来做参数检查。宏展开后逻辑大致类似:

1
2
3
4
5
if (size > _HEAP_MAXREQ)
{
errno = ENOMEM;
return nullptr;
}

也就是说,CRT 会先检查用户请求的大小是不是超过了运行时允许的最大分配尺寸。这个最大值 _HEAP_MAXREQ 通常接近 SIZE_MAX,但略小一点,因为堆管理器还需要在块前面放 header。如果请求太大,直接失败并设置 errnoENOMEM。从内存角度看,这一步只是防止一个明显不可能完成的请求继续往下执行。

接下来是一个很小但很重要的处理:

1
size_t const actual_size = size == 0 ? 1 : size;

也就是说,如果用户调用

1
malloc(0);

CRT 实际会把请求改成至少分配 1 个字节。原因是 C 标准允许 malloc(0) 返回 nullptr 或返回一个可释放的指针,而 Windows 的实现选择第二种策略。这样做有两个好处:第一,调用者仍然可以把这个指针交给 free;第二,分配器的内部逻辑不需要处理“0 字节块”这种特殊情况。

到这里为止,函数只是把请求做了合法化处理。真正的分配发生在下面的循环里:

1
2
3
4
5
for (;;)
{
void* const block = HeapAlloc(__acrt_heap, 0, actual_size);
if (block)
return block;

这里第一次出现了真正重要的函数:HeapAlloc。这就是 Windows 的用户态堆分配 API。__acrt_heap 是 CRT 在初始化时获取的进程默认堆句柄,通常来自 GetProcessHeap()。因此这行代码的真实含义其实是:到当前进程的堆里,找一块大小为 actual_size 的空闲内存块。

然后还检查了一次:

1
2
3
4
5
6
7
8
// If the new handler fails, just return nullptr:
if (_query_new_mode() == 0 || !_callnewh(actual_size))
{
errno = ENOMEM;
return nullptr;
}

// The new handler was successful; try to allocate again...

这里出现了 C++ 运行时和 C 运行时之间的一个桥接机制。_query_new_mode() 用来查询当前 CRT 是否启用了new-handler 模式。在 C++ 里,如果 new 分配失败,可以触发一个用户注册的函数(通过 std::set_new_handler)。CRT提供了一种兼容机制:当 malloc 失败时,也可以尝试调用这个 handler。

简单总结一下就是它首先检查请求是否合法,然后把 0 字节请求转换为最小分配单位,接着调用 Windows 堆分配函数 HeapAlloc。如果堆分配失败,它会尝试通过 C++ 的 new-handler 机制释放一些内存,再重复尝试;只有在 handler 也无法解决问题时,才真正返回 nullptr 并设置 errnoENOMEM

HeapAlloc又在哪呢?