ЭКЗАМЕНАЦИОННЫЙ БИЛЕТ № 16. 1. Ресурсы. Иконки
1. Ресурсы. Иконки.
2. Наследование описателя объекта.
Наследование применимо, только когда процессы связаны родственными отношениями (родительский-дочерний). Например, родительскому процессу доступен один или несколько описателей объектов ядра, и он решает, породив дочерний процесс, передать ему по наследству доступ к своим объектам ядра. Чтобы такой сценарий наследования сработал, родительский процесс должен выполнить несколько операций.
Во-первых, еще при создании объекта ядра этот процесс должен сообщить системе, что ему нужен наследуемый описатель данного объекта. (Имейте в виду описатели объектов ядра наследуются, но сами объекты ядра — нет.)
Чтобы создать наследуемый описатель, родительский процесс выделяет и инициализирует структуру SECURITY_ATTRIBUTES, а затем передает ее адрес требуемой Create-функции. Следующий код создаст объект-мьютекс и возвращает его описатель:
SECURITY_ATTRIBUTES sa;
sa.nLength = sizeof(sa);
sa.lpSecuntyDescriptor = NULL;
sa.bInheritHandle =- TRUE; // делаем возвращаемый описатель наследуемым
HANDLE hMutex = CreateMutex(&sa, FALSE, NULL);
Этот код инициализирует структуру SECURTY_ATTRIBUTES, указывая, что объект следует создать с защитой по умолчанию (в Windows 98 это игнорируется) и что возвращаемый описательдолжен быть наследуемым.
WINDOWS 98:
Хотя Windows 98 не полностью поддерживает защиту, она все же поддержива-
ет наследование и поэтому корректно обрабатывает элемент bInheritHandle.
А теперь перейдем к флагам, которые хранятся в таблице описателей, принадлежащей процессу В каждой ее записи присутствует битовый флаг, сообщающий, является данный описатель наследуемым или нет. Если Вы, создавая объект ядра, передадите в парамере типа PSECURITY_ATTRIBUTES значение NULL, то получите ненаследуемый описатель, и этот флаг будет нулевым. А если элемент bInheritHandle равен TRUE, флaгy пpиcвaивaeтcя 1.
Допустим, какому-то процессу принадлежит таблица описателей, как в таблице 3-2.
Индекс | Указатель на блок памяти объекта ядра | Маска доступа (DWORD с набором битовых флагов) | Флаги (DWORD с набором битовых флагов) |
0xF0000000 | 0x???????? | 0x00000000 | |
0x00000000 | (неприменим) | (неприменим) | |
0xF0000010 | 0х???????? | 0x00000001 |
Таблица 3-2. Таблица описателей с двумя действительными записями
Эта таблица свидетельствует, что данный процесс имеет доступ к двум объектам ядра: описатель 1 (ненаследуемый) и 3 (наследуемый)
Следующий этап — родительский процесс порождает дочерний. Это делается с помощью функции CreateProcess,
BOOL CreateProcess(
PCTSTR pszApplicationName,
PTSTR pszCommandLine,
PSECURITY_ATTRIBUTES psaProcess,
PSECURITY_ATTRIBUTES psaThread,
BOOL bInheritHandles,
DWORD fdwCreale,
PVOIO pvEnvironment,
PCTSTR pszCurDir,
PSTARTUPINFO psiStartInfo,
PPROCESS_INFORMATION ppiProcInfo);
Подробно мы рассмотрим эту функцию в следующей главе, а сейчас я хочу лишь обратить Ваше внимание на параметр blnberitHandles. Создавая процесс, Вы обычно передаете в этом параметре FALSE, тем самым сообщая системе, что дочерний процесс не должен наследовать наследуемые описатели, зафиксированные в таблице родительского процесса. Если же Вы передаете TRUE, дочерний процесс наследует описатели родительского. Тогда операционная система создает дочерний процесс, но не дает ему немедленно начать свою работу. Сформировав в нем, как обычно, новую (пустую) таблицу описателей, она считывает таблицу родительского процесса и копирует все ее действительные записи в таблицу дочернего — причем в те же позиции. Последний факт чрезвычайно важен, так как означает, что описатели будут идентичны в обоих процессах (родительском и дочернем).
Помимо копирования записей из таблицы описателей, система увеличивает значения счетчиков соответствующих объектов ядра, поскольку эти объекты теперь используются обоими процессами. Чтобы уничтожить какой-то объект ядра, его описатель должны закрыть (вызовом CloseHandle) оба процесса. Кстати, сразу после возврата управления функцией CreateProcess родительский процесс может закрыть свой описатель объекта, и это никак не отразится на способности дочернего процесса манипулировать с этим объектом.
В таблице 3-3 показано состояние таблицы описателей в дочернем процессе — перед самым началом его исполнения. Как видите, записи 1 и 2 не инициализированы, и поэтому данные описатели неприменимы в дочсрнсм процессе Однако индекс 3 действительно идентифицирует объект ядра по тому же (что и в родительском) адресу 0xF0000010. При этом маска доступа и флаги в родительском и дочернем процессах тоже идентичны. Так что, если дочерний процесс в свою очередь породит новый ("внука" по отношению к исходному родительскому), "внук" унаследует данный описатель объекта ядра с теми же значением, нравами доступа и флагами, а счетчик числа пользователей этого объекта ядра вновь увеличится на 1.
Индекс | Указатель на блок памяти объекта ядра | Маска доступа (DWORD с набором битовых флагов) | Флаги (DWORD с набором битовых флагов) |
0x00000000 | (неприменим) | (неприменим) | |
0x00000000 | (неприменим) | (неприменим) | |
0xF0000010 | 0х???????? | 0x00000001 |
Таблица 3-3. Таблица описателей в дочернем процессе (после того как он унаследовал от родительского один наследуемый описатель)
Наследуются только описатели объектов, существующие на момент создания дочернего процесса. Если родительский процесс создаст после этого новые объекты
ядра с наследуемыми описателями, то эти описатели будут уже недоступны дочернему процессу.
Для наследования описателей объектов характерно одно очень странное свойство: дочерний процесс не имеет ни малеЙшего понятия, что он унаследовал какие-то описатсли. Поэтому наследование описятелей объектов ядра полезно, только когда дочерний процесс сообщает, что при его создании родительским процессом он ожидает доступа к какому-нибудь объекту ядра. Тут надо заметить, что обычно родительское и дочернее приложения пишутся одной фирмой, но в принципе дочернее приложение может написать и сторонняя фирма, если в этой программе задокументировано, чего именно она ждет от родительского процесса.
Для этого в дочерний процесс обычно передают значение ожидаемого им описателя объекта ядра как аргумент в командной строке. Инициализирующий код дочернего процесса анализирует командную строку (чаще всего вызовом sscanf), извлекает из нее значение описателя, и дочерний процесс получает неограниченный доступ к
объекту. При этом механизм наследования срабатывает только потому, что значение описателя общего объекта ядра в родительском и дочернем процессах одинаково, — и именно по этой причине родительский процесс может передать значение описателя как аргумент в командной строке.
Для наследственной передачи описателя объекта ядра от родительского процесса дочернему, конечно же, годятся и другие формы межпроцессной сяязи Один из приемов заключается в том, что родительский процесс дожидается окончания инициализации дочернего (через функцию WaitForInputIdle рассматриваемую в главе 9), а затем посылает (синхронно или асинхронно) сообщение окну, созданному потоком дочернего процесса.
Еще один прием: родительский процесс добавляет в свой блок переменных окружения новую переменную Она должна быть "узнаваема" дочерним процессом и содержать значение наследуемого описятеля объекта ядра, Далее родительский процесс создает дочерний, тот наследует переменные окружения родительского процесса и, вызвав GetEnvironmentVariable, получает нужный описатель. Такой прием особенно хорош, когда дочерний процесс тоже порождает процессы, — ведь все переменные окружения вновь наследуются.
Изменение флагов описателя
Иногда встречаются ситуации, в которых родительский процесс создает объект ядра с наследуемым описателем, а затем порождает два дочерних процесса. Но наследуемый описатель нужен только одному из них. Иначе говоря, время от времени возникает необходимость контролировать, какой из дочерних процессов наследует описатели объектов ядра. Для этого модифицируйте флаг наследования, связанный с описателем, вызовом SetHandleInformation
BOOL SetHandleInformation(
HANDLE hObject,
DWORD dwMask,
DWORD dwFlags);
Как видите, эта функция принимает три параметра. Первый (bObject) идентифицирует допустимый описатель. ВтороЙ (dwMask) сообщает функции, какой флаг (или флаги) Вы хотите изменить На сегодняшний день с каждым описателем связано два флага:
#define HANDLE FLAG_INHtRIT 0x00000001
#define HANDLE FLAG PROTECT FROM CLOSE 0x00000002
Чтобы изменить сразу все флаги объекта, нужно объединить их побитовой операцией OR.
И, наконец, третий параметр функции SetHandleInformation — dwFlags — указывает, в какое именно состояние следует перевести флаги. Например, чтобы установить флаг наследования для описателя объекта ядра
SetHandleInformation(hobj, HANDLE_FLAG_INHERIT, HANDLE_FLAG_INHERIT);
а чтобы сбросить этот флаг:
SetHandleInformation(hobj, HANDLE_FLAG_INHERIT, 0);
Флаг HANDLE_FLAGPROTECT_FROM_CLOSE сообщает системе, что данный описатель закрывать нельзя:
SetHandleInformation(hobj, HANDLE_FLAG_PROTECT_FROM_CLOSE, HANDLE_FLAG_PROTECT_FROM_CLOSE);
CloseHandle(hobj); // генерируется исключение
Если какой-нибудь поток попытается закрыть защищенный описатель, CloseHandle приведет к исключению. Необходимость в такой защите возникает очень редко. Однако этот флаг весьма полезен, когда процесс порождает дочерний, а тот в свою очередь — еще один процесс. При этом родительский процесс может ожидать, что его "внук" унаследует определенный описатель объекта, переданный дочернему. Но тут вполне возможно, что дочерний процесс, прежде чем породить новый процесс, закрывает нужный описатель. Тогда родительский процесс теряет связь с "внуком", поскольку тот не унаследовал требуемый объект ядра. Защитив описатель от закрытия, Вы исправите ситуацию, и "внук" унаследует предназначенный ему объект.
У этого подхода, впрочем, есть один недостаток. Дочерний процесс, вызвав:
SetHandleInformation(hobj, HANDLEMFLAG_PROlECl_FROM_CLOSE, 0);
CloseHandle(hobj);
может сбросить флаг HANDLE_FLAG_PROTECT_FROM_CLOSE и закрыть затем соответствующий описатель. Родительский процесс ставит на то, что дочерний не исполнит этот код. Но одновременно он ставит и на то, что дочерний процесс породит ему "внука", поэтому в целом ставки не слишком рискованны.
Для полноты картины стоит, пожалуй, упомянуть и функцию GetHandleInformation:
BOOL GetHandleInformation(
HANDLE hObj,
PDWORD pdwFlags);
Эта функция возвращает текущие флаги для заданного описателя в переменной типа DWORD, на которую укапывает pdwFlags. Чтобы проверить, является ли описатель наследуемым, сделайте так:
DWORD dwFlags;
GetHandleInformation(hObj, &dwFlags);
BOOL fHandleIsInheritable = (0 != (dwFlags & HANDLE_FLAG_INHERIT));
3. Динамические библиотеки.
В DLL содержатся все функции Windows API. Три самые важные DLL:
Kernel32.dll (управление памятью, процессами и потоками), User32.dll
(поддержка пользовательского интерфейса, в том числе, функции, связанные с
созданием окон и передачей сообщений) и GDI32.dll (графика и вывод
текста).
В Windows есть другие DLL:
AdvAPI32.dll – содержит функции для защиты объектов, работы с
реестром и регистрации событий
ComDlg32.dll – стандартные диалоговые окна (вроде FileOpen и
FileSave)
ComCtl32.dll – поддерживает стандартные элементы управления
DLL нужно применять для реализации следующих возможностей:
1. Расширение функциональности приложения.
2. Возможность использования разных языков программирования.
3. Более простое управление проектом.
4. Экономия памяти..
5. Разделение ресурсов..
6. Упрощение локализации.
7. Решение проблем, связанных с особенностями различных
платформ..
8. Реализация специфических возможностей.
DLL и адресное пространство процесса.
DLL представляет собой набор модулей исходного кода, в каждом их
которых содержится определенное число функций, вызываемых приложением
или другим DLL. Причем в DLL обычно нет кода, предназначенного для
обработки циклов выборки сообщений и создания окон.
Файлы с исходным кодом компилируются и компонуются также, как и при
создании EXE-файла, но при компоновке нужно указать ключ /DLL.
Чтобы приложение (или другая DLL) могла вызывать функции,
содержащиеся в DLL, образ ее файла нужно сначала спроецировать на адресное
пространство вызывающего процесса. Это выполняется за счет неявного
связывания при загрузке, либо за счет явного – в период выполнения. Теперь все функции DLL доступны всем потокам этого процесса. Когда поток вызывает из DLL какую-то функцию, та считывает свои параметры из списка потока и размещает в этом стеке собственные локальные переменные. Кроме того, любые созданные кодом объекты принадлежат вызывающему потоку или процессу – DLL ничем не владеет.
При проецировании образа DLL-файла на адресное пространство процесса
система создает также экземпляры глобальных и статических переменных.
Неявное связывание EXE – и DLL – модулей
Неявное связывание (implicit linking) - самый распространенный метод.
Исполняемый модуль (EXE) импортирует функции и переменные из DLL, а
DLL– модули экспортирует их в исполняемый модуль. DLL – также может
импортировать функции и переменные их других DLL.
Создание DLL-модуля
DLL может экспортировать переменные, функции или С++ классы в другие
модули.
При разработке DLL сначала создается заголовочный файл, который
включается во все модули исходного кода нашей DLL. Кроме того, его нужно поставлять вместе со своей DLL, чтобы другие разработчики могли включать его в свои модули исходного кода, которые импортируют наши функции или переменные. Единый заголовочный файл, используемый при сборке DLL и любых исполняемых модулей, существенно облегчает поддержку приложения.
При компиляции исходного файла DLL MYLIBAPI определяется как
__declspec (dllexport) до включения заголовочного файла MyLib.h. Такой
модификатор означает, что данная переменная, функция или C++ класс
экспортируется из DLL.
Также следует обратить внимание, что в файле MyLibFile1.cpp перед
экспортируемой переменной или функцией не ставится идентификатор
MYLIBAPI. Он здесь не нужен: проанализировав заголовочный файл,
компилятор запоминает, какие переменные и функции являются
экспортируемыми.
Идентификатор MYLIBAPI включает extern. Модификатор extern не даёт
компилятору искажать имена переменных или функции, и они становятся
доступными исполняемым модулям, написанным на С, С++ или любом другом языке программирования. Этим модификатором можно пользоваться только в коде С++, но ни в коем случае ни в коде на стандартном С.
Мы рассмотрели, как используется заголовочный файл в исходных файлах
DLL.
А в исходных файлах ЕХЕ-модуля MYLIBAPI определять не надо: включая
заголовочный файл, вы определяете этот идентификатор как –
DLLSPEC(DLLIMPORT), и при помещении исходного кода ЕХЕ-модуля
компилятор поймёт, что переменные и функции импортируются из DLL.
Что такое экспорт
Если перед переменной, прототипом функции или С++ классом указан
модификатор – _deсlspec(dllexport), компилятор Microsoft C/C++ встраивает
в конечный obj-файл дополнительную информацию. Она понадобится при
сборке DLL из OBJ-файлов.
Обнаружив такую информацию, компоновщик создает LIB-файл со списком
идентификаторов, экспортируемых из DLL. Этот LIB-файл нужен при сборке
любого EXE-модуля, ссылающегося на такие идентификаторы. Компоновщик
также вставляет в конечный DLL-файл таблицу экспортируемых
идентификаторов – раздел экспорта, в котором содержится список (в
алфавитном порядке) идентификаторов экспортируемых функций, переменных
и классов. Туда же помещается относительный виртуальный адрес (relative
virtual address, RVA) каждого идентификатора внутри DLL-модуля.
Создание EXE-модуля
Вот пример исходного кода EXE-модуля, который импортирует
идентификаторы, экспортируемые DLL, и ссылается на них внутри в процессе
выполнения.
//Модуль: MyExeFilel.cpp
//Сюда включаются стандартные заголовочные файлы Windows и
библиотеки С
#include <windows.h>
//включаем экспортируемые структуры данных, идентификаторы,
функции и переменные
#include “MyLib\MyLib.h”
….
int WINAPI WinMain(HINSTANCE hinstExe, HINSTANCE,
LPTSTR pszCmdLine, int)
{
int nLeft=10, nRight=25;
TCHAR sz[100];
wsprintf(sz, TEXT(“%d + %d = %d”), nLeft, nRight,
Add(nLeft, nRight));
MessageBox(NULL, sz, TEXT(“Calculation”), MB_OK);
wsprintf(sz, TEXT(“The result from the last Add is: %d”),
g_nResult);
MessageBox(NULL, sz, TEXT(“Last Result”), MB_OK);
return (0);
}
Создавая фалы исходного кода для EXE-модуля, нужно включить в них
заголовочный файл DLL, иначе импортируемые идентификаторы окажутся
неопределенными, и компилятор выдаст массу предупреждений об ошибках.
MYLIBAPI в исходных файлах EXE-модуля до заголовочного файла DLL не
определяется. Поэтому при компиляции приведенного выше кода MYLIBAPI за
счет заголовочного файла MyLib.h будет определен как __declspec(dllimport).
Встречая такой модификатор перед именем переменной, функции или С++
класса, компилятор понимает, что данный идентификатор импортируется из
какого-то DLL –модуля.
Далее компоновщик собирает все OBJ-модули в конечный EXE-модуль. Для
этого он должен знать, в каких DLL содержаться импортируемые
идентификаторы, на которые есть ссылки в коде. Информацию об этом он
получает из передаваемого ему LIB-файла (в котором указан список
идентификаторов, экспортируемых DLL)
Что такое импорт
Импортируя идентификатор, необязательно прибегать к
__declspec(dllimport) – можно использовать стандартное ключевое слово
extern языка С. Но компилятор создаст чуть более эффективный код, если ему будет заранее известно, что идентификатор, на который мы ссылаемся,
импортируется из LIB-файла DLL –модуля.
Разрешая ссылки на импортируемые идентификаторы, компоновщик создаст в конечном EXE-модуле раздел импорта (import section).
В нем перечисляются DLL, необходимые этому модулю, и идентификаторы,
на которые есть ссылки из всех используемых DLL.
Выполнение EXE-модуля
При запуске EXE-файла загрузчик операционной системы создает для его
процесса виртуальное пространство и проецирует на него исполняемый модуль.
Далее загрузчик анализирует раздел импорта и пытается спроецировать все
необходимые DLL на адресное пространство процесса.
Поскольку в разделе импорта указано только имя DLL (без пути), загрузчику
приходится самому искать ее на дисковых устройствах в компьютере
пользователя. Поиск DLL осуществляется в следующей последовательности:
1. Каталог, содержащий EXE-файл
2. Текущий каталог процесса
3. Системный каталог Windows
4. Основной каталог Windows
5. Каталоги, указанные в переменной окружения PATH
Проецируя DLL-модули на адресное пространство, загрузчик проверяет в
каждом из них раздел импорта. Если у DLL есть раздел импорта (что обычно и
бывает), загрузчик проецирует следующий DLL-модуль. При этом загрузчик
ведет учет загружаемых DLL, и проецирует их только один раз, даже если
загрузки этих DLL требуют и другие модули.
Найдя и спроецировав на адресное пространство процесса все необходимые
DLL-модули, загрузчик настраивает ссылки на импортируемые
идентификаторы. Для этого вновь просматривает разделы импорта в каждом
модуле, проверяя наличие указанного идентификатора в соответствующей
DLL.
Обнаружив идентификатор, загрузчик отыскивает его RVA и прибавляет к
виртуальному адресу, по которому данная DLL размещена в адресное
пространство процесса, а затем сохраняет полученный адрес в разделе импорта
EXE-модуля. И с этого момента ссылка в коде на импортируемый
идентификатор приводит к выборке его адреса из раздела импорта
вызывающего модуля, открывая таким образом доступ к импортируемой
переменной, функции или функции-члену С++ класса. Как только
динамические связи будут установлены, первичный поток процесса начинает
выполняться.
Загрузчик всех этих DLL и настройка ссылок занимает какое-то время.
Чтобы сократить время загрузки приложения нужно модифицировать базовые
адреса EXE- и DLL-модулей и провести их связывание.
Явная загрузка DLL и связывание идентификаторов
Чтобы поток мог вызвать функцию из DLL-модуля, DLL надо спроецировать
на адресное пространство процесса, которому принадлежит этот поток.
Делается это двумя способами:
1. Код приложения просто ссылается на идентификаторы, содержащиеся в
DLL, и тем самым заставляет загрузчик неявно загружать (и связывать)
нужную DLL при запуске приложения.
2. Явная загрузка и связывание требуемой DLL в период выполнения
приложения. Иначе говоря, поток явно загружает DLL в адресное
пространство процесса, получает виртуальный адрес необходимой DLL-
функции и взывает ее по этому адресу. Изящество этого подхода в том,
что все происходит в уже выполняемом приложении.