明輝手游網(wǎng)中心:是一個免費提供流行視頻軟件教程、在線學(xué)習(xí)分享的學(xué)習(xí)平臺!

用自刪除dll完成應(yīng)用程序的安裝/刪除代碼

[摘要][譯者]:本文譯自 Alex Tilles 在 Windows Developer Network (2003 第12期)發(fā)表的一篇文章:“Writing Your Own Install and Uninstall Code”。這是一篇具有一定技術(shù)含量的文章,相信許多開發(fā)人員都需要本文介紹的技術(shù)...
[譯者]:本文譯自 Alex Tilles 在 Windows Developer Network (2003 第12期)發(fā)表的一篇文章:“Writing Your Own Install and Uninstall Code”。這是一篇具有一定技術(shù)含量的文章,相信許多開發(fā)人員都需要本文介紹的技術(shù),其中包括幾個重要的技術(shù)點:
Rundll32.exe 實用程序的使用方法;

DLL 或 EXE 的自刪除技術(shù);

嵌入資源的處理技巧;

LZCOPY API 使用示范;

compress.exe,expand.exe 使用說明;


摘要


  當(dāng)我在編寫“What To Do”程序(這是作者編寫的一個應(yīng)用程序,小巧玲瓏,很實用——譯者注)時,就想寫一個自己的安裝和卸載代碼,主要目的是想隨心所欲地控制整個安裝/卸載過程中用戶所看到的畫面。本文我們就來討論如何利用自刪除的動態(tài)鏈接庫(DLL)實現(xiàn)自刪除的可執(zhí)行程序,從而實現(xiàn)程序的安裝/卸載。相信很多朋友在編寫 Windows 程序時都想這么做,本文還將展示一些非常有用的相關(guān)技術(shù),一定讓你大開眼界......



實現(xiàn)自刪除卸載程序的難點


  編寫卸載程序最具挑戰(zhàn)性的部分是如何讓卸載程序在刪除完目標程序文件和相關(guān)目錄之后自己刪除自己。此外,卸載程序還必須能在所有 Windows 操作系統(tǒng)平臺(Windows 9x、Windows NT、Windows 2000、Windows XP.....)上運行,不需要用戶下載任何附加組件。我在網(wǎng)上搜索了一番,找到一些相關(guān)的資料介紹如何自刪除可執(zhí)行程序文件,但是大多數(shù)所建議的解決方案都存在一個問題,那就是只能在某個版本的 Windows 上工作。有些方法通過修改線程屬性來實現(xiàn),這樣做一般都會導(dǎo)致定時問題。還有一些方法運行時出現(xiàn)嚴重錯誤,根本就不能用。我琢磨著尋求一種更好的解決方法來實現(xiàn)可執(zhí)行程序的自刪除功能:用自刪除的 DLL 實現(xiàn)自刪除的可執(zhí)行程序,從而突破上述諸方法的局限。



實用程序 rundll32.exe 介紹


  從所周知,DLL的代碼通常需要先加載到內(nèi)存之后才能執(zhí)行,那么如何執(zhí)行某個DLL導(dǎo)出的代碼而不用創(chuàng)建加載和調(diào)用該 DLL 的 EXE 文件呢?方法如下:從 Windows 95 開始的每個 Windows 操作系統(tǒng)版本都附帶一個系統(tǒng)實用程序:rundll32.exe。利用它可以象下面這樣執(zhí)行某些 DLL(但不是所有)輸出的任何函數(shù):

rundll32.exe DllName,ExportedfnName args

ExportedfnName 是DLL輸出的函數(shù)名。在編寫供 rundll32 使用的 DLL時,可以象下面這樣來聲明輸出函數(shù):

extern "C" __declspec(dllexport) void CALLBACK FunctionName (
HWND hwnd,
HINSTANCE hInstance,
LPTSTR lpCmdLine,
int nCmdShow
)
{ ... }  


rundll32.exe 根據(jù)函數(shù)參數(shù)列表對函數(shù)進行調(diào)用,但根據(jù)經(jīng)驗,實際上用得上的參數(shù)值只有一個,那就是 lpCmdLine,該參數(shù)接收運行 rundll32.exe 時傳入的參數(shù)值;__declspec(dllexport)的目的是輸出函數(shù);extern "C" 使輸出的函數(shù)名有修飾符,如:_FunctionName@16 (函數(shù)名中被強制包含函數(shù)參數(shù)的大小,詳細信息請參見 MSDN 中有關(guān)DLL輸出函數(shù)調(diào)用規(guī)范說明)。rundll32.exe 加載指定的 DLL 并調(diào)用通過 args 參數(shù)傳入的 lpCmdLine 的值指定的輸出函數(shù)。有關(guān) rundll32.exe 的正式文檔參見 MSDN 庫相關(guān)資料(Q164787):

http://support.microsoft.com/default.aspx?scid=kb;en-us;164787

實現(xiàn)能自刪除的 DLL

下面是實現(xiàn)自刪除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);
// 刪除創(chuàng)建該進程的可執(zhí)行文件
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的入口函數(shù),當(dāng)首次加載動態(tài)鏈接庫時該函數(shù)被調(diào)用,此時將模塊句柄賦值給全局變量 g_hmodDLL,以便梢后使用它來獲取 DLL 本身的文件名。在 MagicDel 函數(shù)中,lpCmdLine 是DLL要刪除的可執(zhí)行文件的名稱(如:卸載程序的文件名)。要刪除它很容易——用 Sleep 做一個延時,以便可執(zhí)行程序的進程有時間退出并調(diào)用 DeleteFile。為了掌握 MagicDel 的實現(xiàn)細節(jié),你可以將可執(zhí)行程序的進程句柄傳給MagicDel并在調(diào)用 DeleteFile 之前做一個等待,看看會發(fā)生什么?
  要讓 DLL 進行自刪除需要一點訣竅。rundll32 調(diào)用 LoadModule 將 DLL 加載到它的地址空間。如果 DLL 函數(shù)可以返回的話,rundll32 將會退出,從而導(dǎo)致 DLL 被釋放(不是被刪除)。為了解決這個問題,我們可以執(zhí)行下面的代碼:

FreeLibrary(DLL module handle);
      DeleteFile(DLL filename);
      ExitProcess(0);


  MagicDel 函數(shù)是不能按這樣的順序進行直接調(diào)用的,因為 FreeLibary 會使代碼頁無效。為此, MagicDel 采用將等效的匯編指令壓入堆棧,然后執(zhí)行它們,后跟一個 ret 指令,最后調(diào)用 ExitProccess 以防止進程繼續(xù)往下執(zhí)行。我參考 Gary Nebbit 在 Windows 開發(fā)雜志(WDJ)“Tech Tips”欄目發(fā)表的文章編寫了一個匯編代碼塊。如果你用 Visual Studio 以默認選項生成DLL,最終的二進制文件大約為 40K。由于我們打算將 DLL 作為可執(zhí)行程序的資源,它的體積越小越好,為此,我們必須對它進行瘦身處理。思路是將無用的 C 運行時代碼從DLL中刪除掉,具體方法如下:
本文例子使用 Visual Studio.NET 2003 中文版編譯生成 DLL,先設(shè)置項目的編譯/鏈接選項:
項目(P) [項目名稱] 屬性(P)... 鏈接器 輸入 忽略所有默認庫:是(/NODEFAULTLIB),此設(shè)置將 /NODEFAULTLIB 選項傳給鏈接器以便過濾掉運行時代碼。

由于 DLL 入口點(Entry Point)通常是由運行時庫提供(默認為 DllMain),所以完成上述第一步設(shè)置之后,還必須顯式地將 DLL入口點設(shè)置為 DllMain:
項目(P) [項目名稱] 屬性(P)... 鏈接器 高級 入口點:DllMain。
如果此時編譯生成 DLL,編譯器會報如下兩個 無法解析的外部符號( unresolved externals ) 錯誤:
error LNK2019: 無法解析的外部符號 ___security_cookie ,該符號在函數(shù) _MagicDel@16 中被引用
error LNK2019: 無法解析的外部符號 @__security_check_cookie@4 ,該符號在函數(shù) _MagicDel@16 中被引用
解決方法是進行下一步設(shè)置。


項目(P) [項目名稱] 屬性(P)... C/C++ 代碼生成 緩沖區(qū)安全檢查:否,
該設(shè)置不會將 /GS 標志傳給編譯器,從而擺脫 unresolved externals 錯誤。
好了,現(xiàn)在編譯生成 DLL,最終的 DLL 大小為 3K,實際的文件大小只有 2.5K。



實現(xiàn)能自刪除的可執(zhí)行程序


  這里所用的主要思路是將一個能自刪除的 DLL 作為資源保存在擬實現(xiàn)自刪除的可執(zhí)行程序中,然后在需要時重新創(chuàng)建它,同時,啟動一個 rundll32.exe 進程實現(xiàn)刪除行為。
  下面是用于將DLL存儲為資源的頭文件和資源文件。資源類型值只要大于 256 都可以,這是為用戶定義類型預(yù)留的。此外還有一種可選方法是將 DLL 二進制文件以字節(jié)數(shù)組的形式直接存儲在源中:


在資源中包含一個文件
// SelfDelete.h
#define RC_BINARYTYPE 256
#define ID_MAGICDEL_DLL 100

// SelfDelete.rc
#include "SelfDelete.h"
ID_MAGICDEL_DLL RC_BINARYTYPE MagicDel.dll


下面是可執(zhí)行程序關(guān)鍵代碼:

#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 參數(shù)
lstrcat(commandLine, " magicdel.dll,_MagicDel@16 ");
// 3. 添加本文件名
char thisName[MAX_PATH];
GetModuleFileName(hInstance, thisName, sizeof(thisName));
lstrcat(commandLine, thisName);
// 執(zhí)行命令行
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 提供了一個指向原始數(shù)據(jù)的指針。
SelfDelete 的作用是重新創(chuàng)建DLL并生成如下命令行啟動 rundll32.exe:
      path\rundll32.exe magicdel.dll,_MagicDel@16 path\executableName      
  rundll32.exe 位于 Windows 目錄或者 System 目錄中,所以 SelfDelete 檢查它的位置是否正確。當(dāng) CreateProcess 被調(diào)用執(zhí)行命令行時,必須設(shè)置
STARTF_FORCE-OFFFEEDBACK 標志以防止 Windows 在運行 rundll32.exe 時顯示表示忙的沙漏或光標。這樣做以后用戶不會感覺到有新的進程正在運行。在這個新進程退出之后,DLL 和原來的可執(zhí)行文件都不見了。
  為了讓自刪除的可執(zhí)行程序不依賴于 C 運行時DLL,可執(zhí)行程序必須靜態(tài)鏈接到運行時庫代碼。為此修改項目編譯選項即可:
項目(P) [項目名稱] 屬性(P)... C/C++ 代碼生成 運行時庫:[單線程(/ML)] 或者 [多線程(/MT)](或者任何不包含此DLL的選項值)
  此自刪除技術(shù)在所有 Windows 版本中都工作得很穩(wěn)定。在實際運用中,卸載程序首先將自己的拷貝放到 Windows 臨時(Temp)目錄,以便能刪除所有程序文件和相關(guān)目錄,最后它用自刪除的 DLL 把自己刪掉。



編寫安裝程序


  確定了安裝程序要做些什么事情之后,接著是制作安裝程序,F(xiàn)在很多的安裝程序都是由用戶從Internet上下載,然后在本地運行。那么下載的文件體積越小越好,為此最有效的方法是對文件進行壓縮處理。如何讓用戶最先看到的畫面是我的程序畫面而不是其它公司的安裝程序畫面呢,好在Windows提供了這樣的支持。
  首先創(chuàng)建一個交互式的 Setup 程序,它顯示軟件許可協(xié)議,提示用戶安裝選項,拷貝文件,然后進行其余的設(shè)置工作。然后將 Setup 程序的壓縮版本作為資源保存在安裝程序(installer)中。這個安裝程序要做的只是將 Setup 程序二進制資源重建后寫回磁盤,解壓縮,然后用一個新進程啟動它。保存和讀寫二進制資源并不難——本文前面已經(jīng)描述了處理細節(jié)和代碼。
  自從 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 函數(shù)示范了 LZCopy API 的使用方法。安裝程序重新創(chuàng)建 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的依賴,必須用靜態(tài)鏈接到運行庫代碼:
項目(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 版本都內(nèi)建了解壓縮支持,利用它很容易編寫安裝程序。此外,所有 Windows 版本也都包含了另一個實用程序:expand.exe。用它可以在命令行進行解壓縮處理。



總結(jié)


  借助自刪除 DLL,二進制資源以及 Windows 內(nèi)建的解壓縮支持可以創(chuàng)建自己的安裝程序和卸載程序,從而輕松控制用戶安裝和卸載程序時屏幕的每一個方面....