커널 오브젝트와 오브젝트 핸들

2025. 3. 17. 23:44개발/Windows

이 글은 윤성우 님의 '뇌를 자극하는 윈도우즈 시스템 프로그래밍'을 참고하여 공부한 내용입니다.


CreateProcess 함수를 실행하면 인자로 전달한 PROCESS_INFORMATION 구조체에 생성한 프로세스에 대한 커널 오브젝트의 핸들이 반환됩니다. 이때 커널 오브젝트는 무엇일까요?

 

Kernel Object

Windows 운영체제에서 관리하는 프로세스, 스레드, 파일 등과 같은 자원들을 데이터 블록으로 저장한 것을 커널 오브젝트라 부릅니다. 이 커널 오브젝트를 통해 프로세스와 스레드는 해당 자원에 접근할 수 있습니다. 그리고 프로세스와 스레드가 커널 오브젝트에 접근할 수 있게 해주는 것이 핸들(Handle)입니다.

 

Handle

typedef void *HANDLE;

 

핸들은 커널 오브젝트에 접근하기 위한 식별자입니다. HANDLE은 void * 자료형이지만, 직접적으로 커널 오브젝트의 주소를 가리키는 것이 아닌 운영체제에서 관리하는 정수형 식별자입니다. 그리고 운영체제는 핸들과 커널 오브젝트의 위치를 내부적으로 매핑하여 관리합니다. 그래서 프로세스는 핸들을 통해 해당 커널 오브젝트에 접근할 수 있습니다.

 

핸들은 프로세스 별로 각각 독립적입니다. 만약 서로 다른 프로세스가 동일한 객체를 열어 핸들을 가져온다면, 각각의 핸들은 같은 커널 오브젝트를 참조하지만 서로 다른 식별자를 가집니다. 

 

핸들은 위에서 말한 특성을 사용하여 커널 오브젝트에 대한 보안과 안정성을 높입니다. 커널 오브젝트는 운영체제의 중요한 자원이기 때문에 사용자가 임의로 수정해서는 안됩니다. 그래서 실제 메모리 주소가 아닌 핸들을 사용하여 간접적으로 커널 오브젝트에 접근할 수 있게 한 것입니다.

 

또한 해당 커널 오브젝트에 대한 권한을 제한한 핸들을 반환하거나 혹은 핸들 자체를 반환하지 않아, 특정 행동을 금지시켜 보안성을 높일 수 있습니다. 마지막으로 커널 오브젝트의 종류(파일, 프로세스, 이벤트, 동기화 등)에 상관없이 모두 핸들로 접근할 수 있기 때문에 개발자에 대한 편의성을 제공합니다.

 

아래 그림은 CreateProcess 함수를 통해 핸들과 커널오브젝트 생성되는 과정을 보여줍니다.

 

CreateProcess function

함수가 실행되면 가장 먼저 운영체제에서 커널 오브젝트를 생성합니다. 그다음 생성한 커널 오브젝트를 기반으로 프로세스를 생성합니다. 마지막으로 해당 커널 오브젝트에 대한 핸들을 반환합니다. 이 과정을 통해 Process1은 Process2에 핸들을 통해 접근할 수 있게 됩니다. 예시를 통해 자세한 내용을 확인하겠습니다.

 

Oper1.cpp

#include <iostream>
#include <Windows.h>
#include <tchar.h>

int main()
{
    TCHAR cmd[] = TEXT("Oper2.exe");
    STARTUPINFO si = { 0, };
    PROCESS_INFORMATION pi;
    HANDLE handle = GetCurrentProcess();

    if (!SetProcessAffinityMask(handle, 1)) {
        std::cerr << "Failed SetProcessAffinityMask! error code: " << GetLastError() << "\n";
    }

    si.cb = sizeof(si);

    if (CreateProcess(
        NULL,
        cmd,
        NULL, NULL, TRUE,
        0,
        NULL,
        NULL,
        &si, &pi)) {

        DWORD age = 0;
        while (1)
        {
            for (DWORD i = 0; i < 10000; ++i)
                for (DWORD j = 0; j < 10000; ++j);
                    
            _fputts(TEXT("Operation1.exe\n"), stdout);
            age += 1;
            if (age == 2)
                SetPriorityClass(pi.hProcess, IDLE_PRIORITY_CLASS);

           
        }
        CloseHandle(pi.hProcess);
        CloseHandle(pi.hThread);
    }
    else {
        std::cerr << "Failed CreateProcess! error code: " << GetLastError() << "\n";
    }
   
    return 0;
}

 

Oper2.cpp

#include <iostream>
#include <Windows.h>
#include <tchar.h>

int main()
{
    HANDLE handle = GetCurrentProcess();
    SetPriorityClass(handle, HIGH_PRIORITY_CLASS);

    if (SetProcessAffinityMask(handle, 1)) {

        while (1)
        {
            for (DWORD i = 0; i < 10000; ++i)
                for (DWORD j = 0; j < 10000; ++j);

            _fputts(TEXT("Operation2.exe\n"), stdout);
        }

    } else {
        std::cerr << "Failed SetProcessAffinityMask! error code: " << GetLastError() << "\n";
    }
    return 0;
}

 

Oper1.exe Running

Operation1.exe
Operation2.exe
Operation2.exe
Operation2.exe
Operation2.exe
Operation2.exe
Operation2.exe
Operation2.exe
....
Operation2.exe
Operation2.exe
Operation1.exe
Operation2.exe
Operation1.exe
Operation1.exe
Operation1.exe
Operation1.exe
Operation1.exe

 

Flow diagram

 

위 예제는 핸들을 통해 커널 오브젝트에 접근하여 Oper2 프로세스의 우선순위를 2번 변경하는 코드입니다. 처음 Oper1 프로세스가 실행되며 SetProcessAffinityMask 함수를 사용하여 CPU 0번 프로세서를 선호 프로세서 등록하였습니다. 이 선호 프로세서는 비트마스킹을 통해 선택되며, 예제에서는 정수 1을 넣어 가장 최하단의 비트를 1로 만들어 CPU 0번 프로세서를 선호 프로세서로 선택하였습니다.

 

선호 프로세서가 선정되면 운영체제의 스케줄링에 의해 다른 프로세서가 유휴상태여도 선호 프로세서 위주로 작업을 스케줄링합니다. 즉, 현대의 컴퓨터는 멀티코어가 기본이기 때문에, 위 예시와 같이 우선순위에 의한 프로세스 선점 현상을 테스트하기 위해 SetProcessAffinityMask 함수를 통해 싱글코어 환경을 구성한 것입니다.

 

이후 CreateProcess를 통해 Oper2 프로세스를 생성하고 반복출력문을 실행합니다. 생성된 Oper2 프로세스도 동일하게 SetProcessAffinityMask 함수를 통해 CPU 0번 프로세스를 선호 프로세서로 선정합니다. 그리고 SetPriorityClass 함수를 사용하여 자신의 우선순위를 높은 우선순위로 설정합니다. 이후 Oper1과 동일하게 반복출력문을 실행합니다.

 

콘솔에 출력되는 것을 확인해 보니 처음에는 Oper2의 출력이 연속됩니다. 이것은 Oper2 프로세스의 우선순위가 높기 때문에 프로세서를 선점한 것을 보입니다. 그 뒤 Oper1의 조건문에 의해 Oper2의 우선순위를 낮은 우선순위로 설정합니다. 이후에는 Oper1의 우선순위가 높아 프로세서를 선점하여 Oper1의 출력이 연속됩니다.

 

위 예제는 핸들을 사용해 커널 오브젝트를 수정하고 프로세서의 우선순위를 확인해 보는 예제이기도 하지만, 중요한 내용은 프로세스와 커널 오브젝트의 관계입니다. 프로세스는 자신이 아닌 다른 프로세스의 커널 오브젝트에도 접근할 수 잇었습니다. 즉 프로세스는 커널 오브젝트를 사용하지만, 커널 오브젝트를 소유한 것은 아니라는 것입니다.

 

처음에 설명했듯이 커널 오브젝트는 운영체제에서 자원을 관리하기 위해 사용되는 데이터 블록입니다. 그래서 커널 오브젝트는 운영체제에 종속됩니다. 그렇다면 커널 오브젝트는 언제 소멸할까요.

 

Usage Count

프로세스와 스레드는 위에서 했던 것처럼 커널 오브젝트를 공유하여 접근할 수 있습니다. 이때 커널 오브젝트를 사용 혹은 참조하고 있는 수를 Usage Count라고 합니다. std::shared_ptr과 같이 Usage Count가 0이 되면 운영체제에서 커널 오브젝트를 자동으로 제거합니다.

#include <windows.h>
#include <iostream>

int main() {
    WCHAR cmd[] = TEXT("cmd");
    DWORD state;
    STARTUPINFO si = { 0, };
    PROCESS_INFORMATION pi;

    si.cb = sizeof(si);

    if (CreateProcess(
        NULL,
        cmd,
        NULL, NULL, FALSE,
        CREATE_NEW_CONSOLE,
        NULL,
        NULL, 
        &si, &pi)) {

        std::cout << "Success CreateProcess! PID: " << pi.dwProcessId << "\n";

        WaitForSingleObject(pi.hProcess, INFINITE);
        GetExitCodeProcess(pi.hProcess, &state);
        if (state == STILL_ACTIVE)
            std::cout << "STILL_ACTIVE\n";
        else
            std::cout << "state: " << state << "\n";

        CloseHandle(pi.hProcess);
        CloseHandle(pi.hThread);
    }
    else {
        std::cerr << "Failed CreateProcess! error code: " << GetLastError() << "\n";
    }

    return 0;
}

생성된 cmd에 exit 115 명령을 입력하여 종료

 

Success CreateProcess! PID: 12264
state: 115

 

위 예시는 CreateProcess 함수를 통해 cmd를 실행하고 반환값을 조회하는 코드입니다. 가장 인상적인 부분은 GetExitCodeProcess 함수입니다. 자식 프로세스 cmd가 종료되었지만 WaitForSingleObject 함수 이후 종료코드를 반환받은 것입니다. 즉, 프로세스는 소멸하여도 해당 커널 오브젝트는 남아 있어 프로세스의 상태를 조회하고 종료코드를 반환받을 수 있었던 것입니다.

 

Create Process

 

CreateProcess 함수를 사용하여 프로세스를 생성하면, 부모 프로세스는 자식 프로세스에 대한 커널 오브젝트의 핸들을 얻게 됩니다. 이때 Usage Count는 2가 됩니다.

Destroy Process

 

cmd를 exit 명령을 통해 종료하였지만, Usage Count가 0이 되지 않아 커널 오브젝트는 제거되지 않습니다.

 

Close Handle

그래서 CloseHandle 함수를 사용하여, 생성한 프로세스에 대한 핸들을 모두 닫아주어 커널 오브젝트의 Usage Count를 0으로 만들어 제거해야 합니다. 이 과정을 거치지 않으면 메모리 누수가 발생합니다.

'개발 > Windows' 카테고리의 다른 글

IPC  (0) 2025.03.26
Windows 프로세스 생성과 소멸  (0) 2025.03.15
레지스터와 명령어 구조  (0) 2025.03.14
Windows 32/64비트 시스템  (0) 2025.02.26
Windows 유니코드  (0) 2025.02.21