WDM/Architecture2016.01.13 10:20

코드를 작성하다가 복잡한 모듈을 작성하게 되는 경우 아래 현상을 겪을 수 있다.

동기화를 처리하기 위한 CriticalSection과 마이크로소프트 SDK API안에서 내부적으로 CriticalSection이 상호 충돌이 생길 수 있다.


최근 Loadlibrary와 타 기타 CriticalSection과의 충돌이 있어 사례를 소개하고자 한다.


특정프로세스가 행이 걸렸을 경우 덤프툴에 의해 덤프를 뜬 후 디버깅을 해보았다.

00000000`04e0f038 00000000`74d22bf1 : 00000000`00000000 00000000`00000000 00000000`00000000 00000000`00000000 : wow64cpu!CpupSyscallStub+0x9
00000000`04e0f040 00000000`74d9d132 : 00000000`00000000 00000000`74d21920 00000000`00000000 00000000`00000000 : wow64cpu!Thunk0ArgReloadState+0x23
00000000`04e0f100 00000000`74d9c54b : 00000000`00000000 00000000`00000000 00000000`00000000 00000000`00000000 : wow64!RunCpuSimulation+0xa
00000000`04e0f150 00000000`772fd447 : 00000000`00000000 00000000`7efdf000 00000000`7ef6d000 00000000`00000000 : wow64!Wow64LdrpInitialize+0x42b
00000000`04e0f6a0 00000000`772ac34e : 00000000`04e0f760 00000000`00000000 00000000`7efdf000 00000000`00000000 : ntdll! ?? ::FNODOBFM::`string'+0x29134
00000000`04e0f710 00000000`00000000 : 00000000`00000000 00000000`00000000 00000000`00000000 00000000`00000000 : ntdll!LdrInitializeThunk+0xe

역시 64비트 운영체제라서 32비트 프로세스에 대한 자세한 정보를 얻을 수 없다.

이때는 32비트 모드로 변환시켜주어야 한다.




0:000> !wow64exts.sw
Switched to 32bit mode

변환 후 콜 스택을 확인해보자


ChildEBP RetAddr  Args to Child             
0034d5f0 77498e44 000001b8 00000000 00000000 ntdll_77460000!ZwWaitForSingleObject+0x15 (FPO: [3,0,0])
0034d654 77498d28 00000000 00000000 00000001 ntdll_77460000!RtlpWaitOnCriticalSection+0x13e (FPO: [Non-Fpo])
0034d67c 7749c401 775620c0 7763d145 00000000 ntdll_77460000!RtlEnterCriticalSection+0x150 (FPO: [Non-Fpo])
0034d7e8 7749c558 0034d84c 0034d814 00000000 ntdll_77460000!LdrpLoadDll+0x287 (FPO: [Non-Fpo])
0034d820 76942c95 0034d814 0034d864 0034d84c ntdll_77460000!LdrLoadDll+0xaa (FPO: [Non-Fpo])
0034d85c 769a4924 00000000 00000000 0167d514 KERNELBASE!LoadLibraryExW+0x1f1 (FPO: [Non-Fpo])
*** ERROR: Symbol file could not be found.  Defaulted to export symbols for k_mcrypto.dll -
0034d870 7456447a 0034d888 7458ffe8 0034dffc kernel32!LoadLibraryW+0x11 (FPO: [Non-Fpo])
WARNING: Stack unwind information not available. Following frames may be wrong.
0034ddf4 00300063 00320035 00370035 00360034 xxxx!Java_xxxxx+0xa9a
0034ddf8 00320035 00370035 00360034 00360037 0x300063
0034ddfc 00370035 00360034 00360037 00320034 0x320035
0034de00 00360034 00360037 00320034 00380036 0x370035
0034de04 00360037 00320034 00380036 00380038 0x360034
0034de08 00320034 00380036 00380038 00380046 0x360037
0034de0c 00380036 00380038 00380046 00330032 0x320034
0034de10 00380038 00380046 00330032 00370042 0x380036
0034de14 00380046 00330032 00370042 00430031 0x380038
0034de18 00330032 00370042 00430031 00380032 0x380046
0034de1c 00370042 00430031 00380032 00760044 0x330032
0034de20 00430031 00380032 00760044 00640074 0x370042
0034e5bc 00000000 00000000 00000000 00000000 yyyyyyyy!xxxxx::xxxxxxxLog+0x13a1


x모듈과 y모듈이 상호 충돌이 되는 것을 확인 할 수 있으며, LoadLibraryEx 함수호출 되고 CriticalSection 진입 후 Wait에서 대기하고 있다.


진입 된 CriticalSection 객체를 조사해보자


0:000:x86> !cs 775620c0
Critical section   = 0x00000000775620c0 (ntdll_77460000!LdrpLoaderLock+0x0)
DebugInfo          = 0x0000000077564380
LockCount          = 0x8
WaiterWoken        = No
OwningThread       = 0x0000000000002228
RecursionCount     = 0x1
LockSemaphore      = 0x1B8
SpinCount          = 0x0000000000000000

0x2228 Thread에서 잠겨 있음이 확인된다.


해당 객체의 콜스택 정보를 보기 위해 요청을 하였으나 해당 Thread는 이미 종료된 Thread였다.


0:000:x86> ~
.  0  Id: 10a8.1164 Suspend: 0 Teb: 7efdb000 Unfrozen
   1  Id: 10a8.17f8 Suspend: 0 Teb: 7efd8000 Unfrozen
   2  Id: 10a8.1404 Suspend: 0 Teb: 7ef9d000 Unfrozen
   3  Id: 10a8.1408 Suspend: 0 Teb: 7ef9a000 Unfrozen
   4  Id: 10a8.17b8 Suspend: 0 Teb: 7ef97000 Unfrozen
   5  Id: 10a8.1728 Suspend: 0 Teb: 7ef94000 Unfrozen
   6  Id: 10a8.178c Suspend: 0 Teb: 7ef91000 Unfrozen
   7  Id: 10a8.1778 Suspend: 0 Teb: 7ef8e000 Unfrozen
   8  Id: 10a8.177c Suspend: 0 Teb: 7ef8b000 Unfrozen
   9  Id: 10a8.1784 Suspend: 0 Teb: 7ef85000 Unfrozen
  10  Id: 10a8.1770 Suspend: 0 Teb: 7ef82000 Unfrozen
  11  Id: 10a8.1774 Suspend: 0 Teb: 7ef7f000 Unfrozen
  12  Id: 10a8.145c Suspend: 0 Teb: 7ef7c000 Unfrozen
  13  Id: 10a8.cc0 Suspend: 0 Teb: 7ef88000 Unfrozen
  14  Id: 10a8.1844 Suspend: 0 Teb: 7ef79000 Unfrozen
  15  Id: 10a8.1d58 Suspend: 0 Teb: 7ef76000 Unfrozen
  16  Id: 10a8.2318 Suspend: 0 Teb: 7ef73000 Unfrozen
  17  Id: 10a8.1838 Suspend: 0 Teb: 7efd5000 Unfrozen
  18  Id: 10a8.2170 Suspend: 0 Teb: 7ef70000 Unfrozen
  19  Id: 10a8.1c88 Suspend: 0 Teb: 7ef6d000 Unfrozen

위와 같이 콜스택을 찾아가서 CriticalSection 객체를 이용하여 찾을 수 도 있지만, 아래 옵션을 주게 되면 lock이 되어 있는 크리티컬 섹션 정보를 볼 수 있다.


0:000:x86>  !cs -l -o
DebugInfo          = 0x0000000077564380
Critical section   = 0x00000000775620c0 (ntdll_77460000!LdrpLoaderLock+0x0)
LockCount          = 0x8
WaiterWoken        = No
OwningThread       = 0x0000000000002228
RecursionCount     = 0x1
LockSemaphore      = 0x1B8
SpinCount          = 0x0000000000000000

여기서 주의할 점은 Windbg에서의 deadlock, lock, cs는 다른 개념이기 때문에 다르게 접근해야 한다.

해당 이슈로 !locks, !deadlock 명령어를 통해 CriticalSection에 의해 잠겨 있는지 확인해 보면 "NOT LOCKED" 나온다.


해당 이슈는 특정 Thread에서 CriticalSection 진입 후 나오지 않고 쓰래드가 강제종료 된 경우이며, 이 때 다른모듈이 LoadLibrary하는 과정에서 상호 충돌하는 문제이다.


아래 글은 영문글은 마이크로소프트 MSDN에서 발췌하였으며, 해당 이슈에 대해 왜 일어나고 복잡한 코딩을 하는 경우 주의해야 될 부분에 대해 명시 되어 있다.




출처 : https://msdn.microsoft.com/ko-kr/library/windows/desktop/dn633971(v=vs.85).aspx


Dynamic-Link Library Best Practices


Creating DLLs presents a number of challenges for developers. DLLs do not have system-enforced versioning. When multiple versions of a DLL exist on a system, the ease of being overwritten coupled with the lack of a versioning schema creates dependency and API conflicts. Complexity in the development environment, the loader implementation, and the DLL dependencies has created fragility in load order and application behavior. Lastly, many applications rely on DLLs and have complex sets of dependencies that must be honored for the applications to function properly. This document provides guidelines for DLL developers to help in building more robust, portable, and extensible DLLs.

Improper synchronization within DllMain can cause an application to deadlock or access data or code in an uninitialized DLL. Calling certain functions from within DllMain causes such problems.

What Happens When a Library Is Loaded


General Best Practices

DllMain is called while the loader-lock is held. Therefore, significant restrictions are imposed on the functions that can be called within DllMain. As such, DllMain is designed to perform minimal initialization tasks, by using a small subset of the Microsoft® Windows® API. You cannot call any function in DllMain that directly or indirectly tries to acquire the loader lock. Otherwise, you will introduce the possibility that your application deadlocks or crashes. An error in a DllMain implementation can jeopardize the entire process and all of its threads.

The ideal DllMain would be just an empty stub. However, given the complexity of many applications, this is generally too restrictive. A good rule of thumb for DllMain is to postpone as much initialization as possible. Lazy initialization increases robustness of the application because this initialization is not performed while the loader lock is held. Also, lazy initialization enables you to safely use much more of the Windows API.

Some initialization tasks cannot be postponed. For example, a DLL that depends on a configuration file should fail to load if the file is malformed or contains garbage. For this type of initialization, the DLL should attempt the action and fail quickly rather than waste resources by completing other work.

You should never perform the following tasks from within DllMain:

  • Call LoadLibrary or LoadLibraryEx (either directly or indirectly). This can cause a deadlock or a crash.
  • Call GetStringTypeA, GetStringTypeEx, or GetStringTypeW (either directly or indirectly). This can cause a deadlock or a crash.
  • Synchronize with other threads. This can cause a deadlock.
  • Acquire a synchronization object that is owned by code that is waiting to acquire the loader lock. This can cause a deadlock.
  • Initialize COM threads by using CoInitializeEx. Under certain conditions, this function can call LoadLibraryEx.
  • Call the registry functions. These functions are implemented in Advapi32.dll. If Advapi32.dll is not initialized before your DLL, the DLL can access uninitialized memory and cause the process to crash.
  • Call CreateProcess. Creating a process can load another DLL.
  • Call ExitThread. Exiting a thread during DLL detach can cause the loader lock to be acquired again, causing a deadlock or a crash.
  • Call CreateThread. Creating a thread can work if you do not synchronize with other threads, but it is risky.
  • Create a named pipe or other named object (Windows 2000 only). In Windows 2000, named objects are provided by the Terminal Services DLL. If this DLL is not initialized, calls to the DLL can cause the process to crash.
  • Use the memory management function from the dynamic C Run-Time (CRT). If the CRT DLL is not initialized, calls to these functions can cause the process to crash.
  • Call functions in User32.dll or Gdi32.dll. Some functions load another DLL, which may not be initialized.
  • Use managed code.

The following tasks are safe to perform within DllMain:

  • Initialize static data structures and members at compile time.
  • Create and initialize synchronization objects.
  • Allocate memory and initialize dynamic data structures (avoiding the functions listed above.)
  • Set up thread local storage (TLS).
  • Open, read from, and write to files.
  • Call functions in Kernel32.dll (except the functions that are listed above).
  • Set global pointers to NULL, putting off the initialization of dynamic members. In Microsoft Windows Vista™, you can use the one-time initialization functions to ensure that a block of code is executed only once in a multithreaded environment.

Deadlocks Caused by Lock Order Inversion

When you are implementing code that uses multiple synchronization objects such as locks, it is vital to respect lock order. When it is necessary to acquire more than one lock at a time, you must define an explicit precedence that is called a lock hierarchy or lock order. For example, if lock A is acquired before lock B somewhere in the code, and lock B is acquired before lock C elsewhere in the code, then the lock order is A, B, C and this order should be followed throughout the code. Lock order inversion occurs when the locking order is not followed—for example, if lock B is acquired before lock A. Lock order inversion can cause deadlocks that are difficult to debug. To avoid such problems, all threads must acquire locks in the same order.

It is important to note that the loader calls DllMain with the loader lock already acquired, so the loader lock should have the highest precedence in the locking hierarchy. Also note that code only has to acquire the locks it requires for proper synchronization; it does not have to acquire every single lock that is defined in the hierarchy. For example, if a section of code requires only locks A and C for proper synchronization, then the code should acquire lock A before it acquires lock C; it is not necessary for the code to also acquire lock B. Furthermore, DLL code cannot explicitly acquire the loader lock. If the code must call an API such as GetModuleFileName that can indirectly acquire the loader lock and the code must also acquire a private lock, then the code should call GetModuleFileName before it acquires lock P, thus ensuring that load order is respected.

Figure 2 is an example that illustrates lock order inversion. Consider a DLL whose main thread contains DllMain. The library loader acquires the loader lock L and then calls into DllMain. The main thread creates synchronization objects A, B, and G to serialize access to its data structures and then tries to acquire lock G. A worker thread that has already successfully acquired lock G then calls a function such as GetModuleHandle that attempts to acquire the loader lock L. Thus, the worker thread is blocked on L and the main thread is blocked on G, resulting in a deadlock.

Deadlock Caused by Lock Order Inversion

To prevent deadlocks that are caused by lock order inversion, all threads should attempt to acquire synchronization objects in the defined load order at all times.

Best Practices for Synchronization

Consider a DLL that creates worker threads as part of its initialization. Upon DLL cleanup, it is necessary to synchronize with all the worker threads to ensure that the data structures are in a consistent state and then terminate the worker threads. Today, there is no straightforward way to completely solve the problem of cleanly synchronizing and shutting down DLLs in a multithreaded environment. This section describes the current best practices for thread synchronizing during DLL shutdown.

Thread Synchronization in DllMain during Process Exit

  • By the time DllMain is called at process exit, all the process’s threads have been forcibly cleaned up and there is a chance that the address space is inconsistent. Synchronization is not required in this case. In other words, the ideal DLL_PROCESS_DETACH handler is empty.
  • Windows Vista ensures that core data structures (environment variables, current directory, process heap, and so on) are in a consistent state. However, other data structures can be corrupted, so cleaning memory is not safe.
  • Persistent state that needs to be saved must be flushed to permanent storage.

Thread Synchronization in DllMain for DLL_THREAD_DETACH during DLL Unload

  • When the DLL is unloaded, the address space is not thrown away. Therefore, the DLL is expected to perform a clean shutdown. This includes thread synchronization, open handles, persistent state, and allocated resources.
  • Thread synchronization is tricky because waiting on threads to exit in DllMain can cause a deadlock. For example, DLL A holds the loader lock. It signals thread T to exit and waits for the thread to exit. Thread T exits and the loader tries to acquire the loader lock to call into DLL A’s DllMain with DLL_THREAD_DETACH. This causes a deadlock. To minimize the risk of a deadlock:
    • DLL A gets a DLL_THREAD_DETACH message in its DllMain and sets an event for thread T, signaling it to exit.
    • Thread T finishes its current task, brings itself to a consistent state, signals DLL A, and waits infinitely. Note that the consistency-checking routines should follow the same restrictions as DllMain to avoid deadlocking.
    • DLL A terminates T, knowing that it is in a consistent state.

If a DLL is unloaded after all its threads have been created, but before they begin executing, the threads may crash. If the DLL created threads in its DllMain as part of its initialization, some threads may not have finished initialization and their DLL_THREAD_ATTACH message is still waiting to be delivered to the DLL. In this situation, if the DLL is unloaded, it will begin terminating threads. However, some threads may be blocked behind the loader lock. Their DLL_THREAD_ATTACH messages are processed after the DLL has been unmapped, causing the process to crash.


The following are recommended guidelines:

  • Use Application Verifier to catch the most common errors in DllMain.
  • If using a private lock inside DllMain, define a locking hierarchy and use it consistently. The loader lock must be at the bottom of this hierarchy.
  • Verify that no calls depend on another DLL that may not have been fully loaded yet.
  • Perform simple initializations statically at compile time, rather than in DllMain.
  • Defer any calls in DllMain that can wait until later.
  • Defer initialization tasks that can wait until later. Certain error conditions must be detected early so that the application can handle errors gracefully. However, there are tradeoffs between this early detection and the loss of robustness that can result from it. Deferring initialization is often best.




출처 : http://greenfishblog.tistory.com/21

dynamic-link library(DLL)은 응용프로그램이 실행중에 로드하고 호출할 수 있는 공유된 코드데이터로 정의할 수 있습니다. 전형적인 DLL은 응용프로그램을 위해 루틴들을 노출(=Export)시키며, 그 내부(즉, DLL)에서 사용할(internal use) 루틴도 역시 포함되어 있습니다. 이러한 기술은 여러 응용프로그램에서 공통 기능으로 공유할 수 있게 라이브러리 형태로 재사용 가능하게 하여 필요시 로드를 할 수 있도록 해줍니다. DLL 사용의 장점은 코드가 차지하는 공간(code footprint)을 줄이고, 단일 복사본을 공유함으로서 메모리 사용량을 낮추며, 개발과 테스트가 용이하게 하고 모듈화를 가능하게 합니다.

DLL을 만드는 일은 개발자에게 많은 도전을 가져다 줍니다. DLL은 시스템을 통한 강제적인 버전관리가 이뤄지지 않습니다. 즉, 여러개의 DLL이 한 시스템에 있을때, 이러한 버전관리 체크의 부족으로 인한 overwrite는 의존성과 API 충돌을 야기합니다. 개발 환경, 로더 구현 그리고 의존성의 복잡성은 로드 순서와 응용프로그램 행위에 취약성을 만듭니다. 그래도 많은 응용프로그램들은 복잡한 의존성을 가지는 DLL에 의지하고 있습니다. 이 문서는 DLL 개발자들을 위해 가이드라인을 제공하여 견고하고 이식성 있으며 확장성 있는 DLL로 만드는데 도움을 줄 것입니다.

■ 3개의 주요한 DLL 컴포넌트 개발 모델은 다음과 같습니다.

  1. Library Loader
    DLL은 가끔 복잡한 내부의존성(interdependency)을 자니는데, 이는 그들이 로드되어야 하는 순서를 정의합니다. Library Loader는 효과적으로 이러한 의존성을 분석하고, 정확한 로드 순서를 계산한뒤 그 순서대로 로드를 합니다.
  2. DLLMain entry-point function
    이 함수는 Loader에 의해 호출되며, 그 시점은 DLL의 Load 혹은 Unload일때 입니다. Loader는 한 시점에 단 하나의 DLLMain만 호출하도록 연속으로 호출합니다. 더 많은 정보
  3. Loader Lock
    Loader가 순서대로 로드할때 사용되는 프로세스 단위의 동기화 객체입니다. 프로세스 단위의 Library Loader Data를 반드시 읽거나 써야 하는 함수는 반드시 이 Lock을 획득해야 합니다. 물론 이러한 operation을 수행하기 전에 이뤄져야 합니다. Loader Lock은 recursive이며, 이는 같은 쓰레드에서 다시 Lock의 획득이 가능함을 의미합니다.

그림 1

그림 1. DLL 로드시 어떤일이 이뤄지는가?


DLLMain에서의 부적합한 동기화 시도는 응용프로그램에게 deadlock 혹은 초기화 되지 않은 DLL의 data와 code의 접근을 야기하게 됩니다. DLLMain에서의 특정 함수 호출은 이러한 문제를 잃으킵니다.

일반적인 최고의 습관

DLLMain은 Loader Lock이 획득되었을때 호출됩니다. 따라서, DLLMain 내부에서의 호출은 중요한 제약이 강요됩니다. DLLMain은 최소의 초기화 작업을 수행하도록 디자인 되었는데, 이는 Windows API의 몇몇 함수군 호출에 의해서 입니다. DLLMain에서 직접적이든 간접적이든 Loader Lock 획득을 시도하는 어떤 함수도 호출할 수 없습니다. 다시 말해, 이 경우가 발생하면 당신은 deadlock 혹은 crash를 경험하게 됩니다. DLLMain 구현에서의 에러는 해당 프로세스와 그 내부의 쓰레드를 위험에 빠트리게 됩니다.

이상적인 DLLMain은 "그냥 비우는것" 입니다. 그러나, 많은 응용 프로그램의 복잡도를 고려할때 이는 너무한 제약이 됩니다. DLLMain을 다루는 좋은 방법은 많은 초기화 과정을 가능한 뒤로 미뤄라라는 것입니다. 이러한 미뤄진 초기화는 응용프로그램을 더욱더 견고히 해주는데, 그 이유는 Loader Lock가 획득된 동안의 초기화가 이뤄지지 않았기 때문입니다. 역시 이러한 방법은 Windows API의 사용에도 훨씬 많은 안정성을 제공합니다.

몇몇 초기화 작업은 뒤로 미룰순 없을 것입니다. 예를 들어, 설정 파일에 의존성이 있는 DLL이 있는데, 해당 파일이 좋지 않거나 쓰레기 내용이 포함되었을때 그 DLL의 Load가 실패되야 하는 경우가 있을 것입니다. 이런 종류의 초기화 방식은, 다른 작업의 자원 낭비를 하느니 DLL이 그 행위를 시도해보고 빨리 실패하는 것이 좋다고 개념이라 보여집니다.

■ 다음과 같은 작업을 절대로 DLLMain에서 수행해서는 안됩니다.

  • LoadLibrary 혹은 LibraryEx의 직접적 혹은 간접적 호출. 이는 deadlock 혹은 crash를 유발한다.
  • 다른 쓰레드와의 동기화 시도. 이는 deadlock을 유발한다.
  • Loader Lock을 획득하기 위해 기다리는 코드가 획득한 다른 사설 동기화 객제를 획득하려고 하는 시도. 이는 deadlock을 유발한다.
  • CoInitializeEx 사용에 의한 COM 쓰레드 초기화. 특정 상황이 되면 이 함수는 LoadLibrary를 호출한다.
  • 레지스트리 함수군의 호출. 이 함수는 Advapi32.dll에 구현되어 있는데, 만약 AdvApi32.dll이 아직 당신 DLL에서 초기화되지 않았다면, 그 DLL은 메모리를 초기화해제 하며, crash를 유발한다.
  • CreateProces 호출. 이는 다른 DLL을 Load할 수 있다.
  • ExitThread 호출. DLL Detach 과정에서 Exit가 진행중인 Thread는 Loader Lock을 다시 획득하려고 하는 시도가 발생하여 deadlock 혹은 crash가 발생할 수 있다.
  • CreateThread 호출. 다른 Thread와 동기화 작업을 하지 않는다면, 생성중인 Thread가 할 수 있는데, 이는 위험할 수 있다.
  • Named Pipe 혹은 다른 Named Object의 생성(Windows 2000만 해당). Windows 200에서는 Named Object는 Terminal Service DLL에 의해 제공되는데, 만일, 이 DLL이 초기화되지 않았다면, DLL을 로드하게 되어 crash가 유발될 수 있다.
  • 메모리 관리 CRT 함수 호출. 만약 CRT DLL이 초기화 되지 않았다면, crash가 유발된다.
  • User32.dll 혹은 Gdi32.dll 함수 호출. 몇몇 함수들은 아직 초기화되지 않은 DLL을 로드한다.
  • 관리 코드의 사용

■ DLLMain 내에서 안전한 작업은 다음과 같습니다.

  • compile time의 static data의 초기화
  • 동기화 객체의 생성과 초기화
  • 메모리 할당과 dynamic data의 초기화 (위 금지 함수 이외)
  • Thread local storage(TLS) 초기화
  • File의 열기/읽기/쓰기
  • kernel32.dll 함수의 호출 (위 금지 함수 제외)
  • 전역 포인터 변수를 NULL로 할당

Lock 순서의 역(Lock order inversion)에 의한 deadlock

Lock과 같은 다중 동기화 객체 사용을 구현할 때, Lock 순서를 따르는 것은 굉장히 중요합니다. 어느 시점에서 한개 이상의 Lock을 획득하는 것이 필요할때 반드시 Lock hierachy 혹은 Lock 순서라 불리는 명시적인 순서를 정의해야 합니다. 예를 들어, Lock A가 Lock B이전에 획득되었고, Lock C 이전에 Lock B가 획득되었다면 Lock 순서는 A,B,C가 되고, 이 순서는 코드에서 지켜줘야 됩니다. 만약 Lock 순서가 역으로 되는경우가 발생했다면, 예를 들어, Lock A를 획득 하기 전에 Lock B가 획득되었을 경우, 이는 Lock 순서의 역에 의한 deadlock이 발생하게 됩니다. 이렇게 발생한 deadlock은 디버깅하기 힘든 면이 있습니다. 이것을 방지하기 위해 모든 쓰레드에서는 같은 순서대로 Lock을 획득해야만 합니다.

Loader는 이미 획득한 Loader Lock으로 DLLMain을 호출한다는 사실은 굉장히 중요합니다. 그래서 Loader Lock은 Locking hierachy의 가장 높은 우선순위가 되어야 합니다. 그와 마찬가지로 적합한 동기화를 위해 요구된 Lock을 획득하해야 하는 것도 알아야 합니다. 물론 hierachy에 정의된 모든 단일 Lock을 획득해야 할 필요는 없습니다. 예를 들어, A와 C를 적합한 동기화를 위해 획득하였다면, C를 획득하기 이전에 A를 획득해야 하며, B를 획득할 필요는 없습니다. 더 나아가 설명하자면, 프로그램 코드에서는 Loader Lock을 명시적으로 획득 할 수 없습니다. 만약 사적인 Lock을 획득한 상황에서 Loader Lock을 간접적으로 획득하려는 ::GetModuleFileName(...)과 같은 API를 호출해야 한다면, 사설 Lock을 획득하기 이전에 ::GetModuleFileName(...)을 획득해야만 합니다. 이는 Load 순서를 따르게 하기 위함입니다.

그림 2

그림 2. Lock 순서의 역에 의한 deadlock

그림 2. 는 이러한 Lock 순서의 역을 보여주고 있습니다. DLLMain을 포함하는 Main 쓰레드를 가지는 DLL을 생각해 보십시요. Library Loader는 Lock L을 획득했으며, DLLMain을 호출하려고 합니다. Main 쓰레드에서는 동기화 객체인 A, B 그리고 공유 데이터를 접근하기 위해 필요한 G를 생성하고 G를 획득하기 위해 시도하려고 합니다. 그와 별도로, Worker 쓰레드에서는 이미 G를 획득한 상황이며 ::GetModuleHandle(...)를 호출하여 Loader Lock인 L을 획득하려고 시도할 것입니다. 그러면, Worker 쓰레드는 L에 의해 Block 되며, Main 쓰레드는 G에 의해 Block 되며, 이로 인해 deadlock이 발생하게 됩니다.

이러한 상황을 방지하기 위해서는, 모든 쓰레드에서는 항상 순서에 맞게 동기화 객체를 획득하도록 시도해야 합니다.

동기화를 위한 최고의 습관

초기화의 한 부분으로 DLL이 Worker 쓰레드를 생성해야 하는 경우를 생각해 보십시요. DLL이 Cleanup되면 data의 무결성(consistent)을 확신하기 위해 모든 Worker 쓰레드의 동기화가 필요하며, 그 다음 Worker 쓰레드는 종료하게 됩니다. 오늘날, 멀티쓰레드 환경의 DLL을 종료하고 동기화하는데에는 완벽하고 정확한 방법은 없습니다. 다음은 DLL 종료를 하는 동안 이루어질 쓰레드 동기화를 위해 현재까지 나와있는 최고의 습관을 설명하고 있습니다.

■ 프로세스 종료시 DLLMain에서의 쓰레드 동기화
  • 프로세스 종료시 DLLMain이 호출되었다면 모든 프로세스의 쓰레드들은 Clean up이 이뤄지며 주소 공간(Address Space)는 더이상 유지되지 않습니다. 동기화는 이런 경우에는 필요하지 않습니다. 다시 말해 DLL_PROCESS_DETACH는 비워둬도 됩니다.
  • Windows Vista에서는 핵심 data들(환경 변수, 현재 디렉토리, 프로세스 힙, ...)의 유지가 보장됩니다. 그러나 다른 동적 할당된 사설 data는 망가져서 더이상 안전하지 않습니다.
  • 저장이 필요한 영구 유지될 상태들은 저장 매체에 플러쉬되어야 합니다.

■ DLL UnLoad시의 DLL_THREAD_DETACH를 위한 DLLMain의 쓰레드 동기화

  • DLL이 UnLoad될때 주소 공간(Address Space)는 사라지는 것은 아닙니다. 따라서 DLL은 Clean될 예정인 상태입니다. 이것은 쓰레드 동기화, Open된 핸들, 영구 유지해야 하는 상태 그리고 할당된 자원들을 포함합니다.
  • 쓰레드 동기화는 종잡을 수 없는데, DLLMain에서 쓰레드의 종료를 기다리는 것은 deadlock을 유발할 수 있기 때문입니다. 예를 들어, DLL A가 Loader Lock을 획득했습니다. 그리고 Thread T를 종료시키기 위해 Signal을 보냈고 종료를 기다리도록 합니다. 쓰레드 T는 종료가 되며, Loader는 DLL A의 DLL_THREAD_DETACH 호출을 위해 Loader Lock 획득을 시도할 것입니다. 이것이 deadlock을 유발시키게 됩니다. 이러한 리스크를 최소화 하는 방법은 다음과 같습니다.
    • DLL A는 DLLMain에서 DLL_THREAD_DETACH 메시지를 받고, 쓰레드 T에게 종료해라는 Signal을 보냅니다.
    • Thread T는 현재의 작업을 마치고 스스로 상태를 유지하며 DLL A에게 Signal을 보냅니다. 단, 이러한 유지된 상태 체크를 위해서 DLLMain의 deadlock 회피를 위한 제약을 지켜야 합니다.
    • DLL A가 쓰레드 T를 종료 시켰으며, 그것이 아직 유지된 상태임을 알수 있습니다.

만약 DLL이 그것의 모든 쓰레드를 생성하고 나서 UnLoad되었고 실행이 시작되기 전이었다면, 그 쓰레드들은 crash가 발생할 수 있습니다. 만약 DLL이 초기화의 단계로 DLLMain에서 Thread를 생성하였다면, 몇몇 쓰레드들이 아직 초기화가 완료되지 못했고 그들의 DLL_THREAD_ATTACH 메시지가 여전히 DLL에게 전달되기를 기다리고 있을 것입니다. 이런 상황에서 DLL이 UnLoad된다면 쓰레드들의 Terminate가 시작될 것입니다. 그러나 몇몇 쓰레드들은 Loader Lock에 의해 block되어 있을 겁니다. 그들의 DLL_THREAD_ATTACH 메시지들은 DLL이 unmap된 이후 진행될 것이며, 이는 crash를 유발할 것입니다.


■ 다음의 가이드라인을 추천합니다.
  • Application Verifier를 사용하며 DLLMain의 통상적인 오류를 찾으세요.
  • DLLMain에서의 사설 Lock을 사용하신다면, Lock hierachy를 정의하고 유지하십시요. Loader Lock은 가장 마지막에 있어야 합니다.
  • 어떤 호출도 아직 로드되지 않은 다른 DLL로의 의존성이 없다는 것을 확인하세요.
  • 간단한 초기화는 DLLMain 보다는 compile time에서 수행하도록 하세요.
  • DLLMain에서 기다리는 호출을 뒤로 미루십시요.
  • 초기화 작업을 뒤로 미루십시요. 어떤 에러 조건은 빨리 발견되는데, 이는 응용프로그램의 에러처리를 멋지게 해줍니다. 그러나 빨리 발견되는 것과 견고함의 손실은 Trade off가 있습니다. 초기화를 뒤로 미루는 것이 최고의 방법입니다.


Posted by 쫑경