CGIプログラミング便利帳

WindowsとLinuxで利用できるライブラリ,CGIプログラミング豆知識

戻る

CGIを作成するには,ターゲットになるOSの仕組みを理解しておくと,いざというとき に応用することができるようになります.
WindowsとLinuxの違いを理解し,双方のOSがもつ便利な機能を取り入れることで, 効率よくCGIを作成できるようになります.

7.1 WindowsやLinixの便利なAPIをOSに関係なく利用しよう

WindowsのソースをLinuxで動作させたり,またその逆を行う場合,お互いに便利な 部分を取り込んで,どちらでも動作させることが可能なものを作成しておくと, むやみに,#ifdef で分岐させずに,きれいなソースで共通化でき,保守性も向上 します.
Windowsの用のソースをSolaris でコンパイルして動作させるための情報が, サン・マイクロシステムズのWebページ に 「Windows NT 移行キット」として公開されています. Linuxでのプログラミングでも十分活用できますし,Solaris 固有部分も多く ありますので,アプリケーションを作成する場合には参考にするとよいでしょう.

●Win32 API もどき

Linuxでも同等の関数はありますが,WindowsとLinuxで同一のプログラムを動作させる 必要がある場合,同一のAPIがあると,アプリケーションのソースを共通化するために 同じ仕様の関数が必要になります.
単純にWindowsのAPIのほうが使いやすいということで作成しました. ところで,Cランタイム関数で,関数名が異なるが,動作がほぼ同じ場合もあります. この場合は,マクロで関数名を変えてしまえば,ソースを修正することなく, 複数のOSで動作させることができる場合がありますので,工夫してみると よいでしょう.ただし,完全な互換性が保証できない場合がほとんどではないかと 思われますので,意図とおりに動作するかはアプリケーションを実行して問題ないかを 確認する必要があります.


/*----------------------------------------------------------------------------- [形 式] GetTempFileName [引 数] const chat *lpcszPath ... テンポラリファイル格納ディレクトリ const char *lpcszPrefix ... プレフィックス unsigned int uUnique ... ユニークID(0可) char *lpszTempFileName ... ファイルパスを格納する領域 [説 明] テンポラリファイルを作成する [戻 り 値] ユニークID -----------------------------------------------------------------------------*/ unsigned int GetTempFileName(const char *lpcszPath, const char *lpcszPrefix, unsigned int uUnique, char *lpszTempFile) { struct stat statbuff; pid_t pid = 0; time_t tm; unsigned int uRand; char szDir[MAX_PATH]; char *lpsz; strcpy(szDir, lpcszPath); lpsz = szDir + strlen(szDir) - 1; if(*lpsz == '/'){ *lpsz = '\0'; } pid = getpid(); uRand = uUnique; while(1){ if(uUnique == 0){ tm = time(NULL); uRand = (unsigned int)rand_r((unsigned int *)&tm); } sprintf(lpszTempFile, "%s/%s%x%x.tmp", szDir, lpcszPrefix, (unsigned int)pid, uRand); if(stat(lpszTempFile, &statbuff) < 0){ int fd = open(lpszTempFile, O_CREAT | O_RDWR); if(fd == -1){ return 0; } close(fd); break; } else if(uUnique != 0){ return 0; } } return uRand; } /*----------------------------------------------------------------------------- [形 式] GetComputerName [引 数] char *lpszName ... マシン名を格納する領域を指すポインタ unsigned int *lpuSize ... サイズを格納する領域を指すポインタ [説 明] マシン名を得る [戻 り 値] 正常終了 TRUE 異常終了 FALSE -----------------------------------------------------------------------------*/ int GetComputerName(char *lpszName, unsigned int *lpuSize) { struct utsname uts; unsigned int uSize; if(uname(&uts) < 0){ return FALSE; } if((uSize = strlen(uts.nodename)) >= *lpuSize){ return FALSE; } strcpy(lpszName, uts.nodename); *lpuSize = uSize; return TRUE; }

リスト1:Win32APIと同等のLinux用関数の例



#define closesocket(n) close(n) #define WSAGetLastError() (errno) #define GetCurrentProcessId() getpid()

リスト2:LinuxでWin32APIをマクロ関数として使う

●Linuxの便利な関数をWindowsで使う

Linixでは,ファイルパスの区切り文字は"/"を利用します.WindowsでもURLを扱う 処理を行う場合は,Linixで利用されているファイルパス用の関数を利用したくなる 場合があります.
たとえば,fnmatchは,パスを比較する関数ですが,"*"や"?" を埋め込んだパスの 比較ができます.
OSとは関係ありませんが,あったら便利だろうと思う関数を作成して持っておくと 流用できます.大文字小文字の区別をしない文字列比較は,OSによってサポート状態 が異なりますので,OSに依存しない関数として作成しておくと便利です.
ファイル操作などは,OSによって実装方式が異なりますが,関数のデザインを同じ にしておけば,この関数を利用するアプリケーションはOSに依存しなくなります.

●INIファイルアクセスする

Linuxには,レジストリのような環境設定を一元管理するような仕組みはありません. /etc に設定ファイルを置く場合も多いのですが,設定ファイルの保存場所や, ファイルフォーマットはアプリケーションごとに異なり,統一されていません.
Windowsではマルチユーザーに対応する必要から,レジストリにデータを書き込むこと が推奨されていますが,一旦データが書き込まれると削除してもデータベースの ファイルサイズは戻らないため,INIファイルの利用が見直されている側面があります. Win32 API GetPrivateProfileString, Win32 API SetPrivateProfileString は, Windows3.1の遺物扱いのAPIで,仕様はWin32になっても変更はありません.
これらのAPIが扱えるデータサイズは,最大64KBでそれ以上のサイズの設定ファイル を扱うことができません.そこで,簡単なINIファイルアクセス関数を作成します. 作成したAPIは,毎回ファイルへのアクセスを行うため,性能はよくありませんが, これらの関数を利用すれば,WindowsとLinuxの両方で動作するソースファイルで INIファイルにアクセスすることができるようになります.



----------------------------------------------------------------------------- [形 式] fnmatch [引 数] lpcszPattern ... パターン文字列 lpcszString ... 検索文字列 iFlag ... 以下の組み合わせを指定する FNM_NOESCAPE "\"をエスケープ文字ではなく通常の文字として扱う FNM_PATHNAME スラッシュそのものにだけマッチ FNM_PERIOD lpcszStringの先頭ピリオドはlpcszPattern中の ピリオドそのものにしかマッチしない FNM_CASEFOLD 大文字小文字が区別されない [説 明] ファイル名パターンマッチング ※ [] シーケンスには対応していない [戻 り 値] 正常終了 0 異常終了 FNM_NOMATCH ----------------------------------------------------------------------------- int fnmatch(const char *lpcszPattern, const char *lpcszString, int iFlag) { const char *lpcszTop = lpcszString; unsigned char c, cTemp; if((lpcszPattern == NULL)||(lpcszString == NULL)){ return FNM_NOMATCH; } while(((c = CASEFOLDLOWER(*lpcszPattern++, iFlag)) != '\0')){ switch((int)c){ case '*': c = CASEFOLDLOWER(*lpcszPattern, iFlag); while(c == '*'){ c = CASEFOLDLOWER(*++lpcszPattern, iFlag); } if((*lpcszString == '.')&&(iFlag & FNM_PERIOD) &&((lpcszString == lpcszTop)||((iFlag & FNM_PATHNAME) &&(*(lpcszString - 1) == '/')))){ return FNM_NOMATCH; } if(c == '\0'){ if(iFlag & FNM_PATHNAME){ if(strchr(lpcszString, '/') != NULL){ return FNM_NOMATCH; } } return 0; } else if((c == '/') && (iFlag & FNM_PATHNAME)){ if((lpcszString = strchr(lpcszString, '/')) == NULL){ return FNM_NOMATCH; } break; } while((cTemp = CASEFOLDLOWER(*lpcszString, iFlag)) != '\0'){ if(!fnmatch(lpcszPattern, lpcszString, iFlag & ~FNM_PERIOD)){ return 0; } if((cTemp == '/') && (iFlag & FNM_PATHNAME)){ break; } lpcszString++; } return FNM_NOMATCH; case '?': if((*lpcszString == '\0') ||(((*lpcszString == '/')&&(iFlag & FNM_PATHNAME)))){ return FNM_NOMATCH; } if((*lpcszString == '.')&&(iFlag & FNM_PERIOD) &&((lpcszString == lpcszTop)||((iFlag & FNM_PATHNAME) &&(*(lpcszString - 1) == '/')))){ return FNM_NOMATCH; } lpcszString++; break; case '[': return FNM_NOSYS; default: if ((c == '\\') && ((iFlag & FNM_NOESCAPE) == 0)){ if((c = CASEFOLDLOWER(*lpcszPattern++, iFlag)) == '\0'){ c = '\\'; lpcszPattern--; } } if(c != CASEFOLDLOWER(*lpcszString, iFlag)){ return FNM_NOMATCH; } lpcszString++; break; } } return (*lpcszString == '\0') ? 0 : FNM_NOMATCH; }

リスト3:Linuxと同等のWindows用関数の例

7.2 CGIプログラミング豆知識

CGIを作成するときに知っておくと便利なノウハウや豆知識をまとめました.
開発環境に関しては,使い慣れないと少し難解なLinuxを中心にまとめています.

●LinuxのsoとWindowsのDLLの違い

soとDLLは,共にダイナミックリンク(プロセスに対してライブラリを動的にリンク) する方式ですが,OSによって位置付けや動作が異なります(表1).
また,UNIX系OSでも動作が多少異なる場合がありますので,利用するOSの動作は確認 しておく必要があります.
Windowsでよく言われる「再起動」問題ですが,LinuxなどUNIX系OSはシステム内で 利用している(メモリーに読み込まれている)モジュールを削除したり置き換えること ができる代わりに,そのモジュールを利用して実行中のアプリケーションの動作は保証 されません.この動作に関しては,どちらのOSの動作がよいのかは議論の分かれる ところだと思います.

比較項目 so DLL
検索パス LD_LIBRARY_PATH環境変数で指定されたコロン区切りのディレクトリリスト → /etc/ld.so.cache 中にあるライブラリ・キャッシュのリスト → /usr/lib → /lib プロセス(EXE)のカレントディレクトリ → Windows(システム)ディレクトリ(C:\WINDOWS, C:\WINDOWS\SYSTEM, C:\WINWDOWS\SYSTEM32) → PATH環境変数に設定されたパス
環境変数でのデータ受け渡し 双方向に参照可能 DLLを呼び出しているEXEの環境変数のコピーを持つ(EXEからDLLの環境変数は直接参照できない)
プロセス間の共有 しない(各プロセス個別に配置される) コードのメモリ空間を共有し、データ部分をプロセスごとに持つ
ファイルの置き換え(上書き・削除) 可能(ただし、プロセスの動作は保証されない) DLLを利用しているモジュール(EXE)が1つもない場合に可能
静的リンクの方法 soのパスをリンカに指定する DLLのインポートライブラリをリンクする
動的リンクの方法 dlclose, dlerror, dlopen, dlsym を利用する
  • LoadLibrary(Ex), GetProcAddress, FreeLibrary を利用する
  • VC++6.0の遅延ロードオプションを利用してインポートライブラリをリンクする
  • 表1:soとDLLの違い


    ●CGIのCPU負荷を軽減する

    (1) WindowsNT/2000/XP
    CGIのようにプロセスを起動することによるCPU負荷を軽減するために, ISAPIアプリケーションと呼ばれるDLLベースのアプリケーションを作成したり, ASPからDLLで作成した機能を呼び出すことでCPU負荷の軽減を行います.
    DLLベースのアプリケーションは,異常終了するとWebサーバも巻き込んで停止させて しまう恐れがありますので,実装には注意が必要です. IISの公式な情報は, MSDNライブラリから検索するとよいでしょう.

    (2) Linux
    Apacheには,プラグインモジュールがたくさんあり,CGIをプロセスでなくApache の内部モジュールとして動作させる "mod_fastcgi" FirstCGI や,ASPのようにPerlを記述 できる,mod_perlを利用することができます.
    それらの機能を利用する場合,システムリソースにあわせて,Apache自体のCPU負荷や アクセス数の制限を httpd.conf に設定しておかなければ性能を発揮できない場合が あります.また,Apacheでどのようなコンテンツを提供するか?によって異なります. 例えば,CGIを多用する場合と,大容量のファイルをダウンロードさせる場合は, チューニングの方法が異なりますので,状況に応じて最適な設定を行う必要が あります. パフォーマンスチューニングの方法の詳細については, Apache 日本語ページを参考にすると よいでしょう.

    ●MIMEのバウンダリー文字列を生成する

    バウンダリー文字列は,データ内に含まれていない文字列でなければなりません. もし,バウンダリー文字列と同じ文字列がデータ内にあると,データの終端と判断 され,データを正しく処理できなくなります.

    ◆メッセージダイジェスト5を使う

    MD5(Message Digest 5)は,一方向性ハッシュ関数の一種で,次のような特徴が あります.
  • 異なる入力に対して同じ出力になることがほとんどない
  • 出力値から入力を推定することは数学的に大変困難
  • 入力が少し異なると出力は大きく異なる値が出力される
  • 出力のサイズは,入力サイズに依存せず128ビット(16進で32桁)

    このアルゴリズムは実装が簡単で,IP認証やFreeBSD やITU-UT勧告X.509 など, いろいろなところで利用されているます.メッセージとメッセージダイジェストを比較 することで改ざんの有無を確認することができますので,公開鍵の「指紋」の生成や 電子署名として利用されます.
    任意の文字列からメッセージダイジェストを生成すれば,その文字列固有の値になる ため,バウンダリー文字列としても利用することができます. なお,MD5を生成する方法は RFC1321 にC言語のソースコード付で解説されています ので,アルゴリズムの詳細はそちらを参照してください.


    MD5("")                           = d41d8cd98f00b204e9800998ecf8427e
    MD5("abcdefghijklmnopqrstuvwxyz") = c3fcd3d76192e4007dfb496cca67e13b
    MD5("message digest")             = f96b697d7cb7938d525a2f31aaf161d0
    

    リスト6:MD5ダイジェストの例

    ◆UUIDを使う

    簡単にそのような文字列を作成するために,RPC(Remote Procedure Call)を利用する 場合,UUID(Universally Unique Identifer)と呼ばれるOSF/DCEで定義された識別子を 利用することができます.
    Windowsでは,このIDをCOM(Compornet Object Model)や OLE(Object Linking and Embedding)のIID(Interface ID)やCLSID(Class ID)として 利用しています.これらのIDを相称してGUIDと呼んでいます.

    (1) WindowsNT/2000/XP
    コマンドラインから,uuidgenを実行することで取得することもできますが,以下のAPI を利用してプログラム内で取得することも可能です. d:\work>uuidgen
    a3b3bfc0-26ee-11d4-8af0-00c04f87d9a3
    d:\work>
    また,リスト7のAPIを利用してプログラム内で取得することも可能です.


    #include <rpc.h> rpcrt4.lib RPC_STATUS RPC_ENTRY UuidCreate(UUID *Uuid); 戻り値:正常終了... RPC_OK 異常終了...RPC_S_UUID_NO_ADDRESS #include <windows.h> ole32.lib HRESULT CoCreateGuid(GUID *lpGuid); 戻り値:正常終了... S_OK

    リスト7:UUID,GUID関数


    (2) Linux
    この機能を利用するには,e2fsprogsライブラリパッケージを入手して,インストール する必要があります.(このパッケージにはソースファイルも含まれる.)
    コマンドラインから,uuidgenを実行することで,取得することもできますが, 以下のAPIを利用して自分のプログラムから取得することも可能です.
    % uuidgen
    d852576c-14bb-4af0-a118-465964165197
    %



    #include <uuid/uuid.h> libuuid.a void uuid_generate(uuid_t out); void uuid_generate_random(uuid_t out); void uuid_generate_time(uuid_t out);

    リスト8:UUID関数


    ●マルチスレッドに対応する場合の注意点

    Windowsでは,提供されるAPIやCランタイムルーチンは,マルチスレッドに対応して おり,リンクするライブラリを変更することで,マルチスレッド版の関数をリンクする ことができますが,Linux(UNIX系OS)は,スレッドごとの共有メモリーを簡単に作れない ために,Cランタイムルーチンがスタティックにメモリーを確保している関数は, 関数名の末尾に"_r"(おそらく reentrantのr)をつけた別関数として定義して あります.
    time_r localtime_r gmtime_r asctime_r ctime_r rand_r gethostent_r gethostbyaddr_r gethostbyname_r gethostbyname2_r getnetent_r getnetbyaddr_r getnetbyname_r getservent_r getservbyname_r getservbyport_r getprotoent_r getprotobyname_r getprotobynumber_r getnetgrent_r

    アプリケーション側で領域を渡していないのにポインターを返す関数は, 関数内部でスタティックメモリーを利用している可能性があります. このような関数の,関数名に"_r"をつけてヘッダファイルを grep すると, マルチスレッド用の関数が存在するかもしれません.
    作成した処理にマルチスレッド対応でない関数が含まれる場合でも,セマフォなどを 用いて排他制御を行うことで利用できる場合もありますので,作成する機能によって 対処方法を検討するとよいでしょう.

    ●Linuxでファイルシステム間のファイル移動を行うときの注意点

    同一のファイルシステム内でファイルのリネームや移動を行う場合,rename システム コールを利用することができます.
    しかし,NFSによって複数のファイルシステムで構成されるシステム内でファイルの リネームや移動を行うとシステムコールが失敗することがあります.
    このシステムコールは,ディレクトリのリンクを変更(名前を変える)だけで,ファイル 自体の移動は行いません.このため,変更元ファイル名・変更先ファイル名が同一の ファイルシステム内でなければ正しく動作しません.

    ●Linuxで子プロセスを起動させるには

    CGIから時間のかかる処理の実行を行いたい場合,処理要求だけを受け取って 子プロセスで処理を実行させ,結果はメールで送信するといった方法が考えられます. しかし,単純に子プロセスを起動しただけでは思ったような動作をしません.
    Linux上で,動作するWebサーバ上で動作するCGIから,他のプロセスを起動する 場合,単純に fork 関数で処理を分岐させ,exec 関数でプロセスを起動すると, 起動した子プロセスが終了するまで,HTTPのセッションが維持された状態 (IEでは,右上の地球儀がまわったまま)になります.
    これは,fork した子プロセスにも httpd とのパイプが引き継がれているのが原因 です.
    親プロセスがセッションを維持したまま,子プロセスのパイプを切り離すには,exec する前に, 0, 1, 2 のファイルディスクリプタを別のファイルにリダイレクトすれば 期待通りに動作するようになります(リスト9).
    デーモンプロセスを作成する場合,コンソールにつながっている標準入出力を切り離す 必要がありますが,その場合と同様です.
    デーモンプロセスの場合,forkを行う前に,利用しないシグナルについても受信しない ようにしておく必要があります.そして,fork 後に親プロセスを終了させ, 子プロセスで実際の処理を行わせます.
    シグナルの対処は,リスト10のような処理を行います. 特に,SIGALRM, SIGPIPE, SIGCHLD は正しく処理を組み込まなければトラブルを起こす ことがあります.一般的には,これらのシグナルは,SIG_IGN にしておくとよいで しょう.


    if((pid = fork()) == 0){ setsid(); /* 呼び出しプロセスの端末を切り離す */ close(0); close(1); close(2); open("/dev/null", O_RDWR); /* ファイルディスクリプタ0に結びつく */ dup2(0, 1); dup2(0, 2); /* 子プロセスから登録モジュールを起動する */ execlp(cmd_path, cmd_path, param1, param2, param3, NULL); exit(0); } if(pid < 0){ return ERROR; } /* 親プロセスの処理 */ : : : printf("処理を開始しました.\n\n") :

    リスト9:標準入出力を切り離す



    signal(SIGALRM, SIG_IGN); signal(SIGCHLD, SIG_IGN); signal(SIGINT, SIG_IGN); signal(SIGHUP, SIG_IGN); signal(SIGUSR1, SIG_IGN); signal(SIGUSR2, SIG_IGN); signal(SIGTERM, SIG_IGN); signal(SIGPIPE, SIG_IGN); signal(SIGCHLD, SIG_IGN); sigemptyset(&sig_empty); sigfillset(&sig_fill); sigprocmask(SIG_SETMASK, &sig_fill, (sigset_t *)NULL);

    リスト10:シグナルの設定


    ●GDBでCGIをデバッグするには

    gdbでCGIをデバッグする場合,Webサーバから実行するとデバッグを行うことが できません.このため,TELNETクライアント上で実行するシェルにCGIが実行された ときと同様の状態を作り出す必要があります.

  • ソースファイルがカレントディレクトリにない環境で実行するときは,.gdbinit であらかじめ格納場所を指定することができる.
    このファイルをホームディレクトリに作成しておくとGDBを起動すると自動的に読み込まれ,設定の初期化を行う.
    dirコマンドでソースファイルのパスを指定できる.
    例:dir /home/user/src/: /home/user/src2
  • 必要な環境変数の設定を行うシェルスクリプトを作成する(リスト11).
  • % source ファイル名 を実行して,スクリプトファイルを実行中のシェルに反映する
  • % gdb デバッグモジュール名 を実行する

    もし,POSTメソッドで値を入力する必要がある場合は,runコマンドを実行するときに 標準入力から読み込むファイル名を指定することができる.
    また,シェルから実行するときのように,オプションパラメーターも指定できる.
    (gdb) run < ファイル名
    ※作成した環境変数設定ファイルと .bashrc に同一の環境変数が存在する場合,GDBは .bashrc に設定されている値を優先する.


    # CGI用環境変数 export HTTP_USER_AGENT='Test' export GATEWAY_INTERFACE='CGI/1.1' export SCRIPT_NAME='/cgi-bin/xxxx.cgi' export SERVER_SOFTWARE='Test' export SERVER_NAME='xxx.xxx.xxx.xxx' export SERVER_PROTOCOL='HTTP/1.1' export SERVER_PORT='80' # GET export REQUEST_METHOD='GET' export QUERY_STRING='data1=111&data2=2222&mode=1' # POST # 標準入力から指定したバイト数のデータを入力する #export REQUEST_METHOD='POST' #export CONTENT_LENGTH='41'

    リスト11:設定ファイル(bash用)の例

    ●GDBでcoreファイルでトラブルを突き止める

    Linuxでは,アプリケーションが異常終了するとcore ファイルが作成されます. このファイルのファイル名である core は,コア(その昔,メモリーはコアと呼ばれる 磁気フェライトのリングと配線で作られたコアの磁化の方向で情報を記録する メモリーを利用していたこと)に由来します.
    このファイルは,障害が発生したときにアプリケーションが実行していた環境の メモリーの内容をダンプした情報が記録されています. このファイルは,障害が発生したマシンでなくても実行したモジュールとソース ファイルがあればデバッグを行うことができます.
    ソースレベルでデバッグを行うには,"-g" オプションを設定してデバッグシンボル 情報をモジュールに埋め込んでおく必要があります.
    以下は,myappコマンドの実行で出力されたコアダンプを利用して,デバッグを 行う場合の実行方法です.
    [tsuneoka@mystiy test]$ gdb myapp -core core


    ●GDBで関数の戻り値を変数に代入せずに値を変更するには

    GDBでデバッグを行う場合,変数の変更して動作を一時的に変更して動作させる必要 がある場合があります.このとき,関数の戻り値を変数に設定している場合には, その変数の値を修正すれば問題ありませんが,変数に設定せずに比較のみを行っている 場合は,ソースレベルデバッグで動作を変更できません.
    そこで,プログラムの一部を逆アセンブルしてレジスタの値を変更して一時的に 動作を変更します.
    リスト12は,13行を強制的にNULLが設定されたときと同じ動作を再現している ところです.


    10 } 11 puts("--- start ---"); 12 if((fp = fopen(argv[1], "r")) != NULL){ 13 while(fgets( buff, sizeof(buff), fp) != NULL){ 14 printf("%s", buff); 15 } 16 fclose(fp); 17 } 18 puts("--- end ---"); 19 } 20 デバッグシンボルを埋め込んでコンパイルを行います. [tsuneoka@mystiy test]$ gcc -g -o test2 test2.c [tsuneoka@mystiy test]$ gdb test2 GNU gdb 19991004 Copyright 1998 Free Software Foundation, Inc. GDB is free software, covered by the GNU General Public License, and you are welcome to change it and/or distribute copies of it under certain conditions. Type "show copying" to see the conditions. There is absolutely no warranty for GDB. Type "show warranty" for details. This GDB was configured as "i386-redhat-linux"... ポイントになる行にブレークポイントを設定します. (gdb) b 11 Breakpoint 1 at 0x80484d3: file test2.c, line 11. (gdb) b 14 Breakpoint 2 at 0x8048521: file test2.c, line 14. (gdb) b 16 Breakpoint 3 at 0x8048537: file test2.c, line 16. (gdb) r test2.c Starting program: /home/tsuneoka/test/test2 test2.c Breakpoint 1, main (argc=2, argv=0xbffffb34) at test2.c:11 11 puts("--- start ---"); (gdb) list 6 char buff[256]; 7 8 if(argc < 2){ 9 return 0; 10 } 11 puts("--- start ---"); 12 if((fp = fopen(argv[1], "r")) != NULL){ 13 while(fgets( buff, sizeof(buff), fp) != NULL){ 14 printf("%s", buff); 15 } 分岐させたい行(13行)のアドレスを取得します (gdb) info line 13 Line 13 of "test2.c" starts at address 0x8048501 and ends at 0x8048521 . 分岐させたい行(13行)のコードを逆アセンブルします (gdb) disas 0x8048501 0x8048521 Dump of assembler code from 0x8048501 to 0x8048521: 0x8048501 : mov 0xfffffffc(%ebp),%eax 0x8048504 : push %eax 0x8048505 : push $0x100 0x804850a : lea 0xfffffefc(%ebp),%eax 0x8048510 : push %eax 0x8048511 : call 0x80483b4 0x8048516 : add $0xc,%esp 0x8048519 : mov %eax,%eax 0x804851b : test %eax,%eax 0x804851d : jne 0x8048521 0x804851f : jmp 0x8048537 End of assembler dump. 条件を変更するにはレジスタの値をチェックしている行で 停止させる必要があります. (gdb) b *0x804851b Breakpoint 4 at 0x804851b: file test2.c, line 13. (gdb) c Continuing. --- start --- Breakpoint 4, 0x804851b in main (argc=2, argv=0xbffffb34) at test2.c:13 関数からの戻り値の値をチェックして,その値をNULLに設定します. 13 while(fgets( buff, sizeof(buff), fp) != NULL){ (gdb) p $eax $2 = -1073743388 (gdb) set $eax = 0 (gdb) c Continuing. 値の変更後に,16行で停止したことを確認することで, 14行が実行されなかったことを確認できます. Breakpoint 4, main (argc=2, argv=0xbffffb34) at test2.c:16 16 fclose(fp); (gdb)

    リスト12:gdbで戻り値の値を変更する

    ●モジュールが利用している共有ライブラリを参照する

    モジュールが参照している共有ライブラリを参照するには,以下のコマンドを利用 します.なお,OSによってコマンド名や表示される内容が異なりますので, 注意が必要です.

    (1) Windows9x/NT/2000/XP
    Visual Studio をインストールする必要があります.コマンドプロンプトから, vcvars32.batを実行します.これにより,環境変数(パスなど)が設定されます. dumpbin.exeを実行します.
    モジュールがリンクしているDLLは,
    dumpbin /imports ファイル名.exe
    モジュールが参照を許可しているモジュールを参照するには,
    dumpbin /exports ファイル名.dll
    dumpbin.exeの実行例を図1に示します.

    (2) Linux
    シェルからlddを実行します.
    コマンドラインオプションは,OSによって異なりますので,利用方法は, manで調べる必要があります.
    lddの実行例を図2に示します.


    C:>dumpbin /imports client.cgi Microsoft (R) COFF Binary File Dumper Version 6.00.8447 Copyright (C) Microsoft Corp 1992-1998. All rights reserved. Dump of file client.cgi File Type: EXECUTABLE IMAGE Section contains the following imports: ADVAPI32.dll 447000 Import Address Table 453758 Import Name Table 0 time date stamp 0 Index of first forwarder reference 17B RegQueryValueExA MSVCRT.dll 447058 Import Address Table 4537B0 Import Name Table 0 time date stamp 0 Index of first forwarder reference 23F bsearch 2A9 rename 266 fwrite : :

    図1:dumpbin.exeの実行例



    [tsuneoka@mystiy test]$ ldd --help ldd [OPTION]... FILE... --help print this help and exit --version print version information and exit -d, --data-relocs process data relocations -r, --function-relocs process data and function relocations -v, --verbose print all information Report bugs using the `glibcbug' script to . [tsuneoka@mystiy test]$ ldd test libc.so.6 => /lib/libc.so.6 (0x4001b000) /lib/ld-linux.so.2 => /lib/ld-linux.so.2 (0x40000000) [tsuneoka@mystiy test]$

    図2:lddの実行例


    ●バイナリファイルのダンプを採る

    Linux でバイナリダンプを採るには,od コマンドを利用します. Windowsの場合,MS-DOSの時代はDUMP.EXE がありましたが,最近はなくなっている ようです.しかし,Bz Editor などフリーで入手できるツールが多くありますので, それらを入手するとよいでしょう.

    ●LinuxでCGIが呼び出すシステムコールのトレースを採る

    Linuxでシステムコールのトレース情報を取得するには,straceコマンドを利用します. このコマンドは,OSによってサポートされていない場合や,コマンド名が異なります. 一般的なUNIX系OSでは,truss コマンドの場合が多いようです.
    このコマンドをCGIで利用する場合,GDBでデバッグするときのように環境変数を シェルに設定しておいてこのコマンドからCGIを実行することも可能ですし, 以下のようなシェルのCGIを作成して,トレースを行いたいCGIを呼び出すことで, Webの画面でトレース結果を参照することが可能になります.


    #/bin/sh echo Content-Type: text/plain echo /usr/bin/strace xxxx.cgi

    リスト13:トレースCGI



    [tsuneoka@mystiy tsuneoka]$ strace ls execve("/bin/ls", ["ls"], [/* 24 vars */]) = 0 brk(0) = 0x8053608 old_mmap(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x40014000 open("/etc/ld.so.preload", O_RDONLY) = -1 ENOENT (No such file or directory) open("/etc/ld.so.cache", O_RDONLY) = 3 fstat(3, {st_mode=S_IFREG|0644, st_size=21341, ...}) = 0 old_mmap(NULL, 21341, PROT_READ, MAP_PRIVATE, 3, 0) = 0x40015000 close(3) = 0 open("/lib/libtermcap.so.2", O_RDONLY) = 3 fstat(3, {st_mode=S_IFREG|0755, st_size=12224, ...}) = 0 read(3, "\177ELF\1\1\1\0\0\0\0\0\0\0\0\0\3\0\3\0\1\0\0\0000\16\0"..., 4096) = 4096 old_mmap(NULL, 15304, PROT_READ|PROT_EXEC, MAP_PRIVATE, 3, 0) = 0x4001b000 mprotect(0x4001e000, 3016, PROT_NONE) = 0 old_mmap(0x4001e000, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED, 3, 0x2000) = 0x400 1e000 close(3) = 0 open("/lib/libc.so.6", O_RDONLY) = 3 : : :

    図3:lsコマンドの実行をstraceコマンドでトレースした結果