Windowsの開発環境を構築し、コンパイル・デバッグをやってみる

WindowsVisual Studio Community 2013とDebugger Tools for Windowsをインストールし、コンパイラ一式とデバッガを使えるようにする。 また、簡単なC言語コードを書き、コマンドラインからのコンパイルデバッグをやってみる。

環境

Windows 8.1 Pro 64ビット版

コマンドプロンプトからsysteminfoコマンドを実行することで、システム情報が確認できる。

>systeminfo
OS 名:                  Microsoft Windows 8.1 Pro
OS バージョン:          6.3.9600 N/A ビルド 9600
OS ビルドの種類:        Multiprocessor Free
システムの種類:         x64-based PC
プロセッサ:             1 プロセッサインストール済みです。
                        [01]: Intel64 Family 6 Model 69 Stepping 1 GenuineIntel ~1596 Mhz

Visual Studio Community 2013のインストール

Visual Studioでは、個人開発者を対象にCommunity版が無償で提供されているので、これを利用する。 ここでは、標準構成にてインストールする。

f:id:inaz2:20150411050956p:plain

開発用コンソールの準備

後述するCLIコンパイラなどを利用するためには、アプリ一覧の「Visual Studio Tools」の中にある開発用コンソール(Developer Command Prompt for VS2013)を使う必要がある。 そのまま起動した場合カレントディレクトリがコンパイラのあるフォルダとなるが、毎回作業フォルダに移動するのは面倒なので、次のようにして開発用フォルダをカレントディレクトリとするショートカットを準備する。

  • C:\Program Files (x86)\Microsoft Visual Studio 12.0\Common7\Tools\Shortcutsにある「Developer Command Prompt for VS2013」のショートカットを開発用フォルダにコピーする
  • コピーしたショートカットの「作業フォルダー」を空欄に変更する

f:id:inaz2:20150411190051p:plain

WinDbg/CDBのインストール

Windowsで使えるデバッガにはOllyDbgImmunity Debuggerx64dbgなどがあるが、ここではMicrosoftが提供するWinDbgおよびそのCLI版であるCDBを利用する。 これらはWindows 8.1 SDKにおいて「Debugging Tools for Windows」として提供されており、インストール中の選択画面より単独でインストールすることができる。

f:id:inaz2:20150411051024p:plain

環境変数の登録

コマンドプロンプトからCDBを使えるようにするために、PATH環境変数にCDBの置かれたパスを追加する。 また、必要に応じてシンボル情報を取得できるよう、_NT_SYMBOL_PATH環境変数Microsoft public symbol serverを設定する。

「システムの詳細設定」から「環境変数」を選択し、ユーザー環境変数として次を設定する。 PATH環境変数がすでに設定されている場合は、既存のパスの後ろにセミコロンを挟んで追記すればよい。

f:id:inaz2:20150411190103p:plain

PATH             C:\Program Files (x86)\Windows Kits\8.1\Debuggers\x86
_NT_SYMBOL_PATH  srv*C:\Symbols*http://msdl.microsoft.com/download/symbols

コマンドラインからのコンパイルデバッグをやってみる

動作確認も兼ねて、コマンドラインからclでプログラムをコンパイルし、cdbでデバッグしてみる。

C言語でメッセージボックスを表示するプログラムを書いてみる

次のようなコードを書き、開発用フォルダにhello.cとして保存する。

/* hello.c */
#include <windows.h>
#pragma comment(lib, "user32")

int main()
{
    MessageBox(NULL, "Hello, world!", "Hello, caption", MB_OK);
    return 0;
}

MessageBox関数などのWindows APIの定義は、MSDNから調べることができる。

上のドキュメントよりMessageBox関数がuser32.dllにて実装されていることがわかるが、clは標準で対応するインポートライブラリuser32.libをリンクしない。 そこで、ここではソースコード中にpragma commentを書くことにより、リンカにuser32.libをリンクするよう指定している。

開発用コンソールからC言語コードをコンパイルするには、clを使う。 ここでは、/nologoオプションでコピーライトメッセージを非表示にし、/Ziオプションを指定することでデバッグ用のシンボルファイル(pdb)も書き出すようにする。

>cl /Zi hello.c
Microsoft (R) C/C++ Optimizing Compiler Version 18.00.31101 for x86
Copyright (C) Microsoft Corporation.  All rights reserved.

hello.c
Microsoft (R) Incremental Linker Version 12.00.31101.0
Copyright (C) Microsoft Corporation.  All rights reserved.

/out:hello.exe
/debug
hello.obj

同一フォルダにhello.exeが作られるので実行してみる。

>hello.exe

指定した内容のメッセージボックスが表示されることが確認できる。

CDBでデバッグしてみる

デバッガには、GUIwindbgCLIのcdb(と新しいコンソールでcdbを開くntsd)がある。

cdbのもとでプログラムを実行するには次のようにする。 なお、初回起動時は各DLLのシンボル情報を環境変数で設定したシンボルサーバから取得するため、多少時間がかかる。

>cdb hello.exe

Microsoft (R) Windows Debugger Version 6.3.9600.17298 X86
Copyright (c) Microsoft Corporation. All rights reserved.

CommandLine: hello.exe

************* Symbol Path validation summary **************
Response                         Time (ms)     Location
Deferred                                       srv*C:\Symbols*http://msdl.micros
oft.com/download/symbols
Symbol search path is: srv*C:\Symbols*http://msdl.microsoft.com/download/symbols

Executable search path is:
ModLoad: 00f90000 00fbe000   hello.exe
ModLoad: 77e60000 77fc7000   ntdll.dll
ModLoad: 76700000 76840000   C:\WINDOWS\SysWOW64\KERNEL32.DLL
ModLoad: 75980000 75a50000   C:\WINDOWS\SysWOW64\KERNELBASE.dll
ModLoad: 76250000 7639f000   C:\WINDOWS\SysWOW64\USER32.dll
ModLoad: 76850000 76957000   C:\WINDOWS\SysWOW64\GDI32.dll
(4450.2b24): Break instruction exception - code 80000003 (first chance)
eax=00000000 ebx=00000000 ecx=916f0000 edx=00000000 esi=7f16b000 edi=00000000
eip=77f0e67f esp=00def60c ebp=00def638 iopl=0         nv up ei pl zr na pe nc
cs=0023  ss=002b  ds=002b  es=002b  fs=0053  gs=002b             efl=00000246
ntdll!LdrpDoDebuggerBreak+0x2b:
77f0e67f cc              int     3
0:000>

?コマンドでコマンドの一覧が確認できる。

0:000> ?

Open debugger.chm for complete debugger documentation

B[C|D|E][<bps>] - clear/disable/enable breakpoint(s)
BL - list breakpoints
BA <access> <size> <addr> - set processor breakpoint
BP <address> - set soft breakpoint
D[type][<range>] - dump memory
DT [-n|y] [[mod!]name] [[-n|y]fields]
   [address] [-l list] [-a[]|c|i|o|r[#]|v] - dump using type information
DV [<name>] - dump local variables
E[type] <address> [<values>] - enter memory values
G[H|N] [=<address> [<address>...]] - go
K <count> - stacktrace
KP <count> - stacktrace with source arguments
LM[k|l|u|v] - list modules
LN <expr> - list nearest symbols
P [=<addr>] [<value>] - step over
Q - quit
R [[<reg> [= <expr>]]] - view or set registers
S[<opts>] <range> <values> - search memory
SX [{e|d|i|n} [-c "Cmd1"] [-c2 "Cmd2"] [-h] {Exception|Event|*}] - event filter
T [=<address>] [<expr>] - trace into
U [<range>] - unassemble
version - show debuggee and debugger version
X [<*|module>!]<*|symbol> - view symbols
? <expr> - display expression
?? <expr> - display C++ expression
$< <filename> - take input from a command file

(snip)

bpコマンドでhello.exeのmain関数にブレークポイントをセットし、gコマンドで実行を継続する。 ブレークポイントを設定する際には、モジュール名の後に!をつけた後シンボル名を指定する必要があることに注意。

0:000> bp hello!main
*** WARNING: Unable to verify checksum for hello.exe
0:000> g
ModLoad: 766d0000 766f5000   C:\WINDOWS\SysWOW64\IMM32.DLL
ModLoad: 76150000 76247000   C:\WINDOWS\SysWOW64\MSCTF.dll
ModLoad: 75f40000 75ffe000   C:\WINDOWS\SysWOW64\msvcrt.dll
Breakpoint 0 hit
eax=0103d068 ebx=00000000 ecx=00000001 edx=00000000 esi=00000000 edi=00000000
eip=00f91010 esp=00defa68 ebp=00defaac iopl=0         nv up ei pl zr na pe nc
cs=0023  ss=002b  ds=002b  es=002b  fs=0053  gs=002b             efl=00000246
hello!main:
00f91010 55              push    ebp

コールスタックを表示するにはkコマンドを使う。 RetAddrはその関数からのリターンアドレスであり、一つ下の段の関数内のアドレスが入る。 また、kbとするとそれぞれの関数の第3引数までの値も表示することができる。

0:000> k
ChildEBP RetAddr
00defa64 00f91136 hello!main
00defaac 7671919f hello!__tmainCRTStartup+0xfe
00defab8 77eb0bbb KERNEL32!BaseThreadInitThunk+0xe
00defafc 77eb0b91 ntdll!__RtlUserThreadStart+0x20
00defb0c 00000000 ntdll!_RtlUserThreadStart+0x1b
0:000> kb
ChildEBP RetAddr  Args to Child
00defa64 00f91136 00000001 01037060 0103d068 hello!main
00defaac 7671919f 7f16b000 00defafc 77eb0bbb hello!__tmainCRTStartup+0xfe
00defab8 77eb0bbb 7f16b000 31fdcc26 00000000 KERNEL32!BaseThreadInitThunk+0xe
00defafc 77eb0b91 ffffffff 77e9c9c0 00000000 ntdll!__RtlUserThreadStart+0x20
00defb0c 00000000 00f911f9 7f16b000 00000000 ntdll!_RtlUserThreadStart+0x1b

レジスタの値を調べるにはrコマンド、メモリ上のデータを調べるにはdcコマンドを使う。 dccは「dword and Char」を意味し、db(byte)とするとバイト単位での表示、da(ascii)とするとASCII文字列として表示することができる。

0:000> r
eax=0103d068 ebx=00000000 ecx=00000001 edx=00000000 esi=00000000 edi=00000000
eip=00f91010 esp=00defa68 ebp=00defaac iopl=0         nv up ei pl zr na pe nc
cs=0023  ss=002b  ds=002b  es=002b  fs=0053  gs=002b             efl=00000246
hello!main:
00f91010 55              push    ebp
0:000> dc esp
00defa68  00f91136 00000001 01037060 0103d068  6.......`p..h...
00defa78  46f3564c 00000000 00000000 7f16b000  LV.F............
00defa88  ffffe001 00000000 00000000 00defa78  ............x...
00defa98  00000000 00defaec 00f92800 46d6f120  .........(.. ..F
00defaa8  00000000 00defab8 7671919f 7f16b000  ..........qv....
00defab8  00defafc 77eb0bbb 7f16b000 31fdcc26  .......w....&..1
00defac8  00000000 00000000 7f16b000 d4423dd8  .............=B.
00defad8  ffffe001 10804710 fffff580 00defac4  .....G..........
0:000> dc 01037060
01037060  01037068 00000000 6c6c6568 78652e6f  hp......hello.ex
01037070  abab0065 abababab feeeabab feeefeee  e...............
01037080  00000000 00000000 3d790746 1900f861  ........F.y=a...
01037090  554c4c41 53524553 464f5250 3d454c49  ALLUSERSPROFILE=
010370a0  505c3a43 72676f72 61446d61 ab006174  C:\ProgramData..
010370b0  abababab 00ababab 00000000 00000000  ................
010370c0  3b7a0743 0000f860 01036d28 010300c0  C.z;`...(m......
010370d0  3f790744 1800f865 010374f8 01036730  D.y?e....t..0g..
0:000> da 01037068
01037068  "hello.exe"

上の結果から、argv = {"hello.exe", NULL}となっていることを確認できる。

また、poi()関数を使うとそのアドレスに入っている値を取り出す、すなわち、ポインタとみなしてdereferenceすることができる(gdbにおける{void *}に対応)。 これを使うと、上と同じ操作をアドレスを直接指定することなく行うことができる。

0:000> dc esp
00defa68  00f91136 00000001 01037060 0103d068  6.......`p..h...
00defa78  46f3564c 00000000 00000000 7f16b000  LV.F............
00defa88  ffffe001 00000000 00000000 00defa78  ............x...
00defa98  00000000 00defaec 00f92800 46d6f120  .........(.. ..F
00defaa8  00000000 00defab8 7671919f 7f16b000  ..........qv....
00defab8  00defafc 77eb0bbb 7f16b000 31fdcc26  .......w....&..1
00defac8  00000000 00000000 7f16b000 d4423dd8  .............=B.
00defad8  ffffe001 10804710 fffff580 00defac4  .....G..........
0:000> dc poi(esp+8)
01037060  01037068 00000000 6c6c6568 78652e6f  hp......hello.ex
01037070  abab0065 abababab feeeabab feeefeee  e...............
01037080  00000000 00000000 3d790746 1900f861  ........F.y=a...
01037090  554c4c41 53524553 464f5250 3d454c49  ALLUSERSPROFILE=
010370a0  505c3a43 72676f72 61446d61 ab006174  C:\ProgramData..
010370b0  abababab 00ababab 00000000 00000000  ................
010370c0  3b7a0743 0000f860 01036d28 010300c0  C.z;`...(m......
010370d0  3f790744 1800f865 010374f8 01036730  D.y?e....t..0g..
0:000> da poi(poi(esp+8))
01037068  "hello.exe"

ディスアセンブル結果を表示するには、uコマンドを使う。 引数がない場合は、eipからの結果が表示される。

0:000> u
hello!main:
00f91010 55              push    ebp
00f91011 8bec            mov     ebp,esp
00f91013 6a00            push    0
00f91015 680070fb00      push    offset hello!__rtc_tzz <PERF> (hello+0x27000) (
00fb7000)
00f9101a 681070fb00      push    offset hello!__rtc_tzz <PERF> (hello+0x27010) (
00fb7010)
00f9101f 6a00            push    0
00f91021 ff1594b1fb00    call    dword ptr [hello!_imp__MessageBoxA (00fbb194)]
00f91027 33c0            xor     eax,eax

pコマンドでstep over、tコマンドでtrace intoができる。 前者は関数の呼び出し先に入らず、後者は入る。 つまり、それぞれgdbにおけるnextistepiに対応する。

0:000> p
eax=0103d068 ebx=00000000 ecx=00000001 edx=00000000 esi=00000000 edi=00000000
eip=00f91011 esp=00defa64 ebp=00defaac iopl=0         nv up ei pl zr na pe nc
cs=0023  ss=002b  ds=002b  es=002b  fs=0053  gs=002b             efl=00000246
hello!main+0x1:
00f91011 8bec            mov     ebp,esp
0:000> [ENTER]
hello!main+0x3:
00f91013 6a00            push    0
0:000> [ENTER]
hello!main+0x5:
00f91015 680070fb00      push    offset hello!__rtc_tzz <PERF> (hello+0x27000) (
00fb7000)
0:000> [ENTER]
hello!main+0xa:
00f9101a 681070fb00      push    offset hello!__rtc_tzz <PERF> (hello+0x27010) (
00fb7010)
0:000> [ENTER]
hello!main+0xf:
00f9101f 6a00            push    0
0:000> [ENTER]
hello!main+0x11:
00f91021 ff1594b1fb00    call    dword ptr [hello!_imp__MessageBoxA (00fbb194)]
ds:002b:00fbb194={USER32!MessageBoxA (762c27ce)}
0:000> t
eax=0103d068 ebx=00000000 ecx=00000001 edx=00000000 esi=00000000 edi=00000000
eip=762c27ce esp=00defa50 ebp=00defa64 iopl=0         nv up ei pl zr na pe nc
cs=0023  ss=002b  ds=002b  es=002b  fs=0053  gs=002b             efl=00000246
USER32!MessageBoxA:
762c27ce 8bff            mov     edi,edi
0:000> kb
ChildEBP RetAddr  Args to Child
00defa4c 00f91027 00000000 00fb7010 00fb7000 USER32!MessageBoxA
00defa64 00f91136 00000001 01037060 0103d068 hello!main+0x17
00defaac 7671919f 7f16b000 00defafc 77eb0bbb hello!__tmainCRTStartup+0xfe
00defab8 77eb0bbb 7f16b000 31fdcc26 00000000 KERNEL32!BaseThreadInitThunk+0xe
00defafc 77eb0b91 ffffffff 77e9c9c0 00000000 ntdll!__RtlUserThreadStart+0x20
00defb0c 00000000 00f911f9 7f16b000 00000000 ntdll!_RtlUserThreadStart+0x1b

コマンドを何も指定せずにENTERを押した場合は、前のコマンドの繰り返しとなる。 また、dcuコマンドでは、引数なしのコマンドを繰り返すとさらに先に進めた結果が表示される。

gコマンドで実行を継続しメッセージボックスが表示されるのを確認した後、qコマンドでCDBを終了する。

0:000> g
ModLoad: 75640000 7571b000   C:\WINDOWS\SysWOW64\uxtheme.dll
ModLoad: 76580000 766ce000   C:\WINDOWS\SysWOW64\combase.dll
ModLoad: 76440000 764f0000   C:\WINDOWS\SysWOW64\RPCRT4.dll
ModLoad: 75880000 7589d000   C:\WINDOWS\SysWOW64\SspiCli.dll
(snip)
eax=00000000 ebx=77f5a820 ecx=00000000 edx=00000000 esi=00000000 edi=01039a54
eip=77e9a7bc esp=00def924 ebp=00def9f0 iopl=0         nv up ei pl nz na pe nc
cs=0023  ss=002b  ds=002b  es=002b  fs=0053  gs=002b             efl=00000206
ntdll!NtTerminateProcess+0xc:
77e9a7bc c20800          ret     8
0:000> q
quit:

関連リンク