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 쫑경
WDM/Architecture2014.04.23 14:09

당신 회사의 톰캣은 괜찮으십니까?

요즘 화두가 되고 있는 Heartbleed OpenSSL에 대해 설명드리고자 합니다.

취약점은 톰캣설치 폴더의 Bin폴더안에 tcnative-1.dll이 버그가 발견 되었습니다.

버그에 영향이 있는 버전은 1.1.24~29까지 입니다.

이 버전을 사용하여 인증서를 발급할 경우 개인키가 노출되어 SSL패킷이 복호화 될 수 있다는 문제를 가지고 있습니다.

일부는 톰캣을 바꿔야 되는 경우도 있습니다. OpenSSL을 탑재하여 빌드 된 버전들은 수정을 하셔야 합니다.

해당 정보는 아래의 사이트에서 확인 하실 수 있습니다.

tcnative-1.dll은 톰캣 가속엔진입니다. 그래서 기능을 사용하지 않는다면 삭제하시고 사용하시면 됩니다.

삭제를 하게되면 아파치로그에

The APR based Apache Tomcat Native library which allows optimal performance in production environments was not found on the java.library.path

라는 로그가 보일 것 입니다.

삭제를 안하고 사용하고 싶다면 얼마전 아파치에서 릴리즈된 1.1.30버전을 사용하시면 됩니다. 


톰캣 6.0에 해당 DLL만 교체하여 사용해본 결과 이상없이 잘 사용되고 서버도 정상적으로 동작합니다. 톰캣 버전을 안올리셔도 됩니다.

다른 몇몇의 방법들이 있지만 이 방법이 가장 적용하기 쉬운 방법인 것 같아 공유합니다.

Posted by 쫑경
WDM/Architecture2014.03.31 16:38

하드웨어 또는 소프트웨어의 인터럽트가 발생하면 프로세서(CPU가 해당이 되겠지요..간혹 프로세스와 프로세서를 잘못보는 분이 계셔서)는 현재의 작업을 중지합니다. 프로세서는 인터럽트에 대해 우선 순위를 부여하고 우선 순위가 높은 인터럽트를 먼저 처리를 합니다.

커널레벨에서의 프로그래밍을 하거나 함수를 사용할 때 인터럽트레벨이 중요합니다. 프로그래머가 사용할 수 있는 인터럽트 레벨은 0~2까지 밖에 사용할 수 없습니다. 상위권의 인터럽트레벨은 하드웨어가 사용하게 됩니다.

이는 당연한 이치이지만, 커널레벨에서의 프로그램이 드라이버로 작성이 되지만 하드웨어 입장에서는 결국 소프트웨어이기 때문에 하드웨어보다 소프트웨어가 처리 우선순위가 높다는 것은 말이 안되기 때문입니다.


함수에 대한 중요성을 예를들면


NTSTATUS ZwCreateFile(
  _Out_     PHANDLE FileHandle,
  _In_      ACCESS_MASK DesiredAccess,
  _In_      POBJECT_ATTRIBUTES ObjectAttributes,
  _Out_     PIO_STATUS_BLOCK IoStatusBlock,
  _In_opt_  PLARGE_INTEGER AllocationSize,
  _In_      ULONG FileAttributes,
  _In_      ULONG ShareAccess,
  _In_      ULONG CreateDisposition,
  _In_      ULONG CreateOptions,
  _In_opt_  PVOID EaBuffer,
  _In_      ULONG EaLength



ZwCreateFile함수의 MSDN 맨마지막 쪽을 보면


 IRQL  PASSIVE_LEVEL (see Remarks section)


이라고 되어 있습니다. 즉, 이 함수는 APC_LEVEL 이상의 인터럽트인 상태에서 사용하게 되면 BSoD가 발생하게 됩니다.






 인터럽트 없음


 정규 스레드 실행 

 소프트웨어 인터럽트


 비동기 프로시저 호출 발생 



 스레드 스케쥴링 ,지연 프로시저 호출 실행

 하드웨어 인터럽트


 디바이스 인터럽트 요구 레벨 핸들러 실행 



 프로파일링 타이머






 동기화 레벨



 프로세서간 인터럽트 레벨



 전원 장애 레벨


모든 스레드는 보통 제일 하위의 PASSIVE_LEVEL의 인터럽트에서 실행되며, 스케쥴링은 우선 순위의 값을 이용하여 다음에 어떤 스레드가 실행할지를 판단합니다.


보통 드라이버 작성 시 일정 정보를 저장하기 위해 List에 관리하게 되는데, 이때 삽입이나 삭제를 할 경우 PASSIVE_LEVEL에서 작업을 하게 되면 동시 접근이 되어서 메모리 오류가 발생할 수 있는 소지가 다분합니다. 이 부분은 유저레벨에서도 마찬가지겠지요..

그래서 인터럽트의 레벨을 올려, 즉, IRQL의 레벨을 올려서 삽입, 삭제시 작업을 하게 됩니다. 이때 드라이버에서 SpinLock를 사용하여 IRQL을 DISPATCH_LEVEL까지 올려 줍니다.


꼭 드라이버 작성 시 함수를 쓰실 때 어느 IRQL에서 작동이 되는 함수인지 확인하고 코드를 하는 습관을 들이신다면 나중에 도움이 많이 될 듯 싶습니다. 읽어주셔서 감사합니다.

Posted by 쫑경
WDM/Architecture2013.03.24 22:57

간만에 포스팅하는 듯 합니다.

8에 대해 _eprocess offset에 대한 내용이 검색해도 없어서 올립니다.


   +0x000 Pcb              : _KPROCESS

   +0x0a0 ProcessLock      : _EX_PUSH_LOCK

   +0x0a8 CreateTime       : _LARGE_INTEGER

   +0x0b0 RundownProtect   : _EX_RUNDOWN_REF

   +0x0b4 UniqueProcessId  : Ptr32 Void

   +0x0b8 ActiveProcessLinks : _LIST_ENTRY

   +0x0c0 Flags2           : Uint4B

   +0x0c0 JobNotReallyActive : Pos 0, 1 Bit

   +0x0c0 AccountingFolded : Pos 1, 1 Bit

   +0x0c0 NewProcessReported : Pos 2, 1 Bit

   +0x0c0 ExitProcessReported : Pos 3, 1 Bit

   +0x0c0 ReportCommitChanges : Pos 4, 1 Bit

   +0x0c0 LastReportMemory : Pos 5, 1 Bit

   +0x0c0 NoWakeCharge     : Pos 6, 1 Bit

   +0x0c0 HandleTableRundown : Pos 7, 1 Bit

   +0x0c0 NeedsHandleRundown : Pos 8, 1 Bit

   +0x0c0 RefTraceEnabled  : Pos 9, 1 Bit

   +0x0c0 NumaAware        : Pos 10, 1 Bit

   +0x0c0 EmptyJobEvaluated : Pos 11, 1 Bit

   +0x0c0 DefaultPagePriority : Pos 12, 3 Bits

   +0x0c0 PrimaryTokenFrozen : Pos 15, 1 Bit

   +0x0c0 ProcessVerifierTarget : Pos 16, 1 Bit

   +0x0c0 StackRandomizationDisabled : Pos 17, 1 Bit

   +0x0c0 AffinityPermanent : Pos 18, 1 Bit

   +0x0c0 AffinityUpdateEnable : Pos 19, 1 Bit

   +0x0c0 PropagateNode    : Pos 20, 1 Bit

   +0x0c0 ExplicitAffinity : Pos 21, 1 Bit

   +0x0c0 ProcessExecutionState : Pos 22, 2 Bits

   +0x0c0 DisallowStrippedImages : Pos 24, 1 Bit

   +0x0c0 HighEntropyASLREnabled : Pos 25, 1 Bit

   +0x0c0 ExtensionPointDisable : Pos 26, 1 Bit

   +0x0c0 ForceRelocateImages : Pos 27, 1 Bit

   +0x0c0 ProcessStateChangeRequest : Pos 28, 2 Bits

   +0x0c0 ProcessStateChangeInProgress : Pos 30, 1 Bit

   +0x0c0 DisallowWin32kSystemCalls : Pos 31, 1 Bit

   +0x0c4 Flags            : Uint4B

   +0x0c4 CreateReported   : Pos 0, 1 Bit

   +0x0c4 NoDebugInherit   : Pos 1, 1 Bit

   +0x0c4 ProcessExiting   : Pos 2, 1 Bit

   +0x0c4 ProcessDelete    : Pos 3, 1 Bit

   +0x0c4 Wow64SplitPages  : Pos 4, 1 Bit

   +0x0c4 VmDeleted        : Pos 5, 1 Bit

   +0x0c4 OutswapEnabled   : Pos 6, 1 Bit

   +0x0c4 Outswapped       : Pos 7, 1 Bit

   +0x0c4 ForkFailed       : Pos 8, 1 Bit

   +0x0c4 Wow64VaSpace4Gb  : Pos 9, 1 Bit

   +0x0c4 AddressSpaceInitialized : Pos 10, 2 Bits

   +0x0c4 SetTimerResolution : Pos 12, 1 Bit

   +0x0c4 BreakOnTermination : Pos 13, 1 Bit

   +0x0c4 DeprioritizeViews : Pos 14, 1 Bit

   +0x0c4 WriteWatch       : Pos 15, 1 Bit

   +0x0c4 ProcessInSession : Pos 16, 1 Bit

   +0x0c4 OverrideAddressSpace : Pos 17, 1 Bit

   +0x0c4 HasAddressSpace  : Pos 18, 1 Bit

   +0x0c4 LaunchPrefetched : Pos 19, 1 Bit

   +0x0c4 Background       : Pos 20, 1 Bit

   +0x0c4 VmTopDown        : Pos 21, 1 Bit

   +0x0c4 ImageNotifyDone  : Pos 22, 1 Bit

   +0x0c4 PdeUpdateNeeded  : Pos 23, 1 Bit

   +0x0c4 VdmAllowed       : Pos 24, 1 Bit

   +0x0c4 CrossSessionCreate : Pos 25, 1 Bit

   +0x0c4 ProcessInserted  : Pos 26, 1 Bit

   +0x0c4 DefaultIoPriority : Pos 27, 3 Bits

   +0x0c4 ProcessSelfDelete : Pos 30, 1 Bit

   +0x0c4 SetTimerResolutionLink : Pos 31, 1 Bit

   +0x0c8 ProcessQuotaUsage : [2] Uint4B

   +0x0d0 ProcessQuotaPeak : [2] Uint4B

   +0x0d8 PeakVirtualSize  : Uint4B

   +0x0dc VirtualSize      : Uint4B

   +0x0e0 SessionProcessLinks : _LIST_ENTRY

   +0x0e8 ExceptionPortData : Ptr32 Void

   +0x0e8 ExceptionPortValue : Uint4B

   +0x0e8 ExceptionPortState : Pos 0, 3 Bits

   +0x0ec Token            : _EX_FAST_REF

   +0x0f0 WorkingSetPage   : Uint4B

   +0x0f4 AddressCreationLock : _EX_PUSH_LOCK

   +0x0f8 RotateInProgress : Ptr32 _ETHREAD

   +0x0fc ForkInProgress   : Ptr32 _ETHREAD

   +0x100 HardwareTrigger  : Uint4B

   +0x104 CommitChargeJob  : Ptr32 _EJOB

   +0x108 CloneRoot        : Ptr32 _MM_AVL_TABLE

   +0x10c NumberOfPrivatePages : Uint4B

   +0x110 NumberOfLockedPages : Uint4B

   +0x114 Win32Process     : Ptr32 Void

   +0x118 Job              : Ptr32 _EJOB

   +0x11c SectionObject    : Ptr32 Void

   +0x120 SectionBaseAddress : Ptr32 Void

   +0x124 Cookie           : Uint4B

   +0x128 VdmObjects       : Ptr32 Void

   +0x12c WorkingSetWatch  : Ptr32 _PAGEFAULT_HISTORY

   +0x130 Win32WindowStation : Ptr32 Void

   +0x134 InheritedFromUniqueProcessId : Ptr32 Void

   +0x138 LdtInformation   : Ptr32 Void

   +0x13c CreatorProcess   : Ptr32 _EPROCESS

   +0x13c ConsoleHostProcess : Uint4B

   +0x140 Peb              : Ptr32 _PEB

   +0x144 Session          : Ptr32 Void

   +0x148 AweInfo          : Ptr32 Void

   +0x14c QuotaBlock       : Ptr32 _EPROCESS_QUOTA_BLOCK

   +0x150 ObjectTable      : Ptr32 _HANDLE_TABLE

   +0x154 DebugPort        : Ptr32 Void

   +0x158 PaeTop           : Ptr32 Void

   +0x15c DeviceMap        : Ptr32 Void

   +0x160 EtwDataSource    : Ptr32 Void

   +0x168 PageDirectoryPte : Uint8B

   +0x170 ImageFileName    : [15] UChar

   +0x17f PriorityClass    : UChar

   +0x180 SecurityPort     : Ptr32 Void

   +0x184 SeAuditProcessCreationInfo : _SE_AUDIT_PROCESS_CREATION_INFO

   +0x188 JobLinks         : _LIST_ENTRY

   +0x190 HighestUserAddress : Ptr32 Void

   +0x194 ThreadListHead   : _LIST_ENTRY

   +0x19c ActiveThreads    : Uint4B

   +0x1a0 ImagePathHash    : Uint4B

   +0x1a4 DefaultHardErrorProcessing : Uint4B

   +0x1a8 LastThreadExitStatus : Int4B

   +0x1ac PrefetchTrace    : _EX_FAST_REF

   +0x1b0 LockedPagesList  : Ptr32 _MM_AVL_TABLE

   +0x1b8 ReadOperationCount : _LARGE_INTEGER

   +0x1c0 WriteOperationCount : _LARGE_INTEGER

   +0x1c8 OtherOperationCount : _LARGE_INTEGER

   +0x1d0 ReadTransferCount : _LARGE_INTEGER

   +0x1d8 WriteTransferCount : _LARGE_INTEGER

   +0x1e0 OtherTransferCount : _LARGE_INTEGER

   +0x1e8 CommitChargeLimit : Uint4B

   +0x1ec CommitCharge     : Uint4B

   +0x1f0 CommitChargePeak : Uint4B

   +0x1f4 Vm               : _MMSUPPORT

   +0x264 MmProcessLinks   : _LIST_ENTRY

   +0x26c ModifiedPageCount : Uint4B

   +0x270 ExitStatus       : Int4B

   +0x274 VadRoot          : _MM_AVL_TABLE

   +0x28c VadPhysicalPages : Uint4B

   +0x290 VadPhysicalPagesLimit : Uint4B

   +0x294 AlpcContext      : _ALPC_PROCESS_CONTEXT

   +0x2a4 TimerResolutionLink : _LIST_ENTRY

   +0x2ac TimerResolutionStackRecord : Ptr32 _PO_DIAG_STACK_RECORD

   +0x2b0 RequestedTimerResolution : Uint4B

   +0x2b4 SmallestTimerResolution : Uint4B

   +0x2b8 ExitTime         : _LARGE_INTEGER

   +0x2c0 ActiveThreadsHighWatermark : Uint4B

   +0x2c4 LargePrivateVadCount : Uint4B

   +0x2c8 ThreadListLock   : _EX_PUSH_LOCK

   +0x2cc WnfContext       : Ptr32 Void

   +0x2d0 SectionMappingSize : Uint4B

   +0x2d4 SignatureLevel   : UChar

   +0x2d5 SectionSignatureLevel : UChar

   +0x2d6 SpareByte20      : [2] UChar

   +0x2d8 KeepAliveCounter : Uint4B

   +0x2dc DiskCounters     : Ptr32 _PROCESS_DISK_COUNTERS

   +0x2e0 LastFreezeInterruptTime : Uint8B

Posted by 쫑경
WDM/Architecture2011.12.14 10:38

The OB_PRE_CREATE_HANDLE_INFORMATION structure provides information to an ObjectPreCallbackroutine about a thread or process handle that is being opened.


  ACCESS_MASK DesiredAccess;
  ACCESS_MASK OriginalDesiredAccess;



An ACCESS_MASK value that specifies the access rights to grant for the handle. By default, this member equals OriginalDesiredAccess, but the ObjectPreCallback routine can modify this value to restrict the access that is granted.

Drivers can use the following flags for handles to processes:

FlagAllowed operations


Create a new child process of the process.


Create a new thread in the context of the process.


Duplicate handles to or from the context of the process, such as by calling the user-mode DuplicateHandle routine.


Set the working set size for the process, such as by calling the user-mode SetProcessWorkingSetSize routine.


Modify process settings, such as by calling the user-modeSetPriorityClass routine.


Suspend or resume the process.


Terminate the process, such as by calling the user-modeTerminateProcess routine..


Modify the address space of the process, such as by calling the user-mode WriteProcessMemory and VirtualProtectExroutines.


Write to the address space of the process, such as by calling the user-mode WriteProcessMemory routine.


Drivers can use the following flags for handles to threads:

FlagAllowed operations


Enable a server thread to impersonate one of its clients.


Impersonate the operating system's anonymous logon token, such as by calling the user-modeImpersonateAnonymousToken routine.


Modify the thread's execution context, such as by calling the user-mode SetThreadContext routine.


Modify thread settings, such as by calling the user-mode SetThreadIdealProcessor routine. The operations that are permitted by this access right are a superset of those that are permitted by the THREAD_SET_LIMITED_INFORMATION access right.


Modify a limited set of thread settings, such as by calling the user-mode SetThreadAffinityMask andSetThreadPriorityBoost routines.


Modify properties of the thread's impersonation token, such as by calling the user-modeSetTokenInformation routine.


Suspend or resume the thread, such as by calling the user-mode SuspendThread and ResumeThreadroutines.


Terminate the thread, such as by calling the user-mode TerminateThread routine.



An ACCESS_MASK value that specifies the original access that was requested for the handle.


You can never add access rights beyond what is specified in the DesiredAccess member. If the access right is listed as a modifiable flag, the access right can be removed.

출처 :  http://msdn.microsoft.com/en-us/library/windows/hardware/ff558725(v=vs.85).aspx

Posted by 쫑경
TAG OB Filter
WDM/Architecture2009.07.15 16:18

In the preceding main section, I showed how you initialize a WDM driver when it’s first loaded. In general, though, a driver might be called upon to manage more than one actual device. In the WDM architecture, a driver has a special AddDevice function that the PnP Manager can call for each such device. The function has the following skeleton:



The DriverObject argument points to the same driver object that you initialized in your DriverEntry routine. The pdo argument is the address of the physical device object at the bottom of the device stack, even if there are already filter drivers below.

The basic responsibility of AddDevice in a function driver is to create a device object and link it into the stack rooted in this PDO. The steps involved are as follows:

  1. Call IoCreateDevice to create a device object and an instance of your own device extension object.

  2. Register one or more device interfaces so that applications know about the existence of your device. Alternatively, give the device object a name and then create a symbolic link.

  3. Next initialize your device extension and the Flags member of the device object.

  4. Call IoAttachDeviceToDeviceStack to put your new device object into the stack.

Now I’ll explain these steps in more detail. I’ll show a complete example of AddDevice at the very end of this discussion.

In the code snippets that follow, I’ve deliberately left out all the error handling that should be there. That’s so I could concentrate on the normal control flow through AddDevice. You mustn’t imitate this programming style in a production driver?but of course you already knew that. I’ll discuss how to handle errors in the next chapter. Every code sample in the companion content has full error checking in place too.

You create a device object by calling IoCreateDevice. For example:

NTSTATUS status = IoCreateDevice(DriverObject, 

The first argument (DriverObject) is the same value supplied to AddDevice as the first parameter. This argument establishes the connection between your driver and the new device object, thereby allowing the I/O Manager to send you IRPs intended for the device. The second argument is the size of your device extension structure. As I discussed earlier in this chapter, the I/O Manager allocates this much additional memory and sets the DeviceExtension pointer in the device object to point to it.

The third argument, which is NULL in this example, can be the address of a UNICODE_STRING providing a name for the device object. Deciding whether to name your device object and which name to give it requires some thought, and I’ll describe these surprisingly complex considerations a bit further on in the section “Should I Name My Device Object?”

The fourth argument (FILE_DEVICE_UNKNOWN) is one of the device types defined in WDM.H. Whatever value you specify here can be overridden by an entry in the device’s hardware key or class key. If both keys have an override, the device key has precedence. For devices that fit into one of the established categories, specify the right value in one of these places because some details about the interaction between your driver and the surrounding system depend on it. In fact, the device type is crucial for the correct functioning of a file system driver or a disk or tape driver. Additionally, the default security settings for your device object depend on this device type.

The fifth argument (FILE_DEVICE_SECURE_OPEN) provides the Characteristics flag for the device object. (See Table 2-3.) Most of these flags are relevant for mass storage devices. The flag bit FILE_AUTOGENERATED_DEVICE_NAME is for use by bus and multifunction drivers when creating PDOs. I’ll discuss the importance of FILE_DEVICE_SECURE_OPEN later in this chapter in the section “Should I Name My Device Object?” Whatever value you specify here can be overridden by an entry in the device’s hardware key or class key. If both keys have an override, the hardware key has precedence.

The sixth argument to IoCreateDevice (FALSE in my example) indicates whether the device is exclusive. The I/O Manager allows only one handle to be opened by normal means to an exclusive device. Whatever value you specify here can be overridden by an entry in the device’s hardware key or class key. If both keys have an override, the hardware key has precedence.

The exclusivity attribute matters only for whatever named device object is the target of an open request. If you follow Microsoft’s recommended guidelines for WDM drivers, you won’t give your device object a name. Open requests will then target the PDO, but the PDO will not usually be marked exclusive because the bus driver generally has no way of knowing whether you need your device to be exclusive. The only time the PDO will be marked exclusive is when there’s an Exclusive override in the device’s hardware key or the class key’s Properties subkey. You’re best advised, therefore, to avoid relying on the exclusive attribute altogether. Instead, make your IRP_MJ_CREATE handler reject open requests that would violate whatever restriction you require.

The last argument (&fdo) points to a location where IoCreateDevice will store the address of the device object it creates.

If IoCreateDevice fails for some reason, it returns a status code and doesn’t alter the PDEVICE_OBJECT described by the last argument. If it succeeds, it returns a successful status code and sets the PDEVICE_OBJECT pointer. You can then proceed to initialize your device extension and do the other work associated with creating a new device object. Should you discover an error after this point, you should release the device object and return a status code. The code to accomplish these tasks would be something like this:

NTSTATUS status = IoCreateDevice(...);
if (!NT_SUCCESS(status))
  return status;

if (<some other error discovered>)
  return status;

I’ll explain the NTSTATUS status codes and the NT_SUCCESS macro in the next chapter.

Windows XP uses a centralized Object Manager to manage many of its internal data structures, including the driver and device objects I’ve been talking about. David Solomon and Mark Russinovich present a fairly complete explanation of the Object Manager and namespace in Chapter 3, “System Mechanisms,” of Inside Windows 2000, Third Edition (Microsoft Press, 2000). Objects have names, which the Object Manager maintains in a hierarchical namespace. Figure 2-16 is a screen shot of my DEVVIEW application showing the top level of the name hierarchy. The objects displayed as folders in this screen shot are directory objects, which can contain subdirectories and “regular” objects. The objects displayed with other icons are examples of these regular objects. (In this respect, DEVVIEW is similar to the WINOBJ utility that you’ll find in the BIN\WINNT directory of the Platform SDK. WINOBJ can’t give you information about device objects and drivers, though, which is why I wrote DEVVIEW in the first place.)

Figure 2-16. Using DEVVIEW to view the namespace.

Device objects can have names that conventionally live in the \Device directory. Names for devices serve two purposes in Windows XP. Giving your device object a name allows other kernel-mode components to find it by calling service functions such as IoGetDeviceObjectPointer. Having found your device object, they can send you IRPs.

The other purpose of naming a device object is to allow applications to open handles to the device so they can send you IRPs. An application uses the standard CreateFile API to open a handle, whereupon it can use ReadFile, WriteFile, and DeviceIoControl to talk to you. The pathname an application uses to open a device handle begins with the prefix \\.\ rather than with a standard Universal Naming Convention (UNC) name such as C:\MYFILE.CPP or \\FRED\C-Drive\HISFILE.CPP. Internally, the I/O Manager converts this prefix to \??\ before commencing a name search. To provide a mechanism for connecting names in the \?? directory to objects whose names are elsewhere (such as in the \Device directory), the Object Manager implements an object called a symbolic link.

The name \?? has a special meaning in Windows XP. Confronted with this name, the Object Manager first searches a portion of the kernel namespace that is local to the current user session. To see how this works, establish two or more sessions and start DEVVIEW in one of them. Expand the \Sessions folder, and you will eventually see folders for each user. Figure 2-18, which appears later in this chapter, provides an example. If the local search isn’t successful, the Object Manager then searches the \GLOBAL?? folder.

A symbolic link is a little bit like a desktop shortcut in that it points to some other entity that’s the real object of attention. One use of symbolic links in Windows XP is to connect the leading portion of MS-DOS-style names to devices. Figure 2-17 shows a portion of the \GLOBAL?? directory, which includes a number of symbolic links. Notice, for example, that C and other drive letters in the MS-DOS file-naming scheme are actually links to objects whose names are in the \Device directory. These links allow the Object Manager to jump somewhere else in the namespace as it parses through a name. So if I call CreateFile with the name C:\MYFILE.CPP, the Object Manager will take this path to open the file:

Figure 2-17. The \GLOBAL?? directory with several symbolic links.
  1. Kernel-mode code initially sees the name \??\C:\MYFILE.CPP. The Object Manager special-cases the “??” name to mean the DosDevices directory for the current session. (In Figure 2-18, this directory is one of the subdirectories of \Sessions\0\DosDevices.)

  2. The Object Manager doesn’t find “C:” in the session DosDevices directory, so it follows a symbolic link named “Global” to the “GLOBAL??” directory.

  3. The Object Manager now looks up “C:” in the \GLOBAL?? directory. It finds a symbolic link by that name, so it forms the new kernel-mode pathname \Device\HarddiskVolume1\MYFILE.CPP and parses that.

  4. Working with the new pathname, the Object Manager looks up “Device” in the root directory and finds a directory object.

  5. The Object Manager looks up “HarddiskVolume1” in the \Device directory. It finds a device object by that name.

At this point in the process, the Object Manager will create an IRP that it will send to the driver or drivers for HarddiskVolume1. The IRP will eventually cause some file system driver or another to locate and open a disk file. Describing how a file system driver works is beyond the scope of this book, but the sidebar “Opening a Disk File” will give you a bit of the flavor.

If we were dealing with a device name such as COM1, the driver that ended up receiving the IRP would be the driver for \Device\Serial0. How a device driver handles an open request is definitely within the scope of this book, and I’ll be discussing it in this chapter (in the section “Should I Name My Device Object?”) and in Chapter 5, when I’ll talk about IRP processing in general.

A user-mode program can create a symbolic link in the local (session) namespace by calling DefineDosDevice, as in this example (see Figure 2-18):

BOOL okay = DefineDosDevice(DDD_RAW_TARGET_PATH,
  "barf", \\Device\\Beep);
Figure 2-18. Symbolic link created by DefineDosDevice.

You can create a symbolic link in a WDM driver by calling IoCreateSymbolicLink,

IoCreateSymbolicLink(linkname, targname);

where linkname is the name of the symbolic link you want to create and targname is the name to which you’re linking. Incidentally, the Object Manager doesn’t care whether targname is the name of any existing object: someone who tries to access an object by using a link that points to an undefined name simply receives an error. If you want to allow user-mode programs to override your link and point it somewhere else, you should call IoCreateUnprotectedSymbolicLink instead.

The kernel-mode equivalent of the immediately preceding DefineDosDevice call is this:

RtlInitUnicodeString(&linkname, L"\\DosDevices\\barf");
RtlInitUnicodeString(&targname, L"\\Device\\Beep");
IoCreateSymbolicLink(&linkname, &targname);

Deciding whether to give your device object a name requires, as I said earlier, a little thought. If you give your object a name, it will be possible for any kernel-mode program to try to open a handle to your device. Furthermore, it will be possible for any kernel-mode or user-mode program to create a symbolic link to your device object and to use the symbolic link to try to open a handle. You might or might not want to allow these actions.

The primary consideration in deciding whether to name your device object is security. When someone opens a handle to a named object, the Object Manager verifies that they have permission to do so. When IoCreateDevice creates a device object for you, it assigns a default security descriptor based on the device type you specify as the fourth argument. The I/O Manager uses three basic categories to select a security descriptor:

  • Most file system device objects (that is, disk, CD-ROM, file, and tape) receive the “public default unrestricted” access control list (ACL). This list gives just SYNCHRONIZE, READ_CONTROL, FILE_READ_ATTRIBUTES, and FILE_TRAVERSE access to everyone except the System account and all administrators. File system device objects, by the way, exist only so that there can be a target for a CreateFile call that will open a handle to a file managed by the file system.

  • Disk devices and network file system objects receive the same ACL as the file system objects, with some modifications. For example, everyone gets full access to a named floppy disk device object, and administrators get sufficient rights to run ScanDisk. (User-mode network provider DLLs need greater access to the device object for their corresponding file system driver, which is why network file systems are treated differently from other file systems.)

  • All other device objects receive the public open unrestricted ACL, which allows anyone with a handle to the device to do pretty much anything.

You can see that anyone will be able to access a nondisk device for both reading and writing if the driver gives the device object a name at the time it calls IoCreateDevice. This is because the default security allows nearly full access and because no security check at all is associated with creating a symbolic link?the security checks happen at open time, based on the named object’s security descriptor. This is true even if other device objects in the same stack have more restrictive security.

IoCreateDeviceSecure, a function in the .NET DDK, allows you to specify a nondefault security descriptor in situations in which no override is in the registry. This function is too new for us to describe it more fully here.

DEVVIEW will show you the security attributes of the device objects it displays. You can see the operation of the default rules I just described by examining a file system, a disk device, and any other random device.

The PDO also receives a default security descriptor, but it’s possible to override it with a security descriptor stored in the hardware key or in the Properties subkey of the class key. (The hardware key has precedence if both keys specify a descriptor.) Even lacking a specific security override, if either the hardware key or the class key’s Properties subkey overrides the hardware type or characteristics specification, the I/O Manager constructs a new default security descriptor based on the new type. The I/O Manager does not, however, override the security setting for any of the other device objects above the PDO. Consequently, for the overrides (and the administrative actions that set them up) to have any effect, you shouldn’t name your device object. Don’t despair though?applications can still access your device by means of a registered interface, which I’ll discuss soon.

You need to know about one last security concern. As the Object Manager parses its way through an object name, it needs only FILE_TRAVERSE access to the intermediate components of the name. It performs a full security check only on the object named by the final component. So suppose you have a device object reachable under the name \Device\Beep or by the symbolic link \??\Barf. A user-mode application that tries to open \\.\Barf for writing will be blocked if the object security has been set up to deny write access. But if the application tries to open a name like \\.\Barf\ExtraStuff that has additional name qualifications, the open request will make it all the way to the device driver (in the form of an IRP_MJ_CREATE I/O request) if the user merely has FILE_TRAVERSE permission, which is routinely granted. (In fact, most systems even run with the option to check for traverse permission turned off.) The I/O Manager expects the device driver to deal with the additional name components and to perform any required security checks with regard to them.

To avoid the security concern I just described, you can supply the flag FILE_DEVICE_SECURE_OPEN in the device characteristics argument to IoCreate­Device. This flag causes Windows XP to verify that someone has the right to open a handle to a device even if additional name components are present.

If you decide to name the device object, you’ll normally put the name in the \Device branch of the namespace. To give it a name, you have to create a UNICODE_STRING structure to hold the name, and you have to specify that string as an argument to IoCreateDevice:

RtlInitUnicodeString(&devname, L"\\Device\\Simple0");
IoCreateDevice(DriverObject, sizeof(DEVICE_EXTENSION), &devname,

I’ll discuss the use of RtlInitUnicodeString in the next chapter.

Starting in Windows XP, device object names are case insensitive. In Windows 98/Me and in Windows 2000, they are case sensitive. Be sure to spell \Device exactly as shown if you want your driver to be portable across all the systems. Note also the spelling of \DosDevices, particularly if your mother tongue doesn’t inflect the plural form of nouns!

Conventionally, drivers assign their device objects a name by concatenating a string naming their device type (“Simple” in this code fragment) with a 0-based integer denoting an instance of that type. In general, you don’t want to hard-code a name as I just did?you want to compose it dynamically using string-manipulation functions like the following:

static LONG lastindex = -1;
LONG devindex = InterlockedIncrement(&lastindex);
WCHAR name[32];
_snwprintf(name, arraysize(name), L"\\Device\\SIMPLE%2.2d",
RtlInitUnicodeString(&devname, name);

I’ll explain the various service functions used in this code fragment in the next couple of chapters. The instance number you derive for private device types might as well be a static variable, as shown in the code fragment.

The older method of naming I just discussed?naming your device object and creating a symbolic link name that applications can use?has two major problems. We’ve already discussed the security implications of giving your device object a name. In addition, the author of an application that wants to access your device has to know the scheme you adopted to name your devices. If you’re the only one writing the applications that will be accessing your hardware, that’s not much of a problem. But if many different companies will be writing applications for your hardware, and especially if many hardware companies are making similar devices, devising a suitable naming scheme is difficult.

To solve these problems, WDM introduces a new naming scheme for devices that is language-neutral, easily extensible, usable in an environment with many hardware and software vendors, and easily documented. The scheme relies on the concept of a device interface, which is basically a specification for how software can access hardware. A device interface is uniquely identified by a 128-bit GUID. You can generate GUIDs by running the Platform SDK utilities UUIDGEN or GUIDGEN?both utilities generate the same kind of number, but they output the result in different formats. The idea is that some industry group gets together to define a standard way of accessing a certain kind of hardware. As part of the standard-making process, someone runs GUIDGEN and publishes the resulting GUID as the identifier that will be forever after associated with that interface standard.

Figure 2-19. Using GUIDGEN to generate a GUID.

I think of an interface as being analogous to the protein markers that populate the surface of living cells. An application desiring to access a particular kind of device has its own protein markers that fit like a key into the markers exhibited by conforming device drivers. See Figure 2-20.

Figure 2-20. Using device interfaces to match applications and devices.
Registering a Device Interface

A function driver’s AddDevice function should register one or more device interfaces by calling IoRegisterDeviceInterface, as shown here:

#include <initguid.h>
  #include "guids.h"   NTSTATUS AddDevice(...)     {
    IoRegisterDeviceInterface(pdo, &GUID_DEVINTERFACE_SIMPLE,       NULL, &pdx->ifname);     }
  1. We’re about to include a header (GUIDS.H) that contains one or more DEFINE_GUID macros. DEFINE_GUID normally declares an external variable. Somewhere in the driver, though, we have to actually reserve initialized storage for every GUID we’re going to reference. The system header file INITGUID.H works some preprocessor magic to make DEFINE_GUID reserve the storage even if the definition of the DEFINE_GUID macro happens to be in one of the precompiled header files.

  2. I’m assuming here that I put the GUID definitions I want to reference into a separate header file. This would be a good idea, inasmuch as user-mode code will also need to include these definitions and won’t want to include a bunch of extraneous kernel-mode declarations relevant only to our driver.

  3. The first argument to IoRegisterDeviceInterface must be the address of the PDO for your device. The second argument identifies the GUID associated with your interface, and the third argument specifies additional qualified names that further subdivide your interface. Only Microsoft code uses this name subdivision scheme. The last argument is the address of a UNICODE_STRING structure that will receive the name of a symbolic link that resolves to this device object.

The return value from IoRegisterDeviceInterface is a Unicode string that applications will be able to determine without knowing anything special about how you coded your driver and will then be able to use in opening a handle to the device. The name is pretty ugly, by the way; here’s an example that I generated for one of my sample devices: \\?\ROOT#UNKNOWN#0000#{b544b9a2-6995-11d3-81b5-00c04fa330a6}.

All that registration actually does is create the symbolic link name and save it in the registry. Later on, in response to the IRP_MN_START_DEVICE Plug and Play request we’ll discuss in Chapter 7, you’ll make the following call to IoSetDeviceInterfaceState to enable the interface:

IoSetDeviceInterfaceState(&pdx->ifname, TRUE);

In response to this call, the I/O Manager creates an actual symbolic link object pointing to the PDO for your device. You’ll make a matching call to disable the interface at a still later time (just call IoSetDeviceInterfaceState with a FALSE argument), whereupon the I/O Manager will delete the symbolic link object while preserving the registry entry that contains the name. In other words, the name persists and will always be associated with this particular instance of your device; the symbolic link object comes and goes with the hardware.

Since the interface name ends up pointing to the PDO, the PDO’s security descriptor ends up controlling whether people can access your device. That’s good because it’s the PDO’s security that you control in the INF used to install the driver.

Enumerating Device Interfaces

Both kernel-mode and user-mode code can locate all the devices that happen to support an interface in which they’re interested. I’m going to explain how to enumerate all the devices for a particular interface in user mode. The enumeration code is so tedious to write that I eventually wrote a C++ class to make my own life simpler. You’ll find this code in the DEVICELIST.CPP and DEVICELIST.H files that are part of the HIDFAKE and DEVPROP samples in Chapter 8. These files declare and implement a CDeviceList class, which contains an array of CDeviceListEntry objects. These two classes have the following declaration:

class CDeviceListEntry
  CDeviceListEntry(LPCTSTR linkname, LPCTSTR friendlyname);
  CString m_linkname;
  CString m_friendlyname;

class CDeviceList
  CDeviceList(const GUID& guid);
  GUID m_guid;
  CArray<CDeviceListEntry, CDeviceListEntry&> m_list;
  int Initialize();

The classes rely on the CString class and CArray template class that are part of the Microsoft Foundation Classes (MFC) framework. The constructors for these two classes simply copy their arguments into the obvious data members:

CDeviceList::CDeviceList(const GUID& guid)
  m_guid = guid;

CDeviceListEntry::CDeviceListEntry(LPCTSTR linkname,
  LPCTSTR friendlyname)
  m_linkname = linkname;
  m_friendlyname = friendlyname;

All the interesting work occurs in the CDeviceList::Initialize function. The executive overview of what it does is this: it will enumerate all of the devices that expose the interface whose GUID was supplied to the constructor. For each such device, it will determine a friendly name that we’re willing to show to an unsuspecting end user. Finally it will return the number of devices it found. Here’s the code for this function:

int CDeviceList::Initialize()
  HDEVINFO info = SetupDiGetClassDevs(&m_guid, NULL, NULL,     DIGCF_PRESENT │ DIGCF_INTERFACEDEVICE);   if (info == INVALID_HANDLE_VALUE)     return 0;   SP_INTERFACE_DEVICE_DATA ifdata;   ifdata.cbSize = sizeof(ifdata);   DWORD devindex;
  for (devindex = 0;      SetupDiEnumDeviceInterfaces(info, NULL, &m_guid,        devindex, &ifdata); ++devindex)     {     DWORD needed;
    SetupDiGetDeviceInterfaceDetail(info, &ifdata, NULL, 0,       &needed, NULL);     PSP_INTERFACE_DEVICE_DETAIL_DATA detail =        (PSP_INTERFACE_DEVICE_DETAIL_DATA) malloc(needed);     detail->cbSize = sizeof(SP_INTERFACE_DEVICE_DETAIL_DATA);     SP_DEVINFO_DATA did = {sizeof(SP_DEVINFO_DATA)};     SetupDiGetDeviceInterfaceDetail(info, &ifdata, detail,       needed, NULL, &did));
    TCHAR fname[256];     if (!SetupDiGetDeviceRegistryProperty(info, &did,        SPDRP_FRIENDLYNAME, NULL, (PBYTE) fname,        sizeof(fname), NULL)       && !SetupDiGetDeviceRegistryProperty(info, &did,          SPDRP_DEVICEDESC,         NULL, (PBYTE) fname, sizeof(fname), NULL))       _tcsncpy(fname, detail->DevicePath, 256);       fname[255] = 0;
    CDeviceListEntry e(detail->DevicePath, fname);     free((PVOID) detail);     m_list.Add(e);     }   SetupDiDestroyDeviceInfoList(info);   return m_list.GetSize();   }
  1. This statement opens an enumeration handle that we can use to find all devices that have registered an interface that uses the same GUID.

  2. Here we call SetupDiEnumDeviceInterfaces in a loop to find each device.

  3. The only two items of information we need are the detail information about the interface and information about the device instance. The detail is just the symbolic name for the device. Since it’s variable in length, we make two calls to SetupDiGetDeviceInterfaceDetail. The first call determines the length. The second call retrieves the name.

  4. We obtain a friendly name for the device from the registry by asking for either the FriendlyName or the DeviceDesc.

  5. We create a temporary instance named e of the CDeviceListEntry class, using the device’s symbolic name as both the link name and the friendly name.

You might be wondering how the registry comes to have a FriendlyName for a device. The INF file you use to install your device driver?see Chapter 15?can have an HW section that specifies registry parameters for the device. You can provide a FriendlyName as one of these parameters, but bear in mind that every instance of your hardware will have the same name if you do. The MAKENAMES sample describes a DLL-based way of defining a unique friendly name for each instance. You can also write a CoInstaller DLL that will define unique friendly names.

If you don’t define a FriendlyName, by the way, most system components will use the DeviceDesc string in the registry. This string originates in the INF file and will usually describe your device by manufacturer and model.

Sample Code
The DEVINTERFACE sample is a user-mode program that enumerates all instances of all known device interface GUIDs on your system. One way to use this sample is as a way to determine which GUID you need to enumerate to find a particular device.

You need to take some other steps during AddDevice to initialize your device object. I’m going to describe these steps in the order you should do them, which isn’t exactly the same order as their respective logical importance. I want to emphasize that the code snippets in this section are even more fragmented than usual?I’m going to show only enough of the entire AddDevice routine to establish the surrounding context for the small pieces I’m trying to illustrate.

The content and management of the device extension are entirely up to you. The data members you place in this structure will obviously depend on the details of your hardware and on how you go about programming the device. Most drivers would need a few items placed there, however, as illustrated in the following fragment of a declaration:

typedef struct _DEVICE_EXTENSION {
    PDEVICE_OBJECT DeviceObject;
    PDEVICE_OBJECT LowerDeviceObject;
    UNICODE_STRING ifname;
    IO_REMOVE_LOCK RemoveLock;
    DEVSTATE devstate;     DEVSTATE prevstate;     DEVICE_POWER_STATE devpower;     SYSTEM_POWER_STATE syspower;
  1. I find it easiest to mimic the pattern of structure declaration used in the official DDK, so I declared this device extension as a structure with a tag as well as a type and pointer-to-type name.

  2. You already know that you locate your device extension by following the DeviceExtension pointer from the device object. It’s also useful in several situations to be able to go the other way?to find the device object given a pointer to the extension. The reason is that the logical argument to certain functions is the device extension itself (since that’s where all of the per-instance information about your device resides). Hence, I find it useful to have this DeviceObject pointer.

  3. I’ll mention in a few paragraphs that you need to record the address of the device object immediately below yours when you call IoAttachDeviceToDeviceStack, and LowerDeviceObject is the place to do that.

  4. A few service routines require the address of the PDO instead of some higher device object in the same stack. It’s very difficult to locate the PDO, so the easiest way to satisfy the requirement of those functions is to record the PDO address in a member of the device extension that you initialize during AddDevice.

  5. Whichever method (symbolic link or device interface) you use to name your device, you’ll want an easy way to remember the name you assign. In this code fragment, I’ve declared a Unicode string member named ifname to record a device interface name. If you were going to use a symbolic link name instead of a device interface, it would make sense to give this member a more mnemonic name, such as linkname.

  6. I’ll discuss in Chapter 6 a synchronization problem affecting how you decide when it’s safe to remove this device object by calling IoDeleteDevice. The solution to that problem involves using an IO_REMOVE_LOCK object that needs to be allocated in your device extension as shown here. AddDevice needs to initialize that object.

  7. You’ll probably need a device extension variable to keep track of the current Plug and Play state and current power states of your device. DEVSTATE is an enumeration that I’m assuming you’ve declared elsewhere in your own header file. I’ll discuss the use of all these state variables in later chapters.

  8. Another part of power management involves remembering some capability settings that the system initializes by means of an IRP. The devcaps structure in the device extension is where I save those settings in my sample drivers.

The initialization statements in AddDevice (with emphasis on the parts involving the device extension) would be as follows:

NTSTATUS AddDevice(...)
  IoCreateDevice(..., sizeof(DEVICE_EXTENSION), ..., &fdo);
  pdx->DeviceObject = fdo;
  pdx->Pdo = pdo;
  IoInitializeRemoveLock(&pdx->RemoveLock, ...);
  pdx->devstate = STOPPED;
  pdx->devpower = PowerDeviceD0;
  pdx->syspower = PowerSystemWorking;
  IoRegisterDeviceInterface(..., &pdx->ifname);
  pdx->LowerDeviceObject = IoAttachDeviceToDeviceStack(...);

In this code snippet, STOPPED and DEVICE_EXTENSION are things I defined in one of my own header files.

Many devices signal completion of operations by means of an interrupt. As you’ll learn when I discuss interrupt handling in Chapter 7, there are strict limits on what your interrupt service routine (ISR) can do. In particular, an ISR isn’t allowed to call the routine (IoCompleteRequest) that signals completion of an IRP, but that’s exactly one of the steps you’re likely to want to take. You utilize a deferred procedure call (DPC) to get around the limitations. Your device object contains a subsidiary DPC object that can be used for scheduling your particular DPC routine, and you need to initialize it shortly after creating the device object:

NTSTATUS AddDevice(...)
  IoInitializeDpcRequest(fdo, DpcForIsr);

Devices that perform DMA transfers work directly with data buffers in memory. The HAL might require that buffers used for DMA be aligned to some particular boundary, and your device might require still more stringent alignment. The AlignmentRequirement field of the device object expresses the restriction?it’s a bit mask equal to 1 less than the required address boundary. You can round an arbitrary address down to this boundary with this statement:

PVOID address = ...;
SIZE_T ar = fdo->AlignmentRequirement;
address = (PVOID) ((SIZE_T) address & ~ar);

You round an arbitrary address up to the next alignment boundary like this:

PVOID address = ...;
SIZE_T ar = fdo->AlignmentRequirement;
address = (PVOID) (((SIZE_T) address + ar) & ~ar);

In these two code fragments, I used SIZE_T casts to transform the pointer (which may be 32 bits or 64 bits wide, depending on the platform for which you’re compiling) into an integer wide enough to span the same range as the pointer.

IoCreateDevice sets the AlignmentRequirement field of the new device object equal to whatever the HAL requires. For example, the HAL for Intel x86 chips has no alignment requirement, so AlignmentRequirement is 0 initially. If your device requires a more stringent alignment for the data buffers it works with (say, because you have bus-mastering DMA capability with a special alignment requirement), you want to override the default setting. For example:

if (MYDEVICE_ALIGNMENT - 1 > fdo->AlignmentRequirement)
  fdo->AlignmentRequirement = MYDEVICE_ALIGNMENT - 1;

I’ve assumed here that elsewhere in your driver is a manifest constant named MYDEVICE_ALIGNMENT that equals a power of 2 and represents the required alignment of your device’s data buffers.

Your device might well use other objects that need to be initialized during AddDevice. Such objects might include various synchronization objects, linked list anchors, scatter/gather list buffers, and so on. I’ll discuss these objects, and the fact that initialization during AddDevice would be appropriate, in various other parts of this book.

Two of the flag bits in your device object need to be initialized during AddDevice and never changed thereafter: the DO_BUFFERED_IO and DO_DIRECT_IO flags. You can set one (but only one) of these bits to declare once and for all how you want to handle memory buffers coming from user mode as part of read and write requests. (I’ll explain in Chapter 7 what the difference between these two buffering methods is and why you’d want to pick one or the other.) The reason you have to make this important choice during AddDevice is that any upper filter drivers that load afterwards will be copying your flag settings, and it’s the setting of the bits in the topmost device object that’s actually important. Were you to change your mind after the filter drivers loaded, they probably wouldn’t know about the change.

Two of the flag bits in the device object pertain to power management. In contrast with the two buffering flags, these two can be changed at any time. I’ll discuss them in greater detail in Chapter 8, but here’s a preview. DO_POWER_PAGABLE means that the Power Manager must send you IRP_MJ_POWER requests at interrupt request level (IRQL) PASSIVE_LEVEL. (If you don’t understand all of the concepts in the preceding sentence, don’t worry?I’ll completely explain all of them in later chapters.) DO_POWER_INRUSH means that your device draws a large amount of current when powering on, so the Power Manager should make sure that no other inrush device is powering up simultaneously.

Each filter and function driver has the responsibility of building up the stack of device objects, starting from the PDO and working upward. You accomplish your part of this work with a call to IoAttachDeviceToDeviceStack:

  IoCreateDevice(..., &fdo);
  pdx->LowerDeviceObject = IoAttachDeviceToDeviceStack(fdo, pdo);

The first argument to IoAttachDeviceToDeviceStack (fdo) is the address of your own newly created device object. The second argument is the address of the PDO. The second parameter to AddDevice is this address. The return value is the address of whatever device object is immediately underneath yours, which can be the PDO or the address of some lower filter device object. Figure 2-21 illustrates the situation when there are three lower filter drivers for your device. By the time your AddDevice function executes, all three of their AddDevice functions have already been called. They have created their respective FiDOs and linked them into the stack rooted at the PDO. When you call IoAttachDeviceToDeviceStack, you get back the address of the topmost FiDO.

IoAttachDeviceToDeviceStack might conceivably fail by returning a NULL pointer. For this to occur, someone would have to remove the physical device from the system at just the point in time when your AddDevice function was doing its work, and the PnP Manager would have to process the removal on another CPU. I’m not even sure these conditions are enough to trigger a failure. (Or else the driver under you could have forgotten to clear DO_DEVICE_INITIALIZING, I suppose.) You would deal with the failure by cleaning up and returning STATUS_DEVICE_REMOVED from your AddDevice function.

Figure 2-21. What IoAttachDeviceToDeviceStack returns.

Pretty much the last thing you do in AddDevice should be to clear the DO_DEVICE_INITIALIZING flag in your driver object:


While this flag is set, the I/O Manager will refuse to attach other device objects to yours or to open a handle to your device. You have to clear the flag because your device object initially arrives in the world with the flag set. In previous releases of Windows NT, most drivers created all of their device objects during DriverEntry. When DriverEntry returns, the I/O Manager automatically traverses the list of device objects linked from the driver object and clears this flag. Since you’re creating your device object long after DriverEntry returns, however, this automatic flag clearing won’t occur, and you must do it yourself.

Here is a complete AddDevice function, presented without error checking or annotations and including all the pieces described in the preceding sections:

  NTSTATUS status = IoCreateDevice(DriverObject,

  IoRegisterDeviceInterface(pdo, &GUID_DEVINTERFACE_SIMPLE,
    NULL, &pdx->ifname);

  pdx->DeviceObject = fdo;
  pdx->Pdo = pdo;
  IoInitializeRemoveLock(&pdx->RemoveLock, 0, 0, 0);
  pdx->devstate = STOPPED;
  pdx->devpower = PowerDeviceD0;
  pdx->syspower = PowerSystemWorking;

  IoInitializeDpcRequest(fdo, DpcForIsr);

  if (MYDEVICE_ALIGNMENT - 1 > fdo->AlignmentRequirement)
    fdo->AlignmentRequirement = MYDEVICE_ALIGNMENT - 1;
  KeInitializeEvent(&pdx->SomeEvent, NotificationEvent, FALSE);

  pdx->LowerDeviceObject = IoAttachDeviceToDeviceStack(fdo,


Posted by 쫑경
TAG AddDevice
WDM/Architecture2009.06.08 15:39
시스템을 부팅을 하기 위해서는 리콜을 해야한다


0000:7c00h 마스터 부트 레코드(MBR)에 전달하고 있다고 신호를 보내면 작업을 시작한다. 이를 트리거 라고한다..

MBR은 아래와 같은 구조를 가진다..

Posted by 쫑경
TAG MBR, sysboot
WDM/Architecture2009.05.20 16:39

The filter manager is installed with Windows, but it becomes active only when a minifilter driver is loaded. The filter manager attaches to the file system stack for a target volume. A minifilter driver attaches to the file system stack indirectly, by registering with the filter manager for the I/O operations the minifilter driver chooses to filter.

A legacy filter driver's position in the file system I/O stack relative to other filter drivers is determined at system startup by its load order group. For example, an antivirus filter driver should be higher in the stack than a replication filter driver, so it can detect viruses and disinfect files before they are replicated to remote servers. Therefore, filter drivers in the FSFilter Anti-Virus load order group are loaded before filter drivers in the FSFilter Replication group. Each load order group has a corresponding system-defined class and class GUID used in the INF file for the filter driver.

Like legacy filter drivers, minifilter drivers attach in a particular order. However, the order of attachment is determined by a unique identifier called an altitude. The attachment of a minifilter driver at a particular altitude on a particular volume is called an instance of the minifilter driver.

A minifilter driver's altitude ensures that the instance of the minifilter driver is always loaded at the appropriate location relative to other minifilter driver instances, and it determines the order in which the filter manager calls the minifilter driver to handle I/O. Altitudes are allocated and managed by Microsoft.

The following figure shows a simplified I/O stack with the filter manager and three minifilter drivers.


Simplified I/O Stack with Filter Manager and Minifilter Drivers

A minifilter driver can filter IRP-based I/O operations as well as fast I/O and file system filter (FSFilter) callback operations. For each of the I/O operations it chooses to filter, a minifilter driver can register a preoperation callback routine, a postoperation callback routine, or both. When handling an I/O operation, the filter manager calls the appropriate callback routine for each minifilter driver that registered for that operation. When that callback routine returns, the filter manager calls the appropriate callback routine for the next minifilter driver that registered for the operation.

For example, assuming all three minifilter drivers in the above figure registered for the same I/O operation, the filter manager would call their preoperation callback routines in order of altitude from highest to lowest (A, B, C), then forward the I/O request to the next-lower driver for further processing. When the filter manager receives the I/O request for completion, it calls each minifilter driver's postoperation callback routines in reverse order, from lowest to highest (C, B, A).

For interoperability with legacy filter drivers, the filter manager can attach filter device objects to a file system I/O stack in more than one location. Each of the filter manager's filter device objects is called a frame. From the perspective of a legacy filter driver, each filter manager frame is just another legacy filter driver.

Each filter manager frame represents a range of altitudes. If a legacy filter driver is attached to the file system stack, the system attaches a filter manager frame for the range of altitudes above and below the legacy filter driver, to ensure that minifilter drivers attach at the correct location.

If a minifilter driver is unloaded and reloaded, it is reloaded at the same altitude in the same frame from which it was unloaded.

The following figure shows a simplified I/O stack with a two filter manager frames, minifilter driver instances, and a legacy filter driver.


Simplified I/O Stack with Two Filter Manager Frames and a Legacy Filter Driver

원문 : http://msdn.microsoft.com/en-us/library/aa488191.aspx
Posted by 쫑경
WDM/Architecture2009.05.11 13:50
Windows Network Architecture and the OSI Model

The Microsoft Windows operating systems use a network architecture that is based on the seven-layer networking model developed by the International Standards Organization (ISO). Introduced in 1978, the ISO Open Systems Interconnection (OSI) Reference model describes networking as "a series of protocol layers with a specific set of functions allocated to each layer. Each layer offers specific services to higher layers while shielding these layers from the details of how the services are implemented. A well-defined interface between each pair of adjacent layers defines the services offered by the lower layer to the higher one and how those services are accessed."


OSI reference model

Microsoft Windows network drivers implement the bottom four layers of the OSI Reference Model:

Physical Layer
The physical layer is the lowest layer of the OSI model. This layer manages the reception and transmission of the unstructured raw bit stream over a physical medium. It describes the electrical/optical, mechanical, and functional interfaces to the physical medium. The physical layer carries the signals for all of the higher layers. In Windows, the physical layer is implemented by the network interface card (NIC), its transceiver, and the medium to which the NIC is attached.
Data Link Layer
The data link layer is further divided by Institute of Electrical and Electronics Engineers (IEEE) into two sublayers: logical link control (LLC) and media access control (MAC).
The LLC sublayer provides error-free transfer of data frames from one node to another. The LLC sublayer establishes and terminates logical links, controls frame flow, sequences frames, acknowledges frames, and retransmits unacknowledged frames. The LLC sublayer uses frame acknowledgement and retransmission to provide virtually error-free transmission over the link to the layers above.
The MAC sublayer manages access to the physical layer, checks frame errors, and manages address recognition of received frames.
In the Windows network architecture, the LLC sublayer is implemented in the transport driver, and the MAC sublayer is implemented in the NIC. The NIC is controlled by a software device driver called the miniport driver. Windows supports several variations of miniport drivers including WDM miniport drivers, miniport call managers (MCMs), and miniport intermediate drivers.
Network Layer
The network layer controls the operation of the subnet. This layer determines the physical path that the data should take, based on the following:

Transport Layer
The transport layer ensures that messages are delivered error-free, in sequence, and with no loss or duplication. This layer relieves the higher-layer protocols from any concern with the transfer of data between them and their peers. A minimal transport layer is required in protocol stacks that include a reliable network or LLC sublayer that provides virtual circuit capability. For example, because the NetBEUI transport driver for Windows an OSI-compliant LLC sublayer, its transport layer functions are minimal. If the protocol stack does not include an LLC sublayer, and if the network layer is unreliable and/or supports datagrams (as with TCP/IP's IP layer or NWLink's IPX layer), the transport layer should include frame sequencing and acknowledgment, as well as retransmission of unacknowledged frames.

In the Windows network architecture, the LLC, network, and transport layers are implemented by software drivers known as protocol drivers, which are sometimes referred to as transport drivers.

Posted by 쫑경
WDM/Architecture2009.04.21 09:24

0. 서문

현재 Linux가 돌고 있는 시스템 중의 대부분은 Intel IA32 CPU이다. Linux는 Intel 80386부터 시작하여 80486, Pentium 계열의 CPU에서 실행이 된다. 운영체제의 기능을 구현하려면 CPU의 지원을 필요로 하는데, Linux는 80386부터 등장한 32-bit 보호모드(protected mode)의 지원을 이용하여, 메모리 관리, 프로세스 관리 등을 하고 있다. 여기서는 Linux를 구현하기 위해 필요한 보호모드의 기능들을 간단히 살펴보도록 한다.

1. 실제모드(Real Mode)

실제모드는 x86 계열로 처음 등장한 Intel 8086 CPU와 같은 동작 모드를 말하는 것으로, 16 bit CPU인 8086, 80286에서뿐만 아니라, 80386 이후의 모든 32-bit 프로세서에서도 이를 지원한다. x86 계열의 모든 CPU는 처음 시작할 때는 실제모드로 동작한다.

실제모드에서는 20 bit address bus를 사용하여 총 1MB의 메모리를 사용할 수 있으며, 16 bit register를 사용한다. 레지스터의 크기가 16 bit이기 때문에, 20 bit 주소를 나타내기 위해 segment register라는 것을 도입하였다. 이는 16 bit segment register와 16 bit offset을 중첩시켜서 20 bit의 주소를 만들어내는 것이다. 모든 메모리 접근에는 segment와 offset이 같이 필요하며, 하나의 segment를 사용하면 64K(0x10000)만큼의 메모리를 사용할 수 있다. 실제모드에서는 가상 메모리라는 개념이 존재하지 않으며, segment와 offset으로 만들어지는 주소는 바로 물리적인 메모리 주소이다. 또한 이 모드에서 동작하는 모든 프로그램은 메모리의 어떤 영역이든지 맘대로 접근할 수 있으며, cli (clear interrupt), sti (set interrupt)같은 명령어를 포함하여 실제모드에서 사용할 수 있는 모든 명령어들을 모두 사용할 수 있다.

IBM-PC는 8086 CPU를 이용하여 만들어졌는데, 이것이 처음 등장하던 당시엔 1MB의 메모리는 상당히 큰 것이었다. 그래서 IBM은 앞의 640KB(0 - 0x9ffff)만을 프로그램이 사용할 수 있게 하고, 나머지 384KB(0xa0000 - 0xfffff)는 BIOS와 ISA 장치용으로 사용하게 하였는데, 이는 실제모드로 동작하는 각종 프로그램들이 640KB의 메모리만을 사용할 수 밖에 없는 제약을 만들었다. 일반적으로 리눅스 부팅을 할 때 사용되는 LILO도 실제모드로 시작하여 리눅스 커널을 로드하기 때문에, 마찬가지로 640KB의 제약을 받게 된다.

그림 1-1. 실제모드에서의 메모리 변환과정 (주1)

그림 1-2. 실제모드에서의 메모리 계산방법 (주2)

2. 보호모드(Protected Mode)

Intel 80286부터 처음 도입된 보호모드는 80386에 이르러서 완성된 모습을 보여 지금에 이르게 된다. 80286의 보호모드를 간단히 살펴보면, 우선 24 bit address bus를 사용하여 총 16MB(0x1000000)의 메모리를 사용할 수 있게 하였다. segment register는 selector라는 명칭으로 바뀌었고, descriptor table이라는 것을 통하여 16 bit segment를 24 bit base address로 바꾸게 하였다. 이 base address에 offset을 더함으로써 모두 16MB의 메모리를 사용할 수 있었지만, 80286 역시 16 bit CPU였기 때문에 각 segment의 크기는 여전히 64KB의 제한을 가지게 되었다.

이런 제한을 없애고 완전한 32 bit address space와 32 bit register set을 제공하는 80386이 등장하였고, 이후에 운영체제들은 80386에서 제공하는 보호모드의 기능을 활용하게 되었다. 보호모드에서는 CPU가 제공하는 모든 기능과 명령어들을 활용할 수 있다. 보호모드에서는 privilege level이라는 것이 등장하는데, 이는 프로세서를 활용할 수 있는 권한정도를 말한다. 이는 0부터 3까지 있는데, 0이 모든 일을 할 수 있는 모드로 일반적인 운영체제 구현에서 커널모드가 이 상태이며, 3은 응용프로그램처럼 사용자 권한의 실행상태를 나타낸다. 이 privilege level을 통하여 커널모드/사용자모드를 구현하게 된다. 보호모드에서는 32 bit address bus를 통하여 4GB의 메모리를 사용할 수 있으며, 메모리 보호기능과, 페이징(paging) 메커니즘 등을 통해 가상 메모리를 효율적으로 구현할 수 있다. 인터럽트나 예외처리, task switching 등도 모두 보호모드에서 지원하는 기능을 활용한다.

그림 2-1. 보호모드 레지스터와 자료구조 (주3)

3. Intel 80386 Registers

  • 일반 목적의 data register (32 bits) :
    • EAX, EBX, ECX, EDX
    • ESI (source pointer), EDI (destination pointer)
    • ESP (stack pointer), EBP (base pointer)
  • Segment Register / Selector (16 bits) :
    • CS (code segment), DS (data segment), SS (stack segment)
    • ES, FS, GS
  • Flag Register (32 bits) : EFLAGS
  • Instruction pointer : EIP
  • Control Register : 시스템의 동작들을 제어하기 위한 레지스터
    • CR0 : 프로세서의 상태와 동작모드를 제어하는 여러가지 제어 flag를 가지고 있다. 대표적인 flag로는 PG (Paging. paging 사용여부 설정), PE (Protection Enable, 보호모드를 사용하는지 여부)가 있다.
    • CR1 : reserved
    • CR2 : page fault가 발생하였을 때 이것이 발생한 linear address를 가지고 있다.
    • CR3 : page directory가 시작하는 physical address를 가지고 있다.
    • CR4 : 아키텍쳐별로 확장한 여러가지 flag들을 가지고 있다.
  • Descriptor Table Register :
    • GDTR (Global Descriptor Table Register) : GDT의 위치를 가리키는 register. LGDT (Load GDT), SGDT (Store GDT) 명령으로 참조하고 설정한다.
    • LDTR (Local Descriptor Table Register) : GDT안에 LDT descriptor를 가리키는 selector. LLDT (Load LDT), SLDT (Store LDT) 명령으로 참조하고 설정한다. 내부적으로 segment의 base address와 segment limit도 가지고 있지만 외부에서는 selector 값만을 참조할 수 있다.
    • IDTR (Interrupt Descriptor Table Register) : IDT의 위치를 가리키는 register LIDT (Load IDT), SIDT (Store IDT) 명령으로 참조하고 설정한다.
  • Task Register : TR
      TSS (Task State Segment)가 있는 위치를 가리키는 GDT 내의 TSS descriptor를 가리키는 selector. 내부적으로 segment의 base address와 segment limit도 가지고 있지만 외부에서는 selector 값만을 참조할 수 있다. LTR (Load TR), STR (Save TR) 명령으로 참조하고 설정한다.
  • Test Register :
    • TR1 : test parity check
    • TR2, TR3, TR4, TR5 : cache test register
    • TR6, TR7 : TLB (Translation Look-aside Buffers) test registers
    • TR9, TR10, TR11 : BTB (Branch Target Buffers) test registers
    • TR12 : new feature control
  • Debug Register :
    • DR0, DR1, DR2, DR3 : debug address register. breakpoint의 linear address를 가지고 있다.
    • DR4, DR5 : reserved
    • DR6 : debug status register
    • DR7 : debug control register
  • Model Specific Registers (MSRs) :

4. 보호모드에서의 메모리 관리

보호모드에서는 segmentation과 paging 메커니즘을 이용하여 메모리를 관리한다. segmentation은 4GB의 메모리를 segment라는 단위로 쪼개는 것을 말한다. 여기서는 16 bit의 selector와 32 bit의 offset을 이용하여 4GB 범위안에 있는 32 bit의 선형주소(linear address)를 만드는 일을 한다. 이렇게 만들어진 선형주소가 물리주소(physical address)가 되는 것이 아니라 메모리를 4KB 단위로 쪼개서 관리하는 paging mechanism을 거쳐서 물리주소로 변환된다. (control register CR0의 PG flag를 수정함으로써 paging 메커니즘을 사용하지 않을 수도 있다)

그림 4-1. 보호모드에서의 메모리 변환과정 (주4)

segment는 segment descriptor라는 것으로 정의가 된다. 여기에는 segment의 base address와 segment limit (크기), 그리고 DPL (descriptor privilege level)을 비롯한 정보가 들어간다. (여기에 보면 segment type이라는 것이 있다. 이것은 이 descriptor가 나타내는 것이 무엇인지를 말하는 것이다. segment는 일반적인 code/data/stack일 수도 있고, 뒤에 나오는 TSS 일수도, gate일 수도 있는데 이 type을 가지고 무엇을 가리키는 descriptor인지 구별할 수 있으며, 이에 따라 descriptor의 구조가 달라진다.) segment descriptor table은 이들 segment descriptor를 모아두고 있는 것을 말하는데, 여기에는 시스템 전체에 대한 descriptor table인 GDT (global descriptor table)과 프로세스마다 개별적으로 정의하고 있는 LDT (local descriptor table)이 있다. 이들은 모두 메모리 상에 존재하게 되는데, GDT의 위치는 GDTR (GDT register)가 가리키고 있다. LDT의 위치는 LDTR (LDT register)가 가리키고 있는데 이 LDTR은 원래 GDT에 있는 한 segment descriptor를 가리키고 있는 selector이다. 즉 GDT에는 시스템에 있는 모든 LDT에 대한 segment descriptor가 들어있으며, LDTR은 이 중 현재 프로세스의 LDT에 대한 segment descriptor를 가리키는 selector이다.

16 bit인 selector에는 이 descriptor table에서의 index 값과, 이것이 이것이 GDT에서의 index인지, LDT에서의 index인지를 나타내는 TI (table indicator), 권한을 나타내는 RPL (requestor privilege level)이 들어있다. 이 TI와 index를 가지고 해당하는 table에서 segment descriptor를 찾아서 base address를 구하게 된다. 이 값에 offset을 합하면 지정한 selector와 offset에 해당하는 linear address가 만들어지게 된다.

그림 4-2. Selector (주5)

그림 4-3. Segment Descriptor (주6)

그림 4-4. descriptor table 참조 (주7)

그림 4-5. 가상주소에서 선형주소로의 변환 (주8)

이렇게 segmentation을 거쳐 나온 linear address는 paging 메커니즘을 통하여 physical address로 변환이 된다. paging은 요즘의 운영체제에서 모두 구현하고 있는 paging을 지원하기 위한 것이다. 이를 이용하여 실제로 하드웨어적으로 있는 메모리보다 많은 메모리를 사용할 수 있으며, 메모리를 효율적으로 사용할 수 있으며, swapping을 쉽게 구현할 수 있게 된다. 메모리는 4KB 단위의 page로 쪼개지며, 이들은 page directory와 page table로 체계화된다. 각 page들의 시작 위치는 page table entry에 기록이 되며, 각 page table들의 위치는 page directory entry에 들어있다. page directory의 시작 위치는 control register중의 하나인 CR3이 가지고 있다. 앞에서 넘어온 linear address는 page directory에서의 index, page table에서의 index, offset 세가지로 쪼개지며, 이들 table들을 차례로 따라가서 실제 page의 주소를 얻고, 여기에 offset을 더함으로써 실제 physical address가 나오게 된다.

그림 4-6. 선형주소에서 물리주소로의 변환 (주9)

이렇게 가상주소가 물리주소로 변환되는 과정을 정리하면 다음과 같다.

그림 4-7. 가상주소에서 물리주소로의 변환 (주10)

5. 보호모드에서의 태스크 관리

보호모드에서는 각 task별로 TSS (Task State Segment)라는 것을 관리한다. 여기에는 하나의 task의 상태 - 일반 register, segment register, flag, EIP, stack segment selector, stack pointer, LDT selector, page directory base address 등 - 가 저장이 되며, task switching이 일어날 때 이전의 상태를 자동으로 여기에 저장을 하며, 새로운 TSS에 있는 상태가 현재 상태로 복구가 된다. task register인 TR은 현재 task의 TSS를 가리키는 selector이다. 각 TSS는 GDT에서 TSS descriptor로 기술되며, TR은 이 중에서 현재 TSS의 descriptor를 가리키는 것이다.

그림 5-1. Task Register (주11)

task switching을 하는 방법으로 우선 call이나 jmp 명령에 전환할 TSS selector를 지정하는 것이 있다. 이렇게 하면 프로세서는 현재 상태를 현재 TSS에 저장을 하고, 새로운 task의 TSS로 상태를 모두 바꾼후, 새로운 task를 실행한다.

task gate는 task switching을 일으키는 특별한 descriptor로서 LDT나 IDT (Interrupt Descriptor Table)에 들어가는 descriptor이다. 여기에는 TSS descriptor에 대한 selector가 들어있다. task gate는 interrupt나 trap이 발생하였을 때 task switching이 일어나게 하는 것처럼 간접적으로 task switching이 일어나게 하거나, 특정한 task들이 TSS에 접근할 수 있도록 하기 위해서 사용한다. 앞의 경우에서처럼 call이나 jmp에 task gate의 selector를 지정을 해주면 여기서 가리키는 TSS로 task switching이 일어난다. 또한 task gate가 IDT에 있을 때 해당하는 interrupt가 발생하면 이 task gate를 통하여 해당하는 TSS로 task switching이 일어나게 된다.

그림 5-1. Task Gate (주12)

6. 보호모드에서의 인터럽트/예외 처리

인터럽트나 예외가 발생하면 어떤 것이 발생하였는지 식별하는 번호가 나오게 된다. (인터럽트는 CPU의 인터럽트 핀에 의하여 발생하며, 예외는 프로세서가 작업을 하는 중에 잘못된 일을 발견하거나 - 여기에는 fault, trap, abort가 있다 -, 프로그래밍으로 들어가 있는 코드에 의하여 - INT 3, INT n, BOUND 등 - 에 의하여 발생하는 것이다) 이 번호를 vector라고 부르는데 이를 index 삼아, IDT (interrupt descriptor table)에 있는 descriptor를 찾아서 이를 처리하게 된다. IDT는 interrupt 처리에 관련된 descriptor들을 모아둔 것으로 이의 위치는 IDTR (IDT register)가 가지고 있다. 여기에 들어가는 descriptor로는 task gate, interrupt gate, trap gate가 있다. task gate는 앞에서 이야기한 것처럼 TSS selector를 가지고 있는 gate로서, 해당하는 TSS로 task switching이 일어나게 된다. interrupt gate와 trap gate는 GDT/LDT에 있는 segment descriptor에 대한 selector와 offset을 가지고 있는 gate로서, selector를 가지고 segment의 시작주소를 얻고, 여기에 offset을 더하여 이를 처리할 handler의 주소를 얻게 된다. 그리고 이 handler의 위치로 건너가서 해당하는 처리를 한 후 이를 마친후에 복귀하게 된다. 이 과정에서 privilege level이 바뀔 수도 있으며, 처리 중에는 현재 TSS에 있는 stack을 그대로 사용한다. interrupt gate와 trap gate를 통하는 경우의 차이는, interrupt gate를 통하는 경우 처리하는 동안 interrupt를 금지하고 (flag의 IF를 clear) 처리를 마치고 복귀할 때 다시 이를 복구하지만, trap gate를 통하는 경우에는 interrupt를 금지하지 않는다.

그림 6-1. Interrupt Gate와 Trap Gate (주13)

그림 6-2. Interrupt 발생시 Task Gate (주14)

7. 보호모드에서의 시스템콜

시스템콜은 사용자모드에서 커널모드에서 실행되는 코드를 부르기 위한 방법이다. 이를 위해서 위해서 보호모드에서는 call gate라는 것을 제공한다. call gate는 다른 privilege level 사이로 제어권을 넘기는 방법으로, segment selector와 offset을 가지고 있다. 이 selector는 GDT/LDT에 있는 segment descriptor를 가리키고, 여기서 얻어지는 base address에 offset을 더하여 실행할 함수의 위치를 얻게 된다. call이나 jmp 명령에 목적하는 call gate의 segment selector를 지정함으로써 call gate를 통하여 커널모드로 진입하게 된다. 이 때 CPL (current privilege level)과 call gate descriptor의 DPL, call gate selector의 RPL을 비교하여 권한을 검사한다. 시스템콜에 진입하였을 때에는 현재 task의 TSS에 있는 stack을 사용하며, ret 명령을 통해서 결과값과 함께 시스템콜을 부르기 이전의 상태로 되돌아가게 된다.

그림 7-1. Call Gate (주15)

출처 :
Posted by 쫑경