深入探讨windows系统中句柄概念

在Windows应用程序中,设备句柄(Handle)是一个核心概念。句柄本质上是一个标识符,用于代表系统资源,如窗口、文件、内存块、设备上下文等。设备句特指与设备相关的资源,例如设备上下文(Device Context, DC)句柄,它用于绘图操作。

在Windows编程中,当你需要在一个窗口上绘图时,你需要获取该窗口的设备上下文句柄(HDC)。通过这个句柄,你可以调用GDI(Graphics Device Interface)函数来绘制图形、文本等。

句柄是一个抽象的概念,它通常是一个整数值,但应用程序不应该直接解释这个值,而是通过Windows API函数来使用它。句柄的使用使得Windows能够管理资源,并且提供一种安全的方式让应用程序访问这些资源。

例如,当你创建一个窗口时,你会得到一个HWND(窗口句柄)。当你需要在这个窗口上绘图时,你可以使用GetDC函数传入HWND来获取HDC。完成绘图后,你必须释放这个HDC,以便系统可以回收资源。

下面是一个简单的示例,说明如何在一个窗口上绘制一个矩形:

  1. 获取窗口的设备上下文句柄(HDC)。

  2. 使用该句柄调用Rectangle函数。

  3. 释放设备上下文句柄。

注意:在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_VALUENULL表示无效句柄。

  • 作用域:句柄只在当前进程上下文中有效。

  • 继承性:某些句柄可以在父子进程间继承。

设备句柄是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进程在创建时,系统都会为它分配一个私有的句柄表。您可以把它想象成一个“令牌索引簿”。

句柄值 (索引)指向的内核对象内存地址访问掩码
0x000000040x8A123450...
0x000000080x8A1234A0...
0x0000000C(空)...

当应用程序调用 CreateFile 时:

        1)内核创建一个文件内核对象。

        2)内核在该进程的句柄表中找到一个空闲条目。

        3)内核将该条目指向刚创建的文件内核对象。

        4)内核将句柄表索引(例如 0x00000008)返回给应用程序。这个索引值,就是我们得到的句柄

2.3  原理详解:为什么句柄是安全的

  1. 只是一个索引:你的句柄 0x00000008 在你的进程A的句柄表中是索引#2,它指向对象X。但在进程B中,同样的值 0x00000008 可能指向一个完全不同的对象Y,甚至是空的。因此,一个进程无法通过猜测句柄值来访问另一个进程的资源。

  2. 访问验证:当您调用 WriteFile(hFile, ...) 时:

    • 系统切换到内核模式。

    • 系统查找当前进程的句柄表。

    • 使用你传递的句柄值作为索引,找到对应的条目。

    • 验证该条目是否有效,以及你请求的(写)操作是否被该句柄的访问掩码所允许。

    • 如果一切正常,系统通过条目中的指针找到真正的内核对象,并执行写操作。

  3. 引用计数:内核对象有一个引用计数,用于记录有多少个句柄(可能来自不同进程)正在引用它。当通过 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. 内核对象创建

     1)进程A调用CreateFileMapping。                                                                                     2)内核在内核空间创建一个文件映射对象(假设物理地址为0x12345000)。                       3)在进程A的句柄表中添加条目:句柄值 0x44 → 内核对象 0x8A123450。

  2. 进程B访问

    1)进程B调用OpenFileMapping。                                                                                              2)内核找到同一个文件映射对象(仍然是物理地址0x12345000)。                                      3)在进程B的句柄表中添加条目:句柄值 0x58 → 内核对象 0x8A123450

  3. 内存映射

// 两个进程映射同一块共享内存
void* pMemA = MapViewOfFile(hFileMappingA, ...); // 返回进程A用户空间的地址,如0x00340000
void* pMemB = MapViewOfFile(hFileMappingB, ...); // 返回进程B用户空间的地址,如0x00560000

// 虽然虚拟地址不同,但指向相同的物理内存页!

这里通过表格总结一下:

层面是否相同说明
句柄值不同每个进程句柄表中的索引不同
内核对象虚拟地址相同所有进程的内核空间映射相同
内核对象物理地址相同对象实际存储在相同物理内存
用户空间映射地址不同映射到用户空间的地址各不相同

3.4  重要论点

  1. 句柄是进程相关的:同一个内核对象在不同进程中有不同的句柄值

  2. 内核对象地址是系统全局的:在内核空间中,所有进程通过相同的虚拟地址访问同一个内核对象

  3. 这是虚拟内存技术的威力:每个进程都"认为"自己独享整个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;
}

七.  关键工作机制总结

  1. 句柄抽象层:应用程序通过句柄间接访问内核对象

  2. 权限分离:用户模式代码无法直接访问内核内存

  3. 系统调用门铃:通过特定的CPU指令(syscall/sysenter)切换到内核模式

  4. 对象引用计数:确保对象在使用期间不会被意外释放

  5. 安全验证:每次访问都经过严格的安全检查

  6. 资源配额:防止单个进程耗尽系统资源

这种精密的机制确保了Windows系统的稳定性、安全性和可靠性,是现代操作系统设计的典范。

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值