Article: Выявление ошибок памяти Author: Great Date: 06.04.2007 Lang: C/C++ user mode Note: В статье описаны действенные методы для выявления таких ошибок в программах, как переполнение буфера или утечек памяти. Переполнение буфера - очень частая ошибка программистов, особенно начинающих - попытка записи в буфер за его пределами. Конечно, мы не сможем контролировать переполнение локального буфера, но мы сможем контролировать все операции с динамическими буферами. Ошибки такого типа трудноуловимы, потому что результат ошибки проявляется не немедленно, а перед ним может пройти значительное время. Например, программа выделяет два буфера - строку и управляющую структуру. Потом строка случайно перезаписыватся так, что происходит ее переполнение и затирается часть идущей за ней структуры, т.к. буфера выделяются в куче непосредственно друг за другом. Но эта ошибка проявится только при следующем использовании структуры. Таким образом, было бы неплохо отловить то место, когда происходит выход за границы буфера. Кстати, возможна и обратная ситуация: в результате неправильного вычисления индекса массива происходит обращение к элементу, находящемуся до начала массива. Такие ошибки менее часто встречаются, но их так же трудно отловить. Существует один простой метод для "отлова" ошибок такого рода: а) для обнаружения переполнений все буфера в программе выделяются особым образом: резервируется идущих подряд страницы виртуальной памяти, дальше первая из них передается (commit) в использование и буфер размещается на границе страниц в конце первой. Следующая страница недействительная. Любая попытка выхода за пределы буфера повлечет немедленно исключение нарушения доступа: Для большей надежности оставшаяся часть страницы заполняется шаблоном, который потом проверяется на целостность. Примерный код для аллокации и освобождения (откомментирован): Code: #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, ...), а ее тело можно найти в исходнике к статье. б) для обнаружения выхода за границы буфера с другой стороны ("недополнение" буфера) аналогичная методика: резервируются две страницы, вторая из них передается и буфер выделяется в нее начале, предыдущая страница недействительная. Попытки записи перед буфером повлекут исключение нарушения доступа. Оставшаяся часть страницы снова заполняется шаблоном, который при освобождении проверяется на целостность. Код выглядит похоже на предыдущий: Code: // 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; } } Чтобы отловить нарушение доступа, установим свой обработчик и предупредим программиста о возникшей ошибке: Code: // 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: Code: // 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(), код которых выглядит следующим образом: Code: // 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 ); } } } Вызовами этих функций можно обрамлять участки кода, которые не должны оставлять за собой выделенные участки памяти. В качестве примера рассмотрим программу, в которой присутствуют несколько ошибок памяти: Code: // 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/dynamic_memory.cpp
поскольку на практике работа с памятью ограничивается стандартными функциями, будет все же проще перегружать стандартные функции работы с памятью, под юникс тут может пригодится библиотека типа dmalloc. но идея использования самого процессора в качестве детектора переполнений хорошая, вот только на каждый new делать VirtualAlloc() расточительно..
Расточительно, конечно, в связи с чем аллокации можно комбинировать. Например, страницы выделяются через одну, на каждой валидной странице вначале расположен буффер с защитой от "недополнений", а в конце - с защитой от переполнений. ВОобще идея классная, я буду ее развивать далее.
Я немного модифицировал код, чтобы можно было выделять больше 0x1000 байт под динамический буфер. Code: #include <stdio.h> #define STATUS_ACCESS_VIOLATION ((NTSTATUS)0xC0000005L) // winnt #define OVERFLOW_GUARD 1 #define BUFFER_GUARD OVERFLOW_GUARD //#define SIMULATE_MEMORY_LACK 1 #define NUMBERS_OF_MEMORY_REQUESTS_TO_FAIL_AT 2 #define ALLOC_SIZE 10000 struct __allocation { void* mem; int len; char guard_type; } __allocs [ALLOC_SIZE], __allocs_bck [ALLOC_SIZE]; void DebugMessage(char* s, ...); // 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?)" : "" ); } } } return EXCEPTION_CONTINUE_SEARCH; } // 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 ); } } } // Show debug message void DebugMessage(char* s, ...) { char t[ 1024 ]; va_list va; va_start( va, s ); vsprintf( t, s, va ); char msg[ 1024 ]; sprintf(msg, "Memory Debugging Message:\n" "\n" "%s\n" "\n" "If you want to abort execution press 'Abort'\n" "If you want to debug application press 'Retry'\n" "If you want to continue execution press 'Ignore'\n" , t ); OutputDebugString( msg ); switch( MessageBox( NULL, msg, "Memory Debugging Message", MB_ABORTRETRYIGNORE|MB_ICONERROR ) ) { case IDRETRY: __asm int 3; break; case IDABORT: ExitProcess(0); }; } // 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;*/ void* mem = VirtualAlloc( NULL, /*si.dwPageSize*2*/ size + si.dwPageSize, MEM_RESERVE, PAGE_NOACCESS ); if( !mem ) { OutputDebugStringEx(__FUNCTION__" : ERROR : VirtualAlloc(... %d ...) fails! : LasrErr = %08X", size + si.dwPageSize, GetLastError()); return NULL; } mem = VirtualAlloc( mem, /*si.dwPageSize*/ size, MEM_COMMIT, PAGE_READWRITE ); DWORD dwSmth = size % si.dwPageSize ? size % si.dwPageSize : si.dwPageSize; DWORD ps = /*si.dwPageSize - size*/ si.dwPageSize - dwSmth; __asm { mov al, 0xfd mov ecx, ps mov edi, mem rep stosb } mem = (LPVOID)( (DWORD)mem + /*si.dwPageSize - size*/si.dwPageSize - dwSmth ); int i; for( 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); } DWORD dwWholeRegionSize = (size - dwSmth) + si.dwPageSize; VirtualFree( mem, /*si.dwPageSize*/ dwWholeRegionSize, MEM_DECOMMIT ); VirtualFree( mem, /*si.dwPageSize*2*/ dwWholeRegionSize + si.dwPageSize, MEM_RELEASE ); OutputDebugStringEx(__FUNCTION__" : ERROR : RESULT == NULL : i == %d", i); return NULL; } // delete[] operator handler for the overflow-guard protection mode void FreeOverflowGuardedBuffer(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; } DWORD dwSmth = __allocs[i].len % si.dwPageSize ? __allocs[i].len % si.dwPageSize : si.dwPageSize; long ps = /*si.dwPageSize - __allocs[i].len*/ si.dwPageSize - dwSmth; LPVOID addr = (LPVOID)( (DWORD)mem + /*__allocs[i].len - si.dwPageSize*/ dwSmth - si.dwPageSize ); int j; for( j=0; j < ps; j++ ) if( ((unsigned char*)addr)[j] != 0xfd ) break; ps -= j; 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 ); } DWORD dwWholeRegionSize = (__allocs[i].len - dwSmth) + si.dwPageSize; VirtualFree( mem, /*si.dwPageSize*/ dwWholeRegionSize, MEM_DECOMMIT ); VirtualFree( mem, /*si.dwPageSize*2*/ dwWholeRegionSize + si.dwPageSize, MEM_RELEASE ); __allocs[i].len = -1; __allocs[i].mem = NULL; return; } } А вообще, меня жутко напрягает вот это: #define ALLOC_SIZE 10000 Надо бы в виде динамического односвязного списка это всё строить, по идее.
ALSO А так же, в этом коде никак не обрабатывается ситуация, если мы хотим освободить память по invalid-указателю. В реальной программе, с функцией free это привидёт к исключению. Поэтому, я рекомендую добавить Code: __asm int 3 В конец функции FreeOverflowGuardedBuffer P.S.: Наверно вы зададите вопрос - как это вообще возможно - попытаться освободить память по неверному указателю ? Если вы используете замечательный код TC - TransferProgramEx, то эта ошибка может возникнуть. Будьте уверены. Типа: Code: PBYTE gl_pBuffer = NULL; ... void f() { if (gl_pBuffer) delete[] gl_pBuffer; gl_pBuffer = new BYTE[100]; } Что произойдёт, если вы вызовите неск. раз f(), затем FreeOverflowGuardedBuffer(), и снова попытаетесь вызвать f(), будучи в коде уже другого процесса? Хехе. =) А как решить эту проблему, не прибегая к полному отказу от оператора delete в функции типа f() ? Я так понимаю, нужно во временном указателе, до вызова TransferProgramEx запомнить адрес на gl_pBuffer, затем обнулить gl_pBuffer, потом вызвать TransferProgramEx, после её вызова, восстановить gl_pBuffer. Ну, примерно таким образом это выглядит для случая с блоком по выявлению ошибок памяти ( дополнительный массив структур __allocs_bck & модифицированный код функции TransferProgramEx) : Code: struct __allocation { void* mem; int len; char guard_type; } __allocs [ALLOC_SIZE], __allocs_bck [ALLOC_SIZE]; Code: // move memory #ifdef _DEBUGLITE CopyMemory(__allocs_bck, __allocs, sizeof(__allocs)); ZeroMemory(__allocs, sizeof(__allocs)); #endif __CopyMemoryAcrossProcesses( hProcess, (char*) hModule, (char*) Allocated ); #ifdef _DEBUGLITE CopyMemory(__allocs, __allocs_bck, sizeof(__allocs)); ZeroMemory(__allocs_bck, sizeof(__allocs)); #endif Кто знает более лаконичное решение?