GetFileVersionInfo 获取文件版本信息错误原因分析

  • Post author:
  • Post category:其他


概述

这两天遇到一个奇怪的问题:在一个进程中,通过

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

)。



side-by-side component



http://msdn.microsoft.com/en-us/library/dd408052(v=vs.85).aspx

DLL HELL

及解决办法:



http://wenku.baidu.com/link?url=zqy12i40ZhJOAmyWqBKWGy-nOiDtKQeWC6FdeLAt-Rc6ifRg44HP7iYOkyHNAeU5ZeZ_poGpvv5n7W_VSAeYvu_HsqL-4M6mJqzNMdemcnW

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



版权声明:本文为feix713555原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。