Как выполнить команду READ CD для привода CD-ROM в Windows?
Я работаю над приложением, которое должно выдавать сырые команды SCSI на привод CD-ROM. В настоящее время я борюсь с отправкой READ CD (0xBE
) подать команду на привод и получить обратно данные из заданного сектора компакт-диска.
Рассмотрим следующий код:
#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 и должен перенести 1 сектор из LBA 1. Поскольку я работаю только с дисками CD-DA, каждый сектор занимает 2352 байта, что объясняет, почему sizeof(buf)
2352
Проверка ошибок была опущена для краткости. Отладчик показывает, что DeviceIoControl
звонок успешно возвращается и ioctl_bytes
является 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
Это показывает, что команда была успешно выполнена дисководом, так как ScsiStatus
это 0 (SCSI_STATUS_GOOD
), и никакие смысловые данные не были возвращены. Тем не менее, буфер для данных не записывается, так как отладчик показывает, что он заполнен 0xcc
, так как приложение компилируется в режиме отладки.
Однако, когда я изменяю CDB на стандартную команду INQUIRY, как это:
const UCHAR cdb[] = { 0x12, 0, 0, 0, 36, 0 };
Буфер правильно заполнен данными запроса, и я могу прочитать название диска, поставщика и все остальное.
Я уже пытался выровнять целевой буфер в соответствии с документацией Microsoft для SCSI_PASS_THROUGH_DIRECT, в которой говорится, что член DataBuffer в SCSI_PASS_THROUGH_DIRECT является указателем на этот буфер, выровненный по устройству адаптера. Экспериментальное выравнивание буфера до 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, а целевой буфер заполнен данными с компакт-диска. Как и в 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.
2 ответа
Проблема заключается в неправильно сформированном CDB, хотя и с точки зрения синтаксиса. Что я не смог увидеть в спецификации MMC, так это:
Предполагается, что 9-й байт содержит биты, используемые для выбора типа данных, которые должен возвращать накопитель. В указанном коде я установил его на 0, что означает, что я запросил "Нет полей" с накопителя. Изменение этого байта на 0x10
(Данные пользователя) приводят к тому, что версии для Linux и Windows возвращают одни и те же данные для данного сектора. Я до сих пор не знаю, почему Linux вернул некоторые данные в буфер даже с оригинальной формой CDB.
Поэтому надлежащий CDB для команды READ CD при чтении одного сектора CD-DA на LBA 1 должен выглядеть следующим образом:
const unsigned char cdb[] = { 0xBE, 0, 0, 0, 0, 1, 0, 0, 1, 0x10, 0, 0 };
Ваш код BTW всегда будет терпеть неудачу в Windows 7 из-за ограничений безопасности чтения. Вы можете отправлять большинство команд SCSI с помощью API-интерфейса DeviceIOControl, но когда дело доходит до данных или необработанных чтений, вы должны использовать предписанный метод SPTI для чтения сектора, или Windows 7 заблокирует его, с правами администратора или без, поэтому, к вашему сведению, вы можете больше не делайте так, как SCSI, если вы хотите больше совместимости!
Вот как должен выглядеть предписанный SPTI способ, и, к счастью, это гораздо меньше кода, чем создание пакета команд SCSI с использованием OxBE или READ10 (это то, что вам следовало бы использовать, если вы просто хотели получить данные сектора данных, поскольку это 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. Приведенный выше фрагмент кода будет работать для звукового сектора и возвращать 2352 байта. Это может работать вплоть до Windows NT4.0, если ваш вызов CreateFile() правильный. Но да, если вы используете IOCTL_SCSI_PASS_THROUGH_DIRECT и попытаетесь создать свой собственный командный пакет SCSI 0xBE, Windows 7 заблокирует его! Microsoft хочет, чтобы вы использовали IOCTL_CDROM_RAW_READ для необработанных чтений. Вы можете создать другие пакеты команд SCSI для чтения оглавления, получения возможностей накопителя, но команда чтения будет заблокирована, и DeviceIoControl вызовет ошибку "Недопустимая функция". По-видимому, по крайней мере для Windows 10 мое программное обеспечение снова заработало, и ограничение было снято, но поскольку Windows 7 имеет большую базу для установки пользователем, вы захотите сделать это SPTI, предписанным способом ВСЕГДА, плюс IOCTL_CDROM_RAW_READ знает еще несколько менее распространенных Команды чтения, чем универсальная 0xBE для старых странных дисков, так что лучше использовать их в любом случае!