|
|||||||||||||||||||||||
|
|||||||||||||||||||||||
|
Announcements
Chapters
Services
Feature Zones
|
Overview of the AnnoyanceI've noticed for a while that on Windows XP, when you use Explorer to delete a file or directory that is in use by another process, it takes what seems like an eternity before an error message appears telling you the file can't be deleted. During this time, there is no indication that Explorer even noticed the delete command. No hourglass, no nothing. By the time the message box appears, I've already hit the delete key a few more times in a futile attempt to get Explorer to just do something, already. Either delete the file or tell me you can't...throw me a bone here. After reading one of Mark Russinovich's excellent blog entries, I decided I would finally try to figure out what's going on with the painfully-slow in-use error message. About this ArticleIn this article, I'll discuss how I tracked down the problem using Process Explorer from Sysinternals.com and WinDbg (part of Microsoft's free Debugging Tools for Windows), and wrote a device driver that patches the offending code. It sure would have been easier if Windows were open source, but hopefully, after seeing some of the techniques I outline in this article, you'll see why I like to refer to Windows as "pry-open source". I've tried to explain most of the concepts in a way that even non-developers with a decent architectural view of Windows can (hopefully) understand what's going on. Some notes before we get started:
The InvestigationIronically, the very same behavior I'm complaining about is what makes this an easy problem to investigate. The multi-second delay gives us plenty of time (relatively speaking) to peer into explorer.exe's threads and see what's happening. Let's get set up:
Now, to reproduce the annoyance in question:
While you're waiting for the error message to appear, you should see in the Process Explorer that several threads have been created in the explorer.exe process. Process Explorer kindly highlights them in green: During the delay before the error message appears, double-click one of the new threads to see its call stack. The call stack is essentially a record of which function the thread is currently executing and which function it will go back to once the current one is done (and which one it will go back to once that one is done, and so on). You should find that one thread's stack looks particularly incriminating:
If you're unfamiliar with viewing call stacks in the Process Explorer, it's important to know that any function you see in the list is currently in the process of calling the function just above it in the list. For instance, if you look at lines 4 and 5 above, you'll see that kernel32.dll!Sleep is currently being called by shell32.dll!_IsDirectoryDeletable. Even though I don't know exactly what most of the functions in the call stack do, after seeing both " Let's test that hunch by watching
After you attach to a process, WinDbg freezes the process (i.e. all of its threads are paused). In order to see the bp shell32!_IsDirectoryDeletable
A breakpoint will cause WinDbg to halt explorer.exe's execution whenever the Now that the breakpoint is set, let's un-freeze explorer.exe by typing "g" (you can also choose Debug -> Go). Now, repeat the "undeletable" experiment again. This time, WinDbg responds immediately, indicating that our breakpoint has been hit: Breakpoint 0 hit
eax=00000001 ebx=018cf084 ecx=7c80f067 edx=1007001e esi=018cee64 edi=00000104
eip=7ca691a9 esp=018cee14 ebp=018cee30 iopl=0 nv up ei pl zr na po nc
cs=001b ss=0023 ds=0023 es=0023 fs=003b gs=0000 efl=00000246
SHELL32!_IsDirectoryDeletable:
7ca691a9 8bff mov edi,edi
You can safely ignore most of this garbage. The only thing that's important at this point is the fact that explorer.exe is frozen right at the point before it's going to execute the
Even if you aren't fluent in assembly language, you can learn a lot just by watching the highlighted line move back and forth through the code. Keep hitting F10 (which steps through one instruction at a time) and you'll see what I mean. You should notice the code looping back on itself several times (five to be exact), each time hitting the line: call dword ptr [SHELL32!_imp__Sleep]
You should also notice a one-second delay after you hit F10 while on that line. This is most likely the cause of the delay we see in the UI. Also, each time through the loop, you'll see the call to In a nutshell, the code appears to loop five times, each time checking whether the directory can be deleted and, if not, going to sleep for a second. This explains why if, during the delay, you were to close cmd.exe, the "undeletable" directory will disappear shortly after. Since Now, we've observed that the loop happens five times, but where is this number coming from? The number 5 appears twice in To modify
You'll notice that the first three bytes (83 ff 05) match what you see in the second column of the disassembly. If you change the 05 (which I've highlighted in yellow) to 00 and return to the Disassembly window, you'll see that the instruction now reads " Time to see if our change makes any difference. First, let's disable any breakpoint we set earlier so that we won't break into WinDbg while testing: bd *
Next, un-freeze explorer.exe by either typing "g" or choosing Debug -> Go. Now, if you repeat the experiment, the "in use" error message should appear immediately. Problem solved! Some NotesIn the course of my investigation, I determined that there is another function named Also note that the "patching" shown above is only temporary. It will only affect the one instance of explorer.exe that was modified. If the process is killed and restarted, the changes will be gone. The rest of this article will show a method of making these changes more permanent. Making it StickMy first thought when considering how to make my patch permanent was to just open up shell32.dll with a hex editor and modify the two bytes (the "05" in both Before I explain my solution, I need to talk a little bit about "KnownDLLs". Essentially, Windows maintains a list of DLLs in the registry that are commonly used by most Windows applications (shell32.dll is one of these). During system boot, these DLLs are mapped into memory as "section objects". When a process needs to load one of these special DLLs, the system will use the cached section object instead of creating a new one. We'll be using the object \KnownDlls\shell32.dll as a starting point for applying the patch. (This is probably an over-simplified explanation, but should be sufficient for our purposes. See this for a more thorough treatment.) After some more thought, I decided I would write a device driver to patch the code at every boot. This made sense to me for several reasons:
Details of the DriverBasically, the driver will find the \KnownDlls\shell32.dll section and, for each address we want to patch, it will map that memory into the kernel-mode area of the virtual address space, "lock" it (to prevent it from being swapped out of physical memory), and update it. The memory will remain mapped and locked until the driver is unloaded, at which point the changes will be reversed and the memory will be unmapped and unlocked. At 183 lines, the driver isn't the most complicated piece of code out there. One thing to remember is that I'll be presenting the code in a somewhat different order than what you'll see in the source file. This will enable us to follow the driver's logic without being distracted by the fact that the C compiler requires us to declare our variables at the start of the function. The main entry point, which for a driver must be called NTSTATUS DriverEntry(IN PDRIVER_OBJECT pDriverObject,
IN PUNICODE_STRING pRegistryPath)
{
pDriverObject->DriverUnload = OnUnload;
return DoPatch(TRUE);
}
The // Open the KnownDlls named section object for shell32.dll
RtlInitUnicodeString(&us, L"\\KnownDlls\\shell32.dll");
InitializeObjectAttributes(&oa, &us, OBJ_KERNEL_HANDLE, NULL, NULL);
status = ZwOpenSection(&hSection, SECTION_MAP_READ | SECTION_MAP_WRITE, &oa);
// Get a pointer to the section
status = ZwMapViewOfSection(
hSection,
NtCurrentProcess(),
&pSection,
0,
0,
0,
&viewSize,
ViewShare,
0,
PAGE_READWRITE
);
As a result of the The // We'll patch two addresses in the shell32.dll section.
// This array must be zero-terminated.
PATCH_INFO offsets[] = {
{ 0xA916F, NULL, NULL },
{ 0xA91B6, NULL, NULL },
{ 0, NULL, NULL }
};
Let's look at how I determined these memory offsets. The first thing to do is to find shell32.dll's base address. Go back to our WinDbg session from earlier and type: lm m shell32
You should see something similar to this: start end module name
7c9c0000 7d1d4000 SHELL32 (pdb symbols) c:\websymbols\shell32.pdb\
290E0039FDA7491EAB979ECE585EE06D2\
shell32.pdb
We can see here that shell32's base address is 0x7C9C0000. (The "0x" prefix is simply the C/C++ way of indicating a hexadecimal number.) The two "05" bytes we want to change in memory are located at 0x7CA6916F (for Now that we know which addresses to patch, we need to map those addresses into the kernel-mode area of the memory. This will allow us to lock the pages in memory, thus preventing them from being swapped out to disk. In this case the memory we're modifying is "backed" by shell32.dll. If it were to be swapped out, the memory manager would attempt to write the changes back to shell32.dll. This would most likely cause Windows File Protection to swing into action, which is something we want to avoid, since it would most likely undo the changes we're going to make. First, we create a Memory Descriptor List (MDL) that points to each address we want to modify. Then we use that MDL to map the pages into kernel-mode memory and lock them: if (isLoading)
{
pWordToChange = (PULONG)(pSection + pCurrentPatchInfo->offset);
// Create a Memory Descriptor List (MDL)
// for the virtual address at "pWordToChange"
pCurrentPatchInfo->pMdl = IoAllocateMdl(pWordToChange,
sizeof(ULONG), FALSE, FALSE, NULL);
if (pCurrentPatchInfo->pMdl == NULL)
{
DbgPrint("Unable to allocate MDL for VA %08X\n", pWordToChange);
retval = STATUS_UNSUCCESSFUL;
__leave;
}
// Lock the pages in memory and get a pointer
// to the "pWordToChange" in its
// kernel-memory-mapped location
MmProbeAndLockPages(pCurrentPatchInfo->pMdl, KernelMode, IoReadAccess);
pCurrentPatchInfo->pMapped =
(PULONG)MmGetSystemAddressForMdlSafe(pCurrentPatchInfo->pMdl,
NormalPagePriority);
if (pCurrentPatchInfo->pMapped == NULL)
{
DbgPrint("MmGetSystemAddressForMdlSafe"
" returned NULL for VA %08X\n",
pWordToChange);
retval = STATUS_UNSUCCESSFUL;
__leave;
}
}
Note: we'll only do this MDL building, mapping and locking if we're first loading the driver and applying the patch. We'll store the MDLs and pointers for use when we are undoing the patch. (This is what the extra Next, the driver will verify that the memory locations we are going to update actually contain the expected original values. If shell32.dll is ever updated by a service pack or hot fix, I'd hate to just stomp over some unrelated bit of memory that happens to live where // Depending on whether we're loading or unloading the driver, we expect to find
// one of the following values at each offset. Only if there is a match will we
// do the patch.
const ULONG unpatchedBytes = 0xFF2A7D05;
const ULONG patchedBytes = 0xFF2A7D00;
// We'll change both what we're looking for and what we change to based on
// whether the driver is loading or unloading.
expectedBytes = isLoading ? unpatchedBytes : patchedBytes;
newBytes = isLoading ? patchedBytes : unpatchedBytes;
The following code, which verifies the memory location's current contents, is executed once for each address in memory to be patched: if (*pCurrentPatchInfo->pMapped != expectedBytes)
{
DbgPrint(
"Offset %08X (address %08X) didn't match. "
"Actual value: %08X. Expected: %08X\n",
pCurrentPatchInfo->offset,
pCurrentPatchInfo->pMapped,
*pCurrentPatchInfo->pMapped,
expectedBytes
);
retval = STATUS_UNSUCCESSFUL;
__leave;
}
Based on the " All that's left to do now is to actually change the values. The following code is executed once for each of the two addresses we're changing: // Do the patch
*pCurrentPatchInfo->pMapped = newBytes;
DbgPrint(
"Offset %08X at VA %08X changed to %08X\n",
pCurrentPatchInfo->offset,
pCurrentPatchInfo->pMapped,
*pCurrentPatchInfo->pMapped
);
// If we're unloading (i.e. DoPatch(FALSE)),
// we'll unmap and unlock the memory
if (!isLoading)
{
MmUnmapLockedPages(pCurrentPatchInfo->pMapped, pCurrentPatchInfo->pMdl);
MmUnlockPages(pCurrentPatchInfo->pMdl);
IoFreeMdl(pCurrentPatchInfo->pMdl);
}
Also note that if we're undoing the patch, we'll release both the locked memory and the MDL for each offset that was patched. Building the DriverIf you have the DDK installed, you can build the driver yourself. First, bring up the DDK command prompt (mine is under Start -> Programs -> Development Kits -> Windows DDK 3790 -> Build Environments -> Windows Server 2003 -> Windows Server 2003 Free Build Environment). "cd" into the directory where you have the source files, and type: build -c
That's it! The completed driver (named "NoDeleteDelay.sys") should be located in the "objfre_wnet_x86\i386" directory. Note: there are reportedly some issues with the DDK's build utility when your source files are in a directory that contains spaces in the pathname (e.g. "c:\Documents and Settings\Administrator\My Documents" probably isn't going to work). Loading and TestingThere are several ways to get the driver loaded into the kernel. The easiest way during testing is to use Greg Hoglund's driver installer utility:
You'll also notice I'm using DbgView from Sysinternals to view the output from the DbgPrint statements in the code. You can now try the "undeletable" experiment from the beginning of the article and see that the "in use" error message appears instantly. Also, if you click "Stop" in InstDrv and retry the test, you'll once again observe the usual five-second delay. Another way to get the driver into the kernel on a more permanent basis is to use the Installer.exe utility I've included in the article's archive file. Just copy both Installer.exe and NoDeleteDelay.sys to the directory where you'd like them to live, then run Installer.exe. There's no uninstall functionality built in to Installer.exe, but if you want to remove the driver, just use InstDrv.exe from above. Type in "NoDeleteDelay" as the full pathname to the driver (no, it isn't the full path, but since the driver is already installed, it doesn't matter). Click "Stop", followed by "Remove". Some Final ThoughtsThis is a version 1.0 piece of software. Here are some things I've thought about but haven't spent much time investigating. (An explanation of these problems is beyond the scope of the article and not understanding them shouldn't detract from the usefulness of what's been presented.)
Recommended Reading
History
| ||||||||||||||||||||||