How do I execute the READ CD command on a CD-ROM on Windows?
I am working on an application that should issue raw SCSI commands on CD-ROM. I am currently struggling with sending a READ CD ( 0xBE
) command to disk and returning data from a given sector of the CD.
Consider the following code:
#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;
}
The CDB was built according to MMC-6 Revision 2g and should transfer 1 sector from LBA 1. Since I am working with CD-DA discs only, each sector is 2352 bytes, which explains why it sizeof(buf)
is 2352.
Error checking has been omitted for brevity. The debugger shows that the call DeviceIoControl
returns successfully, and ioctl_bytes
- 0x2c
and the values ββinside are sptd.s
as follows:
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
This indicates that the command was successfully executed by the drive because it ScsiStatus
is 0 ( SCSI_STATUS_GOOD
) and no sense data was returned. However, the data buffer is not written because the debugger shows that it is full with 0xcc
because the application was compiled in debug mode.
However, when I change CDB to the standard INQUIRY command like this:
const UCHAR cdb[] = { 0x12, 0, 0, 0, 36, 0 };
The buffer is correctly filled with the request data and I can read the name of the disk, vendor and everything else.
I've already tried aligning the target buffer according to the Microsoft documentation for SCSI_PASS_THROUGH_DIRECT , which says that the DataBuffer member of SCSI_PASS_THROUGH_DIRECT is a pointer to the adapter's buffer being aligned. Experimental buffer alignment to 64 bytes did not work, and an issue IOCTL_SCSI_GET_CAPABILITIES
that should return the alignment required gave me the following information:
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
This leads me to think that no alignment is required since it AlignmentMask
is 1 and that doesn't seem to be causing the problem. I wonder what AdapterUsesPio
is 1, although the device manager says otherwise.
For the record, the code below works correctly on Linux and the target buffer is filled with data from the CD. Same as on Windows, the SCSI status returned is 0 and no sense data is returned.
#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 code is compiled with Visual Studio C ++ 2010 Express and WinDDK 7600.16385.1 on Windows XP. It also runs on Windows XP.
source to share
The problem is that the CDB is not well formed, although it is valid syntactically. What I didn't see in the MMC spec was this:
The 9th byte should contain the bits used to select the type of data the disk should return. In the code in the question, I am setting it to 0, which means that I asked for "No fields" from disk. Changing this byte to 0x10
(User Data) causes both Linux and Windows to return the same data for that sector. I still don't know why Linux returned some data to the buffer even with the original CDB form.
The native CDB for the READ CD command when reading one CD-DA sector in LBA 1 should look like this:
const unsigned char cdb[] = { 0xBE, 0, 0, 0, 0, 1, 0, 0, 1, 0x10, 0, 0 };
source to share
Your BTW code will always fail on Windows 7 due to read security restrictions. You can send most SCSI commands using the DeviceIOControl API, but when it comes to data or raw reads, you have to use the prescribed SPTI method to read the sector or Windows 7 will block it with or without administrator rights, so FYI, you can no longer follow this SCSI path if you want more compatibility!
Here's what would be the SPTI-prescribed way, and luckily it is much less code than building a batch of SCSI commands using OxBE or READ10 (this is what you should have used if you just wanted data sector data like this command SCSI-1, not 0xBE, which is less compatible):
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)
);
Long story short, google around the IOCTL_CDROM_RAW_READ command. The above code snippet will work for the audio sector and return 2352 bytes. This might fall back to Windows NT4.0 if your call to CreateFile () is correct. But yes, if you use IOCTL_SCSI_PASS_THROUGH_DIRECT and try to create your own SCSI 0xBE command packet, Windows 7 will block it! Microsoft wants you to use IOCTL_CDROM_RAW_READ for raw reads. You can create other SCSI command packages to read the TOC, get disk capabilities, but the read command will be blocked and DeviceIoControl will throw an Invalid Function error. Apparently, at least for Windows 10, my software was working again and the limitation was removed, but since Windows 7 has a large user install base,you will need to do it according to the SPTI assigned ANYWAY way, and IOCTL_CDROM_RAW_READ knows a few more less common read commands than the generic 0xBE, one for old oddball drives, so it's better to use it anyway!
source to share