Продолжим начатую тему и в этой части обсудим:
1. Стек драйверов
2. Функция DeviceIoControl()
3. Взаимодействие через ATA интерфейс
---------------------------------------------
Стек драйверов
Прикладная программа общается с физ.устройством через его драйвер, т.к. запросы должны преодолеть запретную черту из юзера в кернел. Как-правило, одно устройство (в нашем случае диск) на разном уровне обслуживает сразу несколько связанных в цепочку драйверов – в литературе, эту цепочку назвали "стеком-драйверов". В то-же время, пользователь не может вызывать функции драйвера напрямую, поэтому система подключает посредников, в лице диспетчера ввода-вывода I/O Manager (по требованию юзера формирует пакеты запроса на ввод-вывод IRP), и диспетчера объектов Object Manager (прописывает символические ссылки на каждый из драйверов в системном пространстве имён).
На рисунке ниже, скрин программы "WinObj" для каталога GLOBAL?? Как видим имя PhysicalDrive0, которое мы передаём функции CreateFile() при открытии физ.диска, в действительности является ссылкой на объект \Device\Harddisk0\DR0 в пространстве имён диспетчера-объектов. Отметим, что любое имя в папке GLOBAL?? может использоваться в аргументах CreateFile(), т.к. эта папка специально создана для юзера и содержит исключительно ссылки на реальные объекты. На моей тестовой машине установлено два HDD и один USB-flash, соответственно в каталоге \Device видны три устройства DR0-DR2:
Диспетчер Plug-and-Play в непрерывном режиме следит за состоянием системной шины PCI на предмет подключённых к ней контролёров физ.устройств. Как только таковой обнаруживается (причём это может быть горячее подключение типа USB/SATA/SAS), диспетчер PnP оповещает об этом драйвер шины
Кол-во драйверов в стеке не ограничено, хотя на практике чаще используют от двух до пяти уровней (слишком большое их кол-во может снизить скорость ввода-вывода). Аппаратный драйвер
Наш запрос оформляется в виде структуры IRP – I/O Request Packet, пакет запроса на в/в. Диспетчер ввода-вывода передаёт его всем драйверам в стеке, а обрабатывать запрос или нет, решает уже сам драйвер. Для этого, в хвосте структуры IRP имеются несколько вложенных структур
Кроме того, поле по смещению
Функция DeviceIoControl()
Будем считать, что с адресатом поверхностно разобрались – теперь об отправителе..
В составе системной библиотеки kernel32.dll есть интересная функция DeviceIoControl(). Прямо из пользовательского приложения она позволяет обращаться к драйверам буквально всех физических устройств в системе. Вот её прототип (при ошибке возвращает логический нуль):
Сложность использования этой функции в том, что ей можно передавать огромное количество IOCTL-кодов, от которых зависят остальные параметры и соответствующие им информационные структуры. Код выбирает одну из функций в драйвере устройства, и передаёт ей различные аргументы через третий свой параметр
Под управляющим сообщением подразумевается IOCTL, который позволяет обращаться к драйверу диска с запросами, отличающимися от стандартных операций чтения/записи в устройство типа ReadFile() и прочие. Помимо запрашиваемой операции, внутри IOCTL имеются биты с определением "прав пользователя" и метода передачи
В качестве примера, на схеме выше я привёл несколько кодов с их константами в hex и bin. Флаги и метод передачи буфера нам не интересны. Для пользователя с правами обычного юзера проблему представляют биты контроля [15:14], в результате чего DeviceIoControl() может возвратить ошибку "Не достаточно прав". Это ограничение не распространяется на админа, поскольку ему всегда открыт доступ на запись (см.левую табличку).
Старшая половина кода с битами [30:16] отведена под "тип устройства", к которому обращается функция. Выделив эту составляющую, диспетчер ввода-вывода делает вывод, какому именно девайсу адресован запрос. В файле Ntddk.h можно найти определения этих типов, а в демо-примерах ниже, клиентами наших запросов будут только типы устройств:
Взаимодействие через ATA интерфейс
1) Перечисление подключённых накопителей
Начнём с обычного обхода всех дисковых устройств..
Для этого можно воспользоваться переданным в DeviceIoControl() кодом IOCTL_STORAGE_QUERY_PROPERTY. Согласно
Здесь, наиболее важным является первый дворд
Если функция DeviceIoControl() отработает без ошибки (должна вернуть EAX=1), на выходе получим буфер с запрошенной информацией, который в красках описывает структура STORAGE_DEVICE_DESCRIPTOR и нам остаётся только выводить на консоль интересные на наш взгляд поля:
Из достоинств этого IOCTL-кода можно выделить только то, что он перечисляет буквально все накопители в системе, включая USB-брелки и накопители на лазерных дисках ATAPI. Однако на этом достоинства и заканчиваются. К примеру, программа ниже на моей машине с Win-7 возвращает какой-то левый серийный номер диска, не имеющий ничего общего с реальным s/n. Так-что доверять ему полностью нельзя, хотя на Win-10 серийник получаю валидный. Значит дело тут не в коде, а в драйверах, к которым идёт обращение с соответствующим запросом. Запушим этот факт в черепную коробку..
В своей демке, я в цикле открываю все диски по их именам в пространстве имён диспетчера-объектов, пока функция CreateFile() не вернёт ошибку. На каждой итерации цикла, достаточно увеличивать только номер диска типа
Все необходимые константы и структуры для этого кода можно найти в инклудах, которые я прикрепил в скрепке. Там-же имеется программа WinObj для знакомства с пространством имён и типами системных объектов (двойной клин на строке раскрывает свойства).
Как видно из примера выше, зарытая в структуру STORAGE_DEVICE_DESCRIPTOR информация очень скудная, зато на запрос отзываются все накопители системы. Более детальный отчёт возвращает следующий IOCTL-код, в котором мы обращаемся уже не к драйверу диска, а напрямую к его контролёру, посредством низкоуровневых АТА-команд.
2) Идентификация IDE устройств
Из общего пространства накопителя, производитель выделяет одну сторону одного из дисков под свои сервисные данные – эта область не доступна пользователю и поддерживает работу только технической составляющей. Среди данных можно выделить две "таблицы дефектных секторов" – Primary (основная, заполняется на этапе тестирования продукта после схода его с конвейера), и Grow – растущая, для скрытия бэд-секторов в процессе эксплуатации устройства. Кроме того имеются служебные секторы для хранения логов S.M.A.R.T., а так-же 512-байтный сектор с исчерпывающим паспортом данного диска.
В своей массе, производитель не регламентирует координаты сервисных секторов, в результате чего мы не можем читать их посредством адресации CHS (Cylinder-Head-Sector) или последовательной LBA (Logical-Block-Address). В таких случаях приходится обращаться с запросами к контролёру диска, отправив ему только АТА-команду, не указывая при этом адрес конкретного сектора. Ясно, что интересы юзверов здесь вообще не учитываются – это нужно в первую очередь системному BIOS, чтобы он по паспорту смог корректно идентифицировать принятое на борт устройство.
Большинство из IOCTL-кодов для передачи АТА-команд имеют кредит доверия в высшем обществе, требуя админских прав. Это следует воспринимать как часть политики Win. Обычно, программисты передают АТА-команды через сквозной интерфейс PASS_THROUGH, который реализуется очень громоздко из-за присущей ему пакетной передачи данных. Однако имеется и более простой вариант, в лице управляющего кода IOCTL_SMART_RCV_DRIVE_DATA – рассмотрим его подробней..
К этому IOCTL привязывается структура
Здесь, в столбце "Field" представлены регистры и как видим значимым является только последний CommandReg, а остальные ходят под флагом N/A (не используется). Контролёр их тупо игнорирует, поэтому можно пихать туда любые значения, но лучше придерживаясь этики, оставить по-нулям. АТА-команда IDENTIFY_DEVICE кодируется значением
В конкретно данном случае получается, что если мы хотим отправить контролёру АТА-команду IDENTIFY_DEVICE, то в эти две структуры должны записать всего-лишь один код команды
В спецификации ATA-Command-Set можно найти подробное описание каждого байта паспорта, а я собрал их все в структуру
Имеется ещё один, связанный с паспортом нюанс.. По непонятной причине, в текстовых строках производители меняют два соседних байта местами. Зачем это нужно, так и осталось для меня загадкой. Так-что до вывода строк на консоль нужно приводить их в человеческий вид – я озадачил этим макрос
Отметим, что это единственный способ получить заложенный вендором реальный серийник диска (именно диска, а не раздела или тома), поскольку мы берём его прямо из служебной зоны накопителя. Нужно учитывать, что производитель заполняет поля паспорта на своё усмотрение, хотя и придерживается общепринятых правил. Поэтому на низкопробных девайсах от наших друзей из страны восходящего солнца может получиться так, что некоторые поля будут пустыми, со-значением нуль. И ещё.. Только устройства ATA/SATA/ATAPI имеют сектор идентификации, и на USB-брелках его искать бесполезно.
3) S.M.A.R.T – состояние накопителей
Современные жёсткие диски HDD/SSD настолько развитый вид оборудования, что могут предсказывать свою смерть. Эта технология заложена в них на генном уровне и известна как SMART – Self Monitoring Analysis and Reporting Technology (самоконтроль, анализ и отчётность для оценки состояния жёсткого диска). В отличии от 512-байтного сектора с паспортом идентификации, в сервисной зоне накопителя данные SMART занимают целый цилиндр, а это 63 сектора или 32 Кб. Контроль за состоянием диска поддерживают огромное кол-во таблиц, куда специальная микро/программа прошивки сбрасывает 16 логов – вот их перечень:
На основе этих логов, по известному только производителю алгоритму вычисляется общее состояние диска. Нам, как рядовым пользователям детали реализации не интересны – нам подавай только готовый результат, для чего имеется основная (под)команда для работы со SMART – READ_DATA с соответствующим ей кодом
Чтобы прочитать SMART диска, в командный регистр
Здесь нужно уточнить некоторые детали.. Первое – это значение регистров
Для прикладных программистов, интерес представляют обычно первые две команды. Следуя логике разработчиков, SMART_READ_DATA =0xD0 должна возвращать готовые к употреблению данные, однако на практике самого главного из всех значений почему-то среди этих данных нет – это пороговое значение атрибута, относительно которого пляшут все остальные. Вот здесь-то и приходит на помощь вторая команда SMART_READ_THRESHOLDS =0xD1. Рассмотрим, какие бывают атрибуты, и по какому принципу рассчитывается здоровье диска.
Всего технология SMART поддерживает 30 различных атрибутов, а вот их идентификаторов аж 255. Дело в том, что у накопителей HDD свой диапазон ID, а у дисков SSD свой. Поэтому общее кол-во приближается к отметке 255. В
Значит в первом байте зарыт идентификатор (номер) атрибута, дальше идёт слово с флагами и если бит нуль в нём взведён, то показатель жизненно-важен. Потом следуют 2-байта – текущее значение атрибута и наихудшее. 4-байтное сырое поле RAW во многих случаях несёт в себе наиболее ценную информацию. В новом накопителе RAW практически всех атрибутов содержат нули, и при первом-же включении начнут меняться, ..например счётчики запусков, времени наработки и т.п..
Однако предсмертное состояние накопителя вычисляется на основании порогового значения атрибута Threshold. Пригодным к эксплуатации считается диск, у которого ни один из текущих атрибутов Current не ниже порогового значения! При его достижении, в ответ на команду SMART_RETURN_STATUS =0xDA контролёр будет возвращать статус BAD.
Это хорошо, только среди этих 12-ти байт значение Threshold всегда равно нулю (не возвращается командой READ_DATA), и его нужно запрашивать явно, второй отдельной командой SMART_READ_THRESHOLDS. В своём инклуде, я определил эти две таблицы в виде таких структур:
Наконец-то пешком (как Ломоносов) мы подобралась к практической составляющей SMART..
Значит как и в предыдущем примере с паспортом диска, будем терзать функцию DeviceIoControl() запросом IOCTL_SMART_RCV_DRIVE_DATA, только в качестве основной команды передадим SMART_EXECUTE_CMD =0xB0, прицепив к ней (под)команду
Заключение
На этой ноте пора уже поставить точку, хотя в масштабах общих возможностей IOCTL не было сказано почти ничего. За бортом осталась работа с разделами диска на уровне файловой системы с кодами FSCTL и многое другое. В процессе экспериментов главное помнить, что в этой войне пленных не берут и при малейших ошибках чтения/записи секторов, можно запросто потерять все данные на диске.
Помимо рассмотренных прикладных методов, с дисками можно общаться и по внутреннему интерфейсу i2c, перемычками или джампером переключив контролёр в сервисный режим. Эта скрытая кладезь позволяет вторгнуться в святая-святых накопителей – их прошивку. Правда при этом Win32-API остаётся уже не у дел, и нужно передавать команды по терминалу. Если освоить данную технику, то можно пустить это дело на коммерческие рельсы, воскрешая мёртвые диски из небытия и восстанавливая с них информацию.
В скрепке цепляю обещанный инклуд, с программами HD-Tune (можно сравнить SMART), и WinObj для просмотра системного пространства имён. До скорого!
1. Стек драйверов
2. Функция DeviceIoControl()
3. Взаимодействие через ATA интерфейс
• Перечисление накопителей
• Идентификация устройств
• S.M.A.R.T – состояние IDE диска
4.. Заключение---------------------------------------------
Стек драйверов
Прикладная программа общается с физ.устройством через его драйвер, т.к. запросы должны преодолеть запретную черту из юзера в кернел. Как-правило, одно устройство (в нашем случае диск) на разном уровне обслуживает сразу несколько связанных в цепочку драйверов – в литературе, эту цепочку назвали "стеком-драйверов". В то-же время, пользователь не может вызывать функции драйвера напрямую, поэтому система подключает посредников, в лице диспетчера ввода-вывода I/O Manager (по требованию юзера формирует пакеты запроса на ввод-вывод IRP), и диспетчера объектов Object Manager (прописывает символические ссылки на каждый из драйверов в системном пространстве имён).
На рисунке ниже, скрин программы "WinObj" для каталога GLOBAL?? Как видим имя PhysicalDrive0, которое мы передаём функции CreateFile() при открытии физ.диска, в действительности является ссылкой на объект \Device\Harddisk0\DR0 в пространстве имён диспетчера-объектов. Отметим, что любое имя в папке GLOBAL?? может использоваться в аргументах CreateFile(), т.к. эта папка специально создана для юзера и содержит исключительно ссылки на реальные объекты. На моей тестовой машине установлено два HDD и один USB-flash, соответственно в каталоге \Device видны три устройства DR0-DR2:
Диспетчер Plug-and-Play в непрерывном режиме следит за состоянием системной шины PCI на предмет подключённых к ней контролёров физ.устройств. Как только таковой обнаруживается (причём это может быть горячее подключение типа USB/SATA/SAS), диспетчер PnP оповещает об этом драйвер шины
pci.sys
. Дальше, управление получает драйвер IDE-шины pciide.sys
, который является первым драйвером в стеке – по сути он и выстраивает весь остальной стек. Здесь, этапы формирования пронумерованы как 0..4, а пользовательский запрос движется в обратном направлении 4-3-2:Кол-во драйверов в стеке не ограничено, хотя на практике чаще используют от двух до пяти уровней (слишком большое их кол-во может снизить скорость ввода-вывода). Аппаратный драйвер
pciidex.sys
ничего не знает о файловой системе и занимается только внутренними делами накопителя, оперируя на уровне байтов. Драйвер среднего уровня atapi.sys
уже более приближён к пользователю и организует блочный обмен данными. Дальше идут класс-драйверы cdRom
и disk.sys
, которые являются высоко-уровневыми, хотя и над ними имеется не показанный здесь драйвер файловой системы ntfs.sys
. Драйверы класса должны обрабатывать все пользовательские запросы, и только если его не существует (бывает и такое для некоторых устройств), приложение может направить запрос напрямую драйверу порта.Наш запрос оформляется в виде структуры IRP – I/O Request Packet, пакет запроса на в/в. Диспетчер ввода-вывода передаёт его всем драйверам в стеке, а обрабатывать запрос или нет, решает уже сам драйвер. Для этого, в хвосте структуры IRP имеются несколько вложенных структур
IO_STACK_LOCATION
(по одной для каждого драйвера в стеке), а кол-во таких структур в одном пакете указывается в поле "StackCount" по смещению 0х22
. Вот как представляет это ядерный отладчик WinDbg:Кроме того, поле по смещению
0х3С
хранит указатель на буфер, который передаёт драйверу функция DeviceIoControl() (см.параметр LpInBuffer). Перед тем-как отправить запрос, диспетчер ввода-вывода помещает в структуру IO_STACK_LOCATION старший и младший код-операции, Major и Minor соответственно. Каждый из драйверов в стеке анализирует эти поля и если в нём не реализована процедура обработки данного мажор-кода, то просто игнорирует запрос. Всего системой определено 28 мажорных кодов, список которых представлен ниже. Названия кодов говорят сами за себя, а приоритетным для нас будет IRP_MJ_DEVICE_CONTROL (управление устройством) со-значением 0х0Е
:
Код:
// Define the major function codes for IRPs. //
IRP_MJ_CREATE 0x00
IRP_MJ_CREATE_NAMED_PIPE 0x01
IRP_MJ_CLOSE 0x02
IRP_MJ_READ 0x03
IRP_MJ_WRITE 0x04
IRP_MJ_QUERY_INFORMATION 0x05
IRP_MJ_SET_INFORMATION 0x06
IRP_MJ_QUERY_EA 0x07
IRP_MJ_SET_EA 0x08
IRP_MJ_FLUSH_BUFFERS 0x09
IRP_MJ_QUERY_VOLUME_INFORMATION 0x0a
IRP_MJ_SET_VOLUME_INFORMATION 0x0b
IRP_MJ_DIRECTORY_CONTROL 0x0c
IRP_MJ_FILE_SYSTEM_CONTROL 0x0d
IRP_MJ_DEVICE_CONTROL 0x0e ;// <--- наш клиент!
IRP_MJ_INTERNAL_DEVICE_CONTROL 0x0f
IRP_MJ_SHUTDOWN 0x10
IRP_MJ_LOCK_CONTROL 0x11
IRP_MJ_CLEANUP 0x12
IRP_MJ_CREATE_MAILSLOT 0x13
IRP_MJ_QUERY_SECURITY 0x14
IRP_MJ_SET_SECURITY 0x15
IRP_MJ_POWER 0x16
IRP_MJ_SYSTEM_CONTROL 0x17
IRP_MJ_DEVICE_CHANGE 0x18
IRP_MJ_QUERY_QUOTA 0x19
IRP_MJ_SET_QUOTA 0x1a
IRP_MJ_PNP 0x1b
Функция DeviceIoControl()
Будем считать, что с адресатом поверхностно разобрались – теперь об отправителе..
В составе системной библиотеки kernel32.dll есть интересная функция DeviceIoControl(). Прямо из пользовательского приложения она позволяет обращаться к драйверам буквально всех физических устройств в системе. Вот её прототип (при ошибке возвращает логический нуль):
C-подобный:
BOOL DeviceIoControl (
HANDLE hDevice ;// имя или дескриптор устройства
DWORD dwIoControlCode ;// IOCTL-код операции для выполнения
LPVOID lpInBuffer ;// указатель на буфер, для передачи доп.данных драйверу
DWORD nInBufferSize ;// ..размер этого буфера
LPVOID lpOutBuffer ;// указатель на буфер, для приёма ответа от драйвера
DWORD nOutBufferSize ;// ..размер этого буфера
LPDWORD lpBytesReturned ;// указатель на переменную, куда вернётся кол-во принятых байт
LPOVERLAPPED lpOverlapped ;// структура для асинхронной операции (Null)
);
Сложность использования этой функции в том, что ей можно передавать огромное количество IOCTL-кодов, от которых зависят остальные параметры и соответствующие им информационные структуры. Код выбирает одну из функций в драйвере устройства, и передаёт ей различные аргументы через третий свой параметр
lpInBuffer
. Соответственно содержимое передаваемого буфера не может быть статичным, и его определяет код-операции. С достаточно объёмным списком поддерживаемых семёркой IOCTL-кодов и их констант можно
Ссылка скрыта от гостей
.Под управляющим сообщением подразумевается IOCTL, который позволяет обращаться к драйверу диска с запросами, отличающимися от стандартных операций чтения/записи в устройство типа ReadFile() и прочие. Помимо запрашиваемой операции, внутри IOCTL имеются биты с определением "прав пользователя" и метода передачи
lpInBuffer
с дополнительной информацией:В качестве примера, на схеме выше я привёл несколько кодов с их константами в hex и bin. Флаги и метод передачи буфера нам не интересны. Для пользователя с правами обычного юзера проблему представляют биты контроля [15:14], в результате чего DeviceIoControl() может возвратить ошибку "Не достаточно прав". Это ограничение не распространяется на админа, поскольку ему всегда открыт доступ на запись (см.левую табличку).
Старшая половина кода с битами [30:16] отведена под "тип устройства", к которому обращается функция. Выделив эту составляющую, диспетчер ввода-вывода делает вывод, какому именно девайсу адресован запрос. В файле Ntddk.h можно найти определения этих типов, а в демо-примерах ниже, клиентами наших запросов будут только типы устройств:
0x04
=Controller, 0x07
=Disk и 0x2d
=MassStorage (устройства хранения данных):
C-подобный:
// Define the various device type values. Note that values used by Microsoft
// Corporation are in the range 0-32767, and 32768-65535 are reserved for use
// by customers.
//
#define FILE_DEVICE_BEEP 0x00000001
#define FILE_DEVICE_CD_ROM 0x00000002
#define FILE_DEVICE_CD_ROM_FILE_SYSTEM 0x00000003
#define FILE_DEVICE_CONTROLLER 0x00000004
#define FILE_DEVICE_DATALINK 0x00000005
#define FILE_DEVICE_DFS 0x00000006
#define FILE_DEVICE_DISK 0x00000007
#define FILE_DEVICE_DISK_FILE_SYSTEM 0x00000008
#define FILE_DEVICE_FILE_SYSTEM 0x00000009
#define FILE_DEVICE_INPORT_PORT 0x0000000a
#define FILE_DEVICE_KEYBOARD 0x0000000b
#define FILE_DEVICE_MAILSLOT 0x0000000c
#define FILE_DEVICE_MIDI_IN 0x0000000d
#define FILE_DEVICE_MIDI_OUT 0x0000000e
#define FILE_DEVICE_MOUSE 0x0000000f
#define FILE_DEVICE_MULTI_UNC_PROVIDER 0x00000010
#define FILE_DEVICE_NAMED_PIPE 0x00000011
#define FILE_DEVICE_NETWORK 0x00000012
#define FILE_DEVICE_NETWORK_BROWSER 0x00000013
#define FILE_DEVICE_NETWORK_FILE_SYSTEM 0x00000014
#define FILE_DEVICE_NULL 0x00000015
#define FILE_DEVICE_PARALLEL_PORT 0x00000016
#define FILE_DEVICE_PHYSICAL_NETCARD 0x00000017
#define FILE_DEVICE_PRINTER 0x00000018
#define FILE_DEVICE_SCANNER 0x00000019
#define FILE_DEVICE_SERIAL_MOUSE_PORT 0x0000001a
#define FILE_DEVICE_SERIAL_PORT 0x0000001b
#define FILE_DEVICE_SCREEN 0x0000001c
#define FILE_DEVICE_SOUND 0x0000001d
#define FILE_DEVICE_STREAMS 0x0000001e
#define FILE_DEVICE_TAPE 0x0000001f
#define FILE_DEVICE_TAPE_FILE_SYSTEM 0x00000020
#define FILE_DEVICE_TRANSPORT 0x00000021
#define FILE_DEVICE_UNKNOWN 0x00000022
#define FILE_DEVICE_VIDEO 0x00000023
#define FILE_DEVICE_VIRTUAL_DISK 0x00000024
#define FILE_DEVICE_WAVE_IN 0x00000025
#define FILE_DEVICE_WAVE_OUT 0x00000026
#define FILE_DEVICE_8042_PORT 0x00000027
#define FILE_DEVICE_NETWORK_REDIRECTOR 0x00000028
#define FILE_DEVICE_BATTERY 0x00000029
#define FILE_DEVICE_BUS_EXTENDER 0x0000002a
#define FILE_DEVICE_MODEM 0x0000002b
#define FILE_DEVICE_VDM 0x0000002c
#define FILE_DEVICE_MASS_STORAGE 0x0000002d
#define FILE_DEVICE_SMB 0x0000002e
#define FILE_DEVICE_KS 0x0000002f
#define FILE_DEVICE_CHANGER 0x00000030
#define FILE_DEVICE_SMARTCARD 0x00000031
#define FILE_DEVICE_ACPI 0x00000032
#define FILE_DEVICE_DVD 0x00000033
#define FILE_DEVICE_FULLSCREEN_VIDEO 0x00000034
#define FILE_DEVICE_DFS_FILE_SYSTEM 0x00000035
#define FILE_DEVICE_DFS_VOLUME 0x00000036
#define FILE_DEVICE_SERENUM 0x00000037
#define FILE_DEVICE_TERMSRV 0x00000038
#define FILE_DEVICE_KSEC 0x00000039
#define FILE_DEVICE_FIPS 0x0000003A
#define FILE_DEVICE_INFINIBAND 0x0000003B
#define FILE_DEVICE_VMBUS 0x0000003E
#define FILE_DEVICE_CRYPT_PROVIDER 0x0000003F
#define FILE_DEVICE_WPD 0x00000040
#define FILE_DEVICE_BLUETOOTH 0x00000041
#define FILE_DEVICE_MT_COMPOSITE 0x00000042
#define FILE_DEVICE_MT_TRANSPORT 0x00000043
#define FILE_DEVICE_BIOMETRIC 0x00000044
#define FILE_DEVICE_PMI 0x00000045
#define FILE_DEVICE_EHSTOR 0x00000046
#define FILE_DEVICE_DEVAPI 0x00000047
#define FILE_DEVICE_GPIO 0x00000048
#define FILE_DEVICE_USBEX 0x00000049
#define FILE_DEVICE_CONSOLE 0x00000050
#define FILE_DEVICE_NFP 0x00000051
#define FILE_DEVICE_SYSENV 0x00000052
#define FILE_DEVICE_VIRTUAL_BLOCK 0x00000053
#define FILE_DEVICE_POINT_OF_SERVICE 0x00000054
#define FILE_DEVICE_STORAGE_REPLICATION 0x00000055
#define FILE_DEVICE_TRUST_ENV 0x00000056
#define FILE_DEVICE_UCM 0x00000057
#define FILE_DEVICE_UCMTCPCI 0x00000058
Взаимодействие через ATA интерфейс
1) Перечисление подключённых накопителей
Начнём с обычного обхода всех дисковых устройств..
Для этого можно воспользоваться переданным в DeviceIoControl() кодом IOCTL_STORAGE_QUERY_PROPERTY. Согласно
Ссылка скрыта от гостей
мелкомягких, к этому коду мы должны приложить буфер с дополнительной информацией STORAGE_PROPERTY_QUERY, структура которого выглядит так:
C-подобный:
struct STORAGE_PROPERTY_QUERY
PropertyId dd 0 ;// идентификатор запроса (смотри "STORAGE_PROPERTY_ID" ниже)
QueryType dd 0 ;// нуль = стандартный запрос
AdditionalParameters rd 30 ;// доп.параметры (нам не нужны)
ends
Здесь, наиболее важным является первый дворд
PropertyId
– это расширение основного IOCTL-кода, и оно может принимать одно из 33-х значений в диапазоне 0..32. В своём примере я использовал нулевой уточняющий код, что означает StorageDeviceProperty
(свойства девайса хранения данных). В списке ниже перечислены другие возможные варианты, которые можете самостоятельно испробовать на досуге:
Код:
;/////////////
;// Константы "STORAGE_PROPERTY_ID" =========================
;// Перечисляет возможные значения члена PropertyId структуры "STORAGE_PROPERTY_QUERY",
;// переданного в качестве входных данных в запрос "IOCTL_STORAGE_QUERY_PROPERTY"
;// для получения свойств устройства хранения или адаптера.
;//==========================================================
StorageDeviceProperty = 0
StorageAdapterProperty = 1
StorageDeviceIdProperty = 2
StorageDeviceUniqueIdProperty = 3
StorageDeviceWriteCacheProperty = 4
StorageMiniportProperty = 5
StorageAccessAlignmentProperty = 6
StorageDeviceSeekPenaltyProperty = 7
StorageDeviceTrimProperty = 8
StorageDeviceWriteAggregationProperty = 9
StorageDeviceDeviceTelemetryProperty = 10
StorageDeviceLBProvisioningProperty = 11
StorageDevicePowerProperty = 12
StorageDeviceCopyOffloadProperty = 13
StorageDeviceResiliencyProperty = 14
StorageDeviceMediumProductType = 15
StorageAdapterRpmbProperty = 16
StorageAdapterCryptoProperty = 17
StorageDeviceIoCapabilityProperty = 18
StorageAdapterProtocolSpecificProperty = 19
StorageDeviceProtocolSpecificProperty = 20
StorageAdapterTemperatureProperty = 21
StorageDeviceTemperatureProperty = 22
StorageAdapterPhysicalTopologyProperty = 23
StorageDevicePhysicalTopologyProperty = 24
StorageDeviceAttributesProperty = 25
StorageDeviceManagementStatus = 26
StorageAdapterSerialNumberProperty = 27
StorageDeviceLocationProperty = 28
StorageDeviceNumaProperty = 29
StorageDeviceZonedDeviceProperty = 30
StorageDeviceUnsafeShutdownCount = 31
StorageDeviceEnduranceProperty = 32
Если функция DeviceIoControl() отработает без ошибки (должна вернуть EAX=1), на выходе получим буфер с запрошенной информацией, который в красках описывает структура STORAGE_DEVICE_DESCRIPTOR и нам остаётся только выводить на консоль интересные на наш взгляд поля:
C-подобный:
struct STORAGE_DEVICE_DESCRIPTOR
Version dd 0 ;// размер этой структуры
Size dd 0 ;// общий размер возвращённых данных
DeviceType db 0 ;// тип устройства
DeviceTypeModifier db 0 ;// модификатор
RemovableMedia db 0 ;// 1 = съёмный носитель
CommandQueueing db 0 ;// 1 = поддерживает NCQ (очередь команд SATA-2, SCSI)
VendorIdOffset dd 0 ;// смещение к строке с именем поставщика
ProductIdOffset dd 0 ;// смещение к имени устройства
ProductRevisionOffset dd 0 ;// смещение к ревизии
SerialNumberOffset dd 0 ;// смещение к серийнику лиска
BusType dd 0 ;// тип шины
RawPropertiesLength dd 0 ;// кол-во специфичных для вендора байт
RawDeviceProperties rb 512 ;// специфичные данные
ends
Из достоинств этого IOCTL-кода можно выделить только то, что он перечисляет буквально все накопители в системе, включая USB-брелки и накопители на лазерных дисках ATAPI. Однако на этом достоинства и заканчиваются. К примеру, программа ниже на моей машине с Win-7 возвращает какой-то левый серийный номер диска, не имеющий ничего общего с реальным s/n. Так-что доверять ему полностью нельзя, хотя на Win-10 серийник получаю валидный. Значит дело тут не в коде, а в драйверах, к которым идёт обращение с соответствующим запросом. Запушим этот факт в черепную коробку..
В своей демке, я в цикле открываю все диски по их именам в пространстве имён диспетчера-объектов, пока функция CreateFile() не вернёт ошибку. На каждой итерации цикла, достаточно увеличивать только номер диска типа
PhysicalDrive0
, дальше 1 и т.д. После того-как с дисками будет покончено, переходим к сидюкам, которые перечисляются аналогичным образом CdRom0
, CdRom1
и т.д. Вот пример реализации задуманного..
C-подобный:
format pe console
include 'win32ax.inc'
include 'equates\storage.inc'
entry start
;//---------
.data
title db 'STORAGE_QUERY - ver.0.1',0
dNameHdd db '\\.\'
devHdd db 'PhysicalDrive0',0 ;// имена дисков в пространстве имён
dNameCd db '\\.\'
devCd db 'CdRom0',0 ;// имена DVD-ROM'ов
inBuff STORAGE_PROPERTY_QUERY ;// структура, которую передаём драйверу вместе с IOCTL-кодом
outBuff STORAGE_DEVICE_DESCRIPTOR ;// структура, которую принимаем от драйвера
hndl dd 0 ;// под дескриптор устройства
retSize dd 0 ;// будет кол-во возвращённых байт
reserve db 0 ;// в хозяйстве пригодится..
;//---------
.code
start: invoke SetConsoleTitle,title
;//==== Заполняем передаваемый буфер ====================
mov [inBuff.PropertyId],StorageDeviceProperty
mov [inBuff.QueryType],PropertyStandardQuery
mov [inBuff.AdditionalParameters],0
;//==== Открываем устройство по имени (сначала диски) ============
@hdd: invoke CreateFile,dNameHdd,GENERIC_READ,FILE_SHARE_READ,\
0,OPEN_EXISTING,0,0
mov [hndl],eax
cmp eax,-1
je @cd ;// если ошибка..
cinvoke printf,<10,10,'Dev: %s',0>,devHdd ;//
call GetDriveInfo ;// зовём процедуру с DeviceIoControl()
inc byte[devHdd+13] ;// имя устройства +1
jmp @hdd ;// повторить, пока CreateFile() не вернёт ошибку..
;//==== Открываем CD-ROM'ы по имени =============================
@cd: invoke CreateFile,dNameCd,GENERIC_READ,FILE_SHARE_READ,\
0,OPEN_EXISTING,0,0
mov [hndl],eax
cmp eax,-1
je @exit
cinvoke printf,<10,10,'Dev: %s',0>,devCd
call GetDriveInfo
inc byte[devCd+6]
jmp @cd
@exit: cinvoke gets,reserve ;// ждём клаву..
cinvoke exit,0 ;// на выход!!!
;//---------
;//==== Функция DeviceIoControl() =====================================
;//==== передаём доп.информацию в буфере "inBuff", получаем в "outBuff"
proc GetDriveInfo
invoke DeviceIoControl,[hndl],IOCTL_STORAGE_QUERY_PROPERTY,\
inBuff,3*4,outBuff,1024,retSize,0
cinvoke printf,<10,' Vendor....: ',0>
mov eax,[outBuff.VendorIdOffset] ;// указатель на строку вендора
or eax,eax ;// проверить на нуль
je @f ;// пропустить, если нет указателя..
add eax,outBuff ;// иначе: добавить смещение от начала буфера
cinvoke printf,<'%s',0>,eax ;// выводим на консоль!
@@: cinvoke printf,<10,' Product...: ',0>
mov eax,[outBuff.ProductIdOffset]
or eax,eax
je @f
add eax,outBuff
cinvoke printf,<'%s',0>,eax
@@: cinvoke printf,<10,' Revision..: ',0>
mov eax,[outBuff.ProductRevisionOffset]
or eax,eax
je @f
add eax,outBuff
cinvoke printf,<'%s',0>,eax
@@: cinvoke printf,<10,' Serial....: ',0>
mov eax,[outBuff.SerialNumberOffset]
or eax,eax
je @f
add eax,outBuff
cinvoke printf,<'%s',0>,eax
@@: mov eax,[outBuff.BusType] ;// EAX = тип шины
shl eax,2 ;// умножить на 4, чтобы получить смещение в таблице
mov esi,typeTbl ;// ESI = таблица возможных вариантов
add esi,eax ;// ESI = указатель на мессагу с типом шины
mov eax,[esi] ;// EAX = адрес сообщения.
cinvoke printf,<10,' BusType...: %s',0>,eax
ret
endp
;//**********************************
section '.idata' import data readable
library msvcrt,'msvcrt.dll',kernel32,'kernel32.dll'
import msvcrt, printf,'printf',gets,'gets',exit,'exit'
include 'api\kernel32.inc'
;//**********************************
;//===== Таблица с типом шин ===========================================
section '.rdata' data readable
typeTbl dd unk,scsi,atapi,ata,b1394,ssa,fibre,usb,raid,iscsi,sas,sata
unk db 'Unknown',0
scsi db 'SCSI',0
atapi db 'ATAPI',0
ata db 'ATA',0
b1394 db '1394',0
ssa db 'SSA',0
fibre db 'FibreChanel',0
usb db 'USB',0
raid db 'RAID',0
iscsi db 'iSCSI',0
sas db 'SAS/NVM',0
sata db 'SATA',0
Все необходимые константы и структуры для этого кода можно найти в инклудах, которые я прикрепил в скрепке. Там-же имеется программа WinObj для знакомства с пространством имён и типами системных объектов (двойной клин на строке раскрывает свойства).
Как видно из примера выше, зарытая в структуру STORAGE_DEVICE_DESCRIPTOR информация очень скудная, зато на запрос отзываются все накопители системы. Более детальный отчёт возвращает следующий IOCTL-код, в котором мы обращаемся уже не к драйверу диска, а напрямую к его контролёру, посредством низкоуровневых АТА-команд.
2) Идентификация IDE устройств
Из общего пространства накопителя, производитель выделяет одну сторону одного из дисков под свои сервисные данные – эта область не доступна пользователю и поддерживает работу только технической составляющей. Среди данных можно выделить две "таблицы дефектных секторов" – Primary (основная, заполняется на этапе тестирования продукта после схода его с конвейера), и Grow – растущая, для скрытия бэд-секторов в процессе эксплуатации устройства. Кроме того имеются служебные секторы для хранения логов S.M.A.R.T., а так-же 512-байтный сектор с исчерпывающим паспортом данного диска.
В своей массе, производитель не регламентирует координаты сервисных секторов, в результате чего мы не можем читать их посредством адресации CHS (Cylinder-Head-Sector) или последовательной LBA (Logical-Block-Address). В таких случаях приходится обращаться с запросами к контролёру диска, отправив ему только АТА-команду, не указывая при этом адрес конкретного сектора. Ясно, что интересы юзверов здесь вообще не учитываются – это нужно в первую очередь системному BIOS, чтобы он по паспорту смог корректно идентифицировать принятое на борт устройство.
Большинство из IOCTL-кодов для передачи АТА-команд имеют кредит доверия в высшем обществе, требуя админских прав. Это следует воспринимать как часть политики Win. Обычно, программисты передают АТА-команды через сквозной интерфейс PASS_THROUGH, который реализуется очень громоздко из-за присущей ему пакетной передачи данных. Однако имеется и более простой вариант, в лице управляющего кода IOCTL_SMART_RCV_DRIVE_DATA – рассмотрим его подробней..
К этому IOCTL привязывается структура
SEND-CMD-IN-PARAM
, а она (в свою очередь) имеет вложенную структуру IDE-REGS
. По названию последней не трудно догадаться, что это бокс для физических регистров контролёра IDE. Соответственно чтобы передать АТА-команду, нам нужно всего-то указать её код в командном регистре 0x1F7
контролёра (см.предыдущую часть статьи), о чём свидетельствует таблица из спецификации
Ссылка скрыта от гостей
(стр.139):Здесь, в столбце "Field" представлены регистры и как видим значимым является только последний CommandReg, а остальные ходят под флагом N/A (не используется). Контролёр их тупо игнорирует, поэтому можно пихать туда любые значения, но лучше придерживаясь этики, оставить по-нулям. АТА-команда IDENTIFY_DEVICE кодируется значением
0хЕС
, которое определёно константой внутри инклуда в скрепке:
C-подобный:
;//
;// входные параметры для SMART_RCV_DRIVE_DATA, SMART_SEND_DRIVE_COMMAND
;//
struct SENDCMDINPARAMS
cBufferSize dd sizeof.SENDCMDINPARAMS ;// размер этой структуры
irDriveRegs IDEREGS ;// вложенная структура с регистрами контролёра
bDriveNumber db 0 ;// номер диска нам не нужен, т.к. мы открываем его по имени
bReserved rb 19 ;// резерв..
ends
struct IDEREGS
FeaturesReg db 0 ;// регистр 0x1F1 – аргумент команды
SectorCountReg db 0 ;// регистр 0x1F2 – счётчик секторов IDE
LBA_Low db 0 ;// мл.байт сектора LBA
LBA_Middle db 0 ;// средний байт ^^^
LBA_High db 0 ;// старший байт ^^^
DriveHeadReg db 0 ;// регистр 0x1F6 – номер головки/устройства
CommandReg db 0 ;// <<<=== АТА-команда ===== регистр 0x1F7
Reserved db 0 ;// (байт выравнивания на 8)
ends
В конкретно данном случае получается, что если мы хотим отправить контролёру АТА-команду IDENTIFY_DEVICE, то в эти две структуры должны записать всего-лишь один код команды
=0хЕС
, и посредством запроса SMART_RCV_DRIVE_DATA передать его функции DeviceIoControl(). Если она отработает удачно, в приёмном буфере получим сначала 16-байтную структуру SENDCMDOUTPARAMS
(с состоянием драйвера), и следом за ней сразу 512-байтный сектор данных – это и будет полезная нагрузка в виде паспорта диска.В спецификации ATA-Command-Set можно найти подробное описание каждого байта паспорта, а я собрал их все в структуру
IDENTIFY_SECTOR
. Чтобы не утонуть в полезном коде, в демонстрационном примере вывожу на консоль только самые важные на мой взгляд данные. Натощак покурив ACS, вы можете добавить свои..Имеется ещё один, связанный с паспортом нюанс.. По непонятной причине, в текстовых строках производители меняют два соседних байта местами. Зачем это нужно, так и осталось для меня загадкой. Так-что до вывода строк на консоль нужно приводить их в человеческий вид – я озадачил этим макрос
SwapByte
, которому передаю три аргумента: куда сбрасывать готовую строку, от куда её брать, и длину в байтах. Вот пример:
C-подобный:
format pe console
include 'win32ax.inc'
include 'equates\storage.inc' ;// см.инклуд в скрепке
entry start
;//---------
.data
macro SwapByte [Buff,Offset,Len] ;// макрос меняет байты строки местами
{ mov esi,Offset
mov edi,Buff
mov ecx,Len
shr ecx,1
@@: lodsw
xchg ah,al
stosw
loop @b
mov byte[edi],0
}
title db 'IOCTL_SMART_RCV_DRIVE_DATA --> IDENTIFY_DEVICE',0
dName db '\\.\PhysicalDrive0',0
drive dd -1
ata db 'ATA',0
sata db 'SATA',0
retSize dd 0
fpuBuff dd 0,0
inBuff SENDCMDINPARAMS ;// структура для передачи драйверу
outBuff SENDCMDOUTPARAMS ;// заголовок приёмного буфера
idSec IDENTIFY_SECTOR ;// структура с описанием полей паспорта
prnBuff db 0 ;// до конца секции-данных, под приведённые в порядок строки
;//---------
.code
start: invoke SetConsoleTitle,title
;//==== Открываем очередной диск ===============================
@@: invoke CreateFile,dName,GENERIC_READ or GENERIC_WRITE,\ ;// для АТА-команд нужны права на R/W
FILE_SHARE_READ,0,OPEN_EXISTING,0,0
cmp eax,-1
jz @exit ;// ошибка!
call ReadIdentifySector ;// иначе: зовём DeviceIoControl()
inc byte[dName+17] ;// меняем номер диска на следующий
jmp @b ;// повторить до ошибки..
@exit: cinvoke gets,prnBuff ;//
cinvoke exit,0 ;// на выход!
;//---------
proc ReadIdentifySector
;//==== Заполняем регистры контролёра диска ==================
mov [inBuff.irDriveRegs.FeaturesReg],0 ;// нет под-команды
mov [inBuff.irDriveRegs.CommandReg],IDENTIFY_DEVICE ;// команда 0хЕС
;//==== Передаём буфер и зовём функцию =======================
invoke DeviceIoControl,eax,IOCTL_SMART_RCV_DRIVE_DATA,\
inBuff,sizeof.SENDCMDINPARAMS,\
outBuff,sizeof.SENDCMDOUTPARAMS + sizeof.IDENTIFY_SECTOR, retSize,0
or eax,eax ;// проверить на ошибку нуль
jne @f ;// вниз, если OK..
add esp,4 ;// иначе: убрать адрес-возврата из стека
сinvoke printf,<10,' :( Contact the admin',0> ;// нет админских прав!
jmp @exit ;// на выход!
;//==== Данные идентификации лежат в приёмном буфере =========
@@: inc [drive]
cinvoke printf,<10,10,' DRIVE %d ',\
10,' ************',0>,[drive] ;// номер драйва
mov ebx,ata
cmp word[idSec.wSATAcapabilites],0
je @f
mov ebx,sata
@@: cinvoke printf,<10,' Interface....: %s',0>,ebx ;// ATA или SATA ???
SwapByte prnBuff,idSec.sModelNumber,40 ;// восстановить порядок байт в строке
cinvoke printf,<10,' Model........: %s',0>,prnBuff
SwapByte prnBuff,idSec.sSerialNumber,20
cinvoke printf,<10,' Serial.......: %s',0>,prnBuff
SwapByte prnBuff,idSec.sFirmwareRev,8
cinvoke printf,<10,' Revision.....: %s',0>,prnBuff
;// Берём макс.номер сектора LBA 48-бит, и получаем размер диска в гектарах.
;// Для расчётов воспользуемся FPU, умножив LBA на 512 (размер сектора):
mov eax,dword[idSec.ulTotalSectors48]
mov ebx,dword[idSec.ulTotalSectors48+4]
push 512 ebx eax
fild qword[esp]
fimul word[esp+8]
push 1024*1024*1024 ;// байты в Гигабайты
fidiv dword[esp]
fstp qword[fpuBuff]
add esp,4*4 ;// выровнить стек от пушей
cinvoke printf,<10,' Capacity.....: %0.1f Gb',0>,dword[fpuBuff],dword[fpuBuff+4]
;// Смотри "ATA_Command_Set" стр.144
movzx eax,word[idSec.wSATAcapabilites]
shr eax,8
and eax,1
cinvoke printf,<10,' Support NCQ..: %d',0>,eax
movzx eax,word[idSec.wAtaMajorVersion]
shl ax,8
mov ebx,7
mov ecx,3
@@: shl ax,1
jc @f
dec bl
loop @b
@@: cinvoke printf,<10,' ATA/ATAPI....: %d',0>,ebx
movzx eax,word[idSec.wUltraDMAmode]
shl ax,9
mov ebx,6
mov ecx,7
@@: shl ax,1
jc @f
dec bl
loop @b
@@: mov eax,133
cmp bl,6
je @dma
mov eax,100
cmp bl,5
je @dma
mov eax,66
@dma: cinvoke printf,<10,' Ultra DMA....: %d = %d Mb/sec',0>,ebx,eax
ret
endp
;//**********************************
section '.idata' import data readable
library msvcrt,'msvcrt.dll',kernel32,'kernel32.dll'
import msvcrt, printf,'printf',gets,'gets',exit,'exit'
include 'api\kernel32.inc'
Отметим, что это единственный способ получить заложенный вендором реальный серийник диска (именно диска, а не раздела или тома), поскольку мы берём его прямо из служебной зоны накопителя. Нужно учитывать, что производитель заполняет поля паспорта на своё усмотрение, хотя и придерживается общепринятых правил. Поэтому на низкопробных девайсах от наших друзей из страны восходящего солнца может получиться так, что некоторые поля будут пустыми, со-значением нуль. И ещё.. Только устройства ATA/SATA/ATAPI имеют сектор идентификации, и на USB-брелках его искать бесполезно.
3) S.M.A.R.T – состояние накопителей
Современные жёсткие диски HDD/SSD настолько развитый вид оборудования, что могут предсказывать свою смерть. Эта технология заложена в них на генном уровне и известна как SMART – Self Monitoring Analysis and Reporting Technology (самоконтроль, анализ и отчётность для оценки состояния жёсткого диска). В отличии от 512-байтного сектора с паспортом идентификации, в сервисной зоне накопителя данные SMART занимают целый цилиндр, а это 63 сектора или 32 Кб. Контроль за состоянием диска поддерживают огромное кол-во таблиц, куда специальная микро/программа прошивки сбрасывает 16 логов – вот их перечень:
На основе этих логов, по известному только производителю алгоритму вычисляется общее состояние диска. Нам, как рядовым пользователям детали реализации не интересны – нам подавай только готовый результат, для чего имеется основная (под)команда для работы со SMART – READ_DATA с соответствующим ей кодом
0хD0
. Эта команда возвращает привычную нам таблицу, которую мы можем наблюдать в окне утилит по сбору информации о жёстких дисках типа Victoria, HD-Tune и прочие.Чтобы прочитать SMART диска, в командный регистр
0x1F7
контролёра мы должны передать основную АТА-команду SMART_EXECUTE_CMD (константа 0хВ0), и дальше в регистре Feature 0x1F1
указать одну из следующих (под)команд, причём начальный сектор LBA производителями оговаривается и всегда будет равен 0xC24F00
. Полный список того-чем можно оперировать в контексте SMART со-значением всех регистров, представлен в таблице ниже:Здесь нужно уточнить некоторые детали.. Первое – это значение регистров
0x1F6
"DriveHead" и 0x1F2
"SectorCount". Современные контролёры уже давно перешли на LBA-адресацию секторов, поэтому поле с номером головки Head 0xA0
они тупо игнорируют и его можно не указывать. Если мы посылаем АТА-команды, то счётчик секторов так-же уходит на скамейку запасных, т.к. по коду основной команды контролёр на автомате вычисляет кол-во требуемых секторов. А вот если мы запрашиваем SMART_READ_LOG =0xD5, то в регистре адреса LBA-Low должны указать, какой именно из 16-ти логов нам нужен (см.первую таблицу).Для прикладных программистов, интерес представляют обычно первые две команды. Следуя логике разработчиков, SMART_READ_DATA =0xD0 должна возвращать готовые к употреблению данные, однако на практике самого главного из всех значений почему-то среди этих данных нет – это пороговое значение атрибута, относительно которого пляшут все остальные. Вот здесь-то и приходит на помощь вторая команда SMART_READ_THRESHOLDS =0xD1. Рассмотрим, какие бывают атрибуты, и по какому принципу рассчитывается здоровье диска.
Всего технология SMART поддерживает 30 различных атрибутов, а вот их идентификаторов аж 255. Дело в том, что у накопителей HDD свой диапазон ID, а у дисков SSD свой. Поэтому общее кол-во приближается к отметке 255. В
Ссылка скрыта от гостей
можно найти перечень всех атрибутов с их идентификаторами, где попавшие под раздачу SSD выделены своей меткой. Под каждый атрибут отводится 12-байт с таким форматом:Значит в первом байте зарыт идентификатор (номер) атрибута, дальше идёт слово с флагами и если бит нуль в нём взведён, то показатель жизненно-важен. Потом следуют 2-байта – текущее значение атрибута и наихудшее. 4-байтное сырое поле RAW во многих случаях несёт в себе наиболее ценную информацию. В новом накопителе RAW практически всех атрибутов содержат нули, и при первом-же включении начнут меняться, ..например счётчики запусков, времени наработки и т.п..
Однако предсмертное состояние накопителя вычисляется на основании порогового значения атрибута Threshold. Пригодным к эксплуатации считается диск, у которого ни один из текущих атрибутов Current не ниже порогового значения! При его достижении, в ответ на команду SMART_RETURN_STATUS =0xDA контролёр будет возвращать статус BAD.
Это хорошо, только среди этих 12-ти байт значение Threshold всегда равно нулю (не возвращается командой READ_DATA), и его нужно запрашивать явно, второй отдельной командой SMART_READ_THRESHOLDS. В своём инклуде, я определил эти две таблицы в виде таких структур:
C-подобный:
struct SMART_ATTRIB
AttributeId db 0
Flags dw 0
Current db 0
Worst db 0
RawData dd 0
AttrSpecific dw 0
Thresholds db 0 ;// ----------------+
Ends |
|
struct SMART_ATTRIB_THRESHOLDS ;//<--------+
AttributeId db 0
Thresholds db 0
Reserved rb 10
ends
Наконец-то пешком (как Ломоносов) мы подобралась к практической составляющей SMART..
Значит как и в предыдущем примере с паспортом диска, будем терзать функцию DeviceIoControl() запросом IOCTL_SMART_RCV_DRIVE_DATA, только в качестве основной команды передадим SMART_EXECUTE_CMD =0xB0, прицепив к ней (под)команду
SMART_READ_DATA
для чтения значений атрибутов, и следом SMART_READ_THRESHOLDS
, чтобы выудить пороговые значения. После того-как получим от функции ответ, в цикле длинною в 30 итераций обойдём все атрибуты, по-ходу сравнивая текущие их значения с порогом. Если текущее окажется меньше, то включаем ревун, иначе – "в Багдаде всё спокойно". Вот пример и его выхлоп:
C-подобный:
format pe console
include 'win32ax.inc'
include 'equates\storage.inc'
entry start
;//---------
.data
title db 'S.M.A.R.T. ver 0.1',0
dName db '\\.\PhysicalDrive'
name db '0',0
good db 'Ok ',0
bad db 'Bad',0
capt db 10,' Attribute ID Life Max Thresh Min Raw Values'
db 10,70 dup('='),0
retSize dd 0
hndl dd 0
attrId dd 0
inBuff dd 0,0,0
vendor STORAGE_DEVICE_DESCRIPTOR
scip SENDCMDINPARAMS
buff_1 rb 18 ;// приёмный буфер для атрибутов SMART
smartId rb 512
buff_2 rb 18 ;// приёмный буфер для пороговых значений
smartTh rb 512
;//---------
.code
start: invoke SetConsoleTitle,title
;//==== Заполняем регистры контролёра ======================================
mov [scip.irDriveRegs.LBA_Middle],SMART_LBA_MIDDLE_REG ;// 0x4F
mov [scip.irDriveRegs.LBA_High], SMART_LBA_HIGH_REG ;// 0xC2
mov [scip.irDriveRegs.CommandReg],SMART_EXECUTE_CMD ;// 0xB0 (основная команда)
;//==== Открываем очередной диск (нужны права админа) ======================
@@: invoke CreateFile,dName,GENERIC_READ or GENERIC_WRITE, FILE_SHARE_READ,\
0,OPEN_EXISTING,0,0
cmp eax,-1
jz @exit ;// выйти, если ошибка
mov [hndl],eax
;//==== Запрашиваем модель диска ===========================================
invoke DeviceIoControl,eax,IOCTL_STORAGE_QUERY_PROPERTY,\
inBuff,3*4,\
vendor,sizeof.STORAGE_DEVICE_DESCRIPTOR,\
retSize,0
mov eax,[vendor.ProductIdOffset]
add eax,vendor
cinvoke printf,<10,10,10,'Disk: %s %s',0>,eax,capt
call GetDiskSMART
inc [name] ;// сл.диск
jmp @b ;// повторить..
@exit: cinvoke gets,buff_1
cinvoke exit,0 ;// на выход!
;//---------
proc GetDiskSMART
mov [scip.irDriveRegs.FeaturesReg],SMART_READ_DATA ;// читаем SMART (доп.команда)
invoke DeviceIoControl,[hndl],IOCTL_SMART_RCV_DRIVE_DATA,\
scip,sizeof.SENDCMDINPARAMS,\
buff_1,512+16,retSize,0
or eax,eax
jnz @ok
ret
@ok: mov [scip.irDriveRegs.FeaturesReg],SMART_READ_THRESHOLDS ;// читаем пороговые значения
invoke DeviceIoControl,[hndl],IOCTL_SMART_RCV_DRIVE_DATA,\
scip,sizeof.SENDCMDINPARAMS,\
buff_2,512+16,retSize,0
;//==== Теперь данные SMART в двух буферах ========================
mov edi,smartId ;// EDI = указатель на атрибуты
mov ebp,smartTh ;// EBP = их пороговые значения
mov ecx,30 ;// всего атрибутов (цикл)
@@: push ebp edi ecx ;// запомнить для сл.итерации
movzx eax,byte[edi+SMART_ATTRIB.AttributeId] ;// ID очередного атрибута..
mov [attrId],eax ;// запомнить для вывода на консоль
or al,al ;// пропустить,
je @unk ;// ..если нуль (вендор не определил)
mov ecx,[tableSize] ;// иначе: ищем его имя в таблице (ECX = всего)
mov ebx,eax ;// EBX = ID атрибута
mov esi,smartTable ;// ESI = адрес таблицы
@find: lodsd ;// EAX = ID из таблицы
cmp eax,ebx ;// сравнить
je @stop ;// если совпало..
add esi,4 ;// иначе: следующий в таблице
loop @find ;// промотать ECX-раз..
jmp @unk ;// прокол :(
;//==== Имя нашли, и берём остальные данные из обеих таблиц ======
@stop: mov eax,[esi]
movzx ebx,byte[edi+SMART_ATTRIB.Current]
movzx ecx,byte[edi+SMART_ATTRIB.Worst]
mov edx,dword[edi+SMART_ATTRIB.RawData]
movzx esi,byte[ebp+SMART_ATTRIB_THRESHOLDS.Thresholds]
mov edi,good
cmp ebx,esi ;// сравнить Current и Threshold
jae @good ;// больше/равно = Гуд
mov edi,bad
@good: cinvoke printf,<10,'%03d %s %s %03d %02d %03d %u',0>,\
[attrId],eax,edi,ebx,esi,ecx,edx
;//==== Один атрибут распарсили, переходим к следующему ===========
@unk: pop ecx edi ebp ;// восстановить длину и указатели
add edi,12 ;// перейти к следующим..
add ebp,12 ;// ^^^
dec ecx ;// всего атрибутов -1
jnz @b ;// повторить, если не нуль
ret
endp
;//**********************************
;// Таблица идентификаторов и указателей на их строки
section '.smart' data readable
smartTable dd 0x01,@01,0x02,@02,0x03,@03,0x04,@04,0x05,@05,0x06,@06
dd 0x07,@07,0x08,@08,0x09,@09,0x0A,@0A,0x0B,@0B,0x0C,@0C
dd 0x0D,@0D,0x64,@64,0x67,@67,0xAA,@AA,0xAB,@AB,0xAC,@AC
dd 0xAD,@AD,0xAE,@AE,0xAF,@AF,0xB0,@B0,0xB1,@B1,0xB2,@B2
dd 0xB3,@B3,0xB4,@B4,0xB5,@B5,0xB6,@B6,0xB7,@B7,0xB8,@B8
dd 0xBB,@BB,0xBC,@BC,0xBD,@BD,0xBE,@BE,0xBF,@BF,0xC0,@C0
dd 0xC1,@C1,0xC2,@C2,0xC3,@C3,0xC4,@C4,0xC5,@C5,0xC6,@C6
dd 0xC7,@C7,0xC8,@C8,0xC9,@C9,0xCA,@CA,0xCB,@CB,0xCC,@CC
dd 0xCD,@CD,0xCE,@CE,0xCF,@CF,0xD1,@D1,0xDC,@DC,0xDD,@DD
dd 0xDE,@DE,0xDF,@DF,0xE0,@E0,0xE1,@E1,0xE2,@E2,0xE3,@E3
dd 0xE4,@E4,0xE6,@E6,0xE7,@E7,0xEA,@EA,0xF0,@F0,0xF1,@F1
dd 0xF2,@F2,0xF7,@F7,0xF8,@F8,0xFA,@FA,0xFE,@FE
tableSize dd ($ - smartTable) /8
;// https://ru.wikipedia.org/wiki/S.M.A.R.T.
;// Наиболее важные атрибуты: 05,07,0A,C4,C5,C6,C8
;// ==================================================
@01 db ' Raw Read Error Rate ',0
@02 db ' Throughput Performance ',0
@03 db ' Spin-Up Time ',0
@04 db ' Start/Stop Count ',0
@05 db ' Realocation Sector Count ',0
@06 db ' Read Channel Margin ',0
@07 db ' Seek Error Rate ',0
@08 db ' Seek Time Performance ',0
@09 db ' Power-On Hours Count ',0
@0A db ' Spin Retry Count ',0
@0B db ' Recalibration Retries ',0
@0C db ' Power Cycle Count ',0
@0D db ' Soft-Read Error Rate ',0
@64 db ' SSD Erase/Program Cycles ',0
@67 db ' SSD Translation Table Rebuild ',0
@AA db ' SSD Reserved Block Count ',0
@AB db ' SSD Program Fail Count ',0
@AC db ' SSD Erase Fail Count ',0
@AD db ' Wear Leveller Worst Case Erase',0
@AE db ' SSD Unexpected Power Loss ',0
@AF db ' SSD Program Fail Count ',0
@B0 db ' SSD Erase Fail Count ',0
@B1 db ' SSD Wear Leveling Count ',0
@B2 db ' SSD Used Reserved Block Count ',0
@B3 db ' SSD Used Reserved Block Count ',0
@B4 db ' SSD Unused Reserved Block ',0
@B5 db ' SSD Program Fail Count ',0
@B6 db ' SSD Erase Fail Count ',0
@B7 db ' SSD SATA Down-shifts ',0
@B8 db ' End-to-End Error ',0
@BB db ' Reported UNC Errors ',0
@BC db ' Command Timeout ',0
@BD db ' High Fly Writes ',0
@BE db ' Airflow Temperature (WDC) ',0
@BF db ' G-Sense Error Rate (Shock) ',0
@C0 db ' Power-off Retract Count ',0
@C1 db ' Load/Unload Cycle Count ',0
@C2 db ' Temperature *C ',0
@C3 db ' Hardware ECC Recovered ',0
@C4 db ' Realocation Event Count ',0
@C5 db ' Current Pending Sector Count ',0
@C6 db ' Uncorrectable Sector Count ',0
@C7 db ' Ultra-DMA CRC Error Count ',0
@C8 db ' Write Error Rate ',0
@C9 db ' Soft Read Error Rate ',0
@CA db ' Data Address Mark Errors ',0
@CB db ' Run Out Cancel ',0
@CC db ' Soft ECC Correction ',0
@CD db ' Thermal Asperity Rate (TAR) ',0
@CE db ' Flying Height ',0
@CF db ' Spin High Current ',0
@D1 db ' Offline Seek Performance ',0
@DC db ' Disk Shift ',0
@DD db ' G-Sense Error Rate (Shock) ',0
@DE db ' Loaded Hours ',0
@DF db ' Load/Unload Retry Count ',0
@E0 db ' Load Friction ',0
@E1 db ' Load Cycle Count ',0
@E2 db ' Load in Time ',0
@E3 db ' Torgue Amplification ',0
@E4 db ' Power Off Retract Count ',0
@E6 db ' GMR Head Amplitude ',0
@E7 db ' SSD Percent Lifetime Remaining',0
@EA db ' Not Correctable ECC Error ',0
@F0 db ' Head Flying Hours ',0
@F1 db ' Total LBAs Written from Host ',0
@F2 db ' Total LBAs Read from Host ',0
@F7 db ' Host Program Page Count ',0
@F8 db ' Background Program Page Count ',0
@FA db ' Read Error Retry Rate ',0
@FE db ' Free Fall Protection ',0
;//**********************************
section '.idata' import data readable
library msvcrt,'msvcrt.dll',kernel32,'kernel32.dll'
import msvcrt, printf,'printf',scanf,'scanf',gets,'gets',exit,'exit'
include 'api\kernel32.inc'
Заключение
На этой ноте пора уже поставить точку, хотя в масштабах общих возможностей IOCTL не было сказано почти ничего. За бортом осталась работа с разделами диска на уровне файловой системы с кодами FSCTL и многое другое. В процессе экспериментов главное помнить, что в этой войне пленных не берут и при малейших ошибках чтения/записи секторов, можно запросто потерять все данные на диске.
Помимо рассмотренных прикладных методов, с дисками можно общаться и по внутреннему интерфейсу i2c, перемычками или джампером переключив контролёр в сервисный режим. Эта скрытая кладезь позволяет вторгнуться в святая-святых накопителей – их прошивку. Правда при этом Win32-API остаётся уже не у дел, и нужно передавать команды по терминалу. Если освоить данную технику, то можно пустить это дело на коммерческие рельсы, воскрешая мёртвые диски из небытия и восстанавливая с них информацию.
В скрепке цепляю обещанный инклуд, с программами HD-Tune (можно сравнить SMART), и WinObj для просмотра системного пространства имён. До скорого!
Вложения
Последнее редактирование: