Добавил:
Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:
Создание эффективных приложений для Windows Джеффри Рихтер 2004 (Книга).pdf
Скачиваний:
348
Добавлен:
15.06.2014
Размер:
8.44 Mб
Скачать

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

Создав DLL- и ЕХЕ-модули, приложение можно запустить. При его запуске загрузчик операционной системы выполняет следующие операции

1.Загрузчик операционной системы создает виртуальное адресное пространство для нового процесса и проецирует па пего исполняемый модуль.

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

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

Создание DLL-модуля

Создавая DT,T., Вы создаете набор функций, которые могут быть вызваны из ЕХЕ-модуля (или другой DLL), DLL может экспортировать переменные, функции или C++классы в другие модули. На самом дслс я бы не советовал экспортировать переменные, потому что это снижает уровень абстрагирования Вашего кода и усложняет его поддержку. Кроме того, С++-классы можно экспортировать, только если импортирующие их модули транслируются тем же компилятором Так что избегайте экспорта С++-классов, если Вы не уверены, что разработчики ЕХЕ-модулей будут пользоваться тем же компилятором.

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

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

Вот пример единою залоловочного файла, включаемого в исходный код DLL- и ЕХЕмодулей

Модуль MyLib.h

Этот заголовочный файл надо включать в самое начало исходных файлов Вашей DLL следующим образом

MyLibFile1.cpp

При компиляции исходного файла DLL, показанного на предыдущем листинге, MYLIBAPI определяется как __declspec(dllexport) до включения заголовочного файла MyLib.h Такой модификатор означает, что данная переменная, функция или С++-класс экспортируется из DLL Заметьте, что идентификатор MYLIBAPI помещен и заголовочный файл до определения экспортируемой переменной или функции

Также обратите внимание, что в файле MyLibFilel.cpp перед экспортируемой переменной или функцией не ставится идентификатор MYITBAPI Он здесь не нуженпроанализировав заголовочный файл, компилятор запоминает, какие переменные и функции являются экспортируемыми.

Идентификатор MYLIRAPT включает extern Пользуйтесь этим модификатором голько в коде на С++, но ни в коем случае не в коде на стандартном С. Обычно компиляторы С++ искажают (mangle) имена функций и переменных, что может приводить к серьезным ошибкам при компоновке Представьте, что DLL написана на С++, а исполняемый код — на стандартном С. При сборке DLL имя функции будет искажено, но при сборке исполняемого модуля — нет. Пытаясь скомпоновать исполняемый модуль, компоновщик сообщит об ошибке исполняемый модуль обращается к несуществующему идентификатору Модификатор extern не дает компилятору искажать имена переменных или функций, и они становятся доступными исполняемым модулям, написанным на С, С++ или любом другом языке программирования

Теперь Вы знаете, как используется заголовочный файл в исходных файлах DLL А как насчет исходных файлов ЕХЕ-модули? В них MYLIBAPI определять не надо: включая заголовочный файл, Вы определяете этот идентификатор как __declspec(dllimport), и при компиляции исходного кода ЕХЕ-модуля компилятор поймет, что переменные и функции импортируются из DLL

Просмотрев стандартные заголовочные файлы Windows (например, WinBase.h), Вы обнаружите, что практически тот же подход исповедует и Microsoft

Что такое экспорт

В предыдущем разделе я упомянул о модификаторе __declspec(dllexport) Если он указан перед переменной, прототипом функции или С++-классом, компилятор Microsoft С/С++ встраивает в конечный OBJ-файл дополнительную информацию Она понадобится компоновщику при сборке DLL из OBJ-файлов

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

в котором содержится список (в алфавитном порядке) идентификаторов экспортируемых функций, псрсмснных и классов. Туда же помещается относительный виртуальный адрес (relative virtual address, RVA) каждого идентификатора внутри DLLмодуля.

Воспользовавшись утилитой DumpBin.exe (с ключом -exports) из состава Microsoft Visual Studio, мы можем увидеть содержимое раздела экспорта в DLL-модуле. Вот лишь небольшой фрагмент такого раздела для Kernel32.dll:

C:\WINNl\SYSiEM32>DUMPBIN -exports Kemel32.Dll

Microsoft (R) COFF Binary File Dumper Version 6.00.8168 Copyright (C) Microsoft Corp 1992-1998 All rights reserved

Dump of file kernel32.dll

File Type DLL

Section contains the following exports for KERNEL32.dll 0 characteristics

36DB3213 time date stamp Mon Mar 01 16 34:27 1999 0 00 version

1 ordinal base 829 number of functions 829 number of names ordinal hint RVA name

1 0 0001A3C6 AddAtomA

2 1 0001A367 AddAtomW

3 2 0003F7C4 AddConsoleAliasA

4 3 0003F78D AddConsoleAliasW

5 4 0004085C AllocConsole

6 5 0002C91D AllocateUserPhysicalPages

7 6 00005953 AreFileApisANSI

8 7 0003F1AO AssignProcessToJobObject

9 8 00021372 BackupRead

10 9 000215CE BackupSeek

11 A OQ021F21 BackupWrite

...

828 33B 00003200 lstrlenA

829 33C 000040D5 lstrlenW Summary

3000 .data

4000 .reloc 4DOOO .rsrc 59000 .text

Как видите, идентификаторы расположены по алфавиту; в графе RVA указывается смещение в образе DLL-файла, по которому можно найти экспортируемый идентификатор Значения в графе ordinal предназначены для обратной совместимости с исходным кодом, написанным для 16-разрядной Windows, — применять их в современных приложениях не следует. Данные из графы hint используются системой и для нас интереса не представляют

NOTE:

Многие разработчики — особенно те, у кого большой опыт программирования для 16-разрядной Windows, — привыкли экспортировать функции из DLL, присваивая им порядковые номера Но Microsoft не публикует такую информацию по системным DLL и требует связывать EXEили DLL-файлы с Windowsфункциями только по именам. Используя порядковый номер, Вы рискуете тем, что Ваша программа не будет работать в других версиях Windows.

Кстати, именно это и случилось со мной. В журнале MicrosoftSystemsJournal я опубликовал программу, построенную на применении порядковых номеров. В Windows NT 3.1 программа работала прекрасно, но сбоила при запуске в Windows NT 3.5. Чтобы избавиться от сбоев, пришлось заменить порядковые номера именами функций, и все встало на свои места.

Я поинтересовался, почему Microsoft отказывается от порядковых номеров, и получил такой ответ: «Мы (Microsoft) считаем, что РЕ-формат позволяет сочетать преимущества порядковых номеров (быстрый поиск) с гибкостью импорта по именам. Учтите и то, что в любой момент в API могут появиться новые функции. А с порядковыми номерами в большом проекте работать очень трудно — тем более, что такие проекты многократно пересматриваются

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

Создание DLL для использования с другими средствами разработки (отличными от Visual C++)

Если Вы используете Visual С++ для сборки как DLL, так и обращающегося к ней EXEфайла, то все скязанное ранее справедливо, и Вы можете спокойно пропустить этот раздел. Но если Вы создаете DLL на Visual С++, а ЕХЕ-файл — с помощью средств разработки от других поставщиков, Вам не миновать дополнительной работы.

Я уже упоминал о том, как применячь модификатор extern при «смешанном» программировании на С и С++ Кроме того, я говорил, что из-за искажения имен нужно применять один и тот же компилятор Даже при программировании на стандартном С инструментальные средства от разных поставщиков создают проблемы Дело в том, что компилятор Microsoft С, экспортируя С-функцию, искажает eе имя, даже если Вы вообще не пользуетесь С++ Это происходит, только когда Ваша функция экспортируется по соглашению __stdcall. (Увы, это самое популярное соглашение ) Тогда компилятор Microsoft искажает имя С-функции. впереди ставит знак подчеркивания, а к концу добавляет суффикс, состоящий из символа @ и числа байтов, передаваемых функции в качестве параметров. Например, следующая функция экспортируется в таблицу экспорта

DLL как _MyFunc@8:

__declspec(dllexport) LONG __stdcall MyFunc(int a, int b);