Издание четвертое windows ® для профессионалов создание эффективных Win32-приложений с учетом специфики 64-разрядной версии Windows


Г ЛАВА 17Проецируемые в память файлыСтатические данные разделяются несколькими



Pdf просмотр
страница42/68
Дата28.11.2016
Размер3.57 Mb.
Просмотров12474
Скачиваний0
1   ...   38   39   40   41   42   43   44   45   ...   68
Г ЛАВА 17
Проецируемые в память файлы
Статические данные разделяются несколькими
экземплярами EXE или DLL
По умолчанию для большей безопасности глобальные и статические данные не разделяются несколькими проекциями одного итого же EXE или DLL. Но иногда удобнее,
чтобы несколько проекций EXE разделяли единственный экземпляр переменной.
Например, вне так то просто определить, запущено ли несколько экземпляров приложения. Если бы у Вас была переменная, доступная всем экземплярам приложения, она могла бы отражать число этих экземпляров. Тогда при запуске нового экземпляра приложения его поток просто проверил бы значение глобальной переменной (обновленное другим экземпляром приложения) и, будь оно больше 1, сообщил бы пользователю, что запустить можно лишь один экземпляр после чего эта копия приложения была бы завершена.
В этом разделе мы рассмотрим метод, обеспечивающий совместное использование переменных всеми экземплярами EXE или DLL. Но сначала Вам понадобятся кое какие базовые сведения.
Любой образ EXE или DLL файла состоит из группы разделов. По соглашению имя каждого стандартного раздела начинается сточки. Например, при компиляции программы весь код помещается в раздел
.text, неинициализированные данные — враз дела инициализированные — в раздел .data.
С каждым разделом связана одна из комбинаций атрибутов, перечисленных в следующей таблице.
Атрибут
Описание
READ
Разрешает чтение из раздела
WRITE
Разрешает запись в раздел
EXECUTE
Содержимое раздела можно исполнять
SHARED
Раздел доступен нескольким экземплярам приложения (этот атрибут отключает механизм копирования при записи)
Запустив утилиту DumpBin из Microsoft Visual Studio (с ключом /Headers), Вы увидите список разделов в файле образа EXE или DLL. Пример такого списка, показанный ниже, относится к EXE файлу HEADER #1
.text name
11A70 virtual size
1000 virtual address
12000 size of raw data
1000 file pointer to raw data
0 file pointer to relocation table
0 file pointer to line numbers
0 number of relocations
0 number of line numbers
60000020 flags
Code
Execute Read
SECTION HEADER #2
.rdata name
1F6 virtual size
см. след. стр.

414
Ч АС Т Ь I I I
УПРАВЛЕНИЕ ПАМЯТЬЮ virtual address
1000 size of raw data
13000 file pointer to raw data
0 file pointer to relocation table
0 file pointer to line numbers
0 number of relocations
0 number of line numbers
40000040 flags
Initialized Data
Read Only
SECTION HEADER #3
.data name
560 virtual size
14000 virtual address
1000 size of raw data
14000 file pointer to raw data
0 file pointer to relocation table
0 file pointer to line numbers
0 number of relocations
0 number of line numbers
C0000040 flags
Initialized Data
Read Write
SECTION HEADER #4
.idata name
58D virtual size
15000 virtual address
1000 size of raw data
15000 file pointer to raw data
0 file pointer to relocation table
0 file pointer to line numbers
0 number of relocations
0 number of line numbers
C0000040 flags
Initialized Data
Read Write
SECTION HEADER #5
.didat name
7A2 virtual size
16000 virtual address
1000 size of raw data
16000 file pointer to raw data
0 file pointer to relocation table
0 file pointer to line numbers
0 number of relocations
0 number of line numbers
C0000040 flags
Initialized Data
Read Write

415
Г ЛАВА 17
Проецируемые в память файлы HEADER #6
.reloc name
26D virtual size
17000 virtual address
1000 size of raw data
17000 file pointer to raw data
0 file pointer to relocation table
0 file pointer to line numbers
0 number of relocations
0 number of line numbers
42000040 flags
Initialized Data
Discardable
Read Only
Summary
1000 .data
1000 .didat
1000 .idata
1000 .rdata
1000 .reloc
12000 Некоторые из часто встречающихся разделов перечислены в таблице ниже.
Имя раздела
Описание
.bss
Неинициализированные данные
.CRT
Неизменяемые данные библиотеки С
.data
Инициализированные данные
.debug
Отладочная информация
.didat
Таблица имен для отложенного импорта (delay imported names Таблица экспортируемых имен
.idata
Таблица импортируемых имен
.rdata
Неизменяемые данные периода выполнения
.reloc
Настроечная информация — таблица переадресации (relocation table)
.rsrc
Ресурсы
.text
Код EXE или Локальная память потока
.xdata
Таблица для обработки исключений
Кроме стандартных разделов, генерируемых компилятором и компоновщиком,
можно создавать свои разделы вили файле, используя директиву компилятора data_seg("имя_раздела")
Например, можно создать раздел Shared, в котором содержится единственная переменная типа LONG:
#pragma data_seg("Shared")
LONG g_lInstanceCount = 0;
#pragma data_seg()

416
Ч АС Т Ь I I I
УПРАВЛЕНИЕ ПАМЯТЬЮ
Обрабатывая этот код, компилятор создаст раздели поместит в него все
инициализированные переменные, встретившиеся после директивы #pragma. В нашем примере в этом разделе находится переменная
g_lInstanceCount. Директива #pragma
data_seg( ) сообщает компилятору, что следующие за ней переменные нужно вновь помещать в стандартный раздел данных, а не в Shared. Важно помнить, что компилятор помещает в новый раздел только инициализированные переменные. Если из предыдущего фрагмента кода исключить инициализацию переменной, она будет включена в другой раздел data_seg("Shared")
LONG g_lInstanceCount;
#pragma Однако в компиляторе Microsoft Visual C++ 6.0 предусмотрен спецификатор
allo
cate, который позволяет помещать неинициализированные данные в любой раздел.
Взгляните на этот код создаем раздели заставляем компилятор поместить в него инициализированные данные data_seg("Shared")
// инициализированная переменная, по умолчанию помещается в раздел Shared int a = 0;
// неинициализированная переменная, по умолчанию помещается в другой раздел int b;
// сообщаем компилятору прекратить включение инициализированных данных в раздел Shared
#pragma data_seg()
// инициализированная переменная, принудительно помещается в раздел Shared
__declspec(allocate("Shared")) int c = 0;
// неинициализированная переменная, принудительно помещается в раздел Shared
__declspec(allocate("Shared")) int d;
// инициализированная переменная, по умолчанию помещается в другой раздел int e = 0;
// неинициализированная переменная, по умолчанию помещается в другой раздел int Чтобы спецификатор
allocate работал корректно, сначала должен быть создан соответствующий раздел. Так что, убрав из предыдущего фрагмента кода первую строку, Вы не смогли бы его скомпилировать.
Чаще всего переменные помещают в собственные разделы, намереваясь сделать их разделяемыми между несколькими проекциями EXE или DLL. По умолчанию каждая проекция получает свой набор переменных. Но можно сгруппировать в отдельном разделе переменные, которые должны быть доступны всем проекциям EXE или тогда система не станет создавать новые экземпляры этих переменных для каждой проекции EXE или Чтобы переменные стали разделяемыми, одного указания компилятору выделить их в какой то раздел мало. Надо также сообщить компоновщику, что переменные в

417
Г ЛАВА 17
Проецируемые в память файлы этом разделе должны быть общими. Для этого предназначен ключ /SECTION компо новщика:
/SECTION:имя,атрибуты
За двоеточием укажите имя раздела, атрибуты которого Вы хотите изменить. В
нашем примере нужно изменить атрибуты раздела Shared, поэтому ключ должен выглядеть так:
/SECTION:Shared,RWS
После запятой мы задаем требуемые атрибуты. При этом используются такие сокращения) и S (SHARED). В данном случае мы указали, что раздел Shared должен быть читаемым, записываемыми «разделяемым».
Если Вы хотите изменить атрибуты более чему одного раздела, указывайте ключ для каждого такого раздела.
Соответствующие директивы для компоновщика можно вставлять прямо в исходный код comment(linker, "Эта строка заставляет компилятор включить строку «/SECTION: Shared,RWS» в особый раздел
.drectve. Компоновщик, собирая OBJ модули, проверяет этот раздел в каждом модуле и действует так, словно все эти строки переданы ему как аргументы в командной строке. Я всегда применяю этот очень удобный метод перемещая файл исходного кода в новый проект, не надо изменять никаких параметров в диалоговом окне Project Settings в Visual Хотя создавать общие разделы можно, Microsoft не рекомендует это делать. Во первых, разделение памяти таким способом может нарушить защиту. Во вторых, наличие общих переменных означает, что ошибка водном приложении повлияет на другое, так как этот блок данных не удастся защитить от случайной записи.
Представьте, Вы написали два приложения, каждое из которых требует от пользователя вводить пароль. При этом Вы решили чуть чуть облегчить жизнь пользователю если одна из программ уже выполняется на момент запуска другой, то вторая считывает пароль из общей памяти. Так что пользователю ненужно повторно вводить пароль, если одно из приложений уже запущено.
Все выглядит вполне невинно. В конце концов только Ваши приложения загружают данную DLL, и только они знают, где искать пароль, содержащийся в общем разделе памяти. Но хакеры не дремлют, и если им захочется узнать Ваш пароль, то максимум, что им понадобится, — написать небольшую программу, загружающую Вашу и понаблюдать за общим блоком памяти. Когда пользователь введет пароль, хакерская программа тут же его узнает.
Трудолюбивая хакерская программа может также предпринять серию попыток угадать пароль, записывая его варианты в общую память. А угадав, сможет посылать любые команды этим двум приложениям. Данную проблему можно было бы решить,
если бы существовал какой нибудь способ разрешать загрузку DLL только определенным программам. Но пока это невозможно — любая программа, вызвав
LoadLibrary,
способна явно загрузить любую DLL.
Программа-пример AppInst
Эта программа, «17 AppInst.exe» (см. листинг на рис. 17 1), демонстрирует, как выяснить, сколько экземпляров приложения уже выполняется в системе. Файлы исходного кода и ресурсов этой программы находятся в каталоге 17 AppInst на компакт дис

418
Ч АС Т Ь I I I
УПРАВЛЕНИЕ ПАМЯТЬЮ
ке, прилагаемом к книге. После запуска AppInst на экране появляется диалоговое окно,
в котором сообщается, что сейчас выполняется только один ее экземпляр.
Если Вы запустите второй экземпляр, оба диалоговых окна сообщат, что теперь выполняется два экземпляра.
Вы можете запускать и закрывать сколько угодно экземпляров этой программы окно любого из них всегда будет отражать точное количество выполняемых экземп ляров.
Где тов начале файла AppInst.cpp Вы заметите следующие строки указываем компилятору поместить эту инициализированную переменную в раздел Shared, чтобы она стала доступной всем экземплярам программы data_seg("Shared")
volatile LONG g_lApplicationInstances = 0;
#pragma data_seg()
// указываем компоновщику, что раздел Shared должен быть читаемым, записываемыми разделяемым comment(linker, "В этих строках кода создается раздел Shared с атрибутами защиты, которые разрешают его чтение, запись и разделение. Внутри него находится одна переменная,
g_lApplicationInstances, доступная всем экземплярам программы. Заметьте, что для этой переменной указан спецификатор
volatile, чтобы оптимизатор не слишком с ней ум ничал.
При выполнении функции
_tWinMain каждого экземпляра значение переменной
g_lApplicationInstances увеличивается на 1, а перед выходом из _tWinMain — уменьшается на 1. Я изменяю ее значение с помощью функции
InterlockedExchangeAdd, так как эта переменная является общим ресурсом для нескольких потоков.
Когда на экране появляется диалоговое окно каждого экземпляра программы,
вызывается функция
Dlg_OnInitDialog. Она рассылает всем окнам верхнего уровня зарегистрированное оконное сообщение (идентификатор которого содержится в переменной
g_aMsgAppInstCountUpdate):
PostMessage(HWND_BROADCAST, g_aMsgAppInstCountUpdate, 0, Это сообщение игнорируется всеми окнами в системе, кроме окон AppInst. Когда его принимает одно из окон нашей программы, код в
Dlg_Proc просто обновляет в диалоговом окне значение, отражающее текущее количество экземпляров (а эта величина хранится в переменной
g_lApplicationInstances).

419
Г ЛАВА 17
Проецируемые в память файлы
AppInst.cpp
/******************************************************************************
Модуль: Автор Copyright (c) 2000, Джеффри Рихтер (Jeffrey Richter)
******************************************************************************/
#include "..\CmnHdr.h"
/* см. приложение А */
#include
#include
#include "Resource.h"
///////////////////////////////////////////////////////////////////////////////
// общесистемное оконное сообщение с уникальным идентификатором g_uMsgAppInstCountUpdate = INVALID_ATOM;
///////////////////////////////////////////////////////////////////////////////
// указываем компилятору поместить эту инициализированную переменную в раздел Shared, чтобы она стала доступной всем экземплярам программы data_seg("Shared")
volatile LONG g_lApplicationInstances = 0;
#pragma data_seg()
// указываем компоновщику, что раздел Shared должен быть читаемым, записываемыми разделяемым comment(linker, "/Section:Shared,RWS")
///////////////////////////////////////////////////////////////////////////////
BOOL Dlg_OnInitDialog(HWND hwnd, HWND hwndFocus, LPARAM lParam) {
chSETDLGICONS(hwnd, IDI_APPINST);
// инициализируем статический элемент управления, g_uMsgAppInstCountUpdate, 0, 0);
return(TRUE);
}
///////////////////////////////////////////////////////////////////////////////
void Dlg_OnCommand(HWND hwnd, int id, HWND hwndCtl, UINT codeNotify) {
switch (id) {
case IDCANCEL:
EndDialog(hwnd, id);
break;
}
}
///////////////////////////////////////////////////////////////////////////////
Рис. 17-1.
Программа-пример AppInst
см. след. стр

420
Ч АС Т Ь I I I
УПРАВЛЕНИЕ ПАМЯТЬЮ
Рис. 17-1.
продолжение
INT_PTR WINAPI Dlg_Proc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam) {
if (uMsg == g_uMsgAppInstCountUpdate) {
SetDlgItemInt(hwnd, IDC_COUNT, g_lApplicationInstances, FALSE);
}
switch (uMsg) {
chHANDLE_DLGMSG(hwnd, WM_INITDIALOG, Dlg_OnInitDialog);
chHANDLE_DLGMSG(hwnd, WM_COMMAND,
Dlg_OnCommand);
}
return(FALSE);
}
///////////////////////////////////////////////////////////////////////////////
int WINAPI _tWinMain(HINSTANCE hinstExe, HINSTANCE, LPTSTR pszCmdLine, int) {
// получаем числовое значение из общесистемного оконного сообщения которое применяется для уведомления всех окон верхнего уровня об изменении счетчика числа пользователей данного модуля g_uMsgAppInstCountUpdate =
RegisterWindowMessage(TEXT("MsgAppInstCountUpdate"));
// запущен еще один экземпляр этой программы) &g_lApplicationInstances, 1);
DialogBox(hinstExe, MAKEINTRESOURCE(IDD_APPINST), NULL, Dlg_Proc);
// данный экземпляр закрывается) &g_lApplicationInstances,
1);
// сообщаем об этом остальным экземплярам программы, g_uMsgAppInstCountUpdate, 0, 0);
return(0);
}
//////////////////////////////// Конец файла //////////////////////////////////
Файлы данных, проецируемые в память
Операционная система позволяет проецировать на адресное пространство процесса и файл данных. Это очень удобно при манипуляциях с большими потоками данных.
Чтобы представить всю мощь такого применения механизма проецирования файлов, рассмотрим четыре возможных метода реализации программы, меняющей порядок следования всех байтов в файле на обратный.
Метод 1: один файл, один буфер
Первый (и теоретически простейший) метод — выделение блока памяти, достаточного для размещения всего файла. Открываем файл, считываем его содержимое в блок памяти, закрываем. Располагая в памяти содержимым файла, можно поменять первый

421
Г ЛАВА 17
Проецируемые в память файлы байт с последним, второй — с предпоследними т. д. Этот процесс будет продолжаться, пока мы не поменяем местами два смежных байта, находящихся в середине файла.
Закончив эту операцию, вновь открываем файл и перезаписываем его содержимое.
Этот довольно простой в реализации метод имеет два существенных недостатка.
Во первых, придется выделить блок памяти такого же размера, что и файл. Это терпимо, если файл небольшой. А если он занимает 2 Гб? Система просто не позволит приложению передать такой объем физической памяти. Значит, к большим файлам нужен совершенно иной подход.
Во вторых, если перезапись вдруг прервется, содержимое файла будет испорчено. Простейшая мера предосторожности — создать копию исходного файла (потом ее можно удалить, но это потребует дополнительного дискового пространства.
Метод 2: два файла, один буфер
Открываем существующий файл и создаем на диске новый — нулевой длины. Затем выделяем небольшой внутренний буфер размером, скажем, 8 Кб. Устанавливаем указатель файла в позицию 8 Кб от конца, считываем в буфер последние 8 Кб содержимого файла, меняем в нем порядок следования байтов на обратный и переписываем буфер в только что созданный файл. Повторяем эти операции, пока не дойдем до начала исходного файла. Конечно, если длина файла не будет кратна 8 Кб, операции придется немного усложнить, но это нестрашно. Закончив обработку, закрываем оба файла и удаляем исходный файл.
Этот метод посложнее первого, зато позволяет гораздо эффективнее использовать память, так как требует выделения лишь 8 Кб. Но и здесь не без проблем, и вот две главных. Во первых, обработка идет медленнее, чем при первом методе на каждой итерации перед считыванием приходится находить нужный фрагмент исходного файла. Во вторых, может понадобиться огромное пространство на жестком диске.
Если длина исходного файла 400 Мб, новый файл постепенно вырастет до этой величины, и перед самым удалением исходного файла будет занято 800 Мб, те. на 400 Мб больше, чем следовало бы. Так что все пути ведут… к третьему методу.
Метод 3: один файл, два буфера
Программа инициализирует два раздельных буфера, допустим, по 8 Кб и считывает первые 8 Кб файла в один буфера последние 8 Кб — в другой. Далее содержимое обоих буферов обменивается в обратном порядке и первый буфер записывается вконец, а второй — в начало того же файла. На каждой итерации программа перемещает восьмикилобайтовые блоки из одной половины файла в другую. Разумеется,
нужно предусмотреть какую то обработку на случай, если длина файла не кратна Кб, и эта обработка будет куда сложнее, чем в предыдущем методе. Но разве это испугает опытного программиста?
По сравнению с первыми двумя этот метод позволяет экономить пространство на жестком диске, так как все операции чтения и записи протекают в рамках одного файла. Что же касается памяти, то и здесь данный метод довольно эффективен, используя всего 16 Кб. Однако он, по видимому, самый сложный в реализации. И, кроме того, как и первый метод, он может испортить файл данных, если процесс вдруг прервется.
Ну а теперь посмотрим, как тот же процесс реализуется, если применить файлы,
проецируемые в память.

422
Ч АС Т Ь I I I
УПРАВЛЕНИЕ ПАМЯТЬЮ
Метод 4: один файл и никаких буферов
Вы открываете файл, указывая системе зарезервировать регион виртуального адресного пространства. Затем сообщаете, что первый байт файла следует спроецировать на первый байт этого региона, и обращаетесь к региону так, будто он на самом деле содержит файл. Если в конце файла есть отдельный нулевой байт, можно вызвать библиотечную функцию
_strrev и поменять порядок следования байтов на обратный.
Огромный плюс этого метода в том, что всю работу по кэшированию файла выполняет сама система не надо выделять память, загружать данные из файла в память,
переписывать их обратно в файл и т. д. и т. п. Но, увы, вероятность прерывания процесса, например из за сбоя в электросети, по прежнему сохраняется, и от порчи данных Вы не застрахованы.
Использование проецируемых в память файлов
Для этого нужно выполнить три операции:
1.
Создать или открыть объект ядра файл, идентифицирующий дисковый файл,
который Вы хотите использовать как проецируемый в память.
2.
Создать объект ядра проекция файла, чтобы сообщить системе размер файла и способ доступа к нему.
3.
Указать системе, как спроецировать в адресное пространство Вашего процесса объект проекция файла — целиком или частично.
Закончив работу с проецируемым в память файлом, следует выполнить тоже три операции:
1.
Сообщить системе об отмене проецирования на адресное пространство процесса объекта ядра проекция файла».
2.
Закрыть этот объект.
3.
Закрыть объект ядра «файл».
Детальное рассмотрение этих операций — в следующих пяти разделах.
Этап 1: создание или открытие объекта ядра «файл»
Для этого Вы должны применять только функцию
CreateFile:
HANDLE CreateFile(
PCSTR pszFileName,
DWORD dwDesiredAccess,
DWORD dwShareMode,
PSECURITY_ATTRIBUTES psa,
DWORD dwCreationDisposition,
DWORD dwFlagsAndAttributes,
HANDLE Как видите, у функции
CreateFile довольно много параметров. Здесь я сосредоточусь только на первых трех
pszFileName, dwDesiredAccess и dwShareMode.
Как Вы, наверное, догадались, первый параметр,
pszFileName, идентифицирует имя создаваемого или открываемого файла (при необходимости вместе с путем. Второй параметр,
dwDesiredAccess, указывает способ доступа к содержимому файла. Здесь задается одно из четырех значений, показанных в таблице ниже.

423
Г ЛАВА 17
Проецируемые в память файлы
Значение
Описание
0
Содержимое файла нельзя считывать или записывать указывайте это значение, если Вы хотите всего лишь получить атрибуты файла
GENERIC_READ
Чтение файла разрешено
GENERIC_WRITE
Запись в файл разрешена
GENERIC_READ
|
GENERIC_WRITE
Разрешено и то и другое
Создавая или открывая файл данных с намерением использовать его в качестве проецируемого в память, можно установить либо флаг GENERIC_READ (только для чтения, либо комбинированный флаг GENERIC_READ | GENERIC_WRITE (чтение/за пись).
Третий параметр,
dwShareMode, указывает тип совместного доступа к данному файлу (см. следующую таблицу).
Значение
Описание
0
Другие попытки открыть файл закончатся неудачно
FILE_SHARE_READ
Попытка постороннего процесса открыть файл с флагом не удастся
FILE_SHARE_WRITE
Попытка постороннего процесса открыть файл с флагом не удастся Посторонний процесс может открывать файл без ограничений
FILE_SHARE_WRITE
Создав или открыв указанный файл,
CreateFile возвращает его описатель, вином случае — идентификатор Большинство функций Windows, возвращающих те или иные описатели, при неудачном вызове дает NULL. Но
CreateFile — исключение ив таких случаях возвращает идентификатор INVALID_HANDLE_VALUE, определенный как) –1).
Этап 2: создание объекта ядра проекция файла»
Вызвав
CreateFile, Вы указали операционной системе, где находится физическая память для проекции файла на жестком диске, в сети, на CD ROM или в другом месте.
Теперь сообщите системе, какой объем физической памяти нужен проекции файла.
Для этого вызовите функцию
CreateFileMapping:
HANDLE CreateFileMapping(
HANDLE hFile,
PSECURITY_ATTRIBUTES psa,
DWORD fdwProtect,
DWORD dwMaximumSizeHigh,
DWORD dwMaximumSizeLow,
PCSTR Первый параметр,
hFile, идентифицирует описатель файла, проецируемого на адресное пространство процесса. Этот описатель Вы получили после вызова
CreateFile.
Параметр
psa — указатель на структуру SECURITY_ATTRIBUTES, которая относится к объекту ядра проекция файла для установки защиты по умолчанию ему присваивается
424



Поделитесь с Вашими друзьями:
1   ...   38   39   40   41   42   43   44   45   ...   68


База данных защищена авторским правом ©nethash.ru 2019
обратиться к администрации

войти | регистрация
    Главная страница


загрузить материал