读Windows核心编程-3-内核对象

(3)内核对象

何为内核对象

内核对象是Windows操作系统内核分配和访问的对象,每个内核对象对应于一个内存块,该内存块只能由内核分配,访问和释放。该内存块是一个数据结构,维护着与对象相关的信息。少数成员是所有内核对象所共有的,如:安全描述符和使用计数,其他多数成员则是每种内核对象所特有的。

内核对象有很多种,如:访问令牌,事件,文件,文件映射,作业,互斥体,管道,进程,线程,信号量,可等待计时器以及线程池工厂等。可使用一些工具来观察,如:WinObjProcess Explorer等。

    内核对象在应用程序中只能通过Windows的一组函数Create*/Open*/CloseHandle以及相应的内核对象句柄(*Handle)来访问,内核对象句柄(32Windows进程中句柄为32位,64位进程中,句柄也变成64位)是一个不透明值,其具体意义也可能与Windows不同版本的具体实现相关,不过能够确定的是:

  1. 句柄是进程相关的,一个进程的句柄传给其他进程是没有意义的;
  2. 句柄可以在同一进程中的所有线程使用;
  3. 句柄包含了进程句柄表的索引值,所以通过句柄就能找到对应的内核对象所在的地址。

使用计数

内核对象的所有这是内核,而非进程,而且进程操作内核对象的唯一手段就是用句柄作为参数来调用相应的Windows APICreate*/Open*/CloseHandle)。Windows操作系统可由此来实现对内核对象的使用计数,就是说同一个内核对象,Windows操作系统知道有多少进程在引用它。只有当引用计数变为0的时候,内核对象才被销毁,因此,一个内核对象的生命周期有可能比创建它的进程生命周期要长。

内核对象安全性

内核对象都使用一个安全描述符来保护,安全描述符定义了谁拥有对象,哪些组和用户被允许访问和使用此对象,哪些被拒绝。该安全信息用参数SECURITY_ATTRIBUTES来指定,如果传NULL给这个参数,则表示使用默认的安全性(具体包括哪些安全信息,取决于当前进程的安全令牌security token)。

除了内核对象,进程还可能使用一些其他对象,如菜单,窗口,鼠标,笔刷和字体,这些属于用户对象或GDI对象,而非内核对象。一个简单的方法来判断是否是内核对象,就看其Create*方法有没有指定安全信息的参数。

进程的内核对象句柄

进程的内核对象句柄都包含了进程句柄表的索引值,而进程的句柄表是在进程初始化的时候系统为其分配的;实际上将句柄值除以4(或者右移2位,以忽略Windows内部使用的最后两位)就能得到实际的句柄表索引值。一个可能的句柄表结构如下图:

读Windows核心编程-3-内核对象

每个句柄包含一个索引值,能够只想该表中的一条记录,而该句柄表中每一条记录都是一个数据结构,这个数据结构包含了:

  1. 一个内核对象的指针;
  2. 一个访问掩码(access mask);
  3. 一些标志位,如可继承性等。

创建内核对象

调用Create*函数来创建内核对象,如下面的列表所示,创建失败时,有的函数会返回NULL(这就是为神马第一个有效句柄值为4的原因),有的则会返回INVALID_HANDLE_VALUE(这个值是-1);但是GetLastError()都会返回ERROR_INVALID_HANDLE。

CreateProcess/CreateThread/CreateFile/CreateFileMapping/CreateMutex/CreateSemaphore

关闭内核对象

关闭内核对象,无论是哪种类型,如何创建的,都使用CloseHandle;如果关闭失败,CloseHandle将返回FALSE。

调用CloseHandle后,将导致进程句柄表中相应的记录被清空,并且将相应内核对象引用计数减1,如果计数变成0则销毁该内核对象。

如果进程忘记调用CloseHandle,那就要等到进程退出时,系统才会为进程关闭所有使用到的内对象句柄。事实上,进程终止时,系统会确保所有的资源都得到释放,包括内核对象,资源,GDI对象以及内存块。(使用Windows任务管理器或Process Explorer可以验证这一点)。

跨进程共享内核对象

由于内核对象是保存在进程内核地址空间中的,所以可以跨进程共享内核对象。系统中所有的进程都共享内核地址空间:对32位系统,这是0x80000000~0xFFFFFFFF之间的内存,对64位系统,这是0x00000400 00000000~0xFFFFFFFF FFFFFFFF之间的空间。

共享内核对象的三种方式:

  1. 使用对象句柄继承
    1. 进程创建子进程或孙进程时候可以使用句柄继承
    2. 被继承的句柄必须设置bInheritHandle=TRUE
    3. 父进程创建子进程的时候必须设置bInheritHandles=TRUE
    4. 上述条件都满足时,子进程会复制父进程中可继承(bInheritHandle=TRUE)的句柄,且相应的句柄值和索引,以及可继承性都与父进程相同。

 

  1. 使用命名对象
    1. 使用Create*创建内核对象时,可以给对象命名以使之能够跨进程使用,其他进程只要用相同的名字来调用Create*/Open*;
    2. 命名对象的问题时没有一个机制来验证该名字是否已经存在,因此需要直接调用Open*,或者在Create*后,调用GetLastError看其是否返回ERROR_ALREADY_EXIST,以判断是否真的创建新的对象,还是只是打开了现有的内核对象;
    3. Create*/Open*取回现有内核对象时,系统都会根据名字查找现有的内核对象,并验证调用进程是否有相应的内核访问(读写)权限,但仍存在安全性问题,例如,恶意程序可以使用跟某些高权限进程相同的内核对象名字;
    4. 专有命名空间可以部分解决安全性和重名问题;相关API:CreatePrivateNameSpace/ClosePrivateNameSpace。

 

  1. 复制对象句柄
    1. DuplicateHandle可以将进程相关的对象句柄复制到另一个进程中,复制后的句柄和目标进程相关;
    2. 复制后的句柄由于和目标进程相关(就是说该句柄在目标进程的句柄表中),因此只能在目标进程中关闭该句柄。

 

示例:

DWORD currentProcessId=GetCurrentProcessId();

    HANDLE hMutexInSource=CreateMutex(NULL, FALSE, _T("MyTestMutex"));

    STARTUPINFO si = { sizeof(si) };

    si.dwFlags = STARTF_USESHOWWINDOW;

    si.wShowWindow = TRUE;

    PROCESS_INFORMATION pi={};

    HANDLE hMutextInTarget=NULL;

    TCHAR cmdline[] =TEXT("c://program files//internet explorer//iexplore.exe http://www.baidu.com/");

 

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

        HANDLE hSourceProcess = GetCurrentProcess();

 

        tcout<<"before duplicate handle"<<endl;

        //display_process_handles(pi.dwProcessId);

        if(!DuplicateHandle(hSourceProcess,hMutexInSource,pi.hProcess,&hMutextInTarget,0,FALSE,DUPLICATE_SAME_ACCESS)){

            display_last_error();

        }

        else{

            tcout<<"after duplicate handle"<<endl;

            //display_process_handles(pi.dwProcessId);

        }

 

        setlocale(LC_ALL, "chs");

        _tprintf(_T(" 新进程的进程ID号:%d\n"), pi.dwProcessId);

        _tprintf(_T(" 新进程的主线程ID号:%d\r\n"), pi.dwThreadId);

 

        CloseHandle(pi.hProcess);

    }

    else{

        display_last_error();

    }

    CloseHandle(hMutexInSource);

你可能感兴趣的:(windows)