_Great_
07.04.2007, 11:58
Article: Выявление ошибок памяти
Author: Great
Date: 06.04.2007
Lang: C/C++ user mode
Note: В статье описаны действенные методы для выявления таких ошибок в программах, как переполнение буфера или утечек памяти.
Переполнение буфера - очень частая ошибка программистов, особенно начинающих - попытка записи в буфер за его пределами. Конечно, мы не сможем контролировать переполнение локального буфера, но мы сможем контролировать все операции с динамическими буферами. Ошибки такого типа трудноуловимы, потому что результат ошибки проявляется не немедленно, а перед ним может пройти значительное время. Например, программа выделяет два буфера - строку и управляющую структуру. Потом строка случайно перезаписыватся так, что происходит ее переполнение и затирается часть идущей за ней структуры, т.к. буфера выделяются в куче непосредственно друг за другом. Но эта ошибка проявится только при следующем использовании структуры. Таким образом, было бы неплохо отловить то место, когда происходит выход за границы буфера. Кстати, возможна и обратная ситуация: в результате неправильного вычисления индекса массива происходит обращение к элементу, находящемуся до начала массива. Такие ошибки менее часто встречаются, но их так же трудно отловить.
Существует один простой метод для "отлова" ошибок такого рода:
а) для обнаружения переполнений все буфера в программе выделяются особым образом: резервируется идущих подряд страницы виртуальной памяти, дальше первая из них передается (commit) в использование и буфер размещается на границе страниц в конце первой. Следующая страница недействительная. Любая попытка выхода за пределы буфера повлечет немедленно исключение нарушения доступа:
http://gr8.cih.ms/uploads/overflow.png
Для большей надежности оставшаяся часть страницы заполняется шаблоном, который потом проверяется на целостность.
Примерный код для аллокации и освобождения (откомментирован):
#define OVERFLOW_GUARD 1
#define UNDERFLOW_GUARD 2
#define BUFFER_GUARD OVERFLOW_GUARD
//#define BUFFER_GUARD UNDERFLOW_GUARD
//#define SIMULATE_MEMORY_LACK 1
#define NUMBERS_OF_MEMORY_REQUESTS_TO_FAIL_AT 2
struct __allocation {
void* mem;
int len;
char guard_type;
} __allocs [1024];
// new[] operator handler for the overflow-guard protection mode
// Allocates buffer at the end of the page, next page will be marked as invalid.
// Any access behind the end of the buffer will be failed
void* AllocateOverflowGuardedBuffer(int size)
{
SYSTEM_INFO si = {0};
GetSystemInfo( &si );
if( size > (signed)si.dwPageSize ) return NULL; // can't allocate buffer greater than one page now...
void* mem = VirtualAlloc( NULL, si.dwPageSize*2, MEM_RESERVE, PAGE_NOACCESS ); // reserve pages
if( !mem ) return NULL;
mem = VirtualAlloc( mem, si.dwPageSize, MEM_COMMIT, PAGE_READWRITE ); // commit first page
// fill template
DWORD ps = si.dwPageSize - size;
__asm
{
mov al, 0xfd
mov ecx, ps
mov edi, mem
rep stosb
}
mem = (LPVOID)( (DWORD)mem + si.dwPageSize - size );
// save info about allocation
for( int i=0; i<sizeof(__allocs)/sizeof(*__allocs); i++ )
if( !__allocs[i].mem )
{
__allocs[i].guard_type = OVERFLOW_GUARD;
__allocs[i].len = size;
return (__allocs[i].mem = mem);
}
// not enough memory to save allocation info, free pages and exit with error
VirtualFree( mem, si.dwPageSize, MEM_DECOMMIT );
VirtualFree( mem, si.dwPageSize*2, MEM_RELEASE );
return NULL;
}
// delete[] operator handler for the overflow-guard protection mode
void FreeOverflowGuardedBuffer(void* mem)
{
SYSTEM_INFO si = {0};
GetSystemInfo( &si );
// find our allocation in allocation table
for( int i=0; i<sizeof(__allocs)/sizeof(*__allocs); i++ )
if( __allocs[i].mem == mem )
{
if( __allocs[i].len == -1 ) // special value indicating freed buffer. don't clear entry in table.
{
DebugMessage(
"Attempt to free already freed buffer 0x%08x.\n"
"Possible it's a result of incorrect destructor call."
,
mem
);
return;
}
// check pattern
long ps = si.dwPageSize - __allocs[i].len;
LPVOID addr = (LPVOID)( (DWORD)mem + __allocs[i].len - si.dwPageSize );
for( int j=0; j < ps; j++ )
if( ((unsigned char*)addr)[j] != 0xfd )
break;
ps -= j;
// pattern mismatch
if( ps )
{
DebugMessage(
"Buffer underflow detected while overflow-guard protection at address 0x%08x.\n"
"It's highly recommended to test program in underflow-guard mode to detect faulting instruction."
,
mem
);
}
// free buffer and return
VirtualFree( mem, si.dwPageSize, MEM_DECOMMIT );
VirtualFree( mem, si.dwPageSize*2, MEM_RELEASE );
__allocs[i].len = -1;
return;
}
}
Функция DebugMessage выводит диалоговое окно с предложениями завершить программу, отладить программу или проигнорировать сообщение. Ее объявление выглядит так: void DebugMessage(char* s, ...), а ее тело можно найти в исходнике к статье.
б) для обнаружения выхода за границы буфера с другой стороны ("недополнение" буфера) аналогичная методика: резервируются две страницы, вторая из них передается и буфер выделяется в нее начале, предыдущая страница недействительная. Попытки записи перед буфером повлекут исключение нарушения доступа. Оставшаяся часть страницы снова заполняется шаблоном, который при освобождении проверяется на целостность. Код выглядит похоже на предыдущий:
// new[] operator handler for the underflow-guard protection mode
// Allocates buffer at the beginning of the page, previous page will be marked as invalid.
// Any access before the beginning of the buffer will be failed
void* AllocateUnderflowGuardedBuffer(int size)
{
SYSTEM_INFO si = {0};
GetSystemInfo( &si );
if( size > (signed)si.dwPageSize ) return NULL;
void* mem = VirtualAlloc( NULL, si.dwPageSize*2, MEM_RESERVE, PAGE_NOACCESS );
if( !mem ) return NULL;
mem = (LPVOID)( (DWORD)mem + si.dwPageSize );
mem = VirtualAlloc( mem, si.dwPageSize, MEM_COMMIT, PAGE_READWRITE );
DWORD ps = si.dwPageSize - size;
LPVOID addr = (LPVOID)( (DWORD)mem + size );
__asm
{
mov al, 0xfd
mov ecx, ps
mov edi, addr
rep stosb
}
for( int i=0; i<sizeof(__allocs)/sizeof(*__allocs); i++ )
if( !__allocs[i].mem )
{
__allocs[i].guard_type = UNDERFLOW_GUARD;
__allocs[i].len = size;
return (__allocs[i].mem = mem);
}
VirtualFree( (LPVOID)( (DWORD)mem + si.dwPageSize ), si.dwPageSize, MEM_DECOMMIT );
VirtualFree( mem, si.dwPageSize*2, MEM_RELEASE );
return NULL;
}
// delete[] operator handler for the underflow-guard protection mode
// Checks memory for overflowing too. But complete debugging of buffer overflow should be performed in
// overflow-guard mode
void FreeUnderflowGuardedBuffer(void* mem)
{
SYSTEM_INFO si = {0};
GetSystemInfo( &si );
for( int i=0; i<sizeof(__allocs)/sizeof(*__allocs); i++ )
if( __allocs[i].mem == mem )
{
if( __allocs[i].len == -1 )
{
DebugMessage(
"Attempt to free already freed buffer 0x%08x.\n"
"Possible it's a result of incorrect destructor call."
,
mem
);
return;
}
long ps = si.dwPageSize - __allocs[i].len;
LPVOID addr = (LPVOID)( (DWORD)mem + __allocs[i].len );
for( int j=0; j < ps; j++ )
if( ((unsigned char*)addr)[j] != 0xfd )
break;
ps -= j;
if( ps )
{
DebugMessage(
"Buffer overflow detected while underflow-guard protection at address 0x%08x.\n"
"It's highly recommended to test program in overflow-guard mode to detect faulting instruction."
,
mem
);
}
VirtualFree( (LPVOID)( (DWORD)mem + si.dwPageSize ), si.dwPageSize, MEM_DECOMMIT );
VirtualFree( mem, si.dwPageSize*2, MEM_RELEASE );
__allocs[i].len = -1;
return;
}
}
Чтобы отловить нарушение доступа, установим свой обработчик и предупредим программиста о возникшей ошибке:
// exception handler
LONG __stdcall MyUnhandledExceptionFilter(EXCEPTION_POINTERS* ep)
{
if( ep->ExceptionRecord->ExceptionCode == STATUS_ACCESS_VIOLATION )
{
for( int i=0; i<sizeof(__allocs)/sizeof(*__allocs); i++ )
{
long addr = ep->ExceptionRecord->ExceptionInformation[1];
long dist;
if(
(addr >= (long)__allocs[i].mem + __allocs[i].len) &&
(addr <= (long)__allocs[i].mem + __allocs[i].len + 10) &&
__allocs[i].guard_type == OVERFLOW_GUARD
)
{
dist = addr - (long)__allocs[i].mem - __allocs[i].len + 1;
DebugMessage(
"Buffer overflow detected!\n"
"Buffer starts at 0x%08x\n"
"Buffer size is %d byte(s) [0x%x]\n"
"Memory referenced: 0x%08x\n"
"Distance between end of buffer: %d byte(s) [0x%x]\n"
"%s"
,
__allocs[i].mem,
__allocs[i].len,
__allocs[i].len,
addr,
dist,
dist,
(dist==1) ?
"\nDistance equals to one byte.\n"
"It's very common case of overflow called one-byte buffer oveflow.\n"
"Possible it is a result of string allocation error (maybe you've forgotten to allocate space for terminating NULL-character?)"
: ""
);
}
else if(
(addr >= (long)__allocs[i].mem - 10) &&
(addr <= (long)__allocs[i].mem) &&
__allocs[i].guard_type == UNDERFLOW_GUARD
)
{
dist = (long)__allocs[i].mem - addr;
DebugMessage(
"Buffer underflow detected!\n"
"Buffer starts at 0x%08x\n"
"Buffer size is %d byte(s) [0x%x]\n"
"Memory referenced: 0x%08x\n"
"Distance between beginning of buffer: %d byte(s) [0x%x]\n"
,
__allocs[i].mem,
__allocs[i].len,
__allocs[i].len,
addr,
dist,
dist
);
}
}
}
return EXCEPTION_CONTINUE_SEARCH;
}
Будем считать переполнением тут любое обращение в пределах 10 байт от границы буфера и будем показывать окошки с сообщениями. Обращения после 10 байт будут просто вызывать Access Violation. В любом случае вернем EXCEPTION_CONTINUE_SEARCH, т.к. мы не предпринимали никаких действий, а только предупредили об ошибке. Управление скорее всего получит системный обработчик, который выдаст до боли знакомое сообщение "программа выполнила недопустимую операцию..." или отладчик, у которого будет второй шанс обработать исключение (second-chance exception) и он остановится на сбойной команде (первый шанс обработать исключение (first-chance exception) был при самом его возникновении).
Устанавливать обработчик и выбирать способ аллокации мы будем в нашем переопределенном операторе new:
// operator new
void* operator new(size_t s)
{
static bool except_handler_set = false;
if( !except_handler_set )
{
except_handler_set = true;
SetUnhandledExceptionFilter( MyUnhandledExceptionFilter );
}
#if SIMULATE_MEMORY_LACK
srand(GetTickCount());
if( rand() % NUMBERS_OF_MEMORY_REQUESTS_TO_FAIL_AT == 0 )
return NULL;
#endif
#if BUFFER_GUARD == OVERFLOW_GUARD
return AllocateOverflowGuardedBuffer( s );
#elif BUFFER_GUARD == UNDERFLOW_GUARD
return AllocateUnderflowGuardedBuffer( s );
#else
# error Unknown BUFFER_GUARD value
#endif
}
// operator delete
void operator delete(void* p)
{
#if BUFFER_GUARD == OVERFLOW_GUARD
FreeOverflowGuardedBuffer( p );
#elif BUFFER_GUARD == UNDERFLOW_GUARD
FreeUnderflowGuardedBuffer( p );
#else
# error Unknown BUFFER_GUARD value
#endif
}
Как можно заметить, мы еще случайно будем отклонять запросы на выделение памяти для тестирования того, как себя будет вести программа в этом случае. Это все задается опционально. Для расширения возможностей отладки можно перехватить и функции выделения памяти в куче: HeapAlloc, LocalAlloc, GlobalAlloc.
Для "отловки" утечек памяти будем после критического региона, работающего с памятью, проверять, освободил ли он всё, что выделял. Для этого нам потребуются две функции: EnterMemoryCheckRegion() и LeaveMemoryCheckRegion(), код которых выглядит следующим образом:
// Clear allocation log __allocs
void EnterMemoryCheckRegion()
{
void* p = __allocs;
int l = sizeof(__allocs) / 4;
__asm
{
mov edi, p
xor eax, eax
mov ecx, l
rep stosd
}
}
// Check for memory leak. Should be called at the end of guarded region of code
void LeaveMemoryCheckRegion()
{
for( int i=0; i<sizeof(__allocs)/sizeof(*__allocs); i++ )
{
if( __allocs[i].mem && __allocs[i].len != -1 )
{
DebugMessage(
"Possible memory leak detected. "
"Buffer at address 0x%08x is not freed at the end of the memory-check region."
,
__allocs[i].mem
);
}
}
}
Вызовами этих функций можно обрамлять участки кода, которые не должны оставлять за собой выделенные участки памяти.
В качестве примера рассмотрим программу, в которой присутствуют несколько ошибок памяти:
// Test class
class A
{
public:
A(int c)
{
printf("!! CONSTRUCTOR [%d] !!\n", c);
}
};
int main()
{
// Clear log
EnterMemoryCheckRegion();
// Create dynamic object
printf("Creating class object\n");
A *b = new A (4);
if( !b )
return printf("Allocation failed (not enough memory?)\n"), 0;
// Allocate dynamic buffer
printf("Allocating\n");
char *a = new char[10];
if( !a )
return printf("Allocation failed (not enough memory?)\n"), 0;
// Valid copying
printf("Valid copying\n");
strcpy(a, "123456789");
printf("Copied, value: '%s'\n", a);
// Invalid copying
printf("Invalid copying\n");
strcpy(a-1, "1234567890");
printf("Copied, value: '%s'\n", a);
printf("Deleting\n");
// Now the overflow will be detected
delete a;
// Second freeing of freed memory
delete a;
// no delete call for the A* b. Memory leak
// It will be detected now
LeaveMemoryCheckRegion();
return 0;
}
При включенной защите от переполнения будет обнаружено:
- "недополнение" буфера (запись перед его началом);
- попытка освобождения уже освобожденной памяти;
- утечка памяти (указатель A* b не был освобожден перед выходом)
Если заменить строчку strcpy(a-1, "1234567890") на strcpy(a, "1234567890"), то произойдет попытка записи 11 байт (10 символов + завершающий ноль) в 10байтный буфер, что будет немедленно обнаружено:
http://gr8.cih.ms/uploads/grd.png
Как видно, программа заботливо сообщает нам о однобайтовом переполнении в динамической памяти.
К сожалению, это не спасает нас от переполнения локальных буферов в стеке, но это уже отдельная тема. А здесь было рассмотрено обнаружение ошибок манипуляции с динамической памятью. На этом, пожалуй, я и закончу. Пока
Полный исходный код: http://gr8.cih.ms/uploads/dynamic_memory.cpp
Author: Great
Date: 06.04.2007
Lang: C/C++ user mode
Note: В статье описаны действенные методы для выявления таких ошибок в программах, как переполнение буфера или утечек памяти.
Переполнение буфера - очень частая ошибка программистов, особенно начинающих - попытка записи в буфер за его пределами. Конечно, мы не сможем контролировать переполнение локального буфера, но мы сможем контролировать все операции с динамическими буферами. Ошибки такого типа трудноуловимы, потому что результат ошибки проявляется не немедленно, а перед ним может пройти значительное время. Например, программа выделяет два буфера - строку и управляющую структуру. Потом строка случайно перезаписыватся так, что происходит ее переполнение и затирается часть идущей за ней структуры, т.к. буфера выделяются в куче непосредственно друг за другом. Но эта ошибка проявится только при следующем использовании структуры. Таким образом, было бы неплохо отловить то место, когда происходит выход за границы буфера. Кстати, возможна и обратная ситуация: в результате неправильного вычисления индекса массива происходит обращение к элементу, находящемуся до начала массива. Такие ошибки менее часто встречаются, но их так же трудно отловить.
Существует один простой метод для "отлова" ошибок такого рода:
а) для обнаружения переполнений все буфера в программе выделяются особым образом: резервируется идущих подряд страницы виртуальной памяти, дальше первая из них передается (commit) в использование и буфер размещается на границе страниц в конце первой. Следующая страница недействительная. Любая попытка выхода за пределы буфера повлечет немедленно исключение нарушения доступа:
http://gr8.cih.ms/uploads/overflow.png
Для большей надежности оставшаяся часть страницы заполняется шаблоном, который потом проверяется на целостность.
Примерный код для аллокации и освобождения (откомментирован):
#define OVERFLOW_GUARD 1
#define UNDERFLOW_GUARD 2
#define BUFFER_GUARD OVERFLOW_GUARD
//#define BUFFER_GUARD UNDERFLOW_GUARD
//#define SIMULATE_MEMORY_LACK 1
#define NUMBERS_OF_MEMORY_REQUESTS_TO_FAIL_AT 2
struct __allocation {
void* mem;
int len;
char guard_type;
} __allocs [1024];
// new[] operator handler for the overflow-guard protection mode
// Allocates buffer at the end of the page, next page will be marked as invalid.
// Any access behind the end of the buffer will be failed
void* AllocateOverflowGuardedBuffer(int size)
{
SYSTEM_INFO si = {0};
GetSystemInfo( &si );
if( size > (signed)si.dwPageSize ) return NULL; // can't allocate buffer greater than one page now...
void* mem = VirtualAlloc( NULL, si.dwPageSize*2, MEM_RESERVE, PAGE_NOACCESS ); // reserve pages
if( !mem ) return NULL;
mem = VirtualAlloc( mem, si.dwPageSize, MEM_COMMIT, PAGE_READWRITE ); // commit first page
// fill template
DWORD ps = si.dwPageSize - size;
__asm
{
mov al, 0xfd
mov ecx, ps
mov edi, mem
rep stosb
}
mem = (LPVOID)( (DWORD)mem + si.dwPageSize - size );
// save info about allocation
for( int i=0; i<sizeof(__allocs)/sizeof(*__allocs); i++ )
if( !__allocs[i].mem )
{
__allocs[i].guard_type = OVERFLOW_GUARD;
__allocs[i].len = size;
return (__allocs[i].mem = mem);
}
// not enough memory to save allocation info, free pages and exit with error
VirtualFree( mem, si.dwPageSize, MEM_DECOMMIT );
VirtualFree( mem, si.dwPageSize*2, MEM_RELEASE );
return NULL;
}
// delete[] operator handler for the overflow-guard protection mode
void FreeOverflowGuardedBuffer(void* mem)
{
SYSTEM_INFO si = {0};
GetSystemInfo( &si );
// find our allocation in allocation table
for( int i=0; i<sizeof(__allocs)/sizeof(*__allocs); i++ )
if( __allocs[i].mem == mem )
{
if( __allocs[i].len == -1 ) // special value indicating freed buffer. don't clear entry in table.
{
DebugMessage(
"Attempt to free already freed buffer 0x%08x.\n"
"Possible it's a result of incorrect destructor call."
,
mem
);
return;
}
// check pattern
long ps = si.dwPageSize - __allocs[i].len;
LPVOID addr = (LPVOID)( (DWORD)mem + __allocs[i].len - si.dwPageSize );
for( int j=0; j < ps; j++ )
if( ((unsigned char*)addr)[j] != 0xfd )
break;
ps -= j;
// pattern mismatch
if( ps )
{
DebugMessage(
"Buffer underflow detected while overflow-guard protection at address 0x%08x.\n"
"It's highly recommended to test program in underflow-guard mode to detect faulting instruction."
,
mem
);
}
// free buffer and return
VirtualFree( mem, si.dwPageSize, MEM_DECOMMIT );
VirtualFree( mem, si.dwPageSize*2, MEM_RELEASE );
__allocs[i].len = -1;
return;
}
}
Функция DebugMessage выводит диалоговое окно с предложениями завершить программу, отладить программу или проигнорировать сообщение. Ее объявление выглядит так: void DebugMessage(char* s, ...), а ее тело можно найти в исходнике к статье.
б) для обнаружения выхода за границы буфера с другой стороны ("недополнение" буфера) аналогичная методика: резервируются две страницы, вторая из них передается и буфер выделяется в нее начале, предыдущая страница недействительная. Попытки записи перед буфером повлекут исключение нарушения доступа. Оставшаяся часть страницы снова заполняется шаблоном, который при освобождении проверяется на целостность. Код выглядит похоже на предыдущий:
// new[] operator handler for the underflow-guard protection mode
// Allocates buffer at the beginning of the page, previous page will be marked as invalid.
// Any access before the beginning of the buffer will be failed
void* AllocateUnderflowGuardedBuffer(int size)
{
SYSTEM_INFO si = {0};
GetSystemInfo( &si );
if( size > (signed)si.dwPageSize ) return NULL;
void* mem = VirtualAlloc( NULL, si.dwPageSize*2, MEM_RESERVE, PAGE_NOACCESS );
if( !mem ) return NULL;
mem = (LPVOID)( (DWORD)mem + si.dwPageSize );
mem = VirtualAlloc( mem, si.dwPageSize, MEM_COMMIT, PAGE_READWRITE );
DWORD ps = si.dwPageSize - size;
LPVOID addr = (LPVOID)( (DWORD)mem + size );
__asm
{
mov al, 0xfd
mov ecx, ps
mov edi, addr
rep stosb
}
for( int i=0; i<sizeof(__allocs)/sizeof(*__allocs); i++ )
if( !__allocs[i].mem )
{
__allocs[i].guard_type = UNDERFLOW_GUARD;
__allocs[i].len = size;
return (__allocs[i].mem = mem);
}
VirtualFree( (LPVOID)( (DWORD)mem + si.dwPageSize ), si.dwPageSize, MEM_DECOMMIT );
VirtualFree( mem, si.dwPageSize*2, MEM_RELEASE );
return NULL;
}
// delete[] operator handler for the underflow-guard protection mode
// Checks memory for overflowing too. But complete debugging of buffer overflow should be performed in
// overflow-guard mode
void FreeUnderflowGuardedBuffer(void* mem)
{
SYSTEM_INFO si = {0};
GetSystemInfo( &si );
for( int i=0; i<sizeof(__allocs)/sizeof(*__allocs); i++ )
if( __allocs[i].mem == mem )
{
if( __allocs[i].len == -1 )
{
DebugMessage(
"Attempt to free already freed buffer 0x%08x.\n"
"Possible it's a result of incorrect destructor call."
,
mem
);
return;
}
long ps = si.dwPageSize - __allocs[i].len;
LPVOID addr = (LPVOID)( (DWORD)mem + __allocs[i].len );
for( int j=0; j < ps; j++ )
if( ((unsigned char*)addr)[j] != 0xfd )
break;
ps -= j;
if( ps )
{
DebugMessage(
"Buffer overflow detected while underflow-guard protection at address 0x%08x.\n"
"It's highly recommended to test program in overflow-guard mode to detect faulting instruction."
,
mem
);
}
VirtualFree( (LPVOID)( (DWORD)mem + si.dwPageSize ), si.dwPageSize, MEM_DECOMMIT );
VirtualFree( mem, si.dwPageSize*2, MEM_RELEASE );
__allocs[i].len = -1;
return;
}
}
Чтобы отловить нарушение доступа, установим свой обработчик и предупредим программиста о возникшей ошибке:
// exception handler
LONG __stdcall MyUnhandledExceptionFilter(EXCEPTION_POINTERS* ep)
{
if( ep->ExceptionRecord->ExceptionCode == STATUS_ACCESS_VIOLATION )
{
for( int i=0; i<sizeof(__allocs)/sizeof(*__allocs); i++ )
{
long addr = ep->ExceptionRecord->ExceptionInformation[1];
long dist;
if(
(addr >= (long)__allocs[i].mem + __allocs[i].len) &&
(addr <= (long)__allocs[i].mem + __allocs[i].len + 10) &&
__allocs[i].guard_type == OVERFLOW_GUARD
)
{
dist = addr - (long)__allocs[i].mem - __allocs[i].len + 1;
DebugMessage(
"Buffer overflow detected!\n"
"Buffer starts at 0x%08x\n"
"Buffer size is %d byte(s) [0x%x]\n"
"Memory referenced: 0x%08x\n"
"Distance between end of buffer: %d byte(s) [0x%x]\n"
"%s"
,
__allocs[i].mem,
__allocs[i].len,
__allocs[i].len,
addr,
dist,
dist,
(dist==1) ?
"\nDistance equals to one byte.\n"
"It's very common case of overflow called one-byte buffer oveflow.\n"
"Possible it is a result of string allocation error (maybe you've forgotten to allocate space for terminating NULL-character?)"
: ""
);
}
else if(
(addr >= (long)__allocs[i].mem - 10) &&
(addr <= (long)__allocs[i].mem) &&
__allocs[i].guard_type == UNDERFLOW_GUARD
)
{
dist = (long)__allocs[i].mem - addr;
DebugMessage(
"Buffer underflow detected!\n"
"Buffer starts at 0x%08x\n"
"Buffer size is %d byte(s) [0x%x]\n"
"Memory referenced: 0x%08x\n"
"Distance between beginning of buffer: %d byte(s) [0x%x]\n"
,
__allocs[i].mem,
__allocs[i].len,
__allocs[i].len,
addr,
dist,
dist
);
}
}
}
return EXCEPTION_CONTINUE_SEARCH;
}
Будем считать переполнением тут любое обращение в пределах 10 байт от границы буфера и будем показывать окошки с сообщениями. Обращения после 10 байт будут просто вызывать Access Violation. В любом случае вернем EXCEPTION_CONTINUE_SEARCH, т.к. мы не предпринимали никаких действий, а только предупредили об ошибке. Управление скорее всего получит системный обработчик, который выдаст до боли знакомое сообщение "программа выполнила недопустимую операцию..." или отладчик, у которого будет второй шанс обработать исключение (second-chance exception) и он остановится на сбойной команде (первый шанс обработать исключение (first-chance exception) был при самом его возникновении).
Устанавливать обработчик и выбирать способ аллокации мы будем в нашем переопределенном операторе new:
// operator new
void* operator new(size_t s)
{
static bool except_handler_set = false;
if( !except_handler_set )
{
except_handler_set = true;
SetUnhandledExceptionFilter( MyUnhandledExceptionFilter );
}
#if SIMULATE_MEMORY_LACK
srand(GetTickCount());
if( rand() % NUMBERS_OF_MEMORY_REQUESTS_TO_FAIL_AT == 0 )
return NULL;
#endif
#if BUFFER_GUARD == OVERFLOW_GUARD
return AllocateOverflowGuardedBuffer( s );
#elif BUFFER_GUARD == UNDERFLOW_GUARD
return AllocateUnderflowGuardedBuffer( s );
#else
# error Unknown BUFFER_GUARD value
#endif
}
// operator delete
void operator delete(void* p)
{
#if BUFFER_GUARD == OVERFLOW_GUARD
FreeOverflowGuardedBuffer( p );
#elif BUFFER_GUARD == UNDERFLOW_GUARD
FreeUnderflowGuardedBuffer( p );
#else
# error Unknown BUFFER_GUARD value
#endif
}
Как можно заметить, мы еще случайно будем отклонять запросы на выделение памяти для тестирования того, как себя будет вести программа в этом случае. Это все задается опционально. Для расширения возможностей отладки можно перехватить и функции выделения памяти в куче: HeapAlloc, LocalAlloc, GlobalAlloc.
Для "отловки" утечек памяти будем после критического региона, работающего с памятью, проверять, освободил ли он всё, что выделял. Для этого нам потребуются две функции: EnterMemoryCheckRegion() и LeaveMemoryCheckRegion(), код которых выглядит следующим образом:
// Clear allocation log __allocs
void EnterMemoryCheckRegion()
{
void* p = __allocs;
int l = sizeof(__allocs) / 4;
__asm
{
mov edi, p
xor eax, eax
mov ecx, l
rep stosd
}
}
// Check for memory leak. Should be called at the end of guarded region of code
void LeaveMemoryCheckRegion()
{
for( int i=0; i<sizeof(__allocs)/sizeof(*__allocs); i++ )
{
if( __allocs[i].mem && __allocs[i].len != -1 )
{
DebugMessage(
"Possible memory leak detected. "
"Buffer at address 0x%08x is not freed at the end of the memory-check region."
,
__allocs[i].mem
);
}
}
}
Вызовами этих функций можно обрамлять участки кода, которые не должны оставлять за собой выделенные участки памяти.
В качестве примера рассмотрим программу, в которой присутствуют несколько ошибок памяти:
// Test class
class A
{
public:
A(int c)
{
printf("!! CONSTRUCTOR [%d] !!\n", c);
}
};
int main()
{
// Clear log
EnterMemoryCheckRegion();
// Create dynamic object
printf("Creating class object\n");
A *b = new A (4);
if( !b )
return printf("Allocation failed (not enough memory?)\n"), 0;
// Allocate dynamic buffer
printf("Allocating\n");
char *a = new char[10];
if( !a )
return printf("Allocation failed (not enough memory?)\n"), 0;
// Valid copying
printf("Valid copying\n");
strcpy(a, "123456789");
printf("Copied, value: '%s'\n", a);
// Invalid copying
printf("Invalid copying\n");
strcpy(a-1, "1234567890");
printf("Copied, value: '%s'\n", a);
printf("Deleting\n");
// Now the overflow will be detected
delete a;
// Second freeing of freed memory
delete a;
// no delete call for the A* b. Memory leak
// It will be detected now
LeaveMemoryCheckRegion();
return 0;
}
При включенной защите от переполнения будет обнаружено:
- "недополнение" буфера (запись перед его началом);
- попытка освобождения уже освобожденной памяти;
- утечка памяти (указатель A* b не был освобожден перед выходом)
Если заменить строчку strcpy(a-1, "1234567890") на strcpy(a, "1234567890"), то произойдет попытка записи 11 байт (10 символов + завершающий ноль) в 10байтный буфер, что будет немедленно обнаружено:
http://gr8.cih.ms/uploads/grd.png
Как видно, программа заботливо сообщает нам о однобайтовом переполнении в динамической памяти.
К сожалению, это не спасает нас от переполнения локальных буферов в стеке, но это уже отдельная тема. А здесь было рассмотрено обнаружение ошибок манипуляции с динамической памятью. На этом, пожалуй, я и закончу. Пока
Полный исходный код: http://gr8.cih.ms/uploads/dynamic_memory.cpp