RomeoGolf

Вс 29 Январь 2017

USB-polygon-8: Обмен по USB, еще о взаимодействии с «чистым» устройством

Завершая этап

В результате проделанной работы получилось «сырое» с точки зрения файловой системы MassStorage-устройство, приспособленное к обмену информацией с ПК по USB. С определенными ограничениями, конечно: во-первых, требуется некоторый самописный код для ПК, причем, требующий прав администратора; во-вторых, на устройстве программа тоже выглядит не особенно изящно, нужно городить какие-то конструкции для идентификации запрашиваемой и получаемой информации по ее адресу, а также какое-то распределение в адресном пространстве источников и приемников информации. Ну, это уже зависит от конкретной задачи, а текущая задача — учебная — фактически выполнена.

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

Однако перед этим хотелось бы провести еще пару экспериментов с «сырым» устройством.

Физическое устройство

Первая «доделка» касается кода для ПК. Доступ к устройству осуществляется при помощи дескриптора (HANDLE), который возвращает функция CreateFile. Этой функции сейчас передается в качестве параметра DevicePath, который, в свою очередь, возвращается функцией SetupDiEnumDeviceInterfaces, принимающей среди параметров в том числе GUID интерфейса, можно диска, можно тома.

Хочу попробовать достучаться до платы, как до физического устройства, PhysicalDrive. Есть мнение, что это будет более единообразно для разных версий ОС Windows, а также должно снять сомнения, к чему же правильнее обращаться — к диску или к тому.

Для этого снова пригодится функция DeviceIoControl, которой вторым параметром нужно передать IOCTL_VOLUME_GET_VOLUME_DISK_EXTENTS.

В коде после комментария // ----- работа с устройством ----- надо добавить объявление новых переменных:

                    int diskNum = 0;                // количество дисков
                    char physicalDrive[50] = { 0 }; // строка под "\\\\.\\PhysicalDrive%d"
                    DWORD bufsize, bufsizeret;      // размеры буфера
                    VOLUME_DISK_EXTENTS buf_ioctl;  // буфер для GET_VOLUME_DISK_EXTENTS
                    bufsize = sizeof(VOLUME_DISK_EXTENTS);

Однако парочку появившихся типов этот код не знает. Не буду подключать соответствующий заголовок, просто запишу после объявления struct scsi_st:

typedef struct _DISK_EXTENT {
  DWORD DiskNumber;
  LARGE_INTEGER StartingOffset;
  LARGE_INTEGER ExtentLength;
} DISK_EXTENT,*PDISK_EXTENT;

typedef struct _VOLUME_DISK_EXTENTS {  DWORD NumberOfDiskExtents;  DISK_EXTENT Extents[ANYSIZE_ARRAY];
} VOLUME_DISK_EXTENTS;

А также такие #define после всех #include:

#define IOCTL_VOLUME_BASE   ((ULONG) 'V')
#define IOCTL_VOLUME_GET_VOLUME_DISK_EXTENTS \
  CTL_CODE(IOCTL_VOLUME_BASE, 0, METHOD_BUFFERED, FILE_ANY_ACCESS)

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

                    result = DeviceIoControl(hDevice,
                            IOCTL_VOLUME_GET_VOLUME_DISK_EXTENTS,
                            NULL,
                            0,
                            &buf_ioctl,
                            bufsize,
                            &bufsizeret,
                            NULL
                            );
                    if (result==0) {
                        OutFormatMsg("PhysicDiscErr");
                    } else {
                        _tprintf("DiscNum = %lu\n", buf_ioctl.NumberOfDiskExtents);
                        diskNum = buf_ioctl.Extents[0].DiskNumber;
                        _tprintf("Disk = %d\n", diskNum);
                        if (diskNum != 0) {
                            sprintf(physicalDrive, "\\\\.\\PhysicalDrive%d", diskNum);
                        }
                        _tprintf("physicalDrive: %s\n", physicalDrive);
                    }

Теперь в консольном выводе появятся новые строки:

. . .
--- This is my device! ---
DiscNum = 1
Disk = 1
physicalDrive: \\.\PhysicalDrive1
WriteFile done
len = 1024
ReadFile done
data_2 = 0
len = 1024

То есть, дисков 1, номер диска 1, строка для CreateFile сформирована. Можно сразу после пытаться получить дескриптор устройства по номеру физического диска:

                    HANDLE hDevice2=CreateFile(
                            physicalDrive,
                            GENERIC_READ | GENERIC_WRITE,
                            //0,
                            //FILE_SHARE_READ or FILE_SHARE_WRITE,
                            0,
                            NULL,
                            OPEN_EXISTING,
                            FILE_FLAG_WRITE_THROUGH | FILE_FLAG_NO_BUFFERING,
                            NULL
                            );
                    if(hDevice2 == INVALID_HANDLE_VALUE) {
                        OutFormatMsg("CreateFile 2 Error");
                    } else {
                        _tprintf(_T("CreateFile 2 done! \n"));

                        // сюда вставить ReadFile и WriteFile;
                        // сюда же можно и обе
                        // DeviceIoControl с IOCTL_SCSI_PASS_THROUGH_DIRECT

                        CloseHandle(hDevice2);
                    }

И в имеющихся ReadFile и WriteFile после их перестановки на новое место (указанное комментарием в коде) заменяю hDevice на hDevice2.

Проверено — работает. Делает все то же, что и предыдущая версия.

Еще можно попробовать заблокировать и размонтировать устройство при помощи DeviceIoControl с параметрами FSCTL_LOCK_VOLUME и FSCTL_DISMOUNT_VOLUME соответственно, но это уже другая история.

Пользовательская SCSI-команда

Теперь обращусь к устройству. До сих пор оно выполняло только стандартные команды, передаваемые при помощи ScsiPassThroughDirect, это WRITE(10) с кодом 0x2A и READ (10) с кодом 0x28.

А если мне надо выполнить какое-то свое действие, не предусмотренное набором SCSI-команд? Или, скажем, надо именно записать, но, допустим, в нулевой «сектор», а операционная система после очередного обновления безопасности не разрешает этого делать, чтобы сильно умелые ручки не портили MBR устройств? «Сектор» здесь написал в кавычках, потому что нет никаких секторов в помине, есть только используемые в обмене адреса данных, которые в нормальном устройстве могли бы действительно указывать на сектор.

Пробую: в SCSI-команде записи закрываю строку с кодом команды, а вместо нее пишу ее копию, но с кодом 0xC1: команды от 0xC0 до 0xFF вроде бы vendor cpecific.

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

Открываю /Lib/SCSI.c, нахожу там функцию bool SCSI_DecodeSCSICommand(void), в ней переключатель switch (CommandBlock.SCSICommandData[0]), благо, кроме него там почти ничего нет. Сразу под строкой case SCSI_CMD_WRITE_10: добавляю строку case 0xC1:. Тем самым заставляю устройство реагировать на команду 0xC1 так же, как и на команду 0x2A.

Проверяю — работает. То есть, запись снова проходит, и на светодиодах отображается переданный с ПК байт.

Таким образом можно дублировать имеющиеся команды, можно писать совсем собственные. Для самописных команд можно особым образом обрабатывать Command Descriptor Block (CDB), а как это сделать — можно подсмотреть в используемой здесь функции SCSI_Command_ReadWrite_10, например. Там есть ближе к началу строки

    BlockAddress = SwapEndian_32(*(uint32_t*)&CommandBlock.SCSICommandData[2]);
    TotalBlocks  = SwapEndian_16(*(uint16_t*)&CommandBlock.SCSICommandData[7]);

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

Ну вот, теперь с «сырым» устройством практически всё. Что касается кода — надо закоммитить текущие изменения в git с тегами «v0.3» для обеих программ.

Далее начнется работа по имитации файловой системы на устройстве, несмотря на отсутствие памяти для этой самой файловой системы.


Теги: