3

我正在开发一个需要向 CD-ROM 驱动器发出原始 SCSI 命令的应用程序。目前,我正在努力向0xBE驱动器发送 READ CD ( ) 命令并从 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_bytesis 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

这表明该命令已被驱动器成功执行,为ScsiStatus0 ( 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,所以不需要对齐,因此这似乎不是问题的原因。有趣的是,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 代码在 Windows XP 上使用 Visual Studio C++ 2010 Express 和 WinDDK 7600.16385.1 编译。它也可以在 Windows XP 上运行。

4

2 回答 2

2

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

在此处输入图像描述

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

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

const unsigned char cdb[] = { 0xBE, 0, 0, 0, 0, 1, 0, 0, 1, 0x10, 0, 0 };
于 2015-04-27T18:25:37.930 回答
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 将引发“无效功能”错误。显然,至少对于 Windows 10,我的软件再次运行,并且限制被删除,但由于 Windows 7 拥有庞大的用户安装基础,您无论如何都希望按照 SPTI 规定的方式进行操作,

于 2015-09-17T06:59:09.573 回答