Introduction...
The madExcept debug memory manager replaces the Delphi memory manager in
your exe/dll file, if you activate the "instantly crash on buffer over/underrun"
option. The debug memory manager has the purpose of raising exceptions
whenever your code does something really bad, like overrun oder underrun a
buffer, or accessing memory which was already freed. With a normal memory
manager, doing such things often has no direct consequence. Instead such bugs
in your code just modify random memory. Minutes or even hours later this
can result in wild crashes in a totally different part of your code, maybe even
in a different thread. Such wild crashes are extremely hard to fix because
even with the best information available about the wild crash, in the best case
all you can just find out is that some memory didn't have the content it was
supposed to have. That won't help you one bit finding out how the memory got
the wrong content. Having your code crash at once when a buffer
overrun/underrun is performed, or when your code accesses freed memory, helps a
lot here because the crash information will directly lead you to the buggy
code.
Buffer overruns...
First let's look at a couple of typical buffer overruns:
|
procedure OverrunExample1;
var pc1 : PAnsiChar;
begin
pc1 := AllocMem(4);
pc1[0] := '1';
pc1[1] := '2';
pc1[2] := '3';
pc1[3] := '4';
strlen(pc1);
[...]
end;
procedure OverrunExample2;
var buf : PWideChar;
begin
GetMem(buf, MAX_PATH);
GetModuleFileNameW(0, buf, MAX_PATH);
[...]
end;
|
|
So what happens if you activate the "instantly crash on buffer overrun"
feature with code like the above? You'll get an access violation in the moment
when the first byte outside of the allocated buffer is read or written. The
crash stack trace will lead you directly to the code which needs to be fixed.
With a normal memory manager, there's a good chance all of the code above
would run through "fine", which would result in random memory being
overwritten.
Buffer underruns...
Buffer underruns are a lot rarer than overruns, but they do occur, too.
Here's how it could happen:
|
procedure UnderrunExample(str: PAnsiChar; index: integer);
begin
str[index] := #0;
end;
|
|
The madExcept debug memory manager cannot detect buffer overruns *and*
underruns at the same time, for technical reasons. You have to choose whether
you want exceptions to be raised for overruns or underruns.
Accessing freed memory...
Another dangerous thing to do is accessing memory which was already freed.
With a normal memory manager, the freed memory will usually still be
accessible because it's going to be reused for a future allocation. Modifying
already freed memory does not necessarily have to result in problems, depending
on whether the memory is already in use again by another allocation or not. If
the freed memory isn't reused yet, modifying its content won't harm. But if
the freed memory is already in use again by some other part of your code,
writing to it could destroy important data. Let's look at an example:
|
procedure AccessingFreedMemoryExample;
var obj : TSomeObject;
str : string;
begin
obj := TSomeObject.Create;
obj.SomeProperty := 100;
obj.Free;
str := IntToStr(something);
obj.SomeProperty := 200;
end;
|
|
The debug memory manager will make sure that freed memory stays unaccessible
for a certain time. It will only be reused with a certain delay. So as a result
if you access a buffer/object/whatever which was already freed, you will get
an instant crash. The crash information is a bit more difficult to interpret,
compared to the buffer overruns/underruns, but it's still much better having
a crash point to "obj.SomeProperty := 200" than having that code overwrite
random memory, which could result in unpredictable behaviour of your program
some minutes later.
Is this Access Violation a buffer overrun/underrun?
In order to get an understanding of how the debug memory manager works,
let's first look at where buffers are typically allocated by the debug memory
manager. For overrun detection, buffers are allocated at the very end of a
memory page, but aligned to 4 bytes. For underrun detection, buffers are always
allocated at the very start of a memory page:
|
GetMem(1) -> xxxxxFFC
GetMem(4) -> xxxxxFFC
GetMem(5) -> xxxxxFF8
GetMem($80) -> xxxxxF80
GetMem(1) -> xxxxx000
GetMem(4) -> xxxxx000
GetMem(5) -> xxxxx000
GetMem($80) -> xxxxx000
|
|
Now let's look at a few AV messages and how to interpret them, when using
the debug memory manager:
|
'Access violation at address xxxxxxxx in module "some.exe". Write of address xxxxx???'
xxxxx000 -> probably a buffer overrun
xxxxx004 -> probably a buffer overrun
xxxxxFF4 -> probably accessing freed memory; buffer size >= $00C bytes
xxxxxF80 -> probably accessing freed memory; buffer size >= $080 bytes
xxxxx810 -> probably accessing freed memory; buffer size >= $7F0 bytes
xxxxxFFF -> probably a buffer underrun
xxxxxFFC -> probably a buffer underrun
xxxxx000 -> probably accessing freed memory; buffer size unknown
xxxxx040 -> probably accessing freed memory; buffer size > $040 bytes
xxxxx138 -> probably accessing freed memory; buffer size > $138 bytes
|
|
The debug memory manager has one significant shortcoming: Due to the way it
is designed, every allocation consumes at least one memory page (that is 4KB)
of RAM. Additionally, another memory page needs to be reserved (but not
allocated) for every allocation, so that buffer overruns/underruns really
produce a crash instead of accessing a different allocation. The reserved pages
don't consume RAM, but they do cost address space in your process.
So e.g. an "AllocMem(1)" call will consume 4KB of RAM and 8KB of address
space. The consumed RAM is actually less critical than the consumed memory
address space. If your application does a lot of allocations, you might sooner
or later run out of address space, no matter how much physical RAM your PC has
or how large your pagefile is. The debug memory manager will in that case
raise an "Out of memory" exception.