在Windows应用程序中,设备句柄(Handle)是一个核心概念。句柄本质上是一个标识符,用于代表系统资源,如窗口、文件、内存块、设备上下文等。设备句特指与设备相关的资源,例如设备上下文(Device Context, DC)句柄,它用于绘图操作。
在Windows编程中,当你需要在一个窗口上绘图时,你需要获取该窗口的设备上下文句柄(HDC)。通过这个句柄,你可以调用GDI(Graphics Device Interface)函数来绘制图形、文本等。
句柄是一个抽象的概念,它通常是一个整数值,但应用程序不应该直接解释这个值,而是通过Windows API函数来使用它。句柄的使用使得Windows能够管理资源,并且提供一种安全的方式让应用程序访问这些资源。
例如,当你创建一个窗口时,你会得到一个HWND(窗口句柄)。当你需要在这个窗口上绘图时,你可以使用GetDC函数传入HWND来获取HDC。完成绘图后,你必须释放这个HDC,以便系统可以回收资源。
下面是一个简单的示例,说明如何在一个窗口上绘制一个矩形:
-
获取窗口的设备上下文句柄(HDC)。
-
使用该句柄调用Rectangle函数。
-
释放设备上下文句柄。
注意:在C/C++中,通常使用WinAPI来编程。
示例代码(C++):
#include <Windows.h>
LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam) {
switch (message) {
case WM_PAINT: {
PAINTSTRUCT ps;
HDC hdc = BeginPaint(hWnd, &ps);
// 使用设备句柄hdc进行绘图
Rectangle(hdc, 50, 50, 200, 200); // 绘制一个矩形
EndPaint(hWnd, &ps);
break;
}
case WM_DESTROY:
PostQuitMessage(0);
break;
default:
return DefWindowProc(hWnd, message, wParam, lParam);
}
return 0;
}
int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nCmdShow) {
// 注册窗口类
WNDCLASS wc = {};
wc.lpfnWndProc = WndProc;
wc.hInstance = hInstance;
wc.lpszClassName = L"ExampleWindowClass";
RegisterClass(&wc);
// 创建窗口
HWND hWnd = CreateWindowEx(0, L"ExampleWindowClass", L"Example", WS_OVERLAPPEDWINDOW,
CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, nullptr, nullptr, hInstance, nullptr);
ShowWindow(hWnd, nCmdShow);
// 消息循环
MSG msg = {};
while (GetMessage(&msg, nullptr, 0, 0)) {
TranslateMessage(&msg);
DispatchMessage(&msg);
}
return 0;
}
在这个例子中,在窗口过程函数中处理WM_PAINT消息。通过BeginPaint函数获取设备上下文句柄(HDC),然后使用这个句柄绘制一个矩形,最后通过EndPaint释放句柄。
设备句柄不仅用于绘图,还可以用于其他设备,如打印机、位图等。但是,无论哪种设备,句柄都提供了一种统一的访问机制,使得应用程序能够以一致的方式使用不同的设备资源。
重要的是,应用程序不应该长期持有设备句柄,而应该在用完后就释放,否则可能会导致资源泄漏。在Windows中,句柄是有限资源,过多未释放的句柄会导致系统性能下降甚至程序崩溃。
一,什么是设备句柄?
设备句柄(Device Handle) 是Windows操作系统用来标识和访问各种系统资源的整数值。它相当于一个"令牌"或"引用",应用程序通过这个句柄来操作对应的设备或资源。
1.1 主要特点
-
不透明标识符:应用程序通常不需要知道句柄的具体内部结构
-
唯一性:每个活动的句柄在特定上下文中是唯一的
-
类型安全:不同类型的句柄有不同的类型定义(如HANDLE、HWND、HDC等)
1.2 常见的设备句柄类型
// 文件句柄
HANDLE hFile = CreateFile("example.txt", ...);
// 窗口句柄
HWND hWnd = CreateWindow(...);
// 设备上下文句柄(用于图形绘制)
HDC hDC = GetDC(hWnd);
// 进程句柄
HANDLE hProcess = OpenProcess(...);
// 线程句柄
HANDLE hThread = CreateThread(...);
// 互斥体句柄
HANDLE hMutex = CreateMutex(...);
1.3 句柄的作用
1)资源访问:通过句柄操作对应的系统资源
2)权限控制:句柄包含了访问权限信息
3)资源管理:系统通过句柄跟踪资源的使用情况
4)抽象层:隐藏底层设备的具体实现细节
使用示例
#include <windows.h>
// 创建文件并获取句柄
HANDLE hFile = CreateFile(
"test.txt",
GENERIC_WRITE,
0,
NULL,
CREATE_ALWAYS,
FILE_ATTRIBUTE_NORMAL,
NULL
);
if (hFile != INVALID_HANDLE_VALUE) {
// 使用句柄写入文件
WriteFile(hFile, "Hello", 5, NULL, NULL);
// 使用完毕后关闭句柄
CloseHandle(hFile);
}
1.4 重要注意事项
-
必须关闭:使用完毕后必须调用相应的关闭函数。
-
无效句柄:通常用
INVALID_HANDLE_VALUE或NULL表示无效句柄。 -
作用域:句柄只在当前进程上下文中有效。
-
继承性:某些句柄可以在父子进程间继承。
设备句柄是Windows编程的基础,正确管理句柄对于编写稳定、高效的应用程序至关重要。而在Windows系统中,应用程序如何使用系统资源,统一的通过句柄去访问那些系统资源的机制和原理是什么?
二,核心思想:抽象与隔离
Windows的设计哲学是:应用程序不应该,也不需要直接访问硬件或内核内存地址。这样做是为了:
1)稳定性:一个程序的错误不会直接影响其他程序或操作系统。
2)安全性:防止恶意程序窥探或修改其他程序的数据。
3)统一性:为不同的硬件提供统一的编程接口。
句柄就是实现这一思想的关键工具。
2.1 机制:如何使用句柄访问资源
应用程序使用句柄访问资源的流程遵循一个清晰的模式,可以概括为“三板斧”。
第1步:获取句柄 - 敲门领令牌
应用程序必须通过一个特定的Windows API函数来向操作系统“申请”访问某个资源。如果成功,系统会创建一个内部结构来管理该资源,并返回一个唯一的句柄给应用程序。
-
文件:
CreateFile-> 返回HANDLE -
窗口:
CreateWindow-> 返回HWND -
图形绘制:
GetDC-> 返回HDC -
进程:
OpenProcess-> 返回HANDLE -
内存映射:
CreateFileMapping-> 返回HANDLE
第2步:使用句柄 - 出示令牌进行操作
在后续的操作中,应用程序将这个句柄作为参数传递给相应的API函数。
// 示例:使用句柄进行文件操作和图形操作
HANDLE hFile = CreateFile("data.txt", ...); // 获取句柄
// 使用句柄写入文件
WriteFile(hFile, buffer, bufferSize, &bytesWritten, NULL); // 使用句柄
HDC hDC = GetDC(hWnd); // 获取图形设备上下文句柄
// 使用句柄画一个矩形
Rectangle(hDC, 10, 10, 100, 100); // 使用句柄
关键点:所有操作函数内部都将这个句柄传递回内核,由内核来执行实际操作。应用程序看不到内核对象,也接触不到真实的硬件。
第3步:关闭句柄 - 归还令牌
当资源使用完毕后,应用程序必须关闭句柄,通知操作系统释放相关资源。
CloseHandle(hFile); // 关闭文件句柄
ReleaseDC(hWnd, hDC); // 释放设备上下文
关键点:如果不关闭句柄,会导致资源泄露,因为系统会认为该资源仍在被使用,从而无法释放。
2.2 原理:幕后发生了什么?
这是最有趣的部分。现在深入到内核层面,看看句柄背后的魔法。
2.2.1 概念1:内核对象
当您调用 CreateFile 或 CreateProcess 时,Windows内核会在内核内存空间(一个受保护的、应用程序无法直接访问的区域)创建一个数据结构。这个结构就是内核对象。
-
这个对象包含了管理该资源所需的所有信息:文件读/写指针、进程ID、安全属性、引用计数等。
-
内核对象是系统级的资源。
2.2.2 概念2:句柄表
每个Windows进程在创建时,系统都会为它分配一个私有的句柄表。您可以把它想象成一个“令牌索引簿”。
| 句柄值 (索引) | 指向的内核对象内存地址 | 访问掩码 |
|---|---|---|
| 0x00000004 | 0x8A123450 | ... |
| 0x00000008 | 0x8A1234A0 | ... |
| 0x0000000C | (空) | ... |
当应用程序调用 CreateFile 时:
1)内核创建一个文件内核对象。
2)内核在该进程的句柄表中找到一个空闲条目。
3)内核将该条目指向刚创建的文件内核对象。
4)内核将句柄表索引(例如 0x00000008)返回给应用程序。这个索引值,就是我们得到的句柄。
2.3 原理详解:为什么句柄是安全的
-
只是一个索引:你的句柄
0x00000008在你的进程A的句柄表中是索引#2,它指向对象X。但在进程B中,同样的值0x00000008可能指向一个完全不同的对象Y,甚至是空的。因此,一个进程无法通过猜测句柄值来访问另一个进程的资源。 -
访问验证:当您调用
WriteFile(hFile, ...)时:-
系统切换到内核模式。
-
系统查找当前进程的句柄表。
-
使用你传递的句柄值作为索引,找到对应的条目。
-
验证该条目是否有效,以及你请求的(写)操作是否被该句柄的访问掩码所允许。
-
如果一切正常,系统通过条目中的指针找到真正的内核对象,并执行写操作。
-
-
引用计数:内核对象有一个引用计数,用于记录有多少个句柄(可能来自不同进程)正在引用它。当通过
CloseHandle关闭一个句柄时,对应对象的引用计数会减一。当引用计数降为0时,内核知道该资源已不再被任何进程使用,便会安全地销毁该内核对象,释放其占用的所有内存和系统资源。
2.4 统一的访问机制
Windows通过句柄实现统一资源访问的精髓在于:
-
统一的抽象层:无论是文件、设备、窗口还是线程,对应用程序来说,它们都只是一些通过API获取和使用的“句柄”。这极大地简化了编程模型。
-
严格的安全边界:句柄机制在用户态和内核态之间建立了一道坚固的墙。应用程序在用户态“挥舞令牌”(句柄),而所有实际工作都由内核在受保护的内核态完成。
-
集中的资源管理:操作系统作为唯一的管理者,通过内核对象和句柄表,对所有系统资源进行跟踪、权限控制和生命周期管理。
这种机制确保了Windows作为一个多任务操作系统的稳定性、安全性和可靠性。那么,系统中的相同的对象,在不同的进程中可能有不同的句柄索引号,那这个对象的内存地址是不是相同的呢?现在进一步分析,这涉及到Windows内存管理的核心机制。
在物理内存层面,内核对象的内存地址对于所有进程是相同的;但在虚拟内存层面,每个进程"看到"的地址是不同的。
三,详细解释:虚拟内存与内核空间
3.1 虚拟地址空间
每个Windows进程都有自己独立的4GB虚拟地址空间(在32位系统中):
-
用户模式空间(0x00000000 - 0x7FFFFFFF):应用程序代码运行于此
-
内核模式空间(0x80000000 - 0xFFFFFFFF):操作系统内核代码和数据
// 进程A的视角
用户空间: 0x00000000 - 0x7FFFFFFF (应用程序代码、堆栈、数据)
内核空间: 0x80000000 - 0xFFFFFFFF (操作系统内核)
// 进程B的视角 - 完全相同的布局!
用户空间: 0x00000000 - 0x7FFFFFFF (不同的应用程序代码)
内核空间: 0x80000000 - 0xFFFFFFFF (相同的操作系统内核)
3.2 内核对象的存储位置
所有内核对象都存在于内核模式空间中。这意味着:
-
它们不在进程的用户模式内存中
-
它们受到硬件级保护,应用程序无法直接访问
3.3 关键机制:全局共享的内核空间
虽然每个进程有自己的虚拟地址空间,但所有进程的内核空间部分都映射到相同的物理内存!
// 实际情况示意图
物理内存中的内核对象: 0x12345000 (真实的物理地址)
进程A的虚拟地址空间:
用户空间: 独立的
内核空间: 0x80000000 → 映射到 → 物理内存中的内核区域
进程B的虚拟地址空间:
用户空间: 独立的
内核空间: 0x80000000 → 映射到 → 相同的物理内存中的内核区域
// 因此,两个进程通过不同的虚拟地址访问同一个物理对象
进程A访问: 0x8A123450 (虚拟地址) → 0x12345000 (物理地址)
进程B访问: 0x8A123450 (虚拟地址) → 0x12345000 (物理地址)
3.3.1 实际示例:进程间共享对象
让我们通过一个具体的例子来说明:
场景:两个进程共享同一个文件映射对象
// 进程A创建文件映射
HANDLE hFileMappingA = CreateFileMapping(
INVALID_HANDLE_VALUE,
NULL,
PAGE_READWRITE,
0,
4096,
L"Global\\MySharedMemory"
);
// 进程B打开同一个文件映射
HANDLE hFileMappingB = OpenFileMapping(
FILE_MAP_ALL_ACCESS,
FALSE,
L"Global\\MySharedMemory"
);
在系统内部发生了什么:
-
内核对象创建:
1)进程A调用
CreateFileMapping。 2)内核在内核空间创建一个文件映射对象(假设物理地址为0x12345000)。 3)在进程A的句柄表中添加条目:句柄值 0x44 → 内核对象 0x8A123450。 -
进程B访问:
1)进程B调用
OpenFileMapping。 2)内核找到同一个文件映射对象(仍然是物理地址0x12345000)。 3)在进程B的句柄表中添加条目:句柄值 0x58 → 内核对象 0x8A123450 -
内存映射:
// 两个进程映射同一块共享内存
void* pMemA = MapViewOfFile(hFileMappingA, ...); // 返回进程A用户空间的地址,如0x00340000
void* pMemB = MapViewOfFile(hFileMappingB, ...); // 返回进程B用户空间的地址,如0x00560000
// 虽然虚拟地址不同,但指向相同的物理内存页!
这里通过表格总结一下:
| 层面 | 是否相同 | 说明 |
|---|---|---|
| 句柄值 | 不同 | 每个进程句柄表中的索引不同 |
| 内核对象虚拟地址 | 相同 | 所有进程的内核空间映射相同 |
| 内核对象物理地址 | 相同 | 对象实际存储在相同物理内存 |
| 用户空间映射地址 | 不同 | 映射到用户空间的地址各不相同 |
3.4 重要论点
-
句柄是进程相关的:同一个内核对象在不同进程中有不同的句柄值
-
内核对象地址是系统全局的:在内核空间中,所有进程通过相同的虚拟地址访问同一个内核对象
-
这是虚拟内存技术的威力:每个进程都"认为"自己独享整个4GB地址空间,而实际上通过内存映射单元(MMU)实现了隔离与共享的完美平衡
这种设计既保证了安全性(进程不能随意访问彼此的内存),又提供了高效的进程间通信机。windows内核对象资源具体有哪些?用户应用程序是如何访问这些内核对象资源的?工作机制和原理是什么?
四. Windows内核对象资源分类
Windows内核对象是系统资源的基本封装单位,主要分为以下几大类:
4.1 进程和线程对象
- EPROCESS: 进程对象
- ETHREAD: 线程对象
- JOB: 作业对象(进程组)
4.2 同步对象
- Event: 事件对象
- Mutex: 互斥体对象
- Semaphore: 信号量对象
- WaitableTimer:可等待计时器
4.3 内存管理对象
- Section: 内存区段对象(用于文件映射和共享内存)
- Heap: 堆对象
4.4 I/O 对象
- File: 文件对象
- Device: 设备对象
- Pipe: 管道对象
- Mailslot: 邮槽对象
4.5 安全对象
- Token: 访问令牌对象
- SecurityDescriptor: 安全描述符
4.6 窗口管理对象
- Desktop: 桌面对象
- WindowStation: 窗口站对象
4.7 注册表对象
- Key: 注册表键对象
五. 用户应用程序访问内核对象的机制
用户应用程序 → 系统API调用 → 切换到内核模式 → 对象管理器 → 安全引用监视器 → 内核对象
↓ ↓ ↓ ↓ ↓
用户模式 系统调用 模式切换 对象查找 权限验证
详细访问机制
5.1 获取对象访问权
应用程序首先必须获得对象的访问权限,通常通过:
// 创建新对象
HANDLE hMutex = CreateMutex(NULL, FALSE, L"MyMutex");
// 打开现有对象
HANDLE hFile = OpenFile(
GENERIC_READ,
FILE_SHARE_READ,
"C:\\file.txt"
);
// 复制现有句柄(常用于进程继承)
HANDLE hInherited = InheritHandle(hExisting);
5.2 对象查找和验证过程
当应用程序调用系统API时:
// 用户态调用
BOOL result = WriteFile(hFile, buffer, size, &written, NULL);
// 内核态处理流程:
NTSTATUS NtWriteFile(
HANDLE FileHandle, // 用户传递的句柄
PVOID Buffer, // 用户缓冲区
ULONG Length, // 数据长度
...)
{
// 1. 通过句柄找到内核对象
PFILE_OBJECT file_object;
status = ObReferenceObjectByHandle(
FileHandle, // 用户句柄
FILE_WRITE_DATA, // 所需权限
*FileObjectType, // 对象类型
KernelMode, // 访问模式
&file_object, // 返回的对象指针
NULL
);
// 2. 执行实际写操作
if (NT_SUCCESS(status)) {
status = DoFileWrite(file_object, Buffer, Length);
ObDereferenceObject(file_object); // 减少引用计数
}
return status;
}
5.3 内核对象管理器的工作机制
5.3.1 对象管理器核心组件
对象管理器
├── 对象类型 (ObjectType)
│ ├── 类型名称 (如 "Process", "File")
│ ├── 方法函数 (Open, Close, Delete, Parse)
│ └── 默认配额
├── 对象头 (OBJECT_HEADER)
│ ├── 对象名称
│ ├── 引用计数
│ ├── 安全描述符
│ └── 类型指针
└── 对象体 (实际对象数据)
5.3.2 对象结构内存布局
// 内核对象在内存中的典型布局
+------------------------+
| OBJECT_HEADER | // 对象头 - 对象管理器使用
| - PointerCount | // 指针引用计数
| - HandleCount | // 句柄引用计数
| - TypeIndex | // 指向对象类型
| - SecurityDescriptor | // 安全信息
+------------------------+
| EPROCESS | // 对象体 - 实际的对象数据
| - ProcessId |
| - ParentProcessId |
| - ImageFileName |
| - ThreadListHead |
| - ... |
+------------------------+
5.4 句柄到内核对象的转换过程
5.4.1 进程句柄表结构
// 每个进程的句柄表条目
typedef struct _HANDLE_TABLE_ENTRY {
union {
PVOID Object; // 对象指针的低位
ULONG ObAttributes; // 对象属性
struct {
// 三级表项指针或对象地址
};
};
union {
ULONG GrantedAccess; // 访问权限掩码
struct {
USHORT GrantedAccessIndex;
USHORT CreatorBackTraceIndex;
};
};
} HANDLE_TABLE_ENTRY, *PHANDLE_TABLE_ENTRY;
5.4.2 句柄解析过程
HANDLE hObject = 0x24; // 应用程序持有的句柄
// 内核中的解析过程:
PVOID ResolveHandle(HANDLE Handle)
{
// 1. 获取当前进程的句柄表
PHANDLE_TABLE handle_table = CurrentProcess->ObjectTable;
// 2. 计算句柄在表中的索引
ULONG index = Handle & HANDLE_TABLE_INDEX_MASK;
// 3. 查找句柄表条目
PHANDLE_TABLE_ENTRY entry = &handle_table->Table[index];
// 4. 获取对象指针(需要处理多级表)
PVOID object = entry->Object & ~OBJECT_HANDLE_FLAG_MASK;
// 5. 返回对象头
return (POBJECT_HEADER)((PUCHAR)object - sizeof(OBJECT_HEADER));
}
5.5 安全验证机制
5.5.1 访问检查流程
BOOLEAN AccessCheck(
PSECURITY_DESCRIPTOR SecurityDescriptor,
HANDLE ClientToken,
ACCESS_MASK DesiredAccess)
{
// 1. 获取对象的安全描述符
PSECURITY_DESCRIPTOR sd = GetObjectSecurity(Object);
// 2. 获取访问者的令牌
PACCESS_TOKEN token = GetCurrentThreadToken();
// 3. 执行权限匹配
GENERIC_MAPPING mapping = GetGenericMapping(ObjectType);
MapGenericMask(&DesiredAccess, &mapping);
// 4. 检查权限
return SeAccessCheck(
sd, // 对象安全描述符
token, // 访问者令牌
DesiredAccess,// 请求的权限
...);
}
六. 完整示例:文件访问的完整流程
// 用户应用程序
HANDLE hFile = CreateFile(
"test.txt", // 文件名
GENERIC_READ | GENERIC_WRITE, // 请求的访问权限
0, NULL, OPEN_EXISTING, 0, NULL);
if (hFile != INVALID_HANDLE_VALUE) {
char buffer[100];
DWORD bytesRead;
// 读取文件
ReadFile(hFile, buffer, sizeof(buffer), &bytesRead, NULL);
CloseHandle(hFile); // 释放句柄
}
// 内核中的处理:
NTSTATUS NtCreateFile(
OUT PHANDLE FileHandle,
IN ACCESS_MASK DesiredAccess,
IN POBJECT_ATTRIBUTES ObjectAttributes,
...)
{
// 1. 解析文件路径
status = IoParseDevice(ObjectAttributes->ObjectName, ...);
// 2. 安全检查
status = SeAccessCheck(..., DesiredAccess, ...);
// 3. 创建文件对象
status = IoCreateFileSpecifyDeviceObjectHint(
FileHandle, DesiredAccess, ObjectAttributes, ...);
// 4. 在调用进程的句柄表中创建条目
status = ObInsertObject(FileObject, NULL, DesiredAccess, 0, NULL, FileHandle);
return status;
}
七. 关键工作机制总结
-
句柄抽象层:应用程序通过句柄间接访问内核对象
-
权限分离:用户模式代码无法直接访问内核内存
-
系统调用门铃:通过特定的CPU指令(syscall/sysenter)切换到内核模式
-
对象引用计数:确保对象在使用期间不会被意外释放
-
安全验证:每次访问都经过严格的安全检查
-
资源配额:防止单个进程耗尽系统资源
这种精密的机制确保了Windows系统的稳定性、安全性和可靠性,是现代操作系统设计的典范。
9091

被折叠的 条评论
为什么被折叠?



