hozugawa.net · A neutral informational page

VirtualAllocによるメモリ確保と解放のベストプラクティス

Windowsアプリケーションで大きなメモリ領域を扱う場合、VirtualAlloc と VirtualFree は、仮想アドレス空間の予約と物理メモリの割り当てを細かく制御できるAPIです。ヒープ管理関数より低いレベルで動作するため、メモリマップ、実行時コード、巨大バッファ、ガードページなどを実装する場面で役立ちます。

一方で、予約とコミット、ページ単位と割り当て粒度、解放フラグの違いを曖昧にすると、メモリリークやアクセス違反につながります。ここでは、Win32メモリ管理の基本から、失敗時の調査、保護属性、実装時の安全なパターンまでを整理します。Windows APIの周辺資料を確認する際は、Windows API資料も関連情報を探す手がかりになります。

予約とコミットを分けて考える

VirtualAlloc の動作は、仮想アドレス範囲を確保する「予約」と、実際に使用可能なページを準備する「コミット」に分けて理解します。MEM_RESERVE はアドレス範囲だけを予約し、他の割り当てとの重複を防ぎます。この段階では、通常、予約した領域へ読み書きできません。

MEM_COMMIT はページをコミットし、プロセスのコミット上限に対してメモリを計上します。新規ページは原則としてゼロ初期化されます。大容量領域を一度にコミットするより、必要な範囲を段階的にコミットすれば、初期負荷やコミット量を抑えられます。ただし、コミット予約を後回しにすると、使用時に失敗する可能性があるため、失敗処理は必ず用意します。

一般的な初回確保では、MEM_RESERVE | MEM_COMMIT を指定します。アドレスを指定しない場合は、システムが適切な空き仮想アドレスを選びます。64ビットプロセスではアドレス空間が広くても、アドレス値を32ビット型へ格納しないことが重要です。

サイズとページ境界を正しく扱う

確保サイズはバイト単位で指定しますが、実際の管理はページ単位で行われます。dwSize は要求値のまま扱われるとは限らず、システムがページ境界に合わせて範囲を調整します。予約時の開始アドレスには、ページサイズとは異なる割り当て粒度が関係します。固定アドレスを指定する設計では、GetSystemInfo などで環境情報を確認してください。

大きなバッファでは、整数オーバーフローも見落とせません。要素数と要素サイズを乗算する前に、上限チェックを行い、SIZE_T で計算します。ゼロサイズや、計算結果がアドレス空間の上限を超える値は、API呼び出し前に拒否する方が安全です。

lpAddress に任意のアドレスを渡す場合、指定範囲が利用可能でなければ確保は失敗します。ASLRやDLLの配置によって実行環境ごとに空き領域は変わるため、特別な理由がない限り、アドレスはNULLにしてシステムへ選択させる設計が扱いやすくなります。

VirtualFreeの解放規則を理解する

予約とコミットを同時に解除する場合は、VirtualFree に MEM_RELEASE を指定し、dwSize を0にします。このとき lpAddress は、予約時に返されたベースアドレスでなければなりません。途中のアドレスや、予約範囲の一部だけを指定して解放することはできません。

コミットだけを取り消して予約を残す場合は、MEM_DECOMMIT を使用します。この場合は解放する範囲とサイズを指定し、必要なら後で同じ予約領域へ再コミットできます。MEM_DECOMMIT と MEM_RELEASE は目的が異なるため、フラグを置き換えるだけの感覚で使わないことが大切です。

確保した領域を管理する構造体には、返されたベースアドレス、予約サイズ、コミット済みサイズ、保護属性などを保持すると、解放処理の誤りを減らせます。エラー経路や例外処理でも一度だけ解放されるよう、C++ではRAII、Cでは単一の後始末経路を設けると管理しやすくなります。

用途に応じてメモリAPIを選ぶ

VirtualAlloc はページ単位の仮想メモリ管理に向いていますが、すべての動的メモリに適しているわけではありません。小さなオブジェクトを頻繁に確保・解放する処理では、内部で再利用を行うヒープAPIの方が効率的です。

API 主な単位 適した用途 解放方法
VirtualAlloc ページ、仮想アドレス範囲 大容量領域、予約とコミットの制御 VirtualFree
HeapAlloc ヒープブロック 小中規模オブジェクトの動的管理 HeapFree
LocalAlloc 互換的なローカルメモリ 古いAPIとの連携 LocalFree
MapViewOfFile ファイルマッピングページ 共有メモリ、メモリマップドファイル UnmapViewOfFile

VirtualAlloc で確保したメモリを free や HeapFree で解放してはいけません。反対に、malloc や HeapAlloc の戻り値を VirtualFree に渡すことも不正です。確保関数と解放関数は必ず同じ系統で対応させます。

保護属性と実行権限を管理する

読み書き用の領域には、通常 PAGE_READWRITE を指定します。実行可能な領域が必要な場合でも、確保直後から PAGE_EXECUTE_READWRITE にするのは避けるべきです。書き込み完了後に VirtualProtect で PAGE_EXECUTE_READ などへ変更し、書き込み可能と実行可能な状態を分離します。

この設計は、DEPやCFGなどの防御機構と整合しやすく、意図しないコード実行の範囲を狭めます。JITコンパイラやプラグイン基盤のように実行権限が必要な場合は、生成したコードのサイズと入口を管理し、不要になったページを速やかに解放します。

ページのアクセス保護を変更した後、別のスレッドが同じ領域を使用すると競合が起こる可能性があります。書き込み、保護属性の変更、命令キャッシュの同期、実行開始という順序を設計し、必要な同期を確保してください。

安全な実装手順を組み立てる

大容量バッファやリングバッファでは、予約を先に行い、実際に使う区間だけをコミットする方式が有効です。未コミット領域を誤って参照しないよう、論理的な使用範囲とコミット済み範囲を別々に記録します。番兵ページやガードページを設ければ、境界を越えた書き込みを検出しやすくなります。

実装時には、次の点を確認するとメモリ管理の不具合を抑えられます。

なお、確保成功後に後続処理が失敗した場合も、予約とコミットの状態に応じて後始末を分けます。部分的にコミットした領域を順番に戻すか、予約全体を一括で解放するかを決めておくと、例外や早期リターンによるリークを防げます。

失敗とリークを調査する

APIがNULLを返した場合は、直ちに GetLastError を呼び出します。後続のログ処理や別API呼び出しによってエラーコードが上書きされることがあるため、取得した値をローカル変数へ保存してから診断します。サイズ、保護属性、予約済み範囲、プロセスのコミット状況も同時に記録すると原因を追いやすくなります。

デバッグ時は、Process Explorer、Windows Performance Recorder、Application Verifierなどを使い、仮想アドレスの使用状況やコミット量を確認できます。ページヒープは通常のヒープ向け機能ですが、周辺のメモリ破壊を発見する補助になります。アクセス違反では、例外アドレスが予約済みか、コミット済みか、保護属性が何かを調べることが有効です。

最も重要なのは、解放を「処理の最後にまとめて行う作業」と考えず、所有権の一部として設計することです。VirtualAlloc と VirtualFree の対応、予約とコミットの区別、ページ保護の意図をコード上で明確にすれば、低レベルAPIの柔軟性を保ちながら、メモリリークと不正アクセスの危険を抑えられます。読者が覚えておくべき要点は、予約・コミット・保護・解放を別々の状態として管理することです。