
2026/06/05 20:16
Win16 メモリ管理
RSS: https://news.ycombinator.com/rss
要約▶
Japanese Translation:
要旨:核心となるメッセージは、16 ビット時代の初期 Windows(1.x から 3.1 まで)が、8086/286 ハードウェア上でページングサポートなしで動作したため、後続の Windows NT アーキテクチャとは根本的に異なる専用メモリ管理システムに依存していた点にある。安定性を保つために、これらのシステムは DOS マルチタスクリングに由来する「New Executable(NE)」形式を採用し、アプリケーションを不透明なセグメントハンドルによって管理されるオーバーレイとして扱っていた。これは重要なコードを物理的な RAM 内に保持するためのものである。開発者は厳格な制約に直面しており:セグメントは緊縮化とフラグメンテーション防止のために固定または移動可能として定義され、
GlobalAlloc を使用してロック/アンロックを行うなどの特定の API ルールの遵守が必須だった。これらを無視すると、セグメントアドレスがアンロック時に無効になるような即座の論理エラーが発生する。さらに、初期の開発資料はこれらの複雑さを過小評価することが多く、開発者がメモリレイアウトに関する堅牢な規則、ローダーパッチング慣例(ローダーは自動的に関数のプロログを修正しデータセグメントを再読み込みさせる)、Windows 用コード用の /Gw コンパイラフラグ、または DLL 構築時の /Aw フラグなどを見逃す場合に脆弱性が高まる。ハードウェア保護機構が欠如したこの環境(限られたハードウェア上で複雑なアプリケーションを設計したもの)に隠されたバグを明らかにするために、SHAKER などの必須ツールの利用が必要だった。本文
16 ビット Windows のメモリ管理:技術解説
本ドキュメントは、**Microsoft NT を除く 16 ビット Windows(Windows 1.x, 2.x, 3.x)**における独特なメモリ管理メカニズムを解説します。当時は「低レベルの詳細はツールが処理する」という前提があり、メモリ管理の重要性が軽視されがちでしたが、以下の要素を理解することで、当時のシステム動作の本質を把握できます。
1. Windows メモリ管理の概要
16 ビット Windows は、物理メモリ不足に対処するため、システム全体を**「高度なオーバーレイマネージャ」**として設計していました。
基本コンセプト
- 物理メモリ制限への対応: PC の RAM がアプリケーションサイズを超えることが多かったため、物理メモリには常に最もアクティブなセグメントのみを配置し、その他はディスクに保存・読み込むオーバーレイ方式を採用しました。
- アーキテクチャの制約: 当時の主流である8086および80286は「ページング(Paging)」機能をサポートしていなかったため、このオーバーレイ方式が必須でした。
セグメントの移動可能性
Windows はセグメントを物理メモリの固定位置ではなく、「移動可能」とみなします。
- 理由: 実行時に
(コード)、CS
(データ)、DS
(スタック)レジスタに適切なセグメントを設定していれば、実際の物理アドレスを意識する必要がありません。SS - 実装要件: コードやデータへのアクセスは「近接コール(Near Call/Jump)」や「近接ポインタ」のみを使用することで保証されます。
実行形式の拡張:NE フォーマット
DOS の従来の
MZ 形式を単一ブロックとして扱うのではなく、New Executable (NE) フォーマットを採用しました。
- 構造: セグメント指向であり、各セグメントを個別にディスク上に保存します。
- 利点: 個々のセグメントを独立してロード(または再ロード)し、メモリ内で自由に移動させることが可能になります。
- エクスポート機能:
: 外部コード(OS など)への呼び出し用。インポート
: 他から呼び出されるアプリケーション側(例:ウィンドウ手続き)への登録用。エクスポート
2. メモリのシフトとハンドリング
Windows のメモリ管理の中心は**「ハンドル(Handle)」**によるセグメント識別です。
ハンドルの仕組み
- 定義: 16 ビットの不透明な値。内部構造を操作すべきではありません。
- 機能: x86 の保護モードのような「セレクトラ」に見えますが、意図的な設計(Intel 286 の保護モード機能をインスピレーション源にしたため)。
- 特徴: セグメントの物理アドレスとは無関係です。
でメモリを割り当てても返されるのはセグメントハンドルであり、実際のアドレスではありません。GlobalAlloc
アドレス取得とロック (GlobalLock
/ GlobalUnlock
)
GlobalLockGlobalUnlockハンドルはそのまま使用できず、アクセスには以下の処理が必要です。
-
アクセス準備:
GlobalLock(Handle)- セグメントアドレスを取得し、メモリをロック(ロックカウント増)します。
- ロック中のみ、セグメントは移動せず、保持されたアドレスが有効です。
-
完了解放:
GlobalUnlock(Handle)- ロックカウントを減らします。カウントがゼロになると、セグメントは再び移動・破棄される可能性があります。
⚠️ 重要:ロック解除後のリスク
GlobalUnlock 直後には即座に移動しない場合もありますが、無効化されるとの保証はありません。
- 危険性: アンロック後に取得したアドレスを使用してアクセスしてしまうと、セグメントがすでに移動していた場合「隠れバグ」を引き起こします。一度アンロックされたセグメントは、OS 側でいつでも移動・破棄される可能性があるため、厳密に解放後にアドレスを参照しないこと。
セグメントの属性
Windows はセグメントを「固定」または「移動可能」、「破棄可能」と「破棄不可」のいずれかに分類します。
-
固定 (Fixed) vs 移動可能 (Movable)
- 移動可能: 通常の設定。解放時に Windows が効率的に再配置(シャッフル)できます。(例:通常のコード・データ)
- 固定: 物理位置を保持。割り込みハンドラなど、ベクトルテーブルが特定のアドレスを指す必要がある場合のみ使用します。
-
破棄可能 (Discardable) vs 破棄不可 (Non-discardable)
- 破棄可能: 未使用時にディスクから削除可能。(例:コードセグメント、読み取り専用リソース)
- 破棄不可: 変更されたため再ロードが困難。(例:書き込み可能なデータセグメント)
3. DLL(動的リンクライブラリ)
Windows は早期に DLL を標準化し、UNIX に先駆けて共有ライブラリシステムを構築しました。
特性
- 形式: アプリケーションと同じNE フォーマットですが、直接実行できません。
- 動作原理: 他プロセス(タスク)によってロード・呼び出されます。
- ネームスペース:
- UNIX と異なり、グローバルな名空間がありません。
- モジュール名の指定と、その中の名前/順序番号による二段階解決を採用しています。
- これにより同名シンボルによる衝突が防ぎつつ、順序番号を使った高速インポートが可能になります。
スタックセグメントの制限
- DLL は独自のスタックを持たず、常に呼び出し元のプロセスのスタック上で動作します。
- 条件:
(スタック) $\neq$SS
(データ) でなければならないため、ビルド時のコンパイラ設定が異なります。DS
プロローグの必要性
コンパイラが
SS == DS 前提のプロローグ(プロローグ/エピログ)を生成しますが、DLL はこれを満たしません。そのため、Windows のロードラによるパッチアップ処理が必要な「太い」プロローグを使用します。
4. コンパイラスイッチとプロローグ詳細
Microsoft C コンパイラは Windows 開発に不可欠でしたが、初期期は設定が秘匿的でした(SDK 上でのみ参照)。主要なスイッチと動作は以下の通りです。
| スイッチ | 意味 | 用途 |
|---|---|---|
| メモリモデル修飾子 () | DLL の生成時に必須(関数内で を再ロードしないため) |
| Windows 特有のプロローグ生成 | エクスポートされた FAR 関数への必須設定 |
自動パッチ処理 (CMACROS.INC
)
CMACROS.INCコンパイラは NE モジュールからエクスポートされた関数に対し、Windows ロードラが修正可能なように以下を行います。
- デフォルトデータセグメントの読み込み:
- ロードラは関数の開始直後(最初の 3 バイト)をパッチして、
レジスタにモジュールのデフォルトデータセグメントを読み込ませます。AX
- ロードラは関数の開始直後(最初の 3 バイト)をパッチして、
- スタックフレームの維持:
- Windows がスタックをウォークするため、
の増減処理で「オフセットとセグメント情報がスタックに存在する」ことを示します。BP
- Windows がスタックをウォークするため、
- プロローグ更新:
- セグメントが移動した場合でも、エクスポートされた関数のプロローグを更新し、正しいアドレスを指すようにします。
ファイルでDEF
が指定された場合のみ、このパッチ処理はスキップされます。NODATA
⚠️ 警告: デフォルトデータセグメント以外のものをアクセスする場合は、適切にロックされている必要があります。
5. OS/2 との比較
16 ビット Windows と 16 ビット OS/2 は似ていますが、ハードウェアの違いによりアプローチが異なります。
| 特徴 | 16 ビット Windows | 16 ビット OS/2 |
|---|---|---|
| メモリモデル | オーバーレイ方式(強制) | ハードウェアによる移動・再ロード可能 |
| 管理責任 | アプリとコンパイラに高い規律が必要 | システム層がハードウェアを利用し、簡素化 |
| プロローグ | Windows 特有の「太い」プロローグ必須 | 特別なプロローグ不要(OS が処理) |
| ロック機構 | / 必須 | ハードウェアサポートにより不要 |
| コンパイラ | , スイッチ必須 | OS 側からサポートされるため簡略化可 |
OS/2 は 80286 の保護モード機能を最大限に活用し、バグの温床である「手動ロック管理」を排除した点が大きいです。
6. テスティングとツール
通常の環境では潜伏するメモリ管理バグは発見されにくいため、特別なツールによる検証が必要です。
-
Shaker (Windows 1.x/3.0 SDK)
- 機能: メモリを強制的に「揺さぶり」、セグメントの破棄・移動を強制します。
- 目的: 正しくロックされていない、または破棄可能な領域へのアクセスなどのバグを引き出すためのストレステストツールです。
-
HeapWalker (Windows 1.x/3.0 SDK)
- 機能: 現在の割り当て済みセグメントとその所有権を表示します。
- 用途: 低メモリ状態をシミュレート(全領域を 1K 単位で解放)し、クラッシュ原因を確認します。
-
Stress (Windows 3.1 SDK)
- Shaker の後継機。リソース不足(内部ヒープ制限、ディスクスペース不足、ファイルハンドル不足)時の動作を検証するために設計されています。
注: 8086 ではハードウェアが未割り当てメモリアクセスを検出しないため、専用のツールなしでは誤りを検出できません。
まとめ
16 ビット Windows は、ページング機能を持たない脆弱なハードウェア環境において、ソフトウェア的な高度なオーバーレイ管理を実現していました。
- コンパイラ設定:
,/Aw
の正確な使用。/Gw - メモリ操作:
/GlobalLock
の厳密な運用と、アンロック後のアドレス参照禁止。GlobalUnlock - エクスポート: 適切な関数のエクスポート処理の実装。
これらを遵守しない場合、セグメントの移動に伴うアクセス失敗やバグを引き起こすリスクが極めて高かったのです。