    ДИНАМИЧЕСКИ ПОДКЛЮЧАЕМЫЕ 
    БИБЛИОТЕКИ 

    глава 12 

    динамически подключаемые библиотеки (dynamic-link libraries, DLLs) — 
краеугольный камень операционной системы Windows, начиная с самой первой ее 
версии. В DLL содержатся все функции интерфейса Win32 API. Три самых важных 
DLL-библиотеки: KERNEL32.DLL (управление памятью, процессами и потоками), 
USER32.DLL (поддержка пользовательского интерфейса, в том числе функции, 
связанные с созданием окон и передачей сообщений) и GDI32.DLL (графика и вывод текста). 
    В Windows есть и другие DLL, функции которых предназначены для более 
специализированных задач. Например, в ADVAPI32.DLL содержатся функции для 
защиты объектов, работы с реестром и регистрации событий, в COMDLG32.DLL — 
стандартные диалоговые окна (вроде File Open и File Save), a LZ32.DLL 
поддерживает декомпрессию файлов. 
    В этой главе я расскажу, как создавать Win32 DLL-модули для приложений, а 
в конце главы опишу некоторые приемы программирования с использованием DLL 
(последняя тема будет продолжена и в главе 18). 

    Создание DLL 

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

    359 

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

    собственные локальные переменные. Кроме того, любые созданные кодом DLL 
объекты принадлежат вызывающему потоку или процессу — DLL в Win32 ничем не владеет. 
    Например, если функция из DLL вызывает VirtualAlloc, резервируется регион 
в адресном пространстве того процесса, которому принадлежит поток, 
обратившийся к функции из DLL. Если DLL будет выгружена из адресного 
пространства процесса, зарезервированный регион не освободится, так как 
система не фиксирует того, что регион выделен библиотечной функцией. 
Считается, что он принадлежит процессу и поэтому освободится, только если 
поток этого процесса вызовет VirtualFree или завершится сам процесс. 
    Вы уже знаете, что глобальные и статические переменные ЕХЕ-файла не 
разделяются его параллельно выполняемыми экземплярами. В Windows 95 это 
достигается за счет выделения специальной области памяти для таких переменных 
при проецировании ЕХЕ-файла на адресное пространство процесса, а в Windows NT 
— с помощью «копирования при записи» (copy-on-write), рассмотренного в главе 
5. Глобальные и статические переменные DLLбиблиотеки обрабатываются точно так 
же. Когда какой-то процесс проецирует образ DLL- | файла на свое адресное 
пространство, система создает также экземпляры глобальных и статических 
переменных. В этой главе я расскажу о приеме, позволяющем нескольким проекциям 
DLL совместно использовать один набор глобальных и статических переменных. 
Однако в Win32 по умолчанию это не делается — чтобы добиться нужного эффекта, 
потребуются дополнительные усилия. 
    В 16-разрядной Windows DLL-библиотеки обрабатываются иначе, чем в Win32. 
первой DLL-библиотека при загрузке становится в некотором смысле частью 
операционной системы. Все приложения, выполняемые в данный момент, получают 
доступ к DLL и всем содержащимся в ней функциям. В Win32-cpeдe DLL нужно 
спроецировать на адресное пространство процесса, прежде чем тот сможет 
вызывать ее функции. 
    Глобальные и статические данные DLL-библиотек в 16-разрядной Windows и   
'• Win32 тоже обрабатываются принципиально по-разному. В первой каждая DLL 
имеет   | свой сегмент данных. В нем находятся все глобальные и статические 
переменные DLL, a также ее закрытая локальная куча. При вызове из DLL функции 
LocalAlloc соответствующая область памяти выделяется из сегмента данных DLL, 
размер которого, как и размер всех других сегментов, ограничен 64 Кб. 
    Такая схема позволяет разделять данные между несколькими процессами, 
поскольку локальная куча доступна DLL независимо от того, каким процессом 
вызвана одна из ее функций. Вот пример использования DLL для разделения данных 
между двумя приложениями: 

    HGLOBAL g_hData = NULL; 
    void SetData(LPVOID IpvData, int nSize) { 
    LPVOID Ipv: 
    g_hData = LocalAlloc(LMEM_MOVEABLE, nSize); 
    Ipv = LocalLock(g_hData); 
    memcpy(lpv, IpvData, nSize); 
    LocalL)nlock(g_hData); 
    } 
    void GetData(LPVOID IpvData, int nSize) { 
    LPVOID Ipv = LocalLock(g_hData); 
    memcpy(IpvData, Ipv, nSize); 
    LocalUnlock(g_hData): 
    } 

    см. след. стр. 

    ______________Глава 12. Динамически подключаемые библиотеки 361 

    Вызов SetData приводит к выделению блока памяти из сегмента данных DLL, ! 
копированию в него данных, на которые указывает параметр IpvData, и сохранению 
описателя блока в глобальной переменной g_hData. Теперь другое приложение 
может вызвать GetData. Та, используя глобальную переменную g_hData, блокирует 
выделенную область локальной памяти, копирует данные из нее в буфер, 
идентифицируемый параметром IpvData и возвращает управление. Вот насколько 
прост способ разделения данных между двумя процессами в 16-разрядной Windows. 
    В Win32 он не работает: во-первых, у DLL в Win32 нет собственных локальных 
куч. Во-вторых, глобальные и статические переменные не разделяются между 
разными проекциями одной DLL; система создает отдельный экземпляр глобальной 
переменной g_hData для каждого процесса, и значения, хранящиеся в разных 
экземплярах переменной, не обязательно одинаковы. 

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

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

    Неявная компоновка 

    Неявная компоновка (implicit linking) — самый распространенный метод 
проецирования образа DLL-файла на адресное пространство процесса. При сборке 
приложения компоновщику нужно указать набор LIB-файлов. Каждый такой файл 
содержит список функций данной DLL, вызов которых разрешен приложениям (или 
другой DLL). Обнаружив, что приложение ссылается на функции, упомянутые в 
LIB-файле для DLL, компоновщик внедряет имя этой DLL в конечный исполняемый 
файл. При загрузке ЕХЕ-файла система просматривает его образ на предмет 
определения необходимых ему DLL, после чего пытается спроецировать их на 
адресное пространство процесса. Поиск DLL осуществляется в: 
    1. каталоге, содержащем ЕХЕ-файл; 
    2. текущем каталоге процесса; 
    3. системном каталоге Windows; 
    4. основном каталоге Windows; 
    5. каталогах, указанных в переменной окружения PATH. 
    Если файл DLL не найден, система открывает окно с сообщением (оно выглядит 
примерно так, как показано ниже) и немедленно завершает процесс. 

    О The dynamiclink library SomeLib dll could not be found in the specified path 
    F;\ADV_WIN'^86.BlN:.;H:\WNNT\System32;H:\WNNT\system:H:\WlNNT:H:\WINNTAsystem 
    32:H:\WINNT:D:WINDOWS.95:D:\WINDOWS.95\COMMAND:DWU95.20;C.-\OOS:C:\^RC;C;\ 
    UTY:C'\MOU SE;C:\TBEACH\D OSAPPS. 
    00 

    Библиотеки, спроецированные на адресное пространство процесса этим 
методом, не отключаются от него до завершения процесса. 

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


    Явная компоновка 

    Образ DLL-файла можно спроецировать на адресное пространство процесса 
явным образом, для чего один из потоков должен вызвать либо LoadLibrary, либо LoadLibraryEx: 

    HINSTANCE LoadLibrary(LPCTSTR IpszLibFile); 
    HINSTANCE LoadLibraryEx(LPCTSTR IpszLibFile, HANDLE hFile, DWORD dwFlags); 

    Обе функции ищут образ DLL-файла (в каталогах, список которых приведен в 
предыдущем разделе) и пытаются спроецировать его на адресное пространство 
вызывающего процесса. Значение типа HINSTANCE, возвращаемое обеими функциями, 
сообщает адрес виртуальной памяти, по которому спроецирован образ файла. Если 
спроецировать DLL на адресное пространство процесса не удалось, функции 
возвращают NULL. 
    В 16-разрядной Windows функция LoadLibrary указывает на ошибку, возвращая 
описатель со значением, меньшим 32. Это код, подсказывающий причину сбоя. 
    В Win32 в таких случаях всегда возвращается NULL, и, чтобы узнать причину 
ошибки, поток должен вызвать GetLastError. 
    Очевидно, Вы обратили внимание на 2 дополнительных параметра функции 
LoadLibraryEx: hFile и dwFlags. Первый зарезервирован и должен быть NULL. Во 
втором можно передать либо 0, либо комбинацию флагов 
DONT_RESOLVE_DLL_REFERENCES, LOAD_LIBRARY_AS_DATAFILE и LOAD_WITH_ALTERED_SEARCH_PATH. 
    DONT_RESOLVE_DLL_REFERENCES Указывает системе спроецировать DLL на 
адресное пространство вызывающего процесса. Проецируя DLL, система обычно 
вызывает из нее специальную функцию DllMain (о ней чуть позже) и с ее помощью 
инициализирует библиотеку. Так вот, данный флаг заставляет систему 
проецировать DLL, не обращаясь к DllMain. 
    Кроме того, DLL может импортировать функции из других DLL. При загрузке 
библиотеки система проверяет, используются ли ею другие DLL; если да, то 
загружает и их. При установке флага DONT_RESOLVE_DLL_REFERENCES дополнительные 
DLL автоматически не загружаются.                                "'~     "' "" ~ 
    LOAD_LIBRARY_AS_DATAFILE Этот флаг очень похож на предыдущий. DLL 
проецируется на адресное пространство процесса так, будто это файл данных. При 
этом система не тратит дополнительного времени на подготовку к исполнению 
какого-либо кода из данного файла. Например, когда DLL проецируется на 
адресное пространство, система считывает информацию из DLL-файла и на ее 
основе определяет, какие атрибуты защиты страниц следует присвоить разным 
частям файла. Если этот флаг не указан, атрибуты защиты устанавливаются 
такими, словно код из данного файла будет исполняться. 
    Этот флаг может понадобиться по нескольким причинам. Во-первых, его стоит 
указать, если DLL содержит только ресурсы и никаких функции. Тогда DLL 
проецируется на адресное пространство процесса, после чего при вызове функций, 
загружающих ресурсы, можно использовать значение HINSTANCE, возвращенное 
LoadLibraryEx. Во-вторых, он может потребоваться, если Вам нужны ресурсы, 
содержащиеся в каком-нибудь ЕХЕ-файле. Обычно загрузка такого файла приводит к 
запуску нового процесса, но этого не произойдет, если его загрузить вызовом 
LoadLibraryEx в адресное пространство Вашего процесса. Получив значение 
HINSTANCE для спроецированного ЕХЕ-файла, Вы фактически получаете доступ к его 
ресурсам. Так как в ЕХЕ-файле нет DllMain, при вызове LoadLibraryEx для 
загрузки ЕХЕ-файла нужно указать флаг LOAD_LIBRARY_AS_DATAFILE. 

    _______________Глава 12. Динамически подключаемые библиотеки 363 

    LOAD_WITH_ALTERED_SEARCH_PATH Изменяет алгоритм, используемый 
LoadLibraryEx при поиске DLL-файла. Обычно поиск осуществляется так, как я уже 
рассказывал. Однако, если данный флаг установлен, функция ищет файл, 
просматривая каталоги в таком порядке: 
    1. Каталог, заданный в параметре IpszLibFile. 
    1. Текущий каталог процесса. 
    3. Системный каталог Windows. 
    4. Основной каталог Windows. 
    5. Каталоги, перечисленные в переменной окружения PATH. 
    И еще один фактор может повлиять на то, где система ищет файлы DLL. В 
реестре есть раздел: 

    HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\ Session Manager\KnownDLLs 

    Содержимое этого раздела может выглядеть примерно так (при просмотре 
реестра с помощью REGEDIT.EXE): 

    Registry Edit ^iew Це1р 
    * U MediaResources3 Name               ' Data 
    ;+._] NetworkProvider                  '^Default)            (value not set) 
    <• _1 NIs                             .!^]advapi32           "advapi32.dll" 
    » _J Print                            . Sjcorndlga?           "comdlg32.dU" 
    i_l PriorityConfrol                     ''aicrtdll                
"crtdll.dll"                           , 
    Gl ProductOptions                  ^DllDirectory         "%SystemRoot%\system32" 
    Cl SecurePipeServers               , L«3gdi32               "gdi32.dll" 
    f <_i SecuntyProviders                ^kemelBZ            "kemel32.dll" 
    Q ServiceGrcupOrder             : : ^32                 ;lz32.dir 
    ?a ServiceProvider                  „^°le32                "I63" 
    - _] Session Manager              •: |° еаиог           ° еа^ 
    -., ,,   .,   ,                       ,;-.j: a3°ledi32               "olecli32.dll" 
    . _1 AppPatehes                :; ;®]olecnv32            "olecnv32.dll" 
    _] DOS Devices               б1 !• aolesvr32            •olesvr32.dll" 
    -J Environment                 •;:il; ea°lethk32             "olethk32.dll" 
    _| Executive                    ^ ^ Э]грсд1                "rpctH.dll" 
    _J FileRenameOpsrations     " | gSHDOCW/        "SHDOCW/.DLL" 
    : CJ Memory Management       ::;S]SHLWAPI          "SHLWAPI.DLL" 
    ' Q Subsystems              !:"'; Quser32            "user32.dll"          
            ; 
    _i Setup                       —ig^version             "version.dll" 
    ;+ L»] SystemResources 
    :     ! GJ TimeZonelnformation                                             
                        : 
    .!_) Update 
    _I VirtusfDeviceDrlvers            »j <j__________________________j        
    Ц 
    My ComputeЛHKE'т_LO&s^i._ЫACШNE\SYSTEM\CurrentControlSe(\CQntrol^lSession Manager\KnownDLLs 

    Как видите, здесь содержится набор параметров, имена которых совпадают с 
именами некоторых DLL-файлов. Значения параметров представляют собой строки, 
идентичные именам параметров, только дополненные расширением «.DLL». (Впрочем 
это не обязательно, и Вы сами убедитесь в этом на следующем примере.) 
    Когда Вы вызываете LoadLibrary или LoadLibraryEx, каждая из них сначала 
проверяет, указано ли имя DLL вместе с расширением «.DLL». Если нет, поиск DLL 
ведется по уже знакомым Вам правилам. 
    Если же расширение «.DLL» указано, функция его отбрасывает и ищет в 
разделе реестра KnownDLLs параметр с идентичным именем. Если его нет, вновь 
применяются описанные ранее правила. Есть — система обращается к значению, 
связанному с параметром, и пытается загрузить определенную в нем DLL. При этом 
система ищет DLL в каталоге, на который указывает значение, связанное с 
параметром реестра DllDirectory. В Windows NT параметру DllDirectory по 
умолчанию присваивается значение «%SystemRoot°/o\System32». 

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

    А теперь представьте для примера, что мы добавили в раздел реестра 
KnownDLLs параметр: 

    Параметр: SomeLib 
    •' Значение: SomeOther-Lib. DLL 

    После вызова: 

    Load Li braгу("SomeLib"): 

    система будет искать файл по обычным правилам. Но если мы вызовем так: 

    Load Li braгу("SomeLib.DLL"); 

    то система увидит, что в реестре есть параметр с идентичным именем (не 
забудьте: она \ ( /   отбрасывает расширение «.DLL»), и в итоге попытается 
загрузить вместо «SomeLib.DLL» библиотеку «SomeOtherLib.DLL». При этом система 
сначала ищет «SomeOtherLib.DLL» в каталоге «%SystemRoot%System32». Если нужный 
файл в этом каталоге есть, то загружается именно он. Нет — LoadLibrary(Ex) 
возвращает NULL, а вызов GetLastError дает код 2 (ERROR_FILE_NOT_FOUND). 
    Если DLL загружается явно, ее можно отключить от адресного пространства 
процесса функцией FreeLibrary. 

    BOOL FreeLibrary(HINSTANCE hinstDll); 

    При вызове FreeLibrary Вы должны передать значение типа HINSTANCE, которое 
идентифицирует выгружаемую DLL. Это значение Вы получите, предварительно 
вызвав LoadLibrary или LoadLibrary Ex. 
    На самом деле LoadLibrary и LoadLibraryEx лишь увеличивают счетчик числа 
пользователей указанной библиотеки, a FreeLibrary его уменьшает. Например, при 
первом вызове LoadLibrary для загрузки DLL система проецирует образ DLL-файла 
на адресное пространство вызывающего процесса и присваивает единицу счетчику 
числа пользователей этой DLL. Если поток того же процесса вызывает LoadLibrary 
для той же DLL еще раз, DLL больше не проецируется; система просто увеличивает 
счетчик числа ее пользователей — вот и все. Чтобы выгрузить DLL из адресного 
пространства процесса, FreeLibrary придется теперь вызывать дважды: первый 
вызов уменьшит счетчик до 1, второй — до 0. Обнаружив, что счетчик числа 
пользователей DLL обнулен, система отключит ее. После этого попытка вызова 
какой-либо функции из данной библиотеки приведет к нарушению доступа, так как 
код по указанному адресу уже не отображается на адресное пространство процесса. 
    Система поддерживает в каждом процессе свой счетчик DLL, т. е. если поток 
процесса А вызывает: 

    HINSTANCE hinstDll = LoadLibraryC'MyLib.DLL"): 

    а затем тот же вызов делает поток в процессе В, то MYLIB.DLL проецируется 
на адресное пространство обоих процессов, а счетчики числа пользователей DLL в 
каждом из них приравниваются 1. Если же поток процесса В вызовет далее: 

    FreeLibraгу(hinstDll); 

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

    HINSTANCE GetModuleHandle(LPGTSTR IpszModuleName); 

    Например, следующий код загружает MYLIB.DLL, только если она еще не 
спроецирована на адресное пространство процесса: 

    ______________Глава 12. Динамически подключаемые библиотеки 365 

    HINSTANCE hinstDll; 
    hinstDll = GetModuleHandleC'MyLib");// подразумевается расширение DLL 
    if (hinstDll == NULL) { 
    hinstDll = LoadLibraryC'MyLib"); // подразумевается расширение DLL 
    \ 
    
    Если у Вас есть значение HINSTANCE для DLL, можно определить и полное имя 
DLL (или ЕХЁ) с помощью GetModuleFileName: 

    DWORD GetModuleFileName(HINSTANCE hinstModule, 
    LPTSTR IpszPath, DWORD cchPath); 

    Первый параметр функции — это значение HINSTANCE для ЕХЕ или DLL. Второй — 
задает адрес буфера, в который она запишет полное имя образа файла. Третий, и 
последний, параметр (cchPath) определяет размер буфера в символах. 
    В 16-разрядной Windows есть функция GetModuleUsage: 

    int GetModuleUsage(HINSTANCE hinstDll); 

    Когда Вы передаете ей HINSTANCE загруженной DLL, система возвращает 
значение счетчика числа пользователей этой DLL. Тем самым Вы узнаете, сколько 
раз вызвать FreeLibrary, чтобы выгрузить DLL. В 16-разрядной Windows 
загруженная DLL становится частью операционной системы и доступна всем 
выполняемым задачам. В Win32 она становится частью адресного пространства 
вызывающего процесса, а не операционной системы, и поэтому GetModuleUsage в 
Win32 API больше не поддерживается. 
    "Лично мне кажется, что Microsoft следовало бы сохранить ее в Win32 API, 
так как иногда нужно знать состояние счетчика DLL в вызывающем процессе. Ведь 
эта информация в операционной системе есть, и Microsoft могла бы элементарно 
модифицировать GetModuleUsage так, чтобы та просто сообщала ее. 
    Уменьшить счетчик числа пользователей DLL можно и с помощью другой Win32функции: 

    VOID FreeLibraryAndExitThread(HINSTANCE hinstDll, DWORD dwExitCode); 
    Она реализована в KERNEL32.DLL так: 
    VOID FreeLibraryAndExitThreadCHINSTANCE hinstDll, DWORD dwExitCode) { 
    FreeLibrary(hinstDll); 
    ExitThread(dwExitCode); 
    \ 

    На первый взгляд, она очень проста, и, наверное, Вы удивляетесь, что это 
Microsoft решила ее написать. Но представьте такой сценарий. Вы пишете DLL, 
которая при первом отображении на адресное пространство процесса создает 
поток. Последний, завершив работу, может отключать DLL от адресного 
пространства процесса. Но для этого он должен вызвать сначала FreeLibrary, а 
сразу за этим — ExitThread. 
    Если поток станет сам вызывать FreeLibrary и ExitThread по отдельности, 
возникнет очень серьезная проблема: вызов FreeLibrary тут же отключит DLL от 
адресного пространства процесса. После возврата из FreeLibrary код, содержащий 
вызов ExitThread, окажется недоступен, и поток попытается выполнить не пойми 
что. Это приведет к нарушению доступа и завершению всего процесса! 
    Если же поток обратится к FreeLibraryAndExitThread, она вызовет 
FreeLibrary и сразу же отключит DLL. Но следующая исполняемая инструкция 
находится в KERNEL32.DLL, а не в только что отключенной DLL. Значит, поток 
сможет продолжить выполнение и вызвать "ЕхПТПгеаа, которая корректно завершит 
его, не возвращая управления. 

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

    Впрочем, FreeLibraryAndExitThread может и не понадобиться. Мне она 
пригодилась лишь раз, когда я занимался очень нетипичной задачей. Да и код я 
составлял под Windows NT 3.1, где этой функции не было. Наверное, поэтому я 
так обрадовался, обнаружив ее в Windows NT 3.5 и Windows 95. 

    Функция входа/выхода 

    У Win32 DLL может быть одна функция входа/выхода, но она необязательна. 
Система вызывает ее в некоторых ситуациях (о чем речь еще впереди) сугубо в 
информационных целях, и обычно она используется DLL для инициализации и 
очистки в конкретных процессах или потоках. Если Вашей DLL подобные 
уведомления не нужны, Вы не обязаны реализовывать эту функцию. Пример — DLL, 
содержащая исключительно ресурсы. Но если эта функция в DLL все же есть, она 
должна выглядеть так: 

    BOOL WINAPI DllMain(HINSTANCE hinstDll, DWORD fdwReason, LPVOID fImpLoad) { 
    switch (fdwReason) { 
    case DLL_PROCESS_ATTACH: 
    // DLL проецируется на адресное пространство процесса 
    break; 
    case DLL_THREAD_ATTACH: 
    // создается поток 
    break; 
    case DLL_THREAD_DETACH: 
    // поток завершается корректно 
    break; 
    case DLL_PROCESS_DETACH: 
    // DLL отключается от адресного пространства процесса 
    break; 
    } 
    return(TRUE); // используется только для DLL_PROCESS_ATTACH 
    } 

    Создавая DLL для 16-разрядной Windows, приходилось компоновать DLL с 
небольшим модулем, написанном на ассемблере. Этот модуль занимался коекакой 
низкоуровневой инициализацией и вызывал Вашу функцию LibMain, передавая ей 
параметры, полученные от системы через регистры процессора. К счастью, 
исходный текст этого модуля Microsoft включала в SDK вместе с его OBJ-файлом, 
что весьма помогало тем, у кого не было макроассемблера.                       
        
    Как я уже неоднократно говорил, Win32 — переносимый API, способный рабо- | 
тать на разных аппаратных платформах. Поэтому Microsoft отказалась от 
ассемблер- | ных модулей. И теперь при создании DLL нужно лишь написать код и 
скомпоновать | его, как при создании приложений. Большинство DLL, 
разработанных Вами для 16-  ' разрядной Windows, должно работать и в Win32; 
правда, иногда приходится вносить   i незначительные изменения. В чем 
модификация неизбежна, так это в LibMain и WEP (Windows Exit Procedure).       
                                         > 
    В 16-разрядной Windows система обращается к LibMain при загрузке 
библиотеки, а к WEP — при выгрузке. С концептуальной точки зрения, WEP была 
удачным нововведением в 16-разрядной Windows (впервые она появилась в версии 
3.0), но на практике с этой функцией возникало слишком много проблем. При 
нехватке свободной памяти 

    см. след. стр. 

    ______________Глава 12. Динамически подключаемые библиотеки 367 

    WEP иногда вызывалась прежде LibMain; да еще при вызове ей мог быть 
отведен крошечный стек ядра. А значит, при использовании в WEP локальных 
переменных вся система могла просто рухнуть. Думаю, Вам будет приятно узнать, 
что в Win32 этих проблем нет. 

    Функция DllMain теперь заменяет LibMain и WEP 16-разрядной Windows. 

    Операционная система вызывает функцию входа/выхода в различных ситуациях. 
При этом параметр hinstDll должен содержать описатель экземпляра DLL. Как и 
hinstExe функции WinMain, это значение — виртуальный адрес, по которому файл 
DLL проецируется на адресное пространство процесса. Обычно последнее значение 
сохраняется в глобальной переменной, чтобы его можно было использовать и при 
вызовах функций, загружающих ресурсы (типа DialogBox или LoadString). Если 
файл DLL загружен неявно, параметр flmpLoad отличен от 0, а если явно — равен 0. 
    Параметр fdwReason сообщает о причине, по которой система вызвала эту 
функцию. Он принимает одно из 4 значений: DLL_PROCESS_ATTACH, 
DLL_PROCESS_DETACH, DLL_THREAD_ATTACH или DLL_THREAD_DETACH. 

    DLL_PROCESS_ATTACH 

    Система вызывает DllMain с этим значением параметра fdwReason сразу после 
того, как DLL спроецирована на адресное пространство процесса. А это 
происходит, только когда образ DLL-файла проецируется в первый раз. Если затем 
поток вызовет LoadLibrary или LoadLibraryEx для уже спроецированной DLL, 
система просто увеличит счетчик числа пользователей этой DLL; так что DllMain 
вызывается со значением DLL_PROCESS_ATTACH лишь раз. 
    Обрабатывая DLL_PROCESS_ATTACH, библиотека должна выполнить в процессе 
инициализацию, необходимую ее функциям. Например, в DLL могут быть функции, 
которым нужна своя куча (создаваемая в адресном пространстве процесса). В этом 
случае DllMain могла бы создать такую кучу, вызвав HeapCreate при обработке 
уведомления DLL_PROCESS_ATTACH, а описатель созданной кучи сохранить в 
глобальной переменной, доступной функциям DLL. 
    При обработке уведомления DLL_PROCESS_ATTACH значение, возвращаемое 
функцией DllMain, указывает, корректно ли прошла инициализация DLL. Например, 
если вызов HeapCreate закончился благополучно, следует вернуть TRUE. А если 
кучу создать не удалось — FALSE. Для любых других значений fdwReason — 
DLL_PROCESS_DETACH, DLL_THREAD_ATТАСН и DLL_THREAD_DETACH — значение, 
возвращаемое DllMain, системой игнорируется. 
    Конечно, где-то в системе должен быть поток, отвечающий за выполнение кода 
DllMain. При создании нового процесса система выделяет для него адресное 
пространство, куда проецируется ЕХЕ-файл и все необходимые ему DLL-модули. 
Далее создается первичный поток процесса, используемый системой для вызова 
DllMain из каждой DLL со значением DLL_PROCESS_ATTACH. Когда все 
спроецированные DLL ответят на это уведомление, система заставит первичный 
поток процесса исполнить стартовый код из стандартной библиотеки С, а потом — 
функцию WinMain (ЕХЕ-файла). Если DllMain хотя бы одной из DLL вернет FALSE, 
сообщая об ошибке при инициализации, система завершит процесс, удалив из его 
адресного пространства образы всех файлов; после чего пользователь увидит окно 
с сообщением о том, что процесс запустить не удалось. 
    А что происходит при явной загрузке DLL? Когда поток вызывает LoadLibrary 
или LoadLibraryEx, система отыскивает указанную DLL и проецирует ее на 
адресное пространство процесса. Затем вызывает DllMain со значением 
DLL_PROCESS_ATTACH, используя поток, вызвавший LoadLibrary или LoadLibraryEx. 
Как только DllMain обработает уведомление, произойдет возврат из LoadLibrary 
или LoadLibraryEx, и поток продолжит работу в 

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

    обычном режиме. Если же DllMain вернет FALSE (неудачная инициализация), 
система автоматически отключит образ файла DLL от адресного пространства 
процесса, а вызов LoadLibrary или LoadLibraryEx даст NULL. 
    В 16-разрядной Windows DLL-функция LibMain вызывается лишь раз — при 
загрузке DLL. Если запускается еще одно приложение, требующее той же CsS^^Si^ 
DLL, система к LibMain этой DLL повторно не обращается. В Win32, напротив, 
DllMain конкретной DLL вызывается со значением DLL_PROCESS_ATTACH всякий раз, 
когда эта DLL проецируется на адресное пространство другого процесса. Если 
LIBXYZ.DLL требуется, скажем, 10 программам, то DllMain этой DLL будет вызвана 
со значением DLL_PROCESS_ATTACH тоже 10 раз. 

    DLL_PROCESS_DETACH 

    При отключении DLL от адресного пространства процесса вызывается ее 
функция DllMain с передачей в параметре fdwReason значения DLL_PROCESS_DETACH. 
Обрабатывая это значение, DLL должна провести очистку в данном процессе. 
Например, вызвать HeapDestroy, чтобы разрушить кучу, созданную ею при 
обработке уведомления DLL_PROCESS_ATTACH. 

    /внимдниЕ\ 
    Если DllMain вызвана со значением DLL_PROCESS_ATTACH и возвращает в 
результате FALSE, сообщая о неудачной инициализации, система впоследствии все 
равно вызовет эту функцию со значением DLL_PROCESS_DETACH. Поэтому сначала 
проверьте: не собираетесь ли Вы очищать то, что инициализировать не удалось. 

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

    BOOL WINAPI DllMaln(HINSTANCE hinstDll, DWORD fdwReason, LPVOID tImpLoad) { 
    static PVOID pvData = NULL; 
    BOOL fOk = TRUE; // пока предполагаем, что все идет хорошо 
    switch (fdwReason) { 
    case DLL_PROCESS_ATTACH: 
    pvData = HeapAlloc(GetProcessHeap(), 0, 1000): 
    if (pvData == NULL) 
    fOk = FALSE: 
    break: 
    case DLL_PROCESS_DETACH: 
    HeapFree(GetProcessHeap(), 0, pvData); 
    break; 
    } 
    return(fOk); // используется только для DLL_PROCESS_ATTACH 
    i 

    Код кажется вполне безобидным. При подключении DLL к адресному 
пространству процесса выделяется небольшой блок памяти. Если его выделить не 
удается, DllMain возвращает FALSE, указывая на ошибку инициализации. 
Выделенный блок памяти освобождается при отключении DLL от адресного 
пространства процесса. Тут-то и возникает проблема. В приведенном фрагменте, 
если DllMain при обработке DLL_PROCESS_АТТАСН возвращает FALSE, система 
вызывает ее потом со значением DLL_PROCESS_

    см. след. стр. 

    ______________Глава 12. Динамически подключаемые библиотеки 369 

    DETACH. А значит, HeapFree будет вызвана для освобождения блока даже в том 
случае, если он не создан. Чтобы исправить ошибку, перепишем обработЛ 
Ky~DLL_PROCESS_DETACH:                                          \ 

    case DLL_PROCESS_DETACH: 
    if (pvData != NULL): 
    HeapFree(GetProcessHeap(), 0, pvData): 
    break;                                                                     
                  \ ____________________________________________________________ 

    Если DLL отключается по причине завершения процесса, то за исполнение кода 
DllMain отвечает поток, вызвавший ExitProcess, — обычно первичный поток 
приложения. Когда Ваша функция WinMain возвращает управление стартовому коду 
из стандартной библиотеки С, тот явно вызывает ExitProcess и завершает процесс. 
    Если DLL отключается в результате вызова FreeLibrary, код DllMain 
исполняется потоком, вызвавшим FreeLibrary. Управление от FreeLibrary не. 
передается до завершения DllMain. 

    /внимАниЕ\ 
    Если процесс завершается в результате вызова TerminateProcess, система не 
вызывает DllMain со значением DLL_PROCESS_DETACH. Значит, ни одна из 
спроецированных на адресное пространство процесса DLL-библиотек не получит 
шанса на очистку до завершения процесса. Последствия могут быть плачевны — 
вплоть до потери данных. Вызывайте TerminateProcess только в самом крайнем случае! 

    На рис. 12-1 показаны операции, выполняемые при вызове LoadLibrary, а на 
рис. 12-2 — при вызове FreeLibrary. 

    DLLJHREAD.ATTACH 

    Когда в процессе создается новый поток, система просматривает все DLL, 
спроецированные в данный момент на адресное пространство этого процесса, и в 
каждой из таких DLL вызывает DllMain со значением DLL_THREAD_ATTACH. Тем самым 
она уведомляет DLL-модули о необходимости инициализации, связанной с данным 
потоком. Например, DLL-версия стандартной библиотеки С создает блок данных, 
чтобы многопоточные приложения могли безопасно использовать ее функции. 
    Только что созданный поток отвечает за выполнение кода в функциях DllMain 
всех DLL. Работа его собственной (стартовой) функции начинается лишь после 
того, как все DLL-модули получат возможность обработать уведомление 
DLL_THREAD_ATTACH. "Если в момент проецирования DLL на адресное пространство 
процесса в нем исполняется несколько потоков, система не вызывает DllMain со 
значением DLL_THREAD_ATТАСН ни для одного из существующих потоков. Вызов 
DllMain с этим значением осуществляется, только если DLL проецируется на 
адресное пространство процесса в момент создания потока. 
    Обратите также внимание, что система не вызывает функции DllMain со 
значением DLL_THREAD_ATTACH и для первичного потока процесса. Любая DLL, 
проецируемая на адресное пространство процесса в момент его создания, получает 
уведомление DLL_PROCESS_ATTACH, но не DLL_THREAD_ATTACH. 

    __Глава 12. Динамически подключаемые библиотеки 371 

    Рис. 12-2. Операции, выполняемые системой при вызове потоком функции FreeLibrary 

    DLLJHREAD.DETACH 

    Завершая поток вызовом ExitThread1, система просматривает все образы 
DLL-файлов, спроецированные в данный момент на адресное пространство процесса, 
и в каждой из этих DLL вызывает DllMain со значением DLL_THREAD_DETACH. Тем 
самым она уведомляет DLL'йодули"о~необходимости очистки, связанной с данным 
потоком. Например, DLL-версия стандартной библиотеки С освобождает блок 
данных, используемый для управления многопоточными приложениями. 
    1. Если функция потока не вызывает ExitThread, а просто возвращает 
управление, система сама вызывает ExitThread. 

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


    /внимАниЕ\ 
    Если поток завершается из-за того, что другой поток вызвал для него 
TerminateThread, система не вызывает DllMain со значением DLL_THREAD_DETACH. 
Зна^д^ цд одна из спроецированных на адресное пространство процесса DLL не 
получит шанса на выполнение очистки до завершения потока, что может привести к 
потере данных. Поэтому TerminateThread следует использовать лишь в самом 
крайнем случае! 

    Если при отключении DLL еще исполняются какие-то потоки, то для них 
DllMain не вызывается со значением DLL_THREAD_DETACH. Вы можете проверить это 
при обработке DLL_PROCESS_DETACH и провести необходимую очистку. 
    Ввиду упомянутых выше правил не исключена такая ситуация: поток вызывает 
LoadLibrary лля загрузки DLL, в результате чего система вызывает из этой 
библиотеки DllMain со значением DLL_PROCESS_ATTACH. Затем поток, загрузивший 
DLL, завершается, что приводит к новому вызову DllMain — на этот раз со 
значением DLL_THREAD_DETACH. DLL уведомляется о завершении потока, но она еще 
не получала DLL_THREAD_ATTACH, уведомляющего о его подключении. Поэтому будьте 
крайне осторожны при выполнении любой очистки, связанной с конкретным потоком. 
К счастью, большинство программ пишется так, что LoadLibrary и FreeLibrary 
вызываются одним потоком. 

    Как система упорядочивает вызовы DliMain 
    Чтобы понять, о чем тут речь, рассмотрим следующий сценарий. В процессе А 

два потока: А и В. На его адресное пространство проецируется DLL-модуль 
SOMEDLL.DLL. Оба потока собираются вызвать CreateThread, чтобы создать еще два 
потока — С и D. 
    Когда поток А вызывает для создания потока С функцию CreateThread, система 
обращается к DllMain из SOMEDLL.DLL со значением DLL_THREAD_ATTACH. Пока поток 
С исполняет код DllMain, поток В вызывает CreateThread для создания потока D. 
Системе нужно вновь обратиться к DllMain со значением DLL_THREAD_ATTACH, и на 
этот раз код функции должен выполнять поток D. Но система упорядочивает вызовы 
DllMain, и поэтому приостановит выполнение потока D, пока поток С не завершит 
обработку кода DllMain и не выйдет из этой функции. 
    Закончив выполнение DllMain, поток С может начать выполнение своей функции 
потока. Теперь система возобновляет поток D и позволяет ему выполнить код 
DllMain, при возврате из которой он начнет обработку собственной функции потока. 
    Обычно никто и не задумывается над тем, что вызовы DllMain 
упорядочиваются. Но я завел об этом разговор потому, что один мой коллега 
как-то раз написал код, в котором была ошибка, связанная именно с 
упорядочиванием вызовов DllMain. Его код выглядел примерно так: 

    BOOL WINAPI DllMainCHINSTANCE hinstDll, DWORD fdwReason, LPVOID fImpLoad) { 
    HANDLE hThread: 
    DWORD dwThreadId; 
    switch (fdwReason) { 
    case DLL_PROCESS_ATTACH: 
    // DLL проецируется на адресное пространство процесса 
    // создаем поток для выполнения какой-то работы 
    hThread = CreateThread(NULL, 0, SomeFunction, NULL, 0, &dwThreadId); 
    
    // задерживаем наш поток до завершения нового потока 
    WaitForSingleObject(hThread, INFINITE); 

    ______________Глава 12. Динамически подключаемые библиотеки 373 
    // доступ к новому потоку больше не нужен 
    GloseHandle(hThread); 
    break; 
    case DLL_THREAD_ATTACH: 
    // создается еще один поток 
    break; 
    case DLL_THREAD_DETACH: 
    // поток завершается корректно 
    break; 
    case DLL_PROCESS_DETACH: 
    // DLL выгружается из адресного пространства процесса 
    break: 
    } 
    return(TRUE); 

    Нашли «жучка»? Мы-то его искали несколько часов. Когда DllMain получает 
уведомление DLL_PROCESS_ATTACH, создается новый поток. Системе нужно вновь 
вызвать эту же DllMain со значением DLL_THREAD_ATTACH. Но выполнение нового 
потока приостанавливается — ведь поток, из-за которого в DllMain было 
отправлено уведомление DLL_PROCESS_АТТАСН, свою работу еще не закончил. 
Проблема кроется в вызове WaitForSingleObject. Она приостанавливает выполнение 
текущего потока до тех пор, пока не завершится новый. Однако у того не будет 
ни единого шанса не только на возобновление, но и на завершение — ведь он 
приостановлен в ожидании того, когда текущий поток выйдет из DllMain. Вот Вам 
и взаимная блокирявка — выполнение обоих потоков задержано навеки. 
    Впервые начав размышлять над этой проблемой, я обнаружил функцию 
DisableThreadL ibraryCa Us: 

    BOOL DisableThreadLibraryCalls(HINSTANCE hinstOU): 

    Впервые она появилась в Windows NT 3.5 и теперь есть в Windows 95. 
    Вызывая ее. Вы сообщаете системе, что уведомления DLL_THREAD_ATTACH и 
DLL_THREAD_DETACH не должны посылаться функции DllMain той DLL, что указана в 
вызове. Мне показалось логичным, что взаимной блокировки не будет, если 
система не станет посылать DLL-библиотеке уведомления. Но, проверив свое 
решение (см. ниже), я убедился, что это не выход. 

    BOOL WINAPI DllMain(HINSTANCE hinstDll, DWORD fdwReason, LPVOID fImpLoad) < 
    HANDLE hThread; 
    DWORD dwThreadId; 
    switch (fdwReason) { 
    case DLL_PROCESS_ATTACH: 
    // DLL проецируется на адресное пространство процесса 
    // предотвращаем вызов DllMain при создании 
    // или завершении потока 
    DisableThreadLibгаryCalIs(hinstDll); 
    // создаем поток для выполнения какой-то работы 
    hThread = CreateThread(NULL, 0, SomeFunction, NULL, 0, &dwThreadId); 

    см. след. стр. 

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

    // задерживаем наш поток до завершения нового потока 
    WaitForSingleObject(hThread, INFINITE); 
    
    // доступ к новому потоку больше не нужен 
    CloseHandle(hThread); 
    break; 
    case DLL_THREAD_ATTACH: 
    // создается еще один поток 
    break; 
    case DLL_THREAO_DETACH: 
    // поток завершается корректно 
    break; 
    case DLL_PROGESS_DETACH: 
    // DLL выгружается из адресного пространства процесса 
    break: 
    } 
    return(TRUE); 

    Потом я понял, в чем дело. Создавая процесс, система создает и 
объект-мьютекс. У каждого процесса свой объект-мьютекс — он не разделяется 
между несколькими процессами. Его назначение — синхронизация всех потоков 
процесса при вызове ими DllMain из библиотек, спроецированных на адресное 
пространство данного процесса. 
    Когда вызывается CreateThread, система создает сначала объект ядра «поток» 
и стек потока, затем обращается к WaitForSingleObject, передавая ей описатель 
объекта-мьютекса данного процесса. Когда поток получит права владения на этот 
мьютекс, система заставит его вызвать DllMain из каждой DLL со значением 
DLL_THREAD_ATTACH. И только тогда система вызовет ReleaseMutex, чтобы 
освободить объект-мьютекс. Вот из-за того, что система работает именно так, 
дополнительный вызов DisableThreadLibraryCalls и не предотвращает взаимной 
блокировки потоков. Единственное, что я смог придумать, — переделать эту часть 
исходного кода, так чтобы ни одна DllMain не вызывала WaitForSingleObject. 

    Функция DllMain и стандартная библиотека С 

    Рассматривая функцию DllMain в предыдущих разделах, я подразумевал, что 
для сборки DLL Вы используете компилятор Microsoft Visual C++. Весьма 
вероятно, что при написании DLL Вам понадобится поддержка со стороны 
стартового кода из стандартной библиотеки С. Например, в DLL есть глобальная 
переменная — экземпляр какого-то класса C++. Прежде чем DLL сможет безопасно 
ее использовать, для переменной нужно вызвать ее конструктор, а это работа для 
стартового кода. 
    При сборке DLL компоновщик встраивает в исполняемый файл адрес DLL-функции 
входа/выхода. Вы задаете этот адрес компоновщику ключом /ENTRY. Если у Вас 
компоновщик фирмы Microsoft и Вы указали ключ /DLL, компоновщик считает (по 
умолчанию), что входная функция называется _DllMainCRTStartup. Эта функция 
содержится в файле стандартной библиотеки и при компоновке статически 
связывается с Вашей DLL — даже если Вы используете DLL-версию стандартной 
библиотеки С. 
    Когда Ваша DLL проецируется на адресное пространство процесса, система на 
самом деле вызывает именно _DllMainCRTStartup, а не Вашу функцию DllMain. 
Получив уведомление DLL_PROCESS_ATTACH, функция _DllMainCRTStartup 
инициализирует стандарт

    ______________Глава 12. Динамически подключаемые библиотеки 375 

    ную библиотеку и конструирует все глобальные и статические С++-объекты. 
Закончив, _DllMainCRTStartup вызывает Вашу функцию DUMain. 
    Как только DLL получает уведомление DLL_PROCESS_DETACH, система вновь 
вызывает _DllMainCRTStartup, которая теперь обращается к Вашей функции DUMain, 
и, когда та вернет управление, _DllMainCRTStartup вызовет деструкторы для всех 
глобальных и статических С++-объектов. Функция _DllMainCRTStartup отвечает и 
за любую дополнительную инициализацию и очистку, связанную со стандартной 
библиотекой С (после того, как поступят уведомления DLL_THREAD_ATTACH и DLL_THREAD_DETACH). 
    Я уже говорил, что реализовывать в коде Вашей DLL-библиотеки функцию 
DUMain не обязательно. И если у Вас нетэтой функции, стандартная библиотека С 
использует свою реализацию DllMain: 

    BOOL WINAPI DllMain(HINSTANCE hinstDll, DWORD fdwReason, LPVOID fImpLoad) 
{ return(TRUE); 
    } 

    При сборке DLL компоновщик, не найдя в Ваших OBJ-файлах функции DllMain, 
подключает DllMain из стандартной библиотеки. 

    Экспорт функций и переменных из DLL 

    Создавая DLL, Вы создаете набор функций и хотите, чтобы они были доступны 
другим ЕХЕили DLL-модулям. Если DLL-функция доступна для вызова из других 
программ, то говорят, что это экспортируемая (exported) функция. Кроме 
функций, Win32 позволяет экспортировать и глобальные переменные. В этом 
разделе мы поговорим о том, как экспортируются и те и другие. 
    Вот как экспортировать из DLL функцию Add и глобальную целочисленную 
переменную g_nUsageCount 

    _declspec(dllexport) int Add(int nLeft, int nRight) { 
    return(nLeft + nRight); 
    \ 
    __declspec(dllexport) int g_nUsageCount = 0; 

    Здесь для Вас нет ничего нового, кроме конструкции _declspec(dllexport). 
Компилятор C/C++ (фирмы Microsoft) распознает ее как новое ключевое слово. 
Компилируя функцию Add и g_nU sageCount, он встраивает в конечный OBJ-файл 
дополнительную информацию, необходимую компоновщику при сборке DLL из OBJ-файлов. 
    Обнаружив такую информацию, компоновщик создает LIB-файл со списком 
идентификаторов, экспортируемых из DLL. Этот LIB-файл нужен при сборке любого 
ЕХЕ-файлаТвызывающего функции из данной DLL. Кроме того, компоновщик вставляет 
в конечный ф^илрЦ; и таблицу экспортируемых идентификаторов (exported 
symbols). Каждый элемент в этой таблице содержит имя экспортируемой функции 
или переменной, а также адрес этой функции или переменной внутри DLL-файла. 
Все списки сортируются по алфавиту. 
    Чтобы представить, как выглядит таблица экспорта, воспользуемся утилитой 
DumpBin, поставляемой в комплекте с Visual C++. Ниже показан полученный с ее 
помощью фрагмент распечатки таблицы экспорта KERNEL32.DLL из Windows 95*: 
    * Прим. перев.: мы даем фрагмент распечатки таблицы экспорта KERNEL32.DLL 
из Windows 95 OSR2 (русская версия), а не из оригинальной Windows 95, и 
поэтому данная таблица несколько отличается от того, что было у автора. 

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

    DUMPBIN -EXPORTS KERNEL32.DLL 
    Microsoft (R) COFF Binary File Dumper Version 3.00.5270 
    Copyright (C) Microsoft Corp 1992-1995. All rights reserved. 
    Dump of file kernel32.dll 
    File Type: DLL 
    Section contains the following Exports for KERNEL32.dll 
    0  characteristics 
    328CD416  time date stamp Fri Nov 15 23:35:34 1996 
    0.00  version 
    1  ordinal base 
    782  number of functions 
    682  number of names 
    ordinal   hint  name 
    50   0     AddAtomA  (0002DBF8) 
    102   1     AddAtomW  (00034FB9) 
    103   2     AllocConsole  (0002F76B) 
    104   3     AllocLSCallback  (00021A9A) 
    105   4     AllocSLCallback  (00021ACD) 
    106   5     AreFileApisANSI  (0002EA7B) 
    107   6     BackupRead  (00035040) 
    108   7     BackupSeek  (0003502E) 
    109   8     BackupWrite  (00035040) 
    110   9     Beep  (0002B8DF) 
    111   A     BeginUpdateResourceA  (00034FD4) 
    112   В     BeginUpdateResourceW  (00034FD4) 
    113   С     BuildCommDCBA  (0002E128) 
    114   D     BuildCommDCBAndTimeoutsA  (0002E153) 
    115   E     BuildCommDCBAndTimeoutsW  (00034FEF) 
    116   F     BuildCommDCBW  (00034FD4) 
    117   10    CallNamedPipeA  (0002E041) 
    118   11     CallNamedPipeW  (00035040) 
    772   29F   IstrcmpiA  (000072AA) 
    773   2АО   IstrcmpiW  (00034FD4) 
    774   2А1    Istrcpy  (000072E4) 
    775   2А2   IstrcpyA  (000072E4) 
    776   2АЗ   IstrcpyW  (00034FD4) 
    777   2А4   Istrcpyn  (0000731E) 
    778   2А5   IstrcpynA  (0000731E) 
    779   2А6   IstrcpynW  (00034FEF) 
    780   2А7   Istrlen  (00007395) 
    781    2А8   IstrlenA  (00007395) 
    782   2А9   IstrlenW  (0002BB2C) 


    ______________Глава 12. Динамически подключаемые библиотеки 377 


    Summary 
    4000 .data 
    5000 .edata 
    13000 . r-sr-c 
    42000 .text 
    7000 _FREQASM 
    2000 _INIT 

    Как видите, идентификаторы расположены по алфавиту; числа в скобках — 
адреса идентификаторов внутри образа файла DLL. В графе hint указывается номер 
элемента в списке (отсчет от 0). Значение в графе ordinal (порядковый номер) 
всегда на 1 больше значения в графе hint*. 

    /внимАниЕ\ 
    Многие разработчики привыкли экспортировать функции из DLL, присваивая ; 
им порядковые номера. Чаще всего это делают при экспорте системных компонентов 
в 16-разрядной Windows. Но для Win32 фирма Microsoft пока не публикует такую 
информацию по системным DLL и требует связывать ЁХЕ- или DLL-файлы с 
Wm32-фyнкциями только по именам. Используя порядковый номер, Вы рискуете тем, 
что Ваша программа не станет работать на других Win32-плaтфopмax. 

    Кстати, именно это и случилось со мной. В журнале Microsoft Systems 
Journal я опубликовал программу, построенную на применении порядковых номеров. 
В Windows NT 3.1 программа работала прекрасно, но сбоила при запуске в Windows 
NT 3.5. Чтобы избавиться от сбоев, пришлось заменить порядковые номера именами 
функций, и все встало на свои места. 
    Я поинтересовался, почему Microsoft отказывается от порядковых номеров, и 
получил такой ответ: «Мы (Microsoft) считаем, что РЕ-формат позволяет сочетать 
преимущества порядковых номеров (быстрый поиск) с гибкостью импорта по именам. 
Учтите и то, что в любой момент в API могут появиться новые функции. А с 
порядковыми номерами в большом проекте работать очень трудно — тем более, что 
такие проекты многократно пересматриваются.» 
    Работая с собственными DLL-модулями и связывая их со своими ЕХЕ-файлами, 
порядковые номера использовать вполне можно. Microsoft гарантирует, что этот 
метод будет работоспособен даже в будущих версиях операционной системы. Но 
лично я стараюсь избегать порядковых номеров и отныне — при связывании — 
применяю только имена. 

    Импорт функций и переменных из DLL 

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

    __declspec(dlllmport) int Add(int nLeft, int nRight); 
    __declspec(dllimport) int g_nUsageCount; 

    * Прим. перев.: в таблице экспорта KERNEL32.DLL из Windows 95 OSR2 
зависимость между графами hint и ordinal оказалась не такой, как в 
оригинальной Windows 95 (или Windows NT 4). 

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

    Как и _declspec(dllexport), _declspec(dllimport) — новое ключевое слово, 
распознаваемое компилятором C/C++ фирмы Microsoft. Оно сообщает компилятору, 
что функция Add и переменная g_nUsageCount находятся в DLL, доступ к которой 
ЕХЕ-файл должен получить при загрузке. Это заставит компилятор сгенерировать 
специальный код для импортируемых идентификаторов (imported symbols). 
    Кроме того, компилятор встроит специальную информацию в конечный объектный 
файл. Она используется при компоновке ЕХЕ-файла, подсказывая компоновщику, 
какие функции следует искать в LIB-файлах. Собирая ЕХЕ-файл, компоновщик 
отыскивает импортируемые функции и переменные. Затем, определив, какой 
LIB-файл содержит эти идентификаторы, добавляет в таблицу импорта новые 
элементы. Каждый элемент содержит имя соответствующего DLL-файла, а также имя 
самого идентификатора. Таблица импорта вносится в конечный ЕХЕ-файл при его 
записи на жесткий диск. 
    Чтобы представить, как выглядит таблица экспорта;, воспользуемся все той 
же утилитой DumpBin, поставляемой с Visual C++. Ниже показана полученная с ее 
помощью таблица импорта CALC.EXE из Windows 95*: 

    DUMPBIN -IMPORTS CALC.EXE 
    Microsoft (R) COFF Binary File Dumper Version 3.00.5270 
    Copyright (C) Microsoft Corp 1992-1995. All rights reserved. 
    Dump of file calc.exe 
    File Type: EXECUTABLE IMAGE 
    Section contains the following Imports 
    SHELL32.dll 
    32A85116 time date stamp Fri Dec 06 20:00:06 1996 
    7FEOB03C   32   ShellAboutA 
    KERNEL32.dll 
    328CD42E time date stamp Fri Nov 15 23:35:58 1996 
    BFF76DCF 171   GlobalUnlock 
    BFF74904 160   GlobalAlloc 
    BFF9CF06 16D   GlobalReAlloc 
    BFF9CF28 16E   GlobalSize 
    BFF74937 161    GlobalCompact 
    BFF76DF1 167   GlobalFree 
    BFF76D8B 16B   GlobalLock 
    BFF77395 2C2   IstrlenA 
    BFF7735B 2B3   IstrcatA 
    BFF9CEBD 2A4   WriteProfileStringA 
    BFF775BD 10E   GetModuleHandleA 
    BFF77654 137   GetStartupInfoA 
    BFF88513   FO   GetEnvironmentStrings 
    BFF89F65   CO   GetCommandLineA 
    BFF91002 25D   Sleep 

    * Прим. перев.: и здесь мы даем распечатку таблицы импорта CALC.EXE из 
Windows 95 OSR2 (русская версия), а не из оригинальной Windows 95. 

    ______________Глава 12. Динамически подключаемые библиотеки 379 

    BFF77270 2В6 IstrcmpA 
    BFF77677 132 GetProfileStringA 
    BFF9A5A3 273 UnhandledExceptionFilter
    BFF7759E 10C GetModuleFileNameA 
    BFF8BB4D B6 GetACP 
    8FF8E4BA 118 GetOEMCP 
    BFF77CA3 B9 GetCPInfo 
    BFF75794 139 GetStdHandle 
    BFF758A5 FO GetFileType 
    BFF81B11 27F VirtualFree 
    BFF7DBOO 27E VirtualAlloc 
    BFF9DOA6 1E2 RaiseException 
    BFF77C56 1C2 MultiByteToWldeChar 
    BFF77C74 28E WideCharToMultiByte 
    BFF76D5C 125 GetProcAddress 
    BFF82941 104 GetLastError 
    BFF74904 1A1 LocalAlloc 
    BFF76E13 1A8 LocalReAlloc , 
    BFF772E4 2BC IstrcpyA 
    BFF9CE4E 12E GetProfileIntA 
    BFF76980 151 GetTickCount 
    BFF74A20 1A5 LocalFree 
    BFF9137C 157 GetVersion 
    BFF8AECD 76 ExitProcess 
    BFF85C93 1FA RtlUnwind 
    USER32.dll 
    3071456F time date stamp Tue Oct 03 17:15:11 1995 
    BFF610F6 225 WinHelpA 
    BFF6235F D8 GetDC 
    BFF6456D 124 GetWindowRect 
    BFF62D94 46 CreateDialogParamA 
    BFF616AO 31 CheckRadioButton 
    BFF64BOC 168 LoadStrlngA 
    BFF62D11 196 RegisterClassExA 
    BFF6439F 114 GetSysColorBrush 
    BFF64F8F 15A LoadCursorA 
    BFF64F79 15E LoadIconA 
    BFF61631 1C6 SetDlgItemTextA 
    BFF64516 DD GetDlgItem 
    BFF62038 B9 FillPed: 
    BFF64954 1DB SetRect 
    BFF62B96 2F CheckMenuItem 
    BFF61A4C 112 GetSubMenu 
    BFF622DB F3 GetMenu 
    BFF61476 1EC SetWindowPos 
    BFF616C1 16E MapDialogRect 
    BFF62032 13B InvalidateRect 
    BFF62347 1C9 SetFocus 
    BFF64697 14D Islconic 
    BFF6437E 113 GetSysColor 
    BFF623B9 80 DestroyMenu 

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

    BFF61B39 20А TrackPopupMenuEx 
    BFF61249 164 LoadMenuA 
    BFF61D94 1A2 ReleaseCapture 
    BFF6234F 1B8 SetCapture 
    BFF64550 DC GetDlgCtrlID 
    BFF62B9E A5 EnableMenuItem 
    BFF6480F 147 IsClipboardFormatAvailable 
    BFF61F52 1A3 ReleaseDC 
    BFF64627 1A9 ScreenToClient 
    BFF62391 81 DestroyWindow 
    BFF62381 190 PostQuitMessage 
    BFF6512B 78 DefWindowProcA 
    BFF623C1 175 MessageBeep 
    BFF61718 126 GetWindowTextA 
    BFF619EF 99 OrawFrameControl 
    BFF645AD CF GetClientRect 
    BFF62C68 1F6 ShowCursor 
    BFF62B5A 1CO SetCursor 
    BFF61790 AA EndPaint 
    BFF619F5 96 DrawEdge 
    BFF6173F 9 BeginPaint 
    BFF62B48 37 CloseClipboard 
    BFF64055 2A CharUpperA 
    BFF61165 D1 GetClipboardData 
    BFF62C70 187 OpenClipboard 
    BFF61508 1EE SetWindowTextA 
    BFF61F66 A7 EnableWindow 
    BFF61681 2E CheckDIgButton 
    BFF61F5A 1F9 ShowWindow 
    BFF623A9 219 UpdateWindow 
    BFF651CF 50 CreateWindowExA 
    BFF64DB6 1AF SendMessageA 
    BFF6124D 156 LoadAcceleratorsA 
    BFF64CBD FF GetMessageA 
    BFF64D22 20C TranslateAcceleratorA 
    BFF64CE9 20F TranslateMessage 
    BFF64D7C 149 IsDialogMessageA 
    BFF63E5F 86 DispatchMessageA 
    BFF638D9 176 MessageBoxA 
    GDI32.dll 
    3071456F time date stamp Tue Oct 03 17:15:11 1995 
    BFF34517 ВС Get8tock0b]ect 
    BFF3134F 8Е GetDeviceCaps 
    BFF310E1 CD GetTextMetricsA 
    BFF34D39 120 SetTextColor 
    BFF34D40 10C SetBkColor 
    BFF321A9 13D TextOutA 
    BFF31B36 C9 GetTextExtentPointA 
    BFF324E7 10D SetBkMode 
    BFF3215A 106 SelectObject 
    BFF3249F 46 DeleteObject 

    _______________Глава 12. Динамически подключаемые библиотеки 381 

    Summary 
    1000 .bss 
    2000 .data 
    1000 .idata 
    2000 .reloc 
    2000 . rsrc 
    AOOO .text 

    При запуске ЕХЕ-файла загрузчик операционной системы просматривает таблицу 
импорта, содержащуюся в этом файле, и пытается найти и спроецировать на 
адресное пространство нового процесса все необходимые DLL. Затем он сохраняет 
адреса идентификаторов, используемых ЕХЕ-файлом, в особой таблице. 
Естественно, это занимает какое-то время, но делается лишь при запуске 
процесса. Всякий раз, когда приложение ссылается на один из таких 
идентификаторов, сгенерированный компилятором код выбирает его адрес из 
таблицы и предоставляет необходимую связь. Число слева от импортируемого 
идентификатора называется «подсказкой» (hint); оно ускоряет вычисление 
загрузчиком адресов идентификаторов. Слева от «подсказки» расположен адрес 
функции — если исполняемый файл привязан к какой-либо DLL.2 
    Импортируя идентификатор, ключевое слово _declspec(dllimport) использовать 
не обязательно. Вместо него можно взять стандартное ключевое слово extern. Но 
компилятор генерирует более эффективный код, если ему заранее известно, что 
идентификатор будет импортирован из DLL. Так что советую применять для 
импортируемых функций и переменных _declspec(dllimport). 
    Поток получает адрес экспортируемой из DLL функции или переменной вызовом GetProcAddress: 

    FARPROC GetProcAddress(HINSTANCE hinstDll, LPCSTR IpszProc); 

    Параметр hinstDll— описатель DLL. Это значение можно получить с помощью 
LoadLibrary или LoadLibraryEx. Параметр IpszProc разрешается указывать в двух 
формах. Во-первых, как адрес строки с нулевым символом в конце, содержащей имя 
интересующей Вас функции: 

    Ipfn = GetProcAddress(hinstDll, "SomeFuncInDll"); 

    Заметьте: тип параметра IpszProc — LPCSTR, а не LPCTSTR. Это значит, что 
функция GetProcAddress принимает только ANSI-строки — ей нельзя передать 
Unicode-строку. А причина в том, что идентификаторы функций и переменных в 
таблице экспорта DLL всегда хранятся как ANSI-строки. 
    Вторая форма параметра IpszProc позволяет указывать порядковый номер 
нужной функции: 

    Ipfn = GetProcAddress(hinstDll, MAKEINTRESOURCE(2)): 

    Здесь подразумевается, что нам известен порядковый номер (2) функции 
SomeFuncInDll, присвоенный ей в данной DLL. И вновь повторю: Microsoft 
настоятельно советует не использовать порядковых номеров; поэтому Вы редко 
встретите второй вариант вызова GetProcAddress. 
    Оба способа дают адрес содержащейся в DLL функции SomeFuncInDll. Если 
функция не найдена, GetProcAddress возвращает NULL. Однако и тот, и другой 
способ не без недостатков. Первый медленнее, так как системе приходится 
проводить поиск и сравнивать строки. Во втором, если Вы передаете порядковый 
номер, не присвоенный ни одной из экспортируемых функций, GetProcAddress может 
вернуть значение, отличное от NULL. (Это относит
    2. Привязка DLL осуществляется с помощью утилиты BIND. С Visual C++ она не 
поставляется, но входит в Win32 SDK. 

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

    ся и к 16-разрядной Windows.) В итоге Ваша программа, ничего не 
«подозревая», получит неправильный адрес. Попытка вызова функции по этому 
адресу почти наверняка приведет к нарушению доступа. Я и сам — когда только 
начинал программировать под Windows и не очень четко понимал эти вещи — 
несколько раз попадал в эту ловушку. Так что будьте внимательны. (Вот Вам, 
кстати, и еще одна причина, почему от использования порядковых номеров следует 
отказаться в пользу символических имен — идентификаторов.) 

    Заголовочный файл DLL 

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

    // MYLIBAPI определена в файле реализации MyLib.C как __declspec(dllexport). 
    // Поэтому при включении заголовочного файла в MyLib.C, функции будут 
    // экспортироваться, а не импортироваться. 
    Bifndef MYLIBAPI 
    ^define MYLIBAPI __declspec(dllimport) 
    ffendif 
    
    MYLIBAPI int Add(int nLeft, int nRight); 
    MYLIBAPI int g_nUsageCount; 
    Этот заголовочный файл надо включать в самое начало исходных файлов Вашей 
DLL следующим образом: 
    // Мы должны определить MYLIBAPI как __declspec(dllexport) до включения 
    // файла MyLib.H. Тогда MyLib.H, "увидев", что мы уже определили MYLIBAPI, 
    // не станет (пере)определять ее как __declspec(dllimport). 
    ffdefine MYLIBAPI __declspec(dllexport) 
    ftinclude "MyLib.H" 
    Illllllllllllllltttllllllllllllllllllllllllllllllllllllllllllllllllllllllltlllllll 
    int Add(int nLeft, int nRight) { 
    retLirn(nLeft + nRight); 
    } 
    MYLIBAPI int g_nUsageCount; 

    Поскольку MYLIBAPI определена как _declspec(dllexport) явно, то при 
компиляции исходного кода Вашей программы компилятор узнает, что функции 
экспортируются. А это позволяет не повторять _declspec(dllexport) перед каждой 
процедурой или переменной. Обратите внимание, что перед телом функции Add нет _declspec(dllexport). 
    Что касается исходного кода, импортирующего из DLL идентификаторы, то Вам 
нужно всего лишь включить в него заголовочный файл. Теперь MYLIBAPI будет 
опреде

    ______________Глава 12. Динамически подключаемые библиотеки 383 

    лена как _declspec(dllimport), и компилятор «поймет», какие идентификаторы 
содержатся в DLL. Просмотрев заголовочные файлы Windows, например WINUSER.H, 
Вы увидите, что Microsoft применяет тот же метод. 

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

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

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

    экспортируется Visual C++ в таблицу экспорта DLL как _MyFunc@8. Если Вы 
решите создать ЕХЕ-файл с помощью средств разработки от другого поставщика, то 
компоновщик попытается скомпоновать функцию _MyFunc, которой нет в файле DLL, 
созданном Visual C++, и, естественно, произойдет ошибка. 
    Чтобы собрать в Visual C++ библиотеку DLL, способную работать с 
инструментарием от другого поставщика, нужно указать Visual C++ экспортировать 
имя функции без искажений. Сделать это можно двумя способами. 
    Первый — создать DEF-файл для Вашего проекта в Visual C++ и включить в 
него раздел EXPORTS: 

    EXPORTS 
    MyFunc 

    Компоновщик Visual C++, анализируя этот DEF-файл, увидит, что 
экспортировать надо обе функции: _MyFunc@8 и MyFunc. Поскольку их имена 
идентичны (не считая вышеописанных искажений), компоновщик на основе 
информации из DEF-файла экспортирует только функцию с именем MyFunc, а 
функцию_МуРмпс@8 не экспортирует вообще. 
    Может, Вы подумали, что при сборке ЕХЕ-файла с такой DLL в Visual C++ 
компоновщик, ожидая имя _MyFunc@8, не найдет Вашу функцию? В таком случае Вам 
будет приятно узнать, что Visual C++ все сделает правильно и корректно 
скомпонует ЕХЕ-файл с функцией MyFunc. 
    Если Вам не по душе использование DEF-файла, можете экспортировать 
неискаженное имя функции еще одним_способом. Добавьте в один из файлов 
исходного кода DLL такую строку: 

    ftpragma comment(linker. "/export;MyFunc=_MyFunc@8") 

    Тогда компилятор потребует от компоновщика экспортировать функцию MyFunc с 
той же точкой входа, что и _MyFunc@8. Этот способ менее удобен, чем первый, 
так как здесь приходится вставлять дополнительную директиву с искаженным 
именем функции. И еще один минус этого способа в том, что из DLL 
экспортируются два идентификатора одной и той же функции: MyFunc и MyFunc@8, 
тогда как при первом способе — только идентификатор MyFunc. По сути, второй 
способ не имеет особых преимуществ перед первым — он просто избавляет от DEF-файла. 

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

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

    Совместное использование данных несколькими проекциями ЕХЕ или DLL 

    Как Вы теперь знаете, система создает отдельные экземпляры любых 
глобальных и статических переменных, содержащихся в образе ЕХЕ- или DLL-файла 
для каждой его проекции. Иначе говоря, если в ЕХЕ есть глобальная переменная и 
Вы запускаете 2 или более копии приложения, каждый процесс получает свою копию 
переменной, т. е. она не разделяется несколькими экземплярами ЕХЕ. Обычно это 
как раз то, что нужно. Но иногда удобнее, чтобы несколько проекций ЕХЕ 
разделяли единственный экземпляр переменной. 
    Например, в Win32 нет простого способа, позволяющего определить, запущено 
ли несколько копий приложения. А если бы у Вас была переменная, доступная всем 
копиям приложения, она могла бы отражать число запущенных копий. Тогда при 
запуске нового экземпляра приложения его поток просто проверил бы значение 
глобальной переменной (обновленное другой копией приложения) и, будь оно 
больше 1, сообщил бы пользователю, что запустить можно лишь одну копию, после 
чего этот экземпляр приложения благополучно бы и завершился. 
    В этом разделе мы рассмотрим метод, обеспечивающий совместное 
использование переменных всеми экземплярами ЕХЕ или DLL. Но сначала Вам 
понадобятся кое-какие базовые сведения. 

    Разделы в ЕХЕ- и DLL-файлах 

    Любой образ ЕХЕ- или DLL-файла состоит из группы разделов (sections). По 
соглашению имя каждого стандартного раздела начинается с точки. Например, при 
компиляции программы исполняемый код помещается в раздел .text, 
неинициализированные данные — в раздел .bss, а инициализированные — в раздел .data. 
    С каждым разделом связана комбинация следующих атрибутов: 

    Атрибут       Описание 

    READ          Разрешает чтение из раздела. 
    WRITE         Разрешает запись в раздел. 
    EXECUTE      Содержимое раздела можно исполнять. 
    SHARED       Раздел доступен нескольким экземплярам приложения. 

    Запустив утилиту DumpBin, можно просмотреть список разделов в ЕХЕ- или 
DLLфайле. Вот какие результаты я получил для файла MODULE.DLL (с 
компакт-диска, прилагаемого к книге): 

    Microsoft (R) COFF Binary File Dumper Version 3.00.5270 Copyright (С) 
Microsoft Corp 1992-1995. All rights reserved. 
    Dump of file module.oil 


    _______________Глава 12. Динамически подключаемые библиотеки 385 


    File Type: DLL 
    Summary 
    1000 .data 
    1000 .idata 
    1000 . rdata 
    1000 . reloc 
    1000 .text 
    1000 Shared 

    Кроме представленного выше краткого списка разделов, можно получить и 
более подробный, указав при запуске DumpBin ключ -HEADERS. Вот некоторые из 
часто встречающихся разделов: 

    Имя раздела   Описание 

    .text           Код приложения или DLL. 
    .bss           Неинициализированные данные. 
    .rdata          Неизменяемые данные периода выполнения. 
    .rsrc             Ресурсы. 
    .edata          Таблица экспортируемых имен. 
    .data           Инициализированные данные. 
    .xdata          Таблица для обработки исключений. 
    .idata          Таблица импортируемых имен. 
    .CRT          Неизменяемые данные стандартной библиотеки С. 
    .reloc           Настроечная информация — таблица переадресации (fixup table). 
    .debug         Отладочная информация. 
    .tis             Локальная память потока. 

    Заметьте: дополнительный раздел с именем Shared в MODULE.DLL создан мной. 
Вы тоже можете создать свои разделы в ЕХЕ- или DLL-файле, используя директиву компилятора: 

    ffpragma data_seg("segname") 

    Например, в файле MODULE.C есть такие строки: 
    
    #pragma data_seg("Shared") 
    LONG g_lModuleUsage = 0; 
    йргадта data_seg() 

    Обрабатывая этот код, компилятор создаст раздел Shared и поместит в него 
все инициализированные переменные, встретившиеся после директивы #pragma. В 
нашем примере в разделе Shared находится переменная g_lModuleUsage. Директива 
#pragma data_seg() сообщает компилятору, что следующие за ней переменные нужно 
вновь помещать в стандартный раздел данных, а не в Shared. Важно помнить, что 
компилятор помещает в новый раздел только инициализированные переменные; 
неинициализированные всегда включаются в раздел .bss. И если бы в предыдущем 
фрагменте не было указано каких-нибудь инициализированных переменных, 
компилятор разместил бы все переменные в .bss: 

    #pragma data_seg("Shared") 
    LONG g_lModuleUsage; 
    
    #pragma data_seg() 


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

    Чаще всего переменные помещают в отдельные разделы, чтобы «разделить» их 
между несколькими проекциями ЕХЕ или DLL. По умолчанию каждая проекция 
получает свой набор переменных. Но можно сгруппировать в отдельном разделе 
переменные, которые должны быть доступны всем проекциям ЕХЕ или DLL; тогда 
система не станет создавать новые экземпляры этих переменных для каждой новой 
проекции ЕХЕ или DLL. 
    Но, чтобы переменные стали разделяемыми, одного указания компилятору 
выделить их в какой-то раздел мало. Надо также сообщить компоновщику, что 
переменные в этом разделе должны быть общими. Для этого предназначен ключ 
/SECTION компоновщика: 

    /SECTION;имя,атрибуты 

    За двоеточием укажите имя раздела, атрибуты которого Вы хотите изменить. 
    В файле MODULE.DLL мы собираемся изменить атрибуты раздела Shared. Их 
следует указать после имени раздела, отделив запятой. При этом используются 
такие сокращения: 
    R (READ), W (WRITE), E (EXECUTE) и S (SHARED). Скажем, чтобы раздел Shared 
стал «читаемым», «записываемым» и «общим», ключ должен выглядеть так: 

    /SECTION:Shared,RWS 

    Чтобы изменить атрибуты более чем у одного раздела, укажите ключ /SECTION 
несколько раз — по разу на каждый такой раздел. 
    Можете вставить в исходные файлы своей программы и директивы компоновщика: 

    ftpragma comment(linker, "/SECTION:Shared,RWS") 

    Эта строка заставляет компилятор включить «/SECTION: Shared,RWS» в особый 
раздел «.drectve». Компоновщик, собирая OBJ-модули, проверяет раздел 
«.drectve» каждого OBJмодуля и действует так, словно все эти строки переданы 
ему как аргументы в командной строке. Я всегда применяю этот очень удобный 
метод: перемещая файл исходного кода в новый проект, не надо изменять никаких 
параметров в диалоговом окне Build Settings в Visual C++. В частности, эта 
директива есть и в моем универсальном заголовочном файле CmnHdr.H (см. 
приложение В). 
    Честно говоря, хоть создание общих разделов и возможно, Microsoft 
настоятельно рекомендует их не использовать. Во-первых, разделение памяти 
таким способом нарушает защиту уровня В (B-level security). Во-вторых, наличие 
общих переменных означает, что ошибка в одном приложении может повлиять на 
другое, так как этот блок данных не удастся защитить от случайной записи. 
    Представьте, Вы написали 2 приложения, каждое из которых требует от 
пользователя ввода пароля. При этом Вы решили чуть-чуть «упростить жизнь» 
пользователю: если одна из программ уже исполняется на момент запуска другой, 
то вторая считывает пароль из общей памяти. Так что пользователю не нужно 
повторно вводить пароль, если одно из приложений уже запущено. 
    Все выглядит вполне невинно. В конце концов только Ваши приложения 
загружают данную DLL, и только они знают, где искать пароль, содержащийся в 
общем разделе памяти. Но хакеры не дремлют, и если им захочется узнать Ваш 
пароль, то максимум, что им понадобится, — написать программу, загружающую 
Вашу DLL, и понаблюдать за общим блоком памяти. Когда пользователь введет 
пароль, «хакерская» программа тут же его узнает. 
    Трудолюбивая «хакерская» программа может также предпринять серию попыток 
угадать пароль, записывая его варианты в общую память. А угадав, сможет 
послать любую команду любому из этих двух приложений. 
    Эту проблему можно было бы решить, если бы существовал какой-нибудь способ 
разрешать загрузку DLL только определенным программам. Но пока это невозможно 
— любая программа, вызвав LoadLibrary, способна явно загрузить DLL. 

    ______________Глава 12. Динамически подключаемые библиотеки 387 


    Программа-пример ModUse 

    Я говорил, что Win32 не поддерживает функцию GetModuleUsage из 
16-разрядной Windows. Но ее можно реализовать самостоятельно на принципе 
разделения памяти. Как это делается, демонстрируют файлы MODUSE.EXE и 
MODULE.DLL. Файлы исходного кода и ресурсов для этих программ находятся в 
каталоге MODUSE на компакт-диске, прилагаемом к книге. Рассмотрим сначала 
MODULE.DLL (см. листинг на рис. 12-3). 

    MODULE.C 
    Модуль: Module.С 
    Автор: Copyright © 1995-1997, Джеффри Рихтер (Jeffrey Richter) 
    #include "..\CmnHdr.H"           /* см. приложение В */ 
    ^include <windows.h> 
    ffdefine MODULEAPI __declspec(dllexport) 
    ftinclude "Module.H" 
    //////////////'////////;//,'-•!:!./^/i'^/":'^'fi^•//f"//':l• ! ! •    1' 
    // Указываем компилятору поместить переменную g_lModuieUsage в 01днльный 
    // раздел данных с именем Shared. Далее сообщаем компоновщику, что данные 
    // в этом разделе должны быть доступны всем выполняемым копиям приложения. 
    ftpragma data_seg("Shared") 
    LONG g_lModuleUsage = 0; 
    ftpragma data_seg() 
    // указываем компоновщику сделать раздел Shared доступным 
    // по чтению и записи, а также общим 
    ftpragma comment(linker, "/section:Shared,rws") 
    ///////////////"/'/////'!///'!/'//"/////''"'/'/m//f///'//f'/г//'"г///"" 
    II g_uMsgModCountChang'e могла бы быть разделяемой, но я решил этого не делать 
    UINT g_uMsgModCntChange = 0; 
    llllllllllllllllllllll//////l/l/!:i!!|!i!!l^i!i^^!!!i!l!!! "! ' ^ 'i''/"" 
    BOOL WINAPI DllMain (HINSTANCE hinstDll, DWORD fdwReason, LPVOID 
IpvReserved) { 
    switch (fdwReason) { 
    case DLL_PROCESS_ATTAGH: 
    // DLL проецируется на адресное пространство процесса 
    // увеличиваем счетчик числа пользователей DLL 
    // при ее подключении к процессу 
    InterlockedIncrernent((PLONG) &g_lModuleUsage); 

    см. след. стр. 

    Рис. 12-3. Листинг динамически подключаемой библиотеки Module 


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


    РИС. 12-3   (продолжение) 

    II Резервируем для себя общесистемное сообщение, Оно используется 
    // для уведомления всех окон верхнего уровня об изменении счетчика 
    // числа пользователей этого модуля. 
    Q_uMsQModCntChange = RegisterWindowMessage(__TEXT("MsgModUsgCntChange")); 
    
    // уведомляем все окна верхнего уровня об изменении 
    // счетчика числа пользователей этого модуля 
    PostMessage(HWND_BROADCAST, g_uMsgModGntChange, О, О); 
    Preak; 
    case DLL_THREAD_ATTACH: 
    // создаем новый поток в текущем процессе 
    break; 
    case DLL_THREAD_DETACH: 
    // поток завершается корректно 
    break; 
    case DLL_PROCESS_DETACH: 
    // DLL выгружается из адресного пространства процесса 
    
    // уменьшаем счетчик числа пользователей DLL 
    // при ее отключении от процесса 
    InterlockedDecrement((PLONG) &g_lModuleUsage); 
    
    // уведомляем все окна верхнего уровня об изменении 
    // счетчика числа пользователей этого модуля 
    PostMessage(HWND_BROAOCAST, g_uMsgModCntChange. О, 0); 
    break; 
    } 
    return(TRUE); 
    /////////////////////////////////////////////////////////////////////////////// 
    MODULEAPI LONG GetModuleUsage (void) { 
    return(g_lModuleUsage); 
    } 
    IHIIIIIIIIIIIIIIII/IIIH//11/1111 конец файла 111/11111111111111111111111111111 

    MODULE.H 
    /....,*.*****....***......****.,...*,*****.,*„,.»,....*..*,..,.*......,******. 
    Модуль; Module.H 
    Автор: Copyright © 1995-1997, Джеффри Рихтер (Jeffrey Richter) 

    см. след. стр. 


    _______________Глава 12. Динамически подключаемые библиотеки 389 


    РИС. 12-3  (продолжение) 

    #lf !defined(MODULEAPI) 
    «define MODULEAPI __declspec(dllimport) 
    #endif 
    // функция, возвращающая счетчик числа пользователей данного модуля 
    MODULEAPI LONG GetModuleUsage (void); 
    I 1111111111.111111/1111111111111111 конец файла 1111111111111 i 111111 
/111 111111111 

    Самое важное в MODULE.С то, что я создал глобальную переменную 
g_lModuleUsage в отдельном разделе, указав компоновщику (ключом /SECTION), что 
этот раздел общий. Я также присвоил переменной 0. Всякий раз, когда MODULE.DLL 
проецируется на адресное пространство процесса, DllMain вызывается со 
значением DLL_PROCESS_ATTACH. Библиотека обрабатывает это так: 

    InterlockedIncre(nent(&g_lModuleUsege): 

    тем самым увеличивая значение переменной на 1. Может, Вас удивило, что это 
я увеличиваю значение вызовом Interlockedlncrement вместо того, чтобы просто написать: 

    g_lModuleUsage++: 

    Согласен: разница между этими двумя операторами незначительна и чаще всего 
просто незаметна. Но, если бы я использовал только постфиксный С-оператор 
приращения, могла бы возникнуть проблема. Если два процесса одновременно 
вызовут LoadLibrary для загрузки MODULE.DLL, значение g_lModuleUsage может 
быть повреждено. А если Вы применяете функцию Interlockedlncrement, система 
гарантирует, что в каждый момент доступ к этим 4 байтам памяти разрешен не 
более чем одному процессу. 
    Другая операция, связанная со счетчиком числа пользователей MODULE.DLL, — 
его уменьшение при каждом вызове DllMain со значением DLL_PROCESS_DETACH. Это 
делается через InterlockedDecrement. Подробнее о функциях Interlockedlncrement 
и InterhckedDecrement см. главу 10. 
    Кроме DllMain, в данной DLL содержится всего одна функция GetModuleUsage-. 

    LONG GetModuleUsage(void); 

    У нее нет параметров; она просто возвращает значение переменной 
g_lModuleUsage. Приложение может вызвать эту функцию, чтобы определить, 
сколько процессов спроецировало MODULE.DLL на свои адресные пространства. 
    Чтобы слегка оживить пример, я написал программу ModUse (MODUSE.EXE) (см. 
листинг на рис. 12-4). Эта крошечная программа, открыв на экране диалоговое 
окно, ждет зарегистрированное оконное сообщение3 «MsgModCntChange», которое 
регистрируется модулем MODULE.DLL, когда его функция DllMain вызывается со 
значением DLL_PROCESS_ATTACH. Оно регистрируется и модулем MODUSE.EXE при 
вызове его функции WinMain. Код сообщения сохраняется в глобальной переменной 
g_uMsgModCntChange. 
    Когда DLL подключается к процессу или отключается от него, она вызывает: 

    PostMessage(HWND_BROADCAST, g_uMsgModCntChange, О, О); 

    Это приводит к передаче кода зарегистрированного сообщения всем окнам 
верхнего уровня в системе. В данном случае это сообщение могут распознать 
только диалоговые окна, СО3, Подробнее о регистрируемых оконных сообщениях см. 
описание функции RegisterWindowMessage в справочнике Microsoft Win32 
Programmer's Reference. 

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

    зданные выполняемыми экземплярами ModUse. Процедура диалогового окна в 
MODUSE.C  ; 
    содержит явную проверку на это сообщение*: 

    if (uMsg == g_uMsgModCntChange) { 
    SetDlgIternInt(hDlg, IDC_MODCNT, GetModuleUsage(), FALSE):                 
    } 

    Получив данное сообщение, программа вызывает из DLL функцию 
GetModuleUsage, чтобы узнать текущее значение счетчика. Далее это значение 
передается в статический элемент управления (метку) — дочернее окно 
диалогового окна. 
    При запуске программы ModUse файл MODULE.DLL неявно проецируется на 
адресное пространство процесса. Подключение DLL к процессу вызывает отправку 
зарегистрированного сообщения всем окнам верхнего уровня. Все эти окна, кроме 
диалогового окна ModUse, игнорируют данное сообщение. А диалоговое окно 
ModUse, получив это сообщение, вызывает GetModuleUsage из DLL, чтобы узнать 
значение счетчика (оно равно 1), которое затем выводится в диалоговом окне: 

    ^HWMWM-l^t 
    Module usage:     1                                                        
              '• 
    При запуске второго экземпляра ModUse произойдет та же последовательность 
со-  ' бытии. На этот раз счетчик MODULE.DLL равен 2; на экране два диалоговых 
окна, способ-  j ных обработать зарегистрированное сообщение. Процедуры обоих 
диалоговых окон вы-  | зывают GetModuleUsage, «видят», что счетчик равен 2, и 
соответственно обновляют содержимое своих окон:                                
                                ] 
    |ияш.||шш.1жп-га   ШШ.-.. „: .. • • „„ .^-l2J 
    Module usage:     2      Module usage:     2 

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

    MODUSE.C 
    Модуль: ModUse.С 
    Автор: Copyright © 1995-1997, Джеффри Рихтер (Jeffrey Richter) 
    ^include "..\CmnHdr.H"              /* см. приложение В */ 
    ftinclude <windows.h> 
    ttinclude <windowsx.h> 
    ftinclude <tchar.h> 
    ^include "Resource.H" 
    #lnclude "..\Module\Module.H" 

    см. след. стр. 

    Рис. 12-4. Листинг программы ModUse 

    * Прим. перев.: в исходном тексте MODUSE.C вместо IDC_MODCNT указано IDC_USAGECOUNT. 


    _________Глава 12. Динамически подключаемые библиотеки 391 


    РИС. 12-4  (продолжение) 


    IIIIHIIIIIIIIIIIIIIIIIIIII/ll/llll/lll/lllfllll/lll/lllll/llllllllllllllllllll 
    UINT g_uMsgModCntChange = 0; 
    /////////////////////////////////////////////////////У///////1////.1.1 
•//".••, • 
    BOOL Dlg_OnInitDialog (HWND hwnd, HWND hwndFocus, LPARAM IParam) { 
    // связываем значок с диалоговым окном 
    chSETDLGICONS(hwnd, IDI_MODUSE, IDI_MODUSE); 
    // принудительно инициализируем статический элемент управления 
    Pos"EMessage(hwnd, g_uMsgModCntChange, О, О); 
    return(TRUE); 
    \ ( 
    !llllll/IIIIIIIIHt//ll//lll/llll/lll//ill/l//llll/illlllllllllllllllllllllllll 
    void Dlg_OnCommand (HWND hwnd, int id, HWND hwndCtI, UINT codeNotify) { 
    switch (id) { 
    case IDCANCEL: 
    EndDialog(hwnd, id); 
    break; 
    } 
    ! 
    //////////////////////////////////////////////7/////1•-//////7/,/.11//.// 
•• ' • • •" ',1'',/11/1 • 
    BOOL CALLBACK Dlg_Proc (HWND hwnd, UINT uMsg, WPARAM wParain, LPARAM 
IParam) { 
    if (uMsg == g_uMsgModCntChange) { 
    SetDlgItemInt(hwnd, IOC_USAGECOUNT, 
    GetModuleUsageO, FALSE); 
    } 
    switch (uMsg) { 
    chHANDLE_DLGMSG(hwnd, WM_INITDIALOG, Dlg_OnInitDialog); 
    chHANDLE_DLGMSG(hwnd. WM_COMMAND, Dlg_OnCommand); 
    } 
    return(FALSE); 
    } 
    ltlllllllllt/lll/lll/l/l/l/l//lll/l/ll////lllll/lllllllll/lllllllll/lllllllllll 
    int WINAPI _tWinMain (HINSTANCE hinstExe, 
    HINSTANCE hinstPrev, LPTSTR pszCmdLine, int nCmdShow) { 
    chWARNIFUNICODEUNDERWIN95(); 

    см. след. стр. 

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


    РИС. 12-4  (продолжение) 

    II получаем числовой код общесистемного сообщения, используемого 
    // DLL для уведомления окон верхнего уровня об изменении счетчика 
    // числа ее пользователей 
    9_uMsgModCntChange = RegisterWindowMessage(__TEXT("MsgModUsgCntChange")); 
    DialogBox(hinstExe, MAKEINTRESOURCE(IDD_MODUSE), NULL, Dlg_Proc); 
    return(O); 
    111111111/11/11/IIIIIIIIIIIIHIIII конец файла 1111//11111111111//11111111111111 


    Программа-пример Multlnst 

    В 16-разрядной Windows многие программисты через параметр hinstExePrev 
функции WinMain определяли, запущен ли уже экземпляр какой-нибудь программы. 
Приложения, не позволявшие запускать более одной копии, проверяли значение 
hinstExePrev и завершались, если оно не равно NULL. В Win32 это значение 
всегда NULL. Поэтому в Win32 не так просто определить, запущен ли уже 
экземпляр программы. 
    Один из способов определения числа запущенных копий программы мы 
рассмотрели в предыдущем разделе. Программа Multlnst (MULTINST.EXE) (см. 
листинг на рис. 12-5) демонстрирует, как запретить запуск более одной копии 
приложения. Файлы исходного кода и ресурсов для этой программы находятся в 
каталоге MULTINST на компакт-диске, прилагаемом к книге. 
    MULTINST.C 
    /.*«,**,,,***..***»,...***.***,*.,.**.,.***..*,^». .,*..... *.,........,*....,. 
    Модуль: Multlnst.С 
    Автор: Copyright © 1995-1997, Джеффри Рихтер (Jeffrey Richter) 
    .**.,.,**,.*,*,**,..***..**,***.****.******,.*....*,..,.**.».,*..,****.****,.*/ 
    
    ftinclude ".,\CmnHdr,H"              /* см. приложение В */ 
    ttlnclude <windows.h> 
    ftinclude <tchar.h> 
    tfinclude "Resource.H" 
    Illlllllllllllllll/l/lllllll/lllllllllllllllllilllllllli/lllltll/llllllllllllll 
    
    II Указываем компилятору поместить переменную g_lModulel)sage в отдельный 
    // раздел данных с именем Shared. Далее сообщаем компоновщику, что данные 
    // в этом разделе должны быть доступны всем выполняемым копиям приложения. 
    йргадта data_seg("Shared") 
    LONG g_lUsageCount = -1; 
    #pragma data_seg() 
    // указываем компоновщику сделать раздел Snared доступным 
    // по чтению и записи, а также общим 
    ffpragma con)ment(linker, "/section:Shared, rws") 

    см. след. стр. 

    Рис. 12-5. Листинг программы Multlnst 

    _____________Глава 12. Динамически подключаемые библиотеки 393 


    РИС. 12-4  (продолжение) 
    llll/1111111111111111111/ill/illllllllll/lllllllllll/llllll/lllllllllllllllllll 
    int WINAPI _tWinMain (HINSTANCE hinstExe, 
    HINSTANCE hinstPrev, LPTSTR pszCmdLine, int nCmdShow) { 
    // экземпляр запущен: увеличить счетчик 
    BOOL fFirstInstance = (InterlockedIncrement(&g_lUsageCount) == 0); 
    chWARNIFUNICODEUNDERWIN95(); 
    // если запущено более одной копии, сообщить об этом 
    // пользователю и завершить программу 
    if (!fFirstInstance) { 
    MessageBox(NULL, 
    
    __ТЕХТ("Application is already running - ") 
    __TEXT("Terminating this instance."), 
    __TEXT("Multiple Instance"), 
    MB_OK | MB_ICONINFORMATION); 
    } else { 
    // мы - единственная запущенная копия: 
    // ждем, пока пользователь нас не завершит 
    MessageBox(NULL, 
    
    __ТЕХТ("Running first instance of application.\n") 
    __TEXT("Select OK to terminate."), 
    __TEXT("Multiple Instance"), 
    MB_OK | MB_ICONINFORMATION); 
    } 
    // копия больше не исполняется: уменьшить счетчик 
    InterlockedDecrecnent(&g_lusageCount): 
    return(O): 
    } 
    //////////////////////// конец файла I / I I 11 i I 11 11 i I I I I i i I 
