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
# http://keicode.com/winprimer/wp04-2.php
#
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%df expects argument of type eintf, but argument 2 has type elong unsigned intf [-Wformat=]
printf("size= %d\n", sizeof(void (ClassA::*)()));
^
member.cpp:17:48: warning: format e%pf 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%df expects argument of type eintf, but argument 3 has type elong unsigned intf [-Wformat=]
member.cpp:18:48: warning: format e%df expects argument of type eintf, but argument 3 has type elong unsigned intf [-Wformat=]
printf("p2= %p, size= %d\n", p2, sizeof(p2));
^
member.cpp:19:48: warning: format e%df expects argument of type eintf, but argument 3 has type elong unsigned intf [-Wformat=]
printf("p3= %p, size= %d\n", p3, sizeof(p3));
^
member.cpp:20:48: warning: format e%df expects argument of type eintf, but argument 3 has type elong unsigned intf [-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>
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 ./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
今回は力尽きたのであまり深入りせずにここまで。メンバー関数ポインター、及びコンパイラ依存コードは深い。