Process và thread là nền tảng mà gần như mọi cơ chế user-mode khác được xây trên đó, nên có một mô hình tư duy rõ ràng về chúng sẽ trả lại lợi ích trong cả debug, tối ưu hiệu năng, và bảo mật. Windows cung cấp một API khá gọn gàng cho tất cả những thứ này, nhưng chủ đề vẫn mờ mịt với phần lớn mọi người - những phần thực sự thú vị (cấu trúc kernel, scheduler, các loại process kỳ lạ) thì tài liệu vừa thưa vừa rải rác.
Bài này là chuyến đi mà tôi từng mong có được. Thay vì kể lại API reference, nó dựng khái niệm lên từ đầu, chỉ ra cấu trúc kernel đằng sau từng thứ, và - phần mà hầu hết các bài viết bỏ qua - hướng bạn tới cách thấy tất cả những điều đó trên một hệ thống đang chạy bằng WinDbg hoặc Process Explorer. Chúng ta cũng sẽ xem API fiber và job nằm ở tầng trên, và cập nhật vài chi tiết (WSL2, Windows container) mà những bài viết cũ hơn chưa kịp có.
Quan hệ giữa Process và Thread
Dưới đây là bản tóm tắt về việc chúng nằm ở đâu và liên kết với nhau thế nào. Đây cũng là cái nhìn tổng quan về cách một số cấu trúc của process và thread móc nối vào nhau.
Giờ hãy đi vào process.
Process
Một ngộ nhận phổ biến là program và process là cùng một thứ. Không phải. Program chỉ là một file nằm trên ổ đĩa chứa code. Còn process là một cái hộp đang sống: nó chứa một hoặc nhiều thread cùng toàn bộ tài nguyên mà những thread đó cần để chạy.
Tài nguyên của một Process
Một process thực sự sở hữu những gì? Tập hợp chính xác thì khác nhau, nhưng gần như mọi process đều mang cùng những phần cốt lõi sau.
Process Identifier (PID) - Một con số duy nhất định danh process trên hệ thống. Hai process có thể trùng tên, nhưng không thể có hai process cùng PID tại cùng một thời điểm.
Không gian địa chỉ ảo riêng - Một khoảng địa chỉ ảo được dành riêng cho process sử dụng. Kích thước khác nhau tuỳ hệ thống và cấu hình.
Executable Code - Code được map vào không gian địa chỉ của process từ program tương ứng. Thú vị là một process có thể tồn tại mà không có code thực thi nào - Windows dùng loại process đó cho vài mục đích đặc biệt.
Handle Table - Chứa con trỏ tới các kernel object mà process đang dùng. Những giá trị handle mà API trả về cho bạn thực chất chỉ là chỉ số trong bảng này. Nó nằm ở kernel mode, không thể chạm trực tiếp từ user mode - và nó chỉ theo dõi kernel object, không bao gồm các loại như GDI hay user object.
Access Token - Mỗi process mang một access token mô tả ngữ cảnh bảo mật của nó: thuộc user nào, có những privilege gì, v.v.
Process Environment Block (PEB) - Một cấu trúc user-mode, mỗi process một cái, chứa đầy thông tin về process - tham số dòng lệnh, có đang bị debug hay không, danh sách module đã nạp, và nhiều nữa. Microsoft không tài liệu hoá nó, nhưng nó trông như thế này:
1struct _PEB {
2 0x000 BYTE InheritedAddressSpace;
3 0x001 BYTE ReadImageFileExecOptions;
4 0x002 BYTE BeingDebugged;
5 0x003 BYTE SpareBool;
6 0x004 void* Mutant;
7 0x008 void* ImageBaseAddress;
8 0x00c _PEB_LDR_DATA* Ldr;
9 0x010 _RTL_USER_PROCESS_PARAMETERS* ProcessParameters;
10 0x014 void* SubSystemData;
11 0x018 void* ProcessHeap;
12 0x01c _RTL_CRITICAL_SECTION* FastPebLock;
13 0x020 void* FastPebLockRoutine;
14 0x024 void* FastPebUnlockRoutine;
15 0x028 DWORD EnvironmentUpdateCount;
16 0x02c void* KernelCallbackTable;
17 0x030 DWORD SystemReserved[1];
18 0x034 DWORD ExecuteOptions:2;
19 0x034 DWORD SpareBits:30;
20 0x038 _PEB_FREE_BLOCK* FreeList;
21 0x03c DWORD TlsExpansionCounter;
22 0x040 void* TlsBitmap;
23 0x044 DWORD TlsBitmapBits[2];
24 0x04c void* ReadOnlySharedMemoryBase;
25 0x050 void* ReadOnlySharedMemoryHeap;
26 0x054 void** ReadOnlyStaticServerData;
27 0x058 void* AnsiCodePageData;
28 0x05c void* OemCodePageData;
29 0x060 void* UnicodeCaseTableData;
30 0x064 DWORD NumberOfProcessors;
31 0x068 DWORD NtGlobalFlag;
32 0x070 _LARGE_INTEGER CriticalSectionTimeout;
33 0x078 DWORD HeapSegmentReserve;
34 0x07c DWORD HeapSegmentCommit;
35 0x080 DWORD HeapDeCommitTotalFreeThreshold;
36 0x084 DWORD HeapDeCommitFreeBlockThreshold;
37 0x088 DWORD NumberOfHeaps;
38 0x08c DWORD MaximumNumberOfHeaps;
39 0x090 void** ProcessHeaps;
40 0x094 void* GdiSharedHandleTable;
41 0x098 void* ProcessStarterHelper;
42 0x09c DWORD GdiDCAttributeList;
43 0x0a0 void* LoaderLock;
44 0x0a4 DWORD OSMajorVersion;
45 0x0a8 DWORD OSMinorVersion;
46 0x0ac WORD OSBuildNumber;
47 0x0ae WORD OSCSDVersion;
48 0x0b0 DWORD OSPlatformId;
49 0x0b4 DWORD ImageSubsystem;
50 0x0b8 DWORD ImageSubsystemMajorVersion;
51 0x0bc DWORD ImageSubsystemMinorVersion;
52 0x0c0 DWORD ImageProcessAffinityMask;
53 0x0c4 DWORD GdiHandleBuffer[34];
54 0x14c void (*PostProcessInitRoutine)();
55 0x150 void* TlsExpansionBitmap;
56 0x154 DWORD TlsExpansionBitmapBits[32];
57 0x1d4 DWORD SessionId;
58 0x1d8 _ULARGE_INTEGER AppCompatFlags;
59 0x1e0 _ULARGE_INTEGER AppCompatFlagsUser;
60 0x1e8 void* pShimData;
61 0x1ec void* AppCompatInfo;
62 0x1f0 _UNICODE_STRING CSDVersion;
63 0x1f8 void* ActivationContextData;
64 0x1fc void* ProcessAssemblyStorageMap;
65 0x200 void* SystemDefaultActivationContextData;
66 0x204 void* SystemAssemblyStorageMap;
67 0x208 DWORD MinimumStackCommit;
68};
Thread - Thực thể bên trong process thực sự chạy code. Mọi process đều bắt đầu với ít nhất một thread - primary thread. Một process không có thread nào về lý thuyết vẫn tồn tại được, nhưng chẳng có gì thực thi, nên không hữu dụng lắm.
Cấu trúc EPROCESS - Cách kernel tự biểu diễn một process object. Nó khổng lồ - chứa gần như mọi thứ kernel biết về một process. Microsoft không tài liệu hoá. Ta sẽ mổ nó ở phần dưới.
Cấu trúc KPROCESS - Lồng bên trong
EPROCESS, nó mang các chi tiết liên quan tới scheduling và memory: con trỏ tới page directory của process, thời gian CPU đã tiêu ở user mode so với kernel mode, affinity, và nhiều nữa. Cũng không được tài liệu hoá. Một góc nhìn đã cắt gọn:
1struct _KPROCESS {
2 struct _DISPATCHER_HEADER Header;
3 struct _LIST_ENTRY ProfileListHead;
4 unsigned int DirectoryTableBase;
5 unsigned long Asid;
6 struct _LIST_ENTRY ThreadListHead;
7 unsigned long ProcessLock;
8 unsigned long Spare0;
9 unsigned int DeepFreezeStartTime;
10 struct _KAFFINITY_EX Affinity;
11 struct _LIST_ENTRY ReadyListHead;
12 struct _SINGLE_LIST_ENTRY SwapListEntry;
13 struct _KAFFINITY_EX ActiveProcessors;
14 long AutoAlignment : 1;
15 long DisableBoost : 1;
16 long DisableQuantum : 1;
17 unsigned long DeepFreeze : 1;
18 unsigned long TimerVirtualization : 1;
19 unsigned long CheckStackExtents : 1;
20 unsigned long SpareFlags0 : 2;
21 unsigned long ActiveGroupsMask : 20;
22 long ReservedFlags : 4;
23 long ProcessFlags;
24 char BasePriority;
25 char QuantumReset;
26 unsigned int Visited;
27 union _KEXECUTE_OPTIONS Flags;
28 unsigned long ThreadSeed[20];
29 unsigned int IdealNode[20];
30 unsigned int IdealGlobalNode;
31 union _KSTACK_COUNT StackCount;
32 struct _LIST_ENTRY ProcessListEntry;
33 unsigned int CycleTime;
34 unsigned int ContextSwitches;
35 struct _KSCHEDULING_GROUP *SchedulingGroup;
36 unsigned long FreezeCount;
37 unsigned long KernelTime;
38 unsigned long UserTime;
39 void *InstrumentationCallback;
40};
Sơ đồ dưới đây gom các phần lại - tài nguyên user-mode mà một process sở hữu, và các object kernel-mode EPROCESS/KPROCESS đứng sau nó.
Tự kiểm tra lấy
Mọi cấu trúc nói ở trên đều có bản đối chiếu sống mà bạn đọc trực tiếp được từ một hệ thống đang chạy. Cách làm như sau.
WinDbg (kernel debugger)
EPROCESS là bản ghi chính của kernel cho một process - tấm gương phía kernel của PEB ở user mode. Nhắm vào lsass.exe trong WinDbg là ta có ngay địa chỉ EPROCESS của nó:
0: kd> !process 0 0 lsass.exe
PROCESS ffffcf0514782180
SessionId: 0 Cid: 02dc Peb: c7661b7000 ParentCid: 024c
DirBase: 10c761002 ObjectTable: ffff840f5f125d00 HandleCount: 1388.
Image: lsass.exe
Từ đó ta dump được chính cấu trúc đó. Output đã cắt bớt, chỉ giữ những field quan trọng:
kd> dt nt!_eprocess ffffcf0514782180
+0x000 Pcb : _KPROCESS
...
+0x4b8 Token : _EX_FAST_REF
...
+0x550 Peb : 0x0000000f`a07b9000 _PEB
...
+0x570 ObjectTable : 0xffff840f`5f125d00 _HANDLE_TABLE
Ba field nổi bật. Pcb là KPROCESS lồng bên trong - nó giữ địa chỉ page-directory vật lý được nạp vào CR3 ở mỗi lần context switch, cùng dữ liệu scheduling: quantum, affinity, và base priority. Peb trỏ sang phía user-land, nối bản ghi kernel này về lại PEB. Còn ObjectTable là con trỏ tới Handle Table theo dõi mọi kernel object mà process đang giữ mở.
Nhìn vào bên trong KPROCESS lồng ở trong:
0: kd> dt nt!_KPROCESS ffffcf0514782180
+0x000 Header : _DISPATCHER_HEADER
+0x018 ProfileListHead : _LIST_ENTRY [ 0xffffcf05`14782198 - 0xffffcf05`14782198 ]
+0x028 DirectoryTableBase : 0x00000001`0c761002
+0x030 ThreadListHead : _LIST_ENTRY [ 0xffffcf05`14779378 - 0xffffcf05`19c1d378 ]
+0x040 ProcessLock : 0
+0x044 ProcessTimerDelay : 0
+0x048 DeepFreezeStartTime : 0
+0x050 Affinity : _KAFFINITY_EX
+0x0f8 AffinityPadding : [12] 0
+0x158 ReadyListHead : _LIST_ENTRY [ 0xffffcf05`147822d8 - 0xffffcf05`147822d8 ]
+0x168 SwapListEntry : _SINGLE_LIST_ENTRY
+0x170 ActiveProcessors : _KAFFINITY_EX
+0x218 ActiveProcessorsPadding : [12] 0
...
Access Token
Token nằm ở EPROCESS.Token dưới dạng một con trỏ có tag. Phải mask bỏ các bit tag trước khi truyền cho !token:
0: kd> dt nt!_EPROCESS ffffcf0514782180 Token.Object
+0x4b8 Token :
+0x000 Object : 0xffff840f`5f1ab9be Void
0: kd> !token @@C++(0x0xffff840f5f1ab9be & ~0xf)
Thread is not impersonating. Using process token...
_EPROCESS 0xffffcf0511067080, _TOKEN 0x0000000000000000
TS Session ID: 0
User: S-1-5-18
User Groups:
00 S-1-5-32-544
Attributes - Default Enabled Owner
01 S-1-1-0
Attributes - Mandatory Default Enabled
02 S-1-5-11
Attributes - Mandatory Default Enabled
03 S-1-16-16384
Attributes - GroupIntegrity GroupIntegrityEnabled
Primary Group: S-1-5-18
Privs:
02 0x000000002 SeCreateTokenPrivilege Attributes -
03 0x000000003 SeAssignPrimaryTokenPrivilege Attributes -
04 0x000000004 SeLockMemoryPrivilege Attributes - Enabled Default
05 0x000000005 SeIncreaseQuotaPrivilege Attributes -
07 0x000000007 SeTcbPrivilege Attributes - Enabled Default
08 0x000000008 SeSecurityPrivilege Attributes -
09 0x000000009 SeTakeOwnershipPrivilege Attributes -
10 0x00000000a SeLoadDriverPrivilege Attributes -
11 0x00000000b SeSystemProfilePrivilege Attributes - Enabled Default
12 0x00000000c SeSystemtimePrivilege Attributes -
13 0x00000000d SeProfileSingleProcessPrivilege Attributes - Enabled Default
14 0x00000000e SeIncreaseBasePriorityPrivilege Attributes - Enabled Default
15 0x00000000f SeCreatePagefilePrivilege Attributes - Enabled Default
16 0x000000010 SeCreatePermanentPrivilege Attributes - Enabled Default
17 0x000000011 SeBackupPrivilege Attributes -
18 0x000000012 SeRestorePrivilege Attributes -
19 0x000000013 SeShutdownPrivilege Attributes -
20 0x000000014 SeDebugPrivilege Attributes - Enabled Default
21 0x000000015 SeAuditPrivilege Attributes - Enabled Default
22 0x000000016 SeSystemEnvironmentPrivilege Attributes -
23 0x000000017 SeChangeNotifyPrivilege Attributes - Enabled Default
25 0x000000019 SeUndockPrivilege Attributes -
28 0x00000001c SeManageVolumePrivilege Attributes -
29 0x00000001d SeImpersonatePrivilege Attributes - Enabled Default
30 0x00000001e SeCreateGlobalPrivilege Attributes - Enabled Default
31 0x00000001f SeTrustedCredManAccessPrivilege Attributes -
32 0x000000020 SeRelabelPrivilege Attributes -
33 0x000000021 SeIncreaseWorkingSetPrivilege Attributes - Enabled Default
34 0x000000022 SeTimeZonePrivilege Attributes - Enabled Default
35 0x000000023 SeCreateSymbolicLinkPrivilege Attributes - Enabled Default
36 0x000000024 SeDelegateSessionUserImpersonatePrivilege Attributes - Enabled Default
Authentication ID: (0,3e7)
Impersonation Level: Anonymous
TokenType: Primary
Source: *SYSTEM* TokenFlags: 0x2000 ( Token in use )
Token ID: 3eb ParentToken ID: 0
Modified ID: (0, 3ec)
RestrictedSidCount: 0 RestrictedSids: 0x0000000000000000
OriginatingLogonSession: 0
PackageSid: (null)
CapabilityCount: 0 Capabilities: 0x0000000000000000
LowboxNumberEntry: 0x0000000000000000
Security Attributes:
Invalid AUTHZBASEP_SECURITY_ATTRIBUTES_INFORMATION with no claims
Process Token TrustLevelSid: S-1-19-1024-8192
Process Explorer (không cần debugger)
Không có debugger? Process Explorer hiển thị cùng dữ liệu đó qua GUI. Khung nhìn chính liệt kê mọi process trên hệ thống - tên, PID, CPU, và memory ngay trong một cái nhìn:

Thread
Thread là thực thể thực sự chạy code trên CPU. Process cung cấp tài nguyên; thread làm việc. Không có thread thì process chẳng chạy được gì, và một process chứa nhiều thread cùng lúc được gọi là multi-threaded.
Thread Scheduling
Khi trên hệ thống có nhiều thread, scheduler chuyển đổi giữa chúng rất nhanh, tạo ra cảm giác rằng tất cả chạy cùng lúc. Thực tế scheduler chỉ đang nhảy qua nhảy lại giữa các thread nhanh đến mức trông như đồng thời. (Trên máy nhiều core thì một số thread thực sự có chạy song song - mỗi logical core một thread - nhưng số thread hầu như luôn nhiều hơn số core rất nhiều, nên việc chuyển đổi vẫn là chuyện chính.)
Khoảng thời gian một thread được nhận trước khi scheduler có thể đẩy nó ra gọi là quantum, đo bằng tick đồng hồ của scheduler chứ không phải bằng giây thực. Quantum rất ngắn - vài millisecond một chữ số trên Windows bản client - đó là lý do hàng chục thread cho ta cảm giác đồng thời.
Priority. Windows là scheduler preemptive, dựa trên priority: nó luôn chạy thread có priority cao nhất đang ở trạng thái ready, và một thread priority cao hơn vừa trở thành ready sẽ chiếm chỗ thread thấp hơn ngay giữa quantum. Priority là một số từ 0 tới 31, kết hợp priority class của process (Idle, Below Normal, Normal, Above Normal, High, Realtime) với relative priority của thread trong class đó. Vài khoảng đáng biết:
- 0 - dành riêng cho zero-page thread (xoá trắng bộ nhớ đã giải phóng ở chế độ nền); không gì khác chạy ở mức 0.
- 1–15 - khoảng dynamic, gần như mọi thread bình thường sống ở đây.
- 16–31 - khoảng real-time, nằm trên phần lớn hoạt động của hệ thống và cần privilege mới vào được. Một thread real-time hỏng hóc có thể làm cả hệ thống chết đói.
Priority boost. Để giữ hệ thống phản hồi tốt, Windows tạm thời đẩy priority lên khi: một thread hoàn tất I/O (để nó xử lý kết quả nhanh), các thread của cửa sổ đang ở foreground trở nên active (UI nhạy hơn), một thread thức lại sau khi chờ một event, hoặc một thread đã ready nhưng bị bỏ chờ quá lâu (chống chết đói). Boost giảm dần trở lại một mức mỗi quantum.
Thread state. Ở mỗi thời điểm một thread nằm ở một trong vài trạng thái - scheduling thực chất chỉ là việc kernel di chuyển thread giữa các trạng thái đó:
Đường đi phổ biến là Ready → Standby → Running. Từ Running, một thread có thể bị đẩy về Ready, rơi vào Waiting trên một synchronization object hoặc I/O, hoặc kết thúc và thành Terminated. Standby nghĩa là “đã được chọn để chạy tiếp theo trên một processor cụ thể” - mỗi processor chỉ có đúng một thread ở trạng thái đó tại một thời điểm.
Tài nguyên của một Thread
Process cung cấp khá nhiều, nhưng mỗi thread vẫn cần vài thứ riêng của nó.
- Context - Một cấu trúc riêng cho từng thread (do kernel quản lý) ghi lại trạng thái của toàn bộ register CPU tại lần cuối thread đó chạy. Điều này quan trọng vì mỗi CPU chỉ có thể có một thread chiếm giữ tại một thời điểm. Khi Windows chuyển thread, nó lưu trạng thái register hiện tại vào context của thread đang đi ra, và phục hồi lại khi thread đó được xếp lịch chạy lần nữa. Layout phụ thuộc processor - đây là bản x64:
1typedef struct _CONTEXT {
2 DWORD64 P1Home;
3 DWORD64 P2Home;
4 DWORD64 P3Home;
5 DWORD64 P4Home;
6 DWORD64 P5Home;
7 DWORD64 P6Home;
8 DWORD ContextFlags;
9 DWORD MxCsr;
10 WORD SegCs;
11 WORD SegDs;
12 WORD SegEs;
13 WORD SegFs;
14 WORD SegGs;
15 WORD SegSs;
16 DWORD EFlags;
17 DWORD64 Dr0;
18 DWORD64 Dr1;
19 DWORD64 Dr2;
20 DWORD64 Dr3;
21 DWORD64 Dr6;
22 DWORD64 Dr7;
23 DWORD64 Rax;
24 DWORD64 Rcx;
25 DWORD64 Rdx;
26 DWORD64 Rbx;
27 DWORD64 Rsp;
28 DWORD64 Rbp;
29 DWORD64 Rsi;
30 DWORD64 Rdi;
31 DWORD64 R8;
32 DWORD64 R9;
33 DWORD64 R10;
34 DWORD64 R11;
35 DWORD64 R12;
36 DWORD64 R13;
37 DWORD64 R14;
38 DWORD64 R15;
39 DWORD64 Rip;
40 union {
41 XMM_SAVE_AREA32 FltSave;
42 NEON128 Q[16];
43 ULONGLONG D[32];
44 struct {
45 M128A Header[2];
46 M128A Legacy[8];
47 M128A Xmm0;
48 M128A Xmm1;
49 M128A Xmm2;
50 M128A Xmm3;
51 M128A Xmm4;
52 M128A Xmm5;
53 M128A Xmm6;
54 M128A Xmm7;
55 M128A Xmm8;
56 M128A Xmm9;
57 M128A Xmm10;
58 M128A Xmm11;
59 M128A Xmm12;
60 M128A Xmm13;
61 M128A Xmm14;
62 M128A Xmm15;
63 } DUMMYSTRUCTNAME;
64 DWORD S[32];
65 } DUMMYUNIONNAME;
66 M128A VectorRegister[26];
67 DWORD64 VectorControl;
68 DWORD64 DebugControl;
69 DWORD64 LastBranchToRip;
70 DWORD64 LastBranchFromRip;
71 DWORD64 LastExceptionToRip;
72 DWORD64 LastExceptionFromRip;
73} CONTEXT, *PCONTEXT;
Hai stack - Mỗi thread sở hữu một user-mode stack và một kernel-mode stack. User-mode stack lo phần việc thông thường như biến cục bộ. Kernel-mode stack không thể chạm tới từ user mode và đóng vai trò một ranh giới bảo mật: khi một thread thực hiện syscall, tham số của nó được copy từ user-mode stack sang kernel-mode stack. Một khi CPU đã chuyển sang kernel mode, kernel kiểm tra các tham số đó - và vì kernel stack không thể bị chạm từ user mode, thread không có cách nào can thiệp vào chúng sau khi đã kiểm tra. Sự tách biệt đó là một tính chất bảo mật then chốt.
Thread Local Storage (TLS) - Vùng lưu trữ riêng cho từng thread, dành cho dữ liệu thuộc về một thread và không nên chia sẻ với thread khác.
Thread ID (TID) - Mỗi thread có một TID duy nhất, đúng như mỗi process có một PID duy nhất.
Thread Environment Block (TEB) - Bản tương ứng ở mức thread của PEB: một cấu trúc giữ phần lớn trạng thái user-mode của một thread - con trỏ tới TLS của nó,
LastErrorValue, con trỏ tới PEB, và nhiều nữa.LastErrorValuebuộc phải riêng theo từng thread - nếuGetLastErrorcủa thread này thấy được mã lỗi của thread khác thì kết quả sẽ là hỗn loạn. TEB không được Microsoft tài liệu hoá.Affinity - Đặt affinity cho một thread là ghim nó vào một CPU cụ thể. Đặt nó vào CPU 3 trên máy bốn core thì thread sẽ chỉ chạy trên CPU 3 cho tới khi nó kết thúc hoặc affinity thay đổi.
Cấu trúc ETHREAD / KTHREAD -
ETHREAD(Executive Thread) là cách kernel biểu diễn một thread object - con trỏ tới PEB,LastErrorValue, đây có phải initial thread hay không, v.v. Lồng bên trong nó,KTHREAD(Kernel Thread) mang dữ liệu scheduling: con trỏ tới kernel stack, khi nào và bao lâu thread sẽ chạy, thời gian tiêu ở user mode, và nhiều nữa. Cả hai đều không được tài liệu hoá.
Đây là cách các thành phần của một thread ghép vào nhau, gồm cả cặp ETHREAD/KTHREAD ở kernel mode:
Tự kiểm tra lấy
Phần bên trong của thread cũng quan sát được y như vậy. Một phiên kernel debugger nhắm vào cùng process lsass.exe cho ta toàn cảnh về thread.
WinDbg (kernel debugger)
Trước hết, xác nhận kích thước kernel stack mặc định - 24 KB trên hệ thống này:
0: kd> dx *(int*)&nt!KeKernelStackSize
*(int*)&nt!KeKernelStackSize : 24576 [Type: int]
Giờ tới các cấu trúc thread. Truy vấn lsass.exe với flag 0 2 sẽ liệt kê các thread của nó và lộ ra địa chỉ ETHREAD của từng cái:
0: kd> !process 0 2 lsass.exe
PROCESS ffffcf0514782180
SessionId: 0 Cid: 02dc Peb: c7661b7000 ParentCid: 024c
DirBase: 10c761002 ObjectTable: ffff840f5f125d00 HandleCount: 1388.
Image: lsass.exe
THREAD ffffcf0514779080 Cid 02dc.02ec Teb: 000000c7661bc000 Win32Thread: 0000000000000000 WAIT: (WrLpcReceive) UserMode Non-Alertable
ffffcf0514779508 Semaphore Limit 0x1
Truyền địa chỉ ETHREAD đó cho !thread sẽ cho một bức ảnh đầy đủ: lý do chờ, priority, số lần context switch, biên của kernel stack, và toàn bộ call stack tại thời điểm ta lấy mẫu.
0: kd> !thread ffffcf0514779080
THREAD ffffcf0514779080 Cid 02dc.02ec Teb: 000000c7661bc000 Win32Thread: 0000000000000000 WAIT: (WrLpcReceive) UserMode Non-Alertable
ffffcf0514779508 Semaphore Limit 0x1
Not impersonating
DeviceMap ffff840f5b635a20
Owning Process ffffcf0514782180 Image: lsass.exe
Attached Process N/A Image: N/A
Wait Start TickCount 17775 Ticks: 22974 (0:00:05:58.968)
Context Switch Count 2 IdealProcessor: 3
UserTime 00:00:00.000
KernelTime 00:00:00.000
Win32 Start Address 0x00007ff7a99c20d0
Stack Init fffffe067d107dd0 Current fffffe067d107610
Base fffffe067d108000 Limit fffffe067d102000 Call 0000000000000000
Priority 10 BasePriority 9 IoPriority 2 PagePriority 5
Kernel stack not resident.
Child-SP RetAddr : Args to Child : Call Site
fffffe06`7d107650 fffff801`61cc7260 : ffffac00`ee1db180 00000000`ffffffff 00000000`00000000 00000000`00000000 : nt!KiSwapContext+0x76
fffffe06`7d107790 fffff801`61cc678f : 00000000`00000003 00000000`00000001 fffffe06`7d107950 00000000`00000000 : nt!KiSwapThread+0x500
fffffe06`7d107840 fffff801`61cc6033 : 00000000`00000000 00000000`00000000 00000000`00000000 ffffcf05`147791c0 : nt!KiCommitThreadWait+0x14f
fffffe06`7d1078e0 fffff801`61d0e386 : ffffcf05`14779508 00000000`00000010 000004e8`fffffb01 000004d0`fffffb00 : nt!KeWaitForSingleObject+0x233
fffffe06`7d1079d0 fffff801`6207dcf8 : ffffffff`ffffffff 00000000`00000001 ffffcf05`1474ee20 00000000`00000000 : nt!AlpcpWaitForSingleObject+0x3e
fffffe06`7d107a10 fffff801`61fe8e25 : ffffffff`00000001 fffffe06`00000000 ffffcf05`1474ee20 ffffcf05`1474ee20 : nt!AlpcpCompleteDeferSignalRequestAndWait+0x3c
fffffe06`7d107a50 fffff801`61fea8bf : 000000c7`6637f6a0 00000000`00000001 00000000`00000000 fffffe06`7d107af8 : nt!AlpcpReceiveMessagePort+0x265
fffffe06`7d107ac0 fffff801`61feab2b : fffffe06`7d107ba0 000000c7`6637f6a0 00000000`00000000 00000000`00000000 : nt!AlpcpReceiveLegacyMessage+0x11f
fffffe06`7d107b60 fffff801`61feabcf : ffffcf05`14779080 00000000`00000200 00000000`00000000 00000000`00000000 : nt!NtReplyWaitReceivePortEx+0xcb
fffffe06`7d107c00 fffff801`61e11505 : ffffcf05`14779080 00000000`00000000 00000000`00000000 ffffcf05`00000000 : nt!NtReplyWaitReceivePort+0xf
fffffe06`7d107c40 00007ffc`9f2ad704 : 00000000`00000000 00000000`00000000 00000000`00000000 00000000`00000000 : nt!KiSystemServiceCopyEnd+0x25 (TrapFrame @ fffffe06`7d107c40)
000000c7`6637f478 00000000`00000000 : 00000000`00000000 00000000`00000000 00000000`00000000 00000000`00000000 : ntdll!NtReplyWaitReceivePort+0x14
ETHREAD và KTHREAD quan hệ với thread đúng như EPROCESS và KPROCESS quan hệ với process - lần lượt là tầng Executive và tầng Kernel. Dump ETHREAD trực tiếp sẽ lộ ra các field thô:
0: kd> dt nt!_ETHREAD ffffcf0514782180
+0x000 Tcb : _KTHREAD
+0x430 CreateTime : _LARGE_INTEGER 0x0
+0x438 ExitTime : _LARGE_INTEGER 0x0
+0x438 KeyedWaitChain : _LIST_ENTRY [ 0x00000000`00000000 - 0x00000000`000002dc ]
+0x448 PostBlockList : _LIST_ENTRY [ 0xffffcf05`147dc788 - 0xffffcf05`147755c8 ]
+0x448 ForwardLinkShadow : 0xffffcf05`147dc788 Void
+0x450 StartAddress : 0xffffcf05`147755c8 Void
+0x458 TerminationPort : (null)
+0x458 ReaperLink : (null)
+0x458 KeyedWaitValue : (null)
+0x460 ActiveTimerListLock : 0x144d0c01`0100d000
+0x468 ActiveTimerListHead : _LIST_ENTRY [ 0x01dceea5`09511735 - 0x00000000`00005f70 ]
+0x478 Cid : _CLIENT_ID
+0x488 KeyedWaitSemaphore : _KSEMAPHORE
+0x488 AlpcWaitSemaphore : _KSEMAPHORE
+0x4a8 ClientSecurity : _PS_CLIENT_SECURITY_CONTEXT
+0x4b0 IrpList : _LIST_ENTRY [ 0xffffcf05`14617310 - 0xffff840f`5f1ab9be ]
+0x4c0 TopLevelIrp : 0
+0x4c8 DeviceToVerify : (null)
+0x4d0 Win32StartAddress : (null)
+0x4d8 ChargeOnlySession : (null)
+0x4e0 LegacyPowerObject : (null)
+0x4e8 ThreadListEntry : _LIST_ENTRY [ 0x00000000`00000000 - 0x00000000`00000000 ]
+0x4f8 RundownProtect : _EX_RUNDOWN_REF
+0x500 ThreadLock : _EX_PUSH_LOCK
+0x508 ReadClusterSize : 0x806504a0
+0x50c MmLockOrdering : 0n-2799
+0x510 CrossThreadFlags : 0
+0x510 Terminated : 0y0
+0x510 ThreadInserted : 0y0
+0x510 HideFromDebugger : 0y0
+0x510 ActiveImpersonationInfo : 0y0
+0x510 HardErrorsAreDisabled : 0y0
...
KTHREAD lồng bên trong mang phần ruột của scheduling - con trỏ stack, cycle time, quantum, và toàn bộ trạng thái theo từng CPU mà scheduler chạm tới:
0: kd> dx -id 0,0,ffffcf0511067080 -r1 (*((ntkrnlmp!_KTHREAD *)0xffffcf0514782180))
(*((ntkrnlmp!_KTHREAD *)0xffffcf0514782180)) [Type: _KTHREAD]
[+0x000] Header [Type: _DISPATCHER_HEADER]
[+0x018] SListFaultAddress : 0xffffcf0514782198 [Type: void *]
[+0x020] QuantumTarget : 0xffffcf0514782198 [Type: unsigned __int64]
[+0x028] InitialStack : 0x10c761002 [Type: void *]
[+0x030] StackLimit : 0xffffcf0514779378 [Type: void *]
[+0x038] StackBase : 0xffffcf0519c1d378 [Type: void *]
[+0x040] ThreadLock : 0x0 [Type: unsigned __int64]
[+0x048] CycleTime : 0x0 [Type: unsigned __int64]
[+0x050] CurrentRunTime : 0x140001 [Type: unsigned long]
[+0x054] ExpectedRunTime : 0x0 [Type: unsigned long]
[+0x058] KernelStack : 0xf [Type: void *]
[+0x060] StateSaveArea : 0x0 [Type: _XSAVE_FORMAT *]
[+0x068] SchedulingGroup : 0x0 [Type: _KSCHEDULING_GROUP *]
[+0x070] WaitRegister [Type: _KWAIT_STATUS_REGISTER]
[+0x071] Running : 0x0 [Type: unsigned char]
[+0x072] Alerted [Type: unsigned char [2]]
[+0x074 ( 0: 0)] AutoBoostActive : 0x0 [Type: unsigned long]
[+0x074 ( 1: 1)] ReadyTransition : 0x0 [Type: unsigned long]
[+0x074 ( 2: 2)] WaitNext : 0x0 [Type: unsigned long]
[+0x074 ( 3: 3)] SystemAffinityActive : 0x0 [Type: unsigned long]
[+0x074 ( 4: 4)] Alertable : 0x0 [Type: unsigned long]
[+0x074 ( 5: 5)] UserStackWalkActive : 0x0 [Type: unsigned long]
[+0x074 ( 6: 6)] ApcInterruptRequest : 0x0 [Type: unsigned long]
[+0x074 ( 7: 7)] QuantumEndMigrate : 0x0 [Type: unsigned long]
[+0x074 ( 8: 8)] UmsDirectedSwitchEnable : 0x0 [Type: unsigned long]
[+0x074 ( 9: 9)] TimerActive : 0x0 [Type: unsigned long]
[+0x074 (10:10)] SystemThread : 0x0 [Type: unsigned long]
[+0x074 (11:11)] ProcessDetachActive : 0x0 [Type: unsigned long]
[+0x074 (12:12)] CalloutActive : 0x0 [Type: unsigned long]
[+0x074 (13:13)] ScbReadyQueue : 0x0 [Type: unsigned long]
[+0x074 (14:14)] ApcQueueable : 0x0 [Type: unsigned long]
[+0x074 (15:15)] ReservedStackInUse : 0x0 [Type: unsigned long]
[+0x074 (16:16)] UmsPerformingSyscall : 0x0 [Type: unsigned long]
[+0x074 (17:17)] TimerSuspended : 0x0 [Type: unsigned long]
[+0x074 (18:18)] SuspendedWaitMode : 0x0 [Type: unsigned long]
[+0x074 (19:19)] SuspendSchedulerApcWait : 0x0 [Type: unsigned long]
[+0x074 (20:20)] CetUserShadowStack : 0x0 [Type: unsigned long]
[+0x074 (21:21)] BypassProcessFreeze : 0x0 [Type: unsigned long]
[+0x074 (31:22)] Reserved : 0x0 [Type: unsigned long]
...
Từ user mode (không cần kernel debugger)
Trên x64, register segment GS luôn trỏ tới TEB của thread hiện tại, nên có thể đọc nó từ bất kỳ phiên WinDbg user-mode nào:
// In WinDbg user-mode session: read the TEB of the current thread
0:000> !teb
TEB at 00000025fac41000
ExceptionList: 0000000000000000
StackBase: 00000025faba0000
StackLimit: 00000025fab8f000
SubSystemTib: 0000000000000000
FiberData: 0000000000001e00
ArbitraryUserPointer: 0000000000000000
Self: 00000025fac41000
EnvironmentPointer: 0000000000000000
ClientId: 00000000000020a4 . 0000000000004c9c
RpcHandle: 0000000000000000
Tls Storage: 000001d3e8e49b00
PEB Address: 00000025fac40000
LastErrorValue: 2
LastStatusValue: c0000034
Count Owned Locks: 0
HardErrorMode: 0
Process Explorer
- Properties → Threads - liệt kê mọi thread cùng TID, địa chỉ bắt đầu (đã phân giải symbol nếu PDB được nạp), thời gian CPU, và priority hiện tại.

Dùng Thread
Tạo một thread thì đơn giản: gọi CreateThread với địa chỉ một hàm và thread mới sẽ bắt đầu thực thi ở đó. Hàm đó là entry point của thread - nó chạy ngay khi thread bắt đầu.
1HANDLE CreateThread(
2 [in, optional] LPSECURITY_ATTRIBUTES lpThreadAttributes,
3 [in] SIZE_T dwStackSize,
4 [in] LPTHREAD_START_ROUTINE lpStartAddress,
5 [in, optional] __drv_aliasesMem LPVOID lpParameter,
6 [in] DWORD dwCreationFlags,
7 [out, optional] LPDWORD lpThreadId
8);
lpThreadAttributes- security attributes cho thread mới. TruyềnNULLđể lấy mặc định.dwStackSize- kích thước stack ban đầu. Truyền0để lấy mặc định.lpStartAddress- hàm entry point.lpParameter- một giá trị tuỳ ý được truyền thẳng vào entry point.dwCreationFlags- truyền0để chạy ngay, hoặcCREATE_SUSPENDEDđể bắt đầu ở trạng thái tạm dừng.lpThreadId- nhận TID của thread mới. TruyềnNULLnếu bạn không cần.
Một lưu ý về C runtime.
CreateThreadlà lời gọi Win32 thô và hoạt động tốt nếu thread không bao giờ chạm tới C runtime (CRT). Nếu nó có dùng hàm CRT, hãy chọn_beginthreadex- hàm này khởi tạo và dọn dẹp phần trạng thái CRT riêng của mỗi thread mà những hàm nhưstrtokdựa vào. Bỏ qua nó có thể làm rò rỉ trạng thái đó một cách âm thầm._beginthreadexgọiCreateThreadở bên trong, nên bạn không mất gì cả.Chữ ký của entry point. Hàm bạn truyền vào phải khớp
LPTHREAD_START_ROUTINE:DWORD WINAPI ThreadProc(LPVOID lpParameter). Viết đúng chữ ký giúp tránh phải cast và tránh undefined behavior do sai calling convention.
Fiber
Fiber là đơn vị thực thi mà bạn tự xếp lịch bằng tay, thay vì để kernel scheduler làm. Một thread có thể giữ nhiều fiber nhưng chỉ chạy một cái tại một thời điểm - và bạn chọn cái nào, một cách tường minh, bằng cách gọi SwitchToFiber. Vì chúng sống hoàn toàn trong user mode bên trong Kernel32.dll, kernel không thấy chúng chút nào.
Fiber còn đáng dùng không? Phần lớn là không. Fiber có trước các primitive async hiện đại và được coi là gần như legacy - chính Microsoft cũng ghi rằng chúng hiếm khi có lợi thế so với code multithread được thiết kế tốt. Chúng cũng tương tác tệ với C runtime và thread-local storage, vì các fiber dùng chung một thread cũng dùng chung slot TLS của thread đó (fiber có fiber-local storage riêng, FLS, qua
FlsAlloc/FlsGetValue, chính là để lách chuyện này). Trừ khi bạn đang port code xây quanh một cooperative scheduler, hãy chọn thread, thread pool, hoặc async I/O. Chúng vẫn là một phương tiện rất tốt để hiểu cooperative scheduling, và đó là lý do ta nói về chúng ở đây.
Điểm phân biệt then chốt là ai làm việc xếp lịch. Kernel xếp lịch thread một cách preemptive; fiber được chuyển đổi một cách cooperative bởi chính code của bạn, và kernel còn không thấy được chúng:
Dùng Fiber
Bắt đầu bằng cách chuyển thread hiện có của bạn thành một fiber với ConvertThreadToFiber:
1LPVOID ConvertThreadToFiber(
2 [in, optional] LPVOID lpParameter
3);
Hàm này trả về địa chỉ context của fiber mới - bạn sẽ cần nó cho SwitchToFiber về sau. Context của fiber tương tự context của thread nhưng còn giữ lpParameter, đỉnh và đáy stack của fiber, cùng vài thứ khác. Từ đây trở đi, thread của bạn chính là một fiber; nó kết thúc khi công việc của fiber xong hoặc khi nó gọi ExitThread (kết thúc cả fiber và thread).
Để tạo thêm fiber, gọi CreateFiber:
1LPVOID CreateFiber(
2 [in] SIZE_T dwStackSize,
3 [in] LPFIBER_START_ROUTINE lpStartAddress,
4 [in, optional] LPVOID lpParameter
5);
dwStackSize- kích thước stack của fiber;0dùng mặc định (nở tới 1 MB).lpStartAddress- hàm chạy khi fiber được xếp lịch.lpParameter- truyền thẳng vào hàm đó.
CreateFiber chỉ tạo fiber - nó không chạy fiber. Để thực sự chuyển sang nó, gọi:
1void SwitchToFiber(
2 [in] LPVOID lpFiber
3);
Truyền địa chỉ context của fiber mà CreateFiber trả về. Việc thực thi chuyển sang ngay lập tức. Khi xong, dọn dẹp bằng:
1void DeleteFiber(
2 [in] LPVOID lpFiber
3);
Process, thread và fiber trong một cái nhìn
| Process | Thread | Fiber | |
|---|---|---|---|
| Là gì | Hộp chứa tài nguyên | Đơn vị thực thi trên CPU | Đơn vị thực thi cooperative |
| Ai xếp lịch | - (chứa thread) | Kernel, kiểu preemptive | Bạn, bằng tay (SwitchToFiber) |
| Sở hữu | Không gian địa chỉ, handle, token | Stack, context, TLS | Một stack + context đã lưu |
| Kernel thấy được? | Có (EPROCESS) | Có (ETHREAD) | Không - thuần user-mode |
| Số lượng | 1+ mỗi hệ thống | 1+ mỗi process | 0+ mỗi thread, một cái chạy tại một thời điểm |
| Tạo bằng | CreateProcess | CreateThread / _beginthreadex | CreateFiber + SwitchToFiber |
Bên trong CreateProcess
Khi một thread muốn dựng lên một process mới, nó gọi CreateProcess và truyền tham số để định hình process mới theo ý mình. Hàm này linh hoạt đủ để phủ gần như mọi tình huống, nhưng đôi khi một mình nó chưa đủ - nên API có thêm một họ nhỏ các hàm bọc ngoài:
CreateProcessAsUsertạo một process thay mặt một user khác bằng cách nhận handle tới primary token của user đó.CreateProcessWithTokenWlàm việc tương tự nhưng đòi một tập privilege khác.CreateProcessWithLogonWnhận thẳng credential (username, domain, password) thay vì handle của token.ShellExecutelà cái khác biệt nhất. Ba hàm trước làm việc với bất kỳ file PE hợp lệ nào bất kể phần mở rộng - bạn có thể đổi tênnotepad.exethànhnotepad.txtvà chúng vẫn chạy được.ShellExecutevàShellExecuteExthì nhận biết loại file: chúng tra chương trình liên kết trongHKLM\SOFTWARE\ClassesvàHKCU\SOFTWARE\Classes, rồi cuối cùng gọiCreateProcessvới đúng file thực thi và đường dẫn file làm tham số. Đưa cho nó một file.txtthì nó chạynotepad.exe filename.txt.
Cả CreateProcess và CreateProcessAsUser đều được export bởi Kernel32.dll. Cả hai cuối cùng đều dồn vào CreateProcessInternal (cũng trong Kernel32.dll), rồi hàm này gọi NtCreateUserProcess trong ntdll.dll. NtCreateUserProcess là chặng cuối trong user mode - khi nó xong, nó thực hiện một syscall và vượt sang kernel mode. Cả CreateProcessInternal và NtCreateUserProcess đều không được tài liệu hoá chính thức.
Còn CreateProcessWithTokenW và CreateProcessWithLogonW thì được export bởi Advapi32.dll. Mỗi hàm thực hiện một lời gọi RPC tới Secondary Logon Service (seclogon.dll, chạy trong svchost.exe), service này lo phần tra credential rồi cuối cùng gọi CreateProcessAsUser thay bạn.
Đặt cả hai đường đi cạnh nhau:
Tham số
CreateProcess nhận mười tham số, nhưng phần lớn là những cái núm kiểu “truyền NULL để lấy mặc định” mà MSDN đã tài liệu hoá cặn kẽ. Vài cái thực sự đáng hiểu:
lpApplicationNameso vớilpCommandLine- nguồn nhầm lẫn muôn thuở. Bạn có thể ghi tên file thực thi ở một trong hai. Điểm gài: nếu bạn đểlpApplicationNamelàNULLvà truyền một tên không đủ đường dẫn tronglpCommandLine, Windows sẽ tìm kiếm qua thư mục của app, các thư mục hệ thống, thư mục hiện tại, vàPATH. Dấu cách trong một đường dẫn không đặt trong ngoặc kép là nguồn kinh điển của những bug kiểu “nó chạy sai chương trình”.bInheritHandles- con có kế thừa những handle có thể kế thừa của cha hay không. Kết hợp với cờbInheritHandletrong từngSECURITY_ATTRIBUTES, đây là cách một process cha chủ động chuyển một handle cụ thể (chẳng hạn một pipe) xuống cho process con.dwCreationFlags- hai cái bạn sẽ dùng nhiều nhất:CREATE_SUSPENDED(primary thread bắt đầu ở trạng thái dừng; resume sau bằngResumeThread- dùng ở khắp nơi từ debugger tới việc set up job) vàDEBUG_PROCESS(bên gọi trở thành debugger của process mới).lpStartupInfo- mộtSTARTUPINFO/STARTUPINFOEXmô tả cửa sổ ban đầu của con, các std handle, và (ở dạng EX) một danh sách attribute cho các tuỳ chọn nâng cao như chọn process cha.lpProcessInformation- phần output: PID mới, TID của primary thread, và handle tới cả hai. Bạn sở hữu hai handle đó và phảiCloseHandlechúng, ngay cả khi bạn không dùng tới - một chỗ rò rỉ rất dễ bỏ sót vì chương trình vẫn chạy bình thường.
Các tham số về token/credential là khác biệt có ý nghĩa duy nhất giữa các thành viên trong họ hàm này. Còn lại thì giống nhau.
Chuyện gì xảy ra trước
main()?CreateProcesstrả về không có nghĩa là code của bạn đang chạy. Kernel map image, rồi loader (ntdll) trong process mới map mọi DLL phụ thuộc, chạyDllMaincủa chúng và các TLS callback, rồi CRT startup khởi tạo runtime - sau đó mới gọi entry point của bạn. Một lượng đáng ngạc nhiên các mánh của kẻ tấn công và của anti-cheat sống đúng trong khoảng thời gian giữa “process đã được tạo” và “câu lệnh đầu tiên của bạn”.
Phân loại Process
Windows định nghĩa vài loại process (phần lớn là) riêng biệt cho những trường hợp cần thêm bảo mật hoặc phục vụ mục đích đặc biệt. Chúng không được khởi chạy như process thường, và mang những thuộc tính khác.
Protected Process
Protected process ban đầu được đưa ra để thoả các yêu cầu DRM mà ngành công nghiệp nội dung đặt ra cho những thứ như HD-DVD.
Bình thường, bất kỳ process nào giữ debug privilege - thường là process được administrator khởi chạy - đều đọc và ghi được bộ nhớ của mọi process khác trên hệ thống. Điều đó hữu dụng trong nhiều tình huống, nhưng vi phạm trực tiếp yêu cầu DRM. Protected process giải quyết chuyện này: chúng cùng tồn tại với process thường nhưng cho bên ngoài rất ít hoặc không cho chút quyền truy cập nào, kể cả bên đang chạy dưới quyền administrator. Để thực sự chạy như một protected process, file thực thi phải được ký bằng Windows Media Certificate đặc biệt và chỉ nạp những DLL được ký tương tự; dữ liệu của nó chỉ kernel hoặc protected process khác chạm tới được. Trong thực tế bạn sẽ thấy điều này ở các process giải mã DRM audio/video (Audiodg.exe, Mfpmp.exe), ở protected crash-reporter (Werfaultsecure.exe - WER thường không đọc được bộ nhớ của một protected process đã crash), và ở chính process System.
Protected Processes Light (PPL)
PPL là dạng mở rộng của protected process, được tạo ra để cho phần mềm của bên thứ ba - ví dụ chương trình antivirus - có được sự bảo vệ tương tự. Điểm khác: mức độ bảo vệ của một PPL phụ thuộc vào signature level của nó, nên có PPL được bảo vệ nhiều hơn PPL khác. Nhiều process hệ thống cốt lõi của Windows được bảo vệ dạng PPL, gồm smss.exe, csrss.exe, và services.exe.
Minimal Process
Về bản chất đây là những process rỗng. Không gian địa chỉ user-mode của chúng trống trơn: không có ntdll.dll hay các DLL subsystem, không có PEB hay TEB, không có initial thread, không có image nào được map. Kernel tạo và quản lý chúng, và không có cách nào từ user mode để sinh ra một cái - chúng tồn tại cho những nhiệm vụ đặc biệt của chính hệ thống.
Minimal process có thể chứa minimal thread, loại thread không có TEB và không có stack.
Một ví dụ thực tế là process memory compression, nó giữ bộ nhớ đã nén của các process đang hoạt động để hệ thống có thể giữ được nhiều hơn trong RAM thay vì phải paging ra đĩa. Nó bị ẩn khỏi Task Manager vì working set của nó trông lớn đến mức đáng báo động (nó lưu bộ nhớ đã nén thuộc về các process khác), và người dùng từng thấy nó đáng nghi. Bạn có thể phát hiện nó trong Process Explorer bằng cách sắp xếp theo working set - nó sẽ nổi lên gần đầu. Nó không có thread nào và không có code riêng.
Pico Process
Pico process ra đời từ một nỗ lực nghiên cứu của Microsoft có tên Project Drawbridge. Một pico process là một minimal process ghép với một driver hỗ trợ gọi là pico provider. Provider có thể quản lý gần như mọi thứ về việc thực thi của pico process - đến mức nó có thể hành xử như một kernel riêng cho process đó, và process không hề biết hệ thống bên dưới thực sự là gì. Tuy vậy, quản lý bộ nhớ, I/O, và xếp lịch thread vẫn là việc của kernel Windows thật.
Một pico provider chặn mọi thao tác của pico process cần kernel xử lý - system call, exception, v.v. - và phản hồi theo cách nó thấy phù hợp. Pico process có thể chứa cả pico thread (minimal thread dành cho pico process) và thread thường. Pico thread mang một context lưu trong thành viên PicoContext của ETHREAD.
Windows Subsystem for Linux
WSL được xây trên mô hình pico process. Nó có thể chạy một hệ thống Linux gần như đầy đủ trên Windows mà không cần một dòng code nào của kernel Linux - điều này khả thi nhờ mức độ kiểm soát đáng kinh ngạc mà pico provider mang lại.
Pico provider của WSL là lxss.sys và lxcore.sys. Chúng mô phỏng hành vi của kernel Linux bằng cách dịch syscall Linux từ pico process của WSL sang NT API, hoặc bằng cách gọi các thành phần được viết riêng từ đầu.
WSL1 so với WSL2 - một lưu ý quan trọng. Mọi thứ ở trên mô tả WSL1, tầng dịch dựa trên pico process. WSL2, mặc định hiện nay, hoạt động hoàn toàn khác: nó chạy một kernel Linux thật bên trong một máy ảo Hyper-V nhẹ. WSL2 không có pico process - nó đạt được khả năng tương thích syscall đầy đủ và hiệu năng tốt hơn, đổi lại mất đi sự tích hợp liền mạch mà mô hình pico đem lại cho WSL1. Nếu hôm nay bạn đang xem các process Linux, gần như chắc chắn bạn đang nhìn vào kernel Linux thật của WSL2, không phải một pico provider.
Trustlet (Secure Process)
Trustlet chạy trong Isolated User Mode (IUM), được bảo vệ bởi Virtual Trust Level của hypervisor Hyper-V - code chạy trong OS bình thường (kể cả kernel) nằm ở trust level thấp hơn và không đọc được bộ nhớ của một trustlet. Một trustlet chỉ được import một danh sách trắng nhỏ các DLL được hệ thống tin cậy (CRT, KernelBase, Advapi, RPC runtime, CNG crypto, và vài thư viện toán) - những thứ không cần syscall để hoạt động. Người dùng không tạo chúng trực tiếp được; kernel tạo, thay mặt cho các tính năng bảo mật. Ví dụ kinh điển là LSAIso, process cô lập giữ các bí mật credential cho Credential Guard.
Các loại process trong một bảng
| Loại | Ai tạo ra | Ý chính |
|---|---|---|
| Normal | Bất kỳ ai | Process thường ngày. |
| Protected / PPL | Bất kỳ ai (cần ký đặc biệt) | Kể cả admin cũng không đọc được bộ nhớ của nó; mức bảo vệ do signature level quyết định. |
| Minimal | Chỉ kernel | Cái vỏ rỗng - không PEB/TEB, không image, không ntdll. Ví dụ: memory compression. |
| Pico | Kernel + một driver pico provider | Minimal process mà syscall/exception được một driver provider xử lý. Nền tảng của WSL1. |
| Trustlet (IUM) | Kernel, cho các tính năng bảo mật | Cô lập bằng hypervisor; OS bình thường không đọc được. Ví dụ: LSAIso / Credential Guard. |
Job
Job là cơ chế của Windows để nhóm nhiều process lại dưới một đơn vị quản lý duy nhất. Mọi giới hạn hay thay đổi áp lên một job đều ảnh hưởng tới mọi process trong đó - khiến chúng hữu dụng cho việc giới hạn tài nguyên, khoanh vùng, và theo dõi một nhóm process liên quan như một khối.
Job object có thể chia sẻ, đặt bảo mật, và đặt tên. Một khi process đã vào job, nó không ra được. Process con do các thành viên sinh ra sẽ tự động vào cùng job, trừ khi CREATE_BREAKAWAY_FROM_JOB được truyền cho CreateProcess và job cho phép breakaway.
Hai chi tiết hiện đại mà bức tranh kinh điển bỏ qua. Thứ nhất, từ Windows 8 job có thể lồng nhau - một process có thể thuộc một hệ thống phân cấp các job, với giới hạn có hiệu lực là cái nghiêm ngặt nhất trong chuỗi. Thứ hai, bạn có thể gắn một I/O completion port vào một job (JOBOBJECT_ASSOCIATE_COMPLETION_PORT) để nhận thông báo khi process vào, ra, hoặc chạm giới hạn - tốt hơn nhiều so với polling.
Job là nền tảng của Windows container. Silo là một job được tăng cường, có thêm khả năng ảo hoá object namespace. Server silo ảo hoá đủ phần hệ thống (registry, networking, object manager) để trông như một OS riêng - và đó chính là thứ đứng sau Windows Server Container và Docker trên Windows. Cái job object bình thường bạn sẽ tạo ở dưới cũng chính là primitive đó, chỉ được mở rộng lên tới tầm container.
Giới hạn của Job
Một số giới hạn bạn có thể đặt cho một job:
- Số process active tối đa - chặn số process có thể tồn tại trong job. Khi đã đạt mức đó, không process mới nào được gán vào và việc tạo process con sẽ thất bại.
- Processor affinity - giữ mọi thành viên trong một tập CPU cụ thể.
- Priority class - đặt priority class cho mọi thành viên. Nếu một thread cố nâng priority class của nó lên trên mức này, yêu cầu sẽ bị bỏ qua âm thầm.
- Giới hạn bộ nhớ ảo - chặn lượng bộ nhớ ảo mà một process, hoặc cả job, có thể commit.
- Đọc/ghi clipboard - cấm mọi thành viên đọc hay ghi clipboard.
Các hàm API để làm việc với Job
Toàn bộ API của job:
| Hàm | Làm gì |
|---|---|
CreateJobObject | Tạo (hoặc mở) một job object có tên |
OpenJobObject | Mở một job đã tồn tại theo tên |
AssignProcessToJobObject | Gán một process vào một job |
SetInformationJobObject | Áp giới hạn lên job |
QueryInformationJobObject | Đọc thông tin và thống kê hiện tại của job |
TerminateJobObject | Kết thúc mọi process trong job |
IsProcessInJob | Kiểm tra một process có thuộc một job cụ thể hay không |
Dùng Job
Quy trình là: tạo job, gán process, đặt giới hạn.
Tạo job bằng CreateJobObject:
1HANDLE CreateJobObjectA(
2 [in, optional] LPSECURITY_ATTRIBUTES lpJobAttributes,
3 [in, optional] LPCSTR lpName
4);
Truyền NULL cho cả hai tham số để lấy một job không tên với security mặc định. lpName đặt tên cho job để chia sẻ giữa các process; nếu tên đó trùng với một mutex, file-mapping object, hoặc waitable timer đã có, lời gọi sẽ thất bại.
Gán process bằng AssignProcessToJobObject:
1BOOL AssignProcessToJobObject(
2 [in] HANDLE hJob,
3 [in] HANDLE hProcess
4);
hJob là handle của job; hProcess là handle tới process bạn đang thêm vào. Dùng GetCurrentProcess() để lấy handle tới chính process đang gọi.
Áp giới hạn bằng SetInformationJobObject:
1BOOL SetInformationJobObject(
2 [in] HANDLE hJob,
3 [in] JOBOBJECTINFOCLASS JobObjectInformationClass,
4 [in] LPVOID lpJobObjectInformation,
5 [in] DWORD cbJobObjectInformationLength
6);
JobObjectInformationClass chọn nhóm giới hạn bạn đang đặt (basic, extended, UI, v.v.); lpJobObjectInformation trỏ tới cấu trúc tương ứng.
Ví dụ thực tế: Cargo hạ cả cây process của nó thế nào

Khi bạn nhấn Ctrl-C trong lúc cargo build, bạn mong cả cây process chết theo - cargo, rustc, mọi build script. Trên Unix chuyện này có sẵn: Ctrl-C gửi signal cho cả process group. Trên Windows thì không - chỉ cargo chết, để lại các process con thành mồ côi.
Cách Cargo xử lý nằm ở src/cargo/util/job.rs: tạo một Job Object, set KILL_ON_JOB_CLOSE, và gán chính cargo vào đó. Process con tự động kế thừa job, nên cả cây được nhóm lại mà không cần theo dõi PID nào.

Ví dụ code
Giờ khi phần nền tảng đã xong, hãy xem từng object trong số này trông thế nào trong thực tế.
Tạo một Process
CreateProcess đơn giản hơn danh sách tham số của nó gợi ra - phần lớn tham số mặc định là NULL.
1#include <stdio.h>
2#include <windows.h>
3
4int main(){
5 STARTUPINFOA si;
6 PROCESS_INFORMATION pi;
7
8 ZeroMemory( &si, sizeof(si) );
9 si.cb = sizeof(si);
10 ZeroMemory( &pi, sizeof(pi) );
11 LPSTR lpCommandLine = "notepad.exe";
12
13 if( !CreateProcessA( NULL, // No module name (use command line)
14 lpCommandLine,
15 0, // Process handle not inheritable
16 0, // Thread handle not inheritable
17 0, // No handle inheritance
18 0, // No creation flags
19 0, // Use parent's environment block
20 0, // Use parent's starting directory
21 &si,
22 &pi )
23 ){
24 printf( "CreateProcess failed (%d).\n", GetLastError() );
25 return -1;
26 }
27 printf("Process Created!\n");
28
29 Sleep(5000);
30
31 // Always close both handles - even if you never use them.
32 CloseHandle( pi.hProcess );
33 CloseHandle( pi.hThread );
34
35 return 0;
36}
Đoạn này chạy notepad.exe rồi thoát sau năm giây. Hai lời gọi CloseHandle ở cuối là bắt buộc - CreateProcess luôn trả cho bạn một process handle và một thread handle, và bạn sở hữu chúng bất kể bạn có làm gì với chúng hay không.
Tạo một Thread
1#include <stdio.h>
2#include <windows.h>
3
4DWORD WINAPI EthicalFunction(LPVOID lpParam)
5{
6 printf("Thread created\n");
7 printf("For educational purposes only*\n");
8 return 0;
9}
10
11int main()
12{
13 HANDLE hThread = CreateThread(NULL, 0, EthicalFunction, NULL, 0, NULL);
14 WaitForSingleObject(hThread, INFINITE);
15 printf("Thread returned\n");
16 printf("Exiting...\n");
17 CloseHandle(hThread);
18 return 0;
19}
EthicalFunction chạy trên thread mới trong khi main bị chặn ở WaitForSingleObject. Khi thread trả về, main chạy tiếp, in thông báo của nó, và đóng handle. Cặp WaitForSingleObject + CloseHandle đó là mức cơ bản cho bất kỳ thread nào bạn tạo.
Tạo một Fiber
Fiber cần thêm một bước: bạn phải chuyển thread hiện tại thành fiber trước khi tạo thêm được.
1#include <stdio.h>
2#include <windows.h>
3
4VOID CALLBACK fiber_function(LPVOID lpParam)
5{
6 printf("Fiber created\n");
7 printf("For educational purposes only*\n");
8 // Switch back to the main fiber - fibers don't return normally.
9 SwitchToFiber(lpParam);
10}
11
12int main()
13{
14 // Step 1: convert this thread into a fiber.
15 LPVOID Context = ConvertThreadToFiber(NULL);
16 // Step 2: create another fiber, passing our context so it can switch back.
17 LPVOID lpFiber = CreateFiber(0, fiber_function, Context);
18 // Step 3: switch to it. Execution transfers immediately.
19 SwitchToFiber(lpFiber);
20 // We're back. Clean up.
21 printf("Fiber returned\n");
22 DeleteFiber(lpFiber);
23 printf("Exiting...\n");
24 return 0;
25}
Để ý rằng fiber_function không trả về - nó chuyển ngược về fiber chính một cách tường minh. Đó chính là mô hình cooperative scheduling ở dạng thu nhỏ: bạn luôn là người kiểm soát thời điểm chuyển đổi xảy ra.
Tạo một Job Object
Ví dụ này tạo một job, đưa hai process vào đó ở trạng thái suspended, rồi resume chúng và kết thúc cả nhóm sau một phút.
1#include <stdio.h>
2#include <windows.h>
3
4int main()
5{
6 HANDLE hJob = CreateJobObject(NULL, "Unemployed");
7
8 // Kill all processes in the job when the job handle closes.
9 JOBOBJECT_EXTENDED_LIMIT_INFORMATION jeli = {0};
10 jeli.BasicLimitInformation.LimitFlags = JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE;
11 SetInformationJobObject(hJob, JobObjectExtendedLimitInformation, &jeli, sizeof(jeli));
12
13 STARTUPINFOA si = {0}; si.cb = sizeof(si);
14 STARTUPINFOA si1 = {0}; si1.cb = sizeof(si1);
15 PROCESS_INFORMATION pi = {0};
16 PROCESS_INFORMATION pi1 = {0};
17
18 // Create both processes suspended so we can assign them before they run.
19 if (!CreateProcessA(NULL, (LPSTR)"notepad.exe", NULL, NULL, FALSE, CREATE_SUSPENDED, NULL, NULL, &si, &pi) ||
20 !CreateProcessA(NULL, (LPSTR)"dvdplay.exe", NULL, NULL, FALSE, CREATE_SUSPENDED, NULL, NULL, &si1, &pi1))
21 {
22 printf("Error creating processes: %d\n", GetLastError());
23 return 1;
24 }
25
26 AssignProcessToJobObject(hJob, pi.hProcess);
27 AssignProcessToJobObject(hJob, pi1.hProcess);
28
29 ResumeThread(pi.hThread);
30 ResumeThread(pi1.hThread);
31 printf("Job created and processes added!\n");
32
33 Sleep(60000);
34 TerminateJobObject(hJob, 0);
35
36 CloseHandle(pi.hProcess);
37 CloseHandle(pi.hThread);
38 CloseHandle(hJob);
39
40 printf("Exiting...\n");
41 return 0;
42}
Cờ CREATE_SUSPENDED là chi tiết then chốt: nó cho bạn một khoảng để gán process vào job trước khi chúng chạy, tránh được một race condition mà process con có thể sinh ra process cháu trước khi việc gán có hiệu lực. Để xác nhận cả hai process đều nằm trong job Unemployed, mở Process Explorer và xem Properties → Job ở notepad.exe hoặc wmplayer.exe.
Những cái bẫy thường gặp
Vài cái bẫy hay cắn người làm việc với các API này:
- Rò rỉ handle.
CreateProcesstrả cho bạn hai handle trongPROCESS_INFORMATION- process và thread. QuênCloseHandlecả hai là một chỗ rò rỉ chậm rất dễ bỏ sót vì chương trình vẫn chạy tốt. CreateThreadvà C runtime. Nếu một thread gọi hàm CRT, hãy dùng_beginthreadexthay choCreateThread-CreateThreadthô để lại trạng thái CRT riêng của thread (như buffer củastrtok) chưa khởi tạo hoặc bị rò rỉ.- Stack reserve cộng dồn lại. Mỗi thread mặc định dành riêng khoảng ~1 MB không gian địa chỉ. Sinh hàng nghìn thread trong một process 32-bit thì bạn sẽ cạn không gian địa chỉ từ rất lâu trước khi cạn CPU - một trong những lý do thread pool tồn tại.
- Priority không phải là bảo đảm. Priority cao hơn làm một thread được ưu tiên, không phải được chạy trước - boost, affinity, và những core khác đang làm gì đều góp phần. Đừng dùng priority như một cơ chế đồng bộ.
- Với tay tới fiber quá sớm. Chúng trông như thread giá rẻ nhưng mang theo những nguy hiểm về TLS/CRT và không cho bạn chút song song đa core nào. Mặc định hãy chọn thread, thread pool, hoặc async I/O.
Đối chiếu sang Linux
Nếu bạn xuất phát từ nền Unix, mô hình Windows tương ứng đại khái như sau:
| Windows | Linux / POSIX |
|---|---|
Process (CreateProcess) | Process (fork + execve) |
Thread (CreateThread) | Thread / task (pthread_create, clone) |
Fiber (CreateFiber) | ucontext (makecontext/swapcontext), green thread |
| Thread affinity | sched_setaffinity / taskset |
| Job object | cgroups (nhóm và giới hạn tài nguyên) |
| Access token | uid/gid + capabilities |
| Handle / handle table | File descriptor / fd table |
Khác biệt lớn nhất về mặt khái niệm: Linux làm mờ ranh giới process/thread - cả hai chỉ là “task” sinh từ clone với các cờ chia sẻ khác nhau. Windows giữ một sự phân chia cứng và tường minh giữa cái hộp process và các thread bên trong nó.
Tổng kết
Ta đã đi qua bốn khối xây dựng của việc thực thi trên Windows - process (hộp chứa tài nguyên), thread (thứ mà kernel xếp lịch kiểu preemptive), fiber (đơn vị cooperative mà bạn tự xếp lịch), và job (nhóm process với giới hạn chung, và là primitive đứng sau Windows container) - cùng với các cấu trúc kernel (EPROCESS/KPROCESS, ETHREAD/KTHREAD) đứng sau chúng và những loại process đặc biệt mà Windows định nghĩa cho bảo mật và cô lập. Trên đường đi ta cũng xem scheduler thực sự quyết định chạy cái gì thế nào, CreateProcess gọi xuống kernel ra sao, và cách tạo từng object bằng code - cộng với cách quan sát tất cả trực tiếp bằng WinDbg hoặc Process Explorer.
Nếu bạn chỉ mang về ba điều: thread được xếp lịch cho bạn còn fiber được xếp lịch bởi bạn; gần như mọi cấu trúc ở đây đều kiểm tra được trên một máy đang chạy; và cái job object bạn vừa dùng để trông hai cửa sổ Notepad, khi mở rộng lên, chính là thứ chạy Windows container. Cảm ơn bạn đã đọc!
Tài liệu tham khảo
- Processes and Threads - Microsoft Docs
- CreateProcess - Microsoft Docs
- CreateJobObject - Microsoft Docs
- AssignProcessToJobObject - Microsoft Docs
- AssignProcessToJobObject fails with “Access Denied” under the debugger - Stack Overflow
- TerminateJobObject - Microsoft Docs
- CreateThread - Microsoft Docs
- CreateFiber - Microsoft Docs
- SwitchToFiber - Microsoft Docs
- Isolated User Mode (IUM) Processes - Microsoft Docs
