Статья SetupAPI – информация об устройствах

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

Да.., в штатной поставке Windows имеется оснастка WMI Control – Windows Management Instrumentation (инструментарий управления Win), которая прекрасно решает проблемы данного характера. Однако вызывать сторонние сервисы и приложения из своих программ слишком накладно по времени, и наше приложение будет жутко тормозить. Личные опыты показали, что одну и ту-же задачу WMI решает аж в 60 раз медленнее, чем если этот-же функционал реализовать прямым вызовом системных функций WinAPI – аргумент явно не в пользу WMI, хотя данная оснастка и требует от нас меньших телодвижений.

Так-что оставим этот инструментарий для сис-админов (хотя не факт, что большая часть из них в полной мере знакомы с ним), а мы попробуем собрать инфу об устройствах с подручными средствами, для чего воспользуемся услугами специально предназначенным для этого набором функций под общим названием setupAPI. Этот набор живёт в одноимённой системной библиотеке setupapi.dll, которая выдаёт на экспорт без малого 600-функций. Жалко, что fasm о них ничего не знает и не имеет служебных структур для работы с ней, но это дело поправимое (см.скрепку в конце статьи). Ознакомиться с кол-вом функций и их именами можно в дизассемблере\отладчике W32Dasm, что демонстрирует скрин ниже:

W32Dasm.png


Основное назначение библиотеки setupapi.dll – это установка драйверов устройств и законченная их регистрация в системном реестре. Данная библиотека состоит из двух самостоятельных модулей:

Модуль "SETUP" – установка устройств и сбор информации о них. Подмножество имён этих функций начинается с приставки SetupDi_XXX. Для наших целей это как-раз то, что доктор прописал.​
Модуль "CM" – Configuration Manager, который занимается конфигурированием и настройкой уже установленных устройств. Отвечающие за эту часть работы функции, соответственно начинаются с приставки СМ_XXX.​

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

Знакомство с классами устройств, и их идентификаторами GUID

Современная архитектура компьютерных систем имеет огромное количество устройств, и только часть из них внешние. К внешним относятся жёсткий диск, клавиатура, мышь, модем, монитор и прочая ботва, с которой нас знакомили на уроках информатики. Однако внутри чипсета материнской платы зарыты внутренние устройства – это различного рода шины, таймеры, контролёры, мосты и много другое. Более того, в системе могут присутствуют одинаковые по типу, но разные по назначению устройства, а значит их нужно как-то фильтровать.

Таким образом, все устройства в системе (будь-то внешние или внутренние) разделяются на классы, например: класс шины PCI, класс USB, видео-класс, класс принтеров, модемов, клавиатуры и т.д. В системе, буквально любое устройство обязательно принадлежит к какому-нибудь классу. Каждый класс устройств идентифицируется своим GUID"ом (глобальный уникальный идентификатор), который представляет из-себя 16-байтную (128 битную) запись вида: {50127DC3-0F36-415E-A6CC-4CB3BE910B65}. Схематически это можно представить так:

iInfo_scheme.png


Информационной базой всех устройств является системный реестр, где для каждого класса-устройств выделена своя ветвь. Все классы собраны в разделе реестра HKLM\SYSTEM\CurrentControlSet\Control\Class. В этой папке мы найдём зарегистрированные когда-либо в системе GUID'ы и даже тех классов, привязанных устройств к которым в текущий момент может и не быть. Например подключили мы к компьютеру месяц назад камеру, а потом убрали её за ненадобность. Это нужно учитывать при сканировании устройств, выставляя флаг DIGCF_PRESENT=2 (только активные девайсы).

regedit.png


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

Забегая вперёд скажу, что для каждого из найденных устройств, функция SetupDiGetDeviceRegistryProperty() как пылесос может вытянуть до 37-ми его характеристик – именно такое количество SPDRP-флагов мы можем передавать ей в аргументе. Как-говорится – хоть лопатой греби..

В талмуде мелкомягких, глобальный идентификатор GUID описывается вполне легальной структурой. Например на рисунке выше, GUID характеризует класс USB-устройств и имеет значение: {36FC9E60-C465-11CF-8056444553540000}. Если копнуть доки, то можно найти значения GUID всех (само)настраивающихся устройств PnP – в представлении ассемблера каждая запись выглядит так, и я собрав их в инклуде setupapi.inc, прикрепил его в скрепке (в штатной поставке fasm'a их нет):

C-подобный:
GUID_DEVCLASS_USB  dd   0x36fc9e60
                   dw   0xc465
                   dw   0x11cf
                   db   0x80,0x56,0x44,0x45,0x53,0x54,0x00,0x00

То-есть в значении GUID, первым идёт двойное слово, дальше слово, ещё слово и в конце - массив байт. Поскольку GUID представляет из-себя определяющее класс-устройств уникальное число, то это число эстафетой передаётся от версии к версии Windows. Не знаю как на остальных системах (особенно х64), но на моих эксперементальных XP и Win-7 идентификаторы GUID совпадают, что позволяет писать кросплатформенные приложения на одном инклуде. Кстати Microsoft тоже утверждает, что GUID'ы классов будут соблюдаться ими в последующих версиях операционных систем, иначе зачем нужно было прописывать их в ?


Основные API-функции для сбора информации

Будем считать, что с теорией разобрались – перейдём к практической части..
Из указанных 600-функций библиотеки setupapi.dll мы будем использовать всего 6-7. Эти функции позволят нам выжать достаточно информации от первого свидания с этой либой. В качестве демонстрации напишем приложение, которое перечислит все идентификаторы GUID системы и отобразит в читабельном виде, какому именно классу принадлежит тот-или-иной GUID. Здесь в окопах нас поджидают некоторые нюансы – разберём их в кратце..

1. В цикле обходит все классы и возвращает нам их GUID'ы функция CM_Enumerate_Classes() с таким прототипом:

C-подобный:
CM_Enumerate_Classes
   ClassIndex   dd  0          ;// индекс класса в системной базе
   ClassGuid    db  16 dup(0)  ;// буфер, для приёма GUID-класса
   Flags        dd  0          ;// резерв..

Если коротко, то нам нужно организовать цикл и на каждой его итерации, начиная с нуля увеличивать индекс класса. Эта функция BOOL, так-что если она вернёт EAX=1, значит мы обошли всю базу и пора из цикла выходить. Второй аргумент – это указатель на 16 байтный буфер, в который функция будет сбрасывать GUID текущего индекса. Третий аргумент не используется и должен быть установлен в нуль.

2. Чтобы привести полученный GUID в читабельную строку (его мы получим в виде hex-значения), задействуем специально предназначенную для этого функцию из библиотеки ole32.dll StringFromGUID2(). Всё-что ей нужно, это указатель на 16-тиричный GUID для преобразования, и указатель на приёмный буфер, куда она сбросит результирующую строку. В случае успеха, fn. возвращает длину записанной в буфер строки. Если получим нуль, значит приёмный буф слишком мал и в аргумент cchMax вернётся трубуемая длинна. Вот её прототип:

C-подобный:
StringFromGUID2
   rguid        dd  0   ;// указатель на GUID
   lpsz         dd  0   ;// указатель на приёмный буфер для строки
   cchMax       dd  0   ;// вернётся длинна строки

Эта функция сбрасывает в буфер GUID в виде Unicode-строки, значит для вывода на консоль её нужно будет преобразовать в Ascii. Для этого будем просто читать по 2-байта, и перезаписывать в тот-же буфер по одному байту (т.е. отсекать парные нули). Вот как выглядит эта информация в секции-данных программы, после того-как функция StringFromGUID2() отработает:

guidBuff.png


3. На заключительном этапе, чтобы наша GUID-строка несла в себе хоть какую-то информацию, нужно будет по GUID получить строку с именем класса-устройства (см.зелёный блок на рисунке выше). В этом нам поможет функция из setupapi.dll с говорящим за себя названием SetupDiGetClassDescription(). Как и предыдущая, эта функция требует на входе указатель на 16-тиричный GUID, а из своей выхлопной трубы со-свистом выдаёт строковое представление данного класса-устройств (обе функции сами вставляют терминальный нуль в конце):

C-подобный:
SetupDiGetClassDescription(
   ClassGuid              dd  0  ;// указатель на hex-GUID класса
   ClassDescription       dd  0  ;// указатель на приёмный буфер для строки
   ClassDescriptionSize   dd  0  ;// размер передаваемого буфера
   RequiredSize           dd  0  ;// если ошибка, вернётся требуемый размер буфера
);

Из рисунка выше видно, что эта функция возвращает строку в кодировке cp-1251 (зелёный блок), т.е. кириллицей. Если мы планируем выводить информацию в виндовую консоль, то в своём пространстве консоль воспринимает только дос-кодировку OEM-866. Соответственно если не перекодировать этот выхлоп, то вместо текста, на экране получим инопланетные крякозябры. Для этого воспользуемся функцией из user32.dll под названием CharToOem(), которая изменит кодировку прямо не отходя от кассы, в том-же буфере.

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

Теперь, законченная реализация кода для вывода имени-класса по его GUID может выглядеть примерно так:

C-подобный:
format   pe console
include 'win32ax.inc'
entry    start
;//-------
.data
libName1    db  'setupapi.dll',0
libName2    db  'ole32.dll',0
fEnumClass  db  'CM_Enumerate_Classes',0
fClassDesc  db  'SetupDiGetClassDescriptionA',0
fStrGuid    db  'StringFromGUID2',0
bEnumClass  dd   0         ;// под EPoint fn.'CM_Enumerate_Classes()'
bClassDesc  dd   0         ;// под EPoint fn.'SetupDiGetClassDescription()'
bStrGuid    dd   0         ;// под EPoint fn.'StringFromGUID()'

capt    db  13,10,' GUID INFO v0.1'
        db  13,10,' ************************',0
ress    db  13,10,' %03d. %s  %s',0
mSec    db  13,10,' ****************************'
        db  13,10,' Log create time: 0.%d msec',0

index   dd  0              ;// индекс для fn.'CM_Enumerate_Classes()'
guid    db  016 dup(0)     ;// буфер под hex-GUID
guidStr db  128 dup(0)     ;// буфер под GUID Unicode-строку
gSize   dd  0              ;// переменная под размер строки
frmt    db  '%s',0         ;// спецификатор для scanf()
buff    db  0              ;// буфер общего назначения (до конца секции-данных)
;//-------
.code
start:
;// Динамически подгружаем 'setupapi.dll' в своё пространство
        invoke  LoadLibrary,libName1
        push    eax                             ;// запомнить её базу..
        invoke  GetProcAddress,eax,fEnumClass   ;// найти fn.CM_Enumerate_Classes()
        mov     [bEnumClass],eax                ;//   ..и запомнить её точку-входа

        pop     eax                             ;// базу на родину
        invoke  GetProcAddress,eax,fClassDesc   ;// найти fn.SetupDiGetClassDescription()
        mov     [bClassDesc],eax                ;//   ..точка-входа в неё

;// Динамически подгружаем 'ole32.dll'
        invoke  LoadLibrary,libName2            ;//
        invoke  GetProcAddress,eax,fStrGuid     ;// найти fn.StringFromGUID()
        mov     [bStrGuid],eax                  ;//   ..точка-входа в неё

;// Шапка и берём системные тики
       cinvoke  printf,capt           ;//
        invoke  GetTickCount          ;//
        push    eax                   ;// запомнить тики на входе!

;//==== Основная процедура сканирования классов в цикле =========
;// все функции вызываются по указателям (не по имени)
@scan:  push    0 guid [index]        ;// аргументы CM_Enumerate_Classes()
        call    [bEnumClass]          ;// получить GUID по текущему индексу!
        or      eax,eax               ;// это последний в списке?
        jnz     @exit                 ;// выйти, если EAX не нуль

        push    0 64 buff guid        ;// иначе: получить в буфер класс текущего GUID'a
        call    [bClassDesc]          ;// fn.SetupDiGetClassDescription()

        push    gSize guidStr guid    ;// сразу преобразовать GUID в строку
        call    [bStrGuid]            ;// fn.StringFromGUID()

        call    PrintDevice           ;// зовём fn.вывода инфы на экран!
        jmp     @scan                 ;// промотать, пока CM_Enum_Class() не вернёт ошибку
;//==== Конец основной процедуры =================================

@exit:  invoke  GetTickCount          ;// EAX = системные тики на выходе
        pop     ebx                   ;// EBX = тики на входе
        sub     eax,ebx               ;// вычислить разницу
       cinvoke  printf,mSec,eax       ;// вывести её на консоль
       cinvoke  scanf,frmt,buff       ;//
       cinvoke  exit,0                ;// конец программы!!!

;//*************************************************
;//**** Функция вывода информации на экран *********
PrintDevice:                          ;//
        mov      esi,guidStr          ;// ESI = указатель на строку GUID
        mov      edi,esi              ;// приёмник, он-же источник данных
@unicodeAsc:                          ;// цикл Unicode-to-Ascii
        lodsw                         ;// AX = слово из ESI
        stosb                         ;// записать его туда-же как байт
        or       ax,ax                ;// всю Unicode-строку прошли?
        jnz      @unicodeAsc          ;// повторить, если нет..

        invoke   CharToOem,buff,buff  ;// меням кодировку класса с 1251 на OEM-866
        inc      [index]              ;// увеличить индекс fn.CM_Enumerate_Classes()
       cinvoke   printf,ress,[index],guidStr,buff   ;// распечатать всё!!!
        ret                           ;// выход из функции во-внешний цикл..
;//-------
section '.idata' import data readable
library  msvcrt,'msvcrt.dll',kernel,'kernel32.dll',user,'user32.dll'
import   msvcrt, printf,'printf',scanf,'scanf',exit,'exit'
import   user,   CharToOem,'CharToOemA'
import   kernel, LoadLibrary,'LoadLibraryA',\
                 GetProcAddress,'GetProcAddress',GetTickCount,'GetTickCount'

guid_class.png


Что мы тут имеем? Значит всего классов-устройств в системе равно 60, и соответственно столько-же идентификаторов GUID. Пусть вас не смущает время выполнение данного кода 560 миллисекунд – это тестировалось на виртуальной машине VirtualBox, скорость которой можно сравнить с активностью уставшей улитки. К примеру на скрине ниже я запустил этот-же кодес на реальном процессоре под хр, так хрюша пробежала эту дистанцию всего за 32 мс – как говорится, почувствуйте разницу: 560/32=17.5 раз быстрее:

guid_class_xp.png



Перечисляем все, или принадлежащие только к одному классу устройства

Получив GUID'ы классов всех устройств проследуем дальше, и попробуем отфильтровать лог по конкретному классу-устройств, например собрать информацию только об устройствах USB или тех, что повесились на шине PCI и т.п. Такую задачу решает аккорд всего из трёх функций, и все они прописаны в библиотеке setupapi.dll. Обычно эти функции имеют приоритет друг перед другом, т.е. их нужно вызывать в определённой последовательности – рассмотрим её..

1. SetupDiGetClassDevs() – возвращает дескриптор инфо-базы по GUID-класса (см.рис.2), или нуль в случае ошибки. На входе принимает 4 аргумента, первые-три из которых опциональны и могут быть равны нулю (в этом случае фильтр отключается и сканируются все устройства). В природе, на все случаи жизни имеются три распространённых шаблона аргументов этой функции, которые закоментированы в исходнике ниже – это шаблон для всех устройств, фильтр по текстовой маске (возможные варианты: USB/PCI/PCMCIA/SCSI), и фильтрация устройств по классу GUID.

C-подобный:
SetupDiGetClassDevs
  ClassGuid    dd  0   ;// указатель на GUID-класса (опционально)
  Enumerator   dd  0   ;// указатель на текстовую маску (опционально)
  hwndParent   dd  0   ;// окно вернего уровня =0
  Flags        dd  0   ;// параметры поиска
;//****************************************
;// Значения аргумента 'Flags'
DIGCF_PRESENT          = 0x02    ;// реально имеющиеся устройства
DIGCF_ALLCLASSES       = 0x04    ;// все классы устройств
DIGCF_PROFILE          = 0x08    ;// выбирать по созданному профилю
DIGCF_DEVICEINTERFACE  = 0x10    ;// выбирать по интерфейсу

2. SetupDiEnumDeviceInfo() – заполняет структуру SP_DEVINFO_DATA, которая описывает конкретный элемент в классе-устройств. Больше эта функция ничего не делает. Позже, мы должны будем передать указатель на эту структуру третьей функции в этой тусовке, чтобы она черпала с неё информацию. Для перечисления всего списка-устройств указанного класса, нужно поместить данную функцию в цикл, каждый раз увеличивая значение индекса на 1. Эта функция BOOL и возвращает EAX=0 при ошибке (последний элемент в списке):

C-подобный:
SetupDiEnumDeviceInfo
  DeviceInfoSet     dd  0      ;// дескриптор инфо-базы
  MemberIndex       dd  0      ;// индекс инфо-элемента (начиная с нуля)
  DeviceInfoData    dd  0      ;// указатель на 'SP_DEVINFO_DATA'
;//****************************************
struct SP_DEVINFO_DATA
  cbSize       dd  28          ;// размер этой структуры
  ClassGuid    db  16 dup(0)   ;// место под GUID (128-бит)
  DevInst      dd  0           ;// дескриптор девнода
  Reserved     dd  0           ;// резерв..
ends

3. SetupDiGetDeviceRegistryProperty() – последняя, довольно творческая единица и делает всю черновую работу. Именно эта функция лезет в системный реестр, заполняя наш приёмный буфер нарытими данными. Для начала посмотрим на её аргументы, а потом разберёмся с деталями:

C-подобный:
SetupDiGetDeviceRegistryProperty(
  DeviceInfoSet            dd  0   ;// дескриптор инфо-базы
  DeviceInfoData           dd  0   ;// указатель на 'SP_DEVINFO_DATA'
  Property                 dd  0   ;// тип информации, которую мы хотим получить
  PropertyRegDataType      dd  0   ;// опционально (ставим нуль)
  PropertyBuffer           dd  0   ;// указатель на приёмный буфер,
  PropertyBufferSize       dd  0   ;//   ...размер приёмного буфа.
  RequiredSize             dd  0   ;// опционально (ставим нуль)

Из все этой братии аргументов, нам интересен лишь третий, под кличкой 'Property'. Он спрашивает у нас, информацию какого характера мы хотим получить? Вот где можно разгуляться с баяном в руках, подставляя в него одну из 37-ми констант (см.инклуд setupapi.inc в скрепке). Эта константа известна как SPDRPSetup Device Registry Property, или выбор свойства из куста реестра. К сожалению аргумент не позволяет инструкцией OR задавать сразу несколько констант, поэтому если мы хотим за один подход вытянуть несколько строк различной инфы, нужно вызывать эту функцию N-ное количество раз, подставляя в этот аргумент соответствующие значения.

C-подобный:
;//******************************************************
;// Коды характеристик выбранного класса устройств
;// применяются для SetupDiGetDeviceRegistryProperty()
;// и позволяют запрашивать 37 свойств (0x25)
;//******************************************************
SPDRP_DEVICEDESC                  = 0x00   ;// DeviceDesc (R/W)
SPDRP_HARDWAREID                  = 0x01   ;// ++ HardwareID (R/W)
SPDRP_COMPATIBLEIDS               = 0x02   ;// CompatibleIDs (R/W)
SPDRP_NTDEVICEPATHS               = 0x03   ;// NtDevicePaths (R)
SPDRP_SERVICE                     = 0x04   ;// Service (R/W)
SPDRP_CONFIGURATION               = 0x05   ;// Configuration (R)
SPDRP_CONFIGURATIONVECTOR         = 0x06   ;// ConfigurationVector (R)
SPDRP_CLASS                       = 0x07   ;// Class (R--tied to ClassGUID)
SPDRP_CLASSGUID                   = 0x08   ;// ClassGUID (R/W)
SPDRP_DRIVER                      = 0x09   ;// Driver (R/W)
SPDRP_CONFIGFLAGS                 = 0x0A   ;// ConfigFlags (R/W)
SPDRP_MFG                         = 0x0B   ;// Mfg (R/W)
SPDRP_FRIENDLYNAME                = 0x0C   ;// FriendlyName (R/W)
SPDRP_LOCATION_INFORMATION        = 0x0D   ;// ++ LocationInformation (R/W)
SPDRP_PHYSICAL_DEVICE_OBJECT_NAME = 0x0E   ;// PhysicalDeviceObjectName (R)
SPDRP_CAPABILITIES                = 0x0F   ;// Capabilities (R)
SPDRP_UI_NUMBER                   = 0x10   ;// UiNumber (R)
SPDRP_UPPERFILTERS                = 0x11   ;// UpperFilters (R/W)
SPDRP_LOWERFILTERS                = 0x12   ;// LowerFilters (R/W)
SPDRP_BUSTYPEGUID                 = 0x13   ;// BusTypeGUID (R)
SPDRP_LEGACYBUSTYPE               = 0x14   ;// LegacyBusType (R)
SPDRP_BUSNUMBER                   = 0x15   ;// BusNumber (R)
SPDRP_ENUMERATOR_NAME             = 0x16   ;// Enumerator Name (R)
SPDRP_SECURITY                    = 0x17   ;// Security (R/W, binary form)
SPDRP_SECURITY_SDS                = 0x18   ;// Security (W, SDS form)
SPDRP_DEVTYPE                     = 0x19   ;// Device Type (R/W)
SPDRP_EXCLUSIVE                   = 0x1A   ;// Device is exclusive-access (R/W)
SPDRP_CHARACTERISTICS             = 0x1B   ;// Device Characteristics (R/W)
SPDRP_ADDRESS                     = 0x1C   ;// Device Address (R)
SPDRP_UI_NUMBER_DESC_FORMAT       = 0x1D   ;// UiNumberDescFormat (R/W)
SPDRP_DEVICE_POWER_DATA           = 0x1E   ;// Device Power Data (R)
SPDRP_REMOVAL_POLICY              = 0x1F   ;// Removal Policy (R)
SPDRP_REMOVAL_POLICY_HW_DEFAULT   = 0x20   ;// Hardware Removal Policy (R)
SPDRP_REMOVAL_POLICY_OVERRIDE     = 0x21   ;// Removal Policy Override (RW)
SPDRP_INSTALL_STATE               = 0x22   ;// Device Install State (R)
SPDRP_LOCATION_PATHS              = 0x23   ;// Device Location Paths (R)
SPDRP_BASE_CONTAINERID            = 0x24   ;// Base ContainerID (R)

Здесь нужно учитывать, что если нам нужно имя устройства, то оно может хранится в одном из двух полей информационной базы – это Description и Name (зависит от типа устройства). Поэтому чтобы не попасть в просак, для надёжности нужно запрашивать имя сразу два раза – первый раз с аргументом SPDRP_DEVICEDESC, и если функция SetupDiGetDeviceRegistryProperty() вернёт ошибку (или пустую строку в буфере), то второй раз подставить SPDRP_FRIENDLYNAME. Не сбрасывайте это со-счетов..

Без практики, понять эту теоритическую муть довольно сложно, так-что соберём всё сказанное под один колпак и напишем небольшое приложение. Здесь я запрашиваю у системы информацию по GUID'у класса "Контролёры жёстких дисков". В инклуде эта переменная значится как GUID_DEVCLASS_HDC. Дальше, подставив в SPDRP-константу соответствующие значения, получаю: имя, адрес на шине PCI, и вендора обнаруженных устройств. Поиграйтесь с этой константой и получите информацию различного рода:

C-подобный:
format   pe console
entry    start
include 'win32ax.inc'
include 'setupAPI.inc'   ;// подключаем инклуд из скрепки
;//-------
.data
devData     SP_DEVINFO_DATA

libName       db  'setupapi.dll',0
GetClassDev   db  'SetupDiGetClassDevsA',0
EnumDevInfo   db  'SetupDiEnumDeviceInfo',0
GetDevProp    db  'SetupDiGetDeviceRegistryPropertyA',0
fEnumDevInfo  dd   0     ;// точки-входа в функции
fGetDevProp   dd   0     ;//  ..^^^^^^^^

capt    db  13,10,' DEVICE LIST v0.1'
        db  13,10,' ************************',0
ress    db  13,10,' %03d. %s'
        db  13,10,'      +-',16,' %s'
        db  13,10,'      +-',16,' %s',13,10,0
mSec    db  13,10,' ****************************'
        db  13,10,' Log create time: 0.%d msec',0
frmt    db  '%s',0
mask    db  'PCI',0      ;// маска PCI\USB\PCMCIA\SCSI
hndl    dd  0            ;// под хэндл инфо-базы
index   dd  0            ;// индекс для поиска в цикле
bus     db  256 dup(0)   ;// три буфера под строки..
ven     db  256 dup(0)   ;//  ..шина, вендор, устройство
buf     db  0            ;//    ..^^^^
;//-------
.code
start:  invoke  LoadLibrary,libName     ;// загрузить 'setupapi.dll'
        push    eax eax                 ;//
        invoke  GetProcAddress,eax,GetClassDev  ;// fn.DiGetClassDevs()

;//==== Шаблоны 4-х аргументов для 'SetupDiGetClassDevs()'
;//=======================================================
;// вариант(1) = обход буквально всех устройств
;//     push    DIGCF_PRESENT or DIGCF_ALLCLASSES
;//     push    0 0 0
;// вариант(2) = обход устройств по маске (USB\PCI\PCMCIA\SCSI)
;//     push    DIGCF_PRESENT or DIGCF_ALLCLASSES
;//     push    0 mask 0
;// вариант(3) = обход устройств по классу GUID
        push    DIGCF_PRESENT           ;// реально имеющиеся девайсы
        push    0 0                     ;//
        push    GUID_DEVCLASS_HDC       ;// GUID = Hard Disk Controller
        call    eax                     ;// fn.DiGetClassDevs()
        mov     [hndl],eax              ;// запомнить дескриптор инфо-базы HDC

        pop     eax                     ;//
        invoke  GetProcAddress,eax,EnumDevInfo
        mov     [fEnumDevInfo],eax      ;// адрес fn.DiEnumDeviceInfo()
        pop     eax                     ;//
        invoke  GetProcAddress,eax,GetDevProp
        mov     [fGetDevProp],eax       ;// адрес DiGetDeviceRegProperty()

       cinvoke  printf,capt             ;// шапка
        invoke  GetTickCount            ;// тики профайлеру,
        push    eax                     ;//  ..запомнить их!

;//**** НАЧАЛО ОСНОВНОЙ ПРОГРАММЫ ***********
@scan:  push    devData [index] [hndl]  ;// fn.DiEnumDeviceInfo()
        call    [fEnumDevInfo]          ;// заполнили struct.'SP_DEVINFO_DATA'
        or      eax,eax                 ;// последняя запись в базе?
        jz      @exit                   ;// да - на выход!
;//--> двойной тест на имя обнаруженного устройства
        push    0 256 buf 0 SPDRP_DEVICEDESC devData [hndl]
        call    [fGetDevProp]           ;// поле "Description"
        or      eax,eax                 ;// пропустить, если нет ошибки..
        jnz     @next                   ;//
        push    0 256 buf 0 SPDRP_FRIENDLYNAME devData [hndl]
        call    [fGetDevProp]           ;// иначе: тест поля "Name"
;//-------------
@next:  push    0 256 bus 0 SPDRP_LOCATION_INFORMATION devData [hndl]
        call    [fGetDevProp]           ;// запрашиваем локацию на PCI-шине
        push    0 256 ven 0 SPDRP_HARDWAREID devData [hndl]
        call    [fGetDevProp]           ;//  ...и следом сразу вендора.

        call    PrintDevice             ;// обработать инфу в приёмных буферах
        jmp     @scan                   ;// продолжить поиск в цикле..
;//**** КОНЕЦ ОСНОВНОЙ ПРОГРАММЫ ************

@exit:  invoke  GetTickCount            ;// профайлер
        pop     ebx                     ;//
        sub     eax,ebx                 ;// вычисляем разницу
       cinvoke  printf,mSec,eax         ;// вывод миллисекунд на консоль
       cinvoke  scanf,frmt,buf          ;//
       cinvoke  exit,0                  ;// выход из программы!!!

;//---- Процедура меняет кодировку в буфере из 1251 в OEM-866
PrintDevice:                            ;//
        invoke   CharToOem,buf,buf      ;// имя устройства
        invoke   CharToOem,bus,bus      ;// его адрес на PCI-шине
        invoke   CharToOem,ven,ven      ;// вендор DEV\VEN
        inc      [index]                ;// увеличить индекс поиска!!!
       cinvoke   printf,ress,[index],buf,bus,ven   ;// инфу на консоль
        ret                             ;// выход из процедуры..
;//-------
section '.idata' import data readable
library  msvcrt,'msvcrt.dll',kernel,'kernel32.dll',user,'user32.dll'
import   msvcrt, printf,'printf',scanf,'scanf',exit,'exit'
import   user,   CharToOem,'CharToOemA'
import   kernel, LoadLibrary,'LoadLibraryA',GetProcAddress,'GetProcAddress',\
                 GetTickCount,'GetTickCount'

hdc_info.png



Заключение

Здесь мы рассмотрели всего 1% из имеющихся 600 функций в библиотеки setupapi.dll. За бортом осталась довольно могучая SetupDiGetDeviceInterfaceDetail() и многие другие. Однако когда-нибудь нужно сделать первый шаг к покорению этой вершины, что открывает богатые возможности для программирования железа из пользовательского режима. Кстати MSDN хорошо раскрывает эту тему и содержит много полезных материалов – учите и вам обязательно зачтётся.

Под занавес статьи, хочу привести пример bat-файла, который поможет вам искать различные константы в огромном море сишных (и не только) инклуд. Он универсальный и ищет текст по указанной маске, рекурсивно обходя все папки и файлы на жёстком диске. Просто кидаете его в корневую папку и подставляете текст для поиска в аргумент команды FINDSTR между двумя прямыми слэшами. Пошурша некоторое время блинами диска, батник вернём вам директории и имена файлов, где имеется указанный текст – очень удобно (для отображения кириллицы, сохраните его в кодировке OEM-866):

Bash:
@echo off
echo.
echo  Поиск по маске "SP_DEVINFO_DATA" начался.
echo  Это займёт пару минут. Пожалуйста ждите...
echo.
echo  Слово найдено в следующих файлах:
echo  ---------------------------------
findstr /s /i /m "\<SP_DEVINFO_DATA\>" *.*
echo  ---------------------------------
echo  Поиск окончен!!!
pause

Всем удачи, до скорого!
 

Вложения

  • setupAPI.zip
    4,2 КБ · Просмотры: 729
Последнее редактирование:

rusrst

Green Team
04.10.2020
22
27
BIT
0
Добрый день. Большое Вам спасибо за очень толковую статью по SetupApi. Побольше бы таких толковых статей!
 
  • Нравится
Реакции: Marylin
Мы в соцсетях:

Обучение наступательной кибербезопасности в игровой форме. Начать игру!