Загрузка dll библиотеки в адресное пространство процесса во время работы приложения называется

Куда же на самом деле загружается динамическая библиотека, если Windows позволяет использовать загруженный код нескольким процессам? В адресное пространство процесса? Но ведь тогда, когда процесс загрузивший DLL прекратить работу и выгрузится из памяти операционной системой, другой процесс потерпит фиаско!

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

Этот буфер может быть привязан к какому-то файлу, тогда его содержимое будет равно содержимому файла, и все изменения на одной стороне будет транслироваться на другую.

Также буфер может быть открыт в гибридном режиме, при котором его начальное содержимое копируется из файла, но дальнейшие изменения обратно не транслируются. Официально это называется Copy-on-Write, копирование при записи, — то есть при записи в участок файла поверх него как бы создаётся перехватывающая заплатка с копией содержимого этого участка.

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

С точки зрения операционной системы это выглядит так:

Виртуальная памяти процессов — физическая память — файл на диске. Иллюстрация взята из статьи «File Mapping» в MSDN

Теперь о том, как это применимо к библиотекам и многократно запущенным программам.
Сначала загрузчик просто открывает исполняемый файл. Затем он читает его заголовок и для каждой его секции создаёт отдельное файловое отображение с правами, указанными в заголовке этой секции: для кода, ресурсов, константных данных и изменяемых данных. Причём последнее отображается в режиме Copy-on-Write, чтобы у каждой программы библиотека обладала собственным набором переменных.


Выше я написал упрощённое объяснение, необходимое для понимания концепции. Теперь расскажу о том, как всё это работает на низком уровне.

Как я уже писал, работа буфера обеспечена куском физической памяти, фрагменты которого отображаются на виртуальную память процессов. Иными словами, когда программа просит операционную систему создать отображение с такими-то правами доступа для такого-то участка такого-то файла, операционная система модифицирует некоторые записи в таблице соответствия, в результате чего какой-то ранее нечитаемый диапазон страниц адресного пространства начинает отображаться на участок внутри буфера.

Это возможно благодаря принципам организации работы виртуальной памяти:

(Иллюстрация взята из ответа на вопрос «Какую модель памяти сегментную или страничную использует windows, linux, macos?»)

Именно указатель на начало этого диапазона и возвращают функции MapViewOfFile() на Windows и mmap() на Linux.

} Region;

};

} PROCESS_HEAP_ENTRY, *LPPROCESS_HEAP_ENTRY, *PPROCESS_HEAP_ENTRY;

Прежде чем перечислять блоки в куче, присвойте NULL элементу lpData, и это заставит функцию HeapWalk инициализировать все элементы структуры Чтобы пе рейти к следующему блоку, вызовите HeapWalk еще раз, переддв сй тот же описатель кучи и адрес той же структуры PROCESS_HFAPENTRY Если HeapWalk вернет FALSE, значит, блоков в куче больше нет Подробное описание элементов структуры PRO CESS_HEAP_ENTRY см. в документации PlatformSDK

Обычно вызовы функции HeapWalk «обрамляют» вызовами HeapLock и HeapUnlock, чтобы посторонние потоки не портили картину, создавая или удаляя блоки в просматриваемой куче

ГЛАВА 19 DLL: основы

Динамически подключаемые библиотеки (dynamic-link libraries, DLL) — краеугольный камень операционной системы Windows, начиная с самой первой ec версии. В DLL содержатся все функции Windows API. Три самые важные DLL: Kernel32.dll (управление памятью, процессами и потоками), User32.dll (поддержка пользовательского интерфейса, в том числе функции, связанные с созданием окон и передачей сообщений) и GDI32.dll (графика и вывод текста).

ВWindows есть и другие DLL, функции которых предназначены для более специализированных задач. Например, в AdvAPI32.dll содержатся функции для защиты объектов, работы с реестром и регистрации событий, в ComDlg32.dll ~ стандартные диалоговые окна (вроде File Open и File Save), a ComCrl32 dll поддерживает стандартные элементы управления.

Вэтой главе я расскажу, как создавать DLL-модули в Ваших приложениях. Вот лишь некоторые из причин, по которым нужно применять DLL:

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

Возможность использования разных языков программирования. У Вас есть выбор, на каком языке писать ту или иную часть приложения Так, пользовательский интерфейс приложения Вы скорее всего будете создавать на

Microsoft Visual Basic, но прикладную логику лучше всего реализовать на С++. Программа на Visual Basic может загружать DLL, написанные на С++, Коболе, Фортране и др.

Более простое управление проектом. Если в процессе разработки программного продукта отдельные его модули создаются разными группами, то при использовании DLL таким проектом управлять гораздо проще Однако конечная версия приложения должна включать как можно меньше файлов (Знал я одну компанию, которая поставляла свой продукт с сотней DLL. Их приложение запускалось ужасающе долго — перед началом работы ему приходилось открывать сотню файлов на диске.)

Экономия памяти. Если одну и ту же DLL использует несколько приложений, в оперативной памяти может храниться только один ее экземпляр, доступный этим приложениям. Пример — DLL-версия библиотеки С/С++ Ею пользуются многие приложения. Если всех их скомпоновать со статически подключаемой версией этой библиотеки, то код таких функций, как sprintf, strcpy, malloc и др., будет многократно дублироваться в памяти Но ссли они компонуются с DLL-версией библиотеки С/С++, в памяти будет присутствовать лишь одна копия кода этих функций, что позволит гораздо эффективнее использовать оперативную память.

Разделение ресурсов. DLL могут содержать такие ресурсы, как шаблоны диалоговых окон, строки, значки и битовые карты (растровые изображения). Эти ресурсы доступны любым программам

Упрощение локализации. DI,L нередко применяются для локализации приложений. Например, приложение, содержащее только код без всяких компонентов пользовательского интерфейса, может загружать DLL с компонентами локализованного интерфейса

Решение проблем, связанных с особенностями различных платформ. В разных версиях Windows содержатся разные наборы функций. Зачастую разработчикам нужны новые функции, существующие в той версии системы, которой они пользуются. Если Ваша версия Windows не поддерживает эти функции, Вам не удастся запустить такое приложение: загрузчик попросту откажется его запускать. Но если эти функции будут находиться в отдельной DLL, Вы загрузите программу даже в более ранних версиях Windows, хотя воспользоваться ими Вы все равно не сможете.

• Реализация специфических возможностей. Определенная функциональность в

Windows доступна только при использовании DLL Например, отдельные виды ловушек (устанавливаемых вызовом SetWindowsHookEx и SetWinEventHook можно задействовать при том условии, что функция уведомления ловушки размещена в DLL. Кроме того, расширение функциональности оболочки Windows возможно лишь за счет создания СОМ-объектов, существование которых допустимо только в DLL. Это же относится и к загружаемым Web-браузером ActiveX-элементам, позволяющим создавать Web-страницы с болсс богатой функциональностью.

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ее

Общая картина

Попробуем разобраться в том, как работают DLL и как опи используются Вами и системой Начнем с общей картины (рис 19-1).

Для пачала рассмотрим неявное связывание EXE- и DLL-модулей. Неявное связывание (implicit linking) — самый распространенный на сегодняшний день метод (Windows поддерживает и явное связывание, но об этом — в главе 20.)

Как видно на рис 19-1, когда некий модуль (например, EXE) обращается к функциям и переменным, находящимся в DLL, в этом процессе участвует несколько файлов и компонентов. Для упрощения будем считать, что исполняемый модуль (EXE) импортирует функции и переменные из DLL, а DLL-модули, наоборот, экспортируют их в исполняемый модуль. Но учтите, что DLL может (и это не редкость) импортировать функции и переменные из других DLL

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

1.Прежде всего Вы должны подготовить заголовочный файл с прототипами функций, структурами и идентификаторами, экспортируемыми из DLL. Этот файл включается в исходный код всех модулей Вашей DLL. Как Вы потом увидите, этот же файл понадобится и при сборке исполняемого модуля (или модулей), который использует функции и переменные из Вашей DLL

2.Вы пишете на С/С++ модуль (или модули) исходного кода с телами функций и определениями переменных, которые должны находиться в DLL. Так как эти модули исходного кода не нужны для сборки исполняемого модуля, они могут остаться коммерческой тайной компании-разработчика.

3.Компилятор преобразует исходный код модулей DLL в OBJ-файлы (по одному на каждый модуль).

4.Компоновщик собирает все OBJ-модули в единый загрузочный DLL-модуль, в который в конечном итоге помещаются двоичный код и переменные (глобальные и статические), относящиеся к данной DLL Этот файл потребуется при компиляции исполняемого модуля

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

Создав DLL, можно перейти к сборке исполняемого модуля.

6.Во все модули исходного кода, где есть ссылки на внешние функции, переменные, структуры данных или идентификаторы, надо включить заголовочный файл, предоставленный разработчиком DLL.

СОЗДАНИЕ DLL

СОЗДАНИЕ ЕХЕ

1) Заголовочный файл с экспортируемыми

6) Заголовочный файл с импортируемыми

прототипами, структурами и

прототипами структурами и идентификаторами 7)

идентификаторами (символьными

Исходные файлы С/С++, из которых вызываются

именами) 2) Исходные файлы С/С++ в

импортируемые функции и переменные 8)

которых реализованы функции и

Компилятор создает OBJ-файл из каждого

определены переменные 3) Компилятор

исходного файла С/С++ 9) Используя OBJ

создаэт OBJ-файл из каждого исходного

модули и LIB-файл и учитывая ссылки на

файла С/С++ 4) Компоновщик собирает

импортируемые идентификаторы компоновщик

DLL из OBJ-модулей 5) Если DLL экспортирует хотя бы одну переменную или функцию, компоновщик создает и

LIB-файл

собирает ЕХЕ-модуль (в котором также размещается таблица импорта — список необходимых DLL и импортируемых идентификаторов)

Рис. 19-1. Так DLL создается и неявно связывается с приложением

1.Вы пишете на С/С++ модуль (или модули) исходного кода с телами функций и определениями переменных, которые должны находиться в ЕХЕ-файле. Естественно, ничто не мешает Вам ссылаться на функции и псрсмснные, определенные в заголовочном файле DLL-модуля

2.Компилятор преобразует исходный код модулей EXE в OBJ-файлы (по одному на каждый модуль).

3.Компоновщик собирает все OBJ-модули в единый загрузочный ЕХЕ-модуль, в который в конечном итоге помещаются двоичный код и переменные (глобальные и статические), относящиеся к данному EXE. В нем также создается раздел импорта, где перечисляются имена всех необходимых DLL-модулей (информацию о разделах см в главе 17) Кроме того, для каждой DLL в этом разделе указывается, па какие символьные имена функций и переменных ссылается двоичный код

Все мы прекрасно знаем, что в операционной системе Windows существуют функции/процедуры, размещаемые внутри динамических библиотек (DLL). Эта особенность является одним из основных отличительных черт современных операционных систем, и Windows тут не исключение.

Динамическая библиотека (DLL) — файл, содержащий исполняемый код (сгруппированный в процедуры/функции) либо данные-ресурсы (шрифты/иконки/иные графические примитивы). Фактически библиотека представляет собой способ разделения общего кода между многими приложениями системы.

Но у библиотек имеется одна характерная особенность: они не запускаются через привычные пользователям механизмы, применимые к исполняемым образам .exe (например, запуск двойным щелчком из проводника), вместо этого процедуры/функции/ресурсы библиотеки доступны для использования исключительно из кода приложений. Одним словом просто так вот щелкнуть в проводнике по .DLL-файлу и запустить его не получится. И все же, у специалиста/пользователя время от времени возникает необходимость воспользоваться функциями той или иной библиотеки, в этом случае имеется два основных пути:

  • Написать собственное приложение, которое будет использовать требуемые функции [той или иной библиотеки];
  • Воспользоваться специализированным средством (утилитой) командной строки rundll32.exe;

Очевидно, что в рамках данной заметки нам интересен именно второй подход. Для его реализации разработчики из MS создали своеобразный «стартер», который вызывает код [заданной] функции (процедуры), размещаемой в одной из доступных в системе библиотек, прямо из командной строки. Идея была реализована Microsoft в виде утилиты под названием rundll32.exe.
RunDll32 — [системная] утилита командной строки, разработанная для загрузки и запуска функций из 32/64-битных библиотек DLL. Тем не менее, ошибочно было бы считать, что утилита rundll32 является средством для «запуска DLL» по аналогии с исполняемым приложением, в действительности же невозможно «запустить DLL», поскольку это не исполняемый образ в классическом понимании (вроде .EXE-образов).

rundll32 позволяет получить доступ только к функциям/процедурам, которые были описаны надлежащим (определенным) образом: вызываемые функции должны быть экспортируемыми и должны удовлетворять специфичным требованиям к определению входных параметров. Функции библиотек, которые не подпадают по описанные требования, «запускаться» посредством rundll32 не будут.

Теоретически rundll32 может использоваться для запуска функций из любых типов файлов, по структуре идентичных с библиотеками DLL: элементов ActiveX (*.ocx), апплетов панели управления (*.cpl), драйверов (*.drv). Бытует мнение, что когда-то функциональные особенности rundll32 были неявными, то есть «скрытыми» от пользователя системы, но при этом самой системой активно эксплуатирующимися. Не совсем понятное утверждение, поскольку у разработчиков никогда и не существовало проблем в том, чтобы набросать алгоритм запуска произвольных функций библиотек и выделить его в отдельный функционал, к тому же в Windows это основа основ. Со временем утилита rundll32 превратилась в достаточно продвинутое средство, и в определенный момент, по причине того, что программа предоставляла возможности общего характера, разработчики решили включить её в базовый комплект ОС в виде отдельной утилиты. В состав Microsoft Windows 95, Windows 98 и Windows Millennium входили программы rundll.exe и rundll32.exe, а в операционных системах Microsoft Windows NT 4.0, Windows 2000 и Windows XP, в сборку была включена единственная утилита с именем rundll32.exe.

Стоит обратить внимание на то, что в Windows Vista/7 и более поздних ОС, утилита rundll32 не рекомендуется к использованию и включена только лишь с целью сохранения совместимости со старыми сценариями/приложениями.

Причина подобного отношения кроется в многочисленных проблемах, связанных с использованием rundll32 в своих программных окружениях. Как же без rundll32, спросите Вы? Оказывается, Microsoft активно внедряет специализированные системные утилиты, которые постепенно заменяют функционал полюбившегося многим программного средства. Поэтому, время от времени в процессах Windows 7 можно таки встретить процесс rundll32, однако встречается подобное явление всё реже и реже.

Утилита всегда размещается в каталоге %SystemRoot%System32 для 32-битных версий Windows 2000/XP/Vista/7+ и в каталогах %SystemRoot%System32 и %SystemRoot%SysWOW64 для 64-битных версий Windows Vista/7+. В случае обнаружения файла с именем rundll32.exe в любом другом каталоге, настоятельно рекомендуется удалить файл и просканировать станцию на наличие вирусов.

Алгоритм работы rundll32

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

  1. Вызывается функция LoadLibrary, которая загружает выбранную DLL;
  2. Вызывается функция GetProcAddress, для получения адреса точки входа вызываемой из библиотеки функции;
  3. Вызывается проверка соответствия функции специфическим требованиям;
  4. Вызывается сама функция и ей передаются входные параметры;
  5. Происходит выход из функции по завершению её работы;
  6. Библиотека DLL выгружается из памяти процесса.

Как увидеть полный путь запуска rundll32

Если в системе ни с того ни с сего вдруг удалось обнаружить запущенный процесс rundll32, и вам стало интересно, а какой же, собственно, функционал утилита выполняет, то можно определить это по полному пути запуска утилиты rundll32. Посмотреть полный путь запуска, то есть параметры командной строки можно при помощи системной утилиты Диспетчер задач Windows (Task Manager).
Вызовите «Диспетчер задач» (Ctrl+Shift+Esc), перейдите в меню «Вид», выберите пункт «Выбрать столбцы..» и пролистав список вниз, найдите пункт «Командная строка» и отметьте его чекбокс, затем нажмите ОК. Результатом будет появление в главном окне диспетчера задач в параметре «командная строка» полной строки запуска rundll32.

Командная строка rundll32

32-битные и 64-битные версии программы

В 64-битных версиях ОС семейства Windows присутствуют 2 варианта программы rundll32.exe:

  • 64-битная версия, расположенная в %SystemRoot%System32;
  • 32-битная версия, расположенная в %SystemRoot%SysWOW64.

В 64-битной ОС, для загрузки 64-битной библиотеки DLL может быть использована 64-битная версия rundll32.exe, находящаяся в директории %SystemRoot%System32. Напротив, 32-битные программы в 64-битной ОС, обращающиеся к %SystemRoot%System32, в целях обеспечения совместимости будут перенаправлены в %SystemRoot%SysWOW64 и, соответственно, будут использовать уже 32-битную версию rundll32.exe.

Пример использования rundll32

Для запуска программы rundll32 используется синтаксис командной строки следующего вида:

rundll32.exe <имя_библиотеки_dll>,<имя_функции> <необязательные_параметры_функции>

В качестве примера предлагаю разобрать команду запуска апплета «Язык и региональные стандарты» панели управления, вкладка «Форматы»:

rundll32.exe shell32.dll,Control_RunDLL intl.cpl,,0

При выполнении данной команды утилита rundll32 вызовет функцию Control_RunDLL, находящуюся в библиотеке shell32.dll, и передаст ей следующие параметры:

Параметр Описание
hWnd Идентификатор (дескриптор) родительского окна, который обычно используется при создании окон в функциях загружаемой библиотеки DLL.
hInstance Дескриптор (заголовок экземпляра) выбранной библиотеки DLL. Иначе, стартовый адрес процесса DLL в адресном пространстве. В нашем случае — библиотеки shell32.dll.
lpCmdLine Командная строка, передаваемая библиотеке. То есть параметры, передаваемые самой библиотеке. В нашем случае intl.cpl,,0;
nCmdShow Режим отображения окон выбранной библиотеки. (данные, передаваемые функции CreateProcess).

Теперь давайте отметим и некоторые требования к синтаксису rundll32:

  • Параметр, описывающий точку входа вызываемой функции (метка EntryPoint) чувствителен к регистру. Это означает, что значение Control_RunDLL не тоже самое что control_rundll. Довольно часто неправильное указание регистра символов вызываемой функции приводит к ошибкам отказа в обнаружении последних.
  • Функция (в приведенном выше примере — функция Control_RunDLL) должна самостоятельно выполнять анализ командной строки и идентифицировать отдельные ее аргументы.
  • Утилиты rundll/rundll32 ищут указанную библиотеку DLL в стандартных местоположениях.

    В 64-разрядных версиях Windows (начиная с Vista) системные библиотеки размещаются в трёх каталогах: %WinDir%System32, %WinDir%SysWOW64 и %WinDir%WinSxS. Причем последний используется компонентной моделью (Side-by-Side).

    В нашем примере библиотека shell32.dll располагается по известному в переменной %PATH% пути, поэтому мы и указываем её в сокращенном виде. Чтобы быть уверенным, что будет загружена именно интересующая нас библиотека DLL, рекомендуется конкретизировать полный путь к файлу библиотеки. В противном случае может возникнуть ситуация, когда в системе может присутствовать одноименная библиотека, находящаяся в стандартном местоположении (например, по путям, определяемым %PATH%), а та, которую хотите использовать Вы, находится по неизвестному системе пути, в этом случае утилитой rundll32 будет вызвана первая библиотека.

  • <имя_библиотеки_dll> не должно содержать недопустимых символов: пробелов, запятых и кавычек (ограничение это накладывается кодом анализатора командной строки утилиты rundll32).
    Ранее, во времена Windows 95/98, к синтаксису командной строки утилиты rundll32 применялись достаточно жесткие требования. Например, чрезвычайно важным являлось наличие запятой («,») между параметром <имя_библиотеки_dll> и именем вызываемой функции. Если запятая опускалась, утилита rundll32 «по-тихому» завершала работу, не выполняя никаких действий с библиотекой. Кроме того, между параметром <имя_библиотеки_dll>, символом запятой и названием вызываемой функции не должно было быть никаких пробелов. Но, со временем анализатор командной строки утилиты претерпевал изменения и эволюционировал, и в данный момент, синтаксис rundll32 позволяет применять пробел между именем библиотеки и именем функции.

Правила создания функций

Очевидно, что в создаваемую нами собственную библиотеку DLL необходимо поместить функцию со следующими входными параметрами (пример описания на Ассемблере):

proc        EntryPoint hWnd,hInstance,lpCmdLine,nCmdShow

            . . .

            ret

endp

При создании экспортируемой функции (в примерах выше она имеет псевдо-имя EntryPoint) необходимо учитывать следующие моменты:

  1. Вместо имени EntryPoint, желательно указать фактическое имя функции. Обратите внимание, что «точка входа», используемая программой rundll32, не зависит от функции (точки входа) DllEntryPoint, которая в 32-разрядных библиотеках DLL осуществляет обработку процессов и оповещение о подключении и отключении потоков. Это точка входа в саму функцию.
  2. Функцию, являющуюся точкой входа для программы rundll32, необходимо определить, используя соглашение о вызовах _stdcall (в C++ по умолчанию для атрибута _stdcall используется значение CALLBACK). Иначе, по умолчанию будет использоваться другое соглашение о вызовах _cdecl. Это приведет к аварийному завершению работы программы rundll32 после вызова данной функции.

Функции, являющейся точкой входа, передаются следующие параметры:

Параметр Описание
hWnd Идентификатор (дескриптор) родительского окна, который обычно используется при создании окон в функциях загружаемой библиотеки DLL.
hInstance Дескриптор (заголовок экземпляра) выбранной библиотеки DLL. Иначе, стартовый адрес процесса DLL в адресном пространстве.
lpCmdLine Командная строка, передаваемая библиотеке. Данная строка представляет собой последовательность символов, завершающуюся символом с кодом 0.
nCmdShow Режим отображения окон выбранной библиотеки. (данные, передаваемые функции CreateProcess).

Пример создания DLL с экспортируемой функцией

Я написал код примера на языке Ассемблера, уж не обессудьте, поскольку пока лишь на этом языке я хоть как-то могу внятно выражать свои мысли :) Компилируется исходный код посредством компилятора FASM, версией для Windows (FASMW.EXE). В библиотеке я описал одну единственную экспортируемую функцию под названием ShowErrorMessage, которая выводит на экран окно ошибки с заданными параметрами. Создадим файл testdll.asm и добавим в него следующее содержимое:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

format      PE GUI 4.0 DLL               ; Формат PE DLL. Версия GUI 4.0.

entry       DllEntryPoint                ; Точка входа

include     ‘%include%win32a.inc’       ; Стандартный инклюд

;— сегмент данных ———————————————————

section     ‘.data’ data readable writeable

_text       db   ‘This is test message’,0   ; Текст ошибки

_title      db   ‘This is title’,0          ; Текст в заголовке окна.

;— сегмент кода ———————————————————-

section     ‘.text’ code readable executable

;— DllEntruPoint: точка входа, должна быть обязательно———————

proc        DllEntryPoint hinstDLL,fdwReason,lpvReserved

            mov   eax,TRUE

            ret

endp

;— ShowErrorMessage(HWND hWnd,DWORD dwError);-наша функция по выводу окна-

proc        ShowErrorMessage hWnd,hInstance,CmdLine

            invoke  MessageBox,[hWnd],[CmdLine],_title,MB_ICONERROR+MB_OK

            ret

endp

;— подключение функций из системных библиотек——————————

section     ‘.idata’ import data readable writeable

library     kernel32,‘KERNEL32.DLL’,user32,‘USER32.DLL’

include     ‘apikernel32.inc’

include     ‘apiuser32.inc’

;— секция экспорта ——————————————————-

; как раз тут то мы и указываем, какие функции будут экспортируемыми.

section     ‘.edata’ export data readable

export      ‘testdll.dll’,ShowErrorMessage,‘ShowErrorMessage’

;— секция переносимых процедур ——————————————-

section     ‘.reloc’ fixups data readable discardable

После компиляции у нас в рабочей директории создается файл testdll.dll, поэтому для проверки функционала можно вызвать команду:

rundll32 testdll.dll,ShowErrorMessage ThisIsTestString

предисловие

Процессы — это программы, которые загружаются в память и готовы к выполнению. Каждый процесс имеет личное виртуальное адресное пространство, состоящее из кода, данных и других ресурсов. 32-разрядные системные процессы выделяют 4G виртуального адресного пространства. Диапазон адресов памяти составляет 0x00000000 ~ 0xFFFFFFFF. Это адресное пространство памяти является независимым для каждого процесса, то есть один процесс не может получить доступ к адресному пространству других процессов.

Например, часть данных сохраняется в памяти процесса A. Предположим, что адрес этих данных равен 0x33333333. Это время, даже если считываются данные адреса памяти процесса B 0x33333333. Эти данные также отличаются от данных по тому же адресу в процессе А. Даже адрес 0x33333333 процесса B вообще не имеет данных.

Если вы хотите добиться обмена данными между несколькими процессами, это связано с проблемой межпроцессного взаимодействия. Конечно, есть много способов общения между процессами. Например, динамическая библиотека ссылок, динамический обмен данными, сопоставление файлов, конвейер, буфер обмена и т. Д. Эта статья в основном рассказывает о том, как использовать динамическую библиотеку ссылок для достижения межпроцессного взаимодействия.

Способ реализовать

DLL — это файл, который содержит ряд функций и данных и может быть вызван приложением.

В общем, когда приложение вызывает функцию в DLL,Операционная системаОбраз файла dll отображается в адресное пространство процесса. В настоящее время dll — это просто некоторый код и данные, которые помещаются в пространство адресного процесса для потоков в процессе. Когда несколько приложений загружают одну и ту же dll, В памяти только одна dll. В это время, если один из процессов вызывает функцию DLL, он получит параметры, переданные ему в стек потоков текущего процесса, и использует стек потоков для хранения необходимых ему переменных. Любой объект, созданный функцией dll, принадлежит вызывающему потоку или вызывающему процессу, и dll не владеет никакими объектами, то есть, если функция в dll вызывает VirtualAlloc, система зарезервирует адрес из адресного пространства вызывающего процесса. В этом случае совместное использование данных библиотеки динамических ссылок не может быть достигнуто. Если вы хотите добиться совместного использования данных, сначала давайте взглянем на следующую диаграмму состава виртуального пространства процесса.

+———————————+ 4 GB

|                  |

| Пространство ядра |

|                  |
|                  |
+———————————+ 3 GB
|                  |
| Общее пространство пользователя |
|                 |
|                 |
+———————————+ 2 GB
|                 |
| Личное пространство пользователя |
|                 |
|                  |
+———————————+ 0 GB

Из этого рисунка видно, что все упомянутые выше вызовы DLL используют 0–2 ГБ виртуального пространства. Если вы хотите добиться совместного использования, вы можете использовать пространство 2GB-3GB. Другими словами, данные должны быть сохранены в этом пространстве. Здесь нам нужно использовать #pragma data_seg () для определения общего именованного сегмента данных в DLL. Конкретно, как этого добиться, мы можем продемонстрировать на следующем примере.

Демо пример

Сначала создайте совместный проект DLL, эта DLL используется для сохранения общих данных.

Этапы реализации

1. Создайте новый проект Win32 и назовите его SharedDll

    

2. DLL выбора типа программы

     

3. Инженерная структура

       

4. Файл SharedDll.h в основном объявляет, что две функции используются для сохранения общих данных и получения общих данных.

// The following ifdef block is the standard way of creating macros which make exporting   
// from a DLL simpler. All files within this DLL are compiled with the SHAREDDLL_EXPORTS  
// symbol defined on the command line. this symbol should not be defined on any project  
// that uses this DLL. This way any other project whose source files include this file see   
// SHAREDDLL_API functions as being imported from a DLL, whereas this DLL sees symbols  
// defined with this macro as being exported.  
#ifdef SHAREDDLL_EXPORTS  
#define SHAREDDLL_API __declspec(dllexport)  
#else  
#define SHAREDDLL_API __declspec(dllimport)  
#endif  
  
// This class is exported from the SharedDll.dll  
class SHAREDDLL_API CSharedDll {  
public:  
    CSharedDll(void);  
    // TODO: add your methods here.  
};  
  
// set string to shared dll  
SHAREDDLL_API void SetValueString(LPCSTR str);  
  
// get string from shared dll  
SHAREDDLL_API LPSTR GetValueString();  

5. В файле SharedDll.cpp используйте команду предварительной обработки #pragma data_seg для установки сегмента общих данных.

……  
 // Объявление общих данных начинается  
#pragma data_seg("SharedData")   
 // Обязательно инициализируйте следующие переменные, иначе данные не будут разделены между несколькими процессами  
char m_strString[256]="";   
 // Конец объявления общих данных       
#pragma data_seg()  
 // Разрешение на определение общего сегмента данных в соединителе доступно для чтения, записи и совместного использования  
#pragma comment(linker,"/SECTION:SharedData,RWS")  
  
 // Интерфейс для получения общих данных  
SHAREDDLL_API LPSTR GetValueString()  
{  
    return m_strString;  
}  
 // Сохранить интерфейс обмена данными  
SHAREDDLL_API  void SetValueString(LPCSTR str)  
{  
    strcpy(m_strString,str);  
}  
……  
 

Пока что проект совместного использования DLL готов. Затем создайте два приложения MFC для сохранения данных в общей библиотеке DLL или получения данных из общей библиотеки DLL.

Первая программа: Создайте программу MCF Dialog, экран выглядит следующим образом. Поле ввода используется для ввода информации о данных, а кнопка [Отправить] используется для отправки данных в общую DLL. Конечно, проект должен импортировать пакет lib из SharedDLL.

     

Код реализации выглядит следующим образом:

……  
 // [Submit] событие нажатия кнопки  
void CFormADlg::OnBnClickedOk()  
{  
         // Получить информацию о входных данных из окна редактирования  
    CEdit* e = (CEdit*) this->GetDlgItem(IDC_EDIT1);  
    CString str;  
    e->GetWindowText(str);  
               // Вызов интерфейса SetValueString файла SharedDLL.dll для сохранения данных в общей DLL  
    SetValueString((LPCTSTR)str);  
}  
……  
 

Вторая программа: как описано выше, создайте новую программу MCF Dialog, экран выглядит следующим образом. Поле редактирования используется для отображения информации о данных, а кнопка [Отображение] используется для получения данных, сохраненных в общей DLL. Этот проект также должен импортировать пакет SharedDLL lib.

      

код шоу, как показано ниже:

……  
// [Display] событие нажатия кнопки  
void CFormBDlg::OnBnClickedOk()  
{  
         // Вызвать интерфейс GetValueString SharedDLL.dll для получения общих данных  
    CString str = (CString)GetValueString();  
                 // Вывод полученных данных в поле редактирования формы FormB  
    CEdit* e = (CEdit*) this->GetDlgItem(IDC_EDIT1);  
    e->SetWindowText(str);  
}  

Таким образом, все проекты были завершены, давайте посмотрим на результаты работы.

Сначала мы копируем SharedDll.dll, FormA.exe и FormB.exe в один каталог, чтобы облегчить их взаимный доступ.

Затем запустите две программы, FormA.exe и FormB.exe.

Введите данные в поле редактирования окна FormA и нажмите кнопку «Отправить».

Затем нажмите кнопку Показать в окне FormB

Из приведенного выше примера мы видим, что связь между процессом FormA и процессом FormB может быть достигнута через библиотеку динамического соединения, но если вы используете библиотеку динамического соединения для межпроцессного взаимодействия, вам необходимо обратить внимание на несколько моментов.
Межпроцессное взаимодействие библиотеки динамического соединения применимо к локальному межпроцессному взаимодействию, но не используется для межсетевого взаимодействия.
Межпроцессное взаимодействие библиотеки динамических ссылок является проблемой, которая не может быть достигнута в режиме реального времени. Если вы хотите реализовать проблему уведомлений в реальном времени, вы можете использовать ее с некоторыми методами синхронизации, включая: Событие, Мьютекс, Семафор Семафор, ресурсы критической области и т. Д.
Если вы хотите использовать динамическую библиотеку для межпроцессного взаимодействия, оба процесса должны импортировать эту динамическую библиотеку.

20.07.2010, 23:49. Показов 112646. Ответов 7


1. Теоретическая часть. Знакомство с Dynamic-Link Library.

  • 1.1. Что такое DLL.
  • 1.2. Использование DLL.
  • 1.3. Необходимость внедрения DLL. Нужно ли это?

2. Практическая часть. Создание Dynamic-Link Library в RAD Studio.

  • 2.1. Создаём первую DLL своими руками.
  • 2.2. Неявная загрузка.
  • 2.3. Явная загрузка.
  • 2.4. Отложенная загрузка.

3. Заключение.

_________________________________________________

1. Теоретическая часть. Знакомство с Dynamic-Link Library.

  • 1.1. Что такое DLL.

Для начала нужно разобраться, что же такое DLL.
DLL — это сокращение фразы Dynamic-Link Library — динамически подключаемая библиотека.
Первоначальная идея введения библиотек заключалась в более рациональном использовании ресурсов ПК, таких как ОЗУ и НЖМД. Предполагалось, что различные приложения будут использовать одну библиотеку, тем самым ликвидируя проблему сохранения одинаковых участков кода в различных местах и многократную загрузку одинаковых участков в ОЗУ.

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

По сути, DLL — это относительно независимый участок кода, имеющий свою оболочку.
Оболочка DLL имеет секции импорта/экспорта.

Секция экспорта перечисляет те идентификаторы объектов (классы, функции, переменные), доступ к которым предоставляет данная DLL. В большинстве случаев именно эта секция вызывает особый интерес у разработчиков. Тем не менее, DLL может вовсе не содержать секцию экспорта. Кроме функций, к которым можно получить доступ извне (exported), существуют также внутренние функции (internal), которые спрятаны от «посторонних глаз» и могут использоваться лишь кодом самой DLL.

Секция импорта предназначена для связи данной DLL с другими.
Большинство библиотек импортируют функции из системных DLL (kernel32.dll, user32.dll и др.). Иногда в стандартный список нужно добавить другие библиотеки, например ws2_32.dll для работы с Socket‘ами.

  • 1.2. Использование DLL.

DLL не может выполняться сама по себе, поэтому требует хост-процесс (главный процесс, в котором предполагается использование DLL).
Перед тем, как библиотеку можно будет использовать, её необходимо загрузить в область памяти хост-процесса. В exe-файле компилятором будет сгенерирована инструкция вызова заданной функции (call).
Т.к. DLL расположена в адресном пространстве процесса, то вызов функции из библиотеки будет иметь вид:

Код

call адрес_вызываемой_процедуры

Например:

Assembler
1
2
3
4
5
6
7
;Произвольный код
call 010199  ; Вызов процедуры
; Произвольный код
; Код с адреса 010199:
add eax, 500
mul eax, edx
ret

При выполнении команды call, процессор передает управление на код с адреса 010199, и выполняет его до команды ret, а затем возвращает управление команде следующей за call. Код который вызывается командой call называется процедурой.

Файл, являющийся динамически загружаемой библиотекой не всегда носит расширение *.dll.
Есть также: *.cpl (библиотеки, используемые апплетом панели управления) и *.ocx.

Библиотеки удобно использовать для разделения приложения на части, выполняющие разные роли.
Например, приложение, обеспечивающее работу с базами данных может быть разделено на часть, отвечающую за выборку данных из базы, находящейся на сервере, и, часть, которая отвечает за визуальное управление приложением. Это явный пример той модульности, о которой я упоминал выше. Вы в праве изменять принципы обмена данными с сервером БД, не затрагивая при этом работу с визуальной частью.

DLL может являться также обычным хранилищем ресурсов, о создании которого я рассказывал в теме https://www.cyberforum.ru/cpp-… 27736.html.

  • 1.3. Необходимость внедрения DLL. Нужно ли это?

Настало время ответить на вопрос: Когда же нужно использовать DLL?
Если вы разрабатываете небольшой проект или тестовое приложение, то внедрение DLL будет для вас пустой тратой времени и сил. Тратой времени: не только времени на создание библиотеки, но ещё и времени, необходимого для загрузки DLL в память хост-процесса. До того, как вы будете использовать объекты из DLL, вам рано или поздно прийдётся выгрузить содержимое в память. А это требует некоторого времени.
Однако, если вашу DLL будет использовать более, чем одна копия приложения (или несколько различных приложений) — наступит явный выигрыш.

Для эксперимента запустите Microsoft Office. Первый запуск долгий. Это обусловлено тем, что все необходимые модули загрузаются в оперативную память. Теперь полностью закройте Office и снова откройте его! Окно появится почти мгновенно. Это обусловлено тем, что модули хранились в ОЗУ (система не тронет их, пока ей не понадобится память для других целей).

При создании больших проектов лучше сразу пытаться представить себе части, которые могут быть уникальными и требующими частого усовершенствования.

Но не старайтесь придать вашему проекту чрезвычайной «уникальности» за счёт помещения каждой функции в отдельную библиотеку! Это можно делать, только зачем тратить время для загрузки каждого модуля?

2. Практическая часть. Создание Dynamic-Link Library в RAD Studio.

  • 2.1. Создаём первую DLL своими руками.

Пришло время постепенно перейти от теории к практике.
Открываем IDE (для написании данной статьи я использовал RAD Studio 2010).
Переходим к File -> New -> Other -> Dynamic-Link Library.
Перед нами возникает диалог:

Dynamic-Link Library: Теория + Практика

Source Type — язык, на котором ведётся разработка.
Use VCL — использование библиотеки визуальных компонентов.
Multi Threaded — опция, указывающая на то, будет ли использоваться многопоточность в данной DLL (VCL уже подразумевает в себе многопоточность).
VC++ Style DLL — опция, указывающая на то, будет ли DLL совместима с компиляторами Microsoft.

Если в совместимости нет нужды и опция не выбрана, то DLL будет иметь точку входу с таким прототипом:

C++
1
int WINAPI DllEntryPoint(HINSTANCE hinst, unsigned long reason, void* lpReserved);

Для обеспечения совместимости точка входа изменяется на:

C++
1
BOOL WINAPI DllMain(HINSTANCE hinstDLL, DWORD fwdreason, LPVOID lpvReserved);

Оставим диалог без изменений и нажмём «ОК«.
Перед нами появится шаблон минимальной DLL. Сохраним его.
Как вы помните, сама по себе DLL работать не может, ей нужен клиент. Поэтому, для удобства сразу создадим новый проект VCL Forms Application.
Для этого переходим в Project Manager, вызываем контекстное меню у нашей Project Group и переходим к Add New Project -> VCL Forms Application.
Для удобста я назвал проекты TestDLL и TestVCL соответственно (и сохранил их в одном каталоге — это избавит меня от копирования DLL или указания абсолютного пути):
Название: 2.png
Просмотров: 12698

Размер: 12.3 Кб
Без изменений запускаем TestVCL, сохраняем и переключаемся к проекту TestDLL (дабл-клик на проекте в Project Manager).

Переходим к Run -> Parameters и в поле Host Application указываем путь к нашему проекту TestVCL.

К шаблону DLL добавляем функцию, которая будет вычислять сумму и выводить результат на экран:

C++
1
2
3
4
5
6
7
#include "TestDLL.h" // создание этого заголовочного файла будет описано ниже
//---------------------------------------------------------------------------
void ShowSum(const int A, const int B)
{
  ShowMessage(IntToStr(A) + " + " + IntToStr(B) + " = " + IntToStr(A + B));
}
//---------------------------------------------------------------------------

В проекте TestDLL добавим также заголовочный файл (TestDLL.h) с таким содержанием:

C++
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
#ifndef __TESTDLL_H
#define __TESTDLL_H
 
/*
символ TESTDLL_EXPORTS по умолчанию определен в Вашем проекте (см. Project Options -> С/С++ -> General->Preprocessor Definitions).
При этом все экспортируемые идентификаторы предваряются символом DLL_SPEC.
В случае определения TESTDLL_EXPORTS в проекте DLL_SPEC определяется как экспортируемый объект; в случае же отсутствия такого определения мы получим импортируемый объект.
Таким образом, один и тот же заголовочный файл может быть использован и в DLL-проекте, и в проекте, который будет использовать данную DLL! Без каких-либо изменений.
*/
 
#ifdef TESTDLL_EXPORTS
#define DLL_SPEC extern "C" __declspec(dllexport)
 
#else
#define DLL_SPEC extern "C" __declspec(dllimport)
#endif // TESTDLL_EXPORTS
 
/*
Каждый экспортируемый идентификатор предваряем __declspec(dllexport).
Эта директива позволяет линкеру определить, что данный идентификатор следует экспортировать из DLL. При этом создается специальный lib-файл, который содержит все экспортируемые идентификаторы из модуля. Также экспортируемые объекты заносятся в раздел экспорта DLL.
*/
 
DLL_SPEC void ShowSum(const int A, const int B);
 
#endif // __TESTDLL_H

Сохраняем. Запускаем. DLL мы подготовили. Теперь необходимо узнать, как же подключить DLL к проекту. Сделать это можно тремя способами. Рассмотрим их подробнее.

  • 2.2. Неявная загрузка.

При неявной загрузке DLL загружается (проецируется на адресное пространство вызывающего процесса) при его создании. Если при загрузке возникает ошибка — процесс останавливается и разрушается.

Для выполнения неявной загрузки приложению требуются:
— Заголовочный файл (*.h) с прототипами функций, описаниями классов и типов, которые используются в приложении.
— Библиотечный файл (*.lib), в котором описывается список экспортируемых из DLL функций (переменных), и их смещения, необходимые для правильной настройки вызовов функций.

В проекте TestVCL подключим наш заголовочный файл:

C++
1
#include "TestDLL.h"

Также, не забываем сделать Project -> Add To Project и добавить в проект TestDLL.lib
Далее, объявим прототип:

C++
1
void DLL_SPEC ShowSum(const int A, const int B);

Теперь осталось только вызвать функцию там, где это необходимо.
Чтобы убедится в том, что всё работает, прописываем в конструкторе формы:

Запускаем и смотрим на результат. Думаю, «3 + 2 = 5» всех устраивает.

  • 2.3. Явная загрузка.

Для того, чтобы выполнить явную загрузку программист должен попыхтеть, управляя DLL через функции WinAPI.
Наиболее часто рассматриваемые WinAPI функции:
DisableThreadLibraryCalls, FreeLibrary, FreeLibraryAndExitThread, GetModuleFileName, GetModuleHandle, GetProcAddress, LoadLibrary

При этом, основными функциями являются:
LoadLibrary[Ex] — позволяют загрузить DLL в адресное пространство хост-процесса.
FreeLibrary — функция, используемая для явной выгрузки DLL.
GetProcAddress — функция, позволяющая получить виртуальный адрес экспортируемой из DLL функции(или переменной) для ее последующего вызова.

Общая методика выглядит так:
1. Загрузить DLL с помощью LoadLibrary.
2. Получить указатели на необходимые объекты с помощью GetProcAddress.
3. Выгрузить DLL после завершения всех действий.

Теперь возникает вопрос, как же проверить теорию на практике?
Всё, что нужно, это добавить TestDLL.lib к проекту (также, как и при неявной загрузке).
А дальше, для проверки снова пишем в конструкторе формы:

C++
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// определяем тип "указатель на функцию"
typedef void __cdecl (*dll_func)(const int A, const int B);
 
dll_func pShowSum = NULL;
 
HMODULE hDLL = LoadLibrary("TestDLL.dll");
if (!hDLL) {
  ShowMessage("Невозможно загрузить TestDLL.dll");
  return;
}
 
// пытаемся найти в таблице экспорта необходимую нам функцию
pShowSum = (dll_func)GetProcAddress(hDLL, "_ShowSum"); // обратите внимание на название функции (объяснение будет ниже)
 
if (!pShowSum) {
  ShowMessage("Невозможно найти функцию ShowSum");
  return;
}
 
pShowSum(3,2);
 
FreeLibrary(hDLL);

И на экране снова красуется победная надпись «3 + 2 = 5»

Остался один неосвещенный вопрос. Почему же название функции «ShowSum» мы ищем в библиотеке с нижним подчёркиванием?

Виновато во всём декорирование имён.

Декорирование (или искажение, mangling) имен — это специфическое явление, присущее компиляторам языка C++, которое необходимо учитывать при разработке DLL на этом языке. Оно заключается в том, что компилятор С++ к имени функции всегда добавляет сокращенный список формальных параметров и тип возвращаемого значения.

Прототип функции Test(int); мог быть преобразован компилятором, например в ?Test@@YAKH@Z.
Естественно, такое декорирование нам вообще не по душе. Избавиться от него можно объявляя все экспортируемые функции с модификатором extern «C» — тогда компилятор не будет искажать имя функции.

Однако, как мы видим, нижние подчёркивание всё же добавилось.
Это один из нюансов среды C++ Builder. Однако, можно отучить его добавлять нижнее подчёркивание таким образом:
Project -> Options -> C++ Compiler -> Output -> Generate underscores on symbol names — перевести в состояние false.

  • 2.4. Отложенная загрузка.

Для чего же нужна отложенная загрузка?
Представьте себе ситуацию: вы написали приложение, использующее стандартные системные библиотеки вашей новой операционной системы, скажем, для проверки орфографии.
Даёте это приложение пользователю, который использует ОС более старой версии, чем у вас и в этой ОС нет функций для проверки орфографии. А пользователю это не сильно и надо. Приложение будет работать, пока не обратится к необходимой функции. То есть, фактически, DLL не нужна до обращения к определённой функции. Исключение отсутствия можно обработать и выдать пользователю предупреждение с просьбой обновить библиотеки своей ОС (и т.п.).

Использование отложенной загрузки DLL в C++ Builder мало отличается от неявной загрузки.
В проект добавляется заголовочный (*.h) файл с описаниями и библиотечный файл (*.lib).
Далее, переходим в Project -> Options -> C++ Linker -> Advanced -> Delay Load DLLs и вписываем название нашей библиотеки (TestDLL.dll).

Когда библиотека теряет свою необходимость её нужно явно выгрузить с помощью __FUnloadDelayLoadedDLL. В качестве параметра передаём имя DLL (с расширением + параметр регистрозависим).
Если вы используете многопоточные приложения — убедитесь, что все потоки завершили работу с DLL.

Примечание: Нельзя использовать отложенную загрузку для библиотек, имеющих секцию импорта (т.е. использующих другие библиотеки), а также Kernel32.dll и RTLDLL.dll (т.к. функции поддержки отложенной загрузки как раз и находятся в последней).

3. Заключение.

Данная статья даёт вам представление о возможных вариантах использования DLL в ваших проектах.
При внимательном ознакомлении с методами загрузки можно выбрать наиболее оптимальный вариант.

Подведя итоги можно выявить плюсы и минусы описанных методов:

Явная загрузка:

+ контроль и управление процессом жизни DLL.
управлением DLL занимается программист посредством WinAPI.

Неявная загрузка:

+ все заботы берет на себя компилятор и сборщик.
ресурсы заняты всё время жизни приложения.

Отложенная загрузка:

+ все заботы берет на себя компилятор и сборщик.
+ возможность использования приложения с не полностью совместимыми DLL.
необходимость усиленного контроля за многопоточными приложениями.

_________________________________________________
С уважением, Михаил (a.k.a MikeSoft)



58



С самого рождения (или чуть позже) операционная система Windows использовала библиотеки динамической компоновки DLL (Dynamic Link Library), в которых содержались реализации наиболее часто применяемых функций. Наследники Windows — NT и Windows 95, а также OS/2 — тоже зависят от библиотек DLL в плане обеспечения значительной части их функциональных возможностей.

Рассмотрим ряд аспектов создания и использования библиотек DLL:

  • как статически подключать библиотеки DLL;
  • как динамически загружать библиотеки DLL;
  • как создавать библиотеки DLL;
  • как создавать расширения МFC библиотек DLL.

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

Практически невозможно создать приложение Windows, в котором не использовались бы библиотеки DLL. В DLL содержатся все функции Win32 API и несчетное количество других функций операционных систем Win32.

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

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

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

Библиотеки импортирования

При статическом подключении DLL имя .lib-файла определяется среди прочих параметров редактора связей в командной строке или на вкладке «Link» диалогового окна «Project Settings» среды Developer Studio. Однако .lib-файл, используемый при неявном подключении DLL, — это не обычная статическая библиотека. Такие .lib-файлы называются библиотеками импортирования (import libraries). В них содержится не сам код библиотеки, а только ссылки на все функции, экспортируемые из файла DLL, в котором все и хранится. В результате библиотеки импортирования, как правило, имеют меньший размер, чем DLL-файлы. К способам их создания вернемся позднее. А сейчас рассмотрим другие вопросы, касающиеся неявного подключения динамических библиотек.

Согласование интерфейсов

При использовании собственных библиотек или библиотек независимых разработчиков придется обратить внимание на согласование вызова функции с ее прототипом.

Если бы мир был совершенен, то программистам не пришлось бы беспокоиться о согласовании интерфейсов функций при подключении библиотек — все они были бы одинаковыми. Однако мир далек от совершенства, и многие большие программы написаны с помощью различных библиотек без C++.

По умолчанию в Visual C++ интерфейсы функций согласуются по правилам C++. Это значит, что параметры заносятся в стек справа налево, вызывающая программа отвечает за их удаление из стека при выходе из функции и расширении ее имени. Расширение имен (name mangling) позволяет редактору связей различать перегруженные функции, т.е. функции с одинаковыми именами, но разными списками аргументов. Однако в старой библиотеке С функции с расширенными именами отсутствуют.

Хотя все остальные правила вызова функции в С идентичны правилам вызова функции в C++, в библиотеках С имена функций не расширяются. К ним только добавляется впереди символ подчеркивания (_).

Если необходимо подключить библиотеку на С к приложению на C++, все функции из этой библиотеки придется объявить как внешние в формате С:

    extern     "С" int MyOldCFunction(int myParam);

Объявления функций библиотеки обычно помещаются в файле заголовка этой библиотеки, хотя заголовки большинства библиотек С не рассчитаны на применение в проектах на C++. В этом случае необходимо создать копию файла заголовка и включить в нее модификатор extern «C» к объявлению всех используемых функций библиотеки. Модификатор extern «C» можно применить и к целому блоку, к которому с помощью директивы #tinclude подключен файл старого заголовка С. Таким образом, вместо модификации каждой функции в отдельности можно обойтись всего тремя строками:

    extern     "С" 
    {
        #include "MyCLib.h"
    }

В программах для старых версий Windows использовались также соглашения о вызове функций языка PASCAL для функций Windows API. В новых программах следует использовать модификатор winapi, преобразуемый в _stdcall. Хотя это и не стандартный интерфейс функций С или C++, но именно он используется для обращений к функциям Windows API. Однако обычно все это уже учтено в стандартных заголовках Windows.

Загрузка неявно подключаемой DLL

При запуске приложение пытается найти все файлы DLL, неявно подключенные к приложению, и поместить их в область оперативной памяти, занимаемую данным процессом. Поиск файлов DLL операционной системой осуществляется в следующей последовательности.

  • Каталог, в котором находится ЕХЕ-файл.
  • Текущий каталог процесса.
  • Системный каталог Windows.

Если библиотека DLL не обнаружена, приложение выводит диалоговое окно с сообщением о ее отсутствии и путях, по которым осуществлялся поиск. Затем процесс отключается.

Если нужная библиотека найдена, она помещается в оперативную память процесса, где и остается до его окончания. Теперь приложение может обращаться к функциям, содержащимся в DLL.

Динамическая загрузка и выгрузка DLL

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

Загрузка обычной DLL

Первое, что необходимо сделать при динамической загрузке DLL, — это поместить модуль библиотеки в память процесса. Данная операция выполняется с помощью функции ::LoadLibrary, имеющей единственный аргумент — имя загружаемого модуля. Соответствующий фрагмент программы должен выглядеть так:

    HINSTANCE hMyDll;
    ::
    if((hMyDll=::    LoadLibrary("MyDLL"))==NULL) { /* не удалось загрузить DLL */ }
    else { /* приложение имеет право пользоваться функциями DLL через hMyDll */ }

Стандартным расширением файла библиотеки Windows считает .dll, если не указать другое расширение. Если в имени файла указан и путь, то только он будет использоваться для поиска файла. В противном случае Windows будет искать файл по той же схеме, что и в случае неявно подключенных DLL, начиная с каталога, из которого загружается exe-файл, и продолжая в соответствии со значением PATH.

Когда Windows обнаружит файл, его полный путь будет сравнен с путем библиотек DLL, уже загруженных данным процессом. Если обнаружится тождество, вместо загрузки копии приложения возвращается дескриптор уже подключенной библиотеки.

Если файл обнаружен и библиотека успешно загрузилась, функция ::LoadLibrary возвращает ее дескриптор, который используется для доступа к функциям библиотеки.

Перед тем, как использовать функции библиотеки, необходимо получить их адрес. Для этого сначала следует воспользоваться директивой typedef для определения типа указателя на функцию и определить переменную этого нового типа, например:

        // тип PFN_MyFunction будет объявлять указатель на функцию, 
    // принимающую указатель на символьный буфер и выдающую значение типа int
    typedef int (WINAPI *PFN_MyFunction)(char *);
    ::
    PFN_MyFunction pfnMyFunction;

Затем следует получить дескриптор библиотеки, при помощи которого и определить адреса функций, например адрес функции с именем MyFunction:

    hMyDll=::LoadLibrary("MyDLL");
    pfnMyFunction=(PFN_MyFunction)::GetProcAddress(hMyDll,"MyFunction");
    ::
    int iCode=(*pfnMyFunction)("Hello");

Адрес функции определяется при помощи функции ::GetProcAddress, ей следует передать имя библиотеки и имя функции. Последнее должно передаваться в том виде, в котором экспортируется из DLL.

Можно также сослаться на функцию по порядковому номеру, по которому она экспортируется (при этом для создания библиотеки должен использоваться def-файл, об этом будет рассказано далее):

    pfnMyFunction=(PFN_MyFunction)::GetProcAddress(hMyDll,
                MAKEINTRESOURCE(1));

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

    ::FreeLibrary(hMyDll);

Загрузка MFC-расширений динамических библиотек

При загрузке MFC-расширений для DLL (подробно о которых рассказывается далее) вместо функций LoadLibraryи FreeLibrary используются функции AfxLoadLibrary и AfxFreeLibrary. Последние почти идентичны функциям Win32 API. Они лишь гарантируют дополнительно, что структуры MFC, инициализированные расширением DLL, не были запорчены другими потоками.

Ресурсы DLL

Динамическая загрузка применима и к ресурсам DLL, используемым MFC для загрузки стандартных ресурсов приложения. Для этого сначала необходимо вызвать функцию LoadLibrary и разместить DLL в памяти. Затем с помощью функции AfxSetResourceHandle нужно подготовить окно программы к приему ресурсов из вновь загруженной библиотеки. В противном случае ресурсы будут загружаться из файлов, подключенных к выполняемому файлу процесса. Такой подход удобен, если нужно использовать различные наборы ресурсов, например для разных языков.

Замечание. С помощью функции LoadLibrary можно также загружать в память исполняемые файлы (не запускать их на выполнение!). Дескриптор выполняемого модуля может затем использоваться при обращении к функциям FindResource и LoadResource для поиска и загрузки ресурсов приложения. Выгружают модули из памяти также при помощи функции FreeLibrary.

Пример обычной DLL и способов загрузки

Приведем исходный код динамически подключаемой библиотеки, которая называется MyDLL и содержит одну функцию MyFunction, которая просто выводит сообщение.

Сначала в заголовочном файле определяется макроконтстанта EXPORT. Использование этого ключевого слова при определении некоторой функции динамически подключаемой библиотеке позволяет сообщить компоновщику, что эта функция доступна для использования другими программами, в результате чего он заносит ее в библиотеку импорта. Кроме этого, такая функция, точно так же, как и оконная процедура, должна определяться с помощью константы CALLBACK:

MyDLL.h
    #define EXPORT extern "C" __declspec (dllexport)
    EXPORT int CALLBACK MyFunction(char *str);

Файл библиотеки также несколько отличается от обычных файлов на языке C для Windows. В нем вместо функции WinMain имеется функция DllMain. Эта функция используется для выполнения инициализации, о чем будет рассказано позже. Для того, чтобы библиотека осталась после ее загрузки в памяти, и можно было вызывать ее функции, необходимо, чтобы ее возвращаемым значением было TRUE:

MyDLL.c
    #include <windows.h>
    #include "MyDLL.h"

    int WINAPI DllMain(HINSTANCE hInstance, DWORD fdReason, PVOID pvReserved)
    {
        return TRUE;
    }
    EXPORT int CALLBACK MyFunction(char *str)
    {
        MessageBox(NULL,str,"Function from DLL",MB_OK);
        return 1;
    }

После трансляции и компоновки этих файлов появляется два файла — MyDLL.dll (сама динамически подключаемая библиотека) и MyDLL.lib (ее библиотека импорта).

Пример неявного подключения DLL приложением

Приведем теперь исходный код простого приложения, которое использует функцию MyFunction из библиотеки MyDLL.dll:

    #include <windows.h>
    #include "MyDLL.h"

    int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance,
            LPSTR lpCmdLine, int nCmdShow)
    {
        int iCode=MyFunction("Hello");
        return 0;
    }

Эта программа выглядит как обычная программ для Windows, чем она в сущности и является. Тем не менее, следует обратить внимание, что в исходный ее текст помимо вызова функции MyFunction из DLL-библиотеки включен и заголовочный файл этой библиотеки MyDLL.h. Также необходимо на этапе компоновки приложения подключить к нему библиотеку импорта MyDLL.lib (процесс неявного подключения DLL к исполняемому модулю).

Чрезвычайно важно понимать, что сам код функции MyFunction не включается в файл MyApp.exe. Вместо этого там просто имеется ссылка на файл MyDLL.dll и ссылка на функцию MyFunction, которая находится в этом файле. Файл MyApp.exe требует запуска файла MyDLL.dll.

Заголовочный файл MyDLL.h включен в файл с исходным текстом программы MyApp.c точно так же, как туда включен файл windows.h. Включение библиотеки импорта MyDLL.lib для компоновки аналогично включению туда всех библиотек импорта Windows. Когда программа MyApp.exe работает, она подключается к библиотеке MyDLL.dll точно так же, как ко всем стандартным динамически подключаемым библиотекам Windows.

Пример динамической загрузки DLL приложением

Приведем теперь полностью исходный код простого приложения, которое использует функцию MyFunction из библиотеки MyDLL.dll, используя динамическую загрузку библиотеки:

    #include <windows.h>
    typedef int (WINAPI *PFN_MyFunction)(char *);

    int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance,
            LPSTR lpCmdLine, int nCmdShow)
    {
        HINSTANCE hMyDll;
        if((hMyDll=LoadLibrary("MyDLL"))==NULL) return 1;

        PFN_MyFunction pfnMyFunction;
        pfnMyFunction=(PFN_MyFunction)GetProcAddress(hMyDll,"MyFunction");
        int iCode=(*pfnMyFunction)("Hello");

        FreeLibrary(hMyDll);
        return 0;
    }

Создание DLL

Теперь, познакомившись с принципами работы библиотек DLL в приложениях, рассмотрим способы их создания. При разработке приложении функции, к которым обращается несколько процессов, желательно размещать в DLL. Это позволяет более рационально использовать память в Windows.

Проще всего создать новый проект DLL с помощью мастера AppWizard, который автоматически выполняет многие операции. Для простых DLL, таких как рассмотренные в этой главе, необходимо выбрать тип проекта Win32 Dynamic-Link Library. Новому проекту будут присвоены все необходимые параметры для создания библиотеки DLL. Файлы исходных текстов придется добавлять к проекту вручную.

Если же планируется в полной мере использовать функциональные возможности MFC, такие как документы и представления, или намерены создать сервер автоматизации OLE, лучше выбрать тип проекта MFC AppWizard (dll). В этом случае, помимо присвоения проекту параметров для подключения динамических библиотек, мастер проделает некоторую дополнительную работу. В проект будут добавлены необходимые ссылки на библиотеки MFC и файлы исходных текстов, содержащие описание и реализацию в библиотеке DLL объекта класса приложения, производного от CWinApp.

Иногда удобно сначала создать проект типа MFC AppWizard (dll) в качестве тестового приложения, а затем — библиотеку DLL в виде его составной части. В результате DLL в случае необходимости будет создаваться автоматически.

Функция DllMain

Большинство библиотек DLL — просто коллекции практически независимых друг от друга функций, экспортируемых в приложения и используемых в них. Кроме функций, предназначенных для экспортирования, в каждой библиотеке DLL есть функция DllMain. Эта функция предназначена для инициализации и очистки DLL. Она пришла на смену функциям LibMain и WEP, применявшимся в предыдущих версиях Windows. Структура простейшей функции DllMain может выглядеть, например, так:

    BOOL WINAPI DllMain (HANDLE hInst,DWORD dwReason, LPVOID IpReserved)
    {
        BOOL bAllWentWell=TRUE;
        switch (dwReason) 
        {
            case DLL_PROCESS_ATTACH:     // Инициализация процесса. 
                break;
            case DLL_THREAD_ATTACH:     // Инициализация потока.
                break;
            case DLL_THREAD_DETACH:     // Очистка структур потока.
                break;
            case DLL_PROCESS_DETACH:     // Очистка структур процесса.
                break;
        }
        if(bAllWentWell)     return TRUE;
        else            return FALSE;
    }

Функция DllMain вызывается в нескольких случаях. Причина ее вызова определяется параметром dwReason, который может принимать одно из следующих значений.

При первой загрузке библиотеки DLL процессом вызывается функция DllMain с dwReason, равным DLL_PROCESS_ATTACH. Каждый раз при создании процессом нового потока DllMainO вызывается с dwReason, равным DLL_THREAD_ATTACH (кроме первого потока, потому что в этом случае dwReason равен DLL_PROCESS_ATTACH).

По окончании работы процесса с DLL функция DllMain вызывается с параметром dwReason, равным DLL_PROCESS_DETACH. При уничтожении потока (кроме первого) dwReason будет равен DLL_THREAD_DETACH.

Все операции по инициализации и очистке для процессов и потоков, в которых нуждается DLL, необходимо выполнять на основании значения dwReason, как было показано в предыдущем примере. Инициализация процессов обычно ограничивается выделением ресурсов, совместно используемых потоками, в частности загрузкой разделяемых файлов и инициализацией библиотек. Инициализация потоков применяется для настройки режимов, свойственных только данному потоку, например для инициализации локальной памяти.

В состав DLL могут входить ресурсы, не принадлежащие вызывающему эту библиотеку приложению. Если функции DLL работают с ресурсами DLL, было бы, очевидно, полезно сохранить где-нибудь в укромном месте дескриптор hInst и использовать его при загрузке ресурсов из DLL. Указатель IpReserved зарезервирован для внутреннего использования Windows. Следовательно, приложение не должно претендовать на него. Можно лишь проверить его значение. Если библиотека DLL была загружена динамически, оно будет равно NULL. При статической загрузке этот указатель будет ненулевым.

В случае успешного завершения функция DllMain должна возвращать TRUE. В случае возникновения ошибки возвращается FALSE, и дальнейшие действия прекращаются.

Замечание. Если не написать собственной функции DllMain(), компилятор подключит стандартную версию, которая просто возвращает TRUE.

Экспортирование функций из DLL

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

Метод __declspec (dllexport)

Можно экспортировать функцию из DLL, поставив в начале ее описания модификатор __declspec (dllexport) . Кроме того, в состав MFC входит несколько макросов, определяющих __declspec (dllexport), в том числе AFX_CLASS_EXPORT, AFX_DATA_EXPORT и AFX_API_EXPORT.

Метод __declspec применяется не так часто, как второй метод, работающий с файлами определения модуля (.def), и позволяет лучше управлять процессом экспортирования.

Файлы определения модуля

Синтаксис файлов с расширением .def в Visual C++ достаточно прямолинеен, главным образом потому, что сложные параметры, использовавшиеся в ранних версиях Windows, в Win32 более не применяются. Как станет ясно из следующего простого примера, .def-файл содержит имя и описание библиотеки, а также список экспортируемых функций:

MyDLL.def
    LIBRARY         "MyDLL"
        DESCRIPTION    'MyDLL - пример DLL-библиотеки'

    EXPORTS
        MyFunction     @1

В строке экспорта функции можно указать ее порядковый номер, поставив перед ним символ @. Этот номер будет затем использоваться при обращении к GetProcAddress (). На самом деле компилятор присваивает порядковые номера всем экспортируемым объектам. Однако способ, которым он это делает, отчасти непредсказуем, если не присвоить эти номера явно.

В строке экспорта можно использовать параметр NONAME. Он запрещает компилятору включать имя функции в таблицу экспортирования DLL:

        MyFunction     @1 NONAME

Иногда это позволяет сэкономить много места в файле DLL. Приложения, использующие библиотеку импортирования для неявного подключения DLL, не «заметят» разницы, поскольку при неявном подключении порядковые номера используются автоматически. Приложениям, загружающим библиотеки DLL динамически, потребуется передавать в GetProcAddress порядковый номер, а не имя функции.

При использовании вышеприведенного def-файл описания экспортируемых функций DLL-библиотеки может быть,например, не таким:

    #define EXPORT extern "C" __declspec (dllexport)
    EXPORT int CALLBACK MyFunction(char *str);
    a таким:
    extern "C" int CALLBACK MyFunction(char *str);

Экспортирование классов

Создание .def-файла для экспортирования даже простых классов из динамической библиотеки может оказаться довольно сложным делом. Понадобится явно экспортировать каждую функцию, которая может быть использована внешним приложением.

Если взглянуть на реализованный в классе файл распределения памяти, в нем можно заметить некоторые весьма необычные функции. Оказывается, здесь есть неявные конструкторы и деструкторы, функции, объявленные в макросах MFC, в частности _DECLARE_MESSAGE_MAP, а также функции, которые написанные программистом.

Хотя можно экспортировать каждую из этих функций в отдельности, есть более простой способ. Если в объявлении класса воспользоваться макромодификатором AFX_CLASS_EXPORT, компилятор сам позаботится об экспортировании необходимых функций, позволяющих приложению использовать класс, содержащийся в DLL.

Память DLL

В отличие от статических библиотек, которые, по существу, становятся частью кода приложения, библиотеки динамической компоновки в 16-разрядных версиях Windows работали с памятью несколько иначе. Под управлением Win 16 память DLL размещалась вне адресного пространства задачи. Размещение динамических библиотек в глобальной памяти обеспечивало возможность совместного использования их различными задачами.

В Win32 библиотека DLL располагается в области памяти загружающего ее процесса. Каждому процессу предоставляется отдельная копия «глобальной» памяти DLL, которая реинициализируется каждый раз, когда ее загружает новый процесс. Это означает, что динамическая библиотека не может использоваться совместно, в общей памяти, как это было в Winl6.

И все же, выполнив ряд замысловатых манипуляций над сегментом данных DLL, можно создать общую область памяти для всех процессов, использующих данную библиотеку.

Допустим, имеется массив целых чисел, который должен использоваться всеми процессами, загружающими данную DLL. Это можно запрограммировать следующим образом:

    #pragma data_seg(".myseg")
    int sharedlnts[10] ;
        // другие переменные общего пользования
    #pragma data_seg()
    #pragma comment(lib, "msvcrt" "-SECTION:.myseg,rws");

Все переменные, объявленные между директивами #pragma data_seg(), размещаются в сегменте .myseg. Директива #pragma comment () — не обычный комментарий. Она дает указание библиотеке выполняющей системы С пометить новый раздел как разрешенный для чтения, записи и совместного доступа.

Полная компиляция DLL

Если проект динамической библиотеки создан с помощью AppWizard и .def-файл модифицирован соответствующим образом — этого достаточно. Если же файлы проекта создаются вручную или другими способами без помощи AppWizard, в командную строку редактора связей следует включить параметр /DLL. В результате вместо автономного выполняемого файла будет создана библиотека DLL.

Если в .def-файле есть строка LIBRART, указывать явно параметр /DLL в командной строке редактора связей не нужно.

Для MFC предусмотрен ряд особых режимов, касающихся использования динамической библиотекой библиотек MFC. Этому вопросу посвящен следующий раздел.

DLL и MFC

Программист не обязан использовать MFC при создании динамических библиотек. Однако использование MFC открывает ряд очень важных возможностей.

Имеется два уровня использования структуры MFC в DLL. Первый из них — это обычная динамическая библиотека на основе MFC, MFC DLL (regular MFC DLL). Она может использовать MFC, но не может передавать указатели на объекты MFC между DLL и приложениями. Второй уровень реализован в динамических расширениях MFC (MFC extensions DLL). Использование этого вида динамических библиотек требует некоторых дополнительных усилий по настройке, но позволяет свободно обмениваться указателями на объекты MFC между DLL и приложением.

Обычные MFC DLL

Обычные MFC DLL позволяют применять MFC в динамических библиотеках. При этом приложения, обращающиеся к таким библиотекам, не обязательно должны быть построены на основе MFC. В обычных DLL можно использовать MFC любым способом, в том числе создавая в DLL новые классы на базе классов MFC и экспортируя их в приложения.

Однако обычные DLL не могут обмениваться с приложениями указателями на классы, производные от MFC.

Если приложению необходимо обмениваться с DLL указателями на объекты классов MFC или их производных, нужно использовать расширение DLL, описанное в следующем разделе.

Архитектура обычных DLL рассчитана на использование другими средами программирования, такими как Visual Basic и PowerBuilder.

При создании обычной библиотеки MFC DLL с помощью AppWizard выбирается новый проект типа MFC AppWizard (dll). В первом диалоговом окне мастера приложений необходимо выбрать один из режимов для обычных динамических библиотек: «Regular DLL with MFC statistically linked» или «Regular DLL using shared MFC DLL». Первый предусматривает статическое, а второй — динамическое подключение библиотек MFC. Впоследствии режим подключения MFC к DLL можно будет изменить с помощью комбинированного списка на вкладке «General» диалогового окна «Project settings».

Управление информацией о состоянии MFC

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

    AFX_MANAGE_STATE(AfxGetStaticModuleState()) ;

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

Динамические расширения MFC

MFC позволяет создавать такие библиотеки DLL, которые воспринимаются приложениями не как набор отдельных функций, а как расширения MFC. С помощью данного вида DLL можно создавать новые классы, производные от классов MFC, и использовать их в своих приложениях.

Чтобы обеспечить возможность свободного обмена указателями на объекты MFC между приложением и DLL, нужно создать динамическое расширение MFC. DLL этого типа подключаются к динамическим библиотекам MFC так же, как и любые приложения, использующие динамическое расширение MFC.

Чтобы создать новое динамическое расширение MFC, проще всего, воспользовавшись мастером приложении, присвоить проекту тип MFC AppWizard (dll) и на шаге 1 включить режим «MFC Extension DLL». В результате новому проекту будут присвоены все необходимые атрибуты динамического расширения MFC. Кроме того, будет создана функция DllMain для DLL, выполняющая ряд специфических операций по инициализации расширения DLL. Следует обратить внимание, что динамические библиотеки данного типа не содержат и не должны содержать объектов, производных от CWinApp.

Инициализация динамических расширений

Чтобы «вписаться» в структуру MFC, динамические расширения MFC требуют дополнительной начальной настройки. Соответствующие операции выполняются функцией DllMain. Рассмотрим пример этой функции, созданный мастером AppWizard.

    static AFX_EXTENSION_MODULE MyExtDLL = { NULL, NULL } ;
    extern "C" int APIENTRY
    DllMain(HINSTANCE hinstance, DWORD dwReason, LPVOID IpReserved)
    {
        if (dwReason == DLL_PROCESS_ATTACH)
        {
            TRACED("MYEXT.DLL Initializing!n") ;
            // Extension DLL one-time initialization
            AfxInitExtensionModule(MyExtDLL, hinstance) ;

            // Insert this DLL into the resource chain
            new CDynLinkLibrary(MyExtDLL);
        }
        else if (dwReason == DLL_PROCESS_DETACH)
        {
            TRACED("MYEXT.DLL Terminating!n") ;
        }
        return 1; // ok
    }

Самой важной частью этой функции является вызов AfxInitExtensionModule. Это инициализация динамической библиотеки, позволяющая ей корректно работать в составе структуры MFC. Аргументами данной функции являются передаваемый в DllMain дескриптор библиотеки DLL и структура AFX_EXTENSION_MODULE, содержащая информацию о подключаемой к MFC динамической библиотеке.

Нет необходимости инициализировать структуру AFX_EXTENSION_MODULE явно. Однако объявить ее нужно обязательно. Инициализацией же займется конструктор CDynLinkLibrary. В DLL необходимо создать класс CDynLinkLibrary. Его конструктор не только будет инициализировать структуру AFX_EXTENSION_MODULE, но и добавит новую библиотеку в список DLL, с которыми может работать MFC.

Загрузка динамических расширений MFC

Начиная с версии 4.0 MFC позволяет динамически загружать и выгружать DLL, в том числе и расширения. Для корректного выполнения этих операций над создаваемой DLL в ее функцию DllMain в момент отключения от процесса необходимо добавить вызов AfxTermExtensionModule. Последней функции в качестве параметра передается уже использовавшаяся выше структура AFX_EXTENSION_MODULE. Для этого в текст DllMain нужно добавить следующие строки.

    if(dwReason == DLL_PROCESS_DETACH) 
    {
        AfxTermExtensionModule(MyExtDLL);
    }

Кроме того, следует помнить, что новая библиотека DLL является динамическим расширением и должна загружаться и выгружаться динамически, с помощью функций AfxLoadLibrary и AfxFreeLibrary,а не LoadLibrary и FreeLibrary.

Экспортирование функций из динамических расширений

Рассмотрим теперь, как осуществляется экспортирование в приложение функций и классов из динамического расширения. Хотя добавить в DEF-файл все расширенные имена можно и вручную, лучше использовать модификаторы для объявлений экспортируемых классов и функций, такие как AFX_EXT_CLASS и AFX_EXT_API,например:

    class AFX_EXT_CLASS CMyClass : public CObject
    (
        // Your class declaration
    }
    void AFX_EXT_API MyFunc() ;

Понравилась статья? Поделить с друзьями:
  • Западноевропейские компании имеют общие с американскими фирмами черты в использовании принципа
  • Заполнение профессиональных карт педагогов по итогам работы за определенный период времени это
  • Заработная плата сотрудника страховой компании в рублях рассчитывается по формуле c 8000 450 n
  • Затраты компании которые не меняются в зависимости от изменений объема производства называются
  • Земельный налог государственная пошлина налог на доходы физических лиц налог на игорный бизнес