exe操作本身


January 1996

Microsoft Systems Journal Homepage

 

Jeffrey Richter wrote Advanced Windows (Microsoft Press, 1995) and Windows 3.1: A Developer's Guide (M&T Books, 1992). Jeff is a consultant and teaches Win32-based programming seminars. He can be reached at v-jeffrr@microsoft.com.

Click to open or copy the DELEXE project files.

QWe're very close to shipping our product. In order for us to get the "Designed for Windows® 95" logo, we must supply an application that uninstalls our software from the user's machine. We have developed an application called UNSETUP.EXE. But how can we make UNSETUP.EXE delete itself? Right now, we can remove all of the application's files and subdirectories but we cannot delete the program that deletes these files and the subdirectory containing it. How can we write a program that can delete itself?

Anthony Spica
Cherry Hill, NJ

AAs I'm sure you are aware, the following code does not work correctly:

 TCHAR szEXEPathname[_MAX_PATH];
GetModuleFileName(NULL, szEXEPathname, _MAX_PATH);
DeleteFile(szEXEPathname);

This code gets the full pathname of the currently running executable and then attempts to delete it by calling DeleteFile. Because Windows 95 and Windows NTª load EXE files into memory as memory-mapped files, the system opens the EXE file when the process is started and automatically closes the file when the process terminates. When DeleteFile is called, the system sees that the EXE file is still opened, DeleteFile returns FALSE, and a subsequent call to GetLastError returns 5, which identifies an access violation error (ERROR_ACCESS_DENIED).

That straightforward technique won't work. The first thing that popped into my mind to solve this problem was the little-known MoveFileEx function:

 BOOL MoveFileEx(LPCTSTR lpExistingFileName, 
                LPCTSTR lpNewFileName, DWORD dwFlags); 

MoveFileEx allows you to move a file from one directory to another. Passing NULL for the lpNewFileName parameter tells the system to move the file to nowhere. This is the same thing as telling the system to delete the file and is basically the same thing as calling DeleteFile. So why would this be a better solution? MoveFileEx's third parameter, dwFlags, allows you to specify flags that alter the behavior slightly. The MOVEFILE_DELAY_UNTIL_REBOOT flag tells the system that the file is not to be moved (deleted) until the system is rebooted. When the system is rebooted, the UNSETUP.EXE file will not be in use, the system will delete the file, and everything's swell.

Unfortunately, there are three problems with the MoveFileEx technique. It doesn't remove the subdirectory that contains the file. Also, the file is not deleted immediately. I know several people who've had Windows NT running continuously over a full year without ever rebooting! If these people install and uninstall several applications, their hard drives would soon start to be populated with a whole bunch of UNSETUP.EXE files. The third and final problem with MoveFileEx is the biggest: it's not implemented on Windows 95.

Windows 95 does offer a technique that lets you move/delete a file when the user reboots. When Windows 95 boots, it runs an application called WININIT.EXE. This application looks for a file called WININIT.INI. If this file exists, WININIT.EXE then looks for a section within the file labeled Rename.

 [Rename]
NUL=C:/Program Files/Win95ADG/UNSETUP.EXE
NUL=C:/Program Files/SomeApp/UNSETUP.EXE
C:/Calc.exe=C:/Windows/Calc.exe

Every line in the Rename section indicates a file that needs to be moved or deleted. The name to the left of the equal sign indicates the new pathname of the file and the name on the right indicates the current pathname of the file. If the name on the left is NUL, WININIT.EXE deletes the file. Because WININIT.INI's contents look very much like any old-style INI file, you might be tempted to use the WritePrivateProfileString function to add entries to the Rename section. Don't-the WritePrivateProfileString function ensures that no more than one entry in a section can have NUL to the left of the equal sign. If every UNSETUP.EXE program used WritePrivateProfileString to add its entry to the WININIT.INI file, the following scenario could occur. The user runs your UNSETUP.EXE; then, before rebooting, the user uninstalls another application. But because WritePrivateProfileString doesn't allow more than one entry to begin with NUL=, your entry is deleted from the file and yourapplication'sUNSETUP.EXEfilewillneverbedeleted.

The ReplaceFileOnReboot function shown in Figure 1 hides from you the implementation differences of moving/deleting a file on reboot between Windows 95 and Windows NT. The function first tries to call MoveFileEx. If this function fails, it then opens (or creates) a WININIT.INI file and properly adds entries to the Rename section. Of course, in a future version of Windows 95, Microsoft will fully support the MoveFileEx function. I have written the ReplaceFileOnReboot function so that it always calls MoveFileEx and will continue to work correctly on future versions of Windows 95.

I decided to try something else since I didn't like the fact that the ReplaceFileOnReboot function still forces the user to reboot in order to delete the UNSETUP.EXE file. Here is the code I used for my next attempt:

 int WINAPI WinMain (HINSTANCE hinstExe, 
                    HINSTANCE hinstExePrev,
                    LPSTR lpszCmdLine, int nCmdShow) {
    TCHAR szEXEPathname[_MAX_PATH];
   HANDLE hfile;

   GetModuleFileName(NULL, szEXEPathname, _MAX_PATH);
   hfile = CreateFile(szEXEPathname, GENERIC_READ,
                      FILE_SHARE_READ, NULL,
                      OPEN_EXISTING,
                      FILE_FLAG_DELETE_ON_CLOSE, NULL);
   CloseHandle(hfile);
   return(0);
}

My idea was that since the EXE file is already open, why not go and open the file a second time by calling CreateFile using the FILE_FLAG_DELETE_ON_CLOSE flag? This flag tells the system that the file should be deleted when it is no longer in use. After CreateFile returns, I immediately call CloseHandle assuming that the system remembers that I want the file deleted when it's no longer in use. So, when the process terminates, I expect the system to automatically delete the file.

After writing the small test application and experimenting with it, I soon discovered that this technique doesn't work at all. When I debugged the program under Windows 95, I saw that the call to CreateFile was succeeding but the system was just ignoring my request to open the file with delete-on-close access. When I closed the file and terminated the process, the executable file remained on my hard disk.

I then went and tested my application under Windows NT. On Windows NT, the call to CreateFile fails returning INVALID_HANDLE_VALUE and GetLastError returns ERROR_ACCESS_DENIED. This is because Windows NT supports the ability to share a file with delete access. When I attempt to open a file specifying the FILE_FLAG_DELETE_ON_CLOSE flag, the system checks to see if the file has already been opened with the FILE_SHARE_DELETE flag. FILE_SHARE_DELETE is not documented in the Win32 SDK documentation, but it can be found in the Windows NT DDK's NTDDK.H file:

 #define FILE_SHARE_DELETE               0x00000004

Taking the failure of my previous technique in stride, I started working on my next idea. I needed to somehow execute code in my process's address space without having my EXE file loaded anymore. That made me think of DLLs. DLLs can be dynamically loaded and unloaded as desired simply by calling LoadLibrary and FreeLibrary. All I'd need to do is dynamically unload my EXE file from my address space just as I would unload a DLL. You may recall that the FreeLibrary function takes a single parameter: the HINSTANCE (or HMODULE) of a loaded DLL.

 BOOL FreeLibrary(HINSTANCE hinstDll);

In Win32®-based programs, the HINSTANCE of a DLL is the virtual memory address where the DLL was loaded into a process's address space. This is true of EXE files as well: the hinstExe parameter passed to WinMain identifies the virtual address where the EXE file got loaded. So it stands to reason that if I call FreeLibrary and pass in the EXE's hinstExe value, the system should unload the EXE file from my process's address space, right?

My code to test this theory looks like this:

 int WINAPI WinMain (HINSTANCE hinstExe, 
                    HINSTANCE hinstExePrev,
                    LPSTR lpszCmdLine, int nCmdShow) {

   TCHAR szEXEPathname[_MAX_PATH];
   GetModuleFileName(NULL, szEXEPathname, _MAX_PATH);
   FreeLibrary(hinstExe);
   DeleteFile(szEXEPathname);
   return(0);
}

The code seems simple enough but if you build this sample code and run it on Windows 95, you'll consistently get access violations and, of course, the UNSETUP.EXE file won't be deleted from the user's hard drive. This was a fun sample to code and execute inside a debugger. When you debug this application, you'll want to keep the EXE file visible in the debugger's memory window. Then, watch what happens when you execute the FreeLibrary call. FreeLibrary actually does unload the EXE file from the process's address space. You can easily see this because the contents of the debugger's memory window changes to all questions marks. But then FreeLibrary returns and the code that contains the call to DeleteFile is gone and this is what causes the access violation.

This is a relatively easy problem to solve, especially if you remember my article "Load Your 32-bit DLL into Another Process's Address Space Using INJLIB," MSJ May 1994. (My Advanced Windows book, Microsoft Press, 1995 has the latest version of this code and some bug fixes I've made since the article appeared.) That article described a technique that allows you to dynamically inject code into another process's address space and execute this injected code. This injected code in the remote process does not come from a DLL or an EXE but instead comes from copying bytes from the local process's address space into the remote process's address space. Your problem requires a stripped-down version of the inject library code (see Figure 2).

For a full understanding of the code in Figure 2, please refer to the inject library article. Basically, the DeleteExecutable function first calls HeapAlloc to allocate a block of memory in your process's address space. Then a function is copied from your EXE file into the dynamically allocated block of memory. The rules that this function must follow are spelled out in the inject library article. Next, a DELEXEINFO data structure is initialized with the full pathname of the process's EXE file, a flag indicating whether the containing subdirectory should also be deleted, and the address of the functions (in KERNEL32.DLL) that are going to be called from the code in the dynamically allocated memory block.

After the data structure is initialized, the code in the memory block is called and is passed the address of the DELEXEINFO structure (which is on the thread's stack). The code in the memory block uses the information in the DELEXEINFO structure to call FreeLibrary to unload the EXE file, then DeleteFile to delete the EXE file (which will succeed now), and then call ExitProcess so that the process terminates. It would be a bad mistake to allow the code in the memory block to return because the calling code no longer exists after FreeLibrary unloads the EXE file.

To use my function, all you have to do is include the DELEXE.H header file and add the DELEXE.C source file to your project. Then in your code, just place a call to my DeleteExecutable function:

 void WINAPI DeleteExecutable(DWORD dwExitCode, 
                             BOOL fRemoveDir);

The first parameter is your process's exit code, and the second parameter tells the function whether it should also attempt to remove the subdirectory that contains the UNSETUP.EXE file. The function is prototyped as returning void because it will never return.

This is my favorite solution because it deletes the EXE file immediately. But now I have some bad news: this method doesn't work on Windows NT. The reason is that Windows NT doesn't allow FreeLibrary to unload the process's EXE file. At first, I thought that Windows NT just sets a really big usage count on the EXE file module. I figured I could call FreeLibrary passing the hinstExe value continuously in a loop. Each call to FreeLibrary would decrement the EXE file's usage count until the EXE file is unloaded. I thought I could detect this because FreeLibrary will return FALSE if I pass an address that does not represent a module. The pseudocode would be something like this:

 while (FreeLibrary(hinstExe))
    ;
DeleteFile(szEXEPathname);

I coded this up and tested it on Windows NT. After several hours, the while loop still had not terminated. Either Windows NT just doesn't allow FreeLibrary to affect an EXE or this loop will take too long to execute and the performance of the UNSETUP.EXE would be too horrendous anyway.

After all of this, I decided on a brute-force method that consistently works on both Windows 95 and Windows NT: batch files. Batch files have the unique ability to delete themselves. If you create a batch file containing this single command:

 del %0.bat

and run it at the command prompt, the batch file will delete itself, followed by the error message:

 The batch file cannot be found.

This error message is harmless. The DeleteExecutableBF function I wrote (see Figure 3) creates a batch file that kills the running executable program. If the executable is called UNSETUP.EXE and resides in the C:/Win95ADG subdirectory, the contents of the batch file look like this:

 :Repeat
del "C:/Win95ADG/UNSETUP.EXE"
if exist "UNSETUP.EXE" goto Repeat
rmdir "C:/Win95ADG"
del "/DelUS.bat"

This batch file attempts to delete the UNSETUP.EXE file. If the file is not deleted because it is still executing, the batch file attempts to delete the file again. When the executable file no longer exists, the batch file removes the subdirectory that contained the file and then deletes itself.

You'll notice that the DeleteExecutableBF function does a lot of work to run the batch file. The system always executes batch files inside a console window. There is no need for the user to know that you are running a batch file, so I set the STARTUPINFO's wShowWindow member to SW_HIDE.

Another thing to be aware of is that the batch file polls for the existence of the executable file. In a preemptive, multithreaded environment, polling is an awful thing to do because you are causing the system to waste precious CPU cycles. But in batch files, there is no way to call WaitForSingleObject or WaitForMultipleObjects, so you must use a polling technique.

However, you can adjust the priority class and relative thread priorities of the processes and threads involved with my solution. When the DeleteExecutableBF function calls CreateProcess to spawn the batch file, I specify both the CREATE_SUSPENDED and IDLE_PRIORITY_CLASS flags. The CREATE_SUSPENDED flag tells the system to create the console window (which will be invisible) to run the batch file, but the system is not allowed to schedule it any CPU time yet. The IDLE_PRIORITY_CLASS flag tells the system that this process should not be scheduled CPU time frequently. This way fewer CPU cycles will be wasted by the batch file while it polls for the executable file to terminate.

After CreateProcess returns, I explicitly set its primary thread's relative thread priority to THREAD_PRIORITY_IDLE. This further reduces the amount of CPU time that will be wasted by the batch file. Then I set the executable thread's relative priority to time critical and the executable's priority class to high. This causes the system to schedule CPU time to the executable's thread very frequently so that it can terminate as soon as possible. I do not set the priority class to real time because this would interfere with threads that are responsible for processing various hardware events.

After all of the thread priorities have been adjusted, I close the handle to the batch file's process and then resume the batch file's thread. This allows the system to schedule CPU time to execute the batch file, but not a lot of time to the batch file because its priority is so low. Finally, I close the handle to the batch file's thread and return from the DeleteExecutableBF function. At this point, you want the executable process to terminate as soon as possible so that the batch file will terminate, stop polling, and stop wasting CPU cycles.

The TDELEXE.C file (see Figure 4) is a small application that simply tests all of my techniques. When you invoke it, it asks the user with a message box (see Figure 5) how the user would like to delete the executable file. Of course, you can only run the program once since the EXE file is destroyed when the process terminates. If you can't easily rebuild the code, I suggest you make a backup of the original EXE file before running it each time.

Figure 5 TDELEXE

Have a question about programming in Win32? You can mail it directly to Win32 Q&A, Microsoft Systems Journal, 825 Eighth Avenue, 18th Floor, New York, New York 10019, or send it to MSJ (re: Win32 Q&A) via:


Internet:

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值