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);
}