Tutorial 13: Memory Mapped Files

このチュートリアルでは、メモリマップトファイル(=memory mapped file、以下MMファイル)が何なのか、そして、その使い方を紹介する。 これを見れば、MMファイルを簡単に使うことができるようになるハズだ。
 ソース   リソーススクリプト   実行結果 

Theory:

前章のチュートリアルをよく見れば、重大な欠陥を発見するだろう。 そう、例えば、確保したメモリ領域より読み込むファイルのほうが大きいときはどうするか?とか、 検索文字列がメモリ領域に収まりきらなかったらどうするか?などである。 前者の対策は、伝統的にファイルを読み終わるまで一定サイズのデータを繰り返し読み込む方法である。 後者の対策は、メモリブロックの最後に特別な文字をあらかじめセットしておくという方法がある。 これは境界値問題(バッファオーバーフロー)と呼ばれ、プログラマの頭痛のたねであり、非常によく起こるバグである。

ファイルの内容をバッファするための十分すぎるほどのメモリ領域を確保できれば良いのだが、 それでは限りあるマシンのリソースを無駄に浪費してしまう。 ファイルマッピングによりこれを解決できる。 ファイルマッピングを使用することにより、 ファイルの全内容がメモリにロードされたものと考えることができ、 メモリポインタを使用することにより、ファイルのデータを読み書きすることができる。 非常に簡単である。 メモリ処理のAPI関数を使用する必要もないし、ファイルIOのAPI関数でファイルを分割する必要もない。 一つの、同じファイルである。

ファイルマッピングは、プロセス間でデータを共有するという用途にも用いられる。 この目的でファイルマッピングを使用する場合は、実際にファイルが無くても良く、 全てのプロセスから共通に参照できるメモリブロックと考えることができる。 しかし、プロセス間でデータを共有するのはいささか難しく、容易に使用するべきではない。 正確にプロセス間、スレッド間でデータの同期を取らないと、 プログラムは簡単にクラッシュしてしまう。

このチュートリアルでは、ファイルマッピングをこのような共有メモリ的な使用方法は扱わない。 単純にファイルマッピングをファイルとメモリをマッピングするという用途に使用する。 実際に、PEローダー(Windowsが提供する実行ファイルを実行するモジュール)は実行ファイルをメモリにマッピングするときにファイルマッピングを使用するのである。 これはとても便利で、ハードディスク上のファイルから必要な部分を選択して読み込むことができる。 Win32では、できるだけこのファイルマッピングを使用すべきだ。

けれども、いくつかの制限がある。一度MMファイルを作成したら、そのセッションではMMファイルのサイズを変更できない。 なので、ファイルマッピングは読み込み専用ファイルや、ファイルサイズに関係ないファイル操作などに適している。 しかしこれは、別にファイルサイズが増加する際にファイルマッピングが使用できないということではなく、 ファイルサイズがどれだけ大きくなるか予想し、そのサイズに基づいてMMファイルを作成すればよい。 この場合、少しめんどくさいが、それだけのことだ。

これで説明は十分だろう。 では実際にファイルマッピングの実装に飛び込んでみよう。 ファイルマッピングを使用するには、以下のようなステップを踏む。

  1. CreateFile関数をCALLし、マッピングしたいファイルをオープンする
  2. CreateFile関数で取得したファイルハンドルを引数にしてCreateFileMapping関数をCALLする。この関数により、指定されたファイルにファイルマッピングオブジェクトを作成する。
  3. MapViewOfFile関数をCALLし、マッピングする領域を、ファイルの一部か全部かを選択する。この関数はマップとファイル領域の先頭へのポインタを返す。
  4. そのポインタを使用してファイルに読み書きを行う
  5. UnmapViewOfFile関数をCALLし、ファイルマッピングを解除する
  6. CreateFileMapping関数で作成したファイルマッピングオブジェクトを引数にしてCloseHandle関数をCALLする
  7. 今度はCreateFile関数で作成したファイルハンドルを引数にしてもう一度CloseHandle関数をCALLして、ファイルを閉じる

Example:

.386
.model flat,stdcall
WinMain proto :DWORD,:DWORD,:DWORD,:DWORD
include \masm32\include\windows.inc
include \masm32\include\user32.inc
include \masm32\include\kernel32.inc
include \masm32\include\comdlg32.inc
includelib \masm32\lib\user32.lib
includelib \masm32\lib\kernel32.lib
includelib \masm32\lib\comdlg32.lib

.const
IDM_OPEN equ 1
IDM_SAVE equ 2
IDM_EXIT equ 3
MAXSIZE equ 260

.data
ClassName db "Win32ASMFileMappingClass",0
AppName db "Win32 ASM File Mapping Example",0
MenuName db "FirstMenu",0
ofn  OPENFILENAME <>
FilterString db "All Files",0,"*.*",0
            db "Text Files",0,"*.txt",0,0
buffer db MAXSIZE dup(0)
hMapFile HANDLE 0                           ; Handle to the memory mapped file, must be
                                                                   ;initialized with 0 because we also use it as
                                                                   ;a flag in WM_DESTROY section too

.data?
hInstance HINSTANCE ?
CommandLine LPSTR ?
hFileRead HANDLE ?                              ; Handle to the source file
hFileWrite HANDLE ?                               ; Handle to the output file
hMenu HANDLE ?
pMemory DWORD ?                                ; pointer to the data in the source file
SizeWritten DWORD ?                              ; number of bytes actually written by WriteFile

.code
start:
       invoke GetModuleHandle, NULL
       mov   hInstance,eax
       invoke GetCommandLine
       mov CommandLine,eax
       invoke WinMain, hInstance,NULL,CommandLine, SW_SHOWDEFAULT
       invoke ExitProcess,eax

WinMain proc hInst:HINSTANCE,hPrevInst:HINSTANCE,CmdLine:LPSTR,CmdShow:DWORD
   LOCAL wc:WNDCLASSEX
   LOCAL msg:MSG
   LOCAL hwnd:HWND
   mov  wc.cbSize,SIZEOF WNDCLASSEX
   mov  wc.style, CS_HREDRAW or CS_VREDRAW
   mov  wc.lpfnWndProc, OFFSET WndProc
   mov  wc.cbClsExtra,NULL
   mov  wc.cbWndExtra,NULL
   push hInst
   pop  wc.hInstance
   mov  wc.hbrBackground,COLOR_WINDOW+1
   mov  wc.lpszMenuName,OFFSET MenuName
   mov  wc.lpszClassName,OFFSET ClassName
   invoke LoadIcon,NULL,IDI_APPLICATION
   mov  wc.hIcon,eax
   mov  wc.hIconSm,eax
   invoke LoadCursor,NULL,IDC_ARROW
   mov  wc.hCursor,eax
   invoke RegisterClassEx, addr wc
   invoke CreateWindowEx,WS_EX_CLIENTEDGE,ADDR ClassName,\
               ADDR AppName, WS_OVERLAPPEDWINDOW,CW_USEDEFAULT,\
              CW_USEDEFAULT,300,200,NULL,NULL,\
   hInst,NULL
   mov  hwnd,eax
   invoke ShowWindow, hwnd,SW_SHOWNORMAL
   invoke UpdateWindow, hwnd
   .WHILE TRUE
       invoke GetMessage, ADDR msg,NULL,0,0
       .BREAK .IF (!eax)
       invoke TranslateMessage, ADDR msg
       invoke DispatchMessage, ADDR msg
   .ENDW
   mov    eax,msg.wParam
   ret
WinMain endp

WndProc proc hWnd:HWND, uMsg:UINT, wParam:WPARAM, lParam:LPARAM
   .IF uMsg==WM_CREATE
       invoke GetMenu,hWnd                      ;Obtain the menu handle
       mov hMenu,eax
       mov ofn.lStructSize,SIZEOF ofn
       push hWnd
       pop ofn.hWndOwner
       push hInstance
       pop ofn.hInstance
       mov ofn.lpstrFilter, OFFSET FilterString
       mov ofn.lpstrFile, OFFSET buffer
       mov ofn.nMaxFile,MAXSIZE
   .ELSEIF uMsg==WM_DESTROY
       .if hMapFile!=0
           call CloseMapFile
       .endif
       invoke PostQuitMessage,NULL
   .ELSEIF uMsg==WM_COMMAND
       mov eax,wParam
       .if lParam==0
           .if ax==IDM_OPEN
               mov ofn.Flags, OFN_FILEMUSTEXIST or \
                               OFN_PATHMUSTEXIST or OFN_LONGNAMES or\
                               OFN_EXPLORER or OFN_HIDEREADONLY
                               invoke GetOpenFileName, ADDR ofn
               .if eax==TRUE
                   invoke CreateFile,ADDR buffer,\
                                               GENERIC_READ ,\
                                               0,\
                                               NULL,OPEN_EXISTING,FILE_ATTRIBUTE_ARCHIVE,\
                                               NULL
                   mov hFileRead,eax
                   invoke CreateFileMapping,hFileRead,NULL,PAGE_READONLY,0,0,NULL
                   mov    hMapFile,eax
                   mov    eax,OFFSET buffer
                   movzx edx,ofn.nFileOffset
                   add     eax,edx
                   invoke SetWindowText,hWnd,eax
                   invoke EnableMenuItem,hMenu,IDM_OPEN,MF_GRAYED
                   invoke EnableMenuItem,hMenu,IDM_SAVE,MF_ENABLED
               .endif
           .elseif ax==IDM_SAVE
               mov ofn.Flags,OFN_LONGNAMES or\
                               OFN_EXPLORER or OFN_HIDEREADONLY
               invoke GetSaveFileName, ADDR ofn
               .if eax==TRUE
                   invoke CreateFile,ADDR buffer,\
                                               GENERIC_READ or GENERIC_WRITE ,\
                                               FILE_SHARE_READ or FILE_SHARE_WRITE,\
                                               NULL,CREATE_NEW,FILE_ATTRIBUTE_ARCHIVE,\
                                               NULL
                   mov hFileWrite,eax
                   invoke MapViewOfFile,hMapFile,FILE_MAP_READ,0,0,0
                   mov pMemory,eax
                   invoke GetFileSize,hFileRead,NULL
                   invoke WriteFile,hFileWrite,pMemory,eax,ADDR SizeWritten,NULL
                   invoke UnmapViewOfFile,pMemory
                   call  CloseMapFile
                   invoke CloseHandle,hFileWrite
                   invoke SetWindowText,hWnd,ADDR AppName
                   invoke EnableMenuItem,hMenu,IDM_OPEN,MF_ENABLED
                   invoke EnableMenuItem,hMenu,IDM_SAVE,MF_GRAYED
               .endif
           .else
               invoke DestroyWindow, hWnd
           .endif
       .endif
   .ELSE
       invoke DefWindowProc,hWnd,uMsg,wParam,lParam
       ret
   .ENDIF
   xor   eax,eax
   ret
WndProc endp

CloseMapFile PROC
       invoke CloseHandle,hMapFile
       mov   hMapFile,0
       invoke CloseHandle,hFileRead
       ret
CloseMapFile endp

end start

Analysis:

invoke CreateFile,ADDR buffer,\
                  GENERIC_READ ,\
                  0,\
                  NULL,OPEN_EXISTING,FILE_ATTRIBUTE_ARCHIVE,\
                  NULL

ファイルオープンダイアログボックスでファイルを選択したら、CreateFile関数でそのファイルをオープンする。 その際、読み取り専用を表すGENERIC_READフラグを指定し、 このプログラムの操作中に他プロセスからのこのファイルに対する操作を許可しないようにするために dwShareMode を0にする。

invoke CreateFileMapping,hFileRead,NULL,PAGE_READONLY,0,0,NULL

そして、オープンしたファイルからMMファイルを作成するために、CreateFileMapping関数をCALLする。 CreatえFileMapping関数のプロトタイプは以下のようになっている。

CreateFileMapping proto hFile                   : DWORD,\
                        lpFileMappingAttributes : DWORD,\
                        flProtect               : DWORD,\
                        dwMaximumSizeHigh       : DWORD,\
                        dwMaximumSizeLow        : DWORD,\
                        lpName                  : DWORD

まずはじめに知っておかなければならないのは、CreateFileMapping関数でファイル全体をメモリにマッピングする必要はないということである。 ファイルの一部にマッピングしておけばよい。 どれぐらいの領域をマッピングするかは、dwMaximumSizeHighとdwMaximumSizeLowの引数にセットする値によるのだが、 もし、実際のファイルサイズより大きなサイズをセットした場合、 そのサイズに合わせてファイルサイズが大きくなる。 ファイルサイズと同サイズのメモリマッピング領域にしたければ、dwMaximumSizeHighとdwMaximumSizeLowの両方のパラメータを0にすればよい。

Windowsにデフォルト設定のセキュリティ属性を持ったMMファイルを作成してもらうためには、 引数lpFileMappingAttributesをNULLに設定すればよい。

flProtectはMMファイルの保護属性を定義する。 このチュートリアルでは、MMファイルを読み込み専用にするため、PAGE_READONLYを使用している。 注意しなければならないのは、この属性はCreateFile関数で指定したフラグと矛盾の無いものにしなければならないということだ。 矛盾(CreateFile関数では読み込み専用なのに、CraeteFileMapping関数では読み書き許可というような状況)が生じると、CreateFileMapping関数は失敗することになる。

lpNameはMMファイル名を指すポインタで、他プロセスとこのファイルを共有する場合は、名前をつけなければならない。 しかし、このサンプルでは、他プロセスと共有していないので、このパラメータは無視している。

mov    eax,OFFSET buffer
movzx  edx,ofn.nFileOffset
add    eax,edx
invoke SetWindowText,hWnd,eax

CreateFileMapping関数が成功すれば、ウィンドウのキャプションをオープンしたファイル名に変更している。 フルパスでバッファに格納されるのだが、キャプションにはファイル名だけを表示したいので、 そのフルパスの入っているバッファのアドレスに、OPENFILENAME構造体のメンバnFileOffsetを加えなければならない。

invoke EnableMenuItem,hMenu,IDM_OPEN,MF_GRAYED
invoke EnableMenuItem,hMenu,IDM_SAVE,MF_ENABLED

予防策として、ユーザが一度に複数ファイルをオープンできないようにするため、 Openメニューアイテムをグレーアウトし、Saveメニューアイテムを選択可能にしている。 EnableMenuItem関数はメニューアイテムの属性を変更するために使用する関数である。

この後は、ユーザが「File」→「Save」メニューを選択するか、プログラムを終了するかを待つことになる。 もしユーザが終了すれば、通常のファイル操作と同じように、MMファイルもクローズしないといけない。 そのコードは以下のようになっている。

.ELSEIF uMsg==WM_DESTROY
    .if hMapFile!=0
        call CloseMapFile
    .endif
    invoke PostQuitMessage,NULL

上記のコードは、ウィンドウプロシージャがWM_DESTROYメッセージを受け取ったときの処理を記述しており、 まずはじめに、hMapFileが0かどうかをチェックしている。 もし0なら、以下のような処理を行うCloseMapFile関数をCALLすることになる。

CloseMapFile PROC
       invoke CloseHandle,hMapFile
       mov    hMapFile,0
       invoke CloseHandle,hFileRead
       ret
CloseMapFile endp

CloseMapFile関数は、Windowsに制御を戻す際に、リソースリークが無いようにするために、 MMファイルと実際のファイルをクローズする。 ユーザがデータを他のファイルに保存するためにSaveメニューアイテムを選択したら、 プログラムはファイルを保存するダイアログボックスを表示し、 そのダイアログボックスにユーザが新しいファイル名を書き込んで、 CreateFile関数で、その名前のファイルを作成する。

invoke MapViewOfFile,hMapFile,FILE_MAP_READ,0,0,0
mov pMemory,eax

出力ファイルが作成された直後に、MapViewOfFile関数をCALLし、 メモリにマップするファイルの領域を指定する。 この関数のプロトタイプは以下のようになっている。

MapViewOfFile proto hFileMappingObject   : DWORD,\
                    dwDesiredAccess      : DWORD,\
                    dwFileOffsetHigh     : DWORD,\
                    dwFileOffsetLow      : DWORD,\
                    dwNumberOfBytesToMap : DWORD

MapViewOfFile関数をCALLしたら、メモリに指定した部分を読み込み、 ファイルのデータが格納されているメモリブロックへのポインタが返ってくる。

invoke GetFileSize,hFileRead,NULL

ファイルがどれぐらいの大きさかを調査するため、GetFileSize関数をCALLすると、 eaxレジスタの値がファイルサイズとなっている。 もし4GB以上のサイズだった場合、第2引数にDWORD型のメモリのアドレスを指定することにより、 上位DWORD(=4バイト)がその第2引数に格納されることになるのだが、 この例ではそのような大きなファイルを扱わないのでNULLを指定している。 4GB未満のファイルサイズの場合はNULLを指定することになっているのである。

invoke WriteFile,hFileWrite,pMemory,eax,ADDR SizeWritten,NULL

メモリにマップされているデータを出力ファイルに書き込む

invoke UnmapViewOfFile,pMemory

入力ファイルに対する処理が終われば、メモリとのマッピングを解放する

call   CloseMapFile
invoke CloseHandle,hFileWrite

そして、全てのファイルをクローズする

invoke SetWindowText,hWnd,ADDR AppName

ウィンドウのキャプションを元に戻す

invoke EnableMenuItem,hMenu,IDM_OPEN,MF_ENABLED
invoke EnableMenuItem,hMenu,IDM_SAVE,MF_GRAYED

Openメニューアイテムを選択可能にし、Saveメニューアイテムをグレーアウトにして使用不可能にする


[戻る]