Chances are if you’ve written C or C++ that you’ve used a debugger. Having written C/C++, you’re also likely to have cut your teeth on handling memory. Recently, I ran into a runtime error that caught my attention.
The mistake was simple, my code accessed unmapped memory. To illustrate an issue of the same type, consider the following example program.
int main()
{
int* p = (int*)0x1122334455667788;
return *p;
}
This program is almost certain to fail. There is a slight chance that 0x1122334455667788
is actually valid memory by
chance. Despite some low probability of success, we expect this program to fail.
The reason we expect it to fail is that we don’t expect 0x1122334455667788
to be a mapped address. So, if we try to
read four bytes from that location, we should see the program crashing. Visual Studio displays a message with the
following text.
Exception Thrown
Exception thrown: read access violation.
address was 0xFFFFFFFFFFFFFFFF.
The address it presents is somewhat peculiar. The address we tried to access is not the one reflected in the message. So, let’s try to alter the program a little bit.
int main()
{
int* p = (int*)0x1122334455667788;
return *(p + 10);
}
Notice that we now access p + 10
. Given the size of an int
, in this case four bytes, the offset should be:
0x1122334455667788 + 10 * 4 = 0x11223344556677B0
If we run this program, we get an even stranger message.
Exception Thrown
Exception thrown: read access violation.
address was 0xFFFFFFFFFFFFFFD7.
The message now says that p is 0xFFFFFFFFFFFFFFD7
which is not true at all. But what causes this message? The
instruction that triggers the crash is mov eax, dword ptr [rax+28h]
. In the former example, without the offset, the
instruction was mov eax, dword ptr [rax]
. In both cases, rax
is in fact 0x1122334455667788
so the debugger should
be able to show us the right address. If we run the latest version of the program in WinDbg, we get the following
message.
CppTestProject!main+0x3c:
00000000`00d8173c 8b4028
mov eax,dword ptr [rax+28h] ds:11223344`556677b0=????????
This shows us an invalid address that does make sense. Here the address that we’re not allowed to read from is
0x11223344556677B0
.
So, why didn’t the Visual Studio debugger show the same message?
Trying to invalid memory at lower addresses does not seem to be a problem for the VS debugger. After messing around with
some addresses, it seems the crossing point is at 0x0000800000000000
. At lower addresses, Visual Studio shows the
right address in the exception window. At addresses higher then 0x0000800000000000
, we get that 0xFFFFFFFFFFFFFFFF
address anomaly.
There is a cue about this in the MSDN documentation. This page tells us that the most virtual memory a 64-bit
the process can have is 128 TB
. We’ll assume that TB is in fact tebibyte. 128 TB
is 140737500000000
bytes.
140737500000000
in hexadecimal is 0x0000800000000000
. A process cannot, under Windows 10, have more than 128 TB
of
virtual memory. Visual Studio seems unable to get the correct address outside the 128 TB
range.
But what does the debugger see?
We suspect that reading data from addresses above 0x8000000000
triggers the wrongful message. The question is now
whether it’s the debugger or some underlying mechanism that is to blame. To test if it’s the debugger, we take the
following program. We compile this program into an exe CrashingProgram.exe
.
#include <Windows.h>
int main()
{
Sleep(500);
char* p = (char*)0x800000000000;
char x = *p;
return 0;
}
This program reads a single byte at address 0x8000000000
. Running the program from the debugger, we get the message
that we expect from earlier. We get the access violation error message, and the expected address as 0xFF...
.
We now make another program that launches CrashingProgram.exe
and attach to it. We call this new program
debugger.exe
. This gist contains the full program. debugger.exe
uses the functions provided by WinAPI to
receive debug events. Using a custom program, we can get to know the exception details passed by Windows. Windows will
throw a bunch of debug events during normal execution. The debugger
program ingores most of the debugging events.
The only event that we do care about are access violation exceptions. In case you’re curious, this page lists the
various event types.
Using the debugger
program, we can check the exception information given by Windows. It turns out that the event from
Windows claims that the violation was at 0xFFFFFF...
. If the CrashingProgram
uses an address only one byte lower,
we get the correct address. That is, accessing 0x7FFFFFFFFFFF
, the debug event contains the correct address.
Conclusion
The conclusion of this investigation is somewhat lacking. We’ve reached a point where the strange behavior comes from Windows itself. Digging deeper would involve quite a bit more effort and with uncertain results.
We can summerise our findings though. It turns out that Windows has an internal limit for virtual memory allocation.
This limitation suggests a few things. First, Windows seem unwilling to allow memory allocation at addresses above
0x800000000000
. Further access violation events report address -1
above the limit range. It seems fair to assume
that Windows misreports these addresses with intention.
Last, we saw that WinDbg did report the right address. From this, we must assume that WinDbg is doing some extra work. WinDbg could be cross-checking with register values and current instruction.
Again, we don’t know for sure why Visual Studio shows this wrong message. Though, we do know that if we see an access
violation at address -1
, we should take that with a grain of salt.