LoginSignup
5
2

More than 1 year has passed since last update.

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

Last updated at Posted at 2021-12-19

この記事について

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

メモリリークとは

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

フックとは

プログラム中の特定の箇所に、後から別のプログラムによって処理を追加できる仕組みをフックという。

共有ライブラリをフックすることで、元のバイナリに手を加えずにいろんな関数を差し替えることができる便利な機能です。

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

動作環境

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

  • Ubuntu(dockerの) or mac

hookする共有ライブラリの準備

  • 下のファイルをビルドする
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);
}
terminal
# 以下のファイルをこんな感じでビルドする。
g++ -fPIC -shared hook.cpp  -o hook.so -rdynamic -ldl

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

  • dynamic オプションをつけてビルドするだけ
# テスト対象のソースのビルドはこんな感じ
g++ ../src/test.cpp -o main.out -rdynamic 
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;
}

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

普通に実行した場合

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

(base) root@3ab9ddec85a8:~/# ./main.out 
start main function---------------
test class constructor
test class destructor
finish main function---------------
(base) root@3ab9ddec85a8:~/# 

new/deleteをフックさせて実行する

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

Linuxで実行する場合
LD_PRELOAD=./hook.so ./main.out

macの場合、DYLD_INSERT_LIBRARIESで実行する必要があるらしい

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

実行結果

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

(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を使い、事前に何か処理をさせることができます。

(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をフックしてメモリリークを検出した!!

参考

  • Binary hacks ⇦ 色々と参考になる便利な本です。
5
2
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
5
2