Управление памятью в реальном режиме Windows

    Недавно Реймонд Чен завершил серию постов, начатую ещё полтора года назад, и посвящённую управлению виртуальной памятью безо всякой поддержки со стороны процессора: Windows до версии 3.0 включительно поддерживала реальный режим 8086. В этом режиме трансляция адреса из «виртуального» (видимого программе) в физический (выдаваемый на системную шину) осуществляется бесхитростным сложением сегмента и смещения — никакой «проверки доступа», никаких «недопустимых адресов». Все адреса доступны всем. При этом в Windows могли одновременно работать несколько программ и не мешать друг другу; Windows могла перемещать их сегменты в памяти, выгружать неиспользуемые, и по мере необходимости подгружать назад, возможно — по другим адресам.

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

    И как же она ухитрялась?

    Управление данными


    Подкачки в реальном режиме Windows не было. Неизменяемые данные (например, ресурсы) просто удалялись из памяти, и при необходимости загружались снова из исполняемого файла. Изменяемые данные выгружаться не могли, но могли (как и любые другие данные) перемещаться: приложение для работы с блоками памяти использует не адреса, а хэндлы; и на время обращения к данным «закрепляет» блок, получая его адрес, а потом — «освобождает», чтобы Windows могла его при необходимости перемещать. Что-то аналогичное появилось спустя дюжину лет в .NET, уже под названием pinning.

    Функции GlobalLock / GlobalUnlock и LockResource / FreeResource сохранились в Win32API для совместимости с теми дремучими временами, хотя в Win32 блоки памяти (в том числе ресурсы) никогда не перемещались.

    Функции LockSegment и UnlockSegment (закреплять/освобождать память по адресу, а не по хэндлу) оставались какое-то время в документации с пометкой «obsolete, do not use», но теперь от них не осталось даже воспоминания.

    Для тех, кому нужно закреплять память на долгий промежуток времени, была ещё функция GlobalWire — «чтобы блок не торчал посередине адресного пространства, перенести его в нижний край памяти и закрепить там»; ей соответствала GlobalUnwire, полностью равносильная GlobalUnlock. Эта пара функций, на удивление, жива в kernel32.dll до сих пор, хотя из документации они уже удалены. Сейчас они просто перевызывают GlobalLock / GlobalUnlock.

    В защищённом режиме Windows функцию GlobalLock заменили «заглушкой»: теперь Windows может перетасовывать блоки памяти, не изменяя их «виртуальный адрес», видимый приложению (селектор: смещение) — а значит, приложению теперь нет надобности закреплять невыгружаемые объекты. Иными словами, закрепление теперь предотвращает выгрузку блока, но не предотвращает его (незаметное для приложения) перемещение. Поэтому для закрепления данных «взаправду» в физической памяти, для тех, кому нужно именно это (например, для работы со внешними устройствами), добавили пару GlobalFix / GlobalUnfix. Так же, как и GlobalWire / GlobalUnwire, в Win32 эти функции стали бесполезными; и они точно так же удалены из документации, хотя остались в kernel32.dll, и перевызывают GlobalLock / GlobalUnlock.

    Управление кодом


    Самое хитрое начинается здесь. Блоки кода — так же, как и неизменяемые данные — удалялись из памяти, и потом загружались из исполняемого файла. Но как Windows обеспечивала, что программы не попытаются вызвать функции в выгруженных блоках? Можно было бы обращаться и к функциям через хэндлы, и перед каждым вызовом функции вызывать гипотетическую LockFunction; но вспомним, что многие функции крутят «message loop», например показывают окно или выполняют DDE-команды, — и их на это время тоже можно было бы выгрузить, т.к. фактически их код в это время не нужен. Тем не менее, при использовании «хэндлов функций» сегмент функции не будет освобождён до тех пор, пока она не вернёт управление вызвавшей функции.

    Вместо этого Windows начинает с предположения, что выгрузить можно любую функцию, которая не выполняется прямо сейчас; а раз прямо сейчас выполняется код менеджера памяти Windows, значит выгрузить можно вообще любую функцию. Ссылки на неё могут оставаться либо в коде программ, либо в стеке, если эта функция не успела вернуться до момента выгрузки.

    Так что Windows проходит по стекам всех запущенных задач (так назывались контексты выполнения в Windows, пока не разделили процессы и потоки), находит адреса возврата, ведущие внутрь выгруженных сегментов, и заменяет их на адреса reload thunks — «заглушек», которые загружают нужный сегмент из исполняемого файла, и передают управление внутрь него, как ни в чём не бывало.

    Чтобы Windows могла пройтись по стеку, программы обязаны поддерживать его в правильном формате: никакого FPO, кадр стека обязан начинаться с BP — указателя на кадр вызвавшей функции. (Поскольку стек состоит из 16-битных слов, значение BP всегда чётное.) Кроме того, Windows должна различать в стеке записи внутрисегментных («близких») и межсегментных («далёких») вызовов, и близкие вызовы может игнорировать — они-то уж точно не ведут в выгруженный сегмент. Поэтому постановили, что нечётное значение BP в стеке означает далёкий вызов, т.е. каждая далёкая функция должна начинаться с пролога INC BP; PUSH BP; MOV BP,SP и заканчиваться эпилогом POP BP; DEC BP; RETF (На самом деле пролог и эпилог были сложнее, но сейчас не об этом.)

    Со ссылками из стека разобрались, а как быть со ссылками из других сегментов кода? Конечно же, Windows не может пройтись по всей памяти, найти все вызовы выгруженных функций, и заменить их все на reload thunks. Вместо этого межсегментные вызовы компилируются с учётом того, что вызываемой функции может не быть в памяти, и фактически вызывают «заглушку» в таблице входов модуля. Эта заглушка состоит из инструкции 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.

    Но как Windows выбирает, какой из сегментов выгрузить? Выбирать наугад было бы рискованно — можем попасть в код, который только что выполнялся, и который придётся тут же загружать обратно. Поэтому Windows использует нечто наподобие «accessed-бита» для сегментов кода: зная, что все межсегментные вызовы функции проходят через её заглушку, они придумали вставить туда (перед 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
    Ads
    AdBlock has stolen the banner, but banners are not teeth — they will be back

    More

    Comments 33

      +3
      очень увлекательно, спасибо.
        +8
        Вот как бы не ругали Майкрософт, а их политика обратной совместимости достойна уважениея. Удалили лы бы все эти пережитки прошлого, а нет, поддерживают.

        Лично видел софт, с самописным менеджером памяти, основанном на Global* функциях, который динапически свопит все на диск, чтобы не превышать установленного лимита на объем используемой памяти. Написан в конце 80х, работает до сих пор как ни в чем не бывало.

        Ну и о эпичном фиксе для SimCity, которая имела привычку использовать память после вызова GlobalFree наверное все слышали.
          0
          Ну и о эпичном фиксе для SimCity, которая имела привычку использовать память после вызова GlobalFree наверное все слышали.

          А можете поподробнее рассказать? Поисковики по словам SimCity и GlobalFree только эту статью на хабре показывают.
            0
            В этой статье описан данный случай
            russian.joelonsoftware.com/Articles/HowMicrosoftLosttheWaronA.html
            или оригинал
            www.joelonsoftware.com/articles/APIWar.html
              0
              с интересом прочитал, спасибо =)
              +2
              Joel Spolsky:
              I first heard about this from one of the developers of the hit game SimCity, who told me that there was a critical bug in his application: it used memory right after freeing it, a major no-no that happened to work OK on DOS but would not work under Windows where memory that is freed is likely to be snatched up by another running application right away. The testers on the Windows team were going through various popular applications, testing them to make sure they worked OK, but SimCity kept crashing. They reported this to the Windows developers, who disassembled SimCity, stepped through it in a debugger, found the bug, and added special code that checked if SimCity was running, and if it did, ran the memory allocator in a special mode in which you could still use memory after freeing it.
                +9
                Попытаюсь перевести этот фрагмент менее криво, чем их Официальный Переводчик:
                Об этом случае я узнал от одного из разработчиков SimCity: в их коде был баг, из-за которого программа обращалась к блокам памяти сразу же после их освобождения. Понятно, что так делать нельзя, но под DOS это работало, потому что некому было использовать только что освобождённую память под что-то другое: SimCity была единственной в системе запущенной программой.

                В Windows, когда одновременно запущено несколько программ, освободившаяся память тут же занимается кем-то другим, и SimCity падала. Когда тестеры Windows проверяли совместимость разных популярных программ с новой ОС, они обратили внимание, что SimCity то и дело падает, и сообщили об этом разработчикам Windows.

                Разработчики Windows принялись отлаживать SimCity, чтоб определить причину падений, и нашли в ней этот баг. Тогда они добавили в Windows специальный код, который проверял, запущена ли SimCity, и если запущена — то изменял алгоритм выделения памяти так, чтобы только что освобождённую память можно было продолжать использовать.
                  0
                  Ну и заодно упомяну свой позапрошлогодний перевод «Байки о несовместимых приложениях» — habrahabr.ru/post/103598/
                  +1
                  Не могли бы рассказать, что за софт?
                    0
                    Мне кажется эта совместимость со старьём — палка о двух концах.
                    С одной стороны — поддержка старого софта очень важна для бизнеса, с другой стороны все остальные тащат и обновляют код, который занимается этой поддержкой и который даром не нужен.

                    Грубо говоря, зачем мне в 21 веке код поддержки СимСити?

                    Меня удивляет, что MS не сделала из вин7 рубежа и оставила старьё и его поддержку в своеобразном отдельном режиме, например в том же режиме совместимости.

                    Кому нужно включали бы его.

                    Добавьте шутки-прибаутки в коде винды и офиса и поражает сколько «лишнего» в продуктах MS =(
                      +1
                      Зря Вы так… Во-первых, этот код кем-то написан, отлажен, кому-то оплачен — как же такое дорогое сокровище выбросить? :) А, во-вторых, чем же забить место в каталоге с ОС? А не забьешь — скажут, мол, плохая система, раз такая маленькая. Юзер, так сказать, должен видеть, что за свои деньги получил много хоть чего-то! Шутка.

                      А если серьезно, то в 8-ке вроде как пытаются сделать шаг в сторону от наследия (хотя для юзера это выглядит как просто уход от привычного десктопа). Пытаются, да не получается, ибо без поддержки наследственности новые компы с предустановленной системой никто не возьмет, если хоть чем-то из старого добра пользуется, да и софта под новые API не всегда много. Как ни крути, прибегает маркетинг и «делает мозг» разработке, мол, верните привычное покупателям!

                      Хотя в 7-ке (Pro и выше) уже были попытки вынести XP «за скобки» — называется XP Mode. Загружаете себе VM с копией XP, и вполне прозрачно в смысле интеграции и лицензионно-чисто работаете в ней. Скажу по опыту — спасает порой!
                        0
                        Майкрософт были бы не Майкрософт, если бы не сделали этот режим с такими условиями:
                        — только для Ultimate-версии Windows 7
                        — качать «виртуализатор» нужно отдельно с сайта MS и активировать отдельно
                        — ставится голая XP. о вирусах и настройке заботиться нужно самому.
                        — потратить нужно минимум минут 40 времени, чтобы оно заработало

                        PS: сам пользуюсь для подключения книжки REB 1100, которую иначе с Win 7 64 не подружить.
                        +1
                        Увы, или к ура, но старые системы это не только СимСити и иже с ним. Это просто огромное количество ПО, которым люди и предприятия реально пользуются. Да простейший пример — 1С 7.7 выпущена в конце девяностых годов, до сих пор хрен знает сколько народу работает за ней, сотни тысяч пользователей наверное. Или банальный Numlock Calculator — семь лет назад остановилась в развитии (автор исходники потерял) и работает как надо (и надеюсь ещё лет десять проработает, ибо аналогов я близко не видел).

                        Да и проблема сохранения совместимости — никакая особо и не проблема. Вон линукс тому пример — регулярно ломают совместимость и бинарную и апишную, и что? Работает не хуже и не лучше, чем windows, только софт уже паругодовой давности не запустишь.
                          0
                          аналог NumLock calculator я себе написал сам — как раз с возможностью считать выражения в строке. Хоть ему и не хватает продвинутых функций, но с 2003го года переписывать его не приходилось.
                            0
                            >>он линукс тому пример — регулярно ломают совместимость и бинарную и апишную, и что

                            Ломают только на уровне ядра, а юзерспейс АПИ остается неизменным и польностью обратно совместимым. У Линуса на этот счет очень четкая позиция «Те, кто считает, что красивый код важнее бинарной совместимости — идиоты»
                        0
                        >> Интересно, всегдашние холиворщики «это была графическая оболочка, а не операционная система» в курсе об этих её необычайных способностях?

                        Наверное потому что в это же время была OS/2, которая могла делать всё тоже самое без костылей? :)
                          +1
                          В какое в то же? OS/2 1.0 вышла в декабре 1987, а Windows 1.0 вышла двумя годами раньше. При том, что обе разрабатывались Microsoft.
                          +3
                          Os/2 никогда не умела ничего подобного поскольку требует для своей работы защищенный режим. речь же идет про реальный.

                          Графической оболочкой вплоть до w98 называли в силу того, что работать без ДОС они не могли.
                          НО, имели в себе все механизмы для того что бы в будущем от этой зависимости отказаться.
                          У программистов просто не хватало времени.

                            –1
                            Оболочкой она была потому, что любое приложение, включая любое ДОС приложение реального режима (даже не протектед, лол) могла пнуть винду-оболочку под зад. Поменять все вектора прерываний, насильно выпилить винду из оперативы — вот это всё. Такое просто недопустимо для операционной системы.
                              +6
                              Эдак из операционных систем исключаются CP/M, DOS и даже MacOS Classic.
                              Странный критерий какой-то.
                                –6
                                DOS — ОС, а Win9x — это оболочка над ОС, которой являлся DOS. Частично верное утверждение, ибо Win9x одновременно был и оболочкой для реального 16-битного режима, в котором вращалось ядро, а это 16-битное ядро позволяло крутить 32-битную приблуду, которая была как ОС для защищённого режима.
                                  +3
                                  Ядро Win9x 32-битное. Virtual Machine Manager называется. В наиболее распространенном случае (когда драйверов реального режима нет) DOS служит исключительно как загрузчик Win9x. 16-битного кода, унаследованного от Win 3.x, в Win 9x много, но не в ядре. Вот тут Рэймонд Чен подробно разбирает, для чего DOS используется в Win9x: blogs.msdn.com/b/oldnewthing/archive/2007/12/24/6849530.aspx
                                    –5
                                    Так в этой статье и сказано, что часть ядра вращается в 16-битном режиме.
                                    А ещё можно было запустить Win9x с флешки и любая программа будет неявно вызывать реальные методы для чтения с флешки, даже если сама Win9x не знает ничего про USB.
                                      +2
                                      Это не часть ядра. Это прослойка для вызова драйверов реального режима, для той же флэшки например, да. Если драйверов реального режима нет, то все ядро всегда работает в 32-битном режиме.
                              –5
                              Причём можно было даже запустить резидента ДО винды и он бы виндой рулит во все щели. А можно и после с тем же эффектов. Так-то!
                                0
                                По-моему, там было дело все же скорее не во времени, а как раз-таки в максимальной совместимости с DOS, вплоть до некоторых драйверов.
                                  –1
                                  1) вплоть до windows millenium
                                  2) защищённый режим появился в 1982 году (80286), а нормальный защищённый режим (с управлением страницами) — в 1985.
                                  в 1996 году вышла Windows NT 4.0 — с нормальными механизмами защиты, не основанная на DOS.
                                  Зависимость от DOS тянули до 1999 (ME). Времени, говорите не было?
                                    +1
                                    Да, не хватало времени. Только не у программистов MS, а у всех остальных. Тот же Warcraft 2 вышёл после Win95, но был досовским приложением (правда, нереального режима — с 4гб ОЗУ). А уж дикое количество устройств, для которых были только досовские драйвера и обязывали использовать ОС-оболочку с 16-битным ядром.
                                      +2
                                      Во-первых, не стоит думать, что i386 стал применяться в ПК сразу же, как только вышел. Поначалу это был очень high-end продукт — любому нормальному домашнему пользователю и 16МБ было за глаза и больше.
                                      Windows же ориентировалась именно на рынок ПК с самого начала и до ветки NT.

                                      Во-вторых, до Windows ME тянулась не «зависимость от DOS», а совместимость с драйверами для DOS / Win16 и с программами, привыкшими копаться в кишках ОС, сохраняемая намеренно и достаточно дорогой ценой.

                                      Тем более, уже ко времени Windows 98, MS-DOS не существовал как продукт. Какая же может быть зависимость от того, чего нет?
                                    +1
                                    Оверлейный линкер для DOS напомнил линковщик из ОС RT11 (которая на PDP-11 ну или ДВК крутилась). Я работал на RT11FB — там было что-то около 56 килобайт памяти, и когда программа в память не лезла, то объектные модули линковались как оверлейные и в процессе работы программы подгружались с диска. Единственно — там приходилось довольно кропотливо планировать приложение, чтобы минимизировать подгрузку овелеев и тщательно планировать где какие данные хранятся и т.д. Многозадачность там тоже была, но невытесняющая — ну то есть можно было запустить приложение в фоновом (background) режиме. Я этим пользовался для автоматизации эксперимента — установка снимала данные через КАМАК, и в то же самое время можно было обрабатывать уже снятые данные. В конце восьмидесятых / начале девяностых это выглядело как чудо (в лабораториях физтеха IBM совместимые компьютеры тогда не были так распространены).
                                      0
                                      Нечто подобное насколько я помню умела и Макось тех же времен. Однако, есть одно но — приложения тех лет частенько забывали сделать вот эти локи и поэтому ошибки с памятью были самыми частыми причинами падений и нестабильности. Да и обе эти системы основанные на таких механизмах управления памятью в реальном режиме, не были особо успешны. И с тех пор как появился защищенный режим работы Windows никогда не меняет отданные старым приложениям хендлы, что приводит к тому, что стабильность этих приложений при переходе на новые версии выросла практически на ровном месте. Примерно так это излагалось в прекрасной книге «Введение в операционные системы» Иртегова :)
                                        +1
                                        Когда защищённый режим только появился, он был доступен лишь немногим мажорам, раскошелившимся на новый мощный компьютер.
                                        А Windows позволяла разработчикам создавать программы, которые одинаково работали бы и в реальном режиме (пользуясь перечисленными в посте «костылями»), и в защищённом (пользуясь всеми преимуществами нового процессора). Именно эта фича — «написано однажды, работает везде» — и была основным козырем первых версий Windows, когда в ходу был целый зоопарк несовместимых компьютеров и внешних устройств.
                                          +2
                                          Оно и понятно :) Я вовсе не критикую такой подход — для своего времени он был решением, позволявшим добиться нужных результатов, а дальше был заменён другими. Вообще подход к совместимости в Windows вызывает если не восхищение, то как минимум уважение.

                                      Only users with full accounts can post comments. Log in, please.