繁体   English   中英

如何在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调用成功返回, 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 <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上运行。

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

在此输入图像描述

第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 };

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

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM