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 = {
0 };
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,
27,
46, pModelNo);
ToLittleEndian(pWords,
10,
19, 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) 等。