
(Интересно, всегдашние холиворщики «это была графическая оболочка, а не операционная система» в курсе об этих её необычайных способностях?)
И как же она ухитрялась?
Управление данными

Функции
GlobalLock
/ GlobalUnlock
и LockResource
/ FreeResource
сохранились в Win32API для совместимости с теми дремучими временами, хотя в Win32 блоки памяти (в том числе ресурсы) никогда не перемещались.Функции
LockSegment
и UnlockSegment
(закреплять/освобождать память по адресу, а не по хэндлу) оставались какое-то время в документации с пометкой «obsolete, do not use», но теперь от них не осталось даже воспоминания.Для тех, кому нужно закреплять память на долгий промежуток времени, была ещё функция
GlobalWire
— «чтобы блок не торчал посередине адресного пространства, перенести его в нижний край памяти и закрепить там»; ей соответствала GlobalUnwire
, полностью равносильная GlobalUnlock
. Эта пара функций, на удивление, жива в kernel32.dll до сих пор, хотя из документации они уже удалены. Сейчас они просто перевызывают GlobalLock
/ GlobalUnlock
.
GlobalLock
заменили «заглушкой»: теперь Windows может перетасовывать блоки памяти, не изменяя их «виртуальный адрес», видимый приложению (селектор: смещение) — а значит, приложению теперь нет надобности закреплять невыгружаемые объекты. Иными словами, закрепление теперь предотвращает выгрузку блока, но не предотвращает его (незаметное для приложения) перемещение. Поэтому для закрепления данных «взаправду» в физической памяти, для тех, кому нужно именно это (например, для работы со внешними устройствами), добавили пару GlobalFix
/ GlobalUnfix
. Так же, как и GlobalWire
/ GlobalUnwire
, в Win32 эти функции стали бесполезными; и они точно так же удалены из документации, хотя остались в kernel32.dll, и перевызывают GlobalLock
/ GlobalUnlock
.Управление кодом
Самое хитрое начинается здесь. Блоки кода — так же, как и неизменяемые данные — удалялись из памяти, и потом загружались из исполняемого файла. Но как Windows обеспечивала, что программы не попытаются вызвать функции в выгруженных блоках? Можно было бы обращаться и к функциям через хэндлы, и перед каждым вызовом функции вызывать гипотетическую
LockFunction
; но вспомним, что многие функции крутят «message loop», например показывают окно или выполняют DDE-команды, — и их на это время тоже можно было бы выгрузить, т.к. фактически их код в это время не нужен. Тем не менее, при использовании «хэндлов функций» сегмент функции не будет освобождён до тех пор, пока она не вернёт управление вызвавшей функции.
Так что Windows проходит по стекам всех запущенных задач (так назывались контексты выполнения в Windows, пока не разделили процессы и потоки), находит адреса возврата, ведущие внутрь выгруженных сегментов, и заменяет их на адреса reload thunks — «заглушек», которые загружают нужный сегмент из исполняемого файла, и передают управление внутрь него, как ни в чём не бывало.
Чтобы Windows могла пройтись по стеку, программы обязаны поддерживать его в правильном формате: никакого FPO, кадр стека обязан начинаться с
BP
— указателя на кадр вызвавшей функции. (Поскольку стек состоит из 16-битных слов, значение BP
всегда чётное.) Кроме того, Windows должна различать в стеке записи внутрисегментных («близких») и межсегментных («далёких») вызовов, и близкие вызовы может игнорировать — они-то уж точно не ведут в выгруженный сегмент. Поэтому постановили, что нечётное значение BP
в стеке означает далёкий вызов, т.е. каждая далёкая функция должна начинаться с пролога INC BP; PUSH BP; MOV BP,SP
и заканчиваться эпилогом POP BP; DEC BP; RETF
(На самом деле пролог и эпилог были сложнее, но сейчас не об этом.)
int 3fh
, и ещё трёх служебных байтов, указывающих, где искать функцию. Обработчик int 3fh
находит по своему адресу возврата эти служебные байты; определяет нужный сегмент; загружает его в память, если он ещё не загружен; и напоследок перезаписывает заглушку в таблице входов абсолютным переходом jmp xxxx:yyyy
на тело функции, так что следующие вызовы этой же функции замедляются лишь на один межсегментный переход, без прерывания.Теперь, когда Windows выгружает функцию, ей достаточно в таблице входов модуля заменить вставленный переход обратно на заглушку
int 3fh
. Системе незачем искать все вызовы выгруженной функции — они все были найдены ещё при компиляции! В «таблицу входов» модуля сведены все далёкие функции, про которые компилятор знает о существовании межсегментных вызовов (сюда относятся, в частности, экспортируемые функции и WinMain
), а также все далёкие функции, которые передавались куда-либо по указателю, а значит, могли вызываться откуда угодно, даже извне кода программы (сюда относятся WndProc
, EnumFontFamProc
и прочие callback-функции).
GetWindowLong(GWL_WNDPROC)
и подобных вызовов, тоже указывают на заглушку, а не на тело функции. Даже GetProcAddress
хитрит, и вместо адреса функции возвращает адрес её заглушки в таблице входов DLL. (В Win32 аналог «таблицы входов» лишь у DLL и остался, под названием «таблицы экспортов».) Статические межмодульные вызовы (вызовы функций, импортируемых из DLL) резолвятся при помощи той же самой GetProcAddress
, и поэтому точно так же вызывают в итоге заглушку. В любом случае оказывается, что при выгрузке функции достаточно исправить заглушку, и не нужно трогать сам вызывающий код.Вся эта премудрость с перемещаемыми сегментами кода пришла в Windows «по наследству» из оверлейного линкера для DOS. Мол, сначала вся схема — в точности в таком виде — появилась в компиляторе Zortech C, а потом и в Microsoft C. Когда создавался формат исполнимых файлов для Windows, за основу взяли уже существующий формат оверлеев для DOS.

int 3fh
или заменяющим его jmp
) инструкцию sar byte ptr cs:[xxx], 1
, которая сбрасывает байт-счётчик из 1 в 0 при каждом вызове функции. Эта инструкция как раз занимает пять байт: можно сохранить существующий формат исполнимого файла, и загружать заглушки int 3fh
через одну, перемежая инструкцией-счётчиком.Значения счётчиков для всех сегментов кода инициализируются в 1, и раз в 250мс Windows обходит все модули, собирает обновлённые значения, и переупорядочивает сегменты кода в своём списке LRU. Обращения к сегментам данных можно отследить и безо всяких ухищрений: все такие обращения и так отмечены явным вызовом
GlobalLock
или аналогичных функций. Так что когда приходит время выгрузить какой-нибудь сегмент, чтобы освободить память — Windows постарается выгрузить тот сегмент, к которому дольше всего не было обращений: либо сегмент кода, счётчик которого дольше всего не сбрасывался в 0, либо сегмент данных, который дольше всего не закреплялся. Рекламные объявления Windows 1.0-2.1 взяты на GUIdebook