Ujarzmić Access Violation

0xC0000005 - ten numer jest zapewne bardzo znany większości osób zajmujących się programowaniem gier, czy tworzeniem pod Windows w ogóle. To oczywiście kod naruszenia dostępu (ang. access violation), występujący gdy odwołujemy się do pamięci, do której nie mamy prawa. Zazwyczaj oznacza to wysypanie aplikacji z znienawidzonym przez użytkowników komunikatem "Program wykonał nieprawidłową operację". Jest jednak na to metoda. Mowa oczywiście o Structured Exception Handling ([1]) Microsoftu, czyli próbie wprowadzenia przez firmę z Redmond wyjątków do języka C. SEH pozwala obsługiwać nie tylko wyjątki generowane przez programistów, ale także wyjątki systemu, takie jak wspomniane naruszenie dostępu oraz inne powodujące wysyp aplikacji. Nie będę dziś wchodził w szczegóły zaawansowanego używania SEH; chciałbym tu podzielić się kawałkiem kodu, który pokazał mi niegdyś st3tc, a którego od tego czasu używam w swoich produkcjach. Wklejam go w takiej postaci, w jakiej używałem ostatnio
#ifdef ANYTHING_USE_SEH

#pragma comment (lib, "Dbghelp.lib")

#include <dbghelp.h>
#include <shellapi.h>
#include <shlobj.h>

#endif //ANYTHING_USE_SEH

namespace Anything
{
	int GameMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nShowCmd)
	{
		(...)
	}
} //end of namespace Anything

#ifdef ANYTHING_USE_SEH

//SEH stuff...
//credit to st3tc for showing me this idea
int GenerateDump(EXCEPTION_POINTERS* pExceptionPointers)
{
	MINIDUMP_EXCEPTION_INFORMATION ExpParam;
	SYSTEMTIME stLocalTime;
	HANDLE hDumpFile;
	char szFileName[MAX_PATH];

	GetLocalTime( &stLocalTime );
	std::sprintf( szFileName, "CrashDump-%04d%02d%02d-%02d%02d%02d.dmp",
	stLocalTime.wYear, stLocalTime.wMonth, stLocalTime.wDay,
	stLocalTime.wHour, stLocalTime.wMinute, stLocalTime.wSecond );

	hDumpFile = CreateFile( szFileName, GENERIC_READ|GENERIC_WRITE,
	FILE_SHARE_WRITE|FILE_SHARE_READ, 0, CREATE_ALWAYS, 0, 0);

	ExpParam.ThreadId = GetCurrentThreadId();
	ExpParam.ExceptionPointers = pExceptionPointers;
	ExpParam.ClientPointers = TRUE;

	MiniDumpWriteDump(GetCurrentProcess(), GetCurrentProcessId(), hDumpFile, MiniDumpNormal, &ExpParam, NULL, NULL);
	return EXCEPTION_EXECUTE_HANDLER;
}

int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nShowCmd)
{
	__try
	{
		int retCode = Anything::GameMain(hInstance, hPrevInstance, lpCmdLine, nShowCmd);
		return retCode;
	}
	__except(GenerateDump(GetExceptionInformation()))
	{
	}
	return 0;
}

#else

int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nShowCmd)
{
	return Anything::GameMain(hInstance, hPrevInstance, lpCmdLine, nShowCmd);
}

#endif //ANYTHING_USE_SEH
(codebox się troszkę popsuł, aby przeczytać niektóre linie niestety trzeba użyć paska przewijania u dołu) Jest to połączenie użycia SEH z biblioteką dbghelp.dll. Ten kod powoduje, że w momencie wystąpienia krytycznego wyjątku systemu, takiego jak Access Violation, program grzecznie się zamyka, generując przy okazji tzw. plik zrzutu (ang. dump file). Ten plik możemy wczytać do debuggera by zobaczyć stan aplikacji (np. rejestry procesora) z chwili wystąpienia wyjątku. Dzięki temu końcowy użytkownik, który napotka tego typu błąd, może wysłać nam ten plik zrzutu (najlepiej razem z logiem z wykonania;) ) a my będziemy mieli dodatkowe ważne informacje pozwalające ustalić jego przyczynę. Warto w tym kodzie zwrócić uwagę, że ciało funkcji WinMain() zostało przeniesione do wewnątrz funkcji Anything::GameMain(). Jest to spowodowane tym, że SEH jest systemem dołączonym do języka C i nie życzy sobie używania kodu zorientowanego obiektowo bezpośrednio w funkcji, w której użyto bloków __try/__except (konkretnie to problem tworzą destruktory klas w C++, które wymagają tzw. odwikłania stosu). Oczywiście SEH to dużo potężniejsze narzędzie. Dobrym przykładem jego zastosowania jest kod gry FreeSpace II, który w przypadku wystąpienia wyjątku systemowego generuje obszernego MessageBox'a z wyświetleniem typu i nazwy błędu oraz rejestru procesora, po czym zapisuje te dane razem ze zrzutem stosu do pliku tekstowego. Te i inne pomysły pozostawiam jako ćwiczenie dla Czytelnika. Polecam też artykuł opisujący jak przy użyciu SEH uzyskać w C++ stos wywołań w chwili błędu.