Microsoft MVP성태의 닷넷 이야기
글쓴 사람
정성태 (techsharer at outlook.com)
홈페이지
첨부 파일
 
(연관된 글이 4개 있습니다.)
(시리즈 글이 8개 있습니다.)
개발 환경 구성: 105. 풀 덤프 파일을 남기는 방법
; https://www.sysnet.pe.kr/2/0/991

.NET Framework: 205. 코드(C#)를 통한 풀 덤프 만드는 방법
; https://www.sysnet.pe.kr/2/0/995

디버깅 기술: 51. 닷넷 응용 프로그램에서 특정 예외가 발생했을 때 풀 덤프 받는 방법
; https://www.sysnet.pe.kr/2/0/1376

.NET Framework: 380. 프로세스 스스로 풀 덤프 남기는 방법
; https://www.sysnet.pe.kr/2/0/1485

디버깅 기술: 81. try/catch로 조용히 사라진 예외를 파악하고 싶다면?
; https://www.sysnet.pe.kr/2/0/10965

.NET Framework: 868. (닷넷 프로세스를 대상으로) 디버거 방식이 아닌 CLR Profiler를 이용해 procdump.exe 기능 구현
; https://www.sysnet.pe.kr/2/0/12049

개발 환경 구성: 462. 시작하자마자 비정상 종료하는 프로세스의 메모리 덤프 - procdump
; https://www.sysnet.pe.kr/2/0/12051

개발 환경 구성: 752. ProcDump - C/C++ 예외 코드 필터를 지정한 덤프 생성
; https://www.sysnet.pe.kr/2/0/13960




try/catch로 조용히 사라진 예외를 파악하고 싶다면?

try/catch를 남용하는 경우, 문제가 발생해도 도대체 그것이 어디서 발생했는지 파악하기가 어려울 수 있습니다. 더욱 문제는, 개발 시 try/catch가 필요한 상황이 나올 수밖에 없기 때문에 관련 예외를 thrown 상태로 설정할 수 없다는 점입니다. 물론, 설정해도 되지만 디버깅 시 자꾸 걸리기 때문에 여간 귀찮은 문제가 아닐 수 없습니다.

사실 이 귀찮음 때문에, 비주얼 스튜디오도 대부분의 예외에 대해 "first-chance exception"인 경우 굳이 멈추지 않고 진행할 수 있도록 Thrown 기본값이 꺼진 상태입니다.

그래서 저는, 때때로 그렇게 사라지는 예외를 최소한으로 줄이기 위한 나름의 방법을 사용하는데요. 바로 procdump.exe를 이용하는 것입니다. 예를 들어, 응용 프로그램의 Process id가 11096인 경우 다음과 같은 식으로 실행해 주면 됩니다.

procdump -e 1 -f "" 11096

"" 문자열은 무엇을 넣어도 상관없습니다. 어차피 그 예외를 기대하지 않는데다, 그냥 상황을 지켜보기 위함이기 때문입니다. 이렇게 실행하면 화면에 try/catch로 사라졌던 모든 예외들이 출력되는 것을 볼 수 있습니다.

[...] Exception: E0434F4D.System.Web.HttpException ("The component '{48D11CC5-C50A-403D-A4AC-0825A56E5C13}' cannot be created.  Apartment threaded components can only be created on pages with an <%@ Page aspcompat=true %> page directive.")

[...] Exception: E0434F4D.System.ObjectDisposedException ("Cannot access a closed file.")

[...] Exception: E0434F4D.System.Net.WebException ("The remote server returned an error: (503) Server Unavailable.")

[...] Exception: E0434F4D.System.Net.Sockets.SocketException ("An operation was attempted on something that is not a socket")

[...] Exception: E0434F4D.System.IO.IOException ("Unable to write data to the transport connection: An operation was attempted on something that is not a socket.")

이를 통해, 자신이 무심코 넘겼던 예외를 다시 한번 돌아보게 됩니다. ^^ 만약, 위의 예외 중에 심각한 것이 있다면 그에 대해 덤프를 남겨서 좀 더 상세하게 분석할 수 있습니다. 예를 들어, 위에서 "System.ObjectDisposedException"을 알고 싶다면 procdump를 다음의 옵션으로 다시 실행하면 됩니다.

procdump -ma  -e 1 -f System.ObjectDisposedException 11096

또는, 비주얼 스튜디오가 설치된 경우라면 Thrown 설정을 해서 쉽게 원인 파악을 할 수 있습니다.

이를 응용할 수 있는 사례를 하나 떠 올려보면. 배포한 응용 프로그램에서 사용자가 어떤 동작을 시켰는데 아무런 실행 결과도 없을 때가 있습니다. 대개의 경우, 개발자가 과다한 try/catch를 사용해 어디선가 예외가 먹혀서 아무런 동작도 발생하지 않은 것처럼 보이는 건데요. 물론 상세한 오류 로그를 남기는 것으로 이 상황을 타개할 수는 있지만 그런 오류 남기는 작업이 안되어 있다면 관련 코드를 넣은 후 다시 사용자 컴퓨터에 배포한 후 실행해 봐야 하는 수고로움이 있습니다.

그럴 때, procdump.exe를 실행해 놓고 사용자가 그 기능을 다시 실행하면 됩니다. 만약 '예외가 먹힌 상황'이었다면 화면에 여지없이 그에 대한 내역이 출력될 것입니다. 아울러 덤프도 남겨서 사후 분석도 가능하고. (덤프를 남겼다면, 잊지 말고 사용자 컴퓨터에 있는 해당 닷넷 버전의 sos.dll과 mscordacwks.dll 파일을 챙기세요. ^^)




[이 글에 대해서 여러분들과 의견을 공유하고 싶습니다. 틀리거나 미흡한 부분 또는 의문 사항이 있으시면 언제든 댓글 남겨주십시오.]

[연관 글]






[최초 등록일: ]
[최종 수정일: 7/10/2021]

Creative Commons License
이 저작물은 크리에이티브 커먼즈 코리아 저작자표시-비영리-변경금지 2.0 대한민국 라이센스에 따라 이용하실 수 있습니다.
by SeongTae Jeong, mailto:techsharer at outlook.com

비밀번호

댓글 작성자
 



2025-07-14 10시51분
Decoding the parameters of a thrown C++ exception (0xE06D7363)
; https://devblogs.microsoft.com/oldnewthing/20100730-00/?p=13273

0xE06D7363 에러 코드는 Visual C++ 컴파일러가 다루는 코드라서 Win32 관련 헤더 파일에는 정의돼 있지 않다고 합니다. 예를 들어, 아래와 같은 코드는 C++에서의 오류일 뿐 Win32와는 직접적으로 상관이 없는 경우입니다.

---------------------
int test()
{
    throw 1;
}
---------------------

따라서, 이런 에러가 발생했을 때는 Win32와 무관하게 Visual C++이 생성한 규칙에 따라 정보를 파악해야 하는데, 위의 글에서는 그 방법을 다룹니다. (단지, Visual C++의 내부 구현 사항이라 향후 달라질 가능성도 있다고 합니다.)

즉, 해당 에러가 발생했을 때의 인자가 4개까지 관여할 수 있는데,

parameter 0: [생략]
parameter 1: "throw ..."로 던져진 예외 개체에 대한 포인터
parameter 2: "throw ..."로 던져진 개체를 설명하는 정보를 가리키는 포인터
parameter 3: 예외가 발생한 모듈의 HINSTANCE (64비트에서만 유효)

예를 들어 예외 개체에 대한 class name을 다음의 단계로 구할 수 있습니다.

EXCEPTION_RECORD
+----------+
| E06D7363 |
+----------+
| ~~~ |
+----------+
|* ~~~ |
+----------+
|* ~~~ |
+----------+
| 3 or 4 | // 3 == 32비트, 4 == 64비트
+----------+
|* ~~~ |
+----------+
|*Object |
+----------+ +---+
|* ------> |~~~|
+----------+ +---+
|*HINSTANCE| |~~~|
+----------+ +---+
                 |~~~|
                 +---+ +---+
                 | -----> |~~~|
                 +---+ +---+ +---+
                          | -----> |~~~|
                          +---+ +---+ +----------+
                                   | -----> |* ~~~ |
                                   +---+ +----------+
                                            |* ~~~ |
                                            +----------+
                                            |Class name|
                                            +----------+

만약 windbg에서 64비트 덤프를 분석하는 경우라면,

0:008> .exr 00000000`015dede0
ExceptionAddress: 000007fefd23bb5d (KERNEL32!RaiseException+0x39)
   ExceptionCode: e06d7363 (C++ EH exception)
  ExceptionFlags: 00000001
NumberParameters: 4 // this is running on 64-bit Windows
   Parameter[0]: 0000000019930520
   Parameter[1]: 00000000015def30 // object being thrown
   Parameter[2]: 00000000100cefa8 // magic Parameter 2
   Parameter[3]: 0000000010000000 // HINSTANCE

이후, parameter 2의 4번째 값을 구하고,

0:008> dd 100cefc8 l2
00000000`100cefc8 00000005 000ceff8
                            ^^^^^^^^

(64비트 환경이므로) 저 값에 HINSTNACE 값을 더한 위치의 2번째 값을 구하고,

0:008> dd 100ceff8 l2
00000000`100ceff8 00000001 000d6670
                            ^^^^^^^^

마지막으로 다시 HINSTNACE + 000d6670의 위치에서 64비트인 경우 16바이트, 32비트인 경우 8바이트 이후의 위치를 덤프하면 클래스 이름이 나옵니다.

0:008> da 100d6670+10
00000000`100d6680 ".PEAVCResourceException@@"

mangling 텍스트를 제외하면 실제 예외 이름은 "CResource­Exception"이 됩니다. 참고로, 만약 저 과정을 32비트에서 했다면 다음과 같이 풀이할 수 있습니다.

0:000> .exr 0008f2e4
ExceptionAddress: 7671b046 (kernel32!RaiseException)
   ExceptionCode: e06d7363 (C++ EH exception)
  ExceptionFlags: 00000001
NumberParameters: 3 // 32-bit platform
   Parameter[0]: 19930520
   Parameter[1]: 0008f384 // object being thrown
   Parameter[2]: 10cfed60 // magic Parameter 2

0:000> dd 10cfed60 l4
10cfed60 00000000 00000000 00000000 10db297c

0:000> dd 10db297c l2
10db297c 00000004 10db2990

0:000> dd 10db2990 l2
10db2990 00000001 10dbccac

0:000> da 10dbccac+8
10dbccb4 ".PAVCFileException@@"

-------------------------------------------------------

이후의 글에서,

Using the wrong HINSTANCE in RegisterClass is like identity theft
; https://devblogs.microsoft.com/oldnewthing/20110715-00/?p=10133

저 사례가 발생했던 경우를 이야기로 풀어내고 있습니다. 간단하게 정리하면, DLL에서 RegisterClass를 사용했는데,

RegisterClassA 함수(winuser.h)
; https://learn.microsoft.com/ko-kr/windows/win32/api/winuser/nf-winuser-registerclassa

WNDCLASS.hInstance 필드에 DLL의 Module Instance가 아닌 EXE의 HINSTANCE를 전달했기 때문입니다.
정성태
2025-07-14 11시11분
If you want to terminate on an unexpected exception, then don’t sniff at every exception; just let the process terminate
; https://devblogs.microsoft.com/oldnewthing/20191024-00/?p=103022

Visual C++에서 noexcept와, 그것을 사용자가 직접 코드로 catch (...)로 잡아 std::terminate를 호출했을 때의 차이점을 설명하고 있습니다.

예를 들어, 아래의 예제 코드는,

#include <cstdlib>
#include <new>
#include <exception>
#include <errno.h>

struct MyCustomExceptionClass
{
    int code;
};

int oopsie()
{
    int value = std::rand();
    if (value >= 0) throw 1; // totally disallowed exception
    return value;
}

int victim() try
{
    return oopsie();
}
catch (MyCustomExceptionClass const& ex)
{
    return ex.code;
}
catch (std::bad_alloc const& ex)
{
    return ENOMEM;
}
catch (...)
{
   std::terminate();
}

int main()
{
    return victim();
}

crash 덤프에서 실제 terminate를 호출한 oopsie 함수가 안 보이는데,

------------------------------
_exit+0x11
abort+0xe8
terminate+0x3b
victim+0x5b ⇐ no sign of oopsie
main+0xd
------------------------------

대신 다음과 같이 victim 함수에서 예외를 처리하도록 바꾸면,

void victim() noexcept try
{
    oopsie();
}
catch (MyCustomExceptionClass const& ex)
{
    return ex.code;
}
catch (std::bad_alloc const& ex)
{
    return ENOMEM;
}
// catch (...)
// {
// std::terminate();
// }

이제 정상적인 호출 스택을 얻을 수 있습니다.

_exit+0x11
abort+0xe8
terminate+0x3b
FindHandler+0x377
__InternalCxxFrameHandler+0xf7
__CxxFrameHandler2+0x26
ExecuteHandler2+0x26
ExecuteHandler+0x24
KiUserExceptionDispatcher+0x26
RaiseException+0x62
_CxxThrowException+0x68
oopsie+0x2c ⇐ here's the bad boy
victim+0x3a
main+0x33

참고로, 위와 같은 스택 풀이(unwinding) 여부는 C++ 표준에서 구현체에 맡기는 걸로 돼 있다고 합니다. 단지 Visual C++의 경우에는 스택 해제를 하지 않는 방식을 채택한 것이고, 따라서 위와 같은 상황에서 oopsie 스택 프레임으로 전환하면 로컬 변수의 값의 값까지 확인할 수 있게 됩니다.

0:000> .frame 5
05 02a9fba0 00751e0a scratch!oopsie+0x2c
0:000> dv
          value = 0n41
정성태

1  [2]  3  4  5  6  7  8  9  10  11  12  13  14  15  ...
NoWriterDateCnt.TitleFile(s)
13942정성태6/5/20251800오류 유형: 959. winget 설치 시 "0x80d02002 : unknown error"
13941정성태6/2/20251749닷넷: 2334. C# - cpuid 명령어를 이용한 CPU 제조사 문자열 가져오기파일 다운로드1
13940정성태6/1/20252118C/C++: 188. C++의 32비트 + Release 어셈블리 코드를 .NET으로 포팅할 때 주의할 점파일 다운로드1
13939정성태5/29/20252859오류 유형: 958. NVIDIA Triton Inference Server - version `GLIBCXX_3.4.32' not found (required by /opt/tritonserver/backends/python/triton_python_backend_stub)
13938정성태5/29/20251988개발 환경 구성: 747. 파이썬 - WSL/docker에 구성한 Triton 예제 개발 환경
13937정성태5/24/20252312개발 환경 구성: 746. Windows + WSL2 환경에서 (tensorflow 등의) NVIDIA GPU 인식
13936정성태5/23/20252077개발 환경 구성: 745. Linux / WSL 환경에 Miniconda 설치하기
13935정성태5/20/20251986오류 유형: 957. 파이썬 - pip 사용 시 "ImportError: cannot import name 'html5lib' from 'pip._vendor'" 오류
13934정성태5/20/20252850스크립트: 77. 파이썬 - 'urllib.request' 모듈의 명시적/암시적 로딩 차이
13933정성태5/19/20252124오류 유형: 956. Visual Studio 2022가 17.12 버전부터 업데이트 되지 않는다면?
13932정성태5/18/20252468스크립트: 76. 파이썬 - Version 문자열 다루기(semver 패키지)
13931정성태5/17/20252801스크립트: 75. 파이썬 - Cython 기본 예제 및 컴파일
13930정성태5/17/20252255개발 환경 구성: 744. 파이썬 - Windows embeddable package 환경에서 외부 패키지 사용하는 방법(ex: UFO² 환경 구성)
13929정성태5/16/20252433오류 유형: 955. 파이썬 - "Windows embeddable package" REPL 환경에서 "NameError: name 'exit' is not defined"
13928정성태5/15/20252591오류 유형: 954. UFO² - "'Invalid URL (POST /v1/chat/completions/chat/completions)'"
13927정성태5/15/20252438오류 유형: 953. OpenAI - The API request of HOST_AGENT failed: OpenAI API request exceeded rate limit: Error code: 429
13926정성태5/14/20253080개발 환경 구성: 743. LLM과 윈도우의 만남 - Desktop AgentOS UFO² 기본 환경 구성
13925정성태5/12/20253104닷넷: 2333. C# - (Console 유형의 프로젝트에서) Clipboard 연동파일 다운로드1
13924정성태5/8/20252761닷넷: 2332. C# - (JetBrains Omea Reader 대상으로) 런타임 시에 메서드 가로채기 [2]파일 다운로드1
13923정성태5/5/20252160스크립트: 74. 파이썬 - C# - Python.NET의 RunSimpleScript, Exec, Eval 차이점파일 다운로드1
13922정성태5/3/20252790스크립트: 73. 파이썬 - Windows embeddable package 버전에서 tkinter 환경 구성
13921정성태5/3/20253349오류 유형: 952. 듀얼 채널 메모리 정렬을 지키지 않은 컴퓨터의 Windows 비정상 종료 현상(Blue Screen) [2]
13920정성태5/3/20253447오류 유형: 951. Typed DataSet 생성 중 "Failed to open a connection to the database" 오류
13919정성태5/2/20252803VS.NET IDE: 201. C# - Typed DataSet(XSD)를 위한 연결 문자열 암호화 [1]파일 다운로드1
13918정성태5/2/20253576VS.NET IDE: 200. C# - app.config 파일의 출력을 Configuration(Debug/Release)에 따라 제어하는 방법파일 다운로드1
13917정성태4/30/20252500VS.NET IDE: 199. Directory.Build.props에 정의한 속성에 대해 Condition 제약으로 값을 변경하는 방법
1  [2]  3  4  5  6  7  8  9  10  11  12  13  14  15  ...