概述
这两天遇到一个奇怪的问题:在一个进程中,通过
GetFileVersionInfo
去获取一个绝对路径文件的版本号时, 实际路径对应文件根本不存在,却获取到了版本号信息。在仔细分析
GetFileVersionInfo
内部实现后,真相终于大白,为了以后能更好的使用这个
API
,故把分析过程记录下来。
问题背景
一进程在判断是否需要打补丁时,需要去读取文件的版本号,读取路径是从一个库中配置的。在测试的库中,配置的条件是判断
C:\Windows\system32\gdiplus.dll
的版本号小于某个补丁,就说明用户机器上面需要安装该补丁。
该进程运行后,分析到库的条件时,就会通过
GetFileVersionIno
获取
C:\Windows\system32\gdiplus.dll
模块的版本号来进行判断, 实际上在
C:\Windows\system32\
目录下面是不存在
gdiplus.dll
文件,但是
GetFileVersionInfo
这个函数却返回了成功,而且可以通过
VerQueryValue
能够查询出版本号, 从而导致扫描出的结果不符合预期。
为什么
GetFileVersionInfo
获取不存在路径的版本号信息会出错呢?
查找原因
查看
msdn,GetFileVersionInfo:
GetFileVersionInfo function
Retrieves version information for the specified file.
Syntax
BOOL WINAPI GetFileVersionInfo(
_In_ LPCTSTR lptstrFilename,
_Reserved_ DWORD dwHandle,
_In_ DWORD dwLen,
_Out_ LPVOID lpData
);
Parameters
lptstrFilename
[in]
Type:
LPCTSTR
The name of the file. If a full path is not specified
, the function uses the search sequence specified by the
LoadLibrary
function.
从
msdn
来看,如果输入不是全路径,则会通过
LoadLibrary
去搜索标识的文件名。
进一步查看
GetFileVersionInfo
内部实现:
BOOL
APIENTRY
GetFileVersionInfoA(
LPSTR lpstrFilename,
DWORD dwHandle,
DWORD dwLen,
LPVOID lpData
)
{
UNICODE_STRING FileName;
ANSI_STRING AnsiString;
NTSTATUS Status;
BOOL bStatus;
RtlInitAnsiString(&AnsiString, lpstrFilename);
Status = RtlAnsiStringToUnicodeString(&FileName, &AnsiString, TRUE);
if (!NT_SUCCESS(Status)) {
SetLastError(Status);
return FALSE;
}
<span style="color:#ff6666;"> bStatus = GetFileVersionInfoW(FileName.Buffer, dwHandle, dwLen, lpData);</span>
RtlFreeUnicodeString(&FileName);
return bStatus;
}
GetFileVersionInfoW
的实现:
BOOL
APIENTRY
GetFileVersionInfoW(
LPWSTR lpwstrFilename,
DWORD dwHandle,
DWORD dwLen,
LPVOID lpData
)
{
VERHEAD *pVerHead;
VERHEAD16 *pVerHead16;
HANDLE hMod;
HANDLE hVerRes;
HANDLE h;
UINT dwTemp;
BOOL bTruncate, rc;
UNREFERENCED_PARAMETER(dwHandle);
// Check minimum size to prevent access violations
if (dwLen < sizeof(((VERHEAD*)lpData)->wTotLen)) {
SetLastError(ERROR_INSUFFICIENT_BUFFER);
return (FALSE);
}
dwTemp = SetErrorMode(SEM_FAILCRITICALERRORS);
<span style="color:#ff0000;">hMod = LoadLibraryEx(lpwstrFilename, NULL, LOAD_LIBRARY_AS_DATAFILE);</span>
SetErrorMode(dwTemp);
if (hMod == NULL) {
// Allow 16bit stuff
__try {
dwTemp = MyExtractVersionResource16W( lpwstrFilename, &hVerRes );
} __except( EXCEPTION_EXECUTE_HANDLER ) {
dwTemp = 0 ;
}
if (!dwTemp)
return (FALSE);
if (!(pVerHead16 = GlobalLock(hVerRes))) {
SetLastError(ERROR_INVALID_DATA);
GlobalFree(hVerRes);
return (FALSE);
}
__try {
dwTemp = (DWORD)pVerHead16->wTotLen;
if ((dwTemp * 3) > dwLen) {
//
// We are forced to truncate.
//
dwTemp = dwLen/3;
bTruncate = TRUE;
} else {
bTruncate = FALSE;
}
// Now mem copy only the real size of the resource. (We alloced
// extra space for unicode)
memcpy((PVOID)lpData, (PVOID)pVerHead16, dwTemp);
if (bTruncate) {
// If we truncated above, then we must set the new
// size of the block so that we don't overtraverse.
((VERHEAD16*)lpData)->wTotLen = (WORD)dwTemp;
}
rc = TRUE;
} __except( EXCEPTION_EXECUTE_HANDLER ) {
rc = FALSE;
}
GlobalUnlock(hVerRes);
GlobalFree(hVerRes);
return rc;
}
<span style="color:#ff0000;"> if (((hVerRes = FindResource(hMod, MAKEINTRESOURCE(VS_VERSION_INFO), VS_FILE_INFO)) == NULL) ||
((pVerHead = LoadResource(hMod, hVerRes)) == NULL)) {
rc = FALSE;</span>
} else {
__try {
dwTemp = (DWORD)pVerHead->wTotLen;
if (((dwTemp * 2) + sizeof(VER2_SIG)) > dwLen) {
// We are forced to truncate.
//
// dwLen = UnicodeBuffer + AnsiBuffer.
//
// if we try to "memcpy" with "(dwLen/3) * 2" size, pVerHead
// might not have such a big data...
//
dwTemp = (dwLen / 2) - sizeof(VER2_SIG);
bTruncate = TRUE;
} else {
bTruncate = FALSE;
}
// Now mem copy only the real size of the resource. (We alloced
// extra space for ansi)
memcpy((PVOID)lpData, (PVOID)pVerHead, dwTemp);
// Store a sig between the raw data and the ANSI translation area so we know
// how much space we have available in VerQuery for ANSI translation.
*((PDWORD)((ULONG_PTR)lpData + dwTemp)) = VER2_SIG;
if (bTruncate) {
// If we truncated above, then we must set the new
// size of the block so that we don't overtraverse.
((VERHEAD*)lpData)->wTotLen = (WORD)dwTemp;
}
rc = TRUE;
} __except( EXCEPTION_EXECUTE_HANDLER ) {
rc = FALSE;
}
}
<span style="color:#ff0000;">FreeLibrary(hMod);</span>
return (rc);
}
从win2k
源码来看,
GetFileVersionInfoW
先是检测输入
BuffSize
是否足够大, 然后直接通过
LoadLibraryEx
把
lpwstrFilename
作为
DATAFILE
进行加载; 如果加载成功则通过
FindResource
查找其模块资源,然后取出资源数据。
到了这里,我们应该可以得出结论,
GetFileVersionInfo
依赖于
LoadLibraryEx
函数的执行结果。对于
Dll
的加载,最重要的是要搞清其搜索路径;到哪里去搞清?当然是问问微软了!
MSDN
上面有一篇专门讲
DLL
搜索顺序的文章(
Dynamic-Link Library Search Order
http://msdn.microsoft.com/en-us/library/ms682586(v=vs.85).aspx
)。
按照
msdn
的说法,应用程序可以通过完整路径,使用重定向,或者
manifest
来控制加载哪个
dll,
否则对于输入是相对路径文件名,其搜索路径如下:
在安全
DLL
搜索模式开启的情况下,搜索顺序是:
1
、应用程序
EXE
所在的路径。
2
、系统目录。
3
、
16
位系统目录
4
、
Windows
目录
5
、当前目录
6
、
PATH
环境变量指定的目录
如果安全
DLL
搜索模式不支持或者被禁用,那么搜索顺序是:
1
、应用程序
EXE
所在的路径。
2
、当前目录
3
、系统目录。
4
、
16
位系统目录
5
、
Windows
目录
6
、
PATH
环境变量指定的目录
安全模式是由
HKEY_LOCAL_MACHINE\System\CurrentControlSet\Control\Session Manager
\
SafeDllSearchMode
这个注册表值控制。默认情况下是开启状态。对于
Win XP
默认是关闭,可以通过建立这样的注册表值来开启。
关于安全
DLL
搜索路径可以参考(
Dynamic-Link Library Security
http://msdn.microsoft.com/en-us/library/ff919712(v=vs.85).aspx
)
根据前面的分析: 该场景下,我们传递的是一个路径
C:\Windows\system32\gdiplus.dll,
只有可能是使用了
dll
重定向,加载到了重定向目录的
dll.
直接写测试程序,加载
C:\Windows\System32\gdiplus.dll,
而该目录下面并不存在
GdiPlus.dll;
按常理应该加载失败;但它却直接读取的是
C:\Windows\winsxs
下面的文件
那如果C:\Windows\System32\
目录下存在
gdiplus.dll
呢,拷贝一个模块到该目录,重命名为
gdiplus.dll,
它仍然是加载的
C:\Windows\winsxs
目录下面的
gdiplus.dll
:
再次测试了把
gdiplus.dll
放在
C:\windows
目录下,加载
C:\Windows\gdiplus.dll,
能够成功加载到
C:\Windows\gdiplus.dll,
把
gdiplud.dll
放到应用程序的
exe
同目录,加载
D:\Data\testLoad\Debug\gdiplus.dll
成功加载到了
D:\Data\testLoad\Debug\gdiplus.dll
的模块。但是直接加载
gdiplus.dll
,则加载到了
C
:
\Windows\winsxs
目录下面的
gdiplus.dll
。
总结下测试结果:
1. 采用
full path
加载
dll,
如果是加载
C:\Windows\system32
目录下面,则会重定向去加载
C:\Windows\winsxs
目录下面的
gdiplus.dll
,其他情况都能正确加载到给定路径的文件。
2. 采用相对文件名
gdiplus.dll
加载
,
尽管
exe
目录下面有
gdiplus.dll
,也会加载到
C:\Windows\winsxs
目录下面的
gdiplus.dll.
也就是说,
GetFileVersionInfo
能够获取不存在的路径
C:\Windows\System32\gdiplus.dll
的版本号是因为触发了
DLL
加载的重定向机制,实际获取的是
C:\Windows\winsxs
目录下面对应文件的信息。
这个被称为
side by side Assembly, msdn
有一篇文章介绍(
side-by-side component
http://msdn.microsoft.com/en-us/library/dd408052(v=vs.85).aspx
),
这个东东的作用就是为了解决
以前
windows
上的“
Dll
地狱”(参考附录参考链接)
问题才产生的新的
DLL
管理解决方案。大家知道,
Dll
是动态加载共享库,同一个
Dll
可能被多个程序所使用,而所谓“
Dll
地狱”就是当不同程序依赖的
Dll
相同,但版本不同时,由于系统不能分辨到底哪个是哪个,所以加载错了
Dll
版本,然后就挂了。于是盖茨就吸取了教训,搞了一个程序集清单的东东,每个程序都要有一个清单,这个清单存在和自己应用程序同名的
.manifest
文件中,里面列出其所需要的所有依赖,这儿所列出的依赖可不是简单地靠文件明来区分的,而是根据一种叫做“强文件名”的东西区分的。
具体细节这里就不详细描述了, 直接参考链接就可以了。
总结
1. GetFileVersionInfo
内部实现是通过
LoadLibrary
加载对应模块,并获取其
Resource
的信息来获取文件信息。
2. LoadLibrary
加载
DLL
时会受到
DLL
重定向的影响,也就是说,如果确实需要判断系统盘
\System32\
目录的会重定向的文件的版本时,不能通过
GetFileVersionInfo
去获取版本信息,因为获取的是重定向路径的
dll
版本信息。只能采用其他方式获取。
附录
Dynamic-Link Library Search Order :
http://msdn.microsoft.com/en-us/library/ms682586(v=vs.85).aspx
)。
http://msdn.microsoft.com/en-us/library/dd408052(v=vs.85).aspx
DLL HELL
及解决办法:
Supported Microsoft Side-by-side Assemblies
http://msdn.microsoft.com/en-us/library/aa376609(v=vs.85).aspx
Activation Contexts
http://msdn.microsoft.com/en-us/library/windows/desktop/aa374153(v=vs.85).aspx