mcommit's message

ソフトウェア開発の仕事をしているsimotinといいます。記事の内容でご質問やご意見がありましたらお気軽にコメントしてください\^o^/

C言語の関数の戻り値を実行時に変更する

「試験をしているんだけど、実行中に関数の戻り値を変える方法はないかな?」

と相談を受けました。

C言語単体テストをする場合、システムコールやライブラリ関数はダミーの関数を用意しておいて自作部分の実装をテストすることはよくありますが、実行中にこのダミー関数の振舞いを変えたいという趣旨です。*1

単純にダミー関数の戻り値をグローバル変数にしてしまえば問題はありません。
ただしテストコードが多いと呼び出し元のテストコードが少し複雑になりそうです。

少し考えてみましたが、

DLL,共有ライブラリならできるのでは?

というのが私の思いついた答えでした。

linuxだとdlopen,dlsym,dlcloseを使って、ダミー関数を共有ライブラリにしておけば実際に呼び出される関数を変更することができます。

ということでそれっぽいコードを書いてみました。

試してみたコード

呼び出し元のソースコード(main.c)

#include <stdio.h>
#include <dlfcn.h>
#include <unistd.h>

int add(int a, int b)
{
    int result;
    void *handle_a = dlopen("./liba.so", RTLD_LAZY);
    int(* add_handler)(int, int) = dlsym( handle_a, __FUNCTION__ );

    result = add_handler(a, b);

    dlclose(handle_a);
    return result;
}

int main(int argc, char **argv)
{
    int result = 0;

    while(1)
    {
        result = add(1,2);
        sleep(1);
        printf("%d\n", result);
    }

    return 0;
}

ダミー関数の実装(add.c)

int add(int a, int b)
{
	return a + b;
}

ビルドと実行


$ gcc -shared add.c -o libadd.so
$ gcc main.c -ldl
$ ./a.out
$ 3
$ 3
$ 3
$ 3
....(1秒間隔で出力される)

共有ライブラリのaddが呼び出されてadd(1,2)の結果が正しく(1+2=3)出力されていることは分かります。

さてここからが本番です。
add.cの中身を

int add(int a, int b)
{
	return a * b;
}

のように書き換えてコンパイルしてみましょう。
この時、先ほど実行した a.out は停止せず動かしっぱなしにします。

別ターミナルで、


$ gcc -shared add.c -o libadd.so

すると、実行している a.outの出力はどうなるでしょうか。


3
3
3
3
2
2
2
2

2に変わっています!
ということで実行中に関数の戻り値を変える事ができました。

わざわざ共有ライブラリを動的にロードするのはまどろっこしい気もしますが規模が大きいテストではこちらの手法の方が保守性が高い気もします。

補足

呼び出し側で sleep(1)を呼び出している箇所がありますが、最初sleep無しで試したところセグメンテーションフォルトで a.out は止まってしまいました。
add.cをコンパイルすることによってロードしているライブラリのファイルに対して競合してしまったんだ思います。
sleep(1)を入れて試した限りでは問題なく動作がしましたが、きちんと排他制御を行うのは正しい姿だと思います。

JITコンパイルについて

rubyでも 2.6 からJITコンパイルが取り入れられたそうですが、今回試した実行時にライブラリを差し替えるのはアプローチとしては同じようなイメージになるかと思います。今回のadd関数は1行で終わるシンプルな関数なので面白くありませんが、これが処理時間のかかる関数だったとして実行中に処理時間のかからない関数に差し替える事ができれば嬉しいですよね。実行中のデータなどで関数の処理にフィードバックをかけて最適な処理にしていくといったこともできます。
*2

感想

同じような事は以前にも試してみていたのですが、単体テストなんかで使えるテクニックだとは気づいていませんでした。

mcommit.hatenadiary.com

この手のテクニックはどの言語でも何かしら手段が提供されていると思います。
私の知っている範囲ではC言語ですと、BINARY HACKS、RubyですとメタプログラミングRubyでそういったテクニックが解説されています。

Binary Hacks ―ハッカー秘伝のテクニック100選

Binary Hacks ―ハッカー秘伝のテクニック100選

メタプログラミングRuby 第2版

メタプログラミングRuby 第2版

メタプログラミング.Netという本もあるんですね。面白そうなので読んでみたいと思います。

メタプログラミング.NET (アスキー書籍)

メタプログラミング.NET (アスキー書籍)

動的に何かを変更するのってなんかかっこよくて面白いですね。

*1:正常系のケースを実行した後そのまま異常系を試験したいというような状況ですね

*2:人工知能系のプログラミングもこういった形で実装できそうですね。