第1章:基础
概述
本章是 Windows 系统编程的入门章节,从架构和编程两个维度展开介绍。你将学习 Windows 操作系统最核心的概念——进程(Process)、线程(Thread)、虚拟内存(Virtual Memory)和动态链接库(DLL),并通过一个简单的控制台应用程序迈出 Windows 系统编程的第一步。此外,本章还将涵盖字符串处理、32 位与 64 位开发的差异、API 错误处理以及 Windows 版本检测等实用主题。
Windows 架构概述
Windows NT 的历史始于 1993 年的 NT 3.1,Windows 10 是这一系列的最新继承者。核心概念历经数十年基本保持不变,但功能已有显著演进。
系统编程被定义为"使用底层 API 来使用和操作 Windows 中的核心对象和机制"。这包括对进程、线程、内存、同步对象、文件 I/O 等内核对象的管理与操控。
进程(Processes)
进程(Process)是一个容器和管理对象,代表程序的运行实例。进程的关键属性包括:
- 可执行程序:初始代码和数据
- 私有虚拟地址空间(Private Virtual Address Space):用于内存分配
- 访问令牌(Access Token / Primary Token):存储默认安全上下文
- 句柄表(Handle Table):指向内核对象(事件、信号量、文件等)的私有句柄表
- 一个或多个执行线程(Threads):正常用户模式进程创建时自带一个线程
进程由进程 ID 唯一标识,ID 在进程对象存在期间保持唯一,销毁后可被复用。可执行文件本身不能唯一标识进程——例如多个 notepad.exe 实例可同时运行,各自拥有独立的地址空间、线程、句柄表和进程 ID。
动态链接库(Dynamic Link Libraries)
DLL 是至少包含代码、数据和资源中一种的可执行文件,但不能直接运行(没有标准 main 函数)。它们通过以下两种方式加载到进程中:
- 静态链接(Static Linking / Load-Time Linking):进程初始化时由操作系统自动加载
- 动态链接(Dynamic Linking / Run-Time Linking):运行时通过
LoadLibrary和GetProcAddress显式请求
DLL 允许多个进程共享物理内存中的代码——System32 目录下的标准 Windows DLL(如 kernel32.dll、ntdll.dll)即属此类。
虚拟内存(Virtual Memory)
每个进程拥有私有的线性虚拟地址空间。地址空间范围从 0 开始(前 64KB 不可分配),最大值取决于进程类型:
| 进程类型 | 地址空间大小 |
|---|---|
| 32 位 Windows 上的 32 位进程(默认) | 2 GB |
32 位 Windows 上的 32 位进程(LARGEADDRESSAWARE) | 最大 3 GB |
| 64 位进程(Windows 8 及更早) | 8 TB |
| 64 位进程(Windows 8.1+) | 128 TB |
64 位 Windows 上的 32 位进程(LARGEADDRESSAWARE) | 4 GB |
"虚拟"的含义在于地址范围与物理 RAM 中的实际存储位置之间存在间接关系。数据可能在 RAM 中,也可能临时存储在页面文件(Page File)中。如果访问的内存不在 RAM 中,CPU 会引发页面错误异常(Page Fault Exception),由内存管理器的页面错误处理程序从文件中获取数据。
线程(Threads)
实际执行代码的实体是线程(Thread),包含在进程内并利用进程资源。线程的关键属性:
- 当前访问模式(用户模式或内核模式)
- 执行上下文(处理器寄存器)
- 栈(Stack):用于局部变量和调用管理
- TLS(Thread Local Storage,线程本地存储)数组
- 基本优先级和当前(动态)优先级
- 处理器亲和性(Processor Affinity)
线程的常见状态:运行中(Running)、就绪(Ready)、等待(Waiting)。我们将在第 7 章详细介绍线程的调度与同步。
通用系统架构
Windows 的通用架构由用户模式(User Mode)和内核模式(Kernel Mode)组件组成。
用户模式组件
- 用户进程:基于映像文件的普通进程(Notepad.exe、cmd.exe 等)
- 子系统 DLL:实现子系统 API 的 DLL,包括
kernel32.dll、user32.dll、gdi32.dll、advapi32.dll、combase.dll等,包含官方文档记录的 Windows API - NTDLL.DLL:系统范围 DLL,实现 Windows 原生 API(Native API),是用户模式最底层代码,负责用户模式到内核模式的转换。还实现堆管理器、映像加载器和用户模式线程池的部分功能
- 服务进程:与服务控制管理器(SCM,Service Control Manager)通信的普通 Windows 进程,SCM 位于
services.exe中
内核模式组件
- 执行体(Executive,NtOskrnl.exe):内核的上层部分,包含对象管理器、内存管理器、I/O 管理器、即插即用管理器、电源管理器、配置管理器等
- 内核层(Kernel):实现最基础且对时间敏感的部分,包括线程调度、中断和异常分发、互斥锁和信号量等
- 设备驱动程序(Device Drivers):可加载的内核模块,代码在内核模式下执行
- Win32k.sys:Windows 子系统的内核模式组件,处理用户界面部分和 GDI API
- HAL(Hardware Abstraction Layer,硬件抽象层):为设备驱动程序提供 API,屏蔽中断控制器、DMA 控制器等硬件细节
系统进程与子系统进程
- 系统进程:包括 Smss.exe、Lsass.exe、Winlogon.exe、Services.exe 等。部分系统进程被终止会导致系统崩溃
- 子系统进程(Csrss.exe):Windows 子系统的"管理器",每个会话一个实例(会话 0 和登录用户会话),是关键进程
- Hyper-V 虚拟机管理程序:存在于支持 VBS 的 Windows 10 和 Server 2016+ 系统中,提供额外安全层
Windows 应用程序开发
Windows API 主要由 C 函数组成,涵盖从进程、线程等基础服务到用户界面、图形、网络等各个方面。
两种应用程序类型(Windows 8+)
- 经典桌面应用程序(Classic Desktop Applications)
- 通用 Windows 应用程序(UWP,Universal Windows Platform)
两者内部机制相同(都使用线程、虚拟内存、DLL、句柄等)。本书聚焦桌面应用程序。
其他 API 风格
- COM(Component Object Model,组件对象模型):1993 年发布,面向组件的编程范式,用于 DirectX、WIC、DirectShow、Media Foundation、BITS、WMI 等。基本概念是接口(Interface)
- MFC(Microsoft Foundation Classes):Windows UI 功能的 C++ 包装器
- ATL(Active Template Library):基于 C++ 模板,用于构建 COM 服务器和客户端
- WTL(Windows Template Library):ATL 的扩展,UI 功能的模板包装器,比 MFC 轻量级
- .NET:框架和运行时(CLR),提供 JIT 编译和垃圾回收等
- WinRT(Windows Runtime):Windows 8+ 新增的 API 层,基于 COM 增强版,支持 C++、C#、JavaScript
你的第一个应用程序
环境要求
- Visual Studio 2017 或 2019(含"使用 C++ 进行桌面开发"工作负载)
- Windows SDK
创建步骤
- VS 2017:文件 -> 新建项目 -> C++/桌面 -> Windows 控制台应用程序
- VS 2019:创建新项目 -> 筛选"控制台"和"C++" -> 选择控制台应用
项目名称:HelloWin
代码示例
#include <windows.h>
#include <stdio.h>
int main() {
SYSTEM_INFO si;
::GetNativeSystemInfo(&si);
printf("Number of Logical Processors: %d\n", si.dwNumberOfProcessors);
printf("Page size: %d Bytes\n", si.dwPageSize);
printf("Processor Mask: 0x%p\n", (PVOID)si.dwActiveProcessorMask);
printf("Minimum process address: 0x%p\n", si.lpMinimumApplicationAddress);
printf("Maximum process address: 0x%p\n", si.lpMaximumApplicationAddress);
return 0;
}输出示例
x86 输出:
Number of Logical Processors: 12
Page size: 4096 Bytes
Processor Mask: 0x00000FFF
Minimum process address: 0x00010000
Maximum process address: 0x7FFEFFFFx64 输出:
Number of Logical Processors: 12
Page size: 4096 Bytes
Processor Mask: 0x0000000000000FFF
Minimum process address: 0x0000000000010000
Maximum process address: 0x00007FFFFFFEFFFF差异源于指针大小:32 位下指针 4 字节,64 位下指针 8 字节。函数名前使用双冒号(::GetNativeSystemInfo)是为了强调该函数是 Windows API 的一部分,而非 C++ 类的成员函数。
处理字符串
Unicode 编码概述
Windows API 中的字符串处理涉及编码问题。主要编码方式:
- UTF-8:ASCII 字符每字符 1 字节,其他语言字符使用更多字节,网页普遍使用。问题在于无法随机访问。
- UTF-16:大多数字符 2 字节,少数需要 4 字节。编程使用更方便,因为大多数字符可通过简单的偏移计算进行随机访问。
- UTF-32:每字符 4 字节,最浪费空间。
Windows 内核使用 UTF-16 编码,每个字符恰好 2 字节。Windows API 也遵循这一规则。
API 中的 ANSI / Unicode 分裂
由于历史原因(从 16 位 Windows 和 Windows 95/98 迁移而来),Windows API 同时包含 UTF-16 和 ASCII 相关函数。例如,CreateMutex 实际存在两个版本:
CreateMutexA(ANSI 版本)CreateMutexW(Wide Character / Unicode 版本)
CreateMutex 是一个宏,根据 UNICODE 编译常量展开为对应版本。Visual Studio 新项目默认定义 UNICODE。
类型定义示例:
typedef LPCSTR LPCTSTR; // const char*(未定义 UNICODE 时)
typedef LPCWSTR LPCTSTR; // const wchar_t*(定义了 UNICODE 时)字符串前缀:
const char name1[] = "Hello"; // 6 字节(含 NULL 终止符)
const wchar_t name2[] = L"Hello"; // 12 字节(含 UTF-16 NULL 终止符)TEXT 宏:根据是否定义 UNICODE 扩展为带或不带 "L" 前缀的字符串:
::CreateMutex(nullptr, FALSE, TEXT("MyMutex"));字符串相关类型
| 常见类型 | ASCII 类型 | Unicode 类型 |
|---|---|---|
TCHAR | char, CHAR | wchar_t, WCHAR |
LPTSTR, PTSTR | char*, CHAR*, PSTR | wchar_t*, WCHAR*, PWSTR |
LPCTSTR, PCTSTR | const char*, PCSTR | const wchar_t*, PCWSTR |
C/C++ 运行时中的字符串
运行时库有两组字符串函数:
- ASCII:以 "str" 开头(
strlen、strcpy、strcat) - Unicode:以 "wcs" 开头(
wcslen、wcscpy、wcscat)
宏版本(根据 _UNICODE 常量展开)以 "_tcs" 开头(_tcslen、_tcscpy、_tcscat),操作 TCHAR 类型字符串。
// 使用 _UNICODE 宏时自动选择正确版本
_tcslen(str); // -> wcslen(str) 或 strlen(str)
_tcscpy(dst, src); // -> wcscpy(dst, src) 或 strcpy(dst, src)字符串输出参数
Windows API 中字符串作为输出参数有两种常见方式。
方式一:调用者分配缓冲区
以 GetSystemDirectory 为例,调用者负责提供缓冲区:
WCHAR path[MAX_PATH];
::GetSystemDirectory(path, MAX_PATH);
printf("System directory: %ws\n", path);所有大小以字符为单位,非字节。失败时返回零。
方式二:API 自行分配内存
以 FormatMessageW 为例,使用 FORMAT_MESSAGE_ALLOCATE_BUFFER 标志,API 自行在堆上分配内存:
int main(int argc, const char* argv[]) {
if (argc < 2) {
printf("Usage: ShowError <number>\n");
return 0;
}
int message = atoi(argv[1]);
LPWSTR text;
DWORD chars = ::FormatMessage(
FORMAT_MESSAGE_ALLOCATE_BUFFER |
FORMAT_MESSAGE_FROM_SYSTEM |
FORMAT_MESSAGE_IGNORE_INSERTS,
nullptr, message, 0,
(LPWSTR)&text, // 强制类型转换
0, nullptr);
if (chars > 0) {
printf("Message %d: %ws\n", message, text);
::LocalFree(text);
}
else {
printf("No such error exists\n");
}
return 0;
}需调用 LocalFree 释放缓冲区——FormatMessage 的文档明确指定了这一要求。
示例输出:
C:\Dev\Win10SysProg\x64\Debug>ShowError.exe 2
Message 2: The system cannot find the file specified.
C:\Dev\Win10SysProg\x64\Debug>ShowError.exe 5
Message 5: Access is denied.
C:\Dev\Win10SysProg\x64\Debug>ShowError.exe 1999
No such error exists安全字符串函数
为缓解缓冲区溢出风险,微软在 C/C++ 运行时库中提供了后缀为 _s 的安全字符串函数(如 strcpy_s、wcscat_s),通过额外参数指定目标缓冲区最大大小。
void wmain(int argc, const wchar_t* argv[]) {
WCHAR buffer[32];
wcscpy_s(buffer, argv[1]); // 静态缓冲区自动计算大小
WCHAR* buffer2 = (WCHAR*)malloc(32 * sizeof(WCHAR));
wcscpy_s(buffer2, 32, argv[1]); // 大小以字符为单位
free(buffer2);
}Windows API 也提供了另一组安全字符串函数,在 <strsafe.h> 中声明:
StringCchCopy(buffer, _countof(buffer), argv[1]);
StringCchCat(buffer, _countof(buffer), L"cat");
StringCchCopy(buffer2, 32, argv[1]);所有大小都以字符为单位指定,而非字节。
32 位与 64 位开发
从 Windows Vista 开始,Windows 有官方 32 位和 64 位版本。从 Windows Server 2008 R2 起,所有服务器版本只有 64 位。
编程模型相同,但数据类型使用需谨慎——64 位系统中指针 8 字节,32 位中仅 4 字节。
错误示例: 在 64 位下指针值被截断
void* p = ...;
int value = (int)p; // 危险:64 位下指针值被截断正确做法: 使用与指针同大小的整数类型
void* p = ...;
INT_PTR value = (INT_PTR)p;常见类型大小
| 类型名称 | 32 位大小 | 64 位大小 | 描述 |
|---|---|---|---|
ULONG_PTR | 4 字节 | 8 字节 | 与指针同大小的无符号整数 |
PVOID, void* | 4 字节 | 8 字节 | 无类型指针 |
BYTE, uint8_t | 1 字节 | 1 字节 | 无符号 8 位整数 |
WORD, uint16_t | 2 字节 | 2 字节 | 无符号 16 位整数 |
DWORD, ULONG, uint32_t | 4 字节 | 4 字节 | 无符号 32 位整数 |
LONGLONG, int64_t | 8 字节 | 8 字节 | 有符号 64 位整数 |
SIZE_T, size_t | 4 字节 | 8 字节 | 与本机整数同大小的无符号整数 |
64 位进程地址空间为 128 TB(Windows 8.1+),32 位进程仅 2 GB。x64 系统上通过 WOW64(Windows 32-bit on Windows 64-bit)层执行 32 位进程。
可用 _WIN64 宏进行条件编译:
#ifdef _WIN64
printf("Processor Mask: 0x%016llX\n", si.dwActiveProcessorMask);
#else
printf("Processor Mask: 0x%08X\n", si.dwActiveProcessorMask);
#endif编码规范
本书采用以下编码规范:
- Windows API 函数:使用双冒号前缀(
::CreateFile、::GetLastError) - 类型名称:采用帕斯卡命名法(PascalCase),UI 相关类以大写 "C" 开头(与 WTL 一致)
- C++ 私有成员变量:以下划线开头、驼峰命名法(
_size、_isRunning),但 WTL 类以m_开头 - 变量名:不使用老式匈牙利命名法(少数例外:句柄用
h前缀,指针用p前缀) - 函数名:采用帕斯卡命名法
- 常用数据类型:优先使用 C++ 标准库类型
- 第三方库:使用 Windows Implementation Library (WIL),通过 Nuget 包提供
- UI 部分:使用 Windows Template Library (WTL)
C++ 用法
本书使用的 C++ 特性包括:
nullptr关键字auto关键字(类型推导)new和delete运算符- 作用域枚举(
enum class) - 类(成员变量和成员函数)
- 模板(在合适场景使用)
- 构造函数和析构函数(用于 RAII 类型构建)
处理 API 错误
不同 Windows API 函数的成功 / 失败表示方式不同,归纳如下:
| 返回类型 | 成功 | 失败 | 获取错误码方式 |
|---|---|---|---|
BOOL | 非 FALSE(0) | FALSE(0) | 调用 GetLastError |
HANDLE | 非 NULL(0) 且非 INVALID_HANDLE_VALUE(-1) | 0 或 -1 | 调用 GetLastError |
void | 通常不会失败 | 无 | 极少情况抛出 SEH 异常 |
LSTATUS / LONG | ERROR_SUCCESS(0) | >0 | 返回值即为错误码 |
HRESULT | >=0(通常 S_OK 即 0) | 负数 | 返回值即为错误码 |
| 其他 | 取决于函数 | 取决于函数 | 查看函数文档 |
BOOL 类型处理示例
BOOL是 32 位有符号整数,非 C++ 的bool类型。不要显式与TRUE(1) 比较。
BOOL success = ::CallSomeAPIThatReturnsBOOL();
if (!success) {
printf("Error: %d\n", ::GetLastError());
}HRESULT 处理示例
IGlobalInterfaceTable* pGit;
HRESULT hr = ::CoCreateInstance(CLSID_StdGlobalInterfaceTable, nullptr, CLSCTX_ALL,
IID_IGlobalInterfaceTable, (void**)&pGit);
if (FAILED(hr)) {
printf("Error: %08X\n", hr);
}
else {
pGit->Release();
}SUCCEEDED 和 FAILED 宏分别返回 true / false。HRESULT_FROM_WIN32 宏将 Win32 错误码转换为 HRESULT。
定义自定义错误代码
应用程序可通过 SetLastError 设置错误码。自定义错误码应设置第 29 位以避免与系统错误码冲突:
#define MY_ERROR_1 ((1 << 29) | 1)
#define MY_ERROR_2 ((1 << 29) | 2)
BOOL SomeApi1(int32_t, int32_t*);
BOOL SomeApi2(int32_t, int32_t*);
bool DoWork(int32_t value, int32_t* result) {
int32_t result1;
BOOL ok = ::SomeApi1(value, &result1);
if (!ok) {
::SetLastError(MY_ERROR_1);
return false;
}
int32_t result2;
ok = ::SomeApi2(value, &result2);
if (!ok) {
::SetLastError(MY_ERROR_2);
return false;
}
*result = result1 + result2;
return true;
}Windows 版本
各版本官方版本号
| 发行版本 | 版本号 |
|---|---|
| Windows NT 3.1 | 3.1 |
| Windows NT 3.5 | 3.5 |
| Windows NT 4.0 | 4 |
| Windows 2000 | 5.0 |
| Windows XP | 5.1 |
| Windows Server 2003 | 5.2 |
| Windows Vista / Server 2008 | 6.0 |
| Windows 7 / Server 2008 R2 | 6.1 |
| Windows 8 / Server 2012 | 6.2 |
| Windows 8.1 / Server 2012 R2 | 6.3 |
| Windows 10 / Server 2016 | 10.0 |
GetVersionEx 的弃用问题
GetVersionEx 已弃用,在 Windows 8.1 和 Windows 10 上调用始终返回 6.2.9200(Windows 8 版本号)。这是因为微软引入了 "Switchback" 机制,将版本号返回为不高于 6.2,除非应用程序通过清单文件声明知晓更高版本。
问题的根源在于大量应用程序存在有缺陷的版本检查代码:
// 有缺陷的检查——在 Vista(6.0)上失败
if (vi.dwMajorVersion >= 5 && vi.dwMinorVersion >= 1) {
// XP 或更高
}在 Windows Vista(主版本号 6,次版本号 0)上,条件 vi.dwMinorVersion >= 1 为 false,导致版本检查失败。
正确的版本检查:
if (vi.dwMajorVersion > 5 ||
(vi.dwMajorVersion == 5 && vi.dwMinorVersion >= 1)) {
// XP 或更高版本
}获取 Windows 版本
通过清单文件声明支持的 Windows 版本
添加清单文件让 GetVersionEx 返回真实版本号的步骤:
- 添加 XML 文件(如
manifest.xml) - 填写清单内容
- 项目属性 -> 清单工具 -> 输入和输出 -> 在其他清单文件中输入文件名
- 将"嵌入清单"设置为"是"
清单文件示例:
<?xml version="1.0" encoding="utf-8"?>
<assembly manifestVersion="1.0" xmlns="urn:schemas-microsoft-com:asm.v1">
<compatibility xmlns="urn:schemas-microsoft-com:compatibility.v1">
<application>
<!-- Windows Vista -->
<!-- <supportedOS Id="{e2011457-1546-43c5-a5fe-008deee3d3f0}" />-->
<!-- Windows 7 -->
<!-- <supportedOS Id="{35138b9a-5d96-4fbd-8e2d-a2440225f93a}" />-->
<!-- Windows 8 -->
<!-- <supportedOS Id="{4a2f28e3-53b9-4441-ba9c-d69d4a4a6e38}" />-->
<!-- Windows 8.1 -->
<!-- <supportedOS Id="{1f676c76-80e1-4239-95bb-83d0f6d0da78}" />-->
<!-- Windows 10 -->
<!-- <supportedOS Id="{8e0f7a12-bfb3-4fe8-b9a5-48fd50a15a9a}" />-->
</application>
</compatibility>
</assembly>取消对应 GUID 的注释后,GetVersionEx 将返回真实的版本号。
推荐的版本检测方法
使用 <versionhelpers.h> 中的辅助函数:
#include <versionhelpers.h>
if (::IsWindows10OrGreater()) {
printf("Running on Windows 10 or greater\n");
}
if (::IsWindowsServer()) {
printf("Running on a Server edition\n");
}这些辅助函数包括:IsWindowsXPOrGreater、IsWindowsXPSP3OrGreater、IsWindows7OrGreater、IsWindows8Point1OrGreater、IsWindows10OrGreater、IsWindowsServer。它们不接受参数,返回 TRUE 或 FALSE。
这些函数的内部实现使用 VerifyVersionInfo:
BOOL VerifyVersionInfo(
_Inout_ OSVERSIONINFOEXW *pVersionInformation,
_In_ DWORD dwTypeMask,
_In_ DWORDLONG dwlConditionMask);另一种方法:KUSER_SHARED_DATA
还有一种未公开但可靠的方法——使用 KUSER_SHARED_DATA 结构(映射到地址 0x7FFE0000):
auto sharedUserData = (BYTE*)0x7FFE0000;
printf("Version: %d.%d.%d\n",
*(ULONG*)(sharedUserData + 0x26c), // 主版本号偏移
*(ULONG*)(sharedUserData + 0x270), // 次版本号偏移
*(ULONG*)(sharedUserData + 0x260)); // 构建号偏移(Windows 10)官方建议优先使用 <versionhelpers.h> 中的公开 API,而非直接读取 KUSER_SHARED_DATA。
练习
编写一个控制台应用程序,调用
GetNativeSystemInfo输出处理器数量、页面大小、处理器掩码、最小和最大应用程序地址。对比 32 位和 64 位编译的输出差异。扩展第一个程序,额外调用
GetComputerName、GetWindowsDirectory和GetProductInfo,输出计算机名称、Windows 目录路径和产品信息。确保对所有 API 调用进行错误处理,且正确管理字符串缓冲区。使用
QueryPerformanceCounter和QueryPerformanceFrequency测量一个简单操作(如遍历一个大数组)的执行时间,以微秒为单位输出结果。尝试对比 Debug 和 Release 构建的性能差异。
总结
本章涵盖了 Windows 系统编程的基础知识,从架构和编程两个维度展开:
- 核心概念:进程作为资源容器和管理对象,线程作为实际执行体,虚拟内存提供私有地址空间隔离,DLL 实现代码共享和模块化
- 系统架构:用户模式与内核模式的分离,各类子系统 DLL、执行体、内核和 HAL 的层次关系
- 第一个应用程序:使用
GetNativeSystemInfo获取系统基础信息,理解 32 位和 64 位输出的差异 - 字符串处理:Unicode 编码基础、ANSI/Unicode API 分裂、TCHAR 通用类型、安全字符串函数
- 跨平台开发:32 位与 64 位下数据类型大小的差异,使用条件编译处理平台差异
- 错误处理:不同返回类型的 API 需要不同的错误获取方式,自定义错误码需设置第 29 位
- 版本检测:使用
<versionhelpers.h>替代已弃用的GetVersionEx
下一章将深入探讨内核对象和句柄(Kernel Objects and Handles)。