ID Number: Q64521
5.10 6.00 6.00a
OS/2
Summary:
In Microsoft C versions 5.1, 6.0, and 6.0a, on rare occasions, a
protection violation may occur under OS/2 while a program is executing
the code for the Microsoft C run-time library function malloc(). This
type of problem can usually be traced to a situation where a program
makes repeated calls to malloc() and at some point between calls
something overwrites its bounds in the dynamically allocated memory.
If this happens and the internal data structures in the heap are
corrupted, the next call to malloc() will inadvertently try to
allocate memory that may not really be available, resulting in the
protection violation.
More Information:
The malloc() function gives you memory in the size you request;
however, malloc() gets the memory from the operating system (OS) in
"blocks". This block size is determined by the value of _amblksiz,
which is 8K by default. Therefore, if you request 100 bytes on your
first malloc() call, malloc() will actually get 8K from the operating
system to satisfy your request. If the next request can be satisfied
from the remainder of the 8K block, then it will be. If not, then
malloc() requests the number of new 8K blocks needed from the OS to
satisfy the new request.
The result is that you always get memory from the OS in 8K multiples,
but the internal malloc() suballocation routines divide it up for the
program's specific requests. The heap, itself, is essentially this
group of 8K blocks that contain memory that can be used and freed by
your program. As you do more and more malloc() calls, the heap grows.
When a segment boundary limits further expansion, a new segment is
added to the heap.
The heap is structured with a 12-byte header at the beginning of the
segment and then there is a 2-byte header at each new allocation.
These headers contain a 2-byte number that is the size of the
allocated amount. If the last bit is a 0 (zero), then the memory is
being used; if the last bit is a 1, then the memory has been freed and
is available.
Because of this scheme, all allocations must be in even-byte amounts.
Requests for odd amounts are satisfied with an even number of bytes of
memory. Odd values are used only to show availability. The top end of
the current heap segment also contains a header with a flag (FFFE),
which indicates the end of the heap segment.
As an example, assume three calls to malloc() are made for 2K, 3K, and
2K bytes. The heap would then appear as follows:
______________
| FFFE | 8K
|------------|
| 1002 |
| remaining |
| bytes |
|------------|
Rover -------> | 3EDh | <--- Next allocation header
|------------| (odd = available)
| 2K |
| allocation |
|------------|
| 800h | <--- Allocated memory header #3
|------------| (even = in use)
| |
| 3K |
| allocation |
|------------|
| C00h | <--- Allocated memory header #2
|------------| (even = in use)
| 2K |
| allocation |
|------------|
| 800h | <--- Allocated memory header #1
|------------| (even = in use)
| HEADER |
|____________| <--- Heap segment header
The rover (shown in the diagram above) is a pointer that always points
to the header of the next new allocation area or to the header of the
last freed heap area, if a segment was just freed. Either way, the
rover is always pointing to an available memory area in the heap.
When you request more memory, the header at the rover is checked
first. If that current header does not show enough space, then the
rover advances to the next header and another check is done. If the
space is in use or not big enough, then the rover continues to the
next. If the rover reaches the top of the segment, it wraps around to
the bottom. The rover knows where the next header is each time by
looking at the current header and moving that many bytes ahead (less 1
if that amount is odd). If the current header shows FFFE, the rover
knows to wrap around to the bottom.
If the rover finally works its way back to its starting point, it
means that no currently free section of the heap is big enough to
satisfy the request, so an attempt is then made to grow the segment.
If that is not possible, then a new segment is added to the heap.
To continue the example, assume that you have the above scenario and
the heap is as shown. Let's also assume that you now want 5K of
memory. If all is well, the search (as described in the preceding
paragraphs) will take place, but no block will be found in the current
heap that is large enough. Therefore, the heap will be grown with
another 8K block from the OS and the actual heap allocation will be
made right where the rover is currently pointing. The program will
behave as expected.
On the other hand, let's assume that before asking for more memory, a
pointer used to deal with data in the second allocated memory section
inadvertently goes out of range and happens to cause the "allocated
memory header #3" to be written over. Let's also assume that this
overwrite causes the value at "allocated memory header #3" (when
interpreted as an integer) to become an odd number larger than 5K (for
example, 8AC1h):
.
.
.
| 2K |
| allocation |
|------------|
This value --> | 8AC1h | <--- Allocated memory header #3
is now garbage |------------| (now odd = available)
| |
| 3K |
| allocation |
|------------|
| C00h | <--- Allocated memory header #2
.
.
.
Then, assuming that we try to malloc() our desired 5K, the header at
the rover will be checked first. Since 3EDh is not large enough, the
rover jumps to the next header at the top of the heap. The FFFE header
signals that the rover should wrap to the bottom of the heap segment.
It gets to the bottom and the first header says the memory is in use.
It goes to the second header and this, too, is unavailable.
Now, the critical part happens. The rover goes up the correct number
of bytes to read the third header, but the value there is 8AC1h. This
is odd (= available) and greater than 5K, so malloc() thinks it has
found its spot. Before returning, malloc() counts off the 5K bytes and
tries to write the new header for the next allocation. Of course this
is out of the range of memory that has already been allocated to us by
the OS, so you get a protection violation when it tries to write.
There are many other possible scenarios, but the result is essentially
the same. If you corrupt the heap inadvertently, you're likely to
crash with a protection violation. Under MS-DOS, you'll end up
trashing a lot of memory. To resolve this problem, you must look for
places in the allocated data where you could be going out-of-range.
There are a number of heap-checking functions available for
identifying problems in the heap (for example, _heapchk, _heapwalk,
and so forth). If you are suffering from malloc() or heap problems,
then a good debugging plan is to check the heap with these functions
before each new malloc() call, until the heap-damaging area of the
code is isolated.
Additional reference words: 6.0 6.00 6.0a 6.00a 5.1 5.10 GPF gpfault
gp-fault