15 мая 2023 года "Исходники.РУ" отмечают своё 23-летие!
Поздравляем всех причастных и неравнодушных с этим событием!
И огромное спасибо всем, кто был и остаётся с нами все эти годы!

Главная Форум Журнал Wiki DRKB Discuz!ML Помощь проекту


Ускорение загрузки Вашего приложения или как работать с IAT.

Under the Hood

Исходник для данной статьи: Feb00UnderTheHood.exe (46KB)

Matt Pietrek автор книги Windows 95 System Programming Secrets. Работает в NuMega Technologies Inc., и с ним можно связаться по адресу mpietrek@tiac.com или http://www.wheaty.net.

 

В декабрьском выпуске MSJ за 1998 год, Джеффри Ричтер (Jeffrey Richter) и я написали статью о такой возможности компилятора Microsoft® Visual C++® 6.0 как DelayLoad. Фактически, мы доказали, насколько хороша данная возможность. К сожалению, до сих пор находятся люди, которые ничего не знают о DelayLoad, либо думают, что эта возможность доступна только в последних версиях Windows NT®.

В очередной раз хочу заявить со всей уверенностью, что DelayLoad это не особенность операционной системы. Она может работать на любой платформе Win32®. Поэтому мне хотелось бы привести утилиту DelayLoadProfile, которая покажет как можно использовать преимущества DelayLoad.

 

Краткий обзор

Давайте рассмотрим, как работает DelayLoad. Обычно, при вызове импортированной функции в DLL, линковщик добавляет информацию о импортированной DLL и функции в Вашу исполняемую программу. Так же не секрет, что информация для всех импортированных функций содержится в секции импорта.

Загрузчик Win32 просматривает секцию импорта и загружает каждую DLL. Далее загрузчик вычисляет адрес каждой импортированной функции и заносит его в раздел секции импорта, известный как Таблица Импорта Адресов (Import Address Table (IAT)). Проще говоря IAT - это массив указателей на функции. То есть, когда вызывается импортированная функция, то используется один из указателей из IAT.

Однако, при использовании DelayLoad, картина несколько меняется. При запускаете DelayLoad с указанием определённой DLL, загрузчик генерирует небольшие обработчики для каждой функции, которые импортировала DelayLoad. Эти обработчики указывают на импортированную DLL и имя функции. После вызова импортированной функции, обработчик сперва вызывает LoadLibrary для загрузки DLL, а затем вызывает GetProcAddress, чтобы получить адрес вызываемой функции. В заключении, обработчик переписывает часть себя так, чтобы последующие вызовы функции шли напрямую в код.

Вышеприведённое описание слегка упрощено. В действительности, обработчик - это небольшой кусок кода, который вызывает подпрограмму, находящуюся в Вашем экзешнике. Эта подпрограмма находится в DELAYIMP.LIB, который должен быть включён в список библиотек для компилятора. Преимущество обработчика состоит в том, что при использовании функции из DLL, LoadLibrary вызывается только один раз и при вызове последующих функций из этой же DLL, вызова LoadLibrary не происходит.

Естевственно, что у DelayLoad есть и свои недостатки по сравнению с импортированием DLL обычным способом. Так как вызов LoadLibrary менее эффективен, чем у обычного загрузчика Win32 и, соответственно разовый вызов GetProcAddress для каждой импортированной функции так же происходит немного медленнее чем это делает обычный загрузчик.

Однако, преимущества DelayLoad с лихвой восполняют эти недостатки. Самое главное преимущество заключается в том, что если Вы не вызываете импортированную функцию, то загрузки DLL не происходит. Кстати, такая ситуация случается намного чаще, чем Вы можете себе представить. Рассмотрим ситуацию, когда в Вашей программе есть определённый код, предназначенный для печати. Если пользователь за время работы программы ни разу не воспользовался печатью, то получается, что WINSPOOL.DRV была загружена напрасно. Поэтому, в данном случае, использование DelayLoad позволяет увеличить скорость работы приложения, так как WINSPOOL.DRV не будет загружена и инициализирована.

Другое преимущество DelayLoad заключается в том, что Вы избегаете вызова API функций, которые недоступны на некоторых платформах. Например, Вам захотелось вызвать AnimateWindow, которая присутствует в Windows® 98 и Windows 2000, однако отсутствует в Windows 95 или Windows NT 4.0. Поэтому, обычным путём Ваша программа не сможет загрузить эту функцию на ранних платформах. Однако, при помощи DelayLoad сделать подпрограмму проверки версии операционной системы и вызывать AnimateWindow только в том случае, если она поддерживается. При этом нет необходимости снабжать программу вызовами LoadLibrary и GetProcAddress.

DelayLoad очень проста в использовании. Если Вам известна DLL, с которой Вы хотите использовать DelayLoad, то просто добавьте /DELAYLOAD:DLLNAME, где DLLNAME - это имя DLL. Так же Вам прийдётся добавить DELAYIMP.LIB к списку библиотек компановщика. Плюс ко всему, Вам понадобится оригинальная библиотека импорта, например SHELL32.LIB. Если собрать всё вместе, то для SHELL32.DLL строка компоновщика будет выглядеть следующим образом:

   SHELL32.LIB /DELAYLOAD:SHELL32.DLL DELAYIMP.LIB

К сожалению, Visual Studio 6.0 IDE не имеет более простого способа, чтобы указать DelayLoading для DLL. В Visual Studio 6.0 Вам прийдётся вручную добавлять фрагмент строки /DELAYLOAD:XXX в поле редактирования Project Setting | Link | Project Options.

Когда используется DelayLoad

Для маленькго проекта легко придумать список DLL, которые являются хорошими кондидатами для DelayLoad. Однако, по мере увеличения проекта и вовлечения в него новых разработчиков очень легко запутаться, кто использует какую DLL. Поэтому для начала подойдёт DLL которая импортирует всего несколько функций.

Однако, мне хотелось упростить и автоматизировать процесс. Поэтому на свет появилась программа DelayLoadProfile. DelayLoadProfile это утилита, которая запускает Ваш экзешник и отслеживает DLL и функции, которые вызывает программа. После завершения Вашей программы DelayLoadProfile выдаёт отчёт о том, какие DLL использовались и сколько вызовов было сделано к каждой DLL. Хорошими кандидатами для DelayLoad являются DLL, которые были импортированы, но к которым не было сделано ни одного вызова.

Как я объясню позже, DelayLoadProfile только подсказывает Вам, какую DLL следует более подробно исследовать через /DELAYLOAD, А дальше Вам самим решать - имеет ли смысл это сделать.

Более подробно о DelayLoadProfile

Концепция DelayLoadProfile довольно проста и заключается в переадресации указателей необходимых функций в экзешной IAT на обработчик. Благодаря этому, обработчик узнаёт, что была вызвана импортированная функция, а затем передаёт управление по изначальному адресу, который был сохранён в IAT загрузчиком Win32. Однако, давайте рассмотрим детально этот процесс.

Сначала, нам надо решить, где будет запущен код, который будет размещать и модифицировать ячейки экзешной IAT, чтобы они укзывали на обработчики. Выполнение из процесса какой-нибудь управляющей программы это один из вариантов. Этот вариант освобождает от забот по получению Вашего кода в экзешном процессе. С другой стороны прибавляется много работы по обработки всех структур данных, необходимых для расположения и исправления ячеек IAT, а так же при последующем сборе результатов. У меня, например, возникали проблемы с вызовами ReadProcessMemory.

Другой подход заключается в том, чтобы проделать всю работу в пространстве процесса экзешника. Данный способ делает почти тривиальным решение задачи доступа к структурам данных, создания обработчиков, переадресации ячеек IAT, а так же вывод конечных результатов. Однако, выполнение определённых действий в процессе, требует, чтобы часть кода DelayLoadProfile была загружена в процесс экзешника во время его выполнения. Я использовал именно этот способ.

Однако возникла ещё одна проблема - как получить мой код в выполняемом процессе. Один из вариантов - это попросить пользователя вызвать код DelayLoadProfile. Однако, это требовало бы от потенциальных пользователей определённых знаний, поэтому я отверг данный вариант.

Взвесив все за и против, я пришёл к выводу о необходимости создания своего рода загрузчика, который будет запускать нужный нам экзешник и внедрять в него DelayLoadProfile DLL. Одна из методик внедрения DLL - это использование CreateRemoteThread для запуска потока в другом процессе, который вызывает LoadLibrary для Вашей DLL. У данной функции есть недостаток - она недоступна в Windows 9x, поэтому пришлось так же отказаться от её использования.

Давние читатели MSJ могут помнить программу, которую я написал более пяти лет назад под названием APISPY32. Она загружала процесс и внедряла в него DLL с целью отслеживания API вызовов. В данном случае это то, что нам надо сделать для DelayLoadProfile. Увы, при запуске APISPY32 на Windows 2000, она не смогла загрузить DLL. Небольшое исследование выявило проблему и я решил что пришло время переделать этот код для нового поколения программистов.

 

Внутренности

Вкратце скажу, что DelayLoadProfile - это система, состоящая из двух частей. Процесс загрузчика запускает Вашу программу. В начале Вашей программы процесс загрузчика помещает DLL в адресное пространство программы. DLL сканирует импортированные функции через IAT Вашего экзешника и переадресовывает их на обработчики, которые создаёт эта DLL. При завершении Вашей программы внедрённая DLL сканирует через обработчики, которые она создала, и суммирует, количество запросов, сделанных к каждой импортированной DLL. Если Вы когда-либо использовали утилиту APIMON из Platform SDK, то обнаружите некоторую схожесть.

DLL, которая выполняет всю работу по отслеживанию использования программой импорта называется DelayLoadProfileDLL (см. Рисунок 1). DelayLoadProfileDLL использует уведомления DLL_PROCESS_ATTACH и DLL_PROCESS_DETACH, посылаемые ей процедурой DllMain, чтобы инициализировать две первичные стадии работы DLL.

Когда DllMain получает уведомление DLL_PROCESS_ATTACH, то DelayLoadProfileDLL вызывает PrepareToProfile. Внутри PrepareToProfile, код размещает нужный IAT экзешника. Для каждой найденной импортированной DLL, код определяет - насколько безопасно использовать редиректа IAT для данной DLL. Делается это путём вызова функции IsModuleOKToHook. В большинстве случаев редирект IAT безопасен, поэтому PrepareToProfile смело вызывает функцию RedirectIAT.

Если Вы имеете представление о связанных с импортом структурах данных в WINNT.H, то RedirectIAT Вам очень пригодится. В самом начале функция распределяет IAT и связанную с ней таблицу импорта имён (Import Names Table). Затем происходит вычисление количества ячеек IAT, которые были получены при поиске NULL указателя. После этого происходит создание массива обработчиков DLPD_IAT_STUB с одним обработчиком для каждой ячейки IAT.

В исходном коде происходит выбор адреса из каждой ячейки IAT и помещение его в инструкцию JMP в обработчике и, соответственно происходит переадресация ячейки IAT на нужный обработчик. Ввиду того, что код продвигается к каждой следующей ячейке IAT то, соответственно происходит перемещение к каждому следующему обработчику, расположенному в массиве DLPD_IAT_STUB. Немного позже мы подробнее поговорим про обработчики DLPD_IAT_STUB.

Нашего внимания заслуживают два аспекта переадресации IAT ячеек на обработчики. Cначала, чаще всего, IAT помещается в секцию экзешника "только чтение". Попытка изменить такой указатель привела бы нарушению прав доступа. К счастью, API функция VirtualProtect, в данном случае, спасает нас и даёт возможность изменять аттрибуты адреса IAT. Для этого мы должны установить атрибут "Чтение-запись" (Read-write). По завершении код восстанавливает аттрибуты защиты памяти.

Другой интересный момент переадрисации IAT заключается в импортировании данных. Почти каждый программист знает, как просто можно импортировать данные прямо в коде программы. Однако переадресация IAT ячеек, которые ссылаются на данные в импортированной DLL, делает это более корректно, а заоодно и избавляет от лишних проблем.

Итак, как Вы определяете где импортируется обычный код, а где импортируются данные ? Коммерческий продукт может использовать довольно сложный алгоритм, чтобы определить тип импорта IAT ячеек. Я решил данную задачу при помощи IsBadWritePtr. Если IAT указывает на память, в которую можно записывать, то вероятно, что там хранятся данные. Аналогично, если она указывает на память "только чтение" (read-only), то скорее всего, что это исполняемый код. Данный способ конечно же не совсем правильный, однако этого достаточно для работы DelayLoadProfile.

Теперь давайте посмотрим на обработчики. Структура DLPD_IAT_STUB в DelayLoadProfileDLL.H содержит смесь кода и данных. Если эту структуру упростить, то обработчик DLPD_IAT_STUB будет выглядеть примерно так:


 CALL    DelayLoadProfileDLL_UpdateCount 
 JMP     XXXXXXXX // original IAT address 
 DWORD   count 
 DWORD   pszNameOrOrdinal 

Когда экзешник вызывает одну из переадресованных функций, то управление передаётся инструкции CALL в обработчике. Подпрограмма DelayLoadProfileDLL_UpdateCount в DelayLoadProfileDLL.CPP просто увеличивает значение счётчика обработчика. Далее инструкция JMP передаёт управление на изначальный адрес, который был сохранён в IAT. Рисунок 2 показывает, каким образом происходит переадресация IAT.

Любители ассемблера наверняка зададутся вопросом, как функция DelayLoadProfileDLL_UpdateCount узнаёт, где в памяти находится поле счётчика обработчика. При беглом взгляде на код видно, что DelayLoadProfileDLL_UpdateCount находит адрес возврата в стёке, куда его положила инструкция CALL. Адрес возврата указывает на интсрукцию JMP XXXXXXXX следующую за вызовом. Так как команда CALL занимает всегда пять байтов, то некоторые арифметические операции над указателями выдают стартовый адрес обработчика и, соответственно позволяют получить доступ к полю счётчика обработчика.

При использовании DelayLoadProfileDLL_UpdateCount у меня возникла одна проблема, которая заслуживает нашего внимания. Изначально, функция не имела инструкций PUSHAD и POPAD для сохранения и восстановления регистров процессора. На некоторых программах код работал прекрасно, в то время как на других его приходилось аварийно завершать. Наконец я вычислил, что проблема кроется в программах, которые импортировали __CxxFrameHandler и _EH_prolog из MSVCRT.DLL. Обе эти API функции ожидали, что регистр EAX будет установлен в необходимое значение, а DelayLoadProfileDLL_UpdateCount нарушала содержимое EAX.

После добавления инструкций PUSHAD и POPAD проблема не исчезла. Тогда, расстроившись, я исследовал сгенерированный компилятором код. Обычно при компиляции debug кода, Visual C++ 6.0 вставляет в прологовкую функцию свой код, чтобы установить все локальные переменные в значение 0xCC. Этот код как раз и нарушал EAX до того как начинала выполняться моя PUSHAD. Для решения данной проблемы пришлось удалить опцию /GZ из параметров компиляции debug для DelayLoadProfileDLL.

Отчёт о результатах

Как только Ваш процесс завершается, то система посылает уведомление DLL_ PROCESS_DETACH всем загруженным DLL. DelayLoadProfileDLL использует данную возможность для сбора информации, накопленной в процессе выполнения. Это значит, что произойдёт сканирование всех массивов обработчиков, подсчёт количества вызовов, которые были сделаны через обработчики и отчёт о том, что было найдено.

На стадии установки, когда IAT переадресовала DelayLoadProfileDLL, поисходит сохранение адреса экзешной IAT в глобальной переменной (g_pFirstImportDesc). Во время завершения, ReportProfileResults использует этот указатель, чтобы снова пройтись через секцию импорта. Для каждой импортированной DLL, она находит адрес первой ячейки IAT. Если это есть IAT, который я переадресовал, то первый указатель в IAT должен указывать на первый обработчик в DLPD_IAT_STUB, распределённой для этой DLL. Код произовит проверку, чтобы гарантировать, что это действительно верно. Если что-то не так, то DelayLoadProfileDLL игнорирует данную DLL.

Обычно, всё выглядит прекрасно и первые ячейки IAT указывают на мои обработчики. Затем выполняются все обработчики для DLL. В каждом обработчике значение поля счётчика прибавляется к общему количеству для DLL. Когда выполнение обработчиков завершается, то ReportProfileResults формирует строку с именем DLL и количеством запросов, которые были сделаны через обработчики. Для вывода результатов используется OutputDebugString.

Загрузка и Внедрение

Программа, которая загружает экзешник и внедряет в него DelayLoadProfileDLL.DLL, как Вы уже догадались, называется DelayLoadProfileDLL.EXE (исходник доступен на сайте MSJ по адресу http://www.microsoft.com/msj). Этот код главным образом использует класс CDebugInjector, который я вкратце опишу. Функция main получает командную строку экзешника и передаёт её в CDebugInjector::LoadProcess. Если процесс создан успешно, то функция main говорит CDebugInjector-у, какую DLL необходимо внедрить. В данном случае DelayLoadProfileDLL.DLL должна быть расположена в той же директории что и DelayLoadProfile.EXE.

Последнее, что что должно быть сделано перед запуском - это вызов CDebugInjector::SetOutputDebugStringCallback. Когда DelayLoadProfileDLL выводит результаты через OutputDebugString, то CDebugInjector просматривает и передаёт их зарегистрированным вызовам. Этот вызов всего навсего печатает строки на консоли. В заключении, функция main вызывает CDebugInjector::Run. При этом просходит запуск нужного нам процесса, а затем (в нужное время) внедрение в него DLL.

На Рисунке 3 показан класс CDebugInjector. В нём как раз содержится самый важный код. CDebugInjector::LoadProcess создаёт указанный процесс как отладочный. Как запустить процесс как отладочный рассказано во многих статьях, а так же в документации MSDN, поэтому я не буду здесь вдаваться в подробности.

Отладочный процесс (в данном случае DelayLoadProfile) должен войти в цикл, который вызывает WaitForDebugEvent и ContinueDebugEvent до тех пор, пока отладка не завершится. Каждый раз когда возвращается WaitForDebugEvent, то что-то происходит в отладке. Это может быть исключение (включая контрольные точки) либо загрузка DLL либо создание потока либо ещё какое-нибудь событие. В документации по WaitForDebugEvent описаны все события, которые могут произойти. Код этого цикла содержится в методе CDebugInjector::Run.

Итак, как запуск процесса как отладочного поможет нам внедрить DLL? Процесс отладчика превосходно контролирует выполнение отладочного процесса. Каждый раз, когда возникает значимое событие, происходит приостановка до тех пор, пока отладчик не вызовет ContinueDebugEvent. Зная это, процесс отладчика может добавить код в адресное пространство отлаживаемого процесса и временно изменить его регистры, чтобы добавленный код нормально выполнялся.

В научных терминах это звучит как: CDebugInjector синтезирует небольшой код обработчика, который вызывает LoadLibrary. Параметр имени DLL в LoadLibrary указывает на имя DLL, которая будет внедрена. CDebugInjector записывает обработчик (и связанное с ним имя DLL) в адресное пространство отлаживаемого процесса. После этого он вызывает SetThreadContext, чтобы изменить указатель инструкции (EIP) для выполнения обработчика LoadLibrary. Вся эта грязная работа происходит в методе CDebugInjector::PlaceInjectionStub.

Сразу после вызова LoadLibrary, в обработчике срабатывает инструкция контрольной точки (INT 3). Это приводит к остановке отладки и передаче управление процессу отладчика. Затем отладчик снова использует SetThreadContext, чтобы восстановить указатель инструкции и другие регистры в их первоначальное значение. После этого идёт вызов ContinueDebugEvent и отладка продолжается с уже внедрённой DLL, как будто ничиго не произошло.

На первый взгляд всё это не выглядит шипко сложно, однако некоторые интересные проблемы всё же возникают. Например, когда наступает нужное время для создания кода обработчика и передачи ему управления? Непосредственно после вызова CreateProcess этого нельзя сделать, потому что, среди прочих причин, импортированная DLL в данной точке не была отображена в память и экзешная IAT не была образована загрузчиком Win32. Другими словами, ещё рановато.

Решение, на котором я в конечном счёте остановился состояло в том, чтобы позволять отладке работать до тех пор, пока не встретится первая контрольная точка. Затем я устанавливал свою собственную контрольную точку на точке входа экзешника. Когда срабатывает эта вторая контрольная точка, то CDebugInjector уже знает, что DLL находится в нужном процессе (включая KERNEL32.DLL) и инициализирована, но никакого кода в экзешнике ещё не запущено. Вот теперь самое подходящее время для внедрения DelayLoadProfileDLL.DLL.

Кстати, а откуда появляется первая контрольная точка ? По определению, процесс Win32, который находится в отладке очень рано вызывает DebugBreak (так же известную как INT 3). В старом коде APISPY32 я использовал DebugBreak как способ для внедрения. К сожалению, в Windows 2000 вызов DebugBreak происходит до инициализации KERNEL32.DLL. Таким образом, CDebugInjector убирает свою контрольную точку когда экзешник собирается получить управление и, соответственно, становится извесно, что KERNEL32.DLL был инициализирован.

Ранее я упомянул контрольную точку, которая возникает после того как возвращается вызов LoadLibrary. Это треться контрольная точка, которую CDebugInjector должен обработать. Все механизмы обработки контрольных точек можно посмотреть в CDebugInjector::HandleException.

Другая проблема адресации с внедрением DLL состоит в том, чтобы определить - где записать обработчик LoadLibrary. Под Windows NT 4.0 и выше можно распределить пространство в другом процессе при помощи VirtualAllocEx, как сделал это я. Но этот способ не работает в Windows 9x, которая не поддерживает VirtualAllocEx. Однако я воспользовался уникальным свойством Windows 9x - это файлы с отображённой памятью. Эти файлы видны во всех адресных пространствах. Я просто создал небольшой файл с отображённой памятью при помощи system page file и поместил туда обработчик LoadLibrary. Получается, что обработчик неявно доступен в отлаживаемом процессе. Подробный код содержится в CDebugInjector::GetMemoryForLoadLibraryStub.

 

Использование DelayLoadProfile

Из командной строки запустите DelayLoadProfile, указав исследуемую программу и некоторые параметры в случае необходимости примерно так:

  DelayLoadProfile notepad c:\autoexec.bat

А вот результаты, которые были получены, применив DelayLoadProfile к CALC.EXE в Windows 2000:

[d:\column\col66\debug]delayloadprofile calc
DelayLoadProfile: SHELL32.dll was called 0 times
DelayLoadProfile: MSVCRT.dll was called 9 times
DelayLoadProfile: ADVAPI32.dll was called 0 times
DelayLoadProfile: GDI32.dll was called 60 times
DelayLoadProfile: USER32.dll was called 691 times

CALC просто запустился и сразу же завершился. Обратите внимание, к SHELL32.DLL и ADVAPI32.DLL не было не одного запроса. Обе эти DLL главные кандидаты на исследование их DelayLoad.

Естевственно, возникает вопрос, почему CALC загружает SHELL32.DLL, но ни разу не вызывает её. При помощи DumpBin /IMPORTS или Depends.EXE, применив их к CALC, довольно просто вычислить, что CALC импортирует из SHELL32.DLL всего навсего одну функцию ShellAboutW. Проще говоря, если Вы не выберете пунк меню Help | About Calculator в CALC, то получится, что для загрузки SHELL32.DLL была напрасно потрачена память. Кстати, SHELL32.DLL неявным образом связана с SHLWAPI.DLL и COMCTL32.DLL, которые тоже загружаются в память и не инициализируются.

Не стоит сразу же прибегать к использованию DelayLoad только потому что DelayLoadProfile сообщила, что DLL не используется. Чтобы использовать DelayLoad, необходимо проверить все связи DLL с другими. Если дело обстоит именно так, то не стоит использовать /DELAYLOAD в Вашем экзешнике. Для определения области использования DLL в Platform SDK есть отличная утилита Depends.EXE.

Следующий вопрос, который может нас заинтересовать при использовании DelayLoadProfile, заключается в том, сколько приложений можно исследовать в течении теста. Очевидно, что если Вы исследуете все аспекты Вашего приложения, то все DLL импортированные в экзешнике будут вызваны. Лично я думаю, что самое важное, это минимальное время загрузки. Это значит, что сразу после запуска Вашего приложения оно должно завершиться. Распределяя работу по загрузке и инициализации Ваших DLL по всему приложению в процессе его выполнения, можно ускорить последовательность начальной загрузки. Кстати, пользователи субъективно судят о скорости работы приложения по времени его запуска.

Я нашёл несколько DLL, от которых можно извлечь выгоду при использования /DELAYLOAD. Как было показано выше - SHELL32.DLL одна из них. Другая - это WINSPOOL.DRV, которая служит для обеспечения печати. Так как пользователи печатают не часто, то она является хорошим претендентом на исследование, так же как OLE32.DLL и OLEAUT32.DLL. Кроме того, некоторые программы, использующие минимальные возможности COM и OLE, так же делают эти DLL возможными кондидатами на исследование. Например, CDPLAYER.EXE в Windows 2000, ссылается на API функцию CreateStreamOnHGlobal в OLE32.DLL. При обычном использовании проигрывателя я не заметил, чтобы эта функция вызывалась.

И напоследок хочу сказать, что DelayLoadProfile не лишена ошибок. Например я знаю, что программы, которые импортируют MFC42.DLL и MFC42U.DLL не будут работать с DelayLoadProfile. Поэтому пришлось добавить аварийный выход. В DelayLoadProfileDLL.CPP за это отвечает функция IsModuleOKToHook. В неё я поместил MFC42.DLL, MFC42U.DLL и KERNEL32.DLL. (т. е. Вы не сможете использовать /DELAYLOAD с KERNEL32.DLL). Если Вам кажется, что какая-то DLL выдаёт ошибку, то для начала, попробуйте добавить её в IsModuleOKToHook.

Надеюсь, что DelayLoadProfile поможет настроить Ваши приложения, используя /DELAYLOAD. Во всяком случае, я хорошо провёл время исправляя казалось бы классический код.