Apache Output Filter and Bucket Brigade

Apache のフィルター モジュールを書きました。

参考にしたのは以下の情報。

データの入出力は、bucket brigade (バケツ リレー) という名前で呼ばれる双方向循環リストを通して行います。何だか Windows カーネルのようだ。これは Apache 1.x から 2 になったときに導入されたらしい。リストを使うことで、ストリームを途中で分割したり、長さの異なるデータに置換したりするのが楽になる、というのがメリットです。

書いたモジュールは以下の 2 つ。コーヒーとサンドイッチ。塩漬けになっていた GitHub を使ってみた。

サンドイッチの方は、上記参考 URL の最後にある apachetutor とほぼ同じ。完全なソースは ↓ で見つかりました。

mod_txt
http://apache.webthing.com/mod_txt/mod_txt.c

フィルターに限らず、Apache のモジュールを開発するときは apxs というツールを使うと便利です。実体は Perl スクリプトです。Apache をソースからビルドしたときは、bin フォルダーに apxs が入っています。

$ ls -l /usr/local/apache-httpd/current/bin
total 4448
-rwxr-xr-x 1 root root 1992139 Dec  3 23:36 ab
-rwxr-xr-x 1 john john    3537 Dec 22 21:51 apachectl
-rwxr-xr-x 1 john john   23533 Dec  3 23:19 apxs
-rwxr-xr-x 1 root root   13657 Dec  3 23:36 checkgid
-rwxr-xr-x 1 john john    8925 Dec  3 23:19 dbmmanage
-rw-rw-r– 1 john john    1109 Dec  3 23:19 envvars
-rw-rw-r– 1 john john    1109 Dec  3 23:19 envvars-std
-rwxr-xr-x 1 root root   24063 Dec  3 23:36 fcgistarter
-rwxr-xr-x 1 root root   80843 Dec  3 23:36 htcacheclean
-rwxr-xr-x 1 root root   51710 Dec  3 23:36 htdbm
-rwxr-xr-x 1 root root   25987 Dec  3 23:36 htdigest
-rwxr-xr-x 1 root root   51035 Dec  3 23:36 htpasswd
-rwxr-xr-x 1 root root 2149389 Dec  3 23:36 httpd
-rwxr-xr-x 1 root root   22250 Dec  3 23:36 httxt2dbm
-rwxr-xr-x 1 root root   25086 Dec  3 23:36 logresolve
-rwxr-xr-x 1 root root   45976 Dec  3 23:36 rotatelogs

apt-get した Apache に対して使うときは、apache2-dev というパッケージに入っているので、それを別途インストールして下さい。手元の Ubuntu だと、スクリプトは /usr/bin にできました。apxs2 というのは単なるシンボリックリンクです。

$ ls -l /usr/bin/apxs*
-rwxr-xr-x 1 root root 19761 Jul 22 07:36 /usr/bin/apxs
lrwxrwxrwx 1 root root     4 Jul 22 07:37 /usr/bin/apxs2 -> apxs

apxs コマンドの主な役割は 3 つです。

  • ソースコードのテンプレートの作成 = オプション -g
  • ビルド = オプション -c
  • Apache へのインストール = オプション -i

開発環境は以下の通り。

  • Ubuntu Server 14.04 LTS x64
  • Linux Kernel 3.13.0-24-generic
  • Apache httpd 2.4.10 (ソースからビルド)
  • gcc (Ubuntu 4.8.2-19ubuntu1) 4.8.2
  • GNU gdb (Ubuntu 7.7.1-0ubuntu5~14.04.2) 7.7.1

まずは、テンプレートを作ります。モジュール名のディレクトリも apxs が作ります。

john@glycine:~$ /usr/local/apache-httpd/current/bin/apxs -g -n coffee
Creating [DIR]  coffee
Creating [FILE] coffee/Makefile
Creating [FILE] coffee/modules.mk
Creating [FILE] coffee/mod_coffee.c
Creating [FILE] coffee/.deps

mod_coffee.c が唯一のソース ファイルです。このファイルのコメントに書いてありますが、生成されたまま何も変更を加えない状態でも動作をテストできるのでやってみます。

apxs を使ってビルド、するわけですが、この後のデバッグでビルドとインストールコマンドを飽きるほど実行するので、スクリプトを作っておきます。もうちょっと汎用的なスクリプト書けよ、という突っ込みはなしの方向で (汗

john@glycine:~/coffee$ cat build.sh
/usr/local/apache-httpd/current/bin/apxs -c mod_coffee.c
john@glycine:~/coffee$ cat install.sh
/usr/local/apache-httpd/current/bin/apxs -i mod_coffee.la

ビルドします。

john@glycine:~/coffee$ ./build.sh
/usr/local/apr/apr-1.5.1/build-1/libtool –silent –mode=compile gcc -std=gnu99 -prefer-pic   -DLINUX -D_REENTRANT -D_GNU_SOURCE -g -O2 -pthread -I/usr/local/apache-httpd/httpd-2.4.10/include  -I/usr/local/apr/apr-1.5.1/include/apr-1   -I/usr/local/apr-util/apr-util-1.5.4/include/apr-1   -c -o mod_coffee.lo mod_coffee.c && touch mod_coffee.slo

/usr/local/apr/apr-1.5.1/build-1/libtool –silent –mode=link gcc -std=gnu99    -o mod_coffee.la  -rpath /usr/local/apache-httpd/httpd-2.4.10/modules -module -avoid-version    mod_coffee.lo

コンパイル エラーはなくビルドは上手くいったので、Apache にインストールします。root 権限が必要です。

john@glycine:~/coffee$ sudo ./install.sh
[sudo] password for john:
no talloc stackframe at ../source3/param/loadparm.c:4864, leaking memory
/usr/local/apache-httpd/httpd-2.4.10/build/instdso.sh SH_LIBTOOL=’/usr/local/apr/apr-1.5.1/build-1/libtool’ mod_coffee.la /usr/local/apache-httpd/httpd-2.4.10/modules
/usr/local/apr/apr-1.5.1/build-1/libtool –mode=install install mod_coffee.la /usr/local/apache-httpd/httpd-2.4.10/modules/
libtool: install: install .libs/mod_coffee.so /usr/local/apache-httpd/httpd-2.4.10/modules/mod_coffee.so
libtool: install: install .libs/mod_coffee.lai /usr/local/apache-httpd/httpd-2.4.10/modules/mod_coffee.la
libtool: install: install .libs/mod_coffee.a /usr/local/apache-httpd/httpd-2.4.10/modules/mod_coffee.a
libtool: install: chmod 644 /usr/local/apache-httpd/httpd-2.4.10/modules/mod_coffee.a
libtool: install: ranlib /usr/local/apache-httpd/httpd-2.4.10/modules/mod_coffee.a
libtool: finish: PATH="/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/sbin" ldconfig -n /usr/local/apache-httpd/httpd-2.4.10/modules
———————————————————————-
Libraries have been installed in:
   /usr/local/apache-httpd/httpd-2.4.10/modules

If you ever happen to want to link against installed libraries
in a given directory, LIBDIR, you must either use libtool, and
specify the full pathname of the library, or use the `-LLIBDIR’
flag during linking and do at least one of the following:
   – add LIBDIR to the `LD_LIBRARY_PATH’ environment variable
     during execution
   – add LIBDIR to the `LD_RUN_PATH’ environment variable
     during linking
   – use the `-Wl,-rpath -Wl,LIBDIR’ linker flag
   – have your system administrator add LIBDIR to `/etc/ld.so.conf’

See any operating system documentation about shared libraries for
more information, such as the ld(1) and ld.so(8) manual pages.
———————————————————————-
chmod 755 /usr/local/apache-httpd/httpd-2.4.10/modules/mod_coffee.so

これで、mod_coffee.so が Apache の modules ディレクトリに作られました。では httpd.conf を編集します。以下の 4 行を追加します。

LoadModule coffee_module modules/mod_coffee.so
<Location /coffee>
    SetHandler coffee
</Location>

最後に httpd を再起動して、ブラウザーで /coffee を開きます。"The sample page from…" という文字列が表示されれば成功です。

image

apxs コマンドでは、-c -i -a というように同時にオプションを指定すると、ビルド、インストール、設定ファイルへの LoadModule の追加を一気にやってくれます、が、実際にモジュールを開発するときは分けて実行した方がよさそうです。コンパイル エラー出まくるし・・。

apxs が生成したテンプレートはコンテンツ ハンドラーで、フィルターではありません。フィルターのテンプレートは作ってくれないので、自分で全部書かないといけません。ハンドラーとなるコールバック関数は、 coffee_register_hooks の中でap_hook_handler によって登録されていますが、これの代わりに ap_register_output_filter を使うと出力フィルターのコールバック関数を登録できます。

サーバーが応答するときに、ap_register_output_filter で登録したコールバック関数が前述の bucket brigade をパラメーターとして呼び出されます。bucket brigade が表現するストリーム データが HTTP 応答として送信されるデータになります。フィルターは、この bucket brigade を加工することができます。Apache は複数のフィルターを持つことができ、コールバック関数がパラメーターとして受け取る bucket brigade は、前のフィルターから渡されてきています。つまり、bucket brigade を受け取ったフィルターは、データを加工した後、次のフィルターに加工済みの bucket brigade を流すことでデータを出力したことになります。このあたりがバケツ リレーという名前の由来になっているのでしょう。

bucket brigade は双方向循環リストでありapr_bucket_brigade 構造体で表されます。リストの個々の要素はバケツと呼ばれ、apr_bucket 構造体で表されます。バケツには種類があり、メモリ上の BLOB を表す HEAP だけでなく、ディスク上のファイルを表す FILE、ストリームの末尾を表す EOS などがあります。EOS などのメタデータではなく、実データが入っているバケツからは、apr_bucket_read などの関数を使ってデータを char の配列として取り出すことができます。

フィルターは、入力データの bucket brigade に対して、バケツの分割や削除、新しいバケツの追加などを行うことができます。最終的には次のフィルターに対して bucket brigade の形でデータを流せばそれが応答になるので、次のフィルターに渡さなかったバケツはフィルターによって削除されたように見えます。次のフィルターに bucket brigade を渡すのは、ap_pass_brigade 関数で行います。渡すことができるのは bucket brigade であり、バケツ単体を渡すことはできません。

ここでポイントになるのは、上から流れてきたバケツ リレーと同じものを下のフィルターに渡す必要は全くないということです。フィルターの中で新たにリストの構造 (=apr_bucket_brigade) を作って、中のバケツを新たなリストに移動させて次のフィルターに渡しても全く問題ありません。元のバケツ リレーのリストからバケツを削除したとしても、次のフィルターに渡さない限りその構造は使われないためです。

mod_sed や mod_substitute のソースを見ると、コールバック関数の中で bucket brigade の中身を一つずつ読む大きなループを書くのが一般的なように見えます。このとき、次のフィルターに bucket brigade を渡す方法は 2 通り考えられます。ループの中で、バケツ毎に ap_pass_brigade を呼び出す方法、もしくは、ループの中では新しい bucket brigade にバケツを追加するだけにとどめ、ループの後でまとめて ap_pass_brigade を呼び出す方法です。どちらの方法でも構いませんが、後者の方法を取るときは、FLUSH バケツが来たときに、自分がバッファーしているバケツを即時に次のフィルターへ流す処理を実装しないといけません。

Guide to writing output filters – Apache HTTP Server Version 2.5
http://httpd.apache.org/docs/trunk/developer/output-filters.html

A FLUSH bucket indicates that the filter should flush any buffered buckets (if applicable) down the filter chain immediately.

例えば mod_substitute は前者の方法にしているので、以下のようなコメントがあります。

/*
* No need to handle FLUSH buckets separately as we call
* ap_pass_brigade anyway at the end of the loop.
*/

細かいことを挙げていくとキリがありませんが、以上が調べていて分かった bucket brigade の大体のイメージです。で、何のフィルターを書いたかというと、サンドイッチは、流れてきたデータを HTML ではなくテキストとして表示するためのフィルターで、コーヒーは、どんなバケツが流れてきているのかをコンソール、もしくはエラー ログに出力するためのフィルターです。

GitHub にあるコードをビルドして、Apache にインストールした後、httpd.conf に以下の設定を追加します。

AddOutputFilterByType SANDWICH text/html
Sandwich_Header "conf/header"
Sandwich_Footer "conf/footer"

AddOutputFilterByType Sed text/html
OutputSed "s/Welcome/WELCOME/g"

AddOutputFilterByType SUBSTITUTE text/html
Substitute "s/h3>/h2>/"

AddOutputFilterByType COFFEE text/html
Coffee_LogOption 1

mod_sandwich が使うヘッダーとフッターのファイルを作成します。

john@glycine:~$ cat /usr/local/apache-httpd/current/conf/header
<!DOCTYPE html><html><head></head><body><pre>

john@glycine:~$ cat /usr/local/apache-httpd/current/conf/footer
</pre></body></html>

index.html はこれを使います。

<!DOCTYPE html>
<html><head>
<meta http-equiv="x-ua-compatible" content="IE=8">
</head><body><h3>Welcome!</h3><p>This is a body text.</p></body></html>

httpd をデバッガーから起動してブラウザーでサーバーにアクセスすると、以下のように表示されます。

image

httpd.conf に設定したように、データは mod_sandwich –> mod_sed –> mod_substitute –> mod_coffee の順に処理されます。welcome は大文字に置換されますが、h3 エレメントは最初の mod_sandwich によって ">" がエスケープされているので置換されません。最後の mod_coffee によって、デバッガー上でには以下のようなログが出力されます。

ENTERING coffee_filter
Processing data bucket (len=46)
        ==> ap_pass_brigade returned 0
Processing data bucket (len=1)
        ==> ap_pass_brigade returned 0
Processing data bucket (len=27)
        ==> ap_pass_brigade returned 0
Processing data bucket (len=25)
        ==> ap_pass_brigade returned 0
Processing data bucket (len=87)
        ==> ap_pass_brigade returned 0
Processing data bucket (len=140)
        ==> ap_pass_brigade returned 0
Processing data bucket (len=1)
        ==> ap_pass_brigade returned 0
Processing data bucket (len=21)
        ==> ap_pass_brigade returned 0
Processing data bucket (len=1)
        ==> ap_pass_brigade returned 0
LEAVING coffee_filter
ENTERING coffee_filter
Processing EOS bucket.
        ==> ap_pass_brigade returned 0
LEAVING coffee_filter

上記出力から、coffee_filter は 2 回呼び出されていることが分かります。ほとんどは 1 回目の呼び出し時に処理されていますが、EOS だけの bucket brigade が 2 回目の呼び出し時に流れてきています。一回のリクエストであっても、フィルターのコールバック関数が何回呼ばれるのかは、その他のフィルターなどの設定に依存します。

フィルターの多くは、入力データを見て、その内容に応じてデータを加工することになるはずです。例えばストリームの先頭の方のデータを見て、末尾の方のデータを変更する、となったときに、先頭のデータを元にする情報はどこかに保持していないといけません。しかし、コールバック関数が何度呼ばれるか分からない以上、それをコールバック関数のローカル変数として保持させるわけにはいきません。このため、多くのフィルターでは、コールバック関数のもう一つのパラメーターである ap_filter_t 構造体の ctx を使います。このポインターに、フィルター独自の構造体を割り当てておくことで、異なるタイミングで呼ばれたコールバック関数内でも一貫したデータを扱えるようになります。

デバッガーから、mod_coffee が呼ばれるときのコールスタックを見てみます。

(gdb) i functions coffee
All functions matching regular expression "coffee":

File mod_coffee.c:
static apr_status_t coffee_filter(ap_filter_t *, apr_bucket_brigade *);
static void coffee_register_hooks(apr_pool_t *);
(gdb) break coffee_filter
Breakpoint 1 at 0x7ffff3aacd30: file mod_coffee.c, line 95.
(gdb) command 1
Type commands for breakpoint(s) 1, one per line.
End with a line saying just "end".
>bt
>c
>end
(gdb) i break
Num     Type           Disp Enb Address            What
1       breakpoint     keep y   0x00007ffff3aacd30 in coffee_filter at mod_coffee.c:95
        bt
        c
(gdb) !touch /usr/local/apache-httpd/current/htdocs/index.html
(gdb) r -X
The program being debugged has been started already.
Start it from the beginning? (y or n) y
Starting program: /usr/local/apache-httpd/httpd-2.4.10/bin/httpd -X
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/lib/x86_64-linux-gnu/libthread_db.so.1".
warning: Temporarily disabling breakpoints for unloaded shared library "/usr/local/apache-httpd/httpd-2.4.10/modules/mod_coffee.so"
[New Thread 0x7ffff306c700 (LWP 19607)]
...snip...
[New Thread 0x7fffe1feb700 (LWP 19633)]
[Thread 0x7ffff306c700 (LWP 19607) exited]
[Switching to Thread 0x7fffe27ec700 (LWP 19632)]

Breakpoint 1, coffee_filter (f=0x7fffdc008bf8, bb=0x7fffdc00a248) at mod_coffee.c:95
95      static apr_status_t coffee_filter(ap_filter_t *f, apr_bucket_brigade *bb) {
#0  coffee_filter (f=0x7fffdc008bf8, bb=0x7fffdc00a248) at mod_coffee.c:95
#1  0x00007ffff56f6194 in filter_harness (f=0x7fffdc008bf8, bb=0x7fffdc00a248) at mod_filter.c:323
#2  0x00007ffff54f1bb5 in substitute_filter (f=0x7fffdc008bd0, bb=0x7fffdc00a0d0) at mod_substitute.c:511
#3  0x00007ffff56f6194 in filter_harness (f=0x7fffdc008bd0, bb=0x7fffdc00a0d0) at mod_filter.c:323
#4  0x00007ffff52e81f6 in sed_response_filter (f=0x7fffdc008ba8, bb=0x7fffdc009040) at mod_sed.c:376
#5  0x00007ffff56f6194 in filter_harness (f=0x7fffdc008ba8, bb=0x7fffdc009040) at mod_filter.c:323
#6  0x00007ffff38a9e6f in sandwich_filter_handler (filter=0x7fffdc008b80, bb=0x7fffdc009040) at mod_sandwich.c:165
#7  0x00007ffff56f6194 in filter_harness (f=0x7fffdc008b80, bb=0x7fffdc009040) at mod_filter.c:323
#8  0x0000000000439f07 in default_handler (r=0x7fffdc002970) at core.c:4369
#9  0x000000000044a260 in ap_run_handler (r=0x7fffdc002970) at config.c:169
#10 0x000000000044a7a9 in ap_invoke_handler (r=r@entry=0x7fffdc002970) at config.c:433
#11 0x000000000045dd5a in ap_process_async_request (r=r@entry=0x7fffdc002970) at http_request.c:317
#12 0x000000000045ad70 in ap_process_http_async_connection (c=0x7fffec0372a0) at http_core.c:143
#13 ap_process_http_connection (c=0x7fffec0372a0) at http_core.c:228
#14 0x0000000000453580 in ap_run_process_connection (c=0x7fffec0372a0) at connection.c:41
#15 0x0000000000465d94 in process_socket (my_thread_num=24, my_child_num=0, cs=0x7fffec037218, sock=<optimized out>,
    p=<optimized out>, thd=<optimized out>) at event.c:1035
#16 worker_thread (thd=<optimized out>, dummy=<optimized out>) at event.c:1875
#17 0x00007ffff754e182 in start_thread (arg=0x7fffe27ec700) at pthread_create.c:312
#18 0x00007ffff727afbd in clone () at ../sysdeps/unix/sysv/linux/x86_64/clone.S:111
ENTERING coffee_filter
Processing data bucket (len=46)
        ==> ap_pass_brigade returned 0
Processing data bucket (len=1)
        ==> ap_pass_brigade returned 0
Processing data bucket (len=27)
        ==> ap_pass_brigade returned 0
Processing data bucket (len=25)
        ==> ap_pass_brigade returned 0
Processing data bucket (len=87)
        ==> ap_pass_brigade returned 0
Processing data bucket (len=140)
        ==> ap_pass_brigade returned 0
Processing data bucket (len=1)
        ==> ap_pass_brigade returned 0
Processing data bucket (len=21)
        ==> ap_pass_brigade returned 0
Processing data bucket (len=1)
        ==> ap_pass_brigade returned 0
LEAVING coffee_filter

Breakpoint 1, coffee_filter (f=0x7fffdc008bf8, bb=0x7fffdc00a248) at mod_coffee.c:95
95      static apr_status_t coffee_filter(ap_filter_t *f, apr_bucket_brigade *bb) {
#0  coffee_filter (f=0x7fffdc008bf8, bb=0x7fffdc00a248) at mod_coffee.c:95
#1  0x00007ffff56f6194 in filter_harness (f=0x7fffdc008bf8, bb=0x7fffdc00a248) at mod_filter.c:323
#2  0x00007ffff54f1bb5 in substitute_filter (f=0x7fffdc008bd0, bb=0x7fffdc00a0d0) at mod_substitute.c:511
#3  0x00007ffff56f6194 in filter_harness (f=0x7fffdc008bd0, bb=0x7fffdc00a0d0) at mod_filter.c:323
#4  0x00007ffff52e81f6 in sed_response_filter (f=0x7fffdc008ba8, bb=0x7fffdc009040) at mod_sed.c:376
#5  0x00007ffff56f6194 in filter_harness (f=0x7fffdc008ba8, bb=0x7fffdc009040) at mod_filter.c:323
#6  0x00007ffff38a9e6f in sandwich_filter_handler (filter=0x7fffdc008b80, bb=0x7fffdc009040) at mod_sandwich.c:165
#7  0x00007ffff56f6194 in filter_harness (f=0x7fffdc008b80, bb=0x7fffdc009040) at mod_filter.c:323
#8  0x0000000000439f07 in default_handler (r=0x7fffdc002970) at core.c:4369
#9  0x000000000044a260 in ap_run_handler (r=0x7fffdc002970) at config.c:169
---Type <return> to continue, or q <return> to quit---
#10 0x000000000044a7a9 in ap_invoke_handler (r=r@entry=0x7fffdc002970) at config.c:433
#11 0x000000000045dd5a in ap_process_async_request (r=r@entry=0x7fffdc002970) at http_request.c:317
#12 0x000000000045ad70 in ap_process_http_async_connection (c=0x7fffec0372a0) at http_core.c:143
#13 ap_process_http_connection (c=0x7fffec0372a0) at http_core.c:228
#14 0x0000000000453580 in ap_run_process_connection (c=0x7fffec0372a0) at connection.c:41
#15 0x0000000000465d94 in process_socket (my_thread_num=24, my_child_num=0, cs=0x7fffec037218, sock=<optimized out>,
    p=<optimized out>, thd=<optimized out>) at event.c:1035
#16 worker_thread (thd=<optimized out>, dummy=<optimized out>) at event.c:1875
#17 0x00007ffff754e182 in start_thread (arg=0x7fffe27ec700) at pthread_create.c:312
#18 0x00007ffff727afbd in clone () at ../sysdeps/unix/sysv/linux/x86_64/clone.S:111
ENTERING coffee_filter
Processing EOS bucket.
        ==> ap_pass_brigade returned 0
LEAVING coffee_filter

4 つのフィルターが、httpd.conf に設定した通りの順で呼ばれていることが分かります。行番号を見ると、フィルターから ap_pass_brigade を呼び出すことで filter_harness が呼ばれ、次のフィルターのコールバック関数に繋がることが分かります。

次にフィルターの順番を入れ替えます。mod_coffee を mod_sandwich の直後にしました。

AddOutputFilterByType SANDWICH text/html
Sandwich_Header "conf/header"
Sandwich_Footer "conf/footer"

AddOutputFilterByType COFFEE text/html
Coffee_LogOption 1

AddOutputFilterByType Sed text/html
OutputSed "s/Welcome/WELCOME/g"

AddOutputFilterByType SUBSTITUTE text/html
Substitute "s/h3>/h2>/"

ブラウザーから見たときの出力結果は変わりませんが、mod_coffee によるコンソール出力がかなり長くなります。

ENTERING coffee_filter
Processing data bucket (len=47)
        ==> ap_pass_brigade returned 0
Processing data bucket (len=0)
        ==> ap_pass_brigade returned 0
Processing data bucket (len=4)
        ==> ap_pass_brigade returned 0
Processing data bucket (len=8)
        ==> ap_pass_brigade returned 0
Processing data bucket (len=6)
        ==> ap_pass_brigade returned 0
Processing data bucket (len=4)
        ==> ap_pass_brigade returned 0
Processing data bucket (len=4)
        ==> ap_pass_brigade returned 0
Processing data bucket (len=1)
        ==> ap_pass_brigade returned 0
Processing data bucket (len=4)
        ==> ap_pass_brigade returned 0
Processing data bucket (len=4)
        ==> ap_pass_brigade returned 0
(..snip..)
Processing data bucket (len=0)
        ==> ap_pass_brigade returned 0
Processing data bucket (len=4)
        ==> ap_pass_brigade returned 0
Processing data bucket (len=5)
        ==> ap_pass_brigade returned 0
Processing data bucket (len=4)
        ==> ap_pass_brigade returned 0
Processing data bucket (len=0)
        ==> ap_pass_brigade returned 0
Processing data bucket (len=4)
        ==> ap_pass_brigade returned 0
Processing data bucket (len=5)
        ==> ap_pass_brigade returned 0
Processing data bucket (len=4)
        ==> ap_pass_brigade returned 0
Processing data bucket (len=2)
        ==> ap_pass_brigade returned 0
Processing data bucket (len=22)
        ==> ap_pass_brigade returned 0
Processing EOS bucket.
        ==> ap_pass_brigade returned 0
LEAVING coffee_filter

出力が長くなる理由は、mod_sandwich が特殊文字をエスケープするたびにバケツを分割しているためです。例えば上記のうち、len=4 となっているバケツは &gt; か &lt; という 4 文字の文字列と考えられます。しかし、先ほどと違って coffee_filter は一度しか呼ばれません。mod_sed か mod_substitute のどちらかが、細々に分割されたバケツをある程度繋げたのだと判断できます。

広告

Apache Live-Debugging on Linux

数ヶ月前、初めて仕事で必要になって、オープンソースのソフトをちょこちょこっと改変してビルドして使うという機会がありました。Windows ばっかり使っているとだめですね。弄ったのは openssl のコードで、それを元に Apache の mod_ssl を動かしたのですが、そのときは Apache のデバッグ方法がよく分からず、勉強に費やす時間もなかったので printf を書きまくる力技で何とか目的の動作を実現させました。が、複雑なことをやろうとすると、デバッガーを繋いで変数を見たり、コールスタックを見たりする必要が出てきます。後日、その方法を一通り覚えたのでまとめておこうと思います。

自作のモジュールをデバッグするだけなら、apache 本体は apt-get したものを使えば一応事足ります。しかし多くの場合は apache 内部の変数や関数を見たくなる時もありそうなので、apache 本体のデバッグ情報も得るため、全部ソースからビルドしておきます。自分用のメモも兼ねてその手順から。

Linux 初心者としてけっこう困るのが、ディレクトリーの使い分け方。複数の流派があって、どこに何を置けばいいのか決められない。最近は統一しようという動きがあるみたいですが。プログラムをソースからビルドしてインストールする際、以下のブログに書かれているルールが明確でいい感じだったので、真似させてもらうことにしました。

Apache 2.4をソースからインストール – Starlight
http://d.hatena.ne.jp/Kazuhira/20121117/1353159552

/usr/local/[ソフトウェア名]/[ソフトウェア名-Version]
/usr/local/[ソフトウェア名]/current  # 現在使用中のバージョンへのシンボリックリンク

環境はこちら。Hyper-V 上の仮想マシンですが、だからといって特別なことはありません。

  • Ubuntu Server 14.04 LTS x64
  • Linux Kernel 3.13.0-24-generic
  • gcc (Ubuntu 4.8.2-19ubuntu1) 4.8.2
  • GNU gdb (Ubuntu 7.7.1-0ubuntu5~14.04.2) 7.7.1

gcc と gdb を apt-get するコマンドはこれ。

$ sudo apt-get install build-essential libtool manpages-dev gdb

将来使いそうなので、OpenSSL もビルドしておきます。依存関係を考慮すると、必要なモジュールは以下の通り。

名前 URL バージョン
(2014/12 での最新)
依存関係
OpenSSL http://www.openssl.org/source/ 1.0.1j なし
APR (=Apache Portal Runtime) http://apr.apache.org/download.cgi 1.5.1 なし
APR-util http://apr.apache.org/download.cgi 1.5.4 APR
PCRE
(= Perl Compatible Regular Expressions)
http://pcre.org/ 8.36 なし
Apache http://httpd.apache.org/download.cgi 2.4.10 APR
APR-util
PCRE
OpenSSL

 

ソースからビルドすると言っても、実はとても簡単で、configure で Makefile を作って、make でビルドして、sudo make install でインストールするだけです。どうやらこれを "configure-make-make install dance" というようです。configure を実行するところで、インストール ディレクトリや、コンパイル オプション、リンカ オプションなどをいろいろ指定できます。

冒頭で決めたルールに従って、実行したコマンドを載せておきます。なお、ソースは全部 /usr/src にダウンロードして、そのまま同じところに解凍しました。

まずは OpenSSL から。configure のときに -fPIC オプションをつけないと、Apache のビルドの時にエラーが出るので注意。

$ wget http://www.openssl.org/source/openssl-1.0.1j.tar.gz
$ tar -zxvf openssl-1.0.1j.tar.gz
$ cd openssl-1.0.1j/
$ ./config –prefix=/usr/local/openssl/openssl-1.0.1j -fPIC
$ make
$ sudo make install
$ sudo ln -s /usr/local/openssl/openssl-1.0.1j /usr/local/openssl/current

Ubuntu 12.04 には、もともと OpenSSL 1.0.1f が /usr/bin に入っているので、ビルドした 1.0.1j へのシンボリックリンクで置き換えておきます。

$ openssl version
OpenSSL 1.0.1f 6 Jan 2014

$ sudo mv /usr/bin/openssl /usr/bin/openssl-1.0.1f
$ sudo ln /usr/local/openssl/current/bin/openssl /usr/bin/openssl

john@glycine:/usr/src/openssl-1.0.1j$ openssl version
OpenSSL 1.0.1j 15 Oct 2014

次、APR。

$ wget http://apache.mesi.com.ar/apr/apr-1.5.1.tar.gz
$ tar -zxvf apr-1.5.1.tar.gz
$ cd apr-1.5.1/
$ ./configure –prefix=/usr/local/apr/apr-1.5.1
$ make
$ sudo make install
$ sudo ln -s /usr/local/apr/apr-1.5.1 /usr/local/apr/current

次は APR-util。APR のディレクトリを指定するときに、シンボリックリンクの current を使っています。

$ wget http://apache.claz.org/apr/apr-util-1.5.4.tar.gz
$ tar -zxvf apr-util-1.5.4.tar.gz
$ cd apr-util-1.5.4/
$ ./configure –prefix=/usr/local/apr-util/apr-util-1.5.4 \
> –with-apr=/usr/local/apr/current
$ make
$ sudo make install
$ sudo ln -s /usr/local/apr-util/apr-util-1.5.4 /usr/local/apr-util/current

次は PCRE。

$ wget ftp://ftp.csx.cam.ac.uk/pub/software/programming/pcre/pcre-8.36.tar.gz
$ tar -zxvf pcre-8.36.tar.gz
$ cd pcre-8.36/
$ ./configure –prefix=/usr/local/pcre/pcre-8.36
$ make
$ sudo make install
$ sudo ln -s /usr/local/pcre/pcre-8.36 /usr/local/pcre/current

ここまででハマるポイントはありませんでした。ではいよいよ Apache のビルドです。これまでに作ったモジュールのパスを全部指定します。また、mod_so、mod_ssl、mod_rewrite をデフォルトで有効にしておきます。何となくです。

$ wget http://apache.mesi.com.ar/httpd/httpd-2.4.10.tar.gz
$ tar -zxvf httpd-2.4.10.tar.gz
$ cd httpd-2.4.10/
$ ./configure –prefix=/usr/local/apache-httpd/httpd-2.4.10 \
> –with-apr=/usr/local/apr/current \
> –with-apr-util=/usr/local/apr-util/current \
> –with-pcre=/usr/local/pcre/current \
> –with-ssl=/usr/local/openssl/current \
> –enable-so \
> –enable-ssl \
> –enable-rewrite
$ make 
$ sudo make install
$ sudo ln -s /usr/local/apache-httpd/httpd-2.4.10 /usr/local/apache-httpd/current

$ sudo /usr/local/apache-httpd/current/bin/apachectl -V
Server version: Apache/2.4.10 (Unix)
Server built:   Dec  3 2014 23:31:13
Server’s Module Magic Number: 20120211:36
Server loaded:  APR 1.5.1, APR-UTIL 1.5.4
Compiled using: APR 1.5.1, APR-UTIL 1.5.4
Architecture:   64-bit
Server MPM:     event
  threaded:     yes (fixed thread count)
    forked:     yes (variable process count)
Server compiled with….
-D APR_HAS_SENDFILE
-D APR_HAS_MMAP
-D APR_HAVE_IPV6 (IPv4-mapped addresses enabled)
-D APR_USE_SYSVSEM_SERIALIZE
-D APR_USE_PTHREAD_SERIALIZE
-D SINGLE_LISTEN_UNSERIALIZED_ACCEPT
-D APR_HAS_OTHER_CHILD
-D AP_HAVE_RELIABLE_PIPED_LOGS
-D DYNAMIC_MODULE_LIMIT=256
-D HTTPD_ROOT="/usr/local/apache-httpd/httpd-2.4.10"
-D SUEXEC_BIN="/usr/local/apache-httpd/httpd-2.4.10/bin/suexec"
-D DEFAULT_PIDLOG="logs/httpd.pid"
-D DEFAULT_SCOREBOARD="logs/apache_runtime_status"
-D DEFAULT_ERRORLOG="logs/error_log"
-D AP_TYPES_CONFIG_FILE="conf/mime.types"
-D SERVER_CONFIG_FILE="conf/httpd.conf"

OpenSSL のところで触れましたが、OpenSSL は -fPIC オプションをつけてコンパイルしないといけません。PIC とは、Position Independent Code の略です。

Position-independent code – Wikipedia, the free encyclopedia
http://en.wikipedia.org/wiki/Position-independent_code

もし、PIC なしで OpenSSL をコンパイルすると、Apache のビルド時に以下のエラーが出ます。この場合、OpenSSL を make clean して configure から実行し直せば問題なしです。

/usr/bin/ld: /usr/local/openssl/current/lib/libssl.a(s3_srvr.o): relocation R_X86_64_32 against `.rodata’ can not be used when making a shared object; recompile with -fPIC
/usr/local/openssl/current/lib/libssl.a: error adding symbols: Bad value
collect2: error: ld returned 1 exit status

無事に Apache がビルドできたら、念のため httpd.conf のバックアップを取り、とりあえず起動してみます。起動は bin に入っている apachectl というスクリプトを使うと楽です。実行可能ファイルの実体は httpd というファイルです。AH00558 という警告は、httpd.conf の ServerName オプションに適当な名前を指定すれば発生しなくなります。

john@glycine:~$ sudo /usr/local/apache-httpd/current/bin/apachectl start
AH00558: httpd: Could not reliably determine the server’s fully qualified domain name, using 127.0.1.1. Set the ‘ServerName’ directive globally to suppress this message
john@glycine:~$ ps -ef | grep
Usage: grep [OPTION]… PATTERN [FILE]…
Try ‘grep –help’ for more information.
john@glycine:~$ ps -ef | grep apache
root      1195     1  0 22:16 ?        00:00:00 /usr/sbin/apache2 -k start
www-data  1198  1195  0 22:16 ?        00:00:01 /usr/sbin/apache2 -k start
www-data  1199  1195  0 22:16 ?        00:00:01 /usr/sbin/apache2 -k start
root     10953     1  0 23:43 ?        00:00:00 /usr/local/apache-httpd/httpd-2.4.10/bin/httpd -k start
daemon   10954 10953  0 23:43 ?        00:00:00 /usr/local/apache-httpd/httpd-2.4.10/bin/httpd -k start
daemon   10955 10953  0 23:43 ?        00:00:00 /usr/local/apache-httpd/httpd-2.4.10/bin/httpd -k start
daemon   10956 10953  0 23:43 ?        00:00:00 /usr/local/apache-httpd/httpd-2.4.10/bin/httpd -k start
john     11041  1431  0 23:43 pts/1    00:00:00 grep –color=auto apache
john@glycine:~$

ブラウザで見ると、"It works!" というテキストのみのページが表示されます。これは htdocs にある index.html です。

image

 

これで Apache が起動できるようになりました。次に、モジュールをデバッグする手順です。どのモジュールでもいいのですが、後でやろうとしていることの都合上、ここではフィルターの一つであるmod_sed モジュールをデバッグします。なぜかヘルプのページが Apache 2.5 になっている・・。

mod_sed – Apache HTTP Server Version 2.5
http://httpd.apache.org/docs/trunk/mod/mod_sed.html

httpd.conf に mod_sed の設定を追加します。まず、元のファイルに以下の行がコメントアウトされているはずなので、行の先頭の # を消します。

LoadModule sed_module modules/mod_sed.so

次に、適当な Directory セクションを追加するなり、元からあるものを選ぶなりして、以下の 2 行 (青字) を追加します。

<Directory "/usr/local/apache-httpd/httpd-2.4.10/htdocs">
(snip)

    #
    # Controls who can get stuff from this server.
    #
    Require all granted

    AddOutputFilter Sed html
    OutputSed "s/HERE/Hello/g"

</Directory>

最後に、htdocs/index.html を適当に書き換えます。

<!DOCTYPE html>
<html>
<head>
</head>
<body>
<h3>HERE</h3>
<p>This is a body text.</p>
</body>
</html>

mod_sed が正常に動作するかどうかを確認するため、httpd を再起動してブラウザーで開きます。

john@glycine:~$ sudo /usr/local/apache-httpd/current/bin/apachectl -k stop
john@glycine:~$ sudo /usr/local/apache-httpd/current/bin/apachectl -k start
john@glycine:~$ ps -ef | grep apache
john      2357  1490  0 23:37 pts/1    00:00:00 vi /usr/local/apache-httpd/current/conf/httpd.conf
root      2491     1  0 23:43 ?        00:00:00 /usr/local/apache-httpd/httpd-2.4.10/bin/httpd -k start
daemon    2492  2491  0 23:43 ?        00:00:00 /usr/local/apache-httpd/httpd-2.4.10/bin/httpd -k start
daemon    2493  2491  0 23:43 ?        00:00:00 /usr/local/apache-httpd/httpd-2.4.10/bin/httpd -k start
daemon    2494  2491  0 23:43 ?        00:00:00 /usr/local/apache-httpd/httpd-2.4.10/bin/httpd -k start
john      2577  1406  0 23:43 pts/0    00:00:00 grep –color=auto apache
john@glycine:~$

見出しの HERE が mod_sed によって Hello に置換されているのが分かります。

image

さて、いよいよ httpd に gdb をアタッチするわけですが、上記 ps -ef コマンドを見る限り、httpd のプロセスが複数起動されています。詳しくは知りませんが、並列処理のためにプロセスをフォークして複数プロセスで要求を処理するのでしょう。これだと、要求が来たときにどのプロセスで処理されるのかが分からないので、不可能ではありませんが、デバッグが面倒くさくなりそうです。Apache には、デバッグ用のためのシングル プロセス モードが用意されているのでそれを使います。httpd の実行時に -X オプションをつければデバッグ モードになります。

Apache を gdb でデバッグするときの公式のガイドが ↓ にあります。

Apache HTTPD Debugging Guide – The Apache HTTP Server Project
http://httpd.apache.org/dev/debugging.html

では実際にやってみます。

john@glycine:~$ sudo gdb /usr/local/apache-httpd/current/bin/httpd
GNU gdb (Ubuntu 7.7.1-0ubuntu5~14.04.2) 7.7.1
Copyright (C) 2014 Free Software Foundation, Inc.
License GPLv3+: GNU GPL version 3 or later <
http://gnu.org/licenses/gpl.html>
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law.  Type "show copying"
and "show warranty" for details.
This GDB was configured as "x86_64-linux-gnu".
Type "show configuration" for configuration details.
For bug reporting instructions, please see:
<
http://www.gnu.org/software/gdb/bugs/>.
Find the GDB manual and other documentation resources online at:
<
http://www.gnu.org/software/gdb/documentation/>.
For help, type "help".
Type "apropos word" to search for commands related to "word"…
Reading symbols from /usr/local/apache-httpd/current/bin/httpd…done.
(gdb) r -X
Starting program: /usr/local/apache-httpd/httpd-2.4.10/bin/httpd -X
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/lib/x86_64-linux-gnu/libthread_db.so.1".
[New Thread 0x7ffff3676700 (LWP 2776)]
[New Thread 0x7ffff2e75700 (LWP 2777)]
[New Thread 0x7ffff2674700 (LWP 2778)]
[New Thread 0x7ffff1e73700 (LWP 2779)]
[New Thread 0x7ffff1672700 (LWP 2780)]
[New Thread 0x7ffff0e71700 (LWP 2781)]
[New Thread 0x7fffebfff700 (LWP 2782)]
[New Thread 0x7fffeb7fe700 (LWP 2783)]
[New Thread 0x7fffeaffd700 (LWP 2784)]
[New Thread 0x7fffea7fc700 (LWP 2785)]
[New Thread 0x7fffe9ffb700 (LWP 2786)]
[New Thread 0x7fffe97fa700 (LWP 2787)]
[New Thread 0x7fffe8ff9700 (LWP 2788)]
[New Thread 0x7fffe87f8700 (LWP 2789)]
[New Thread 0x7fffe7ff7700 (LWP 2790)]
[New Thread 0x7fffe77f6700 (LWP 2791)]
[New Thread 0x7fffe6ff5700 (LWP 2792)]
[New Thread 0x7fffe67f4700 (LWP 2793)]
[New Thread 0x7fffe5ff3700 (LWP 2794)]
[New Thread 0x7fffe57f2700 (LWP 2795)]
[New Thread 0x7fffe4ff1700 (LWP 2796)]
[New Thread 0x7fffe47f0700 (LWP 2797)]
[New Thread 0x7fffe3fef700 (LWP 2798)]
[New Thread 0x7fffe37ee700 (LWP 2799)]
[New Thread 0x7fffe2fed700 (LWP 2800)]
[New Thread 0x7fffe27ec700 (LWP 2801)]
[New Thread 0x7fffe1feb700 (LWP 2802)]
[Thread 0x7ffff3676700 (LWP 2776) exited]

スレッドが大量に作られました。別のコンソール セッションからプロセスを確認すると、確かに httpd のインスタンスは 1 つだけです。

john@glycine:~$ ps -ef | grep httpd
root      2769  1406  0 23:58 pts/0    00:00:00 sudo gdb /usr/local/apache-httpd/current/bin/httpd
root      2770  2769  0 23:58 pts/0    00:00:00 gdb /usr/local/apache-httpd/current/bin/httpd
daemon    2772  2770  0 23:58 pts/0    00:00:00 /usr/local/apache-httpd/httpd-2.4.10/bin/httpd -X
john      2805  1490  0 23:59 pts/1    00:00:00 grep –color=auto httpd

ブラウザーからページを開けるので、サーバーとして問題はなさそうです。さて、ここからやりたいことは当然、ブレークさせてブレークポイントを設定することです。が、ここからの手順が若干トリッキー、というかあまり情報がありませんでした。

普通は、デバッガー上でキーボードから Ctrl-C なり Ctrl-Break を押してブレークさせます。実際にやるとこうなります。sed_response_filter は mod_sed 内の関数です。

^C <<<< デバッガー上で Ctrl-C
[Thread 0x7fffe37ee700 (LWP 2799) exited]
[Thread 0x7fffe2fed700 (LWP 2800) exited]

Program received signal SIGHUP, Hangup.
[Switching to Thread 0x7fffe1feb700 (LWP 2802)]
0x00007ffff727b653 in epoll_wait () at ../sysdeps/unix/syscall-template.S:81
81      ../sysdeps/unix/syscall-template.S: No such file or directory.
(gdb) i functions sed_response_filter
All functions matching regular expression "sed_response_filter":

File mod_sed.c:
static apr_status_t sed_response_filter(ap_filter_t *, apr_bucket_brigade *);
(gdb) break sed_response_filter
Breakpoint 1 at 0x7ffff54ec0a0: file mod_sed.c, line 269.
(gdb) c
Continuing.
[Thread 0x7ffff2674700 (LWP 2778) exited]
[Thread 0x7fffe1feb700 (LWP 2802) exited]
[Thread 0x7fffe4ff1700 (LWP 2796) exited]
[Thread 0x7fffe27ec700 (LWP 2801) exited]
[Thread 0x7fffe7ff7700 (LWP 2790) exited]
[Thread 0x7fffe3fef700 (LWP 2798) exited]
[Thread 0x7ffff0e71700 (LWP 2781) exited]
[Thread 0x7fffe47f0700 (LWP 2797) exited]
[Thread 0x7fffe9ffb700 (LWP 2786) exited]
[Thread 0x7fffe57f2700 (LWP 2795) exited]
[Thread 0x7fffe97fa700 (LWP 2787) exited]
[Thread 0x7fffe5ff3700 (LWP 2794) exited]
[Thread 0x7fffe6ff5700 (LWP 2792) exited]
[Thread 0x7fffe67f4700 (LWP 2793) exited]
[Thread 0x7ffff1672700 (LWP 2780) exited]
[Thread 0x7fffe77f6700 (LWP 2791) exited]
[Thread 0x7ffff2e75700 (LWP 2777) exited]
[Thread 0x7fffe87f8700 (LWP 2789) exited]
[Thread 0x7ffff1e73700 (LWP 2779) exited]
[Thread 0x7fffe8ff9700 (LWP 2788) exited]
[Thread 0x7fffeb7fe700 (LWP 2783) exited]
[Thread 0x7fffea7fc700 (LWP 2785) exited]
[Thread 0x7fffebfff700 (LWP 2782) exited]
[Thread 0x7fffeaffd700 (LWP 2784) exited]
[Inferior 1 (process 2772) exited normally]
(gdb) c
The program is not being run.
(gdb)

ブレークポイントは問題なくセットできましたが、continue した瞬間にスレッドが全部死にました。というか Ctrl-C のときに既にスレッド 2799 と 2800 が死んでいて、たぶんこれが原因です。この後、continue は不可能になってしまいました。run -X をもう一度実行して新たにプロセスを開始すればブレークポイントもそのまま有効になりますが、ブレークさせるたびにプロセスが死んでいてはデバッグになりません。

Linux に詳しくないので間違っているかもしれませんが、Ctrl-C を押すと SIGINT シグナルが送られて、Apache には独自のハンドラーがあって、ワーカー スレッドが SIGINT を処理するとスレッドが終了してしまうようになっているのかもしれません。(全くの勘です。ご指摘があれば嬉しいです。自分で調べろよって話ですが。)

(2015/1/2 追記)
Ctrl+C でスレッドが死ぬ現象は、Apache がマルチスレッド モードでビルドされている場合のため、Apache をビルドするときにマルチスレッドではないモデルを選んでおけば、ソースからビルドした Apache でも Ctrl+C によってブレークさせることができます。具体的には、Apache の configure を実行するときに "–with-mpm=prefork" オプションを追加してください。

Multi-Processing Modules (MPMs) – Apache HTTP Server Version 2.4
http://httpd.apache.org/docs/2.4/mpm.html

prefork – Apache HTTP Server Version 2.4
http://httpd.apache.org/docs/2.4/mod/prefork.html

マルチスレッド モデルのときの解決策ですが、SIGINT ではなく、SIGTRAP を kill コマンドで送信するという手がありました。

john@glycine:~$ ps -ef | grep httpd
root      2824  1406  0 00:13 pts/0    00:00:00 sudo gdb /usr/local/apache-httpd/current/bin/httpd
root      2825  2824  6 00:13 pts/0    00:00:00 gdb /usr/local/apache-httpd/current/bin/httpd
daemon    2827  2825  0 00:13 pts/0    00:00:00 /usr/local/apache-httpd/httpd-2.4.10/bin/httpd -X
john      2861  1490  0 00:13 pts/1    00:00:00 grep –color=auto httpd
john@glycine:~$ sudo kill -SIGTRAP 2827
[sudo] password for john:
no talloc stackframe at ../source3/param/loadparm.c:4864, leaking memory

kill コマンドを実行すると、デバッガーにブレークインしてくれるので、先ほどと同じように sed_response_filter にブレークポイントを設定し、 conitnue します。今度はスレッドが死なないはずです。sed_response_filter は、名前の通り HTTP レスポンスを返す時に呼ばれるので、ブラウザーでページを開くだけでブレークします。以下は、コールスタックや引数の f を表示させている例です。

[New Thread 0x7fffe3fef700 (LWP 2853)]
[New Thread 0x7fffe37ee700 (LWP 2854)]
[New Thread 0x7fffe2fed700 (LWP 2855)]
[New Thread 0x7fffe27ec700 (LWP 2856)]
[New Thread 0x7fffe1feb700 (LWP 2857)]
[Thread 0x7ffff3676700 (LWP 2831) exited]

Program received signal SIGTRAP, Trace/breakpoint trap. <<<< kill コマンドによる SIGTRAP
0x00007ffff75560d1 in do_sigwait (sig=0x7fffffffe2ac, set=<optimized out>)
    at ../nptl/sysdeps/unix/sysv/linux/../../../../../sysdeps/unix/sysv/linux/sigwait.c:60
60      ../nptl/sysdeps/unix/sysv/linux/../../../../../sysdeps/unix/sysv/linux/sigwait.c: No such file or directory.
(gdb) break sed_response_filter
Breakpoint 1 at 0x7ffff54ec0a0: file mod_sed.c, line 269.
(gdb) c
Continuing.
[Switching to Thread 0x7fffe27ec700 (LWP 2856)]

Breakpoint 1, sed_response_filter (f=0x7fffdc007c50, bb=0x7fffdc008e18) at mod_sed.c:269
269     {
(gdb) bt
#0  sed_response_filter (f=0x7fffdc007c50, bb=0x7fffdc008e18) at mod_sed.c:269
#1  0x0000000000439f07 in default_handler (r=0x7fffdc002970) at core.c:4369
#2  0x000000000044a260 in ap_run_handler (r=0x7fffdc002970) at config.c:169
#3  0x000000000044a7a9 in ap_invoke_handler (r=r@entry=0x7fffdc002970) at config.c:433
#4  0x000000000045dd5a in ap_process_async_request (r=r@entry=0x7fffdc002970) at http_request.c:317
#5  0x000000000045ad70 in ap_process_http_async_connection (c=0x7fffec0372a0) at http_core.c:143
#6  ap_process_http_connection (c=0x7fffec0372a0) at http_core.c:228
#7  0x0000000000453580 in ap_run_process_connection (c=0x7fffec0372a0) at connection.c:41
#8  0x0000000000465d94 in process_socket (my_thread_num=24, my_child_num=0, cs=0x7fffec037218, sock=<optimized out>,
    p=<optimized out>, thd=<optimized out>) at event.c:1035
#9  worker_thread (thd=<optimized out>, dummy=<optimized out>) at event.c:1875
#10 0x00007ffff754e182 in start_thread (arg=0x7fffe27ec700) at pthread_create.c:312
#11 0x00007ffff727afbd in clone () at ../sysdeps/unix/sysv/linux/x86_64/clone.S:111
(gdb) p *f->r
$8 = {pool = 0x7fffdc0028f8, connection = 0x7fffec0372a0, server = 0x6c4618, next = 0x0, prev = 0x0, main = 0x0,
  the_request = 0x7fffdc003b58 "GET /?q=aaa HTTP/1.1", assbackwards = 0, proxyreq = 0, header_only = 0,
  proto_num = 1001, protocol = 0x7fffdc003b98 "HTTP/1.1", hostname = 0x7fffdc003d88 "10.10.90.10",
  request_time = 1419322660195931, status_line = 0x0, status = 304, method_number = 0, method = 0x7fffdc003b70 "GET",
  allowed = 0, allowed_xmethods = 0x0, allowed_methods = 0x7fffdc002c28, sent_bodyct = 0, bytes_sent = 0,
  mtime = 1419313062006926, range = 0x0, clength = 104, chunked = 0, read_body = 0, read_chunked = 0,
  expecting_100 = 0, kept_body = 0x0, body_table = 0x0, remaining = 0, read_length = 0, headers_in = 0x7fffdc002c68,
  headers_out = 0x7fffdc004650, err_headers_out = 0x7fffdc004778, subprocess_env = 0x7fffdc008928,
  notes = 0x7fffdc004528, content_type = 0x6f23e0 "text/html", handler = 0x6f23e0 "text/html", content_encoding = 0x0,
  content_languages = 0x0, vlist_validator = 0x0, user = 0x0, ap_auth_type = 0x0,
  unparsed_uri = 0x7fffdc003b80 "/?q=aaa", uri = 0x7fffdc0079f8 "/index.html",
  filename = 0x7fffdc007a50 "/usr/local/apache-httpd/httpd-2.4.10/htdocs/index.html",
  canonical_filename = 0x7fffdc007a10 "/usr/local/apache-httpd/httpd-2.4.10/htdocs/index.html", path_info = 0x0,
  args = 0x7fffdc007a08 "q=aaa", used_path_info = 2, eos_sent = 1, per_dir_config = 0x7fffdc004108,
  request_config = 0x7fffdc0038f0, log = 0x6c4638, log_id = 0x0, htaccess = 0x0, output_filters = 0x7fffdc007c50,
  input_filters = 0x7fffdc003da8, proto_output_filters = 0x7fffdc003a58, proto_input_filters = 0x7fffdc003da8,
  no_cache = 0, no_local_copy = 0, invoke_mtx = 0x7fffdc003dd0, parsed_uri = {scheme = 0x0, hostinfo = 0x0,
    user = 0x0, password = 0x0, hostname = 0x0, port_str = 0x7fffdc003da0 "50000", path = 0x7fffdc003b88 "/",
    query = 0x7fffdc003b90 "q=aaa", fragment = 0x0, hostent = 0x0, port = 50000, is_initialized = 1,
    dns_looked_up = 0, dns_resolved = 0}, finfo = {pool = 0x7fffdc006918, valid = 7598960, protection = 1604,
    filetype = APR_REG, user = 1000, group = 1000, inode = 2768083, device = 2049, nlink = 1, size = 104,
    csize = 4096, atime = 1419313378554926, mtime = 1419313062006926, ctime = 1419313062086926,
    fname = 0x7fffdc007a50 "/usr/local/apache-httpd/httpd-2.4.10/htdocs/index.html", name = 0x0, filehand = 0x0},
  useragent_addr = 0x7fffec037120, useragent_ip = 0x7fffec037600 "10.10.1.252"}

これで Linux 上のサービス (Linux 的にはデーモンか) のデバッグも余裕・・・!

事前に httpd の PID を取得しなくても、killall という素敵なコマンドがありました。これで一発。

$ sudo killall -SIGTRAP httpd

Accessing IE’s DOM structure from C#

IE のレンダリング エンジンと言えば Trident と呼ばれる mshtml.dll です。なんと単一の DLL でサイズが 20MB もあります。この Trident、COM 経由でデータにアクセスするための API を実装しているらしい、ということで試してみました。検索すると、わりといろいろな人が試していてメジャーな方法らしい。ウィキにも書いてありました。

Trident (layout engine) – Wikipedia, the free encyclopedia
http://en.wikipedia.org/wiki/Trident_(layout_engine)

基本となる元ネタはこの KB。今回は Win32 ではなく C# で書き直すことにします。

How to get IHTMLDocument2 from a HWND
http://support.microsoft.com/kb/249232/en

上記 KB のプログラムをざっと見ると、ウィンドウ ハンドルに対して WM_HTML_GETOBJECT というウィンドウ メッセージを送ると、COM インターフェースである IHTMLDocument2 が返ってくるらしい。ウィンドウ ハンドルは普通の方法で列挙して、クラス名が "Internet Explorer_Server" であるものを探せばいいようだ。簡単でいい感じ。

IHTMLDocument2 インターフェースのリファレンスは MSDN も載っています。最新版だと IHTMLDocument8 まであるらしい。

Scripting Object Interfaces (MSHTML) (Windows)
http://msdn.microsoft.com/en-us/library/hh801967(v=vs.85).aspx

今回の開発環境はこれで。プログラムは Windows 8 や 8.1 でも動くはずです。さすがに試していませんが、.NET Framework 4.0 を入れれば XP や Vista でも行けるはず・・。

  • OS: Windows 7 SP1 x64
  • IE: Internet Explorer 11 + KB3003057 (Nov. 2014 Update)
  • IDE: Visual Studio 2012
  • CLI: .NET Framework 4.0

適当な C# プロジェクトを作って、リファレンスを追加します。が、ここで一つ罠が。Visual Studio の Reference Manager を見ると、Microsoft HTML Object Library という Type Library が既定で存在しています。これは既に mshtml のライブラリが GAC に登録されているからであり、おそらく .NET か Windows のインストール時に追加されたものと考えられます。

image

手元の環境だと、"C:\Windows\assembly\GAC\Microsoft.mshtml\7.0.3300.0__b03f5f7f11d50a3a" に存在する Microsoft.mshtml.dll がそのライブラリでした。

image

このライブラリを使っても問題はないのですが、IE のアップデート時や累積パッチの適用時に GAC は更新されないらしく、古いのです。開発環境の Windows 7 において Object Explorer から確認すると分かりますが、例えば IHTMLDocument5 インターフェースまでしか存在せず、 IHTMLDocument6 以降がありません。

image

便利なことに、IE 更新時には適用される mshtml.dll  に対応する Type Library ファイル mshtml.tlb も一緒に提供されるので、このファイルからアセンブリ DLL を作れば最新のライブラリを使うことができます。TLB から DLL を作る方法は、以前に紹介した tlbimp というツールが使えます。"Developer Command Prompt for VS2012" を開いてコマンドを実行するだけです。

出力例はこんな感じ。ファイル名は mshtml.ie11.dll にしていますが、何でもよいです。system32 にある mshtml.tlb から DLL を作りましたが、生成された DLL  は 32bit からでも 64bit からでも使うことができます。

E:\VSDev\Projects\iehack> tlbimp /reference:C:\windows\system32\mshtml.tlb /out:mshtml.ie11.dll
Microsoft (R) .NET Framework Type Library to Assembly Converter 4.0.30319.17929
Copyright (C) Microsoft Corporation.  All rights reserved.

TlbImp : error TI2005 : No input file has been specified.

E:\VSDev\Projects\iehack>tlbimp C:\windows\system32\mshtml.tlb /out:mshtml.ie11.dll
Microsoft (R) .NET Framework Type Library to Assembly Converter 4.0.30319.17929
Copyright (C) Microsoft Corporation.  All rights reserved.

TlbImp : warning TI3001 : Primary interop assembly ‘Microsoft.mshtml, Version=7.0.3300.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a’ is already registered for type library ‘C:\windows\system32\mshtml.tlb’.
TlbImp : warning TI3015 : At least one of the arguments for ‘mshtml.ie11.IActiveIMMApp.GetDefaultIMEWnd’ cannot be marshaled by the runtime marshaler.  Such arguments will therefore be passed as a pointer and may require unsafe code to manipulate.
TlbImp : warning TI3016 : The type library importer could not convert the signature for the member ‘mshtml.ie11._userBITMAP.pBuffer’.
TlbImp : warning TI3016 : The type library importer could not convert the signature for the member ‘mshtml.ie11._FLAGGED_BYTE_BLOB.abData’.
TlbImp : warning TI3015 : At least one of the arguments for ‘mshtml.ie11.IEventTarget2.GetRegisteredEventTypes’ cannot be marshaled by the runtime marshaler.  Such arguments will therefore be passed as a pointer and may require unsafe code to manipulate.
TlbImp : warning TI3015 : At least one of the arguments for ‘mshtml.ie11.IEventTarget2.GetListenersForType’ cannot be marshaled by the runtime marshaler.  Such arguments will therefore be passed as a pointer and may require unsafe code to manipulate.
TlbImp : warning TI3016 : The type library importer could not convert the signature for the member ‘mshtml.ie11.tagSAFEARRAY.rgsabound’.
TlbImp : warning TI3015 : At least one of the arguments for ‘mshtml.ie11.ICanvasPixelArrayData.GetBufferPointer’ cannot be marshaled by the runtime marshaler.  Such arguments will therefore be passed as a pointer and may require unsafe code to manipulate.
TlbImp : Type library imported to E:\VSDev\Projects\iehack\mshtml.ie11.dll

 

警告がたくさん出ましたが、よく分からないので (汗) 無視します。

生成された mshtml.ie11.dll をプロジェクトに追加して Object Explorer で見てみると、今度は IHTMLDocument8 がありました。それにしてもクラスやらインターフェースが多いです。アセンブリだけで 15MB 近いサイズになります。

image

あとは素直に C# を書くだけです。

//
// Program.cs
//
// http://support.microsoft.com/kb/249232/en
//

using System;
using System.Collections.Generic;
using System.Globalization;
using System.Runtime.InteropServices;
using System.Text;
using System.Threading.Tasks;

namespace iehack {
    class Program {
        public delegate int WNDENUMPROC(IntPtr hwnd, IntPtr lParam);

        [DllImport("user32.dll", SetLastError = true, CharSet = CharSet.Auto)]
        static extern int GetClassName(IntPtr hWnd, StringBuilder lpClassName, int nMaxCount);

        [DllImport("user32.dll", SetLastError = true)]
        static extern int EnumChildWindows(IntPtr hwndParent, WNDENUMPROC lpEnumFunc, IntPtr lParam);

        [DllImport("user32.dll", SetLastError = true)]
        static extern uint GetWindowThreadProcessId(IntPtr hWnd, out uint lpdwProcessId);

        [DllImport("user32.dll", SetLastError = true, CharSet = CharSet.Auto)]
        static extern uint RegisterWindowMessage(string lpString);

        [Flags]
        public enum SendMessageTimeoutFlags : uint {
            SMTO_NORMAL = 0x0,
            SMTO_BLOCK = 0x1,
            SMTO_ABORTIFHUNG = 0x2,
            SMTO_NOTIMEOUTIFNOTHUNG = 0x8,
            SMTO_ERRORONEXIT = 0x20
        }

        [DllImport("user32.dll", SetLastError = true, CharSet = CharSet.Auto)]
        public static extern IntPtr SendMessageTimeout(
            IntPtr hWnd,
            uint Msg,
            IntPtr wParam,
            IntPtr lParam,
            SendMessageTimeoutFlags fuFlags,
            uint uTimeout,
            out IntPtr lpdwResult);

        [DllImport("oleacc.dll", PreserveSig = false)]
        [return: MarshalAs(UnmanagedType.Interface)]
        static extern object ObjectFromLresult(IntPtr lResult,
             [MarshalAs(UnmanagedType.LPStruct)] Guid refiid, IntPtr wParam);

        private static int EnumWindowsProc(IntPtr hwnd, IntPtr lParam) {
            // http://msdn.microsoft.com/en-us/library/windows/desktop/ms633576(v=vs.85).aspx
            // The maximum length for lpszClassName is 256.

            StringBuilder ClassName = new StringBuilder(256);
            int Ret = GetClassName(hwnd, ClassName, ClassName.Capacity);
            if (Ret != 0) {
                if (string.Compare(ClassName.ToString(), "Internet Explorer_Server", true, CultureInfo.InvariantCulture) == 0) {
                    var TargetList = GCHandle.FromIntPtr(lParam).Target as List<IntPtr>;
                    if (TargetList != null) {
                        TargetList.Add(hwnd);
                    }
                }
            }

            return 1;
        }

        private static int EnumTopWindowsProc(IntPtr hwnd, IntPtr lParam) {
            EnumChildWindows(hwnd, EnumWindowsProc, lParam);
            return 1;
        }

        static uint WM_HTML_GETOBJECT = 0;

        public static object GetDom(IntPtr Window, Guid InterfaceType) {
            const int Timeout = 1000;

            if (WM_HTML_GETOBJECT == 0) {
                WM_HTML_GETOBJECT = RegisterWindowMessage("WM_HTML_GETOBJECT");
            }

            IntPtr Result = IntPtr.Zero;
            SendMessageTimeout(Window, WM_HTML_GETOBJECT,
                IntPtr.Zero, IntPtr.Zero,
                SendMessageTimeoutFlags.SMTO_ABORTIFHUNG,
                Timeout,
                out Result);

            return ObjectFromLresult(Result, InterfaceType, IntPtr.Zero);
        }

        List<IntPtr> WindowHandles;

        public void Run() {
            WindowHandles = new List<IntPtr>();
            var ListHandle = GCHandle.Alloc(WindowHandles);
            EnumChildWindows(IntPtr.Zero, EnumTopWindowsProc, GCHandle.ToIntPtr(ListHandle));

            int i = 0;
            foreach (var ie in WindowHandles) {
                uint pid, tid;
                tid = GetWindowThreadProcessId(ie, out pid);

                Console.WriteLine("[{0}] hWnd = 0x{1:x}, pid = 0x{2:x4}, tid = 0x{3:x4}", i++, ie.ToInt64(), pid, tid);

                var dom2 = GetDom(ie, typeof(mshtml.ie11.IHTMLDocument2).GUID) as mshtml.ie11.IHTMLDocument2;
                if (dom2 != null) {
                    Console.WriteLine("url = " + dom2.url);

                    var dom6 = GetDom(ie, typeof(mshtml.ie11.IHTMLDocument6).GUID) as mshtml.ie11.IHTMLDocument6;
                    if (dom6 != null) {
                        Console.WriteLine("docmode = " + dom6.documentMode);
                    }
                }
            }
        }

        static void Main(string[] args) {
            var p = new Program();
            p.Run();
        }
    }
}

やろうと思えばもっと工夫できそうな気もしますが、データが取得できることが確認できればいいので最低限で。このサンプルを実行すると、ウィンドウ クラス名が "Internet Explorer_Server" であるウィンドウ全てに対して、そのコントロールが開いている URL と DocMode を表示します。

DocMode とは、IE8 から導入された機能です。主に HTML レイアウトの後方互換性を維持するために、古いバージョンの IE のレンダリング モードをある程度まで再現することができるようになっています。例えば IE8 向けに作られたサイトで、IE11 で開くとレイアウトが崩れてしまうような場合には、HTML 内の meta 要素で DocMode を明示的に 8 に指定して回避できることがあります。

Specifying legacy document modes (Internet Explorer)
http://msdn.microsoft.com/en-us/library/jj676915(v=vs.85).aspx

IE8/IE9の「ブラウザーモード」と「ドキュメントモード」のまとめ: 小粋空間
http://www.koikikukan.com/archives/2011/02/07-005555.php

手元の環境で実行した結果がこんな感じです。IE の各タブだけでなく、以下の例では Windows Live Writer で使われている Web コントロールの情報も表示されています。

E:\VSDev\Projects\iehack\bin\x64\Release>iehack.exe
[0] hWnd = 0x40914, pid = 0x1d94, tid = 0x0d6c
url =
https://www.google.com/?gws_rd=ssl
docmode = 11
[1] hWnd = 0x1505aa, pid = 0x1c58, tid = 0x2010
url =
https://www.facebook.com/
docmode = 10
[2] hWnd = 0x804e6, pid = 0x1c58, tid = 0x1094
url = about:blank
docmode = 11
[3] hWnd = 0x204d2, pid = 0x118c, tid = 0x0948
url = file://C:\Users\John\AppData\Local\Temp\WindowsLiveWriter1286139640\B8DD92B2061F\index.htm
docmode = 7

このサンプルでは試していませんが、KB249232 の例で背景色を変更しているように、値を取得するだけではなく、変更することもできます。しかし DocumentMode プロパティを始めとして、読み取り専用のプロパティもあります。

How Pointer-to-Member Function works

前回の記事で Detours のサンプル コードを Visual Studio 2010/2013 でビルドするとでコンパイル エラー C2440 になることを紹介しました。該当箇所は、member というサンプルの member.cpp に実装された main 関数です。

class CDetour /* add ": public CMember" to enable access to member variables... */ 

  public: 
    void Mine_Target(void); 
    static void (CDetour::* Real_Target)(void); 
 
    // Class shouldn't have any member variables or virtual functions. 
}; 
 
void (CDetour::* CDetour::Real_Target)(void) = (void (CDetour::*)(void))&CMember::Target; 
 
/* ----snip---- */ 
 
#if (_MSC_VER < 1310) 
    void (CMember::* pfTarget)(void) = CMember::Target; 
    void (CDetour::* pfMine)(void) = CDetour::Mine_Target; 
 
    Verify("CMember::Target", *(PBYTE*)&pfTarget); 
    Verify("*CDetour::Real_Target", *(PBYTE*)&CDetour::Real_Target); 
    Verify("CDetour::Mine_Target", *(PBYTE*)&pfMine); 
#else 
    Verify("CMember::Target", (PBYTE)(&(PBYTE&)CMember::Target)); 
      <<<< member.cpp(88) : error C2440: 'type cast' : cannot convert from 'void (__thiscall CMember::* )(void)' to 'PBYTE &' 
             Reason: cannot convert from 'overloaded-function' to 'PBYTE *' 
             There is no context in which this conversion is possible
 
 
    Verify("*CDetour::Real_Target", *(&(PBYTE&)CDetour::Real_Target)); 
 
    Verify("CDetour::Mine_Target", (PBYTE)(&(PBYTE&)CDetour::Mine_Target)); 
      <<<< member.cpp(90) : error C2440: 'type cast' : cannot convert from 'void (__thiscall CDetour::* )(void)' to 'PBYTE &' 
             Reason: cannot convert from 'overloaded-function' to 'PBYTE *' 
             There is no context in which this conversion is possible
 
#endif 

確か _MSC_VER = 1310 は、Visual Studio .NET 2003 のコンパイラーのバージョンだった気がします。2003、2005 あたりだとコンパイルが通るのでしょうか。メンバ関数であるCMember::Target や CDetour::Mine_Target をリテラルとしてポインターに変換して Verify に渡そうとしていますが、キャストできないというエラーです。エラーになっていない CDetour::Real_Target は、メンバ関数の名前ではなく、クラス内の static メンバ変数であり、値は &CMember::Target で初期化されています。この初期化のように、メンバ関数へのポインターは関数名の先頭に & を付けるという理解でしたが・・・。

このコードの意図は、メンバ関数へのポインターを汎用ポインターにキャストすることですが、そもそもそんなことできたっけ、ということで調べると MSDN のフォーラムで次のような議論を見つけました。この中で出てくる "p2 = &(void*&)A::memfun" という構文はまさに Detours で使われているものと同じです。というか回答者も Detours で見たことがあるとか言ってるし。

why casting member function pointer to void *& works?
http://social.msdn.microsoft.com/Forums/vstudio/en-US/11d7e717-f1c2-4909-857d-2346f5a11c7e/why-casting-member-function-pointer-to-void-works?forum=vclanguage

とりあえずフォーラムにあったプログラムに似たものを書いて試してみます。こんなコード。個人的な慣習でファイルを main.cpp とtest.cpp に分けていますが、一つのファイルにまとめても問題ありません。

// 
// main.cpp 
//
 
 
void RunTest(); 
 
int main(int argc, char **argv) { 
    RunTest(); 
    return 0; 

 
// 
// test.cpp 
//
 
 
#include <stdio.h> 
 
class ClassA { 
public: 
    void Func1() { 
        printf("+ClassA::Func1()\n"); 
    } 
}; 
 
void RunTest() { 
    void (ClassA::*p1)() = &ClassA::Func1; 
    void *p2 = (void*)p1; 
    void *p3 = (void*&)p1; 
    void *p4 = &(void*&)ClassA::Func1; 
 
    printf("size= %d\n", sizeof(void (ClassA::*)())); 
    printf("p1= %p, size= %d\n", p1, sizeof(p1)); 
    printf("p2= %p, size= %d\n", p2, sizeof(p2)); 
    printf("p3= %p, size= %d\n", p3, sizeof(p3)); 
    printf("p4= %p, size= %d\n", p4, sizeof(p4)); 
 
    ClassA a; 
    (a.*p1)(); 

Class::Func1 を 4 通りの方法で汎用ポインターにキャストするコードです。p4 への代入が Detours のサンプルと同じです。さて、これを Visual Studio 2013 でコンパイルしてみます。今回は Detours に倣って、Visual Studio を使わずに Makefile を作って nmake でビルドする方法をとります。

・・・。とかいって汎用的な Makefile を作るのに 1 時間以上かかるっていう・・・。何とかできたのがこれ。GNU Make と違って、タブ文字の代わりに半角スペースを使っても怒られません。この点は素晴らしい。ただし、wildcard とか使えないし、文法がけっこう違う。さすが MS。


# http://msdn.microsoft.com/en-us/library/x6bt6xe7.aspx&#160;
# http://keicode.com/winprimer/wp04-2.php&#160;
#
 
 
CC=cl 
LINKER=link 
RM=del /q 
 
TARGET=test.exe 
OUTDIR=.\bin 
OBJS=\ 
$(OUTDIR)\main.obj\ 
$(OUTDIR)\member.obj 
 
CFLAGS=\ 
/nologo\ 
/Zi\ 
/c\ 
/Fo"$(OUTDIR)\\"\ 
/Fd"$(OUTDIR)\\"\ 
/D_UNICODE\ 
/DUNICODE\ 
# /O2\ 
/W4 
 
LFLAGS=\ 
/NOLOGO\ 
/DEBUG\ 
/SUBSYSTEM:CONSOLE 
 
all: clean $(OUTDIR)\$(TARGET) 
 
clean: 
-@if not exist $(OUTDIR) md $(OUTDIR) 
@$(RM) /Q $(OUTDIR)\* 2>nul 
 
$(OUTDIR)\$(TARGET): $(OBJS) 
$(LINKER) $(LFLAGS) /PDB:"$(@R).pdb" /OUT:"$(OUTDIR)\$(TARGET)" $** 
 
.cpp{$(OUTDIR)}.obj: 
$(CC) $(CFLAGS) $< 

コード最適化は無効、警告レベルは 4 に留めています。-Wall にすると標準ヘッダーの stdio.h や Windows.h から大量の警告が出るので使えません。終わってますね。

メイクの仕方は GNU とほぼ同じで、ファイル名を Makefile にして、Visual Studio のプロンプトから nmake コマンドを実行するだけです。make ではなく nmake となることに注意して下さい。で、結果はこちら。

G:4_VSDev\Projects\box>nmake 
 
Microsoft (R) Program Maintenance Utility Version 12.00.21005.1 
Copyright (C) Microsoft Corporation.  All rights reserved. 
 
        cl  /nologo /Zi /c /Fo".\bin\\" /Fd".\bin\\" /D_UNICODE /DUNICODE       /W4 main.cpp 
main.cpp 
main.cpp(3) : warning C4100: 'argv' : unreferenced formal parameter 
main.cpp(3) : warning C4100: 'argc' : unreferenced formal parameter 
        cl  /nologo /Zi /c /Fo".\bin\\" /Fd".\bin\\" /D_UNICODE /DUNICODE       /W4 member.cpp 
member.cpp 
member.cpp(12) : error C2440: 'type cast' : cannot convert from 'void (__cdecl ClassA::* )(void)' to 'void *' 
        There is no context in which this conversion is possible 
member.cpp(14) : error C2440: 'type cast' : cannot convert from 'void (__cdecl ClassA::* )(void)' to 'void *&' 
        Reason: cannot convert from 'overloaded-function' to 'void **' 
        There is no context in which this conversion is possible
 
NMAKE : fatal error U1077: '"C:\Program Files (x86)\Microsoft Visual Studio 12.0\VC\BIN\amd64\cl.EXE"' : return code '0x2' 
Stop. 

Detours のサンプルと同じ C2440 エラーが p2 と p4 の代入のところで発生しました。ここで面白いのは、p3 の代入が通る点です。p2 との違いは、参照型の有無です。void* へはキャストできなくても、参照型の void*& にするとキャストが可能になるようです。

ということで、エラーになっていた p2 と p4 に関する処理はコメントにしてビルドし、実行結果を見るとこのようになります。

G:4_VSDev\Projects\box>nmake 
 
Microsoft (R) Program Maintenance Utility Version 12.00.21005.1 
Copyright (C) Microsoft Corporation.  All rights reserved. 
 
        cl  /nologo /Zi /c /Fo".\bin\\" /Fd".\bin\\" /D_UNICODE /DUNICODE       /W4 main.cpp 
main.cpp 
main.cpp(3) : warning C4100: 'argv' : unreferenced formal parameter 
main.cpp(3) : warning C4100: 'argc' : unreferenced formal parameter 
        cl  /nologo /Zi /c /Fo".\bin\\" /Fd".\bin\\" /D_UNICODE /DUNICODE       /W4 member.cpp 
member.cpp 
        link  /NOLOGO /DEBUG /SUBSYSTEM:CONSOLE /PDB:".\bin\test.pdb" /OUT:".\bin\test.exe" .\bin\main.obj .\bin\member.obj 
 
G:4_VSDev\Projects\box>bin\test.exe 
size= 8 
p1= 00007FF7F40D100A, size= 8 
p3= 00007FF7F40D100A, size= 8 
+ClassA::Func1() 

何の問題もなさそうです。念のためデバッガーを使って、どのようなコードが生成されたのかを確認します。

G:4_VSDev\Projects\box>E:\debuggers\pub.x64\cdb bin\test.exe 
 
Microsoft (R) Windows Debugger Version 6.3.9600.16384 AMD64 
Copyright (c) Microsoft Corporation. All rights reserved. 
 
CommandLine: bin\test.exe 
 
************* Symbol Path validation summary ************** 
Response                         Time (ms)     Location 
Deferred                                       cache*E:\symbols.pub 
Deferred                                       srv*http://msdl.microsoft.com/download/symbols 
Symbol search path is: cache*E:\symbols.pub;srv*http://msdl.microsoft.com/download/symbols 
Executable search path is: 
ModLoad: 00007ff7`f40d0000 00007ff7`f4106000   test.exe 
ModLoad: 00007ffe`850e0000 00007ffe`8528c000   ntdll.dll 
ModLoad: 00007ffe`84570000 00007ffe`846ae000   C:\WINDOWS\system32\KERNEL32.DLL 
ModLoad: 00007ffe`82360000 00007ffe`82475000   C:\WINDOWS\system32\KERNELBASE.dll 
(3668.366c): Break instruction exception - code 80000003 (first chance) 
ntdll!LdrpDoDebuggerBreak+0x30: 
00007ffe`851a1dd0 cc              int     3 
0:000> uf test!RunTest 
*** WARNING: Unable to verify checksum for test.exe 
test!RunTest: 
00007ff7`f40d1050 4883ec48        sub     rsp,48h 
00007ff7`f40d1054 488d05afffffff  lea     rax,[test!ILT+5(?Func1ClassAQEAAXXZ) (00007ff7`f40d100a)] 
00007ff7`f40d105b 4889442428      mov     qword ptr [rsp+28h],rax
 
00007ff7`f40d1060 488b442428      mov     rax,qword ptr [rsp+28h] 
00007ff7`f40d1065 4889442430      mov     qword ptr [rsp+30h],rax 
00007ff7`f40d106a ba08000000      mov     edx,8 
00007ff7`f40d106f 488d0da21c0200  lea     rcx,[test!__xt_z+0x148 (00007ff7`f40f2d18)] 
00007ff7`f40d1076 e849010000      call    test!printf (00007ff7`f40d11c4) 
00007ff7`f40d107b 41b808000000    mov     r8d,8 
00007ff7`f40d1081 488b542428      mov     rdx,qword ptr [rsp+28h] 
00007ff7`f40d1086 488d0d9b1c0200  lea     rcx,[test!__xt_z+0x158 (00007ff7`f40f2d28)] 
00007ff7`f40d108d e832010000      call    test!printf (00007ff7`f40d11c4) 
00007ff7`f40d1092 41b808000000    mov     r8d,8 
00007ff7`f40d1098 488b542430      mov     rdx,qword ptr [rsp+30h] 
00007ff7`f40d109d 488d0d9c1c0200  lea     rcx,[test!__xt_z+0x170 (00007ff7`f40f2d40)] 
00007ff7`f40d10a4 e81b010000      call    test!printf (00007ff7`f40d11c4) 
00007ff7`f40d10a9 488d4c2420      lea     rcx,[rsp+20h] 
00007ff7`f40d10ae ff542428        call    qword ptr [rsp+28h]
 
00007ff7`f40d10b2 4883c448        add     rsp,48h 
00007ff7`f40d10b6 c3              ret 
0:000> bp 00007ff7`f40d10ae 
0:000> g 
size= 8 
p1= 00007FF7F40D100A, size= 8 
p3= 00007FF7F40D100A, size= 8 
Breakpoint 0 hit 
test!RunTest+0x5e: 
00007ff7`f40d10ae ff542428        call    qword ptr [rsp+28h] ss:000000d0`199ffaa8={test!ILT+5(?Func1ClassAQEAAXXZ) (000 
07ff7`f40d100a)} 
0:000> t 
test!ILT+5(?Func1ClassAQEAAXXZ): 
00007ff7`f40d100a e9c1000000      jmp     test!ClassA::Func1 (00007ff7`f40d10d0) 
0:000> g 
+ClassA::Func1() 
ntdll!NtTerminateProcess+0xa: 
00007ffe`85170f0a c3              ret 
0:000> q 
quit: 

C++ 上での &ClassA::Func1 には win32c!ILT+5(?Func1ClassAQEAAXXZ) というシンボルが割り当てられており、lea 命令でローカル変数領域の rsp+28 に代入されています。シンボルが指す00007ff7`f40d100a という数値が printf で出力される値であり、ポインターそのものの値と言えそうです。

変数の p1 経由でメンバ関数を呼び出すコードは、rsp+28h に保存したアドレスを call するようになっています。ただし 00007ff7`f40d100a は、Func1 の先頭ではなく、jmp 命令があるだけです。win32c!ILT+5 というシンボル名から分かるように、ポインターに代入されたアドレスは、ルックアップテーブル (ILT = Import Lookup Table) のアドレスになっています。

他のコンパイラーも試してみることにします。まずは gcc (Ubuntu 4.8.2-19ubuntu1) 4.8.2。GNU Make 用の Makefile はこんな感じ。これも作るのにけっこう時間がかかったのは内緒。

CC=gcc 
RM=rm -f 
TARGET=test 
SRCS=$(wildcard *.cpp) 
OBJS=$(SRCS:.cpp=.o) 
CFLAGS=-Wall

all: clean $(TARGET)
 
clean:
        $(RM) $(OBJS) $(TARGET) 

$(TARGET): $(OBJS)
        $(CC) -o $@ $^ $(LIBDIRS) $(LIBS) 

$(OBJS): $(SRC) 
        $(CC) $(INCLUDES) -c $(SRCS)

そしてコンパイル結果がこれ。

john@ubuntu14041c:~/box$ make
rm -f main.o member.o test
gcc  -c main.cpp member.cpp
member.cpp: In function evoid RunTest()f:
member.cpp:12:23: warning: converting from evoid (ClassA::*)()f to evoid*f [-Wpmf-conversions]
     void *p2 = (void*)p1;
                       ^
member.cpp:14:33: error: invalid use of non-static member function evoid ClassA::Func1()f
     void *p4 = &(void*&)ClassA::Func1;

                                 ^
member.cpp:16:52: warning: format e%df expects argument of type eintf, but argument 2 has type  elong unsigned intf [-Wformat=]
     printf("size= %d\n", sizeof(void (ClassA::*)()));
                                                    ^
member.cpp:17:48: warning: format e%pf expects argument of type evoid*f, but argument 2 has type evoid (ClassA::*)()f [-Wformat=]
     printf("p1= %p, size= %d\n", p1, sizeof(p1));
                                                ^
member.cpp:17:48: warning: format e%df expects argument of type eintf, but argument 3 has type  elong unsigned intf [-Wformat=]
member.cpp:18:48: warning: format e%df expects argument of type eintf, but argument 3 has type  elong unsigned intf [-Wformat=]
     printf("p2= %p, size= %d\n", p2, sizeof(p2));
                                                ^
member.cpp:19:48: warning: format e%df expects argument of type eintf, but argument 3 has type  elong unsigned intf [-Wformat=]
     printf("p3= %p, size= %d\n", p3, sizeof(p3));
                                                ^
member.cpp:20:48: warning: format e%df expects argument of type eintf, but argument 3 has type  elong unsigned intf [-Wformat=]
     printf("p4= %p, size= %d\n", p4, sizeof(p4));
                                                ^
make: *** [main.o] Error 1

警告は無視するとして、エラーは p4 の代入時の 1 つだけです。なんと p2 の代入は通りました。p4 関連の処理をコメントにして実行すると、結果は次のようになります。

john@ubuntu14041c:~/box$ ./test
size= 16
p1= 0x400670, size= 0
p2= 0x400670, size= 8
p3= 0x400670, size= 8
+ClassA::Func1()

なんとなんと、メンバ関数へのポインターのサイズが、普通の 64bit ポインターの倍、16 バイトになっていました。でかい。ローカル変数 p1 のサイズが 16 バイトになるため、p1 の printf の結果が正しく出力されていません。一方、p2 と p3 は 8 バイトの汎用ポインターであるため、そもそも代入という操作は成立してはいけないことになります。p2 への代入については警告が出ていますが、参照型を付加した p3 への代入については警告は出ていません。コードがおかしいのは間違いないですが、警告は出て欲しいものです。gdb でデバッグしてみます。うーん使い慣れない・・。

john@ubuntu14041c:~/box$ gdb  ./test 
GNU gdb (Ubuntu 7.7-0ubuntu3.1) 7.7 
Copyright (C) 2014 Free Software Foundation, Inc. 
License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>&#160;
This is free software: you are free to change and redistribute it. 
There is NO WARRANTY, to the extent permitted by law.  Type "show copying" 
and "show warranty" for details. 
This GDB was configured as "x86_64-linux-gnu". 
Type "show configuration" for configuration details. 
For bug reporting instructions, please see: 
<http://www.gnu.org/software/gdb/bugs/&gt;. 
Find the GDB manual and other documentation resources online at: 
<http://www.gnu.org/software/gdb/documentation/&gt;. 
For help, type "help". 
Type "apropos word" to search for commands related to "word"... 
Reading symbols from ./test...(no debugging symbols found)...done. 
(gdb) disassemble /r RunTest 
Dump of assembler code for function _Z7RunTestv: 
   0x0000000000400598 <+0>:     55      push   %rbp 
   0x0000000000400599 <+1>:     48 89 e5        mov    %rsp,%rbp 
   0x000000000040059c <+4>:     48 83 ec 30     sub    $0x30,%rsp 
   0x00000000004005a0 <+8>:     48 c7 45 f0 70 06 40 00 movq   $0x400670,-0x10(%rbp) 
   0x00000000004005a8 <+16>:    48 c7 45 f8 00 00 00 00 movq   $0x0,-0x8(%rbp)

   0x00000000004005b0 <+24>:    48 8b 45 f0     mov    -0x10(%rbp),%rax 
   0x00000000004005b4 <+28>:    83 e0 01        and    $0x1,%eax 
   0x00000000004005b7 <+31>:    48 85 c0        test   %rax,%rax 
   0x00000000004005ba <+34>:    75 06   jne    0x4005c2 <_Z7RunTestv+42> 
   0x00000000004005bc <+36>:    48 8b 45 f0     mov    -0x10(%rbp),%rax 
   0x00000000004005c0 <+40>:    eb 1d   jmp    0x4005df <_Z7RunTestv+71> 
   0x00000000004005c2 <+42>:    ba 00 00 00 00  mov    $0x0,%edx 
   0x00000000004005c7 <+47>:    48 8b 45 f8     mov    -0x8(%rbp),%rax 
   0x00000000004005cb <+51>:    48 01 d0        add    %rdx,%rax 
   0x00000000004005ce <+54>:    48 8b 10        mov    (%rax),%rdx 
   0x00000000004005d1 <+57>:    48 8b 45 f0     mov    -0x10(%rbp),%rax 
   0x00000000004005d5 <+61>:    48 83 e8 01     sub    $0x1,%rax 
   0x00000000004005d9 <+65>:    48 01 d0        add    %rdx,%rax 
   0x00000000004005dc <+68>:    48 8b 00        mov    (%rax),%rax 
   0x00000000004005df <+71>:    48 89 45 e0     mov    %rax,-0x20(%rbp) 
   0x00000000004005e3 <+75>:    48 8d 45 f0     lea    -0x10(%rbp),%rax 
   0x00000000004005e7 <+79>:    48 8b 00        mov    (%rax),%rax 
   0x00000000004005ea <+82>:    48 89 45 e8     mov    %rax,-0x18(%rbp) 
   0x00000000004005ee <+86>:    be 10 00 00 00  mov    $0x10,%esi 
   0x00000000004005f3 <+91>:    bf 25 07 40 00  mov    $0x400725,%edi 
   0x00000000004005f8 <+96>:    b8 00 00 00 00  mov    $0x0,%eax 
   0x00000000004005fd <+101>:   e8 5e fe ff ff  callq  0x400460 <printf@plt> 
   0x0000000000400602 <+106>:   48 8b 55 f0     mov    -0x10(%rbp),%rdx 
   0x0000000000400606 <+110>:   48 8b 45 f8     mov    -0x8(%rbp),%rax 
   0x000000000040060a <+114>:   b9 10 00 00 00  mov    $0x10,%ecx 
   0x000000000040060f <+119>:   48 89 d6        mov    %rdx,%rsi 
   0x0000000000400612 <+122>:   48 89 c2        mov    %rax,%rdx 
   0x0000000000400615 <+125>:   bf 2f 07 40 00  mov    $0x40072f,%edi 
   0x000000000040061a <+130>:   b8 00 00 00 00  mov    $0x0,%eax 
   0x000000000040061f <+135>:   e8 3c fe ff ff  callq  0x400460 <printf@plt> 
   0x0000000000400624 <+140>:   48 8b 45 e0     mov    -0x20(%rbp),%rax 
   0x0000000000400628 <+144>:   ba 08 00 00 00  mov    $0x8,%edx 
   0x000000000040062d <+149>:   48 89 c6        mov    %rax,%rsi 
   0x0000000000400630 <+152>:   bf 41 07 40 00  mov    $0x400741,%edi 
   0x0000000000400635 <+157>:   b8 00 00 00 00  mov    $0x0,%eax 
   0x000000000040063a <+162>:   e8 21 fe ff ff  callq  0x400460 <printf@plt> 
   0x000000000040063f <+167>:   48 8b 45 e8     mov    -0x18(%rbp),%rax 
   0x0000000000400643 <+171>:   ba 08 00 00 00  mov    $0x8,%edx 
   0x0000000000400648 <+176>:   48 89 c6        mov    %rax,%rsi 
   0x000000000040064b <+179>:   bf 53 07 40 00  mov    $0x400753,%edi 
---Type <return> to continue, or q <return> to quit--- 
   0x0000000000400650 <+184>:   b8 00 00 00 00  mov    $0x0,%eax 
   0x0000000000400655 <+189>:   e8 06 fe ff ff  callq  0x400460 <printf@plt> 
   0x000000000040065a <+194>:   48 8b 45 f0     mov    -0x10(%rbp),%rax 
   0x000000000040065e <+198>:   48 8b 55 f8     mov    -0x8(%rbp),%rdx 
   0x0000000000400662 <+202>:   48 8d 4d df     lea    -0x21(%rbp),%rcx 
   0x0000000000400666 <+206>:   48 01 ca        add    %rcx,%rdx 
   0x0000000000400669 <+209>:   48 89 d7        mov    %rdx,%rdi 
   0x000000000040066c <+212>:   ff d0   callq  *%rax
 
   0x000000000040066e <+214>:   c9      leaveq 
   0x000000000040066f <+215>:   c3      retq 
End of assembler dump. 
(gdb) break *0x000000000040066c 
Breakpoint 1 at 0x40066c 
(gdb) r 
Starting program: /home/john/box/test 
size= 16 
p1= 0x400670, size= 0 
p2= 0x400670, size= 8 
p3= 0x400670, size= 8 
 
Breakpoint 1, 0x000000000040066c in RunTest() () 
(gdb) info registers 
rax            0x400670 4195952 
rbx            0x0      0 
rcx            0x7fffffffe59f   140737488348575 
rdx            0x7fffffffe59f   140737488348575 
rsi            0x7fffffea       2147483626 
rdi            0x7fffffffe59f   140737488348575 
rbp            0x7fffffffe5c0   0x7fffffffe5c0 
rsp            0x7fffffffe590   0x7fffffffe590 
r8             0x7ffff7b8b900   140737349466368 
r9             0x0      0 
r10            0x7ffff7dd26a0   140737351853728 
r11            0x246    582 
r12            0x400490 4195472 
r13            0x7fffffffe6c0   140737488348864 
r14            0x0      0 
r15            0x0      0 
rip            0x40066c 0x40066c <RunTest()+212> 
eflags         0x206    [ PF IF ] 
cs             0x33     51 
ss             0x2b     43 
ds             0x0      0 
es             0x0      0 
fs             0x0      0 
gs             0x0      0 
(gdb) x/16bx $rbp-0x21 
0x7fffffffe59f: 0x00    0x70    0x06    0x40    0x00    0x00    0x00    0x00 
0x7fffffffe5a7: 0x00    0x70    0x06    0x40    0x00    0x00    0x00    0x00 
(gdb) si 
0x0000000000400670 in ClassA::Func1() () 
(gdb) disassemble /r $rip 
Dump of assembler code for function _ZN6ClassA5Func1Ev: 
=> 0x0000000000400670 <+0>:     55      push   %rbp 
   0x0000000000400671 <+1>:     48 89 e5        mov    %rsp,%rbp 
   0x0000000000400674 <+4>:     48 83 ec 10     sub    $0x10,%rsp 
   0x0000000000400678 <+8>:     48 89 7d f8     mov    %rdi,-0x8(%rbp) 
   0x000000000040067c <+12>:    bf 14 07 40 00  mov    $0x400714,%edi 
   0x0000000000400681 <+17>:    e8 ca fd ff ff  callq  0x400450 <puts@plt> 
   0x0000000000400686 <+22>:    c9      leaveq 
   0x0000000000400687 <+23>:    c3      retq 
End of assembler dump. 
(gdb) x/s "0x400714 
Unterminated string in expression. 
(gdb) x/s 0x400714 
0x400714:       "+ClassA::Func1()" 
(gdb) 

何これ。Windows とは全然違う内容が広がっている・・。

まず、気になる 16 バイトの変数の正体ですが、mov を 2 回実行して 0x0, 0x400670 という即値をローカル変数領域に保存しています。これがメンバ関数ポインターの正体です。面白いのは (a.*p1)(); `を実行するところです。変数は 16 バイトですが、メンバ関数のアドレスは 8 バイトです。これは 16 バイトのうち下位 8 バイトが関数アドレスになっているようで、rbp-10 に保存したアドレスを rax に入れて call しています。では、上位 8 バイトは何に使われるのでしょうか。今回の例では、値は 0 です。

この 4 つの命令がそれです。

0x000000000040065e <+198>:   48 8b 55 f8     mov    -0x8(%rbp),%rdx
0x0000000000400662 <+202>:   48 8d 4d df     lea    -0x21(%rbp),%rcx
0x0000000000400666 <+206>:   48 01 ca        add    %rcx,%rdx
0x0000000000400669 <+209>:   48 89 d7        mov    %rdx,%rdi

上位 8 バイトを取り出して、rbp-21 に加算してから rdi に入れています。gcc の x64 における thiscall はよく分かりませんが、this ポインターは rdi として渡すようです。この動作は printf 関数の第一引数を edi に入れていることからも裏付けられます。this ポインター、すなわち RunTest におけるオブジェクト a はローカル変数なので、おそらく rbp-21 です。デバッグの例だと値は 0x7fffffffe59f です。ポインターの癖にアラインされていませんね。実に奇妙です。

rbp-21 の中身をダンプすると、オフセット+1 のところに ClassA::Func1 のアドレスと一致する 00400670 という数値が見つかりました。Windows 的に考えると先頭の 1 バイトがかなり邪魔です。フラグとして使われるなど、何か意味があるのでしょうか。

メンバ関数ポインターの上位 8 バイトは、rbp-21 からのオフセットとして使われています。gcc が作るバイナリにおいて、this ポインターの値はオブジェクトの先頭という意味ではなく、オフセットを使って適当な位置を指し示す際の起点、という意味合いなのかもしれません。コードをいろいろ変えてみて、オフセットが 0 以外になるのがどんな場合なのかを調べてみたいものです。

最後に OS X で試してみます。コンパイラーは gcc ではなく clang です。バージョンはこれ↓

proline:box $ clang –version
Apple LLVM version 6.0 (clang-600.0.54) (based on LLVM 3.5svn)
Target: x86_64-apple-darwin14.0.0
Thread model: posix

make は GNU Make を使うので、Makefile は ubuntu と同じのをそのまま使えます、が、一行目を CC=clang をに変えておきます。gcc を実行しても、clang が実行されるだけです。

$ make 
rm -f main.o member.o test 
clang  -c main.cpp member.cpp 
member.cpp:12:16: error: cannot cast from type 'void (ClassA::*)()' to pointer type 'void *' 
    void *p2 = (void*)p1; 
               ^~~~~~~~~ 
member.cpp:14:33: error: call to non-static member function without an object argument 
    void *p4 = &(void*&)ClassA::Func1; 
                        ~~~~~~~~^~~~~ 
member.cpp:16:26: warning: format specifies type 'int' but the argument has type 'unsigned long' 
      [-Wformat] 
    printf("size= %d\n", sizeof(void (ClassA::*)())); 
                  ~~     ^~~~~~~~~~~~~~~~~~~~~~~~~~ 
                  %lu 
member.cpp:17:34: warning: format specifies type 'void *' but the argument has type 
      'void (ClassA::*)()' [-Wformat] 
    printf("p1= %p, size= %d\n", p1, sizeof(p1)); 
                ~~               ^~ 
member.cpp:17:38: warning: format specifies type 'int' but the argument has type 'unsigned long' 
      [-Wformat] 
    printf("p1= %p, size= %d\n", p1, sizeof(p1)); 
                          ~~         ^~~~~~~~~~ 
                          %lu 
member.cpp:18:38: warning: format specifies type 'int' but the argument has type 'unsigned long' 
      [-Wformat] 
    printf("p2= %p, size= %d\n", p2, sizeof(p2)); 
                          ~~         ^~~~~~~~~~ 
                          %lu 
member.cpp:19:38: warning: format specifies type 'int' but the argument has type 'unsigned long' 
      [-Wformat] 
    printf("p3= %p, size= %d\n", p3, sizeof(p3)); 
                          ~~         ^~~~~~~~~~ 
                          %lu 
member.cpp:20:38: warning: format specifies type 'int' but the argument has type 'unsigned long' 
      [-Wformat] 
    printf("p4= %p, size= %d\n", p4, sizeof(p4)); 
                          ~~         ^~~~~~~~~~ 
                          %lu 
6 warnings and 2 errors generated. 
make: *** [main.o] Error 1 

gcc と同じ結果になるんだろうと予想していましたが、意外なことに Visual Studio と同じです。p4 はもちろん、p2 の代入についても怒られました。こちらも同じく p3 の代入は警告も出ず、スルーです。これは参照型の裏技だなぁ・・。

p2 と p4 をコメントにして、実行結果はこうなりました。今度は gcc と同じで、16 バイトの変数が使われています。

proline:box $ ./test
size= 16
p1= 0x101037f00, size= 0
p3= 0x101037f00, size= 8
+ClassA::Func1()

次に lldb でデバッグします。gdb とはコマンドが似ているようで違うので困ります。好みの問題かもしれませんが、オプションの指定方法や出力結果は lldb の方が洗練されている気がします。

ポインター周りの動作は gcc とほぼ同じです。16 バイトのうち、下位 8 バイトが実際の関数アドレス 0x0000000100000f00 になっています。

proline:box $ sudo lldb ./test 
(lldb) target create "./test" 
Current executable set to './test' (x86_64). 
(lldb) disassemble -b -n RunTest 
test`RunTest(): 
test[0x100000de0]:  55                       pushq  %rbp 
test[0x100000de1]:  48 89 e5                 movq   %rsp, %rbp 
test[0x100000de4]:  48 83 ec 70              subq   $0x70, %rsp 
test[0x100000de8]:  48 8d 45 d0              leaq   -0x30(%rbp), %rax 
test[0x100000dec]:  48 8b 0d 1d 02 00 00     movq   0x21d(%rip), %rcx         ; (void *)0x0000000100000f00: ClassA::Func1() 
test[0x100000df3]:  48 89 4d f0              movq   %rcx, -0x10(%rbp) 
test[0x100000df7]:  48 c7 45 f8 00 00 00 00  movq   $0x0, -0x8(%rbp) 
test[0x100000dff]:  48 8b 4d f0              movq   -0x10(%rbp), %rcx 
test[0x100000e03]:  48 89 4d e8              movq   %rcx, -0x18(%rbp) 
test[0x100000e07]:  48 8d 3d 38 01 00 00     leaq   0x138(%rip), %rdi         ; "size= %d\n" 
test[0x100000e0e]:  ba 10 00 00 00           movl   $0x10, %edx 
test[0x100000e13]:  89 d1                    movl   %edx, %ecx 
test[0x100000e15]:  31 d2                    xorl   %edx, %edx 
test[0x100000e17]:  40 88 d6                 movb   %dl, %sil 
test[0x100000e1a]:  40 88 75 cf              movb   %sil, -0x31(%rbp) 
test[0x100000e1e]:  48 89 ce                 movq   %rcx, %rsi 
test[0x100000e21]:  44 8a 45 cf              movb   -0x31(%rbp), %r8b 
test[0x100000e25]:  48 89 45 c0              movq   %rax, -0x40(%rbp) 
test[0x100000e29]:  44 88 c0                 movb   %r8b, %al 
test[0x100000e2c]:  48 89 4d b8              movq   %rcx, -0x48(%rbp) 
test[0x100000e30]:  e8 f1 00 00 00           callq  0x100000f26               ; symbol stub for: printf 
test[0x100000e35]:  48 8b 4d f0              movq   -0x10(%rbp), %rcx 
test[0x100000e39]:  48 8b 75 f8              movq   -0x8(%rbp), %rsi 
test[0x100000e3d]:  48 89 75 e0              movq   %rsi, -0x20(%rbp) 
test[0x100000e41]:  48 89 4d d8              movq   %rcx, -0x28(%rbp) 
test[0x100000e45]:  48 8b 75 d8              movq   -0x28(%rbp), %rsi 
test[0x100000e49]:  48 8b 55 e0              movq   -0x20(%rbp), %rdx 
test[0x100000e4d]:  48 8d 3d fc 00 00 00     leaq   0xfc(%rip), %rdi          ; "p1= %p, size= %d\n" 
test[0x100000e54]:  48 8b 4d b8              movq   -0x48(%rbp), %rcx 
test[0x100000e58]:  44 8a 45 cf              movb   -0x31(%rbp), %r8b 
test[0x100000e5c]:  89 45 b4                 movl   %eax, -0x4c(%rbp) 
test[0x100000e5f]:  44 88 c0                 movb   %r8b, %al 
test[0x100000e62]:  e8 bf 00 00 00           callq  0x100000f26               ; symbol stub for: printf 
test[0x100000e67]:  48 8b 75 e8              movq   -0x18(%rbp), %rsi 
test[0x100000e6b]:  48 8d 3d f0 00 00 00     leaq   0xf0(%rip), %rdi          ; "p3= %p, size= %d\n" 
test[0x100000e72]:  41 b9 08 00 00 00        movl   $0x8, %r9d 
test[0x100000e78]:  44 89 ca                 movl   %r9d, %edx 
test[0x100000e7b]:  44 8a 45 cf              movb   -0x31(%rbp), %r8b 
test[0x100000e7f]:  89 45 b0                 movl   %eax, -0x50(%rbp) 
test[0x100000e82]:  44 88 c0                 movb   %r8b, %al 
test[0x100000e85]:  e8 9c 00 00 00           callq  0x100000f26               ; symbol stub for: printf 
test[0x100000e8a]:  48 8b 4d f0              movq   -0x10(%rbp), %rcx 
test[0x100000e8e]:  48 8b 55 f8              movq   -0x8(%rbp), %rdx 
test[0x100000e92]:  48 8b 75 c0              movq   -0x40(%rbp), %rsi 
test[0x100000e96]:  48 01 d6                 addq   %rdx, %rsi 
test[0x100000e99]:  48 89 ca                 movq   %rcx, %rdx 
test[0x100000e9c]:  48 81 e2 01 00 00 00     andq   $0x1, %rdx 
test[0x100000ea3]:  48 81 fa 00 00 00 00     cmpq   $0x0, %rdx 
test[0x100000eaa]:  89 45 ac                 movl   %eax, -0x54(%rbp) 
test[0x100000ead]:  48 89 4d a0              movq   %rcx, -0x60(%rbp) 
test[0x100000eb1]:  48 89 75 98              movq   %rsi, -0x68(%rbp) 
test[0x100000eb5]:  0f 84 1f 00 00 00        je     0x100000eda               ; RunTest() + 250 
test[0x100000ebb]:  48 8b 45 98              movq   -0x68(%rbp), %rax 
test[0x100000ebf]:  48 8b 08                 movq   (%rax), %rcx 
test[0x100000ec2]:  48 8b 55 a0              movq   -0x60(%rbp), %rdx 
test[0x100000ec6]:  48 81 ea 01 00 00 00     subq   $0x1, %rdx 
test[0x100000ecd]:  48 8b 0c 11              movq   (%rcx,%rdx), %rcx 
test[0x100000ed1]:  48 89 4d 90              movq   %rcx, -0x70(%rbp) 
test[0x100000ed5]:  e9 08 00 00 00           jmp    0x100000ee2               ; RunTest() + 258 
test[0x100000eda]:  48 8b 45 a0              movq   -0x60(%rbp), %rax 
test[0x100000ede]:  48 89 45 90              movq   %rax, -0x70(%rbp) 
test[0x100000ee2]:  48 8b 45 90              movq   -0x70(%rbp), %rax 
test[0x100000ee6]:  48 8b 7d 98              movq   -0x68(%rbp), %rdi 
test[0x100000eea]:  ff d0                    callq  *%rax 
test[0x100000eec]:  48 83 c4 70              addq   $0x70, %rsp 
test[0x100000ef0]:  5d                       popq   %rbp 
test[0x100000ef1]:  c3                       retq 
test[0x100000ef2]:  90                       nop 
test[0x100000ef3]:  90                       nop 
test[0x100000ef4]:  90                       nop 
test[0x100000ef5]:  90                       nop 
test[0x100000ef6]:  90                       nop 
test[0x100000ef7]:  90                       nop 
test[0x100000ef8]:  90                       nop 
test[0x100000ef9]:  90                       nop 
test[0x100000efa]:  90                       nop 
test[0x100000efb]:  90                       nop 
test[0x100000efc]:  90                       nop 
test[0x100000efd]:  90                       nop 
test[0x100000efe]:  90                       nop 
test[0x100000eff]:  90                       nop 
 
(lldb) break set -a 0x100000eea 
Breakpoint 1: address = 0x0000000100000eea 
(lldb) r 
Process 626 launched: './test' (x86_64) 
size= 16 
p1= 0x100000f00, size= 0 
p3= 0x100000f00, size= 8 
Process 626 stopped 
* thread #1: tid = 0x20d7, 0x0000000100000eea test`RunTest() + 266, queue = 'com.apple.main-thread', stop reason = breakpoint 1.1 
    frame #0: 0x0000000100000eea test`RunTest() + 266 
test`RunTest() + 266: 
-> 0x100000eea:  callq  *%rax 
   0x100000eec:  addq   $0x70, %rsp 
   0x100000ef0:  popq   %rbp 
   0x100000ef1:  retq 
(lldb) reg read 
General Purpose Registers: 
       rax = 0x0000000100000f00  test`ClassA::Func1() 
       rbx = 0x0000000000000000 
       rcx = 0x0000000100000f00  test`ClassA::Func1() 
       rdx = 0x0000000000000000 
       rdi = 0x00007fff5fbffc90 
       rsi = 0x00007fff5fbffc90 
       rbp = 0x00007fff5fbffcc0 
       rsp = 0x00007fff5fbffc50 
        r8 = 0x00007fff5fbffaf0 
        r9 = 0x00007fff75a3b300  libsystem_pthread.dylib`_thread 
       r10 = 0x000000000000000a 
       r11 = 0x0000000000000246 
       r12 = 0x0000000000000000 
       r13 = 0x0000000000000000 
       r14 = 0x0000000000000000 
       r15 = 0x0000000000000000 
       rip = 0x0000000100000eea  test`RunTest() + 266 
    rflags = 0x0000000000000246 
        cs = 0x000000000000002b 
        fs = 0x0000000000000000 
        gs = 0x0000000000000000 
 
(lldb) disassemble -b -a 0x0000000100000f00 
test`ClassA::Func1(): 
   0x100000f00:  55                    pushq  %rbp 
   0x100000f01:  48 89 e5              movq   %rsp, %rbp 
   0x100000f04:  48 83 ec 10           subq   $0x10, %rsp 
   0x100000f08:  48 8d 05 65 00 00 00  leaq   0x65(%rip), %rax          ; "+ClassA::Func1()\n" 
   0x100000f0f:  48 89 7d f8           movq   %rdi, -0x8(%rbp) 
   0x100000f13:  48 89 c7              movq   %rax, %rdi 
   0x100000f16:  b0 00                 movb   $0x0, %al 
   0x100000f18:  e8 09 00 00 00        callq  0x100000f26               ; symbol stub for: printf 
   0x100000f1d:  89 45 f4              movl   %eax, -0xc(%rbp) 
   0x100000f20:  48 83 c4 10           addq   $0x10, %rsp 
   0x100000f24:  5d                    popq   %rbp 
   0x100000f25:  c3                    retq 

上位 8 バイトには 0 が代入されるところまでは gcc と同じですが、メンバ関数を呼び出す処理が複雑怪奇なことになっています。何か上位 8 バイトの値に応じて条件分岐とか出てきているし・・・何だこれは。最終的には rdi レジスターの値を作るオフセットとして使われ、this ポインターになるところは同じようです。

test[0x100000e8a]:  48 8b 4d f0              movq   -0x10(%rbp), %rcx
test[0x100000e8e]:  48 8b 55 f8              movq   -0x8(%rbp), %rdx
test[0x100000e92]:  48 8b 75 c0              movq   -0x40(%rbp), %rsi
test[0x100000e96]:  48 01 d6                 addq   %rdx, %rsi
test[0x100000e99]:  48 89 ca                 movq   %rcx, %rdx
test[0x100000e9c]:  48 81 e2 01 00 00 00     andq   $0x1, %rdx
test[0x100000ea3]:  48 81 fa 00 00 00 00     cmpq   $0x0, %rdx
test[0x100000eaa]:  89 45 ac                 movl   %eax, -0x54(%rbp)
test[0x100000ead]:  48 89 4d a0              movq   %rcx, -0x60(%rbp)
test[0x100000eb1]:  48 89 75 98              movq   %rsi, -0x68(%rbp)
test[0x100000eb5]:  0f 84 1f 00 00 00        je     0x100000eda               ; RunTest() + 250

test[0x100000ebb]:  48 8b 45 98              movq   -0x68(%rbp), %rax
test[0x100000ebf]:  48 8b 08                 movq   (%rax), %rcx
test[0x100000ec2]:  48 8b 55 a0              movq   -0x60(%rbp), %rdx
test[0x100000ec6]:  48 81 ea 01 00 00 00     subq   $0x1, %rdx
test[0x100000ecd]:  48 8b 0c 11              movq   (%rcx,%rdx), %rcx
test[0x100000ed1]:  48 89 4d 90              movq   %rcx, -0x70(%rbp)
test[0x100000ed5]:  e9 08 00 00 00           jmp    0x100000ee2               ; RunTest() + 258

test[0x100000eda]:  48 8b 45 a0              movq   -0x60(%rbp), %rax
test[0x100000ede]:  48 89 45 90              movq   %rax, -0x70(%rbp)

test[0x100000ee2]:  48 8b 45 90              movq   -0x70(%rbp), %rax
test[0x100000ee6]:  48 8b 7d 98              movq   -0x68(%rbp), %rdi
test[0x100000eea]:  ff d0                    callq  *%rax

今回は力尽きたのであまり深入りせずにここまで。メンバー関数ポインター、及びコンパイラ依存コードは深い。