Партнерка на США и Канаду по недвижимости, выплаты в крипто
- 30% recurring commission
- Выплаты в USDT
- Вывод каждую неделю
- Комиссия до 5 лет за каждого referral
В-общем, решил я написать небольшой цикл статей, посвященных windows. Важно отметить, что эти статьи вовсе не претендуют на некие «постулаты». Скорее наоборот. Вероятно, число ошибок тут огромное число, и связано оно, прежде всего, с невежеством автора. Увы, этого недостатка я исправить не могу. Однако любые ваши замечания, по технической части статьи, или же по схеме изложения можно, а точнее очень нужно(!) писать мне. Будем надеяться, что эти статьи принесут кому-то хоть маленькую пользу.
--------
AquaFox
Небольшой экскурс в операционную систему Windows.
Глава 1. Системные объекты: краткое описание.
Введение.
Для начала попробуем уяснить, что есть «системный объект». Интуитивно каждый может представить себе: некоторая «вещь» внутри системы, которая характеризируется теми или иными свойствами и предназначенная для определенных действий. Однако, цель нашего изложения – дать наиболее точное описание рассматриваемых терминов. В дальнейших статьях я планирую описать различные объекты операционной системы windows и привести, по возможности, «наиболее частые ошибки» в понимании тех или иных процессов, происходящих внутри компьютера, по возможности показать, как подобные механизмы реализованы в других ОС, например в Unix. Однако очевидно, что любое изложение должно базироваться на некоторых терминах, иначе оно теряет смысл. Невозможно объяснить, человеку, почему нельзя простым call’ом вызвать процедуру из другого процесса, если тот не до конца понимает, а что собственно «процесс» такое. Кроме того, у этой главы есть еще одна цель. После ее прочтения, я надеюсь, многие «тонкости» подобные тем, что я только что изложил станут сами собой очевидны.
Разновидности объектов и подсистемы.
Следует сразу отметить, что дать сколько-нибудь прямое и точное определение системного объекта у меня не получилось. На это есть некоторые причины. Как известно, операционная система – продукт, вне всякого сомнения, сложный, состоящий из большого числа файлов, и написанный явно не одним человеком. И даже не одной сплоченной командой. Можно сказать, что вся операционная система состоит из множества подсистем. Эти подсистемы различаются, конечно же, не теми библиотеками, в которых они располагаются, и уж тем более не расположением в адресном пространстве. В общем случае можно сказать, что основное отличие этих подсистем – в том, что их писали разные команды разработчиков. Это становится сразу видно, потому что схожие, казалось бы, вещи зачастую реализованы в каждой подсистеме по-своему.
Примечание 1. Сразу следует отметить, что часто под словом «подсистема» понимают такие вещи, как например ntvdm и wow(windows on windows). Здесь под «подсистемой» подразумевается просто набор реализация стандартных функций, позволяющих запускать на одной операционной системе программы, написанные для другой. Это определение тоже правильное, но, тем не менее, я безо всякого зазрения совести «похищу» его, и назову подсистемой совершенно другое понятие. Однако же следует иметь ввиду, что те «подсистемы» про которые написано в комментарии и «подсистемы», которые обсуждаю я в этом изложении не имеют между собой ничего общего.
В своем изложении мы будем рассматривать три подсистемы windows, каждая из которых представлена отдельным dll-файлом:
1) user32
2) gdi32
3) kernel32
Первая библиотека предназначена для взаимодействия с пользователем, вторая – за непосредственную прорисовку элементов (чувствуете переплетение?), а последняя – за некоторые «базовые» возможности операционной системы. Каждая из этих подсистем имеет свой набор объектов, которые мы будем называть соответственно user-объекты, gdi-объекты и kernel-объекты.
Примечание 2. Одна из библиотек пользовательского режима, называется «kernel» что в переводе с английского означает «ядро». Никакого отношения к ядру эта библиотека не имеет – почему ее назвали именно «ядро» для меня загадка. Наверное, имелось в виду некоторое средоточение (метафорически выражаясь – ядро) основных системных функций. Так или иначе, следует помнить, что никакого отношения к ядерному режиму (kernel-mode) операционной системы эта библиотека не имеет. Это исключительно «user-модная» библиотека.
Сами по себе все системные объекты представляют собой некоторые структуры в «ядерной» области памяти операционной системы. Это может быть как одна структура, так и некоторый их набор. Важно отметить, что получить доступ непосредственно к объекту мы не можем – во-первых, он лежит в недоступной нам «системной» памяти. Во-вторых, microsoft зачастую не публикует форматы структур, оставляя за собой право нещадно менять их в последующих версиях. Именно по этому, отныне мы забудем про то, что в действительности представляют собой системные объекты и будем здесь и везде понимать под ними некоторую «сущность», для работы с которой следует использовать системные функции.
Для того, что бы как-то «именовать» системный объект вводится понятия описателя (или хэндла(handle)). Описатель системного объекта – это 32-битное число, которое однозначно идентифицирует системный объект в его области видимости. По сути своей описатель – есть «номер» элемента в некоторой системной «таблице описателей». Мы будем передавать описатель в функции операционной системы, а уже эти функции будут некоторым образом с ним работать.
Примечание 3. Следует отметить, что вся память в операционной системе делится на два типа – есть память ядра и есть память пользовательского режима. Кроме того, сам процессор может работать в двух* режимах – ядерном и пользовательском (kernel-mode и user-mode). Соответственно код, который выполняется в ядерном режиме называют ядерным кодом, а тот, который выполняется в пользовательском – пользовательским кодом. Доступ к ядерной области памяти может получить только ядерный код, пользовательскому коду он недоступен.
* - на самом деле, процессор IA-32 имеет четыре режима работы, но в windows используются только два – наиболее «привилегированный» и наиболее «защищенный». Бытует мнение, что сервисы выполняются на своем «сервисном» уровне процессорных привилегий. Это не так. Сервисы в windows делятся на «ядерные» и «пользовательские». И, соответственно, выполняются в том или ином режиме. Сразу следует отметить, что апплет «Службы» в windows отображает только пользовательские сервисы.
Помимо этих трех подсистем в windows есть еще множество других, например «подсистема оболочки» (shell) или подсистема сокетов (sockets).
Рассмотрим подробнее каждый из типов системных объектов.
Подсистема user
Эта подсистема отвечает за взаимодействие с пользователем на самом базовом уровне. Именно эта подсистема управляет окнами, меню и отвечает за передачу оконных сообщений. Наиболее интересные объекты этой подсистемы:
- window (окно) menu (меню) desktop (рабочий стол) workstation (рабочая станция)
Важной особенностью всех этих объектов – между ними нет почти ничего общего. Как мы заметим далее, в остальных двух подсистемах будут существовать некоторые общие функции, работающие с любым из объектов этой подсистемы. В отличие от них, в подсистеме user нет ни одной функции, которая бы работала со всеми объектами сразу.
Описатели этой подсистемы глобальны для всей ОС. Кроме того, каждый описатель однозначно характеризует один user-объект. Важно отметить, что многие из user-объектов требуют ЯВНОГО удаления. Например, рабочие столы не уничтожаются сами по себе, даже если закроется создавший их процесс.
Подсистема GDI
Это так называемая «графическая» подсистема. Эта система предназначалась для работы с графикой в не зависимости от того, какими аппаратными средствами ее вывод будет осуществляться, будь то принтер, монитор или, скажем, еще что-то. Собственно, аббревиатура gdi расшифровывается как «graphic device independence». Основным объектом этой подсистемы является «графический контекст» (DC), который несет в себе специфичную информацию об устройстве вывода. Сам по себе контекст не хранит изображений, не смотря на то, что все графические функции рисуют именно на DC.
У GDI есть некоторые функции, которые действуют на все, или почти на все объекты этой подсистемы, например, DeleteObject или GetObjectType.
Вроде бы, все описатели GDI так же глобальны.
Кроме того, в GDI есть некоторый набор «стандартных» объектов. Получить описатель стандартного объекта можно, используя функцию GetStockObject.
Примечание 4. Помимо унификации, разработчики GDI ставили также целью написать очень быструю систему прорисовки. Поэтому системные вызовы GDI-функций особенные. В процедуре-переходнике от пользовательского режима к ядерному лежат огромные «куски» кода, предназначенные для ускорения и особой обработки GDI-вызовов.
Подсистема kernel
Эта подсистема несет в себе огромное количество объектов. Приведем некоторые из них:
- мьютексы (mutex) семафоры (semaphore) ивенты (event) критические секции (critical section) ждущие таймеры (Waitable-timer) трубы (pipe) файлы (file) маппинги (mapping) задания, жобы (job) процессы (process) нити (thread) волокна (fiber)
Вообще-то список довольно велик, сама подсистема kernel возможно наиболее обширная из всех.
Объекты этой подсистемы имеют ряд особенностей. Во-первых, в отличие от первых двух подсистем описатель объекта вовсе не является уникальным «номером» объекта в системе. Описатель объекта у этой подсистеме несет в себе как минимум две информации: собственно «ссылку» на объект и атрибуты доступа, с которыми этот описатель открывался. Каждый объект в подсистеме kernel имеет некоторый список доступа (Access control list, ACL). В этом списке указывается, какие права на этот объект могу получить те или иные сущности операционной системы. Заметим, не те, которые они имеют, а те, которые они могут получить. Результирующие атрибуты доступа, в конце концов, хранятся в полях описателя. Если ACL у объекта вдруг поменяется, то это не возымеет действия на уже открытые описатели. Например, если процесс открыл файл на запись, а потом вы «урезали» ACL файла, запретив запись, то процесс все равно сможет писать в файл, используя уже открытый описатель. Но уже не один процесс, в том числе и этот не смогут создать не одного нового описателя этого файла с правами записи. Вообще, вопросам безопасности, вероятно, будет посвящена отдельная статья.
Кроме того, важно отметить, что ВСЕ описатели являются локальными – они действуют внутри одного процесса. Если процесс закрывается – закрываются все его описатели.
Однако это еще не означает, что уничтожается сам системный объект. Все описатели закрываются с помощью функции CloseHandle, дублируются с помощью DuplicateHandle. Кроме того, есть еще ряд функций, например для работы с ACL, которые действуют на многие объекты этой подсистемы.
Глава 2. Методы распределения ресурсов
Введение
Первую настоящую (не вводную) статью в цикле я посвящу объектам подсистемы kernel, предназначенным для управления такими ресурсами операционной системы как «рабочее время» и «память». Таких объектов существуют два:
- процесс (process) нить или поток (thread)
Кроме того, краем мы коснемся еще двух вспомогательных объектов:
- задания (job) волокна (fiber)
Первый предназначен для объединения процессов в некоторый единый «блок». Второй – для разбиения потока на переключаемые вручную мини-блоки.
Процесс
Процесс – это системный объект, определяющий адресное пространство и набор некоторых kernel-описателей. Каждый процесс состоит хотя бы из одной нити.
Адресное пространство - термин не всегда точно определяемый. Можно назвать адресным пространство набор адресов виртуальной памяти доступных процессу. Собственно, важно отметить, что именно адресов, а не самих ячеек памяти.
Примечание 1. Слово «виртуальный» означает «воображаемый» то есть то, что существует только в воображении человека. Так виртуальная память – понятие абстрактное. Можно сказать, что все ячейки памяти, адресуемые адресным пространством, представляют собой в совокупности виртуальную память.
Совершенно очевидно, что этот набор адресов где-то хранится.
Примечание 2. Нелишним будет напомнить, что «адресом» в ОС windows мы называем число из четырех байтов.
В принципе, можно было бы хранить в оперативной памяти компьютера для каждого процесса просто массив доступных ему адресов. Однако если вспомнить что каждый адрес у нас указывает на один байт, а сам при этом занимает 4(!) байта, то мы увидим, что одна лишь таблица адресов для каждого процесса будет занимать в 4 раза больше оперативной памяти, чем сама память процесса.
Эта проблема решается элементарным образом – все адресное пространство делится на блоки размером по четыре килобайта. И в системной таблице, которая храниться в ядре операционной системы для каждого процесса, лежат только начальные адреса этих блоков, а не каждого байта. Такие блоки называют страницами памяти.
Рассмотрение устройства адресного пространства мы отложим на следующую статью. Пока лишь отметим, два важных момента. Во-первых, страничная организация памяти это не причуда ОС windows, данная технология поддерживается процессорами IA-32 на аппаратном уровне и страничную организацию памяти используют все «современные» ОС.
Во-вторых, в только что описанной модели, мы говорили, что у каждого процесса есть таблица, которая описывает все его адресное пространство. На самом деле это не так. Таких таблиц (описывающих АП процесса) две. Одна из них – своя для каждого процесса, она называется локальной таблицей дескрипторов (LDT) .Вторая таблица – глобальная, она едина для всех процессов в операционной системе и называется «глобальная таблица дескрипторов» (GDT).
Однако, как мы сказали, рассмотрение адресного пространства будет перенесено в другую статью.
Как мы уже отмечали, каждый процесс содержит свою таблицу kernel-описателей. Из этого следует, например, такое важное следствие (о котором я уже упоминал) – все описатели локальны внутри процесса, то есть они теряют смысл вне него.
Каждый процесс характеризуется некоторым уникальным числом – идентификатором процесса. Функция OpenProcess, которую можно использовать для получения описателя процесса с определенными правами принимает в качестве определяющего параметра именно id процесса.
Кроме этого, не менее важным свойством процесса можно назвать имя пользователя, от которого этот процесс запущен. Во-первых, это свойство определяет ACL самого процесса, как системного объекта – благодаря этому, например, пользователи из группы User не могут получить описатель процесса пользователя system с правами PROCESS_TERMINATE или с правами PROCESS_VM_WRITE. Как следствие этого – программа, запущенная от имени обычного пользователя не сможет использовать полученный описатель, что бы закрыть процесс (вызвать TerminateProcess) или что бы изменить его память (вызвать WriteProcessMemory).
Примечание 3. Я позволю себе некоторую вольность в использовании термина «системный». Так, мы будем называть системным процесс, если он запущен от имени учетной записи NT_AUTHORY_SYSTEM. Однако, под системным потоком, как будет сказано позже, мы будем понимать нечто принципиально иное.
Примечание 4. Также заметим, что вы всегда можете заменить список доступа по умолчанию для процесса, явно указав его в функции CreateProcess.
Во-вторых, это свойство определяет, какие права будут у самого процесса. Так, на процесс, запущенный от имени пользователя User будут распространяться все права и ограничения, которые действуют для этого пользователя.
Важно так же отметить, что часто «захваченные» процессом описатели не только потребляют память, но и могут ограничивать деятельность других процессов. Так, например, если один процесс получит описатель файла с флагом SHARE_DENY_ALL, то не один другой процесс не сможет получить описатель этого файла до тех пор, пока «наш» процесс не закроет свой «эксклюзивный» описатель. Что бы внезапное завершение процессов не приводило к деградации операционной системы, все описатели процесса закрываются автоматически при его завершении. Кроме того, по завершении каждый процесс оставляет свою «последнюю волю» - 32-разрядное число, которое называется «кодом завершения». Это число обычно используется для сигнализации об ошибках, процесс указывает его либо как возвращаемое значение своей entry-функции, либо как аргумент функции ExitProcess или TerminateProcess. Получить его можно, вызвав функцию GetExitCodeProcess.
Примечание 5. Важно отметить, что процесс как системный объект уничтожается тогда, и только тогда, когда закрываются все его описатели. У запущенного процесса всегда есть некоторый открытый описатель, поэтому он не может уничтожиться не завершившись. Однако если процесс завершается, то это еще не означает, что уничтожается системный объект «процесс». При завершении процесса, освобождается его локальная память, закрываются все его описатели. Но часть информации о процессе, например, код завершения будет уничтожена только тогда, когда будут закрыты ВСЕ описатели, указывающие на него. В этом заключается различие понятий «завершения» и «уничтожения» процесса. Но, тем не менее, следует отметить, что однажды завершившийся процесс уже не может быть запущен вновь – в «завершенном» процессе нельзя создать новый поток.
Кроме того, немаловажно отметить, что процесс является объектом синхронизации, он переходит в сигнальное состояние, когда завершается.
Примечание 6. Об использовании объектов синхронизации будет рассказано в отдельной статье.
Нить.
Нить(thread) – системный объект, предназначенный для распределения процессорного времени. Именно нить «выполняется» в операционной системе. Процесс является лишь «носителем» кода, данных и описателей. А поток – представляет собой собственно исполняемую единицу. Каждый поток имеет свой стек. Потоки выполняются «параллельно» то есть инструкции каждого потока выполняются как бы на отдельном процессоре. Если процессоров меньше чем потоков – операционная система идет на «хитрости» переключая нити по таймеру. Учитывая, что у большинства компьютеров процессоров очень не много, а число нитей в windows даже в самые спокойные времена переваливает за десяток, вопрос о переключении нитей является очень важным.
У каждого потока есть так называемое «состояние». Это некоторое свойство, которое показывает, что поток «делает» в данный момент. Поток может либо «работать» то есть выполнять некоторый код либо этот поток может находиться в «ждущем» состоянии. Ожидающие потоки не берутся планировщиком в расчет до тех пор, пока они не перейдут в «рабочее» состояние. Например, при вызове функции GetMessage поток переводится в ожидающие состояние, которое меняется на рабочее в тот момент, когда в этот поток приходит оконное сообщение. Механизм перехода потока из ждущего состояния в рабочее интересен, но, увы, выходит за рамки нашего повествования. Но я надеюсь, что теперь вы понимаете, что разница между Sleep(1) и for(i=0;i<1000;i++); заключается не только в точности определения временного интервала.
Примечание 7. Функция Sleep пользовательского режима в windows позволяет делать задержки с точностью до 1 миллисекунды. Функция ядерного режима DelayThreadExecution (которую, собственно, и вызывает Sleep) позволяет делать задержку с точностью до 100 наносекунд (то есть в десять тысяч раз точнее). Но (если мне не изменяет память) планировщик в ОС windows вызывается один раз в пару десятков микросекунд. Таким образом, точность обеих функций, и sleep и DelayThreadExecution очень сильно зависят от числа работающих нитей. Если в системе, скажем, более сотни высокоприоритетных нитей, то низкоприоритетная нить, вызвавшая Sleep(1) рискует «проспать» в несколько раз дольше, чем 1 милисекунда.
Потоки переключаются не спонтанным образом – операционная система старается распределить процессорное время согласно определенным параметрам потоков. У каждого потока есть два таких параметра – уровень приоритета и так называемый IRQL. Эти два параметра определяют, какой процент процессорного времени будет выделен каждому потоку. Выделением времени занимается специальная системная «служба» - планировщик потоков. Уровень приоритета определяет, на сколько много, по сравнению с другими потоками, этот поток получит времени. Чем выше приоритет – тем больше процессорного времени получит нить. То есть, к примеру, если у нас есть две нити – первая с приоритетом 1, а вторая – с приоритетом 2, то за 1 секунду времени первый поток будет работать 333 миллисекунды, а второй – 666.
IRQL указывает то, на сколько поток «критичен». Поток с высоким IRQL никогда не может прерваться для того, что бы отдать время потоку с меньшим IRQL. Планировщик, распределяя процессорное время, «выкидывает» из своих списков сначала все «ждущие» потоки, а затем он определяет максимальный IRQL из оставшегося списка. Далее, он выкидывает из списка потоков все те, что имеют IRQL ниже определенного. И только потом распределяет процессорное время согласно приоритетам.
Таким образом, подведя черту можно сказать, что при равном IRQL любой поток, пусть даже с самым маленьким приоритетом когда-нибудь да получит управление. В отличие от этого, поток с низким IRQL НИКОГДА не получит управления если в системе есть хоть один поток с более высоким IRQL.
Перед переключением потоков планировщик сохраняет в определенном блоке памяти многие регистры процессора. Этот блок памяти называют «контекстом» потока. Фактически, контекст определяет состояние процессора на момент переключения. Перед тем как «отнять» у потока выполнение планировщик сохраняет его контекст. А перед тем как «вернуть» - «загружает» содержимое контекста обратно в регистры процессора. Таким образом получается, что регистры процессора будут неизменными, в какой бы момент времени поток не прервался – тем самым достигается имитация «параллельности» выполнения.
Также, поток может находиться на одном из двух уровней выполнения – либо на «ядерном» уровне, либо на «пользовательском». Потоки переходят из одного режима в другой с помощью специальных инструкций процессора, но это (механизм переключения режимов) также выходит за рамки статьи. Фактически «режим» потока определяется флагом IOPL процессора, когда поток выполняется, и соответствующими ему битами в контексте, когда поток не выполняется.
Примечание 8. Важно отметить, что функция TerminateThread, как и другие функции, завершающие потоки всегда ждут момента, когда поток выйдет из «ядерного» режима. Таким образом, поток, который по какой-то причине там задержался, завершить его не получится.
Поток может быть привязан к некоторому процессу, а может и не быть. Завершение процесса произойдет только после завершения всех его потоков. И наоборот: если все потоки процесса завершаются, то завершается и сам процесс. Функция TerminateProcess таким образом будет последовательно «убивать» потоки процесса, после чего уже приступит к завершению его самого.
Поток, не привязанный ни к одному процессу, мы будем называть системным. Системные потоки, очевидно, могут выполняться только в ядерном режиме, так как в пользовательском режиме поток всегда привязан к некоторому локальному адресному пространству – и как следствие – к некоторому процессу. Системные потоки создаются функцией PsCreateSystemThread.
Можно сказать, что поток – это набор регистров. Среди других регистров процессора, контекст потока хранит значение EIP – этот регистр определяет, следующую инструкцию, которую следует выполнить процессору. Таким образом, в контексте потока ячейка, отведенная под EIP, будет указывать на тот адрес, по которому перешел бы процессор, если бы выполнение потока не было бы прервано. Именно по этому значению выполнит абсолютный прыжок планировщик, когда решит передать управление потоку.
В регистре ESP хранится указатель на вершину стека потока. Важно отметить, что функция CreateRemoteThread выделяет некоторое адресное пространство под стек. Размер стека для потока можно указать среди параметров этой функции, но следует помнить, что минимальный размер стека задается именно в заголовке PE-файла, и если вы укажете в параметре функции меньший размер, то ваше значение будет проигнорировано.
Кроме контекста поток содержит еще набор объектов синхронизации, которыми он владеет. При завершении потока некоторые объекты синхронизации, которыми он до этого владел, изменяют свой статус.
Например, критические секции, освобождаются, если «заблокировавший» их поток завершится, не покинув критическую секцию. Это сделано для того, что бы предотвратить смертельные блокировки – deadlock’и (очевидно, что завершившийся поток уже никогда не сможет сам вызвать функцию, освобождающую критическую секцию).
В заключение отметим локальное хранилище потока. Thread local storage (TLS) предназначен для хранения данных, «локальных» относительно каждого потока. Данные хранятся в TLS-слотах, каждый из которых имеет свой уникальный номер. Функция TlsAlloc создает такой слот и возвращает его номер. ID слота глобален внутри всего процесса. Функции TlsGetValue и TlsSetValue получают и устанавливают значение слота соответственно.
Можно сказать, что TLS – это некоторая матрица T(i, j), где – i – номер слота, j – номер потока. При создании потока в эту матрицу добавляется столбик, а при вызове TlsAlloc – добавляется строчка. TlsGetValue в этом случае берет значение из ячейки T(i, j) где i – номер слота, преданный функции в качестве параметра, а j – идентификатор текущего потока. TlsSetValue таким же образом помещает значение в слот. Каждый TLS-слот имеет размер 4 байта.
Примечание 9. В. NET Framework реализована удобная поддержка TLS. Если переменная объявлена с атрибутом <ThreadStatic()> то она размещается не просто в памяти, а в TLS. При обращении к ней, вместо простых ассемблерных команд используются функции windows API. Тем самым переменная имеет уникальное значение для каждого потока. Однако следует понимать, что работа с такими переменными немного медленнее, чем с обычными. Впрочем, в. NET framework эта разница в скорости едва ли будет заметна.
Ну и в конце главы так же заметим, что поток является объектом синхронизации, переходящим в сигнальное состояние после завершений. Также, после завершения поток оставляет за собой 4-байтовое число – код возврата. Этот код определяется либо функцией ExitThread\TerminateThread либо как возвращаемое значение основной функции потока.
Примечание 10. Функция CreateThread, создающая поток принимает в качестве аргумента, в том числе, и адрес некоторой функции. Эта функция и будет первой вызвана в новом потоке. Ее мы будем называть «основной функцией потока». Если эта функция успешно отработает, то возвращенное ею значение и будет кодом возврата потока.
Примечание 11. Немаловажный факт – windows нигде и никак не определяет, на какую область памяти указывает EIP-потока. В том числе, она не будет препятствовать, если кто-то решит освободить область памяти, в которой лежит код одного из потоков.
Например, если один поток выполняет какую-то функцию DLL, а второй поток решит ее (DLL) выгрузить из памяти, windows этому ни коим образом не воспрепятствует. И эта милая ситуация приведет к исключению Page error, когда поток DLL недосчитается страниц, в которых лежит его код. После этого исключения, процесс, как правило, вынужден будет вызвать программу Microsoft Error Reporting.
Продолжение следует :)


