Windows для профессионалов

       

DLL и адресное пространство процесса


Зачастую создать DLL проще, чем приложение, потому что она является лишь набором автономных функций, пригодных для использования любой программой, причем в DLL обычно нет кода, предназначенного для обработки циклов выборки сообщений или создания окон DLL представляет собой набор модулей исходного кода, в каждом из которых содержится определенное число функций, вызываемых приложением (исполняемым файлом) или другими DLL. Файлы с исходным кодом компилируются и компонуются так же, как и при создании ЕХЕ-файла Но, создавая DLL, Вы должны указывать компоновщику ключ /DLL. Тогда компоновщик записывает в конечный файл информацию, по которой загрузчик операционной системы определяет, что данный файл — DLL, а не приложение.

Чтобы приложение (или другая DLL) могло вызывать функции, содержащиеся в DLL, образ ее файла нужно сначала спроецировать на адресное пространство вызывающего процесса. Это достигается либо за счет неявного связывания при загрузке, либо за счет явного — в период выполнения. Подробнее о неявном связывании мы поговорим чуть позже, а о явном — в главе 20.

Как только DLL спроецирована на адресное пространство вызывающего процесса, ее функции доступны всем потокам этого процесса Фактически библиотеки при этом теряют почти всю индивидуальность: для потоков код и данные DLL — просто дополнительные код и данные, оказавшиеся в адресном пространстве процесса. Когда поток вызывает из DLL какую-то функцию, та считывает свои параметры из стека потока и размещает в этом стеке собственные локальные переменные. Кроме того, любые созданные кодом DLL объекты принадлежат вызывающему потоку или процессу — DLL ничем пе владеет.

Например, если DLL-функция вызывает VirtualAlloc, резервируется регион в адресном пространстве того процесса, которому принадлежит поток, обратившийся к DLL функции. Если DLL будет выгружена из адресного пространства процесса, зарезервированный регион не освободится, так как система не фиксирует того, что регион зарезервирован DLL-функцисй. Считается, что он принадлежит процессу и поэтому освободится, только если поток этого процесса вызовет VirtualFree или завершится сам процесс.


Вы уже знаете, что глобальные и статические переменные ЕХЕ-файла пе разделяются его параллельно выполняемыми экземплярами. В Windows 98 это достигается за счет выделения специальной области памяти для таких переменных при проецировании ЕХЕ-файла на адресное пространство процесса, а в Windows 2000 — с помощью механизма копирования при записи, рассмотренного в главе 13. Глобальные и статические переменные DLL обрабатываются точно так же. Когда какой-то процесс проецирует образ DLL-файла на свое адресное пространство, система создает также экземпляры глобальных и статических переменных.

NOTE:
Важно понимать, что единое адресное пространство состоит из одного исполняемого модуля и нескольких DLL-модулей. Одни из них могут быть скомпонованы со статически подключаемой библиотекой С/С++, другие — с DLL-версией той же библиотеки, а третьи (написанные нс на С/С++) вообще ею не пользуются. Многие разработчики допускают ошибку, забывая, что в одном адресном пространстве может одновременно находиться несколько библиотек С/С++. Взгляните на этот код:

VOID EXEFunc()
{

PVOID pv = DLLFunc();

// обращаемся к памяти, на которую указывает pv;
// предполагаем, что pv находится в С/С++-куче ЕХЕ-файла

free(pv);

}

PVOID DLLFunc()
{



// выделяем блок в С/С++-куче DLL return(malloo(100));

}

Ну и что Вы думаете? Будет ли этот код правильно работать? Освободит ли ЕХЕ-функция блок, выделенный DLL-функцией? Ответы на все вопросы одинаковы - может быть. Для точных ответов информации слишком мало. Если оба модуля (EXE и DLL) скомпонованы с DLL-версией библиотеки С/С++, код будет работать совершенно нормально. По если хотя бы один из модулей связан со статической библиотекой С/С++, вызов free окажется неудачным. Я не раз видел, как разработчики обжигались на подобном коде.

На самом деле проблема, решается очень просто, если в модуле есть функция, выделяющая память, в нем обязательно должна быть и противоположная функция, которая освобождает память. Давайте-ка перепишем предыдущий код так:

VOID EXEFunc()
{

PVOID pv = DLLFunc();
// обращаемся к памяти, на которую указывает pv, // не делаем никаких предположений по поводу С/С++-кучи DLLFreeFunc(pv);

}

PVOID DllLFunc()
{

// выделяем блок в С/С++-кую DLL
PVOID pv = malloc(100); return(pv);

}

BOOL DLLFreeFunc(PVOID pv)
{

// освобождаем блок, выделенный в С/С++-куче OLL
return(free(pv));

}

Этот код будет работать при любых обстоятельствах. Создавая свой модуль, не забывайте, что функции других модулей могут быть написаны па других языках, а значит, и ничего не знать о malloc и free. Не стройте свой код на подобных допущениях. Кстати, то же относится и к С++-опсраторам new и delete, реализованным с использованием malloc frее.


Содержание раздела