この記事について
この記事は先日作成した、下記リンクの続きとしてリークの検出するための機能についての記事です。
ここでは、new/deleteで確保したメモリがmain関数終了後にどれだけ残っているかを出力させる方法を書いてみようと思います。
メモリリークを検出するためにnew/deleteをフックしてみた
先日の記事での問題点
先日の記事では、new/deleteをフックしてアドレスを出力しているだけだったため、リークしているメモリを見つけるには、 newした時のアドレスとdeleteした時のログからペアになるアドレスを探さないといけない といったものでした。
どんな機能を追加したのか
以下の機能を追加しました。
- new/delete で確保/削除したアドレスを管理する機能
- C++ のmapで管理しようかと思ったが、内部でnewとかが呼ばれるのでほぼC言語で配列を作っているだけです。
- main関数終了後、確保されているアドレスを出力する機能
- まぁ、ただのデストラクタです。。
- ついでに、リターンアドレスから関数名を取得する機能の追加
- main関数はうまくいかなかったから、無理矢理作った。。。
実装
変数名とか適当につけたため、読みにくいかもしれないがこんな感じ、これを下記のコマンドでビルドするだけ。
g++ -fPIC -shared ../src/hook.cpp -o hook.so -rdynamic -ldl -std=c++2a
#include <dlfcn.h>
#include <stdio.h>
#include <stdarg.h>
#include <stdlib.h>
#include <cxxabi.h>
#include <cstring>
#include <iostream>
namespace HOOK
{
struct _hook
{
_hook()
{
printf("Start Hook Lib--------------\n");
}
~_hook()
{
printf("Finish Hook Lib-------------\n");
}
};
_hook tmp;
};
/* リターンアドレスから関数名を名をデマングルする関数 */
const char *ConvRetAddrToDmglFuncName(void *returnAddress);
/* allocateしたアドレスを保存する構造体 */
typedef struct mapedUnit
{
unsigned int index; /* index */
void *allocedPtr; /* allocated pointor */
void *returnAddress; /* return Addr */
} mapedUnit;
namespace MemManage
{
class pManager
{
public:
/* newしたアドレスを管理対象に追加する関数 */
void addMap(void *p, void *retAddr);
/* deleteしたアドレスを管理対象から削除する関数 */
char *removeMap(void *p);
pManager();
~pManager();
private:
mapedUnit *ptr;
int counter = 0;
unsigned int index = 0;
int mapSize = sizeof(mapedUnit) * 255;
};
pManager _pManager;
}
#include "hook.hpp"
/*
g++ -fPIC -shared hook.cpp -o hook.so -rdynamic -ldl -std=c++20
*/
void *operator new(size_t size)
{
void *retAddr = __builtin_return_address(0);
void *p = malloc(size);
printf("[new (size:%4lu)][called: %s]:[%p]\n",
size,
ConvRetAddrToDmglFuncName(retAddr),
p);
MemManage::_pManager.addMap(p, retAddr);
return p;
}
void *operator new[](size_t size)
{
void *retAddr = __builtin_return_address(0);
void *p = malloc(size);
printf("[new[] (size:%4lu)][called: %s]:[%p]\n",
size,
ConvRetAddrToDmglFuncName(retAddr),
p);
MemManage::_pManager.addMap(p, retAddr);
return p;
}
void operator delete(void *p)
{
void *retAddr = __builtin_return_address(0);
char *allocedFunc = MemManage::_pManager.removeMap(p);
printf("[delete ][called: %s]: [%p](allocated: %s)\n",
ConvRetAddrToDmglFuncName(retAddr),
p,
allocedFunc);
free(p);
}
void operator delete[](void *p)
{
void *retAddr = __builtin_return_address(0);
char *allocedFunc = MemManage::_pManager.removeMap(p);
printf("[delete[]][called: %s]: [%p](allocated: %s)\n",
ConvRetAddrToDmglFuncName(retAddr),
p,
allocedFunc);
free(p);
}
void MemManage::pManager::addMap(void *p, void *retAddr)
{
// void *(*NewFunc)(size_t) = operator new;
mapedUnit *arr = ptr + counter;
mapedUnit tmp = {index, p, retAddr};
memcpy(arr, &tmp, sizeof(mapedUnit));
counter++;
index++;
if (counter > mapSize)
{
mapSize += mapSize;
mapedUnit *pTmp = (mapedUnit *)realloc(ptr, mapSize);
if (pTmp == nullptr)
{
exit(1);
}
else if (ptr != pTmp)
{
free(ptr);
ptr = pTmp;
}
}
};
char *MemManage::pManager::removeMap(void *p)
{
mapedUnit *endPtr = ptr + (counter - 1);
char *str = (char *)"this pointor not found";
for (int i = counter - 1; 0 <= i; i--)
{
mapedUnit *arr = ptr + i;
// 受け取ったポインタが格納したポインタと同じだった場合
if (arr->allocedPtr == p)
{
str = (char *)ConvRetAddrToDmglFuncName(arr->returnAddress);
// 末尾のアドレスと同じじゃない場合、末尾の中身で書き換える
if (endPtr != arr)
{
memcpy(arr, endPtr, sizeof(mapedUnit));
}
// 末尾の構造体を空にする
memset(endPtr, 0x00, sizeof(mapedUnit));
counter--;
break;
}
}
return str;
};
MemManage::pManager::pManager()
{
counter = 0;
ptr = (mapedUnit *)malloc(mapSize);
memset(ptr, 0x00, mapSize);
printf("[start]Memory management------------\n");
}
MemManage::pManager::~pManager()
{
printf("[end]Memory management------------\n"
"🌟these pointor is leaked\n");
mapedUnit tmp;
memset(&tmp, 0x00, sizeof(mapedUnit));
for (int i = 0; i <= counter + 2; i++)
{
mapedUnit *arr = ptr + i;
// 空だった場合、continueする
if (!memcmp(arr, &tmp, sizeof(mapedUnit)))
{
continue;
}
printf("[index: %4d][allocated:%s (%p)](%p)\n",
arr->index,
ConvRetAddrToDmglFuncName(arr->returnAddress),
arr->returnAddress,
arr->allocedPtr);
// 一応、freeしておく
free(arr->allocedPtr);
}
printf("-----------------\n");
free(ptr);
};
const char *ConvRetAddrToDmglFuncName(void *returnAddress)
{
Dl_info info;
dladdr(returnAddress, &info);
int status;
char *demangled = abi::__cxa_demangle(info.dli_sname, 0, 0, &status);
return (demangled != nullptr) ? demangled : "main()";
}
テスト対象のソース
前とほぼ同じソースなので、ビルド方法とかは割愛します!
#include <stdio.h>
#include <iostream>
/* テスト用クラス */
class test
{
public:
int *t;
test()
{
printf("test class constructor\n");
t = new int[2]{1, 2};
}
~test()
{
printf("test class destructor\n");
delete[] t;
}
};
int main()
{
/* 色々newしているが、deleteで解放してない */
printf("start main function---------------\n");
int *i = new int[6]{1, 2, 3, 4, 5, 6};
char *str = new char[12];
int *j = new int(0);
test *tmp = new test();
delete str;
// delete tmp;
printf("finish main function---------------\n");
return 0;
}
実行結果
普通に実行した場合
まぁ、前と同じソースなので変わりません、、
(base) root@3ab9ddec85a8:~# ./main.out
start main function---------------
test class constructor
finish main function---------------
(base) root@3ab9ddec85a8:~#
hookして実行した場合
メイン関数終了後、確保したアドレスを管理していたクラスのデストラクターで解放されていないアドレスとnewを呼び出した関数を出力させた。
indexはメモリを確保した順番だが、面倒なのでソートするのはやめました。
まぁ、確保した時のタイムスタンプでもよかったかもしれないですね笑
(base) root@3ab9ddec85a8:~/Desktop/workspace/buildUb# LD_PRELOAD=./hook.so ./main.out
Start Hook Lib--------------
[start]Memory management------------
start main function---------------
[new[] (size: 24)][called: main()]:[0x5579b653cab0]
[new[] (size: 12)][called: main()]:[0x5579b653cad0]
[new (size: 4)][called: main()]:[0x5579b653caf0]
[new (size: 8)][called: main()]:[0x5579b653cb10]
test class constructor
[new[] (size: 8)][called: test::test()]:[0x5579b653cb30]
[delete ][called: main()]: [0x5579b653cad0](allocated: main())
finish main function---------------
[end]Memory management------------
🌟these pointor is leaked
[index: 0][allocated:main() (0x5579b523022e)](0x5579b653cab0)
[index: 4][allocated:test::test() (0x5579b5230388)](0x5579b653cb30)
[index: 2][allocated:main() (0x5579b5230288)](0x5579b653caf0)
[index: 3][allocated:main() (0x5579b523029c)](0x5579b653cb10)
-----------------
Finish Hook Lib-------------
解説
解説しようと思ったけど、特に難しいことはしてないのでやっぱりやめます。
(聞きたいことなどあれば、答えます。)
最後に
C/C++でメモリリークは誰もがうっかり作り込んでしまう問題だとは思いますが、根気強く対処していきましょう!
ソースコードを見て気がついたかと思いますが、たぶんマルチスレッドではうまく動かないです。
後日、時間がある時に対策するための記事を書いてみます。
今回はhookして検出するのが目的でしたのでやってませんが、静的にリンクさせても問題ないはずです。
また、windowsでもリターンアドレスと関数のデマングル以外は特にコンパイラに依存しないように作ったので、その辺りを直せば動くと思います。(まぁ、試してないんですけど。。😅)