Windows NT 시스템 콜을 설명하는 대부분의 문서들은 중요한 부분들을 숨겨놓고 있다. 이로 인해 유저 모드 어플리케이션이 커널 모드로 진입하는 것을 이해하려고 할 때 혼돈이 생긴다. 다음 글은 시스템 서비스를 수행하기 위해 커널 모드로 전환하는 데에 Windows NT가 사용하는 정확한 메커니즘 밝힐 것이다. 이 설명은 보호 모드에서 동작하는 x86 호환 CPU에 대한 것이다. Windows NT가 지원하는 다른 플랫폼에서도 유사한 메커니즘이 사용될 것이다.
By John Gulbrandsen 8/19/2004
John.Gulbrandsen@SummitSoftConsulting.com
Translated into Korean by Snoya 10/01/2004
Homepage: system-inside.com
MSN: neo3k@hanmail.net
E-mail: neo3k@hanmail.net
커널 모드란 무엇인가?
대부분의 개발자(심지어 커널 모드 개발자도)들이 생각하는 것과 다르게, x86 CPU에는 "커널 모드"라고 불리는 모드가 없다. 모토롤라 68000과 같은 CPU는 CPU에 내장된 두 가지의 프로세서 모드를 가진다. 즉, 현재 user-mode 에서 실행되는지 supervisor-mode 에서 실행되는지를 나타내는 status 레지스터를 가진다. 인텔 x86 CPU는 그러한 플래그를 가지고 있지 않다. 대신에 수행 중인 프로그램의 권한을 결정하기 위한 현재 실행중인 코드 세그먼트의 특권 레벨이 있다. x86 CPU의 보호 모드에서 동작하는 어플리케이션의 각각의 코드 세그먼트는 세그먼트 디스크립터라고 하는 8바이트 크기의 데이터 구조에 의해 서술된다. 세그먼트 디스크립터는 세그먼트의 시작 주소와 세그먼트의 크기, 그리고 특권 레벨 등과 같은 정보를 가지고 있다. 특권 레벨 3을 가지는 코드 세그먼트에서 실행되는 코드는 유저 모드에서 실행된다고 말하고 특권 레벨 0을 가지는 코드 세그먼트에서 실행되는 코드는 커널 모드에서 실행된다고 말한다. 즉, 커널 모드(특권 레벨 0)과 유저 모드(특권 레벨 3)은 CPU가 아닌 코드의 속성이다. 인텔은 특권 레벨 0을 "링 0", 특권 레벨 3을 "링 3"이라고 부른다. x86 CPU에는 Windows NT는 사용하지 않는 두 개의 특권 레벨이 더 있다(링 1, 링 2). 특권 레벨 1,2가 사용되지 않는 이유는 Windows NT는 여러 다른 하드웨어 플랫폼에서 동작하도록 설계되어 있고 다른 CPU는 인텔 x86 CPU와 같이 4개의 특권 레벨을 가지고 있을지 없을지 모르기 때문이다.
x86 CPU는 낮은 특권 레벨(숫자는 더 높음)에서 실행되는 코드가 높은 특권 레벨(숫자는 더 낮음)에서 실행되는 코드를 호출하는 것음 금지한다. 만약 이것이 시도되면, 일반 보호(General Protection, GP) 예외가 CPU에 의해 자동으로 발생한다. 운영체제의 일반 보호 예외 핸들러가 호출되고 적절한 처리가 이루어질 것이다(사용자에게 경고, 어플리케이션 종료 등). 특권 레벨을 포함한 위에서 논의한 모든 메모리 보호 기능은 x86 CPU의 기능이지 Windows NT의 것이 아니라는 점에 유의하라. CPU의 지원없이는 Windows NT는 위와 같은 메모리 보호를 수행할 수 없다.
세그먼트 디스크립터는 어디에 있는가?
시스템에 존재하는 각각의 코드 세그먼트는 세그먼트 디스크립터에 의해 서술되기 때문에, 그리고 시스템에는 많은 코드 세그먼트들이 존재할 것이기 때문에, 세그먼트 디스크립터는 어딘가에 저장되어 있어야 할 것이고 프로그램이 어느 세그먼트의 코드를 실행하기를 원할 때 그것을 허용하거나 금지하기 위해 CPU는 그것을 읽을 수 있어야 한다. 인텔은 CPU 칩 자체에 그 모든 정보를 저장하는 것이 아닌 메인 메모리에 저장하기로 결정하였다. 메인 메모리에는 두 개의 테이블이 있다; 전역 디스크립터 테이블(Global Descriptor Table, GDT) 그리고 지역 디스크립터 테이블(Local Descriptor Table, LDT). 이들 디스크립터 테이블의 주소와 크기를 저장하기 위한 두 개의 레지스터가 CPU 안에 있다. 이 레지스터는 전역 디스크립터 테이블 레지스터(Global Descriptor Table Register, GDTR)과 지역 디스크립터 테이블 레지스터(Local Descriptor Table Register, LDTR)이다. 이들 디스크립터 테이블을 설치하고 GDTR, LDTR 레지스터에 GDT, LDT 의 주소를 적재하는 것은 운영체제의 책임이다. 이것은 부트 과정에서 매우 이른 시기에, 심지어 CPU가 보호 모드로 전환되기 전에 이루어져야 한다. 왜냐하면 디스크립터 테이블 없이는 보호 모드에서 메모리 세그먼트가 액세스될 수 없기 때문이다. 아래 그림 1은 GDTR, LDTR, GDT, LDT 사이의 관계를 나타낸다.

두 개의 세그먼트 디스크립터 테이블이 있기 때문에 세그먼트 디스크립터를 선택하기위해 인덱스만을 사용하는 것은 충분하지 않다. 둘 중 어느 테이블을 선택할 지를 결정하는 1 비트가 필요하다. 테이블 지시가 비트와 결합된 인덱스를 세그먼트 셀렉터라고 한다. 세그먼트 셀렉터의 형식은 아래와 같다.

위 그림 2와 같이, 세그먼트 셀렉터는 RPL(Requestor Privilege Level)이라고 하는 2 비트 필드를 포함한는데 그것은 이 셀렉터가 가리키는 코드 세그먼트 디스크립터에 접근할 수 있는지를 결정하는데 사용된다. 예를 들어, 특권 레벨 3(유저 모드) 코드가 셀렉터가 가리키는 코드 세그먼트 디스크립터에 의해 서술되는 코드 세그먼트의 코드를 호출하려고 할 때, 셀렉터의 RPL 특권 레벨 0에서 실행되는 코드만을 읽을 수 있다고 지시한다면 일반 보호 예외가 발생한다(?). 이렇게 x86 CPU는 링 3(유저 모드) 코드가 링 0(커널 모드) 코드에 접근하는 것을 막는다. 사실은 이것보다 약간 더 복잡하다. 보다 자세한 것을 알기 원하면 더 읽을거리 목록에 있는 "Protected Mode Software Architecture"의 RPL 부분을 참고하라. 우리의 목적을 위해서는 이정도만 알고 있으면 충분하다.
인터럽트 게이트
유저 모드에서 실행되는 어플리케이션 코드가 커널 모드 코드를 호출할 수 없다면 어떻게 Windows NT의 시스템 콜이 동작하는 것일까? 다시 해답은 CPU의 기능을 이용하는 것이다. 서로 다른 특권 레벨 사이에서의 제어 이행을 위해서 Windows NT는 인터럽트 게이트라고 하는 x86 CPU의 기능을 이용한다. 인터럽트 게이트를 이해하기 위해 우리는 먼저 x86 CPU 보호 모드에서 인터럽트가 어떻게 사용되는지 알아야 한다.
대부분의 다른 CPU처럼, x86 CPU 또한 각각의 인터럽트를 어떻게 처리할 것인지에 대한 정보를 포함하는 인터럽트 벡터 테이블을 가지고 있다. 리얼 모드에서 x86 CPU의 인터럽트 벡터 테이블은 단순히 인터럽트 서비스 루틴(Interrupt Service Routine, ISR)의 주소(4바이트 크기)를 가진다. 그러나 보호 모드에서 인터럽트 벡터 테이블은 8바이트 크기의 인터럽트 게이트 디스크립터를 가진다. 인터럽트 게이브 디스크립터는 어느 코드 세그먼트에 ISR이 존재하는지 그리고 그 코드 세그먼트에서 ISR은 어디에서 시작하는지에 대한 정보를 가지고 있다. 인터럽트 벡터 테이블이 단순한 포인터가 아닌 인터럽트 게이트 디스크립터를 가지는 이유는, 유저 모드 코드는 커널 모드 코드를 직접 호출할 수 없다는 법칙 때문이다. 인터럽트 게이트 디스크립터의 특권 레벨을 검사함으로써 CPU는 호출하는 어플리케이션이 명확한 위치에 있는 보호된 코드를 호출하는 것이 허용된다는 것을 확인한다(이것이 바로 "인터럽트 게이트"라는 이름이 붙은 이유이다. 즉, 그것은 유저 모드 코드가 커널 모드 코드로 제어를 이행할 수 있도록 하는 명확히 정의된 문이다).
인터럽트 게이트 디스크립터는 인터럽트 서비스 루틴을 가지고 있는 코드 세그먼트를 서술하는 코드 세그먼트 디스크립터를 가리키는 세그먼트 셀렉터를 포함한다. 우리의 Windows NT 시스템 콜의 경우, 세그먼트 셀렉터는 전역 디스크립터 테이블에 있는 코드 세그먼트 디스크립터를 가리킨다. 전역 디스크립터 테이블은 모든 "전역" 세그먼트 디스크립터를 가지고 있다. 즉, 시스템에서 실행중인 어느 특정 프로세서에 국한되는 것이 아니다(GDT는 운영체제의 코드와 데이터 세그먼트를 서술하는 세그먼트 디스크립터를 가진다). 그림 3은 'int 2e' 명령으로 인한 인터럽트 디스크립터 테이블과 전역 디스크립터 테이블 엔트리 그리고 목적지 코드 세그먼트에 있는 인터럽트 서비스 루틴 사이의 관계를 나타낸다.

NT 시스템 콜을 알아보자
어느 정도 배경 지식을 쌓았으므로 이제 Windows NT 시스템 콜이 어떻게 유저 모드에서 커널 모드로 전환하는지 정확하게 알아볼 준비가 되었다. Windows NT 에서의 시스템 콜은 "int 2e" 명령을 실행함으로써 시작된다. 'int' 명령어는 CPU로 하여금 소프트웨어 인터럽트를 발생하도록 한다. 즉, 인터럽트 디스크립터 테이블로 가서 인덱스 2e에 있는 인터럽트 게이트 디스크립터를 읽는다. 인터럽트 게이트 디스크립터는 ISR을 가지고 있는 코드 세그먼트를 가리키는 세그먼트 셀렉터를 포함한다. 또한 그 목표 코드 세그먼트 내의 ISR이 위치하는 오프셋도 포함한다. CPU는 GDT 또는 LDT 에 있는 엔트리를 결정하기 위해 세그먼트 셀렉터를 사용할 것이다. 일단 CPU가 목표 세그먼트 디스크립터에 있는 정보를 알게되면 그 정보를 CPU로 로드한다. 또한 인터럽트 게이트 디스크립터에 있는 오프셋 값을 EIP 레지스터에 로드한다. 이 시점에서 CPU는 커널 모드 코드 세그먼트에 있는 ISR을 실행할 준비가 거의 되었다.
CPU는 자동으로 커널 모드 스택으로 전환한다
CPU가 커널 모드 코드 세그먼트에 있는 ISR을 실행하기 전에, 커널 모드 스택으로 전환되어야 한다. 그 이유는, 커널 모드 코드는 커널 모드에서 실행되기 위한 충분한 공간이 필요하기 때문에 유저 모드 스택을 신용할 수가 없기 때문이다. 예를 들어, 멍청한 유저 모드 코드가 스택 포인터를 유효하지 않은 메모리 주소를 가리키도록 했을 때 'int 2e' 명령을 수행하면 커널 모드 함수가 그 스택 포인터를 사용할 때 시스템 충돌을 일으킬 것이다. 따라서 x86 보호 모드 환경에서 각각의 특권 레벨은 자신의 스택을 가진다. 인터럽트 게이트 디스크립터 등을 통해 더 높은 특권 레벨로의 함수 호출이 이루어질 때, CPU는 자동으로 유저 모드 프로그램의 SS, ESP, EFLAGS, CS, EIP 레지스터를 커널 모드 스택에 저장한다. Windows NT 시스템 서비스 디스패쳐 함수(KiSystemService)의 경우 유저 모드 코드가 'int 2e'를 호출하기 전에 스택에 넣은 인자에 접근할 필요가 있다. 규칙에 의해, 유저 모드 코드는 'int 2e'를 호출하기 전에 유저 모드 스택의 인자들을 가리키는 포인터를 EBX 레지스터에 적재해야 한다. 그러면 KiSystemService는 호출되는 시스템 함수가 필요로하는 만큼의 인자를 유저 모드 스택에 커널 모드 스택으로 복사한다. 그림 4는 이에 대하여 설명하고 있다.

어떤 시스템 호출을 우리는 호출하는가?
모든 Windows NT 시스템 콜은 커널 모드로 전환하기 위해 'int 2e' 소프트웨어 인터럽트를 사용한다. 그러면 유저 모드 코드는 어떻게 커널 모드 코드에게 어떤 시스템 함수가 실행되어야 하는지를 알릴까? 대답은 'int 2e' 가 호출되기 전에 EAX 레지스터에 인덱스가 적재되어야 한다는 것이다. 커널 모드 ISR은 EAX 레지스터를 보고 유저 모드에서 넘어온 모든 인자가 올바르다면 해당 함수를 호출한다. ISR에 의해 커널 모드 함수로 인자가 전달된다.
시스템 콜에서 돌아와서
일단 시스템 콜이 완료되면 IRET 명령에 의해 CPU는 자동으로 프로그램의 원래 레지스터 값을 복원한다. 커널 모드 스택에 저장된 레지스터 값들을 꺼내고 CPU는 'int 2e' 다음의 유저 모드 코드를 계속 실행한다.
실험
인터럽트 디스크립터 테이블 2e 번째 엔트리에 있는 인터럽트 게이트 디스크립터를 조사함으로써 우리는 CPU가 Windows NT 시스템 서비스 디스패쳐 루틴을 이 글에서 설명한 것처럼 발견하는 것을 확인할 수 있다. 이 문서의 샘플 파일에는 GDT, LDT, IDT 내의 디스크립터를 표시하는 WindDbg 확장 DLL을 포함한다.
예제 코드 다운로드: ProtMode.zip
이 확장 DLL은 'protmode.dll' (Protected Mode) 파일이다. kdextx86.dll 이 있는 디렉토리에 이 DLL을 복사한 후 WinDbg에서 ".load protmode.dll" 명령을 내리면 이 확장 DLL이 로드된다(역자 주: 보통, 경로는 다음과 같을 것이다. C:Program FilesDebugging Tools for Windowsw2kfre(또는 w2kchk, winxp)). 타겟 플랫폼에 연결되었다면 브레이크를 걸어라. 'int 2e'에 대한 IDT 디스크립터를 표시하는 명령은 "!descriptor IDT 2e" 이다. 이 명령은 다음과 같은 정보를 표시한다.
kd>!descriptor IDT 2e
------------------- Interrupt Gate Descriptor --------------------
IDT base = 0x80036400, Index = 0x2e, Descriptor @ 0x80036570
80036570 c0 62 08 00 00 ee 46 80
Segment is present, DPL = 3, System segment, 32-bit descriptor
Target code segment selector = 0x0008 (GDT Index = 1, RPL = 0)
Target code segment offset = 0x804662c0
------------------- Code Segment Descriptor --------------------
GDT base = 0x80036000, Index = 0x01, Descriptor @ 0x80036008
80036008 ff ff 00 00 00 9b cf 00
Segment size is in 4KB pages, 32-bit default operand and data size
Segment is present, DPL = 0, Not system segment, Code segment
Segment is not conforming, Segment is readable, Segment is accessed
Target code segment base address = 0x00000000
Target code segment size = 0x000fffff
'descriptor' 명령은 다음과 같은 정보를 파해쳐낸다:
- IDT 인덱스 2e 에 있는 디스크립터의 주소는 0x80036570 이다.
- 그 주소에 있는 디스크립터의 실제 값은 C0 62 08 00 00 EE 46 80 이다.
- 그 값은 다음을 의미한다:
- 인터럽트 게이트 디스크립터의 세그먼트 셀렉터에 의해 서술되는 코드 세그먼트 디스크립터를 포함하는 세그먼트가 존재한다.
- 최소한 특권 레벨 3에서 실행되는 코드도 이 인터럽트 게이트를 이용할 수 있다.
- 우리의 시스템 콜(2e)를 위한 인터럽트 핸들러를 가지고 있는 세그먼트는 GDT의 인덱스 1 에 위치한 세그먼트 디스크립터에 의해 서술된다.
- KiSystemService는 타겟 세그먼트 내의 오프셋 0x804662c0 에서 시작한다.
- 인터럽트 게이트 디스크립터의 세그먼트 셀렉터에 의해 서술되는 코드 세그먼트 디스크립터를 포함하는 세그먼트가 존재한다.
"!descriptor IDT 2e" 명령은 또한 GDT 인덱스 1 에 있는 타겟 코드 세그먼트 디스크립터를 덤프한다. 아래는 이 GDT 디스크립터에 대한 설명이다:
- GDT 인덱스 1 에 있는 코드 디스크립터의 주소는 0x80036008 이다.
- 그주소에 있는 디스크립터의 실제 값은 FF FF 00 00 00 9B CF 00 이다.
- 그 값은 다음을 의미한다:
- 단위는 4KB 페이지이다. 이것이 의미하는 것은, 크기 필드 값(0x000fffff)은 실제 크기를 얻기 위해 가상 메모리 페이지 크기(4096 바이트)로 곱해져야 한다는 것이다. 그 결과 4GB 크기가 된다. 이것이 바로 커널 모드 코드가 메모리의 커널 모드 영역뿐만 아니라 유저 모드 영역에도 접근할 수 있는 이유이다(역자 주: GDT 인덱스 3에 있는 유저 모드 세그먼트도 4GB의 크기를 가진다. 그러나 상위 2G의 커널 영역에는 접근할 수 없는데 그러한 보호는 세그멘테이션이 아닌 페이징 단계에서 보호하는 것으로 알고 있다. 따라서 크기가 4GB이기 때문에 전체 메모리 영역에 접근할 수 있다는 말은 문제가 있는 것 같다).
- 커널 모드 세그먼트이다(DPL=0).
- 이 세그먼트는 conforming 이 아니다. 더 읽을거리 목록 중 "Protected Mode Software Architecture" 를 참고하라.
- 이 세그먼트는 읽기 가능하다. 더 읽을거리 목록 중 "Protected Mode Software Architecture" 를 참고하라.
- 이 세그먼트는 accessed 되었다. 더 읽을거리 목록 중 "Protected Mode Software Architecture" 를 참고하라.
- 단위는 4KB 페이지이다. 이것이 의미하는 것은, 크기 필드 값(0x000fffff)은 실제 크기를 얻기 위해 가상 메모리 페이지 크기(4096 바이트)로 곱해져야 한다는 것이다. 그 결과 4GB 크기가 된다. 이것이 바로 커널 모드 코드가 메모리의 커널 모드 영역뿐만 아니라 유저 모드 영역에도 접근할 수 있는 이유이다(역자 주: GDT 인덱스 3에 있는 유저 모드 세그먼트도 4GB의 크기를 가진다. 그러나 상위 2G의 커널 영역에는 접근할 수 없는데 그러한 보호는 세그멘테이션이 아닌 페이징 단계에서 보호하는 것으로 알고 있다. 따라서 크기가 4GB이기 때문에 전체 메모리 영역에 접근할 수 있다는 말은 문제가 있는 것 같다).
ProtMode.dll WinDbg 확장 DLL을 빌드하기 위해서는, Visual Studio 6.0 으로 프로젝트를 연 후 빌드를 클릭하라. ProtMode.dll 과 같은 확장 DLL을 작성하는 방법은 "Debugging Tools for Windows" 에 들어있는 SDK을 참고하라(역자 주: WinDbg을 설치하면 도움말 파일이 설치되는 데 확장 DLL을 작성하는 데에도 도움이 될 뿐만 아니라 전체적으로 훌륭한 문서라고 생각된다. 기회가 된다면 한글로 번역하고픈 마음이다). 마이크로소프트에서 무료로 다운받을 수 있다.
더 읽을거리
인텔 x86 CPU 보호 모드에 대한 다음 두 훌륭한 자료:
1) "Intel Architecture Software Developers Manual, Volume 3 - System Programming Guide". Available from Intel's web site in PDF format.
2) "Protected Mode Software Architecture" by Tom Shanley. Available from Amazon.com (published by Addison Wesley).
반드시 읽어보야 할 x86 CPU 프로그래밍에 대한 자료:
1) Intel Architecture Software Developers Manual, Volume 1 - Basic Architecture.
2) Intel Architecture Software Developers Manual, Volume 2 - Instruction Set Reference Manual.
이들 책들은 인텔 웹 사이트에서 PDF 포맷으로 얻을 수 있다 (당신은 또한 볼륨 3을 제외한 두 권의 책을 하드카피로 무료로 얻을 수 있다. 그러나 볼륨 3은 PDF 포맷으로만 유효하다).
저자에 대하여
John Gulbrandsen 는 Summit Soft Consulting 의 설립자이며 회장이다. John 은 임베디드와 Windows 시스템 개발뿐만 아니라 Microprocessor-, digital-, analog- electronics 설계에도 정규 지식을 가지고 있다. John 은 1992년(Windows 3.0)부터 Windows를 프로그램해왔다. 그는 C++, C#, VB를 이용하여 Windows 어플리게이션과 웹 시스템을 작성하고 또한 SoftIce를 이용하여 Windows 커널 모드 디바이스 드라이버를 개발하고 디버깅한다.
To contact John drop him an email: John.Gulbrandsen@SummitSoftConsulting.com
Summit Soft Consulting에 대하여
Summit Soft Consulting 는 사우스 캘리포니아에 위치한 마이크로소프트 운영체제와 그 핵심 기술을 전문으로 하는 컨설팅 회사이다. 우리는 커널 모드와 NT internals 프로그래밍을 포함한 Windows 시스템 개발을 주로 다른다.
Summit Soft Consulting 방문하기: http://www.summitsoftconsulting.com/ Posted by Dual



