Windows内部概述-2-
线程:
执行代码的实体是线程。一个线程的包含在进程里面的,线程使用进程提供的资源来运行代码。
一个线程拥有以下的内容:
1:明确的运行模式,用户态或者内核态。
2:执行的环境,包括寄存器和执行状态。
3:一个或两个栈空间,用来存放变量和调用管理。
4:Thread Local Storage(TLS) 线程本地存储数组,用来存储线程专用数据和提供统一的访问语法。
5:基本的优先级和当前(动态)的优先级。
6:和处理器关联,用来指示允许线程在那个一个处理器上运行。
线程的状态 | |
---|---|
Running | 正在当前逻辑处理器上执行代码。 |
Ready | 等待被列入运行队列中,因为所有的相关处理机都是在运行或者不可用中。 |
Waiting |
线程栈
每个线程当运行时有一个栈空间来存放变量、传给函数的参数和函数调用前存储返回地址的位置。
一个线程有至少一个栈堆留在系统的内核空间,而且空间非常小(默认为12KB 32位系统和64系统上24KB)。用户态的线程有第二个堆栈在它的用户进程地址空间范围内是挺大的一个空间(默认情况下可以增长到1mb)。
当线程处于运行状态或者就绪态时,内核堆栈总是停留在RAM内存中。而对于用户态的堆栈来说就是另一方面了,可以被直接分页交互,就像用户模式的内存一样。
就堆栈大小而言的话用户模式下的堆栈和内核模式下的堆栈使用情况不同。用户模式下的堆栈一开始会提交少量的内存(可以小到和一个页面一样大小),将剩下的内存地址空间作为保留内存,这段保留内存是没有被分配的。这样做的目的是在线程执行代码的时候可以动态地增长堆栈空间。为了达到动态增长堆栈的目的,通常是在已经提交了的堆栈内存中往后设置一个被称为PAGE_GUARD状态的内存保护页(有时是多个页),如果线程需要更多的堆栈空间,它将会访问保护页面,这样就会触发一个由内存管理器处理的异常,然后内存管理器再删除保护状态并提交该保护状态的页面,再将下一个页面标记为保护页面,这样让堆栈根据需要来增长。
用户态的线程堆栈大小由以下内容确定:
1:可执行的文件在PE文件中有一个默认的提交和保留状态堆栈大小,如果线程没有修改就是默认的。
2:当在创建线程的时候,可以通过函数参数来指定所需堆栈大小,预先提交的大小或保留的大小。
线程小结:线程才是真正跑代码的东西,线程利用的是进程提供的代码和数据来运行,线程可以分为三个状态来标识,同时线程也分用户态和内核态,两者的存储堆栈大小不同,用户态的堆栈也有三个状态目的是为了动态的增长堆栈大小,线程的堆栈大小可以由PE文件结构和创建线程函数来规定。
系统服务(System Call 系统调用)
一个应用程序会执行各自操作系统应该执行的操作,例如:分配内存,打开文件,创建线程等等。这些涉及操作系统的操作,最后都是由操作系统内核来运行代码实现,那么用户模式下如何进行这样的操作呢?其实很简单,肯定是内核提供了API接口给我们使用。
例如:打开Sublime新建一个文件,那么这里的Sublime的代码通过调用CreateFile这个API来实现新建文件,CreateFile这个API是记录在kernel32.dll这个动态链接库里面的,kernel32.dll是Windows其中的一个子系统的dll。但是这函数仍然是在用户太下运行的,所以它仍然是无法直接打开文件的。在进行错误检查后,一个名叫在NTDLL.dll中的API函数NtCreateFile会被调用,该NTDLL.dll是一个基础的dll所以这个NtCreateFile也被称为本地的API函数,但是实际上它仍然属于用户模式的最低代码层。该API有一个重要作用就是实现了向内核状态的转换,但在它转换前它将一个称为系统服务号码的数字赋值给CPU的(inter/arm架构上的)eax寄存器,然后再 发出一个特殊的CPU指令,在跳转到被称为系统服务调度程序的预定义例程时也同时转换到内核模式中。
然后系统服务调度程序反过来将eax中的值作为系统服务调度表(SSDT)的索引,通过该表,代码会跳转到系统服务(系统调用)本身上,对于我们这里的创建文件,SSDT将指向I/O管理器的NtCreateFile函数来创建文件。(这里的NtCreateFile和NTDLL.dll的NTDLL.dll中的NtCreateFile名称一样,参数也一样)。在系统服务执行完后,线程就会返回用户模式继续执行后面的命令。
1 CreateFile
2 NTDLL.dll中的NtCreateFile
3 mov eax.n 跳转到系统调度程序,且进入内核态
4 系统调度程序根据eax的值从SSDT中找到要提供的服务
5 使用内核的NtCreateFile来创建文件
系统服务总结:当涉及到和OS相关的操作时通常都会调用内核的内容来实现,这就会通过要实现的功能进入内核,然后内核系统调度程序根据要实现的功能再在内核中实现后又返回到下一条指令中继续执行。
通用系统架构:
User processes | 基于文件的正常进程,如:QQ.EXE |
Subsystem DLLs | 子系统DLL里面包含了子系统提供的API。子系统就是对内核所公开的功能的某种视图。(就是公开给大家知道让大家调用的),从技术角度来说自Windows8.1之后只有一个子系统,就是Windows子系统。子系统dll包括著名的文件,如kernel32.dll、user32.dll、gdi32.dll、advapi32.dll、combase.dll等。这些主要包括官方记录的窗口api。 |
NTDLL.DLL | 一个系统范围内的dll,实现了Windows内置的API,但是仍然是处于用户模式中,虽然是用户模式中最低的代码层。它最大的作用就是实现了从用户层向内核层的转换来进行系统调用,NTDLL还提供了堆管理、文件加载和一部分的用户线程池。 |
Service Processes | 服务进程是用来对Service Control Manager进行管理和交互的一个普通进程。SCM就是Windows的服务控制管理,可以在cmd+d下输入services.msc来查看。SCM可以启动、停止、暂停、恢复并向其它和服务有关的发送消息。服务通常在其中一个特殊的窗口帐户下执行——本地系统、网络服务或本地服务。 |
Executive | 执行程序是NtOskrnl.exe("内核")的上层,它承载了内核模式下的大部分代码。主要包括各种"管理器",对象管理器、内存管理器、I/O管理器、即插即用管理器,电源管理器和配置管理器等等。它比大多的更底层的内核模块都要大。 |
Kernel | 内核层实现了操作系统内核模式中最基本和对时间最敏感部分的代码。包含了线程调度、中断和异常分发以及各自原语实现,例如:互斥体,信号体。一些内核代码直接用特定CPU的机器语言编写来提高效率并且可以直接访问CPU里的一些细节。 |
Device Drivers | 设备驱动程序是可以被加载的内核模块,它们里面的代码直接在内核状态下执行而且有和内核一样的权限和功能。 |
Win32k.sys | Windows子系统的内核模式组件。从本质上来说,这是一个内核驱动模块用来处理部分Windows的用户态接口API和经典图形设备接口(GDI)API。这也就意味着所有的Windows操作(如CreateWindowsEx,GetMessage等等)都是由这个组件来处理的。该组件几乎与UI界面无关。 |
Hardware Abstraction Layer (HAL) | 硬件抽象层HAL是最接近CPU硬件的一个抽象层,它允许设备驱动程序使用不需要了解中断控制器等方面的详细和具体知识的API,这一层对于编写用于处理硬件设备的设备驱动程序非常有用, |
System Processes | 系统进程是一个总称,用来描述操作系统内置的进程,是默认开机就会有的进程。他们是非常重要的,没有它们系统无法正常运行。终止了一些系统进程可能会导致系统崩溃。一些系统进程是内置的本机进程,这意味着他们只使用内置的API(也就是NTDLL提供的API)。例如:Smss.exe,Services.exe等等。 |
Subsystem Process | 子系统进程。运行Csrss.exe镜像的Windows子系统进程可以看作是内核的助手,用于管理在Windows系统下运行的进程。这是一个关键的进程,如果关闭了它那么系统就会崩溃。通常每个会话都会有一个Csrss.exe的实例。所以在一个标准的系统里面有两个实例是一定会存在的:一个用于会话0,另一个用于已登录的用户会话(通常为1)。 |
Hyper-V Hypervisor | 微软虚拟机:如果Hyper-V hypervisor是Win10以及更高版本之上的,那么就支持基于虚拟化的安全性Virtualization Based Security (VBS),基于虚拟化的安全性VBS提供了一个额外的安全层,其中实际机器实际上是由Hyper-V控制的虚拟机。 |
系统架构小结:这个概念属实有点多了,慢慢再消化把。
句柄和内核对象
Windows公开了各种类型的系统对象,共用户模式进程和内核驱动使用。这些对象的实例设计是在系统空间中的结构体,由用户或内核模式的执行代码向对象管理器请求时创建的。对这些对象的引用将会被计数,只有当最后一个引用的对象被释放后才会销毁内存并从内存中释放资源。
因为这些实例对象是位于系统空间中,所以不能在用户态被访问。用户态如果要访问这些系统提供的对象必须采用间接访问机制,将其称为句柄handle。句柄是对基于进程维护的表中条目的索引,所有的句柄是针对进程的,每个进程中的句柄不能共用,它在逻辑上指向驻留在系统空间中的内核对象,可以想象成表,其中的句柄和内核对象的关系可以多对一,一对一但是不能一对多。由各种的Create和Open开头的函数来创建或获取一个对象和获取这些对象对应的句柄。例如:CreateMutex这个用户态的API会创建一个Mutex对象,如果成功,函数将返回该对象的句柄。返回值为零,表示一个无效的句柄(函数调用失败),同样如果调用OpenMutex函数来试图打开一个互斥体的句柄,如果存在就返回句柄,不存在就失败返回NULL(0)。
内核(驱动程序)可以使用句柄或者直接指向对象的指针来操作。这样通常是为了想要调用基于Windows对象的API。在某些情况下,由用户模式提供句柄给驱动必须通过ObReferenceObjectByHandle这个API函数转换为指针的形式。
在Windows中有一个全局的系统句柄:这个句柄表存放在system进程里面,系统的句柄表只有一个,这个句柄表就是都可以用不局限于进程里了,系统句柄表里的句柄也被称为内核句柄。
句柄都是4的倍数,第一个有效的句柄是4,0永远不是有效句柄值。
在内核模式情况下可以使用句柄也可以直接使用指针,到底是指针还是句柄通常看函数需要的参数。内核代码可以使用ObReferenceObjectByHandle函数来将指针的句柄变成指针。如果对内核对象的引用成功会导致它的计数增加,所以就是如果用户模式的代码关闭了句柄这并不危险并不会导致空指针,因为要所有的引用都释放了才会销毁该内存。无论通过句柄做了什么,该句柄对应的对象都可以安全地访问,直到内核代码调用ObDerefenceObject函数,以减少对象的引用计数;如果内核代码错过此调用,则是资源泄漏,只能在下次系统启动时解决(也就是重启)。
所有的系统对象都会进行引用计数。每个内核对象包含两个计数,句柄计数和指针计数。
句柄计数是指:应用层对内核对象的句柄使用计数。
指针计数是指:内核驱动对内核对象的指针引用次数。
对象管理器维护着句柄计数的值和内核对象引用的次数值。一旦一个内核对象不再被需要,使用或者创建它的客户端需要对其进行关闭句柄(如果使用的是句柄来访问对象)或者是取消引用对象(如果用的是内核指针)。取消或者关闭后句柄/指针就没有效果了,如果一个内核对象的引用计数达到零,则对象管理器就会销毁该对象,释放空间。
内核对象名称
有一些内核对象有名字,这些有名称的内核对象可以通过名字被对应的Open函数来打开对象。并不是所有的内核对象都有名字,比如:进程和线程就没有名字,他们只有ID。这就是为什么OpenProcess和OpenThread函数需要线程/进程的ID而不是名字了。还有比较奇怪的一种情况:没有名称的对象是一个文件。文件名不是和对象名一一对应,两者是不同的概念。
在用户模式的情况下,如果具有该名称的对象不存在就会调用构建函数来依据对象的名称来创建一个对象;如果存在就打开该对应的名称的对象。
提供给创建函数Create*的对象名称并不是该对象的最终名称,名称由\Sessions\x\BaseNamedObjects\来命名对象,其中x是调用者的会话ID,如果会话为0,则由\BaseNamedObjects\来命名。如果调用者碰巧在应用程序容器(通常是通用Windows平台进程)中运行,则前置字符串将更为复杂,并且由唯一的应用程序容器SID:\Sessions\x\AppContainerNamedObjects{AppContainerSID}组成
以上的内容都表明对象名称是和会话相关的(如果是AppContainer应用程序容器则是和包相关)。如果必须在会话之间来共享对象则可以在会话0时使用Global预设计对象名来创建对象,例如,使用名为Global的CreateMutex函数创建互斥体将在\BaseNamed对象下创建它。
图所示的视图是对象管理器命名空间,由命名对象的层次结构组成。将整个结构保存在内存中,并根据需要由对象管理器(执行人员的一部分)进行操作。请注意,未命名的对象不是此结构的一部分,这意味着在WinObj中看到的对象不包含所有现有对象,而是使用名称创建的所有对象。
每个进程都由一个私有的针对内核对象的私有句柄表,它可以通过Process Explorer 或者Handles Sysinternals tools来查看:
默认的情况是只能看句柄类型和名字,但是可以在view中选择显示未命名的句柄,还可以右键选中这个地方来增加查看句柄的内容:
句柄视图中的各个列为每个句柄提供了更多信息。 显示了很多真实的对象名称:Mutexes (Mutants), Semaphores, Events, Sections, ALPC Ports, Jobs, Timers等等还有一些用的比较少的对象类型。然而,有一些显示的名字并不是他根本的名字:
对象 | 原因 |
---|---|
进程和线程 | 由唯一的ID来标识,显示的是ID |
文件对象 | 它显示该文件对象指向的文件名(或者设备名)。它和对象的名称是不同的,因为文件名,所以无法获取对文件对象的句柄--只能创建一个新的文件对象来访问相同的基础文件或设备。 |
注册表 | 关键对象名称随注册表项的路径显示。但是这不是一个名称,它与文件对象的推理相同。 |
目录对象显示的路径 | 目录对象显示的路径,而不是真正的对象名称。目录不是文件系统对象,而是对象管理器目录——可以使用系统内部WinObj工具轻松查看这些目录 |
令牌(token) | 令牌(token)对象名称与存储在令牌中的用户名一起显示 |
访问现有的对象
进程资源管理器的句柄视图中的“访问”列显示用于打开或创建句柄的访问掩码。此访问掩码是允许执行的操作的关键具有一个特定的句柄。例如,如果客户端代码想要终止一个进程,它必须首先调用Open进程函数,以获得对所需进程的句柄,其访问掩码为(至少)PROCESS_TERMINATE,否则就没有办法用该句柄终止该进程。如果调用成功,则终止进程将成功。
下面是一个在给定进程ID时终止进程的用户模式示例:
bool KillProcess(DWORD pid) {
// open a powerful-enough handle to the process
HANDLE hProcess = OpenProcess(PROCESS_TERMINATE, FALSE, pid);
if (!hProcess)
return false;
// now kill it with some arbitrary exit code
BOOL success = TerminateProcess(hProcess, 1);
// close the handle
CloseHandle(hProcess);
return success != FALSE;
}
“访问掩码”列提供了访问掩码的文本说明(对于某些对象类型),使它更容易识别对特定句柄所允许的精确访问。双击句柄条目将显示该对象的一些属性。
图中的属性包括对象的名称(如果有)、其类型、描述及其地址:
在内核内存中,可以获取打开的句柄的数目,以及一些特定的对象信息,例如:所显示的事件对象的状态和类型。请注意,所显示的参考文献并没有表示实际的对该对象的未完成的引用数。一种查看实际参考计数的正确方法该对象是使用内核调试器的!trueref命令,如下所示:
1 lkd> !object 0xFFFFA08F948AC0B0 2 Object: ffffa08f948ac0b0 Type: (ffffa08f684df140) Event 3 ObjectHeader: ffffa08f948ac080 (new version) 4 HandleCount: 2 PointerCount: 65535 5 Directory Object: ffff90839b63a700 Name: ShellDesktopSwitchEvent 6 lkd> !trueref ffffa08f948ac0b0 7 ffffa08f948ac0b0: HandleCount: 2 PointerCount: 65535 RealPointerCount: 3
小结:windows系统公开了自己在内核中的对象和类,内核中的对象和类一一对应,只有唯一一个,在用户态中可以使用句柄来调用内核对象,内核中可以用句柄也可以用指针,内核对象被引用后会计数,如果为0就会销毁它。每个进程有一个表来专门记录该进程对内核对象的引用。还有一个系统进程system记录的是系统句柄表。
Windows内部概述总结:
进程
虚拟内存
线程
系统服务
系统架构
以上就是WIndows的内部五大版块,每一个都能弄很久,所以总结起来实在是不太行,只有个大概的概念就好了,后面深入了就明白了。