I’m trying to send ATA commands to a physical disk in Windows, and get the response from the device.
Note: In this case I want to send the IDENTIFY DEVICE
(0xEC)
command. The device will respond with
a 512-byte block of data. (In
particular I’m interested in bit 0 of
word 119 - the device’s support for
the TRIM
command).
I know that I need to use CreateFile
to open the device:
handle = CreateFile(
"\\.\PhysicalDrive0", GENERIC_READ, FILE_SHARE_READ,
nil, // no security attributes
OPEN_EXISTING,
0, // flags and attributes
nil // no template file
);
But after this I’m stymied about what to do.
I thought about sending 0xEC
using [DeviceIoControl][4]
:
// const ATACommand_IdentifyDevice = 0xEC;
uint bytesReturned = 0;
DeviceIoControl(handle,
0xEC, // IO Control Code
nil, // input buffer not needed
0, // input buffer is zero bytes
@buffer, // output buffer to store the returned 512-bytes
512, // output buffer is 512 bytes long
out bytesReturned,
nil // not an overlapped operation
);
But this is completely wrong. An IoControlCode sent to DeviceIoControl must be a valid IO_CTL, which are built using the macro:
#define CTL_CODE(DeviceType, Function, Method, Access) (
((DeviceType) << 16) | ((Access) << 14) | ((Function) << 2) | (Method)
)
Looking at the SDK, there are a number of valid Disk Management Control Codes, e.g.:
- IOCTL_DISK_CREATE_DISK
- IOCTL_DISK_GET_DRIVE_GEOMETRY
- IOCTL_DISK_GET_DRIVE_GEOMETRY_EX
- IOCTL_DISK_GET_PARTITION_INFO
- IOCTL_STORAGE_QUERY_PROPERTY
But none of them are IDENTIFY DEVICE
command, or return anything it returns.
So I believe I have to use some “raw” method of sending commands.
Searching around, I came across and undocumented IOCTL
#define DFP_RECEIVE_DRIVE_DATA 0x0007c088
Which when you break down the IOCTL pieces, means:
Custom: (0)
Device Type: (7) FILE_DEVICE_DISK
Required Access: (3) METHOD_NEITHER
Custom: (0)
Function Code: (34)
Transfer Type: (0)
But there is no documentation anywhere on what the inputBuffer
must contain, its size, and what its outputBuffer
will contain, or its required. Nor can I figure out what functionCode
34 (0x22) is.
My question: How do I send raw ATA commands (e.g. 0xEC) to an ATA device, and read its response?
See also
- IOCTL_ATA_PASS_THROUGH Control Code
- IOCTL_ATA_PASS_THROUGH_DIRECT Control Code
- ATA_PASS_THROUGH_EX Structure
Answer pieces
Open the drive with ReadWrite access:
handle = CreateFile(
"\\.\PhysicalDrive0",
GENERIC_READ or GENERIC_WRITE, // IOCTL_ATA_PASS_THROUGH requires read-write
FILE_SHARE_READ,
nil, // no security attributes
OPEN_EXISTING,
0, // flags and attributes
nil // no template file
);
Setup an ATA_PASS_THROUGH_EX
structure as our input buffer to use with IOCTL_ATA_PASS_THROUGH
IO control code:
ATA_PASS_THROUGH_EX inputBuffer;
inputBuffer.Length = sizeof(ATA_PASS_THROUGH_EX);
inputBuffer.AtaFlags = ATA_FLAGS_DATA_IN;
inputBuffer.DataTransferLength = 0;
inputBuffer.DataBufferOffset = 0;
// todo: put the ATA command (e.g. 0xEC) somewhere
uint inputBufferSize = sizeof(ATA_PASS_THROUGH_EX);
Setup an output buffer to hold our expected 512-byte response from the drive:
Byte[] outputBuffer = new Byte[512];
uint outputBufferSize = 512;
Call DeviceIoControl
:
int ioControlCode = IOCTL_ATA_PASS_THROUGH; // or maybe IOCTL_ATA_PASS_THROUGH_DIRECT
uint bytesReturned = 0;
DeviceIoControl(handle, ioControlCode,
inputBuffer, inputBufferSize,
outputBuffer, outputBufferSize,
out bytesReturned,
nil // not an overlapped operation
);
Close the file handle:
handle.Close();
You need to use IOCTL_ATA_PASS_THROUGH/IOCTL_ATA_PASS_THROUGH_DIRECT, these are quite well documented. Also, you need GENERIC_READ|GENERIC_WRITE access for CreateFile.
Be aware that pre XP SP2 does not support these properly. Also, if you have a nForce based MB with nvidia drivers, your SATA drives will appear as SCSI and you can't use this passthrough.
In some cases, the SMART IOCTL's (e.g. SMART_RCV_DRIVE_DATA) will work on nForce drivers. You can use these to get IDENTIFY and SMART data, but not much else.
The open source smartmontools is a good place to start looking for sample code.
EDIT: Sample from an app talking to ATA devices.
EResult DeviceOperationManagerWin::executeATACommandIndirect(ATACommand & Cmd) {
const uint32 FillerSize = 0;
Utils::ByteBuffer B;
B.reserve(sizeof(ATA_PASS_THROUGH_EX) + 4 + Cmd.bufferSize());
ATA_PASS_THROUGH_EX & PTE = * (ATA_PASS_THROUGH_EX *) B.appendPointer(sizeof(ATA_PASS_THROUGH_EX) + FillerSize + Cmd.bufferSize());
uint8 * DataPtr = ((uint8 *) &PTE) + sizeof(ATA_PASS_THROUGH_EX) + FillerSize;
memset(&PTE, 0, sizeof(ATA_PASS_THROUGH_EX) + FillerSize);
PTE.Length = sizeof(PTE);
PTE.AtaFlags = 0;
PTE.AtaFlags |= Cmd.requiresDRDY() ? ATA_FLAGS_DRDY_REQUIRED : 0;
switch (Cmd.dataDirection()) {
case ddFromDevice:
PTE.AtaFlags |= ATA_FLAGS_DATA_IN;
break;
case ddToDevice:
PTE.AtaFlags |= ATA_FLAGS_DATA_OUT;
memcpy(DataPtr, Cmd.buffer(), Cmd.bufferSize());
break;
default:
break;
}
PTE.AtaFlags |= Cmd.is48Bit() ? ATA_FLAGS_48BIT_COMMAND : 0;
PTE.AtaFlags |= Cmd.isDMA() ? ATA_FLAGS_USE_DMA : 0;
PTE.DataTransferLength = Cmd.bufferSize();
PTE.TimeOutValue = Cmd.timeout();
PTE.DataBufferOffset = sizeof(PTE) + FillerSize;
PTE.DataTransferLength = Cmd.bufferSize();
PTE.CurrentTaskFile[0] = Cmd.taskFileIn0().Features;
PTE.CurrentTaskFile[1] = Cmd.taskFileIn0().Count;
PTE.CurrentTaskFile[2] = Cmd.taskFileIn0().LBALow;
PTE.CurrentTaskFile[3] = Cmd.taskFileIn0().LBAMid;
PTE.CurrentTaskFile[4] = Cmd.taskFileIn0().LBAHigh;
PTE.CurrentTaskFile[5] = Cmd.taskFileIn0().Device;
PTE.CurrentTaskFile[6] = Cmd.taskFileIn0().Command;
PTE.CurrentTaskFile[7] = 0;
if (Cmd.is48Bit()) {
PTE.PreviousTaskFile[0] = Cmd.taskFileIn1().Features;
PTE.PreviousTaskFile[1] = Cmd.taskFileIn1().Count;
PTE.PreviousTaskFile[2] = Cmd.taskFileIn1().LBALow;
PTE.PreviousTaskFile[3] = Cmd.taskFileIn1().LBAMid;
PTE.PreviousTaskFile[4] = Cmd.taskFileIn1().LBAHigh;
PTE.PreviousTaskFile[5] = Cmd.taskFileIn1().Device;
PTE.PreviousTaskFile[6] = 0;
PTE.PreviousTaskFile[7] = 0;
}
DWORD BR;
if (!DeviceIoControl(FHandle, IOCTL_ATA_PASS_THROUGH, &PTE, B.size(), &PTE, B.size(), &BR, 0)) {
FLastOSError = GetLastError();
LOG_W << "ioctl ATA_PT failed for " << Cmd << ": " << FLastOSError << " (" << Utils::describeOSError(FLastOSError) << ")";
return Utils::mapOSError(FLastOSError);
}
Cmd.taskFileOut0().Error = PTE.CurrentTaskFile[0];
Cmd.taskFileOut0().Count = PTE.CurrentTaskFile[1];
Cmd.taskFileOut0().LBALow = PTE.CurrentTaskFile[2];
Cmd.taskFileOut0().LBAMid = PTE.CurrentTaskFile[3];
Cmd.taskFileOut0().LBAHigh = PTE.CurrentTaskFile[4];
Cmd.taskFileOut0().Device = PTE.CurrentTaskFile[5];
Cmd.taskFileOut0().Status = PTE.CurrentTaskFile[6];
Cmd.taskFileOut1().Error = PTE.PreviousTaskFile[0];
Cmd.taskFileOut1().Count = PTE.PreviousTaskFile[1];
Cmd.taskFileOut1().LBALow = PTE.PreviousTaskFile[2];
Cmd.taskFileOut1().LBAMid = PTE.PreviousTaskFile[3];
Cmd.taskFileOut1().LBAHigh = PTE.PreviousTaskFile[4];
Cmd.taskFileOut1().Device = PTE.PreviousTaskFile[5];
Cmd.taskFileOut1().Status = PTE.PreviousTaskFile[6];
if (Cmd.dataDirection() == ddFromDevice) {
memcpy(Cmd.buffer(), DataPtr, Cmd.bufferSize());
}
return resOK;
}
EDIT: Sample without external dependencies.
IDENTIFY requires a 512 byte buffer for data:
unsigned char Buffer[512 + sizeof(ATA_PASS_THROUGH_EX)] = { 0 };
ATA_PASS_THROUGH_EX & PTE = *(ATA_PASS_THROUGH_EX *) Buffer;
PTE.Length = sizeof(PTE);
PTE.TimeOutValue = 10;
PTE.DataTransferLength = 512;
PTE.DataBufferOffset = sizeof(ATA_PASS_THROUGH_EX);
Set up the IDE registers as specified in ATA spec.
IDEREGS * ir = (IDEREGS *) PTE.CurrentTaskFile;
ir->bCommandReg = 0xEC;
ir->bSectorCountReg = 1;
IDENTIFY is neither 48-bit nor DMA, it reads from the device:
PTE.AtaFlags = ATA_FLAGS_DATA_IN | ATA_FLAGS_DRDY_REQUIRED;
Do the ioctl:
DeviceIOControl(Handle, IOCTL_ATA_PASS_THROUGH, &PTE, sizeof(Buffer), &PTE, sizeof(Buffer), &BR, 0);
Here you should insert error checking, both from DeviceIOControl and by looking at IDEREGS for device reported errors.
Get the IDENTIFY data, assuming you have defined a struct IdentifyData
IdentifyData * IDData = (IdentifyData *) (Buffer + sizeof(ATA_PASS_THROUGH_EX));
Based on the answer https://stackoverflow.com/a/5071027/15485 by Erik I wrote the following self-contained code. I tested it on a DELL laptop with an SSD disk and running Windows 7.
// Sending ATA commands directly to device in Windows?
// https://stackoverflow.com/questions/5070987/sending-ata-commands-directly-to-device-in-windows
#include <Windows.h>
#include <ntddscsi.h> // for ATA_PASS_THROUGH_EX
#include <iostream>
// I have copied the struct declaration from
// "IDENTIFY_DEVICE_DATA structure" http://msdn.microsoft.com/en-us/library/windows/hardware/ff559006(v=vs.85).aspx
// I think it is better to include the suitable header (MSDN says the header is Ata.h and suggests to include Irb.h)
typedef struct _IDENTIFY_DEVICE_DATA {
struct {
USHORT Reserved1 :1;
USHORT Retired3 :1;
USHORT ResponseIncomplete :1;
USHORT Retired2 :3;
USHORT FixedDevice :1;
USHORT RemovableMedia :1;
USHORT Retired1 :7;
USHORT DeviceType :1;
} GeneralConfiguration;
USHORT NumCylinders;
USHORT ReservedWord2;
USHORT NumHeads;
USHORT Retired1[2];
USHORT NumSectorsPerTrack;
USHORT VendorUnique1[3];
UCHAR SerialNumber[20];
USHORT Retired2[2];
USHORT Obsolete1;
UCHAR FirmwareRevision[8];
UCHAR ModelNumber[40];
UCHAR MaximumBlockTransfer;
UCHAR VendorUnique2;
USHORT ReservedWord48;
struct {
UCHAR ReservedByte49;
UCHAR DmaSupported :1;
UCHAR LbaSupported :1;
UCHAR IordyDisable :1;
UCHAR IordySupported :1;
UCHAR Reserved1 :1;
UCHAR StandybyTimerSupport :1;
UCHAR Reserved2 :2;
USHORT ReservedWord50;
} Capabilities;
USHORT ObsoleteWords51[2];
USHORT TranslationFieldsValid :3;
USHORT Reserved3 :13;
USHORT NumberOfCurrentCylinders;
USHORT NumberOfCurrentHeads;
USHORT CurrentSectorsPerTrack;
ULONG CurrentSectorCapacity;
UCHAR CurrentMultiSectorSetting;
UCHAR MultiSectorSettingValid :1;
UCHAR ReservedByte59 :7;
ULONG UserAddressableSectors;
USHORT ObsoleteWord62;
USHORT MultiWordDMASupport :8;
USHORT MultiWordDMAActive :8;
USHORT AdvancedPIOModes :8;
USHORT ReservedByte64 :8;
USHORT MinimumMWXferCycleTime;
USHORT RecommendedMWXferCycleTime;
USHORT MinimumPIOCycleTime;
USHORT MinimumPIOCycleTimeIORDY;
USHORT ReservedWords69[6];
USHORT QueueDepth :5;
USHORT ReservedWord75 :11;
USHORT ReservedWords76[4];
USHORT MajorRevision;
USHORT MinorRevision;
struct {
USHORT SmartCommands :1;
USHORT SecurityMode :1;
USHORT RemovableMediaFeature :1;
USHORT PowerManagement :1;
USHORT Reserved1 :1;
USHORT WriteCache :1;
USHORT LookAhead :1;
USHORT ReleaseInterrupt :1;
USHORT ServiceInterrupt :1;
USHORT DeviceReset :1;
USHORT HostProtectedArea :1;
USHORT Obsolete1 :1;
USHORT WriteBuffer :1;
USHORT ReadBuffer :1;
USHORT Nop :1;
USHORT Obsolete2 :1;
USHORT DownloadMicrocode :1;
USHORT DmaQueued :1;
USHORT Cfa :1;
USHORT AdvancedPm :1;
USHORT Msn :1;
USHORT PowerUpInStandby :1;
USHORT ManualPowerUp :1;
USHORT Reserved2 :1;
USHORT SetMax :1;
USHORT Acoustics :1;
USHORT BigLba :1;
USHORT DeviceConfigOverlay :1;
USHORT FlushCache :1;
USHORT FlushCacheExt :1;
USHORT Resrved3 :2;
USHORT SmartErrorLog :1;
USHORT SmartSelfTest :1;
USHORT MediaSerialNumber :1;
USHORT MediaCardPassThrough :1;
USHORT StreamingFeature :1;
USHORT GpLogging :1;
USHORT WriteFua :1;
USHORT WriteQueuedFua :1;
USHORT WWN64Bit :1;
USHORT URGReadStream :1;
USHORT URGWriteStream :1;
USHORT ReservedForTechReport :2;
USHORT IdleWithUnloadFeature :1;
USHORT Reserved4 :2;
} CommandSetSupport;
struct {
USHORT SmartCommands :1;
USHORT SecurityMode :1;
USHORT RemovableMediaFeature :1;
USHORT PowerManagement :1;
USHORT Reserved1 :1;
USHORT WriteCache :1;
USHORT LookAhead :1;
USHORT ReleaseInterrupt :1;
USHORT ServiceInterrupt :1;
USHORT DeviceReset :1;
USHORT HostProtectedArea :1;
USHORT Obsolete1 :1;
USHORT WriteBuffer :1;
USHORT ReadBuffer :1;
USHORT Nop :1;
USHORT Obsolete2 :1;
USHORT DownloadMicrocode :1;
USHORT DmaQueued :1;
USHORT Cfa :1;
USHORT AdvancedPm :1;
USHORT Msn :1;
USHORT PowerUpInStandby :1;
USHORT ManualPowerUp :1;
USHORT Reserved2 :1;
USHORT SetMax :1;
USHORT Acoustics :1;
USHORT BigLba :1;
USHORT DeviceConfigOverlay :1;
USHORT FlushCache :1;
USHORT FlushCacheExt :1;
USHORT Resrved3 :2;
USHORT SmartErrorLog :1;
USHORT SmartSelfTest :1;
USHORT MediaSerialNumber :1;
USHORT MediaCardPassThrough :1;
USHORT StreamingFeature :1;
USHORT GpLogging :1;
USHORT WriteFua :1;
USHORT WriteQueuedFua :1;
USHORT WWN64Bit :1;
USHORT URGReadStream :1;
USHORT URGWriteStream :1;
USHORT ReservedForTechReport :2;
USHORT IdleWithUnloadFeature :1;
USHORT Reserved4 :2;
} CommandSetActive;
USHORT UltraDMASupport :8;
USHORT UltraDMAActive :8;
USHORT ReservedWord89[4];
USHORT HardwareResetResult;
USHORT CurrentAcousticValue :8;
USHORT RecommendedAcousticValue :8;
USHORT ReservedWord95[5];
ULONG Max48BitLBA[2];
USHORT StreamingTransferTime;
USHORT ReservedWord105;
struct {
USHORT LogicalSectorsPerPhysicalSector :4;
USHORT Reserved0 :8;
USHORT LogicalSectorLongerThan256Words :1;
USHORT MultipleLogicalSectorsPerPhysicalSector :1;
USHORT Reserved1 :2;
} PhysicalLogicalSectorSize;
USHORT InterSeekDelay;
USHORT WorldWideName[4];
USHORT ReservedForWorldWideName128[4];
USHORT ReservedForTlcTechnicalReport;
USHORT WordsPerLogicalSector[2];
struct {
USHORT ReservedForDrqTechnicalReport :1;
USHORT WriteReadVerifySupported :1;
USHORT Reserved01 :11;
USHORT Reserved1 :2;
} CommandSetSupportExt;
struct {
USHORT ReservedForDrqTechnicalReport :1;
USHORT WriteReadVerifyEnabled :1;
USHORT Reserved01 :11;
USHORT Reserved1 :2;
} CommandSetActiveExt;
USHORT ReservedForExpandedSupportandActive[6];
USHORT MsnSupport :2;
USHORT ReservedWord1274 :14;
struct {
USHORT SecuritySupported :1;
USHORT SecurityEnabled :1;
USHORT SecurityLocked :1;
USHORT SecurityFrozen :1;
USHORT SecurityCountExpired :1;
USHORT EnhancedSecurityEraseSupported :1;
USHORT Reserved0 :2;
USHORT SecurityLevel :1;
USHORT Reserved1 :7;
} SecurityStatus;
USHORT ReservedWord129[31];
struct {
USHORT MaximumCurrentInMA2 :12;
USHORT CfaPowerMode1Disabled :1;
USHORT CfaPowerMode1Required :1;
USHORT Reserved0 :1;
USHORT Word160Supported :1;
} CfaPowerModel;
USHORT ReservedForCfaWord161[8];
struct {
USHORT SupportsTrim :1;
USHORT Reserved0 :15;
} DataSetManagementFeature;
USHORT ReservedForCfaWord170[6];
USHORT CurrentMediaSerialNumber[30];
USHORT ReservedWord206;
USHORT ReservedWord207[2];
struct {
USHORT AlignmentOfLogicalWithinPhysical :14;
USHORT Word209Supported :1;
USHORT Reserved0 :1;
} BlockAlignment;
USHORT WriteReadVerifySectorCountMode3Only[2];
USHORT WriteReadVerifySectorCountMode2Only[2];
struct {
USHORT NVCachePowerModeEnabled :1;
USHORT Reserved0 :3;
USHORT NVCacheFeatureSetEnabled :1;
USHORT Reserved1 :3;
USHORT NVCachePowerModeVersion :4;
USHORT NVCacheFeatureSetVersion :4;
} NVCacheCapabilities;
USHORT NVCacheSizeLSW;
USHORT NVCacheSizeMSW;
USHORT NominalMediaRotationRate;
USHORT ReservedWord218;
struct {
UCHAR NVCacheEstimatedTimeToSpinUpInSeconds;
UCHAR Reserved;
} NVCacheOptions;
USHORT ReservedWord220[35];
USHORT Signature :8;
USHORT CheckSum :8;
} IDENTIFY_DEVICE_DATA, *PIDENTIFY_DEVICE_DATA;
// Taken from smartmontools
// Copies n bytes (or n-1 if n is odd) from in to out, but swaps adjacents
// bytes.
static void swapbytes(char * out, const char * in, size_t n)
{
for (size_t i = 0; i < n; i += 2) {
out[i] = in[i+1];
out[i+1] = in[i];
}
}
// Taken from smartmontools
// Copies in to out, but removes leading and trailing whitespace.
static void trim(char * out, const char * in)
{
// Find the first non-space character (maybe none).
int first = -1;
int i;
for (i = 0; in[i]; i++)
if (!isspace((int)in[i])) {
first = i;
break;
}
if (first == -1) {
// There are no non-space characters.
out[0] = '\0';
return;
}
// Find the last non-space character.
for (i = strlen(in)-1; i >= first && isspace((int)in[i]); i--)
;
int last = i;
strncpy(out, in+first, last-first+1);
out[last-first+1] = '\0';
}
// Taken from smartmontools
// Convenience function for formatting strings from ata_identify_device
void ata_format_id_string(char * out, const unsigned char * in, int n)
{
bool must_swap = true;
#ifdef __NetBSD__
/* NetBSD kernel delivers IDENTIFY data in host byte order (but all else is LE) */
// TODO: Handle NetBSD case in os_netbsd.cpp
if (isbigendian())
must_swap = !must_swap;
#endif
char tmp[65];
n = n > 64 ? 64 : n;
if (!must_swap)
strncpy(tmp, (const char *)in, n);
else
swapbytes(tmp, (const char *)in, n);
tmp[n] = '\0';
trim(out, tmp);
}
int main(int argc, char* argv[])
{
HANDLE handle = ::CreateFileA(
"\\\\.\\PhysicalDrive0",
GENERIC_READ | GENERIC_WRITE, //IOCTL_ATA_PASS_THROUGH requires read-write
FILE_SHARE_READ,
0, //no security attributes
OPEN_EXISTING,
0, //flags and attributes
0 //no template file
);
if ( handle == INVALID_HANDLE_VALUE ) {
std::cout << "Invalid handle\n";
}
// IDENTIFY command requires a 512 byte buffer for data:
const unsigned int IDENTIFY_buffer_size = 512;
const BYTE IDENTIFY_command_ID = 0xEC;
unsigned char Buffer[IDENTIFY_buffer_size + sizeof(ATA_PASS_THROUGH_EX)] = { 0 };
ATA_PASS_THROUGH_EX & PTE = *(ATA_PASS_THROUGH_EX *) Buffer;
PTE.Length = sizeof(PTE);
PTE.TimeOutValue = 10;
PTE.DataTransferLength = 512;
PTE.DataBufferOffset = sizeof(ATA_PASS_THROUGH_EX);
// Set up the IDE registers as specified in ATA spec.
IDEREGS * ir = (IDEREGS *) PTE.CurrentTaskFile;
ir->bCommandReg = IDENTIFY_command_ID;
ir->bSectorCountReg = 1;
// IDENTIFY is neither 48-bit nor DMA, it reads from the device:
PTE.AtaFlags = ATA_FLAGS_DATA_IN | ATA_FLAGS_DRDY_REQUIRED;
DWORD BR = 0;
BOOL b = ::DeviceIoControl(handle, IOCTL_ATA_PASS_THROUGH, &PTE, sizeof(Buffer), &PTE, sizeof(Buffer), &BR, 0);
if ( b == 0 ) {
std::cout << "Invalid call\n";
}
IDENTIFY_DEVICE_DATA * data = (IDENTIFY_DEVICE_DATA *) (Buffer + sizeof(ATA_PASS_THROUGH_EX));
// Nota Bene: I think some endianness control is needed
char model[40+1];
ata_format_id_string(model, data->ModelNumber, sizeof(model)-1);
char serial[20+1];
ata_format_id_string(serial, data->SerialNumber, sizeof(serial)-1);
char firmware[8+1];
ata_format_id_string(firmware, data->FirmwareRevision, sizeof(firmware)-1);
std::cout << "ModelNumber: " << model << "\n";
std::cout << "SerialNumber: " << serial << "\n";
std::cout << "FirmwareRevision: " << firmware << "\n";
return 0;
}
You need IOCTL_ATA_PASS_THROUGH Control Code