04. 프로세스 - CreateProcess

Windows/System 2009.08.21 22:10

BOOL CreateProcess함수를 이용하면 새로운 프로세스를 생성할 수 있다.

BOOL CreateProcess(
PCTSTR pszApplicationName,
PSECURITY_ATTRIBUTES psaProcess,
PSECURITY_ATTRIBUTES psaThread,
BOOL bInheritHandles,
DWORD fdwCreate,
PVOID pvEnvironment,
PCTSTR pszCurDir,
PSTARTUPINFO psiStartInfo,
PROCESS_INFORMATION ppiProcInfo);

스레드가 CreateProcess를 호출하면 시스템은 사용 카운트가 1인 프로세스 커널 오브젝트를 생성한다.
프로세스 커널 오브젝트는 프로세스 자체를 의미하는 것은 아니며, 운영체제가 프로세스를 관리하기 위한 목적으로 생성한 조그마한 데이터 구조체다. 프로세스 커널 오브젝트를 프로세스에 대한 각종 통계 정보를 가지고 있는 작은 데이터 구조체라고 생각할 수도 있다. 프로세스 커널 오브젝트가 생성되고 나면 시스템은 새로운 프로세스를 위한 가상 주소 공간을 생성하고, 실행 파일의 코드와 데이터 및 수행에 필요한 추가적인 DLL 파일들을 프로세스의 주소 공간 상에 로드한다.

다음 단계로, 시스템은 새로 생성된 프로세스의 주 스레드를 위한 스레드 커널 오브젝트(사용 카운트 1)를 생성한다. 주 스레드는 링커에 의해 진입점으로 지정된 C/C++ 런타임 시작 코드를 실행한다. 이러한 시작 코드는 종국에는 사용자가 작성한 WInMain, wWinMain, main, 또는 wmain함수를 호출하게 된다. 만일 시스템이 성공적으로 프로세스를 생성하고 주 스레드를 생성하였다면 CreateProces는 TRUE를 반환한다.

 ※ CreateProcess 함수는 새로 생성된 프로세스가 완전히 초기화되기 전에 TRUE를 반환한다. 이것은 운영체제의 로더가 새로 생성된 프로세스가 필요로 하는 모든 DLL을 로드하기 전에 CreateProcess가 반환될 수 있다는 의미이다. 만일 필요한 DLL이 없거나 올바르게 초기화가 진행되지 않으면 새로 생성된 프로세스는 곧바로 종료된다. 하지만 CreateProcess는 이미 TRUE를 반환했을 것이기 때문에 이 경우 부모 프로세스는 자식 프로세스에 어떠한 초기화 문제가 발생했는지 알 수 없다.

(1) pszApplicationName 과 pszCommandLine
 pszApplicationName과 pszCommandLine 매개변수로는 각각 프로세스를 생성할 실행 파일명과 새로운 프로세스에게 전달할 명령행 문자열을 지정하게 된다. 먼저 pszCommandLine 매개변수에 대해 알아보기로 하자
pszCommandLine 매개변수의 자료형이 PTSTR인 점에 주목할 필요가 있다. 이것은 우리가 전달하는 문자열이 CreateProcess 함수 내에서 변경될 수 있는 형태(non-constant string)로 전달되어야 함을 의미한다. CreateProcess는 내부적으로 우리가 전달하는 명령행 문자열에 변경 작업을 수행한다. 하지만 반환 직전에 그 내용을 원래의 값으로 돌려놓는다.

만일 명령행 문자열을 읽기 전용의 파일명 형태로 전달하게 되면 접근 위반이 발생하기 때문에 이 점은 매우 중요하다. 다음과 같이 코드를 작성하면 마이크로소프트 C/C++ 컴파일러는 "NOTEPAD"라는 문자열을 읽기 전용의 메모리에 배치하기 때문에 접근 위반을 유발하게 된다.

STARTUPINFO si = { sizeof(si) };
PROCESS_INFORMATION pi;
CreateProcess(NULL, TEXT("NOTEPAD"), NULL, NULL, FALSE, 0, NULL, NULL, &si, &pi);

이러한 접근 위반은 CreateProcess가 내부적으로 전달된 문자열을 수정하려 할 때 발생하게 된다. (이전 버전의 마이크로소프트 C/C++ 컴파일러는 위와 같이 문자열을 전달하는 경우에도 읽기/쓰긱 ㅗ모두 가능한 메모리 상에 문자열을 배치하였으므로 CreateProcess를 호출하더라도 접근 위반을 유발하지 않았다. 이 문제를 해결하기 위한 최상의 방법은 다음의 예와 같이 문자열 상수를 임시 버퍼에 복사한 후 CreateProcess를 호출하는 것이다.

STARTUPINFO si = { sizeof(si) };
PROCESS_INFORMATION pi;
TCHAR szCommandLine[] = TEXT("NOTEPAD");
CreateProcess(NULL, szCommandLine, NULL, NULL, FALSE, 0, NULL, NULL, &si, &pi);

마이크로소프트 C/C++ 컴파일러는 중복 문자열을 제거하고 문자열을 읽기 전용 섹션에 배치시킬 수 있도록 /Gf와 /GF 컴파일러 스위치를 제공한다(Edit & Continue와 같은 Visual Studio 디버거의 기능을 활성화하기 위해 /ZI 컴파일 스위치를 사용하면 /GF 스위치를 수반하게 된다.) 이러한 문제를 해결하는 가장 좋은 방법은 /GF 컴파일러 스위치를 사용하되 임시 버퍼를 이용하는 것이다. 물론 마이크로스프트가 CreateProcess를 수정해서 개발자가 임시 버퍼를 사용하지 않아도 되도록 함수 내부에서 전달된 문자열의 복사본을 만들어서 사용하도록 하면 될 것이다. 차기 버전의 윈도우에서는 이와 같이 변경되기를 기대해 본다.

흥미로운 사실은 윈도우 비스타에서 ANSI 버전의 CreateProcess를 위와 같은 방식으로 호출할 때에는 접근 위반이 발생하지 않는다는 것이다. 이는 유니코드로의 변경을 위해 문자열에 대한 복사본이 내부적으로 만들어지기 때문이다.

CreateProcess의 두 번째 매개변수인 pszCommandLine 을 이용하면 CreateProcess 가 새로운 프로세스를 생성하기 위해 필요한 추가 정보를 제공할 수 있다. pszCommandLine을 통해 전달되는 문자열의 첫 번째 토큰은 실행하고자 하는 프로그램의 파일명으로 간주되며, 확장자가 전달되지 않으면 .exe로 가정한다. CreateProcess는 실행 파일을 찾기 위해 다음과 같이 순차적으로 검색을 진행한다.

1. 생성할 프로세스의 실행 파일명에 포함된 디렉토리
2. 생성할 프로세스의 현재 디렉토리
3. 윈도우 시스템 디렉토리. 즉 GetSYstemDirectory가 반환하는 System32 서브폴더
4. 윈도우 디렉토리
5. PATH 환경변수에 포함된 디렉토리들

물론 생성할 프로세스의 파일명이 전체 경로를 포함하고 있는 경우라면 이러한 전체 경로만을 이용하여 실행 파일을 찾게 되고 나머지 디렉토리에서는 검색을 수행하지 않는다. 시스템이 실행 파일을 찾으면 실행 파일의 코드와 데이터는 새로운 프로세스의 주소 공간에 매핑된다. 이후 링커에 의해 애플리케잉션 진입점으로 지정된 C/C++ 런타임 시작 함수를 호출한다. 앞서 말한 바와 강이 C/C++ 런타임 시작 함수는 프로세스의 명령행을 검토하여 실행 파일명 다음으로 전달되는 첫 번째 인자를 가리키는포인터를 (w)WinMain의 pszCmdLine 매개변수를 통해 전달된다.

pszApplicationName 매개변수를 NULL로 지정하는 한(대부분의 경우 NULL)로 지정 이와 같이 작업이 수행된다. 하지만 pszApplicationName 매개변수로 실행 파일명을 담고 있는 문자열의 주소를 전달할 수도 있다. pszApplication 매개변수로 파일명을 지정하는 경우 파일명의 확장자를 .exe로 가정하지 않기 때문에 반드시 확장자를 포함하도록 파일명을 지정해야 한다. 이 경우 Create-Process 는 파일명에 경로명이 없다면 파일이 현재 디렉토리 상에 있을 것이라고 가정하게 된다. 따라서 파일이 현재 디렉토리에 존재하지 않으면 CreateProcess는 다른 디렉토리를 검색하지 않으며 실패를 반환한다.

pszApplicationName 매개변수로 파일명을 지정하는 경우라 하더라도 pszCommandLine 매개변수를 통해 새로운 프로세스를 위한 명령행 문자열을 전달할 수 있다. 예를 들어 CreateProcess를 다음과 같이 사용하는 경우를 생각해 보자.

// 전달하는 메모리는 읽기/ 쓰기가 가능한 메모리 상에 위치해야 한다.
TCHAR szPath[] = TEXT("WORDPAD README.TXT");

// 새로운 프로세스를 생성한다.
CreateProcess(TEXT("C:\\WINDOWS\\SYSTEM32\\NOTEPAD.EXE"), szPath, ...);

위 코드는 메모장 NotePad을 수행한다. 하지만 메모장의 명령행에는 WORDPAD README.TXT가 전달된다. 이것이 이상해 보이지만 CreateProcess는 이와 같이 동작한다. 사실 pszApplicationName 매개변수를 통해 실행 파일명을 지정할 수 있도록 한 것은 CreateProcess가 윈도우의 POSIX 서브 시스템을 지원하도록 하기 위해 포함시킨 것이다.

(2) psaProcess, psaThread, bInheritHandles
새로운 프로세스를 생성하기 위해 시스템은 새로운 프로세스 커널 오브젝트와 스레드 커널 오브젝트(프로세스의 주 스레드)를 생성해야 한다. 2개의 오브젝트들은 모두 커널 오브젝트이므로 부모 프로세스는 각각에 대해 보안 특성을 지정할 수 있어야 한다. CreateProcess 함수에서는 psaProcess와 psaThread 매개변수를 통해 프로세스 커널 오브젝트와 스레드 커널 오브젝트 각각에 대해 원하는 보안 특성을 지정할 수 있다. 기본 보안 해설자를 사용하길 원한다면 각 매개변수를 NULL로 지정하면 된다. 그렇지 않은 경우라면 SECURITY_ATTRIBUTES 구조체를 생성하여 프로세스 오브젝트와 스레드 오브젝트 각각에 대해 적절한 보안 권한을 설정하면 된다.

psaProcess와psaThread 매개변수로 SECURITY_ATTRIBUTES를 사용하는 또 다른 이유는 두개의 커널 오브젝트 핸들을 상속 가능하도록 생성하여 추후 부모 프로세스가 새로운 자식 프로세스를 생성할 때 이 커널 오브젝트들을 사용할 수 있도록 하기 위함이다.

커널 오브젝트 핸들 상속을 보여주기 위한 간단한 프로그램이다. A 프로세스는 CreateProcess를 호출할 때 psaProcess 매개변수로 bInheritHandle 값을 TRUE로 설정한 SECURITY_ATTRTI

신고

'Windows > System' 카테고리의 다른 글

04. 프로세스 - CreateProcess  (0) 2009.08.21
03. 프로세스  (0) 2009.08.21
02. 커널 오브젝트 下  (0) 2009.08.18
01. 커널 오브젝트 上  (3) 2009.08.18

03. 프로세스

Windows/System 2009.08.21 03:28
프로세스란 일반적으로 수행 중인 프로그램의 인스턴스 라고 정의한다.

프로세스의 구성요소
 ① 프로세스 커널 오브젝트 : 프로세스를 관리하기 위한 목적으로 운영체제가 사용하는 커널 오브젝트, 시스템은 프로세스에 대한 각종 통계 정보를 프로세스 커널 오브젝트에 저장하기도 한다.
 ② 주소 공간 : 실행 모듈이나 DLL(Dynamic-Link Library)의 코드와 데이터를 수용하는 주소 공간, 이러한 주소 공간은 스레드 스택이나 힙 할당과 같은 동적 메모리 할당에 사용되는 공간도 포함된다.

- 스레드
 프로세스는 자력으로 수행될 수 없다. 프로세스가 무언가를 수행하기 위해서는 반드시 프로세스의 컨텍스트(Context) 내에서 수행되는 스레드(Thread)가 있어야 한다. 스레드는 프로세스의 주소 공간 상에 위치하고 있는 코드를 수행할 책임이 있다. 하나의 프로세스는 다수의 스레드를 가질 수 있으며, 이러한 스레드들은 프로세스 주소 공간 내에서 "동시에" 코드를 수행한다. 이렇게 되려면 각 스레드들은 자신만의 CPU 레지스터 집합과 스택을 가져야만 한다. 각 프로세스는 프로세스 주소 공간 내의 코드를 수행 하기 위해 적어도 한 개의 스레드를 가지고 있다.

 프로세스가 생성되면 시스템은 자동적으로 첫 번째 스레드를 생성해 주는데, 이0를 주 스레드(Primary Thread)라고 부른다. 이 스레드는 추가적인 스레드를 생성할 수 있고, 이렇게 생성된 스레드들이 더욱더 많은 스레드를 만들어낼 수도 있다. 만일 프로세스의 주소 공간 내의 코드를 수행할 스레드가 없다면 프로세스는 계속해서 존재해야 할 이유가 없고, 따라서 시스템은 자동적으로 프로세스와 프로세스 주소 공간을 파괴한다.


- 스케줄링
 모든 스레드가 동시에 수행될 수 있도록 하기 위해 운영체제는 CPU 시간을 조금씩 나누어준다. 각 스레드들은 라운드 로빈(Round-Robin) 방식으로 주어지는 단위 시간(Quantum)만큼만 수행 될 수 있다. 이렇게 되면 마치 모든 스레드들이 동시에 수행되고 있는것처럼 보이게 된다.
다수의 CPU를 가지고 있는 머신의 경우 각 스레드에게 공평하게 CPU 시간을 나누어주는 알고리즘은 상당히 복잡하다. 마이크로소프트 윈도우의 경우 다수의 스레드를 동시에 수행시키기 위해 각 CPU별로 서로 다른 스레드를 수행하도록 스케줄링하고 있다. 윈도우 운영체제에서는 스레드에 대한 모든 관리와 스케줄링을 윈도우 커널이 담당한다. 따라서 다수의 CPU를 가지고 있는 머신의 장점을 사용하기 위해 코드를 변경해야 할 필요는 없다. 하지만 여러 개의 CPU를 가진 머신의 장점을 최대한 살리기 위해 애플리케이션의 알고리즘을 적절히 변경하는 것은 여전히 유효한 방법이다.


1. 윈도우 어플리케이션 작성

- 윈도우가 지원하는 애플리케이션

 ① GUI(Graphic User Interface) : 그래픽 폰트를 사용하며, 윈도우를 만들 수 있고, 메뉴를 가지기도 하고, 다이얼로그 박스를 통해 사용자와의 상호작용을 수행하기도 한다. 그리고 윈도우스러운 표준화된 모든 요소들을 사용하게 된다. 윈도우와 함께 배포되는 보조 프로그램(메모장, 계산기, 워드패드등)들은 대부분 GUI 기반의 애플리케이션이다.

 ② CUI(Console User Interface) : 텍스트를 기반으로 한다. 일반적으로 윈도우를 생성하지도 않고, 메시지를 처리하지도 않으며, 그래픽 유저 인터페이스를 필요로 하지도 않는다. 비록 CUI 기반의 애플리케이션도 화면 상에 단일의 윈도우 생성하고 그 안에서 수행되긴 하지만, 이 윈도우는 단순히 텍스트만을 출력한다. 명령 프롬프트(Command Promt)가 전형적인 CUI 기반 애플리케이션의 예이다.

 ※ 두 가지 형태의 애플리케이션은 경계가 명확하지 않다. CUI 기반의 애플리케이션도 다이얼로그 박스를 사용하는 것이 가능하다. 예를 들어 명령 쉘은 쉘이 제공하는 모든 명령어를 기억하는 대신에 그래픽 다이얼로그 박스에서 명령을 선택하여 수행할 수 있다 . 뿐만 아니라 GUI 기반의 애플리케이션도 텍스트 문자열을 콘솔 창에 출력할 수 있다.

- 링커 스위치(Linker Switch)
마이크로소프트트의 Visual Studio를 이용하여 애플리케이션 프로젝트를 생성하면, Visual Studio 통합 환경은 실행 파일의 형태에 알맞은 서브시스템 타입을 실행 파일에 포함시킬 수 있도록 다양한 링커 스위치를 설정한다.
CUI 기반 애플리케이션을 위한 링커 스위치 : /SUBSYSTEM:CONSOLE 
GUI 기반 애플리케이션을 위한 링커 스위치 : /SUBSYSTEM:WINDOWS

사용자가 애플리케이션을 수행하면 운영체제의 로더(Loader)는 실행 파일의 헤더를 확인하여 서브시스템 값을 가져온다. 만일 이 값이 CUI 기반의 애플리케이션이 콘솔 윈도우를 사용할 수 있도록 조치한다. 만일 CUI기반의 애플리케이션이 명령 프롬프트에서 수행되면 명령 프롬프트가 사용중인 윈도우를 사용하고, 윈도우 익스플로어에서 수행되면 새로운 콘솔 윈도우를 생성한다
 헤더로부터 확인한 서브시스템 값이 GUI기반의 애플리케이션을 의미하는 값이면 로더는 콘솔 윈도우를 생성하지 않고 애플리케이션을 바로 로드한다. 애플리케이션이 수행되면 운영체제는 애플리케이션이 어떤 형태인지에 대해 더 이상 신경 쓰지 않는다.
 윈도우 애플리케이션은 애플리케이션이 수행을 시작할 진입점 함수를 반드시 가져야 한다. C/C++ 개발자는 두 가지 형태의 진입점 함수를 사용할 수 있다.
 어떤 진입점 함수를 사용할지는 유니코드 문자열의 사용 여부에 달려 있다. 사실 운영체제는 우리가 작성한 진입점 함수를 직접 호출하지는 않으며, C/C++ 런타임에 의해 구현된 C/C++ 런타임 시작 함수(C++ Runtime Startup Function)를 호출한다. 이러한 함수는 링크 시 -entry:명령행 옵션(Command-Line Option)을 통해 설정된다. C/C++ 런타임 시작 함수는 malloc이나 free와 같은 함수가 호출될 수 있도록 C/C++ 런타임 라이브러리에 대한 초기화를 수행한다. 또한 개발자가 코드 상에서 선언한 각종 전역 오브젝트나 static 으로 선언된 C++  오브젝트들을 코드가 수행되기 전에 적절히 생성하는 역할을 수행한다.

[표] 애플리케이션 타입과 진입점
 애플리케이션 타입  진입점  실행 파일에 포함되는 런타임 시작 함수
 ANSI 문자(열) ,GUI 어플리케이션  _tWinMain(WinMain)  WinMainCRTStartup
 유니코드 문자(열), GUI 애플리케이션  _tWinMain(wWinMain)  wWinMainCRTStartup
 ANSI 문자(열), CUI 애플리케이션  _tmain(main)  mainCRTStartup
 유니코드 문자(열), CUI 애플리케이션   _tmain(wmain)  wmainCRTStartup

링커는 실행 파일을 링크하는 단계에서 적절한 C/C++ 런타임 시작 함수를 선택해야 한다.
① /SUBSYSTEM:WINDOWS : 링커 스위치가 설정되어 있으면 링커는 WInMain 이나 wWinMain 함수를 찾게 된다.
함수를 찾을 수 있는 경우 :  WinMainCRTStartup이나 wWinMainCRTStartup 함수를 호출하도록 설정한다.
함수가 찾을 수 없는 경우 :  외부 기호를 확인할 수 없습니다(unresolved external symbol) 에러를 반환한다.

 ② /SUBSYSTEM:CONSOLE : 링커 스위치가 지정되면 링커는 main 이나 wmain 함수를 찾는다.
 함수를 찾을 수 있는 경우 : mainCRTStartup이나 wmainCRTStartup 함수를 호출하도록 설정한다.
 함수가 찾을 수 없는 경우 :  외부 기호를 확인할 수 없습니다(unresolved external symbol) 에러를 반환한다.

프로젝트 설정에서 /SUBSYSTEM 링커 스위치를 완전히 제거할 수도 있다는 것은 잘 알려져 있지 않은 사실이다. 만일 이러한 링커 스위치를 제거하게 되면 링커는 자동적으로 애플리케이션에 적합한 설정 값을 찾아낸다. 링크 단계에서 링커는 코드에서 4개의 함수 중(WinMain, wWinMain, main, wmain) 어떤 것이 구현되었는지를 확인하고 적절한 서브시스템 설정을 추정한다. 이렇게 결정된 정보를 근간으로 어떤 C/C++ 시작함수가 실행 파일에 포함되어야 하는지를 결정하게 된다.

 모든 C/C++ 런타임 시작함수는 기본적으로 동일한 작업을 수행한다. 차이점이라면 C 런타임 라이브러리의 초기화 이후에 수행해야 할 진입점 함수가 어떤 것이냐에 따라 ANSI 문자열이나 유니코드 문자열을 처리해야 한다는 점 정도일 것이다. Visual C++에는 C/C++ 런타임 라이브러리의 소스 코드가 포함되어 있다. crtexe.c 파일을 살펴보면 4개의 시작 함수에 대한 구현 내용을 찾아볼 수 있는다.

- 시작 함수가 수행하는 작업
 ① 새로운 프로세스의 전체 명령행을 가리키는 포인터를 획득

 ② 새로운 프로세스의 환경변수를 가리키는 포인터를 획득

 ③ C/C++ 런타임 라이브러리의 전역변수를 초기화. 사용자 코드가 StdLib.h 파일을 인클루드하면 이 변수에 접근할 수 있다.
 변수명 타입   설명과 이 변수를 대체하는 윈도우 함수
 _osver  unsigned int  운영체제의 빌드 버전.
 예) 비스타 RTM은 6000이므로 _osver는 6000값을 가진다
 _winmajor  unsigned int  16비트로 나타낸 윈도우 major버전.
 윈도우 비스타의 경우 6.
 GetVersionEx 대체 사용 가능
 _winminor  unsigned int  16비트로 나타낸 윈도우 minor 버전.
 윈도우 비스타의 경우 0.
 GetVersionEx 대체 사용 가능
 _winver  unsinged int  (_winmajor << 8) + _winminor.
 GetVersionEx 대체 사용 가능
 __argc  unsigned int  명령행을 통해 전달된 인자의 개수.
 GetCommandLine을 대체 사용 가능
 __argv
 _wargv
 char
 wchar_t
 ANSI / 유니코드 문자열을 가리키는 __argc 크기의 배열
 배열의 각 요소는 명령행 인자를 가리킨다.
 _UNICODE가 정의되어 있으면 __argv가 NULL, 정의되어 있지 않으면 __wargv가 NULL이다.
 GetCommandLine을 대체 사용 가능
 _environ
 _wenviron
 char
 wchar_t
 ANSI/유니코드를 가리키는 배열. 각 배열 요소는 환경변수 문자열을 가리킨다. _UNICODE가 정의되어 있으면 _environ이 NULL, 정의되어 있지 않으면 _wenviron 이 NULL이다.
 GetEnviromentStrings, GetEnvironmentVariable 대체 사용 가능
 _pgmptr
 _wpgmptr
 char
 wchar_t
 ANSI/유니코드로 표현되는 수행 중인 프로그램의 전체 경로와 이름.
 _UNICODE가 정의도어 있으면 _pgmptr이 NULL, 정의되어 있지 않으면 _wpgmptr이 NULL이다.
 GetModuleFileName의 첫 번째 매개변수로 NULL을 전달하는 형태로 대체 사용 가능

④ 모든 전역 오브젝트와 static C++ 클래스 오브젝트의 생성자를 호출한다.


(1) 프로세스의 인스턴스 핸들

 모든 실행 파일과 DLL 파일은 프로세스의 메모리 공간 상에 로드될 때 고유의 인스턴스 핸들을 할당 받는다. 이러한 인스턴스 핸들은 (w)WinMain의 첫 번째 매개변수인 hInstanceExe를 통해 전달된다. 이 핸들 값은 보통 리소스를 로드할 때 사용된다.
ex) 실행 파일 이미지에 포함되어 있는 아이콘 리소스를 로드하려는 경우 다음의 함수를 호출해야 한다.
HICON LoadIcon(HINSTANCE hInstance, PCSTR pszIcon);

LoadIcon의 첫 번째 매개변수로는 리소스가 포함되어 있는 파일(실행 파일이나 DLL)의 인스턴스 핸들을 지정하면 된다. 많은 애플리케이션에서 (w)WinMain의 hInstanceExe 매개변수를 전역변수에 저장해 두어 실행 파일의 전체소스에서 이 값에 손쉽게 접근할 수 있도록 하곤 한다.

플랫폼 SDK 문서를 살펴보면 HMODULE 형 인자를 요구하는 함수들이 있음을 알 수 있다. 예를 들면 다음에 나오는 GetModuleFileName과 같은 함수가 있다.
DWORD GetModuleFileName(HMOUDLE hInstModule, PTSTR pszPath, DWORD cchPath);

WinMain의 hInstanceExe 매개변수의 실제 값은 시스템이 프로세스의 메모리 주소 공간 상에 실행 파일을 로드할 시작 메모리 주소(base memory address)다. 예를 들어 시스템이 실행 파일을 열어서 그 내용을 0x00400000에 로드하고자 한다면 (w)WinMain의 hInstanceExe 매개변수는 0x00400000을 가지게 된다.

실행 파일이 로드될 시작 주소는 링커에 의해 결정된다. 서로 다른 링커는 서로 다른 기본 시작 주소를 가질 수 있다. Visual Studio의 링커는 역사적인 이유로 0x00400000을 기본 시작 주소로 사용하고 있는데, 0x00400000은 윈도우 98에서 실행 파일을 로드할 수 있는 가장 하단의 메모리 주소였다. 애플리케이션이 로드되는 시작 주소는 마이크로소프트 링커의 경우 /BASE:address 옵션을 사용하여 변경할 수 있다.

 아래의 GetModuleHandle 함수는 실행 파일이나 DLL 파일이 프로세스의 메모리 공간 상의 어디에 로드되어 있는지를 가리키는 핸들/시작 주소를 반환한다.

HMODULE GetModuleHandle(PCTSTR pszModule);

이 함수를 호출할 때에는 호출하는 프로세스의 주소 공간에 로드되어 있는 실행 파일명이나 DLL 파일명을 '\0'으로 끝나는 문자열로 전달하면 된다. 시스템이 지정한 실행 파일이나 DLL 파일을 찾아내면 GetModuleHandle 함수는 파일이 로드된 시작 주소를 반환한다. 반면 시스템이 해당 파일을 찾을 수 없다면 NULL을 반환한다. GetModuleHandle을 호출할 때 pszModule 매개변수로 NULL 값을 전달할 수도 있는데, 이 경우 GetModuleHandle은 현재 수행 중인 실행 파일이 로드된 시작 주소를 반환한다. 만일 이 함수가 DLL 내에서 호출된다면 어떤 모듈에 포함되어 코드가 수행 중인지 알아내기 위한 두 가지 방법이 있다.

① 링커에 의해 정의되는 가상변수인 __ImageBase가 현재 수행 중인 모듈의 시작 주소를 가리키고 있다는 사실을 활용할 수 있다. 이 변수 값은 앞서 알아본바와 같이 C 런타임 시작 코드가 (w)WinMain 함수를 호출할 때 사용되는 값이다.

② 첫 번째 매개변수로 GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS를 두 번째 매개변수로 현재 수행 중인 함수의 주소를 지정하여 GetModuleHandleEx함수를 호출한다. 마지막 매개변수로 전달되는 값은 HMODULE을 가리키는 포인터 값인데, 두번째 매개변수로 전달한 함수를 포함하고 있는 DLL의 시작주소를 반환해 준다.

※ 실제로 HMODULE과 HINSTANCE는 완전히 동일하다. 어떤 함수가 HMODULE을 요구한다면 HINSTANCE를 넘겨줘도 무방하며, 그 반대도 마찬가지다. 16비트 윈도우에서는 HMODULE과 HINSTANCE가 완전히 구분되는 자로형으로 존재했지만 지금은 혼용하고 있다.
신고

'Windows > System' 카테고리의 다른 글

04. 프로세스 - CreateProcess  (0) 2009.08.21
03. 프로세스  (0) 2009.08.21
02. 커널 오브젝트 下  (0) 2009.08.18
01. 커널 오브젝트 上  (3) 2009.08.18

02. 커널 오브젝트 下

Windows/System 2009.08.18 06:43
4. 프로세스간 커널 오브젝트의 공유

(1) 오브젝트 핸들의 상속을 이용하는 방법
오브젝트를 공유하고자 하는 프로세스들이 부모-자식(Parent-Child) 관계를 가질 때에만 사용될 수 있다. 하나 혹은 다수의 커널 오브젝트 핸들이 부모 프로세스에 의해 사용되고 있고, 부모 프로세스가 자식 프로세스가 사용하고 있는 커널 오브젝트에 접근할 수 있도록 해주는 방법이다.


- 오브젝트 핸들의 상속이 정상 동작하기 위해서 수행해 주어야 할것
 ① 부모 프로세스는 커널 오브젝트를 생성할 때 이를 가리키,는 핸들이 상속될 수 있음을 시스템에게 알려주어야 한다. 
※ 윈도우에서는 "오브젝트 상속"이라는 개념은 존재 하지 않는다. 윈도우는 "오브젝트 핸들의 상속"을 지원하지 오브젝트 자체를 상속하지 않는다.

 ② 상속 가능한 핸들을 만들기 위해서는 부모 프로세스가 SECURITY_ATTRIBUTES 구조체를 초기화하고 초기화된 값을 Create함수에 전달해야 한다.
예) Mutex 오브젝트를 생성하고 상속 가능한 핸들을 얻어내는 코드
(기본 보안 해설자(Default Security Descriptor)를 사용하고 상속 가능한 핸들(inheritable handle)을 반환하도록 SECURITY_ATTRIBUTES 구조체를 초기화한다.)
SECURITY_ATTRIBUTES sa;
sa.nLength = sizeof(sa);
sa.lpSecurityDescriptor = NULL;
sa.bInheritHandle = TRUE;// 상속 가능한 핸들을 만듬
HANDLE hMutex = CreateMutex(&sa, FALSE, NULL);

프로세스 핸들 테이블에 저장된 플래그 정보. 각 핸들 테이블 요소는 핸들이 상속 가능한지 여부를 가리키는 플래그 비트(Flag bit)를 가지고 있다.
상속 불가능(플래그 비트는 0이 되는 경우) : 커널 오브젝트를 생성 시 PSECURITY_ATTRIBUTES 매개변수로 NULL을 전달하거나 해당 구조체의 멤버변수인 bInheritHandle에 FALSE로 지정한다.
상속 가능(플래그 비트가 1이 되는 경우) - bInheritHandle 멤버를 TRUE로 지정한다.

표) 두 개의 유효한 요소를 가진 프로세스 핸들 테이블
 인덱스 커널 오브젝트의 메모리 블록을 가리키는 포인터  액세스 마스크(각 비트별 플래그 값을 가지는 DWORD)  플래그 
 1  0xF0000000  0x????????  0x00000000
 2  0x00000000  (N/A)  (N/A)
 3  0xF0000010  0x????????  0x00000001
해당 프로세스는 두 개의 커널 오브젝트(1,3)에 접근 가능하고, 인덱스 1인 핸들은 상속이 불가능하며, 인덱스가 3인 핸들은 상속이 가능하다.

 ③ 부모 프로세스가 자식 프로세스를 생성한다.
매개변수로 TRUE를 전달하면 자식 프로세스는 부모 프로세스의 상속 가능한 핸들 값들을 상속하게 된다. 이 매개변수에 TRUE를 전달하여 CreateProcess 함수를 호출하면 운영체제는 자식 프로세스를 생성한다. 하지만 자식 프로세스가 코드를 바로 수행하는 것을 허용하지 않는다. 물론 여느 프로세스의 생성 절차와 마찬가지로 자식 프로세스의 생성 과정에서 비어 있는 프로세스 핸들 테이블이 만들어진다.
CreateProcess의 bInheritHandles 매개변수에 TRUE를 전달하면 운영체제는 추가적인 작업을 수행한다.
부모 프로세스의 핸들 테이블을 조사하여 상속 가능한 핸들을 찾아낸다.
시스템은 찾아낸 항목들을 자식 프로세스의 핸들 테이블에 복사한다.
복사 위치는 부모 프로세스 핸들 테이블의 위치와 정확히 일치한다.
이렇게 함으로서 특정 커널 오브젝트를 구분하는 핸들 값이 부모 프로세스와 자식 프로세스에 걸쳐 동일한 값을 이용가능하다.
운영체제는 커널 오브젝트 내의 사용 카운트를 증가시킨다.
※ 부모 프로세스나 자식 프로세스의 종료 순서는 무관하다.

표) 부모 프로세스의 상속 가능한 핸들을 상속한 이후의 자식 프로세스의 핸들 테이블
 인덱스 커널 오브젝트의 메모리 블록을 가리키는 포인터  액세스 마스크(각 비트별 플래그 값을 가지는 DWORD)  플래그 
 1  0x00000000  (N/A)  (N/A)
 2  0x00000000  (N/A)  (N/A)
 3  0xF0000010  0x????????  0x00000001

커널 오브젝트의 내용은 운영체제에서 수행되는 모든 프로세스가 공유할 수 있는 커널 주소 공간 상에 저장된다.
32비트 운영체제의 경우  - 0x80000000 ~ 0xFFFFFFFF
64비트 운영체제의 경우 - 0x00000400'00000000 ~ 0xFFFFFFFF'FFFFFFFF

액세스 마스크는 부모 프로세스의 액세스 마스크와 동일하며, 플래그 정보도 동일하다.

※ 자식 프로세스가 bInheritHandle 매개변수를 TRUE로 CreateProcess를 호출하여 자신의 자식 프로세스(GrandChild Process)를 생성하면 생성된 프로세스 또한 동일한 핸들 값, 동일한 액세스 마스크, 동일한 플래그를 상속 받을 것이며, 오브젝트에 대한 사용자 카운트는 증가될 것이다.

※ 오브젝트 핸들 상속을 사용하면 자식 프로세스는 어떤 핸들이 상속된 것인지 알 수 없다. 커널 오브젝트 핸들 상속은 자식 프로세스가 다른 프로세스에 의해 생성될 때 어떤 커널오브젝트에 접근해야 할지 알고 있을 때에 한해서 유용하다.


- 차일드 프로세스가 사용할 커널 오브젝트의 핸들을 전달하는 방법
 ① 차일드 프로세스 수행 시 명령행 인자를 분석하여(보통 _stscanf_s를 사용하여) 핸들 값을 얻어낼 수 있다. 

 ② 프로세스간 통신 방법을 이용하여 부모 프로세스가 자식 프로세스에게 상속한 커널 오브젝트의 핸들을 전달할 수도 있다.
- 부모 프로세스가 자식 프로세스가 완전히 초기화될 때까지 대기한 후에(WaitForInputIdle 함수 이용) 자식 프로세스의 스레드가 생성한 윈도우로 SendMessage 혹은 PostMessage를 한다.
- 부모 프로세스가 환경변수 블록에 상속할 커널 오브젝트에 대한 핸들 값을 가지고 있는 새로운 환경변수를 추가하는 것이다. 변수의 이름은 자식 프로세스를 생성하면 자식 프로세스는 부모 프로세스의 환경변수를 상속하게 되는데, 이때 상속된 오브젝트 핸들 값을 얻기 위해 단순히 GetEnvironmentVariable 함수를 호출하면 된다.


- 핸들 플래그를 변경하는 방법
부모 프로세스가 상속 가능한 커널 오브젝트 핸들을 생성한 이후에 두 개의 자식 프로세스를 생성해야 하고, 이중 하나의 자식 프로세스에게만 커널 오브젝트 핸들을 상속하고 싶을 수도 있다. 바꾸어 말하면, 특정 자식 프로세스만이 커널 오브젝트 핸들을 상속하도록 제어하고 싶을 수 있다. 이런 경우 SetHandleInformation 함수를 이용하여 커널 오브젝트 핸들의 상속 플래그를 변경하면 된다.

BOOL SetHandleInformation(
HANDLE hObject, // 유효한 핸들 값을 전달
DWORD dwMask, // 어떤 플래그를 변경하고자 하는지를 전달한다.
DWORD dwFlags); // 설정하고자 하는  플래그를 전달한다


두 개의 플래그를 동시에 지정하기 위해서는 비트 OR 연산을  사용하면 된다.

#define HANDLE_FLAG_INHERIT 0x00000001
#define HANDLE_FLAG_PROTECT_FROM_CLOSE 0x00000002 // 운영체제에게 이 플래그가 설정된 핸들은 삭제할 수 없음을 알려주는 플래그

디버거(Debuger)가 프로세스를 디버깅 중에 보호된 핸들(Protected Handle)을 종료하려 하면 CloseHandle함수가 예외(Exception)를 발생.

그렇지 않으면 CloseHandle은 단순히 FALSE 값을  반환한다. 핸들을 단순히 삭제되지 않도록 하기 위해 보호된 핸들을 사용하는 것은 흔한 방법이 아니다. 하지만 이 플래그는 자식 프로세스가 또 다른 자식 프로세스를 생성하는 구조를 가진 부모 프로세스를 만들어야 하는 경우에 유용하게 사용될 수 있다. 부모 프로세스가 자식 프로세스를 거쳐 손자 프로세스(GrandChild Process)를 생성하기도 전에 상속받은 핸들을 닫아버리려고 시도할 수도 있다. 이 경우 부모 프로세스는 손자 프로세스와 핸들을 이용한 통신에 실패할 것이다.
이 경우 HANDLE_FLAG_PROTECT_FROM_CLOSE를 지정한 보호된 핸들을 이용하면 그랜드차일드 프로세스가 유효한 핸들을 받을 가능성이 좀 더 증대된다.

※ 자식 프로세스가 다음과 같이 HANDLE_FLAG_PROTECT_FROM_CLOSE 플래그를 해제하고 핸들을 닫아버리는 경우가 생길수 있으니 주의하자.
예)
SetHandleInformation(hObj, HANDLE_FLAG_PROTECT_FROM_CLOSE, 0);
CloseHandle(hObj);

부모 프로세스는 자식 프로세스가 이러한 코드를 수행하지 않을 것을 희망하고, 자식 프로세스가 손자 프로세스를 수행해 줄 것을 희망한다. 하지만 이러한 바람이 그다지 위험하지는 않다.

BOOL GetHandleInformation(
HANDLE hObject,
PDWORD pdwFlags);

이 함수는 지정된 커널 오브젝트 핸들의 플래그 값을 pdwFlags가 가리키는 DWORD 값을 이용하여 얻어온다.
예) 핸들의 상속 여부를 확인 위한 코드
DWORD dwFlags;
GetHandleInformation(hObj, &dwFlags);
BOOL fHandleIsInheritable = (0 != dwFlags & HANDLE_FLAG_INHERIT);



(2) 명명된 오브젝트를 사용하는 방법
- 이름을 가지는 커널 오브젝트
대부분의 커널 오브젝트는 이름을 가질 수 있다.
CreateMutex, CreateEvent, CreateSemaphore, CreateWaitableTimer, CreateFileMapping, CreateJobObject
이 함수들은 공통적으로 마지막 매개변수로 'PCTSTR pszName'을 가진다.
이 매개변수에 NULL을 전달하면 명명되지 않은(익명의) 커널 오브젝트를 생성하게 된다.
'\0'로 끝나는 문자열을 가리키는 주소를 전달시 오브젝트의 이름을 지정할 수 있다. (최대 MAX_PATH(260) 길이)

※ 서로 다른 타입의 커널 오브젝트라고 하더라도 동일한 네임스페이스를 공유하기 때문에 주의해야 한다.
"MyObj"라는 이름의 오브젝트를 생성할 때 이미 "MyObj"라는 이름의 오브젝트가 존재할 수도 있고 존재하지 않을 수도 있다. 예) 다른 타입의 커널 오브젝트가 동일한 이름의 다른 커널 오브젝트를 호출한 경우
HANDLE hMutex = CreateMutex(NULL, FALSE, TEXT("MyObj"));
HANDLE hSem = CreateSemaphore(NULL, 1, 1,  TEXT("MyObj"));
DWORD dwErrorCode = GetLastError();
// dwErrorCode 에 ERROR_INVALID_HANDLE(6)이 반환됨을 알 수 있다. 이러한 에러 코드는 에러 상황을 정확하게 설명하고 있지도 못할 뿐더러 어떻게 에러를 수정해야 할지도 알기 어렵다.


- 오브젝트를 공유하는 방법

 ① HANDLE hMutextProcessA = CreateMutex(NULL, FALSE, TEXT("MyMutex");
A프로세스가 수행되어 다음과 같은 함수를 호출한다. 이 함수 호출은 새로운 뮤텍스 커널 오브젝트를 생성하여 "MyMutex"라고 명명한다. A 프로세스의 핸들인 hMutexProcessA 핸들은 상속 가능 핸들이 아님을 주의해야 한다. 명명된 커널 오브젝트를 생성할 때에는 상속 가능한 핸들을 생성할 필요가 없다.

 ② HANDLE hMutexProcessB = CreateMutex(NULL, FALSE, TEXT(MyMutex");
 B프로세스가 수행되면 다음과 같은 코드를 수행한다. B프로세스 수행시 B프로세스는 A프로세스의 자식 프로세스일 필요는 없으며, 윈도우 탐색기나 다른 애플리케이션에 의해 수행될 수도 있다. B프로세스는 핸들 상속 대신 명명된 오브젝트의 이점을 사용할 것이기 때문에 굳이 A프로세스의 자식 프로세스일 필요는 없다.

 ③ B프로세스가 CreateMutexd를 호출하게 되면 운영체제는 먼저 MyMutex라는 이름의 커널 오브젝트가 존재하는지 확인한다.

 ④동일 이름의 오브젝트가 존재한다면, 다음으로 오브젝트의 타입을 확인한다. 왜냐하면 "MyMutex"라는 이름의 뮤텍스를 생성하려는 것이기 때문에 이미 생성된 오브젝트의 타입도 뮤텍스이어야 할 것이다.

 ⑤ 운영체제는 B프로세스가 오브젝트에 대한 최대 접근 권한을 가지고 있는지 확인한다. 만일 그렇다면 운영체제는 B프로세스의 핸들 테이블 상에 비어 있는 항목을 추가하고 이미 존재하고 있던 커널 오브젝트를 가리키도록 설정한다.
 
 ⑥ 만일 오브젝트의 타입이 일치하거나 접근 권한이 없는 경우 CreateMutex는 실패하고 NULL을 반환한다.

※ B프로세스가 CreateMutex 호출에 성공한다 하더라도 실제로는 새로운 뮤텍스가 생성되는 것이 아니라 기존의 뮤텍스 오브젝트에 접근할 수 있는 B프로세스 고유의 핸들 값이 생성될 뿐이다. B프로세스의 핸들 테이블을 이러한 오브젝트를 참조하는 항목을 가지고 있으며, 뮤텍스 오브젝트의 사용 카운트 값은 증가될 것이다. 이제 뮤텍스 오브젝트는 A프로세스와 B 프로세스 양쪽 모두에서 관련 핸들을 삭제할 때까지 파괴되지 않는다. A 프로세스와 B프로세스 각각은 자신만의 핸들 값을 가지고 동일한 뮤텍스 커널 오브젝트를 사용하게 되므로 이러한 동작은 적절하다.

※ 커널 오브젝트 생성 함수는 항상 커널 오브젝트에 대한 최대 접근 권한을 가지고 있는 핸들을 반환한다. 만일 핸들에 대한 가용 접근 권한을 제한하고 시퓨다면 커널 오브젝트 생성 함수의 확장 버전(Ex가 붙은)을 사용하면 된다. 이러한 함수들은 dwDesiredAcess라는 DWORD 타입의 매개변수를 추가적으로 필요로 한다. 예를 들어 CreateSemaphoreEx 함수를 호출 시 SEMAPHORE_MODIFY_STATE의 사용 여부에 따라 ReleaseSemaphore의 호출이 가능하거나 불가능한 핸들을 생성할 수 있다. [각 커널 오브젝트별로 사용 가능한 값의 목록(윈도우 SDK문서)]


- 명명된 오브젝트가 이미 생성되어 있는 경우에 활용할 수 있는 함수
OpenMutex, OpenEvent, OpenSemaphore, OpenWaitableTimer, OpenFileMapping, OpenJobObject
DWORD dwDesireAcess, BOOL bInheritHandle, PCTSTR pszName
모든 함수들이 동일한 원형을 가지고 있다. 마지막 매개변수인 pszName은 커널 오브젝트의 이름을 지정하는데 사용된다. 이 값으로 NULL을 사용해서는 안되며, 반드시 '\0'로 끝나는 문자열을 지정해야 한다.

이러한 함수들은 커널 오브젝트를 위한 단일의 네임 스페이스(Name Space)내에서 검색을 시도한다.

지정된 이름의 오브젝트를 발견하지 못할 경우 : NULL을 반환, GetLastError는 ERROR_FILE_NOT_FOUND(2)를 반환한다.
커널 오브젝트가 존재하지만 타입이 다를 경우 : NULL을 반환, GetLastError는 ERROR_INVALID_HANDLE(6)을 반환한다.

타입까지 일치하면 다음으로 (dwDesiredAcess 매개변수를 통해) 요청된 접근 권한이 허가되는지를 확인한다. 그렇다면 함수를 호출한 프로세스의 핸들 테이블이 갱신되고, 커널 오브젝트의 사용 카운트가 증가된다. 만일 bInheritHandle 매개변수로 TRUE를 넘겨준 경우 반환되는 핸들은 상속 가능 핸들이 될 것이다.

Create* 함수와 Open* 함수 사이의 주요 차이점은 커널 오브젝트가 존재하지 않는 경우에 Create*는 새로운 커널 오브젝트를 생성하지만 Open은 실패한다.

앞서 언급한 바와 같이 마이크로소프트는 어떻게 유일한 오브젝트의 이름을 생성할지에 대한 어떠한 명명규칙도 제공하지 않는다. 따라서 서로 다른 회사에서 두 개의 프로그램이 "MyObject"라는 이름의 오브젝트를 동시에 생성하려 하면 문제가 발생할 수 있다. 유일한 이름을 구성하기 위해 오브젝트 이름을 [GUID 값이나 GUID를 문자열로 변경한 값]을 사용하는 것이 좋다.
다른 방법으로는 [사설 네임 스페이스를 사용하는 방법]도 있다.

※ 명명된 오브젝트는 동일한 애플리케이션이 여러 번 수행되지 못하도록 하기 위해서도 자주 사용된다. 이를 위해 _tmain이나 _tWinMain 함수에서 Create*류의 함수를 호출하여 명명된 오브젝트를 생성한다(어떤 타입의 오브젝트를 생성하는지는 문제가 되지 않는다.) Create*류의 함수가 반환되면 GetLastError를 호출해서 그 결과가 ERROR_ALREADY_EXISTS라면 이미 동일한 애플리케이션이 수행중이라고 판단할 수 있으며, 이 경우 새로 수행된 애플리케이션을 종료하면 된다. 이러한 동작 방식을 설명하기 위한 코드를 아래에 나타냈다.


- 터미널 서비스 네임스페이스
 터미널 서비스의 경우 앞선 시나리오와 조금의 차이가 있다. 터미널 서비스를 수행하는 머신은 커널 오브젝트에 대해 다수의 네임스페이스를 가진다. 모든 터미널 서비스 클라이언트 세션(Client Session)에서 접근 가능한 커널 오브젝트를 위한 전역 네임스페이스(Global namespace)가 있는데, 이는 주로 서비스 타입의 애플리케이션에 의해 사용된다. 이와는 별도로 각 클라이언트 세션은 자기만의 고유 네임스페이스를 가진다. 이러한 구성으로 인해 두 개 혹은 다수의 세션에서 동일한 애플리케이션이 각기 수행될지라도 서로간에 영향을 미치지 않게 된다. 하나의 세션은 설사 오브젝트의 이름이 같은 경우라 하더라도 다른 세션의 오브젝트에 접근할 수 없다. 이러한 시나리오는 서버 머신에서만 적용되는 내용이 아니라 리모트 데스크톱(Remote Desktop)이나 빠른 사용자 전환(Fast User Switching)에서도 동일하게 적용된다.




(3) 오브젝트 핸들의 복사를 이용하는 방법
BOOL DuplicateHandle(
HANDLE hSourceProcessHandle,
HANDLE hSourceHandle,
HANDLE hTargetProcessHandle,
PHANDLE phTargetHandle,
DWORD dwDesiredAccess,
BOOL bInheritHandle,
DWORD dwOptions);

단순히 말하자면, DuplicateHandle 함수는 특정 프로세스 핸들 테이블 내의 항목을 다른 프로세스 핸들 테이블로 복사하는 함수다. DuplicateHandle은 여러 개의 매개변수를 취하지만 매우 직관적이다. DuplicateHandle 함수는 3개의 서로 다른 프로세스가 수행 중인 경우에도 사용될 수 있다.

DuplicateHandle 함수를 사용하려면 첫 번째와 세 번째 매개변수인 hSourceProcessHandle과 hTargetProcessHandle에 프로세스 커널 오브젝트의 핸들을 넘겨주어야 한다. 이러한 핸들은 DuplicateHandle 함수를 호출하는 프로세스와 연관되어 있는 프로세스들일 것이다. 추가로, 이 두 매개변수에는 반드시 프로세스 커널 오브젝트에 대한 핸들을 전달해야 한다. 만일 다른 타입의 커널 오브젝트를 전달하면 이 함수는 실패하게 된다.

두 번째 매개변수인 hSourceHandle 로는 어떤 타입의 커널 오브젝트라도 전달할 수 있으며, DuplicateHandle 함수를 호출한 프로세스와 아무런 연관성을 가지지 않는다. 대신 hSourceProcessHandle 매개변수로 지정된 핸들 값이 가리키는 프로세스에서만 의미를 가지는 프로세스 고유의 값이다. 네 번째 매개변수인 phTargetHandle로는 HANDLE 변수의 주소 값을 전달하게 되며, 함수 호출 이후에 hTargetProcessHandle 값이 가리키는 프로세스에서만 사용될 수 있는 고유의 핸들 값을 전달받게 된다. 물론 이 값은 소스 핸들의 복사본이다.

DuplicateHandle 의 마지막 3개의 매개변수에는 타깃 프로세스의 고유의 커널 오브젝트 핸들이 가진 속성 정보인 액세스 마스크와 상속 플래그의 값을 지정하게 된다.
dwOptions 매개변수는 0 혹은 DUPLICATE_SAME_ACCESS와 DUPLICATE_CLOSE_SOURCE의 조합으로 지정 될 수 있다.
DUPLICATE_SAME_ACCESS : 타깃 핸들이 소스 프로세스의 핸들과 동일한 액세스 마스크를 가지기를 원한다는 사실을 DuplicateHandle에게 알려주게 된다. 이 플래그를 사용하면 dwDesiredAcess 매개변수는 무시된다.

DUPLICATE_CLOSE_SOURCE : 소스 프로세스의 핸들을 삭제한다. 이 플래그를 사용하면 하나의 프로세스에서 다른 프로세스로 쉽게 커널 오브젝트를 이동시킬 수 있으며, 커널 오브젝트의 사용 카운트에는 영향을 주지 않는다.

예) DuplicateHandle 함수가 어떻게 동작하는 과정
S프로세스는 몇몇 커널 오브젝트에 접근 권한을 가진 소스 프로세스라고 하고, T프로세스는 S프로세스가 소유한 커널 오브젝트에 접근하기 위한 타깃 프로세스라고 하자. C프로세스는 중계 역할을 하는 프로세스로, DuplicateHandle 함수를 호출하게 될 것이다. 예제에서는 DuplicateHandle 함수가 어떻게 동작하는지를 설명하기 위해 핸들 값은 하드코드된 숫자를 사용할 것이다. 실제로는 다양한 핸들 값이 사용될 수 있으며, 이 값은 함수에 인자로 전달될 것이다.

C프로세스의 핸들 테이블은 두 개의 핸들 값을 가지고 있다. 인덱스 값이 1인 커널 오브젝트는 S프로세스에서 사용하는 커널 오브젝트이고, 인덱스 값이 2인 커널오브젝트는 T프로세스에서 사용하는 커널 오브젝트이다.

[표] C프로세스의 핸들 테이블

 인덱스 커널 오브젝트의 메모리 블록을 가리키는 포인터   액세스 마스크(각 비트별 플래그 값을 가지는 DWORD)  플래그
 1  0xF0000000  0x????????  0x00000000
 2  0xF0000000  0x????????  0x00000000

[표] S프로세스의 핸들 테이블
 인덱스 커널 오브젝트의 메모리 블록을 가리키는 포인터   액세스 마스크(각 비트별 플래그 값을 가지는 DWORD)  플래그
 1  0x00000000  (N/A)  (N/A)
 2  0xF0000020(다른 커널 오브젝트)  0x????????  0x00000000


[표] DuplicateHandle 호출 이전의 T 프로세스의 핸들 테이블
 인덱스 커널 오브젝트의 메모리 블록을 가리키는 포인터   액세스 마스크(각 비트별 플래그 값을 가지는 DWORD)  플래그
 1  0x00000000  (N/A)  (N/A)
 2  0xF0000020(다른 커널 오브젝트)  0x????????  0x00000000

DuplicateHandle(1, 2, 2, &hObj, 0, TRUE, DUPLICATE_SAME_ACCESS);

[표] DuplicateHandle 호출 이후의 T 프로세스 핸들 테이블
 인덱스 커널 오브젝트의 메모리 블록을 가리키는 포인터   액세스 마스크(각 비트별 플래그 값을 가지는 DWORD)  플래그
 1  0xF0000020  0x????????  0x00000001
 2  0xF0000030(다른 커널 오브젝트)  0x????????  0x00000000


커널 오브젝트의 상속 방법과 마찬가지로 DuplicateHandle 함수의 단점 중의 하나는 T 프로세스가 새로운 커널 오브젝트에 접근 가능하게 되었다는 사실을 전혀 통보받지 못한다는 것이다. 따라서 C프로세스는 T프로세스에게 어떤 방법으로든 새로운 커널 오브젝트에 접근이 가능해졌다는 사실을 알려주어야 한다. 이때 프로세스간 통신 방법을 이용하여 T프로세스에게 hObj 변수가 가지고 있는 핸들 값을 전달해야 한다. T프로세스는 이미 수행 중인 프로세스이므로 명령행 인자를 사용하거나 환경변수를 바꾸는 것과 같은 방법은 사용될 수 없다.대신 윈도우 메시지 프로세스간 통신 메커니즘(InterProcess Communication)을 사용하면 된다.
신고

'Windows > System' 카테고리의 다른 글

04. 프로세스 - CreateProcess  (0) 2009.08.21
03. 프로세스  (0) 2009.08.21
02. 커널 오브젝트 下  (0) 2009.08.18
01. 커널 오브젝트 上  (3) 2009.08.18

01. 커널 오브젝트 上

Windows/System 2009.08.18 04:20

1. 커널(Kernel)
 -  커널 
 원래의 뜻은 곧식의 낟알, 알맹이이라는 뜻으로, 그만큼 운영체제(OS)의 가장 기본적이고 핵심이 되는 부분이기 때문에 코어(Core)라고 불리기도 한다.
 운영체제는 커널기반 위에 외부와의 통신을 가능하게 하는 입출력 프로그램, 기본 사용자 인터페이스, 하드웨어 드라이버등이 덧붙여서 완성된다.

- 역할
 ① 자원관리 : 한정된 시스템 자원을 효율적으로 관리하여 프로그램의 실행을 원할하도록 돕는다. 프로세스에 자원을 분배하는 것을 스케줄링이라고 한다.
 ② 보안 : 하드웨어와 프로세스간의 보안을 책임진다.
 ③ 추상화 : 운영체제의 내부를 감추고 일관성 있는 하드웨어를 제공하기 위해 하드웨어 추상화(같은 종류의 장비에 대한 공통 명령어의 집합)들로 구현된다.



2. 커널 오브젝트(Kernel Object)
- 커널 오브젝트
 커널(Kernel)에 의해 할당된 단순한 메모리 블록이다. 이 메모리 블록은 생성한 커널에 의해서만 접근이 가능한 구조체로 구성되어 있으며, 애플리케이션이 구조체를 옮기거나 내용을 변경할 수 없다.


 - 종류
Event Object, File Object, File-Mapping Object, Mutex Object, Process Object, Semaphore Object, Thread Object.. 등이 있고 운영체제에서는 이러한 커널오브젝트를 생성하고 조작한다.


- 데이터 구조체
 커널 오브젝트에 대한 정보를 저장하는 구조체이다. 커널 오브젝트에 대한 세부 정보들을 저장하고 있다.
커널 오브젝트간에는 공통적으로 존재하는 값(Security Descriptor, Usage Count 등)도 있지만 대부분의 값들은 각 오브젝트별로 독특하다.  예를 들어 Process Object는 Process ID, Base Priority, Exit Code를 가지지만 File Object는 Byte Offset, Sharing Mode, Open Mode를 가진다.
 커널에 의해서만 접근이 가능하기 때문에 애플리케이션에서 데이터 구조체가 저장되어 있는 메모리 위치를 직접 접근하여 그 내용을 변경하는 것은 불가능하다. 마이크로소프트는 커널 오브젝트의 구조체가 가능한 한 일관되게 유지될 수 있도록 하기 위해 이러한 제약사양을 의도적으로 만들어두었다. 이렇게 구조체에 대한 직접적인 접근을 제한함으로서 마이크로소프트는 이미 개발되어 있는 애플리케이션에 영향을 미치지 않고도 구조체에 내용을 임의로 추가, 삭제, 변경할 수 있다.


- 커널 오브젝트의 조작
커널 오브젝트 구조체의 변경을 위해 MS는 정제된 방법을 통해 구조체의 내용에 접근할 수 있도록 일련의 함수 집합을 제공 하고 있는데 이를 통해 커널 오브젝트의 내부적인 값에 접근할 수 있다.

 ① 커널 오브젝트를 생성하는 함수를 호출 시에 생성된 오브젝트에 대한 핸들을 반환한다.
 ② 핸들은 프로세스 내의 모든 스레드에 의해 사용 가능한 값이지만 특별한 의미를 가지고 있지는 않다. 
 ③ 운영체제는 매개변수로 전달된 핸들 값들을 통해 어떤 커널 오브젝트를 조작하고자 하는지 구분할 수 있다.

※ 핸들 값은 프로세스별로 독립된 프로세스 핸들 테이블이 존재한다. 따라서 동일한 핸들 값이라도 전혀 다른 커널 오브젝트를 참조할 수 있기 때문에 이 핸들 값을 다른 프로세스에 전달하여 사용하는것은 불가능하다.


- 사용 카운트(Usage Count) 
 모든 커널 오브젝트 타입이 가지고 있는 공통적인 값이다. 커널 오브젝트는 프로세스(Process)가 아니라 커널에 의해 소유되기 때문에 프로세스가 특정함수를 통해 커널 오브젝트를 생성한 후 종료된다고 하더라도 반드시 생성된 커널 오브젝트가 프로세스와 함께 삭제되는 것은 아니다.

사용 카운트가 변경되는 경우
 ① 커널 오브젝트가 최초로 생성되면 사용카운트는 1로 설정
 ② 다른 프로세스가 생성된 커널 오브젝트에 대한 접근 권한을 획득하면 사용카운트가 증가된다.
 ③ CloseHandle()함수 호출시 사용카운트가 감소된다.
 ④ 프로세스가 종료시에 커널 오브젝트가 아직 열려 있는 모든 커널오브젝트에 대하여 자동적으로 사용 카운트를 감소시킨다.
 ⑤ 사용 카운트가 0까지 내려가면 이 오브젝트는 커널에 의해 삭제된다.


- 보안 해설자(Security Descriptor)
커널 오브젝트는 보안 해설자로 보호될 수 있다. 주로 서버 애플리케이션을 개발할 때 많이 사용된다.
대부분의 커널 오브젝트 생성 함수들은 SECURITY_ATTRIBUTES 구조체에 대한 포인터를 인자로 받아들인다. 
 일반적으로 커널 오브젝트를 기본 보안 설정으로 생성하기 위해 NULL값을 파라미터로 설정하여 함수를 호출한다. 기본 보안 설정은 관리자 그룹의 모든 구성원과 커널 오브젝트를 생성한 프로세스가 이 커널 오브젝트에 대한 모든 접근 권한을 갖도록 한다.

보안해설자가 가지고 있는 정보
 ① 커널 오브젝트의 소유자
 ② 커널 오브젝트를 사용하고 있는 그룹과 사용자
 ③ 접근이 제한된 그룹과 사용자

typedef struct _SECURITY_ATTRIBUTES { // sa 
    DWORD  nLength;					// 버전확인을 위한 정보
    LPVOID lpSecurityDescriptor;	// 초기화된 SD 주소
    BOOL   bInheritHandle;			// 상속을 위한 멤버 변수
} SECURITY_ATTRIBUTES; 

 ※ 커널 오브젝트 이외에도 유저 오브젝트(Menu, Window, Cursor, Icon, Brush, Font..), GDI(Graphic Device Interface) 오브젝트가 존재한다. 커널오브젝트인지의 여부를 결정하는 간단한 방법은 오브젝트를 생성하는 함수에서 보안 특성을 지정하는 매개변수의 존재여부를 확인하는 것이다.

3. 핸들 테이블(Handle Table)
 프로세스가 초기화되면 운영체제는 프로세스를 위해 커널 오브젝트 핸들 테이블을 할당한다. 이러한 핸들 테이블은 사용자 오브젝트나 GDI 오브젝트에 의해서는 사용되지 않고 유일하게 커널 오브젝트에 의해서만 사용된다.

 인덱스 커널 오브젝트의 메모리 블록을 가리키는 포인터  액세스 마스크(각 비트별 플래그 값을 가지는 DWORD)  플래그 
   0x????????  0x????????  0x???????? 
 2  0x????????  0x????????   0x????????
 ..  ...  ...   ... 


- 커널 오브젝트 생성
 ① 프로세스가 최초로 초기화되면 프로세스의 핸들 테이블은 비어 있다.
 ② 프로세스 내의 스레드가 커널 오브젝트를 생성하는 함수를 호출하면 커널은 커널 오브젝트를 위한 메모리 블록을 할당하고 초기화한다.
 ③ 커널은 프로세스의 핸들 테이블을 조사하여 비어 있는 공간을 찾아낸다.
 ④ 핸들 테이블의 빈 공간에 초기화를 수행한다.
 ⑤ 커널 오브젝트를 생성하는 함수는 고유한 핸들 값을 반환한다.

이러한 핸들 값을 4로 나누면(윈도우가 내부적으로 사용하고 있는 하위 2비트를 무시하기 위해 오른쪽으로 2를 시프트) 커널 오브젝트에 대한 정보를 저장하고 있는 프로세스 핸들 테이블의 인덱스 값을 얻을 수 있다. 유효한 커널 오브젝트 핸들 값은 4부터 시작된다.

커널 오브젝트 핸들을 인자로 취하는 함수를 호출할 때에는 항상 Create* 류의 함수 중 하나를 호출하여 반환된 핸들 값을 전달해야 한다. 내부적으로 이러한 함수들은 프로세스 핸들 테이블로부터 사용하고자 하는 커널 오브젝트의 실제 주소를 얻어낸 후 잘 정의된 방식으로 커널 오브젝트의 자료 구조를 변경한다.

 ※ 유효하지 않은 핸들을 사용시 함수는 실패하고 GetLastError 호출 결과로 6(ERROR_INVALID_HANDLE)을 반환한다. 
 ※ 커널 오브젝트를 생성하는 함수가 실패하면 반환되는 핸들 값은 보통 NULL(0)이 된다. 시스템의 가용 메모리가 매우 작거나 보안 문제로 인해서 함수가 실패하는 경우 몇몇 함수들은 INVALID_HANDLE_VALUE(-1) 값을 반환 한다. 따라서 커널 오브젝트를 생성하는 함수의 반환 값을 확인할때 주의해야 한다.


- 커널 오브젝트 삭제
프로젝트를 어떻게 생성했는지와 상관없이 CloseHandle 함수를 호출하여 더 이상 커널 오브젝트를 사용하지 않을 것임을 시스템에게 알려줄 수 있다.
BOOL CloseHandle(HANDLE hObject);

CloseHandle함수를 호출하지 않더라도 프로세스 종료시 운영체제는 사용 카운트를 감소시키기고 프로세스가 사용하던 모든 자원을 반환하여 누수가 발생하지 않지만 프로세스가 실행중인 경우에는 자원의 낭비가 발생할 수 있다.

CloseHandle 함수 호출시
 ① 프로세스의 핸들 테이블을 검사하여 전달받은 핸들 값을 통해 실제 커널 오브젝트에 접근 가능한지를 확인한다. 
 ② 핸들이 유효한 값이고 시스템이 커널 오브젝트의 자료 구조를 획득하게 되면, 구조체 내의 사용 카운트를 감소시킨다.
 ③ 사용 카운트가 0이 되면 이 커널 오브젝트는 메모리에서 삭제된다.

프로세스 종료시
 ① 자동으로 프로세스 핸들 테이블을 검색한다.
 ② 프로세스 종료 이전에 닫지 않은 커널 오브젝트의 핸들 값이 운영체제에 의해 자동으로 닫혀진다.
 ③ 사용 카운트가 0이 되면 이 커널 오브젝트는 메모리에서 삭제된다.

 ※ 유효하지 않은 핸들로 CloseHandle함수 호출시 프로세스는 정상적으로 수행되고 FALSE를 반환한다. GetLastError호출시 ERROR_INVALID_HANDLE을 반환한다. 디버깅시에는 에러를 디버깅할 수 있도록 0xC0000008("An invalid handle was specified")예외가 발생한다.

 ※ CloseHandle을 호출한 이후에는 핸들 값 자체도 NULL로 초기화하는 것이 좋다. 만약 초기화하지 않는다면 에러를 반환할수도 있지만 나쁜 경우로 핸들테이블에 다른 커널 오브젝트를 가리키는 경우에는 적절하지 않은 커널 타입을 전달했다는 에러를 유발하거나 새로 생성된 커널 오브젝트가 삭제했던 오브젝트와 동일한 타입일 경우 충돌을 일으킬 수가 있다.



참고 : 위키피디아, Windows VIA C/C++,

신고

'Windows > System' 카테고리의 다른 글

04. 프로세스 - CreateProcess  (0) 2009.08.21
03. 프로세스  (0) 2009.08.21
02. 커널 오브젝트 下  (0) 2009.08.18
01. 커널 오브젝트 上  (3) 2009.08.18


티스토리 툴바