文章524
标签277
分类67

简单实现C++内存分配跟踪

有的时候我们想要跟踪我们的代码到底分配了多少的内存,一个常用的方法是使用 Valgrind 工具进行内存分析;

但是对于一些场景,我们不想这么麻烦,那么此时我们可以通过简单的覆盖 malloc、free 等函数实现!

源代码:


简单实现C++内存分配跟踪

简单实现

我们知道,C、C++ 中主要是通过 malloc、free 函数以及 new、delete、new[]、delete[] 等运算符进行内存分配;

对于 malloc、free 函数而言:

虽然他们是在编译时通过链接到 glibc 库加入的,但是我们可以通过宏定义来替换他们,从而实现自己的逻辑;

对于 new、delete 等运算符,可以通过全局覆盖(override)他们来实现 Hook;

具体实现的代码很简单,如下:

track_memory.h

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
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
#ifndef TRACK_MEMORY_H_
#define TRACK_MEMORY_H_

/**
* Implements simple memory tracking for use in C++ applications.
* Define: TRACK_MEMORY to enable during debug and unit testing, and undef for production.
* Define: PRINT_MEMORY_TRACKING to print memory tracking
*/

#include <cassert>
#include <cstdio>
#include <cstdlib>

#ifdef TRACK_MEMORY
#include <map>

size_t gAllocatedMemory = 0;
bool gTrackAllocation = true;
typedef std::map<void *, size_t> AllocationMap;
static AllocationMap gAllocationMapStandard;
static AllocationMap gAllocationMapArray;

/**
* Allocates using a map to keep track of sizes.
*/
inline void *wrap_malloc(size_t size,
const char *file,
int line,
const char *func,
AllocationMap &map = gAllocationMapStandard) {

assert(size != 0);
void *ptr;

if (gTrackAllocation) {
gAllocatedMemory += size;
ptr = malloc(size);

gTrackAllocation = false;
map[ptr] = size;
gTrackAllocation = true;

#ifdef PRINT_MEMORY_TRACKING
printf("[Malloc %s:%d:%s] Allocated mem 0x%8.8lx: %8ld (%ld)\n",
file, line, func, (unsigned long) ptr, gAllocatedMemory, size);
#endif
} else {
ptr = malloc(size);
}

if (ptr == nullptr)
throw std::bad_alloc();
else
return ptr;
}

/**
* Deletes stuff allocated with tracked_new.
*/
inline void wrap_free(void *ptr,
const char *file,
int line,
const char *func,
AllocationMap &map = gAllocationMapStandard) {

if (gTrackAllocation) {
size_t size = map[ptr];
assert(size != 0);
gAllocatedMemory -= size;

#ifdef PRINT_MEMORY_TRACKING
printf("[Delete %s:%d:%s] Deallocated mem 0x%8.8lx: %8ld (-%ld)\n",
file, line, func, (unsigned long) ptr,
gAllocatedMemory,
size);
#endif

gTrackAllocation = false;
map.erase(ptr);
gTrackAllocation = true;
}

free(ptr);
}

#define malloc(X) wrap_malloc(X, __FILE__, __LINE__, __FUNCTION__)
#define free(X) wrap_free(X, __FILE__, __LINE__, __FUNCTION__)

void *operator new(size_t size) noexcept(false) {
return wrap_malloc(size, __FILE__, __LINE__, __FUNCTION__, gAllocationMapStandard);
}

void *operator new[](size_t size) noexcept(false) {
return wrap_malloc(size, __FILE__, __LINE__, __FUNCTION__, gAllocationMapArray);
}

void operator delete(void *ptr) noexcept {
wrap_free(ptr, __FILE__, __LINE__, __FUNCTION__, gAllocationMapStandard);
}

void operator delete[](void *ptr) noexcept {
wrap_free(ptr, __FILE__, __LINE__, __FUNCTION__, gAllocationMapArray);
}

#endif /* TRACK_MEMORY */

#endif // TRACK_MEMORY_H_

为了使用起来更加方便,我定义了:

  • TRACK_MEMORY:启用内存监控的总开关;
  • PRINT_MEMORY_TRACKING:是否打印内存分配内容;

同时,定义了:gAllocationMapStandard 和 gAllocationMapArray 来保存分配的内存空间;

并且使用 gAllocatedMemory 来记录当前已分配的字节数;


覆盖malloc 和 free

代码中,wrap_malloc 和 wrap_free 包装了在 glibc 中的 malloc 和 free 函数;

随后,通过宏覆盖了原来的 malloc 和 free:

1
2
#define malloc(X) wrap_malloc(X, __FILE__, __LINE__, __FUNCTION__)
#define free(X) wrap_free(X, __FILE__, __LINE__, __FUNCTION__)

注意:这里必须要先定义函数,再进行宏替换,否则会将 wrapper 函数中的 malloc 也给替换掉!

这里实现的是单线程的内存分配跟踪,对于多线程的实现,需要使用 Lock 将上面共享的内容保护起来!


覆盖new和delete

上面的代码完成了对 malloc 和 free 函数的覆盖;

那么对于 new和delete 等运算符来说,我们只需要调用上面已经实现的包装函数即可:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
void *operator new(size_t size) noexcept(false) {
return wrap_malloc(size, __FILE__, __LINE__, __FUNCTION__, gAllocationMapStandard);
}

void *operator new[](size_t size) noexcept(false) {
return wrap_malloc(size, __FILE__, __LINE__, __FUNCTION__, gAllocationMapArray);
}

void operator delete(void *ptr) noexcept {
wrap_free(ptr, __FILE__, __LINE__, __FUNCTION__, gAllocationMapStandard);
}

void operator delete[](void *ptr) noexcept {
wrap_free(ptr, __FILE__, __LINE__, __FUNCTION__, gAllocationMapArray);
}

这里需要注意的是:

由于运算符重载函数是无法 inline 的,因此 __LINE__、__FILE__ 实际上只能获取到这个文件的路径;

并且,由于我们在重载时,无法实现在和原来的 void *operator new(size_t size) 函数签名完全相同下、增加入参的重载(否则会不知道究竟使用哪个实现!)

如果有小伙伴们知道怎么做,可以在下方留言评论~

关于重写库函数或系统调用:

至此,我们的功能编写完成!


测试用例

在使用时,我们在源代码的开头引入这个头文件,并定义对应的宏开启功能即可!

即:

1
2
3
4
#define TRACK_MEMORY
#define PRINT_MEMORY_TRACKING

#include "track_memory.h"

例如:

main.cc

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#define TRACK_MEMORY
#define PRINT_MEMORY_TRACKING

#include "track_memory.h"

int main() {
char *buf = new char[100];
delete[] buf;

struct TestStruct {
long a;
long b;
};

auto *testStruct = new TestStruct;
delete testStruct;

auto *ptr = malloc(sizeof(TestStruct));
free(ptr);
}

执行后输出:

1
2
3
4
5
6
[Malloc /root/data/cpp-learn/track_memory.h:94:operator new []] Allocated mem 0x01b0f260:      100 (100)
[Delete /root/data/cpp-learn/track_memory.h:102:operator delete []] Deallocated mem 0x01b0f260: 0 (-100)
[Malloc /root/data/cpp-learn/track_memory.h:90:operator new] Allocated mem 0x01b0f720: 16 (16)
[Delete /root/data/cpp-learn/track_memory.h:98:operator delete] Deallocated mem 0x01b0f720: 0 (-16)
[Malloc /root/data/cpp-learn/main.cc:18:main] Allocated mem 0x01b0f720: 16 (16)
[Delete /root/data/cpp-learn/main.cc:19:main] Deallocated mem 0x01b0f720: 0 (-16)

即可完成对内存分配的跟踪,非常方便!


附录

源代码:

文章参考:



本文作者:Jasonkay
本文链接:https://jasonkayzk.github.io/cn/2022/11/12/%E7%AE%80%E5%8D%95%E5%AE%9E%E7%8E%B0C-%E5%86%85%E5%AD%98%E5%88%86%E9%85%8D%E8%B7%9F%E8%B8%AA/
版权声明:本文采用 CC BY-NC-SA 3.0 CN 协议进行许可