Win32 MASM プログラミング入門

Written by けんいち

Updated : 2004/08/29 - 21:34:29 , Ver 1.10


1 下準備 Win32プログラミング始める前に
+ 注意事項
+ MASMを手に入れる
+ パッチを当てる
+ VC++でMASMを使えるようにする
+ 最初のMASMプログラム
2 ハードウェアの仕組み コンピュータについて
+ ハードウェアについて
+ アセンブラでできること
+ レジスタについて
+ その他レジスタ
+ メモリ
+ スタック
+ I/0・割り込み
+ BIOS/システムコール/VRAM
3 Windows プログラミング Windowsプログラミング
+ Windowsプログラミング
+ 一番最初のプログラム
+ インスタンスハンドルについて
+ ローカル変数
+ ウィンドウの作成
4 リソースの利用 リソースを使う場合
+ リソースの使用法
+ インクルードファイル
+ アイコンとダイアログボックスを使った例
5 チップス ちょっとしたヒント
+ インクルードファイルの種類
+ AとWの違い、呼び出し規約
+ プロシージャのプロトタイプ宣言
+ NULLと'\0'
+ ExitProcessについて
6 テキストビュワー テキストビュワーを作成
+ テキストビュワーのプログラム
7 FPUで数値演算 浮動小数点演算について
+ 浮動小数点演算について
+ FPU用レジスタについて
+ プログラミングの仕方
+ 実際のプログラミング
8 MMXでSIMD演算 MMXを使う
+ MMX演算の概要
+ MMXが使えるかどうかチェック
+ 飽和演算・非飽和演算
+ 実際のプログラム・加算ブレンド

Chapter 1   下準備

Abstract : Win32プログラミング始める前に

Chapter 1 - 1   注意事項 : Updated on 2004/08/29

MASM プログラミングするにあたって、自己の責任でプログラミングしてください。 MASM は低レベルの命令を実行できますので、バグが発生した場合簡単にコンピュータに とって致命的なエラーとなりえます。 もしかするとPC自体が動かなくなってしまうこともあるかもしれません。 そういった場合には、

このサイトを参考にしたとしても、生じた損害に対し 私は一切責任を負いかねますので、自己責任でプログラミングを行ってください。

注意:この文章を書いたのは2000年〜2001年頃です。テスト環境としてはWindows98で テストしています。現在と比べて内容的に古くなって いるかも知れません。間違っている部分や違っている部分など意見や感想などがありましたら、 掲示板もしくはメールの方へよろしくお願いします。

また、不定期に内容が変更される場合があります。

Chapter 1 - 2   MASMを手に入れる : Updated on 2004/08/29

さて、やっと始まりました、MASMプログラミング。長らくお待たせしました。書いてる本人も勉強しながら書いているので、あいまいなところもあると思いますが、そこらへんは許してね。変なところがあったら指摘してください。

MASM とは、マイクロソフト社製のアセンブラツールです。今ではフリーで提供されていますので、アセンブラの勉強をしたいなと思っている人には最適です。ぜひ勉強してみましょう。コンピュータについて今まで分からなかったことが分かるかもしれません。しかしながら、いかんせん、MASM 解説のページというのは本当に少ないですね(2000年当時本当に少なかった。最近どんどん増えてきました)。 趣味でやるにはなかなか手がつけづらいです。まあ、それがここを作るきっかけになったわけです。ここでは、基本的なことしかやりませんが、もっとMASM をやってみたいという方は、いろいろなところから専門書が出ていますので、そちらを参照してください。

まずはじめに、MASM を手に入れなければなりません。 上述のとおり、現在ではフリーで提供されていますので、 マイクロソフトのサイトからゲットしてきましょう。 マイクロソフトの DDK (Driver Development Kit) というものの中に内包されています。この DDK は、20メガ以上あるので、 ダウンロードするだけで結構時間がかかります。古い MASM であるなら、 ほかのサイトに MASM 単体でおいてある場合があるので、 そこからゲットしてきましょう。

DDK に入っている MASM はバージョンが古く MMX 命令が使えないので、 バージョンをあげるため「パッチ」を当てなければなりません。 この「パッチ」もマイクロソフトのサイトにあるので落として解凍し、 解凍されたファイルをすべて MASM のあるディレクトリの中に入れて 「パッチ」実行ファイルを実行してください。 これについては後で詳しく説明します。

MASM のバージョンが最新のものになったなら、 VC++ に組み込んで MASM を使えるようにしましょう。 環境に取り込んでしまえば、自分で手作業でアセンブリする手間が省け、 リンクも自動的にやってくれます。これも、後で詳しく説明します。

まず最初にしなければならないことは、 MASM をゲットしなければいけないことです。以下のサイトから、 MASM をゲットしてきてください。

サイト名 説明
Microsoft DDK Site DDK(27M) があります
MASM32 Site MASM のみを手に入れるならここ
MASM Patch Patch をつかってバージョンをあげます。いらなかったら落とさなくてもいいです
Intel CPU マニュアル PDF ファイルで上・中・下の三冊あります。一応落としておいたほうがいいかもしれません

この他にも、いくつかダウンロードサイト(公認かどうかは謎)があるので、そちらから落としてきてもいいです。

さて、MASM をゲットしましたか?MASM のプログラムファイル名は ML.EXE という名前です。このプログラムにソースファイルを食わせることによって、バイナリファイルを吐き出してくれます。バイナリを吐き出してくれたら、後は必要なリンクを行い、実行形式にすれば実行することが出来ます。

-----MASMについてヴォーガさんか情報を頂いたので載せておきます-----

 はじめまして。ヴォーガと申します。プログラミング関連、特に MASM の
ページは大変貴重で、参考にさせていただいています。ありがとうございます。
 ちょっとだけ気になったのですが、MASM の入手方法のところ。DDK を入手
する前提で書かれている部分が更新されていませんでした。現在は DDK は米
MS サイトより CD-ROM を注文しなければならないため、かなり面倒です。
Visual C++ 6.0 に MASM を加える最も簡単な(正当な?)方法は、Processor
Pack の追加ではないかと思います。最初から 6.15 が入りますので、パッチ
も不要です。
 SP4 、SP5 専用のため、SP6 導入済みの場合は /C オプションで解凍して手
作業でインストールする必要があります。因みに、SP5 用は何故か米国サイト
にしかありません。
 尚、ご存じとは思いますが、VisualStudio.NET には ML.EXE は標準添付です。
2002 は 7.00 、2003 は 7.10 が付いています。
 以上、参考まで。既にご存じの情報でしたらご容赦を。

http://www.microsoft.com/japan/msdn/vstudio/downloads/ppack/default.asp
http://msdn.microsoft.com/vstudio/downloads/tools/ppack/default.aspx

どうも情報ありがとうございます>ヴォーガさん

Chapter 1 - 3   パッチを当てる : Updated on 2004/08/20

次にパッチを当てて、MASM をバージョンアップさせましょう。MMX 命令を別につかう必要はない!という人は別にしなくてもいいですよ。さて、パッチの当て方ですが、まずパッチを解凍します。実際には ML614.EXE というプログラムを実行します。そうすると必要なファイルが解凍されるので、そのすべてのファイルを MASM 本体のあるディレクトリ(ML.EXE のあるディレクトリ)に入れて、PATCH.EXE を実行します。これでバージョンアップされました。

DDK をインストールした場合、MASM は 98DDK\bin\win98 ディレクトリにあるので注意してください。

Chapter 1 - 4   VC++でMASMを使えるようにする : Updated on 2004/08/20

それでは、MASM を VC++ 上で使えるようにしてみましょう。MASM を VC++ で使えるようにするためには、VC++ の「カスタムビルド」を使います。

まずはじめに、VC++ 上で ML.EXE が実行できないといけないので、実行パスを通します。「ツール」の「オプション」を選んで次のように設定します。

やっぱり画像を使うと楽ですね〜。一目瞭然とはこのことをいいますね。上のように実行パスを通しておいてください。「表示するディレクトリ」に注意してね。必ず「実行可能ファイル」を選んでください。

それでは、実際に簡単なプログラムを見てみましょう。最初は、Win 32 プログラミングでおなじみのように、プロジェクトを立ち上げます。新規作成で、Win32 application の空のプロジェクトを作成してください。その次に、MASM のソースコードを作成します。これも新規作成で、拡張子が ASM になるようにファイルを作成します。

次に、カスタムビルドの設定をします。メニューの「設定」を選択し、アセンブラを実行するファイルを選択します。そしてカスタムビルドタブを選んで、次のように設定します。

説明の個所とコマンド、出力のテキストボックスを変更します。コマンドのテキストボックス欄には、

ml /c /coff /Cx /nologo /Fo$(OutDir)\ /Fl$(InputName) /Zi $(InputPath)

とします。これはデバッグモードの場合ですが、リリースモードの場合は、コマンドにある /Zi フラグをはずします。

これで、ビルド実行ボタンを押せば、自動的にアセンブラしリンクも行ってくれます。ちょっと面倒なのは、アセンブラのファイルを作成するごとにカスタムビルドを設定しなければいけないことですね。これはかなり面倒。

もっと詳しい資料はこちら → Microsoft Support Q106399 (英語)

カスタムビルドについて、もっと詳しく知りたいという人がいるなら、MSDN ライブラリにごっそりと資料があるから、そっちをご参照。

Chapter 1 - 5   最初のMASMプログラム : Updated on 2004/08/20

C 言語の勉強のはじめに「Hello World」プログラミングを作るように、 MASM でかんたんなプログラムを作ってみましょう。もちろんわからなくてもかまいません。 まだ、ぜんぜん詳しくやっていないのだから。次第にわかるようになってくるでしょう。

;**********************************************************************
; はじめての MASM プログラミング
; 見てわかるように、コメントはセミコロン
;**********************************************************************

.586
.model flat, stdcall

NULL            EQU     0

MessageBoxA     proto :dword, :dword, :dword, :dword
ExitProcess     proto :dword

.data

TITLE1          DB 'アセンブラのテスト', 0
MESSAGE         DB 'はろ〜わ〜るど', 0

.code
WinMainCRTStartup   proc

    invoke MessageBoxA, NULL, offset MESSAGE, offset TITLE1, 0
    invoke ExitProcess, 0
    ret
WinMainCRTStartup   endp
end

これをアセンブルして実行ファイルにすると、ちゃんとメッセージボックスが表示されたでしょうか。

(注)ソースコードは便宜上色を変えている部分がありますが、 実際には C の開発環境のように色は変わりません。 いづれ、アセンブラ用の開発環境を作りたいなあと思ってます。(色が変わって見やすいやつね)


Chapter 2   ハードウェアの仕組み

Abstract : コンピュータについて

Chapter 2 - 1   ハードウェアについて : Updated on 2004/08/20

さて第2回目です。今回は、ハードウェアの仕組みについてやります。ハードウェアの仕組みといっても、USBの規格はどうなっているかとか、マザーボードはどうなっているかとか、そういうことはしません。第一回目の終わりにも書いたように、ほとんど CPU の仕組みについて説明をしていきます。「少なくとも、これだけは知ってなくてはまずいだろう」といったことを解説していくつもりです。

もちろん、勉強のために MASM のプログラムをちょこっとは紹介するつもりです。お楽しみに!

次の手順で解説していきます。

  1. アセンブラでできること
  2. 記憶領域(レジスタ/メモリ/スタック)
  3. 外部入力と外部出力(I/O)・割り込み
  4. BIOS・システムコール・VRAM

Chapter 2 - 2   アセンブラでできること : Updated on 2004/08/20

まず、なぜアセンブラを使うのでしょう?その理由には、いくつかあります。ほとんどのプログラミングは、C言語で事足りることがほとんどです。一般的に言って、言語が高級になるほど、インタープリタ性が高まって、言語の抽象度が下がり理解しやすくなりますが、その言語で実現できることは、減少し淘汰(制限)されてきます。つまり、Basic と C言語 の間では Basic で実現可能なことは C言語でだいたい実現できます。反対に、C言語で実現可能なことが必ずしも Basic では実現できるとは限らないということができます。同じことがC言語とアセンブラの関係についてもいえます。たとえばC言語では、ハードウェアを直接操作はできません(ここではインラインアセンブラもアセンブラに入れています)。また、デバイスへのプリミティブ(原始的)な操作もC言語では実現しづらいです。そのため、直接ハードウェアを操作したい場合などにアセンブラを使います。直接ハードディスクのトラックやセクタを操作したい場合にアセンブラを使うといったらわかりやすいでしょう。

もちろん、ハード面に直接命令を下すだけにアセンブラを使うわけでもありません。プログラムに高速性を要求する場合、C言語よりもアセンブラのほうが一般的に速いので、こういう場合はアセンブラでプログラムを組みます。しかしながら、アセンブラはあまりに低級すぎるので、プログラミングにはかなりの労力を要し、量も多くなります。それゆえ、アセンブラだけでプログラミングをするのは、現実的ではありません。多くの場合、C言語などの高級(中級)言語と組み合わせて使うことになります。

Chapter 2 - 3   レジスタについて : Updated on 2004/08/20

記憶領域には、3種類があります。レジスタとメモリとスタックです。この他にも、補助記憶装置としてハードディスクやフロッピーディスクなどがあります。アセンブラでは、主にレジスタとスタックを用います。3つの記憶領域について順に説明していきましょう。

レジスタは、CPUの内部にある記憶領域です。CPUの内部にあるので、アクセス速度もかなり速いです。しかしながら、レジスタは、すこししかありません。レジスタの種類には、3つあります。汎用レジスタセグメントレジスタフラグレジスタの3つです。

レジスタ種類 解説
汎用レジスタ 8つあります。プログラムの中で使われます。
セグメントレジスタ 6つあります。セグメントの管理に使われます。
状態レジスタ 1つあります。CPUの状態を記憶しています。一般に変更できません。

プログラミングでは、ほとんど汎用レジスタを使います。汎用レジスタには、次の8種類があります。すべて32ビットです。

レジスタ名 解説
EAX アキュムレーター。数値計算に使われるレジスタ。
EBX ベースレジスタ。補助計算や、その他の用途に使われるレジスタ。
ECX カウンターレジスタ。補助計算や、カウンタ、ループ命令で使われるレジスタ。
EDX データレジスタ。補助計算や、データ用操作用のレジスタ。
ESI ソースインデックス。補助計算や、データ処理命令で使われるレジスタ。
EDI デスティネーションインデックス。補助計算や、データ処理命令で使われるレジスタ。
ESP スタックポインタ。スタック処理で使われるレジスタ。
EBP ベースポインタ。スタック処理で使われるレジスタ。

C言語で変数の修飾子に register を指定できるでしょう?あの修飾子を指定した場合、 変数に ESI(ソースインデックス) と EDI(デスティネーションインデックス) が割り当てられます。

また、計算用のレジスタとして ESP(スタックポインタ)と EBP (ベースポインタ) を使うことはできません。これらのレジスタには、スタックの管理という重要な任務があるためです。 数値計算は、実質6つのレジスタでやることになります。またそれぞれのレジスタには、 固有の役目もあるので、それにあわせてプログラムを組まなければなりません。

32ビットの場合、アキュムレータは EAX で扱いますが下位16ビットを AX として扱うこともできます。 AX はさらに分割されて、上位8ビットを AH 、下位8ビットを AL であらわします。これは、EBX ECX EDX の場合も同様です。

それでは、そろそろ理論ばっかりでは面白くないので、レジスタを使ったプログラムを見てみましょう。

;*****************************************************************
;  レジスタを使ったプログラム
;*****************************************************************

.586                   ; Pentium 用のコードを書く場合必要
.model flat, stdcall

NUM         EQU   1

wsprintfA   proto c :dword,:dword,:dword,:dword,:dword
MessageBoxA proto   :dword,:dword,:dword,:dword
ExitProcess proto   :dword

.data

BUFFER      DB 64 DUP(0)
STRINGS     DB '%d + %d = %d', 0
TITLENAME   DB '答えは??', 0

.code

WinMainCRTStartup proc

    mov eax, NUM     ; アキュムレータに代入
    mov ebx, NUM + 1 ; ベースレジスタに代入
    add eax, ebx     ; アキュムレータとベースレジスタを足す

    invoke wsprintfA, offset BUFFER, offset STRINGS, NUM, ebx, eax
    invoke MessageBoxA, 0, offset BUFFER, offset TITLENAME, 0
    invoke ExitProcess, 0

    ret
WinMainCRTStartup endp

end

足し算をして、メッセージボックスに表示するプログラムです。レジスタは、EAX(アキュムレータ) と EBX(ベースレジスタ) を使用しています。プログラム中にある mov や add をニーモニックといいます。ニーモニックは、マシン語のビット表現を文字表現で表したものです。たとえば mov だったら、MOVe の略で「代入」を意味します。 add は ADD で「加算」を意味します。プログラムではアキュムレータとベースレジスタに数を代入して、その足し算をしています。

MASM では、関数をプロシージャと呼びます。プロシージャの定義は、 proc - endp で行います。とりあえず上のプログラムでは、 WinMainCRTStartup というプロシージャが定義されていて、その中のマシン語命令が実行されていると思ってくれれば結構です。

この関数はスタートアップコードと呼ばれてプログラムの一番最初に呼び出される関数です。通常Cのプログラムを組む場合、スタートアップコードは自動的に作成されてリンクされます。スタートアップコードは、プログラムを初期化したりするのに使われます。VC++ の統合環境を使って MASM プログラムを行う場合には、直接このコードが呼び出されます。VC++ で Windows プログラミングをする場合、スタートアップコードは WinMainCRTStartup 関数ですが、Console 用のプログラムの場合は mainCRTStartup 関数がスタートアップコードになります。また統合環境によって、スタートアップコード関数の名前は異なります。

Chapter 2 - 4   その他レジスタ : Updated on 2004/08/20

状態レジスタ(フラグレジスタ)は、ひとつしかありません。レジスタの内部には、CPU の状態を保存しています。一般に状態レジスタ全体を変更することはできません(スタックを使えば変更できるが、よっぽどのことがない限りしないほうがいい)。フラグの種類は、ステータスフラグ制御フラグシステムフラグの3つがあります。ステータスフラグは、演算の結果の状態が保存されます。ステータスフラグの種類を次に示しましょう。

フラグ名 ビット 解説
CF (Carry Flag) 0 キャリーフラグ。桁上がりするとセットされる。
PF (Parity Flag) 2 パリティフラグ。演算結果が偶数の場合セットされる。
AF (Adjust Flag) 4 アジャストフラグ。2進化10進(BCD)演算で使われる。
ZF (Zero Flag) 6 ゼロフラグ。演算結果がゼロの場合セットされる。
SF (Sign Flag) 7 符号フラグ。演算結果が負の場合セットされる。
OF (Overflow Flag) 11 オーバーフローフラグ。オーバーフローを起こすとセットされる。

一般にステータスフラグは変更できませんが、キャリーフラグのみ変更可能です。ニーモニック STC (SeT Carry flag)、 CLC (CLear Carry flag)命令で変更できます。

制御フラグは DF (Direction Flag ; 方向フラグ) しかありません。ストリング命令(メモリ転送命令)での、メモリ転送方向を指定します。ニーモニックでは、STD (SeT Direction flag) CLD (CLear Direction flag) を使います。

システムフラグでは、仮想8086命令や割り込み制御などに使われますが、ここでは割愛します。興味がある人は、インテルのアーキテクチャマニュアルを参照してください。そちらのほうが、詳しく書いてあります。

それでは、状態レジスタを使った簡単なプログラムを見てみましょう。

;*****************************************************************
;  状態レジスタを使ったプログラム
;*****************************************************************

.586
.model flat, stdcall   ; メモリモデルはフラットで…

NUM         equ   1

wsprintfA   proto c :dword,:dword,:dword
MessageBoxA proto   :dword,:dword,:dword,:dword
ExitProcess proto   :dword

.data

BUFFER      db 64 DUP(0)
STRINGS     db '2の10乗は %d です', 0
TITLENAME   db '2の10乗を解く問題', 0

.code

WinMainCRTStartup proc

    mov eax, NUM
    mov ecx, 10

MYLOOP:
    mov ebx, eax
    add eax, ebx
    dec ecx       ; ecx の値を1減少
    jnz MYLOOP    ; ゼロフラグがたたないときはループ

    invoke wsprintfA, offset BUFFER, offset STRINGS, eax
    invoke MessageBoxA, 0, offset BUFFER, offset TITLENAME, 0
    invoke ExitProcess, 0

    ret
WinMainCRTStartup endp

end

前章のプログラムを思いっきりコピペしたような内容ですが(笑)、許してください。とりあえず WinMainCRTStartup 関数の中身を見てみましょう。今回は2の10乗を求める問題です。ECX レジスタの内容を1ずつ減少していって、ECX レジスタが0になったら終了するようなループを 作ってみました。 dec (DECrement) というニーモニックは、レジスタの内容を1減少させる命令です。jnz (Jump if Non-Zero flag) とは、ゼロフラグが立たない間、 指定されたアドレスにジャンプする条件ジャンプ命令です。ここでは MYLOOP というアドレスにジャンプします。MYLOOP のように、名前の後にコロンを付けてアドレスに 名前を指定することができます。

今回は状態フラグの使い方を見るために、面倒な方法で記述しましたが、ループを実現するのに もっと便利なニーモニックが用意されています。 loop (LOOP if ecx is non-zero) というものがあります。上の内容を一括してやってくれます。 書き換えると次のようになります。

; ここから
    mov ecx, 10    ; ループさせたい回数だけ ECX に代入

MYLOOP:
    mov ebx, eax
    mov eax, ebx
    loop MYLOOP    ; ECX を1減じて0になるまで繰り返す
; ここまで

実を言うと、2の階乗をもとめるもっと簡単な方法もあるのです。シフトを使う方法ですね。

さて、レジスタにはもうひとつ種類があります。セグメントレジスタというレジスタです。 セグメントレジスタの種類には次の種類があります。すべて16ビットです。

レジスタ 解説
CS コードセグメント 実行コードが置かれるセグメント
DS ES FS GS データセグメント データが置かれるセグメント
SS スタックセグメント スタックが置かれるセグメント

ES は、エキストラセグメントなんて16ビットのころは呼ばれていましたが、FS DS が加わったことにより、すべて「データセグメント」と呼ばれるようです。

セグメントとは、連続したメモリを必要に応じて分割した、その1つ1つのことを指します。たとえば、1つのプログラムには、実行コードとデータがありますよね?これを分割した領域に置こうというのが、セグメントの考え方です。連続したメモリであるにもかかわらず、まるでメモリ領域がいくつかあるようにメモリを使うことができます。例えば、実行コードは CS セグメントに置かれて データは DS セグメントに置かれます。セグメントは同じ連続したメモリ上に割り当てられているので、セグメント同士が重なり合ってしまうこともあります。

例題3のプログラムを見てみれば .code というのがコードセグメントを定義しています。同様に .data がデータセグメントを定義しています。この他にも .stack などもあって、スタックセグメントも定義することができます。ひとつのセグメントには、4Gバイトまで(!)使うことができます。驚きですね。たとえば CS セグメントだったら CS:00000000h 〜 CS:FFFFFFFFh までの領域があります。もちろん、メモリはこんなにないので使うことはできませんが…

セグメントはコードの大きさに応じてOSに割り当てられているので、勝手に変えてはいけません。

Chapter 2 - 5   メモリ : Updated on 2004/08/20

記憶領域である「メモリ」に値を代入することもできます。 まあとりあえず、プログラムを見てみましょう。

;*****************************************************************
;  メモリを使ったプログラム
;*****************************************************************

.586
.model flat, stdcall  
; stdcall は関数の呼び出しの仕方を定義!

NUM         equ   32

MessageBoxA proto   :dword,:dword,:dword,:dword
ExitProcess proto   :dword

.data

BUFFER      db 128 dup(?)
TITLENAME   db 'メモリに代入する問題', 0

.code

WinMainCRTStartup proc

    mov    ecx, NUM            ; カウンタの回数
    mov    edx, offset BUFFER  ; バッファのポインタ
    mov    ah, 20h
    ;

 _LOOP0:
    mov    byte ptr [edx], ah
    inc    ah
    inc    edx
    loop   _LOOP0

    invoke MessageBoxA, 0, offset BUFFER, offset TITLENAME, 0
    invoke ExitProcess, 0

    ret

WinMainCRTStartup endp

end

またまたくだらないプログラムですいません。今回のプログラムは、 128バイトの静的バッファを確保して、その中に文字を次々に代入していくプログラムです。実際には「 !"#$%&'()*+,-./0123456789:<=>?」という文字列が入ります。 アスキーコードの 20h から 3Fh までですね。メモリに代入する場合には ptr 演算子を使います。[edx]とは edx の指すメモリをあらわします。byte ptr [edx] とは、「edx の指すメモリにバイト値 (8ビット)で値を代入せよ」という意味です。ワード値で値を代入する場合には、 word ptr [edx] となります。

Chapter 2 - 6   スタック : Updated on 2004/08/20

スタックは、後入れ先だし (Last-In First-Out) 型のデータ構造です。アセンブラでスタックは重要な役割を果たします。 レジスタの内容の一時退避に使ったり、プロシージャの呼び出しのときに引数をスタックに入れたりします。 特に C言語やPascal などは、関数の呼び出しで引数をスタックに入れて呼び出しを行います。 スタックがどのようなデータ構造かわからないという方は、「C言語とデータ構造」などの本を参照してください。 結構有名なデータ構造なので、ここでの詳しい説明はしません。

さてそれでは、スタックを用いたプログラムを見てみましょう。

;*****************************************************************
;  スタックを使ったプログラム
;*****************************************************************

.586
.model flat, stdcall

NUM         equ   5

wsprintfA   proto c :dword, :dword, :dword
MessageBoxA proto   :dword, :dword, :dword, :dword
ExitProcess proto   :dword

.data

BUFFER      db 64 dup(0)
TITLENAME   db 'スタックを使う問題', 0
STR1        db 'カウントダウン %d', 0

.code

WinMainCRTStartup proc

    mov     ecx, NUM

 _LOOP0:
    push    ecx       ; SubFunc で ECX を変更しても大丈夫なようにスタックに保存
    call    SubFunc
    pop     ecx
    loop    _LOOP0

    invoke ExitProcess, 0

    ret

WinMainCRTStartup endp

SubFunc proc          ; 新しいプロシージャを定義しよう

    invoke wsprintfA, offset BUFFER, offset STR1, ecx
    invoke MessageBoxA, 0, offset BUFFER, offset TITLENAME, 0

    mov     ecx, 99   ; ここで ECX の値を変えても、スタックに保存してあるから大丈夫

    ret

SubFunc endp

end

今回は、SubFunc という新しいプロシージャ(関数)を作ってみました。 簡単なプログラムなので、特に問題はないでしょう。 push がレジスタの内容をスタックに代入します。 pop はスタックから値をレジスタに代入します。 call はプロシージャの呼び出しを行います。このとき、 現在実行中のアドレスをスタックに代入してからプロシージャにジャンプします。 プロシージャからもどる場合には、ret を使います。ret はスタックから値を取り出して、 呼び出しもとのアドレスに戻ります。

Chapter 2 - 7   I/0・割り込み : Updated on 2004/08/20

CPU で計算するだけでは、コンピュータの本当の機能を引き出すことができません。コンピュータの機能を活用するためには、周りの環境の入力や出力を制御できなければなりません。たとえば、キーボードから入力を受け取ったりディスプレイに結果を表示したりなど。これを制御するために I/O 空間を使います。I/O 空間は16ビットの空間で16進数で言うと 0000h - FFFFh の空間があります (16進数には後置修飾で [h] をつける)。この空間にデータを転送したり(外部出力)、 データを受け取ったり(外部入力)することで、周りの環境にアクセスすることができます。

ウィンドウズには、「システム情報」を表示するプログラムがあります。 「スタートメニュー」の「アクセサリ」の中の「システムツール」の中にあります (ない場合はインストールされていないのでしょう)。この中を見て、どのように I/O 空間が割り当てられているか見ることができます。

割り込みには外部割込みと内部割込みの2種類あります。外部割込み (下図) とは、 外部から入力があった場合、現在のプログラムを一時中止して、特有のプログラムを実行する機能のことです。 たとえば、プログラムAを実行中にキーボードが押されたとしましょう。 この場合キーボードの押されたキーをバッファに保存しておかなければなりません。 そこで外部割込みが発生してバッファに保存されます。もちろん外部割込みが起こる際には、 フラグとレジスタの値はスタックに退避されるので、もともとのプログラムには何の影響もありません。

外部割込みの例

内部割込みの例

これとは違い、内部割込みとはプログラムから明示的にMS-DOSのファンクションコールや、 BIOSのプロシージャを呼び出すことを言います。

プログラムの例を示しておきましょう。

;******************************************************************************
; 内部割込みを使ったプログラム
;   このプログラムはMS-DOS用のプログラムです
;   MS-DEVでは実行できません
;******************************************************************************

.model small, c   ; モデル文を先に記述するとセグメントは16ビットになる
.286

;******************************************************************************
; プロセスの終了マクロ
;******************************************************************************
end_process macro ret_value

            mov   al, ret_value
            mov   ah, 4ch
            int   21h

            endm
;******************************************************************************
; 文字列の表示マクロ
;******************************************************************************
display     macro string

            mov   dx, offset string
            mov   ah, 09h
            int   21h

            endm

;******************************************************************************
; 1文字入力(エコーなし)マクロ
;   AL = キーボードから入力された文字
;******************************************************************************
read_kbd    macro

            mov   ah, 08h
            int   21h

            endm

;******************************************************************************
; データ定義
;******************************************************************************
.data

MSG         db 'このプログラムは即効で終了します', 0dh, 0ah, '$'

;******************************************************************************
; 実行プロセス
;******************************************************************************
.code

START:
            mov   ax, _DATA
            mov   ds, ax

            display     MSG   ; メッセージを表示

            read_kbd          ; ボタン入力待ち

            end_process 0     ; プログラムの終了


end START ; end 擬似命令の後にはスタート地点を記述

コメントにもあるように、MS-DEVからは実行できません。MS-DEVのリンカは32ビットなので、 このプログラムは16ビットのリンカでリンクしなければなりません。16ビットのリンカはこちらからどうぞ。

ftp://ftp.microsoft.com/softlib/mslfiles/lnk563.exe

展開すると16ビットのリンカが現れます。32ビット用のリンカではないので注意。

今回はマクロ定義しています。マクロは関数と違って、呼び出し元に直接展開します。C++のインライン関数みたいなものです。マクロはみれば分かるように macro - endm で定義されています。 ファンクションコールについては、16ビットアセンブラのほうを参照してください。

Chapter 2 - 8   BIOS/システムコール/VRAM : Updated on 2004/08/20

BIOS とはマザーボードについている、低レベルなハードウェア操作をするプログラムのことです。BIOS はソフトウェアとは言われずにファームウェアなんて呼ばれたりします。直接ハードディスクのセクタに書き込んだり読み込んだりする処理や、フロッピーディスクの操作などの低レベルな関数で成り立っています。

システムコールとは、BIOS や OS の低レベル関数を呼び出すことを言います。

VRAM (Video RAM) には、キャラクタ VRAM と グラフィック VRAM があります。キャラクタ VRAM とは、メモリ上の文字をディスプレイに映し出す RAM です。メモリ上に直接文字を入力することで、画面上に文字を同期することができます。グラフィック VRAM とは、メモリ上のデータをグラフィックとして映し出す RAM です。メモリと画面のグラフィックは対応しているので、メモリに書き込むことで、直接絵やドットなどを画面に映し出すことができます。グラフィックボードなどについている VRAM はこのためにあるんですね。

たいていの場合 グラフィックVRAM は、A000 : 0000 に割り当てられています。

とりあえずハードウェアの仕組みはこれで終了です。

よくわかんなかったという方は、「はじめて読む8086」などよくまとまっていてわかりやすいです。買わなくても、図書館に行けばあると思います。

次からは Windows プログラミングをやっていきます。基礎知識としては、Windows API についての知識が必要ですが、Windowsプログラミングのところで紹介しているので、基礎知識がない方は そこで軽く勉強してみてください。ここで詳しく説明することはしません。


Chapter 3   Windows プログラミング

Abstract : Windowsプログラミング

Chapter 3 - 1   Windowsプログラミング : Updated on 2004/08/20

さて、それでは早速MASMでWindowsプログラミングをはじめましょう!

マシン語命令や擬似命令を説明するつもりでしたが、16ビットのところでいくつか紹介しているので、知りたいという方はそちらをご覧ください。C 言語との協調についてもほとんど16ビットと同じですが、ウィンドウズプログラミングをする場合には、関数の呼び出しについて若干相違があります。それについては順次説明していきます。

基礎知識として、Windows API の知識、C 言語の知識、コンピュータの CPU の知識が必要です。それについては、ここでは詳しくやらないので、自分で勉強してください。

それでは、はじめてみましょう。

Chapter 3 - 2   一番最初のプログラム : Updated on 2004/08/20

さてさて、MASMでWindowsプログラミングをしていくわけですが、 一番最初に示したプログラムに解説を加えておきましょう。

;++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
; アセンブラプログラミング
;++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

.486
.model flat, stdcall

;++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
; 定義
;++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
NULL      =       0
MB_OK     =       0

;++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
; プロトタイプ
;++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
MessageBoxA proto :dword, :dword, :dword, :dword
ExitProcess proto :dword

;++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
; データセグメント
;++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
.data

MSG1      db	'メッセージテストだよん', 0
TITLE1    db	'タイトルだよん', 0

;++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
; コードセグメント
;++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
.code

WinMainCRTStartup	proc
    invoke MessageBoxA, NULL, offset MSG1, offset TITLE1, MB_OK
    invoke ExitProcess, 0
    ret
WinMainCRTStartup	endp

end

さて、一つ一つ解説を加えていきます。まず、.486擬似命令ですが、これはおそらく大体意味はわ かると思います。MASMに対して、486用の命令を生成せよという命令ですね。たとえば、.386と置けば、 80386用の命令を生成していくことになります。また、.386pというように、CPU名の後ろにpを付加する 事で特権命令も使うことができるようになります。(pはprivilage→特権の略)

つぎに、.model擬似命令ですが、この命令はメモリモデルと呼び出し規約を設定します。Win32 プログラムの場合はフラットメモリモデルを使っているので、メモリモデルにはflatを指定します。 また、関数(プロシージャ)の呼び出し規約には、Windowsライブラリの呼び出し規約であるstdcallを 指定します。C言語と協調させる場合には、ここにcと指定しておきましょう。

プログラム内では、WinMainCRTStartupというプロシージャを定義していますが、プロシージャ を指定するには、proc擬似命令をつかいます。また、プロシージャの呼び出しには、いろいろやり方 がありますが、簡略化された呼び出し方法としてinvoke擬似命令を使うことが出来ます。この命令 を使うことで、MASM上においてプロシージャの呼び出しを簡単に呼び出しを行うことができるようになります。

また、Windowsプログラムの終了では、かならずExitProcessを呼び出すようにしましょう。 VC++(管理人はVC++を使っているのでその場合の話)では、プログラマーの見えないところでExitProcess を呼び出してくれています。なぜ呼び出さなければいけないかというと、ExitProcessを呼び出さないと、 完全にプロセスは終了しないからです。呼び出さない場合は、メモリ上にゴミが残ることもありえます。

Chapter 3 - 3   インスタンスハンドルについて : Updated on 2004/08/20

次にインスタンスハンドルの取得方法をやりましょう。インスタンスハンドルとは、 それぞれのモジュールの実体を示すハンドルです。これではよく意味がわかりませんか? では、もう少し詳しく説明をしましょう。

Windowsでは、全てのオブジェクト――C++で言うオブジェクトと違います、 アプリケーションが扱う部品のこと――を"ハンドル"で扱います。Windowsにとって見れば、 実行ファイルもメモリ上にロードされれば、何らかの形でこの"インスタンス"を 管理しなければいけないわけです。そこで、Windowsは、アプリケーションがそれ自身のインスタンス について操作を行う場合に"インスタンスハンドル"をそのキーとして渡すわけなんですね。

余計分かりにくいですね…(^o^;

とりあえず、説明はそのくらいにしておきましょう。ところで、なぜインスタンスハンドルが 必要なのかというと、実行ファイル(もしくはモジュール)に付属しているリソースを取得する場合には、 このインスタンスハンドルを"キー"としてWindowsに渡すわけなんです。 インスタンスハンドルの存在意義は、こう考えてみれば当然必要なわけです。

とりあえず、プログラムを見てみます。

;++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
; アセンブラプログラミング
;++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
.586
.model flat, stdcall

;++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
; 定義
;++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
NULL	=	0
MB_OK	=	0

;++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
; プロトタイプ
;++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
MessageBoxA       proto   :dword, :dword, :dword, :dword
GetModuleHandleA  proto   :dword
wsprintfA         proto c :dword, :dword, :dword
ExitProcess       proto   :dword

;++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
; データセグメント
;++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
.data

MSG1	db	'インスタンスハンドル: 0x%X', 0
TITLE1	db	'タイトルだよん', 0

STR1	db	64 dup(0)
hModule	dd	?

;++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
; コードセグメント
;++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
.code

WinMainCRTStartup	proc
    invoke GetModuleHandleA, NULL
    mov    hModule, eax

    invoke wsprintfA, offset STR1, offset MSG1, hModule
    invoke MessageBoxA, NULL, offset STR1, offset TITLE1, MB_OK
    invoke ExitProcess, 0

    ret
WinMainCRTStartup	endp

end

モジュールハンドルの取得にはGetModuleHandleを使います。関数名の後ろについている Aは文字列がAsciiコードであることを示しています。関数名の後ろがWの場合は、 文字列がUNICODEの場合に使います。これは覚えて置いて損はないと思います。

WindowsAPIについては、MSDNライブラリで調べましょう。

Chapter 3 - 4   ローカル変数 : Updated on 2004/08/20

つぎに、ローカル変数を定義してみましょう。

;++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
; アセンブラプログラミング
;++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

.586
.model flat, stdcall
option casemap:none

;++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
; 定義
;++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
NULL	=	0
MB_OK	=	0

;++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
; プロトタイプ
;++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
MessageBoxA       proto   :dword, :dword, :dword, :dword
GetModuleHandleA  proto   :dword
wsprintfA         proto c :dword, :dword, :dword
ExitProcess       ptoro   :dword

;++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
; データセグメント
;++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
.data

MSG1	db	'インスタンスハンドル: 0x%X', 0
TITLE1	db	'タイトルだよん', 0

;++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
; コードセグメント
;++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
.code

WinMainCRTStartup	proc
    ;---------------------------
    ; ローカル変数だよん
    ;---------------------------
    local pBuf[64] :byte
    local hModule  :dword

    ;---------------------------
    ; プログラム本体
    ;---------------------------
    invoke GetModuleHandleA, NULL
    mov    hModule, eax

    invoke wsprintfA, addr pBuf, offset MSG1, hModule

    invoke MessageBoxA, NULL, addr pBuf, offset TITLE1, MB_OK
    invoke ExitProcess, 0

    ret
WinMainCRTStartup	endp

end

プロシージャ内部にlocal擬似命令を指定することによって、ローカル変数を使うことが出来ます。上を見れば分かるように、ローカル変数の宣言は、

local  name[] :size

とします。上を見れば分かるように、配列も指定することが出来ます。

Chapter 3 - 5   ウィンドウの作成 : Updated on 2004/08/29

それでは、やっとウィンドウを作成してみましょう。プログラムはちょっと 長いのですが、こちらを見てみてください。

;******************************************************************************
; アセンブラウィンドウプログラミング windows.asm
;******************************************************************************

.586
.model flat, stdcall
option casemap:none

;******************************************************************************
; 定義
;******************************************************************************

NULL              =       0
WM_DESTROY        =       2h
IDI_APPLICATION   =       32512
IDC_ARROW         =       32512
WHITE_BRUSH       =       0
WS_OVERLAPPED     =       00000000h
WS_SYSMENU        =       00080000h
WS_CAPTION        =       00C00000h
WS_EX_APPWINDOW   =       00040000h
SW_SHOWNORMAL     =       1
CW_USEDEFAULT     =       80000000h

HANDLE  typedef     dword
LPSTR   typedef     dword

POINT        struct
    x               dword   ?
    y               dword   ?
POINT        ends

WNDCLASS     struct
    style           dword   ?
    lpfnWndProc     dword   ?
    cbClsExtra      dword   ?
    cbWndExtra      dword   ?
    hInstance       HANDLE  ?
    hIcon           HANDLE  ?
    hCursor         HANDLE  ?
    hbrBackground   HANDLE  ?
    lpszMenuName    LPSTR   ?
    lpszClassName   LPSTR   ?
WNDCLASS     ends

MSG          struct
    hWnd            HANDLE  ?
    message         dword   ?
    wParam          dword   ?
    lParam          dword   ?
    time            dword   ?
    point           POINT   <?, ?>
MSG          ends

;******************************************************************************
; プロシージャ定義
;******************************************************************************

CreateWindowExA     proto   dwExStyle   :dword,
                            pClassName  :LPSTR,
                            pWindowName :LPSTR,
                            dwStyle     :dword,
                            x           :dword,
                            y           :dword,
                            nWidth      :dword,
                            nHeight     :dword,
                            hWndParent  :HANDLE,
                            hMenu       :HANDLE,
                            hInstance   :HANDLE,
                            pParam      :dword
RegisterClassA      proto   pWndClass   :dword
GetStockObject      proto   nObject     :dword
LoadIconA           proto   hInstance   :HANDLE,
                            lpIconName  :LPSTR
LoadCursorA         proto   hInstance   :HANDLE,
                            lpCurName   :LPSTR
GetMessageA         proto   lpMsg       :dword,
                            hWnd        :HANDLE,
                            nMsgFilMin  :dword,
                            nMsgFilMax  :dword
TranslateMessage    proto   lpMsg       :dword
DispatchMessageA    proto   lpMsg       :dword
PostQuitMessage     proto   nExitCode   :dword
DefWindowProcA      proto   hWnd        :HANDLE,
                            Msg         :dword,
                            wParam      :dword,
                            lParam      :dword
GetModuleHandleA    proto   lpstr       :LPSTR
ShowWindow          proto   hWnd        :HANDLE,
                            nCmdShow    :dword
UpdateWindow        proto   hWnd        :HANDLE
ExitProcess         ptoro   nExitCode   :dword

;*****************ユーザー定義***********************
InitInstance        proto   hInstance   :HANDLE
CreateMyWindow      proto   hInstance   :HANDLE

;******************************************************************************
; データセグメント
;******************************************************************************
    .data

ClassName   db  'FirstWnd', 0
WindowName  db  'First Application', 0

;******************************************************************************
; コードセグメント
;******************************************************************************
    .code

;******************************************************************************
; メイン関数
;******************************************************************************
WinMainCRTStartup    proc
    ;*************************
    ; ローカル変数
    ;*************************
    local    msg     :MSG
    local    hWnd    :HANDLE
    local    hInst   :HANDLE

    ;*************************
    ; プログラム
    ;*************************
    invoke GetModuleHandleA, NULL
    mov     hInst, eax

    invoke InitInstance, hInst
    cmp     eax, NULL
    je      ExitProg

    invoke CreateMyWindow, hInst
    cmp     eax, NULL
    je      ExitProg
    mov     hWnd, eax

    invoke ShowWindow, hWnd, SW_SHOWNORMAL
    invoke UpdateWindow, hWnd

MSGLOOP:
    invoke GetMessageA, addr msg, NULL, 0, 0
    or      eax, eax
    je      ExitProg

    invoke TranslateMessage, addr msg
    invoke DispatchMessageA, addr msg
    jmp     MSGLOOP

ExitProg:
    invoke ExitProcess, msg.wParam

    ret
WinMainCRTStartup    endp

;******************************************************************************
; ウィンドウクラスの登録
;******************************************************************************
InitInstance    proc    hInstance:HANDLE
    ;*************************
    ; ローカル変数
    ;*************************
    local    wc    :WNDCLASS

    ;*************************
    ; プログラム
    ;*************************

    mov     wc.style, 0
    mov     wc.lpfnWndProc, offset MainWndProc
    mov     wc.cbClsExtra, 0
    mov     wc.cbWndExtra, 0

    mov     eax, hInstance
    mov     wc.hInstance, eax
	
    invoke LoadIconA, NULL, IDI_APPLICATION
    mov     wc.hIcon, eax

    invoke LoadCursorA, NULL, IDC_ARROW
    mov     wc.hCursor, eax

    invoke GetStockObject, WHITE_BRUSH
    mov     wc.hbrBackground, eax

    mov     wc.lpszMenuName, NULL
    mov     wc.lpszClassName, offset ClassName

    invoke RegisterClassA, addr wc

    ret
InitInstance    endp

;******************************************************************************
; ウィンドウの作成
;******************************************************************************
CreateMyWindow    proc    hInstance:HANDLE
    ;*************************
    ; プログラム
    ;*************************

    invoke CreateWindowExA, WS_EX_APPWINDOW, offset ClassName,
        offset WindowName, WS_SYSMENU or WS_CAPTION, CW_USEDEFAULT,
        CW_USEDEFAULT, 300, 400, NULL, NULL, hInstance, NULL

    ret
CreateMyWindow    endp

;******************************************************************************
; ウィンドウプロシージャ
;******************************************************************************
MainWndProc    proc    hWnd:HANDLE, Msg:dword, wParam:dword, lParam:dword

    .if Msg == WM_DESTROY
        invoke PostQuitMessage, 0
    .else
        invoke DefWindowProcA, hWnd, Msg, wParam, lParam
    .endif

    ret
MainWndProc    endp

end

プログラムを見れば分かるように、Windowsプログラミングは非常に面倒くさいことが分かりますね。 今回は1から作っていきましたが、必要な関数や構造体をあらかじめ定義しておかなければなりません。 MASM32には、必要な関数や構造体の定義をまとめたファイルを配っています。それを使ってもいいでしょう。

# それにしても、ここでMASM32を紹介してから、あそこもかなり 認知されるようになってきましたね。
# いろいろなところで紹介されるようになったし。

上のプログラムでは、便宜上データ型として、ハンドル (HANDLE) と文字列 (LPSTR) と 整数に分けています。Windowsプログラミングをやったことがある人なら、HINSTANCEやHWNDが どうして同じHANDLEに統一されるか分かるかな?考えてみたら面白いかもね。

MASMでWindowsプログラミングをしてみて、率直な感想としては、MASMでWindows プログラミングをするくらいなら、C で書いたほうがはるかに楽です。また、C++ で書いたほうがもっと楽です。Cと比べるとMASMで書いたからといって、とくに速くなるという訳でもないので、 必要な場所を MASM で記述したほうが賢いやり方かもしれません。もっと言ってしまうと インラインアセンブラのほうが圧倒的にいい。

プログラミングについては非効率なので本気で Windows プログラムを作ろうとしている方には お勧めできません。

やはりアセンブラの使うべきところは、もっとプリミティブな部分(組み込み系や下部層のOSの記述)に 使うべきなのでしょう。


Chapter 4   リソースの利用

Abstract : リソースを使う場合

Chapter 4 - 1   リソースの使用法 : Updated on 2004/08/20

今回はリソース取得について説明します。C言語でのWindows リソースを取得するのと何ら変わりはしないのですけれども…

さて、リソースには2種類あることをご存知でしょうか?ぼくは勝手に 内部リソース / 外部リソース と名づけてしまっています。

内部リソースは、実行ファイルにくっついてくるリソースのことです。実行ファイルにくっつけておけば、リソースのロードを素早くに行うことが出来ます。ただし、リソースが実行ファイルに含まれてしまうので、画像ファイルなどをリソースにふくめてしまうと、実行ファイルがやたら巨大になってしまうことがあります。ビットマップで、実行ファイルがやたら大きくなることがあるので、気をつけましょう。

さて、外部リソースですが、これは文字通り実行ファイルに含まれないリソースのことを指します。実行中にほかのファイルからロードしたり、自分で作ったりしなければなりません。その分、時間がかかってしまうことにもなります。

この外部リソースを、他の実行ファイルからリソースとして取得してくることも出来ます。その方法はMSDNライブラリかなんかで、自分で調べてみてくださいね。

意味的には、内部リソースを静的リソース、外部リソースを動的リソースなんて呼んでもいいですね。そこらへんは、自分の好きなように呼んでください。

Chapter 4 - 2   インクルードファイル : Updated on 2004/08/20

前のように、いちいち関数を定義して言っては大変ですね?ということで、MASM32 についてくる関数と定数を定義したインクルードファイルを流用してしまいましょう。次の4つのファイルをとりあえず覚えておくと楽です。

  1. windows.inc 定数が定義してあるインクルードファイル
  2. kernel32.inc カーネル関数(コア関数)が定義してあるファイル
  3. user32.inc ユーザー関数(ユーザーインタフェース関数)が定義してあるファイル
  4. gdi32.inc GDI関数(グラフィックデバイスインターフェース関数)が定義してあるファイル

Chapter 4 - 3   アイコンとダイアログボックスを使った例 : Updated on 2004/08/29

それでは、実際にプログラムを見てみましょう。今回使うリソースはダイアログボックスとアイコンです。

プログラムを以下に示しておきます。

;******************************************************************************
; リソースの取得 resource.asm
;******************************************************************************

.486
.model flat, stdcall
option casemap:none

include <windows.inc>
include <kernel32.inc>
include <user32.inc>
include <gdi32.inc>

include <resource.inc>

;******************************************************************************
; プロトタイプ
;******************************************************************************

DlgProc             proto   hWnd    :HWND, 
                            Msg     :UINT, 
                            wParam  :WPARAM, 
                            lParam  :LPARAM
WinMainCRTStartup   proto
OnCommand           proto   hWnd    :HWND,
                            wParam  :WPARAM

;******************************************************************************
;  データ / 変数
;******************************************************************************

.data
MSG1    db          "ちゃんと表示されたでしょう?", 0
TITLE1  db          "リソース取得テスト", 0
hInst   HINSTANCE   ?

;******************************************************************************
;  コード
;******************************************************************************

.code
;------------------------------------------------------------------------------
;  ダイアログプロシージャ
;------------------------------------------------------------------------------
DlgProc proc \
    hWnd    : HWND,
    Msg     : UINT,
    wParam  : WPARAM,
    lParam  : LPARAM

    .if Msg == WM_INITDIALOG
        invoke LoadIconA, hInst, IDI_MAINICON
        invoke SendMessage, hWnd, WM_SETICON, ICON_BIG, eax
    .elseif Msg == WM_CLOSE
        invoke EndDialog, hWnd, 0
    .elseif Msg == WM_COMMAND
        invoke OnCommand, hWnd, wParam
    .else
        mov     eax, FALSE
        ret
    .endif

    mov     eax, TRUE
    ret
DlgProc endp

;------------------------------------------------------------------------------
;  コマンド処理
;------------------------------------------------------------------------------
OnCommand proc \
    hWnd    : HWND,
    wParam  : WPARAM

    mov     eax, wParam
    and     eax, 0ffffh

    .if eax == IDOK
        invoke SendMessage, hWnd, WM_CLOSE, 0, 0
    .elseif eax == IDCANCEL
        invoke MessageBox, hWnd, addr MSG1, addr TITLE1, MB_OK
    .endif

    ret
OnCommand endp

;------------------------------------------------------------------------------
;  メインプロシージャ
;------------------------------------------------------------------------------
WinMainCRTStartup proc
    invoke GetModuleHandle, NULL
    mov     hInst, eax
    invoke DialogBoxParamA, hInst, IDD_TEST, NULL, addr DlgProc, NULL
    invoke ExitProcess, 0
    ret
WinMainCRTStartup endp

;------------------------------------------------------------------------------
;  End of File
;------------------------------------------------------------------------------
end

しかしながら、これをアセンブルして実行してもダイアログボックスは表示されません。 というのも、まだリソースを作っていないからですね。ダイアログボックスを表示 するためには、リソースとしてダイアログボックスを作らなければなりません。 アイコンも使用するのであれば、アイコンも必要です。作ったリソースをリソース コンパイラでコンパイルして実行ファイルに一緒にリンクして初めてリソースを 使うことが可能となります。 そこで次のようなリソースを作ってみましょう。

まず、ダイアログボックスです。

VC++で作成する場合には、次のようにプロパティを設定します。

プロパティ 内容
ID IDD_TEST = 101
スタイル オーバーラップ
境界線 細線
その他のスタイル 「中央」をチェック

つぎに、アイコンですが、これは任意でいいです。とりあえず、僕は次のようなアイコンを用意しました。

うーん、微妙に適当ですね…(苦笑)。IDは次のように指定します。

ID IDI_MAINICON = 102

そうしたら、resource.incファイルを作りましょう。このファイルにリソースのIDを保存しておきます。

;******************************************************************************
; リソースID定義 
;****************************************************************************** 
IDD_TEST     = 101 
IDI_MAINICON = 102

あとは、プロジェクトをビルドして実行するだけです。そうするとダイアログボックスが表示されたでしょうか?


Chapter 5   チップス

Abstract : ちょっとしたヒント

Chapter 5 - 1   インクルードファイルの種類 : Updated on 2004/08/20

いままで、すこしですが、MASMでプログラミングを作ってきて見ました。

今回はちょっと寄り道して、MASM プログラミングの一口メモを紹介します。一見 MASM ってアセンブラだから、難しそうに見えますが、全然難しくありません。むしろ C をやったことがある人なら、余裕で分かると思います。

さてそんなところで、ちょっと一口メモを紹介しましょう。

MASM で Windows プログラミングをする場合、関数を定義しなければなりません。その関数を毎回定義していては面倒なので、あらかじめヘッダーファイルにまとめておいたほうがいいでしょう。

けれど自分でまとめるのは面倒なので、MASM32 のサイトから MASM を落としてきて、その中にあるヘッダーファイルを使うとものすごく楽です。そのヘッダーファイルをもう少し詳しく紹介します。

コア関数群
Include File 解説
windows.inc Windows プログラミングで使用する定数の定義。
kernel32.inc カーネル(コア)関数。プロセス管理、ファイル操作などの関数が定義。
user32.inc ユーザー関数。ユーザーインターフェイス関数の定義。
gdi32.inc GDI 関数。グラフィックデバイスインターフェース関数の定義。
advapi32.inc 拡張関数。暗号化、セキュリティ、レジストリ操作などの関数の定義。
サブ関数群
Include File 解説
comctl32.inc コモンコントロール
comdlg32.inc コモンダイアログ
d3drm.inc DirectX 3D Retained Mode
ddraw.inc DirectX DirectDraw
dinput.inc DirectX DirectInput
dplayx.inc DirectX DirectPlay
dsetup.inc DirectX DirectSetup
dsound.inc DirectX DirectSound
glaux.inc OpenGL
glu32.inc OpenGL
imm32.inc IME API
lsapi32.inc LS API
lz32.inc LZ 圧縮
netapi32.inc NetAPI
odbc32.inc ODBC API
opengl32.inc OpenGL
scrnsavew.inc Screen Saver API
setupapi.inc Setup API
shell32.inc Shell API
tapi32.inc Telephony API
wininet.inc Internet API
winmm.inc Multi Media API
winspool.inc Printer API
wsock32.inc WinSock API

もっとあるけど、これくらい。

Chapter 5 - 2   AとWの違い、呼び出し規約 : Updated on 2004/08/20

関数名の終りに A がついていたり、W がついていたりしますよね?あれは何でしょう。

実は文字列を Unicode でプログラミングする場合、W がついているほうを使います。A がついているほうは、文字列をアスキーコードでプログラミングする場合は A のほうを使います。

W は Wide の W 、A は Ascii (もしくは ANSI )の A です、たぶん…。

また、関数の呼び出し規約ですが、MASM で Windows プログラミングする場合、呼び出し規約には C の呼び出し規約と、STDCALL の呼び出し規約を用います。両者には少しですが違いがあります。

VC++ の場合、ユーザー関数は C 規約で統一されています。関数修飾子として、 __cdecl が自動的に付加されます。一方 API では、__stdcall で定義されています。

実は WINAPI や CALLBACK といった修飾子は __stdcall と定義されています。API からコールされることを考えれば当然の結果といえます。

C の呼び出し規約と、STDCALL の呼び出し規約の違いは、スタックの開放のちがいです。C は呼び出し側でスタックを開放しますが、STDCALL は関数の内部でスタックの開放を行います。以下は C と STDCALL の呼び出し規約です。

処理 C
(__cdecl)
STDCALL
(__stdcall)
関数名の先頭に _ を付加する
引数を逆の順序でスタックへプッシュ
引数スタックを呼び出し側で開放 ×
引数スタックを呼び出された側で開放 ×
VARARG 使用可

詳しくは MASM のヘルプ参照。

Chapter 5 - 3   プロシージャのプロトタイプ宣言 : Updated on 2004/08/20

関数(プロシージャ)の呼び出しを行う場合、invoke を使うと楽ですが、あらかじめ proto でプロトタイプ宣言を行わなければなりません。また invoke は、明示的に呼び出し規約を指定してやらないと、呼び出すことが出来ません。

invoke は、マクロみたいなもので、invokeを使うことによって、関数の呼び出しが楽になります。スタックへのプッシュを自動化してくれています。

また、addr と offset は両者とも、変数、もしくは定数のアドレスを取得します。なにが違うのでしょう。

実際に offset のみでプログラミングしようと思うと、かなりの困難が伴います。offset では、実行時のスタックアドレスを取得できないのです。名前の通り、offset は識別子のオフセットアドレスを取得します。

addr を使うことで、実行時のスタック変数のアドレスを取得することが出来ます。

Chapter 5 - 4   NULLと'\0' : Updated on 2004/08/20

前にも言いましたが、NULL = 0 もしくは、'\0' = 0 ではありません。確かに、ヘッダーファイルなどを見ると、NULL = 0 と定義されています。しかしそれは便宜上定義されているものなので、意味的には違います。もしかしたらほかの環境では NULL = -1 と定義されているかもしれません。

ポインタやハンドルなどが未定義の場合は、必ず NULL を使うようにしましょう。Windows から与えられるあらゆるハンドルは、実は (void *) 型なので NULL が妥当ということも分かるでしょう。

また、C 言語のはなしになってしまいますが、'\0' は文字列の終端を表す記号なので、文字列以外に使ってはいけません。NULL も同様にそれ以外のことに用いてはなりません。

(情報系の勉強をすれば、終端記号や未定義とかはよく出てくる概念です。そっち系の勉強をしたい人は、記憶にとどめておいて損はないかも。)

Chapter 5 - 5   ExitProcessについて : Updated on 2004/08/20

Windows プログラムの終了時にはかならず、ExitProcess を呼び出すようにしましょう。もちろん、使わなくてもプログラムを終了することは出来ます。

しかしながら、DLLなどを呼び出した場合、ExitProcessを使わないでプロセスを終了してしまうと、プロセスに結合したDLLがメモリ上に残ってしまいます。ExitProcessを呼び出すことによって、DLLにプログラムが終了することを通知され、それによって、DLLもメモリ上から解放されます。

とりあえず、チップスとしてはこれくらいかな。


Chapter 6   テキストビュワー

Abstract : テキストビュワーを作成

Chapter 6 - 1   テキストビュワーのプログラム : Updated on 2004/08/29

今回は、もうちょっと入り組んだプログラムを作ってみようと思います。そうはいっても、テキストビュワー。ただテキストファイルを開いて、を表示するだけのプログラムです。

さて、それではテキストビュワーを作成していきましょう。作り方はいたって簡単。ウィンドウのクライアント領域にエディットボックスをぴったりと貼り付ければいいのです。

実際には、WM_CREATE メッセージを拾ったときに、エディットボックスを作成してクライアント領域に貼り付けます。イメージ的には以下の通り。

ファイルを開く機能だけつけて、ファイルの中身を閲覧できるようにします。

プログラムは以下のとおりです。 → プロジェクト(VC++用)

 ; --------------------------------------------------------------------
 ;
 ;  Text Viewer main.asm
 ;
 ; --------------------------------------------------------------------

.586
.model flat, stdcall
option casemap:none

include <windows.inc>
include <kernel32.inc>
include <user32.inc>
include <gdi32.inc>
include <comdlg32.inc>

include <resource.inc>

 ; --------------------------------------------------------------------
 ;
 ;  Procedure Definitions
 ;
 ; --------------------------------------------------------------------

InitInstance        proto   hInstance   :HINSTANCE
CreateMyWindow      proto   hInstance   :HINSTANCE
OnCreate            proto   hWnd        :HWND
OnSize              proto   hWnd        :HWND
OnCommand           proto   hWnd        :HWND, 
                            id          :UINT
OnFileOpen          proto   hWnd        :HWND


 ; --------------------------------------------------------------------
 ;
 ;  Data
 ;
 ; --------------------------------------------------------------------

.data

CLASSNAME   DB  'FirstWnd', 0
TCLASSNAME  DB  'EDIT', 0
APPNAME     DB  'テキストビュワー', 0
FILTER      DB  'テキスト文章 (*.txt)', 0, '*.txt', 0
            DB  'テキスト文章 (*.doc)', 0, '*.doc', 0, 0


g_hEditWnd  HWND    NULL
g_szFile    DB  260 DUP (0)

 
 ; --------------------------------------------------------------------
 ;
 ;  Code
 ;
 ; --------------------------------------------------------------------

.code

 ;******************************************************************************
 ; メイン関数
 ;******************************************************************************

WinMainCRTStartup proc
    local    msg     :MSG
    local    hWnd    :HWND
    local    hInst   :HINSTANCE

    invoke GetModuleHandleA, NULL
    mov     hInst, eax

    invoke InitInstance, hInst
    cmp     eax, NULL
    je      _Exit

    invoke CreateMyWindow, hInst
    cmp     eax, NULL
    je      _Exit
    mov     hWnd, eax

    invoke ShowWindow, hWnd, SW_SHOWNORMAL
    invoke UpdateWindow, hWnd

 _MsgLoop:
    invoke GetMessageA, addr msg, NULL, 0, 0
    or      eax, eax
    je      _Exit

    invoke TranslateMessage, addr msg
    invoke DispatchMessageA, addr msg
    jmp     _MsgLoop

 _Exit:
    invoke ExitProcess, msg.wParam
    ret
WinMainCRTStartup endp

 ;******************************************************************************
 ; ウィンドウクラスの登録 
 ;
 ;   ATOM InitInstance hInstance:HISNTANCE
 ;******************************************************************************

InitInstance proc \
    hInstance       :HINSTANCE
    local   wc      :WNDCLASS

    mov     wc.style, 0
    mov     wc.lpfnWndProc, offset MainWndProc
    mov     wc.cbClsExtra, 0
    mov     wc.cbWndExtra, 0

    mov     eax, hInstance
    mov     wc.hInstance, eax
	
    invoke LoadIconA, hInstance, IDI_MAIN_ICON
    mov     wc.hIcon, eax

    invoke LoadCursorA, NULL, IDC_ARROW
    mov     wc.hCursor, eax

    invoke GetStockObject, WHITE_BRUSH
    mov     wc.hbrBackground, eax

    mov     wc.lpszMenuName, IDR_MAIN_MENU
    mov     wc.lpszClassName, offset CLASSNAME

    invoke RegisterClassA, addr wc

    ret
InitInstance endp

 ;******************************************************************************
 ; ウィンドウの作成
 ;
 ;   HWND CreateMyWindow hInstance:HINSTANCE
 ;******************************************************************************

CreateMyWindow proc \
    hInstance   :HINSTANCE

    invoke CreateWindowExA,
        WS_EX_APPWINDOW, 
        offset CLASSNAME,
        offset APPNAME, 
        WS_OVERLAPPEDWINDOW, 
        CW_USEDEFAULT,
        CW_USEDEFAULT, 
        CW_USEDEFAULT, 
        CW_USEDEFAULT, 
        NULL, 
        NULL, 
        hInstance, 
        NULL

    ret
CreateMyWindow endp

 ;******************************************************************************
 ; ウィンドウプロシージャ
 ;   
 ;   LRESULT MainWndProc hWnd:HWND, Msg:DWORD, wParam:DWORD, lParam:DWORD
 ;******************************************************************************

MainWndProc proc \
    hWnd    :HWND, 
    Msg     :DWORD, 
    wParam  :DWORD, 
    lParam  :DWORD

    .if Msg == WM_DESTROY
        invoke PostQuitMessage, 0
    .elseif Msg == WM_CREATE
        invoke OnCreate, hWnd
    .elseif Msg == WM_SIZE
        invoke OnSize, hWnd
    .elseif Msg == WM_COMMAND
        mov     eax, wParam
        and     eax, 0000FFFFh
        invoke OnCommand, hWnd, eax
    .else
        invoke DefWindowProcA, hWnd, Msg, wParam, lParam
        ret
    .endif

    mov     eax, 0
    ret
MainWndProc endp

 ;******************************************************************************
 ; WM_CREATE メッセージの処理
 ;
 ;   BOOL OnCreate hWnd : HWND
 ;******************************************************************************

OnCreate proc \
    hParentWnd      :HWND
    local   hInst   :HINSTANCE
    local   rc      :RECT

    invoke GetModuleHandle, NULL
    mov     hInst, eax

    invoke GetClientRect, hParentWnd, addr rc

    invoke CreateWindowExA,
        WS_EX_CLIENTEDGE, 
        offset TCLASSNAME,
        NULL, 
        WS_VISIBLE or WS_CHILD or ES_MULTILINE or ES_AUTOHSCROLL \
        or ES_AUTOVSCROLL or WS_HSCROLL or WS_VSCROLL, 
        0,
        0, 
        rc.right, 
        rc.bottom, 
        hParentWnd, 
        NULL, 
        hInst, 
        NULL
    mov     g_hEditWnd, eax

    invoke SendMessage, g_hEditWnd, EM_SETLIMITTEXT, 0FFFFh, 0

    mov     eax, TRUE
    ret
OnCreate endp

 ;******************************************************************************
 ; WM_SIZE メッセージの処理
 ;
 ;   void OnSize hWnd :HWND
 ;******************************************************************************

OnSize proc \
    hWnd        :HWND
    local   rc  :RECT

    invoke GetClientRect, hWnd, addr rc

    invoke SetWindowPos, g_hEditWnd, NULL, 0, 0, 
        rc.right, rc.bottom, SWP_NOMOVE or SWP_NOZORDER
    ret
OnSize endp

 ;******************************************************************************
 ; WM_COMMAND メッセージの処理
 ;
 ;   void OnCommand hWnd:HWND, id:UINT
 ;******************************************************************************

OnCommand proc \
    hWnd    :HWND,
    id      :UINT
    
    .if id == IDM_EXIT
        invoke SendMessage, hWnd, WM_CLOSE, 0, 0
    .elseif id == IDM_FOPEN
        invoke OnFileOpen, hWnd
    .endif

    ret
OnCommand endp

 ;******************************************************************************
 ; IDM_FOPEN メニューメッセージの処理
 ;
 ;   void OnFileOpen hWnd:HWND
 ;******************************************************************************

OnFileOpen proc \
    hWnd            :HWND
    local   ofn     :OPENFILENAME
    local   hFile   :HANDLE
    local   pBuffer :DWORD
    local   dwRead  :DWORD

    ;  ファイルオープンダイアログの表示
    invoke RtlZeroMemory, addr ofn, sizeof OPENFILENAME

    mov     ofn.lStructSize, sizeof OPENFILENAME
    mov     eax, hWnd
    mov     ofn.hWndOwner, eax
    mov     ofn.lpstrFile, offset g_szFile
    mov     ofn.nMaxFile, 260
    mov     ofn.lpstrFilter, offset FILTER
    mov     ofn.nFilterIndex, 1
    mov     ofn.lpstrFileTitle, NULL
    mov     ofn.nMaxFileTitle, 0
    mov     ofn.lpstrInitialDir, NULL
    mov     ofn.Flags, OFN_PATHMUSTEXIST or OFN_FILEMUSTEXIST \
            or OFN_HIDEREADONLY

    invoke GetOpenFileName, addr ofn

    or      eax, eax
    je      _OnFileExit

    ; ファイルを開く
    invoke CreateFile, ofn.lpstrFile, GENERIC_READ, 0, NULL, 
        OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL
    mov     hFile, eax

    ; データ取得用バッファの取得
    invoke GlobalAlloc, GPTR, 0FFFFh
    mov     pBuffer, eax

    ; データの読み取り
    invoke ReadFile, hFile, pBuffer, 0FFFFh, addr dwRead, NULL

    ; データの書き込み
    invoke SetWindowText, g_hEditWnd, pBuffer

    ; バッファを削除
    invoke GlobalFree, pBuffer

    ; ファイルハンドルを閉じる
    invoke CloseHandle, hFile

 _OnFileExit:
    ret
OnFileOpen endp

end

ファイルの選択に関しては、コモンダイアログのファイルオープンダイアログを使っています。これを使うことで、容易にファイル選択用のダイアログボックスを使うことが出来ます。詳しくはプログラムを見てみてください。

ビルドする時の注意事項ですが、コモンダイアログボックスを使っているので、コモンダイアログボックスを使うために、コモンダイアログボックスのダイナミックリンクライブラリ(DLL)のライブラリをリンクしなければいけません。(commlib.lib ?)


Chapter 7   FPUで数値演算

Abstract : 浮動小数点演算について

Chapter 7 - 1   浮動小数点演算について : Updated on 2004/08/20

FPU を用いた浮動小数点演算について、ここでは説明を行います。実際に使ってみよう!ということで、実際に命令を使ってみましょう。統合環境としては VC++ を使いますが、べつに VC++ じゃなくても使うことは出来ますが、ほかの環境でテストしてみたい場合には、自分で調べてください。計算命令についても、ここでは解説するつもりはないので、インテルマニュアル参照。(だいたい分かるでしょ)

FPU の演算部分は MASM で記述するので、MASM を用意しておいてください。FPU の制御系の命令については解説を行うつもりはないのであしからず。インテルマニュアルで自分で調べてください。

ここを見ている人で、FPU の機能を知らないなんて人はいないでしょう。FPU は、浮動小数点演算用の専用ユニットです。ペンティアム 80386 CPU まではオプションでしたが、80486 からは標準で CPU 内部に搭載されるようになりました。(80486SX は CPU の値段を下げるために FPU はオプション)

FPU には、いくつかのレジスタと専用命令がついています。計算用のレジスタとして 8 本のスタックタイプのレジスタが搭載されています。計算命令には、加減乗除、超越関数、三角関数など数値演算などに必要な機能はだいたいそろっています。

Chapter 7 - 2   FPU用レジスタについて : Updated on 2004/08/20

浮動小数点計算用のレジスタとして、8本のレジスタが用意されています。が、なぜスタック形式のレジスタなのでしょう?

実は計算を行うにはスタックを使ったほうが楽なのです。これはアルゴリズムの本とかでスタックを用いて計算をしていくところを見たことがある人もいるでしょう。(見たことのない人は自分で勉強しませう)

実際には逆ポーランド記法の計算方法でプログラミングしていきます。だからとくにレジスタが何本あるとか、そういうことを意識しなくても、プログラミングをしていくことが出来ます。(もちろん8本を越えてプログラムをする場合には、問題がありますが…)このプログラミングスタイルに反した形で、スタック上に独立してデータを読み込み、逆ポーランド式の計算方法を無視して計算などを行うと、FPUを使うにもかかわらず遅くなったりすることがあるので注意が必要です。

計算で特定のスタックを扱いたい場合には、st(n) という表記法を使います。n には、現在のスタックから数えて n 番目のスタックを指定します。例えば現在のスタックトップから一つ前のスタックの内容を参照したい場合には、st(1) とします。2つ前だったら st(2) とします。

ただし、未使用領域のスタックにアクセスしようとするとエラーになります。

Chapter 7 - 3   プログラミングの仕方 : Updated on 2004/08/20

ということで、論より証拠、実際にどのようにプログラミングしていくか見てみましょう。

( 5.98 + 34.89 ) * (34.89 - 23.0)

この計算を実際に浮動小数点命令を用いてプログラミングを行うと、次のようになります。

; _a = 5.98, _b = 34.89, _c = 23.0 とする

fld     _a  ; @FPU スタックに _a の値をプッシュ
fadd    _b  ; A現在のスタックの値と _b の値を加算

fld     _b  ; BFPU スタックに _b の値をプッシュ
fsub    _c  ; C現在のスタックの値から _c の値を減算

fmul        ; D現在のスタックと一つ前のスタックの掛けた値を一つ前のスタックへプッシュ

このような計算を行った場合、どのようにスタックに積まれて、スタックからデータを出されているのでしょうか。実はスタックの状態としては、次のようになっています。

Chapter 7 - 4   実際のプログラミング : Updated on 2004/08/20

それでは、次に実際のプログラムを見てみましょう。数値演算の部分をMASMで記述して、C言語はその演算を呼び出す形にしてみましょう。まずはC言語側のプログラムから。

#include <stdio.h>

#ifdef __cplusplus
extern "C" double func(double a, double b, double c);
#endif

int main()
{
    printf("func() = %f\n", func(5.98, 34.89, 23.0));

    return 0;
}

funcという関数はMASMで定義しておきます。それでは、次にそのfuncを見てみましょう。

.586
.model flat, c

.code

func proc \
    _a: real8,
    _b: real8,
    _c: real8

    fld     _a
    fadd    _b
    fld     _b
    fsub    _c
    fmul

    ret
func endp

end

関数の戻り値ですが、関数の戻り値は、スタックのトップに入れておきます。

それでは、実際にいくつかの演算をしてみて、プログラムをしてみましょう。

(1) a * (b + c)
(2) |a + b| * sin(c)
(3) a * √(a + b) * cos(c)

それぞれ次のようになります。

; (1) a * (b + c)

fld     a
fld     b
fadd    c
fmul

; (2) |a + b| * sin(c)

fld     a
fadd    b
fabs
fld     c
fsin
fmul

; (3) a * √(a + b) * cos(c)

fld     a
fld     a
fadd    b
fsqrt
fld     c
fcos
fmul
fmul

浮動小数点については、こんな感じです。そんなに難しくないですね。 浮動小数点演算のエラー処理については、インテルのマニュアルを参照してください。


Chapter 8   MMXでSIMD演算

Abstract : MMXを使う

Chapter 8 - 1   MMX演算の概要 : Updated on 2004/08/20

■MMXとは?

まずは MMX 命令について説明しましょう。

MMX は、単純なデータ要素からなる大規模配列に対する反復演算を行います。仕様用途としては、動画、グラフィックス、ビデオの結合、画像処理、オーディオ合成、音声合成、圧縮などがあります。

といっても分からないと思うので、次から具体例に話を進めましょう。サンプルプログラムは VC++ 上で動かすことを想定しています。

■MMX命令の概要

MMX 命令は、SIMD (Single Instruction , Multiple - Data) 実行モデルという形を取っています。どういうことかというと、一つの命令で複数のデータをいっぺんに計算します。

ここに配列 char A[8] と char B[8] という配列があったとしましょう。この配列の1つ1つの要素を MMX 命令を使えばいっぺんに計算を行うことが出来ます。イメージ的には以下のとおり。

上の例を見れば分かるように一つの命令でいっぺんに並列演算を行ってくれます。なるほど、これを見れば画像処理やオーディオ合成、音声合成で処理能力を大きく向上させることが出来ますね。

MMX には、8 つの 64 ビット MMX レジスタ (MM0, MM1, ... MM7) を持っています。この MMX レジスタは、8 本の FPU レジスタを流用しているので、浮動小数点演算と MMX 演算を混合させる場合には注意が必要です。

MMX 演算には、4つのデータ型を計算する時に用います。パックド・バイト/パックド・ワード/パックド・ダブルワード/クワッドワードです。パックド・バイトというのは、64 ビットレジスタを 8 ビットずつ 8 つの領域に分けた状態のデータ型です。イメージ的には、下の図。箱で囲まれているから、パックド(Packed)。

Chapter 8 - 2   MMXが使えるかどうかチェック : Updated on 2004/08/20

MMX を使う前に MMX 命令を使うことが出来るかどうか、チェックが必要です。まあこの辺は実装してもしなくても、プログラマの裁量に任せられていますが、一応はチェックして使えないならつかえない旨を表示するべきでしょうね。

チェックには CPUID 命令を使います。具体的にプログラムを載せておきましょう。

/* CPUID の存在チェック */

BOOL IsCPUID()
{
    DWORD dwPreEFlags, dwPostEFlags;

    _asm {
        /* EFlags の値の取得 */
        pushfd
        pop     eax
        mov     dwPreEFlags, eax

        /* CPUID 命令の存在のチェック (ID フラグのチェック) */
        xor     eax, 00200000h
        push    eax
        popfd

        /* EFlags の値の取得 */
        pushfd
        pop     eax
        mov     dwPostEFlags, eax
    }

    if(dwPreEFlags == dwPostEFlags)
        return FALSE;
    return TRUE;
}

/* MMX テクノロジサポートのチェック */

BOOL IsMMX()
{
    DWORD dwRetEDX;

    /* CPUID 命令のチェック */
    if(!IsCPUID()) return FALSE;

    /* MMX テクノロジのチェック */
    _asm {
        mov     eax, 1
        cpuid
        and     edx, 00800000h
        mov     dwRetEDX, edx
    }
    if(dwRetEDX) return TRUE;
    return FALSE;
}

Chapter 8 - 3   飽和演算・非飽和演算 : Updated on 2004/08/20

MMX 演算には飽和演算と非飽和演算があります。飽和演算というのは、演算でオーバーフロー/アンダーフローしてしまった場合にその最大値/最小値を自動的に代入してくれる演算です。非飽和の場合はそれをしてくれません。

といっても、言葉にしても意味不明でしょうから、具体例を出しましょう。例えば、パックド・バイトでのMMX 符号なし演算を考えてみましょう。一つの要素が 240 + 23 = 263 で、オーバーフローしてしまった場合、自動的に最大値を代入してくれます。つまりこの場合は、255 になります。次の図を見れば、一目瞭然でしょう。

ちなみに PADDUSB というのは、符号なし飽和加算演算を行う MMX 命令です。(Packed Add Unsigned with Satuation ; Byte の略)

MMX命令には以下のものがあります。簡単に説明しておきましょう。

データ転送命令
MOVD 32 ビットデータ転送命令
MOVQ 64 ビットデータ転送命令
算術加減演算命令
PADDx / PSUBx 非飽和演算命令 x = B(BYTE) / W(WORD) / D(DWORD)
PADDSx / PSUBSx 符号付き飽和演算命令 x = B(BYTE) / W(WORD)
PADDUSx / PSUBUSx 符号なし飽和演算命令 x = B(BYTE) / W(WORD)
乗算命令
PMULHx / PMULLx 乗算命令 x = W(WORD) / D(DWORD)
論理演算命令
PAND / PANDN / POR / PXOR  
シフト命令
PSLLx / PSRLx 右/左論理シフト
PSRAx 右算術シフト
変換命令
PACKSSxx 符号付き飽和パック化 xx = WB / DW
PACKUSxx 符号なし飽和パック化 xx = WB
PUNPCKHxx 上位アンパック化 xx = BW / WD / DQ
PUNPCKLxx 下位アンパック化 xx = BW / WD / DQ
制御
EMMS MMX の終了

最後の EMMS 命令は、MMX の使用を終了したときに必ず行うようにしましょう。MMX は FPU のレジスタを流用しているので、きちんとレジスタを EMMS 命令で初期化しておかなければならないからです。

Chapter 8 - 4   実際のプログラム・加算ブレンド : Updated on 2004/08/20

それでは実際に MMX を使って演算してみましょう。今回は 2 つのデータの加算を行います。画像データだったならば、「加算ブレンディング(Add Alpha-Blending)」って言うやつです。

/* Dest = Dest + Src を行います 
 * データの長さは 8 の倍数でなければなりません 
 */

void AddData(BYTE *lpDestBuf, BYTE *lpSrcBuf1, BYTE *lpSrcBuf2, DWORD nLength)
{
    _asm {
        /* 初期化 */
        mov     ecx, nLength;
        shr     ecx, 3
        mov     edi, lpDestBuf
        mov     ebx, lpSrcBuf1
        mov     edx, lpSrcBuf2

        /* ループ演算 */
    ADD_LOOP:
        movq    mm0, [ebx]
        movq    mm1, [edx]

        /* データ加算 */
        paddusb mm0, mm1

        /* 結果をセーブ */
        movq    [edi], mm0

        /* デクリメント・インクリメント */
        add     edi, 8
        add     ebx, 8
        add     edx, 8

        dec     ecx
        jnz     ADD_LOOP

        /* MMX の終了 */
        emms
    }
}

これを見る限りでも、演算の量は 1/8 になっていますね。

ということで、MMX について大体は分かったのではないでしょうか。 これを応用していけば、画像処理などにおいて処理能力を大幅に改善することが出来ます。 あとは自分で実際にいろんなプログラムを組んでみて、実感してみてください。