Debug Memory Manager


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;
  pc1 := AllocMem(4);
  pc1[0] := '1';
  pc1[1] := '2';
  pc1[2] := '3';
  pc1[3] := '4';
  strlen(pc1);  // <- buffer overrun (read access)

procedure OverrunExample2;
var buf : PWideChar;
  GetMem(buf, MAX_PATH);                 // this should have been MAX_PATH * sizeOf(WideChar)
  GetModuleFileNameW(0, buf, MAX_PATH);  // <- buffer overrun (write access)

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);  // index = -1
  str[index] := #0;  // <- buffer underrun (write access)

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;
  obj := TSomeObject.Create;
  obj.SomeProperty := 100;
  obj.Free;                     // "obj" got freed here
  str := IntToStr(something);   // the released memory might already be reused for the string allocation here
  obj.SomeProperty := 200;      // this might destroy the contents of the string

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:

// with "instantly crash on buffer *overrun* activated":

GetMem(1)    ->  xxxxxFFC
GetMem(4)    ->  xxxxxFFC
GetMem(5)    ->  xxxxxFF8
GetMem($80)  ->  xxxxxF80

// with "instantly crash on buffer *underrun* activated":

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???'

// with "instantly crash on buffer *overrun* activated":

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

// with "instantly crash on buffer *underrun* activated":

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.