Skip to content
Published at:

第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。

DLL 是至少包含代码、数据和资源中一种的可执行文件,但不能直接运行(没有标准 main 函数)。它们通过以下两种方式加载到进程中:

  • 静态链接(Static Linking / Load-Time Linking):进程初始化时由操作系统自动加载
  • 动态链接(Dynamic Linking / Run-Time Linking):运行时通过 LoadLibraryGetProcAddress 显式请求

DLL 允许多个进程共享物理内存中的代码——System32 目录下的标准 Windows DLL(如 kernel32.dllntdll.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 位进程(LARGEADDRESSAWARE4 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.dlluser32.dllgdi32.dlladvapi32.dllcombase.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

代码示例

cpp
#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: 0x7FFEFFFF

x64 输出:

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

类型定义示例:

cpp
typedef LPCSTR LPCTSTR;  // const char*(未定义 UNICODE 时)
typedef LPCWSTR LPCTSTR; // const wchar_t*(定义了 UNICODE 时)

字符串前缀:

cpp
const char     name1[] = "Hello";   // 6 字节(含 NULL 终止符)
const wchar_t  name2[] = L"Hello";  // 12 字节(含 UTF-16 NULL 终止符)

TEXT:根据是否定义 UNICODE 扩展为带或不带 "L" 前缀的字符串:

cpp
::CreateMutex(nullptr, FALSE, TEXT("MyMutex"));

字符串相关类型

常见类型ASCII 类型Unicode 类型
TCHARchar, CHARwchar_t, WCHAR
LPTSTR, PTSTRchar*, CHAR*, PSTRwchar_t*, WCHAR*, PWSTR
LPCTSTR, PCTSTRconst char*, PCSTRconst wchar_t*, PCWSTR

C/C++ 运行时中的字符串

运行时库有两组字符串函数:

  • ASCII:以 "str" 开头(strlenstrcpystrcat
  • Unicode:以 "wcs" 开头(wcslenwcscpywcscat

宏版本(根据 _UNICODE 常量展开)以 "_tcs" 开头(_tcslen_tcscpy_tcscat),操作 TCHAR 类型字符串。

cpp
// 使用 _UNICODE 宏时自动选择正确版本
_tcslen(str);   // -> wcslen(str) 或 strlen(str)
_tcscpy(dst, src); // -> wcscpy(dst, src) 或 strcpy(dst, src)

字符串输出参数

Windows API 中字符串作为输出参数有两种常见方式。

方式一:调用者分配缓冲区

GetSystemDirectory 为例,调用者负责提供缓冲区:

cpp
WCHAR path[MAX_PATH];
::GetSystemDirectory(path, MAX_PATH);
printf("System directory: %ws\n", path);

所有大小以字符为单位,非字节。失败时返回零。

方式二:API 自行分配内存

FormatMessageW 为例,使用 FORMAT_MESSAGE_ALLOCATE_BUFFER 标志,API 自行在堆上分配内存:

cpp
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_swcscat_s),通过额外参数指定目标缓冲区最大大小。

cpp
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> 中声明:

cpp
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 位下指针值被截断

cpp
void* p = ...;
int value = (int)p;  // 危险:64 位下指针值被截断

正确做法: 使用与指针同大小的整数类型

cpp
void* p = ...;
INT_PTR value = (INT_PTR)p;

常见类型大小

类型名称32 位大小64 位大小描述
ULONG_PTR4 字节8 字节与指针同大小的无符号整数
PVOID, void*4 字节8 字节无类型指针
BYTE, uint8_t1 字节1 字节无符号 8 位整数
WORD, uint16_t2 字节2 字节无符号 16 位整数
DWORD, ULONG, uint32_t4 字节4 字节无符号 32 位整数
LONGLONG, int64_t8 字节8 字节有符号 64 位整数
SIZE_T, size_t4 字节8 字节与本机整数同大小的无符号整数

64 位进程地址空间为 128 TB(Windows 8.1+),32 位进程仅 2 GB。x64 系统上通过 WOW64(Windows 32-bit on Windows 64-bit)层执行 32 位进程。

可用 _WIN64 宏进行条件编译:

cpp
#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 关键字(类型推导)
  • newdelete 运算符
  • 作用域枚举(enum class
  • 类(成员变量和成员函数)
  • 模板(在合适场景使用)
  • 构造函数和析构函数(用于 RAII 类型构建)

处理 API 错误

不同 Windows API 函数的成功 / 失败表示方式不同,归纳如下:

返回类型成功失败获取错误码方式
BOOLFALSE(0)FALSE(0)调用 GetLastError
HANDLE非 NULL(0) 且非 INVALID_HANDLE_VALUE(-1)0 或 -1调用 GetLastError
void通常不会失败极少情况抛出 SEH 异常
LSTATUS / LONGERROR_SUCCESS(0)>0返回值即为错误码
HRESULT>=0(通常 S_OK 即 0)负数返回值即为错误码
其他取决于函数取决于函数查看函数文档

BOOL 类型处理示例

BOOL 是 32 位有符号整数,非 C++ 的 bool 类型。不要显式与 TRUE(1) 比较。

cpp
BOOL success = ::CallSomeAPIThatReturnsBOOL();
if (!success) {
    printf("Error: %d\n", ::GetLastError());
}

HRESULT 处理示例

cpp
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();
}

SUCCEEDEDFAILED 宏分别返回 true / falseHRESULT_FROM_WIN32 宏将 Win32 错误码转换为 HRESULT

定义自定义错误代码

应用程序可通过 SetLastError 设置错误码。自定义错误码应设置第 29 位以避免与系统错误码冲突:

cpp
#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.13.1
Windows NT 3.53.5
Windows NT 4.04
Windows 20005.0
Windows XP5.1
Windows Server 20035.2
Windows Vista / Server 20086.0
Windows 7 / Server 2008 R26.1
Windows 8 / Server 20126.2
Windows 8.1 / Server 2012 R26.3
Windows 10 / Server 201610.0

GetVersionEx 的弃用问题

GetVersionEx 已弃用,在 Windows 8.1 和 Windows 10 上调用始终返回 6.2.9200(Windows 8 版本号)。这是因为微软引入了 "Switchback" 机制,将版本号返回为不高于 6.2,除非应用程序通过清单文件声明知晓更高版本。

问题的根源在于大量应用程序存在有缺陷的版本检查代码:

cpp
// 有缺陷的检查——在 Vista(6.0)上失败
if (vi.dwMajorVersion >= 5 && vi.dwMinorVersion >= 1) {
    // XP 或更高
}

在 Windows Vista(主版本号 6,次版本号 0)上,条件 vi.dwMinorVersion >= 1false,导致版本检查失败。

正确的版本检查:

cpp
if (vi.dwMajorVersion > 5 ||
    (vi.dwMajorVersion == 5 && vi.dwMinorVersion >= 1)) {
    // XP 或更高版本
}

获取 Windows 版本

通过清单文件声明支持的 Windows 版本

添加清单文件让 GetVersionEx 返回真实版本号的步骤:

  1. 添加 XML 文件(如 manifest.xml
  2. 填写清单内容
  3. 项目属性 -> 清单工具 -> 输入和输出 -> 在其他清单文件中输入文件名
  4. 将"嵌入清单"设置为"是"

清单文件示例:

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> 中的辅助函数:

cpp
#include <versionhelpers.h>

if (::IsWindows10OrGreater()) {
    printf("Running on Windows 10 or greater\n");
}
if (::IsWindowsServer()) {
    printf("Running on a Server edition\n");
}

这些辅助函数包括:IsWindowsXPOrGreaterIsWindowsXPSP3OrGreaterIsWindows7OrGreaterIsWindows8Point1OrGreaterIsWindows10OrGreaterIsWindowsServer。它们不接受参数,返回 TRUEFALSE

这些函数的内部实现使用 VerifyVersionInfo

cpp
BOOL VerifyVersionInfo(
    _Inout_ OSVERSIONINFOEXW *pVersionInformation,
    _In_    DWORD dwTypeMask,
    _In_    DWORDLONG dwlConditionMask);

另一种方法:KUSER_SHARED_DATA

还有一种未公开但可靠的方法——使用 KUSER_SHARED_DATA 结构(映射到地址 0x7FFE0000):

cpp
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

练习

  1. 编写一个控制台应用程序,调用 GetNativeSystemInfo 输出处理器数量、页面大小、处理器掩码、最小和最大应用程序地址。对比 32 位和 64 位编译的输出差异。

  2. 扩展第一个程序,额外调用 GetComputerNameGetWindowsDirectoryGetProductInfo,输出计算机名称、Windows 目录路径和产品信息。确保对所有 API 调用进行错误处理,且正确管理字符串缓冲区。

  3. 使用 QueryPerformanceCounterQueryPerformanceFrequency 测量一个简单操作(如遍历一个大数组)的执行时间,以微秒为单位输出结果。尝试对比 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)。