Драйверы режима ядра: Часть 5: Полнофункциональный драйвер

Дата публикации 21 янв 2003

Драйверы режима ядра: Часть 5: Полнофункциональный драйвер — Архив WASM.RU

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

Код (Text):
  1.  
  2.  
  3.  ;@echo off
  4.  ;goto make
  5.  
  6.  ;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
  7.  ;
  8.  ; VirtToPhys - Драйвер режима ядра
  9.  ;
  10.  ;  Переводит виртуальный адрес в физический
  11.  ;
  12.  ;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
  13.  
  14.  .386
  15.  .model flat, stdcall
  16.  option casemap:none
  17.  
  18.  ;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
  19.  ;                              В К Л Ю Ч А Е М Ы Е    Ф А Й Л Ы                                    
  20.  ;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
  21.  
  22.  include \masm32\include\w2k\ntstatus.inc
  23.  include \masm32\include\w2k\ntddk.inc
  24.  include \masm32\include\w2k\ntoskrnl.inc
  25.  include \masm32\include\w2k\w2kundoc.inc
  26.  
  27.  includelib \masm32\lib\w2k\ntoskrnl.lib
  28.  
  29.  include \masm32\Macros\Strings.mac
  30.  
  31.  include <b>..</b>\common.inc
  32.  
  33.  ;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
  34.  ;                             Н Е И З М Е Н Я Е М Ы Е    Д А Н Н Ы Е                                
  35.  ;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
  36.  
  37.  .const
  38.  CCOUNTED_UNICODE_STRING    "\\Device\\devVirtToPhys", g_usDeviceName, 4
  39.  CCOUNTED_UNICODE_STRING    "\\??\\slVirtToPhys", g_usSymbolicLinkName, 4
  40.  
  41.  ;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
  42.  ;                                              К О Д                                                
  43.  ;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
  44.  
  45.  .code
  46.  
  47.  ;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
  48.  ;                                    GetPhysicalAddress                                            
  49.  ;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
  50.  
  51.  GetPhysicalAddress proc dwAddress:DWORD
  52.  
  53.      mov eax, dwAddress
  54.      mov ecx, eax
  55.  
  56.      shr eax, 22
  57.      shl eax, 2
  58.  
  59.      mov eax, [0C0300000h][eax]
  60.  
  61.      .if ( eax & (mask pde4kValid) )
  62.          .if !( eax & (mask pde4kLargePage) )
  63.              mov eax, ecx
  64.              shr eax, 10
  65.              and eax, 1111111111111111111100y
  66.              add eax, 0C0000000h
  67.              mov eax, [eax]
  68.  
  69.              .if eax & (mask pteValid)
  70.                  and eax, mask ptePageFrameNumber
  71.  
  72.                  and ecx, 00000000000000000000111111111111y
  73.                  add eax, ecx
  74.              .else
  75.                  xor eax, eax
  76.              .endif
  77.          .else
  78.              and eax, mask pde4mPageFrameNumber
  79.              and ecx, 00000000001111111111111111111111y
  80.              add eax, ecx
  81.          .endif
  82.      .else
  83.          xor eax, eax
  84.      .endif
  85.  
  86.      ret
  87.  
  88.  GetPhysicalAddress endp
  89.  
  90.  ;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
  91.  ;                                   DispatchCreateClose                                            
  92.  ;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
  93.  
  94.  DispatchCreateClose proc pDeviceObject:PDEVICE_OBJECT, pIrp:PIRP
  95.  
  96.      mov eax, pIrp
  97.      assume eax:ptr _IRP
  98.      mov [eax].IoStatus.Status, STATUS_SUCCESS
  99.      and [eax].IoStatus.Information, 0
  100.      assume eax:nothing
  101.  
  102.      fastcall IofCompleteRequest, pIrp, IO_NO_INCREMENT
  103.  
  104.      mov eax, STATUS_SUCCESS
  105.      ret
  106.  
  107.  DispatchCreateClose endp
  108.  
  109.  ;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
  110.  ;                                     DispatchControl                                              
  111.  ;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
  112.  
  113.  DispatchControl proc uses esi edi ebx pDeviceObject:PDEVICE_OBJECT, pIrp:PIRP
  114.  
  115.  local status:NTSTATUS
  116.  local dwBytesReturned:DWORD
  117.  
  118.      and dwBytesReturned, 0
  119.  
  120.      mov esi, pIrp
  121.      assume esi:ptr _IRP
  122.  
  123.      IoGetCurrentIrpStackLocation esi
  124.      mov edi, eax
  125.      assume edi:ptr IO_STACK_LOCATION
  126.  
  127.      .if [edi].Parameters.DeviceIoControl.IoControlCode == IOCTL_GET_PHYS_ADDRESS
  128.          .if ( [edi].Parameters.DeviceIoControl.OutputBufferLength >= DATA_SIZE ) && ( [edi].Parameters.DeviceIoControl.InputBufferLength >= DATA_SIZE )
  129.  
  130.              mov edi, [esi].AssociatedIrp.SystemBuffer
  131.              assume edi:ptr DWORD
  132.  
  133.              xor ebx, ebx
  134.              .while ebx < NUM_DATA_ENTRY
  135.  
  136.                  invoke GetPhysicalAddress, [edi][ebx*(sizeof DWORD)]
  137.  
  138.                  mov [edi][ebx*(sizeof DWORD)], eax
  139.                  inc ebx
  140.              .endw
  141.  
  142.              mov dwBytesReturned, DATA_SIZE
  143.              mov status, STATUS_SUCCESS
  144.          .else
  145.              mov status, STATUS_BUFFER_TOO_SMALL
  146.          .endif
  147.      .else
  148.          mov status, STATUS_INVALID_DEVICE_REQUEST
  149.      .endif
  150.  
  151.      assume edi:nothing
  152.  
  153.      m2m [esi].IoStatus.Status, status
  154.      m2m [esi].IoStatus.Information, dwBytesReturned
  155.  
  156.      assume esi:nothing
  157.  
  158.      fastcall IofCompleteRequest, pIrp, IO_NO_INCREMENT
  159.  
  160.      mov eax, status
  161.      ret
  162.  
  163.  DispatchControl endp
  164.  
  165.  ;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
  166.  ;                                       DriverUnload                                                
  167.  ;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
  168.  
  169.  DriverUnload proc pDriverObject:PDRIVER_OBJECT
  170.  
  171.          invoke IoDeleteSymbolicLink, addr g_usSymbolicLinkName
  172.  
  173.          mov eax, pDriverObject
  174.          invoke IoDeleteDevice, (DRIVER_OBJECT PTR [eax]).DeviceObject
  175.  
  176.      ret
  177.  
  178.  DriverUnload endp
  179.  
  180.  ;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
  181.  ;               В Ы Г Р У Ж А Е М Ы Й   П Р И   Н Е О Б Х О Д И М О С Т И   К О Д                  
  182.  ;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
  183.  
  184.  .code INIT
  185.  
  186.  ;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
  187.  ;                                       DriverEntry                                                
  188.  ;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
  189.  
  190.  DriverEntry proc pDriverObject:PDRIVER_OBJECT, pusRegistryPath:PUNICODE_STRING
  191.  
  192.  local status:NTSTATUS
  193.  local pDeviceObject:PVOID
  194.  
  195.      mov status, STATUS_DEVICE_CONFIGURATION_ERROR
  196.  
  197.      invoke IoCreateDevice, pDriverObject, 0, addr g_usDeviceName, FILE_DEVICE_UNKNOWN, \
  198.                                               0, FALSE, addr pDeviceObject
  199.  
  200.      .if eax == STATUS_SUCCESS
  201.          invoke IoCreateSymbolicLink, addr g_usSymbolicLinkName, addr g_usDeviceName
  202.          .if eax == STATUS_SUCCESS
  203.              mov eax, pDriverObject
  204.              assume eax:PTR DRIVER_OBJECT
  205.              mov [eax].MajorFunction[IRP_MJ_CREATE*(sizeof PVOID)],           offset DispatchCreateClose
  206.              mov [eax].MajorFunction[IRP_MJ_CLOSE*(sizeof PVOID)],            offset DispatchCreateClose
  207.              mov [eax].MajorFunction[IRP_MJ_DEVICE_CONTROL*(sizeof PVOID)],   offset DispatchControl
  208.              mov [eax].DriverUnload,                                          offset DriverUnload
  209.              assume eax:nothing
  210.              mov status, STATUS_SUCCESS
  211.          .else
  212.              invoke IoDeleteDevice, pDeviceObject
  213.          .endif
  214.      .endif
  215.  
  216.      mov eax, status
  217.      ret
  218.  
  219.  DriverEntry endp
  220.  
  221.  ;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
  222.  ;                                                                                                  
  223.  ;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
  224.  
  225.  end DriverEntry
  226.  
  227.  :make
  228.  
  229.  set drv=VirtToPhys
  230.  
  231.  \masm32\bin\ml /nologo /c /coff %drv%.bat
  232.  \masm32\bin\link /nologo /driver /base:0x10000 /align:32 /out:%drv%.sys /subsystem:native /ignore:4078 %drv%.obj rsrc.obj
  233.  
  234.  del %drv%.obj
  235.  move %drv%.sys ..
  236.  
  237.  echo.
  238.  pause
  239.  
  240.  



Имя устройства и символьной ссылки

Начнем с определения структур UNICODE_STRING, описывающих имя устройства и символьной ссылки. Как я уже говорил в третьей части, ядро любит работать со строками именно в этом формате.

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

Код (Text):
  1.  
  2.  
  3.  .const
  4.  uszDeviceName          dw "\", "D", "e", "v", "i", "c", "e", "\", "D", "e", "v", "N", "a", "m", "e", 0
  5.  uszSymbolicLinkName    dw "\", "?", "?", "\", "D", "e", "v", "N", "a", "m", "e", 0
  6.  
  7.  .code
  8.  DriverEntry proc <b>. . .</b>
  9.  <b>. . .</b>
  10.  local usDeviceName:UNICODE_STRING
  11.  local usSymbolicLinkName:UNICODE_STRING
  12.  <b>. . .</b>
  13.      invoke RtlInitUnicodeString, addr usDeviceName, offset uszDeviceName
  14.      invoke RtlInitUnicodeString, addr usSymbolicLinkName, offset uszSymbolicLinkName
  15.  
  16.  

Задача функции RtlInitUnicodeString измерить unicode-строку и заполнить структуру UNICODE_STRING (см. Часть 3). Т.к. сами unicode-строки в этом коде определены статически, т.е. никогда не меняются, то можно, еще на этапе компиляции, заполнить структуру UNICODE_STRING. Это проще, нагляднее и требует меньшего количества байт (8 байт на структуру UNICODE_STRING + максимум 3 байта на выравнивание. Против минимум 14 байт на вызов функции RtlInitUnicodeString + временные издержки). Именно так и поступим, а макрос CCOUNTED_UNICODE_STRING еще больше облегчит нам жизнь, и весь вышеприведенный код (мы добавим еще выравнивание) превратится в две элегантные строки.

Код (Text):
  1.  
  2.  
  3.  CCOUNTED_UNICODE_STRING "\\Device\\DevName", usDeviceName,       4
  4.  CCOUNTED_UNICODE_STRING "\\??\\DevName",     usSymbolicLinkName, 4
  5.  
  6.  

Не знаю как вам, а мне этот вариант правится значительно больше. Функция же RtlInitUnicodeString требуется когда длина строки заранее неизвестна. В ntoskrnl.exe, кстати, имеется целый набор функций, как для заполнения структур *_STRING, так и для работы со строками вообще.

Код (Text):
  1.  
  2.  
  3.  .const
  4.  CCOUNTED_UNICODE_STRING    "\\Device\\devVirtToPhys", g_usDeviceName, 4
  5.  CCOUNTED_UNICODE_STRING    "\\??\\slVirtToPhys", g_usSymbolicLinkName, 4
  6.  
  7.  

Таким образом, мы имеем две глобальные переменные g_usDeviceName и g_usSymbolicLinkName типа UNICODE_STRING с именем устройства и именем символьной ссылки соответственно. Как я уже говорил в прошлый раз, префиксы "dev" и "sl" я добавил только для наглядности.

На более ранних выпусках Windows NT в пространстве имен диспетчера объектов отсутствует каталог "\??". В этом случае надо заменить в имени символьной ссылки "\??" на "\DosDevices". Причем, в более поздних выпусках это тоже будет работать, т.к. для совместимости в корневом каталоге пространства имен диспетчера объектов имеется символьная ссылка "\DosDevices", значением которой является строка "\??"


Процедура инициализации драйвера

Кое-что о процедуре DriverEntry мы уже узнали в третьей части статьи. Прежде чем мы пойдем дальше, обратите внимание на строку:

Код (Text):
  1.  
  2.  
  3.  .code INIT
  4.  
  5.  

Весь код, помеченный таким образом, помещается компоновщиком в отдельную секцию PE-файла с атрибутом Discardable. Благодаря этому, система автоматически выгрузит такой код при необходимости. Это позволяет экономно использовать ресурсы системной памяти. Весь код в процедуре DriverEntry нужен только один раз - во время инициализации драйвера. После этого он будет бесполезно занимать память. Но, так как он помещен в секцию "INIT", этого не произойдет. Но только в том случае, если кроме секции INIT на странице (страницах) больше ничего нет, т.к. частично выгружать страницы система не умеет.

Поскольку, процедура DriverEntry просто крошечная, и весь остальной код и данные тоже занимают очень мало места, а в параметре компоновщика /align:32 мы определили выравнивание секций по границе 32 байта, то весь драйвер, со всеми своими потрохами, умещается всего на одной странице памяти (4кБ). А как известно, минимальной единицей выделения памяти, на данный момент, является страница. Т.е. можно было и не использовать отдельную секцию, т.к. в данном случае это ничего не дает. В драйверах для более ранних выпусков Windows NT процедура DriverEntry занимала довольно приличное место, поэтому собственно Microsoft так и поступила.

Код (Text):
  1.  
  2.  
  3.      mov status, STATUS_DEVICE_CONFIGURATION_ERROR
  4.  
  5.  

Прежположим, что во время инициализации драйвера произойдет ошибка. Если это случится, то это значение и вернется системе, и на стороне режима пользователя вызов функции StartService завершится ошибкой.

Код (Text):
  1.  
  2.  
  3.      invoke IoCreateDevice, pDriverObject, 0, addr g_usDeviceName, FILE_DEVICE_UNKNOWN, \
  4.                                               0, FALSE, addr pDeviceObject
  5.  
  6.  

Поскольку, драйвер призван управлять виртуальным устройством, то первым делом мы и пытаемся это устройство создать. Функция IoCreateDevice выделяет память под необходимые структуры, в частности DEVICE_OBJECT, и инициализирует их. Ее прототип выглядит следующим образом:

Код (Text):
  1.  
  2.  
  3.  IoCreateDevice proto std call DriverObject:PDRIVER_OBJECT, DeviceExtensionSize:DWORD, \
  4.                                DeviceName:PUNICODE_STRING,  DeviceType:DEVICE_TYPE, \
  5.                                DeviceCharacteristics:DWORD, Exclusive:BOOL, \
  6.                                DeviceObject: PDEVICE_OBJECT
  7.  
  8.  

DriverObject

- указатель на объект "драйвер" (структура DRIVER_OBJECT), созданный системой;

DeviceExtensionSize

- произвольный размер области дополнительной памяти устройства (device extension), которую можно выделить в каждом объекте "устройство".

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

DeviceName

- необязательный указатель на имя устройства в формате UNICODE_STRING.

Для нас этот параметр обязателен. Мы должны создать именованное устройство, иначе мы не сможем создать символьную ссылку, а значит, код режима пользователя не сможет получить доступ к устройству;

DeviceType

- определяет уникальный идентификатор типа устройства.

Этого момента мы коснулись в прошлый раз, при описании управляющего кода ввода-вывода. У нас этот параметр всегда будет FILE_DEVICE_UNKNOWN;

DeviceCharacteristics

- дополнительная информация об устройстве.

У нас ничего такого нет, поэтому ставим в 0;

Exclusive

- определяет монопольный доступ к устройству.

Опять же в прошлый раз, при описании функции CreateFile, я говорил, что мне не удалось получить монопольный доступ к устройству используя параметр dwShareMode. Это можно сделать с помощью Exclusive. Трудно сказать нужно нам это или нет, поэтому, мы разрешаем одновременное использование устройства несколькими приложениями устанавливая этот параметр в FALSE;

DeviceObject

- этот параметр является возвращаемым и будет указывать, при успешном завершении функции IoCreateDevice, на высокопарно называемый Microsoft объект "устройство", являющийся на самом деле структурой DEVICE_OBJECT. Через эту структуру система и будет им управлять.

Если создать символьную ссылку не удастся, то указатель на объект "устройство" потребуется для его удаления. Поэтому, мы передаем в этом параметре адрес локальной переменной pDeviceObject. Можно сохранить указатель на объект "устройство" и в глобальной переменной и использовать его в процедуре выгрузки драйвера, но я этого делать не стал, чтобы не создавать лишнюю секцию ?data. А при выгрузке драйвера мы извлечем этот указатель прямо из объекта "драйвер".

Код (Text):
  1.  
  2.  
  3.      .if eax == STATUS_SUCCESS
  4.          invoke IoCreateSymbolicLink, addr g_usSymbolicLinkName, addr g_usDeviceName
  5.  
  6.  

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

Код (Text):
  1.  
  2.  
  3.          .if eax == STATUS_SUCCESS
  4.              mov eax, pDriverObject
  5.              assume eax:ptr DRIVER_OBJECT
  6.              mov [eax].MajorFunction[IRP_MJ_CREATE*(sizeof PVOID)],           offset DispatchCreateClose
  7.              mov [eax].MajorFunction[IRP_MJ_CLOSE*(sizeof PVOID)],            offset DispatchCreateClose
  8.              mov [eax].MajorFunction[IRP_MJ_DEVICE_CONTROL*(sizeof PVOID)],   offset DispatchControl
  9.  
  10.  

Если создание символьной ссылки прошло успешно, переходим к следующему этапу.

В состав структуры DRIVER_OBJECT входит массив MajorFunction. В этот масив помещаются указатели на процедуры диспетчеризации (dispatch routines), предназначенные для обработки разных типов пакетов запроса ввода-вывода (IRP). Каждый элемент этого массива соответствует своему типу IRP. Если, например, драйверу необходимо обрабатывать запрос типа IRP_MJ_SHUTDOWN, уведомляющий о завершении работы системы, то он должен поместить в соответствующую позицию массива MajorFunction указатель на функцию, которой запросы этого типа и будут направляться. Если такая функциональность драйверу не нужна, как в нашем случае, то и заполнять этот элемент массива MajorFunction не требуется. Т.о. мы совсем не обязаны заполнять все элементы массива MajorFunction, коих в Windows 2000 DDK определено аж целых 28 штук (IRP_MJ_MAXIMUM_FUNCTION+1). Все зависит от задач стоящих перед драйвером. Например, драйвер beeper.sys и так прекрасно справлялся со своей работой, вообще не устанавливая никаких процедур диспетчеризации. В элементы массива MajorFunction не заполненные драйвером диспетчер ввода-вывода заносит указатель на внутреннюю функцию IopInvalidDeviceRequest. Эта функция уведомляет о попытке обращения к неподдерживаемой данным драйвером функции.

Для того, чтобы код режима пользователя смог успешно вызывать функции CreateFile, DeviceIoControl и CloseHandle, мы должны обрабатывать как минимум три типа запросов:

IRP_MJ_CREATE

- формируется диспетчером ввода-вывода при вызове кодом режима пользователя функции CreateFile;

IRP_MJ_DEVICE_CONTROL

- формируется диспетчером ввода-вывода при вызове кодом режима пользователя функции DeviceIoControl;

IRP_MJ_CLOSE

- формируется диспетчером ввода-вывода при вызове кодом режима пользователя функции CloseHandle.

Запросы типов IRP_MJ_CREATE и IRP_MJ_CLOSE будет обрабатывать одна функция DispatchCreateClose. Почему узнаем позже.

В ntddk.inc, среди прочих, определены константы могущие представлять для нас интерес:

Код (Text):
  1.  
  2.  
  3.  IRP_MJ_CREATE               equ 0
  4.  . . .
  5.  IRP_MJ_CLOSE                equ 2
  6.  IRP_MJ_READ                 equ 3
  7.  IRP_MJ_WRITE                equ 4
  8.  . . .
  9.  IRP_MJ_DEVICE_CONTROL       equ 0Eh
  10.  . . .
  11.  IRP_MJ_CLEANUP              equ 12h
  12.  
  13.  

Это порядковые номера, определяющие положение указателя на процедуру диспетчеризации в массиве MajorFunction. Умножив соответствующую константу на размер указателя, равный 4 байтам, мы получим смещение в массиве MajorFunction, по которому должен быть расположен указатель на соответствующую процедуру диспетчеризации.

Код (Text):
  1.  
  2.  
  3.              mov [eax].DriverUnload,                                          offset DriverUnload
  4.  
  5.  

Еще одно ключевое поле структуры DRIVER_OBJECT DriverUnload. Туда мы должны поместить, если хотим иметь возможность динамически выгружать драйвер, указатель на процедуру, которую система будет вызывать при обращении кода режима пользователя к функции ControlService с параметром SERVICE_CONTROL_STOP.

Имена процедур инициализации, диспетчеризации и выгрузки драйвера могут быть произвольными.

Код (Text):
  1.  
  2.  
  3.              assume eax:nothing
  4.              mov status, STATUS_SUCCESS
  5.  
  6.  

Если мы благополучно дошли до этой точки, то инициализация драйвера прошла успешно, о чем мы и сообщаем системе, возвращая код успеха STATUS_SUCCESS.

Код (Text):
  1.  
  2.  
  3.          .else
  4.              invoke IoDeleteDevice, pDeviceObject
  5.          .endif
  6.      .endif
  7.  
  8.  

Если же создать символьную ссылку не удалось, то мы должны удалить объект "устройство", созданный на предыдущем этапе функцией IoCreateDevice, чтобы привести систему в прежнее состояние. Именно должны! Это вам не режим пользователя, где после завершения процесса все объекты ему принадлежащие автоматически уничтожаются системой. В режиме ядра никто за нас делать грязную работу не будет. Это общее правило. Выделили память - освободите. Создали объект - удалите. Получили ссылку на объект - "отдайте" назад. И т.д. и т.п. Запомните это.

Код (Text):
  1.  
  2.  
  3.      mov eax, status
  4.      ret
  5.  
  6.  

Возвращаем системе текущий код состояния. Если это STATUS_SUCCESS, то драйвер остается в памяти и диспетчер ввода-вывода будет формировать и направлять его устройствам пакеты запроса ввода-вывода. Если это какое-либо другое значение, то драйвер удаляется.

Таким образом, после удачного завершения DriverEntry в системе появится три новых объекта:

Объект "драйвер", представляющий отдельный драйвер в системе.

Из этого объекта (точнее из массива MajorFunction структуры DRIVER_OBJECT) диспетчер ввода-вывода получает адреса процедур диспетчеризации;

Объект "устройство", представляющий в системе устройство.

Из этого объекта (точнее из поля DriverObject структуры DEVICE_OBJECT) диспетчер ввода-вывода получает указатель на объект "драйвер" это устройство обслуживающий;

Объект "файл", представляющий для кода режима пользователя объект "устройство".

Из этого объекта (точнее из поля DeviceObject структуры FILE_OBJECT) диспетчер ввода-вывода получает указатель на объект "устройство";

Этот объект прозрачен для нас. О том, как объект "файл" возникает, мы говорили в прошлый раз;

Символьная ссылка, видимая коду режима пользователя.

Используется диспетчером объектов для трансляции во внутреннее имя устройства.

На рис. 5-1 показаны основные взаимосвязи этих объектов. Эта схема поможет вам глубже понять дальнейший материал.

Рис. 5-1. Взаимосвязи объектов "файл", "устройство" и "драйвер" с пакетом запроса ввода-вывода, и кодом драйвера




Процедуры диспетчеризации запросов ввода-вывода

Все процедуры диспетчеризации имеют одинаковый прототип.

Код (Text):
  1.  
  2.  
  3.  DispatchRoutine proto stdcall pDeviceObject:PDEVICE_OBJECT, pIrp:PIRP
  4.  
  5.  

pDeviceObject

- указатель на объект "устройство" (структура DEVICE_OBJECT).

Если драйвер обслуживает несколько устройств, то по значению этого параметра можно определить, к какому именно устройству пришел запрос. Или обратиться к области дополнительной памяти устройства. В нашем случае, в процедурах диспетчеризации, использовать указатель на объект "устройство" не требуется;

pIrp

- указатель на текущий пакет запроса ввода-вывода (структура _IRP).

Диспетчер ввода-вывода создает IRP, представляющий операцию ввода-вывода. Через параметр pIrp драйверу передается указатель на IRP. Получив IRP, драйвер выполняет указанную в пакете операцию и возвращает его диспетчеру ввода-вывода, чтобы тот, либо завершил эту операцию, либо передал пакет другому драйверу для дальнейшей обработки. Завершать IRP или передавать его дальше решает сам драйвер. Наш драйвер одноуровневый, поэтому никакой дальнейшей обработки не требуется. По завершении операции ввода-вывода, результаты возвратятся коду режима пользователя, а IRP будет удален.

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


Процедура обработки IRP типов IRP_MJ_CREATE и IRP_MJ_CLOSE

Для начала о том, почему процедура DispatchCreateClose обрабатывает у нас запросы такого разного типа. Запросы типов IRP_MJ_CREATE и IRP_MJ_CLOSE направляются драйверу, когда код режима пользователя вызывает функции CreateFile и CloseHandle, соответственно. Единственное, что нам надо сделать для обработки обоих запросов, это заполнить блок статуса ввода-вывода (I/O status block) - вложенную в _IRP структуру IO_STATUS_BLOCK - и вернуть код успеха. Поэтому, чтобы не плодить лишние процедуры-близнецы, я их и объединил.

Если же предполагается, при обработке запроса типа IRP_MJ_CREATE, выделять дополнительную память, инициализировать какие-либо переменные и .т.п, то при закрытии описателя устройства, т.е. при вызове кодом режима пользователя CloseHandle, эти структуры придется удалять. Т.е. действия драйвера при получении пакетов IRP_MJ_CREATE и IRP_MJ_CLOSE будут отличаться. В этом случае, естественно, процедуры обработки следует разделить.

Код (Text):
  1.  
  2.  
  3.  DispatchCreateClose proc pDeviceObject:PDEVICE_OBJECT, pIrp:PIRP
  4.  
  5.      mov eax, pIrp
  6.      assume eax:ptr _IRP
  7.      mov [eax].IoStatus.Status, STATUS_SUCCESS
  8.      and [eax].IoStatus.Information, 0
  9.      assume eax:nothing
  10.  
  11.  

Заполняем блок статуса ввода-вывода, определяющего состояние запроса ввода-вывода.

Значение поля Status определяет, как завершится на стороне режима пользователя вызов функций CreateFile и CloseHandle. Мы хотим, чтобы пользовательский процесс открывал и закрывал описатель устройства без ошибок. Поэтому, помещаем в поле Status код успеха STATUS_SUCCESS.

Смысл поля Information зависит от типа запроса. Это может быть значение количества байт переданных в пользовательское приложение или указатель на структуру. В данном случае мы поместим в это поле 0. При обработке запроса типа IRP_MJ_DEVICE_CONTROL (в процедуре DispatchControl), мы должны будем поместить туда количество байт, которое необходимо скопировать в пользовательский буфер.

Код (Text):
  1.  
  2.  
  3.      fastcall IofCompleteRequest, pIrp, IO_NO_INCREMENT
  4.  
  5.      mov eax, STATUS_SUCCESS
  6.      ret
  7.  
  8.  DispatchCreateClose endp
  9.  
  10.  

Вызов функции IofCompleteRequest инициирует операцию завершения ввода-вывода (I/O completion).

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

Если драйвер обслуживает физическое устройство, то операция ввода-вывода может длиться ощутимое время. Пока поток ждет завершения операции, система не предоставляет ему процессорное время (при синхронных операциях ввода-вывода, как в нашем случае). После окончания операции ожидавший поток вправе немедленно возобновить выполнение и обработать полученные данные. Именно через второй параметр функции IofCompleteRequest драйвер и сообщает на какую величину повысить приоритет ожидавшего потока. Для устройств "тугодумов" предусмотрены бОльшие значения повышения приоритета. Например, для звуковых карт DDK рекомендует использовать константу IO_SOUND_INCREMENT, равную 8.

В нашем случае, повышения приоритета потока не требуется, и мы передаем IO_NO_INCREMENT равную нулю.

Функция IofCompleteRequest является fastcall-функцией (см. ниже). В префиксе имени функции присутствует символ 'f'. Существует, кстати сказать, и stdcall вариант IoCompleteRequest. Обратите внимание на отсутствие символа 'f' в префиксе. Но, в образовательных целях, мы будем использовать быструю версию. Эта не единственная fastcall-функция - есть и другие. И у них также есть свои stdcall аналоги, которые, как правило, являются оболочками вокруг соответствующих fastcall-функций.


Соглашение о передаче параметров

В функциях API ядра Windows NT используется три типа соглашений о передаче параметров (calling convention): stdcall, cdecl и fastcall. Последний тип не поддерживается компилятором masm.

Такие функции принимают первый аргумент в регистре ecx, второй в edx, остальные, при наличии таковых, помещаются в стек в обратном порядке (справа налево). Стек очищает вызванная функция.

Декорированные имена функций fastcall начинаются с символа "@", после имени добавляется символ "@" с десятичным числом, обозначающим суммарный размер аргументов в байтах. Например, функция IofCompleteRequest декорируется следующим образом:

Код (Text):
  1.  
  2.  
  3.  @IofCompleteRequest@8
  4.  
  5.  

Это значит, что это fastcall-функция, экспортируемое имя IofCompleteRequest, принимает два аргумента размером DWORD каждый.

В файле \include\w2k\ntoskrnl.inc она определена следующим образом (на ключевое слово SYSCALL можете не обращать внимание):

Код (Text):
  1.  
  2.  
  3.  EXTERNDEF SYSCALL @IofCompleteRequest@8:PROC
  4.  IofCompleteRequest TEXTEQU &lt@IofCompleteRequest@8&gt
  5.  
  6.  

Для упрощения вызова таких функций я написал макрос fastcall:

Код (Text):
  1.  
  2.  
  3.  fastcall MACRO api:REQ, p1, p2, px:VARARG
  4.  
  5.  local arg
  6.  
  7.      ifnb &ltpx&gt
  8.          % for arg, @ArgRev( &ltpx&gt )
  9.              push arg
  10.          endm
  11.      endif
  12.  
  13.      ifnb &ltp1&gt
  14.  
  15.          ifdifi &ltp1&gt, &ltecx&gt
  16.              mov ecx, p1
  17.          endif
  18.  
  19.          ifnb &ltp2&gt
  20.              ifdifi &ltp2&gt, &ltedx&gt
  21.                  mov edx, p2
  22.              endif
  23.          endif
  24.  
  25.      endif
  26.  
  27.      call api
  28.  
  29.  ENDM
  30.  
  31.  

Здесь, для экономии места, я привожу упрощенную версию этого макроса. Чтобы не плодить лишние файлы с макросами, мне пришлось поместить этот макрос в \include\w2k\ntddk.inc, где и можно посмотреть полный вариант. В оригинальном ntddk.h такого макроса, естественно, нет.


Виды управления буферами при обработке IRP

Напоследок, хочу еще немножко помучить вас теорией ;-)

Диспетчер ввода-вывода поддерживает три вида управления буферами:

  • буферизованный ввод-вывод (buffered I/O);
  • прямой ввод-вывод (direct I/O);
  • ввод-вывод без управления (neither I/O).

Этого момента мы чуть-чуть касались в прошлой статье. Теперь поговорим более обстоятельно. Здесь, мы будем рассматривать только случай применения функции DeviceIoControl для организации ввода-вывода. Использование ReadFile и WriteFile не рассматриваются. Пример использования ReadFile смотрите в \src\Article4-5\NtBuild.


буферизованный ввод-вывод

Именно этот вид управления используется в драйвере VirtToPhys.

Диспетчер ввода-вывода выделяет в системном пуле неподкачиваемой памяти буфер, равный по размеру наибольшему (если определены, как входной, так и выходной) из пользовательских буферов.

Создавая IRP, при операции записи, диспетчер ввода-вывода копирует данные из пользовательского входного буфера в выделенный системный буфер и передает драйверу его адрес в поле AssociatedIrp.SystemBuffer структуры _IRP. Размер скопированных данных помещается в поле Parameters.DeviceIoControl.InputBufferLength структуры IO_STACK_LOCATION (на эту структуру указывает поле Tail.Overlay.CurrentStackLocation структуры _IRP, значение которого можно получить используя макрос IoGetCurrentIrpStackLocation).

Драйвер обрабатывает IRP и помещает выходные данные (при наличии таковых) в тот же самый системный буфер. При этом, находящиеся там входные данные, естественно, переписываются. Т.е. пользовательских буферов два, а системный один. Если входные данные нужны драйверу, то он должен их где-нибудь сохранить.

Завершая IRP, при операции чтения, диспетчер ввода-вывода копирует данные из системного буфера в пользовательский выходной буфер. Размер копируемых данных извлекается из поля IoStatus.Information структуры _IRP. Это поле должен заполнить драйвет - только он знает сколько байт поместил в системный буфер.

Таким образом, диспетчер ввода-вывода дважды копирует буферы. Именно поэтому, этот вид управления самый медленный, но при сравнительно малых размерах буферов издержки незначительны.

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

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

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

Таким образом, этот вид управления буферами следует использовать при относительно небольших размерах буферов. Или если опереция ввода-вывода выполняется не часто.

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


прямой ввод-вывод

Используется для организации прямого доступа к памяти (direct memory access, DMA).

Я не буду подробно рассматривать этот вид управления, т.к. в контексте данных статей ему нет применения.

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

Диспетчер ввода-вывода блокирует выходной буфер в памяти, делая его неподкачиваемым, и создает MDL (Memory Descriptor List), описывающий физические страницы занимаемые буфером. Указатель на MDL помещается в поле MdlAddress структуры _IRP. Подробнее см. DDK.

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


ввод-вывод без управления

Диспетчер ввода-вывода помещает в поле DeviceIoControl.Type3InputBuffer структуры IO_STACK_LOCATION указатель на пользовательский входной буфер, а в поле UserBuffer структуры _IRP указатель на пользовательский выходной буфер и оставляет драйверу возможность управлять ими самостоятельно. Т.о. вся ответственность за управление пользовательскими буферами ложится на драйвер. Он может блокировать их в памяти, отображать на системное адресное пространство или обращаться к ним напрямую и т.д.

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

Поскольку, мы всегда (в рассмотренных драйверах) выполняемся при IRQL = PASSIVE_LEVEL, то и блокировать пользовательские буферы тоже нет необходимости.

Остается одна проблема - пользовательский поток может передать заведомо неверный адрес буфера, например, попадающий в системное адресное пространство или адрес невыделенной области памяти и т.п. Или пользовательский процесс может быть многопоточным, и один из потоков может освободить память занимаемую буфером во время обработки драйвером запроса ввода-вывода. Такие ситуации надо просто предвидеть и корректно обрабатывать. Использование структурной обработки исключений (Structured Exception Handling, SEH) при этом обязательно (пример см. \src\Article4-5\NtBuild). Учтите только, что хотя структурная обработка исключений в режиме ядра принципиально ничем не отличается от режима пользователя, обрабатывать таким образом все исключения не удастся. Например, попытка деления на ноль приведет к появлению BSOD даже при установленном обработчике SEH.


Процедура обработки IRP типа IRP_MJ_DEVICE_CONTROL

Запрос типа IRP_MJ_DEVICE_CONTROL направляется драйверу, когда код режима пользователя вызывает функцию DeviceIoControl.

Код (Text):
  1.  
  2.  
  3.      and dwBytesReturned, 0
  4.  
  5.  

Если произойдет ошибка, то диспетчер ввода-вывода не должен ничего копировать в пользовательский буфер.

Код (Text):
  1.  
  2.  
  3.      mov esi, pIrp
  4.      assume esi:ptr _IRP
  5.  
  6.      IoGetCurrentIrpStackLocation esi
  7.      mov edi, eax
  8.      assume edi:ptr IO_STACK_LOCATION
  9.  
  10.  

Макрос IoGetCurrentIrpStackLocation извлекает из структуры _IRP указатель на текущий блок стека ввода-вывода (I/O stack). Это структура IO_STACK_LOCATION. Таких блоков в пакете запроса ввода-вывода может быть несколько, в зависимости от того, сколько драйверов обслуживают запрос. У нас драйвер один и блок стека будет один.

Код (Text):
  1.  
  2.  
  3.      .if [edi].Parameters.DeviceIoControl.IoControlCode == IOCTL_GET_PHYS_ADDRESS
  4.  
  5.  

Если управляющий код ввода-вывода не соответствует ожидаемому, ничего делать не будем. Этот код, на стороне режима пользователя, передается в функцию DeviceIoControl в параметре dwIoControlCode. О правилах формирования управляющего кода мы подробно говорили в прошлый раз.

Определение значения управляющего кода ввода-вывода IOCTL_GET_PHYS_ADDRESS и констант NUM_DATA_ENTRY и DATA_SIZE вынесено в отдельный файл common.inc

Код (Text):
  1.  
  2.  
  3.  NUM_DATA_ENTRY         equ 4
  4.  DATA_SIZE              equ (sizeof DWORD) * NUM_DATA_ENTRY
  5.  IOCTL_GET_PHYS_ADDRESS equ CTL_CODE(FILE_DEVICE_UNKNOWN, 800h, METHOD_BUFFERED, FILE_READ_ACCESS + FILE_WRITE_ACCESS)
  6.  
  7.  

Эти значения требуются, как в исходном тексте программы управления, так и в исходном тексте драйвера.

Код (Text):
  1.  
  2.  
  3.          .if ( [edi].Parameters.DeviceIoControl.OutputBufferLength >= DATA_SIZE ) && ( [edi].Parameters.DeviceIoControl.InputBufferLength >= DATA_SIZE )
  4.  
  5.  

Проверяем размер входного и выходного буферов. Если хотя бы один из них меньше чем требуется, прекращаем обработку запроса.

Поля OutputBufferLength и InputBufferLength вложенной в IO_STACK_LOCATION структуры DeviceIoControl соответствуют параметрам nOutBufferSize и nInBufferSize функции режима пользователя DeviceIoControl.

Код (Text):
  1.  
  2.  
  3.              mov edi, [esi].AssociatedIrp.SystemBuffer
  4.  
  5.  

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

Код (Text):
  1.  
  2.  
  3.              assume edi:ptr DWORD
  4.  
  5.  

Т.к. адрес имеет размер двойного слова, даем понять компилятору, что регистр edi указывает на ячейку памяти размером DWORD.

Код (Text):
  1.  
  2.  
  3.              xor ebx, ebx
  4.              .while ebx < NUM_DATA_ENTRY
  5.  
  6.                  invoke GetPhysicalAddress, [edi][ebx*(sizeof DWORD)]
  7.  
  8.                  mov [edi][ebx*(sizeof DWORD)], eax
  9.                  inc ebx
  10.              .endw
  11.  
  12.  

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

Получив преобразованное значение, помещаем его обратно в системный буфер, на то же самое место.

Код (Text):
  1.  
  2.  
  3.              mov dwBytesReturned, DATA_SIZE
  4.              mov status, STATUS_SUCCESS
  5.  
  6.  

Если мы добрались до этой точки, то обработка запроса закончена. Помещаем в переменные dwBytesReturned и status соответствующие значения.

Код (Text):
  1.  
  2.  
  3.          .else
  4.              mov status, STATUS_BUFFER_TOO_SMALL
  5.          .endif
  6.      .else
  7.          mov status, STATUS_INVALID_DEVICE_REQUEST
  8.      .endif
  9.  
  10.  

Если происходит какая-нибудь ошибка, то переменная status получает соответствующий код ошибки.

Код (Text):
  1.  
  2.  
  3.      assume edi:nothing
  4.  
  5.      push status
  6.      pop [esi].IoStatus.Status
  7.  
  8.  

Завершая операцию ввода-вывода, помещаем в поле Status блока статуса ввода-вывода текущее значение переменной status. На стороне режима пользователя этот код транслируется в код ошибки, который можно получить, вызвав функцию GetLastError. Трансляция происходит, естественно, не один к одному, т.к. в режимах пользователя и ядра разные наборы кодов ошибок.

Код (Text):
  1.  
  2.  
  3.      push dwBytesReturned
  4.      pop [esi].IoStatus.Information
  5.  
  6.  

В поле Information блока статуса ввода-вывода помещаем текущее значение переменной dwBytesReturned. Завершая IRP, диспетчер ввода-вывода извлекает из этого поля значение количества байт, которое он должен скопировать из системного в пользовательский буфер, указатель на который передается в параметре lpOutBuffer функции DeviceIoControl на стороне режима пользователя. Это же значение вернется коду режима пользователя в двойном слове, указатель на которое помещен в параметр lpBytesReturned функции DeviceIoControl.

Если произойдет какая-либо ошибка, то переменная dwBytesReturned у нас инициализирована нулевым значением. Если же все пойдет по плану, то в переменной dwBytesReturned окажется значение DATA_SIZE. Именно это количество байт диспетчер ввода-вывода и скопирует в пользовательский буфер.

Код (Text):
  1.  
  2.  
  3.      assume esi:nothing
  4.  
  5.      fastcall IofCompleteRequest, pIrp, IO_NO_INCREMENT
  6.  
  7.      mov eax, status
  8.      ret
  9.  
  10.  

Даем указание диспетчеру ввода-вывода завершить обработку IPR и возвращаем системе текущий код состояния.


Процедура трансляции адресов

Код режима ядра, в отличие от кода режима пользователя, может транслировать виртуальные адреса в физические и наоборот. Для этого необходим доступ к каталогу страниц (page directory) и самим таблицам страниц (page tables). В Windows NT для x86-систем каталог страниц процесса расположен по виртуальному адресу 0C0300000h (режим проецирования памяти Physical Address Extension мы не рассматриваем), а таблицы страниц процесса начинаются с виртуального адреса 0C0000000h.

Я не буду рассматривать теорию механизма трансляции адресов - очень подробно об этом можно почитать в рекомендованной в первой части статьи литературе или найти в сети.

Я предполагал, что данный материал будут читать люди с разной подготовкой. Если вы впервые услышали об этих понятиях, можно пропустить этот раздел. А чтобы вам было не так обидно, удалите код процедуры GetPhysicalAddress, а ее вызов замените на вызов функции ядра MmGetPhysicalAddress. Таким образом MmGetPhysicalAddress превращается в "черный ящик", на вход которого поступает виртуальный адрес, а на выходе вы имеете соответствующий ему физический.

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

Код (Text):
  1.  
  2.  
  3.      mov eax, dwAddress
  4.      mov ecx, eax
  5.  
  6.  

Сохраняем виртуальный адрес в регистре ecx для дальнейшего использования.

Код (Text):
  1.  
  2.  
  3.      shr eax, 22
  4.  
  5.  

Выделяем старшие 10 бит, которые являются индексом каталога страниц (page directory index).

Код (Text):
  1.  
  2.  
  3.      shl eax, 2
  4.  
  5.  

Умножаем индекс на размер элемента каталога страниц (page directory entry, PDE), равный 4 байтам. Т.о. в регистре eax получаем смещение нужного PDE от начала каталога.

Код (Text):
  1.  
  2.  
  3.      mov eax, [0C0300000h][eax]
  4.  
  5.  

Извлекаем содержимое PDE.

Код (Text):
  1.  
  2.  
  3.      .if ( eax & (mask pde4kValid) )
  4.  
  5.  

12 младших бит PDE (см. запись HARDWARE_PDE4K_X86 в \include\w2k\w2kundoc.inc) содержат атрибуты PDE. Если флаг Valid (бит 0) установлен, то PDE действителен.

Код (Text):
  1.  
  2.  
  3.          .if !( eax & (mask pde4kLargePage) )
  4.  
  5.  

Флаг Large Page (бит 7) указывает, что PDE относится к 4-мегабайтной странице (на данный момент, такие страницы используются ядром (по крайней мере в Windows 2000 это так) для проецирования ntoskrnl.exe и hal.dll в диапазон виртуальных адресов 80000000h - 9FFFFFFFh). Для PDE соответствующих 4-килобайтным и 4-мегабайтным страницам трансляция адреса отличается.

Код (Text):
  1.  
  2.  
  3.              mov eax, ecx
  4.              shr eax, 10
  5.              and eax, 1111111111111111111100y
  6.  
  7.  

Если PDE соответствует 4-килобайтной странице, выделяем биты 12-21, которые являются индексом таблицы страниц (page table index), одновременно "умножая" индекс на размер элемента таблицы страниц (page table entry, PTE) равный 4 байтам. Т.о. в регистре eax получаем смещение нужного PTE от начала всего массива таблиц страниц.

Код (Text):
  1.  
  2.  
  3.          add eax, 0C0000000h
  4.          mov eax, [eax]
  5.  
  6.  

Прибавив базовый адрес массива таблиц страниц процесса, извлекаем PTE.

Код (Text):
  1.  
  2.  
  3.          .if eax & (mask pteValid)
  4.  
  5.  

12 младших бит PТE (см. запись HARDWARE_PTE_X86 в \include\w2k\w2kundoc.inc) содержат атрибуты PTE. Если флаг Valid (бит 0) установлен, то PTE соответствует странице в физической памяти.

Код (Text):
  1.  
  2.  
  3.              and eax, mask ptePageFrameNumber
  4.  
  5.  

Если PTE действителен, то его старшие 20 бит содержат номер фрейма страницы (page frame number, PFN) являющийся номером страницы в пространстве физических адресов. Этой командой мы одним махом выделяем PFN и "умножаем" его на размер страницы (4кб) и получаем физический адрес страницы.

Код (Text):
  1.  
  2.  
  3.                  and ecx, 00000000000000000000111111111111y
  4.                  add eax, ecx
  5.              .else
  6.                  xor eax, eax
  7.              .endif
  8.  
  9.  

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

Код (Text):
  1.  
  2.  
  3.          .else
  4.              and eax, mask pde4mPageFrameNumber
  5.  
  6.  

Если PDE соответствует 4-мегабайтной странице (см. запись HARDWARE_PDE4M_X86 в \include\w2k\w2kundoc.inc), выделяем биты 22-31, получая PFN, который, в случае больших страниц, и является физическим адресом страницы.

Код (Text):
  1.  
  2.  
  3.              and ecx, 00000000001111111111111111111111y
  4.              add eax, ecx
  5.          .endif
  6.      .else
  7.          xor eax, eax
  8.      .endif
  9.  
  10.  

Выделяем и добавляем адрес байта в пределах страницы. Обработку больших страниц я добавил для полноты. В нашем случае, все четыре адреса соответствуют 4-килобайтным страницам.

Если что-то не так возвращаем 0. На стороне режима пользователя надо бы еще проверить, не вернул ли драйвер вместо какого-нибудь адреса 0. Но, для простоты, я этого делать не стал.

Работа с записями не часто встречается в практике программирования на ассемблере. Если не знакомы с директивой mask, то вместо

Код (Text):
  1.  
  2.  
  3.  mask pde4kValid
  4.  mask pde4kLargePage
  5.  mask pteValid
  6.  mask ptePageFrameNumber
  7.  mask pde4mPageFrameNumber
  8.  
  9.  

используйте

Код (Text):
  1.  
  2.  
  3.  01y
  4.  010000000y
  5.  01y
  6.  11111111111111111111000000000000y
  7.  11111111110000000000000000000000y
  8.  
  9.  



Процедура выгрузки драйвера

Диспетчер ввода-вывода вызывает процедуру DriverUnload, когда код режима пользователя закрывает описатель устройства драйвера, точнее, последний описатель, т.к. может быть открыто несколько описателей устройства, или несколькими процессами. Если же существует хотя бы один описатель устройства, то к нему (устройству) можно обратиться, а это приведет к посылке его драйверу IRP, т.е. драйвер должен быть зыгружен.

Код (Text):
  1.  
  2.  
  3.      invoke IoDeleteSymbolicLink, addr g_usSymbolicLinkName
  4.  
  5.      mov eax, pDriverObject
  6.      invoke IoDeleteDevice, (DRIVER_OBJECT PTR [eax]).DeviceObject
  7.  
  8.  

Тут код говорит сам за себя. В процедуре выгрузки драйвера мы должны произвести действия прямо противоположные действиям в процедуре инициализации, т.е. удалить все созданные нами объекты. Как я уже говорил выше, в режиме ядра никто за нас подтирать не будет.

Таблица 5-1 подытоживает рассмотреный материал. В ней показано, к вызову каких процедур в драйвере приводит обращение кода режима пользователя к той или иной функции ввода-вывода, с указанием контекста и уровня запроса прерывания.

Таблица 5-1. Соответствие пользовательских функций процедурам драйвера


Режим пользователя

Режим ядра

Контекст

IRQL

StartService

DriverEntry

Процесса System

PASSIVE_LEVEL

CreateFile

IRP_MJ_CREATE

Вызывающего процесса

PASSIVE_LEVEL

DeviceIoControl

IRP_MJ_DEVICE_CONTROL

Вызывающего процесса

PASSIVE_LEVEL

ReadFile

IRP_MJ_READ

Вызывающего процесса

PASSIVE_LEVEL

WriteFile

IRP_MJ_WRITE

Вызывающего процесса

PASSIVE_LEVEL

CloseHandle

IRP_MJ_CLEANUP, IRP_MJ_CLOSE

Вызывающего процесса

PASSIVE_LEVEL

ControlService,,SERVICE_CONTROL_STOP

DriverUnload

Процесса System

PASSIVE_LEVEL




Компиляция

Код (Text):
  1.  
  2.  
  3.  :make
  4.  
  5.  set drv=skeleton
  6.  
  7.  \masm32\bin\ml /nologo /c /coff %drv%.bat
  8.  \masm32\bin\link /nologo /driver /base:0x10000 /align:32 /out:%drv%.sys /subsystem:native /ignore:4078 %drv%.obj rsrc.obj
  9.  
  10.  del %drv%.obj
  11.  move %drv%.sys <b>..</b>
  12.  
  13.  echo.
  14.  pause
  15.  
  16.  

Все это мы с вами уже разобрали в части 3. Я добавил ключ /ignore:4078, т.к. из-за того, что у нас появилась вторая секция кода "INIT" с отличающимися атрибутами компоновщик выдает предупреждение:

Код (Text):
  1.  
  2.  
  3.  LINK : warning LNK4078: multiple "INIT" sections found with different attributes (E2000020)
  4.  
  5.  

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


Добавляем ресурсы

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

Код (Text):
  1.  
  2.  
  3. VS_VERSION_INFO VERSIONINFO
  4.  FILEVERSION 1,0,0,0
  5.  PRODUCTVERSION 1,0,0,0
  6.  FILEFLAGSMASK 0x3fL
  7.  FILEFLAGS 0x0L
  8.  FILEOS 0x40004L
  9.  FILETYPE 0x1L
  10.  FILESUBTYPE 0x0L
  11. BEGIN
  12.     BLOCK "StringFileInfo"
  13.     BEGIN
  14.         BLOCK "040904E4"
  15.         BEGIN
  16.             VALUE "Comments", "Written by Four-F\0"
  17.             VALUE "CompanyName", "Four-F Software\0"
  18.             VALUE "FileDescription", "Kernel-Mode Driver VirtToPhys v1.00\0"
  19.             VALUE "FileVersion", "1, 0, 0, 0\0"
  20.             VALUE "InternalName", "VirtualToPhysical\0"
  21.             VALUE "LegalCopyright", "Copyright © 2003, Four-F\0"
  22.             VALUE "OriginalFilename", "VirtToPhys.sys\0"
  23.             VALUE "ProductName", "Kernel-Mode Driver Virtual To Physical Address Converter\0"
  24.             VALUE "ProductVersion", "1, 0, 0, 0\0"
  25.         END
  26.     END
  27.     BLOCK "VarFileInfo"
  28.     BEGIN
  29.         VALUE "Translation", 0x409, 1200
  30.     END
  31. END
  32.  
  33.  

Тут ничего принципиально нового. Стандартный ресурсный скрипт. Компилируется и линкуется как обычно. В свойствах файла драйвера можно посмотреть что получилось.


Еще немного об отладке

Вытащить всю (или почти всю) "подноготную" объектов "драйвер" и "устройство" можно с помощью команд driver <имя драйвера> и device <имя устройства> отладчика SoftICE. Вот как это выглядит на моей машине:

Рис. 5-2. Результат выполнения команды driver VirtToPhys

Рис. 5-3. Результат выполнения команды device devVirtToPhys

Как вы понимаете, информация отображаемая SoftICE, в основном, извлекается из структур DRIVER_OBJECT и DEVICE_OBJECT соответственно. Используя эту информацию можно легко найти эти объекты в памяти, поставить точки прерывания на процедуры диспетчеризации и выгрузки драйвера и т.п.


Заключение

Вот мы, наконец, и разобрали основы программирования драйверов режима ядра на языке ассемблера. Надеюсь, эти статьи помогли вам легко "въехать в тему". За дополнительной информацией обращайтесь, прежде всего, к DDK, книге Walter Oney " Programming the Microsoft Windows Driver Model" и журналу "The NT Insider" http://www.osr.com/.

В архиве к этой статье вы обнаружите:

\include\

- некоторые обновленные включаемые файлы;

\src\Article4-5\NtBuild

- код драйвера возвращающего номер сборки (build number) и выпуск системы. Демонстрирует использование функции ReadFile, вида управления буферами neither и использование SEH;

\src\Article4-5\Skeleton

- каркас драйвера режима ядра;

\src\Article4-5\VirtToPhys

- код описанного в данной статье драйвера.

© Four-F

0 2.189
archive

archive
New Member

Регистрация:
27 фев 2017
Публикаций:
532