为什么VC编译的程序在不同系统上运行经常报错?
在XP(SP2 ?)以前,安装VC运行时库时,安装包只会将各种DLL释放到system32目录并注册相关信息到注册表,这样LoadLibrary时加载这些DLL也不会出错,因为对于应用程序来说,当前只有一个对应的运行时库被注册到系统中,不存在多个不同版本的问题,但也很容易造成兼容性问题,如使用VS2008编译的程序,在仅安装了VS2005运行时库的系统中可能会出现崩溃错误。
不同版本运行时库带来的冲突
假如一个VC2005编译的程序,一旦出现运行时库不匹配而导致运行错误,我们就需要安装匹配的VC2005运行时库到系统里就可以解决了.
但是如果问题那么容易解决就好了,不过对于这种问题如何解决,痛苦的也是微软的码农。
在上面VC2005程序问题的基础上,再扩展一下,假如同时又有一个VC2008编译的程序运行出现错误,这样我们又得安装VC2008的运行时库了,新的运行时库又会覆盖掉System32里的同名DLL,那么问题来了,原来的VC2005程序又无法运行了,崩溃!不可能每运行一次就安装一次运行时库把?
所以,微软在新的补丁中提供了一个叫Side-By-Side的模块,也就是简称SXS,该模块最初应该不是为了解决以上问题的,只是顺带而已,SXS应该是为了解决混乱的.NET Framework而设计的。但是不管怎样,有了SXS,就可以在系统中同时安装各种不同版本的运行时库也不会有冲突了。
既然系统中安装了不同版本的运行时库,那么程序怎么知道自己要使用的到底是VC2005还是VC2008的运行库呢?
微软如何设计SXS来解决不同运行环境所带来的兼容性问题的?
在XP sp3,以及Vista, WIN7,WIN8,WIN10 后,为了在系统中同时提供多种CPU平台和不同VS版本的运行时库来兼容不同的应用软件,微软做了大概下面3个改进,
1)PE文件自带依赖库签名信息
2)在C:WindowsWinSxS中存放各种不同版本的运行时库
3)有了上面的2个改进,剩下的无非就是系统运行EXE文件或者加载DLL时,如何根据依赖库签名信息来正确加载相应的运行时库了。
以上就是系统的Side-By-Side(SXS)组件设计时所要实现的大体目标和功能了。
程序开发中如何让SXS的识别不同版本依赖库?
在VS开发时,可在工具中设置manifest来指定当前程序的依赖库签名信息(运行时库名称,版本,如win32 x86 版本号 等等),manifest可以嵌入到资源中,也可以放在本地的xml格式的文本文件,目前最标准的也是VS开发工具默认做法,就是将运行时库通过导入表静态链接到PE文件(EXE或DLL),同时将manifest嵌入到PE文件的资源里,资源号是RT_MANIFEST(24),再交给系统的PE加载器去自动加载对应的运行时库。
虽然本文是说MSVCR90.DLL,但原理上是可以适用于各种各样的运行时库的。
Delphi开发时没法设置manifest资源,会出现什么问题?
注意:RAD 10已经可以设置manifest资源了
系统在创建进程时,在NTDLL和内核中有部分API和代码统称为PE加载器,在EXE使用LoadLibrary加载DLL时,内部也是一个类似的且专门映射DLL到进程的PE加载器,所以这里说加载器仅仅是一个模块概念,并不是一个实际的EXE程序。
在Delphi开发的EXE程序中,如果采用Loadlibrary去动态加载MSVCR90.DLL,在XP上大部分是正常的,在VISTA/WIN7以上可能就会报错R6034,这个错误是MSVCR90.DLL中在入口函数的运行环境自检中所报的错误,大体上是该DLL会判断当前进程的内存环境中是否包含正确的manifest,没有则报错,实际中该错误可能并不是真的因为错误版本的DLL报错的,只是该DLL为了安全起见而报错,当然真正原因是因为调用LoadLibrary的EXE程序没有嵌入包含运行时库依赖信息的manifest,所以加载器无法通过当前进程的manifest判断是否加载的DLL就是EXE所期望的正确的MSVCR90.DLL,最终报错。
Delphi中如何解决没法正确加载Manifest资源所导致的问题?
要解决该问题,就需要让加载器能够获取到正确的manifest信息,而manifest信息其实只是一个载体,实际上的使用对象是叫做ActiveContext,也就是说manifest最终会被ActiveContext加载并接管处理。而加载器也是通过ActiveContext来处理被解析映射过的manifest信息。
总体上,就是只要进程中存在正确的ActiveContext,加载器就能够取得正确的依赖库路径并加载。
文章到这里已经相对清晰了,要解决本文提到的问题,现在变成了如何创建包含manifest的ActiveContext,到这里还没有解释ActiveContext是什么,有兴趣的朋友可以查找MSDN中关于“Activation Context”的信息。
下面给出代码展示如何使用LoadLibrary加载msvcr90.dll,先看一段正常但会报错的代码:
procedure LoadMSVCRT(); var sDllName: string; begin sDllName := 'MSVCR90.DLL'; hLib := LoadLibrary(PChar(sDllName)); end;
上面这段代码在WIN7或WIN10上一执行,通常情况下就会弹出R6034的错误,而以下代码则可避免该问题
procedure LoadMSVCRT(); var sDllName: string; hLib: HModule; aActCtx: TActCtx; hCtx: THandle; lpCookie: ULONG_PTR; begin // 激活清单文件,可正确加载MSVCR90.DLL FillChar(aActCtx, SizeOf(TActCtx), 0); aActCtx.cbSize := SizeOf(TActCtx); aActCtx.lpSource := PChar(ExtractFilePath(ParamStr(0))+'MSVCRT.manifest'); hCtx := Winapi.Windows.CreateActCtx(aActCtx); if hCtx<>INVALID_HANDLE_VALUE then begin Winapi.Windows.ActivateActCtx(hCtx, lpCookie); end; sDllName := 'MSVCR90.DLL'; hLib := LoadLibrary(PChar(sDllName)); if hCtx<>INVALID_HANDLE_VALUE then begin DeactivateActCtx(0, lpCookie); ReleaseActCtx(hCtx); end; end;
以上代码所使用的manifest内容随便找一下就有,具体如下(从某个VC程序的资源中拷贝出来的):
<assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0"> <trustInfo xmlns="urn:schemas-microsoft-com:asm.v3"> <security> <requestedPrivileges> <requestedExecutionLevel level="asInvoker" uiAccess="false"></requestedExecutionLevel> </requestedPrivileges> </security> </trustInfo> <dependency> <dependentAssembly> <assemblyIdentity type="win32" name="Microsoft.VC90.CRT" version="9.0.21022.8" processorArchitecture="x86" publicKeyToken="1fc8b3b9a1e18e3b"></assemblyIdentity> </dependentAssembly> </dependency> </assembly>
注意其中的版本号,如果系统中没有安装对应的运行时库,仍然会有可能报错。这里说可能会报错,是因为系统的SXS会在找不到对应的依赖库时,会自动尝试匹配与manifest中接近的库文件(如当PE文件中的IAT无法被全部正常解析,则会尝试其他更新的依赖库,直到成功或失败)。
本文提到的方法同样适用于shellcode对PE文件加壳时需要面对的manifest处理问题,另外,manifest可以在代码中直接生成到内存,而ActiveContext除了上面以文件的加载方式外,还可以直接在内存中加载内容,具体可以查看MSDN中关于ActiveContext的使用方法。
注:
A . 关于WinSxs(即Windows Side-by-Side),更高级的操作可查找Sxs.dll相关API,号称完美解决不同版本COM和DLL兼容性问题(最初应该是解决.NET不同framework版本的兼容性问题)
B. manifest是一个范围较大的配置文件,可在以后为某些新的功能增加更多配置项,而运行时库依赖配置仅仅是其中一项