책/제프리 리처의 WINDOWS VIA C, C++

WINDOWS VIA C/C++(31p ~ 41p): Chapter01 에러 핸들링

readme.md 2024. 1. 4. 00:42

제프리 리처의 WINDOWS VIA C/C++
https://www.yes24.com/Product/Goods/3205340
페이지 31p ~ 41p

요약

  • 첫 번째 챕터로 에러 핸들링에 대한 내용을 담고 있다.
  • 윈도우 함수가 호출되면 호출된 함수는 전달된 인자의 유효성을 확인하고 함수의 기능을 수행하려 하는데, 이 과정에서 전달된 인자가 유효하지 않거나 다른 이유로 해당 기능을 수행할 수 없으면 실패를 반환한다.
  • 실패를 반환하면 왜 함수가 실패했는지 이유를 알아내야 하며, 마이크로소프트에서는 발생할 가능성이 있는 모든 에러를 32비트 숫자로 정의해 두었다.
  • 윈도우 함수가 실패하면 내부적으로 함수를 호출한 스레드의 지역 저장소(thread-local storage)에 적절한 에러 코드를 저장하기 때문에, 멀티 스레드 환경에서도 각각의 스레드는 고유한 에러 코드를 유지할 수 있게 된다.

메모

1. 윈도우 함수의 대표적인 반환 자료형

자료형 실패시 반환 값 성공시 반환 값
VOID 절대 실패하지 않는 함수. -
BOOL 실패하면 0을 반환. 성공하면 0이 아닌 값을 반환.
HANDLE 실패하면 대개 NULL을 반환. 몇몇 함수는 INVALID_HANDLE_VALUE(-1)을 반환. 성공하면 유효한 오브젝트 핸들을 반환.
PVOID 실패하면 NULL을 반환. 성공하면 PVOID가 데이터를 저장하고 있는 메모리 주소를 반환.
LONG/DWORD 실패하면 0이나 -1을 반환. 결과값에 대한 정확한 정보는 공식 문서 참고. 성공하면 각 자료형으로 개수를 반환.

반환 자료형의 중요성

  • 윈도우 함수가 실패하면 왜 실패했는지 이유를 알아내는 과정이 반드시 필요하다. 반환 자료형을 통해 함수의 성공, 실패 여부를 판단할 수 있을 것이다.
  • 마이크로소프트는 발생할 가능성이 있는 모든 에러 코드를 32비트 숫자로 정의해 두었다.
  • 호출한 함수가 실패했다고 판단되면, 함수를 호출한 직후 반드시 다음과 같은 함수를 사용하여 스레드에 저장된 에러 코드를 확인해야 한다.
// 가장 최근에 호출된 함수의 에러 코드를 스레드 지역 저장소에서 가져옴
DWORD GetLastError();
  • DWORD 반환 타입은 32비트 크기의 unsigned long과 같은 타입으로 숫자를 보다 유용한 정보로 변환하기 위해 WinError.h 파일에는 해당 숫자들을 의미 있는 메시지로 치환해 두었다.

서로 다른 이유로 성공하는 케이스

  • 특정 함수들은 성공의 이유가 여러 가지가 있다.
    • 특정한 이름의 이벤트 커널 오브젝트를 생성하는 함수를 호출하면 실제로 새로운 커널 오브젝트가 생성되는 경우 외에도, 이미 동일한 이름의 커널 오브젝트가 존재하는 경우에도 성공을 반환한다.
  • 따라서 어떤 이유로 함수가 성공했는지 구체적으로 알아야 할 필요가 있으며, 에러 코드를 저장하는 방식을 이용하여 이를 구분할 수 있도록 만들어 놓았다.
    • CreateEvent 문서에서는 동일한 이름의 이벤트 오브젝트가 존재하면 성공을 했어도 GetLastError의 반환값은 ERROR_ALREADY_EXISTS가 된다.

스레드 지역 저장소에 저장된 에러 코드 확인하기(Visual Studio 사용)

  • 디버깅을 하는 과정에서 스레드 지역 저장소에 기록된 에러 코드를 확인할 수 있다.
  • Visual Studio 디버거의 Watch 창을 통해 현재 수행 중인 스레드의 마지막 에러 코드와 메시지를 확인할 수 있다.
    • Watch 창의 특정 행을 선택하고 $err,hr을 입력하면 된다.

Error Lookup 유틸리티

FormatMessage

DWORD FormatMessage(
    DWORD dwFlags,                // 플래그
    LPCVOID pSource,            // 메시지 정의 위치. 플래그에 따라 달라짐. 해당 사항 없으면 무시
    DWORD dwMessageId,            // 에러 코드
    DWORD dwLanguageId,            // 언어 식별자
    PTSTR pszBuffer,            // null로 끄나는 문자열을 수신하는 버퍼 포인터
    DWORD nSize,                // 출력 버퍼 크기 지정. 플래그 설정에 따라 달라짐(최대, 최소)
    va_list *Arguments            // 형식 문자에 사용할 값들
)
  • 사용법은 다음과 같다.
      // 다이얼로그 핸들러가 가지고 있는 에러 코드를 획득
      DWORD dwError = GetDlgItemInt(hwnd, IDC_ERRORCODE, NULL, FALSE);

      HLOCAL hlocal = NULL;   // 에러 메시지를 저장하기 위한 버퍼 선언

      // 에러 메시지를 얻기 위해 시스템 지역 설정
      // 아래의 MAKELANGID는 0을 반환함
      DWORD systemLocale = MAKELANGID(LANG_NEUTRAL, SUBLANG_NEUTRAL);

      // Get the error code's textual description
      BOOL fOk = FormatMessage(
         FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_IGNORE_INSERTS |
         FORMAT_MESSAGE_ALLOCATE_BUFFER, 
         NULL, dwError, systemLocale, 
         (PTSTR) &hlocal, 0, NULL);

      if (!fOk) {
         // 네트워크 관련 에러 확인
         // DLL을 통해 자신만의 에러 코드와 메시지를 포함할 수 있음
         HMODULE hDll = LoadLibraryEx(TEXT("netmsg.dll"), NULL, 
            DONT_RESOLVE_DLL_REFERENCES);

         if (hDll != NULL) {
            fOk = FormatMessage(
               FORMAT_MESSAGE_FROM_HMODULE | FORMAT_MESSAGE_IGNORE_INSERTS |
               FORMAT_MESSAGE_ALLOCATE_BUFFER,
               hDll, dwError, systemLocale,
               (PTSTR) &hlocal, 0, NULL);
            FreeLibrary(hDll);
         }
      }

      if (fOk && (hlocal != NULL)) {
         SetDlgItemText(hwnd, IDC_ERRORTEXT, (PCTSTR) LocalLock(hlocal));
         LocalFree(hlocal);
      } else {
         SetDlgItemText(hwnd, IDC_ERRORTEXT, 
            TEXT("No text found for this error number."));
      }
  • 첫 번째 인자로 전달되는 플래그는 각각 다음과 같다.
    • FORMAT_MESSAGE_FROM_SYSTEM: 운영체제가 정의하고 있는 에러 코드와 대응되는 메시지 텍스트 획득 요청.
    • FORMAT_MESSAGE_IGNORE_INSERTS: 에러 메시지 텍스트에 자리 표시자를 실질적으로 변경하지 않을 것을 요청.
    • FORMAT_MESSAGE_ALLOCATE_BUFFER: 에러 메시지 텍스트를 저장할 수 있는 메모리 공간 할당을 요청. 할당된 메모리 블록을 가리키는 hlocal 변수에 값이 반환됨

2. 자신만의 에러 코드 정의

  • 함수는 여러 이유로 인해 실패할 수 있을 것이며, 원인을 함수의 호출자에게 알려주어야 한다.
  • 함수가 실패했음을 나타내기 위해서는 실패의 이유를 스레드의 마지막 에러 코드로 설정하고 FALSE, INVALID_HANDLE_VALUE, NULL과 같이 적절한 값을 반환하도록 함수를 작성한다.
  • 스레드의 마지막 에러 코드를 설정하는 함수는 다음과 같다.
VOID SetLastError(DWORD dwErrCode);
  • WinError.h에 정의되어 있는 에러 코드 외에 자신만의 에러 코드를 작성하고 싶다면 다음과 같은 규칙에 따라 에러 코드를 만들면 된다.
비트 31-30 29 28 27-16 15-0
내용 심각도 마이크로소프트/고객 예약됨 식별 코드 예외 코드
의미 0 = 성공 1 = 정보 2 = 주의 3 = 에러 0 = 마이크로소프트가 정의 1 = 고객이 정의 항상 0 256까지는 마이크로소프트에 의해 예약됨 마이크로소프트나 고객이 정의한 코드