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

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

请考虑以下代码:

 #include  #include  #include  #include  #include  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的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,因此不需要AlignmentMask ,因此看起来这不是问题的原因。 有趣的是, AdapterUsesPio是1,尽管设备管理器说不然。

为了记录,下面的代码在Linux上正常工作,目标缓冲区充满了来自CD的数据。 与Windows相同,返回的SCSI状态为0,并且不返回任何感知数据。

 #include  #include  #include  #include  #include  #include  #include  #include  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上运行。

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

在此处输入图像描述

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

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

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

由于读取安全性限制,您的代码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,获取驱动器function,但是读取命令将被阻止,并且DeviceIoControl将引发“无效function”错误。 显然,至少对于Windows 10,我的软件再次运行并且限制已被删除,但由于Windows 7具有大量用户安装基础,因此您将希望以SPTI规定的方式执行此操作,而且IOCTL_CDROM_RAW_READ知道一些不太常见的情况。读取命令,而不是通用的0xBE,用于旧的古怪驱动器,所以最好还是使用它!