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

... 61  62  63  64  65  66  [67]  68  69  70  71  72  73  74  75  ...
NoWriterDateCnt.TitleFile(s)
12325정성태9/12/202018824개발 환경 구성: 515. OpenVPN - 재부팅 후 ICS(Internet Connection Sharing) 기능이 동작 안하는 문제
12324정성태9/11/202019252개발 환경 구성: 514. smigdeploy.exe를 이용한 Windows Server 2016에서 2019로 마이그레이션 방법
12323정성태9/11/202019527오류 유형: 649. Copy Database Wizard - The job failed. Check the event log on the destination server for details.
12322정성태9/11/202023563개발 환경 구성: 513. Azure VM의 RDP 접속 위치 제한 [1]
12321정성태9/11/202017942오류 유형: 648. netsh http add urlacl - Error: 183 Cannot create a file when that file already exists.
12320정성태9/11/202021194개발 환경 구성: 512. RDP(원격 데스크톱) 접속 시 비밀 번호를 한 번 더 입력해야 하는 경우
12319정성태9/10/202019489오류 유형: 647. smigdeploy.exe를 Windows Server 2016에서 실행할 때 .NET Framework 미설치 오류 발생
12318정성태9/9/202018032오류 유형: 646. OpenVPN - "TAP-Windows Adapter V9" 어댑터의 "Network cable unplugged" 현상
12317정성태9/9/202022700개발 환경 구성: 511. Beats용 Kibana 기본 대시 보드 구성 방법
12316정성태9/8/202020231디버깅 기술: 170. WinDbg Preview 버전부터 닷넷 코어 3.0 이후의 메모리 덤프에 대해 sos.dll 자동 로드
12315정성태9/7/202022473개발 환경 구성: 510. Logstash - FileBeat을 이용한 IIS 로그 처리 [2]
12314정성태9/7/202022863오류 유형: 645. IIS HTTPERR - Timer_MinBytesPerSecond, Timer_ConnectionIdle 로그
12313정성태9/6/202022623개발 환경 구성: 509. Logstash - 사용자 정의 grok 패턴 추가를 이용한 IIS 로그 처리
12312정성태9/5/202029699개발 환경 구성: 508. Logstash 기본 사용법 [2]
12311정성태9/4/202022408.NET Framework: 937. C# - 간단하게 만들어 보는 리눅스의 nc(netcat), json_pp 프로그램 [1]
12310정성태9/3/202021315오류 유형: 644. Windows could not start the Elasticsearch 7.9.0 (elasticsearch-service-x64) service on Local Computer.
12309정성태9/3/202019362개발 환경 구성: 507. Elasticsearch 6.6부터 기본 추가된 한글 형태소 분석기 노리(nori) 사용법
12308정성태9/2/202022756개발 환경 구성: 506. Windows - 단일 머신에서 단일 바이너리로 여러 개의 ElasticSearch 노드를 실행하는 방법
12307정성태9/2/202023796오류 유형: 643. curl - json_parse_exception / Invalid UTF-8 start byte
12306정성태9/1/202019452오류 유형: 642. SQL Server 시작 오류 - error code 10013
12305정성태9/1/202022831Windows: 172. "Administered port exclusions"이 아닌 포트 범위 항목을 삭제하는 방법
12304정성태8/31/202020858개발 환경 구성: 505. 윈도우 - (네트워크 어댑터의 우선순위로 인한) 열거되는 IP 주소 순서를 조정하는 방법
12303정성태8/30/202021454개발 환경 구성: 504. ETW - 닷넷 프레임워크 기반의 응용 프로그램을 위한 명령행 도구 etrace 소개
12302정성태8/30/202021457.NET Framework: 936. C# - ETW 관련 Win32 API 사용 예제 코드 (5) - Private Logger파일 다운로드1
12301정성태8/30/202019667오류 유형: 641. error MSB4044: The "Fody.WeavingTask" task was not given a value for the required parameter "IntermediateDir".
12300정성태8/29/202021327.NET Framework: 935. C# - ETW 관련 Win32 API 사용 예제 코드 (4) CLR ETW Consumer파일 다운로드1
... 61  62  63  64  65  66  [67]  68  69  70  71  72  73  74  75  ...