Monday, May 17, 2004

 

How to delete junk files after installation

1.
用自删除dll实现应用程序的安装/卸载代码

对于Rundll32的讨论可以参见Flier的大作“RunDll32 的使用方法与实现原理”
http://flier_lu.blogone.net/main.asp?id=1190096


作者:Alex Tilles
翻译:NorthTibet
出处:http://www.vckbase.com/document/viewdoc/?id=1043

实现自删除卸载程序的难点
  编写卸载程序最具挑战性的部分是如何让卸载程序载删除完目标程序文件和相关目录之后自己删除自己。此外,卸载程序还必须能在所有 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的示范代码:

[code]
#include
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
}
}
[/code]

  上面这段代码首先删除某个文件,然后自删除。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 二进制文件以字节数组的形式直接存储在源中:
在资源中包含一个文件

[code]
// SelfDelete.h
#define RC_BINARYTYPE 256
#define ID_MAGICDEL_DLL 100

// SelfDelete.rc
#include "SelfDelete.h"
ID_MAGICDEL_DLL RC_BINARYTYPE MagicDel.dll
下面是可执行程序关键代码:#include
#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);
}
[/code]

  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 的示例代码:

[code]
// 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
#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(source), &ofs, OF_READ);
int zhfDest = LZOpenFile(const_cast(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);
}
[/code]

  从代码中可以看到压缩的 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 内建的解压缩支持可以创建自己的安装程序和卸载程序,从而轻松控制用户安装和卸载程序时屏幕的每一个方面....

2.
作者:李安东
出处:http://www.csdn.net/develop/article/14/14063.shtm

假如我的程序要调用一个setup.exe程序,自动安装一个软件,完成安装后再把临时文件全部删除,应怎样实现呢?虽然很简单,但有一个问题需要解决,就是如何判断何时已经安装完成了呢?当然可以用
//Wait for until it terminated:
while(GetExitCodeProcess(newinfo.hProcess,&dwExitCode)&&
dwExitCode==STILL_ACTIVE);

来等待setup.exe运行结束,但是问题可能并不这么简单,常常是setup.exe又调用了别的子进程(例如_delis和inst5176什么的),而setup.exe退出后,子进程并未退出,即任务仍未完成。因此这时删除临时文件和文件夹仍然会导致安装失败和删除文件失败。(我判断早期的WinRAR创建的TempMode自解压文件,在启动setup.ex后安装之所以会失败,可能就是因为判断错误,即在未完成安装时就把临时文件删除了。)


这个问题可以按如下方法解决(供参考):
1、用系统函数CreateEvent()创建一个事件hEvent;
2、启动释放在临时目录(比如C:\WINDOWS\TEMP\MYTEMP)下的setup.exe后,然后执行如下语句:
//Wait for the self-extract process exit:
::ResetEvent(hEvent);
while(::WaitForSingleObject(hEvent,500)==WAIT_TIMEOUT)
{ IsExit(); }


即先将事件hEvent复位到无信号状态,并循环调用IsExit()函数;


3、在IsExit()函数中列举系统中所有进程:
(1)、调用系统函数CreateToolhelp32Snapshot()并指定TH32CS_SNAPPROCESS参数,获取一个系统中所有进程的列表(snapshot);
(2)、调用系统函数Process32First()获取第一个进程的信息;
(3)、循环调用系统函数Process32Next()获取其余进程的信息。
上述函数调用中有一个参数lppe是一个PROCESSENTRY32类型的结构。lppe.th32ProcessID参数包含了获取的进程标识符;lppe.szExeFile为该进程的可执行文件路径和名称。
因此在上述处理过程中,每次获取lppe后均判断lppe.szExeFile 中的路径是否是安装程序所在的临时目录,如果不存在这样的进程,则说明安装已经完成,则调用SetEvent()函数,将hEvent事件设置为有信号,从而使第二步中的循环结束;


4、关闭事件句柄,删除安装程序的所有临时文件和文件夹(例如MYTEMP),完成安装。
注意:在调用列举进程的函数时必须添加#include 指令。


下面是示例代码(已调试通过):
// MySfx.cpp : Defines the entry point for the application.
//

#include "stdafx.h"
#include "resource.h"

// Foward declarations of functions included in this code module:
BOOL InitInstance(HINSTANCE, int);
void RemoveThem(char *strPath);
void IsExit();

int APIENTRY WinMain(HINSTANCE hInstance,
HINSTANCE hPrevInstance,
LPSTR lpCmdLine,
int nCmdShow)
{
// Perform application initialization:
return InitInstance (hInstance, nCmdShow);
}

//
// FUNCTION: InitInstance(HANDLE, int)
//
// PURPOSE: Saves instance handle and creates main window
//
// COMMENTS:
//
// In this function, we save the instance handle in a global variable and
// create and display the main program window.
//
#if defined(_DEBUG)
#define THISFILE_LENGTH 159785
#else
#define THISFILE_LENGTH 28672
#endif

char sPath[256];
HANDLE hEvent;

BOOL InitInstance(HINSTANCE hInstance, int nCmdShow)
{
char sModule[256],sTemFile[256];
//Gets temporary directory:
::GetTempPath(255,sPath);
strcat(sPath,"Mytemp");
::CreateDirectory(sPath,NULL);
strcpy(sTemFile,sPath);
strcat(sTemFile,"\\Sfx.exe");
::GetModuleFileName(NULL,sModule,255);

//Opens the module file:
HANDLE hFile=::CreateFile(sModule,GENERIC_READ,FILE_SHARE_READ,
NULL,OPEN_EXISTING,FILE_ATTRIBUTE_NORMAL,NULL);
if(INVALID_HANDLE_VALUE==hFile)return 0;
//Creates the temprory file:
HANDLE hFileTemp=::CreateFile(sTemFile,GENERIC_WRITE|GENERIC_READ,
0,NULL,CREATE_ALWAYS,FILE_ATTRIBUTE_NORMAL,NULL);
if(INVALID_HANDLE_VALUE==hFileTemp)
{
::CloseHandle(hFile); return 0;
}
::SetFilePointer(hFile,THISFILE_LENGTH,NULL,FILE_BEGIN);
//Now begin to read and write:
while(TRUE)
{
BYTE buf[40*1024];
DWORD dwNumberOfBytesRead;
if(::ReadFile(hFile,buf,40*1024,&dwNumberOfBytesRead,NULL)==0)
break;
DWORD dwNumberOfBytesWritten;
if(dwNumberOfBytesRead>0)
if(!::WriteFile(hFileTemp,buf,dwNumberOfBytesRead,
&dwNumberOfBytesWritten,NULL))break;
if(dwNumberOfBytesRead<40*1024)break;
}//while(TRUE)
::CloseHandle(hFile);
::CloseHandle(hFileTemp);

//Prepare to extract files and setup the application:
//Creates a auto-reset event object:
hEvent=::CreateEvent(
NULL, // SD
FALSE, // reset type
FALSE, // initial state
NULL // object name
);

//Executes self-extract file to extract files:
STARTUPINFO info;
PROCESS_INFORMATION newinfo;
::GetStartupInfo(&info);
::CreateProcess(sTemFile,NULL,NULL,NULL,FALSE,
CREATE_DEFAULT_ERROR_MODE,NULL,sPath,&info,&newinfo);

//Wait for the self-extract process exit:
::ResetEvent(hEvent);
while(::WaitForSingleObject(hEvent,500)==WAIT_TIMEOUT)
{
IsExit();
}

//Executes setup:
strcpy(sTemFile,sPath);
strcat(sTemFile,"\\Setup.exe");
::CreateProcess(sTemFile,NULL,NULL,NULL,FALSE,
CREATE_DEFAULT_ERROR_MODE|CREATE_NO_WINDOW,
NULL,sPath,&info,&newinfo);
//Wait for setup process and other started by it exit:
::ResetEvent(hEvent);
while(::WaitForSingleObject(hEvent,500)==WAIT_TIMEOUT)
{
IsExit();
}
::CloseHandle(hEvent);

//Remove tempary files and folders:
RemoveThem(sPath);

return FALSE;
}

void RemoveThem(char *strPath)
{
char strTemFile[256];
strcpy(strTemFile,strPath);
strcat(strTemFile,"\\*.*");
WIN32_FIND_DATA FindFileData;
HANDLE hFind=FindFirstFile(strTemFile,&FindFileData);
if(hFind!=INVALID_HANDLE_VALUE)
while(TRUE)
{
if(FindFileData.cFileName[0]=='.')
{
if(!FindNextFile(hFind,&FindFileData))break;
continue;
}
strcpy(strTemFile,strPath);
strcat(strTemFile,"\\");
strcat(strTemFile,FindFileData.cFileName);
if(FindFileData.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY)
RemoveThem(strTemFile);//recursive call if it's a subdirectory.
else ::DeleteFile(strTemFile);//Delete it if it's a file.
if(!FindNextFile(hFind,&FindFileData))break;
}
::CloseHandle(hFind);
::RemoveDirectory(strPath);
}

void IsExit()
{
//Enumerate current processes:
//This process don't exit until the processes belonged to setup are all terminated:
HANDLE hSnapshot;
PROCESSENTRY32 pe;
pe.dwSize=sizeof(pe);
BOOL blExist=FALSE;
size_t len=strlen(sPath);

hSnapshot=::CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS,0);
if(hSnapshot<0) goto L1;

if(::Process32First(hSnapshot,&pe)==FALSE)
{
::CloseHandle(hSnapshot); goto L1;
}
if(_strnicmp(sPath,pe.szExeFile,len)==0)
blExist=TRUE;

while(blExist==FALSE && ::Process32Next(hSnapshot,&pe))
{
if(_strnicmp(sPath,pe.szExeFile,len)==0)
{
blExist=TRUE; break;
}
}
::CloseHandle(hSnapshot);

L1: if(blExist==FALSE) ::SetEvent(hEvent);
}

本文的意图不是要开发一个工具软件(因为市面上已有此类工具),其主要目的是想与有兴趣的朋友一起切磋一下实现思路。使用Sleep()函数确实更加简便,谢谢高手指点。

(按:现在大家都用msi自动完成这一工作,免得自己麻烦)



<< Home

This page is powered by Blogger. Isn't yours?