ホーム ざれごと ワシントン州 ツール NT豆知識 Win32プログラミングノート 私的用語 ジョーク いろいろ ゲーム雑記 Favorites 掲示板 Mail
自分の書いたコードがどんな風にアセンブラに落ちているのか、確かめたくなることはありませんか? え、ない? ほんと? いや別にいいんですけど。
というわけで、コンパイル結果が気になるような人はたいていもう知っているはずなんですが、念のため。 一番簡単なのは、コンパイラにリスティングファイルを作らせることでしょう。 プロジェクトオプション、C/C++、リスティングファイルから指定できます。
出力結果は、中間オブジェクト出力先ディレクトリに a.cod とかで出てきます。 後は個人のお楽しみ。
普通にやると eip はインラインアセンブラでは取れませんが、次の関数を呼び出すことで取得できます。
LPVOID __declspec(naked) GetEip() { __asm mov eax, [esp]; __asm ret; }
↓の方で解説していますが、__declspec(naked) によって余分なプロローグ・エピローグコードが生成されないようにしておいて、スタックトップに積まれている eip を取ってくる、というわけです。 ちょっとだけ hacky ですが、__emit で直接コードを埋め込むよりはましかな、と。
VC++のヘルプはhhw.exe、つまりHTMLヘルプなわけです。 中身は普通のHTMLになってまして、前のwinhlp32のヘルプよりは結構良いかな、と思うこともあります。 しかしながら、やはりヘルプを行ったり来たりが直線的にしか出来なくて、こっちとあっちとそっちを交互に見たい場合は、参照する順番に頭を使わなくてはなりません。
んが、所詮HTMLヘルプ、エンジンは IE と同じモノを使っているので、実は IE と同じく、リンク先を新しいウィンドウで開くことができるのです。
IE で、リンク先を新しいウィンドウで開くときはどうしていますか? アンカーを右クリックして、"新しいウィンドウで開く" メニューを選択してたりします? 残念ながら、HTMLヘルプでは、アンカーを右クリックしても "プロパティ" しかメニューには出てきません。
ではどうするかというと、実は IE にも用意されている、
Shift + 左クリック
がその答えです。
アンカーを Shift + 左クリック してみると、新しい IE のウィンドウが開き、そこにはHTMLヘルプの内容がちゃーんと表示されます。
そこから先のリンクもきちんと動くので、必要なだけウィンドウを開いて調べまくることが出来る、というわけです。
意味があるかどうかはさておき、ファイルに保存も出来るし、Favorite メニューに入れることも出来まっせ。
パスを変えて再インストールしたら、思いっきり無効リンクになってしまいますが。
意外と知らない人が多いらしいので、書いてみました。
例えば、winmm.libをリンクする必要があるとしたら、どうします?プロジェクトのオプションで指定するのが普通ですが、同じソースを使いまわすときなんかは、毎回指定するのが面倒ですね。 というわけで、リンカに対する指示をソースコードに埋め込む pragma で指定してもおっけー。
#pragma comment(lib, "winmm.lib")
ライブラリ(lib)に対するコメントには、リンカへのオプションや、リンクするライブラリを指定できるというわけです。
Win32では、DLLは各プロセス毎に全く別のインスタンスとしてロードされる(アドレスも変わるよ)なので、DLLを介してプロセス間でメモリを共有しようとすると、メモリマップドファイルを使うか、 共用セクションを使う
わけです。
共有セクションは、コンパイラに対する #pragma 指示と、リンカに対するリンクオプションで作ることが出来ます。
リンクオプションは、コードに埋め込むことも出来ます。
#pragma comment(linker, "/section:shared,rws") #pragma data_seg("shared") namespace shared { UINT msgPrivate = NULL; // notification message to the main app HWND hwndAppMain = NULL; // main window of the main app HHOOK hhk = NULL; // hook handler }; #pragma data_seg()
気持ちnamespaceを使ってみたりして。
気をつけなくちゃいけないのは、共有する変数を明示的に初期化する必要があるということ。
初期化指示されていない外部変数はBSSへ入れられてしまいます。 BSSは、exe中に領域を確保されずローダがロード時に0で埋めるので、普通は初期化指示をしないほうがexeのサイズが小さくなって有利なのですが。
共用セクションに入れて欲しい変数の初期化を書いておかないと(初期値が0でもNULLでも)、その変数はBSSセクションへ入れられてしまって、プロセス間でシェアされない、ということになります。
また、プロセスをまたいだポインタには全く意味がないため、共用セクションにポインタを直接入れても無駄…というか、それはバグです。 共用するなら、ハンドルを介して行うか、プロセスごとに用意したベースアドレスからのオフセットにするべきでしょう。老婆心ながら。
VC++6.0では、リンカのデフォルトで、セクションが4KBバウンダリ(x86のページサイズ)に配置されるようになっています。 あらかじめCPUの自然なバウンダリである4KBにセクションがまとめられているため、ローダのパフォーマンス向上が望めます。 しかし、最終的なバイナリが小さい場合、4KBバウンダリのせいでサイズが予想外に大きくなることがあります。 好き好きですが、次のオプションを、プロジェクト設定のリンカタブ、"Project Options" の欄に直接書き込むと、セクションが4KBバウンダリにまとめられることがなくなります。
/OPT:NOWIN98
リンカオプションで、
/merge:.rdata=.text
とやっておくと、rdata(read-only)のデータが、コードセクションである.textにマージされます。 セクションの属性がちゃう、というリンカのwarningが出ちゃいますが…。
このリンカのwarningは、
/ignore:4078で消せます。まぁ気分の問題か。
何が嬉しいかというと、.rdataが少ない場合、.textにマージすると、1ページだけメモリが稼げるかもしれない、ということです。 どちらもread-onlyではあるわけで。 しかし、使える局面は限られるでしょうね。 ↓みたいな、ごく小さなDLLを作る場合とか。
以上まとめてコードに書くなら、こんな感じ。
#pragma comment(linker, "/opt:nowin98") #pragma comment(linker, "/merge:.rdata=.text") #pragma comment(linker, "/ignore:4078")
なお、VC6では、文字列リテラルは.dataに入れられるようになっています。 これを.rdataへ落とし込むには、コンパイラフラグ /GF が必要になります。 寡聞にして、コンパイラフラグを pragma で指定する方法を知らないので、ここばかりはプロジェクト設定で指定しています。 /GF は、プロジェクトオプションに直接書き込んでやればおっけー。
#pragma comment(lib, "setupapi.lib") #pragma comment(linker, "/delayload:setupapi.dll")
リンカへのオプション指定を使って、ディレイロードを行っています*。 注意点としては、ディレイロードに失敗すると例外が上がるので、呼び出し側できとんと捕捉してあげることが挙げられます。
* ただし、Visual Studio .NET 付属のコンパイラでは、これらのリンカ指定が使えなくなってしまったようです。 ちょっと頭が痛い…。
…というときは、しょっぱなに
BOOL APIENTRY DllMain(HANDLE hModule, DWORD ul_reason_for_call, LPVOID lpReserved) { switch (ul_reason_for_call) { case DLL_PROCESS_ATTACH: DisableThreadLibraryCalls(hModule); break;
って感じで DisableThreadLibraryCalls() を呼んでおくと、多少パフォーマンスが稼げます。 ただし、CRTを(スタティックリンクで)使っているときは、スレッドに対する初期化が行われなくなるので、やってはいけません。 多分。
フック用のDLLで、
大した処理もしていない割には、
なんだかサイズが小さくならない、というときは。
ちなみに、バイナリの大きさ=全てのセクションサイズの合計、というわけではありません。 上記のようにデフォルトで4KBバウンダリに切り上げられるし、それでなくとも、少なくとも16バイトバウンダリに切り上げらるはず。 実際のセクションのサイズを知りたい場合は、
link -dump -headers your_dll.dllのようにして、PEヘッダを表示させるとおっけー。
使わないものはリンクする必要なし。 というわけで、CRTを全く使わないなら、リンクする必要はないわけです。 不要な初期化ルーチンやらの分だけ、DLLが小さくなります。
というわけで、プロジェクトのオプションで、デフォルトのライブラリを無視、にすると、CRTのライブラリはリンクされなくなります。
コードに埋め込むなら、こう。
#pragma comment(linker, "/nodefaultlib")
が、それだけでめでたし、とはなりません。
VC++のデフォルトでは _DllMainCRTStartup(HMODULE, DWORD, LPVOID) というCRT初期化用のダミー DllInit() がスタートアップルーチンになっています。ライブラリの中に入っているので、リンクエラーになってしまいま
す。
ソースコードを見てみると、CRTの初期化以外に大したことはしていないので、プロジェクトのリンクオプションで、スタートアップを DllMain に書き換えてしまえばおっけー。
コードに埋め込むなら、こう。
#pragma comment(linker, "/entry:\"DllMain\"")
もうひとつ、デバッグ版では、関数の呼び出し方法に起因するスタックレベルの非整合のチェック用に(__stdcallか__cdeclか)、_chkesp() という関数が使われているようで、これまたリンクエラーになります。 VC++ のデバッグ支援機能の一つなんですが、ここは目をつぶって、空の関数を定義してやればおっけー。
#ifdef _DEBUG extern "C" void _chkesp() { } #endif
なお蛇足ながら、nodefaultlibは、オブジェクトに埋め込まれたライブラリを無視するという仕様なので、↑に挙げた、リンクするライブラリをコードに埋め込むという手は使えなくなります。
ちゃんとやるなら……。
VC++コンパイラの吐くコードは、 関数の呼び出し前にespをesiに保存し、呼出し後に esi と esp を比較してから _chkesp() を呼んでいるので、_chkesp() の中ではインラインアセンブラでゼロフラグをチェックするアセ
ンブラコードを書いて、ブレークでもかければ良いでしょう。
Base APIの、DebugBreak()とか。
で、実際にやってみると、障害が二つあって意外とてこずりました。
extern "C" void _chkesp( { __asm { jz ok; } OutputDebugString(TEXT("Stack level doesn't match!\n")); DebugBreak(); ok: ; }
すると、こ〜んなコードが生成されたのでした。
__chkesp PROC NEAR ; COMDAT ; 108 : { 00000 55 push ebp 00001 8b ec mov ebp, esp 00003 83 ec 40 sub esp, 64 ; 00000040H 00006 53 push ebx 00007 56 push esi 00008 57 push edi 00009 8d 7d c0 lea edi, DWORD PTR [ebp-64] 0000c b9 10 00 00 00 mov ecx, 16 ; 00000010H 00011 b8 cc cc cc cc mov eax, -858993460 ; ccccccccH 00016 f3 ab rep stosd ; 109 : __asm { ; 110 : jz ok; 00018 74 23 je SHORT $ok$16732 ; 111 : } ; 112 : OutputDebugString(TEXT("Stack level doesn't match!\n")); 0001a 8b f4 mov esi, esp 0001c 68 00 00 00 00 push OFFSET FLAT:??_C@_1DI@MEOM@?$AAS?$AAt?$AAa?$AAc?$AAk?$AA?5?$AAl?$AAe?$AAv?$AAe?$AAl?$AA?5?$AAd?$AAo?$AAe?$AAs?$AAn?$AA?8?$AAt?$AA?5@ ; `string' 00021 ff 15 00 00 00 00 call DWORD PTR __imp__OutputDebugStringW@4 00027 3b f4 cmp esi, esp 00029 e8 00 00 00 00 call __chkesp ; 113 : DebugBreak(); 0002e 8b f4 mov esi, esp 00030 ff 15 00 00 00 00 call DWORD PTR __imp__DebugBreak@0 00036 3b f4 cmp esi, esp 00038 e8 00 00 00 00 call __chkesp $ok$16732: ; 114 : ok: ; 115 : ; ; 116 : } 0003d 5f pop edi 0003e 5e pop esi 0003f 5b pop ebx 00040 83 c4 40 add esp, 64 ; 00000040H 00043 3b ec cmp ebp, esp 00045 e8 00 00 00 00 call __chkesp 0004a 8b e5 mov esp, ebp 0004c 5d pop ebp 0004d c3 ret 0 __chkesp ENDP
こりゃたまりません。
64バイトのローカルエリアをスタック上に作って、0xccで埋めてしまうコードまで入ったりして…。
というわけで、最初の問題に対しては、いろいろコンパイルオプションを探してみたのですがうまくいきません。
2番目の問題に対しては、C/C++で関数呼び出しを書くと、スタックレベルチェックのコードが生成されてしまうようです。
途中で、逃げのオプションを探す気力が尽きてしまいました。
アセンブラで書けばOKだろうと。
結局たどり着いたのは次のようなコードです。
const TCHAR _stack_level_unmatch[] = TEXT("Stack level doesn't match!\n");
extern "C"
void __declspec(naked) _chkesp()
{
__asm {
jz ok;
push offset _stack_level_unmatch;
call dword ptr OutputDebugString;
call dword ptr DebugBreak;
ok:
ret;
};
}
__declspec(naked) がポイントでした。 これはコーリングコンベンションではなく、関数の実体を修飾するもので、プロローグ・エピローグのコードを全く生成しないようコンパイラに指示します。 アセンブラで"ret"を書かないと、関数からのリターンもしないという徹底した仕様のようで(笑)。
_chkespの再帰的呼び出しを避けるため、デバッグ用APIの呼び出しも、アセンブラで行いました。
なお、アプリから見たAPIのエントリは、インポートテーブルのアドレスになるので、間接ジャンプを使う必要があります。
これで、おおむねOKそうです。
codファイルの結果を見ても、自分の書いたアセンブラコード以外には、余計なものは一切付いていません。
偉いぞ > __declspace(naked)。
__chkesp PROC NEAR ; COMDAT ; 136 : __asm { ; 137 : jz ok; 00000 74 11 je SHORT $ok$16741 ; 138 : push offset _stack_level_unmatch; 00002 68 00 00 00 00 push OFFSET FLAT:__stack_level_unmatch ; 139 : call dword ptr OutputDebugString; 00007 ff 15 00 00 00 00 call DWORD PTR __imp__OutputDebugStringW@4 ; 140 : call dword ptr DebugBreak; 0000d ff 15 00 00 00 00 call DWORD PTR __imp__DebugBreak@0 $ok$16741: ; 141 : ok: ; 142 : ret; 00013 c3 ret 0 __chkesp ENDP _TEXT ENDS END
……とさんざん格闘した後で、このリンクを見つけました。
[ Q191669 - PRB: LNK2001: Unresolved External Symbol __chkesp ]
そっか、コンパイルオプションで逃げる手もあったのか
(/GZ をコンパイルオプションから外す)。
まぁいいや。ちゃんとスタックのチェックが出来るし…(^_^;
DLLの場合、ベースアドレスのデフォルトは 0x10000000 になっていますが、フック用 DLL などの場合は、ちょーっとずらしてやると、他の DLL とアドレスがバッティングする確率が小さくなります。
詳しい説明は省きますが、アドレスがバッティングしないとみんなちょっとだけ幸せになります。
プロジェクトオプションの、リンク、アウトプット、ベースアドレスで適当な値を指定します。
4KBバウンダリならおっけーのはず。それとも64KBバウンダリだったかな。忘れた。
コードに埋め込むなら、例えばこう。
#pragma comment(linker, "/base:\"0x18000000\"")
ExeファイルのサブプロジェクトとしてDLLを作っているとき、 コンパイル結果のlib、exp、dllなどをexeと同じディレクトリに入れると何かと便利です。 が、中間ファイルの出力先までexeと同じにしてしまうと、ちょっとハマります。 コンパイラ、リンカが使うファイルの一部に名前が固定のものがあって、バッティングするようです。
というわけで、サブプロジェクトのDLLでは、最終バイナリの出力先は親exeと同じにして、中間ファイルはDLL固有のものにするのが良さそうです。
MFCを使っていればTRACEx()等のマクロでデバッグメッセージを、デバッガへ出力することが出来て便利ですね。 でも、CRTもMFCも使わないDLLでは、デバッグ用メッセージの出力はどうすりゃいいんでしょう。
実は、 OutputDebugString() というAPIがあり、結局TRACEx()なども最終的にこいつを呼んでいます。 ということで、直接このAPIを呼べばおっけー。
アサート代わりには、 DebugBreak() を呼んでブレークポイント例外を起こせばおっけーでしょう。
OutputDebugString() は文字列を一つ引数に取るだけで、そのままでは TRACEx() と同じには使えません。 普通は vsprintf() なんぞを使うわけですが、CRTを使わない場合は当然使えません。 というわけで、Win32 API版の wvsprintf() を使います。 浮動小数点が扱えない以外は、vsprintf() の代替に大体なります :-)。
こんな感じ。
#include <stdarg.h> #if defined(_DEBUG) || defined(DEBUG) namespace hirofumi { inline void VOutputDebugString(LPCTSTR str, ...) { TCHAR buf[256]; va_list args; va_start(args, str); wvsprintf(buf, str, args); OutputDebugString(buf); va_end(args); } }; #define ASSERT(x) \ if (!(x)) { \ hirofumi::VOutputDebugString(TEXT("Assertion failed! in %s (%d) %s\n"), \ TEXT(__FILE__), __LINE__, TEXT(#x)); \ DebugBreak(); \ } else #define VERIFY(x) ASSERT(x) #define TRACE0(x) OutputDebugString(TEXT(x)) #define TRACE1(x, a) hirofumi::VOutputDebugString(TEXT(x), a); #define TRACE2(x, a, b) hirofumi::VOutputDebugString(TEXT(x), a, b); #define TRACE3(x, a, b, c) hirofumi::VOutputDebugString(TEXT(x), a, b, c) #define TRACE4(x, a, b, c, d) hirofumi::VOutputDebugString(TEXT(x), a, b, c, d) #else // Release version #define ASSERT(x) #define VERIFY(x) x #define TRACE0(x) #define TRACE1(x, a) #define TRACE2(x, a, b) #define TRACE3(x, a, b, c) #define TRACE4(x, a, b, c, d) #endif
グローバルフックの入っているDLLの出力は、残念ながらVC++のIDEのデバッグ出力には全部は表示されません。 他のプロセスにインジェクトされたDLLの分が出ないのです。 ブレークポイントも効きません(当然だけど)。 というわけで、どうしましょう。
私はNTしか知らないのでNT限定ということになってしまいます。9xの人はごめん。
もしマシンが2台あれば、カーネルデバッガを設定しましょう。
全てのデバッグ出力は、カーネルデバッガで見ることが出来ます。
カーネルデバッガなんて大げさな。という向きには、スタンドアロンで解決する方法もあります。 ntsdというユーザモード用のデバッガをプロセスにアタッチしてやると、目的のプロセスにインジェクトされたDLLの出力を見ることが出来るようになります。 ntsdはWindows 2000のCDのSUPPORTというディレクトリの下に入っているので、パスの通っている場所にコピーしておいてください。
まずは、tlistというリソースキットに入っているコマンドなど使うか、タスクマネージャを使ってプロセスIDを取得します。 プロセスIDが分かったら、次のようにしてntsdを起動します。
ntsd -p pid -g
-p オプションがpidを指定してプロセスにデバッガをアタッチするオプションで、-g はイニシャルブレークなしに実行を継続するオプションです。 これで、目的のプロセスにインジェクトされたDLLのデバッグ出力を見ることが出来ます。
あるいは、最初からntsdの下でプロセスを起動することも出来ます。
ntsd -g app.exe
ntsdはデバッガなので、当然デバッグも出来ます。 しかし、IDEのようなソースコードデバッガではなく、アセンブリレベルのデバッガなので、x86アセンブラに関する知識が必要となります。 ただし、ntsdはVC++の吐いたシンボルファイル|プログラムデータベース(*.pdb)が扱えるので、シンボリックデバッグまでは可能です。
-g オプションをつけないで起動するか、プロセスにフォーカスをあわせて F12 を押すと、デバッガのプロンプトへ落ちます。 そこで !sympath というコマンドを使って、シンボルのあるパスを指定します。 Windows 2000のシンボル(CDに入っています)と同時に扱いたい場合は、パスをセミコロンで区切って複数指定します。
!sympath を設定して !reload yourdll.dll とタイプすると、yourdll.pdb というシンボルファイルを探しに行きます。 x yourdll!* で、yourdll.dll の中にあるシンボルの一覧が表示されます。
これ以上はこのページにそぐいませんが、少しだけ書いておくと、
bp アドレス または シンボル |
ブレークポイントの設定
|
bl |
ブレークポイントのリスト
|
be ブレークポイントの番号 |
ブレークポイントを有効にします。引数は、bl で表示されるbpの番号。
|
bd ブレークポイントの番号 |
ブレークポイントを一時的に無効にします。
|
bc ブレークポイントの番号 |
ブレークポイントをクリアします。
|
d |
ダンプ。ddでDWORD、dwでWORD、dbでBYTE。dcでBYTE兼キャラクタ。
|
u [アドレス] |
ディスアセンブル
|
r [レジスタ名] |
レジスタダンプ
|
kb |
スタックトレースの表示。ただの k、kv などバリエーションがある。
|
q |
終了(デバッグ対象のプロセスも終了しちゃうので注意)
|
? |
ヘルプ |
DOSの頃を知っている人には、symdebのNT版と言えば通りが早いかも。 もちろん、symdebよりはずっと強力になっています。
ホーム ざれごと ワシントン州 ツール NT豆知識 Win32プログラミングノート 私的用語 ジョーク いろいろ ゲーム雑記 Favorites 掲示板 Mail