How does “dir” work?

I heard an interesting interview question recently. The question is: “How does the ‘dir’ command work?”. At the surface, it’s an easy question, but we can use this question to drive all the way down through the internals of the operating system.

Let’s start at the top, from a user’s perspective. A user is sitting at a command prompt in windows (cmd.exe), and types “dir”. The first thing that happens is that the command goes to a parser. The parser looks for internal commands before looking for a program named “dir”, since a large amount of functionality is implemented as built-in commands. The parser finds the internal command for dir, and executes it as a subroutine of cmd.exe.

At this point, the internal dir command will use the windows APIs to enumerate the files in the current directory. In windows, file enumeration is done through the FindFirstFile/FindNextFile functions. These functions are located in the kernel32.dll module. Despite it’s name, kernel32 is a user mode module that resides in the same address space as cmd.exe. How can we figure out what FindFirstFile does? We break out the debugger, of course!

To follow along, grab a copy of windbg, and start a debugging session for cmd.exe. You should be able to follow along with any version of windows, but I’m using Windows 7 for this experiment.

After letting the command prompt start up, we’ll break into the debugger and set a breakpoint on kernel32!FindFirstFileExW. I discovered this was the right breakpoint by setting a breakpoint on all of the FindFirstFile functions and seeing this one get called first. Now that we have a breakpoint set, resume execution of the target process with a “g” command, and head back over to the console we are debugging. Enter a “dir” command, and if all is well, we’ll immediately break back into the debugger at FindFirstFileExW.

The line you see will likely look something like this:

00000000`7767c4a8 ff25b20f0800 jmp qword ptr [kernel32!_imp_FindFirstFileExW (00000000`776fd460)] ds:00000000`776fd460={KERNELBASE!FindFirstFileExW (000007fe`fd724d40)}

This is an indirect jump using a symbol called “_imp_FindFirstFileExW”. Why an indirect jump? Shouldn’t kernel32.dll know where FindFirstFileExW is? Actually, it turns out that the “real” FindFirstFileExW is in kernelbase.dll, and the kernel32 version just calls through to the imported function in kernelbase. So let’s step into the “real version” in kernelbase. You can use a “t” command in windbg to do this.

Now we see something that looks more like a real function:

000007fe`fd724d40 fff3 push rbx
000007fe`fd724d42 56 push rsi

Since we have a live debugging session, we have a huge advantage when analyzing what the function does, because we can trace through the function as it executes rather than trying to guess what it would do while looking at the disassembly. One of the most powerful commands for this is the “wt” command, which means “watch and trace”. It traces through a function execution, listing out the function names each time a function is called or a function returns. It also has the nice feature of listing all the system calls that are executed. In the summary for FindFirstFileExW, we see the following system calls:

2 system calls were executed

Calls System Call
1 ntdll!NtQueryDirectoryFile
1 ntdll!ZwOpenFile

If you look at the trace from wt, you’ll see that ZwOpenFile comes first, and then NtQueryDirectoryFile. This makes sense, once you realize that a directory is a type of file. It is first opened with ZwOpenFile, and then we can query information from it using NtQueryDirectoryFile. Both of these functions are documented on the msdn website, as these functions are the entrypoint into kernel from usermode, but are also available to kernel-mode components such as drivers.

How does ZwOpenFile and NtQueryDirectoryFile work then? In windows, many sources of data are mapped into a single unified namespace. You can see this when you map a network drive and access it as if it were a local disk. You can also see this in a more subtle way when you have two filesystems, say fat32 and ntfs, both accessible through the same APIs. Internally, this is implemented with tables of handler functions that depend on the location within the namespace. In other words, querying for files on an NTFS volume routes the requests through the NTFS driver, while FAT32 volumes route requests through the FAT32 driver. The filesystem drivers are layered on top of the disk drivers, until eventually we get to a request for a block on the physical location of the data (like a disk drive).

This was a bit of a whirlwind tour, but should give you a good idea of what happens when you type “dir” at the console. I think if I write another post like this, I’ll take a look at how “malloc” works on a Windows system.

Leave a Reply

Your email address will not be published. Required fields are marked *