Рихтер Дж., Назар К. - Windows via C C++. Программирование на языке Visual C++ - 2009
.pdf620 Часть IV. Динамически подключаемые библиотеки
дняшний день метод. (Windows поддерживает и явное связывание, но об этом — в главе 20.)
Сборка DLL-модуля |
Сборка ЕХЕ-модуля |
|||
1) |
Заголовок с экспортируемыми прототипа- |
6) |
Заголовок с экспортируемыми прототипа- |
|
|
ми, структурами и идентификаторами. |
|
ми, структурами и идентификаторами. |
|
2) |
Файлы с исходным кодом на C/C++ с реали- |
7) |
Файлы с исходным кодом на C/C++, ссы- |
|
|
зацией экспортируемых функций и пере- |
|
лающимся на импортированные функции и |
|
|
менных. |
|
переменные. |
|
3) |
Компилятор генерирует .obj-файл для каж- |
8) |
Компилятор генерирует .obj-файл для каж- |
|
|
дого из файлов из файлов с кодом на C/C++. |
|
дого из файлов из файлов с кодом на |
|
4) |
Компоновщик генерирует DLL из .obj- |
|
C/C++. |
|
|
файла. |
9) |
Компоновщик комбинирует .obj-модули и |
|
5) |
Компоновщик также генерирует .lib-файл, |
|
разрешает |
ссылки на импортированные |
|
ссылки на импортированные функции и пе- |
|
функции и переменные, используя .lib- |
|
|
ременные, если найдена хотя бы одна экс- |
|
файл. В результате получается .ехе-файл (с |
|
|
портируемая функция или переменная. |
|
таблицей |
импортированных функций — |
|
|
|
списком необходимых DLL и идентифика- |
|
|
|
|
торов). |
|
Рис. 19-1. Так DLL создается и неявно связывается с приложением
Как видно на рис. 19-1, когда некий модуль (например, ЕХЕ) обращается к функциям и переменным, находящимся в DLL, в этом процессе участвует несколько файлов и компонентов. Для упрощения будем считать, что исполняемый модуль (ЕХЕ) импортирует функции и переменные из DLL, a DLL-модули, наоборот, экспортируют их в исполняемый модуль. Но учтите, что DLL может (и это не редкость) импортировать функции и переменные из других DLL.
Глава 19. DLL - основы.docx 621
Собирая исполняемый модуль, который импортирует функции и переменные из DLL, вы должны сначала создать эту DLL. А для этого нужно следующее.
1.Прежде всего вы должны подготовить заголовочный файл с прототипами функции, структурами и идентификаторами, экспортируемыми из DLL. Этот файл включается в исходный код всех модулей вашей DLL. Как вы потом увидите, этот же файл понадобится и при сборке исполняемого модуля (или модулей), который использует функции и переменные из вашей DLL.
2.Вы пишете на C/C++ модуль (или модули) исходного кода с телами функций и определениями переменных, которые должны находиться в DLL. Так как эти модули исходного кода не нужны для сборки исполняемого модуля, они могут остаться коммерческой тайной компании-разработчика
3.Компилятор преобразует исходный код модулей DLL в OBJ-файлы (по одному на каждый модуль).
4.Компоновщик собирает все OBJ-модули в единый загрузочный DLL-модуль, в который в конечном итоге помещаются двоичный код и переменные (глобальные и статические), относящиеся к данной DLL. Этот файл потребуется при компиляции исполняемого модуля.
5.Если компоновщик обнаружит, что DLL экспортирует хотя бы одну переменную или функцию, то создаст и LIB-файл. Этот файл совсем крошечный, поскольку в нем нет ничего, кроме списка символьных имен функций и переменных, экспортируемых из DLL. Этот LIB-файл тоже понадобится при компиляции ЕХЕ-файла.
Создав DLL, можно перейти к сборке исполняемого модуля.
6.Во все модули исходного кода, где есть ссылки на внешние функции, переменные, структуры данных или идентификаторы, надо включить заголовочный файл, предоставленный разработчиком DLL.
7.Вы пишете на C/C++ модуль (или модули) исходного кода с телами функций и определениями переменных, которые должны находиться в ЕХЕ-файле. Естественно, ничто не мешает вам ссылаться на функции и переменные, определенные в заголовочном файле DLL-модуля.
8.Компилятор преобразует исходный код модулей ЕХЕ в OBJ-файлы (по одному на каждый модуль).
9.Компоновщик собирает все OBJ-модули в единый загрузочный ЕХЕ-мо-дуль, в который в конечном итоге помешаются двоичный код и переменные (глобальные и статические), относящиеся к данному ЕХЕ. В нем так же создается раздел импорта, где перечисляются имена всех необходимых DLL-моделей (информацию о разделах см. в главе 17). Кроме того, для каждой DLL в этом разделе указывается, на какие символьные имена функций и переменных ссылается двоичный код исполняемого файла. Эти сведения потребуются загрузчику операционной системы, а как именно он ими пользуется — мы узнаем чуть позже.
622 Часть IV. Динамически подключаемые библиотеки
Создав DLL- и ЕХЕ-модули, приложение можно запустить. При его запуске загрузчик операционной системы выполняет следующие операции.
10.Загрузчик операционной системы создает виртуальное адресное пространство для нового процесса и проецирует на него исполняемый модуль.
11.Далее загрузчик анализирует раздел импорта, находит все необходимые DLLмодули и тоже проецирует на адресное пространство процесса. Заметьте, что DLL может импортировать функции и переменные их другой DLL, а значит, у нее может быть собственный раздел импорта. Заканчивая подготовку процесса к работе, загрузчик просматривает раздел импорта каждого модуля и проецирует все требуемые DLL-модули на адресное пространство этого процесса. Как видите, на инициализацию процесса может уйти довольно длительное время. После отображения ЕХЕ- и всех DLL-модулей на адресное пространство процесса его первичный поток готов к выполнению, и приложение может начать работу. Далее мы подробно рассмотрим, как именно это происходит.
Создание DLL-модуля
Создавая DLL, вы создаете набор функций, которые могут быть вызваны из ЕХЕмодуля (или другой DLL). DLL может экспортировать переменные, функции или С++-классы в другие модули. На самом деле я бы не советовал экспортировать переменные, потому что это снижает уровень абстрагирования вашего кода и усложняет его поддержку. Кроме того, С++-классы можно экспортировать, только если импортирующие их модули транслируются тем же компилятором. Так что избегайте экспорта С++-классов, если вы не уверены, что разработчики ЕХЕмодулей будут пользоваться тем же компилятором.
При разработке DLL вы сначала создаете заголовочный файл, в котором содержатся экспортируемые из нее переменные (типы и имена) и функции (прототипы и имена). В этом же файле надо определить все идентификаторы и структуры данных, используемые экспортируемыми функциями и переменными. Заголовочный файл включается во все модули исходного кода вашей DLL. Более того, вы должны поставлять его вместе со своей DLL, чтобы другие разработчики могли включать его в свои модули исходного кода, которые импортируют ваши функции или переменные. Единый заголовочный файл, используемый при сборке DLL и любых исполняемых модулей, существенно облегчает поддержку приложения.
Вот пример единого заголовочного файла, включаемого в исходный код DLL- и ЕХЕ-модулей.
Глава 19. DLL - основы.docx 623
/***********************************************************
Module: MyLlb.h /***********************************************************
#ifdef MYLIBAPI
//MYLIBAPI должен быть определен во всех модулях исходного кода DLL
//до включения этого файла
//здесь размещаются все экспортируемые функции и переменные
#else
//этот заголовочный файл включается в исходный код ЕХЕ-файла;
//указываем, что все функции и переменные импортируются
#define MYLIBAPI extern "С" __declspec(dllimport)
#endif
////////////////////////////////////////////////////////////
// здесь определяются все структуры данных и идентификаторы (символы)
////////////////////////////////////////////////////////////
//Здесь определяются экспортируемые переменные.
//Примечание: избегайте экспорта переменных.
MYLIBAPI int g_nResult;
////////////////////////////////////////////////////////////
// здесь определяются прототипы экспортируемых функций
MYLIBAPI int Add(int nLeft, int nRight);
/////////////////// End of File ////////////////////////////
Этот заголовочный файл надо включать в самое начало исходных файлов вашей DLL следующим образом.
624 Часть IV. Динамически подключаемые библиотеки
/************************************************************
Module: MyLibFilel.cpp
************************************************************/
//сюда включаются стандартные заголовочные файлы Windows и библиотеки С
#include <windows.h>
//этот файл исходного кода DLL экспортирует функции и переменные
#define MYLIBAPI extern "С" __declspec(dllexport)
//включаем экспортируемые структуры данных, идентификаторы, функции
//и переменные #include "MyLib.h"
/////////////////////////////////////////////////////////////////////
// здесь размещается исходный код этой DLL int g_nResult;
int Add(int nLeft, int nRight) { g_nResult = nLeft + nRight; return(g_nResult);
}
///////////////////////////// End of File ///////////////////////////
При компиляции исходного файла DLL, показанного на предыдущем листинге, MYLIBAPI определяется как__declspec(dllexport) до включения заголовочного файла MyLib.h. Такой модификатор означает, что данная переменная, функция или С++-класс экспортируется из DLL. Заметьте, что идентификатор MYLIBAPI помещен в заголовочный файл до определения экспортируемой переменной или функции.
Также обратите внимание, что в файле MyLibFilel.cpp перед экспортируемой переменной или функцией не ставится идентификатор MYLIBAPI. Он здесь не нужен: проанализировав заголовочный файл, компилятор запоминает, какие переменные и функции являются экспортируемыми.
Идентификатор MYLIBAPI включает extern. Пользуйтесь этим модификатором только в коде на C++, но ни в коем случае не в коде на стандартном С. Обычно компиляторы C++ искажают (mangle) имена функций и переменных, что может приводить к серьезным ошибкам при компоновке. Представьте, что DLL написана на C++, а исполняемый код — на стандартном С. При сборке DLL имя функции будет искажено, но при сборке исполняемого модуля — нет. Пытаясь скомпоновать исполняемый модуль, компоновщик сообщит об ошибке: исполняемый модуль обращается к несуществующему идентификатору. Модификатор extern не дает компилятору иска-
Глава 19. DLL - основы.docx 625
жать имена переменных или функций, и они становятся доступными исполняемым модулям, написанным на С, C++ или любом другом языке программирования.
Теперь вы знаете, как используется заголовочный файл в исходных файлах DLL. А как насчет исходных файлов ЕХЕ-модуля? В них MYLIBAPI определять не надо: включая заголовочный файл, вы определяете этот идентификатор как _declspec(dllimport), и при компиляции исходного кода ЕХЕ-модуля компилятор поймет, что переменные и функции импортируются из DLL.
Просмотрев стандартные заголовочные файлы Windows (например, WinBase.h), вы обнаружите, что практически тот же подход исповедует и Майкрософт.
Что такое экспорт
В предыдущем разделе я упомянул о модификаторе __declspec(dllexport). Если он указан перед переменной, прототипом функции или С++-классом, компилятор Microsoft C/C++ встраивает в конечный OBJ-файл дополнительную информацию. Она понадобится компоновщику при сборке DLL из OBJ-файлов.
Обнаружив такую информацию, компоновщик создает LIB-файл со списком идентификаторов, экспортируемых из DLL. Этот LIB-файл нужен при сборке любого ЕХЕ-модуля, ссылающегося на такие идентификаторы. Компоновщик также вставляет в конечный DLL-файл таблицу экспортируемых идентификаторов — раздел экспорта, в котором содержится список (в алфавитном порядке) идентификаторов экспортируемых функций, переменных и классов. Туда же помещается
относительный виртуальный адрес (relative virtual address, RVA) каждого иден-
тификатора внутри DLL-модуля.
Воспользовавшись утилитой DumpBin.exe (с ключом -exports) из состава Microsoft Visual Studio, мы можем увидеть содержимое раздела экспорта в DLLмодуле. Вот лишь небольшой фрагмент такого раздела для Kernel32.dll:
C:\Windows\System32>DUMPBIN -exports Kernel32.DLL
Microsoft (R) COFF/PE Dumper Version 8.00.50727.42
Copyright (C) Microsoft Corporation. All rights reserved.
Dump of file Kernel32.DLL
File Type: DLL
Section contains the following exports for KERNEL32.dll
00000000 characteristics
4549AD66 time date stamp Thu Nov 02 09:33:42 2006 0.00 version
626 Часть IV. Динамически подключаемые библиотеки
1 ordinal base
1207 number of functions
1207 number of names
ordinal |
hint |
RVA |
name |
3 |
0 |
|
AcquireSRWLockExclusive (forwarded to |
|
|
|
|
|
|
|
NTDLL.RtlAcquireSRWLockExclusive) |
4 |
1 |
|
AcquireSRWLockShared (forwarded to |
|
|
|
NTDLL.RtlAcquireSRWLockShared) |
5 |
2 |
0002734D |
ActivateActCtx = _ActivateActCtx@8 |
6 |
3 |
000088E9 |
AddAtomA = _AddAtomA@4 |
7 |
4 |
0001FD7D |
AddAtomW = _AddAtomW@4 |
8 |
5 |
000A30AF |
AddConsoleAliasA = _AddConsoleAliasA@12 |
9 |
6 |
000A306E |
AddConsoleAliasW = _AddConsoleAliasW@12 |
10 |
7 |
00087935 |
AddLocalAlternateComputerNameA = |
|
|
|
_AddLocalAlternateComputerNameA@8 |
11 |
8 |
0008784E |
AddLocalAlternateComputerNameW = |
|
|
|
_AddLocalAlternateComputerNameW@8 |
12 |
9 |
00026159 |
AddRefActCtx = _AddRefActCtx@4 |
13 |
A |
00094456 |
AddSIDToBoundaryDescriptor = |
|
|
|
_AddSIDToBoundaryDescriptor@8 |
... |
|
|
|
1205 |
4B4 |
0004328A |
lstrlen = _lstrlenA@4 |
1206 |
4B5 |
0004328A |
lstrlenA = _lstrlenA@4 |
1207 |
4B6 |
00049D35 |
lstrlenW = _lstrlenW@4 |
|
|
|
|
Summary |
|
3000 |
.data |
A000 |
.reloc |
1000 |
.rsrc |
C9000 |
.text |
Как видите, идентификаторы расположены по алфавиту; в графе RVA указывается смещение в образе DLL-файла, по которому можно найти экспортируемый идентификатор. Значения в графе ordinal предназначены для обратной совместимости с исходным кодом, написанным для 16-разрядной Windows, — применять их в современных приложениях не следует. Данные из графы hint используются системой и для нас интереса не представляют.
Примечание. Многие разработчики — особенно те, у кого большой опыт программирования для 16-разрядной Windows, — привыкли экспортировать функции из DLL, присваивая им порядковые номера. Но Майкрософт не публикует такую информацию по системным DLL и требует связывать ЕХЕили DLL-файлы с Windows-функциями только
Глава 19. DLL - основы.docx 627
по именам. Используя порядковый номер, вы рискуете тем, что ваша программа не будет работать в других версиях Windows. Я поинтересовался, почему Майкрософт отказывается от порядковых номеров, и получил такой ответ: «Мы (Майкрософт) считаем, что РЕ-формат позволяет сочетать преимущества порядковых номеров (быстрый поиск) с гибкостью импорта по именам. Учтите и то, что в любой момент в API могут появиться новые функции. А с порядковыми номерами в большом проекте работать очень трудно — тем более, что такие проекты многократно пересматриваются».
Работая с собственными DLL-модулями и связывая их со своими ЕХЕфайлами, порядковые номера использовать вполне можно. Майкрософт гарантирует, что этот метод будет работоспособен даже в будущих версиях операционной системы. Но лично я стараюсь избегать порядковых номеров и отныне применяю при связывании только имена.
Создание DLL для использования с другими средствами разработки (отличными от Visual C++)
Если вы используете Visual C++ для сборки как DLL, так и обращающегося к ней ЕХЕ-файла, то все сказанное ранее справедливо, и вы можете спокойно пропустить этот раздел. Но если вы создаете DLL на Visual C++, а ЕХЕ-файл — с помощью средств разработки от других поставщиков, вам не миновать дополнительной работы.
Я уже упоминал о том, как применять модификатор extern при «смешанном» программировании на С и C++. Кроме того, я говорил, что из-за искажения имен нужно применять один и тот же компилятор. Даже при программировании на стандартном С инструментальные средства от разных поставщиков создают проблемы. Дело в том, что компилятор Microsoft С, экспортируя С-функцию, искажает ее имя, даже если вы вообще не пользуетесь C++. Это происходит, только когда ваша функция экспортируется по соглашению __stdcall. (Увы, это самое популярное соглашение.) Тогда компилятор Microsoft искажает имя С-функции: впереди ставит знак подчеркивания, а к концу добавляет суффикс, состоящий из символа @ и числа байтов, передаваемых функции в качестве параметров. Например следующая функция экспортируется в таблицу экспорта DLL как
_MyFunc@8:
__declspec(dllexport) LONG __stdcall MyFunc(int a, int b);
Если вы решите создать ЕХЕ-файл с помощью средств разработки от другого поставщика, то компоновщик попытается скомпилировать функцию MyFunc, которой нет в файле DLL, созданном компилятором Microsoft, и, естественно, произойдет ошибка.
Чтобы средствами Microsoft собрать DLL, способную работать с инструментарием от другого поставщика, нужно указать компилятору Microsoft экспортировать имя функции без искажений. Сделать это можно двумя спо-
628 Часть IV. Динамически подключаемые библиотеки
собами. Первый — создать DEF-файл для вашего проекта и включить в него раз-
дел EXPORTS так:
EXPORTS
MyFunc
Компоновщик от Microsoft, анализируя этот DEF-файл, увидит, что экспортировать надо обе функции: _MyFunc@8 и MyFunc. Поскольку их имена идентичны (не считая вышеописанных искажений), компоновщик на основе информации из DEF-файла экспортирует только функцию с именем MyFunc, а функцию_MyFunc@8 не экспортирует вообще.
Может, вы подумали, что при сборке ЕХЕ-файла с такой DLL компоновщик от Microsoft, ожидая имя _MyFunc@8, не найдет вашу функцию? В таком случае вам будет приятно узнать, что компоновщик все сделает правильно и корректно скомпонует ЕХЕ-файл с функцией MyFunc.
Если вам не по душе DEF-файлы, можете экспортировать неискаженное имя функции еще одним способом. Добавьте в один из файлов исходного кода DLL такую строку:
#pragma comment(linker, "/export:MyFunc=_MyFunc@8")
Тогда компилятор потребует от компоновщика экспортировать функцию MyFunc с той же точкой входа, что и _MyFunc@8. Этот способ менее удобен, чем первый, так как здесь приходится самостоятельно вставлять дополнительную директиву с искаженным именем функции. И еще один минус этого способа в том, что из DLL экспортируется два идентификатора одной и той же функции: MyFunc и _MyFunc@8, тогда как при первом способе — только идентификатор MyFunc. По сути, второй способ не имеет особых преимуществ перед первым — он просто избавляет от DEF-файла.
Создание ЕХЕ-модуля
Вот пример исходного кода ЕХЕ-модуля, который импортирует идентификаторы, экспортируемые DLL, и ссылается на них в процессе выполнения.
/********************************************************************
Module: MyExeFile1. срр
********************************************************************/
//сюда включаются стандартные заголовочные файлы Windows и библиотеки С
#include <windows.h> #include <strsafe.h> #include <stdlib.h>
//включаем экспортируемые структуры данных, идентификаторы, функции
//и переменные
#include "MyLib\MyLib.h"
Глава 19. DLL - основы.docx 629
/////////////////////////////////////////////////////////////////////
int WINAPI _tWinMain(HINSTANCE, HINSTANCE, LPTSTR, int) {
int nleft = 10, nRight = 25;
TCHAR sz[100];
StringCchPrintf(sz, _countof(sz), TEXT("%d + %d = %d"), nLeft, nRight, Add(nLeft, nRight));
MessageBox(NULL, sz, TEXT("Calculation"), MB_OK);
StringCchPrintf(sz, _countof(sz),
TEXT(The result from the last Add is: %d"), g_nResult); MessageBox(NULL, sz, TEXT("Last Result"), MB_OK); return(0);
}
//////////////////////////// End of File ////////////////////////////
Создавая файлы исходного кода для ЕХЕ-модуля, вы должны включить в них заголовочный файл DLL, иначе импортируемые идентификаторы окажутся неопределенными, и компилятор выдаст массу предупреждений и сообщений об ошибках.
MYLIBAPI в исходных файлах ЕХЕ-модуля до заголовочного файла DLL не определяется. Поэтому при компиляции приведенного выше кода MYLIBAPI за счет заголовочного файла MyLib.h будет определен как __declspec (dllimport). Встречая такой модификатор перед именем переменной, функции или С++- класса, компилятор понимает, что данный идентификатор импортируется из како- го-то DLL-модуля. Из какого именно, ему не известно, да это его и не интересует. Компилятору нужно лишь убедиться в корректности обращения к импортируемым идентификаторам.
Далее компоновщик собирает все OBJ-модули в конечный ЕХЕ-модуль. Для этого он должен знать, в каких DLL содержатся импортируемые идентификаторы, на которые есть ссылки в коде. Информацию об этом он получает из передаваемого ему LIB-файла. Я уже говорил, что этот файл — просто список идентификаторов, экспортируемых DLL. Компоновщик должен удостовериться в существовании идентификатора, на который вы ссылаетесь в коде и узнать, в какой DLL он находится. Если компоновщик сможет разрешить все ссылки на внешние идентификаторы, на свет появится ЕХЕ-модуль.
Что такое импорт
В предыдущем разделе я упомянул о модификаторе __declspec(dllimport). Импортируя идентификатор, необязательно прибегать к __declspec(dllimport) — можно использовать стандартное ключевое слово extern языка C. Но компилятор создаст чуть более эффективный код, если ему будет заранее известно,