How to read the manufacturer’s serial number from a Hard Drive with Delphi — and without WMI

It’s surprisingly a non-trivial issue to retrieve the manufacturer’s information from a hard drive. Some people recommend using the GetVolumeInformation API function. The problem is that this is not the serial number printed on the hard drive and assigned by the manufacturer, but is (according to the MSDN documentation):

This function returns the volume serial number that the operating system assigns when a hard disk is formatted. To programmatically obtain the hard disk’s serial number that the manufacturer assigns, use the Windows Management Instrumentation (WMI) Win32_PhysicalMedia property SerialNumber.

Notice also that it recommends to use the WMI Win32_Physical Media property “SerialNumber” to obtain the manufacturer’s serial. Interestingly enough, this won’t work 100% of the time. The reason is that this property is optionally defined by the manufacturer and must be supplied by the hard disk driver.

So if we can’t use GetVolumeInformation() and we can’t rely on WMI 100% of the time, what can we do? Well, it involves querying several sources of information on a system directly. It also means we have to care if the drive is SATA, IDE, etc. Well, the smart folks at WinSim Inc., wore an open-source application called DiskId32:

DiskId32 is a Win32 console utility for reading the manufacturer’s information from your hard drives. It runs under Windows 9X, Win NT, Win 2K and Win XP (Pro/Home). DiskId32 does not run on plain old DOS and I highly doubt that it works on Windows 3.x. It does NOT require administrator rights for Win NT/2K.

I have seen this application recommended on many occasions, although there are other solutions that use similar methods.

There is an open-source application called DiskID32 that is written in C++. Looking at the source code, it takes many scenarios in mind when attempting to retrieve disk information. It also works on Vista and Windows 7. You can find full history on the application and it’s change-log here: http://www.winsim.com/diskid32/diskid32.html

Of course, I don’t expect you to convert it to Delphi. I found someone that has already done that. However, if you download the code from his website, you will have to fix 1 error in “hwid_impl.pas” on line 1504. The BROKEN CODE is:

while ((HardDriveSerialNumber[ip] <> Chr(0)) and (ip < 1024)) do begin if ('-' = HardDriveSerialNumber[ip]) then continue;

The problem is that the counter variable ip is not incremented when a dash is detected in the serial number. Result is an endless loop.. You would need to change it to:

while ((HardDriveSerialNumber[ip] <> Chr(0)) and (ip < 1024)) do
begin
   if ('-' = HardDriveSerialNumber[ip]) then
   begin
      inc(ip);
      continue;
   end;
end;
About these ads

5 comments

  1. function GetIdeDiskSerialNumber: string;
    type
      TSrbIoControl = packed record
        HeaderLength: ULONG;
        Signature: array[0..7] of Char;
        Timeout: ULONG;
        ControlCode: ULONG;
        ReturnCode: ULONG;
        Length: ULONG;
      end;
      SRB_IO_CONTROL = TSrbIoControl;
      PSrbIoControl = ^TSrbIoControl;
    
      TIDERegs = packed record
        bFeaturesReg: Byte; // Used for specifying SMART "commands".
        bSectorCountReg: Byte; // IDE sector count register
        bSectorNumberReg: Byte; // IDE sector number register
        bCylLowReg: Byte; // IDE low order cylinder value
        bCylHighReg: Byte; // IDE high order cylinder value
        bDriveHeadReg: Byte; // IDE drive/head register
        bCommandReg: Byte; // Actual IDE command.
        bReserved: Byte; // reserved for future use. Must be zero.
      end;
      IDEREGS = TIDERegs;
      PIDERegs = ^TIDERegs;
    
      TSendCmdInParams = packed record
        cBufferSize: DWORD; // Buffer size in bytes
        irDriveRegs: TIDERegs; // Structure with drive register values.
        bDriveNumber: Byte; // Physical drive number to send command to (0,1,2,3).
        bReserved: array[0..2] of Byte; // Reserved for future expansion.
        dwReserved: array[0..3] of DWORD; // For future use.
        bBuffer: array[0..0] of Byte; // Input buffer.
      end;
      SENDCMDINPARAMS = TSendCmdInParams;
      PSendCmdInParams = ^TSendCmdInParams;
    
      TIdSector = packed record
        wGenConfig: Word;
        wNumCyls: Word;
        wReserved: Word;
        wNumHeads: Word;
        wBytesPerTrack: Word;
        wBytesPerSector: Word;
        wSectorsPerTrack: Word;
        wVendorUnique: array[0..2] of Word;
        sSerialNumber: array[0..19] of Char;
        wBufferType: Word;
        wBufferSize: Word;
        wECCSize: Word;
        sFirmwareRev: array[0..7] of Char;
        sModelNumber: array[0..39] of Char;
        wMoreVendorUnique: Word;
        wDoubleWordIO: Word;
        wCapabilities: Word;
        wReserved1: Word;
        wPIOTiming: Word;
        wDMATiming: Word;
        wBS: Word;
        wNumCurrentCyls: Word;
        wNumCurrentHeads: Word;
        wNumCurrentSectorsPerTrack: Word;
        ulCurrentSectorCapacity: ULONG;
        wMultSectorStuff: Word;
        ulTotalAddressableSectors: ULONG;
        wSingleWordDMA: Word;
        wMultiWordDMA: Word;
        bReserved: array[0..127] of Byte;
      end;
      PIdSector = ^TIdSector;
    
    const
      IDE_ID_FUNCTION = $EC;
      IDENTIFY_BUFFER_SIZE = 512;
      DFP_RECEIVE_DRIVE_DATA = $0007C088;
      IOCTL_SCSI_MINIPORT = $0004D008;
      IOCTL_SCSI_MINIPORT_IDENTIFY = $001B0501;
      DataSize = sizeof(TSendCmdInParams) - 1 + IDENTIFY_BUFFER_SIZE;
      BufferSize = SizeOf(SRB_IO_CONTROL) + DataSize;
      W9xBufferSize = IDENTIFY_BUFFER_SIZE + 16;
    var
      hDevice: THandle;
      cbBytesReturned: DWORD;
      pInData: PSendCmdInParams;
      pOutData: Pointer; // PSendCmdInParams;
      Buffer: array[0..BufferSize - 1] of Byte;
      srbControl: TSrbIoControl absolute Buffer;
    
      procedure ChangeByteOrder(var Data; Size: Integer);
      var
        ptr: PChar;
        i: Integer;
        c: Char;
      begin
        ptr := @Data;
        for i := 0 to (Size shr 1) - 1 do
        begin
          c := ptr^;
          ptr^ := (ptr + 1)^;
          (ptr + 1)^ := c;
          Inc(ptr, 2);
        end;
      end;
    
    begin
      Result := '';
      FillChar(Buffer, BufferSize, #0);
      if Win32Platform = VER_PLATFORM_WIN32_NT then
      begin // Windows NT, Windows 2000
        // Get SCSI port handle
        hDevice := CreateFile('\\.\Scsi0:', GENERIC_READ or GENERIC_WRITE,
          FILE_SHARE_READ or FILE_SHARE_WRITE, nil, OPEN_EXISTING, 0, 0);
        if hDevice = INVALID_HANDLE_VALUE then
          Exit;
        try
          srbControl.HeaderLength := SizeOf(SRB_IO_CONTROL);
          System.Move('SCSIDISK', srbControl.Signature, 8);
          srbControl.Timeout := 2;
          srbControl.Length := DataSize;
          srbControl.ControlCode := IOCTL_SCSI_MINIPORT_IDENTIFY;
          pInData := PSendCmdInParams(PChar(@Buffer) + SizeOf(SRB_IO_CONTROL));
          pOutData := pInData;
          with pInData^ do
          begin
            cBufferSize := IDENTIFY_BUFFER_SIZE;
            bDriveNumber := 0;
            with irDriveRegs do
            begin
              bFeaturesReg := 0;
              bSectorCountReg := 1;
              bSectorNumberReg := 1;
              bCylLowReg := 0;
              bCylHighReg := 0;
              bDriveHeadReg := $A0;
              bCommandReg := IDE_ID_FUNCTION;
            end;
          end;
          if not DeviceIoControl(hDevice, IOCTL_SCSI_MINIPORT, @Buffer,
            BufferSize, @Buffer, BufferSize, cbBytesReturned, nil) then
            Exit;
        finally
          CloseHandle(hDevice);
        end;
      end
      else
      begin // Windows 95 OSR2, Windows 98
        hDevice := CreateFile('\\.\SMARTVSD', 0, 0, nil, CREATE_NEW, 0, 0);
        if hDevice = INVALID_HANDLE_VALUE then
          Exit;
        try
          pInData := PSendCmdInParams(@Buffer);
          pOutData := PChar(@pInData^.bBuffer);
          with pInData^ do
          begin
            cBufferSize := IDENTIFY_BUFFER_SIZE;
            bDriveNumber := 0;
            with irDriveRegs do
            begin
              bFeaturesReg := 0;
              bSectorCountReg := 1;
              bSectorNumberReg := 1;
              bCylLowReg := 0;
              bCylHighReg := 0;
              bDriveHeadReg := $A0;
              bCommandReg := IDE_ID_FUNCTION;
            end;
          end;
          if not DeviceIoControl(hDevice, DFP_RECEIVE_DRIVE_DATA, pInData,
            SizeOf(TSendCmdInParams) - 1, pOutData, W9xBufferSize,
            cbBytesReturned, nil) then
            Exit;
        finally
          CloseHandle(hDevice);
        end;
      end;
      with PIdSector(PChar(pOutData) + 16)^ do
      begin
        ChangeByteOrder(sSerialNumber, SizeOf(sSerialNumber));
        SetString(Result, sSerialNumber, SizeOf(sSerialNumber));
      end;
    end;
    how to use:
    
    var
      s: string;
      rc: DWORD;
    begin
      s := GetIdeDiskSerialNumber;
      if s = '' then
      begin
        rc := GetLastError;
        if rc = 0 then
          WriteLn('IDE drive is not support SMART feature')
        else
          WriteLn(SysErrorMessage(rc));
      end
      else
        WriteLn('Disk serial number: ''', s, '''');
    end.
    

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s