Как выполнить команду READ CD для привода CD-ROM в Windows?

Я работаю над приложением, которое должно выдавать сырые команды SCSI на привод CD-ROM. В настоящее время я борюсь с отправкой READ CD (0xBE) подать команду на привод и получить обратно данные из заданного сектора компакт-диска.

Рассмотрим следующий код:

#include <windows.h>
#include <winioctl.h>
#include <ntddcdrm.h>
#include <ntddscsi.h>
#include <stddef.h>

int main(void)
{
  HANDLE fh;
  DWORD ioctl_bytes;
  BOOL ioctl_rv;
  const UCHAR cdb[] = { 0xBE, 0, 0, 0, 0, 1, 0, 0, 1, 0, 0, 0 };
  UCHAR buf[2352];
  struct sptd_with_sense
  {
    SCSI_PASS_THROUGH_DIRECT s;
    UCHAR sense[128];
  } sptd;

  fh = CreateFile("\\\\.\\E:", GENERIC_READ | GENERIC_WRITE,
    FILE_SHARE_READ | FILE_SHARE_WRITE, NULL, OPEN_EXISTING,
    FILE_ATTRIBUTE_NORMAL, NULL);

  memset(&sptd, 0, sizeof(sptd));
  sptd.s.Length = sizeof(sptd.s);
  sptd.s.CdbLength = sizeof(cdb);
  sptd.s.DataIn = SCSI_IOCTL_DATA_IN;
  sptd.s.TimeOutValue = 30;
  sptd.s.DataBuffer = buf;
  sptd.s.DataTransferLength = sizeof(buf);
  sptd.s.SenseInfoLength = sizeof(sptd.sense);
  sptd.s.SenseInfoOffset = offsetof(struct sptd_with_sense, sense);
  memcpy(sptd.s.Cdb, cdb, sizeof(cdb));

  ioctl_rv = DeviceIoControl(fh, IOCTL_SCSI_PASS_THROUGH_DIRECT, &sptd,
    sizeof(sptd), &sptd, sizeof(sptd), &ioctl_bytes, NULL);

  CloseHandle(fh);

  return 0;
}

CDB был собран в соответствии с MMC-6 Revision 2g и должен перенести 1 сектор из LBA 1. Поскольку я работаю только с дисками CD-DA, каждый сектор занимает 2352 байта, что объясняет, почему sizeof(buf) 2352

Проверка ошибок была опущена для краткости. Отладчик показывает, что DeviceIoControl звонок успешно возвращается и ioctl_bytes является 0x2cв то время как значения внутри sptd.s являются следующими:

Length              0x002c      unsigned short
ScsiStatus          0x00        unsigned char
PathId              0x00        unsigned char
TargetId            0x00        unsigned char
Lun                 0x00        unsigned char
CdbLength           0x0c        unsigned char
SenseInfoLength     0x00        unsigned char
DataIn              0x01        unsigned char
DataTransferLength  0x00000930  unsigned long
TimeOutValue        0x0000001e  unsigned long
DataBuffer          0x0012f5f8  void *
SenseInfoOffset     0x0000002c  unsigned long

Это показывает, что команда была успешно выполнена дисководом, так как ScsiStatus это 0 (SCSI_STATUS_GOOD), и никакие смысловые данные не были возвращены. Тем не менее, буфер для данных не записывается, так как отладчик показывает, что он заполнен 0xcc, так как приложение компилируется в режиме отладки.

Однако, когда я изменяю CDB на стандартную команду INQUIRY, как это:

const UCHAR cdb[] = { 0x12, 0, 0, 0, 36, 0 };

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

Я уже пытался выровнять целевой буфер в соответствии с документацией Microsoft для SCSI_PASS_THROUGH_DIRECT, в которой говорится, что член DataBuffer в SCSI_PASS_THROUGH_DIRECT является указателем на этот буфер, выровненный по устройству адаптера. Экспериментальное выравнивание буфера до 64 байт не сработало, и выдача IOCTL_SCSI_GET_CAPABILITIES, который должен вернуть требуемое выравнивание, дал мне следующую информацию:

Length                      0x00000018  unsigned long
MaximumTransferLength       0x00020000  unsigned long
MaximumPhysicalPages        0x00000020  unsigned long
SupportedAsynchronousEvents 0x00000000  unsigned long
AlignmentMask               0x00000001  unsigned long
TaggedQueuing               0x00        unsigned char
AdapterScansDown            0x00        unsigned char
AdapterUsesPio              0x01        unsigned char

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

Напомним, что приведенный ниже код правильно работает в Linux, а целевой буфер заполнен данными с компакт-диска. Как и в Windows, возвращенное состояние SCSI равно 0, и никакие данные о состоянии не возвращаются.

#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <string.h>
#include <scsi/sg.h>
#include <scsi/scsi.h>
#include <linux/cdrom.h>
#include <sys/ioctl.h>

int main(void)
{
  int fd = open("/dev/sr0", O_RDONLY | O_NONBLOCK);
  if(fd == -1) { perror("open"); return 1; }

  {
    struct sg_io_hdr sgio;
    unsigned char cdb[] = { 0xBE, 0, 0, 0, 0, 1, 0, 0, 1, 0, 0, 0 };
    unsigned char buf[2352];
    unsigned char sense[128];
    int rv;

    sgio.interface_id = 'S';
    sgio.dxfer_direction = SG_DXFER_FROM_DEV;
    sgio.cmd_len = sizeof(cdb);
    sgio.cmdp = cdb;
    sgio.dxferp = buf;
    sgio.dxfer_len = sizeof(buf);
    sgio.sbp = sense;
    sgio.mx_sb_len = sizeof(sense);
    sgio.timeout = 30000;

    rv = ioctl(fd, SG_IO, &sgio);
    if(rv == -1) { perror("ioctl"); return 1; }
  }
  close(fd);
  return 0;
}

Код Windows скомпилирован с помощью Visual Studio C++ 2010 Express и WinDDK 7600.16385.1 для Windows XP. Он также работает на Windows XP.

2 ответа

Решение

Проблема заключается в неправильно сформированном CDB, хотя и с точки зрения синтаксиса. Что я не смог увидеть в спецификации MMC, так это:

Предполагается, что 9-й байт содержит биты, используемые для выбора типа данных, которые должен возвращать накопитель. В указанном коде я установил его на 0, что означает, что я запросил "Нет полей" с накопителя. Изменение этого байта на 0x10 (Данные пользователя) приводят к тому, что версии для Linux и Windows возвращают одни и те же данные для данного сектора. Я до сих пор не знаю, почему Linux вернул некоторые данные в буфер даже с оригинальной формой CDB.

Поэтому надлежащий CDB для команды READ CD при чтении одного сектора CD-DA на LBA 1 должен выглядеть следующим образом:

const unsigned char cdb[] = { 0xBE, 0, 0, 0, 0, 1, 0, 0, 1, 0x10, 0, 0 };

Ваш код BTW всегда будет терпеть неудачу в Windows 7 из-за ограничений безопасности чтения. Вы можете отправлять большинство команд SCSI с помощью API-интерфейса DeviceIOControl, но когда дело доходит до данных или необработанных чтений, вы должны использовать предписанный метод SPTI для чтения сектора, или Windows 7 заблокирует его, с правами администратора или без, поэтому, к вашему сведению, вы можете больше не делайте так, как SCSI, если вы хотите больше совместимости!

Вот как должен выглядеть предписанный SPTI способ, и, к счастью, это гораздо меньше кода, чем создание пакета команд SCSI с использованием OxBE или READ10 (это то, что вам следовало бы использовать, если вы просто хотели получить данные сектора данных, поскольку это SCSI-1, а не 0xBE, что менее совместимо):

RAW_READ_INFO rawRead;

if ( ghCDRom ) {
    rawRead.TrackMode = CDDA;
    rawRead.SectorCount = nSectors;
// Must use standard CDROM data sector size of 2048, and not 2352 as one would expect
// while buffer must be able to hold the raw size, 2352 * nSectors, as you *would* expect!
    rawRead.DiskOffset.QuadPart = LBA * CDROM_SECTOR_SIZE;
// Call DeviceIoControl, and trap both possible errors: a return value of FALSE
// and the number of bytes returned not matching expectations!
    return (
        DeviceIoControl(ghCDRom, IOCTL_CDROM_RAW_READ, &rawRead, sizeof(RAW_READ_INFO), gAlignedSCSIBuffer, SCSI_BUFFER_SIZE, (PDWORD)&gnNumberOfBytes, NULL)
        &&
        gnNumberOfBytes == (nSectors * RAW_SECTOR_SIZE)
    );

Короче говоря, Google вокруг команды IOCTL_CDROM_RAW_READ. Приведенный выше фрагмент кода будет работать для звукового сектора и возвращать 2352 байта. Это может работать вплоть до Windows NT4.0, если ваш вызов CreateFile() правильный. Но да, если вы используете IOCTL_SCSI_PASS_THROUGH_DIRECT и попытаетесь создать свой собственный командный пакет SCSI 0xBE, Windows 7 заблокирует его! Microsoft хочет, чтобы вы использовали IOCTL_CDROM_RAW_READ для необработанных чтений. Вы можете создать другие пакеты команд SCSI для чтения оглавления, получения возможностей накопителя, но команда чтения будет заблокирована, и DeviceIoControl вызовет ошибку "Недопустимая функция". По-видимому, по крайней мере для Windows 10 мое программное обеспечение снова заработало, и ограничение было снято, но поскольку Windows 7 имеет большую базу для установки пользователем, вы захотите сделать это SPTI, предписанным способом ВСЕГДА, плюс IOCTL_CDROM_RAW_READ знает еще несколько менее распространенных Команды чтения, чем универсальная 0xBE для старых странных дисков, так что лучше использовать их в любом случае!

Другие вопросы по тегам