Добавил:
Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:

Рихтер Дж., Назар К. - Windows via C C++. Программирование на языке Visual C++ - 2009

.pdf
Скачиваний:
6274
Добавлен:
13.08.2013
Размер:
31.38 Mб
Скачать

620 Часть 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. Но компилятор создаст чуть более эффективный код, если ему будет заранее известно,

Соседние файлы в предмете Программирование на C++