Windows Via C/C++ note 1

近期在学这本《Windows via C/C++》(也就是《Windows核心编程》),发现比我想象的要有趣一点。里面提到了一些概念性的东西,这里先将其记录下来

Windows Via C/C++ note 1

内核对象 (Kernel Object)

在 Windows 操作系统中,有很多涉及到底层的操作,例如文件操作,进程操作,注册表操作,网络操作,事件操作等等。为了能够将用户从繁琐的内核操作中分离出来,Windows 提供了很多的 API 函数来帮助用户能够更好的操作,例如对于文件操作,我们有CreateFile,WriteFile; 对于进程,我们可以使用CreateProcess,也有CreateToolhelp32Snapshot 这样的操作函数。这些函数操作的对象就是我们本文要讨论的内核对象。例如:

  • 文件对象
  • 事件对象
  • 互斥量(Mutex)对象
  • 管道(Pipe)对象
  • 进程对象
  • 线程对象
    ….

这些内核对象都是一个内存块,由操作系统内核进行分配,并且也只能由操作系统内核访问。内核对象中的属性随着内核种类的变化而变化。形如进程对象拥有 PID 的属性,而文件对象有一个字节偏移数作为参数。但是大部分的内核对象都有下列两个属性

  • 使用计数
  • 安全描述符

这些属性只能够由操作系统内核进行修改和使用,从而避免应用程序对内核进行过多的修改。

句柄

上文中提到,内核对象只能够由内核进行处理,那么应用程序应该如何访问这些对象呢?答案就是使用句柄(Handle)。当调用一个创建内核对象的函数后,系统会生成一个句柄返回到应用层。之后应用层将会使用这个句柄对对应的内核对象进行操作


为了保证操作系统的可靠性,句柄的值是与进程相关的,但是同时,句柄也支持在不同的进程间共享。

内核对象的生命周期

由于内核对象可以被多个进程共享,所以即使说由进程A创建了一个内核对象,在进程A结束之后。在进程B中也使用了同一个内核对象,此对象也不应该被回收。为了实现这点,每个内核对象都由一个使用计数的参数,用来表示当前有多少个进程在使用当前内核对象。当没有进程使用当前内核对象的时候,该使用计数会变为0。操作系统会检测内核对象的引用次数,当次数为0的时候就会 销毁当前对象(不完全是这样) ,这样的话就能够保证系统中不存在没有被任何进程引用的内核对象。

让引用计数减少的最基本的方法就是调用函数CloseHandle,每当关闭一个句柄,就算是减少了一次对当前内核次数的引用

**: 上文提到,内核对象的引用计数变为0的时候会销毁对象,并且调用CloseHandle的时候会减少引用次数这种说法不完全正确。比如说当使用CreateProcess创建的进程对象在被CloseHandle之后也会继续运行直到程序结束,而CreateFileMapping创建的共享空间句柄就会因为CloseHandle而导致内核对象被销毁。个人理解,销毁与否关键取决于当前内核对象所管理的对象是否结束(比如说进程的话要等进程运行结束才算是结束,而内存空间只要没有任何东西运行,所以关闭之后就相当于是当前引用结束了)

安全描述符(SD)

安全描述符之前接触比较少,之后应该会单独开篇来讲一下这个东西,这里就简单介绍一下。
内核对象的访问限制。一个安全描述符主要描述这些事情:

  • 当前对象的拥有着(一般就是当前对象的创建者)
  • 哪些组/用户能够访问或者使用该对象
  • 哪些组/用户被拒绝访问当前对象

在内核中可以通过一个叫做 SECURITY_ATTRIBUTES 的结构进行设置。

句柄表

这个概念是由这本书提出来的,并不保证实际情况就是如此
进程初始化的时候,将会由操作系统为其分配一个句柄表。这个句柄表相当于是一个数据结构类似的东西,然后里面包含一个内核对象的指针,一个访问掩码(access masl)还有一些标志

1
2
3
4
5
6
7
+-------+-------------------------+-------------------------------+------------+
| index | 指向内核对象内存块的指针 | 访问掩码(包含标志未的一个DWORD) | 标志 |
+-------+-------------------------+-------------------------------+------------+
| 0x10 | 0xF0001000 | 0x???????? | 0x00000001 |
+-------+-------------------------+-------------------------------+------------+
| 0x20 | 0x00000000 | (不可用) | (不可用) |
+-------+-------------------------+-------------------------------+------------+

如上,为一个记录了有效句柄的句柄表。其中索引1为一个有效的内核对象的句柄。

当使用创建内核对象的函数时,就会对当前的表格进行填充。这类函数形如:

  • CreateThread
  • CreateFile
  • CreateFileMapping
  • CreateSemaphre(信号量)

通过这些函数得到的句柄可以被同一进程中的所有线程共同使用。在使用句柄的时候,Windows进程往往会将当前的句柄值右移两位作为真正的句柄值。所以第一个有效的句柄值往往是4。(众所周知,在 Windows 操作系统中,System 进程的 PID 值为4,这里可能有所关联)

作用域

正如前文提到的,内核对象是存放在内核空间中的,部分的API会听过为内核对象命名的操作。通过使用名字访问的话,就相当于是跨进程,此时被称为放在全局命名空间。若不使用命名的话,则是在同一进程中共享。
然而,在 Windows Vista 之后,对于命名有了要求。全局命名空间名必须要为

1
Global\ObjectName

的形式。
同样我们也能够显示的指明一个对象放入当前会话空间

1
Local\ObjectName

这是因为, Windows Vista 之后,在 Windows 登陆的时候会创建一个叫做 Session 0 的会话。在这个会话中的服务都是在登陆期间执行的。通过这样做能够有效的将应用程序和系统服务进行隔离。同样,每一个登陆的用户都将获得一个会话(一般是从 Session 1开始)不同的用户可以登陆不同的会话。

内核对象观测

推荐一个 Sysinternals 提供的工具(似乎是被微软收购了),叫做Process Explorer
严肃警告:这个公司的软件都会装驱动,还想玩游戏的个位千万千万不要装到跑游戏的机器里面,相关故事请参考我的惨痛经历,顺便里面提到的Procmon其实在检测文件行为的时候还是蛮好用的