Cで関数呼び出しをオーバーライドする
-
03-07-2019 - |
質問
呼び出しを記録するために、さまざまなAPIへの特定の関数呼び出しをオーバーライドしたいのですが、実際の関数に送信される前にデータを操作することもできます。
たとえば、ソースコードで getObjectName
という関数を何千回も使用するとします。異なる結果を見るためにこの関数の動作を変更したいので、一時的にこの関数をオーバーライドしたいです。
次のような新しいソースファイルを作成します:
#include <apiheader.h>
const char *getObjectName (object *anObject)
{
if (anObject == NULL)
return "(null)";
else
return "name should be here";
}
他のすべてのソースを通常どおりコンパイルしますが、APIのライブラリとリンクする前に、まずこの関数に対してリンクします。これは、オーバーライド関数内で実際の関数を明らかに呼び出せないことを除いて、正常に機能します。
「オーバーライド」する簡単な方法はありますかリンク/コンパイルエラー/警告を取得せずに機能?理想的には、リンクオプションをいじったり、プログラムの実際のソースコードを変更したりするのではなく、余分なファイルを1つまたは2つコンパイルしてリンクするだけで、関数をオーバーライドできるようにしたいのです。
解決
呼び出しをキャプチャ/変更するのがソースのみである場合、最も簡単な解決策は、ヘッダーファイル( intercept.h
)を次のようにまとめることです:
#ifdef INTERCEPT
#define getObjectName(x) myGetObectName(x)
#endif
次のように関数を実装します( intercept.c
に含まれていない intercept.h
):
const char *myGetObjectName (object *anObject) {
if (anObject == NULL)
return "(null)";
else
return getObjectName(anObject);
}
次に、通話を傍受する各ソースファイルに次のものがあることを確認します。
#include "intercept.h"
上部。
その後、&quot; -DINTERCEPT
&quot;でコンパイルすると、すべてのファイルが実際の関数ではなく関数を呼び出しますが、関数は実際の関数を呼び出すことができます。
&quot; -DINTERCEPT
&quot;なしでコンパイルする傍受の発生を防ぎます。
すべての呼び出し(ソースからの呼び出しだけでなく)をインターセプトする場合、少し注意が必要です-これは通常、実際の関数の動的な読み込みと解決( dlload-
および< code> dlsym- type calls)が、私はそれがあなたの場合に必要だとは思わない。
他のヒント
gccを使用すると、Linuxでは次のように-wrap
リンカーフラグを使用できます。
gcc program.c -Wl,-wrap,getObjectName -o program
そして関数を次のように定義します
const char *__wrap_getObjectName (object *anObject)
{
if (anObject == NULL)
return "(null)";
else
return __real_getObjectName( anObject ); // call the real function
}
これにより、 getObjectName()
へのすべての呼び出しが(リンク時に)ラッパー関数に確実に転送されます。ただし、Mac OS Xのgccでは、この非常に便利なフラグはありません。
g ++でコンパイルする場合は、 extern&quot; C&quot;
でラッパー関数を宣言することを忘れないでください。
LD_PRELOAD
トリックを使用して関数をオーバーライドできます- man ld.so
を参照してください。関数で共有ライブラリをコンパイルし、 LD_PRELOAD = mylib.so myprog
のようにバイナリを変更します(バイナリを変更する必要さえありません!)。
関数の本体(共有ライブラリ内)に次のように記述します:
const char *getObjectName (object *anObject) {
static char * (*func)();
if(!func)
func = (char *(*)()) dlsym(RTLD_NEXT, "getObjectName");
printf("Overridden!\n");
return(func(anObject)); // call original function
}
プログラムを変更/再コンパイルせずに、stdlibからでも共有ライブラリの関数をオーバーライドできるため、ソースを持たないプログラムでトリックを実行できます。いいですね?
GCCを使用する場合、関数を weak
にすることができます。これらのはオーバーライドできます非弱関数による:
test.c :
#include <stdio.h>
__attribute__((weak)) void test(void) {
printf("not overridden!\n");
}
int main() {
test();
}
それは何をしますか?
$ gcc test.c
$ ./a.out
not overridden!
test1.c :
#include <stdio.h>
void test(void) {
printf("overridden!\n");
}
それは何をしますか?
$ gcc test1.c test.c
$ ./a.out
overridden!
残念ながら、他のコンパイラでは機能しません。ただし、独自のファイルにオーバーライド可能な関数を含む弱い宣言を含めることができます。GCCを使用してコンパイルする場合は、API実装ファイルにインクルードのみを配置します。
weakdecls.h :
__attribute__((weak)) void test(void);
... other weak function declarations ...
functions.c :
/* for GCC, these will become weak definitions */
#ifdef __GNUC__
#include "weakdecls.h"
#endif
void test(void) {
...
}
... other functions ...
この方法の欠点は、APIファイルに対して何かを行わないと完全に機能しないことです(これらの3行とweakdeclsが必要です)。しかし、一度その変更を行うと、1つのファイルにグローバル定義を書き込み、それをリンクすることにより、関数を簡単にオーバーライドできます。
多くの場合、変更することが望ましい 既存のコードベースの動作 関数のラップまたは置換。いつ それらのソースコードを編集する 関数は実行可能なオプションであり、これは 簡単なプロセスである。いつ 関数のソースは 編集(例:関数が システムCライブラリによって提供されます)、 代替手法は 必須。ここでは、そのようなものを提示します UNIX、Windows、および Macintosh OS Xプラットフォーム。
これは、OS X、Linux、およびWindowsでこれがどのように行われたかを網羅した優れたPDFです。
ここに記載されていない驚くべきトリックはありません(これは驚くべき一連の応答です)。しかし、素晴らしい読み物です。
Windows、UNIX、およびMacintosh OSでの任意の機能のインターセプトXプラットフォーム(2004)、ダニエルS.マイヤーズおよびアダムL.バジネット。
PDFを別の場所から直接ダウンロードできます(冗長性のため)。
そして最後に、前の2つのソースが何らかの形で炎上した場合、ここにGoogle検索結果があります 。
関数ポインタをグローバル変数として定義できます。呼び出し元の構文は変更されません。プログラムが起動すると、コマンドラインフラグまたは環境変数がロギングを有効にするように設定されているかどうかを確認し、関数ポインターの元の値を保存して、ロギング関数に置き換えます。特別な「ロギングを有効にする」必要はありません。ビルドします。ユーザーは「フィールドで」ロギングを有効にできます。
呼び出し元のソースコードを変更できる必要がありますが、呼び出し先は変更できません(サードパーティのライブラリを呼び出すときにこれが機能します)。
foo.h:
typedef const char* (*GetObjectNameFuncPtr)(object *anObject);
extern GetObjectNameFuncPtr GetObjectName;
foo.cpp:
const char* GetObjectName_real(object *anObject)
{
return "object name";
}
const char* GetObjectName_logging(object *anObject)
{
if (anObject == null)
return "(null)";
else
return GetObjectName_real(anObject);
}
GetObjectNameFuncPtr GetObjectName = GetObjectName_real;
void main()
{
GetObjectName(NULL); // calls GetObjectName_real();
if (isLoggingEnabled)
GetObjectName = GetObjectName_logging;
GetObjectName(NULL); // calls GetObjectName_logging();
}
2つのスタブライブラリを含むリンカでそれを行うトリッキーな方法もあります。
ライブラリ#1はホストライブラリに対してリンクされ、別の名前で再定義されているシンボルを公開します。
ライブラリ#2はライブラリ#1に対してリンクされ、呼び出しをインターセプトし、ライブラリ#1で再定義されたバージョンを呼び出します。
ここでのリンクの注文には十分注意してください。そうしないと機能しません。
所有していないコードに適したソリューションを使用して、@ Johannes Schaubの答えに基づいて構築します。
オーバーライドする関数を弱く定義された関数にエイリアスし、自分で再実装します。
override.h
#define foo(x) __attribute__((weak))foo(x)
foo.c
function foo() { return 1234; }
override.c
function foo() { return 5678; }
Makefileでパターン固有の変数値を使用しますコンパイラフラグ -include override.h
を追加します。
%foo.o: ALL_CFLAGS += -include override.h
脇: -D 'foo(x)__attribute __((weak))foo(x)'
を使用してマクロを定義することもできます。
ファイルをコンパイルして再実装( override.c
)にリンクします。
-
これにより、コードを変更することなく、ソースファイルから単一の関数をオーバーライドできます。
-
欠点は、オーバーライドするファイルごとに個別のヘッダーファイルを使用する必要があることです。
共有ライブラリ(Unix)またはDLL(Windows)を使用してこれを行うこともできます(パフォーマンスが多少低下します)。次に、ロードされるDLL / soを変更できます(デバッグ用に1つのバージョン、非デバッグ用に1つのバージョン)。
過去に同様のことを行い(達成しようとしているものを達成するためではなく、基本的な前提は同じです)、うまくいきました。
[OPコメントに基づいて編集]
実際に私がしたい理由の一つ 関数をオーバーライドするのは 彼らの行動が異なると思われる 異なるオペレーティングシステム。
これに対処するための一般的な2つの方法(私が知っている)があります。それは、共有lib / dll方法またはリンクするさまざまな実装を作成することです。
両方のソリューション(共有ライブラリまたは異なるリンク)の場合、foo_linux.c、foo_osx.c、foo_win32.c(または、より良い方法はlinux / foo.c、osx / foo.c、win32 / foo.cです) )そして、適切なものとコンパイルしてリンクします。
異なるプラットフォーム用の異なるコードとdebug -vs- releaseの両方を探している場合、おそらく最も柔軟であるため、共有lib / DLLソリューションを使用する傾向があります。