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

时间:2015-04-26 22:51:13

标签: c winapi windows-xp ioctl cd-rom

我正在开发一个需要向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调用成功返回,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),并且未返回任何感知数据。但是,由于调试器显示填充了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,它应该返回所需的对齐方式,它给了我以下信息:

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相同,返回的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 XP上使用Visual Studio C ++ 2010 Express和WinDDK 7600.16385.1编译Windows代码。它也可以在Windows XP上运行。

2 个答案:

答案 0 :(得分:2)

问题在于不正确形成的CDB,尽管在语法方面是有效的。我在MMC规范中看不到的是:

enter image description here

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

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

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

答案 1 :(得分:2)

您的代码由于读取安全性限制,BTW在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)
    );

简而言之,谷歌围绕着IOCTL_CDROM_RAW_READ命令。上面的代码片段适用于音频扇区并返回2352字节。如果您的CreateFile()调用正确,这可以一直回到Windows NT4.0。但是,是的,如果您使用IOCTL_SCSI_PASS_THROUGH_DIRECT并尝试构建自己的0xBE SCSI命令包,Windows 7将阻止它! Microsoft希望您使用IOCTL_CDROM_RAW_READ进行原始读取。您可以构建其他SCSI命令数据包来读取TOC,获取驱动器功能,但是读取命令将被阻止,并且DeviceIoControl将引发一个&#34;无效功能&#34;错误。显然,至少对于Windows 10,我的软件再次运行并且限制已被删除,但由于Windows 7具有庞大的用户安装基础,因此您将希望以SPTI规定的方式执行此操作,而且IOCTL_CDROM_RAW_READ知道一些不太常见的情况。读取命令而不是通用0xBE用于旧古怪的驱动器,所以最好还是使用它!