[譯者]:本文譯自 Alex Tilles 在 Windows Developer Network (2003 第12期)發表的一篇文章:“Writing Your Own Install and Uninstall Code”。這是一篇具有一定技術含量的文章,相信許多開發人員都需要本文介紹的技術,其中包括幾個重要的技術點:
Rundll32.exe 實用程序的使用方法;
DLL 或 EXE 的自刪除技術;
嵌入資源的處理技巧;
LZCOPY API 使用示范;
compress.exe,expand.exe 使用說明;
摘要
當我在編寫“What To Do”程序(這是作者編寫的一個應用程序,小巧玲珑,很實用——譯者注)時,就想寫一個自己的安裝和卸載代碼,主要目的是想隨心所欲地控制整個安裝/卸載過程中用戶所看到的畫面。本文我們就來討論如何利用自刪除的動態鏈接庫(DLL)實現自刪除的可執行程序,從而實現程序的安裝/卸載。相信很多朋友在編寫 Windows 程序時都想這麼做,本文還將展示一些非常有用的相關技術,一定讓你大開眼界......
實現自刪除卸載程序的難點
編寫卸載程序最具挑戰性的部分是如何讓卸載程序在刪除完目標程序文件和相關目錄之後自己刪除自己。此外,卸載程序還必須能在所有 Windows 操作系統平台(Windows 9x、Windows NT、Windows 2000、Windows XP.....)上運行,不需要用戶下載任何附加組件。我在網上搜索了一番,找到一些相關的資料介紹如何自刪除可執行程序文件,但是大多數所建議的解決方案都存在一個問題,那就是只能在某個版本的 Windows 上工作。有些方法通過修改線程屬性來實現,這樣做一般都會導致定時問題。還有一些方法運行時出現嚴重錯誤,根本就不能用。我琢磨著尋求一種更好的解決方法來實現可執行程序的自刪除功能:用自刪除的 DLL 實現自刪除的可執行程序,從而突破上述諸方法的局限。
實用程序 rundll32.exe 介紹
從所周知,DLL的代碼通常需要先加載到內存之後才能執行,那麼如何執行某個DLL導出的代碼而不用創建加載和調用該 DLL 的 EXE 文件呢?方法如下:從 Windows 95 開始的每個 Windows 操作系統版本都附帶一個系統實用程序:rundll32.exe。利用它可以象下面這樣執行某些 DLL(但不是所有)輸出的任何函數:
rundll32.exe DllName,ExportedfnName args
ExportedfnName 是DLL輸出的函數名。在編寫供 rundll32 使用的 DLL時,可以象下面這樣來聲明輸出函數:
extern "C" __declspec(dllexport) void CALLBACK FunctionName (
HWND hwnd,
HINSTANCE hInstance,
LPTSTR lpCmdLine,
int nCmdShow
)
{ ... }
rundll32.exe 根據函數參數列表對函數進行調用,但根據經驗,實際上用得上的參數值只有一個,那就是 lpCmdLine,該參數接收運行 rundll32.exe 時傳入的參數值;__declspec(dllexport)的目的是輸出函數;extern "C" 使輸出的函數名有修飾符,如:_FunctionName@16 (函數名中被強制包含函數參數的大小,詳細信息請參見 MSDN 中有關DLL輸出函數調用規范說明)。rundll32.exe 加載指定的 DLL 並調用通過 args 參數傳入的 lpCmdLine 的值指定的輸出函數。有關 rundll32.exe 的正式文檔參見 MSDN 庫相關資料(Q164787):
http://support.microsoft.com/default.aspx?scid=kb;en-us;164787
實現能自刪除的 DLL
下面是實現自刪除DLL的示范代碼:
#include <windows.h>
HMODULE g_hmodDLL;
extern "C" BOOL WINAPI DllMain(HINSTANCE hinstDLL, DWORD reason, LPVOID)
{
if (reason == DLL_PROCESS_ATTACH)
g_hmodDLL = hinstDLL;
return TRUE;
}
extern "C" __declspec(dllexport) void CALLBACK MagicDel(HWND,
HINSTANCE,
LPTSTR lpCmdLine,
int)
{
// 延時2秒
Sleep(2000);
// 刪除創建該進程的可執行文件
DeleteFile(lpCmdLine);
// 刪除DLL自己
char filenameDLL[MAX_PATH];
GetModuleFileName(g_hmodDLL, filenameDLL, sizeof(filenameDLL));
__asm
{
lea eax, filenameDLL
push 0
push 0
push eax
push ExitProcess
push g_hmodDLL
push DeleteFile
push FreeLibrary
ret
}
}
上面這段代碼首先刪除某個文件,然後自刪除。DllMain 是DLL的入口函數,當首次加載動態鏈接庫時該函數被調用,此時將模塊句柄賦值給全局變量 g_hmodDLL,以便梢後使用它來獲取 DLL 本身的文件名。在 MagicDel 函數中,lpCmdLine 是DLL要刪除的可執行文件的名稱(如:卸載程序的文件名)。要刪除它很容易——用 Sleep 做一個延時,以便可執行程序的進程有時間退出並調用 DeleteFile。為了掌握 MagicDel 的實現細節,你可以將可執行程序的進程句柄傳給MagicDel並在調用 DeleteFile 之前做一個等待,看看會發生什麼?
要讓 DLL 進行自刪除需要一點訣竅。rundll32 調用 LoadModule 將 DLL 加載到它的地址空間。如果 DLL 函數可以返回的話,rundll32 將會退出,從而導致 DLL 被釋放(不是被刪除)。為了解決這個問題,我們可以執行下面的代碼:
FreeLibrary(DLL module handle);
DeleteFile(DLL filename);
ExitProcess(0);
MagicDel 函數是不能按這樣的順序進行直接調用的,因為 FreeLibary 會使代碼頁無效。為此, MagicDel 采用將等效的匯編指令壓入堆棧,然後執行它們,後跟一個 ret 指令,最後調用 ExitProccess 以防止進程繼續往下執行。我參考 Gary Nebbit 在 Windows 開發雜志(WDJ)“Tech Tips”欄目發表的文章編寫了一個匯編代碼塊。如果你用 Visual Studio 以默認選項生成DLL,最終的二進制文件大約為 40K。由於我們打算將 DLL 作為可執行程序的資源,它的體積越小越好,為此,我們必須對它進行瘦身處理。思路是將無用的 C 運行時代碼從DLL中刪除掉,具體方法如下:
本文例子使用 Visual Studio.NET 2003 中文版編譯生成 DLL,先設置項目的編譯/鏈接選項:
項目(P)| [項目名稱] 屬性(P)... | 鏈接器 | 輸入 | 忽略所有默認庫:是(/NODEFAULTLIB),此設置將 /NODEFAULTLIB 選項傳給鏈接器以便過濾掉運行時代碼。
由於 DLL 入口點(Entry Point)通常是由運行時庫提供(默認為 DllMain),所以完成上述第一步設置之後,還必須顯式地將 DLL入口點設置為 DllMain:
項目(P)| [項目名稱] 屬性(P)... | 鏈接器 | 高級 | 入口點:DllMain。
如果此時編譯生成 DLL,編譯器會報如下兩個 無法解析的外部符號( unresolved externals ) 錯誤:
error LNK2019: 無法解析的外部符號 ___security_cookie ,該符號在函數 _MagicDel@16 中被引用
解決方法是進行下一步設置。
error LNK2019: 無法解析的外部符號 @__security_check_cookie@4 ,該符號在函數 _MagicDel@16 中被引用
項目(P)| [項目名稱] 屬性(P)... | C/C++ | 代碼生成 | 緩沖區安全檢查:否,
該設置不會將 /GS 標志傳給編譯器,從而擺脫 unresolved externals 錯誤。
好了,現在編譯生成 DLL,最終的 DLL 大小為 3K,實際的文件大小只有 2.5K。
實現能自刪除的可執行程序
這裡所用的主要思路是將一個能自刪除的 DLL 作為資源保存在擬實現自刪除的可執行程序中,然後在需要時重新創建它,同時,啟動一個 rundll32.exe 進程實現刪除行為。
下面是用於將DLL存儲為資源的頭文件和資源文件。資源類型值只要大於 256 都可以,這是為用戶定義類型預留的。此外還有一種可選方法是將 DLL 二進制文件以字節數組的形式直接存儲在源中:
在資源中包含一個文件
// SelfDelete.h
#define RC_BINARYTYPE 256
#define ID_MAGICDEL_DLL 100
// SelfDelete.rc
#include "SelfDelete.h"
ID_MAGICDEL_DLL RC_BINARYTYPE MagicDel.dll
下面是可執行程序關鍵代碼:
#include <windows.h>
#include "SelfDelete.h"
void WriteResourceToFile(HINSTANCE hInstance,
int idResource,
char const *filename)
{
// 存取二進制資源
HRSRC hResInfo = FindResource(hInstance, MAKEINTRESOURCE(idResource),
MAKEINTRESOURCE(RC_BINARYTYPE));
HGLOBAL hgRes = LoadResource(hInstance, hResInfo);
void *pvRes = LockResource(hgRes);
DWORD cbRes = SizeofResource(hInstance, hResInfo);
// 將二進制資源寫到文件
HANDLE hFile = CreateFile(filename, GENERIC_WRITE, 0, 0, CREATE_ALWAYS,
FILE_ATTRIBUTE_NORMAL, 0);
DWORD cbWritten;
WriteFile(hFile, pvRes, cbRes, &cbWritten, 0);
CloseHandle(hFile);
}
void SelfDelete(HINSTANCE hInstance)
{
WriteResourceToFile(hInstance, ID_MAGICDEL_DLL, "magicdel.dll");
// 生成命令行
// 1. 查找 rundll32.exe
char commandLine[MAX_PATH * 3];
GetWindowsDirectory(commandLine, sizeof(commandLine));
lstrcat(commandLine, "\\rundll32.exe");
if (GetFileAttributes(commandLine) == INVALID_FILE_ATTRIBUTES)
{
GetSystemDirectory(commandLine, sizeof(commandLine));
lstrcat(commandLine, "\\rundll32.exe");
}
// 2. 添加 rundll32.exe 參數
lstrcat(commandLine, " magicdel.dll,_MagicDel@16 ");
// 3. 添加本文件名
char thisName[MAX_PATH];
GetModuleFileName(hInstance, thisName, sizeof(thisName));
lstrcat(commandLine, thisName);
// 執行命令行
PROCESS_INFORMATION procInfo;
STARTUPINFO startInfo;
memset(&startInfo, 0, sizeof(startInfo));
startInfo.dwFlags = STARTF_FORCEOFFFEEDBACK;
CreateProcess(0, commandLine, 0, 0, FALSE, NORMAL_PRIORITY_CLASS, 0, 0,
&startInfo, &procInfo);
}
int WINAPI WinMain(HINSTANCE hInstance,
HINSTANCE hPrevInstance,
LPSTR lpCmdLine,
int nCmdShow)
{
SelfDelete(hInstance);
}
WriteResourceToFile 的功能是存取二進制資源,以便能在磁盤中重建 DLL。Windows 資源 API 提供了一個指向原始數據的指針。
SelfDelete 的作用是重新創建DLL並生成如下命令行啟動 rundll32.exe:
path\rundll32.exe magicdel.dll,_MagicDel@16 path\executableName
rundll32.exe 位於 Windows 目錄或者 System 目錄中,所以 SelfDelete 檢查它的位置是否正確。當 CreateProcess 被調用執行命令行時,必須設置
STARTF_FORCE-OFFFEEDBACK 標志以防止 Windows 在運行 rundll32.exe 時顯示表示忙的沙漏或光標。這樣做以後用戶不會感覺到有新的進程正在運行。在這個新進程退出之後,DLL 和原來的可執行文件都不見了。
為了讓自刪除的可執行程序不依賴於 C 運行時DLL,可執行程序必須靜態鏈接到運行時庫代碼。為此修改項目編譯選項即可:
項目(P)| [項目名稱] 屬性(P)... | C/C++ | 代碼生成 | 運行時庫:[單線程(/ML)] 或者 [多線程(/MT)](或者任何不包含此DLL的選項值)
此自刪除技術在所有 Windows 版本中都工作得很穩定。在實際運用中,卸載程序首先將自己的拷貝放到 Windows 臨時(Temp)目錄,以便能刪除所有程序文件和相關目錄,最後它用自刪除的 DLL 把自己刪掉。
編寫安裝程序
確定了安裝程序要做些什麼事情之後,接著是制作安裝程序。現在很多的安裝程序都是由用戶從Internet上下載,然後在本地運行。那麼下載的文件體積越小越好,為此最有效的方法是對文件進行壓縮處理。如何讓用戶最先看到的畫面是我的程序畫面而不是其它公司的安裝程序畫面呢,好在Windows提供了這樣的支持。
首先創建一個交互式的 Setup 程序,它顯示軟件許可協議,提示用戶安裝選項,拷貝文件,然後進行其余的設置工作。然後將 Setup 程序的壓縮版本作為資源保存在安裝程序(installer)中。這個安裝程序要做的只是將 Setup 程序二進制資源重建後寫回磁盤,解壓縮,然後用一個新進程啟動它。保存和讀寫二進制資源並不難——本文前面已經描述了處理細節和代碼。
自從 Windows 95 開始的每個 Windows 平台都帶一組解壓縮文件的 API——LZCopy。下面是安裝程序使用這個 API 的示例代碼:
/ install.h
//
#define RC_BINARYTYPE 256
#define ID_COMPRESSED_SETUP 100
//
// install.rc
//
#include "install.h"
ID_COMPRESSED_SETUP RC_BINARYTYPE AppSetup.ex_
//
// install.cpp
//
#include <windows.h>
#include "install.h"
void WriteResourceToFile(HINSTANCE hInstance,
int idResource,
char const *filename)
{
// 參見前述代碼
}
void DecompressFile(char const *source, char const *dest)
{
OFSTRUCT ofs;
ofs.cBytes = sizeof(ofs);
int zhfSource = LZOpenFile(const_cast<char *>(source), &ofs, OF_READ);
int zhfDest = LZOpenFile(const_cast<char *>(dest), &ofs,
OF_CREATE | OF_WRITE);
LZCopy(zhfSource, zhfDest);
LZClose(zhfSource);
LZClose(zhfDest);
}
int WINAPI WinMain(HINSTANCE hInstance,
HINSTANCE hPrevInstance,
LPSTR lpCmdLine, int nCmdShow)
{
WriteResourceToFile(hInstance, ID_COMPRESSED_SETUP, "AppSetup.ex_");
DecompressFile("AppSetup.ex_", "AppSetup.exe");
DeleteFile("AppSetup.ex_");
// 啟動 AppSetup.exe
PROCESS_INFORMATION procInfo;
STARTUPINFO startInfo;
memset(&startInfo, 0, sizeof(startInfo));
CreateProcess(0, "AppSetup.exe", 0, 0, FALSE, NORMAL_PRIORITY_CLASS, 0, 0,
&startInfo, &procInfo);
}
從代碼中可以看到壓縮的 Setup 程序是如何作為安裝程序的資源保存的。按照本文前面討論的思路。DecompressFile 函數示范了 LZCopy API 的使用方法。安裝程序重新創建 AppSetup.exe,然後運行它。為了順利編譯和生成安裝程序,需要將 lz32.lib 添加到項目的編譯選項中,通常這個文件在 Visual Studio 的安裝目錄中,如:
Visual Studio .NET 2003:
C:\Program Files\Microsoft Visual Studio .NET 2003\Vc7\PlatformSDK\Lib
Visual C++ 6.0:
C:\Program Files\Microsoft Visual Studio\VC98\Lib
在 Visual Studio.NET 中的添加方法是:
項目(P)| [項目名稱] 屬性(P)... | 鏈接器 | 附加庫目錄:[添加上述路徑之一]
此外,為了擺脫對 C運行時DLL的依賴,必須用靜態鏈接到運行庫代碼:
項目(P)| [項目名稱] 屬性(P)... | C/C++ | 代碼生成 | 運行時庫:[單線程(/ML)] 或者 [多線程(/MT)](或者任何不包含此DLL的選項值)
注意這裡安裝程序不必等待 Setup 程序完成工作,因為 AppSetup.exe 可以在完成工作後用自刪除 DLL 來進行自我刪除。
使用 LZCopy API 最具技巧性的部分是它只能解壓縮由 compress.exe 壓縮的文件。compress.exe是微軟公司的一款壓縮文件命令行實用程序,它隨 SDK 一起提供。也可以在微軟的官方FPT站點下載:ftp://ftp.microsoft.com/softlib/mslfiles/CP0982.EXE。運行EXE後會有幾個解包文件,其中包括 compress.exe,其它的文件可以忽略或刪除。compress.exe 的使用方法如下:
compress SourceName DestinationName
所有 Windows 版本都內建了解壓縮支持,利用它很容易編寫安裝程序。此外,所有 Windows 版本也都包含了另一個實用程序:expand.exe。用它可以在命令行進行解壓縮處理。
總結
借助自刪除 DLL,二進制資源以及 Windows 內建的解壓縮支持可以創建自己的安裝程序和卸載程序,從而輕松控制用戶安裝和卸載程序時屏幕的每一個方面....
本文配套源碼