출처: http://www.jigi.net/2281?category=0
Chapter 2. Kernel Object
Copyright© 1998 by chpark95
Without my agreement, You can’t use this material for commercial purpose.
WHAT IS KERNEL OBJECT? 2
USAGE COUNTER 2
SECURITY 3
A PROCESS’S KERNEL OBJECT HANDLE TABLE 4
CREATING A KERNEL OBJECT 4
CLOSING A KERNEL OBJECT 5
SHARING KERNEL OBJECTS ACROSS PROCESS BOUNDARIES 6
OBJECT HANDLE INHERITANCE 6
CHANGING THE HANDLES FLAGS 8
NAMED OBJECTS 8
DUPLICATING OBJECT HANDLES 10
윈도우 시스템에 대한 이해는 일단 커널 오브젝트(Kernel Object)를 이해하는 것에서부터 출발한다. 이 장에서는 각각의 커널 오브젝트를 그리 상세히 다루지는 않는다. 대신 커널 오브젝트의 개념을 다룸으로써 윈도우 시스템의 전반적인 이해를 돕도록 하겠다.
What is Kernel Object?
커널 오브젝트란 무엇인가를 논하기 전에 커널 오브젝트에는 무엇이 있는가를 먼저 살펴보겠다.
1. Event Objects
2. File-mapping objects
3. File objects
4. Mailslot objects
5. Mutex Objects
6. Pipe Objects
7. Process Objects
8. Semaphore Objects
9. Thread Objects
위에 있는 것들을 통칭하여 커널 오브젝트라고 한다. 커널 오브젝트는 커널에 의하여 관리되며 System-wide하게 존재한다. 각 커널 오브젝트의 실상은 사실 커널 메모리내에 존재하는 구조체라고 볼 수 있다. 예를 들어 파일 오브젝트를 생각해보자. 파일이라는 것은 원래 자신의 시스템 상에 오직 하나만 존재하며(같은 이름의 파일이 있으면 어떻해요?.. 라든가 이런류의 질문은 하지 말기를..) 만약 사용자가 CreateFile() 등의 함수를 이용하여 파일 오브젝트를 생성하게 되면 커널안에 파일에 관련된 구조체가 생성되고 시스템 전체에서 오직 하나만 관리되게 된다. 따라서 하나의 파일 오브젝트는 여러 프로세스에서 공유될 수 있다. 또한 모든 커널 오브젝트는 비록 구조체 형태로 존재하기는 하지만 사용자가 바로 이 구조체에 접근하여 필드값들을 바꿀 수는 없다. 대신 바로 Win32 API를 이용하여 이 구조체에 접근하여 값을 읽고 설정할 수 있다. 만약 여러분이 OS를 배웠다면 OS에서의 시스템 콜에 대한 개념에 대하여 알고 있을 것이며 위의 이야기와 시스템 콜의 개념이 매우 비슷하다는 것을 알 수 있을 것이다. 윈도우 커널은 생성된 커널 오브젝트에 대한 프로세스내에서만 통용되는 DWORD(32-bit)값의 커널 오브젝트 ID를 사용자에게 리턴하게 되고 사용자는 이 핸들(통상 이런걸 핸들이라고 부른다. 윈도우 핸들, 파일 핸들..)과 Win32 API를 이용하여 커널 오브젝트를 manage하게 된다.
Usage Counter
커널 오브젝트는 그 커널 오브젝트를 생성한 프로세스가 아닌 커널 자체에 포함된다.(무슨말인지.. 헷갈릴 것이다.) 예를 들어 A라는 프로세스가 Kernel-A라는 커널 오브젝트를 생성했다고 하자. 만약 A가 작업을 끝내고 죽는다면 Kernel-A라는 커널 오브젝트로 죽게될까? 답은 그럴수도 있고 아닐수도 있다이다. 만약 A프로세스가 생성한 Kernel-A 오브젝트를 B라는 프로세스에서 사용하고 있다면 A프로세스가 죽어도 Kernel-A 오브젝트는 계속 커널 위의 메모리에 남게 될 것이다. 커널은 이러한 관리를 위하여 Usage Count라는 것을 생성된 커널 오브젝트마다 유지하고 있다.(흠.. 이부분은 만약 COM을 공부하는 분이 있다면.. 쉽게 이해할 것이다.)
Security
커널 오브젝트는 기본적으로 보안 항목(Security Descriptor)을 가지고 있다. 예를 들어 자신을 생성한 것은 누구이며, 누가 커널 오브젝트에 접근할 수 있나등등.. 이 부분은 주로 서버 프로그램에서 주로 사용하는 기능이며, 따라서 윈도우95/98에서는 해당사항이 없다. 커널 오브젝트를 생성하는 거의 모든 Win32 API들은 SECURITY_ATTRIBUTES 라는 매개변수 필드를 가지고 있다. 물론 윈도우95/98의 커널 오브젝트 생성 Win32 API에도 이 매개변수 필드는 있다.(당연하지.. 같은 Win32 API를 쓰는데.. 전에도 이야기했지만 Win32 API는 윈도우 95/98/NT/CE/2000에서 공통으로 쓰이는 프로그래밍 인터페이스다.) 그러나 실제로 이 필드에 어떠한 값을 넣어도 윈도우 95/98은 이 값을 무시한다. 다시 본론으로 돌아가서 하나 예를 들어보자. 다음의 CreateFileMapping이라는 함수는 Memory-Mapped File을 생성할때 쓰이는 함수이다. 여하튼 이 함수의 원형을 보면,
HANDLE CreateFileMapping( HANDLE hFile,
LPSECURITY_ATTRIBUTES lpFileMappingAttributes,
DWORD flProtect,
DWORD dwMaximumSizeHigh,
DWORD dwMaximumSizeLow,
LPCTSTR lpName );
두번째 파라미터가 바로 이 커널 오브젝트에 보안을 설정하는 파라미터이다. 대부분의 경우는 이 부분에 NULL값을 대입한다. 그렇게 되면 기본값이 설정된다. 기본 설정은 이 커널 오브젝트를 만든 사용자(시스템 로그온한 아이디로 구별)에게 최대한의 접근을 허용하고 다른 사용자는 접근거부하는 것이다. 그럼 여기서 SECURITY_ATTRIBUTES 구조체를 살펴보도록하자.
typedef struct _SECURITY_ATTRIBUTES {
DWORD nLength;
LPVOID lpSecurityDescriptor;
BOOL bInheritHandle;
} SECURITY_ATTRIBUTES;
nLength는 이 구조체의 크기를 나타내는 것이며, 세번째의 bInheritHandle은 뒤에 나오니 설명 생략한다. 마지막으로 가운데 값이 바로 Security Descriptor에 대한 포인터값이 된다. 예제 코드를 들자면
SECURITY_ATTRIBUTES sa;
sa.nLength = sizeof(sa);
sa.lpSecurityDescriptor = pSD;
sa.bInheritHandle = FALSE;
HANDLE hFileMapping = CreateFileMapping((HANDLE)0xffffffff, &sa, PAGE_READWRITE, 0, 1024, “MyFileMapping”);
커널 오브젝트에 덧붙여서, 커서, 마우스 포인터, 아이콘, GDI Object(font, brush, window..)등의 오브젝트들도 존재한다. 이러한 오브젝트들도 System-wide하게 존재하며 커널이 관리한다. 그러나 이것들을 커널 오브젝트라고 부르지는 않는다. 위에 나열한 것들은 통상 User Object, GDI Object라고 부른다. 그렇다면 커널 오브젝트와 사용자 오브젝트(흠.. 말이 길어서 줄여씀..)의 차이점은 무엇일까? 그것은 바로 사용자 오브젝트에는 보안 설정이 존재하지 않는다는 것이다. 예를 들어 아이콘 같은 경우는 누가 가져다 쓰던 문제가 되지 않을 것이다. 따라서 사용자 오브젝트를 생성하는 함수에는 보안 필드가 빠져있다.
A Process’s Kernel Object Handle Table
다음에 나오는 내용은 제프리가 자기 예상에 윈도우 커널에는 분명히 이런 구조가 있을 것이다라고 예상하고 쓴 것이므로 믿지 말기 바란다. 이 내용 자체가 MS에서만 알고 있는 비밀이고 따라서 어떠한 문서도 없으므로 아래의 내용이 사실이라고 확인 할 수가 없다. 그러니 제프리의 말처럼 이 내용을 윈도우 시스템이 이렇게 되어 있구나 하고 배울 생각으로 보지말고 같은 프로그래머의 입장에서 같이 생각해보는 입장으로 보기를 바란다.
일단 맨 처음 프로세스가 생성되면 다음과 같은 핸들 테이블을 생성할 것이다.
index 커널 오브젝트에 대단 메모리 블록 주소 Access Mask Flags
1 0x???????? 0x???????? 0x????????
2 0x???????? 0x???????? 0x????????
… … … …
Creating a Kernel Object
처음 프로세스가 생성되면 위의 핸들테이블은 빈 상태가 된다. 만약 프로세스에서 처음으로 커널 오브젝트를 생성하게되면 (예를 들어 CreateFileMapping() 등의 함수를 호출하여서..) 커널은 일단 프로세스의 핸들 테이블에서 빈 자리를 찾게되고, 지금의 경우는 프로세스가 생성된 후, 처음 커널 오브젝트를 생성하는 것이므로 맨 처음 인덱스에 해당되는 커널 오브젝트 구조체(앞에서 이야기했음)의 시작주소를 넣고, Access 필드와 Inheritance 필드를 채우게 된다. 다시 말하지만 커널 오브젝트 구조체는 커널 내부에 존재하게 된다. 다음의 함수들은 여러 종류의 커널 오브젝트를 만드는 함수들이다.
HANDLE CreateThread(LPSECURITY_ATTRIBUTES lpThreadAttributes, DWORD dwStackSize, LPTHREAD_START_ROUTINE lpStartAddress, LPVOID lpParameter, DWORD dwCreationFlags, LPDWORD lpThreadId);
HANDLE CreateFile(LPCTSTR lpFileName, DWORD dwDesiredAccess, DWORD dwShareMode, LPSECURITY_ATTRIBUTES lpSecurityAttributes, DWORD dwCreationDistribution, DWORD dwFlagsAndAttributes, HANDLE hTemplateFile);
HANDLE CreateFileMapping(HANDLE hFile, LPSECURITY_ATTRIBUTES lpFileMappingAttributes, DWORD flProtect, DWORD dwMaximumSizeHigh, DWORD dwMaximumSizeLow, LPCTSTR lpName);
HANDLE CreateSemaphore(LPSECURITY_ATTRIBUTES lpSemaphoreAttributes, LONG lInitialCount, LONG lMaximumCount, LPCTSTR lpName);
위의 함수들은 전부 다 커널 오브젝트에 대한 핸들값을 리턴한다. 실제 이 핸들값들은 핸들 테이블에서의 커널 오브젝트의 인덱스 값이라고 볼 수 있다.(그러나 이 핸들값이라는 것이 MS에 의하여 공식적으로 설명된 적이 없기 때문에 꼭 그렇다고 볼 수는 없다.) 그리고 진짜 중요한 것은 이 커널 오브젝트에 대한 핸들값은 process-relative 하다는 것이다. 이게 왜 중요하냐하면 이 커널 오브젝트에 대한 핸들값이 process-relative 하기 때문에 절대로 이 프로세스 외부의 다른 프로세스에서 이 핸들값으로 커널 오브젝트를 접근할 수 없다는 것이다. 커널 오브젝트는 커널 레벨에서 존재하며 커널이 관리한다. 따라서 system-wide하게 단 하나만 존재한다. 이것은 다른 프로세스에서도 이 커널 오브젝트를 사용할 수 있다는 것이 된다. 그러나 다른 프로세스에서 생성한 커널 오브젝트에 접근하기 위해서는 그냥 커널 오브젝트 핸들값을 넘겨주는 등의 작업과는 다른 작업이 이루어져야한다. 그 이유는 앞에서 이야기한대로 커널 오브젝트에 대한 핸들값이 process-relative하기 때문이다.
Closing a Kernel Object
프로세스 내에서 생성한 커널 오브젝트를 시스템에 반환하는 함수는 다음과 같다.
BOOL CloseHandle(HANDLE hobj);
여기서 또 주의할 점은 이렇게 커널 오브젝트에 대한 핸들을 닫아준다고 해서 커널 오브젝트가 없어지는 것은 아니라는 것이다. 실상 위의 함수를 이용해 핸들을 닫아주면 일단 커널은 그 프로세스의 핸들 테이블에서 해당하는 핸들값이 있는 인덱스를 클리어시킨다. 그리고 나서 그 커널 오브젝트에 대한 Usage Count를 살펴보고, 만약 0이라면 커널에서 삭제하고 아니라면 그냥 살려둔다. 한번 CloseHandle을 하게되면 그 순간부터 닫힌 커널 오브젝트는 그 프로세스내에서 Invalid 하다.
만약 프로그래머가 잘못하여 핸들을 닫지 않았다고 생각해보자. 그렇다고 하더라도 그 프로세스가 죽을때가 되면 커널이 알아서 커널 오브젝트에 대한 Usage Count를 내려주게 된다. 그러나 그 프로세스가 실행되고 있는 동안에는 닫지 않은 커널 오브젝트에 대한 리소스는 낭비되고 있는게 사실이다. 따라서 왠만하며 사용이 끝난 커널 오브젝트는 그 즉시 닫아 주는 것이 좋다.
Sharing Kernel Objects Across Process Boundaries
앞에서도 이야기하였지만 커널 오브젝트는 system-wide하게 유지되는 것이며 따라서 그 커널 오브젝트를 만든 프로세스내의 쓰레드 뿐만 아니라 다른 프로세스의 쓰레드에 의해서도 접근가능해야한다. 다음의 예는 커널 오브젝트가 프로세스 사이에 공유되는 경우에 대한 예들이다.
1. File-mapping 객체는 두개의 프로세스 사이에 특정 데이터 블록을 공유할 때 쓰일 수 있다.
2. Named pipes 같은 경우는 서로 다른 머신(네트워크로 연결된)사이에 일정한 데이터 블록을 전송할 때 쓰일 수 있다.
3. Mutex, semaphores, event 들은 다른 프로세스내의 쓰레드들 사이에 동기화가 필요할 때 쓰일 수 있다.
위의 경우중 3번째 경우는 자바의 synchronized와 같은 경우일 것이다.
그러나 문제는 커널 오브젝트에 대한 핸들이 process-relative하다는 것이 문제이다. 어떠한 방법으로 커널 오브젝트에 대한 핸들을 다른 프로세스에게 넘겨준다 하더라도 그 값은 그것을 생성한 프로세스내에서만 의미가 있기 때문에 넘겨받은 프로세스로서는 그것을 가지고 그 커널 오브젝트에 접근할 방법이 없다. “공유하는 경우가 많은데, 프로세스 사이에 커널 오브젝트 핸들을 공유못하게 한다면 뭔가 잘못되지 않았는가?” 뭐, 이렇게 반문할 수도 있다. MS쪽에서 이런식으로 커널 오브젝트를 설계한 이유는 다음의 두가지이다. 첫째는 보안문제이며, 두번째는 시스템의 성능때문이다. 뭐, 이것들에 대한 설명은 책에도 자세히 있는바, 어차피 지금 우리가 당면한 문제는 그냥 커널 오브젝트 핸들을 넘겨주는 것으로는 공유가 안된다는 것이고, 그렇다면 공유하는 방법이 있을 것이 분명하다. 다음의 방법들을 잘 읽어보고 그 방법을 터득하기 바란다.(노파심에서 이야기지만, 지금은 이게 뜬구름 잡는 것처럼 들릴지도 모른다. 그러나, 아주 많이 쓰인다. 꼭 알아두길..)
Object Handle Inheritance
이 방법은 한 프로세스가 자신의 자식 프로세스를 spawn 할때만 가능한 방법이다. 우리가 커널 오브젝트 생성 함수들을 볼 때 파라미터로 넘겨주는 SECURITY_ATTRIBUTES 구조체에 bInheritHandle 이라는 필드가 끼어 있는 것을 본 것이 기억날 것이다. 바로 이 부분에 TRUE를 넣어주게 되면 이 값을 파라미터로 받아서 생성된 커널 오브젝트는 그 자식 프로세스에게 물려줄 수 있는 상태가 된다. 뭐, 좀 자세히 예를 들어서 설명하면,
SECUTIRY_ATTRIBUTES sa;
sa.nLength = sizeof(sa);
sa.lpSecurityDescriptor = NULL;
sa.bInheritHandle = TRUE;
HANDLE hMutex = CreateMutex(&sa, FALSE, NULL);
.
뭐, 이렇게 Mutex를 생성했다고 하자. 그렇게 되면 일단 이 커널 오브젝트를 생성한 프로세스의 핸들 테이블은 다음과 같은 상태가 될 것이다.
index 커널 오브젝트에 대단 메모리 블록 주소 Access Mask Flags
1 0x???????? 0x???????? 0x????????
2 0xF0000000 0x???????? 0x00000001
… … … …
위의 표에서 두번째 행에 바로 이 Mutex에 대한 정보가 들어가게 될 것이다. 두번째 행의 맨 끝 열을 보면 플래그가 1로 세팅되어 있는 것을 볼 수 있다. 이것은 위에서 우리가 bInheritHandle을 TRUE로 세팅했기 때문이다. 흠.. 일단 이상태에서 이 프로세스가 자신의 자식 프로세스를 생성한다고 해보자. 자식 프로세스를 생성하는 함수는 다음과 같다. (너무 많은 파라미터를 보고 놀라지 말기를..)
BOOL CreateProcess(LPCTSTR lpApplicationName, LPTSTR lpCommandLine, LPSECURITY_ATTRIBUTES lpProcessAttributes, LPSECURITY_ATTRIBUTES lpThreadAttributes, BOOL bInheritHandles, DWORD dwCreationFlags, LPVOID lpEnvironment, LPCTSTR lpCurrentDirectory, LPSTARTUPINFO lpStartupInfo, LPPROCESS_INFORMATION lpProcessInformation);
위의 파라미터 중 bInheritHandles라는 파라미터를 TRUE로 세팅하게 되면 그때부터 이 자식 프로세스는 자기 부모 프로세스의 커널 오브젝트 핸들 중 bInheritHandle(부모 프로세스의 핸들테이블에서..)가 TRUE인 것을 물려받게 된다. 좀 자세히 설명하면 일단 위의 함수를 실행하면 커널은 프로세스를 만들고 그 프로세스에 대한 핸들 테이블도 생성한다. 그 다음 만약 bInheritHandles 파라미터가 TRUE라면 커널은 일단 부모 프로세스의 핸들 테이블을 뒤져서 세번째 열이 TRUE인 인덱스에 있는 커널 오브젝트들을 그대로 카피해서 자식 프로세스의 핸들테이블에 집어 넣는다. 여기서 중요한 것은 만약 부모 프로세스에 2번째 행에 있었으면 자식 프로세스의 핸들 테이블에도 2번째행에 들어간다. 진짜 카피하는 것이다. 이렇게 되면 그때부터 자식 프로세스도 그 커널 오브젝트를 사용할 수 있게 된다. 그러나 이방법에 한가지 문제가 있다. 생성된 자식 프로세스가 어떠한 방법으로든 자신이 어떤 커널 오브젝트의 핸들을 물려받았으며, 그 핸들값이 무엇인지를 알아야한다는 것이다. 다시 한번 이야기하지만 자식 프로세스 핸들 테이블에 카피 된 것은 카피 된것이고 자식 프로세스가 이 커널 오브젝트를 사용하기 위해서는 그 핸들값을 알아야한다.(사실 정확히 이야기하면 그 핸들값이라는 것이 바로 핸들 테이블에서의 인덱스 값이다…) 이 문제는 일단 자식과 부모사이에, 그러니까 프로그래머가 프로그램을 만들 때, 두 프로그램을 전부다 알고 있어야한다는 것이다. 만약 프로그래머가 뻔히 “이 프로그램이 이놈을 생성할때는 이런 핸들값을 넘긴다” 알고 있을 때만 가능하다는 것이다. 만약 그렇다면 뭐, 문제야 간단하다. 그냥 생성될 때 코맨드라인으로 핸들값만 넘기면 된다. 결론을 이야기하자면 이 방법은 별로 안쓴다.(^.^)
Changing the Handles Flags
이건 생략.. 복잡하고, 잘 쓰지 않는다.
Named Objects
가장 많이 쓰는 방법으로서 커널 오브젝트를 생성할 때 이름을 준후 다른 프로세스에서 같은 이름으로 커널 오브젝트를 찾으면 있는 경우는 그 커널 오브젝트를 넘겨주는 방법이다.(흠.. 한마디로 쉽다..) 다음과 같은 커널 오브젝트 생성함수를 보도록 하자.
HANDLE CreateFileMapping(HANDLE hFile, LPSECURITY_ATTRIBUTES lpFileMappingAttributes, DWORD flProtect, DWORD dwMaximumSizeHigh, DWORD dwMaximumSizeLow, LPCTSTR lpName);
HANDLE CreateSemaphore(LPSECURITY_ATTRIBUTES lpSemaphoreAttributes, LONG lInitialCount, LONG lMaximumCount, LPCTSTR lpName);
HANDLE CreateEvent(LPSECURITY_ATTRIBUTES lpEventAttributes, BOOL lInitialCount, LONG lMaximumCount, LPCTSTR lpszName);
HANDLE CreateWaitableTimer(LPSECURITY_ATTRIBUTES lpTimerAttributes, BOOL bManualReset, LPCTSTR lpszName);
HANDLE CreateMutex(LPSECURITY_ATTRIBUTES lpMutexAttributes, BOOL bInitialOwner, LPCTSTR lpszName);
위 함수들의 공통점은 모두 다 마지막에 lpszName 파라미터를 가지고 있다는 것이다. 이것이 바로 이 커널 오브젝트의 이름이 된다. 물론 이 값에 NULL을 줄수도 있지만 만약 이 커널 오브젝트 system-wide하게 공유되길 바란다면 이 부분에 이름을 주어야 한다. 다음과 같은 예를 들어보자.
HANDLE hMutexA = CreateMutex(NULL, FALSE, “chparkmutex”);
이 다름에 다른 프로세스에서 다음과 같이 한다면
HANDLE hMutexB = CreateMutex(NULL, FALSE, “chparkmutex”);
이 함수를 실행시키면 일단 커널은 커널에 chparkmutex라는 이름의 커널 오브젝트가 있는지 검사한다. 만약 있다면 그 다음에는 타입을 검사한다. 다행이도 이 경우는 두가지 다 Mutex를 만든 것이므로 타입도 같다. 그렇게 되면 커널은 새로 커널 오브젝트를 만드는 것이 아니라 기존에 있던 chparkmutex라는 이름을 가진 Mutex 오브젝트의 핸들값을 리턴하고 단지 이 커널 오브젝트의 Usage Count를 하나 증가시킨다. 그러나 만약 다음과 같이 한다면
HANDLE hSemB = CreateSemaphore(NULL, 1, 1, “chparkmutex”);
이 코드를 실행시키면 일단 같은 이름을 가진 커널 오브젝트가 있는 것 까지는 괜챦지만, 타입 체크시 에러가 난다.(당연하겠지..)
그러나 이 방법에도 맹점은 존재한다. 무엇인고 하니 뭐, 타이핑을 잘못하여 이름이 조금 틀려서 chparkmutec이라고 쳤다고해보자. 그러면 일단 커널이 볼때는 이런 이름을 가진 커널 오브젝트가 없으니 새로 하나 만들어서 그 핸들 값을 리턴할 것이다. 이는 우리가 원하는 일이 아니다. 그러나 에러가 안나니 제대로 되는지 전혀 알 수 없다. 이러한 것을 막기 위한 함수들이 존재하는데 다음과 같다.
HANDLE OpenMutex(DWORD dwDesiredAccess, BOOL bInheritHandle, LPCTSTR lpszName);
HANDLE OpenEvent(DWORD dwDesiredAccess, BOOL bInheritHandle, LPCTSTR lpszName);
HANDLE OpenSemaphore(DWORD dwDesiredAccess, BOOL bInheritHandle, LPCTSTR lpszName);
HANDLE OpenWaitableTimer(DWORD dwDesiredAccess, BOOL bInheritHandle, LPCTSTR lpszName);
HANDLE OpenFileMapping(DWORD dwDesiredAccess, BOOL bInheritHandle, LPCTSTR lpszName);
이 함수들을 쓰게 되면 만약 같은 이름을 가진 커널 오브젝트가 없다면 에러를 내게된다. 따라서 함수의 수행이 제대로 되는지 정확히 판단 할 수 있게된다. 결국은 만들어놓은 커널 오브젝트를 얻고자 한다면 Create.. 보다는 Open..을 써야한다는 것이다.
Duplicating Object Handles
어~~ 머리아파.. 뭐, 위에 이름을 이용한 방법이 최고로 많이 쓰인다.. 나머지는 읽어봐.. 나도 머리아파..
Chapter 2. Kernel Object
Copyright© 1998 by chpark95
Without my agreement, You can’t use this material for commercial purpose.
WHAT IS KERNEL OBJECT? 2
USAGE COUNTER 2
SECURITY 3
A PROCESS’S KERNEL OBJECT HANDLE TABLE 4
CREATING A KERNEL OBJECT 4
CLOSING A KERNEL OBJECT 5
SHARING KERNEL OBJECTS ACROSS PROCESS BOUNDARIES 6
OBJECT HANDLE INHERITANCE 6
CHANGING THE HANDLES FLAGS 8
NAMED OBJECTS 8
DUPLICATING OBJECT HANDLES 10
윈도우 시스템에 대한 이해는 일단 커널 오브젝트(Kernel Object)를 이해하는 것에서부터 출발한다. 이 장에서는 각각의 커널 오브젝트를 그리 상세히 다루지는 않는다. 대신 커널 오브젝트의 개념을 다룸으로써 윈도우 시스템의 전반적인 이해를 돕도록 하겠다.
What is Kernel Object?
커널 오브젝트란 무엇인가를 논하기 전에 커널 오브젝트에는 무엇이 있는가를 먼저 살펴보겠다.
1. Event Objects
2. File-mapping objects
3. File objects
4. Mailslot objects
5. Mutex Objects
6. Pipe Objects
7. Process Objects
8. Semaphore Objects
9. Thread Objects
위에 있는 것들을 통칭하여 커널 오브젝트라고 한다. 커널 오브젝트는 커널에 의하여 관리되며 System-wide하게 존재한다. 각 커널 오브젝트의 실상은 사실 커널 메모리내에 존재하는 구조체라고 볼 수 있다. 예를 들어 파일 오브젝트를 생각해보자. 파일이라는 것은 원래 자신의 시스템 상에 오직 하나만 존재하며(같은 이름의 파일이 있으면 어떻해요?.. 라든가 이런류의 질문은 하지 말기를..) 만약 사용자가 CreateFile() 등의 함수를 이용하여 파일 오브젝트를 생성하게 되면 커널안에 파일에 관련된 구조체가 생성되고 시스템 전체에서 오직 하나만 관리되게 된다. 따라서 하나의 파일 오브젝트는 여러 프로세스에서 공유될 수 있다. 또한 모든 커널 오브젝트는 비록 구조체 형태로 존재하기는 하지만 사용자가 바로 이 구조체에 접근하여 필드값들을 바꿀 수는 없다. 대신 바로 Win32 API를 이용하여 이 구조체에 접근하여 값을 읽고 설정할 수 있다. 만약 여러분이 OS를 배웠다면 OS에서의 시스템 콜에 대한 개념에 대하여 알고 있을 것이며 위의 이야기와 시스템 콜의 개념이 매우 비슷하다는 것을 알 수 있을 것이다. 윈도우 커널은 생성된 커널 오브젝트에 대한 프로세스내에서만 통용되는 DWORD(32-bit)값의 커널 오브젝트 ID를 사용자에게 리턴하게 되고 사용자는 이 핸들(통상 이런걸 핸들이라고 부른다. 윈도우 핸들, 파일 핸들..)과 Win32 API를 이용하여 커널 오브젝트를 manage하게 된다.
Usage Counter
커널 오브젝트는 그 커널 오브젝트를 생성한 프로세스가 아닌 커널 자체에 포함된다.(무슨말인지.. 헷갈릴 것이다.) 예를 들어 A라는 프로세스가 Kernel-A라는 커널 오브젝트를 생성했다고 하자. 만약 A가 작업을 끝내고 죽는다면 Kernel-A라는 커널 오브젝트로 죽게될까? 답은 그럴수도 있고 아닐수도 있다이다. 만약 A프로세스가 생성한 Kernel-A 오브젝트를 B라는 프로세스에서 사용하고 있다면 A프로세스가 죽어도 Kernel-A 오브젝트는 계속 커널 위의 메모리에 남게 될 것이다. 커널은 이러한 관리를 위하여 Usage Count라는 것을 생성된 커널 오브젝트마다 유지하고 있다.(흠.. 이부분은 만약 COM을 공부하는 분이 있다면.. 쉽게 이해할 것이다.)
Security
커널 오브젝트는 기본적으로 보안 항목(Security Descriptor)을 가지고 있다. 예를 들어 자신을 생성한 것은 누구이며, 누가 커널 오브젝트에 접근할 수 있나등등.. 이 부분은 주로 서버 프로그램에서 주로 사용하는 기능이며, 따라서 윈도우95/98에서는 해당사항이 없다. 커널 오브젝트를 생성하는 거의 모든 Win32 API들은 SECURITY_ATTRIBUTES 라는 매개변수 필드를 가지고 있다. 물론 윈도우95/98의 커널 오브젝트 생성 Win32 API에도 이 매개변수 필드는 있다.(당연하지.. 같은 Win32 API를 쓰는데.. 전에도 이야기했지만 Win32 API는 윈도우 95/98/NT/CE/2000에서 공통으로 쓰이는 프로그래밍 인터페이스다.) 그러나 실제로 이 필드에 어떠한 값을 넣어도 윈도우 95/98은 이 값을 무시한다. 다시 본론으로 돌아가서 하나 예를 들어보자. 다음의 CreateFileMapping이라는 함수는 Memory-Mapped File을 생성할때 쓰이는 함수이다. 여하튼 이 함수의 원형을 보면,
HANDLE CreateFileMapping( HANDLE hFile,
LPSECURITY_ATTRIBUTES lpFileMappingAttributes,
DWORD flProtect,
DWORD dwMaximumSizeHigh,
DWORD dwMaximumSizeLow,
LPCTSTR lpName );
두번째 파라미터가 바로 이 커널 오브젝트에 보안을 설정하는 파라미터이다. 대부분의 경우는 이 부분에 NULL값을 대입한다. 그렇게 되면 기본값이 설정된다. 기본 설정은 이 커널 오브젝트를 만든 사용자(시스템 로그온한 아이디로 구별)에게 최대한의 접근을 허용하고 다른 사용자는 접근거부하는 것이다. 그럼 여기서 SECURITY_ATTRIBUTES 구조체를 살펴보도록하자.
typedef struct _SECURITY_ATTRIBUTES {
DWORD nLength;
LPVOID lpSecurityDescriptor;
BOOL bInheritHandle;
} SECURITY_ATTRIBUTES;
nLength는 이 구조체의 크기를 나타내는 것이며, 세번째의 bInheritHandle은 뒤에 나오니 설명 생략한다. 마지막으로 가운데 값이 바로 Security Descriptor에 대한 포인터값이 된다. 예제 코드를 들자면
SECURITY_ATTRIBUTES sa;
sa.nLength = sizeof(sa);
sa.lpSecurityDescriptor = pSD;
sa.bInheritHandle = FALSE;
HANDLE hFileMapping = CreateFileMapping((HANDLE)0xffffffff, &sa, PAGE_READWRITE, 0, 1024, “MyFileMapping”);
커널 오브젝트에 덧붙여서, 커서, 마우스 포인터, 아이콘, GDI Object(font, brush, window..)등의 오브젝트들도 존재한다. 이러한 오브젝트들도 System-wide하게 존재하며 커널이 관리한다. 그러나 이것들을 커널 오브젝트라고 부르지는 않는다. 위에 나열한 것들은 통상 User Object, GDI Object라고 부른다. 그렇다면 커널 오브젝트와 사용자 오브젝트(흠.. 말이 길어서 줄여씀..)의 차이점은 무엇일까? 그것은 바로 사용자 오브젝트에는 보안 설정이 존재하지 않는다는 것이다. 예를 들어 아이콘 같은 경우는 누가 가져다 쓰던 문제가 되지 않을 것이다. 따라서 사용자 오브젝트를 생성하는 함수에는 보안 필드가 빠져있다.
A Process’s Kernel Object Handle Table
다음에 나오는 내용은 제프리가 자기 예상에 윈도우 커널에는 분명히 이런 구조가 있을 것이다라고 예상하고 쓴 것이므로 믿지 말기 바란다. 이 내용 자체가 MS에서만 알고 있는 비밀이고 따라서 어떠한 문서도 없으므로 아래의 내용이 사실이라고 확인 할 수가 없다. 그러니 제프리의 말처럼 이 내용을 윈도우 시스템이 이렇게 되어 있구나 하고 배울 생각으로 보지말고 같은 프로그래머의 입장에서 같이 생각해보는 입장으로 보기를 바란다.
일단 맨 처음 프로세스가 생성되면 다음과 같은 핸들 테이블을 생성할 것이다.
index 커널 오브젝트에 대단 메모리 블록 주소 Access Mask Flags
1 0x???????? 0x???????? 0x????????
2 0x???????? 0x???????? 0x????????
… … … …
Creating a Kernel Object
처음 프로세스가 생성되면 위의 핸들테이블은 빈 상태가 된다. 만약 프로세스에서 처음으로 커널 오브젝트를 생성하게되면 (예를 들어 CreateFileMapping() 등의 함수를 호출하여서..) 커널은 일단 프로세스의 핸들 테이블에서 빈 자리를 찾게되고, 지금의 경우는 프로세스가 생성된 후, 처음 커널 오브젝트를 생성하는 것이므로 맨 처음 인덱스에 해당되는 커널 오브젝트 구조체(앞에서 이야기했음)의 시작주소를 넣고, Access 필드와 Inheritance 필드를 채우게 된다. 다시 말하지만 커널 오브젝트 구조체는 커널 내부에 존재하게 된다. 다음의 함수들은 여러 종류의 커널 오브젝트를 만드는 함수들이다.
HANDLE CreateThread(LPSECURITY_ATTRIBUTES lpThreadAttributes, DWORD dwStackSize, LPTHREAD_START_ROUTINE lpStartAddress, LPVOID lpParameter, DWORD dwCreationFlags, LPDWORD lpThreadId);
HANDLE CreateFile(LPCTSTR lpFileName, DWORD dwDesiredAccess, DWORD dwShareMode, LPSECURITY_ATTRIBUTES lpSecurityAttributes, DWORD dwCreationDistribution, DWORD dwFlagsAndAttributes, HANDLE hTemplateFile);
HANDLE CreateFileMapping(HANDLE hFile, LPSECURITY_ATTRIBUTES lpFileMappingAttributes, DWORD flProtect, DWORD dwMaximumSizeHigh, DWORD dwMaximumSizeLow, LPCTSTR lpName);
HANDLE CreateSemaphore(LPSECURITY_ATTRIBUTES lpSemaphoreAttributes, LONG lInitialCount, LONG lMaximumCount, LPCTSTR lpName);
위의 함수들은 전부 다 커널 오브젝트에 대한 핸들값을 리턴한다. 실제 이 핸들값들은 핸들 테이블에서의 커널 오브젝트의 인덱스 값이라고 볼 수 있다.(그러나 이 핸들값이라는 것이 MS에 의하여 공식적으로 설명된 적이 없기 때문에 꼭 그렇다고 볼 수는 없다.) 그리고 진짜 중요한 것은 이 커널 오브젝트에 대한 핸들값은 process-relative 하다는 것이다. 이게 왜 중요하냐하면 이 커널 오브젝트에 대한 핸들값이 process-relative 하기 때문에 절대로 이 프로세스 외부의 다른 프로세스에서 이 핸들값으로 커널 오브젝트를 접근할 수 없다는 것이다. 커널 오브젝트는 커널 레벨에서 존재하며 커널이 관리한다. 따라서 system-wide하게 단 하나만 존재한다. 이것은 다른 프로세스에서도 이 커널 오브젝트를 사용할 수 있다는 것이 된다. 그러나 다른 프로세스에서 생성한 커널 오브젝트에 접근하기 위해서는 그냥 커널 오브젝트 핸들값을 넘겨주는 등의 작업과는 다른 작업이 이루어져야한다. 그 이유는 앞에서 이야기한대로 커널 오브젝트에 대한 핸들값이 process-relative하기 때문이다.
Closing a Kernel Object
프로세스 내에서 생성한 커널 오브젝트를 시스템에 반환하는 함수는 다음과 같다.
BOOL CloseHandle(HANDLE hobj);
여기서 또 주의할 점은 이렇게 커널 오브젝트에 대한 핸들을 닫아준다고 해서 커널 오브젝트가 없어지는 것은 아니라는 것이다. 실상 위의 함수를 이용해 핸들을 닫아주면 일단 커널은 그 프로세스의 핸들 테이블에서 해당하는 핸들값이 있는 인덱스를 클리어시킨다. 그리고 나서 그 커널 오브젝트에 대한 Usage Count를 살펴보고, 만약 0이라면 커널에서 삭제하고 아니라면 그냥 살려둔다. 한번 CloseHandle을 하게되면 그 순간부터 닫힌 커널 오브젝트는 그 프로세스내에서 Invalid 하다.
만약 프로그래머가 잘못하여 핸들을 닫지 않았다고 생각해보자. 그렇다고 하더라도 그 프로세스가 죽을때가 되면 커널이 알아서 커널 오브젝트에 대한 Usage Count를 내려주게 된다. 그러나 그 프로세스가 실행되고 있는 동안에는 닫지 않은 커널 오브젝트에 대한 리소스는 낭비되고 있는게 사실이다. 따라서 왠만하며 사용이 끝난 커널 오브젝트는 그 즉시 닫아 주는 것이 좋다.
Sharing Kernel Objects Across Process Boundaries
앞에서도 이야기하였지만 커널 오브젝트는 system-wide하게 유지되는 것이며 따라서 그 커널 오브젝트를 만든 프로세스내의 쓰레드 뿐만 아니라 다른 프로세스의 쓰레드에 의해서도 접근가능해야한다. 다음의 예는 커널 오브젝트가 프로세스 사이에 공유되는 경우에 대한 예들이다.
1. File-mapping 객체는 두개의 프로세스 사이에 특정 데이터 블록을 공유할 때 쓰일 수 있다.
2. Named pipes 같은 경우는 서로 다른 머신(네트워크로 연결된)사이에 일정한 데이터 블록을 전송할 때 쓰일 수 있다.
3. Mutex, semaphores, event 들은 다른 프로세스내의 쓰레드들 사이에 동기화가 필요할 때 쓰일 수 있다.
위의 경우중 3번째 경우는 자바의 synchronized와 같은 경우일 것이다.
그러나 문제는 커널 오브젝트에 대한 핸들이 process-relative하다는 것이 문제이다. 어떠한 방법으로 커널 오브젝트에 대한 핸들을 다른 프로세스에게 넘겨준다 하더라도 그 값은 그것을 생성한 프로세스내에서만 의미가 있기 때문에 넘겨받은 프로세스로서는 그것을 가지고 그 커널 오브젝트에 접근할 방법이 없다. “공유하는 경우가 많은데, 프로세스 사이에 커널 오브젝트 핸들을 공유못하게 한다면 뭔가 잘못되지 않았는가?” 뭐, 이렇게 반문할 수도 있다. MS쪽에서 이런식으로 커널 오브젝트를 설계한 이유는 다음의 두가지이다. 첫째는 보안문제이며, 두번째는 시스템의 성능때문이다. 뭐, 이것들에 대한 설명은 책에도 자세히 있는바, 어차피 지금 우리가 당면한 문제는 그냥 커널 오브젝트 핸들을 넘겨주는 것으로는 공유가 안된다는 것이고, 그렇다면 공유하는 방법이 있을 것이 분명하다. 다음의 방법들을 잘 읽어보고 그 방법을 터득하기 바란다.(노파심에서 이야기지만, 지금은 이게 뜬구름 잡는 것처럼 들릴지도 모른다. 그러나, 아주 많이 쓰인다. 꼭 알아두길..)
Object Handle Inheritance
이 방법은 한 프로세스가 자신의 자식 프로세스를 spawn 할때만 가능한 방법이다. 우리가 커널 오브젝트 생성 함수들을 볼 때 파라미터로 넘겨주는 SECURITY_ATTRIBUTES 구조체에 bInheritHandle 이라는 필드가 끼어 있는 것을 본 것이 기억날 것이다. 바로 이 부분에 TRUE를 넣어주게 되면 이 값을 파라미터로 받아서 생성된 커널 오브젝트는 그 자식 프로세스에게 물려줄 수 있는 상태가 된다. 뭐, 좀 자세히 예를 들어서 설명하면,
SECUTIRY_ATTRIBUTES sa;
sa.nLength = sizeof(sa);
sa.lpSecurityDescriptor = NULL;
sa.bInheritHandle = TRUE;
HANDLE hMutex = CreateMutex(&sa, FALSE, NULL);
.
뭐, 이렇게 Mutex를 생성했다고 하자. 그렇게 되면 일단 이 커널 오브젝트를 생성한 프로세스의 핸들 테이블은 다음과 같은 상태가 될 것이다.
index 커널 오브젝트에 대단 메모리 블록 주소 Access Mask Flags
1 0x???????? 0x???????? 0x????????
2 0xF0000000 0x???????? 0x00000001
… … … …
위의 표에서 두번째 행에 바로 이 Mutex에 대한 정보가 들어가게 될 것이다. 두번째 행의 맨 끝 열을 보면 플래그가 1로 세팅되어 있는 것을 볼 수 있다. 이것은 위에서 우리가 bInheritHandle을 TRUE로 세팅했기 때문이다. 흠.. 일단 이상태에서 이 프로세스가 자신의 자식 프로세스를 생성한다고 해보자. 자식 프로세스를 생성하는 함수는 다음과 같다. (너무 많은 파라미터를 보고 놀라지 말기를..)
BOOL CreateProcess(LPCTSTR lpApplicationName, LPTSTR lpCommandLine, LPSECURITY_ATTRIBUTES lpProcessAttributes, LPSECURITY_ATTRIBUTES lpThreadAttributes, BOOL bInheritHandles, DWORD dwCreationFlags, LPVOID lpEnvironment, LPCTSTR lpCurrentDirectory, LPSTARTUPINFO lpStartupInfo, LPPROCESS_INFORMATION lpProcessInformation);
위의 파라미터 중 bInheritHandles라는 파라미터를 TRUE로 세팅하게 되면 그때부터 이 자식 프로세스는 자기 부모 프로세스의 커널 오브젝트 핸들 중 bInheritHandle(부모 프로세스의 핸들테이블에서..)가 TRUE인 것을 물려받게 된다. 좀 자세히 설명하면 일단 위의 함수를 실행하면 커널은 프로세스를 만들고 그 프로세스에 대한 핸들 테이블도 생성한다. 그 다음 만약 bInheritHandles 파라미터가 TRUE라면 커널은 일단 부모 프로세스의 핸들 테이블을 뒤져서 세번째 열이 TRUE인 인덱스에 있는 커널 오브젝트들을 그대로 카피해서 자식 프로세스의 핸들테이블에 집어 넣는다. 여기서 중요한 것은 만약 부모 프로세스에 2번째 행에 있었으면 자식 프로세스의 핸들 테이블에도 2번째행에 들어간다. 진짜 카피하는 것이다. 이렇게 되면 그때부터 자식 프로세스도 그 커널 오브젝트를 사용할 수 있게 된다. 그러나 이방법에 한가지 문제가 있다. 생성된 자식 프로세스가 어떠한 방법으로든 자신이 어떤 커널 오브젝트의 핸들을 물려받았으며, 그 핸들값이 무엇인지를 알아야한다는 것이다. 다시 한번 이야기하지만 자식 프로세스 핸들 테이블에 카피 된 것은 카피 된것이고 자식 프로세스가 이 커널 오브젝트를 사용하기 위해서는 그 핸들값을 알아야한다.(사실 정확히 이야기하면 그 핸들값이라는 것이 바로 핸들 테이블에서의 인덱스 값이다…) 이 문제는 일단 자식과 부모사이에, 그러니까 프로그래머가 프로그램을 만들 때, 두 프로그램을 전부다 알고 있어야한다는 것이다. 만약 프로그래머가 뻔히 “이 프로그램이 이놈을 생성할때는 이런 핸들값을 넘긴다” 알고 있을 때만 가능하다는 것이다. 만약 그렇다면 뭐, 문제야 간단하다. 그냥 생성될 때 코맨드라인으로 핸들값만 넘기면 된다. 결론을 이야기하자면 이 방법은 별로 안쓴다.(^.^)
Changing the Handles Flags
이건 생략.. 복잡하고, 잘 쓰지 않는다.
Named Objects
가장 많이 쓰는 방법으로서 커널 오브젝트를 생성할 때 이름을 준후 다른 프로세스에서 같은 이름으로 커널 오브젝트를 찾으면 있는 경우는 그 커널 오브젝트를 넘겨주는 방법이다.(흠.. 한마디로 쉽다..) 다음과 같은 커널 오브젝트 생성함수를 보도록 하자.
HANDLE CreateFileMapping(HANDLE hFile, LPSECURITY_ATTRIBUTES lpFileMappingAttributes, DWORD flProtect, DWORD dwMaximumSizeHigh, DWORD dwMaximumSizeLow, LPCTSTR lpName);
HANDLE CreateSemaphore(LPSECURITY_ATTRIBUTES lpSemaphoreAttributes, LONG lInitialCount, LONG lMaximumCount, LPCTSTR lpName);
HANDLE CreateEvent(LPSECURITY_ATTRIBUTES lpEventAttributes, BOOL lInitialCount, LONG lMaximumCount, LPCTSTR lpszName);
HANDLE CreateWaitableTimer(LPSECURITY_ATTRIBUTES lpTimerAttributes, BOOL bManualReset, LPCTSTR lpszName);
HANDLE CreateMutex(LPSECURITY_ATTRIBUTES lpMutexAttributes, BOOL bInitialOwner, LPCTSTR lpszName);
위 함수들의 공통점은 모두 다 마지막에 lpszName 파라미터를 가지고 있다는 것이다. 이것이 바로 이 커널 오브젝트의 이름이 된다. 물론 이 값에 NULL을 줄수도 있지만 만약 이 커널 오브젝트 system-wide하게 공유되길 바란다면 이 부분에 이름을 주어야 한다. 다음과 같은 예를 들어보자.
HANDLE hMutexA = CreateMutex(NULL, FALSE, “chparkmutex”);
이 다름에 다른 프로세스에서 다음과 같이 한다면
HANDLE hMutexB = CreateMutex(NULL, FALSE, “chparkmutex”);
이 함수를 실행시키면 일단 커널은 커널에 chparkmutex라는 이름의 커널 오브젝트가 있는지 검사한다. 만약 있다면 그 다음에는 타입을 검사한다. 다행이도 이 경우는 두가지 다 Mutex를 만든 것이므로 타입도 같다. 그렇게 되면 커널은 새로 커널 오브젝트를 만드는 것이 아니라 기존에 있던 chparkmutex라는 이름을 가진 Mutex 오브젝트의 핸들값을 리턴하고 단지 이 커널 오브젝트의 Usage Count를 하나 증가시킨다. 그러나 만약 다음과 같이 한다면
HANDLE hSemB = CreateSemaphore(NULL, 1, 1, “chparkmutex”);
이 코드를 실행시키면 일단 같은 이름을 가진 커널 오브젝트가 있는 것 까지는 괜챦지만, 타입 체크시 에러가 난다.(당연하겠지..)
그러나 이 방법에도 맹점은 존재한다. 무엇인고 하니 뭐, 타이핑을 잘못하여 이름이 조금 틀려서 chparkmutec이라고 쳤다고해보자. 그러면 일단 커널이 볼때는 이런 이름을 가진 커널 오브젝트가 없으니 새로 하나 만들어서 그 핸들 값을 리턴할 것이다. 이는 우리가 원하는 일이 아니다. 그러나 에러가 안나니 제대로 되는지 전혀 알 수 없다. 이러한 것을 막기 위한 함수들이 존재하는데 다음과 같다.
HANDLE OpenMutex(DWORD dwDesiredAccess, BOOL bInheritHandle, LPCTSTR lpszName);
HANDLE OpenEvent(DWORD dwDesiredAccess, BOOL bInheritHandle, LPCTSTR lpszName);
HANDLE OpenSemaphore(DWORD dwDesiredAccess, BOOL bInheritHandle, LPCTSTR lpszName);
HANDLE OpenWaitableTimer(DWORD dwDesiredAccess, BOOL bInheritHandle, LPCTSTR lpszName);
HANDLE OpenFileMapping(DWORD dwDesiredAccess, BOOL bInheritHandle, LPCTSTR lpszName);
이 함수들을 쓰게 되면 만약 같은 이름을 가진 커널 오브젝트가 없다면 에러를 내게된다. 따라서 함수의 수행이 제대로 되는지 정확히 판단 할 수 있게된다. 결국은 만들어놓은 커널 오브젝트를 얻고자 한다면 Create.. 보다는 Open..을 써야한다는 것이다.
Duplicating Object Handles
어~~ 머리아파.. 뭐, 위에 이름을 이용한 방법이 최고로 많이 쓰인다.. 나머지는 읽어봐.. 나도 머리아파..