![](https://habrastorage.org/storage2/fba/abd/c3e/fbaabdc3e3fb72d23ada9ac5c270a992.jpg)
(Интересно, всегдашние холиворщики «это была графическая оболочка, а не операционная система» в курсе об этих её необычайных способностях?)
И как же она ухитрялась?
Управление данными
![](https://habrastorage.org/storage2/16c/b50/3e1/16cb503e1208e316f196190dcf6ea680.jpg)
Функции
GlobalLock
/ GlobalUnlock
и LockResource
/ FreeResource
сохранились в Win32API для совместимости с теми дремучими временами, хотя в Win32 блоки памяти (в том числе ресурсы) никогда не перемещались.Функции
LockSegment
и UnlockSegment
(закреплять/освобождать память по адресу, а не по хэндлу) оставались какое-то время в документации с пометкой «obsolete, do not use», но теперь от них не осталось даже воспоминания.Для тех, кому нужно закреплять память на долгий промежуток времени, была ещё функция
GlobalWire
— «чтобы блок не торчал посередине адресного пространства, перенести его в нижний край памяти и закрепить там»; ей соответствала GlobalUnwire
, полностью равносильная GlobalUnlock
. Эта пара функций, на удивление, жива в kernel32.dll до сих пор, хотя из документации они уже удалены. Сейчас они просто перевызывают GlobalLock
/ GlobalUnlock
.![](https://habrastorage.org/storage2/27e/63a/077/27e63a077bd00ae9bcd96468509c9c8b.jpg)
GlobalLock
заменили «заглушкой»: теперь Windows может перетасовывать блоки памяти, не изменяя их «виртуальный адрес», видимый приложению (селектор: смещение) — а значит, приложению теперь нет надобности закреплять невыгружаемые объекты. Иными словами, закрепление теперь предотвращает выгрузку блока, но не предотвращает его (незаметное для приложения) перемещение. Поэтому для закрепления данных «взаправду» в физической памяти, для тех, кому нужно именно это (например, для работы со внешними устройствами), добавили пару GlobalFix
/ GlobalUnfix
. Так же, как и GlobalWire
/ GlobalUnwire
, в Win32 эти функции стали бесполезными; и они точно так же удалены из документации, хотя остались в kernel32.dll, и перевызывают GlobalLock
/ GlobalUnlock
.Управление кодом
Самое хитрое начинается здесь. Блоки кода — так же, как и неизменяемые данные — удалялись из памяти, и потом загружались из исполняемого файла. Но как Windows обеспечивала, что программы не попытаются вызвать функции в выгруженных блоках? Можно было бы обращаться и к функциям через хэндлы, и перед каждым вызовом функции вызывать гипотетическую
LockFunction
; но вспомним, что многие функции крутят «message loop», например показывают окно или выполняют DDE-команды, — и их на это время тоже можно было бы выгрузить, т.к. фактически их код в это время не нужен. Тем не менее, при использовании «хэндлов функций» сегмент функции не будет освобождён до тех пор, пока она не вернёт управление вызвавшей функции.![](https://habrastorage.org/storage2/020/544/d2f/020544d2fef2234cfb9a629708bc134d.jpg)
Так что Windows проходит по стекам всех запущенных задач (так назывались контексты выполнения в Windows, пока не разделили процессы и потоки), находит адреса возврата, ведущие внутрь выгруженных сегментов, и заменяет их на адреса reload thunks — «заглушек», которые загружают нужный сегмент из исполняемого файла, и передают управление внутрь него, как ни в чём не бывало.
Чтобы Windows могла пройтись по стеку, программы обязаны поддерживать его в правильном формате: никакого FPO, кадр стека обязан начинаться с
BP
— указателя на кадр вызвавшей функции. (Поскольку стек состоит из 16-битных слов, значение BP
всегда чётное.) Кроме того, Windows должна различать в стеке записи внутрисегментных («близких») и межсегментных («далёких») вызовов, и близкие вызовы может игнорировать — они-то уж точно не ведут в выгруженный сегмент. Поэтому постановили, что нечётное значение BP
в стеке означает далёкий вызов, т.е. каждая далёкая функция должна начинаться с пролога INC BP; PUSH BP; MOV BP,SP
и заканчиваться эпилогом POP BP; DEC BP; RETF
(На самом деле пролог и эпилог были сложнее, но сейчас не об этом.)![](https://habrastorage.org/storage2/ef9/4e7/0fe/ef94e70feded73b45d181a33e7e1cc1f.jpg)
int 3fh
, и ещё трёх служебных байтов, указывающих, где искать функцию. Обработчик int 3fh
находит по своему адресу возврата эти служебные байты; определяет нужный сегмент; загружает его в память, если он ещё не загружен; и напоследок перезаписывает заглушку в таблице входов абсолютным переходом jmp xxxx:yyyy
на тело функции, так что следующие вызовы этой же функции замедляются лишь на один межсегментный переход, без прерывания.Теперь, когда Windows выгружает функцию, ей достаточно в таблице входов модуля заменить вставленный переход обратно на заглушку
int 3fh
. Системе незачем искать все вызовы выгруженной функции — они все были найдены ещё при компиляции! В «таблицу входов» модуля сведены все далёкие функции, про которые компилятор знает о существовании межсегментных вызовов (сюда относятся, в частности, экспортируемые функции и WinMain
), а также все далёкие функции, которые передавались куда-либо по указателю, а значит, могли вызываться откуда угодно, даже извне кода программы (сюда относятся WndProc
, EnumFontFamProc
и прочие callback-функции).![](https://habrastorage.org/storage2/e09/3a8/16e/e093a816eacb004e01182d57057b07de.jpg)
GetWindowLong(GWL_WNDPROC)
и подобных вызовов, тоже указывают на заглушку, а не на тело функции. Даже GetProcAddress
хитрит, и вместо адреса функции возвращает адрес её заглушки в таблице входов DLL. (В Win32 аналог «таблицы входов» лишь у DLL и остался, под названием «таблицы экспортов».) Статические межмодульные вызовы (вызовы функций, импортируемых из DLL) резолвятся при помощи той же самой GetProcAddress
, и поэтому точно так же вызывают в итоге заглушку. В любом случае оказывается, что при выгрузке функции достаточно исправить заглушку, и не нужно трогать сам вызывающий код.Вся эта премудрость с перемещаемыми сегментами кода пришла в Windows «по наследству» из оверлейного линкера для DOS. Мол, сначала вся схема — в точности в таком виде — появилась в компиляторе Zortech C, а потом и в Microsoft C. Когда создавался формат исполнимых файлов для Windows, за основу взяли уже существующий формат оверлеев для DOS.
![](https://habrastorage.org/storage2/8af/bdd/f24/8afbddf241977379a738fd3b26455d1d.jpg)
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