Windowsのリアルタイムメモリ管理

最近、レイモンドチェンは1年半前に始まった一連の投稿を完了し、プロセッサからのサポートなしで仮想メモリの管理に専念しました。 (システムバスで発行される)は、セグメントとオフセットの巧妙な追加によって実行されます-「アクセス制御」、「無効なアドレス」はありません。 すべてのアドレスは誰でも利用できます。 同時に、複数のプログラムがWindowsで同時に動作し、互いに干渉することはありません。 Windowsはメモリ内のセグメントを移動し、未使用のセグメントをアンロードし、必要に応じて、場合によっては他のアドレスにロードし直すことができます。

(興味深いことに、これらの並外れた能力を知っているホリボルスキキは、「オペレーティングシステムではなく、グラフィカルシェルでした」という既存のホリボルシキ?)

そして、彼女はどのように管理しましたか?

データ管理


リアルタイムWindowsにはスワップはありませんでした。 不変データ(リソースなど)はメモリから削除され、必要に応じて実行可能ファイルから再度読み込まれました。 可変データはアンロードできませんでしたが、(他のデータと同様に)移動できました。メモリブロックを操作するアプリケーションはアドレスを使用せず、ハンドルを使用します。 また、データにアクセスするときは、ブロックを「修正」してアドレスを取得し、必要に応じてWindowsが移動できるように「解放」します。 十数年後に.NETに似たものが登場し、すでにピン止めと呼ばれていました。

関数GlobalLock / GlobalUnlockおよびLockResource / FreeResourceは、メモリブロック(リソースを含む)がWin32APIで移動することはありませんでしたが、これらの古代との互換性のためにFreeResourceに保持されました。

LockSegmentおよびUnlockSegment (ハンドルではなく、アドレスでメモリをUnlockSegment /解放する)は、「廃止、使用しない」とマークされたドキュメントにしばらく残っていましたが、メモリが残っていません。

長期間メモリを修正する必要がある人のために、 GlobalWire機能もありました-「ブロックがアドレス空間の中央にGlobalWireないように、メモリの下端に移動して修正します」。 GlobalUnwireと一致し、 GlobalUnwireと完全に同等です。 このペアの関数は、驚いたことに、kernel32.dllでまだ機能していますが、ドキュメントからは既に削除されています。 現在は、 GlobalLock / GlobalUnlockです。

保護モードのWindowsでは、 GlobalLock機能GlobalLock 「スタブ」 GlobalLock置き換えられました。Windowsは、アプリケーションに表示される「仮想アドレス」を変更せずにメモリブロックをシャッフルできるようになりました(セレクター:オフセット)-これは、アプリケーションがアップロードできないオブジェクトを修正する必要がなくなったことを意味します。 言い換えれば、ピン留めはブロックのアンロードを防止しますが、(アプリケーションには見えない)ブロックの移動は防止しません。 したがって、物理メモリ内のデータを「実際に」修正するために、それだけが必要な人(たとえば、外部デバイスで作業するため)に、 GlobalFix / GlobalUnfixペアを追加しました。 GlobalWire / GlobalUnwireと同様に、Win32ではこれらの関数は役に立たなくなりました。 また、同じようにドキュメントから削除されますが、kernel32.dllに残り、 GlobalLock / GlobalUnlock GlobalLock GlobalUnlock

コード管理


最も難しいのはここから始まります。 コードのブロック-不変データ-はメモリから削除され、実行可能ファイルからロードされました。 しかし、Windowsは、プログラムがアンロードされたブロック内の関数を呼び出そうとしないことをどのように確認しましたか? ハンドルを使用して関数にアクセスし、各関数呼び出しの前に仮想のLockFunction呼び出すことがLockFunctionます。 ただし、ウィンドウの表示やDDEコマンドの実行など、多くの関数が「メッセージループ」をひねり、この時間にアンロードすることもできます。 実際、現時点ではそれらのコードは必要ありません。 ただし、「関数ハンドル」を使用する場合、関数セグメントは、呼び出し元の関数に制御を戻すまで解放されません。

代わりに、Windowsは、現在実行されていない関数をアンロードできると想定することから始めます。 Windowsメモリマネージャーのコードが現在実行されているため、 すべての関数アンロードできます。 この関数がアンロードする前に戻らなかった場合、リンクはプログラムコードまたはスタックのいずれかに残ることができます。

そのため、Windowsは実行中のすべてのタスクのスタック(プロセスとスレッドを分離するまでWindowsのいわゆる実行コンテキスト)を調べ、アンロードされたセグメント内の先頭のリターンアドレスを見つけ、 リロードサンクのアドレスで置き換えます。何も起こらなかったかのように、実行可能ファイルから、制御をその中に転送します。

Windowsがスタック上を歩くことができるように、プログラムは正しい形式でそれをサポートする必要があります。FPOなし、スタックフレームは呼び出し関数のフレームへのBPポインターで始まる必要があります。 (スタックは16ビットワードで構成されるため、 BP値は常に偶数です。)さらに、Windowsはスタック内のセグメント内(「閉じる」)呼び出しとセグメント間(「遠」)呼び出しを区別する必要があります。それらは、アンロードされたセグメントに正確にはつながりません。 したがって、彼らは、スタック内のBPの奇数の値は遠方の呼び出し、つまり すべての遠隔機能は、 INC BP; PUSH BP; MOV BP,SPプロローグで開始する必要がありますINC BP; PUSH BP; MOV BP,SP INC BP; PUSH BP; MOV BP,SP INC BP; PUSH BP; MOV BP,SP 、およびエピローグPOP BP; DEC BP; RETF終わるPOP BP; DEC BP; RETF POP BP; DEC BP; RETF POP BP; DEC BP; RETF (実際には、プロローグとエピローグはより複雑でしたが、これは今ではそうではありません。)

スタックからリンクを見つけましたが、他のコードセグメントからのリンクはどうですか? もちろん、Windowsはメモリ全体を調べて、アンロードされた関数へのすべての呼び出しを見つけ、それらすべてをリロードサンクに置き換えることはできません。 代わりに、セグメント間呼び出し 、呼び出された関数がメモリ内にない可能性があるという事実を考慮してコンパイルされ、実際にモジュール入力テーブルの 「スタブ」を呼び出します 。 このスタブは、 int 3fh命令と、関数を探す場所を示す3バイトのサービスバイトで構成されます。 int 3fhは、リターンアドレスでこれらのサービスバイトを見つけます。 目的のセグメントを定義します。 まだロードされていない場合は、メモリにロードします。 そして最後に、入力テーブルのスタブを関数本体への絶対遷移jmp xxxx:yyyy上書きします。これにより、同じ関数への次の呼び出しは、中断することなく1つのセグメント間遷移だけで遅くなります。

これで、Windowsが関数をアンロードするときに、モジュール入力テーブル内の関数が挿入された遷移をint 3fh戻すだけで十分です。 システムは、アンロードされた関数へのすべての呼び出しを検索する必要はありません-それらはすべてコンパイル時でも見つかりました! モジュールの「 WinMainテーブル」には、セグメント間呼び出しの存在についてコンパイラが知っているすべての遠隔関数(特に、エクスポートされた関数とWinMain含まれます)、およびポインターによってどこかに渡されたすべての遠隔関数が含まれます。プログラムコードの外部からも、どこからでも呼び出されます(これには、 WndProcEnumFontFamProc 、およびその他のコールバック関数が含まれます)。

離れた関数へのポインタの代わりに、スタブへのポインタがどこにでも渡されます。 つまり、 GetWindowLong(GWL_WNDPROC)および同様の呼び出しから取得したアドレスも、関数の本体ではなくスタブを指しているということです。 GetProcAddressでもGetProcAddress関数のアドレスの代わりに、DLLエントリテーブルのスタブのアドレスを返します。 (Win32では、DLLの「入力テーブル」の類似物は、「エクスポートテーブル」という名前で残りました。)静的モジュール間呼び出し(DLLからインポートされた関数の呼び出し)は、同じGetProcAddressを使用して解決しますGetProcAddress 。 いずれにせよ、関数をアンロードするときに、スタブを修正するだけで十分であり、呼び出し元のコード自体に触れる必要はありません。

再配置可能なコードセグメントに関するこのすべての知恵は、DOSのオーバーレイリンカーから「継承によって」Windowsにもたらされました。 同様に、最初はスキーム全体- まさにこの形式で -がZortech Cコンパイラに登場し、次にMicrosoft Cに登場しました。Windowsの実行可能ファイル形式が作成されたとき、DOSの既存のオーバーレイ形式が基礎として採用されました。

しかし、Windowsはアンロードするセグメントをどのように選択しますか? ランダムに選択するのは危険です。実行されたばかりのコードをすぐにダウンロードする必要があります。 そのため、Windowsはコードセグメントに「アクセスビット」のようなものを使用します。関数へのすべてのセグメント間呼び出しがスタブを通過することを知って、命令sar byte ptr cs:[xxx], 1int 3fhまたはjmp置き換える前) int 3fh挿入するint 3fhましたsar byte ptr cs:[xxx], 1 、関数が呼び出されるたびにバイトカウンターを1から0にリセットします。 この命令は5バイトでint 3fhます。既存の実行可能ファイル形式を保存し、カウンター命令を散在させて、1つを介してint 3fhをロードできます。

すべてのコードセグメントのカウンター値は1に初期化され、250ミリ秒ごとに、Windowsはすべてのモジュールをバイパスし、更新された値を収集し、LRUリスト内のコードセグメントを並べ替えます。 データセグメントへの呼び出しは、何のトリックもなしで追跡できます。そのような呼び出しはすべて、 GlobalLockまたは同様の関数への明示的な呼び出しによってGlobalLockマークされてGlobalLockます。 そのため、メモリを解放するためにセグメントをアンロードするときが来ると-Windowsは、最も長くアクセスされていないセグメントをアンロードしようとします:カウンタが最長時間0にリセットされていないコードセグメント、または最も長く続いていないデータセグメント修正されました。

GUIdebookで撮影されたWindows広告1.0-2.1

Source: https://habr.com/ru/post/J154713/


All Articles