A large percentage of Windows APIs return zero to indicate success and non-zero for failure. Most of the MSDN documentation for each API included with the operating system contains a Return Value section. The Return Value section documents what return values can be expected.
Windows allows for developers to access an additional return code via the Last Error mechanism. A last error code is maintained for every thread in the system. It allows for API developers to return further information to the calling application such as why an API failed.
The last error code is stored in the thread environment block (TEB). A TEB is a user mode structure that contains various information about each thread.
There's a few different ways to read the TEB and the last error code contained within it. With a debugger there's the choice of several extensions or the TEB can be read directly. An application can use the GetLastError API.
Windows makes use of the FS segment register to store information about the current thread. The address of the TEB for the current thread is stored at FS:[18].
0:000> dd fs:18 l 1
003b:00000018 7ffdf000
The TEB can either be displayed by using the !teb extension or the dt command. The !teb extension will show a formatted view whereas the dt command will show the full structure.
0:000> !teb
TEB at 7ffdf000
ExceptionList: 0007fb00
StackBase: 00080000
StackLimit: 0006f000
...
0:000> dt _TEB 7ffdf000
ntdll!_TEB
+0x000 NtTib : _NT_TIB
+0x01c EnvironmentPointer : (null)
+0x020 ClientId : _CLIENT_ID
...
The last error code is a ULONG value. It's stored in the LastErrorValue field at an offset of 52 (0x34) bytes from the start of the TEB.
0:000> dt _TEB 7ffdf000 LastErrorValue
ntdll!_TEB
+0x034 LastErrorValue : 0
The GetLastError API uses the same information when it returns the last error code. It reads the location of the TEB from the FS register and then returns a ULONG value from 52 bytes into that address range.
7c839325 mov eax,fs:[00000018]
7c83932b mov eax,[eax+0x34]
7c83932e ret
In the debugger it can be entered as one command.
0:000> dd poi(fs:18)+34 l 1
7ffdf034 00000000
There's one final debugger extension and that is !gle. The !gle extension returns both the LastErrorValue and LastStatusValue values. !gle goes one step further and checks several known DLLs in an attempt to locate the error code in any of their message tables. If a match is found then !gle displays both the error code and the error message.
0:000> !gle
LastErrorValue: (Win32) 0 (0) - The operation completed ...
LastStatusValue: (NTSTATUS) 0 - STATUS_WAIT_0
The LastErrorValue field is also writable. DLLs can call SetLastError to set the error code. Ever since Windows XP, SetLastError has an undocumented trick up it's sleeve. The following information applies to both Windows XP and Windows Server 2003.
Hiding inside of kernel32's address space is a global variable called g_dwLastErrorToBreakOn. It turns out that SetLastError checks the value of this variable and if it's non-zero, calls DbgBreakPoint if the two conditions match. The downside is that SetLastError is only called from within KERNEL32.DLL. Other calls to SetLastError are redirected to a function located in NTDLL.DLL, RtlSetLastWin32Error.
Still g_dwLastErrorToBreakOn can be extremely useful when trying to track down when an error occurs in Kernel32 APIs and the last error value is set. Of course you can set a breakpoint on SetLastError and check which value is passed but it can be somewhat slow. Using the global variable incurs no overhead as it's always checked.
In Windows Vista, SetLastError in Kernel32 jumps directly to RtlSetLastWin32Error in NTDLL and checks the global, g_dwLastErrorToBreakOn, which is now part of NTDLL.DLL. This allows the last error mechanism for all DLLs to be easily debugged.
The variable needs to be edited directly to enter the value to break on. In this case the error is ERROR_ACCESS_DENIED. For Windows Vista, the global ntdll!g_dwLastErrorToBreakOn should be set instead.
0:000> ed kernel32!g_dwLastErrorToBreakOn 5
Once the breakpoint is triggered, the application breaks into the debugger and a full stack trace can be read.
0:013> k
01c7d524 ntdll!DbgBreakPoint
01c7d534 kernel32!SetLastError+0x24
01c7d544 kernel32!BaseSetLastNTError+0x17
01c7d828 kernel32!FindFirstFileExW+0x4c5
01c7d84c shell32!SHFindFirstFile+0x2a
01c7daa0 shell32!SHFindFirstFileRetry+0x5b
01c7dcf0 shell32!CFileSysEnum::Init+0x14b
...
All error codes that are returned by the operating system are listed in winerror.h. The MSDN Library also maintains a list of system error codes. Application defined error codes will not be listed.