读取硬盘序列号

http://blog.163.com/jinfd@126/blog/static/6233227720133218314327

【注意】本文代码可以在XP系统下成功,但在 WIN7 系统中不行,因为 WIN7 对直接打开磁盘驱动器做了限制,必须要管理员授权。否则以普通用户身份运行会在 CreateFile 时返回 INVALID_HANDLE_VALUE(5:没有权限),从而无法获取硬盘序列号。如何在 WIN7 下面不需要以管理员身份运行就可以得到硬盘序列号呢,因为涉及一些软件保护的敏感信息,故暂不在此处发表。


        网上有不少相关的问题回答和博客,帖子,讲到这件事。不过我比较不满的是很多人他们都不会说自己的代码是否参考,引用,也不说原创。而且我发现他们有照搬别人的代码的嫌疑。比如说检出序列号字符串的那个辅助函数(好像叫做 ConvertToString) ,其实这个实现主要是用于颠倒字节顺序,但是它的实现方式就是合理的吗,效率上是可以的吗,代码是优美的吗?在我看来老外这里用的方法不合理。而且它把 WORD 数组先灌入一个 DWORD 数组,然后再调用 ConvertToString 这样的函数去做字节转化,在我看来完全是脱裤子放屁,闲的蛋疼。而且里面用了一个 1KB 的static 数组去存储结果,在返回这个地址,让调用方去把结果拷走,你不拷走会发生什么后果呢,你下次再调用,这个地址里的内容又变了。所以这样一个函数如果不看实现,光给你这么一个原型声明(char* ConvertToString...),是一定会让用户发生困惑的。你返给我的是个什么地址,该地址是持久的吗?需要我去释放吗?而我们中国人可倒是好,直接 Copy 过来,也不看。好一点的中国人还会大概看一下这个步骤,罗列出具体步骤,不好的就直接把代码贴上去了,他根本也不问也不管里面的代码是什么啊!而那代码我看起来不爽,所以我调试通过以后就想改成比较正确的适合别人使用的方式。

        为此,我参考了一些资料。当然,里面有的细节我还没有完全弄明白,比如 SENDCMDINPARAMS 里有一些寄存器变量,这个是输入的参数,可是要我怎么设呢,MSDN 网站也语焉不详,为此我只能照搬原来的代码,因为不明细节,所以也只能如此了。

        最早我看那个辅助函数就觉得很奇怪,为什么要调用这个辅助函数呢,后来我看了另一个中国人的博客,他指出是对返回的数据的 WORD 里面的字节序做调整。所以我才明白这个函数原来主要是把字节进行两两颠倒,好比一串连续的字节:01 23 45 67 89,必须转接成 10 32 54 76 98 这个样子。因为识别设备返回值是一个 WORD [256] 数组(512 Bytes),应用程序调用 DeviceIoControl 像设备驱动程序发送命令(识别设备),然后 DeviceIoControl 把结果填充到这个 WORD 数组。这个 WORD 数组就是驱动程序给出的识别设备的结果,具体是哪个设备,主要是由提供给 DeviceIoControl 的第一个参数(设备句柄)来甄别的,设备句柄通过 CreateFile 获得。哪么识别设备的结果是 256 个 WORD(相当于 uint16),哪么他们的含义是如何规定的呢,这个属于 ATA / ATAPI 的技术标准,是一个统一规定,内容可以搜索网络,有三份文档(PDF文档)详细给出了 ATA/ATAPI 的技术规范,当然不同的资料上你会看到里面给出的定义还略微不同,应该是历史发展的原因。也就是这个标准规定了比如 WORD 数组里哪些是硬盘的序列号等,所以你会看到代码里有通过对硬编码的 WORD 数组索引范围,获取相关信息。现在你就知道了,这些索引值,都是这些文档里的规定。ATA、ATAPI 是什么呢,英文大概是 AT Attachment with Packet Interface ,我们不去管它了。

        下面是我对网络上那些代码进行修改后得到的代码,是一个win32 console 程序。没有那么多废话,可读性,可用性比网络上的代码更好(你可以看到我整理后的代码非常简短)。当然,如果我们把硬盘序列号作为分发软件时绑定物理硬件使用,实际上就没必要再去做调整 byte order 这一步,因为你并不是为了打印和显示它们,只是关心硬件相关的数据而已。

        ----<代码如下>----

#include "stdafx.h"

#ifndef _WIN32_WINNT
#define _WIN32_WINNT  0x0501
#endif

#include <windows.h>
#include <winioctl.h>

//
BOOL GetPhyDriveSerial(LPTSTR pModelNo, LPTSTR pSerialNo);
void ToLittleEndian(PUSHORT pWords,  int nFirstIndex,  int nLastIndex, LPTSTR pBuf);
void TrimStart(LPTSTR pBuf);

int _tmain( int argc, _TCHAR* argv[])
{
    TCHAR szModelNo[ 48], szSerialNo[ 24];
     if(GetPhyDriveSerial(szModelNo, szSerialNo))
    {
        _tprintf(_T( " : 0 1 2\n"));
        _tprintf(_T( " : 012345678901234567890123456789\n"));
        _tprintf(_T( "Model No: %s\n"), szModelNo);
        _tprintf(_T( "Serial No: %s\n"), szSerialNo);
        TrimStart(szSerialNo);
        _tprintf(_T( "Serial No: %s\n"), szSerialNo);
    }
     else
    {
        _tprintf(_T( "Failed.\n"));
    }
    getchar();
     return  0;
}

//
// Model Number: 40 ASCII Chars
// SerialNumber: 20 ASCII Chars
//
BOOL GetPhyDriveSerial(LPTSTR pModelNo, LPTSTR pSerialNo)
{
     //-1是因为 SENDCMDOUTPARAMS 的结尾是 BYTE bBuffer[1];
    BYTE IdentifyResult[ sizeof(SENDCMDOUTPARAMS) + IDENTIFY_BUFFER_SIZE -  1];
    DWORD dwBytesReturned;
    GETVERSIONINPARAMS get_version;
    SENDCMDINPARAMS send_cmd = {  };

    HANDLE hFile = CreateFile(_T( "\\\\.\\PHYSICALDRIVE0"), GENERIC_READ | GENERIC_WRITE,    
        FILE_SHARE_READ | FILE_SHARE_WRITE, NULL, OPEN_EXISTING,  0, NULL);
     if(hFile == INVALID_HANDLE_VALUE)
         return FALSE;

     //get version
    DeviceIoControl(hFile, SMART_GET_VERSION, NULL,  0,
        &get_version,  sizeof(get_version), &dwBytesReturned, NULL);

     //identify device
    send_cmd.irDriveRegs.bCommandReg = (get_version.bIDEDeviceMap &  0x10)? ATAPI_ID_CMD : ID_CMD;
    DeviceIoControl(hFile, SMART_RCV_DRIVE_DATA, &send_cmd,  sizeof(SENDCMDINPARAMS) -  1,
        IdentifyResult,  sizeof(IdentifyResult), &dwBytesReturned, NULL);
    CloseHandle(hFile);

     //adjust the byte order
    PUSHORT pWords = (USHORT*)(((SENDCMDOUTPARAMS*)IdentifyResult)->bBuffer);
    ToLittleEndian(pWords,  2746, pModelNo);
    ToLittleEndian(pWords,  1019, pSerialNo);
     return TRUE;
}

//把WORD数组调整字节序为little-endian,并滤除字符串结尾的空格。
void ToLittleEndian(PUSHORT pWords,  int nFirstIndex,  int nLastIndex, LPTSTR pBuf)
{
     int index;
    LPTSTR pDest = pBuf;
     for(index = nFirstIndex; index <= nLastIndex; ++index)
    {
        pDest[ 0] = pWords[index] >>  8;
        pDest[ 1] = pWords[index] &  0xFF;
        pDest +=  2;
    }    
    *pDest =  0;
    
     //trim space at the endof string; 0x20: _T(' ')
    --pDest;
     while(*pDest ==  0x20)
    {
        *pDest =  0;
        --pDest;
    }
}

//滤除字符串起始位置的空格
void TrimStart(LPTSTR pBuf)
{
     if(*pBuf !=  0x20)
         return;

    LPTSTR pDest = pBuf;
    LPTSTR pSrc = pBuf +  1;
     while(*pSrc ==  0x20)
        ++pSrc;

     while(*pSrc)
    {
        *pDest = *pSrc;
        ++pDest;
        ++pSrc;
    }
    *pDest =  0;
}

        在代码开头,有一些宏定义了比如 _WIN32_WINNT 的版本,它们是来自 win32 程序的 stdafx.h 里,出现在包含 windows.h 等头文件之前,因为 console 程序 IDE 不会自动添加这些内容,而没有这些宏,代码中的一些结构体的定义就不会被编译到。所以我们必须手工加上这些宏定义。

        序列号开头可能有空格补充,所以我又用 TrimStart 这个辅助函数,可以去掉字符串开头的空格。
        硬件厂商应该保证,ModelNumber + SerialNumber 组合在一起是唯一标识,不会重复。
        上面的代码产生如下输出:

         :  0         1         2
         :  012345678901234567890123456789
Model  No: WDC WD1600AAJS-60M0A0
Serial No:     WD-WCAV30353688
Serial No: WD-WCAV30353688

        参考资料:
        Information Technology - AT Attachment with Packet Interface - 7(ATA/ATAPI-7) 等。

你可能感兴趣的:(读取硬盘序列号)