오랜동안 실행 중인 서버 프로그램이 갑자기 아무 이유없이 크래시를 일으키는 경우를 가끔 볼 수 있습니다. 그 이유 중 하나가 마찬가지로 메모리 단편화일 것입니다. 충분한 물리 메모리가 있음에도 불구하고 가상메모리가 단편화 되어 더 이상 메모리 할당이 불가능해질 수 있다는 것이지요.
그런데, 최근에 저는 또 한가지 이유를 발견하였는데 바로 스택 보호 페이지가 리셋되는 경우입니다. "단순히 정상인 메모리를 읽기만 함"으로써도 프로그램을 크래시 시킬 수 있다는 약간은 놀라운 사실입니다.
윈도우즈의 각각의 스레드 스택은 기본으로 1MB가 예약되어 있습니다. 하지만, 1MB전부가 commit (물리메모리로 매핑)된 것은 아니며, 스택 보호 페이지라는 것을 만들어 스택 크기가 커져 이 보호페이지를 액세스하게되면 예외(exception)가 발생하며, 이 때 OS 커널은 해당페이지를 Commit하고 새로운 보호페이지를 설정합니다. 아래 그림을 참고하시기 바랍니다.

이러한 방법으로 처음부터 모든 스레드에 1MB의 물리메모리를 스택으로 할당하지 않아도 되기 때문에 메모리 낭비를 상당히 줄일 수 있습니다.
보호 페이지가 사라진 상태에서 당장 문제가 나타나지는 않으나, 나중에 스택 보호 페이지가 없는 스레드가 스택을 많이 사용하게 되어 보호 페이지와 그 위의 commit되지 않은 페이지를 액세스하게 되면 Access Violation나며 어플리케이션이 종료될 가능성이 높습니다. 어플리케이션의 예외처리기 (unhandled exception handler)를 사용하여 오류 분석을 해봐도 원인을 찾기는 쉽지 않습니다.
이 문제를 실제로 보여주는 예제가 아래에 있습니다. 아래 코드를 복사하여 BrokenGuardPage.cpp라는 이름으로 저장한 다음, "cl BrokenGuardPage.cpp"와 같이 컴파일하면 예제 실행파일을 만들 수 있습니다.
AccessStackGuardFromOtherThread 함수는 새로운 스레드 하나를 생성하고 있으며 지역변수의 포인터를 새로운 스레드에 전달합니다. 새로 생성된 스레드는 전달된 지역변수 메모리값에 -0x2000을 하여 스택보호 페이지에 대해 읽기 만을 시도합니다.
이 프로그램을 실행하면 프로그램 STATUS_GUARD_PAGE_VIOLATION 예외가 먼저 발생합니다. 이 때 Unhandled exception handler가 설정되어 있어 프로그램은 종료하지 않고 계속실행이 됩니다. 다음으로 Crash 함수가 main 스레드로 부터 호출되는데, Crash는 단순히 큰 지역변수를 가지고 이를 초기화하려고 시도하고 있습니다. Crash 함수에 잘못된 코드는 전혀 없습니다만, Access Violation을 발생하며 프로그램이 종료되어 마지막의 "printf("End of Program.\n")"는 실행되지 않습니다.
//
// BrokenGuardPage.cpp
//
// Just use following commands for compile:
// cl BrokenGuardPage.cpp
//
// www.devnote.net (9/26/2008)
//
////////////////////////////////////////////////////////////////
#include <stdio.h>
#include <process.h>
#include <windows.h>
//--------------------------------------------------------------
void Crash () {
printf("Crash\n");
// This function simply allocate a big local array and initalizes first element
char test[32000];
test[0] = 0;
}
//--------------------------------------------------------------
static unsigned __stdcall ThreadProc (LPVOID param) {
printf ("ThreadProc\n\tTry to access Stack Guard Page\n");
// main thread's local variable
char * ptr = (char *)param;
ptr -= 0x2000;
// Just try to read stack guard page
volatile char ch = *ptr;
return 0;
}
//--------------------------------------------------------------
static void AccessStackGuardFromOtherThread () {
printf("AccessStackGuardFromOtherThread\n");
unsigned localVar;
HANDLE exceptionThread = (HANDLE)_beginthreadex(
NULL,
0, // stack size
ThreadProc,
&localVar, // Pass local variable pointer
0, // suspended = false
NULL
);
// Wait for the thread finish
WaitForSingleObject(exceptionThread, INFINITE);
}
//--------------------------------------------------------------
static LONG WINAPI OurUnhandledExceptionFilter (EXCEPTION_POINTERS * ep) {
printf ("OurUnhandledExceptionFilter: exception code = %X\n", ep->ExceptionRecord->ExceptionCode);
if (ep->ExceptionRecord->ExceptionCode == STATUS_GUARD_PAGE_VIOLATION)
printf("STATUS_GUARD_PAGE_VIOLATION :%0X\n", ep->ExceptionRecord->ExceptionInformation[1]);
return EXCEPTION_EXECUTE_HANDLER;
}
//--------------------------------------------------------------
void main () {
SetUnhandledExceptionFilter(OurUnhandledExceptionFilter);
AccessStackGuardFromOtherThread();
Crash();
printf("End of Program.\n"); // This will never be called
}
인터넷을 검색해보니 이미 이 문제에 관해 아래와 블로그 글들이 올라와 있군요.
http://blogs.msdn.com/oldnewthing/archive/2006/01/17/513779.aspx
같은 프로세스에서 뿐만 아니라, 읽기 메모리 권한(
PROCESS_VM_READ)을 다른 프로세스에 부여하는 것만으로도 똑같이 프로그램 크래시를 유발할 수 있습니다. 또한 예외처리 핸들러에서 가끔 사용되는 IsBadxxxPtr API들도 똑같은 문제를 유발시킵니다. 아래 블로그를 참고하시기 바랍니다.
http://blogs.msdn.com/larryosterman/archive/2004/05/18/134471.aspx