この記事について
この記事は、mtrace
とかリークを検出できるツールを入れられないといった環境で開発を強いられた方々のお役に立てたらと思い作成しました。
(そういえば、new/deleteってhookできるんですね)
メモリリークとは
プログラムが確保したメモリを解放するのを忘れ、確保したままになってしまうこと
次第にメモリ資源を食いつぶしてゆき,いずれプログラムやシステムに異常をきたすという問題です。
フックとは
プログラム中の特定の箇所に、後から別のプログラムによって処理を追加できる仕組みをフックという。
共有ライブラリをフックすることで、元のバイナリに手を加えずにいろんな関数を差し替えることができる便利な機能です。
この記事ではDYLD_INSERT_LIBRARIES
,LD_PRELOAD
を使った共有ライブラリのhookをしていきます。
動作環境
以下の環境を想定しています。
- Ubuntu(dockerの) or mac
hookする共有ライブラリの準備
- 下のファイルをビルドする
#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);
}
# 以下のファイルをこんな感じでビルドする。
g++ -fPIC -shared hook.cpp -o hook.so -rdynamic -ldl
テスト用のソースをビルドする
- dynamic オプションをつけてビルドするだけ
# テスト対象のソースのビルドはこんな感じ
g++ ../src/test.cpp -o main.out -rdynamic
#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
に作成したライブラリのパスを指定してテスト用のファイルを実行するだけ。
LD_PRELOAD=./hook.so ./main.out
macの場合、DYLD_INSERT_LIBRARIESで実行する必要があるらしい
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されたアドレスが対応しているかを見ればとりあえず 解放が漏れているかどうか確認できる。
とは書きましたが、まぁ不親切ですよね。。
後日、リークしたメモリを検出する方法も含めて書きます。
以下のリンクにリークしたメモリを出力する機能を追加した記事を作成しました。
参考
- Binary hacks ⇦ 色々と参考になる便利な本です。