从资源的角度来说, 程序本质可看做对资源的访问和计算, Windows 系统将资源抽象成对象
-
- 负责计算和 IO 资源的访问, 如 Process, Thread, Mutex, FileMapping, File 等
- 一个内核对象可以创建多个句柄, 只要创建的进程具有对象名字和访问权限
- 内核对象在最后一个句柄关闭后才会销毁, 进程终止时会自动关闭进程持有的内核对象句柄
-
- 负责窗口资源的访问, 如 Window, Menu, Hook, Icon, Cursor 等
- 一个用户对象只能创建一个句柄, 但句柄是公开的, 只要进程属于同一 Desktop 且具有句柄值和访问权限就能访问
- 用户对象在调用相应销毁函数后立即被销毁, 进程终止时会自动销毁进程创建的用户对象, 特别的, Window, Hook 等资源在创建线程终止时就被销毁
-
- 负责图形资源的访问, 如 Bitmap, Brush, Font, DC 等
- 一个 GDI 对象只能创建一个句柄, 且仅能在创建进程内部访问
- GDI 对象在调用相应销毁函数后立即被销毁, 进程终止时会自动销毁进程创建的用户对象
其中, 内核对象作为最主要的资源对象, 系统为其提供许多机制来加强管理
每个进程维护一张句柄表用于访问内核对象, 内核对象句柄仅在进程内有效
- 索引:
HANDLE- 0, -1 保留作为无效句柄
- 1, 2, 3 保留用作标准输入输出
- 内核对象指针
- 名字
- 引用计数
- 安全描述符
- ...
- 访问掩码: 该句柄的访问权限掩码
- 属性标志: 如 PROTECT_FROM_CLOSE, INHERIT 等
GetHandleInformationSetHandleInformationCompareObjectHandlesDuplicateHandleCloseHandle
大多数内核对象都可以通过名字访问, 所有对象都实际存在于底层 NT 命名空间内, 但不同类型的对象名字处于不同的子命名空间内. 可以使用 WinObj 查看详细内容
- 文件相关的内核对象
- 文件路径名均位于
\GLOBAL?? - 其内包含
C:链接到\Device\HarddiskVolume3GLOBALROOT链接到\
- 文件路径名均位于
- 其它内核对象
- 不同会话中创建的对象默认位于不同命名空间
- 服务会话 (session 0) 默认位于
\BaseNamedObjects - 终端会话 (如 session 1) 默认位于
\Sessions\1\BaseNamedObjects
- 服务会话 (session 0) 默认位于
- 这些命名空间内都存在两个符号链接
Global链接到\BaseNamedObjects, 用于跨会话共享Local链接到\Session\1\BaseNamedObjects, 用于不跨会话共享
- 不同会话中创建的对象默认位于不同命名空间
-
Session
- 包含若干 Window Station 对象,服务会话为 Session 0,用户会话为 Session 1 等
- 不同会话的部分内核对象的默认命名空间不同
-
Window Station
- 包含一个剪切板、一张原子表和若干 Desktop 对象,系统会为每个用户会话创建 WinSta0
- 只允许激活(终端) session 中的 WinSta0 中的一个 Desktop 与终端设备交互
- 每个进程可以与一个 Window Station 关联
-
Desktop
- 包含若干 windows, menus, hooks 等用户对象,Winlogon 用于用户登录和 UAC 授权,Default 用于用户应用程序
- 用户对象仅能在同一 Desktop 内部访问
- 每个线程可以与一个 Desktop 关联
参考
-
访问令牌 (Access Token)
- user SID
- group SIDs
- logon SID
- owner SID
- primary group SID
- integrity SIDs
- privileges LUIDs
- default DACL
- restricting SIDs
- whether impersonation
- ...
-
安全描述符 (Security Descriptor)
- owner SID
- primary group SID
- DACL
- SACL
- ...
-
访问控制列表 (Access Control Lists)
- 访问控制表项 (Access Control Entries)
- trustee SID
- type flag
- inherit flags
- access mask
- SACL Access Right: 访问对象 SACL 的权限
- Generic Access Rights: 被映射到一组 Standard Access Rights 和 Object-specific Access Right
- Standard Access Rights: 用于控制对对象本身的操作
DELETEREAD_CONTROLSYNCHRONIZEWRITE_DACWRITE_OWNER
- Object-specific Access Right: 针对不同类型对象的特殊操作的权限
- 访问控制表项 (Access Control Entries)
- 安全对象的访问控制流程
- 若没有 DACL, 则允许所有访问
- 若存在 DACL, 则使用第一个匹配的 ACE 访问控制, 若没有匹配的 ACE 则拒绝所有访问
- 根据 SACL 是否记录该访问尝试
参考
-
可信级别 (Integrity Level)
- system: 通常为系统服务
- high: 通常为管理员权限进程
- medium: 通常为标准用户权限进程
- low: 通常显式设置了 exe 文件的 security descriptors 或进程的 access token
- untrust: 同上, 但级别更低
-
access token 中的 integrity SIDs 表示可信级别
-
security descriptors 中的 SACL 中存储可信级别和强制策略
-
ACM: 可信级别校验发生在 DACL 校验之前, 默认拒绝较低可信级别的写入访问
-
Privileges: 某些系统特权仅允许高可信级别进程运行
-
UIPI: 限制低可信级别对高可信级别的访问机制
- 验证窗口句柄
- 发送窗口消息 (API 调用返回成功, 消息被静默丢弃)
- Hooks
- DLL 注入
- 除非应用程序具有 UIAccess 标志且满足以下条件, 则允许对高可信级别进程驱动 UI 自动化
- 程序具有可信的数字签名
- 程序安装在
%ProgramFiles%或%WinDir%(某些标准用户可写的子目录除外) 目录下
用户访问控制 (UAC) 让标准用户不用重新登录就能使用管理员权限
- Administrator Broker Model: 使用
ShellExecute创建管理员权限的新进程 - Operating System Service Model: 使用 IPC 与 Service 通信
- Elevated Task Model: 使用 Task Scheduler 服务运行应用程序
-
操作系统提供了“进程”与“线程”的概念, 简化了程序对系统中“计算资源”的使用, 如 CPU 和内存等。
-
进程是系统进行内存资源分配的基本单位, 一个进程具有唯一的进程标识符、优先级类、虚拟地址空间、异常处理程序、内核对象句柄、安全上下文、环境变量、命令行参数等, 并且至少有一个执行线程。
-
线程是系统进行 CPU 资源分配的基本单位, 同一进程中所有线程共享虚拟地址空间等进程资源, 此外每个线程负责维护自己的唯一的线程标识符、线程优先级、线程本地存储、线程上下文等。
进程创建流程:
-
CreateProcess- 对于 GUI 程序, 可控制子进程第一次调用
CreateWindow和ShowWindow的默认参数, 如位置、大小、nCmdShow 等 - 对于 CUI 程序, 可控制子进程的控制台窗口的句柄、位置、大小等
- 对于 GUI 程序, 可控制子进程第一次调用
-
将静态导入的(依赖)DLL 加载到内存
-
C Run-Time library (CRT) 入口函数, 负责初始化全局变量等, 然后调用用户程序入口函数
-
用户程序入口函数
mainfor SUBSYSTEM:CONSOLE, 默认继承父进程控制台窗口或自动创建新控制台窗口WinMainfor SUBSYSTEM:WINDOWS,默认不会继承或创建控制台窗口
int main(int argc, char* argv[], char* env[]); int APIENTRY WinMain(HINSTANCE hInst, HINSTANCE hPrevInstance, PSTR lpCmdLine, int nShowCmd);
EXE 搜索路径 (仅当指定无路径的文件名时)
- 进程 exe 文件所在目录
- 进程当前目录
C:\Windows\System32C:\Windows\SystemC:\Windows- 环境变量 PATH
CreateProcessOpenProcessGetProcessIdGetCurrentProcess: 伪句柄, 仅进程内有效GetCurrentProcessIdEnumProcessesGetCommandLineCommandLineToArgvGetEnvironmentStrings: 格式为Var0=Value0\0Var1=Value1\0\0FreeEnvironmentStringsExpandEnvironmentStrings: 扩展替换变量如%USERPROFILE%GetEnvironmentVariableSetEnvironmentVariableGetCurrentDirectorySetCurrentDirectory
-
子进程可以继承父进程如下属性
- 环境变量
- 当前目录
- 控制台
- 内核对象句柄 (句柄标志
bInheritHandle且创建进程参数bInheritHandles都设置为true) - 错误模式
- 进程 CPU 亲和性 (
dwCreationFlags设置INHERIT_PARENT_AFFINITY) - 作业
-
子进程不可继承父进程如下属性
- 用户对象和 GDI 对象的句柄
- 伪句柄
- 虚拟地址空间
- 优先级类
SetHandleInformationUpdateProcThreadAttribute
-
进程终止原因
-
ExitProcess- 进程中最后一个线程终止 (默认 CRT 实现主线程退出时终止进程)
- 用户模式中发生的硬件异常或软件异常未被捕获处理时
- 调用
ExitWindowsEx时- 用户注销或关机时
- 内核模式中发生的硬件异常或软件异常未被捕获时 (蓝屏)
-
TerminateProcess
-
-
进程终止流程
- 终止进程内所有非主线程, 并释放线程资源
- 主线程退出前, 卸载所有 DLL 模块
- 释放其他进程资源, 如关闭内核对象句柄
- 设置进程退出码
- 触发进程对象
-
C++ 静态生命周期的变量
- 仅能保证同一编译单元中的构造和析构顺序, 且析构顺序与构造顺序相反
- 构造
static(模块加载,调用 DllMain 前)thread_local(线程加载)- in-block
static/thread_local(首次调用函数)
- 析构
thread_load(线程退出)- in-block
thread_local(线程退出) std::atexit保证在注册时就已初始化的任何static销毁之前调用static(模块卸载,调用 DllMain 后)- in-block
static(模块卸载)
- 注意: 无法保证变量的析构函数一定会被调用
ExitProcessTerminateProcessGetExitCodeProcessSetProcessShutdownParameters: 设置关机时终止优先级
作业用来管理一组进程, 进程关联作业后不可再取消
- 控制子进程的作业继承
JOB_OBJECT_LIMIT_BREAKAWAY_OKJOB_OBJECT_LIMIT_SILENT_BREAKAWAY_OK
- 限制资源访问
JOBOBJECT_BASIC_LIMIT_INFORMATIONJOBOBJECT_BASIC_UI_RESTRICTIONSJOBOBJECT_CPU_RATE_CONTROL_INFORMATIONJOBOBJECT_EXTENDED_LIMIT_INFORMATIONJOBOBJECT_NOTIFICATION_LIMIT_INFORMATION
- 监视进程状态
JOBOBJECT_ASSOCIATE_COMPLETION_PORT
- 资源使用情况
JOBOBJECT_BASIC_ACCOUNTING_INFORMATIONJOBOBJECT_BASIC_AND_IO_ACCOUNTING_INFORMATION
- 作业对象销毁时终止作业内所有进程
JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE
CreateJobObjectOpenJobObjectSetInfomationJobObjectQueryInfomationJobObjectAssignProcessToJobObjectTerminateJobOnjectIsProcessInJob
- 若线程函数需要访问 CRT, 则应该使用
_beginthreadex而非CreateThread来保证线程安全 - 线程栈最大默认 1M, 可通过控制编译时链接器参数或运行时
CreateThread参数来改变, 大小向上取整 1M
CreateThread: 安全属性、栈大小、暂停状态CreateRemoteThread: 在其他进程中创建线程OpenThreadGetThreadIdGetCurrentThread: 伪句柄, 仅进程内有效, 使用DuplicateHandle转换为真句柄GetCurrentThreadId
-
线程终止原因
ExitThread- 线程函数返回
ExitProcessTerminateThreadTerminateProcess
-
线程终止结果
- 释放线程拥有的资源, 如 TLS (Thread Local Storage), windows, hooks 等
- 设置线程退出码
- 触发线程对象
- 如果线程是进程里唯一的线程, 则终止进程
ExitThreadTerminateThreadGetExitCodeThread
- 上下文切换原因
- 时间片到期, 大概 20 ms
- 主动放弃剩余时间片
- 被高优先级线程抢占
- 进入同步等待状态或暂停状态
SuspendThreadResumeThreadSleepSwitchToThread: 相对Sleep(0), 允许切换低优先级线程GetThreadTimesGetProcessTimes
| IDLE_PRIORITY_CLASS | BELOW_NORMAL_PRIORITY_CLASS | NORMAL_PRIORITY_CLASS | ABOVE_NORMAL_PRIORITY_CLASS | HIGH_PRIORITY_CLASS | REALTIME_PRIORITY_CLASS | |
|---|---|---|---|---|---|---|
| THREAD_PRIORITY_IDLE | 1 | 1 | 1 | 1 | 1 | 16 |
| THREAD_PRIORITY_LOWEST | 2 | 4 | 6 | 8 | 11 | 22 |
| THREAD_PRIORITY_BELOW_NORMAL | 3 | 5 | 7 | 9 | 12 | 23 |
| THREAD_PRIORITY_NORMAL | 4 | 6 | 8 | 10 | 13 | 24 |
| THREAD_PRIORITY_ABOVE_NORMAL | 5 | 7 | 9 | 11 | 14 | 25 |
| THREAD_PRIORITY_HIGHEST | 6 | 8 | 10 | 12 | 15 | 26 |
| THREAD_PRIORITY_TIME_CRITICAL | 15 | 15 | 15 | 15 | 15 | 31 |
- 通过设置进程优先级和线程优先级, 系统确认线程使用的基本优先级
- 基本优先级 0 保留给 zero-page thread, 它负责将 free page 置零
- 注意
REALTIME_PRIORITY_CLASS优先级会中断系统线程, 比如键鼠输入、磁盘冲刷等
GetPriorityClassSetPriorityClassGetThreadPrioritySetThreadPriority
- 动态优先级用来最终确认线程的执行优先级
- 系统在以下情况会动态提升基本优先级 0 - 15 的线程
NORMAL_PRIORITY_CLASS优先级的进程成为前台进程时, 提升其优先级大于或等于任何后台进程- 当窗口接受到用户输入时, 提升窗口所属的线程的优先级
- 当线程同步等待的条件被满足时, 提升该线程的优先级
- 优先级反转: 高优先级线程等待低优先级线程的资源, 而中优先级线程一直抢占低优先级线程, 导致高优先级线程被中优先级线程阻塞。系统会自动检测这种情况, 并动态提升低优先级线程至所有等待它的线程中的最大优先级
- 当低优先级线程长时间处于饥饿状态, 提升该线程的优先级
- 动态提升之后, 动态优先级每个时间片降低 1 级, 且绝不低于基本优先级
GetProcessPriorityBoostSetProcessPriorityBoostGetThreadPriorityBoostSetThreadPriorityBoost
服务质量 (Quality of Service) 会影响线程运行的处理器核心和功率, 具体见 QoS
参考
利用 CPU 亲和性可以限制进程或线程运行在制定的 CPU 上
- 操作系统可包含多个物理处理器 (physical processor)
- 物理处理器可包含多个核心 (core)
- 核心可包含多个逻辑处理器 (logical processor)
- 每 64 个逻辑处理器为一个处理器组 (processor group)
- Windows 11 之后线程可以跨多个处理器组, 默认优先在主组中运行
- 为什么需要同步?
- 保护共享数据被同时读写
- 等待某个事件发生
- 用户模式
- 内核模式
WaitForSingleObjectExWaitForMultipleObjectsExMsgWaitForMultipleObjectsExSignalObjectAndWait
// mydll.h
#pragma once
#ifdef MYDLL_EXPORT
#define MYDLL_API __declspec(dllexport) // dll 实现文件中使用
#else
#define MYDLL_API __declspec(dllimport) // 用户文件中使用
#endif
#ifdef __cplusplus
extern "C" { // 使用 __cdecl 调用约定, 且禁用名称篡改
#endif
MYDLL_API int my_func1(LPCWSTR lpszMsg);
MYDLL_API int __stdcall my_func2(LPCWSTR lpszMsg); // 使用 __stdcall 调用约定和名称篡改
#ifdef __cplusplus
} // extern "C"
#endif-
动态链接:
- 加载时, 即创建进程时, 此时仅保证 Kernel32.dll 已被加载
- 运行时, 即调用
LoadLibrary时, 注意提前卸载模块可能导致某些地方仍在使用模块内的函数
-
内存映射
-
符号解析
-
调用 CRT 动态库入口, 负责构造和析构全局变量等
-
调用 DllMain
/* * 进程内所有 DllMain 的执行都需要获取同一个互斥锁 (进程唯一) * 所以谨慎在 DllMain 中调用 LoadLibrary, FreeLibrary, CreateThread, ExitThread 等函数 */ BOOL WINAPI DllMain(HINSTANCE hinstDLL, DWORD fdwReason, LPVOID lpvReserved) { switch (fdwReason) { case DLL_PROCESS_ATTACH: // 首次加载时, 根据加载方式的不同: // - 加载时动态链接: 在主线程执行, lpvReserved 为 non-NULL, 返回 FALSE 表示发生错误而终止进程 // - 运行时动态链接: 在调用 LoadLibrary 的线程执行, lpvReserved 为 NULL, 返回 FALSE 表示 LoadLibrary 返回 FALSE break; case DLL_PROCESS_DETACH: // 模块卸载时, 根据卸载方式的不同: // - 进程退出: 在调用了 ExitProcess 的线程中执行, lpvReserved 为 non-NULL // - 手动卸载: 在调用了 FreeLibrary 的线程中执行, lpvReserved 为 NULL // - 注意, 调用了 TerminateThread 或 TerminateProcess 不会调用 DllMain break; case DLL_THREAD_ATTACH: // - 创建新线程时, 在运行其线程函数前执行 // - 不会在 DLL_PROCESS_ATTACH 的线程执行 // - 不会在加载时已经存在的线程中执行 // - 若线程调用了 DisableThreadLibraryCalls 则不会执行 break; case DLL_THREAD_DETACH: // - 线程终止时, 在运行其线程函数返回后执行 // - 可能没有对应的 DLL_THREAD_ATTACH, 因为加载时线程已存在 // - 若线程调用了 DisableThreadLibraryCalls 则不会执行 // - 注意, 调用了 TerminateThread 或 TerminateProcess 不会调用 DllMain break; } return TRUE; }
常见问题:
LoadLibrary/GetModuleHandleEx会增加模块引用计数,FreeLibrary会减少模块引用计数_beginthreadex会增加启动函数所在的模块的引用计数, 同时在其线程终止时减少对应的引用计数- 引用计数为 0 时卸载模块, 此时会调用 DllMain
- 同一进程中的所有 DllMain 执行前需要获取同一把互斥锁, 为避免死锁
- 谨慎在 DllMain 中调用
LoadLibrary/GetModuleHandleEx/FreeLibrary - 禁止在 DllMain 中等待线程启动或线程退出
- 谨慎在 DllMain 中调用
- 如何优雅地卸载模块
- 使用
_beginthreadex启动线程 - 提供 Deinitialize 方法发送请求给 Controller 线程
- Controller 线程负责终止其他所有模块相关的线程
- 然后 Controller 作为最后一个模块相关线程退出, 不要调用
ExitThread或FreeLibraryAndExitThread之类的 Win32 API 退出线程, 否则清理代码不会执行, 比如引用计数不会减少 - 最后在
DllMain DLL_PROCESS_DETACH中销毁静态变量 (如果有的话)
- 使用
LoadLibraryLoadLibraryEx: 可设置修改标准搜索路径、仅加载资源数据, 通常对同一 dll 不能与LoadLibrary混用GetProcAddressFreeLibraryFreeLibraryAndExitThreadEXTERN_C IMAGE_DOS_HEADER __ImageBase;: 由链接器创建的变量, 位于该模块的基地址DisableThreadLibraryCalls: 线程创建和终止不在调用指定模块的 DllMain, 通常不应该调用此函数因为 CRT 和 TLS 依赖模块的线程通知EnumProcessModulesGetModuleHandle: 不递增引用计数GetModuleHandleEx: 默认递增引用计数GetModuleFileNameGetModuleBaseNameGetWindowModuleFileNameQueryFullProcessImageName: 用来获取其它进程的 exe 文件路径更加高效且准确
更详细 DLL 搜索顺序见 Dynamic-link library search order
桌面应用(非 UWP 应用)标准搜索顺序,
当使用 LoadLibrary 或 LoadLibraryEx 且文件名未指定路径或指定相对路径时,使用标准搜索顺序,
如果 DLL 有依赖(静态导入)则默认其依赖加载方式就如同使用不带路径的文件名调用 LoadLibrary
- DLL Redirection (
app.exe.local) - API sets (
api-*/ext-*) - SxS manifest redirection
- Loaded-module list
- Known DLLs (
HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\Session Manager\KnownDLLs) - The package dependency graph of the process (
<PackageDependency>in<Dependencies>) - The executable's folder
- The system folder (
C:\Windows\System32) - The 16-bit system folder (
C:\Windows\System) - The Windows folder (
C:\Windows) - The current folder
PATH
以下方法可以更改搜索路径或顺序
- 关闭安全 DLL 搜索模式(
HKEY_LOCAL_MACHINE\System\CurrentControlSet\Control\Session Manager\SafeDllSearchMode设置为 0),关闭后11. The current folder前移至8. The system folder之前 SetDllDirectory,删除11. The current folder并在7.后添加The folder specified by the lpPathName parameter of SetDllDirectoryLoadLibraryEx(absolutePath, NULL, LOAD_WITH_ALTERED_SEARCH_PATH),当加载其依赖时7. The executable's folder被替换为7. The loaded Dll's folderLoadLibraryEx(dllName, NULL, LOAD_LIBRARY_SEARCH*),仅搜索指定目录
关于 Windows 运行时环境和打包部署建议见 Deployment in Visual C++ 和 Universal CRT deployment
-
部署方案
- 集中部署: 利用微软提供的部署工具将依赖的 dll 下载并安装到系统目录且随系统升级, 详细教程见 Redistributing Visual C++ Files
- 本地部署: 将依赖的 dll 放到应用程序目录, 在搜索系统目录之前会先搜索应用程序目录
- 静态链接: 将依赖用
/MT链接参数静态链接到 exe 或 dll, 可能导致同时使用多个不同版本的库
-
msvcrt.dllVS 2015之前的旧版 C 运行时
ucrtbase.dll- C 标准库, Windows 10 之后作为系统基本组件
- 其中还包括一些转发 API Sets
vcruntime<version>.dll- C++ 运行时库, 包含异常处理、调试支持、运行检测等功能
vcruntime140.dll是基础库,vcruntime140_1.dll是扩展库, 后者保证与前者的 ABI 兼容- 位于 64 和 32 位系统目录
msvcp<version>.dll- C++ 标准库, 包含 STL 等
<version>同上- 位于 64 和 32 位系统目录
-
本地部署时
- CRT 库安装位置在
C:\Program Files (x86)\Windows Kits\10\Redist\10.0.22000.0\ucrt\DLLs\ - VC 库安装位置在
C:\Program Files\Microsoft Visual Studio\2022\Community\VC\Redist\MSVC\14.38.33130\x64\Microsoft.VC143.CRT
- CRT 库安装位置在
- 虚拟地址空间
| 分区 | x86 | x64 |
|---|---|---|
| 空指针赋值区 | 0x00000000 - 0x0000FFFF | 0x00000000'00000000 - 0x00000000'0000FFFF |
| 用户模式分区 | 0x00010000-0x7FFEFFFF | 0x00000000'00010000 - 0x000007FF'FFFEFFFF |
| 64KB 禁入分区 | 0x7FFF0000 - 0x7FFFFFFF | 0x000007FF'FFFF0000 - 0x000007FF'FFFFFFFF |
| 内核模式分区 | 0x80000000 - 0xFFFFFFFF | 0x00000800'00000000 - 0xFFFFFFFF'FFFFFFFF |
-
页面: CPU 进行虚拟地址翻译的粒度, 故系统进行内存管理和分配的粒度必须是其整数倍, x86/x64 页面大小为 4K, Windows 用户内存分配粒度为 64K
-
页面状态
MEM_FREE: 该地址尚未被分配, 无法访问MEM_RESERVE: 该地址已被分配, 但还未调拨存储器, 无法访问MEM_COMMIT: 该地址已被分配, 且已调拨存储器 (storage), 第一次访问时系统会为其准备物理内存页面和对应数据
-
页面类型
MEM_PRIVATE:- 后备存储器为页交换文件 (paging file)
- 第一次访问时无需从页交换文件中加载页面, 直接申请物理内存页面并置零
MEM_IMAGE:- 后备存储器为映像文件, 如 exe 或 dll,
- 映像文件为 PE 格式, 内部有多个不同的段, 加载不同的段时会使用不同的页面保护属性
MEM_MAPPED:- 后备存储器可以为页交换文件 (如共享内存) 或其它磁盘文件 (如文件映射)
-
页面保护
PAGE_NOACCESSPAGE_READONLYPAGE_READWRITEPAGE_WRITECOPYPAGE_EXECUTEPAGE_EXECUTE_READPAGE_EXECUTE_READWRITEPAGE_EXECUTE_WRITECOPYPAGE_GUARD: 设置一次性内存访问异常处理
-
地址访问
-
CPU 中的内存管理单元 (MMU) 负责根据页表基址寄存器 (PTBR) 存储的页表基址从翻译后备缓冲器 (TLB) 获取虚拟地址对应的页表表项 (PTE) 从而翻译为物理地址
-
操作系统为每个进程维护一个多级页表, 最后一级页表表项记录虚拟地址对应的物理地址, 其它级页表表项记录下一级页表基地址, 通常只有一级页表常驻内存
-
根据页表索引到页面的物理地址后, 有以下两种情况
- 对应页面已经在内存中, 说明该页面在此之前已经被本进程或其它进程访问过了
- 对应页面没有在内存中, 说明该页面可能是第一次访问, 或之前被冲刷到后备存储器了
- 触发缺页异常, 控制流转到操作系统异常处理程序
- 利用文件系统驱动, 将对应的页面从其后备存储器中加载到内存
- 当内存中无空闲页面时, 根据某种缓存驱逐策略来选择使用页面, 若为脏页则先将其冲刷到其后备存储器再使用
-
-
工作集 (Working Set)
- 每个进程维护一个工作集, 管理驻留在内存中的页面
- 最作集大小默认最小 50 个页面,最大 345 个页面(在现代系统中会自动动态调整)
-
内存池 (Memory Pool)
- 内核使用的内存分为两种
- paged pool: 可交换出物理内存。单处理器系统有 3 个, 多处理器系统有 5 个
- nonpaged pool: 驻留在物理内存中
GetSystemInfo: CPU 硬件信息GetPerformanceInfo: 系统运行时性能信息GlobalMemoryStatusEx: 系统内存使用情况GetProcessMemoryInfo: 进程内存使用情况VirtualQueryVirtualAlloc: 控制内存页面状态, 申请大页内存VirtualFreeVirtualProtectCreateFileCreateFileMappingMapViewOfFileHeapCreateHeapDestroyHeapAllocHeapFreeHeapReAllocGetProcessHeap: 每个进程有一个默认的线程安全的堆GetProcessHeapsGetProcessWorkingSetSizeSetProcessWorkingSetSize
参考 Error Handling、Structured Exception Handling、Windows Error Reporting
-
返回码: 几乎所有系统 API 的调用都可能会失败, 通常返回特殊的值表示调用失败
BOOL: 返回FALSE表示失败HANDLE: 返回NULL或INVALID_HANDLE_VALUE(特别注意)LPVOID: 返回NULL表示失败HRESULT: 使用SUCCEEDED(hr)或FAILED(hr)来检测成功或失败
-
错误码: 当系统 API 调用失败时 (少数是在调用成功时) 设置一个线程独立的错误码来表示失败 (或成功) 的原因
std::string GetLastErrorAsString() {
std::string message;
DWORD errorMessageID = ::GetLastError();
if(errorMessageID == 0) {
return message;
}
LPSTR messageBuffer = nullptr;
size_t size = FormatMessageA(
FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_IGNORE_INSERTS,
NULL, errorMessageID, MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), (LPSTR)&messageBuffer, 0,
NULL);
if (size) {
message = std::string(messageBuffer, size);
LocalFree(messageBuffer);
}
return message;
}-
错误模式: 当进程发生严重错误时, 系统默认会显示错误对话框, 对话框会挂起进程 (同步执行)
SEM_FAILCRITICALERRORS, 不显示对话框转而将错误发送给进程, 通常应该设置改标志SEM_NOALIGNMENTFAULTEXCEPT, 在支持的平台上自动修复内存对齐错误SEM_NOGPFAULTERRORBOX, 不显示 WER 对话框SEM_NOOPENFILEERRORBOX, 不显示当OpenFile传入OF_PROMPT标志且对应文件不存在时的对话框
-
结构化异常处理 (SEH): 使用 SEH 后编译器禁止在
__try中构造 C++ 对象, 因为 SEH 的栈展开不会调用析构函数
/*
* Termination-Handler Syntax
*/
__try {
// 可以使用 __leave 提前离开 __try 块且避免非正常离开和性能处罚
} __finally {
// 只要控制流离开 __try 块就会执行 __finally 块, 包括栈展开, 除非因线程终止而离开 __try 块
}
/*
* Exception-Handler Syntax
*/
__try {
// 当发生底层的硬件或软件错误时会触发异常, 也可以调用 RaiseException 手动触发
}
// 过滤表达式结果可以是以下值之一:
// EXCEPTION_CONTINUE_EXECUTION 表示继续执行发生异常的指令, 注意可能导致死循环
// EXCEPTION_EXECUTE_HANDLER 表示执行后续 __except 块
// EXCEPTION_CONTINUE_SEARCH 表示搜索外部 __except 块
__except (filter-expression) {
// 当搜索到能够处理异常的 __except 块时, 先进行栈展开 (unwind) 直到 __excpet 块所在栈帧, 然后继续执行 __except 块
}-
向量化异常处理 (VEH)
- 在异常触发后且在首次通知调试器后调用
- 返回
EXCEPTION_CONTINUE_EXECUTION表示继续执行发生异常的指令, 跳过后续 VEH - 返回
EXCEPTION_CONTINUE_SEARCH表示执行后续 VEH, 然后开始搜索 SEH
-
向量化继续处理 (VCH)
- 在继续执行发生异常的指令之前调用
- 返回
EXCEPTION_CONTINUE_EXECUTION表示继续执行发生异常的指令, 跳过后续 VCH - 返回
EXCEPTION_CONTINUE_SEARCH表示执行后续 VCH, 然后从异常指令开始重新搜索 SEH
-
未处理异常
UnhandledExceptionFilter作为最外部的全局 SEH 的过滤表达式- 其内部会调用
SetUnhandledExceptionFilter设置的过滤函数 - C++ 程序默认设置了未处理异常过滤函数, 用来过滤 C++ 异常
-
Window 错误报告 (WER)
- Dumps: 需要设置
HKEY_LOCAL_MACHINE\Software\Microsoft\Windows\Windows Error Reporting\LocalDumps - Recovery: 需要调用
RegisterApplicationRecoveryCallback - MessageBox: 需要设置
HKEY_LOCAL_MACHINE\Software\Microsoft\Windows\Windows Error Reporting\DontShowUI为 0, 且GetErrorMode标志没有SEM_NOGPFAULTERRORBOX - Restart: 需要调用
RegisterApplicationRestart
- Dumps: 需要设置
GetLastErrorSetLastErrorFormatMessageGetErrorModeSetErrorModeGetThreadErrorModeSetThreadErrorMode_set_se_translator: 将结构化异常翻译为 C++ 异常从而可以被try-catch语句捕获_controlfp_s: 默认系统关闭所有浮点异常, 因此计算结果可以是 NAN 或 INFINITY 而不是异常。_clearfp: 必须在浮点异常处理块中调用该函数来获取并清除浮点异常标志RaiseExceptionGetExceptionCode: 仅可在过滤表达式和__except块中调用GetExceptionInformation: 仅可在过滤表达式中调用, 因为执行__except块时异常栈帧已被销毁AbnormalTermination: 仅可在__finally块中调用AddVectoredExceptionHandlerRemoveVectoredExceptionHandlerAddVectoredContinueHandlerRemoveVectoredContinueHandlerUnhandledExceptionFilterSetUnhandledExceptionFilterRegisterApplicationRecoveryCallbackRegisterApplicationRestart
-
Service Control Manager
- 加载顺序
- 顺序组: HKEY_LOCAL_MACHINE\System\CurrentControlSet\Control\ServiceGroupOrder
- 组内顺序: HKEY_LOCAL_MACHINE\System\CurrentControlSet\Control\GroupOrderList
- 依赖顺序
- last-known-good (LKG) configuration: 每次成功启动系统, 都会将启动配置备份, 当下次启动失败时则使用该备份配置尝试重新启动
- 加载顺序
-
Service Configuration Program
OpenSCManagerCreateServiceOpenServiceChangeServiceConfigQueryServiceConfigDeleteServiceGetServiceKeyNameGetServiceDisplayNameEnumServicesStatusExEnumDependentServicessc.exe
-
Service Control Program
StartServiceControlServiceQueryServiceStatusExsc.exe
-
Service Program
StartServiceCtrlDispatcher->ServiceMain(in new thread)RegisterServiceCtrlHandlerEx->LPHANDLER_FUNCTION_EX(in dispatcher thread)SetServiceStatus
-
User Accounts
NT AUTHORITY\LocalServiceNT AUTHORITY\NetworkServiceLocalSystem
操作系统提供了“内核对象”的机制, 简化了程序对系统中“IO 资源”的使用, 如设备、注册表、文件、管道、网络等。
注册表是 Windows 用来存储配置的层次结构数据库(B-Tree), 支持事务。
-
Key: Subkey 和 Value 的集合
- Name: 不可包含
\, 忽略大小写
- Name: 不可包含
-
Value: 包含数据的记录
- Name: 可以包含
\, 忽略大小写 - Type
- Data
- Name: 可以包含
-
Computer
HKEY_LOCAL_MACHINE: 系统全局配置Software: 软件配置, 32 位程序访问该 Key 会被重定向到WOW6432NodeCLASS: 文件扩展名配置
WOW6432Node: 具体见 Registry Keys Affected by WOW64
HKEY_CURRENT_USER: 当前用户配置Software: 软件配置, 不会重定向 32 位程序CLASS: 文件扩展名配置
Environment: 用户环境变量
HKEY_CLASSES_ROOT: 包含HKEY_CURRENT_USER\Software\Classes和HKEY_LOCAL_MACHINE\Software\Classes合并后的视图HKEY_USERS: 所有用户配置实际所在,HKEY_CURRENT_USER重定向到某子项
RegEnumKeyExRegQueryInfoKeyRegGetKeySecurityRegNotifyChangeKeyValueRegOpenKeyExWRegOpenCurrentUser: 打开当前模拟用户的HKEY_CURRENT_USERRegOpenUserClassesRoot: 打开当前模拟用户的HKEY_CLASSES_ROOT视图, 注意需要模拟用户配置已被加载 (比如用户登录) , 使用LoadUserProfile手动加载RegCreateKeyExRegDeleteKeyRegCloseKeyRegFlushKeyRegQueryValueExRegQueryMultipleValuesRegSetValueExRegDeleteValueRegSaveKeyEx: 将注册表条目保存到文件, 注意这不同于.reg文件 (用regedit.exe来导入)RegLoadKey: 不覆盖已存在的 key, 可以用RegUnLoadKey还原RegReplaceKey: 覆盖已存在的 keyRegRestoreKey
NTFS 支持事务
| 特性 | MBR 分区表 | GPT 分区表 |
|---|---|---|
| 适用固件 | BIOS | UEFI |
| 分区大小 | 最大支持近 2TB | 最大支持近 18EB |
| 主分区 | 最多只能有 3 个主分区与 1 个扩展分区 | 全部是主分区 |
| 引导加载器 | 在 MBR 头部以及主分区头部 | 在 ESP 中 |
| 分区表健壮性 | 无校验、 无备份 | 有校验、有备份 |
FindFirstVolumeFindNextVolumeFindVolumeCloseGetVolumeInformationGetDiskFreeSpaceExIDiskQuotaControlIEnumDiskQuotaUsers
- C:
- Windows (
%SystemRoot%): Window 系统目录- System: 16 位兼容系统目录
- System32: 64 位系统目录
- SystemWOW64: 32 位系统目录, (Windows 32-bit on Windows 64-bit)
- 注意文件系统重定向,对于 32 位程序,System32 被重定向到 SysWOW64,使用 Sysnative 访问真正的 System32,具体细节见 File System Redirector
- Program Files (
%ProgramFiles%): 64 位应用程序安装目录 (所有用户) - Program Files (x86) (
%ProgramFiles(x86)%): 32 位应用程序安装目录 (所有用户) - ProgramData (
%ProgramData%): 应用程序数据 (所有用户) - Users
- username (
%USERPROFILE%)- AppData
- Local (
%LOCALAPPDATA%): 应用程序状态数据 (当前用户)- Programs: 应用程序安装目录 (当前用户)
- Temp (
%TEMP%): 应用程序缓存数据(当前用户)
- Roaming (
%APPDATA%): 应用程序配置数据 (当前用户)
- Local (
- AppData
- username (
- Windows (
-
文件: 一个文件由若干个文件流组成, 文件流包含了文件的元数据和数据, 引用文件流格式
"filename:streamName:streamType", 比如myfile.dat:stream1:$DATA,filename::$DATA, 可能需要添加路径.\以防止文件名被解析为盘符- 唯一对象标识
- 安全描述符
- 重解析点
- 文件名和属性
- 文件名
- 日期时间 (创建、访问、修改)
- 文件大小 (使用、分配)
- 只读
- 隐藏
- 压缩
- 加密
- 可被备份
- 可被索引
- ...
- 文件数据
- ...
-
链接
- 硬链接
- 即目录项, 每个硬链接指向单独的文件流记录“文件名和属性”
- 不可跨文件系统
- 软连接 (Junctions)
- 利用重解析点, 仅可指向目录
- 可跨本地文件系统, 不可跨网络文件系统
- 符号链接
- 利用重解析点
- 可跨网络文件系统
- 硬链接
FindFirstFileFindNextFileFindFirstStreamFindNextStreamFindCloseGetFileInformationByHandleSetFileInformationByHandleGetFileAttributesSetFileAttributesGetFileTypeGetBinaryTypeGetFileTimeSetFileTimeGetFileSizeCreateFileDeleteFile: 直到最后一个文件句柄被关闭才真正删除文件, 而在此之前文件就无法被打开了CopyFileCopyFileExA: 可以获取拷贝进度MoveFile: 目标路径不能已存在MoveFileEx: 可以获取移动进度ReplaceFile: 目标路径必须已存在, 仅覆盖数据文件流和少量属性CreateDirectoryRemoveDirectoryReadDirectoryChangesWCreateHardLinkCreateSymbolicLink
-
文件名
- 不能包含特殊字符:
<>:"/\|?*NUL控制字符等 - 不能使用保留名称: CON, PRN, AUX, NUL, COM0, LPT0 等, 即使如 NUL.txt 带扩展名也不行
- 不能以
.或SPC结尾 - 默认忽略大小写
- 不能包含特殊字符:
-
路径名
-
路径是以
\分隔组件名 (卷、目录、文件) 的字符串 -
相对路径: 如
file.txt.\file.txt..\file.txt\file.txtC:file.txt- 会根据当前卷和当前目录, 或指定卷的当前目录扩展成完全限定路径
- 限制最大长度为 MAX_PATH (260), 包含末尾
NUL - 可以使用
.../
-
完全限定路径: 如
C:\file.txt- 限制最大长度为 MAX_PATH (260), 包含末尾
NUL - 可以使用
.../
- 限制最大长度为 MAX_PATH (260), 包含末尾
-
Win32 文件命名空间: 如
\\?\C:\file.txt- 限制最大长度大概为 32767, 包含末尾
NUL, 但单个文件名不能超过 255 - 不能使用
.../, 且必须为完全限定路径
- 限制最大长度大概为 32767, 包含末尾
-
Win32 设备命名空间: 如
\\.\PhysicalDrive0- 直接指定 NT 命名空间中的
GLOBAL??里的设备对象
- 直接指定 NT 命名空间中的
-
GetLongPathNameGetShortPathNameGetFullPathNameGetTempPath2: 返回路径长度最大 MAX_PATH+1, 不会校验路径是否已存在GetTempFileName: 自动或手动创建唯一文件名<prefix><uuuu>.TMPSHGetKnownFolderPath
-
以下标志会影响缓存行为
FILE_FLAG_RANDOM_ACCESSFILE_FLAG_SEQUENTIAL_SCANFILE_FLAG_NO_BUFFERINGFILE_FLAG_WRITE_THROUGHFILE_ATTRIBUTE_TEMPORARY
-
创建文件对象时, 会包含一个数据文件流的偏移量, 称为文件指针, 每次调用读写接口都会自动更新指针位置。
ReadFileReadFileExWriteFileWriteFileExFlushFileBuffersSetFilePointerSetEndOfFile
-
Server
WSAStartupWSASocketbindlisten(tcp only)AcceptEx(tcp only)WSASend/WSASendTo/WSARecv/WSARecvFromLPFN_DISCONNECTEX(tcp only)closesocketWSACleanup
-
Client
WSAStartupWSASocketGetAddrInfoExLPFN_CONNECTEX(implicitbind, tcp only)FreeAddrInfoExWSASend/WSASendTo/WSARecv/WSARecvFromLPFN_DISCONNECTEX(tcp only)WSARecvclosesocketWSACleanup
通过
WSAIoctl可以获取不同的底层 service provider 提供特定的 API
- 匿名管道:
- 单向通讯
- 创建后只能通过子进程继承的方式获取 handle
- 见示例 Creating a Child Process with Redirected Input and Output
CreatePipeGetStdHandleSetStdHandle
- 命名管道:
- 支持单向和双向
- 创建后可通过管道名字的方式获取 handle, 如
\\.\pipe\PipeName - 仅当服务端存在空闲的命名管道实例, 客户端才能打开它
- 见示例 Named Pipe Server 和 Named Pipe Client
CreateNamedPipe: 服务端创建命名管道ConnectNamedPipe: 服务端等待连接, 客户端可能在此之前就打开管道了, 比如在CreateNamedPipe或DisconnectNamedPipe之后DisconnectNamedPipe: 服务端关闭连接, 使客户端句柄失效 (客户端仍需要调用 CloseHandle) , 同时丢弃缓冲区中的数据CreateFile: 客户端连接服务端命名管道WaitNamedPipe: 客户端等待服务端空闲管道
auto data = COPYDATASTRUCT {
.dwData = data_type,
.cbData = data_size,
.lpData = data_buf,
};
// 系统需要确定何时释放缓冲区, 所以 WM_COPYDATA 只能使用 SendMessage 同步发送消息
SendMessage(target_hwnd, WM_COPYDATA, hwnd, &data);参考 Creating Named Shared Memory
CreateFileMappingOpenFileMappingMapViewOfFileUnmapViewOfFile
-
套接字
- 每个连接需要单独的 2 对读写缓冲区, 缓冲可以被复用
- 完善的缓冲区管理和连接管理, 支持流传输
- 服务端监听网络端口需要设置防火墙规则
- 适用场景: 跨机器的网络通讯
-
双向命名管道 (Windows 10 之后支持 UNIX Domain Socket 可以代替命名管道)
- 每个连接需要单独的 1 对读写缓冲区, 缓冲可以被复用
- 有缓冲区管理, 但需要自己完善连接管理 (特别是断开连接会丢弃缓冲区数据), 支持流传输
- 适用场景: 本机的进程间通讯
-
数据拷贝
- 每次发送数据都需要分配缓冲区, 且只能同步发送, 但消息不用排队
- 有缓冲区管理, 没有连接管理, 不支持流传输
- 适用场景: UI 同步逻辑通讯
-
共享内存
- 可以直接在共享内存中构造消息对象, 单次通讯可省去一次拷贝 (构造期间需要加锁, 通常来说构造数据比拷贝更慢)
- 可以直接在共享内存中访问并处理消息对象, 单次通讯可省去一次拷贝 (处理期间需要加锁, 通常来说处理数据比拷贝更慢)
- 没有缓冲区管理, 没有连接管理, 没有流传输
- 适用场景: 传输 Raw Data (数据量大且处理快)、用于实现多播通讯
参考
Windows 支持三种异步 IO 机制:
- Overlapped IO: 利用可同步对象
- APC: 利用 APC 任务队列
- IOCP: 利用 IOCP 消息队列
其中前两者可以使用同一个多路复用接口 MsgWaitForMultipleObjectsEx 一起监听, 而 IOCP 则不行。
-
使用流程
- 创建 IOCP
CreateIoCompletionPort(iocp only) - 创建 HANDLE
CreateFile使用FILE_FLAG_OVERLAPPED标志 - 关联 HANDLE
CreateIoCompletionPort(iocp only) - 调用异步 IO 接口
- 等待 IO 事件完成
GetQueuedCompletionStatus/WaitForMultipleObjectsEx - IO 事件完成, 唤醒等待线程
- Overlapped IO
- 唤醒等待对应 Event 对象的线程
- APC
- 唤醒 Alertable 状态的线程并执行对应回调函数
- IOCP
- FIFO 顺序通知等待线程
- IOCP 设置有最大并发数, 超过后阻塞后续等待线程
- 当线程处于其他阻塞状态时 (如
SuspendThread或WaitForMultipleObjectsEx) , 系统会通知其他线程, 这时如果阻塞线程被唤醒, 则 IOCP 关联线程会超过最大并发数
- Overlapped IO
- 创建 IOCP
-
Overlapped-IO/IOCP 支持的异步 IO 有:
ReadFileWriteFileAcceptExLPFN_CONNECTEXLPFN_DISCONNECTEXWSASendWSASendToWSASendMsgWSARecvWSARecvFromLPFN_WSARECVMSGConnectNamedPipeTransactNamedPipeWaitCommEventLockFileExDeviceIoControlReadDirectoryChangesWPostQueuedCompletionStatus(iocp only)
-
APC 支持的异步 IO 有:
ReadFileExWriteFileExSetWaitableTimerSetWaitableTimerExQueueUserAPC
-
取消异步 IO
CancelIoCancelIoEx
-
线程池
BindIoCompletionCallback- 详细内容见 Thread Pool API
参考
- Desktop Window
- Work Area
- Taskbar
- Start Button
- Taskbar Buttons
- Icon and Label
- Overlay Icon
- Progress Bar
- Jump List
- Thumbnail Toolbar
- Notification Area
- Icon
- Tooltip (Hover)
- Popup Window (Left click)
- Primary Window (Left double-click)
- Context Menu (Right-click)
- Application Window
- Non-Client Area
- Title Bar
- Application Icon
- Window Menu
- Window Title
- Minimize and Maximize Button
- Close Button
- Menu Bar
- Border
- Scroll Bar
- Title Bar
- Client Area
- Non-Client Area
HWND CreateWindowExW(
[in] DWORD dwExStyle, // 扩展样式, 控制窗口状态和行为
[in, optional] LPCWSTR lpClassName, // 窗口类, 用于控制窗口状态和行为, 通常在多个窗口间共享
[in, optional] LPCWSTR lpWindowName,// 窗口名, 用于描述应用, 通常显示在窗口标题栏和任务栏按钮上
[in] DWORD dwStyle, // 基础样式, 控制窗口状态和行为
[in] int X, // 窗口左上角位置
[in] int Y,
[in] int nWidth, // 窗口大小
[in] int nHeight,
[in, optional] HWND hWndParent, // 指定父窗口 (for child window) 或 Owner 窗口 (for top-level window)
[in, optional] HMENU hMenu, // 指定子窗口 ID (for child window) 或 Menu 句柄 (for top-level window)
[in, optional] HINSTANCE hInstance, // 指定窗口关联的模块, 用于搜索窗口类
[in, optional] LPVOID lpParam // 窗口自定义数据, 通常将窗口封装成 class 并在这里传递 this 指针
);
RegisterClassEx: 返回 Atom 可以通过宏 MAKEINTATOM 转换当窗口类名来用UnregisterClassExCreateWindowExIsWindow: 检测 HWND 是否有效DestroyWindowEndTask: 先尝试发送WM_CLOSE, 若失败则可以选择强制关闭窗口
MVVM 作为现代流行的 UI 设计模式, 引入了数据驱动的概念, 即 UI 是状态的纯函数, 只要状态数据相同, 呈现的 UI 也一定相同, 通过更改数据来更新 UI
- Top-level Window
- 条件:
dwStyle包含WS_OVERLAPPED或WS_POPUP
- 特点:
- 作为 Desktop Window 的子窗口
- 若无 Owner 则默认显示 Taskbar Button
WS_EX_APPWINDOW强制显示 Taskbar ButtonWS_EX_TOOLWINDOW强制不显示 Taskbar Button
- 条件:
EnumWindowsEnumThreadWindowsFindWindow: 根据 Window name 和 Class name 查找窗口
- Owned Window
- 条件:
- Top-level Window
pWndParent为 owner 窗口的HWND(owner 也必须是 Top-level Window)
- 特点:
- 总是在 owner 窗口上方
- 当 owner 窗口最小化时隐藏, 当 owner 窗口恢复时显示
- 跟随 owner 窗口一起销毁
- 条件:
GetLastActivePopupShowOwnedPopupsGetWindow: 可以获取 Owner 窗口
- Child Window
- 条件:
dwStyle包含WS_CHILDhWndParent为父窗口的HWND(不能为NULL)hMenu为自定义子窗口 ID
- 特点:
- 总是在父窗口上方
- 跟随父窗口一同显示/隐藏、移动、销毁
- 超出父窗口 client area 部分将被裁剪
- 禁用状态的子窗口的消息直接发送给父窗口
- 条件:
WS_CLIPCHILDREN: 从该父窗口的绘制区域裁剪掉其子窗口区域, 防止覆盖子窗口WS_CLIPSIBLINGS: 从该子窗口的绘制区域裁剪掉其兄弟窗口区域, 防止覆盖兄弟窗口IsChildGetParentSetParentGetAncestorEnumChildWindowsFindWindowExChildWindowFromPoint: 检测坐标属于哪个子窗口ChildWindowFromPointEx: 可以跳过某些状态的子窗口RealChildWindowFromPoint: 仅检测直系子窗口
- Message-only Window
- 条件
- Child Window
hWndParent为HWND_MESSAGE
- 特点
- 仅用于接受并处理消息, 不会显示
- 条件
WS_CAPTION: title bar and unsizing borderWS_SYSMENU: application icon, window menu and close button (requrieWS_CAPTION)WS_MINIMIZEBOX: minimize buttonWS_MAXIMIZEBOX: maximize and restore buttonWS_BORDER: unsizing borderWS_THICKFRAME: sizing borderWS_VSCROLL: vertical scroll barWS_HSCROLL: horizontal scroll bar- Overlapped
WS_OVERLAPPED: title bar and unsizing borderWS_OVERLAPPEDWINDOW=WS_OVERLAPPED | WS_CAPTION | WS_SYSMENU | WS_MINIMIZEBOX | WS_MAXIMIZEBOX | WS_THICKFRAME
- Popup
WS_POPUP: only client areaWS_POPUPWINDOW | WS_CAPTION=WS_POPUP | WS_BORDER | WS_SYSMENU | WS_CAPTION
- Child
WS_CHILD: only client area (cannot have a menu bar, cannot be used with theWS_POPUP)
GetTitleBarInfoGetWindowLongPtrSetWindowLongPtr
typedef struct tagWNDCLASSEXW {
UINT cbSize; // 该结构体大小
UINT style; // 控制一些绘制行为
WNDPROC lpfnWndProc; // 窗口处理函数
int cbClsExtra; // 通常为 0
int cbWndExtra; // 通常为 0
HINSTANCE hInstance; // 该窗口类关联的模块
HICON hIcon; // 图标, 会显示在 start, taskbar, title bar, system tray 等
HCURSOR hCursor; // 鼠标
HBRUSH hbrBackground; // 窗口背景颜色
LPCWSTR lpszMenuName; // 菜单资源名, 通常使用 MAKEINTRESOURCE
LPCWSTR lpszClassName; // 窗口名
HICON hIconSm; // 小图标, 若为空则自动从原图标创建
} WNDCLASSEXW, *PWNDCLASSEXW, *NPWNDCLASSEXW, *LPWNDCLASSEXW;- 创建窗口时会提供窗口类名和模块句柄, 用来搜索对应的窗口类
- local class
- global class:
style设置CS_GLOBALCLASS - system class: 第一次调用窗口函数时创建
RegisterClassExUnregisterClassGetClassNameGetClassInfoExGetClassLongPtrSetClassLongPtr
窗口名通常会显示在窗口的 title bar 和 taskbar button 里, 搜索窗口时通常也会用到窗口名
GetWindowTextLength: 对于其他进程中的 Child Window 无效GetWindowText: 对于其他进程中的 Child Window 无效SetWindowText: 对于其他进程中的 Child Window 无效WM_GETTEXTLENGTHWM_GETTEXTWM_SETTEXTInternalGetWindowText: 直接读取结构体获取, 而非上面的接口通过窗口消息, 且保证返回 Unidoe String
Top-level Window 默认使用屏幕坐标系, Child Window 默认使用客户区坐标系
WS_CAPTION: 可以让用户拖动窗口改变位置WS_THICKFRAME: 可以让用户拖动边框改变大小CW_USEDEFAULT: 创建窗口时初始位置和大小, 仅对 Overlapped Window 有效, 默认位置基于屏幕左上角偏移GetWindowInfoGetWindowRect: 若窗口已经显示过一次, 则返回大小包含边缘阴影 (Windows 10 之后窗口周围存在看不见的 margin 和 padding, 正常大小只有下左右三边存在, 最大化窗口上下左右四边都存在)GetClientRectWindowFromPointChildWindowFromPointChildWindowFromPointExRealChildWindowFromPointClientToScreenScreenToClientMapWindowPointsAdjustWindowRect: 根据 client-area 大小和 style 计算窗口大小AdjustWindowRectExCalculatePopupWindowPosition: 计算弹窗位置, 不超出屏幕或 work areaMoveWindow: 改变窗口位置和大小, 并控制窗口绘制SetWindowPos: (可以异步) 改变窗口位置和大小、Z-Order、显示状态等SetWindowPlacement: 设置正常、最小化、最大化状态的位置与大小, 还有显示状态BeginDeferWindowPosDeferWindowPos: 同时更改多个窗口位置大小、Z-Order、显示状态等EndDeferWindowPosWM_ENTERSIZEMOVE: 当点击标题栏 (移动位置) 、边框 (改变大小) 或WM_SYSCOMMAND处理SC_MOVE或SC_SIZE时WM_EXITSIZEMOVE: 结束以上操作时WM_WINDOWPOSCHANGING: 当位置、大小、Z-Order、显示/隐藏将要改变时, 对包含WS_OVERLAPPED和WS_THICKFRAME样式的窗口额外发送WM_GETMINMAXINFO消息WM_WINDOWPOSCHANGED: 当位置、大小、Z-Order、显示/隐藏将要改变时, 若位置或/和大小改变则额外发送WM_MOVE或/和WM_SIZE
WS_EX_TOPMOST: 可将 Z 轴视作 Normal 和 Topmost 两段, Topmost 窗口始终保持在 Normal 窗口和 taskbar 上面GetWindow: 可以根据 Z-order 获取子窗口GetNextWindowGetTopWindowBringWindowToTopSetWindowPos: (可以异步) 改变窗口位置和大小、Z-Order、显示状态等CascadeWindowsBeginDeferWindowPosDeferWindowPos: 同时更改多个窗口位置大小、Z-Order、显示状态等EndDeferWindowPos
前台窗口 (Foreground Window) 或激活窗口 (Active Window) 指用户当前正在使用的 Top-level 窗口, 即键盘焦点位于该窗口或其子窗口 (通常称为 Focused Window) , 前台激活窗口通常位于 (normal or topmost) Z 轴顶部。将窗口设置为前台窗口需要调用者满足一些必要条件。
因为历史原因导致出现了 foregound 和 active 两个术语。
GetLastActivePopupGetActiveWindow: 获取前台激活窗口 (仅限当前线程)SetActiveWindow: 设置前台激活窗口 (仅限当前线程)GetGUIThreadInfo: 可以获取前台激活窗口 (可以指定其他进程)GetForegroundWindowSetForegroundWindowAllowSetForegroundWindow: 在下次用户输入或下次某进程调用AllowSetForegroundWindow时失效LockSetForegroundWindow: 短暂时间后自动解锁WM_ACTIVATEAPP: 当焦点切换到不同的应用程序时, 发送给涉及的两个应用的所有窗口WM_ACTIVE: 当窗口聚焦或失焦时发送
激活窗口必须是 Top-level 窗口, 焦点窗口 (Focused Window) 是激活窗口本身或其子窗口
GetFocusSetFocusWM_KILLFOCUSWM_SETFOCUS
WS_MANIMIZEBOX: 为用户提供最大化按钮WS_MINIMIZEBOX: 为用户提供最小化按钮WS_MAXIMIZE: 创建窗口时设置初始最大化WS_MINIMIZE: 创建窗口时设置初始最小化IsZoomed: 窗口是否最大化IsIconic: 窗口是否最小化CloseWindow: 最小化窗口OpenIcon: 从最小化恢复窗口ShowWindow: 最小化、最大化、恢复、显示、隐藏、默认状态、控制是否前台激活ShowWindowAsync: 异步 ShowWindowSetWindowPlacement: 设置正常、最小化、最大化状态的位置与大小, 还有显示状态GetWindowPlacement
若当前进程在被创建时指定了 STARTUPINFO.wShowWindow 且 STARTUPINFO.dwFlag 包含 STARTF_USESHOWWINDOW, 则进程第一次调用 ShowWindow 的参数被忽略, 而强制使用父进程指定的参数。
WS_VISIBLE: 创建窗口后自动调用ShowWindow(SW_SHOW), 对于 Overlapped Window 若x设置为CW_USEDEFAULT则y也需要设置为CW_USEDEFAULT才有效IsWindowVisible:WS_VISIBLE仅表示该窗口是否在其父窗口中可见, 而若其父窗口不可见则该窗口也不可见ShowWindow: 最小化、最大化、恢复、显示、隐藏、默认状态、控制是否前台激活ShowWindowAsync: 异步 ShowWindowAnimateWindow: 显示隐藏时展示动画 (仅 client area)ShowOwnedPopupsWM_SHOWWINDOW
禁用子窗口导致子窗口的消息被父窗口接收处理, 禁用前台激活窗口导致键盘失焦且不会转移到其他窗口
WS_DISABLED: 创建窗口时设置初始禁用状态IsWindowEnabledEnableWindowWM_ENABLE:EnableWindow返回前发送该消息
WS_EX_LAYERED: 窗口支持透明度从而可以实现半透明效果和非矩形窗口, 若 Alpha 通道为 0 则透过鼠标事件WS_EX_TRANSPARENT: 在窗户下方的兄弟(由同一线程创建)被绘制之前不绘制窗户。窗口看起来是透明的,因为底层兄弟窗口的部分已经被绘制。SetLayeredWindowAttributes: 通过图层混合的方式确认最终的窗口像素 Alpha 通道UpdateLayeredWindow: 直接绘制包含 Alpha 通道的像素纹理
在调用 SetLayeredWindowAttributes 或 UpdateLayeredWindow 之前窗口不会被绘制, 调用 SetLayeredWindowAttributes 设置属性将导致 UpdateLayeredWindow 失效, 除非清除设置的属性
除了窗口用户数据 (CreateWindowEx 最后一个参数, 可通过 GetWindowLongPtr 和 GetWindowLongPtr 读写) , 还可以使用窗口属性为窗口附加额外的数据。
EnumPropsEnumPropsExGetPropSetPropRemoveProp: 通常在WM_DESTROY消息处理中调用
当线程第一次调用窗口系统相关函数时, 会创建一个消息队列用来接收消息。该线程创建的窗口的消息都被发送到该线程的消息队列中。
MSG msg{};
while (true) {
auto res = GetMessage(&msg, NULL, 0, 0); // 同步阻塞读取消息队列
if (res == -1) { // Error
break;
}
if (res == 0) { // WM_QUIT
break;
}
TranslateMessage(&msg); // 将键盘按键消息翻译生成字符消息, 放入消息队列
DispatchMessage(&msg); // 将消息分派到对应的窗口处理函数
}| 范围 | 描述 |
|---|---|
| 0 ~ WM_USER-1 | 系统保留 |
| WM_USER ~ 0x7FFF | 在窗口类内部使用的自定义消息 |
| WM_APP ~ 0xBFFF | 应用程序内部使用的自定义消息 |
| 0xC000 ~ 0xFFFF | 非以上两者的自定义消息, 调用 RegisterWindowMessage 返回以保证系统唯一 |
| 0xFFFF ~ | 系统保留 |
消息可能由系统发送、其他窗口发送、默认处理函数或其他 API 函数创建发送
GetMessage: 同步阻塞读取消息, 可设置消息过滤 (无法过滤WM_QUIT)PeekMessage: 同步非阻塞读取消息, 可设置消息过滤 (无法过滤WM_QUIT) , 可选择是否从队列中删除消息 (无法删除WM_PAINT除非更新区域为空)
消息读取的优先级
- Sent message (内部直接处理, 不会返回消息)
- Posted message
- Input (hardware) messages and system internal events
- Sent messages (again, 内部直接处理, 不会返回消息)
WM_PAINT多个消息会合并成一个 (脏区合并)WM_TIMERWM_QUIT
GetMessageTime: 获取当前消息的创建时间GetMessageCursor: 获取当前消息创建时的光标位置SendMessage: 同步发送消息SendMessageTimeout: 同步发送消息SendNotifyMessage: 目标窗口同线程则同步发送, 不同线程则异步发送消息SendMessageCallback: 目标窗口同线程则同步发送, 不同线程则异步发送消息InSendMessageExReplyMessage: 使消息的同步发送者即刻返回, 不用继续等待PostMessage: 异步发送消息, 发送到消息队列PostThreadMessage: 异步发送消息, 指定线程而非指定窗口 (该消息的hwnd为NULL)PostQuitMessage: 异步发送WM_QUIT, 该消息无法被忽略ChangeWindowMessageFilter: 允许低可信级别发送指定到指定进程 (不推荐)ChangeWindowMessageFilterEx: 允许低可信级别发送指定到指定窗口AttachThreadInput: 与指定线程共享键鼠输入状态IsGUIThread
窗口过程(Window Procdure)或称窗口处理函数, 由窗口类提供。 注意在处理消息时先查看对应的文档, 因为不同消息的参数和返回值的意义不同。
LRESULT CALLBACK MainWndProc(
HWND hwnd, // 窗口句柄
UINT uMsg, // 消息 ID
WPARAM wParam, // 消息特定参数 1
LPARAM lParam) // 消息特定参数 2
{
switch (uMsg) {
case WM_CREATE:
// client area 创建完成时
// lParam 指向结构包含窗口创建信息
// 返回 0 表示正常, 返回 -1 导致 CreateWindowEx 返回 NULL
return 0;
case WM_PAINT:
// 当窗口更新区域不为空时 (如窗口被遮挡或暴露) , 或窗口消息队列无其他消息时, 或调用 UpdateWindow (仅当更新区域不为空时有效) 或 RedrawWindow (仅当设置 RDW_INTERNALPAINT 时有效)
return 0;
case WM_CLOSE:
// 窗口关闭请求, 如点击右上角关闭按钮、任务栏关闭按钮、Alt-F4 快捷键、消息发送等
// 通常在此调用 DestroyWindow
return 0;
case WM_DESTROY:
// 窗口已被销毁, 此时子窗口还未销毁
// 通常在此调用 PostQuitMessage
return 0;
case WM_QUIT:
// 通常无法接收到此消息, 因为该消息导致 GetMeesage 返回 0 从而退出消息循环
return 0;
default:
return DefWindowProc(hwnd, uMsg, wParam, lParam);
}
}
DefWindowProc: 系统默认窗口处理函数CallWindowProc: 调用窗口处理函数, 会将消息转为 Unicode 或 ANSIGetWindowLongPtr: 可以用来获取指定窗口的窗口处理函数SetWindowLongPtr: 可以用来设置指定窗口的窗口处理函数, 新的处理函数可以通过CallWindowProc调用旧的处理函数, 以实现基类扩展GetClassLongPtrSetClassLongPtr: 也可以设置窗口类的窗口处理函数, 这样会影响到所有使用该类的窗口GetWindowThreadProcessIdIsHungAppWindowIsWindowUnicode
参考 Hook
- hook 分为局部 (仅作用于目标线程的消息队列) 和全局 (作用于 desktop 内消息队列)
- hook 会在目标处理特定消息的时候被调用, 可能在消息入队后、
GetMessage后、处理前、处理后 - hook 创建后, 在目标线程下次处理消息时, 会加载 hook dll
- 32 位进程无法为 64 位进程注入 dll, 反之亦然
- 虽然异构的 dll 无法注入, 但异构的 hook 仍然有效, 原理是在被注入目标处理消息时, 系统将消息同步转发给注入进程, 注入进程将调用 hook 函数处理消息, 然后再由系统返回到被注入目标。如果注入进程没有消息循环, 则可能导致异构的被注入目标卡死。
- hook 会在线程终止时被销毁, hook 被销毁后, 在目标线程下次处理消息时, 会尝试卸载 hook dll
SetWindowsHookExUnhookWindowsHookExCallNextHookExCallMsgFilter
参考:
- 当按下和抬起键盘按键时, 键盘会发送扫描码 (scan code) 到 CPU
- 键盘驱动程序接受扫描码, 并根据键盘布局 (layout) 将其转换成设备无关的虚拟键码 (virtual key code)
- 封装成窗口消息发送到系统消息队列
- 系统再将消息分派到焦点 (focus) 窗口或激活 (active) 窗口
WM_KEYDOWNWM_KEYUPWM_SYSKEYDOWNWM_SYSKEYUP
系统仅在以下情况发送 WM_SYSKEY* 而非 WM_KEY* 消息
- 按下或抬起 F10
- ALT 键处于按下状态
- 没有焦点窗口, 转而发送给激活窗口
case WM_KEYDOWN:
case WM_KEYUP:
case WM_SYSKEYDOWN:
case WM_SYSKEYUP:
{
WORD vkCode = LOWORD(wParam); // virtual-key code
WORD keyFlags = HIWORD(lParam);
WORD scanCode = LOBYTE(keyFlags); // scan code
BOOL isExtendedKey = (keyFlags & KF_EXTENDED) == KF_EXTENDED; // extended-key flag, 1 if scancode has 0xE0 prefix
if (isExtendedKey)
scanCode = MAKEWORD(scanCode, 0xE0);
BOOL wasKeyDown = (keyFlags & KF_REPEAT) == KF_REPEAT; // previous key-state flag, 1 on autorepeat
WORD repeatCount = LOWORD(lParam); // repeat count, > 0 if several keydown messages was combined into one message
BOOL isKeyReleased = (keyFlags & KF_UP) == KF_UP; // transition-state flag, 1 on keyup
}
break;WM_CHAR: Unicode 窗口过程为 UTF-16 编码, 可以包含一对或两对, ANSI 窗口过程为 ANSI Page CodeWM_UNICHA: UTF-32 编码, 通常由应用发送WM_SYSCHAR: ALT 键为按下状态时按下的字符键
TranslateMessage 函数处理按键消息, 并根据系统键盘布局, 会额外生成字符消息放到消息队列
MapVirtualKey: 转换 virtual-key code 为 scan code 或 character, 或转换 scan code 为 virtual-key codeVkKeyScanEx: 转换 character 为 virtual-key code 和 shift stateToUnicode: 转换 virtual-key code 和键盘状态为 character
获取或影响键盘状态
GetKeyState: 获取当前消息生成时的按键状态GetAsyncKeyState: 获取处理当前消息时的硬件级按键状态GetKeyNameText: 获取按键名称SendInput: 模拟用户输入, 该函数不会重置按键状态, 所以调用之前的按键状态会影响该函数产生的消息BlockInput: 阻止非模拟用户输入事件发送到应用程序
快捷键 (Shorcut) 指在应用内部使用的按键组合, 可以执行应用功能, 该功能通常在 menu 中提供
MSG msg{};
while (true) {
auto res = GetMessage(&msg, NULL, 0, 0); // 同步阻塞读取消息队列
if (res == -1) { // Error
break;
}
if (res == 0) { // WM_QUIT
break;
}
if (!TranslateAccelerator(hwndMain, haccel, &msg)) {
TranslateMessage(&msg); // 将键盘按键消息翻译生成字符消息, 放入消息队列
DispatchMessage(&msg); // 将消息分派到对应的窗口处理函数
}
}TranslateAccelerator: 根据设置的快捷键表, 将WM_KEYDOWN和WM_SYSKEYDOWN翻译成WM_COMMAND或WM_SYSCOMMANDLoadAcceleratorsCreateAcceleratorTableDestroyAcceleratorTableWM_COMMAND: 可能由菜单命令、子控件通知消息、快捷键发送WM_SYSCOMMAND: 可能由窗口菜单命令、触发窗口菜单命令的快捷键发送WM_APPCOMMAND:DefWindowProc会处理WM_XBUTTONUP和WM_NCXBUTTONUP, 和键盘上的多媒体按键, 并生成该消息, 若消息未被处理则冒泡到父窗口。
热键 (Hotkey) 指全局响应的按键组合, 当用户按下指定热键时窗口会收到 WM_HOTKEY 消息, 即使窗口不是焦点窗口或激活窗口。
RegisterHotKeyUnregisterHotKeyWM_HOTKEY
参考
- 用户移动鼠标 (Mouse) 时屏幕上显示的表示光标 (Cursor) 的位图也会跟着移动, 光标位图的其中一个像素用于定位, 称为 hot spot
- 默认仅 hot spot 下面的窗口会接收鼠标输入事件, 即使该窗口不是激活窗口
- 前台窗口可以使用
SetCapture捕获所有区域的鼠标事件, 直到调用RealseCapture或用户点击了另一个线程的窗口
| Message | Meaning |
|---|---|
WM_NCHITTEST |
点击测试, 用于确认鼠标位于 Client area 还是 Non-client area |
WM_LBUTTONDOWN |
主键单击按下 |
WM_LBUTTONDBLCLK |
主键双击按下 |
WM_LBUTTONUP |
主键抬起 |
WM_RBUTTONDOWN |
辅键单击按下 |
WM_RBUTTONDBLCLK |
辅键双击按下 |
WM_RBUTTONUP |
辅键抬起 |
WM_MBUTTONDOWN |
中键单机按下 |
WM_MBUTTONDBLCLK |
中键双击按下 |
WM_MBUTTONUP |
中键抬起 |
WM_XBUTTONDOWN |
四/五键单机按下 |
WM_XBUTTONDBLCLK |
四/五键双击按下 |
WM_XBUTTONUP |
四/五键抬起 |
WM_MOUSEWHEEL |
滚轮滚动 |
WM_MOUSEMOVE |
鼠标移动 |
WM_MOUSEHOVER |
鼠标悬停 |
WM_MOUSELEAVE |
鼠标离开客户区 |
- 在所有鼠标事件发送前, 会前发送
WM_NCHITTEST进行点击测试判断鼠标事件发生在客户区还是非客户区, 根据返回值不同发送不同消息, 主要分为WM_*和WM_NC*两种 - 调用
TrackMouseEvent可以跟踪额外鼠标事件WM_MOUSEHOVER和WM_MOUSELEAVE, 当后者触发时则跟踪失效, 需要重新调用函数才能继续跟踪 - 双击事件需要设置窗口类样式
CS_DBLCLKS - 双击事件顺序
WM_LBUTTONDOWNWM_LBUTTONUPWM_LBUTTONDBLCLKWM_LBUTTONUP
- Mouse Sonar: 提示用户光标位置
SPI_GETMOUSESONARSPI_SETMOUSESONAR
- Mouse Vanish: 输入时隐藏光标
SPI_GETMOUSEVANISHSPI_SETMOUSEVANISH
GetMessageCursor: 获取当前消息创建时的光标位置GetCursorPos: 获取当前光标位置SetCursorPos: 设置当前光标位置GetClipCursor: 获取光标限制区域ClipCursor: 设置光标限制区域GetCursor: 获取光标GetCursorInfo: 获取光标信息SetCursor: 设置光标, 为 NULL 则删除光标ShowCursor: 增加或减小计数, 其大于或等于 0 时显示光标, 小于 0 时隐藏光标
参考 Raw Input
Raw Input 机制用于直接获取设备的原始输入, 比如获取鼠标相对移动距离等. 每个进程针对同一设备仅能注册一次, 只有最新注册的那次才生效
GetRawInputDeviceListGetRawInputDeviceInfoGetRegisteredRawInputDevicesRegisterRawInputDevices: 注册设备, 可以设置标志来禁止系统产生WM_MOUSEMOVE等 legacy input 消息GetRawInputBuffer: 用于直接读取多个WM_RAWINPUT消息, 跳过GetMessageGetRawInputData: 用于将WM_RAWINPUT消息转换为RAWINPUT结构
参考
-
输入法编辑器 (IME) 作为 DLL 加载到应用程序, 应用程序利用输入法管理器 (IMM) 接口与 IME 交互
-
系统为每个线程分配一个输入上下文用于维护输入法状态, 默认线程中所有窗口共享一个输入上下文
-
默认输入控件会创建 IME 窗口
- 状态窗口: 指示 IME 已打开, 并能设置转换模式
- 组合窗口: 显示输入字符和转换后的字符
- 候选窗口: 显示转换字符的候选列表
ImmGetContextImmReleaseContextImmCreateContextImmAssociateContextImmGetCompositionStringImmSetCompositionString: 组合字符存储状态包括属性、分句信息、输入字符、文本输入光标位置ImmGetCandidateListCountImmGetCandidateListWM_IME_SETCONTEXT: 窗口被激活并显示 IME 窗口WM_IME_NOTIFY: IME 窗口状态发生改变WM_IME_COMPOSITION: 组合字符状态发生改变WM_IME_CHAR: 组合字符转换完成, 默认将该消息转换为WM_CHAR
参考
UpdateWindowRedrawWindowWM_PAINT- 当窗口更新区域不为空时 (如窗口被遮挡或暴露)
- 当窗口消息队列无其他消息时
- 当调用 UpdateWindow (仅当更新区域不为空时有效)
- 当调用 RedrawWindow (仅当设置 RDW_INTERNALPAINT 时有效)
BeginPaintEndPaint
注意: 因为重绘通常是异步的, 所以防抖 (防止手抖误触) 和节流 (防止重复点击) 是有必要的
参考
Desktop Window Manager 负责绘制所有 client area 之外的图形区域, 比如窗口标题栏、边框圆角和阴影、缩略图、半透明背景合成等. 同时, dwm 也负责将所有显示窗口的绘制结果进行合成, 最终显示到屏幕上.
Non-client area 的标题栏占据了重要位置, 如何能在标题栏上绘制 UI 并保留原本的标题栏功能呢?
- 在
WM_CREATE中使用SWP_FRAMECHANGED参数调用SetWindowPos以保证生成WM_NCCALCSIZE消息 - 处理
WM_NCCALCSIZE以扩展 client area 大小。注意左右下边界有 margin 和 padding, 上边界仅在窗口最大化时有 margin 和 padding - 绘制标题栏, 注意处理
WM_ACTIVE消息并重绘标题栏 - 处理标题栏鼠标输入
DwmGetWindowAttributeDwmSetWindowAttributeDwmGetColorizationColorDwmRegisterThumbnailDwmSetIconicThumbnailDwmUpdateThumbnailPropertiesDwmQueryThumbnailSourceSizeDwmUnregisterThumbnailDwmExtendFrameIntoClientAreaDwmDefWindowProcWM_DWMCOLORIZATIONCOLORCHANGED
参考
-
Controls
- 使用方法:
CreateWindow指定特定的 ClassName 并将父窗口设置为应用窗口 - 获取或改变状态: 给 Control 窗口发送消息
- 使用方法:
-
Dialog
- Model Dialog
- Model Dialog 有自己的消息循环, 会阻塞 Owner 窗口的消息循环, 且 Owner 窗口会被禁用 (无法被激活)
- Model Dialog 保证处于显示状态, 即使没有设置
WS_VISIBLE, 且跟随 Owner 窗口隐藏和销毁 - Model Dialog 销毁时返回值
DialogBoxDialogBoxIndirectDialogBoxParamDialogBoxIndirectParamEndDialog
- Modeless Dialog
- Modeless Dialog 使用 Owner 的消息循环, 且 Owner 窗口不会被禁用, 即当 Modeless Dialog 存在时也可以激活 Owner
- Modeless Dialog 的显示/隐藏状态与普通 Owned 窗口一样, 所有 Owner 需要适当维护其状态
- Modeless Dialog 没有返回值
CreateDialogCreateDialogIndirectCreateDialogParamCreateDialogIndirectParamEndDialog
- Model Dialog
绝大多数与 UI 交互相关的设置都可以通过以下两个函数获取或设置
SystemParametersInfoGetSystemMetrics
- 主屏的左上角为坐标原点 (0, 0), 其他屏幕的坐标可能是负数
- 不同虚拟桌面的使用同一个坐标系, 但位于不同虚拟桌面的窗口不可见
GetDesktopWindow的位置和大小等于主屏GetShellWindow的位置和大小等于虚拟屏幕EnumDisplayMonitorsEnumDisplayDevicesGetMonitorInfoEnumDisplaySettingsExChangeDisplaySettingsExMonitorFromPointMonitorFromRectMonitorFromWindow
参考
DPI Awareness Mode
-
Unaware: 应用不感知 DPI, 系统自动缩放窗口渲染位图 (导致模糊)
-
System Awareness: 应用使用系统主屏的 DPI, 当 DPI 变化时系统自动缩放其渲染位图 (导致模糊) , 比如移动窗口到其他屏幕或改变屏幕 DPI 设置
-
Per-Monitor Awareness V1: 应用使用窗口主要显示的屏幕的 DPI, 当 DPI 变化时会收到窗口消息
WM_DPICHANGED -
Per-Monitor Awareness V2: 相较 V1, 可以自动缩放非客户区和系统组件
| API | Minimum version of Windows | DPI Unaware | System DPI Aware | Per Monitor DPI Aware V1 | |
|---|---|---|---|---|---|
| SetProcessDPIAware | Windows Vista | - | SetProcessDPIAware() | - | - |
| SetProcessDpiAwareness | Windows 8.1 | SetProcessDpiAwareness(PROCESS_DPI_UNAWARE) | SetProcessDpiAwareness(PROCESS_SYSTEM_DPI_AWARE) | SetProcessDpiAwareness(PROCESS_PER_MONITOR_DPI_AWARE) | - |
| SetProcessDpiAwarenessContext | Windows 10, version 1607 | SetProcessDpiAwarenessContext(DPI_AWARENESS_CONTEXT_UNAWARE) | SetProcessDpiAwarenessContext(DPI_AWARENESS_CONTEXT_SYSTEM_AWARE) | SetProcessDpiAwarenessContext(DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE) | SetProcessDpiAwarenessContext(DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2) |
在 DPI awareness 被设置一次后, 上述函数调用会返回失败
Windows 10 之前 DPI Awareness 是进程级的属性, 现在则是 Top-level 窗口级的属性 (Child 窗口必须与其所属的 Top-level 窗口统一) , 用法是在创建窗口前调用 SetThreadDpiAwarenessContext 改变 DPI Awareness Context, 创建完成后可以还原设置。
DPI Awareness 常见问题:
- 使用
WM_DPICHANGED中提供的窗口 rect 可以保证鼠标位于相同的相对位置 - 因为大多数 API 文档缺乏 DPI 相关内容, 所以当使用 DPI Unaware 或 System DPI Aware 时注意有些 API 返回的位置尺寸信息是逻辑像素而非物理像素
- 如果两个 DPI Awareness Context 不同的窗口组成一个 Top-level 窗口, 则系统会自动继承或重置窗口该属性
| Operation | Windows 8.1 | Windows 10 (1607 and earlier) | Windows 10 (1703 and later) |
|---|---|---|---|
| CreateWindow (In-Proc) | N/A | Child inherits (mixed mode) | Child inherits (mixed mode) |
| CreateWindow (Cross-Proc) | Forced reset (of caller's process) | Child inherits (mixed mode) | Forced reset (of caller's process) |
| SetParent (In-Proc) | N/A | Forced reset (of current process) | Fail (ERROR_INVALID_STATE) |
| SetParent (Cross-Proc) | Forced reset (of child window's process) | Forced reset (of child window's process) | Forced reset (of child window's process) |
High DPI 常用的适配方案主要有两种
-
设备无关像素 (DIP): 开发时使用设备无关像素, 其实际大小根据系统或屏幕缩放设置变化, 优点是保证字体等尺寸总是具有适合的物理显示大小, 一般用于 UI 框架
-
固定分辨率: 开发时假定窗口为固定的长 x 宽分辨率, 在实际应用时, 当窗口高度变化时缩放尺寸, 当窗口宽度变化时显示更多画面, 一般用于游戏引擎
GetSystemMetricsForDpiSystemParametersInfoForDpiAdjustWindowRectExForDpiGetDpiForWindowGetDpiForMonitorGetDpiForSystem
参考
剪切板在 Window Station 中共享, 任何剪切板操作都应该由用户驱动, 即任何用户无法感知到的剪切板操作都是不可取的。
// 打开剪切板
if (!OpenClipboard(hwnd))
return FALSE;
// 使用 GlobalAlloc 带 GMEM_MOVEABLE 标志分配内存
auto hglbCopy = GlobalAlloc(GMEM_MOVEABLE, (length + 1) * sizeof(TCHAR));
if (hglbCopy == NULL) {
CloseClipboard();
return FALSE;
}
// 锁定内存, 获取指针, 拷贝数据, 解锁内存
auto lptstrCopy = GlobalLock(hglbCopy);
memcpy(lptstrCopy, data, length * sizeof(TCHAR));
lptstrCopy[length] = (TCHAR) 0;
GlobalUnlock(hglbCopy);
// 清空剪切板并写入数据
EmptyClipboard();
SetClipboardData(CF_TEXT, hglbCopy);-
延迟拷贝
- 调用
SetClipboardData时数据内存参数为NULL, 则表示延迟拷贝 - 当某处需要粘贴时, 选择延迟拷贝的窗口会收到
WM_RENDERFORMAT, 此时应该调用SetClipboardData来写入数据, 此时无需提前调用OpenClipboard - 当选择延迟拷贝的窗口将被销毁时, 为避免丢失拷贝数据, 窗口会收到
WM_RENDERALLFORMATS, 此时应该先尝试OpenClipboard, 在写入所有格式的数据 - 通常当数据小于 100 KiB 时, 延迟拷贝内部开销将总是大于数据拷贝开销
- 调用
-
数据格式
- 同一份数据可以提供多种格式, 比如
CF_TEXT,CF_UNICODETEXT, 系统内部会根据粘贴请求的格式, 尝试进行数据格式转换 - 系统会自动释放标准格式 (除了
CF_OWNERDISPLAY) 数据的内存。其他格式, 如自定义格式, 则在WM_DESTROYCLIPBOARD中需要手动释放
- 同一份数据可以提供多种格式, 比如
-
剪切板历史
ExcludeClipboardContentFromMonitorProcessing: 写入该格式的数据到剪切板会防止剪切板中所有格式的数据被保留到本地或云端剪切板历史CanIncludeInClipboardHistory: 写入DWORD0 到该格式的数据到剪切板会防止剪切板中所有格式的数据被同步到本地剪切板历史(不影响同步云端剪切板历史), 写入DWORD1 则指明要保留到本地剪切板历史CanUploadToCloudClipboard: 写入DWORD0 到该格式的数据到剪切板会防止剪切板中所有格式的数据被同步到云端剪切板历史(不影响同步本地剪切板历史), 写入DWORD1 则指明保留要到云端剪切板历史
-
剪切板监听, 详见 Creating a Clipboard Viewer Window
SetClipboardViewerWM_CHANGECBCHAINChangeClipboardChainWM_DRAWCLIPBOARD
拷贝
GetOpenClipboardWindow: 可获取当前打开剪切板的窗口GetClipboardOwner: 可获取上个写入剪切板的窗口OpenClipboard: 打开剪切板, 内部会尝试获取互斥锁, 同时只能有一个窗口打开剪切板, 但并不阻止多个窗口写入剪切板EmptyClipboard: 清空剪切板内容, 会向上个写入剪切板的窗口发送WM_DESTROYCLIPBOARD消息SetClipboardData: 写入数据到剪切板, 需要指定数据格式和数据内存RegisterClipboardFormat: 注册自定义格式GetClipboardFormatName: 获取格式名称CloseClipboard: 关闭剪切板, 内部释放互斥锁粘贴
EnumClipboardFormatsGetPriorityClipboardFormatIsClipboardFormatAvailableCountClipboardFormatsGetClipboardData
-
Windows SDK 最低支持版本
_WIN32_WINNT指定大版本NTDDI_VERSION指定更细化的版本, 需要同时指定_WIN32_WINNT
-
减小头文件内容以加速编译
WIN32_LEAN_AND_MEAN: 去除 Cryptography, DDE, RPC, Shell, and Windows Sockets 等很少用到的 APINOapi: 其中api可换成诸如COMM(去除 serial communication API) 等
利用资源编译器 rc 可以将一些二进制数据集成进 exe 里。
resource.h声明资源数据接口resource.rc导入resource.h并定义资源数据desktop.cpp导入resource.h引用资源
参考
Windows 系统中每个 locale 设置包含了 4 个不同的 code pages
- ANSI:
A后缀版本的系统 API 交互 - OEM: Console 交互的默认字符集编码, FAT32 文件系统非扩展文件名
- EBCDIC
- Mac
现代 Windows 系统所有 locale 设置都能使用 Unicode
- Unicode (UTF-16):
W后缀版本的系统 API 交互, 系统底层使用 UTF-16 编码, 调用A后缀版本 API 时会先尝试根据将 ANSI 转换为 Unicode - Unicode (UTF-8): 推荐的文本文件存储编码
在 Code Page 时代字符集和编码指同一个东西, 而后来 Unicode 时代则区分了几个概念
- 字符集 (character set): 指具有语义的人类可读的字符 (character) 集合, Unicode 根据用途将字符集合分为多个平面 (plane), 其中每个字符都有一个编号称为码点 (code point)
- 编码 (encoding): 将抽象的字符转换为机器可识别的数据, 通常就是将字符的码点进行编码, 比如 UTF-8, UTF-16, UTF-32
- 字形 (glyph): 最终在屏幕上显示的图像符号, 一个字形可能由多个 Unicode 字符组成, 比如 ☝🏿 由 ☝ 和 🏿 组成,
à由a和̀组成
GetACPGetOEMCPGetConsoleCPGetCPInfoExMultiByteToWideCharWideCharToMultiBytechcp.com: 命令行工具用于更改当前终端的 code page
参考
参考
- WdfDriverCreate
- WdfDeviceCreate
参考
COM 是 Windows API 中除了 Win32 之外的另一种 API 类型,很多新的上层系统 API 都以 COM 形式发布,比如 DirectX、WMI 等等。WinRT 则是在 COM 基础上包装了一层,拥有更现代化的语言特性,通过 C++ Header Only Library 的形式引用。
HRESULT CoInitializeEx(
[in, optional] LPVOID pvReserved,
[in] DWORD dwCoInit
);-
每个线程在调用 COM API 之前需要初始化 COM,有些基于 COM 的高级 API 内部会自动调用初始化
-
每次成功的
CoInitializeEx调用都需要对应一次CoUninitialize调用才能正确析构 -
为当前线程初始化 COM 时需要选择线程模型
- STA (Single-Threaded Apartments)
- 一个进程可以存在 0~任意个 STA,必要时 COM 会创建 Host STA 线程
- 与 STA 通讯依赖 Message,所以需要保证 STA 线程有 Message Pumping
- MTA (Multithreaded Apartments)
- 一个进程最多只能存在 1 个全局的 MTA,必要时 COM 会创建工作线程加入 MTA
- 与 MTA 通讯依赖 RPC,RPC 消息可能被 MTA 中任意可用的线程接收
- STA (Single-Threaded Apartments)
-
以下情况会发生与 STA/MTA 通讯
- 通过 Proxy/Stub 跨 Apartment 访问接口
- Out-Process-Server Object 的方法调用会通过 RPC 传给 Server 进程,再通过 RPC 返回到工作线程,若 Client 是 STA 则会再通过 Message 转发给对应 STA
调用方同步等待会调用
CoWaitForMultipleHandles,该函数在 STA 中调用时内部会运行COM modal loop来处理消息避免完全阻塞,类似SendMessage
class IUnknown
{
public:
virtual ULONG AddRef( void) = 0;
virtual ULONG Release( void) = 0;
virtual HRESULT QueryInterface(REFIID riid, void** ppvObject) = 0;
template<class Q>
HRESULT QueryInterface(Q** pp)
{
return QueryInterface(__uuidof(Q), (void**)pp);
}
};
#include <wrl/client.h>
Microsoft::WRL::ComPtr<IFileOpenDialog> fileOpenDialog;
HRESULT hr = CoCreateInstance(
CLSID_FileOpenDialog, // CLSID
nullptr, // No aggregation
CLSCTX_INPROC_SERVER, // Context
IID_PPV_ARGS(&fileOpenDialog)
);- COM Server 通过注册表来注册 COM API
- Client 只能通过接口指针来间接引用,且只应该在 Apartment 内部访问,跨 Apartment 访问应该使用 Proxy/Stub
- 如果 Client 和 Server 的线程模型不匹配,则 COM 会在已有的或新建对应类型的线程来创建 Object,而 Client 会得到一个 Proxy
- COM 标准只支持古老的
Begin_*/Finish_*式的异步接口,若需要callback或async/await形式的异步接口需要自己利用CoMarshalInterthreadInterfaceInStream/CoGetInterfaceAndReleaseStream来实现





















