#navi_header|Assembler| これまでDOSやBIOS上でのアセンブラプログラミングを体験してきました。 本記事ではWindows上での32bitアセンブラプログラミングを体験してみます。 アセンブラはNASMを使い、リンカはVisualC++2008 Express Edition SP1のlink.exeを使います。 #more|| #outline|| ---- * "Hello, World!" printf()版 まずはC言語での定番、printf("Hello, World!") をアセンブラで書いてみましょう。 main(), printf()ともに呼び出し規約はcdeclなので、アセンブラレベルでは"_"を頭につけて、CRTライブラリとのリンク時に名前解決出来るようにしておきます。また "_main"はCRTライブラリ側からリンクされる為"global"を指定して公開します。"_printf"については本体がCRTライブラリにあるため、"extern"を指定して他のモジュールからインポートするシンボルであることを明示します。 参考:[[617]] hello1.asm: #pre||> global _main extern _printf section .text _main: PUSH EBP MOV EBP, ESP PUSH msg CALL _printf MOV EAX, 2 MOV ESP, EBP POP EBP RET section .data msg: db `Win32\r\nWorld!`, 0 ||< コンパイル: > nasm -fwin32 hello1.asm ここで注意ですが、出力フォーマット"-f"オプションには"win32"を指定します。歴史的な理由で、"obj"を指定するとOMF(Borlandコンパイラなどが使うオブジェクトファイル形式で、COFFとは異なる)、また"coff"を指定するとDJGPPというDOS用に移植したgcc向けのフォーマットで出力されてしまいます。 紛らわしいですが、Visual C++ のリンカで扱えるCOFF形式で出力するには、"-fwin32"を指定します。 これでhello1.objが生成されます。リンクして実行してみます。"_main"があるので自動的にコンソールアプリケーションとしてリンクされます。msvcrt.libを明示的に指定します。 > link /OUT:hello1.exe hello1.obj msvcrt.lib > hello1.exe Win32 World! > echo %ERRORLEVEL% 2 "_main"の戻り値(EAX)は、コマンドプロンプト上では"%ERRORLEVEL%"環境変数として取得出来ます。 なおリンクではマルチスレッド対応のDLLリンク形式である"msvcrt.lib"を使っています。静的リンクやデバッグバージョンを使う場合は他のCRTライブラリを指定します。VC++が提供しているCRTライブラリについては下記MSDNを参照して下さい。 - "MSDN Library" > "Development Tools and Languages" > "Visual Studio 2010" > "Visual Studio" > "Visual Studio Languages" > "Visual C++" > "Visual C++ Reference" > "Visual C++ Libraries Reference" > "Run-Time Library" > "C Run-Time Libraries" -- http://msdn.microsoft.com/en-us/library/abx4dbyh.aspx * "Hello, World!" Win32 API - MessageBox()版 今度はGUIアプリケーションとして Win32API の MessageBox() 関数で"Hello, World!"を表示してみます。 MessageBox()にもASCII版のMessageBoxA()とUnicode版のMessageBoxW()がありますが、今回はASCII版を使います。 呼び出し規約はWINAPI、つまりstdcall呼び出し規約を使います。( [[616]] ) WinMain()やMessageBoxA()のシンボル名は、アセンブラ中では"_"を接頭辞とし、"@" + スタック上に積まれた引数のバイト数 を接尾辞とします。WinMain()もMessageBoxA()も4バイトの引数を4つ取りますので、"@16"を後ろにつけます。 hello2.asm: #pre||> global _WinMain@16 extern _MessageBoxA@16 section .text _WinMain@16: PUSH EBP MOV EBP, ESP PUSH 0 PUSH caption PUSH msg PUSH 0 CALL _MessageBoxA@16 MOV EAX, 2 MOV ESP, EBP POP EBP RET 16 section .data caption: db "Hello, ", 0 msg: db `Win32\r\nWorld!`, 0 ||< コンパイル: > nasm -fwin32 hello2.asm リンク: > link /OUT:hello2.exe hello2.obj user32.lib msvcrt.lib 実行するとメッセージボックスが表示され、OKボタンで終了します。 なお、Win32GUIアプリケーションの場合に"ERRORLEVEL"で終了コードを取り出すには一工夫必要です。 というのは、単純にコマンドプロンプトからGUIアプリケーションを起動すると、コマンドプロンプト側はGUIアプリケーションの終了を待ちません。コマンドプロンプトでは続けて別のアプリケーションやコマンドを実行出来るようになっており、GUIアプリケーションの戻り値を受け取れません。 これを回避するには、コマンドプロンプトのstartコマンド + "/wait"オプション付きでGUIアプリケーションを実行します。起動されたGUIアプリケーションの終了を待ち、"ERRORLEVEL"で終了コードを取得することが出来ます。 > start /wait hello2.exe (メッセージボックスのOKボタンをクリックするまで、コマンドプロンプトに戻らない) > echo %ERRORLEVEL% 2 ERRORLEVELの詳細とGUIアプリでの取得方法については、下記リンクなどを参考にして下さい。 - How do I get the application exit code from a Windows command line? - Stack Overflow -- http://stackoverflow.com/questions/334879/how-do-i-get-the-application-exit-code-from-a-windows-command-line --- ERRORLEVEL環境変数が紹介されています。 - Setting the MS-DOS Errorlevel in a Program -- http://support.microsoft.com/kb/57658 --- 元々はMS-DOS時代に INT 21h, AH=4Ch で戻り値を指定していたことが書かれています。 - ERRORLEVEL is not %ERRORLEVEL% -- http://blogs.msdn.com/b/oldnewthing/archive/2008/09/26/8965755.aspx --- ERRORLEVELと環境変数としての%ERRORLEVEL%の違いが書かれています。 - why doesn't winmain set the errorlevel? - Stack Overflow -- http://stackoverflow.com/questions/592075/why-doesnt-winmain-set-the-errorlevel --- WinMain()を使ったGUIアプリケーションの場合のERRORLEVELの取得について書かれています。 * CRTを使わない "Hello, World!" これまでのサンプルではいずれもVisual C++が提供するCRT(C RunTime library)である"msvcrt.lib"をリンクしてきました。今度はCRTを使わないWin32アセンブラプログラミングを体験してみましょう。 CRTを使わない場合、エントリポイントとなる関数を自分で用意する必要があります。通常のWinMain(), main()などはCRTが提供する真のエントリポイントが呼ばれた後、CRTが必要とする様々な前処理やC++ならグローバルオブジェクトのコンストラクタ処理などが呼ばれた後に実行されます。CRTを使わない場合、そうした処理も自前で実装する必要があります。本記事の場合は、サンプルとして極めて単純な処理しか行わないので、そうした下準備は不要となり、すぐにメイン処理に進むことが出来ます。 CRTを使う場合、エントリポイントのアドレスはリンカが自動的に決定してくれます。main()関数が定義してあればコンソールアプリケーションとしてリンクし、CRT中のmainCRTStartup()をエントリポイントに設定します。WinMain()関数が定義してあればGUIアプリケーションとしてリンクし、CRT中のWinMainCRTStartup()をエントリポイントに設定します。 CRTを使わない場合、自分で用意したエントリポイントを "/ENTRY" リンカオプションで指定します。また、コンソール/GUIアプリケーションの区別も、"/SUBSYSTEM" リンカオプションで指定します。コンソールアプリケーションの場合のエントリポイントはcdel規約、GUIアプリケーションの場合のエントリポイントはstdcall規約にします。 CRT中のエントリポイントと "/ENTRY" リンカオプションの詳細は下記MSDNを参照して下さい。 - "MSDN Library" > "Development Tools and Languages" > "Visual Studio .NET" > "Visual C++" > "Building a C/C++ Program" > "C/C++ Building Reference" > "Linking" > "Linker Options" > "/ENTRY (Entry-Point Symbol)" -- http://msdn.microsoft.com/en-us/library/f9t8842e.aspx ** "Hello, World!" console without CRT まずコンソール版をCRT無しにしてみます。CRTが無いとprintf()等が使えなくなりますので、Win32 APIだけでコンソールに出力してみます。 GetStdHandle(STD_OUTPUT_HANDLE)で標準出力のファイルハンドルを取得し、WriteFile()を使って文字列を書き込みます。両APIの詳細についてはMSDNを参照して下さい。 ソースコードは次のようになります。 hello3.asm: #pre||> global _MyEntry extern _GetStdHandle@4 extern _WriteFile@20 section .text _MyEntry: PUSH EBP MOV EBP, ESP SUB ESP, 8 ; スタック上に確保したローカル変数を見やすくする為のマクロ %define hFile EBP - 4 %define lpNumberOfBytesWritten EBP - 8 ; HANDLE hFile = GetStdHandle(STD_OUTPUT_HANDLE); PUSH -11 ; STD_OUTPUT_HANDLE CALL _GetStdHandle@4 MOV [hFile], EAX ; WriteFile() arguments ; __inout_opt LPOVERLAPPED lpOverlapped PUSH 0 ; __out_opt LPDWORD lpNumberOfBytesWritten LEA EAX, [lpNumberOfBytesWritten] PUSH EAX ; __in DWORD nNumberOfBytesToWrite MOV EAX, [msg_len] PUSH EAX ; __in LPCVOID lpBuffer PUSH msg ; __in HANDLE hFile MOV EAX, [hFile] PUSH EAX CALL _WriteFile@20 MOV ESP, EBP POP EBP RET section .data msg: db `Win32\r\nWorld!`, 0 msg_len: dd $ - msg ||< コンパイル+リンク+実行: > nasm -fwin32 hello3.asm > link /OUT:hello3.exe /ENTRY:MyEntry /SUBSYSTEM:CONSOLE hello3.obj kernel32.lib > hello3 Win32 World! > echo %ERRORLEVEL% 1 WriteFile()が成功を返したEAX = 1がそのままプロセスの戻り値になっていることが確認出来ます。 ** "Hello, World!" win32-gui without CRT 続いてMessageBox()を使ったサンプルを、CRT無しに対応させてみます。 こちらは元々MessageBoxA()呼び出しだけなので、そのままWinMainをエントリポイントに指定出来ます。 hello2.asmそのままでは面白くないので、"WinMain"を"MyWinMain"にリネームし、戻り値も変えてみました。 hello4.asm: #pre||> global _MyWinMain@16 extern _MessageBoxA@16 section .text _MyWinMain@16: PUSH EBP MOV EBP, ESP PUSH 0 PUSH caption PUSH msg PUSH 0 CALL _MessageBoxA@16 MOV EAX, 4 MOV ESP, EBP POP EBP RET 16 section .data caption: db "Hello, ", 0 msg: db `Win32\r\nWorld!`, 0 ||< コンパイル+リンク+実行: > nasm -fwin32 hello4.asm > link /OUT:hello4.exe /ENTRY:MyWinMain /SUBSYSTEM:WINDOWS hello4.obj user32.lib > start /wait hello4.exe (メッセージボックスのOKボタンをクリックするまで、コマンドプロンプトに戻らない) > echo %ERRORLEVEL% 4 * まとめ NASMとVC++の提供するリンカを使って、Win32アセンブラプログラミングを体験しました。 今回はVC++のリンカを使いましたが、フリーのWin32用リンカとして"ALink"や"GoLink"などもあります。また"GoLink"開発元では、アセンブラ/リソースコンパイラ/デバッガも開発されています。 - Jeremy Gordon's Windows+assembler source page -- http://www.godevtool.com/ - ALINK -- http://alink.sourceforge.net/ NASMでのWin32アセンブラプログラミングはWeb上にも資料が豊富に存在し、お手軽・お気軽に始められる環境だと言えます。 但し記事によってはNASM以外のツールチェインが異なる場合があり、GoLinkを使っていたりMSYS/MinGW環境だったりしますので、そこだけ注意が必要でしょう。 #navi_footer|Assembler|