メモリリークを検出するためにnew/deleteをフックしてみた


# この記事について
この記事は、`mtrace`とかリークを検出できるツールを入れられないといった環境で開発を強いられた方々のお役に立てたらと思い作成しました。
(そういえば、new/deleteってhookできるんですね)

# メモリリークとは
プログラムが確保したメモリを解放するのを忘れ、確保したままになってしまうこと
次第にメモリ資源を食いつぶしてゆき，いずれプログラムやシステムに異常をきたすという問題です。

# フックとは
プログラム中の特定の箇所に、後から別のプログラムによって処理を追加できる仕組みをフックという。  
**共有ライブラリをフックすることで、元のバイナリに手を加えずにいろんな関数を差し替えることができる便利な機能です。**

この記事では`DYLD_INSERT_LIBRARIES`,`LD_PRELOAD`を使った共有ライブラリのhookをしていきます。


## 動作環境
以下の環境を想定しています。

- Ubuntu(dockerの) or mac

## hookする共有ライブラリの準備
- 下のファイルをビルドする

```c++:hook.cpp
#include <dlfcn.h>
#include <stdio.h>
#include <stdarg.h>
#include <stdlib.h>

/*
g++ -fPIC -shared hook.cpp  -o hook.so -rdynamic -ldl
*/

namespace Hook
{
    struct _hook
    {
        _hook()
        {
            printf("Start new/delete Hook --------------\n");
        }
        ~_hook()
        {
            printf("Finish new/delete Hook -------------\n");
        }
    };
    _hook tmp;

};

/* new,new[]では確保したメモリのサイズ,アドレス,呼び出し元の関数を表示させる　*/
void *operator new(size_t n)
{
    Dl_info info;
    dladdr(__builtin_return_address(0), &info);
    void *p = malloc(n);
    printf("[%s]hook new operator alloc size :%lu[%p]\n", info.dli_sname, n, p);
    return p;
}
void *operator new[](size_t n)
{
    /* 呼び出した関数名を取得する。 */
    Dl_info info;
    dladdr(__builtin_return_address(0), &info);
    void *p = malloc(n);

    printf("[%s]hook new operator[] alloc size :%lu[%p]\n", info.dli_sname, n, p);
    return p;
}

/* delete,delete[]ではfreeにするアドレス,呼び出し元の関数を表示させる　*/
void operator delete(void *p)
{
    Dl_info info;
    dladdr(__builtin_return_address(0), &info);
    printf("[%s]hook delete operator : [%p]\n", info.dli_sname, p);
    free(p);
}

void operator delete[](void *p)
{
    Dl_info info;
    dladdr(__builtin_return_address(0), &info);
    printf("[%s]hook delete[] operator : [%p]\n", info.dli_sname, p);
    free(p);
}
```


```shell:terminal
# 以下のファイルをこんな感じでビルドする。
g++ -fPIC -shared hook.cpp  -o hook.so -rdynamic -ldl
```


## テスト用のソースをビルドする

- dynamic オプションをつけてビルドするだけ

```sh
# テスト対象のソースのビルドはこんな感じ
g++ ../src/test.cpp -o main.out -rdynamic 
```

```cpp:test.cpp
#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 tmp;

    printf("finish main function---------------\n");
    return 0;
}
```

## ビルドしたソースを実行してみる

### 普通に実行した場合

まあ、いろいろリークしてるけど普通に動く

```shell
(base) root@3ab9ddec85a8:~/# ./main.out 
start main function---------------
test class constructor
test class destructor
finish main function---------------
(base) root@3ab9ddec85a8:~/# 
```
### new/deleteをフックさせて実行する

`LD_PRELOAD`に作成したライブラリのパスを指定してテスト用のファイルを実行するだけ。

```sh:Linuxで実行する場合
LD_PRELOAD=./hook.so ./main.out
```
macの場合、DYLD_INSERT_LIBRARIESで実行する必要があるらしい

```sh:macで実行する場合
DYLD_INSERT_LIBRARIES=./hook.so  DYLD_FORCE_FLAT_NAMESPACE=YES ./main.out
``` 

### 実行結果  
new/deleteした際に自作のnew/deleteのoperator関数が動作していることがわかると思う。
あとは、newされたアドレスとdeleteされたアドレスが対応しているかを見ればとりあえず
解放が漏れているかどうか確認できる。

```sh
(base) root@3ab9ddec85a8:~/# LD_PRELOAD=./hook.so ./main.out
Start new/delete Hook --------------
start main function---------------
[main]hook new operator[] alloc size :24[0x557c777206b0]
[main]hook new operator[] alloc size :12[0x557c777206d0]
[main]hook new operator alloc size :4[0x557c777206f0]
[main]hook new operator alloc size :8[0x557c77720710]
test class constructor
[_ZN4testC1Ev]hook new operator[] alloc size :8[0x557c77720730]
test class destructor
[_ZN4testD2Ev]hook delete[] operator : [0x557c77720730]
[main]hook delete operator : [0x557c77720710]
finish main function---------------
Finish new/delete Hook -------------
(base) root@3ab9ddec85a8:~/# 
```

---

## とりあえず、解説

### LD_PRELOADについて
   LD_PRELOADは、プリロードするオブジェクトを指定する環境変数でこれが指定されていると、実行時に指定された共有オブジェクトがロードされ、その後でバイナリファイルで指定された共有オブジェクトが処理される。そのため、自作した共有オブジェクトで作成された関数が先に定義され既存の関数に置き換えて動作させることができる。

### namespaceについて
   `LD_PRELOAD`した際のログを見た際、気になった方もいると思うので一応解説です。
namespace内のコンストラクタはmain関数よりも前に実行され、デストラクタはmain関数の後で実行されます。これは、普通に実行した場合も`LD_PRELOAD`で実行した場合でもmain関数の前後で動かすことができます。
pythonのようなC++で作成されたバイナリでも同じように`LD_PRELOAD`を使い、事前に何か処理をさせることができます。


```sh
(base) root@3ab9ddec85a8:~/# LD_PRELOAD=./hook.so python3
Start new/delete Hook --------------
Python 3.9.7 (default, Sep 16 2021, 13:09:58) 
[GCC 7.5.0] :: Anaconda, Inc. on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> exit()
Finish new/delete Hook -------------
(base) root@3ab9ddec85a8:~/# 

```

## 最後に

`LD_PRELOAD`は便利な機能ではありますがいろいろ組み合わせて悪用する事もできますので使い方などご注意ください。

あと、new/deleteなどの中でstd::mapなど迂闊にコンテナとかを使うと再起的にnewが呼ばれたりするので注意してください。

~~実行結果で`newされたアドレスとdeleteされたアドレスが対応しているかを見ればとりあえず
解放が漏れているかどうか確認できる。`とは書きましたが、まぁ不親切ですよね。。
後日、リークしたメモリを検出する方法も含めて書きます。~~

以下のリンクにリークしたメモリを出力する機能を追加した記事を作成しました。
- [new/deleteをフックしてメモリリークを検出した!!](https://qiita.com/rsm223_rip/items/cc7bcddcdc4ead78f30e)

## 参考

- [Binary hacks](https://www.amazon.co.jp/Binary-Hacks-%E2%80%95%E3%83%8F%E3%83%83%E3%82%AB%E3%83%BC%E7%A7%98%E4%BC%9D%E3%81%AE%E3%83%86%E3%82%AF%E3%83%8B%E3%83%83%E3%82%AF100%E9%81%B8-%E9%AB%98%E6%9E%97-%E5%93%B2/dp/4873112885/ref=sr_1_1?__mk_ja_JP=%E3%82%AB%E3%82%BF%E3%82%AB%E3%83%8A&crid=15BL0FVMVED1M&keywords=binary+hacks&qid=1639925079&sprefix=Binary%2Caps%2C309&sr=8-1)  ⇦  色々と参考になる便利な本です。