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
정성태

... [16]  17  18  19  20  21  22  23  24  25  26  27  28  29  30  ...
NoWriterDateCnt.TitleFile(s)
13590정성태3/31/202411365Linux: 70. Python - uwsgi 응용 프로그램이 k8s 환경에서 OOM 발생하는 문제
13589정성태3/29/202410830닷넷: 2233. C# - 프로세스 CPU 사용량을 나타내는 성능 카운터와 Win32 API파일 다운로드1
13588정성태3/28/202412589닷넷: 2232. C# - Unity + 닷넷 App(WinForms/WPF) 간의 Named Pipe 통신 [2]파일 다운로드1
13587정성태3/27/202410811오류 유형: 900. Windows Update 오류 - 8024402C, 80070643
13586정성태3/27/202414853Windows: 263. Windows - 복구 파티션(Recovery Partition) 용량을 늘리는 방법
13585정성태3/26/202411867Windows: 262. PerformanceCounter의 InstanceName에 pid를 추가한 "Process V2"
13584정성태3/26/202414208개발 환경 구성: 708. Unity3D - C# Windows Forms / WPF Application에 통합하는 방법 [9]파일 다운로드1
13583정성태3/25/202410870Windows: 261. CPU Utilization이 100% 넘는 경우를 성능 카운터로 확인하는 방법
13582정성태3/19/202413073Windows: 260. CPU 사용률을 나타내는 2가지 수치 - 사용량(Usage)과 활용률(Utilization)파일 다운로드1
13581정성태3/18/202412028개발 환경 구성: 707. 빌드한 Unity3D 프로그램을 C++ Windows Application에 통합하는 방법
13580정성태3/15/202410681닷넷: 2231. C# - ReceiveTimeout, SendTimeout이 적용되지 않는 Socket await 비동기 호출파일 다운로드1
13579정성태3/13/202412166오류 유형: 899. HTTP Error 500.32 - ANCM Failed to Load dll
13578정성태3/11/202413685닷넷: 2230. C# - 덮어쓰기 가능한 환형 큐 (Circular queue)파일 다운로드1
13577정성태3/9/202412572닷넷: 2229. C# - 닷넷을 위한 난독화 도구 소개 (예: ConfuserEx)
13576정성태3/8/202412531닷넷: 2228. .NET Profiler - IMetaDataEmit2::DefineMethodSpec 사용법
13575정성태3/7/202413411닷넷: 2227. 최신 C# 문법을 .NET Framework 프로젝트에 쓸 수 있을까요?
13574정성태3/6/202412799닷넷: 2226. C# - "Docker Desktop for Windows" Container 환경에서의 IPv6 DualMode 소켓
13573정성태3/5/202411022닷넷: 2225. Windbg - dumasync로 분석하는 async/await 호출
13572정성태3/4/202410924닷넷: 2224. C# - WPF의 Dispatcher Queue로 알아보는 await 호출의 hang 현상파일 다운로드1
13571정성태3/1/202410851닷넷: 2223. C# - await 호출과 WPF의 Dispatcher Queue 동작 확인파일 다운로드1
13570정성태2/29/202412408닷넷: 2222. C# - WPF의 Dispatcher Queue 동작 확인파일 다운로드1
13569정성태2/28/202411994닷넷: 2221. C# - LoadContext, LoadFromContext 그리고 GAC파일 다운로드1
13568정성태2/27/202412110닷넷: 2220. C# - .NET Framework 프로세스의 LoaderOptimization 설정을 확인하는 방법파일 다운로드1
13567정성태2/27/202412308오류 유형: 898. .NET Framework 3.5 이하에서 mscoree.tlb 참조 시 System.BadImageFormatException파일 다운로드1
13566정성태2/27/202411935오류 유형: 897. Windows 7 SDK 설치 시 ".NET Development" 옵션이 비활성으로 선택이 안 되는 경우
13565정성태2/23/20249998닷넷: 2219. .NET CLR2 보안 모델에서의 개별 System.Security.Permissions 제어
... [16]  17  18  19  20  21  22  23  24  25  26  27  28  29  30  ...