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

в 0:06, , рубрики: ddk, driver, kernel-mode, системное программирование, метки: , ,

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

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

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

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

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

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

	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 %08Xn", 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 %08Xn", 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: Don't mapped memory into kernel spacen"));
			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 %08Xn", 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 spacen"));
			return STATUS_NONE_MAPPED;
			}

		}
	else
		{
		KdPrint(("SharedMemory: Don't allocate memory for MDLn"));
		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 ReleaseSharedMemoryn"));
	
	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 freedn", 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(hAccessDriver, 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 и видим успешное обращение к разделяемой памяти.

При нажатии на кнопку происходит освобождение памяти и удаление созданной структуры 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

Автор: kulinich


* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js