Exception&log

27
Exception & Log NHN NEXT 남현욱

Transcript of Exception&log

Page 1: Exception&log

Exception & Log

NHN NEXT남현욱

Page 2: Exception&log

01

Exception

Page 3: Exception&log

01 Exception

•StructuredExceptionHandling(SEH)프로그램을 아무리 잘 만들어도 예외 상황(Exception)은 피할 수 없다. 예외는 프로그램 외부

의 불가항력적인 상황때문에 발생하는 경우가 많기 때문이다(존재해야 할 파일이 존재하지 않

거나, 메모리 할당을 받아야하는데 메모리가 부족하거나, 기타 등등). 이런 예외 상황을 처리하

기 위해 윈도우즈에서는 SEH를 이용한다. SEH를 이용하면 실행 코드와 예외처리 코드를 분리

시켜 코드의 가독성, 유지 보수성을 높여준다는 장점을 갖고 있다. 구조나 사용방법은 C++ try

catch 구문과 거의 똑같다.

__try : 실행 코드 블럭

__except ( exception filter ) : 예외 상황 발생시 예외에 따라 실행.

Page 4: Exception&log

01 Exception

•ExceptionFilterSEH에서 __except는 인자로 exception filter를 받는다. 이 필터는 예외를 누가 처리할지, 예

외를 처리한 다음에 어떻게 할 것인지 등등을 지정하는 역할을 담당한다. exception filter에는

아래 세 가지 종류가 있다. 아래 세 가지 값은 excpt.h 파일에 정의되어 있음.

•EXCEPTION_EXECUTE_HANDLER

•EXCEPTION_CONTINUE_EXECUTION

•EXCEPTION_CONTINUE_SEARCH

Page 5: Exception&log

01 Exception

•ExceptionFilterEXCEPTION_EXECUTE_HANDLER이 필터가 적용될 경우, __try 블록에서 예외가 발생한 시점에 그 이후 __try 코드 블록의 실행을

모두 생략하고 global unwind를 수행한 후 __except블록의 코드 및 그 이후 코드들을 수행하

게 된다.int main() { int a = 0, b = 0; __try { b = 5 / a; //divide by 0 printf("ignore."); } __except(EXCEPTION_EXECUTE_HANDLER) { printf("execption."); } printf("after exception.");

}

//실행시 execption. after exception. 출력

Page 6: Exception&log

01 Exception

•ExceptionFilterEXCEPTION_CONTINUE_EXECUTION이 건 위 필터와 정 반대로 동작한다. 이 필터가 지정되어 있을 경우 __try 블록에서 예외가 발생

했던 코드로 다시 돌아가 해당 코드를 수행한다. 즉 이 필터가 지정되어 있을 경우 반복 시행에서

뭔가 예외 상황을 해결할 수 있는 처리를 해주어야한다는 것이다. 그렇지 않을 경우 다른 문제가

발생하거나 원인도 모른 채 프로그램이 죽거나 할 수 있다.

int main(){ int a = 0, b = 0; //divide by 0 __try{ b = 5/a; } __except(ExceptHandle(&a)) {} }

int ExceptHandle(int* divider){ scanf("%d",&divider); return divider ? EXCEPTION_EXECUTE_HANDLER : EXCEPTION_CONTINUE_EXECUTION; }실행시 0으로 나눈 것에서 에러 발생 -> 0 아닌 수 나올 때까지 반복 실행 -> 0 아닌 수 나오면 __except 안의 코드 실행 후 다음 코드로 진행.

Page 7: Exception&log

01 Exception

•ExceptionFilterEXCEPTION_CONTINUE_SEARCH이 필터가 지정되어 있을 경우 현재 지정된 __except에서 예외 처리를 수행하지 않고 콜스택을

거꾸로 되찾아가면서 다른 __except 블록에 처리를 넘긴다(거기서도 EXCEPTION_CONTIN-

UE_SEARCH라면 다시 그것보다 더 먼저 호출한 함수쪽으로 쭉 진행). 그러다가 아무런 핸들러

도 찾지 못한다면 종료된다.

void foo(){ int a = 0, b = 0; __try { b = 5 / a; // divide by 0 } __except (EXCEPTION_CONTINUE_SEARCH) { printf("ignore."); }}

int main(){ __try { foo(); } __except (EXCEPTION_EXECUTE_HANDLER) { printf("exception."); }}

실행시 exception. 출력됨.

Page 8: Exception&log

01 Exception

•UnhandledException앞선 필터들을 통해 예외가 발생했을 때 SEH를 통해 어떤식으로 예외를 처리할 수 있는지

살펴보았다. 그런데 이런 예외 중에서 처리되지 않은 예외가 발생했을 때 특정 핸들러를

호출하고 싶을 수 있다. 예를 들어 아까전에 EXCEPTION_CONTINUE_SEARCH 필터를

설정했을 경우 끝까지 핸들러를 찾지 못하면 그냥 프로그램이 죽어버리는데, 이런 상황에

Unhandled Exception filter를 설정해주면 해당 함수가 호출된다. 처리되지 않은 예외가

발생했을 때 프로그램의 dump를 뜬 다음 프로그램을 종료시키거나 하는 처리를 할 수가 있는

것이다. 이 설정은 windows의 SetUnhandledExceptionFilter 함수를 통해 할 수 있다.

LPTOP_LEVEL_EXCEPTION_FILTER WINAPI SetUnhandledExceptionFilter( _In_ LPTOP_LEVEL_EXCEPTION_FILTER lpTopLevelExceptionFilter

);

LPTOP_LVEL_EXCEPTION_FILTER는 LPEXCEPTION_POINTERS를 인자로 받고 LONG을

리턴하는 WINAPI 호출 규약의 함수 포인터이다.

( LONG WINAPI filter(LPEXCEPTION_POINTERS * info); )

Page 9: Exception&log

01 Exception

•Dump프로그램이 죽을 때 덤프 정보를 남기려면 어떻게 해야 할까?덤프 파일을 만들어주는 함수를

만든 다음 이 함수를 SetUnhandledExceptionFilter 함수를 이용해 등록하면 처리되지 않은

예외가 발생했을때 이 함수가 호출되어 자동으로 덤프 파일을 만들어줄 것이다. 덤프를 만들기

위해 MiniDumpWriteDump 함수를 쓸 수 있다.

BOOL WINAPI MiniDumpWriteDump( _In_ HANDLE hProcess, // 덤프 뜰 프로세스 핸들 _In_ DWORD ProcessId, // 덤프 뜰 프로세스 아이디 _In_ HANDLE hFile, // 덤프 기록할 파일 핸들 _In_ MINIDUMP_TYPE DumpType, // 기록할 덤프의 타입(후술) //덤프 뜰 상황이 발생하게 만든 예외 정보(NULL이면 기록되지 않음) _In_ PMINIDUMP_EXCEPTION_INFORMATION ExceptionParam, //덤프 파일에 기록할 사용자 정의 정보(NULL이면 기록되지 않음) _In_ PMINIDUMP_USER_STREAM_INFORMATION UserStreamParam, //확장된 미니 덤프 정보를 받을 콜백 루틴(NULL이면 아무 콜백도 호출 안 됨) _In_ PMINIDUMP_CALLBACK_INFORMATION CallbackParam);

Page 10: Exception&log

01 Exception

•Dump사용가능한 MiniDump의 타입에는 굉장히 많은 종류가 있다. 전체 목록 및 각각의

기능이 궁금하다면 MSDN의 MINIDUMP_TYPE 문서를 보면 된다. 대표적인 2가지는

MiniDumpNormal과 MiniDumpWithFullMemory이다. MiniDumpNormal은 로컬

레지스터 값을 포함해서 덤프를 뜨고, MiniDumpWithFullMemory는 전체 메모리 값을

포함하여 덤프를 뜬다.

사용예제

MINIDUMP_EXCEPTION_INFORMATION exceptionInfo; exceptionInfo.ThreadId = GetCurrentThreadId(); exceptionInfo.ExceptionPointers = e; exceptionInfo.ClientPointers = FALSE; HANDLE hProcess = GetCurrentProcess(); DWORD dwProcessID = GetCurrentProcessId(); DWORD dwThreadID = GetCurrentThreadId();

MiniDumpWriteDump(hProcess, dwProcessId, hFile, MiniDumpWithFullMemory, &exceptionInfo, NULL, NULL);

Page 11: Exception&log

01 Exception

•StackWalk64스레드의 스택 정보를 기록하고 싶다면 StackWalk64 함수를 쓰면 된다. 인자가 굉장히 많고

복잡하다.BOOL WINAPI StackWalk64( _In_ DWORD MachineType, _In_ HANDLE hProcess, _In_ HANDLE hThread, _Inout_ LPSTACKFRAME64 StackFrame, _Inout_ PVOID ContextRecord, _In_opt_ PREAD_PROCESS_MEMORY_ROUTINE64 ReadMemoryRoutine, _In_opt_ PFUNCTION_TABLE_ACCESS_ROUTINE64 FunctionTableAccessRoutine, _In_opt_ PGET_MODULE_BASE_ROUTINE64 GetModuleBaseRoutine, _In_opt_ PTRANSLATE_ADDRESS_ROUTINE64 TranslateAddress);

Page 12: Exception&log

01 Exception

•StackWalk64

MachineType생성할 스택 추적 정보의 컴퓨터 아키텍쳐 종류이다. 아래 3가지 값중 하나를 가진다.

•IMAGE_FILE_MACHINE_I386 Intel x86 아키텍쳐

•IMAGE_FILE_MACHINE_IA64 Intel Itanium 아키텍쳐

•IMAGE_FILE_MACHINE_AMD64 x64(AMD64 or EM64T) 아키텍쳐

hProcess,hThread스택을 추적할 프로세스 및 스레드 핸들.

StackFrameSTACKFRAME64 구조체에 대한 포인터. StackWalk64 함수 호출이 성공적으로 이루어지면

이 포인터에 다음 스택 프레임에 대한 정보가 들어간다.

Page 13: Exception&log

01 Exception

•StackWalk64

ContextRecordCONTEXT 구조체에 대한 포인터. MachineType 인자가 IMAGE_FILE_MACHINE_I386

이 아닐 때만 필요하다(해당 MachineType이 아니라도 사용가능한 context record

값을 넘기는게 권장된다고 함). 이 인자로 넘어간 ContextRecord 값은 수정될 수 있다.

RtlCaptureContext 함수로 얻어올 수 있다고 한다.

ReadMemoryRoutine메모리를 읽어오는 데 필요한 함수를 지정한다. 아무것도 지정하지 않을 경우 기본값

(ReadProcessMemoryPros64)로 지정됨.

FunctionTableAccessRoutine(optional)프로세스의 run time function table에 대한 접근을 제공하는 함수를 지정.

SymFunctionTableAccess64 라는 함수를 보통 쓴다고 한다

Page 14: Exception&log

01 Exception

•StackWalk64

GetModuleBaseRoutine(optional)주어진 가상함수에 대한 module base를 제공하는 함수. 보통 SymGetModuleBase64

함수를 쓴다고 함.

TranslateAddress(optional)16비트 주소에 대한 변환을 제공하는 함수. 필요없으면 NULL을 넘긴다.

Page 15: Exception&log

01 Exception

•StackWalk64사용 예제

StackWalk64를 쓰기 위한 기본 변수들

BOOL res;HANDLE hProcess;HANDLE hThread;CONTEXT context = {0, };STACKFRAME64 stack = {0, };ULONG frame;const int MaxNameLen = 255;SYMBOL_INFO* symbol = (SYMBOL_INFO*)malloc(sizeof(SYMBOL_INFO) + MaxNameLen);DWORD64 displacement64 = 0;DWORD displacement = 0;IMAGEHLP_LINE64 imageHelpLine;DWORD dwSymOptions = SymGetOptions();

Page 16: Exception&log

01 Exception

•StackWalk64사용 예제

Symbol Option 초기화

dwSymOptions |= SYMOPT_LOAD_LINES;dwSymOptions |= SYMOPT_UNDNAME;dwSymOptions |= SYMOPT_EXACT_SYMBOLS;SymSetOptions(dwSymOptions);

memset(symbol, 0, sizeof(SYMBOL_INFO) + MaxNameLen);symbol->SizeOfStruct = sizeof(SYMBOL_INFO);symbol->MaxNameLen = MaxNameLen;

memset(&imageHelpLine, 0, sizeof(imageHelpLine));imageHelpLine.SizeOfStruct = sizeof(imageHelpLine);

SymInitialize(GetCurrentProcess(), ".", 1);

Page 17: Exception&log

01 Exception

•StackWalk64사용 예제

stack 구조체 초기화 및 context 정보 가져오기

RtlCaptureContext(&context);memset(&stack, 0, sizeof(STACKFRAME64));

hProcess = GetCurrentProcess();hThread = GetCurrentThread();stack.AddrPC.Offset = context.Eip;stack.AddrPC.Mode = AddrModeFlat;stack.AddrStack.Offset = context.Esp;stack.AddrStack.Mode = AddrModeFlat;stack.AddrFrame.Offset = context.Ebp;stack.AddrFrame.Mode = AddrModeFlat;stack.AddrBStore.Mode = AddrModeFlat;stack.AddrReturn.Mode = AddrModeFlat;

Page 18: Exception&log

01 Exception

•StackWalk64사용 예제

스택 정보 가져오기

for (frame = 0;; frame++){ res = StackWalk64 ( IMAGE_FILE_MACHINE_I386, hProcess, hThread, &stack, &context, NULL, SymFunctionTableAccess64, SymGetModuleBase64, NULL );

Page 19: Exception&log

01 Exception

•StackWalk64사용 예제

심볼 정보 가져오기 및 출력

SymFromAddr(hProcess, stack.AddrPC.Offset, &displacement64, symbol); SymGetLineFromAddr64(hProcess, stack.AddrPC.Offset, &displacement, &imageHelpLine); printf ( "Frame %lu:\nSymbol name: %s\nFile Name : %s\n" "Line Number : %d\nPC address: 0x%08LX\n" "Frame address: 0x%08LX\n", frame, symbol->Name, imageHelpLine.FileName, imageHelpLine.LineNumber, (ULONG64)stack.AddrPC.Offset, (ULONG64)stack.AddrStack.Offset, (ULONG64)stack.AddrFrame.Offset );

Page 20: Exception&log

01 Exception

•StackWalk64사용 예제

마무리

if (!res) { break; } }

SymCleanup(hProcess); free(symbol);}

Page 21: Exception&log

02

Log

Page 22: Exception&log

02 Log

•C++LogLibraryC++에는 이미 잘 만들어진 로그 라이브러리들이 많다. 그 중 몇 가지만 살펴보자.

glog구글에서 만든 C++ 로그 라이브러리. 어플리케이션 수준의 로깅을 구현. 디버그 모드에서만 로

그를 출력하는 기능, 특정 상태에서만 로그를 기록하는 기능 등등을 지원한다.

사용법관련링크 : http://jjalidev.blogspot.kr/2013/10/glog.html

log4Cxxlog4j를 본따 C++ 버젼으로 만든 라이브러리. 빠르고 확장성이 좋다고 한다. 평가가 좋은 편이

지만 2008년 이후로 업데이트가 없다.

사용법관련링크 : http://mindgear.tistory.com/184

Page 23: Exception&log

02 Log

•Boost::logC++ 라이브러리중 가장 유명하고 많이 쓰이는 라이브러리인 Boost 라이브러리의 log 기능을

한 번 써보자. Boost 설치는 링크 참조. 글에 적힌 걸 그대로 따라하기만 하면 생각보다 어렵지

않게 설치할 수 있다. Simplicity, Extensibility, Performance를 가진 라이브러리를 목표로

만들었다고 함. 메뉴얼링크

Page 24: Exception&log

02 Log

•Boost::log간략한 구조 설명( 앞 슬라이드의 그림 참조)

LoggingSourceslogger는 실제 기록될 메시지를 형식화(format)하기 위한 스트림을 제공해주는 오브젝트다.

라이브러리에서 다양한 logger를 지원하며 직접 logger를 만들어 쓰거나 확장해 쓸 수 있다.

Attributesandattributevalueslog source를 초기화하기 위해서는 log record에 관련된 모든 정보를 logging core에 넘겨주

어야한다. 이 데이터들은 attribute들의 집합으로 구성되어 있으며 각각의 attribute들은 기본

적으로 하나의 함수이다. attribute set에는 global / thread-specific, source-specific의

세 가지 종류가 있다.

Loggingcoreandfilteringattribute value 집합이 모두 구성되면, logging core는 해당 log record가 기록되어야 하는

log인지 판단하고 필요없으면 버린다(filtering).

Sinksandformatting필터링을 통과하면 해당 record를 적절히 형식화해서 해당 record가 기록되어야 할 sink로 보

낸다. front-end / back-end로 구성.

Page 25: Exception&log

02 Log

•Boost::logTriviallogging제일 간단한 로깅. 그냥 해당 메시지 콘솔 창에 출력하는 기능을 한다(std::cout).

#include <boost/log/trivial.hpp>

int main(){ BOOST_LOG_TRIVIAL(trace) << "A trace severity message"; BOOST_LOG_TRIVIAL(debug) << "A debug severity message"; BOOST_LOG_TRIVIAL(info) << "An informational severity message"; BOOST_LOG_TRIVIAL(warning) << "A warning severity message"; BOOST_LOG_TRIVIAL(error) << "An error severity message"; BOOST_LOG_TRIVIAL(fatal) << "A fatal severity message";

return 0;}

Page 26: Exception&log

02 Log

•Boost::logSetupSinklog가 출력될 sink를 직접 설정해서 쓰기.

namespace logging = boost::log;namespace src = boost::log::sources;namespace sinks = boost::log::sinks;namespace keywords = boost::log::keywords;

int main(){ using namespace logging::trivial;

logging::add_file_log ( keywords::file_name = "sample_%N.log", keywords::rotation_size = 10 * 1024 * 1024, keywords::format = "[%TimeStamp%] : %Message%" );

logging::core::get()->set_filter ( severity >= info ); logging::add_common_attributes();

src::severity_logger<severity_level> lg;

BOOST_LOG_SEV(lg, trace) << "A trace severity message"; BOOST_LOG_SEV(lg, debug) << "A debug severity message"; BOOST_LOG_SEV(lg, info) << "An informational severity message"; BOOST_LOG_SEV(lg, warning) << "A warning severity message"; BOOST_LOG_SEV(lg, error) << "An error severity message"; BOOST_LOG_SEV(lg, fatal) << "A fatal severity message";

return 0;}

사용법이 상당히 방대해서 예제는 이정도로.. 메뉴얼 문서에 튜토리얼이 굉장히 잘 정리되어있으니 그걸 참고하자!

Page 27: Exception&log

02 Log

•멀티스레드에서의로그싱글 스레드에서는 아무 문제가 없지만, 역시나 멀티 스레드가 끼면 문제가 복잡해진다. 여러 스

레드에서 동시에 이런 저런 내용들을 로깅하려고 할 때 순서가 꼬일 수 있기 때문이다. boost 같

은 경우에는 sink에서 frond-end와 back-end를 나눈 뒤 front-end에서 스레드간 동기화 등

의 작업을 수행하게 만들고 back-end에서 실제로 로그를 기록하는 작업을 수행하는 식으로 이

런 순서 문제를 해결했다(실제 내부 구조가 어떤 식으로 동작하는지는 잘 모르겠다). 개인적으로

생각하기에는 아래 2가지 방법 정도가 있을 것 같다.

queue이용하기thread-safe한 queue를 만들어서 거기 로그할 내용을 집어넣은 뒤 꺼내서 실제로 기록하는

것이다.

GCE같은구조이용하기코딩 과제에 있는 GCE 클래스처럼 만들면 lock 안 걸고 순서 보장하는 log 기록이 가능할 것 같

다.(GCE도 그런 역할을 하니까) log를 기록하는 애가 아무도 없으면 직접 기록하고, 누군가 log

를 기록 중이라면 걔한테 자기가 해야할 log까지 떠맡기고 그냥 자기 할 일 하는 방식.