Microsoft MVP성태의 닷넷 이야기
디버깅 기술: 218. Windbg로 살펴보는 Win32 Critical Section [링크 복사], [링크+제목 복사],
조회: 1909
글쓴 사람
정성태 (seongtaejeong at gmail.com)
홈페이지
첨부 파일

(시리즈 글이 2개 있습니다.)
디버깅 기술: 218. Windbg로 살펴보는 Win32 Critical Section
; https://www.sysnet.pe.kr/2/0/13904

Windows: 281. C++ - Windows / Critical Section의 안정화를 위해 도입된 "Keyed Event"
; https://www.sysnet.pe.kr/2/0/13905




Windbg로 살펴보는 Win32 Critical Section

그러고 보니, 제 블로그에서 Critical Section에 대한 글을 쓴 적이 없군요. ^^; 기본적인 사용법은 이미 다른 글에 많이 소개돼 있기 때문에, 대신 Windbg로 추적하는 과정을 보겠습니다.

자, 그럼 이를 위해 간단하게 다음과 같이 예제 코드를 작성하고,

#include <Windows.h>
#include <iostream>

using namespace std;

CRITICAL_SECTION g_cs; // 전역 변수

int main()
{
    InitializeCriticalSection(&g_cs);

    CRITICAL_SECTION l_cs; // 로컬 변수

    cout << "Before InitializeCriticalSection" << endl;
    cin.get();

    InitializeCriticalSection(&l_cs);

    cout << "Before EnterCriticalSection" << endl;
    cin.get();

    EnterCriticalSection(&l_cs);

    cout << "Before LeaveCriticalSection" << endl;
    cin.get();

    LeaveCriticalSection(&l_cs);

    cout << "Before DeleteCriticalSection" << endl;
    cin.get();

    DeleteCriticalSection(&l_cs);
    cout << "Press any key to exit" << endl;
    cin.get();

    return 0;
}

첫 번째 "cin.get()" 호출에서 실행을 멈췄을 때 Windbg로 붙여 cs 변수를 이런 식으로 찾아들어갈 수 있습니다.

0:005> ~
   0  Id: e554.ac68 Suspend: 1 Teb: 00000017`812b6000 Unfrozen
   1  Id: e554.10064 Suspend: 1 Teb: 00000017`812b8000 Unfrozen
   2  Id: e554.b98 Suspend: 1 Teb: 00000017`812ba000 Unfrozen
   3  Id: e554.9dd0 Suspend: 1 Teb: 00000017`812bc000 Unfrozen
   4  Id: e554.b598 Suspend: 1 Teb: 00000017`812c0000 Unfrozen
.  5  Id: e554.10508 Suspend: 1 Teb: 00000017`812c4000 Unfrozen

0:004> ~0s
ntdll!NtReadFile+0x14:
00007ffe`2cb5f8b4 c3              ret

0:000> k
 # Child-SP          RetAddr               Call Site
00 00000017`8112f1f8 00007ffe`29c4d71d     ntdll!NtReadFile+0x14
01 00000017`8112f200 00007ffc`d946760c     KERNELBASE!ReadFile+0x8d
02 00000017`8112f270 00007ffc`d9466a53     ucrtbased!_read_nolock+0xb9c [minkernel\crts\ucrt\src\appcrt\lowio\read.cpp @ 566] 
03 00000017`8112f370 00007ffc`d940fc26     ucrtbased!_read+0x333 [minkernel\crts\ucrt\src\appcrt\lowio\read.cpp @ 396] 
04 00000017`8112f3d0 00007ffc`d94104d0     ucrtbased!common_refill_and_read_nolock<char>+0x1f6 [minkernel\crts\ucrt\src\appcrt\stdio\_filbuf.cpp @ 146] 
05 00000017`8112f460 00007ffc`d93c717e     ucrtbased!__acrt_stdio_refill_and_read_narrow_nolock+0x20 [minkernel\crts\ucrt\src\appcrt\stdio\_filbuf.cpp @ 181] 
06 00000017`8112f4a0 00007ffc`d93c749e     ucrtbased!_fgetc_nolock+0x10e [minkernel\crts\ucrt\src\appcrt\stdio\fgetc.cpp @ 23] 
07 00000017`8112f510 00007ffd`425f6359     ucrtbased!fgetc+0x28e [minkernel\crts\ucrt\src\appcrt\stdio\fgetc.cpp @ 52] 
08 00000017`8112f5a0 00007ffd`42603515     MSVCP140D!std::_Fgetc<char>+0x19 [D:\a\_work\1\s\src\vctools\crt\github\stl\inc\__msvc_filebuf.hpp @ 75] 
09 00000017`8112f5e0 00007ffd`426038b8     MSVCP140D!std::basic_filebuf<char,std::char_traits<char> >::uflow+0xc5 [D:\a\_work\1\s\src\vctools\crt\github\stl\inc\__msvc_filebuf.hpp @ 490] 
0a 00000017`8112f6d0 00007ffd`42610671     MSVCP140D!std::basic_filebuf<char,std::char_traits<char> >::underflow+0x78 [D:\a\_work\1\s\src\vctools\crt\github\stl\inc\__msvc_filebuf.hpp @ 470] 
0b 00000017`8112f730 00007ffd`4266c96f     MSVCP140D!std::basic_streambuf<char,std::char_traits<char> >::sgetc+0x51 [D:\a\_work\1\s\src\vctools\crt\github\stl\inc\streambuf @ 129] 
0c 00000017`8112f770 00007ff6`8dcd695f     MSVCP140D!std::basic_istream<char,std::char_traits<char> >::get+0x7f [D:\a\_work\1\s\src\vctools\crt\github\stl\inc\istream @ 347] 
0d 00000017`8112f7d0 00007ff6`8dcd2f09     ConsoleApplication1!main+0x6f [C:\temp\ConsoleApplication1\ConsoleApplication1\ConsoleApplication1.cpp @ 14] 
0e 00000017`8112f910 00007ff6`8dcd2db2     ConsoleApplication1!invoke_main+0x39 [D:\a\_work\1\s\src\vctools\crt\vcstartup\src\startup\exe_common.inl @ 79] 
0f 00000017`8112f960 00007ff6`8dcd2c6e     ConsoleApplication1!__scrt_common_main_seh+0x132 [D:\a\_work\1\s\src\vctools\crt\vcstartup\src\startup\exe_common.inl @ 288] 
10 00000017`8112f9d0 00007ff6`8dcd2f9e     ConsoleApplication1!__scrt_common_main+0xe [D:\a\_work\1\s\src\vctools\crt\vcstartup\src\startup\exe_common.inl @ 331] 
11 00000017`8112fa00 00007ffe`2babe8d7     ConsoleApplication1!mainCRTStartup+0xe [D:\a\_work\1\s\src\vctools\crt\vcstartup\src\startup\exe_main.cpp @ 17] 
12 00000017`8112fa30 00007ffe`2cabbf6c     KERNEL32!BaseThreadInitThunk+0x17
13 00000017`8112fa60 00000000`00000000     ntdll!RtlUserThreadStart+0x2c

0:000> .frame 0d
0d 00000017`8112f7d0 00007ff6`8dcd2f09     ConsoleApplication1!main+0x6f [C:\temp\ConsoleApplication1\ConsoleApplication1\ConsoleApplication1.cpp @ 14] 

0:000> dv /v
00000017`8112f7f8              l_cs = struct _RTL_CRITICAL_SECTION

l_cs 변수의 주소와 구조체 이름을 얻었으니 간단하게 dt 명령어로 내부 필드를 볼 수 있습니다.

// InitializeCriticalSection 호출 전의 변수 상태

0:000> dt _RTL_CRITICAL_SECTION 00000017`8112f7f8
ConsoleApplication1!_RTL_CRITICAL_SECTION
   +0x000 DebugInfo        : 0xcccccccc`cccccccc _RTL_CRITICAL_SECTION_DEBUG
   +0x008 LockCount        : 0n-858993460
   +0x00c RecursionCount   : 0n-858993460
   +0x010 OwningThread     : 0xcccccccc`cccccccc Void
   +0x018 LockSemaphore    : 0xcccccccc`cccccccc Void
   +0x020 SpinCount        : 0xcccccccc`cccccccc

아직은 뭐, 지역 변수로 위치만 잡고 있는 상태이기 때문에 쓰레기 값이 들어가 있는 정도인데요, 이후 InitializeCriticalSection 함수를 호출하면 이런 식으로 초기화가 됩니다.

ConsoleApplication1!_RTL_CRITICAL_SECTION
   +0x000 DebugInfo        : 0xffffffff`ffffffff _RTL_CRITICAL_SECTION_DEBUG
   +0x008 LockCount        : 0n-1
   +0x00c RecursionCount   : 0n0
   +0x010 OwningThread     : (null) 
   +0x018 LockSemaphore    : (null) 
   +0x020 SpinCount        : 0x20007d0

그다음 EnterCriticalSection 함수를 호출하면 OwningThread에 값이 기록되고,

0:005> dt _RTL_CRITICAL_SECTION 00000017`8112f7f8
ConsoleApplication1!_RTL_CRITICAL_SECTION
   +0x000 DebugInfo        : 0xffffffff`ffffffff _RTL_CRITICAL_SECTION_DEBUG
   +0x008 LockCount        : 0n-2
   +0x00c RecursionCount   : 0n1
   +0x010 OwningThread     : 0x00000000`0000ac68 Void
   +0x018 LockSemaphore    : (null) 
   +0x020 SpinCount        : 0x20007d0

0:005> ~
   0  Id: e554.ac68 Suspend: 1 Teb: 00000017`812b6000 Unfrozen
   1  Id: e554.10064 Suspend: 1 Teb: 00000017`812b8000 Unfrozen
   2  Id: e554.b98 Suspend: 1 Teb: 00000017`812ba000 Unfrozen
   3  Id: e554.9dd0 Suspend: 1 Teb: 00000017`812bc000 Unfrozen
   4  Id: e554.b598 Suspend: 1 Teb: 00000017`812c0000 Unfrozen
.  5  Id: e554.10508 Suspend: 1 Teb: 00000017`812c4000 Unfrozen

따라서 현재 어떤 스레드가 Critical Section을 소유하고 있는지 확인할 수 있습니다. 이후 LeaveCriticalSection 함수를 호출하면 OwningThread에 NULL이 기록될 것이고,

ConsoleApplication1!_RTL_CRITICAL_SECTION
   +0x000 DebugInfo        : 0xffffffff`ffffffff _RTL_CRITICAL_SECTION_DEBUG
   +0x008 LockCount        : 0n-1
   +0x00c RecursionCount   : 0n0
   +0x010 OwningThread     : (null) 
   +0x018 LockSemaphore    : (null) 
   +0x020 SpinCount        : 0x20007d0

마지막으로 DeleteCriticalSection 함수를 호출하면 이런 식으로 자원이 정리됩니다.

ConsoleApplication1!_RTL_CRITICAL_SECTION
   +0x000 DebugInfo        : (null) 
   +0x008 LockCount        : 0n0
   +0x00c RecursionCount   : 0n0
   +0x010 OwningThread     : (null) 
   +0x018 LockSemaphore    : (null) 
   +0x020 SpinCount        : 0




만약 Critical Section으로 인한 hang 현상이 발생했다면, 위에서 살펴봤듯이 OwningThread를 확인하면 어떤 스레드가 Critical Section을 소유하고 있는지 확인할 수 있고, 그 스레드로 이동해 호출 스택을 확인함으로써 문제의 원인을 찾을 수 있습니다.

WinDbg : Debugging A Critical Section Based Dead Lock Scenario
; https://jumpdollar.blogspot.com/2014/10/windbg-debugging-critical-section-based.html

게다가 Critical Section을 열거하는 !cs 명령어도 제공하는데요,

// -l 옵션을 함께 주면 "LOCKED" 상태인 Critical Section만 출력

0:000> !cs
-----------------------------------------
DebugInfo          = 0x00007ffe2cbd1fc0
Critical section   = 0x00007ffe2cbd1f00 (ntdll!RtlpProcessHeapsLock+0x0)
NOT LOCKED
LockSemaphore      = 0x0
SpinCount          = 0x00000000020007d0
-----------------------------------------
DebugInfo          = 0x00007ffe2cbd1ff0
Critical section   = 0x0000026848da02c0 (+0x26848DA02C0)
NOT LOCKED
LockSemaphore      = 0x0
SpinCount          = 0x00000000020007d0
-----------------------------------------
DebugInfo          = 0x00007ffe2cbd2020
Critical section   = 0x0000026848d002c0 (+0x26848D002C0)
NOT LOCKED
LockSemaphore      = 0x0
SpinCount          = 0x00000000020007d0
-----------------------------------------
DebugInfo          = 0x00007ffe2cbca860
Critical section   = 0x00007ffe2cbca838 (ntdll!LdrpSnapsLock+0x0)
NOT LOCKED
LockSemaphore      = 0x0
SpinCount          = 0x0000000004000000

인자로 개별 Critical Section을 지정해서 조회하는 것도 가능하고,

// CRITICAL_SECTION 지역 변수의 주소가 0000000d`c32ffa48인 것으로 가정

0:000> !cs 0000000d`c32ffa48
-----------------------------------------
Critical section   = 0x0000000dc32ffa48 (+0xDC32FFA48)
DebugInfo          = 0xffffffffffffffff
LOCKED
LockCount          = 0x0
WaiterWoken        = No
OwningThread       = 0x000000000000f2c8
RecursionCount     = 0x1
LockSemaphore      = 0x0
SpinCount          = 0x00000000020007d0

전역 변수라면 심벌을 이용해 바로 찾을 수도 있습니다.

0:001> !cs ConsoleApplication1!g_cs
-----------------------------------------
Critical section   = 0x00007ff7c714d750 (ConsoleApplication1!g_cs+0x0)
DebugInfo          = 0xffffffffffffffff
NOT LOCKED
LockSemaphore      = 0x0
SpinCount          = 0x00000000020007d0

참고로 (Exts.dll 확장 DLL에서 제공하는) !cs 명령어와 함께 Ntsdexts.dll 확장 DLL에서도 이와 유사한 !critsec 명령어가 있긴 하지만,

0:001> !critsec ConsoleApplication1!g_cs

DebugInfo for CritSec at 00007ff7c714d750 could not be read
Probably NOT an initialized critical section.

CritSec ConsoleApplication1!g_cs+0 at 00007ff7c714d750
LockCount          NOT LOCKED
RecursionCount     0
OwningThread       0

그다지 알 필요는 없을 듯합니다. ^^;




그런데, !cs 명령어의 출력 결과를 보면 예제 코드에서 우리가 직접 정의한 Critical Section의 전역 변수(g_cs)와 지역 변수(l_cs)가 모두 누락돼 있습니다. 정확한 선정 기준은 알 수 없지만 테스트를 통해 짐작은 할 수 있는데요, 이를 위해 다음과 같이 예제 코드를 변경해 보겠습니다.

#include <Windows.h>
#include <iostream>
#include <thread>

using namespace std;

CRITICAL_SECTION g_cs;

void DoWork(CRITICAL_SECTION* pcs, int seconds)
{
    EnterCriticalSection(pcs);
    Sleep(1000 * seconds);
    LeaveCriticalSection(pcs);
}

int main()
{
    InitializeCriticalSection(&g_cs);

    CRITICAL_SECTION l_cs;
    InitializeCriticalSection(&l_cs);

    cout << "addr of l_cs: " << &l_cs << endl;
    cout << "addr of g_cs: " << &g_cs << endl;

    thread t1([&]() {
        DoWork(&g_cs, 2);
        DoWork(&l_cs, 5);
        });

    thread t2([&]() {
        DoWork(&g_cs, 2);
        DoWork(&l_cs, 5);
        });

    t1.join();
    t2.join();

    cout << "Press ENTER key to exit..." << endl;
    cin.get();

    return 0;
}

위와 같이, 한 번이라도 경합을 벌이고 나면 이제 !cs 명령어의 출력에 g_cs, l_cs 변수가 포함됩니다.

0:004> !cs
-----------------------------------------
...[생략]...
-----------------------------------------
DebugInfo          = 0x00007ffe2cbd2050
Critical section   = 0x0000028fbc9402c0 (+0x28FBC9402C0) // 지역 변수 l_cs
NOT LOCKED
LockSemaphore      = 0x0
SpinCount          = 0x00000000020007d0
-----------------------------------------
DebugInfo          = 0x00007ffe2cbd2080
Critical section   = 0x00007ff7f5ff1470 (ConsoleApplication2!g_cs+0x0) // 전역 변수 g_cs
NOT LOCKED
LockSemaphore      = 0xFFFFFFFF
SpinCount          = 0x00000000020007cf

위의 출력에서 재미있는 점은, DebugInfo 필드의 값도 새롭게 채워져 있다는 점입니다. 아마도 DebugInfo 값이 채워지는 그 시점에 !cs 명령어가 알 수 있는 Critical Section의 목록에 추가되는 것으로 짐작됩니다.

그리고 DebugInfo 역시 _RTL_CRITICAL_SECTION_DEBUG로 알려진 구조체이므로 이 값을 조회하는 것도 가능합니다.

0:004> dt _RTL_CRITICAL_SECTION_DEBUG 0x00007ffe2cbd2080
ConsoleApplication2!_RTL_CRITICAL_SECTION_DEBUG
   +0x000 Type             : 0
   +0x002 CreatorBackTraceIndex : 0
   +0x008 CriticalSection  : 0x00007ff7`f5ff1470 _RTL_CRITICAL_SECTION
   +0x010 ProcessLocksList : _LIST_ENTRY [ 0x00007ffe`2cbca8d0 - 0x00007ffe`2cbd2060 ]
   +0x020 EntryCount       : 0
   +0x024 ContentionCount  : 1
   +0x028 Flags            : 0
   +0x02c CreatorBackTraceIndexHigh : 0
   +0x02e Identifier       : 0x4353

참고로 아래는 SDK에 담긴 헤더 파일에서 확인한 구조체 정의입니다.

// winnt.h

typedef struct _RTL_CRITICAL_SECTION_DEBUG {
    WORD   Type;
    WORD   CreatorBackTraceIndex;
    struct _RTL_CRITICAL_SECTION *CriticalSection;
    LIST_ENTRY ProcessLocksList;
    DWORD EntryCount;
    DWORD ContentionCount; // 경합이 발생한 누적 횟수
    DWORD Flags;
    WORD   CreatorBackTraceIndexHigh;
    WORD   Identifier;
} RTL_CRITICAL_SECTION_DEBUG, *PRTL_CRITICAL_SECTION_DEBUG, RTL_RESOURCE_DEBUG, *PRTL_RESOURCE_DEBUG;

typedef struct _RTL_CRITICAL_SECTION {
    PRTL_CRITICAL_SECTION_DEBUG DebugInfo;

    //
    //  The following three fields control entering and exiting the critical
    //  section for the resource
    //

    LONG LockCount;
    LONG RecursionCount;  // 동일한 스레드가 같은 lock을 진입한 횟수
    HANDLE OwningThread;        // from the thread's ClientId->UniqueThread
    HANDLE LockSemaphore;
    ULONG_PTR SpinCount;        // force size on 64-bit systems when packed
} RTL_CRITICAL_SECTION, *PRTL_CRITICAL_SECTION;

// minwinbase.h

typedef RTL_CRITICAL_SECTION CRITICAL_SECTION;




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







[최초 등록일: ]
[최종 수정일: 3/27/2025]

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

비밀번호

댓글 작성자
 




... 16  17  [18]  19  20  21  22  23  24  25  26  27  28  29  30  ...
NoWriterDateCnt.TitleFile(s)
13490정성태12/19/202310091개발 환경 구성: 698. Golang - GLIBC 의존을 없애는 정적 빌드 방법
13489정성태12/19/202310040개발 환경 구성: 697. GoLand에서 ldflags 지정 방법
13488정성태12/18/20239500오류 유형: 884. HTTP 500.0 - 명령행에서 실행한 ASP.NET Core 응용 프로그램을 실행하는 방법
13487정성태12/16/202310606개발 환경 구성: 696. C# - 리눅스용 AOT 빌드를 docker에서 수행 [1]
13486정성태12/15/20239726개발 환경 구성: 695. Nuget config 파일에 값 설정/삭제 방법
13485정성태12/15/20239405오류 유형: 883. dotnet build/restore - error : Root element is missing
13484정성태12/14/202310136개발 환경 구성: 694. Windows 디렉터리 경로를 WSL의 /mnt 포맷으로 구하는 방법
13483정성태12/14/202310395닷넷: 2184. C# - 하나의 resource 파일을 여러 프로그램에서 (AOT 시에도) 사용하는 방법파일 다운로드1
13482정성태12/13/202311618닷넷: 2183. C# - eFriend Expert OCX 예제를 .NET Core/5+ Console App에서 사용하는 방법 [2]파일 다운로드1
13481정성태12/13/202310527개발 환경 구성: 693. msbuild - .NET Core/5+ 프로젝트에서 resgen을 이용한 리소스 파일 생성 방법파일 다운로드1
13480정성태12/12/202312363개발 환경 구성: 692. Windows WSL 2 + Chrome 웹 브라우저 설치
13479정성태12/11/202310017개발 환경 구성: 691. WSL 2 (Ubuntu) + nginx 환경 설정
13477정성태12/8/202310644닷넷: 2182. C# - .NET 7부터 추가된 Int128, UInt128 [1]파일 다운로드1
13476정성태12/8/202310497닷넷: 2181. C# - .NET 8 JsonStringEnumConverter의 AOT를 위한 개선파일 다운로드1
13475정성태12/7/202310754닷넷: 2180. .NET 8 - 함수 포인터에 대한 Reflection 정보 조회파일 다운로드1
13474정성태12/6/202310339개발 환경 구성: 690. 닷넷 코어/5+ 버전의 ilasm/ildasm 실행 파일 구하는 방법 - 두 번째 이야기
13473정성태12/5/202310674닷넷: 2179. C# - 값 형식(Blittable)을 메모리 복사를 이용해 바이트 배열로 직렬화/역직렬화파일 다운로드1
13472정성태12/4/202310098C/C++: 164. Visual C++ - InterlockedCompareExchange128 사용 방법
13471정성태12/4/202310629Copilot - To enable GitHub Copilot, authorize this extension using GitHub's device flow
13470정성태12/2/202311242닷넷: 2178. C# - .NET 8부터 COM Interop에 대한 자동 소스 코드 생성 도입 [1]파일 다운로드1
13469정성태12/1/202311153닷넷: 2177. C# - (Interop DLL 없이) CoClass를 이용한 COM 개체 생성 방법파일 다운로드1
13468정성태12/1/202310158닷넷: 2176. C# - .NET Core/5+부터 달라진 RCW(Runtime Callable Wrapper) 대응 방식파일 다운로드1
13467정성태11/30/202310913오류 유형: 882. C# - Unhandled exception. System.Runtime.InteropServices.COMException (0x800080A5)파일 다운로드1
13466정성태11/29/202310743닷넷: 2175. C# - DllImport 메서드의 AOT 지원을 위한 LibraryImport 옵션
13465정성태11/28/202310449개발 환경 구성: 689. MSBuild - CopyToOutputDirectory가 "dotnet publish" 시에는 적용되지 않는 문제파일 다운로드1
13464정성태11/28/202310356닷넷: 2174. C# - .NET 7부터 UnmanagedCallersOnly 함수 export 기능을 AOT 빌드에 통합파일 다운로드1
... 16  17  [18]  19  20  21  22  23  24  25  26  27  28  29  30  ...