Pull to refresh

Реализация разделяемой памяти между драйвером и приложением

Reading time6 min
Views9.7K

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



Драйвер собирался под 32-битную ОС Windows XP (так же проверял его работу и
в 32-битной Windows 7).


Полностью описывать разработку драйвера, начиная с установки WDK(DDK), выбора инструментов разработки, написания стандартных функций драйвера и т.д., я не буду (при желании можно почитать вот и вот, хотя там тоже не много информации). Чтобы статья не получилась слишком раздутой, опишу только способ реализации разделяемой памяти.

Немного теории

Драйвер не создает специального программного потока для выполнения своего кода, а выполняется в контексте потока, активного на данный момент. Поэтому считается, что драйвер выполняется в контексте произвольного потока (Переключение контекста). Очень важно, чтобы при отображении выделенной памяти в пользовательское адресное пространство, мы находились в контексте потока приложения, которое будет управлять нашим драйвером. В данном случае это правило соблюдается, т.к. драйвер является одноуровневым и обращаемся мы к нему с помощью запроса IRP_MJ_DEVICE_CONTROL, следовательно контекст потока не будет переключаться и мы будем иметь доступ к адресному пространству нашего приложения.

Выделение памяти

#pragma LOCKEDCODE
NTSTATUS AllocateSharedMemory(PDEVICE_EXTENSION pdx, PIRP Irp)
	{							// AllocateSharedMemory
	KdPrint(("SharedMemory: Entering AllocateSharedMemory\n"));

	int memory_size = 262144000;    // 250 Mb

	PHYSICAL_ADDRESS pstart = { 0x0, 0x00000000 };
	PHYSICAL_ADDRESS pstop = { 0x3, 0xffffffff };
	PHYSICAL_ADDRESS pskip = { 0x0, 0x00000000 };
	
	// pointer to the output memory => pdx->vaReturned
	pdx->vaReturned = (unsigned short **) GenericGetSystemAddressForMdl(Irp->MdlAddress);

	// create MDL structure (pointer on MDL => pdx->mdl)
	pdx->mdl = MmAllocatePagesForMdl(pstart, pstop, pskip, memory_size);
	if (pdx->mdl != NULL)
		{
		KdPrint(("SharedMemory: MDL allocated at address %08X\n", pdx->mdl));

		// get kernel space virtual address
		pdx->kernel_va = (unsigned short*) MmGetSystemAddressForMdlSafe(pdx->mdl, NormalPagePriority);
		if (pdx->kernel_va != NULL)
			{
			KdPrint(("SharedMemory: pdx->kernel_va allocated at address %08X\n", pdx->kernel_va));
			for (int i = 0; i < 10; ++i)
				{
				pdx->kernel_va[i] = 10 - i;	// write in memory: 10-9-8-7-6-5-4-3-2-1
				}
			}
		else
			{
			KdPrint(("SharedMemory: Not mapped memory into kernel space\n"));
			return STATUS_NONE_MAPPED;
			}
		
		// get user space virtual address
		pdx->user_va = (unsigned short*) MmMapLockedPagesSpecifyCache(pdx->mdl, UserMode, MmCached, 
															NULL, FALSE, NormalPagePriority);
		if (pdx->user_va != NULL)
			{
			KdPrint(("SharedMemory: pdx->user_va allocated at address %08X\n", pdx->user_va));
			
			// return pointer on sharing memory into user space
			*pdx->vaReturned = pdx->user_va;
			}
		else
			{
			KdPrint(("SharedMemory: Don't mapped memory into user space\n"));
			return STATUS_NONE_MAPPED;
			}

		}
	else
		{
		KdPrint(("SharedMemory: Don't allocate memory for MDL\n"));
		return STATUS_MEMORY_NOT_ALLOCATED;
		}
	
	return STATUS_SUCCESS;
	}							// AllocateSharedMemory

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

pdx->vaReturned = (unsigned short **) GenericGetSystemAddressForMdl(Irp->MdlAddress);

Следующий шаг — выделение неперемещаемой физической памяти размером memory_size и построение на ее основе структуру MDL (Memory Descriptor List), указатель на которую сохраняем в переменной pdx->mdl:

pdx->mdl = MmAllocatePagesForMdl(pstart, pstop, pskip, memory_size);


Как видно из изображения, структура MDL нам нужна для описания зафиксированных физических страниц.

Затем получаем диапазон виртуальных адресов для MDL в системном адресном пространстве и сохраняем указатель на эти адреса в переменной pdx->kernel_va:

pdx->kernel_va = (unsigned short*) MmGetSystemAddressForMdlSafe(pdx->mdl, NormalPagePriority);

Эта функция возвратит указатель, по которому мы сможем обращаться к выделенной памяти в драйвере (причем независимо от текущего контекста потока, т.к. адреса получены из системного адресного пространства).

В цикле запишем первые 10 ячеек памяти числами от 10 до 1, чтобы можно было проверить доступность выделенной памяти из пользовательского режима:

for (int i = 0; i < 10; ++i)
	{
	pdx->kernel_va[i] = 10 - i;	// write in memory: 10-9-8-7-6-5-4-3-2-1
	}

Теперь необходимо отобразить выделенную память в адресное пространство приложения, которое обратилось к драйверу:

pdx->user_va = (unsigned short*) MmMapLockedPagesSpecifyCache(pdx->mdl, UserMode, MmCached, NULL, FALSE, NormalPagePriority);

Переменная pdx->vaReturned является указателем на указатель и объявляется в структуре pdx (см. driver.h в папке source_driver). С помощью нее передадим указатель pdx->user_va в приложение:

*pdx->vaReturned = pdx->user_va;


Освобождение памяти

#pragma LOCKEDCODE
NTSTATUS ReleaseSharedMemory(PDEVICE_EXTENSION pdx, PIRP Irp)
	{							// ReleaseSharedMemory
	KdPrint(("SharedMemory: Entering ReleaseSharedMemory\n"));
	
	if (pdx->mdl != NULL)
		{
		MmUnmapLockedPages(pdx->user_va, pdx->mdl);
		MmUnmapLockedPages(pdx->kernel_va, pdx->mdl);
		MmFreePagesFromMdl(pdx->mdl);
		IoFreeMdl(pdx->mdl);
		KdPrint(("SharedMemory: MDL at address %08X freed\n", pdx->mdl));
		}

	return STATUS_SUCCESS;
	}							// ReleaseSharedMemory


Здесь происходит освобождение адресного пространства приложения:

MmUnmapLockedPages(pdx->user_va, pdx->mdl);

ситемного адресного пространства:

MmUnmapLockedPages(pdx->kernel_va, pdx->mdl);

затем освобождаются физические страницы:

MmFreePagesFromMdl(pdx->mdl);

и «убиваем» MDL:

IoFreeMdl(pdx->mdl);


Обращаемся к драйверу из пользовательского режима

(Весь код приложения смотрите в прилагаемых материалах)

Первое, что необходимо сделать, это получить манипулятор устройства (handle) с помощью функции CreateFile():

hDevice = CreateFile(L"\\\\.\\SharedMemory", GENERIC_READ|GENERIC_WRITE,
       FILE_SHARE_READ|FILE_SHARE_WRITE, NULL, OPEN_EXISTING, 0, NULL);


Затем необходимо отправить запрос ввода/вывода драйверу с помощью функции DeviceIoControl():

unsigned short**    vaReturned  =  new unsigned short*();
ioCtlCode = IOCTL_ALLOCATE_SHARED_MEMORY;
checker = DeviceIoControl(hDevice, ioCtlCode, NULL, 0,
               		vaReturned, sizeof(int), &bytesReturned, NULL);

Вызов функции преобразуется в IRP пакет, который будет обрабатываться в диспетчерской функции драйвера (см. DispatchControl() в файле control.cpp драйвера). Т.е. при вызове DeviceIoControl() управление передастся функции драйвера, код которой выше был описан. Так же, при вызове функции DeviceIoControl() в программе DebugView (надо галочку поставить, чтобы она отлавливала события режима ядра) увидим следующее:



По возвращению управления приложению переменная vaReturned будет указывать на разделяемую память (точнее будет указывать на указатель, который уже будет указывать на память). Сделаем небольшое упрощение, чтобы получить обычный указатель на память:

unsigned short*     data = *vaReturned;

Теперь по указателю data мы имеем доступ к разделяемой памяти из приложения:

При нажатии на кнопку «Allocate memory» приложение передает управление драйверу, который выполняет все действия, описанные выше, и возвращает указатель на выделенную память, доступ к которой из приложения будет осуществляться через указатель data. Кнопкой «Fill TextEdit» выводим содержимое первых 10-и элементов, которые были заполнены в драйвере, в QTextEdit и видим успешное обращение к разделяемой памяти.

При нажатии на кнопку «Release memory» происходит освобождение памяти и удаление созданной структуры MDL.


Исходники

1. source_driver.rar.
2. source_app.rar.
3. source_generic_oney.

За основу драйвера (source_driver) я взял один из примеров у Уолтера Они (примеры прилагаются к его книге «Использование Microsoft Windows Driver Model»). Так же необходимо скачать библиотеку ядра Generic, т.к. эта библиотека нужна как при сборке, так и при работе драйвера.

Тем, кто хочет попробовать сам

Создаем директорию (н-р, C:\Drivers) и распаковываем туда исходники (source_driver, source_generic_oney и source_app). Если не будете пересобирать драйвер, то достаточно установить новое оборудование вручную (указав inf-файл: sharedmemory.inf) через Панель управления-установка нового оборудования (для Windows XP). Затем надо запустить habr_app.exe (source_app/release).

Если решите пересобирать, то:
1. Необходимо установить WDK.
2. Сначала нужно будет пересобрать библиотеку Generic, т.к. в зависимости от версии ОС папки с выходными файлами могут по разному называться (н-р, для XP — objchk_wxp_x86, для Win7 — objchk_win7_x86).
3. После 1 и 2 пункта можно пробовать собрать драйвер командой «build» с помощью x86 Checked Build Environment, входящую в WDK.

Источники

  • Статьи с wasm.ru
  • Уолтер Они “Использование Microsoft Windows Driver Model” (ISBN 978-5-91180-057-4, 0735618038).
  • Джеффри Рихтер «Windows для профессионалов. Создание эффективных Win32-приложений» (ISBN 5-272-00384-5, 1-57231-996-8).
  • msdn

UPD: хабраюзер DZhon привел ссылку разделяемая память средствами Qt.
Tags:
Hubs:
+37
Comments6

Articles