用自刪除dll實現應用程式的安裝/卸載代碼

來源:互聯網
上載者:User
譯者]:本文譯自 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 內建的解壓縮支援可以建立自己的安裝程式和卸載程式,從而輕鬆控制使用者安裝和卸載程式時螢幕的每一個方面....

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.