【C言語/gcc】関数をフックする(関数呼び出しのトレース等)

gccのフックの仕組みを紹介するページのアイキャッチ

このページにはプロモーションが含まれています

このページでは、gcc に用意された関数のフックについて紹介していきたいと思います。

gcc はC言語のコンパイラの1つです。今回は、この gcc に用意された関数のフックの仕組みを利用する方法フックを利用する例を示していきます。gcc 以外のコンパイラを利用している方も多いかも知れませんが、他のコンパイラの場合も同様の仕組みが用意されているケースも多いと思います。そのため、やり方は異なるかも知れませんが、他のコンパイラを利用されている方にも考え方や実現できることの例に関しては参考になるのではないかと思います。

なので、gcc を利用していない方も是非ページを読み進めていただければと思います!

フックとは

まずは、基礎的な話から簡単に説明していきたいと思います。すぐにフックの利用の仕方について知りたいという方は フックの利用の仕方 までスキップしてください。

最初にフックについて説明しておきます。フックとは、プログラムの特定の箇所に “独自の処理” を割り込ませる仕組みのことを言います。

フックの説明図

今回の例では、「関数の呼び出しタイミング」と「関数の終了タイミング」に、つまり関数の入り口の箇所と出口の箇所に独自の処理を割り込ませる例を紹介していきます。最終的には、独自の処理として実行された関数名を printf で出力する処理を割り込ませる例を示していきます。

ページの最初でも触れたように、今回は gcc に用意されたフックの仕組みを利用したものになります。おそらく、C言語であれば gcc 以外のコンパイラでも同様の仕組みが用意されているケースも多いと思います。また、他のプログラミング言語でもフックの仕組みが用意されている場合もありますし、フレームワークなどにもフックの仕組みが導入されている場合もあります。

このように、フックは様々な言語やフレームワーク、ソフトウェアに用意されている仕組みになります。

フックの利用例

続いて、フックの利用例について紹介しておきます。

せっかくなので、このサイトを例に説明していきたいと思います。このサイトはワードプレスというソフトウェアを利用して作成していますが、このワードプレスにもフックの仕組みが用意されています。そして、このフックを利用することで、製作者が独自のカスタマイズを行えるようになっています。ワードプレスで作成されているウェブサイトは非常に多く存在しますが、ウェブサイト毎にカスタマイズされているため、見た目や機能がウェブサイト毎に異なります。そして、このカスタマイズを実現するための仕組みの1つがフックとなります。

例えばですが、このページには目次が表示されるようになっています。この目次はフックの仕組みを利用して実現しているものになります。

このページに表示されている目次

具体的には、ワードプレスには「ページの表示を行う際に独自の処理を割り込ませるためのフックの仕組み」が用意されており、ここで「ページのコンテンツから見出しを探索し、その探索した見出しから目次を生成してコンテンツに挿入する」という処理を割り込ませることで目次の表示を実現することができるようになっています。

下図はフックを利用して目次を表示する際の処理の流れを示すイメージ図になります。本来であれば、コンテンツをデータベースから取得し、そのコンテンツがそのまま出力されてページとして表示されることになるのですが、フックの仕組みが存在するため、コンテンツ取得後に目次を生成し、それをコンテンツに挿入する処理を割り込ませて実行させることができるようになっています。その結果、ページに目次も含めたコンテンツが表示されることになります。

ワードプレスでフックの仕組みを利用して目次の表示を実現する様子

このように、フックの仕組みさえ用意しておけば、あとはユーザーが独自の処理を割り込ませることで、ユーザー独自のカスタマイズを行うことができるようになります。フックの仕組みがなければ、少なくともワードプレスの場合はカスタマイズできることが制限されてしまい、ここまで流行らなかったのではないかと思います。

また、このページでは、コンパイル対象となる各関数の「入り口」と「出口」に、”その関数の関数名と呼び出し元の関数の関数名を printf で出力する処理” を割り込ませる例”を紹介していきます。

例えば、急な異動によって大規模なシステムの開発を担当することになったとしましょう。大規模なシステムの場合、ソースコードの規模も大きくなり、各関数の呼び出しタイミングや各関数間の呼び出し関係を把握するのが困難になります。もちろん設計書に詳細なシーケンス図等が記載されていれば、この把握は容易なものとなります。ですが、設計書が無かったり、シーケンス図が記載されていなかったり、シーケンス図の粒度が荒いような場合、設計書だけでは各関数の呼び出しタイミングまでは把握できません。そして、こういったケースは非常に多く存在すると思います。

システムの処理の流れが分からずに担当者が困ってしまっている様子

単に関数の実行タイミングを知りたいだけであれば、各関数の入り口と出口あたりで printf を行う処理を直接追記することでも実現可能です。ですが、これだと関数が多いと追記が大変ですし、ソースコードの変更が必要になるため、最悪の場合、この変更によってシステムやプログラムの品質が低下する可能性もあります。

しかし、このページで紹介するフックの仕組みを利用すれば、既存のソースコードの変更なしに printf を実行する処理を割り込ませることが可能です。

関数の入り口と出口でprintfを割り込ませる様子

そして、その出力結果から、設計や実装の詳細を読み解くことができるようになります。例えば、関数の呼び出し関係をシーケンス図として出力してやれば、どんな流れで各関数が呼び出しされているかを見える化するようなこともできます。

処理の見える化によりシステムの処理の流れが理解できた様子

これにより、システムのソフトウェア構成を理解することもできますし、デバッグ等を効率的に行うこともできるようになります。

そして、前述の通り、フックの仕組みを利用すれば既存のソースコードの変更なしに上記のようなことを行うことが可能となります。ただし、既存のソースコードは変更不要ですが、新規でのソースコードの追加は必要となります。それでも、既存のソースコードの変更が不要なので、既存のソースコードの品質を低下させてしまうという心配は不要です。

スポンサーリンク

フックの利用の仕方

さて、ここからは gcc での関数の入り口と出口に特定の処理をフックするための手順について説明していきます。ここでは「funcs.c というソースコードのファイルに定義された各関数の入り口と出口」に特定の処理をフックする例を用いて説明していきたいと思います。

このフックする(割り込ませる)特定の処理は、funcs.c とは異なるファイルとして hook.c を用意し、そこに関数として定義していきます。そして、これらの2つのソースコードを別々にコンパイルして2つのオブジェクトファイルを生成し、最後にリンクを行なって1つの実行可能ファイルを作成していきます。

作成していくファイルや行う操作の説明図

2つのソースコードファイルをわざわざ用意し、それぞれを別々にコンパイルする理由は「ソースコード毎にコンパイル時に指定するオプションを異なるものにするため」になります。この辺りは後述の解説の中で詳細を解説していきます。

funcs.c の用意

前述の通り、ここでは例として funcs.chook.c の2つのソースコードファイルを用意していきます。

まずは funcs.c の用意の仕方、および funcs.c のコンパイルについて解説していきます。この funcs.c に関しては通常のC言語のソースコードと同様の手順で作成してやれば良いです。いつも通り関数を定義し、さらにプログラムのエントリーポイントとなる main 関数もいつも通り定義してください。既存のソースコードが存在するのであれば、それをそのまま利用するのでも良いです。フックを利用するからといって特別な手順でソースコードを用意する必要はありません。

実例としては、今回は下記のように実装した funcs.c を使ってフックの利用例を示していきたいと思います。かなりてきとうなソースコードになっていますが、特に何も変わった点のない普通のソースコードであることは理解していただけるのではないかと思います。

funcs.c
void funcD(void) {
    // do nothing
}

void funcC(void) {
    funcD();
}

void funcB(void) {
    funcC();
    funcD();
}

void funcA(void) {
    funcB();
    funcC();
}

int main(void) {
    funcA();
    funcB();
}

funcs.c のコンパイル

続いて、用意した funcs.c のコンパイルを行います。前述の通り、ソースコードとしては通常通りに用意していただければ良いのですが、コンパイルの仕方に関しては特別な手順が必要となります。

具体的には、gcc でソースコードをコンパイルする際に -finstrument-functions というオプションを指定する必要があります。

例えば、いつもは下記のようにコンパイルを行なっているのであれば、

gcc -c funcs.c -o funcs.o

フックの仕組みを利用するためには、下記のように追加で -finstrument-functions を指定してやる必要があります。

gcc -c funcs.c -o funcs.o -finstrument-functions

ちなみに、-c オプションはソースコードを単体でコンパイルのみを行うことを指示するためのオプションになります。後々 hook.c を単体でコンパイルした結果を生成し、これらをリンクすることで1つの実行可能ファイルを生成していくことになります。

また、上記は単なるコマンド実行例の1つあり、実現したいことに応じて別のコンパイルオプションを指定しても良いです。例えばデバッグ情報を生成したいのであれば -g オプションを指定しても良いです。重要なのは -finstrument-functions を指定する点になります。

さて、ここで追加で指定した -finstrument-functions はフックの仕組みを利用するために非常に重要なオプションとなります。この -finstrument-functions を指定してコンパイルを行なった場合、コンパイル対象のソースコードで定義される各関数の入り口と出口、すなわち各関数が呼び出されたタイミングと関数が終了するタイミングで特定の関数が呼び出されるようになります。

そして、その “特定の関数” とは下記の2つになります。つまり、関数の入り口、つまり関数が呼び出された際に __cyg_profile_func_enter が、関数の出口、つまり関数を抜け出す際に __cyg_profile_func_exit が割り込んで呼び出されるようになります。

  • 入り口:void __cyg_profile_func_enter(void* call_func, void* call_site)
  • 出口:void __cyg_profile_func_exit(void* call_func, void* call_site)

これらの関数を呼び出すような処理は実装不要です。なぜなら、-finstrument-functions を指定して gcc を実行することで、これらが各関数の入り口と出口で自動的に呼び出されるようにコンパイルが行われるからになります。

つまり、既存のソースコードを変更しなくても、関数の入り口(関数が呼び出されたタイミング)で __cyg_profile_func_enter が、関数の出口(関数が終了するタイミング)で __cyg_profile_func_exit が割り込んで呼び出されるようになります。

-finstrment-functionsオプションの効果を示す図

したがって、__cyg_profile_func_enter__cyg_profile_func_exit を独自の処理が実行されるように関数定義してやれば、まさにフックの意味合いの通り、プログラムの特定の箇所に独自の処理を割り込ませる仕組みを実現することができることになります。今回の場合は、”プログラムの特定の箇所” が各関数の入り口と出口であり、”独自の処理” が __cyg_profile_func_enter__cyg_profile_func_exit に実装した処理ということになります。

独自の処理が関数の入り口と出口とで実行されるようになる様子を示す図

スポンサーリンク

hook.c の用意

ただし、__cyg_profile_func_enter__cyg_profile_func_exit には独自の処理を実装する必要があり、これらの関数は別途自身で定義する必要があります。

ということで、次はこれらの関数の定義の仕方について解説していきます。前述の通り、funcs.c とは別のファイルとして hook.c を用意し、hook.c にこれらの関数を定義するようにしていきたいと思います。

__cyg_profile_func_enterと__cyg_profile_func_exitをhook.cに定義する様子

まず、これらの関数の型は決まっていて、共に2つの void* 型の引数を受け取り、さらに返却値の型は void である関数として定義する必要があります。これらは、__cyg_profile_func_enter__cyg_profile_func_exit を定義する際のルールとなります。

  • void __cyg_profile_func_enter(void* call_func, void* call_site);
  • void __cyg_profile_func_exit(void* call_func, void* call_site);

voidvoid* をご存知ない方は、下記ページで解説を行なっていますので、別途こちらのページを参照していただければと思います。

voidとvoid*型の解説ページのアイキャッチ 【C言語】void型とvoid*型(void型ポインタ)について解説

つまり、これらの関数を定義する際のテンプレは下記のようなものになります。内部の処理に関しては、自身が関数の入り口や出口で割り込ませたい処理を記述すれば良いです。

関数のテンプレ
void __cyg_profile_func_enter(void* call_func, void* call_site) {

}

void __cyg_profile_func_exit(void* call_func, void* call_site) {

}

これらは、フックの仕組みにより各関数の入り口と出口で自動的に呼び出されるようになる関数になります。自動的にこれらの関数が呼び出されるようになるのは、コンパイル時に -finstrument-functions オプションが指定された場合のみとなります。

そして、上記の2つの関数が呼び出しされる際には、1つ目の void* 型の引数 call_func、2つ目の void* 型の引数 call_site にはそれぞれ下記のようなアドレスが指定されることになります。この引数の指定も自動的に行われることになります。

  1. call_func:呼び出された関数のアドレス
  2. call_site:呼び出した位置のアドレス

メモリ上に配置されるものは全てアドレスが割り当てられて “メモリ上の位置” が管理できるようになっています。関数もプログラム実行時にメモリ上に配置されることになり、例に漏れることなくアドレスが割りてられます。そして、__cyg_profile_func_enter や __cyg_profile_func_exit が実行される際には、呼び出された関数のアドレス call_func と、その関数を呼び出し位置のアドレス call_site が引数と指定されることになります。

__cyg_profile_func_enterに指定される引数の説明図

ここでは、単に引数で指定されたアドレスを printf 関数で出力するような関数として __cyg_profile_func_enter__cyg_profile_func_exit を下記のように定義したいと思います。

関数の実装例
#include <stdio.h>

void __cyg_profile_func_enter(void* call_func, void* call_site) {
    printf("%p -> %p\n", call_site, call_func);
}

void __cyg_profile_func_exit(void* call_func, void* call_site) {
    printf("%p -> %p\n", call_func, call_site);
}

__cyg_profile_func_enter では call_siteの値 -> call_funcの値 を出力するため、この出力結果から、実行中の関数が call_site (呼び出し元) から call_func (呼び出し先) に遷移することが確認できるようになります。また、__cyg_profile_func_exit では call_funcの値 -> call_siteの値 を出力するため、この出力結果から、実行中の関数が call_func (呼び出し先) から call_site (呼び出し元) に遷移することが確認できるようになります。

関数の入り口と出口で実行中の関数が遷移する様子が出力されるようになることを示す図

ただ、単にアドレスが出力されたとしても人間の目では関数名までは把握できません。この解決方法に関しては後述の アドレスからの関数名の取得 で解説します。実は、これらの引数に指定されるアドレスから対応する関数名等を取得することが可能です。

また、上記のソースコードは単に例であって、__cyg_profile_func_enter__cyg_profile_func_exit の内部の処理に関しては、自身で割り込ませたい処理に応じて好きなように実装してやれば良いです。

hook.c のコンパイル

__cyg_profile_func_enter__cyg_profile_func_exit が hook.c に定義できたら、次は hook.c のコンパイルを行います。この hook.c のコンパイルを行う際は、funcs.c のコンパイル時と同様に -c オプションを指定してコンパイルのみを行うようにしてください。

gcc -c hook.c -o hook.o

この __cyg_profile_func_enter__cyg_profile_func_exit を定義しているソースコードのコンパイル時には -finstrument-functions オプションの指定はしないように注意してください。理由は単純で、-finstrument-functions を指定してコンパイルした場合、__cyg_profile_func_enter__cyg_profile_func_exit が延々と実行されるようになり、いずれはプログラムが異常終了することになります。

__cyg_profile_func_enter や __cyg_profile_func_exit も関数の1つです。そのため、これらが定義されているソースコードを -finstrument-functions オプションを指定してコンパイルすると、これらの関数の入り口と出口でも再度 __cyg_profile_func_enter や __cyg_profile_func_exit が呼び出されるようになります。

さらに、再度 __cyg_profile_func_enter や __cyg_profile_func_exit が呼び出される際にも、関数の入り口と出口とで再び __cyg_profile_func_enter や __cyg_profile_func_exit が呼び出されるようになり、この呼び出しが延々と無限に実行され続けることになります。

__cyg_profile_file_enterが延々と実行され続ける様子

この場合、いずれはプログラムが異常終了することになります。私の環境で実際に試した際には下記のようなエラーが発生しました。

zsh: segmentation fault  ./main.exe

このようなことにならないよう、__cyg_profile_func_enter__cyg_profile_func_exit を定義しているソースコードのコンパイル時には -finstrument-functions オプションの指定はしないようにする必要があります。つまり、フックを利用する場合、これらの __cyg_profile_func_enter__cyg_profile_func_exit を定義しているソースコードと、それ以外のソースコードとでコンパイルのオプションとは別々のものを指定する必要があります。

ソースコードに応じて-finstrment-functionオプションの指定の有無を変更する必要があることを示す図

そして、コンパイル時に別のオプションが指定できるように、今回はわざわざソースコードファイルを2つ用意するようにしています。

実は、ソースコードの書き方を変更すれば、特定の関数に関してのみフックの仕組みを無効化することも可能で、これについては後述の 特定の関数のフックの無効化 で解説します。

リンク

以上で、funcs.c のコンパイル結果と hook.c のコンパイル結果を得ることができましたので、最後にこれらのコンパイル結果をリンクして実行可能ファイルを生成します。

これらのコンパイル結果をそれぞれ funcs.ohook.o であるとし、更にこれらが同じフォルダ内に存在する場合、下記のコマンドを実行することで実行可能ファイルを生成することができます。

gcc funcs.o hook.o -o main.exe

スポンサーリンク

プログラムの実行

最後に、先ほど生成した実行可能ファイルの実行を行ってみましょう!これはいつも通り下記のようにコマンドラインで実行可能ファイルのパスを指定することで行うことができます。

./main.exe

ここまで紹介してきたソースコードをそのまま利用されてきた方であれば、この実行によって下記のような出力結果が得られるのではないかと思います。出力されるアドレスは下記のものとは異なるかもしれませんが、ここで重要なことは、下記のように “何かしらの出力が行われるようになった” という点になります。

0x7fff2051ef3d -> 0x106007ef0
0x106007f09 -> 0x106007ec0
0x106007ed9 -> 0x106007e90
0x106007ea9 -> 0x106007e60
0x106007e79 -> 0x106007e30
0x106007e30 -> 0x106007e79
0x106007e60 -> 0x106007ea9
0x106007eae -> 0x106007e30
0x106007e30 -> 0x106007eae
0x106007e90 -> 0x106007ed9
0x106007ede -> 0x106007e60
0x106007e79 -> 0x106007e30
0x106007e30 -> 0x106007e79
0x106007e60 -> 0x106007ede
0x106007ec0 -> 0x106007f09
0x106007f0e -> 0x106007e90
0x106007ea9 -> 0x106007e60
0x106007e79 -> 0x106007e30
0x106007e30 -> 0x106007e79
0x106007e60 -> 0x106007ea9
0x106007eae -> 0x106007e30
0x106007e30 -> 0x106007eae
0x106007e90 -> 0x106007f0e
0x106007ef0 -> 0x7fff2051ef3d

元々 funcs.c では printf 等での出力は全く行っていません。ですが、フックの仕組みを利用し、funcs.c の各関数の入り口と出口で __cyg_profile_func_enter と __cyg_profile_func_exit のそれぞれが割り込んで実行され、これらの関数の中で printf 関数を実行しているため、上記のような出力結果が得られるようになっています。

このように、フックの仕組みを利用することでプログラムの特定の箇所に独自の処理を割り込んで実行させることが可能となります。

gcc の場合は、ソースコードのコンパイル時に -finstrument-functions を指定することで、そのソースコードで定義される各関数の入り口と出口にフックの仕組みが適用されることになります。これにより、各関数の入り口と出口では __cyg_profile_func_enter と __cyg_profile_func_exit が割り込んで実行されることになります。

これを利用すれば、各関数の入り口と出口であなたの好きな処理を割り込ませて実行させることが可能になります。具体的には、その処理を __cyg_profile_func_enter と __cyg_profile_func_exit に記述するだけで良いです。ここでは printf で呼び出された関数や呼び出し位置のアドレスを表示するだけでしたが、これは単なる1つの例であり、他にも様々な処理を割り込ませることが可能です。

以上が、gcc に用意されたフックの仕組みの説明及び、利用例の紹介となります。

アドレスからの関数名の取得

で、ここからは応用編になります。

確かに先ほどフックの仕組みの利用例を示したのですが、単なるアドレスが表示されているだけで、このアドレスだけが表示されても人間には何の関数が呼ばれたのかが分かりません。

そのため、先ほど示した例を、アドレスではなく関数名を表示するように改良したいと思います。前述の通り、__cyg_profile_func_enter と __cyg_profile_func_exit には呼び出し時に下記の2つのアドレスが引数として渡されることになります。

  1. call_func:呼び出された関数のアドレス
  2. call_site:呼び出した位置のアドレス

更に、Mac や Linux ではアドレスから関数名等の情報を取得する関数 dladdr が利用可能です。つまり、上記の引数で渡されるアドレスをから関数名を取得し、それをアドレスの代わりに出力するようにしてやれば、人間にも理解しやすい出力結果を得ることができるようになります。

関数の入り口と出口でアドレスではなく関数名が出力されるようになる様子

dladdr による関数名の取得

ここで、この dladdr 関数に関して簡単に説明しておきます。この dladdrdlfcn.h で下記のように宣言されている関数になります。

dladdr
#include <dlfcn.h>

int dladdr(const void* addr, Dl_info* info);

dladdr第1引数で指定したアドレス addr から関数の情報を取得する関数になります。もう少し正確に言えばシンボルの情報を取得する関数になりますが、ここでは簡単に関数の情報と記して説明していきます。この第1引数には __cyg_profile_func_enter と __cyg_profile_func_exit の引数 call_funccall_site のように、関数そのもののアドレス関数の呼び出し位置のアドレスを指定可能です。

dladdr は関数の情報に成功した場合は 0 以外 を返却します(失敗時は 0 を返却)。さらに、成功時には第2引数で指定したアドレス info に関数の情報が格納されることになります。

第2引数には Dl_info 構造体のアドレスを指定する必要があり、dladdr 関数を実行することで第1引数で指定したアドレスに対応する関数の “関数名のアドレス” dladdr 関数内でdli_sname メンバにセットされることになります。

そのため、下記のような get_func_name を用意しておけば、関数名を取得したいアドレスを引数に指定して実行してやることで返却値として関数名を取得することができるようになります。

get_fnc_name
#include <dlfcn.h>

const char* get_func_name(void* address) {
    Dl_info info;

    // addressに対する関数の情報を取得
    int ret = dladdr(address, &info);
    if (ret == 0) {
        return NULL;
    }

    // 関数名を返却
    return info.dli_sname;

}

スポンサーリンク

関数名の出力

関数名を取得するための関数 get_func_name が用意できれば、後は __cyg_profile_func_enter と __cyg_profile_func_exit から get_func_name を呼び出して関数名を取得し、それを printf 関数で出力するように変更してやれば良いだけです。

このような変更を行なった hook.c は下記のようになります。

hook.c
#include <stdio.h>
#include <dlfcn.h>

const char* get_func_name(void* address) {
    Dl_info info;

    // addressに対する関数の情報を取得
    int ret = dladdr(address, &info);
    if (ret == 0) {
        return NULL;
    }

    // 関数名のアドレスを返却
    return info.dli_sname;
}

void __cyg_profile_func_enter(void* call_func, void* call_site) {
    // 関数名を取得
    const char *call_func_name = get_func_name(call_func);
    const char *caller_func_name = get_func_name(call_site);
    
    printf("%s -> %s\n", caller_func_name, call_func_name);
}

void __cyg_profile_func_exit(void* call_func, void* call_site) {
    // 関数名を取得
    const char *call_func_name = get_func_name(call_func);
    const char *caller_func_name = get_func_name(call_site);
    
    printf("%s -> %s\n", call_func_name, caller_func_name);
}

上記のソースコードの注意点を1つ挙げておくと、それは get_func_name__cyg_profile_func_enter__cyg_profile_func_exit から呼び出されるという点になります。このため、get_func_name を定義したソースコードを -finstrument-functions オプションを指定してコンパイルした場合、get_func_name の入り口と出口でも __cyg_profile_func_enter__cyg_profile_func_exit が呼び出されるようになります。そうなると、これらの関数からまた get_func_name が呼び出されることになり、これらの関数の呼び出しが延々と繰り返されるようになってしまいます。

今回紹介したフックの仕組みを利用する上で一番注意すべき点は、このような無限に関数の呼び出しが発生する点になると思います。これを避けるためには、フックの仕組みを適用しない関数の定義は別ファイルに分離し、そのファイルのコンパイル時には -finstrument-functions オプションを指定しないようにしてやれば良いです。

このため、get_func_name 関数は funcs.c ではなく hook.c に定義するようにしています。

私も試していて延々と関数呼び出しが行われてプログラムが異常終了してしまったことも多かったので、くどいようですが再度注意点として説明させていただきました。

コンパイルとリンク

続いて、ソースコードのコンパイルとリンクを行っていきましょう。

hook.c のコンパイル

今回は、フックの利用の仕方 で示したソースコードから hook.c のみを変更しているため、hook.c の方のみを再度コンパイルしていきたいと思います。コンパイル時に指定が必要になるオプションが異なることに注意してください。dladdr 関数を利用するように変更したため、コンパイル時は -D_GNU_SOURCE というオプションを追加で指定する必要があります。これを指定しない場合、コンパイルエラーが発生する可能性があります。

したがって、hook.c をコンパイルする際には下記のようなコマンドを実行する必要があります。

gcc -c hook.c -o hook.o -D_GNU_SOURCE

実は、私の Mac の場合は -D_GNU_SOURCE のオプションは不要でした。が、私の Linux の環境の場合は -D_GNU_SOURCE オプションがないとコンパイル時に下記のようなエラーが発生しました。環境によっては _GNU_SOURCEdefine されていないと Dl_info 構造体が定義されないようです。そのため、基本的には -D_GNU_SOURCE オプションを指定して _GNU_SOURCE を define するようにしてやるので良いと思います。

ちなみに、このオプションを追加する代わりに、ソースコードの先頭で下記のように _GNU_SOURCE を define するようにしても問題ありません。要は、_GNU_SOURCE を define するようにしてやれば良いだけで、その手段はコンパイル時のオプション指定でもソースコードの変更でも問題ありません。

_GNU_SOURCEのdefine
#define _GNU_SOURCE

#include <stdio.h>
#include <dlfcn.h>
〜略〜

リンク

次は、コンパイルによって生成した funcs.ohook.o とをリンクして実行可能ファイルを生成していきます。

基本的には、dladdr を利用するようになったため、リンク時には フックの利用の仕方 で紹介したコマンドに対して -ldl-rdynamic を追加して指定する必要があります。

したがって、下記のようにコマンドを実行してリンクを行う必要があります。

gcc funcs.o hook.o -o main.exe -ldl -rdynamic

-ldldladdr が定義されているライブラリにリンクするため、-rdynamic はシンボル名を取得するために必要なオプションとなります。実は、これに関しても、私の Mac の場合は -ldl-rdynamic を指定しなくても上手く動作したのですが、Linux の場合は -rdynamic を指定しないとプログラム実行時に下記のように出力される関数名が全て (null) になってしまいました。出力結果が下記のようになってしまった場合は、特に -rdynamic オプションの指定を忘れていないかを確認してみると良いと思います。

(null) -> (null)
(null) -> (null)
(null) -> (null)
(null) -> (null)
(null) -> (null)
〜略〜

スポンサーリンク

プログラムの実行

最後に、先ほどリンクによって生成された main.exe を実行してみましょう!

main.exe を実行すれば下記のような出力結果が得られると思います。環境によっては start の部分が (null) で表示されることもあると思いますが、これらはエントリーポイントである main 関数を呼び出す関数になります。

start -> main
main -> funcA
funcA -> funcB
funcB -> funcC
funcC -> funcD
funcD -> funcC
funcC -> funcB
funcB -> funcD
funcD -> funcB
funcB -> funcA
funcA -> funcC
funcC -> funcD
funcD -> funcC
funcC -> funcA
funcA -> main
main -> funcB
funcB -> funcC
funcC -> funcD
funcD -> funcC
funcC -> funcB
funcB -> funcD
funcD -> funcB
funcB -> main
main -> start

このように関数名が表示されれば、人間の目でも各関数の呼び出し関係や呼び出しタイミングが分かりますね!ただ、これでもちょっと分かりにくい…。実は、PlantUML などを利用すれば、上記のような出力結果からシーケンス図等を作成することも可能になります。そのやり方に関しては別途ページを用意して紹介していきたいと思います。

とりあえず、ここまでの説明によって、各関数の入り口と出口で関数の名前を出力する方法については理解していただけたのではないかと思います。

スポンサーリンク

特定の関数のフックの無効化

さて、ここまでフックの仕組みを利用したくない関数は hook.c に別途定義するようにしてきました。これは、hook.c のみ -finstrument-functions オプションを指定せずにコンパイルするためで、そしてこれは延々と関数が呼び出されてしまうことを防ぐためでしたね!

このように、特定の関数が延々と呼び出されることを防止するためにはソースコードファイルを分離してしまうのが一番単純な方法にはなるのですが、このためにはソースコードを複数用意する必要があってコンパイルが面倒になってしまいます…。

-finstrument-functions の無効化

安心してください。gcc にはこれを解決するための仕組みが用意されています。ということで、次はソースコード内の特定の関数のみフックを無効化する方法について説明します。

これは、言い換えれば  -finstrument-functions オプションを無効化するための方法であり、これを利用すれば -finstrument-functions オプションを指定してコンパイルしたとしても、特定の関数に関しては関数の入り口と出口で __cyg_profile_func_enter__cyg_profile_func_exit が実行されなくなります。

やり方は単純で、フックを無効化したい関数定義の直前に下記を追記してやれば良いだけになります。これにより、下記を追記した関数に対してのみ -finstrument-functions オプションが無効化されるようになります。

フックの無効化
__attribute__((no_instrument_function))

-finstrument-functions を無効化する例

次は、このフックの無効化の実例を確認していきましょう!

まずは、下記のような main.c について考えてみたいと思います。

main.c
#include <stdio.h>
#include <dlfcn.h>

const char* get_func_name(void* address) {
    Dl_info info;

    // addressに対する関数の情報を取得
    int ret = dladdr(address, &info);
    if (ret == 0) {
        return NULL;
    }

    // 関数名のアドレスを返却
    return info.dli_sname;
}

void __cyg_profile_func_enter(void* call_func, void* call_site) {
    // 関数名を取得
    const char *call_func_name = get_func_name(call_func);
    const char *caller_func_name = get_func_name(call_site);
    
    printf("%s -> %s\n", caller_func_name, call_func_name);
}

void __cyg_profile_func_exit(void* call_func, void* call_site) {
    // 関数名を取得
    const char *call_func_name = get_func_name(call_func);
    const char *caller_func_name = get_func_name(call_site);
    
    printf("%s -> %s\n", call_func_name, caller_func_name);
}

void funcC(void) {
    // do nothing
}

void funcB(void) {
    funcC();
}

void funcA(void) {
    funcB();
    funcC();
}

int main(void) {
    funcA();
    funcB();
}

上記の main.c は今までの例で紹介してきた funcs.chook.c を統合して少し簡略化したソースコードファイルになります。

この main.c を下記のようにコンパイルすれば、-finstrument-functions オプションの効果により各関数の入り口と出口で __cyg_profile_func_enter__cyg_profile_func_exit が割り込んで実行されるようになります。また、今回はソースコードファイルが1つだけなので -c オプションを指定する必要はありません。下記のようにコンパイルを実行すれば実行可能ファイル main.exe が生成されます。

gcc main.c -o main.exe -finstrument-functions -ldl -rdynamic -D_GNU_SOURCE

ただし、この main.exe を実行するとプログラムが異常終了してしまうと思います…。-finstrument-functions オプションの効果により __cyg_profile_func_enter の入り口でも __cyg_profile_func_enter が呼び出されるようになるため、延々と関数が実行され続けることになります。__cyg_profile_func_exit と get_func_name に関しても同様の理由で延々と関数が実行され続けることになります(いずれはプログラムが異常終了します)。

__cyg_profile_file_enterが延々と実行され続ける様子

このような現象を防ぐためには main.c を下記のように変更してやれば良いです。先ほど示した main.c からは -finstrument-functions オプションを無効化したい関数の定義の直前に __attribute__((no_instrument_function)) を追記するように変更しています。

main.c
#include <stdio.h>
#include <dlfcn.h>

__attribute__((no_instrument_function))
const char* get_func_name(void* address) {
    Dl_info info;

    // addressに対する関数の情報を取得
    int ret = dladdr(address, &info);
    if (ret == 0) {
        return NULL;
    }

    // 関数名のアドレスを返却
    return info.dli_sname;
}

__attribute__((no_instrument_function))
void __cyg_profile_func_enter(void* call_func, void* call_site) {
    // 関数名を取得
    const char *call_func_name = get_func_name(call_func);
    const char *caller_func_name = get_func_name(call_site);
    
    printf("%s -> %s\n", caller_func_name, call_func_name);
}

__attribute__((no_instrument_function))
void __cyg_profile_func_exit(void* call_func, void* call_site) {
    // 関数名を取得
    const char *call_func_name = get_func_name(call_func);
    const char *caller_func_name = get_func_name(call_site);
    
    printf("%s -> %s\n", call_func_name, caller_func_name);
}

void funcC(void) {
    // do nothing
}

void funcB(void) {
    funcC();
}

void funcA(void) {
    funcB();
    funcC();
}

int main(void) {
    funcA();
    funcB();
}

前述の通り、関数定義の直前に__attribute__((no_instrument_function)) を記述しておけば、その関数に関しては -finstrument-functions のオプション指定が無効化されます。そのため、__cyg_profile_func_enter と __cyg_profile_func_exitget_func_name に関してはフックの仕組みが適用されないことになります。つまり、これらの関数の入り口と出口では処理が割り込んで実行されることがなくなります。

そのため、先ほどと同様に下記のようにコンパイルを行なってプログラムを実行したとしても、関数が延々と呼び出され続けることがなくなり正常にプログラムが終了することになります。

これを利用すれば、これまでのようにわざわざ複数のソースコードファイルを用意する必要性がなくなります。つまり、1つのソースコードでも同様のことが実現可能となります。特に、お試しでフックの仕組みを利用してみる際にはソースコードファイルが1つの方が楽に試せます。是非このような仕組みが存在することも覚えておきましょう!

スポンサーリンク

オススメ書籍

gcc に用意された関数のフックに関する解説は以上となります。

さて、今回解説した内容は結構難易度が高くて理解に苦労した方もおられると思うのですが、その一方で「こんなことが出来るんだ!」と感動した方もおられるのではないかと思います。私も後者の一人です!ソースコードの変更なしで処理を割り込ませられるところが便利ですね!

実は、今回紹介したフックだけでなく、gcc 等にはこういったテクニックがたくさん用意されています。そして、これらのテクニックを利用することでオブジェクトファイルやソースコードファイルの解析、さらにはデバッグを効率的に行うことができるようになります。また、これらのテクニックを知っておくと効率化が図れるだけでなく、あなた自身が出来ること自体も増やすことができ、更にプログラミングやコンピュータに対する視野を広げることができると思います。

そして、こういったテクニックを網羅的に解説されているのが下記の BINARY HACKS になります。この本には今回紹介したフックに関する情報だけでなく、オブジェクトファイルをハックする方法メモリリークを検出する方法プログラムの実行プロファイルを取得する方法など多くの情報が記載されています。はっきり言って難易度は高い参考書になりますが、これを読めば確実にあなたの実力やスキルも向上すると思います。特に実行可能ファイルやオブジェクトファイルの解析に興味のある方、gcc を利用する開発者や組み込みソフトウェアエンジニアの方にオススメの本になります!是非書店見かけた時などは目を通してみてください!

まとめ

このページでは、gcc に用意された関数のフックについて紹介しました!

gcc にはコンパイルオプションとして -finstrument-functions が用意されており、このオプションを指定することによりソースコードの変更なしで各関数の入り口と出口に独自の処理を割り込ませることができるようになります。これを利用すればソースコードの解析やデバッグ等も効率的に行えるようになると思いますので、是非今回紹介したフックの仕組みやフックの利用の仕方については覚えておいてください!

同じカテゴリのページ一覧を表示