clangでWindows SDKを使ってみた

Windows7環境でSDKを使ったプログラムを開発しようとしてみた。
使ったバージョンは以下の通り。

なお、以前に書いた「clang+boostでWinアプリ開発」とかの記事*1を前提としているので注意されたし。
最終的にはここに書いたこととmakefileの内容がすべてではあるけど、以前の記事で書いたことは説明を省いている。


SDKを使う際には、MinGW側で用意してくれていたヘッダからWindows SDK側のヘッダに移行するので色々いじる必要がありそうだった。
以下やったことの内容をメモ。

SDKのヘッダファイルを無理やり書き換えてコンパイルを通したもの

driverspecs.h

コンパイルエラーになるので、コンパイラへのアノテーション関連マクロを無効化。
以下のマクロ定義を空っぽに変更。マクロ自体は残して「__$drv_group」などの定義部分をコメントアウトした。

  • __drv_deref(annotes)
  • __drv_in(annotes)
  • __drv_in_deref(annotes)
  • __drv_out(annotes)
  • __drv_out_deref(annotes)
  • __drv_when(cond, annotes)
  • __drv_at(expr,annotes)
  • __drv_fun(annotes)
  • __drv_ret(annotes)
  • __drv_arg(expr,annotes)

※:__drv_when、__drv_atに関しては__drv_declspec部分は残して__$drv_groupだけコメントアウト
  こういう感じ

	#define __drv_when(cond, annotes)										\
	  __drv_declspec("SAL_when(" SPECSTRINGIZE(cond) ")") //__$drv_group(##__drv_nop(annotes))
	#define __drv_at(expr,annotes)\
	  __drv_declspec("SAL_at(" SPECSTRINGIZE(expr) ")") //__$drv_group(##__drv_nop(annotes))
oaidl.h

一か所「_VARIANT_BOOL bool」というメンバ名でコンパイルエラーになるので、メンバ名を「bool_」に変更する。
$(MINGW)\includeにも同名のファイルがあるけど、内容が違うので気にしない。

winnt.h
  • 「LONGLONG」および「ULONGLONG」がdouble定義になっている箇所を「long long」、「unsigned long long」に変更する
wtypes.h

MinGW側のヘッダがwtypes.hでVARIANT_BOOLを定義していて、さらに_VARIANT_BOOLを参照していて衝突するので、以下のように変更する。

//#define _VARIANT_BOOL    /##/	// Windows SDKでは、このように定義されているものを

typedef VARIANT_BOOL _VARIANT_BOOL;	// こちらに変更する
excpt.h

このファイルは$(MINGW)\includeに入っているものを改造。
変更内容は「#include 」を削除。
Windows SDK側のwindef.hがwinnt.hをincludeしてるので、excpt.hがEXCEPTION_DISPOSITIONを定義する前に参照してしまってコンパイルエラーとなる。
windef.hは他でもincludeしているので単純に削除して依存関係がうまくいくように調整。

sal.h

ファイルが無いため、ここ*2からもらってくる。
さらに以下の定義が足りないので追加する。(2015/06/26 閲覧)

//C:\Program Files\Microsoft SDKs\Windows\v7.1\Include\winnt.h:363:9: error: unknown type name '__nullterminated'
// typedef __nullnullterminated WCHAR UNALIGNED *PUZZWSTR;
#define __nullnullterminated
#define __nullterminated
//C:\Program Files\Microsoft SDKs\Windows\v7.1\Include\winnt.h:366:9: error: unknown type name '__possibly_notnullterminated'
//typedef __possibly_notnullterminated WCHAR *PNZWCH;
#define __possibly_notnullterminated
//C:\Program Files\Microsoft SDKs\Windows\v7.1\Include\winnt.h:542:19: error: expected parameter declarator
//typedef __success(return >= 0) long HRESULT;
#define __success(expr)
//C:\Program Files\Microsoft SDKs\Windows\v7.1\Include\winnt.h:1201:1: error: unknown type name '__post'
#define __post
//C:\Program Files\Microsoft SDKs\Windows\v7.1\Include\winnt.h:2116:5: error: unknown type name '__maybenull'
#define __maybenull
//C:\Program Files\Microsoft SDKs\Windows\v7.1\Include\winnt.h:8626:5: error: unknown type name '__notnull'
#define __notnull
#define __elem_writableTo(size)
#define __inner_checkReturn
#define __byte_writableTo(size)
#define __pre
#define __callback
#define __drv_nop(annotes)
#define __deref
#define __notvalid
#define __reserved
#define __valid
#define __exceptthat
#define __inner_control_entrypoint(GDI)
#define __refparam
#define  __elem_readableTo(x)
#define __format_string
#define __typefix(x)
//#define SPECSTRINGIZE(x)
//#define PURE
//C:\Program Files\Microsoft SDKs\Windows\v7.1\Include\objidl.h:11280:68: error: C++ requires a type specifier for all declarations
#define __RPC__out_xcount_part(x, y)
//C:\Program Files\Microsoft SDKs\Windows\v7.1\Include\objidl.h:11284:33: error: unknown type name '__RPC__in_xcount_full'
#define __RPC__in_xcount_full(x)
//C:\Program Files\Microsoft SDKs\Windows\v7.1\Include\objidl.h:13063:106: error: expected parameter declarator
#define __RPC__inout_xcount(x)
//C:\Program Files\Microsoft SDKs\Windows\v7.1\Include\objidl.h:13064:103: error: expected parameter declarator
#define __RPC__in_xcount(x)
//oaidl.h:2134:47: error: expected parameter declarator
#define __RPC__in_range(x, y)

追加したDLL

Windows SDKに入ってない?DirectX側から持ってきたらアプリが起動するようになったのは以下のDLLたち。

D3DCompiler_42.dll
d3dx10_42.dll
d3dx11_42.dll
d3dx9_42.dll
Detoured.dll

Detoured.dllってなんぞ?と思ったけど、ファイルのプロパティを見ると「Microsoft Research Detours Package」って書いてある。
おそらくここ*3が公式サイトと思われる。

Detours
Software package for re-routing Win32 APIs underneath applications.

ということで、APIの転送をやっているらしい。

ビルド時オプション

-D_M_IX86

MinGW側のwindows.hで定義されていたが、Windows SDKにはdefineされていないので追加。
本当は「600」とかに定義しておいた方がよいかも。

-D_STDCALL_SUPPORTED

Windows SDKのヘッダ数か所で「__stdcall」を使うdefine定義を有効にするために追加。
これがないと「WINAPI」とかがデフォルトの呼び出し規約になってしまう。
(clangってデフォルトは何だっけ?cdecl?)
こいつを定義していないせいでひどい目にあったのだが、それについては後述したい。

-fms-extensions -fenable-experimental-ms-inline-asm

デフォルトだとMS形式のインラインアセンブラコンパイルできないのでコンパイルオプションに付与する。

オプションを付けると以下の警告が出る。

warning: MS-style inline assembly is not supported [-Wmicrosoft]

こちら*4によると

the the asm("...") syntax is part of the C++ standard, whereas Microsoft's __asm { ... }; is not.

ということらしい。ので、当該警告をoffにするのもありかと。「-Wno-microsoft」かな?
自分は当該警告オプションを付けずに他の警告オプションをいくつか付けている。makefileの内容を後で掲載するのでそちらを見てほしい。

-Wl,--enable-stdcall-fixup

WindowsのDLLとリンクするときに「@」が邪魔になるのでこのオプションを付与する。
コンパイラごとの命名規則?はここ*5に一覧表が書いてあって分かりやすい。(clangはMinGW列ね)

makefile

以上を踏まえて、例えばmakefileはこんな感じにする。
boostとDirectX SDKも一緒にくっつけて、今まで作っていた簡単な画面表示程度のアプリがビルドできることを確認した。

CC		= clang
MINGW	= C:\MinGW
DX_TOP	= C:\DirectXSDK
DX_INC	= $(DX_TOP)\Include
DX_LIB	= $(DX_TOP)\Lib\x86
DX_LIBS	= $(DX_LIB)\d3d11.lib $(DX_LIB)\d3dx11.lib $(DX_LIB)\d3d10.lib $(DX_LIB)\dxgi.lib $(DX_LIB)\dxguid.lib
FXC		= $(DX_TOP)\Utilities\bin\x64\fxc
FX_INC	= $(DX_TOP)
BO_TOP	= C:\boost_1_55_0
BO_LIB	= $(BO_TOP)\stage\lib
BO_LIBS	= $(BO_LIB)\libboost_thread-clang32-mt-s-1_55.lib $(BO_LIB)\libboost_system-clang32-mt-s-1_55.lib
WIN_LIB	= C:\Windows\SysWOW64
WIN_LIBS= $(WIN_LIB)\user32.dll $(WIN_LIB)\gdi32.dll $(WIN_LIB)\kernel32.dll
INC		=								\
	-I $(DX_INC)						\
	-I $(BO_TOP)						\
	-I .								\
	-I "C:\Program Files\Microsoft SDKs\Windows\v7.1\Include"		\

LIBS	=					\
	$(DX_LIBS)				\
	$(BO_LIBS)				\
	-lstdc++				\
	$(WIN_LIBS)				\
	d3dx11_42.dll	\

CFLAGS	= -Wall -W -std=c++11 -DBOOST_THREAD_USE_LIB -Wno-c++11-narrowing -D_M_IX86 -Wno-unknown-pragmas -Wno-reserved-user-defined-literal -fms-extensions -fenable-experimental-ms-inline-asm -D_STDCALL_SUPPORTED
LFLAGS	= -static -mwindows -Wl,--allow-multiple-definition -Wl,--enable-stdcall-fixup -Wl,-Map=mapfile.map
SRCS	= 					\
	main.cpp				\

OBJS	= $(SRCS:.cpp=.o)
TARGET	= sdk-test.exe

.PHONY: all clean env delete_objs delete_target

all: CFLAGS += -O2
all: $(TARGET)

debug: CFLAGS += -g
debug: LFLAGS += -g
debug: $(TARGET)

$(TARGET): $(OBJS) $(BLOBS)
	$(CC) -o $@ $(OBJS) $(LFLAGS) $(LIBS)

env:
	set PATH=%PATH%;$(MINGW)\bin
	set C_INCLUDE_PATH=$(MINGW)\include;$(MINGW)\lib\clang\3.2\include
	set "CPLUS_INCLUDE_PATH=%C_INCLUDE_PATH%;$(MINGW)\lib\gcc\mingw32\4.6.2\include\c++;$(MINGW)\lib\gcc\mingw32\4.6.2\include\c++\mingw32"

trigger-help:
	$(DX_TOP)\help\windows_graphics.chm

clean: delete_objs delete_target

delete_objs:
	del /f /q $(OBJS) $(BLOBS)

delete_target:
	del /f /q $(TARGET)


.SUFFIXES: .cpp .o

.cpp.o:
	$(CC) -c $(INC) $(CFLAGS) -o $*.o $<

_STDCALL_SUPPORTEDが無いせいで呼び出し規約不一致によりアプリクラッシュした件

この項では参考情報としてトラブルシューティングのステップを順に書いていきたい。
DLL側とアプリ側で呼び出し規約が一致しない場合にどのような現象となるのか報告しておきたい。*6

現象面

前提としてWindows SDK側のwindef.hなどには、_STDCALL_SUPPORTEDなら「#define WINAPI __stdcall」みたいな定義がある。
のだが、そんなことになっているとは知らず最初は_STDCALL_SUPPORTEDをdefineしていなかった。

そうすると

  • WindowsのDLL:stdcall
  • アプリ側:cdecl?

と呼び出し規約が一致しなくなる。

結果的にはアプリがクラッシュするわけだけど、以下のように若干ややこしい見え方をしていた。

  • std::fstream内部のファイルディスクリプタ的な情報にアクセスした瞬間に死亡(スタック破壊か?)
  • スタックポインタは一見正常値に見える(ポインタは壊れていない?←実はこの推測は間違い)
  • ファイルアクセス前に取得している時刻情報もおかしい(スタック破壊っぽい?)

ちなみに上記は関係なさそうな処理をどんどん削除していって現象が再現する最小のコードにした状態で確認した。

不具合発生箇所の切り分け

現象面だけ見てもなんだかよくわからないのでWinDbg経由でアプリを起動してどのあたりで死んでいるのか見てみた。
ただ、直接死んだファイルアクセス箇所は別段異常が見当たらない。
仕方がないので呼び出し元に戻ってみるが、おかしなところは見つからない。

ここで、時刻情報もおかしい事実を思いだした。fstreamに現在時刻を出力しているのだが、時刻がめちゃくちゃな値になっていた。
時刻情報はWindows APIを呼び出して取得している。そして今回変更したのはWindows SDKの切り替えだけ。
ということは、API呼び出しで何か良くないことが起きている、という推論にたどり着いた。

Windows API呼び出し箇所の確認

クラッシュ箇所のファイルアクセスよりも2関数ほど前に該当のAPI呼び出しがあった。
スタックの内容を破壊したのなら関数から戻る際にクラッシュするのが普通だと思う。
イメージ的には以下のようになるはず。

void stack_bomb(void)
{
	char	buf[128];

	api_oops();		//この関数内部でスタックの内容を壊したとする。ケース(1):この関数からstack_bomb()に戻る過程でクラッシュする

	funcA();		//funcA()用にスタックを伸長するのでfuncA()関連でクラッシュするケースは少ないと思う

	buf[xxx];		//もしかしたらローカル変数もおかしいことになっているかも?

	funcB();		//同様にここでクラッシュするケースも少ないと思う。が、今回はこの内部でクラッシュした

	return;			//ケース(2):stack_bomb()から戻る過程でクラッシュする
}

スタックの内容を見たが、特に壊れている雰囲気はない。
funcB()で死んだ理由を逆アセンブル結果で確認すると、引数で渡したポインタ変数へのアクセスで死んでいる。
ポインタを見ると確かにおかしなアドレス値になっている。
本来ならここで正しいアドレス値がスタックのどこかに入っていないか確認すればよかったかもしれない。
ただ、STLが内部で保持しているデータ構造なので確認が面倒ではないかと思う。どちらにしろ当時は思い浮かばなかった。

ひらめきの訪れ<仮説を立てる>

それで、ここからは完全に直感なんだけど「もしかしてスタックポインタずれてない?」と思い浮かんだ。
「なぜ?」と聞かれても困るが、それまでの状況証拠を見ているときに突然脳みそが提示してきた。もしかしたら「スタックの内容は壊れてない。スタックポインタもそれっぽいアドレス。なら少しだけアドレスがずれているのかも?」みたいな発想なのかもしれない。
自分の持論として「エンジニアの直感は75%の確率で正しい」という謎の持論があるので、まずは真偽を確かめることにした。

仮説の検証

やり方は単純。上記のapi_oops()、funcA()、funcB()の呼び出し前後でスタックポインタ値が変化するかどうかを見ればよい。
WinDbg上でレジスタを表示しながらステップ実行してみる。
そうすると案の定、api_oops()だけ呼び出し前後でスタックポインタ値が少しずれていた。(具体的には4バイト)

真の不具合内容を確認

ここまで来ればあとは単純なパワー作業だ。機械語の逆アセンブル結果とにらめっこしながらスタックポインタがずれる原因を確認すればよい。
で、1命令ずつ確認していった結果、呼び出し元と呼び出し先でスタックポインタの扱いが一致してないことが分かった。

不具合混入の原因を調査

そうと分かればあと一息。この手の問題は呼び出し規約と決まっている。
Windows APIとclangの呼び出し規約を確認s・・・ん?あれ??
そもそもWINAPIってstdcallだよね。って話。昔調べたぞ。WinMain()とかに付けるあれだ、あれ。

ここでようやく「Windows SDKのヘッダファイルがWINAPIをどう定義してるか」って話にたどり着いた。(長い)
あとはgrepしてWINAPIの定義を見つけ出し、「_STDCALL_SUPPORTED」が定義済みの時にstdcallになる、ということを確認した。

感想

ということで、_STDCALL_SUPPORTEDを定義しないとクラッシュするね。って話になる。
最初にAPI呼び出しでクラッシュしてると分かっていればもっと早く答えにたどり着いたかもしれないけど、一見別の関数で死んでいるように見えたので遠回りしてしまった。
スタックポインタ値も「おおよそスタックポインタ値の範囲にある」というのはすぐわかるが、「4バイトずれている」というのは関数呼び出しの前後で比較しない限り分からない。
呼び出し規約が違う、というのも逆アセンブル結果で比較する程度しかないのかな。と思う。もしかしたらもっとすぐ確認する方法があるかもしれないけど。