뭉근 : 느긋하게 타는 불

이번 포스트는 Windbg에서 프로세스 리스트를 볼 때 왜 유휴 프로세스(Idle Process)는 없는가? 에 대한 내용이다. Process Explorer로 보면 아래와 같이 Idle Process가 나오는데, 왜 프로세스 리스트를 직접 출력해보면 유휴 프로세스를 찾을 수 없는 것일까? 궁금하면 아래의 글을 읽어보기 바란다.



들어가기

윈도우는 시스템 프로세스 중 커널 영역에서만 동작하는 프로세스로 Idle Process(유휴 프로세스)와 System Process(시스템 프로세스)를 사용한다. 이 두 프로세스들은 유저 모드에서 실행되지 않기 때문에 커널 영역에서만 존재한다.   

System Process는 커널 모드 시스템 스레드(kernel-mode system thread)들을 포함한다. 이 스레드들은 오직 커널 주소 공간의 코드들만을 실행하여 커널 레벨에서만 동작한다.

Idle Process는 CPU의 각 프로세서의 유휴 상태를 나타내기 위한 Idle Thread(유휴 스레드)를 포함한다. 프로세서에 더 이상 실행 가능한 스레드가 없을 때, 윈도우는 해당 프로세서의 유휴 스레드를 실행한다. 이는 멀티프로세서 시스템에서 하나의 CPU가 실행될 때, 다른 CPU들이 실행할 스레드들을 가지지 않는 상황이 있기 때문이다. 따라서 Idle Thread는 프로세서의 수만큼 생성된다.

Idle Process와 Idle Thread들은 특수한 케이스로 EPROCESS / ETHREAD 구조체로 표현되지만 익스큐티브 오브젝트 관리자에 의해 생성되는 프로세스와 스레드 오브젝트들이 아니기 때문에 시스템의 프로세스 리스트에 포함되지 않는다[0].


---------------------------------------------------------------------------------

0: kd> !process 0 0

**** NT ACTIVE PROCESS DUMP ****

PROCESS fffffa800ccc2990

SessionId: none Cid: 0004 Peb: 00000000 ParentCid: 0000

DirBase: 00187000 ObjectTable: fffff8a000001740 HandleCount: 479.

Image: System

   

PROCESS fffffa800d2fcb30

SessionId: none Cid: 00e8 Peb: 7fffffde000 ParentCid: 0004

DirBase: 2495c000 ObjectTable: fffff8a0005c07d0 HandleCount: 30.

Image: smss.exe

   

… (생략)

   

PROCESS fffffa800f2b3b30

SessionId: 1 Cid: 0948 Peb: 7fffffd4000 ParentCid: 08d8

DirBase: 3091b000 ObjectTable: fffff8a0017fdea0 HandleCount: 134.

Image: vmtoolsd.exe

   

PROCESS fffffa800f2e6b30

SessionId: 1 Cid: 0954 Peb: 7efdf000 ParentCid: 08d8

DirBase: 30cce000 ObjectTable: fffff8a0018be990 HandleCount: 41.

Image: runonce.exe

---------------------------------------------------------------------------------


위의 결과를 보면 프로세스 리스트가 System Process부터 시작한다는 것을 알 수 있다. 물론 언급했던 것과 같이 Idle Process는 나오지 않는다.

커널은 프로세스 리스트를 유지하기 위해 이중 연결 리스트(double-linked list)를 사용한다. 프로세스들은 생성된 순서대로 이 이중 연결 리스트에 들어가고, 프로세스가 종료된다면 역시 이중 연결 리스트에서 제거된다.

따라서 !process 0 0 명령어를 사용하지 않고도 직접 이 이중 연결 리스트를 따라가 리스트에 있는 각각의 프로세스를 추출할 수 있다. (* 아마도 !process 명령어도 이 리스트를 따라가지 않나 싶다. 실제로 확인해보지는 않았다 :) )   

윈도우 커널은 프로세스 리스트의 헤드를 전역 변수로 관리하며, 이에 대한 심볼 정보를 PsActiveProcessHead로 공개하고 있다. 간단히 말해서 전역 변수인 PsActiveProcessHead를 시작으로 실행 중인 프로세스들의 정보에 접근한다.   

Windbg의 dq 명령을 통해 직접 순회하여 볼 수도 있지만, !list 명령을 통해 다음과 같이 각각의 리스트 요소에 대해 정보를 일괄적으로 얻을 수 있다. 아래의 !list 명령어는 리스트의 각 요소인 ActiveProcessLinks를 참조한다.


---------------------------------------------------------------------------------

0: kd> !list "-t nt!_EPROCESS.ActiveProcessLinks.Flink -e -x \"dd @$extret L1; dt nt!_EPROCESS @$extret ImageFileName\" poi(nt!PsActiveProcessHead)-0x188"

dd @$extret L1; dt nt!_EPROCESS @$extret ImageFileName

fffffa80`0ccc2990 00580003

+0x2e0 ImageFileName : [15] "System"

   

dd @$extret L1; dt nt!_EPROCESS @$extret ImageFileName

fffffa80`0d2fcb30 00580003

+0x2e0 ImageFileName : [15] "smss.exe"

   

… (생략)

   

dd @$extret L1; dt nt!_EPROCESS @$extret ImageFileName

fffffa80`0f2b3b30 00580003

+0x2e0 ImageFileName : [15] "vmtoolsd.exe"

   

dd @$extret L1; dt nt!_EPROCESS @$extret ImageFileName

fffffa80`0f2e6b30 00580003

+0x2e0 ImageFileName : [15] "runonce.exe"

---------------------------------------------------------------------------------

* 0x188은 Windows 7 x64 시스템의 ActiveProcessLinks 오프셋이다. 이는 버전마다 다를 수 있다.

  

!process 명령어를 사용했을 때와 마찬가지로 System Process는 나타나지만, Idle Process는 나타나지 않는다. 즉 Idle 프로세스는 프로세스의 이중 연결 리스트에 포함되지 않는다.

그렇다면 Idle Process는 어떻게 생성되길래 System Process도 포함되어 있는 프로세스 리스트에는 존재하지 않는건가?

   

Idle Process 찾아가기

일단 Idle Process의 존재부터 알아본다. 윈도우 인터널스[0]에 Idle Process로 접근하는 방법이 나와있지만, Windbg로 디버깅시에는 전역 심볼인 PsIdleProcess을 이용하여 더 간단하게 접근할 수 있다.

   

---------------------------------------------------------------------------------

0: kd> dt _EPROCESS poi(PsIdleProcess) ImageFileName ActiveProcessLinks

nt!_EPROCESS

+0x188 ActiveProcessLinks : _LIST_ENTRY [ 0x00000000`00000000 - 0x00000000`00000000 ]

+0x2e0 ImageFileName : [15] "Idle"

---------------------------------------------------------------------------------

   

이 Idle Process의 ActiveProcessLinks 필드를 살펴보면 Flink와 Blink가 모두 NULL로 처리되어 있음을 확인할 수 있다. 즉 Idle Process는 다른 프로세스들을 가리키지 않는다.

* KiInitialProcess 로도 접근할 수 있다. PsIdleProcess는 EPROCESS 구조체의 주소에 대한 주소를 가진 반면 KiInitialProcess는 _EPROCESS 구조체의 주소를 가진다.


Idle Process는 시스템에서 반드시 생성하는 정적인 데이터이기 때문에 전역 심볼로 관리된다. 생성 및 파괴를 반복하는 일반적인 프로세스와 다르게 한번 생성되면 시스템이 실행되는 동안 파괴되지 않는다. 그렇다면 System 프로세스는 어떨까? System 프로세스 또한 아래와 같이 전역 변수인 PsInitialSystemProcess로 관리된다.

   

---------------------------------------------------------------------------------

0: kd> dt _EPROCESS poi(PsInitialSystemProcess) ImageFileName

nt!_EPROCESS

+0x2e0 ImageFileName : [15] "System"

---------------------------------------------------------------------------------

   

사실 커널 디버깅이 아닌, 실제 커널 코드 코딩(ex. 드라이버)를 할 때에 PsInitialSystemProcess는 전역 변수로 export[1]되어 있어 프로그래머가 접근할 수 있다. 하지만 Idle Process의 디버깅 심볼인 PsIdleProcess는 MS에서 심볼 정보만 제공하기 때문에 커널 코드 작성 시에 사용할 수 없다. 사실 생각해보면 유휴 프로세스에 드라이버가 접근할 필요가 없다. 인터널스에서도 언급한대로, 유휴 프로세스는 유휴 스레드를 위한 프로세스로 내부 객체를 살펴보면 일반적인 프로세스들과 달리 대부분의 필드들이 사용할 필요가 없기 때문에 NULL(0)으로 채워져 있다.

또한 구현 측면에서 보면 인터널스에 업근된 대로, 초기 유휴 스레드(PsInitialSystemThread)와 유휴 프로세스(PsInitialSystemProcess) 구조체는 정적으로 할당돼 프로세스 관리자와 객체 관리자가 초기화되기 전에 시스템 부팅을 위해 사용된다. 그 이후 유휴 스레드 구조체는 추가 프로세서가 동작할 때 동적으로 할당된다.

그렇다면 위의 말이 진짜일까? 궁금해진다.

   

Idle Process 생성

디버깅 심볼 정보인 PsIdleProcess와 PsInitialSystemProcess가 사용되는 부분을 찾으면 아래와 같이 PspInitPhase0 함수에서 Idle Process와 System Process의 EPROCESS.ImageFileName에 각 프로세스 이름을 쓰는 것을 볼 수 있다. 다른 프로세스들은 파일 이름을 기반으로 프로세스 이름이 정해지는데 비해, 이 두 프로세스들은 특수한 프로세스로서 프로세스 이름이 정해진 커널 코드에 의해 정해진다.

   

* 0x16c는 윈도우 7 32비트 상에서 _EPROCESS.ImageFileName 필드의 오프셋이다.

   

PspInitPhase0 함수는 커널을 초기화할 때 실행되는 함수 중 하나로, 커널은 KiSystemStartup 함수를 시작으로 다음과 초기화 함수를 호출한다. PsActiveProcessHead를 참조하는 모든 코드를 살펴보면 이 PspInitPhase0 함수에서 아래와 같이 PsActiveProcessHead를 초기화하는 것을 볼 수 있다.

   

   

코드를 살펴보면 KeGetCurrentThread 함수를 이용하여 현재 쓰레드가 속한 프로세스를 얻는다. 프로세스의 몇몇 필드를 초기화한 후 전역 변수 PsIdleProcess에 해당 프로세스의 주소를 대입한다. EPROCESS 구조체 필드는 이게 다가 아닌데 나머지는 어디서 초기화하는 걸까? 또한 KeGetCurrentThread는 어떤 쓰레드를 얻어오는건가?

   

커널 초기 쓰레드의 생성

KiSystemStartup 함수부터 살펴보면 다음과 같은 순서로 커널 초기화 함수가 실행된다.

   

KiSystemStartup()

  - KiInitialThread를 스택에 저장

  - KiInitilizePcr()

  - KiIntializeKernel()

    - KiInitializeProcess()

    - KiInitializeThread()

    - InitBootProcessor()

      - PsInitSystem()

      - PspInitPhase0()

   

KiSystemStartup 함수는 전역 변수 KiInitialThread를 스택에 저장한다.

   

   

이후 코드에서 스택에 저장한 값을 커널 프로세서 관리 구조체(Kernel Processor Control Region)인 KPCR의 IdleThread에 그 값을 등록한다.

   

   

   

이후 KiInitializeKernel 함수를 실행한다. 이 때 스레드 초기화를 위해 앞서 스택에 저장한 KiInitialThread를 호출 인자로 넘겨준다. KiInitilizeKernel을 호출할 때 KiInitialProcess 또한 인자로 같이 넘긴다.

   


KiInitializeProcess 함수를 먼저 실행하여 Idle Process의 나머지 필드들을 초기화한다. 사실 이 함수의 내부를 보면 복잡한 초기화가 아니라 앞에서 0으로 채웠던 것처럼 다른 필드들도 NULL 값으로 채운다.

   

   

Idle Process의 필드를 초기화한 후, 시스템 스레드를 초기화하는 KiInitializeThread 함수를 실행한다.

   

   

이 함수에서는 크게 KeInitThread 함수와 KeStartThread 함수를 호출한다. KeInitThread 함수를 호출하는 코드를 찾아보면 일반적인 스레드를 생성할 때 호출하는 용도로도 사용되는 것을 알 수 있는데, 이 때 입력받는 인자가 다르다. 실제로 PspAllocateThread를 찾아가보면 아래와 같이 인자가 일반적인 스레드와 다르게 들어간다는 것을 알 수 있다.

   

   

   

다시 KiInitilizeThread 함수로 돌아와 생각해보면 이 함수가 호출될 때 입력 받는 인자는 Idle Thread와 Idle Process이다. KiInitializeThread 함수에서 실행되는 KeInitThread 함수는 다음 코드와 같이 Idle Thread가 속한 프로세스를 Idle Process라고 명시한다.

   

   

앞서 커널의 첫 쓰레드는 언제 어떻게 생성되어 이 스레드로부터 Idle Process를 어떻게 얻어올 수 있는지 궁금했었는데 이제 해결되었다. KeGetCurrentThread 함수를 호출하기 전에 첫번째로 KPCR.Prcb.CurrentThread에KiInitialThread (Idle Thread)를 등록하고 KiInitializeThread 함수에서 Idle Thread가 속한 프로세스로 Idle Process를 등록하는 것이다.

   

프로세스 리스트에 Idle Process가 포함되지 않는 이유

IdleProcess는 PspCreateProcess에 의해 생성되지 않기 때문에 PsActiveProcessHead의 리스트에 등록되지 않는다. 물론 다음 코드처럼 System 프로세스부터는 PspCreateProcess에 의해 생성되고, 이 PspCreateProcess 함수 내부에서 PsActiveProcessHead를 참조하여 프로세스 리스트에 새로 생성되는 프로세스를 등록한다.

   

   

PspCreateProcess 함수의 내부에서 호출하는 PspInsertProcess 함수는 다음 코드와 같이 이중 연결 리스트의 마지막에 새로운 프로세스를 추가한다.

   

   

이 때까지의 Idle Process를 초기화하는 과정을 정리하면 아래와 같다.

   

   

결론

Idle Process는 커널에 _EPROCESS 구조체 자체, 즉 C언어의 변수 선언으로 "EPROCESS KiInitialProcess"처럼 정적으로 존재하기 때문에 PspCreateProcess와 같은 프로세스 할당 함수를 사용하지 않는다. 따라서 PsActiveProcessHead로부터 얻을 수 있는 프로세스 리스트에는 Idle Process가 없다. 더욱이 Idle Process는 다른 여타 프로세스와 달리 실행 파일이 없기 때문에 커널 코드 내에서 프로세스 이름이 정해진다. (System Process 또한 커널 코드 내에서 프로세스 이름이 정해진다. )

   

참조

[0] Windows Internals, 마크 러시노비치 등, 5장, p616

[1] https://msdn.microsoft.com/en-us/library/windows/hardware/ff559943(v=vs.85).aspx

   

커널 함수에서 사용하는 구조체 분석에 사용된 문서들

http://www.nirsoft.net/kernel_struct/vista/LOADER_PARAMETER_BLOCK.html

https://translate.google.co.kr/translate?hl=ko&sl=ru&tl=en&u=http%3A%2F%2Fhex.pp.ua%2Fntstatus%2Fpage024.php

http://www.osronline.com/showThread.cfm?link=3787

http://doxygen.reactos.org/d3/d0c/ntoskrnl_2include_2internal_2powerpc_2ke_8h_a88a282c034ffdbc59b89bab1b67676bb.html

http://www.osronline.com/showThread.cfm?link=72000

   

이외에 커널 분석에 도움될만한 내용

이번에 커널 코드를 살펴보면서 _LIST_ENTRY 구조체의 초기화 방식을 알게 되었다. 대부분이 자기 자신을 가리키도록 해놓았다. 이렇게 해놓으면 리스트 엔트리가 초기화 되었는지 되지 않았는지를 바로 파악할 수 있구나 생각하였다.

   

   

따라서 커널 구조체 분석시 LIST_ENTRY 구조체로 이루어진 필드 멤버가 자기 자신을 가리킨다면 별 다른 의미를 가진 것이 아니라 단순히 초기화된 상태라는 것이다.

'디버깅' 카테고리의 다른 글

MS 심볼 구성에 관하여  (0) 2014.12.17
Windows 10 _KPCR and _KPRCB  (0) 2014.11.14
8바이트 변수 초기화 문제  (0) 2014.11.14
Windbg에서 KDBG 찾기  (0) 2014.06.19
Window8 Remote Kernel Debugging on Vmware using WinDbg  (0) 2014.04.11