Windows內核-句柄
概述
進程的地址空間分為系統空間和用戶空間,內核對象都保存在系統空間中,用戶空間不能通過地址作為指針來引用它們,Windows使用句柄(handle)來對內核對象進行引用。看起來很小,但是涉及的內容很多。
句柄
所謂的句柄值其實是進程結構體中句柄表中的索引,通過該句柄值在句柄表中進行邏輯換算就可以變成內核對象的指針來進行操作。
因為是進程句柄表中的索引,所以句柄只在進程中有效。一個進程中的句柄值傳遞給另外一個進程后,句柄值將不再有效。這種表被稱為私有句柄表或者進程句柄表。
在Windows中還有一種句柄表叫全局句柄表是Windows全局都可以使用的,和私有句柄表稍許區別。
在EPROCESS結構體中,對應的ObjectTable字段來包含進程的句柄表信息。
相關結構體:
注:OS環境為Win7 SP1 32位
//0x3c bytes (sizeof)struct _HANDLE_TABLE{ ULONG TableCode; //0x0 //指向句柄表的存儲結構(非常重要的字段) struct _EPROCESS* QuotaProcess; //0x4 //句柄表的內存資源存儲在此進程中 VOID* UniqueProcessId; //0x8 //創建進程的ID,用于回調函數 struct _EX_PUSH_LOCK HandleLock; //0xc //句柄表鎖,僅在句柄表擴展時使用 struct _LIST_ENTRY HandleTableList; //0x10 //所有的句柄表形成一個鏈表,該字段作為一個鏈表節點 //鏈表頭為全局變量HandleTableListHead struct _EX_PUSH_LOCK HandleContentionEvent; //0x18 //訪問句柄時發生競爭,就通過該推鎖進行等待 struct _HANDLE_TRACE_DEBUG_INFO* DebugInfo; //0x1c //僅當使用調試句柄時才有意義 LONG ExtraInfoPages; //0x20 //審計信息所占用的頁面數量 union { ULONG Flags; //0x24 //標志域 UCHAR StrictFIFO:1; //0x24 //是否使用隊列的風格,FIFO先進先出,先釋放的地方先使用。 }; ULONG FirstFreeHandle; //0x28 //當前句柄表中的空閑句柄表項的索引值 //句柄索引值按HANDLE_VALUE_INC逐個遞增,在win7 sp1 32位中為4字節 struct _HANDLE_TABLE_ENTRY* LastFreeHandleEntry; //0x2c //當前句柄表中最后一個空閑句柄表項的地址 ULONG HandleCount; //0x30 //正在使用的句柄表項數量 ULONG NextHandleNeedingPool; //0x34 //下一次句柄表擴展的起始句柄索引,也就是下一個新的句柄表的首地址 ULONG HandleCountHighWatermark; //0x38
};
//0x8 bytes (sizeof) struct _HANDLE_TABLE_ENTRY{ union { VOID* Object; //0x0 ULONG ObAttributes; //0x0 struct _HANDLE_TABLE_ENTRY_INFO* InfoTable; //0x0 ULONG Value; //0x0 }; union { ULONG GrantedAccess; //0x4 struct { USHORT GrantedAccessIndex; //0x4 USHORT CreatorBackTraceIndex; //0x6 }; ULONG NextFreeTableEntry; //0x4 };};/*該結構體后續再解釋 目前只需知道該結構體的低32位到低2位保存的是內核對象的首地址 以下在結構體中的低地址的union的32-2位中保存著首地址 union { VOID* Object; //0x0 ULONG ObAttributes; //0x0 struct _HANDLE_TABLE_ENTRY_INFO* InfoTable; //0x0 ULONG Value; //0x0 }; 例如:_handle_table_entry == 00000001`8812ad09 那么對應的對象首地址就為 8812ad09將低3位清零的結果:8812ad08*/
句柄表
句柄表是一個多層結構,最多有三層,最少有一層。層數由TableCode的低2位的值來判斷,低2位為0時為1層,為1時為2層,為2時為3層。
例如:TableCode = 0x88884222; 其中的低二位為10B,也就是0x2,所以該句柄表就為三層結構。
最底層結構中保存的內容才是實實在在需要的內容,叫做句柄項(_HANDLE_TABLE_ENTRY),往上的層數內容都是在當前層數的句柄表不夠時新建的數組來保存之前的句柄表的首地址。類似于分頁管理中的機制,通過好幾個數組來嵌套,最終有效的,實實在在指向了頁的起始地址的只有頁表項。

句柄表產生流程:
注,以WRK中的代碼為例:
在執行體中創建進程時會首先為新進程分配一個單層句柄表,然后并初始化。
句柄在句柄表中呈線性增加,當增加一個句柄時會在當前最后一個句柄表的數組中往后添加一個句柄表項。
接著隨著進程中句柄數的增加,如果當前句柄表不夠使用,就會擴展句柄表,以此由單層到2層最多到3層。Windows中進程的句柄是有限制的在WRK中最多有2的24次方個。類似于分頁管理機制。
創建單層句柄表采用:ExCreateHandleTable()函數來完成,初始化句柄表采用ExpAllocateHandleTable()函數來完成,擴展句柄表采用ExpAllocateHandleTableEntrySlow()函數來完成。
以上函數在WRK中都有仔細記載。
TableCode字段
TableCode字段是句柄表信息中最關鍵的字段,前面寫道該字段的低2位標識了句柄表的層數,除了該作用還有別的作用。
該字段的低兩位清零后的地址為句柄表的最高層表的首地址。
句柄通過句柄表得到內核對象的流程:
一個有效的句柄有四種可能:

-1和-2的情況不需要詳解,主要是句柄值為一個正常的4倍數的情況。
同時還要區分私有句柄表和全局句柄表,兩者的內容有一點小小的區別。
通過私有句柄表:
(參考WRK)

通過公有句柄表:
私有句柄表和公有句柄表唯一的區別是句柄表項的不同,私有表的_HANDLE_TABLE_ENTRY內存放的是指向OBJECT_HEADETR對象頭的首地址,而公有表中存放的是對象Body的首地址。
Windbg查看
私有句柄表
首先在虛擬機中采用Process Explorer來查看notepad++中的句柄內容:

然后根據句柄值來找到對應的內核對象的地址:
1、在Windbg中找到notepad++的句柄表_HANDLE_TABLE 結構體首地址:
kd> !process 0 0PROCESS 86e97d20 SessionId: 1 Cid: 03e8 Peb: 7ffdf000 ParentCid: 0594 DirBase: 07f70000 ObjectTable: a79b91c0 HandleCount: 72. Image: notepad++.exe // 首地址為:a79b91c0
2、通過_HANDLE_TABLE結構體得到TableCode內容:
kd> dt _HANDLE_TABLE a79b91c0ntdll!_HANDLE_TABLE +0x000 TableCode : 0x8b4a0000 +0x004 QuotaProcess : 0x86e97d20 _EPROCESS +0x008 UniqueProcessId : 0x000003e8 Void +0x00c HandleLock : _EX_PUSH_LOCK +0x010 HandleTableList : _LIST_ENTRY [ 0x84145e28 - 0xa87854f8 ] +0x018 HandleContentionEvent : _EX_PUSH_LOCK +0x01c DebugInfo : (null) +0x020 ExtraInfoPages : 0n0 +0x024 Flags : 0 +0x024 StrictFIFO : 0y0 +0x028 FirstFreeHandle : 0xcc +0x02c LastFreeHandleEntry : 0x8b4a0ff8 _HANDLE_TABLE_ENTRY +0x030 HandleCount : 0x48 +0x034 NextHandleNeedingPool : 0x800 +0x038 HandleCountHighWatermark : 0x4b
3、解析TableCode:
0x8b4a0000 的前四位為: 1000 表明只有一層句柄表, 清零后得到句柄表的首地址:
0x8b4a0000
4、根據句柄表的層數和句柄值來得到句柄所在的最底層句柄表。
4.1 如果只有一層就是TableCode前30位的值 4.2 如果有兩層就需要先將句柄值除以512,看看占滿了多少個最底層句柄表,然后將在TableCode中找到前面占滿了的最底層句柄表的首地址的存放地址,再后面一個就是對應的最底層句柄表了。然后將句柄值-占滿句柄表的個數*512等到在對應的最底層句柄表中句柄的偏移值,然后將該值*2得到句柄表項在句柄表中的偏移值。 4.3 和4.2類似。 這里的情況就是4.1,直接可以得到對應表的地址為0x8b4a0000
5、通過句柄得到句柄項在表中的偏移:
句柄表中存放的是句柄項,句柄項是一個結構體里面包含了句柄值://0x8 bytes (sizeof)struct _HANDLE_TABLE_ENTRY{ union { VOID* Object; //0x0 //指向句柄代表的對象 ULONG ObAttributes; //0x0 struct _HANDLE_TABLE_ENTRY_INFO* InfoTable; //0x0 ULONG Value; //0x0 }; union { ULONG GrantedAccess; //0x4 struct { USHORT GrantedAccessIndex; //0x4 USHORT CreatorBackTraceIndex; //0x6 }; ULONG NextFreeTableEntry; //0x4 };};
因為句柄表項結構大小為8字節,而句柄的大小為4字節,所以在得到句柄表中句柄表項的偏移時,還需要將對應句柄表的句柄值*2。 例如這里的句柄表值為0x28,那么對應到句柄表中的偏移為0x28*2
6、查看值:
//通過句柄表+偏移值的方式得到句柄表項中的對象地址,然后將后三位清零后得到對象的頭地址,//然后將頭地址往下偏移就可以得到對象首地址 這里查看前面圖中句柄值為0x28的內容:kd> dq 0x8b4a0000+0x28*28b4a0050 000f01ff`87b3f329 000f037f`87b3ea418b4a0060 00020019`a7a133c9 00000001`a87956198b4a0070 00000804`86e6b0d9 00000804`86de52918b4a0080 00000804`88002da1 00000804`86de0b998b4a0090 00000804`87cb48d1 00000804`88002c398b4a00a0 00000804`86cee6f9 00000804`880bb2718b4a00b0 00000804`86db1779 001f0001`87c722a98b4a00c0 001f0003`87bccf21 001f0001`87c46831 得到句柄表項中的對象值為87b3f329 將前三位清零后為: 87b3f328 往下偏移0x18后為: 87b3f340 查看對象內容: kd> !object 87b3f340Object: 87b3f340 Type: (866f67a0) Desktop ObjectHeader: 87b3f328 (new version) HandleCount: 12 PointerCount: 689 Directory Object: 00000000 Name: Default
和前面Desktop內核對象對應的值一樣。
公有句柄表:
公有句柄表和私有句柄表的區別很小,首先公有句柄表有一個全局變量PspCidTable來保存起始地址。
這里需要糾正一下Windows內核原理與實現一書中的內容:

在Win7中全局句柄表和system進程句柄表內容不同:
PROCESS 866d2900 SessionId: none Cid: 0004 Peb: 00000000 ParentCid: 0000 DirBase: 00185000 ObjectTable: 8b401b28 HandleCount: 430. Image: System System的句柄表地址為;8b401b28 kd> dd PspCidTable8418fd54 8b401080 00000000 80000020 00000101全局句柄表地位為:8b401080
但是system進程的句柄表又有個全局變量ObpKernelHandleTable來表示,看著名字這個system進程的句柄表應該叫內核句柄表吧,猜測是給內核驅動使用的句柄表。
公有句柄表和私有句柄表的區別:
1、地址的區別:公有句柄表由PspCidTable全局變量來保存,私有地址表在進程中保存。
2、句柄表項的區別:公有句柄表項中的對象地址是對象body的首地址,而私有地址表的對象地址是對象的頭的首地址。
查看公有句柄表:
和前面差不多,只是在查看內核對象時稍有區別:
kd> dd PspCidTable8414cd54 8b401080 00000000 80000020 00000101 kd> dt _handle_table 8b401080ntdll!_HANDLE_TABLE +0x000 TableCode : 0xa3aec001 +0x004 QuotaProcess : (null) +0x008 UniqueProcessId : (null) +0x00c HandleLock : _EX_PUSH_LOCK +0x010 HandleTableList : _LIST_ENTRY [ 0x8b401090 - 0x8b401090 ] +0x018 HandleContentionEvent : _EX_PUSH_LOCK +0x01c DebugInfo : (null) +0x020 ExtraInfoPages : 0n0 +0x024 Flags : 1 +0x024 StrictFIFO : 0y1 +0x028 FirstFreeHandle : 0xa1c +0x02c LastFreeHandleEntry : 0xa3ae4430 _HANDLE_TABLE_ENTRY +0x030 HandleCount : 0x272 +0x034 NextHandleNeedingPool : 0x1000 +0x038 HandleCountHighWatermark : 0x275 //TableCode后三位為1,說明有兩層。 kd> dd 0xa3aec000a3aec000 8b404000 a3ae4000 00000000 00000000a3aec010 00000000 00000000 00000000 00000000a3aec020 00000000 00000000 00000000 00000000 //數組中只有前兩個有值,說明最底層句柄表只有兩個//查看第一個表的內容:kd> dq 8b4040008b404000 fffffffe`00000000 00000000`866d29018b404010 00000000`866d2629 00000000`866f1a098b404020 00000000`866f5c81 00000000`866e60218b404030 00000000`866e6d49 00000000`866e6a718b404040 00000000`866e6799 00000000`866e64c18b404050 00000000`866e7021 00000000`866e7d498b404060 00000000`866e7a71 00000000`866e77998b404070 00000000`866e74c1 00000000`866e8021 //隨便選個句柄表項查看對應的內核對象:kd> dt _handle_table_entry 8b404050ntdll!_HANDLE_TABLE_ENTRY +0x000 Object : 0x866e7021 Void +0x000 ObAttributes : 0x866e7021 +0x000 InfoTable : 0x866e7021 _HANDLE_TABLE_ENTRY_INFO +0x000 Value : 0x866e7021 +0x004 GrantedAccess : 0 +0x004 GrantedAccessIndex : 0 +0x006 CreatorBackTraceIndex : 0 +0x004 NextFreeTableEntry : 0 //將object前三位清零查看對應的內核對象:kd> !object 0x866e7020Object: 866e7020 Type: (866d2de8) Thread ObjectHeader: 866e7008 (new version) HandleCount: 0 PointerCount: 1
句柄的權限
句柄是擁有操作權限的,不然胡亂使用很恐怖的,稍微懂點就可以讓你的Windows系統壞掉。
(注:每個內核對象的句柄權限是有區別的,類似于對象頭和對象的關系,這里以進程對象句柄舉例。
句柄的權限就保存在剛剛的句柄表項中:
struct _HANDLE_TABLE_ENTRY{ union { VOID* Object; //0x0 ULONG ObAttributes; //0x0 struct _HANDLE_TABLE_ENTRY_INFO* InfoTable; //0x0 ULONG Value; //0x0 }; union { ULONG GrantedAccess; //0x4 struct { USHORT GrantedAccessIndex; //0x4 USHORT CreatorBackTraceIndex; //0x6 }; ULONG NextFreeTableEntry; //0x4 };};
將該結構體劃分為幾個板塊:

在有一些博客上是這樣講的:
(1)這一塊共兩個字節,低字節保留位恒為0,高字節是給SetHandleInformation這個函數用的,比如寫成函數SetHandleInformation(Handle,HANDLE_FLAG_PROTECT_FROM_CLOSE,HANDLE_FLAG_PROTECT_FROM_CLOSE),那么這個位置將會被寫入0x02;
HANDLE_FLAG_PROTECT_FROM_CLOSE宏的值為0x00000002,取最低字節,最終(1)這塊是0x0200。
(2)這塊是訪問掩碼,是給OpenProcess這個函數用的
OpenProcess(dwDesiredAccess,BInheritHandle,dwProcessId);具體的存的就是這個函數的第一個參數的值。
(3)和(4)這兩塊共計四個字節,其中bit0-bit2存的是這個句柄的屬性,其中bit2 bit0 默認為0,1;bit1表示的函數是該句柄是否可繼承;OpenProcess的第二個參數與bit1有關;
bit31-bit3則是存放的該內核對象在內核中的具體的地址。
原文鏈接:https://blog.csdn.net/csnn2019/article/details/113818969
這個結論有對也有錯。
根據上述博客提供的結果再加上我的驗證得出的正確結論如下:
64位分為:

(32-55bit)24位:
首先介紹這24位的原因是,它是通過OpenProcess函數來指定訪問掩碼的,這個函數用過比較熟練。
其中訪問掩碼的Microsoft官方文檔:
Process Security and Access Rights - Win32 apps | Microsoft Docs
(https://docs.microsoft.com/en-us/windows/win32/procthread/process-security-and-access-rights)
在官方文檔中介紹了通用的訪問掩碼:

和針對進程的訪問掩碼:


其中將不通用訪問掩碼加起來可以得到 0xFFFF,然后將通用的訪問掩碼加起來得到 0x1F。
然后我查看了官方文檔闡述了如果采用PROCESS_ALL_ACCESS得到的句柄的前32位值的大小為:0x001FFFFF。
采用代碼驗證:
#include #includeusing namespace std;int main(){ cout << "test" << endl; DWORD tempId; cin >> tempId; auto tempHandle = OpenProcess(PROCESS_ALL_ACCESS, 0,tempId); if (tempHandle == NULL) { cout << "打開進程失敗" << endl; return 0; } CloseHandle(tempHandle); return 0;}
將結果放到od里然后定位到if (tempHandle == NULL)語句中,因為這里可以直接看到handle值:

然后運行到這里,并輸入進程的PID,這里我選擇輸入任務管理器中的notepad++的PID:

然后查看eax的值得到句柄值:

handle == ==eax ==== 0x24
然后用Windbg段下來后通過前面講述的查看私有句柄表的方式得到該句柄對應的句柄表項:
kd> !process 0 0 PROCESS 86906d20 SessionId: 1 Cid: 053c Peb: 7ffdf000 ParentCid: 0a3c DirBase: 33575000 ObjectTable: a6caa668 HandleCount: 9. Image: ApplicationTest1.exe //你的程序名稱 kd> dt _handle_table a6caa668ntdll!_HANDLE_TABLE +0x000 TableCode : 0x88608000 +0x004 QuotaProcess : 0x86906d20 _EPROCESS +0x008 UniqueProcessId : 0x0000053c Void +0x00c HandleLock : _EX_PUSH_LOCK +0x010 HandleTableList : _LIST_ENTRY [ 0x8859c788 - 0x8842c8a8 ] +0x018 HandleContentionEvent : _EX_PUSH_LOCK +0x01c DebugInfo : (null) +0x020 ExtraInfoPages : 0n0 +0x024 Flags : 0 +0x024 StrictFIFO : 0y0 +0x028 FirstFreeHandle : 0x28 +0x02c LastFreeHandleEntry : 0x88608ff8 _HANDLE_TABLE_ENTRY +0x030 HandleCount : 9 +0x034 NextHandleNeedingPool : 0x800 +0x038 HandleCountHighWatermark : 0xa kd> dq 0x88608000+0x24*0x288608048 001fffff`88175969 0000002c`0000000088608058 00000030`00000000 00000034`0000000088608068 00000038`00000000 0000003c`0000000088608078 00000040`00000000 00000044`0000000088608088 00000048`00000000 0000004c`0000000088608098 00000050`00000000 00000054`00000000886080a8 00000058`00000000 0000005c`00000000886080b8 00000060`00000000 00000064`00000000
可以看到對應的句柄表項為:
001fffff`88175969
所以32-55bit的值為1FFFFF和微軟的文檔是吻合的。(不要懷疑微軟的文檔)。
(56-64bit)8位:
這里的結果還要分為高四位和低四位,高四位的值始終為0,而低四位的值會因為SetHandleInformation()函數設置的HANDLE_FLAG_PROTECT_FROM_CLOSE標志位而改變。
SetHandleInformation()函數官方文檔 :
SetHandleInformation function (handleapi.h) - Win32 apps | Microsoft Docs
(https://docs.microsoft.com/en-us/windows/win32/api/handleapi/nf-handleapi-sethandleinformation?msclkid=cd41032bac1a11ecb388de747b53ec30)
我的驗證代碼如下:
#include #includeusing namespace std;int main(){ cout << "test" << endl; DWORD tempId; cin >> tempId; auto tempHandle = OpenProcess(0x0001, 0,tempId); if (tempHandle == NULL) { cout << "打開進程失敗" << endl; return 0; } SetHandleInformation(tempHandle, HANDLE_FLAG_PROTECT_FROM_CLOSE, HANDLE_FLAG_PROTECT_FROM_CLOSE); CloseHandle(tempHandle); return 0;}
這一次我在打開句柄時只賦值了一個0x0001的訪問掩碼,為了方便觀察。
通用采用了前面的辦法,然后停在了SetHandleInformation(tempHandle, HANDLE_FLAG_PROTECT_FROM_CLOSE, HANDLE_FLAG_PROTECT_FROM_CLOSE);函數這里:

然后查看調用前的句柄表項值:
kd> dq 0x8864a000+0x24*0x28864a048 00000001`88175969 0000002c`000000008864a058 00000030`00000000 00000034`000000008864a068 00000038`00000000 0000003c`000000008864a078 00000040`00000000 00000044`00000000
接著查看調用后的:

kd> dq 0x8864a000+0x24*0x28864a048 02000001`88175969 0000002c`000000008864a058 00000030`00000000 00000034`00000000
就從00000001變到了02000001,也就是56-64中的低4位從0變成了2。
(3-31bit)29位:
這個前面實驗過很多次了,這里就不實驗了。
(0-3bit)3位
在WRK中它的值如下:
#define OBJ_HANDLE_ATTRIBUTES (OBJ_PROTECT_CLOSE | OBJ_INHERIT | OBJ_AUDIT_OBJECT_CLOSE)
0bit位表示OBJ_AUDIT_OBJECT_CLOSE,
1bit位表示OBJ_INHERIT ,
2bit位表示OBJ_PROTECT_CLOSE 。
OBJ_ADIT_OBJECT_CLOSE已經被取締為表示句柄表項的鎖標志了,如果為1表示句柄表項被鎖住了。(這個實驗我暫時弄不出來)
OBJ_INHERIT :表示是否可以被該進程創建的子進程繼承。
OBJ_PROTECT_CLOSE 指示關閉該對象時是否產生一個審計事件。(這個我也弄不出來)
OBJ_INHERIT可以在三個地方修改:
1:在打開句柄時函數的OpenProcess(dwDesiredAccess,BInheritHandle,dwProcessId),中第二個參數的指定。
2:采用SetHandleInformation(tempHandle, HANDLE_FLAG_INHERIT, HANDLE_FLAG_INHERIT)來增加權限。
3:直接修改句柄表項的內容。
全局句柄表
前面介紹了私有句柄表,但是沒有完整的解釋私有句柄表的內容。
首先私有句柄表只包含進程和線程。進程有一個唯一ID,在EPROCESS中叫做UniqueProcessId,ETHREAD有一個_CLIENT_ID字段包含了線程的唯一ID和線程對應的進程ID。
這里的唯一ID是在創建進程和線程時通過ExCreateHandle函數在全局句柄表PspClidTable中創建的句柄索引值,此表也被叫做CID句柄表(Client ID handle table)。在WRK中可以找到有效證據:
NTSTATUSPspCreateProcess( OUT PHANDLE ProcessHandle, IN ACCESS_MASK DesiredAccess, IN POBJECT_ATTRIBUTES ObjectAttributes OPTIONAL, IN HANDLE ParentProcess OPTIONAL, IN ULONG Flags, IN HANDLE SectionHandle OPTIONAL, IN HANDLE DebugPort OPTIONAL, IN HANDLE ExceptionPort OPTIONAL, IN ULONG JobMemberLevel ){ ... Process->UniqueProcessId = ExCreateHandle (PspCidTable, &CidEntry); ...}
所以PID和TID其實也是句柄值,只不過是對應的全局句柄表。所以進程的PID和線程的TID也是和進程一樣都是4的倍數,至于0值,在windows中0值是給空閑進程留著的。
然后全局句柄表的首地址保存在全局變量PspClidTable中,至于原因得問Microsoft了。
最后是全局句柄表中的句柄表項對應對象地址的是內核對象Body地址,而不是私有句柄表中的內核對象的對象頭地址。
由于保存的是body地址,所以在內核中,根據進程或線程的唯一ID值,可以很快的找到對應的內核對象,例如以下API:

別的就和私有句柄表沒差了。
句柄小結
句柄是在應用層的一種內核對象的使用方式通常是和API一起使用,在內核中也可以使用,句柄要通過句柄表來和內核對象進行關聯,句柄表又分為私有句柄表和公有句柄表。
當通過API+句柄的形式來使用內核對象時,操作系統通過句柄值來訪問句柄表得到對應的句柄表項的內容,然后根據句柄表項的內容驗證句柄的訪問權限,后再進行API對應的操作來操作內核對象。
參考資料
《Windows內核原理與實現》 潘愛民
微軟官方文檔
借鑒博客
私有句柄表(內核對象,并非用戶對象),全局句柄表_尋夢&之璐的博客-CSDN博客(https://blog.csdn.net/csnn2019/article/details/113818969?msclkid=41ffee33ab4f11ec96839b89637392ec)