出力のサイズは,入力サイズに依存せず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が実行された
ときと同様の状態を作り出す必要があります.
% 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コマンドでトレースした結果