【※ 当記事は2020年6月29日時点の情報です】
ペイヴメント(@pavement1234)です。
中級向けC言語のデバッグテクニックを知りたい。
バージョン情報
Windows10 Home(64bit)1903
VMware Player 15 もしくは Virtual Box 6.1 で動くUbuntu18.04
標準関数のスタブ作成方法がわかる
インターポジショニング(関数の上書き)
やり直しC言語:インターポジショニングによるtime(3)のスタブ化を参考にしました。
time関数(実行する度に結果が変わる関数)を題材にしています。
まずこういうコードを単体試験しようとすると、
時間が毎回変わるので一定の結果は得られません。
1 2 3 4 5 6 7 8 |
#include <stdio.h> #include <time.h> int main( int argc, char** argv ) { printf("%ld\n", time( NULL ) ); return 0; } |
コンパイルして実行。
現在のUNIX時間(数値)が表示されます。
$ gcc -g -o main main.c
$
$ ./main
$ 1555888123
常に0を返すtime関数のスタブを作ってみました。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
#include <stdio.h> #include <time.h> time_t time( time_t * tloc ) { if( tloc == NULL ){ return 0; } return *tloc = 0; } int main( int argc, char** argv ) { printf("%ld\n", time( NULL ) ); return 0; } |
コンパイルして実行。
常に0が返ります。
$ gcc -g -o main2 main2.c
$
$ ./main2
$ 0
main.cとstub.cを分離します。
1 2 3 4 5 6 7 |
#include <stdio.h> int main( int argc, char** argv ) { printf("%ld\n", time( NULL ) ); return 0; } |
1 2 3 4 5 6 7 8 9 |
#include <time.h> time_t time( time_t* tloc ) { if( tloc == NULL ){ return 0; } return *tloc = 0; } |
stub.cを共有ライブラリとしてビルドして、main.cと動的リンクさせます。
しかしこのまま実行すると共有ライブラリが見つからないというエラーが出ます。
$ gcc -shared -fPIC -o libstub.so stub.c
$ gcc -I./ -L./ -g -o main main.c -lstub
$ ./main
$ ./main: error while loading shared libraries: libstub.so: cannot open shared object file: No such file or directory
LD_LIBRARY‗PATHに、libstub.soのあるパスを指定することで、
無事スタブ共有ライブラリを見つけることが出来ました。
$ export LD_LIBRARY_PATH=<libstub.soがあるディレクトリ>
$ ./main
$ 0
標準関数をローカル関数で再定義することをインターポジショニングと呼ぶそうです。
単体試験に使えそうですね。
余談ですがUNIX時間(数値)を年月日時分秒に変換できる
UNIX時間変換ツールが便利です。
動的ライブラリ(*.so)のデバッグ手法
デバッグ対象の動的ライブラリ(*.so)を構築
まずプログラムはこんな感じ。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
#include <stdio.h> int func1( void ) { printf( "func1\n" ); return 0; } void func2( int i ) { printf( "func2(%d)\n", i ); } int func3( char* str ) { printf( "func3(%s)\n", str ); return 0; } |
コンパイルはこう(-sharedで共有ライブラリ指定。
-fPICでPosition Independent Codeを指定)。
$ gcc -shared -fPIC -o libdltest.so dltest.c
デバッグ用ドライバー(呼び出しプログラム)の作成
こんな感じです。
Man page of DLOPENなどを見ると色々オプションがありますが今日は深く考えない。
ポイントはシンボル名を指定してdlsymを呼ぶと関数ポインタが得られるということ。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
#include <stdio.h> #include <dlfcn.h> void* handle; int ( *func1 )( void ); void ( *func2 )( int ); int ( *func3 )( char* ); int main( void ) { handle = dlopen( "./libdltest.so", RTLD_LAZY ); if( handle != NULL ){ func1 = ( int (*)( void ) )dlsym( handle, "func1" ); if( func1 != NULL) { ( *func1 )(); } func2 = ( void (*)( int ) )dlsym( handle, "func2" ); if( func2 != NULL) { ( *func2 )( 123 ); } func3 = ( int (*)( char* ) )dlsym( handle, "func3" ); if( func3 != NULL) { ( *func3 )( "abcde" ); } dlclose( handle ); } return 0; } |
コンパイルはこう(-ldlでlibdl.soをリンクします)。
$ gcc -g -o main main.c -ldl
実行する。
$ ./main
$ func1
$ func2(123)
$ func3(abcde)
関数ポインタ
久々に関数ポインタを使いました。
忘れないように覚え書きしておきます。
関数プロトタイプのようにも見えますが
あくまでも関数ポインタ変数の定義です。
変数名はfunc1、
戻り値と引数を定義します。
int( *func1 )( void );
dlsymの戻り値(関数ポインタ)をfunc1に代入するときは、
このようにキャストします。
func1 = ( int (*)( void ) )dlsym( handle, “func1” );
呼び出しはこんな感じ。
func1();でも呼べるけど、
関数ポインタであることを明示しておいた方がいいです。
( *func1 )();
応用編ですが関数ポインタは配列にできます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
#include <stdio.h> void test1( void ) { printf( "test1\n"); } void test2( void ) { printf( "test2\n"); } void test3( void ) { printf( "test3\n"); } int main( void ) { int( *func[] )( void ) = { test1, test2, test3 }; ( *func[0] )(); ( *func[1] )(); ( *func[2] )(); return 0; } |
コンパイルして実行するとこんな感じ。
$ gcc -g -o func_ptr_array func_ptr_array.c
$ ./func_ptr_array
$ test1
$ test2
$ test3
まとめ
C言語のデバッグテクニックとして
「標準関数のスタブ作成」
「動的ライブラリのデバッグ方法」をご紹介しました。
最近プログラムをたくさん書いてなかったので、
デバッグの効率化のテクニックを忘れ始めてました。
ちょこちょこ自分で手を動かして、カンを鈍らせないようにしたいと思います。
関連記事(プログラミング)
【プログラミング】マルチコア環境における排他制御の課題
【プログラミング】gccの基本オプションの使い方
【C言語】デバッグのテクニック(標準関数のスタブ作成、動的ライブラリのデバッグ方法)
【プログラミング】迂回(Detours)という技術について
【プログラミング】64bit OSの32bitエミュレーションの状況