如何在 Windows 中向 CD-ROM 驱动器发出 READ CD 命令?

How to issue a READ CD command to a CD-ROM drive in Windows?

我正在开发一个需要向 CD-ROM 驱动器发出原始 SCSI 命令的应用程序。目前,我正在努力向驱动器发送 READ CD (0xBE) 命令并从 CD 的给定扇区取回数据。

考虑以下代码:

#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组装的,应该从LBA 1转移1个扇区。由于我只使用CD-DA光盘,每个扇区是2352字节,这解释了为什么sizeof(buf) 是 2352.

为简洁起见,省略了错误检查。调试器显示DeviceIoControl调用return成功,ioctl_bytes0x2c,而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),并且没有检测数据returned。但是,数据缓冲区未写入,因为调试器显示它已填充 0xcc,因为应用程序是在调试模式下编译的。

但是,当我像这样将 CDB 更改为标准 INQUIRY 命令时:

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

缓冲区已正确填充查询数据,我能够读取驱动器名称、供应商和其他所有内容。

我已经尝试对齐目标缓冲区,根据 Microsoft's documentation for SCSI_PASS_THROUGH_DIRECT,它说 SCSI_PASS_THROUGH_DIRECT 的 DataBuffer 成员是指向此适配器设备对齐缓冲区的指针。实验性地将缓冲区对齐到 64 字节没有用,发出一个 IOCTL_SCSI_GET_CAPABILITIES,它应该 return 所需的对齐,给了我以下信息:

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 上运行正常,并且目标缓冲区中充满了来自 CD 的数据。与 Windows 相同,returned SCSI 状态为 0,没有检测数据 returned。

#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 代码在 Windows XP 上使用 Visual Studio C++ 2010 Express 和 WinDDK 7600.16385.1 编译。在 Windows XP 上也是 运行。

问题出在格式不正确的 CDB 中,尽管在语法方面是有效的。我在 MMC 规范中没有看到的是:

第 9 个字节应该包含用于选择驱动器应该使用的数据类型的位 return。在问题的代码中,我将其设置为0,这意味着我向驱动器请求了"No fields"。将此字节更改为 0x10(用户数据)会导致 Linux 和 Windows 版本 return 对给定扇区使用相同的数据。我仍然不知道为什么 Linux return 即使使用 CDB 的原始形式,也会在缓冲区中编辑一些数据。

当在 LBA 1 读取一个 CD-DA 扇区时,​​READ CD 命令的正确 CDB 应该如下所示:

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

顺便说一句,由于读取安全限制,您的代码在 Windows 7 中总是会失败。您可以使用 DeviceIOControl API 发送大多数 SCSI 命令,但是当涉及到数据或原始读取时,您必须使用规定的 SPTI 方法读取一个扇区,否则 Windows 7 将阻止它,有或没有管理员权限,仅供参考,如果您想要更高的兼容性,您不能再以 SCSI 方式执行此操作!

这是 SPTI 规定的方式,幸运的是,它比使用 OxBE 或 READ10 构建 SCSI 命令数据包的代码少得多(如果您只想要数据的数据,您应该使用它扇区,因为它是 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 命令。上面的代码片段适用于音频扇区和 return 2352 字节。如果您的 CreateFile() 调用正确,这可以一直工作到 Windows NT4.0。但是,是的,如果您使用 IOCTL_SCSI_PASS_THROUGH_DIRECT 并尝试构建您自己的 0xBE SCSI 命令包,Windows 7 将阻止它! Microsoft 希望您使用 IOCTL_CDROM_RAW_READ 进行原始读取。您可以构建其他 SCSI 命令数据包来读取 TOC、获取驱动器功能,但读取命令将被阻止并且 DeviceIoControl 将引发 "Invalid function" 错误。显然,至少对于 Windows 10,我的软件再次运行并且限制被删除,但由于 Windows 7 拥有庞大的用户安装基础,您无论如何都希望按照 SPTI 规定的方式进行操作,再加上 IOCTL_CDROM_RAW_READ 知道一些比通用 0xBE 更不常见的读取命令,用于旧的古怪驱动器,所以最好还是使用它!