[Webプログラマのための]速習コードリーディング
WEB+DB PRESS vol35 (2006年10月) 特別企画[Webプログラマのための]速習コードリーディングの原稿です。 コードブログの派生です。 https://www.codeblog.org/blog/inoue/
前節の概論を踏まえて,Apacheのソースコードの読み方のコツを具体的に説明します.参照するソースは,執筆時点のApache2.2系の最新版の2.2.3です.また特に断りが無い限り,Unix系OSを前提にします.また,UnixのコマンドはGNU製ツールを使います.
ソースコードの取得とビルド
Apacheのソースコードを取得して,ビルドしてみましょう.バイナリファイルではなくソースコードを読むのだからビルドなんて必要無いと思うかもしれません.もちろん,ビルド作業はソースコードを読むための必須の作業ではありません.しかし,次の理由でビルドすることを薦めます.
ひとつは,ビルド処理の工程でソースファイル自体を生成することがあるからです.慣習的にconfig.hに類する名前を持つヘッダファイルには,システム固有の定義が並びます.これらの定義を知らずにソースを読むと,見当違いのコードを読む危険があります.同様の理由で,ビルド時にコンパイラに渡る引数も重要です. 次の理由は,Apacheのような巨大なソフトウェア特有の事情です.Apacheのモジュールは典型的ですが,そもそも該当するソースコードがコンパイルされていない,つまり実行ファイルに含まれてもいない,ことがあります.自分の手でビルドすれば,オブジェクトファイルの有無でソースファイルがコンパイルされたかどうかが分かります(脚注1). 最後の理由は,やや気分的なものですが,そもそもビルドすら通せないソースでは,読むことは難しいはずです.
<脚注1>厳密に言えば,実行ファイルにリンクされるかどうかは別問題です.</脚注1>
ソースコードの取得とビルド方法の手順の例 (http://httpd.apache.org/から近いミラーサイトを選択してください) $ wget http://ftp.kddilabs.jp/infosystems/apache/httpd/httpd-2.2.3.tar.bz2 $ tar jxt httpd-2.2.3.tar.bz2 $ cd httpd-2.2.3 1. 一般ユーザで書き込み可能なディレクトリにインストールする例 $ ./configure --prefix=~/local $ make install 2. デフォルトのディレクトリ(/user/local/)にインストールする例 $ ./configure # make install (rootユーザでインストール)
Apacheのフックのコードリーディング
前節の説明にあるように,Apacheのソースコードの特徴のひとつがフック処理です.ここでは実際のフックの実装を読解します.フックの実装を読むことで,マクロを読むコツもつかめます.
フックの呼び出し元はap_run_foo()の形式をしています.fooの部分にはApacheが定義する様々なフック名が入ります.フック関数の登録は,ap_hook_foo()の形式をしています.Apacheモジュールは独自のフック関数を実装して任意のフックに登録することが可能です.この仕組みにより,Apacheは高い拡張性を実現しています.
フック呼び出しのコード
ap_process_request_internal()はHTTPリクエストの受信後に呼ばれる処理です。この処理からフックの呼び出し部分を抜粋したコードを示します.引用中では,ap_run_translate_name()とap_run_map_to_storage()がフックの呼び出しです.フックの仕組みを知らずにこのコードを見ると,ap_run_translate_name()という関数の定義がどこかにあると考えるはずです.しかし,Apacheのソースツリーの中にap_run_translate_name()の定義はそのままでは見つかりません.なぜなら,ap_run_translate_name()の関数定義は,マクロの展開で生成されるからです.
フックの呼び出しのコード例 [code-1] /server/request.c: AP_DECLARE(int) ap_process_request_internal(request_rec *r) { 省略 if (!file_req) { if ((access_status = ap_location_walk(r))) { return access_status; } if ((access_status = ap_run_translate_name(r))) { return decl_die(access_status, "translate", r); } } 省略 if ((access_status = ap_run_map_to_storage(r))) { /* This request wasn't in storage (e.g. TRACE) */ return access_status; } 省略 }
フックのコードを生成するマクロ定義
次のふたつのマクロが、フックの登録ap_hook_foo()とフックの呼び出しap_run_foo()のコードを作り出す中心です.このマクロを見てコードを読むのが嫌になるかもしれませんが,少し我慢してください.コードの書き手からすれば,このようなマクロには,「マクロの中身は気にせず形式だけ意識してください」という暗黙のメッセージがあります.つまり,コードリーディングの観点では,その実装がどうあれ,ap_run_foo()の形式を見ればフックの呼び出し,ap_hook_foo()の形式を見ればフックの登録,と思えば良いのです.
/srclib/apr-util/include/apr_hooks.h: [code-2](改行を加えてあります) #define APR_IMPLEMENT_EXTERNAL_HOOK_BASE(ns,link,name) \ link##_DECLARE(void) ns##_hook_##name(\ ns##_HOOK_##name##_t *pf,const char * const *aszPre,\ const char * const *aszSucc,int nOrder) \ { \ ns##_LINK_##name##_t *pHook; \ if(!_hooks.link_##name) \ { \ _hooks.link_##name=apr_array_make(\ apr_hook_global_pool,\ 1,\ sizeof(ns##_LINK_##name##_t));\ apr_hook_sort_register(#name,&_hooks.link_##name); \ } \ pHook=apr_array_push(_hooks.link_##name); \ pHook->pFunc=pf; \ pHook->aszPredecessors=aszPre; \ pHook->aszSuccessors=aszSucc; \ pHook->nOrder=nOrder; \ pHook->szName=apr_hook_debug_current; \ if(apr_hook_debug_enabled) \ apr_hook_debug_show(#name,aszPre,aszSucc); \ } \ APR_IMPLEMENT_HOOK_GET_PROTO(ns,link,name) \ { \ return _hooks.link_##name; \ } /srclib/apr-util/include/apr_hooks.h: #define APR_IMPLEMENT_EXTERNAL_HOOK_RUN_ALL(\ ns,link,ret,name,args_decl,args_use,ok,decline) \ APR_IMPLEMENT_EXTERNAL_HOOK_BASE(ns,link,name) \ link##_DECLARE(ret) ns##_run_##name args_decl \ { \ ns##_LINK_##name##_t *pHook; \ int n; \ ret rv; \ \ if(!_hooks.link_##name) \ return ok; \ \ pHook=(ns##_LINK_##name##_t *)_hooks.link_##name->elts; \ for(n=0 ; n < _hooks.link_##name->nelts ; ++n) \ { \ rv=pHook[n].pFunc args_use; \ \ if(rv != ok && rv != decline) \ return rv; \ } \ return ok; \ }
マクロの展開
マクロの展開結果が気になる人もいると思います.複雑なマクロの展開結果を知りたい場合,コードをじっと眺めているより,プリプロセッサ(cppコマンド)に展開させてしまうのが手です.gccの場合,-Eオプションを渡すことでプリプロセッサの出力結果を見ることが可能です.
server/request.cのプリプロセッサの出力結果を知るには次のようにします.
プリプロセッサでマクロ展開をする手順 $ cd server $ rm request.o ...コンパイラのフラグを知るためにオブジェクトファイルを削除 $ make request.o ...コンパイラのフラグを知るためにmakeを実行 gcc -g -O2 -pthread -DLINUX=2 -D_REENTRANT -D_GNU_SOURCE -D_LARGEFILE64_SOURCE [省略] -c request.c [このコンパイルオプションをそのまま使い,-cを-Eに変えてコンパイル. 標準出力に出力されるので,出力をファイルに落す. $ gcc -g -O2 -pthread -DLINUX=2 -D_REENTRANT -D_GNU_SOURCE -D_LARGEFILE64_SOURCE [省略] -E request.c > request.E
request.Eファイルに,マクロの展開後のコードがあります.エディタで開いて検索すると,ap_run_translate_name()とap_hook_translate_name()の関数定義が見つかります.整形してコメント付きで引用すると次のようになります(一部,マクロの定数をそのまま残しています).
フックのマクロ展開結果 [code-3] static struct { apr_array_header_t *link_translate_name;/* 動的配列 */ } _hooks; /* フック関数の登録API */ void ap_hook_translate_name( ap_HOOK_translate_name_t *pf,const char * const *aszPre, const char * const *aszSucc,int nOrder) { ap_LINK_translate_name_t *pHook; if (!_hooks.link_translate_name) { /* フック関数チェーンの配列を生成 */ _hooks.link_translate_name = apr_array_make( apr_hook_global_pool, 1, sizeof(ap_LINK_translate_name_t)); /* フック関数チェーンをソートする */ apr_hook_sort_register("translate_name", &_hooks.link_translate_name); } /* 配列の新規要素取り出し */ pHook = apr_array_push(_hooks.link_translate_name); /* 登録するフック関数の関数ポインタ */ pHook->pFunc = pf; /* ソート用(直前のフック関数) */ pHook->aszPredecessors = aszPre; /* ソート用(直後のフック関数) */ pHook->aszSuccessors = aszSucc; /* ソート用(指定順序) * APR_HOOK_FIRST,APR_HOOK_MIDDLE,APR_HOOK_LAST */ pHook->nOrder = nOrder; /* 以下,デバッグ用コード */ pHook->szName = apr_hook_debug_current; if (apr_hook_debug_enabled) apr_hook_debug_show("translate_name",aszPre,aszSucc); } /* フック関数チェーンの呼び出しAPI */ int ap_run_translate_name(request_rec *r) { ap_LINK_translate_name_t *pHook; int n; int rv; if (!_hooks.link_translate_name) return OK; /* フック関数チェーンの配列の要素を順に呼ぶ */ pHook = (ap_LINK_translate_name_t*) _hooks.link_translate_name->elts; for(n = 0 ;n < _hooks.link_translate_name->nelts; ++n) { /* フック関数の呼び出し */ rv = pHook[n].pFunc(r); /* フック関数の戻り値が非OKなら, * フック関数チェーンの呼び出しを停止 */ if (rv != OK) return rv; } return OK; }
フック登録のコード
translate_nameフックの登録箇所のコード例を示します.register_hooks()はApacheの起動時に一度呼ばれる処理です.このap_hook_translate_name()の呼び出しで,link_translate_name配列([code-3]参照)に関数ポインタap_core_translate()を挿入します(フック関数の登録).他の箇所のap_hook_translate_name()は,それぞれにフック関数を登録します. ap_run_translate_name()[code-1参照]は,これらの登録フック関数を順に呼び出します.
/server/core.c: [code-4] AP_DECLARE_NONSTD(int) ap_core_translate(request_rec *r) { 関数の中身(省略) } static void register_hooks(apr_pool_t *p) { 省略 ap_hook_translate_name(ap_core_translate,NULL,NULL,APR_HOOK_REALLY_LAST);
プロセスが落ちる場合
CやC++で書かれたソフトウェアに致命的なバグがある場合,落ちる(crash)か固まる(freeze)かの症状が現れます.それぞれの場合についての対応を書きます.
Apacheはかなり成熟したソフトウェアなので,簡単に落ちたり固まったりしません.この原稿を書くに当たって,うまく落せるパターンは無いかと探しましたが見つかりませんでした.反則技ですが,わざとバグを埋め込んで落とすことにしました.
落ちるコード /server/config.c: [code-5] AP_CORE_DECLARE(int) ap_invoke_handler(request_rec *r) { 省略 r = NULL; ...落すために埋め込んだ行 result = ap_run_handler(r);
このApacheを起動してWebブラウザからアクセスするとApacheが落ちるはずです.Unix系OSでは,プロセスが落ちると,落ちた時のメモリイメージをファイルにダンプしたcoreファイルができます(<脚注2>).
<脚注2>coreファイルができない場合,ulimitとカレントディレクトリの2点を確認してください.ulimit -aを実行すると,コアファイルのサイズ制限を表示できます.コアファイルのサイズ制限がゼロの場合,コアファイルができません.コアファイルができない別の原因として,プロセスがカレントディレクトリに書き込み権限が無い場合があります.GNU/Linuxで実行プロセスのカレントディレクトリを知る方法は,プロセスIDを調べて/proc/${PID}/cwdを調べます.また,Apacheの場合,CoreDumpDirectory設定ディレクティブでcoreファイルのできるディレクトリを指定できます.</脚注2>
次のように第二引数にプロセスの実行ファイル,第三引数にcoreファイルを指定してgdbを起動します.
$ gdb httpd core #0 0x0809209e in status_handler (r=0x0) at mod_status.c:236 236 if (strcmp(r->handler, STATUS_MAGIC_TYPE) && (gdb) bt ...backtraceの略.関数コールスタックを表示 #0 0x0809209e in status_handler (r=0x0) at mod_status.c:236 #1 0x08074da5 in ap_run_handler (r=0x0) at config.c:157 #2 0x080753a4 in ap_invoke_handler (r=0x81781f8) at config.c:372 #3 0x0808d0ff in ap_process_request (r=0x81781f8) at http_request.c:258 #4 0x0808ab08 in ap_process_http_connection (c=0x8172378) at http_core.c:184 #5 0x0807b725 in ap_run_process_connection (c=0x8172378) at connection.c:43 #6 0x080a823a in child_main (child_num_arg=0) at prefork.c:640 #7 0x080a8407 in make_child (s=0x8169060, slot=7) at prefork.c:736 #8 0x080a84d8 in startup_children (number_to_start=249) at prefork.c:754 #9 0x080a8f87 in ap_mpm_run (_pconf=0x80d2b20, plog=0x8110c18, s=0x80d7760) at prefork.c:975 #10 0x0806262c in main (argc=3, argv=0xbffffb74) at main.c:717 (gdb) p r $1 = (request_rec *) 0x0
gdbの出力結果から,mod_status.cの236行目でNULLポインタを参照したせいで落ちたことが分かります.コールスタックをさかのぼり,バグを探します.
プロセスが固まる場合
次にプロセスが固まった場合の例を示します.また意図的にバグを仕込んでみます.
プロセスが固まるコード [code-6] /server/protocol.c: static int read_request_line(request_rec *r, apr_bucket_brigade *bb) { 省略 次の行を無限ループするように書き換える /* } while ((len <= 0) && (++num_blank_lines < max_blank_lines)); */ } while (num_blank_lines < max_blank_lines);
このApacheを起動してWebブラウザからアクセスするとApacheが固まり反応が無くなるはずです.まずApacheのプロセスIDを調べます(psコマンドもしくはlogs/httpd.pidファイルの中身). 次のように第二引数にプロセスの実行ファイル,第三引数にプロセスIDを指定してgdbを起動します(起動プロセスにattach).Apacheのようにマルチプロセスで動くプログラムの場合,該当プロセスを見つけるのに苦労するかもしれません.
$ gdb httpd プロセスID (gdb) bt ...backtraceの略.関数コールスタックを表示
プロセスが固まる原因で多いのは次の3つです.コールスタックをさかのぼり,該当するバグを探します.落ちるバグに比べると原因をつきとめにくいことが多いですが,根気よくコードを見ればバグは必ず見つかります.
- 無限ループ
- リソース待ち(pipeの読み込みなど)
- マルチスレッドの場合,mutex lockのdead lock (脚注3)
<脚注3>複数のスレッドが同時に更新処理を行うと不整合が起きる共有データがある場合,ロックをして同時更新を防ぎます(更新処理が終わればアンロックします).この時に使うロックをmutex lockと呼びます.異なるスレッドがお互いのアンロックを待ち合う状況をdead lockと呼びます.例えば,ふたつのmutex lock(lock-Aとlock-B)がある時を考えます.lock-A,lock-Bの順序でロックをするスレッド(スレッド1)と,lock-B,lock-Aの順序でロックをするスレッド(スレッド2)が走る場合にdead lockが起きます.スレッド1がlock-Aをロックした後にスレッド2に実行が移って,スレッド2がlock-Bをロックします.スレッド2はlock-Aをロックできません.なぜならスレッド1がロックしているからです.スレッド1もlock-Bをロックできません.なぜならスレッド2がロックしているからです.双方が待ち状態のまま進展しないため,処理が止まります.</脚注3>
エラーログからソースを追う
次のような不正なシンボリックファイルを作ってしまった場合を考えます.Webブラウザからfoo.htmlにアクセスすると,Apacheのエラーログに次のようなメッセージが出ます.
循環ループするシンボリックファイル: $ cd htdocs $ ln -s foo.html . エラーログ Symbolic link not allowed or link target not accessible: ${ApacheのDocumentRoot}/foo.html
このエラーメッセージをソースコードから探してみます.Emacsを使っている場合,M-x grep-findが便利です.該当するコードは以下の箇所です.見て分かるようにエラーメッセージの全てで探すと行がマッチしません.律義に全てのメッセージを使ってgrepすると見つからず,'Symbolic link'や'Symbolic link not allowed'のように手抜きをして探すと見つかります.ささやかな教訓は,あまり長い文字列で検索しない方が良いこと,grepで見つからなくてもすぐに諦めないこと,の二点です.
/server/request.c: [code-7] AP_DECLARE(int) ap_directory_walk(request_rec *r) { 省略 if (thisinfo.filetype == APR_LNK) { /* Is this a possibly acceptable symlink? */ if ((res = resolve_symlink(r->filename, &thisinfo, opts.opts, r->pool)) != OK) { ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, "Symbolic link not allowed " "or link target not accessible: %s", r->filename); return r->status = res; } }
resolve_symlink()がエラーを返した場合ことが分かります.resolve_symlink()の定義は同じファイル内にあります.Emacsを使っている場合,M-x imenuが便利です.
resolve_symlink()のコードを眺めると,エラーを返す箇所が複数あることが分かります.
このコードをじっと眺めていても分からないので,デバッガ(gdb)で追ってみます.
$ gdb httpd (gdb) b resolve_symlink ...bはbreakpointの略.resolve_symlinkの入力時に補完が効きます (gdb) r -X ...rはrunの略.-XはApacheに渡すコマンドライン引数です(起動プロセスをひとつに制限します.デバッグの時に便利です)
上記のようにgdbからApacheを起動します.別プロセスのWebブラウザからfoo.htmlにアクセスしてください.次のようにブレークポイントで止まります.
ブレークポイントで止まった所 (再び,apr_statで止めて,戻り値を表示まで) Breakpoint 1, resolve_symlink (引数表示) at request.c:351 351 if (!(opts & (OPT_SYM_OWNER | OPT_SYM_LINKS))) { (gdb) b apr_stat Breakpoint 2 at 0x4008aaa0: file file_io/unix/filestat.c, line 234. (gdb) c Continuing. Breakpoint 2, apr_stat (引数表示) at file_io/unix/filestat.c:234 234 if (wanted & APR_FINFO_LINK) (gdb) finish ...実行再開して,apr_stat()を抜けたところで再停止 Run till exit from #0 apr_stat (引数表示) at file_io/unix/filestat.c:234 0x08070d71 in resolve_symlink (引数表示) at request.c:359 359 if ((res = apr_stat(&fi, d, lfi->valid & ~(APR_FINFO_NAME Value returned is $1 = 40 ...apr_stat()の戻り値が40
gdbでステップ実行すると,apr_stat()がエラー番号40を返すことが分かります(GNU/Linuxの場合). apr_stat()は関数名のprefixから想像がつくように,APR(Apache Portable Runtime)のAPIです(前節参照).実装は/srclib/apr/file_io/unix/filestat.cにあります.コメントを除去してコードを引用すると次のようになります.
/srclib/apr/file_io/unix/filestat.c: [code-8] APR_DECLARE(apr_status_t) apr_stat(apr_finfo_t *finfo, const char *fname, apr_int32_t wanted, apr_pool_t *pool) { struct_stat info; int srv; if (wanted & APR_FINFO_LINK) srv = lstat(fname, &info); else srv = stat(fname, &info); if (srv == 0) { finfo->pool = pool; finfo->fname = fname; fill_out_finfo(finfo, &info, wanted); if (wanted & APR_FINFO_LINK) wanted &= ~APR_FINFO_LINK; return (wanted & ~finfo->valid) ? APR_INCOMPLETE : APR_SUCCESS; } else { return errno; } }
このコードを理解するには少しUnixの知識を必要とします.まず,lstat()およびstat()がシステムコールだという知識です.これらがシステムコールであることは,man statやman lstatが,セクション2にあることで分かります(<脚注4>).次に必要な知識は,システムコールは成功時に0を返すことと,エラー時にerrno変数にエラー番号をセットすることです.apr_stat()の引用コードを見ると,apr_stat()の戻り値が40だったことで,errnoの値が40だったことが分かります.更にもうひとつ知っておくと良い知識は,システムコールのエラー番号の定義がerrno.hにあることです.昔のUnixでは/usr/include/errno.hでしたが,最近のGNU/Linuxではerrno.hを分割しています.値が40のエラー番号を探すと,Debian sargeの場合/usr/include/asm-generic/errno.hにELOOPが見つかります. シンボリックリンクの参照が循環していることの予想がつきます.
/usr/include/asm-generic/errno.h: #define ELOOP 40 /* Too many symbolic links encountered */
<脚注4>stat(2)やlstat(2)のように表記します.ある関数がシステムコールかどうかをマニュアルだけで調べる方法は完全ではありませんが,たいていは事足ります.</脚注4>
参考: gdbの良く使うコマンド ||略||コマンド名||意味|| ||b||breakpoint||ブレークポイントの設定|| ||r||run||実行開始|| ||c||continue||実行再開|| ||n||next||関数に入らずステップ実行|| ||s||step||関数に入るステップ実行|| ||p||print||変数の値の表示|| ||x||examine||メモリの値の表示.x/64xb PTR(アドレス)で,指定アドレスから64バイトの領域を16進数表示.x/64cb PTR(アドレス)で,指定アドレスから64バイトの領域をキャラクタ表示|| ||bt||backtrace||コールスタックの表示|| ||f||frame||コールスタックの表示移動||