Microsoft Windows 2000 应用程序兼容性
Kyle Marsh
Microsoft Corporation
1999 年 11 月
摘要:讨论使应用程序在 Microsoft(R) Windows(R) 2000 上存在不兼容性的几个问题。其中有以下几部分:
介绍
设置和安装问题
Windows 2000 兼容性问题
应用程序稳定性问题
Windows 平台之间的差异
介绍
几个月来,我一直从事一项任务,即找出 Windows 2000 操作系统中的应用程序兼容性问题。在这里我真正要讨论的是,造成应用程序与 Windows 2000 不兼容的原因。没有人真正关心使应用程序兼容的原因。
我一直在与 Windows 2000 测试组合作,他们在过去的几个月中已测试了数百个应用程序。我们已将应用程序在 Windows 2000 上正常或不正常运行的原因进行书面论述。我们发现的问题可以归为四类:
无法在 Windows 2000 上安装的应用程序。 这是迄今我们发现的最大问题。应用程序在 Windows 2000 上安装的方式并无甚特殊之处;问题是这些应用程序不让自己安装到这一新版本的操作系统中。
我们对操作系统所做的、影响应用程序运行的更改。每当 Microsoft Windows NT(R) 开发组面临选择,是使系统作为平台更稳定或更强大,还是保障应用程序的兼容性,他们总是牺牲后者而取稳定性。Windows 2000 开发工作的一个主要目标就是让系统作为平台更加稳定。遗憾的是,为了实现这一点而必须进行的某些改动,已导致应用程序在 Windows 2000 上不兼容。
我们已对操作系统进行的更改不会影响应用程序的兼容性,但会中断某些应用程序。
过于依赖 Windows 9x 平台的应用程序。我们在开发 Windows 2000 时,考虑到有众多 Windows 9x 用户需要升级,因此对 Windows 9x 应用程序进行了测试,将它们移植到 Windows 2000 中。我们发现某些应用程序过于依赖 Windows 9x。
设置和安装问题
我们要讨论的第一类问题是设置和安装问题;最常见的问题无疑是无法在 Windows 2000 上安装应用程序。实际上,导致无法安装应用程序的一个最普遍的原因,在于 Windows 2000 是 Windows NT 的 5.0 版。
测试组以多种方式测试应用程序。他们将应用程序安装在基于 Windows 2000 的系统中,或者将应用程序安装在 Windows NT 4.0 或 Windows 95 中,然后再将系统升级到 Windows 2000,以便进行测试。
我们拿来一台未安装任何操作系统的机器后,安装上 Windows 2000,再安装应用程序,与上述升级的情况相比,前者的兼容性数目要少得多。
版本检查
造成应用程序无法安装在 Windows 2000 上的第一位原因,是它们无法正确处理版本号。我们发现很多应用程序都进行以下示例代码所做的操作。它们在运行过程中会调用 GetVersionEX,然后写下一条“if”语句,该语句规定:“如果系统是版本 3,因为没有新的 Shell,我不能正常运行,所以我可能无法安装。如果系统是版本 4,我可以进行安装和设置”。问题出在如果系统是版本 5,这一“if”语句就没有了下文。因为版本号是 5.0,这些应用程序由于自身原因而无法安装,所以我们发现了一系列这样那样的问题。
if (osvi.dwMajorVersion == 3)
{
// 请这样做
}
else if (osvi.dwMajorVersion == 4)
{
// 请那样做
}
测试组继续寻找解决方案,并蒙蔽了许多此类应用程序。在早期的编译中,我们能够采取措施改变 GetVersionEx 的返回值。我们可以改变其返回值,欺骗应用程序,告诉它版本号就是 4.0,然后程序就能够继续安装,并正常运行。但有部分应用程序的设计思想就是不能安装在 Windows 2000 上。对于病毒扫描程序或其他低级实用程序来说,受限于某一操作系统版本是可以理解的。不过,这些应用程序会显示消息来说明这一点。我们查找的是那些不能安装或无法正常运行、又根本没有通知用户的应用程序。
怎样才能正确地检查版本号?在 Windows 2000 中我们将添加一个新的 API: VerifyVersionInfo,这一 API 在运行时将依次检查主版本号、次版本号以及服务包。如果出现了操作系统的新版本,应用程序仍然能够在其上安装并运行。实际上应用 VerifyVersionInfo 的选项和方式还有很多,但如果只是检查“要是操作系统升级了,应用程序该如何处理”这一类问题,您只需调用这三个标志,然后检查主版本号、次版本号以及服务包。您能够定义以下语句:“我的程序需要运行在 Windows NT 4.0、SP2 上”,然后询问 VerifyVersionInfo“我正在运行的操作系统是否已达到这一标准?”,该 API 将返回真值或假值。
VerifyVersionInfo(&osvi,
VER_MAJORVERSION |
VER_MINORVERSION |
VER_SERVICEPACKMAJOR,
dwlConditionMask);
采用这一方式检查版本,就可以符合 Windows 2000 应用程序的规范,其基本思想是“只要存在新版本的操作系统,就要在新版本上进行安装。”
应用 VerifyVersionInfo 的一个问题是目前该 API 只能在 Windows 2000 平台上运行。为了检查 Windows 95 等旧平台的版本,您必须应用GetVersionEx。查看以下示例代码,即可发现它的功能与 VerifyVersionInfo 基本相同:依次检查主版本号、次版本号以及服务包。
BOOL bIsWindowsVersionOK(DWORD dwMajor, DWORD dwMinor, DWORD dwSPMajor )
{
OSVERSIONINFO osvi;
// 初始化 OSVERSIONINFO 结构
//
ZeroMemory(&osvi, sizeof(OSVERSIONINFO));
osvi.dwOSVersionInfoSize = sizeof(OSVERSIONINFO);
GetVersionEx((OSVERSIONINFO*)&osvi);
// 首先,主版本
if ( osvi.dwMajorVersion > dwMajor )
return TRUE;
else if ( osvi.dwMajorVersion == dwMajor )
{
// 然后,次版本
if (osvi.dwMinorVersion > dwMinor )
return TRUE;
else if (osvi.dwMinorVersion == dwMinor )
{
// 对,最好检查一下 Service Pack
if ( dwSPMajor && osvi.dwPlatformId == VER_PLATFORM_WIN32_NT )
{
HKEY hKey;
DWORD dwCSDVersion;
DWORD dwSize;
BOOL fMeetsSPRequirement = FALSE;
if (RegOpenKeyEx(HKEY_LOCAL_MACHINE,
"System\\CurrentControlSet\\Control\\Windows", 0,
KEY_QUERY_VALUE, &hKey) == ERROR_SUCCESS)
{
dwSize = sizeof(dwCSDVersion);
if (RegQueryValueEx(hKey, "CSDVersion",
NULL, NULL, (unsigned char*)&dwCSDVersion,
&dwSize) == ERROR_SUCCESS)
{
fMeetsSPRequirement = (LOWORD(dwCSDVersion) >= dwSPMajor);
}
RegCloseKey(hKey);
}
return fMeetsSPRequirement;
}
return TRUE;
}
}
return FALSE;
}
//
// 此示例适用于 Windows 2000 和更新版本
//
BOOL bIsWindowsVersionOK(DWORD dwMajor, DWORD dwMinor, DWORD dwSPMajor )
{
OSVERSIONINFOEX osvi;
ZeroMemory(&osvi, sizeof(OSVERSIONINFOEX));
osvi.dwOSVersionInfoSize = sizeof(OSVERSIONINFOEX);
osvi.dwMajorVersion = dwMajor;
osvi.dwMinorVersion = dwMinor;
osvi.wServicePackMajor = dwSPMajor;
// 设置条件掩码。
VER_SET_CONDITION( dwlConditionMask, VER_MAJORVERSION, VER_GREATER_EQUAL );
VER_SET_CONDITION( dwlConditionMask, VER_MINORVERSION, VER_GREATER_EQUAL );
VER_SET_CONDITION( dwlConditionMask, VER_SERVICEPACKMAJOR, VER_GREATER_EQUAL );
// 执行测试。
return VerifyVersionInfo(&osvi,
VER_MAJORVERSION | VER_MINORVERSION
| VER_SERVICEPACKMAJOR,dwlConditionMask);
}
首先,需要检查主版本号。如果当前操作系统的主版本号高于所需主版本号,则不需要进行任何检查,直接向下运行即可。如果主版本号相等,则以同样方式检查次版本号。最后再检查服务包号。在我们获得发布的某一版本时,就可以说“没关系,我们不在乎是 Service Pack 3、4 还是 5”。如果主版本号或次版本号有所增长,系统同样可以处理。我们查找了所有要检查版本信息的应用程序,发现它们将分别检查每一组件。它们在运行中会说“噢,主版本号是 5,我只要求 4,不错。次版本号是 .0,很好,但 Service Pack 是 0,我需要的是 3”。很明显,Windows 2000 还没有推出 SP3,所以这种检查版本信息的方法是错误的。
检查 Windows NT 的版本号时需要注意以下细节:检查版本号和服务包信息的方法有三种。第一种是获取 GetVersionEx 的返回值,然后检查“szCSDVersion”字符串。实际的服务包号嵌入在该字符串的某一位置。分析这一串字符实在是比较繁琐,而且您必须始终牢记它是否进行了本地化,这是很难做到的。这不是一种最好的方式。如果系统运行的是 Windows NT 4.0,您只需检查以下注册键值,其中有一个数字是服务包号。提取这一数字,并进行一个“等于”或“大于”比较即可:
HCLM\System\CurrentControlSet\Control\Windows\CSDVersion
如果您运行的是 Windows 2000 或更高版本,则仍可以使用 GetVersionEx,但需要将其传递到 OSVERSIONINFOEX 结构,而不是正在使用的OSVERSIONINFO 结构。考虑到起始处操作员成员的大小,Windows 2000 会将其视为一个较大的结构,并且给出一个新的字段(服务包、主版本、次版本),作为进行比较时所用的整数。如果您还是使用 VerifyVersionInfo,您会发现这是最方便的一种方式。
DLL 版本检查
在检查 Windows 版本的同时,我们还发现了另一个与版本相关的问题,即用户不检查 DLL 的版本。无论 DLL 是系统目录中的 Microsoft DLL,还是您自己的 DLL,在将其复制到系统前,都必须进行版本检查。检查的目的是防止将旧版的 DLL 复制到新版的上面。
必须确认在用户自己的 DLL 中已添加了版本信息,以便进行检查。进行这一操作非常重要,它能够避免出现各种麻烦。不要试图更改系统目录中的 DLL,甚至不要考虑对系统 DLL 进行升级或降级,或覆盖同一 DLL。系统 DLL 是由 Windows 2000 在 CD 或服务包中提交的、位于系统目录中的 DLL。
如果您打算编制新的 Windows 2000 应用程序,则需要使用 Windows Installer,它能够为您检查 DLL 的版本。只要说明需要将某一特定的 DLL 置于特定的目录中,即可发现 Windows Installer 在为您进行版本检查工作。您不需要再处理这一小块代码。
DLL Hell
如果没有正确地检查 DLL 版本,结果毋庸质疑会发生 DLL Hell。我肯定不需要向您解释什么是 DLL Hell,您一定在这上面花费了不少时间。因为我最初曾参与开发 ctl3d.dll,对这些东西就象熟悉我的邻居一样。
我们花费了几年的时间,努力去突破那些影响 DLL 正常工作的障碍,我们最终认定应用程序开发商无法保持后向兼容性。每个人都在朝这方面努力,这一目标很伟大,但却无法实现。实际上,DLL 不可能保持后向兼容性。结果就是:某一应用程序如果要正常结束,必须取决于某一 DLL 的某个特定版本,而另一应用程序则取决于该 DLL 的另一版本,因为这两个应用程序在这一共享组件(即共享 DLL)方面发生了冲突,导致无法共存于同一系统中。
到目前为止,我们仍然认为对于 Windows 应用程序来说,DLL 共享功能是一个非常良好和重要的组成部分。我们同样认为 DLL 能为您提供的功能还是非常有价值的。问题是如果不能测试 DLL 的版本,而跨应用程序对 DLL 进行全局共享会带来非常多的问题。
并行 DLL
在 Windows 2000 中,我们开始实施某些称之为并行 DLL 的内容。我们希望您开发的应用程序也开始向并行版本策略靠拢。在 Windows 2000 中,我们采取了一些预防性措施,以减少 DLL Hell。我们要考虑的第一位的事情是无论安装了何种应用程序,都要保证系统处于受保护状态,保持其完整性。随后我们将讨论 Windows 文件保护。
我们要做的另一件事情是实现组件的并行,同时希望应用程序供应商也这样做。我们针对的是微软的组件;至于您自己的组件,我们也建议您这样做。我们将采用并行版本功能,而对于您自己目前正在进行全局共享的组件,我们也希望您能采用并行版本功能。
系统稳定性
Windows NT 小组正在努力进行的另一项工作是确保系统保持长时间的稳定。微软允许通过分发服务包的形式将各种功能吸纳到 Windows NT 中,这是一个问题。安装了 Windows NT 的客户和公司在选取服务包时已非常警惕,因为这些东西绝不仅仅是在更正某些问题。通常情况下它会作出某些更正,然后说“噢,我们在这里添加了这个特性,在那里添加了那个功能”,这些东西使得系统无法达到所预期的稳定性。
按照常规策略,服务包只能包含对错误的更正,不能有其他内容。如果我们认为操作系统中需要新增某些重要的特性和功能,我们将推出 Windows 2000 的 .x 版本。您会得到类似 Windows NT 5.1、5.2 等此类内容,另外还有针对 Windows NT 的三个不同版本发布的服务包。我们将继续努力保持每一平台功能的完整性,其中甚至涉及到 QFE、错误检查和 hot fix。每次有了新的功能集或新特性,都会发布新版操作系统。
我们在微软所进行的最后一项工作是确保了解随同产品发布了哪些组件,我们强烈建议您遵照执行。我们正在尽最大可能地减少不同产品中发布的组件的数量。如果某一特定组件需要与另一特定组件协同工作,我们会尽量将这两个组件一同发布。对于所有这些能够重新分发的组件,我们将定出发布的结构顺序。
并行 DLL
如果需要将组件由全局共享组件或 DLL 更改为新的并行 DLL,需要对 DLL 进行某些改动。这种对 DLL 自身的改动是必须的。您必须得声明:“我所设计的组件将允许同时运行多个版本。”
为了使某一组件成为真正的并行组件,首先需要对 DLL 进行重命名,并且更改可能存在于 OCX 控件、COM 对象中的所有 GUID。这种重命名的工作只需进行一次,就能保证您获得一个并行运行的新 DLL,该 DLL 将不再是全局共享。
DLL 被重命名后,应用程序会将其安装到自主管理的目录中,而不会安装到系统目录。这样,应用程序开发人员就可以说:“我已对带有这一 DLL 的特定版本的产品进行了全面测试,而且我还确认在我再次进行测试之前,这一 DLL 不会进行升级。”这就给了做为开发员的您足够的信心:任何人无法使用共享组件扰乱您的应用程序,导致系统崩溃并将我们带回到 DLL Hell。
如果您是作为用户使用这些组件,您可以在自己的目录(而不是系统目录)中注册一个相对路径。这样会加载一个落在系统某处的本地版本,而不是全局副本。
我们对 LoadLibrary 功能进行了修改,从而确保:如果应用程序以相对路径注册了一个组件,我们也始终以相对路径完成加载,而不管这一组件是位于系统目录中,还是运行在别的什么地方。由此可以确保您获得用以测试应用程序的那一份副本。
隔离的应用程序
我们还修改了 LoadLibrary 代码,以便支持 DLL 重定向。由此管理员可以将 DLL 的加载过程重定向到某一位置,并由本地目录加载 DLL。经过这一处理后,您的 DLL 就可以处于隔离状态。假设某一大单位的某个人要测试他们能否采用您的应用程序,他们安装了另一应用程序,然后测试这两者能否协同工作。他们发现结果是不能,管理员就开始查找这两个应用程序在何处,在哪一组件上发生了冲突。找到这一组件后,管理员从同时使用这两个程序的雇员的角度进行了考虑,提取 DLL(或包含对象的 OCX),并将其置于应用程序所在的目录。然后管理员创建了一个名为 foo.exe 的文件,其后又加上 .local。如果调用 LoadLibrary,LoadLibrary 发现这里有一个 foo.exe.local 文件,它会首先加载应用程序目录中的文件,而不会考虑应用程序用于 LoadLibrary 调用本身的特定路径。这种方式有助于人们区分需要同一组件的不同版本的多个应用程序,使所有这些应用程序运行于同一系统中。
Windows 文件保护
为了确保系统的稳定性和平台的可靠性,第一步就是保障系统不会遇到任何 DLL Hell 问题。我们希望无论发生了什么事情,系统仍然能够运行,能够引导,即用户可以对系统的稳定性有充分的信心。
有了 Windows 文件保护 (WFP),如果应用程序试图更改某一系统文件,Windows 2000 会将其恢复原状。对部分功能,应用程序会安装并说:“瞧,我需要这个 DLL 的新版本…”去实现某一功能,或根本这就是一个错误的应用程序,不会正确地执行版本检查功能。Windows 2000 将检查这一点,会发现文件已改动。如果 Windows 2000 发现这是一个系统文件,并声明“我不允许改动这一文件”,它会将文件恢复原状。
如果要升级那些已被系统锁定的文件,只能采用 Windows NT 小组所发放的几种文件替换机制:服务包、QFE 或 hot fix。它们能够实现对系统文件的替换,而其他应用程序却不能。
举个例子,mfc42.dll 是我们锁定的一个文件。通过语言组自身将不能再升级该 DLL,只有 Windows NT 小组能够更改系统中的这一文件。如果 C 程序设计人员需要升级他们的 DLL(而且假定他们希望在 Windows 2000 上市之后而下一版的 Windows NT 发布之前进行升级),只能采用并行的组件版本功能。
大多数 *.sys、*.dll、*.exe 和 *.ocx 文件以及几个字体文件在保护之列。
如此还存在几个兼容性问题。首先,防病毒程序必须认识并正确处理 Windows 文件保护功能,再在此基础上进行应用程序的备份和恢复;不能简单地对这些文件进行复制、备份和恢复。因为您没有系统所支持的文件替换机制,如果您这样做,将取消 Windows 文件保护功能。
为了防止人们进行这类操作,我们在系统中添加了几个 API。
WFP API
第一个 API 是 SFCGetNextProtectedFile。可用这一 API 可以获得所有受保护或能保护的文件的清单。您可以以一个空值开始重复调用这一 API,以获得受保护文件的列表。
BOOL WINAPI SfcGetNextProtectedFile
(IN HANDLE RpcHandle,IN PPROTECTED_FILE_DATA ProtFileData );
//
// 此功能将列出受保护文件
//
void ListProtectedFiles(HWND hWnd)
{
HWND hwndList;
PROTECTED_FILE_DATA pfd;
int iCount;
char szFileName[260];
int iLen;
RECT rt;
hwndList = GetWindow(hWnd,GW_CHILD);
if ( hwndList == NULL )
{
GetClientRect(hWnd, &rt);
// 第一次创建“列表”控件
hwndList = CreateWindow("LISTBOX", NULL,
WS_CHILD | WS_VISIBLE | LBS_STANDARD | LBS_NOINTEGRALHEIGHT |
LBS_USETABSTOPS,
0,20,rt.right,rt.bottom-40,
hWnd,
NULL,
hInst,
NULL);
}
else
SendMessage(hwndList, LB_RESETCONTENT, 0, 0);
ZeroMemory(&pfd,sizeof(PROTECTED_FILE_DATA));
iCount = 0;
while ( g_pfnSfcGetNextProtectedFile(NULL, &pfd) != 0 )
{
// 为此“ANSI 应用程序”将 WCHAR 转换到 ANSI
iLen = WideCharToMultiByte(CP_ACP,NULL,pfd.FileName, wcslen(pfd.FileName),
szFileName,260,NULL,NULL);
szFileName[iLen] = '\0';
SendMessage(hwndList, LB_ADDSTRING, 0, (LPARAM)szFileName);
iCount++;
}
}
另一个更为直接的 API 是 SfcIsFileProtected。该 API 能更为便捷地为绝大多数应用程序直接调用,并回答以下问题:“看看这一文件,它是否受到保护?”但是请记住,它需要指向这一文件的完整路径。您不能只是简单地指定 NTS.sys,而是需要给出到达 NTS.sys 所处位置的路径。如果您将这一文件名传递给 API,它会说:“是的,这个文件已受到保护”,或“这不是一个受保护的文件”。在您进行任何备份或恢复操作时都需要使用该 API。如果您希望进行任何安装设置,或可能会更新某一系统文件,您都可以调用这一 API。如果您希望将某一目标文件置于系统目录中,在进行这一操作之前,您需要调用这一 API,以避免取消 Windows 文件保护功能。下一版的 Windows Installer(与 Windows 2000 一同发布)将在复制文件之前进行检查,因此不会意外地启动 WFP。
BOOL WINAPI SfcIsFileProtected (IN HANDLE RpcHandle,IN LPCWSTR ProtFileName);
//
// 此函数使用“文件”打开对话框,以便从用户获取文件名并检查其是否受保护。
void CheckFileForProtection(HWND hWnd)
{
OPENFILENAME OpenFileName;
CHAR szFile[MAX_PATH] = "\0";
CHAR szSystem32[MAX_PATH];
strcpy( szFile, "");
ZeroMemory(&OpenFileName, sizeof(OPENFILENAME));
// 填充 OPENFILENAME 结构以支持模板和挂接。
OpenFileName.lStructSize = sizeof(OPENFILENAME);
OpenFileName.hwndOwner = hWnd;
OpenFileName.hInstance = hInst;
OpenFileName.lpstrFile = szFile;
OpenFileName.nMaxFile = sizeof(szFile);
OpenFileName.lpstrTitle = "Select a File";
OpenFileName.Flags = OFN_FILEMUSTEXIST;
if (g_pfnSHGetFolderPath != NULL )
g_pfnSHGetFolderPath(NULL, CSIDL_SYSTEM, NULL, NULL, szSystem32);
else
szSystem32[0] = '\0';
OpenFileName.lpstrInitialDir = szSystem32;
// 调用公共对话函数。
if (GetOpenFileName(&OpenFileName))
{
// 检查文件
WCHAR wzFileName[260];
int iLen;
iLen = MultiByteToWideChar(CP_ACP,NULL,szFile, strlen(szFile), wzFileName, 260);
wzFileName[iLen] = '\0';
if (g_pfnSfcIsFileProtected(NULL, wzFileName) == TRUE )
{
MessageBox(hWnd,"Is Protected", szFile, MB_OK);
}
else
MessageBox(hWnd,"Is NOT Protected", szFile, MB_OK);
}