CppGuide社区 CppGuide社区
首页
  • 最新谷歌C++风格指南(含C++17/20)
  • C++17详解
  • C++20完全指南
  • C++23快速入门
  • C++语言面试问题集锦
  • 🔥C/C++后端开发常见面试题解析 (opens new window)
  • 网络编程面试题 (opens new window)
  • 网络编程面试题 答案详解 (opens new window)
  • 聊聊WebServer作面试项目那些事儿 (opens new window)
  • 字节跳动面试官现身说 (opens new window)
  • 技术简历指南 (opens new window)
  • 🔥交易系统开发岗位求职与面试指南 (opens new window)
  • 第1章 高频C++11重难点知识解析
  • 第2章 Linux GDB高级调试指南
  • 第3章 C++多线程编程从入门到进阶
  • 第4章 C++网络编程重难点解析
  • 第5章 网络通信故障排查常用命令
  • 第6章 高性能网络通信协议设计精要
  • 第7章 高性能服务结构设计
  • 第8章 Redis网络通信模块源码分析
  • 第9章 后端服务重要模块设计探索
  • 🚀 全部章节.pdf 下载 (opens new window)
  • 源码分析系列

    • leveldb源码分析
    • libevent源码分析
    • Memcached源码分析
    • TeamTalk源码分析
    • 优质源码分享 (opens new window)
    • 🔥远程控制软件gh0st源码分析
  • 从零手写C++项目系列

    • C++游戏编程入门(零基础学C++)
    • 🔥使用C++17从零开发一个调试器 (opens new window)
    • 🔥使用C++20从零构建一个完整的低延迟交易系统 (opens new window)
    • 🔥使用C++从零写一个C语言编译器 (opens new window)
    • 从零用C语言写一个Redis
  • Windows 10系统编程
  • 🔥Windows Native API编程
  • 🔥Windows x64 ShellCode入门教程
  • 🔥Windows Shellcode实战
  • Go语言特性

    • Go开发实用指南
    • Go系统接口编程
    • 高效Go并发编程
    • Go性能调优
    • Go项目架构设计
  • Go项目实战

    • 使用Go从零开发一个数据库
    • 🔥使用Go从零开发一个编译器 (opens new window)
    • 🔥使用Go从零开发一个解释器 (opens new window)
    • 🔥用Go从零写一个编排器(类Kubernetes) (opens new window)
  • Rust编程

    • Rust编程指南
  • 数据库

    • SQL零基础指南
    • MySQL开发与调试指南
  • Linux内核

    • 心中的内核 —— 在阅读内核代码之前先理解内核
    • 🔥Linux 5.x内核开发与调试 完全指南 (opens new window)
    • TCP源码实现超详细注释版.pdf (opens new window)
GitHub (opens new window)
首页
  • 最新谷歌C++风格指南(含C++17/20)
  • C++17详解
  • C++20完全指南
  • C++23快速入门
  • C++语言面试问题集锦
  • 🔥C/C++后端开发常见面试题解析 (opens new window)
  • 网络编程面试题 (opens new window)
  • 网络编程面试题 答案详解 (opens new window)
  • 聊聊WebServer作面试项目那些事儿 (opens new window)
  • 字节跳动面试官现身说 (opens new window)
  • 技术简历指南 (opens new window)
  • 🔥交易系统开发岗位求职与面试指南 (opens new window)
  • 第1章 高频C++11重难点知识解析
  • 第2章 Linux GDB高级调试指南
  • 第3章 C++多线程编程从入门到进阶
  • 第4章 C++网络编程重难点解析
  • 第5章 网络通信故障排查常用命令
  • 第6章 高性能网络通信协议设计精要
  • 第7章 高性能服务结构设计
  • 第8章 Redis网络通信模块源码分析
  • 第9章 后端服务重要模块设计探索
  • 🚀 全部章节.pdf 下载 (opens new window)
  • 源码分析系列

    • leveldb源码分析
    • libevent源码分析
    • Memcached源码分析
    • TeamTalk源码分析
    • 优质源码分享 (opens new window)
    • 🔥远程控制软件gh0st源码分析
  • 从零手写C++项目系列

    • C++游戏编程入门(零基础学C++)
    • 🔥使用C++17从零开发一个调试器 (opens new window)
    • 🔥使用C++20从零构建一个完整的低延迟交易系统 (opens new window)
    • 🔥使用C++从零写一个C语言编译器 (opens new window)
    • 从零用C语言写一个Redis
  • Windows 10系统编程
  • 🔥Windows Native API编程
  • 🔥Windows x64 ShellCode入门教程
  • 🔥Windows Shellcode实战
  • Go语言特性

    • Go开发实用指南
    • Go系统接口编程
    • 高效Go并发编程
    • Go性能调优
    • Go项目架构设计
  • Go项目实战

    • 使用Go从零开发一个数据库
    • 🔥使用Go从零开发一个编译器 (opens new window)
    • 🔥使用Go从零开发一个解释器 (opens new window)
    • 🔥用Go从零写一个编排器(类Kubernetes) (opens new window)
  • Rust编程

    • Rust编程指南
  • 数据库

    • SQL零基础指南
    • MySQL开发与调试指南
  • Linux内核

    • 心中的内核 —— 在阅读内核代码之前先理解内核
    • 🔥Linux 5.x内核开发与调试 完全指南 (opens new window)
    • TCP源码实现超详细注释版.pdf (opens new window)
GitHub (opens new window)
  • Windows Native API编程 专栏说明
  • 第1章 原生API(Native API)开发入门
    • 1.1 Windows系统架构
    • 1.2 什么是原生API(Native API)?
    • 1.3 入门指南
    • 1.4 动态链接到NtDll.Dll
    • 1.5 访问原生API(Native API)
      • 1.5.1 <Winternl.h>头文件
    • 1.6 总结
  • 第2章 原生API(Native API)基础
  • 第3章 原生应用程序(Native Applications)
  • 第4章:系统信息
  • 第5章:进程
  • 第6章:线程
  • 第7章:对象与句柄
  • 第 8 章:内存(第一部分)
  • 第9章:I/O
  • 第10章:ALPC
  • 第11章 安全性(Security)
  • 第12章 内存(第二部分)
  • 第13章 注册表
目录

第1章 原生API(Native API)开发入门

# 第1章 原生API(Native API)开发入门

在本章中,我们将介绍与应用程序开发相关的Windows系统架构,进而说明原生API在其中所处的位置。最后,我们将学习如何在Visual Studio C++应用程序中添加所需的头文件,以便能够使用这些原生API。

本章包含以下内容:

  • Windows系统架构
  • 什么是原生API(Native API)?
  • 入门指南
  • 动态链接到NtDll.Dll
  • 访问原生API(Native API)

# 1.1 Windows系统架构

图1-1展示了简化后的Windows系统架构。用户模式(user-mode)进程(运行如Notepad.exe、Explorer.exe等镜像文件)需要执行某些操作时,任何有实际意义的操作(从系统角度而言)都需要调用内核(kernel)代码。例如,打开和操作文件、分配内存、创建线程、加载动态链接库(DLL)等,所有这些操作都需要内核介入。

img

图1-1:Windows系统架构(简化版)

典型的应用程序会调用有文档记录的Windows API,这些API实现在一组子系统动态链接库(Subsystem DLLs)中的某一个里。例如,CreateFile API实现在kernel32.dll中(其实际实现位于kernelbase.dll,kernel32.dll会跳转到kernelbase.dll,但这一细节在实际使用中并不重要)。

“子系统(Subsystem)”一词源自Windows NT中引入的原始子系统概念。如需详细了解,可参考《Windows内核原理与实现》(Windows Internals)第7版第一部分。简单来说,Windows最初有OS/2、POSIX和Windows几个子系统,如今仅剩下Windows子系统。该子系统提供了系统开发人员熟悉的API,如CreateFile。另需注意,此处的“子系统”与“Windows Subsystem for Linux”(Windows Linux子系统)并无关联,更多信息可参考《Windows内核原理与实现》(Windows Internals)一书。

CreateFile API在完成一些参数检查后,会调用NtDll.Dll中的原生API NtCreateFile,NtDll.Dll是仍处于用户模式(user-mode)的最底层动态链接库(DLL),原生API(Native API)即在此处实现。其作用是实现向内核模式(kernel-mode)的过渡,以便调用真正的NtCreateFile函数。在x64处理器上,这一过程通过将一个值加载到EAX寄存器,然后调用syscall机器指令来完成。EAX寄存器中存储的值表明了向内核请求的“服务”类型。以下是某台Windows 10计算机上NtDll.dll中NtCreateFile函数的反汇编代码:

ntdll!NtCreateFile:
00007ffa`d138db40  mov        r10, rcx
00007ffa`d138db43  mov        eax, 55h ; 0x55代表NtCreateFile系统调用
00007ffa`d138db48  test       byte ptr [SharedUserData+0x308 (00000000`7ffe0308)],1
00007ffa`d138db50  jne        ntdll!NtCreateFile+0x15 (00007ffa`d138db55)
00007ffa`d138db52  syscall
00007ffa`d138db54  ret
00007ffa`d138db55  int        2Eh
00007ffa`d138db57  ret
1
2
3
4
5
6
7
8
9

在上述代码片段中,值0x55代表NtCreateFile系统调用(system call)。每个系统调用都有其专属的系统调用号。在底层的内核侧,该值被用作**系统服务调度表(System Service Dispatch Table,SSDT)**的索引,该表存储了系统调用实现的实际地址。

从技术上讲,在64位Windows系统中,SSDT中存储的值并非绝对地址,而是相对于SSDT起始位置的32位偏移量(存储在最高28位中)。低4位存储通过栈传递给系统调用的参数个数,这样系统服务调度程序(调用系统调用的通用函数)就能知道在调用返回后需要从栈中清理多少个参数。

syscall指令之前的jne分支指令通常不会执行,因此会调用syscall。至少出于兼容性考虑,如果测试的位为0,或者直接调用int 0x2E,该指令仍然有效。

NtDll.Dll中所有系统调用的调用方式完全相同,唯一的区别在于EAX寄存器中存储的数值。需要注意的是,内核中“真正的”系统调用与用户模式(user-mode)(NtDll.Dll中)的系统调用具有完全相同的函数原型。NtDll.Dll某种程度上充当了“跳板”的角色,实现向内核的跳转;而在内核侧,会调用真正的实现。

一旦系统调用在内核中执行(具体是否需要与设备驱动程序交互,取决于系统调用本身),调用会返回至用户模式(user-mode)。需注意,执行调用的是同一个线程——线程始于用户模式(user-mode),执行syscall时线程切换到内核模式(kernel-mode),最终在执行反向指令(sysret)时线程切换回用户模式(user-mode)。

如图1-1所示,还有一个类似NtDll的动态链接库(DLL)——Win32u.Dll。它为USER和GDI(图形设备接口,Graphics Device Interface)API提供与NtDll.Dll相同的功能。例如,User32.dll中实现的CreateWindowEx函数会调用Win32u.dll中实现的NtUserCreateWindowEx。以下是该函数的系统调用过程:

win32u!NtUserCreateWindowEx:
00007ffa`cefd1eb0  mov  r10, rcx
00007ffa`cefd1eb3  mov  eax, 1074h
00007ffa`cefd1eb8  test byte ptr [SharedUserData+0x308 (00000000`7ffe0308)],1
00007ffa`cefd1ec0  jne  win32u!NtUserCreateWindowEx+0x15 (00007ffa`cefd1ec5)
00007ffa`cefd1ec2  syscall
00007ffa`cefd1ec4  ret
00007ffa`cefd1ec5  int  2Eh
00007ffa`cefd1ec7  ret
1
2
3
4
5
6
7
8
9

或许并不意外,这段代码与NtCreateFile的代码完全相同,唯一的区别在于EAX中存储的系统服务号——NtUserCreateWindowEx的系统服务号为0x1074。USER和GDI的系统调用号均以0x1000开头(第12位为1),这是有意设计的。在内核侧,这些系统调用的目标是Win32k.sys。Win32k.sys被称为“Windows子系统的内核组件”,主要实现操作系统的窗口管理功能以及经典GDI的相关调用。

# 1.2 什么是原生API(Native API)?

本专栏将重点关注NtDll.Dll提供的系统调用,即原生API(Native API)。这些API更具“研究价值”,因为它们涉及Windows最核心的功能模块,如进程、线程、内存、I/O等。从这个角度来看,子系统动态链接库(Subsystem DLLs)被绕过了,具体如图1-2所示。

img

图1-2:直接调用系统调用

原生API(Native API)大多没有官方文档,只有其中部分API在Windows驱动程序开发工具包(Windows Driver Kit,WDK)中有文档间接记录,供驱动程序开发人员使用。例如,NtCreateFile就在WDK中有相关文档。然而,大多数原生API(Native API)都缺乏官方文档,甚至连函数原型都未正式提供。这就引出了一个问题:我们如何使用这些没有文档记录的API?

在解决这个问题之前,我们先思考另一个或许更显而易见的问题:我们为什么要使用原生API(Native API)?标准的、有文档记录的Windows API存在什么问题吗?

使用原生API(Native API)具有以下优势:

  • 性能更优:使用原生API(Native API)可绕过标准Windows API,减少一个软件层,从而提升运行速度。
  • 功能更强:部分功能无法通过标准Windows API实现,但可借助原生API(Native API)实现。
  • 减少依赖:使用原生API(Native API)可消除对子系统动态链接库(Subsystem DLLs)的依赖,从而生成体积更小、更精简的可执行文件。
  • 灵活性更高:在Windows启动的早期阶段,仅依赖NtDll.dll的原生应用程序(Native Applications)可以运行,而其他应用程序则无法运行。

尽管如此,使用原生API(Native API)也存在潜在的劣势:

  • 大多缺乏文档记录,上手难度较大。这也是本专栏编写的初衷!
  • 缺乏文档记录还意味着微软可能会在不提前通知的情况下修改原生API(Native API),且用户无权提出异议。不过,原生API(Native API)中功能的移除或现有功能的修改并不常见,因为微软自身的一些工具和应用程序也在大量使用原生API(Native API)。

回到第一个问题:我们如何获取原生API(Native API)函数的原型?一个来源是WDK,它提供了良好的起点,尽管并不完整。如果你有相关意愿,逆向工程(reverse engineering)也能提供帮助。幸运的是,我们并不需要这么做。GitHub上的winsiderss/phnt项目(前身为processhacker)提供了大多数原生API(Native API)的函数原型,以及相关的常量、枚举和结构体,可直接使用。

注:撰写本专栏时,该项目的URL为https://github.com/winsiderss/phnt。

# 1.3 入门指南

让我们从创建一个“Hello World”类型的应用程序开始,使用一个原生API(Native API)。我们将使用的API是NtQuerySystemInformation,它的函数原型相对简单,但功能强大:

NTSTATUS NTAPI NtQuerySystemInformation(
    _In_  SYSTEM_INFORMATION_CLASS SystemInformationClass,
    _Out_writes_bytes_opt_(SystemInformationLength) PVOID SystemInformation,
    _In_  ULONG SystemInformationLength,
    _Out_opt_ PULONG ReturnLength
);
1
2
3
4
5
6

NTAPI宏会展开为__stdcall,这是大多数Windows API和原生API(Native API)使用的标准调用约定(calling convention)。该宏仅在32位进程中有实际意义,因为64位进程中只有一种调用约定。

我们将在第3章详细讨论NtQuerySystemInformation API。目前,我们只需使用它,通过指定某个SYSTEM_INFORMATION_CLASS值及其关联的结构体,获取一些基本的系统信息。以下是我们将使用的枚举值及其对应的结构体:

typedef enum _SYSTEM_INFORMATION_CLASS {
    SystemBasicInformation,
} SYSTEM_INFORMATION_CLASS;

typedef struct _SYSTEM_BASIC_INFORMATION {
    ULONG Reserved;
    ULONG TimerResolution;
    ULONG PageSize;
    ULONG NumberOfPhysicalPages;
    ULONG LowestPhysicalPageNumber;
    ULONG HighestPhysicalPageNumber;
    ULONG AllocationGranularity;
    ULONG_PTR MinimumUserModeAddress;
    ULONG_PTR MaximumUserModeAddress;
    ULONG_PTR ActiveProcessorsAffinityMask;
    CCHAR NumberOfProcessors;
} SYSTEM_BASIC_INFORMATION, *PSYSTEM_BASIC_INFORMATION;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

我们将在Visual Studio中创建一个新的C++控制台应用程序项目,并手动添加这些定义。然后调用该API,并显示返回的部分信息。以下是完整的main函数:

int main()
{
    SYSTEM_BASIC_INFORMATION sysInfo;
    NTSTATUS status = NtQuerySystemInformation(
        SystemBasicInformation,
        &sysInfo,
        sizeof(sysInfo),
        nullptr
    );
    
    if (status == 0)
    {
        //
        // call succeeded
        //
        printf("Page size: %u bytes\n", sysInfo.PageSize);
        printf("Processors: %u\n", (ULONG)sysInfo.NumberOfProcessors);
        printf("Physical pages: %u\n", sysInfo.NumberOfPhysicalPages);
        printf("Lowest Physical page: %u\n", sysInfo.LowestPhysicalPageNumber);
        printf("Highest Physical page: %u\n", sysInfo.HighestPhysicalPageNumber);
    }
    else
    {
        printf("Error calling NtQuerySystemInformation (0x%X)\n", status);
    }
    
    return 0;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28

编译这段代码会产生一个链接器错误,提示找不到NtQuerySystemInformation的实现:

HelloNative.obj : error LNK2019: unresolved external symbol "long __cdecl NtQuerySystemInformation(enum _SYSTEM_INFORMATION_CLASS,void *,unsigned long,unsigned long *)" (?NtQuerySystemInformation@@YAJW4_SYSTEM_INFORMATION_CLASS@@PEAXKPEAK@Z) referenced in function main
1

尽管只显示了一个错误,但实际上存在两个问题。第一个问题是链接器不知道NtQuerySystemInformation的实现位置。我们知道它位于NtDll.dll中,但链接器并不知道,因此我们需要告知链接器。

一种方法是将Visual Studio安装时提供的导入库(import library)ntdll.lib添加到链接器的“输入”节点中(可在此添加额外的导入库),如图1-3所示。

图1-3:Visual Studio中链接器的“输入”节点

注:最好在图1-3所示对话框的顶部选择“所有配置”和“所有平台”,这样就无需在每个配置和平台中重复设置。

另一种实现相同效果的方法是在源代码中使用#pragma指令添加相同的导入库:

#pragma comment(lib, "ntdll")
1

即使进行了上述修改(无论使用哪种方法),我们仍然会遇到相同的链接器错误。问题在于NtQuerySystemInformation是在C++文件中定义的,会由C++编译器进行处理。C++编译器允许同名函数存在(函数重载),只要参数不同即可,因此编译器会对函数名进行修饰(添加一些特殊符号),以体现其参数的唯一性。这就导致链接器无法将该函数名与NtDll.dll中真正的C语言实现的函数名关联起来。

幸运的是,解决方案很简单:使用extern "C"修饰符修饰该函数,强制编译器将其视为C语言函数(而非C++函数):

extern "C" NTSTATUS NTAPI NtQuerySystemInformation(
    _In_  SYSTEM_INFORMATION_CLASS SystemInformationClass,
    _Out_writes_bytes_opt_(SystemInformationLength) PVOID SystemInformation,
    _In_  ULONG SystemInformationLength,
    _Out_opt_ PULONG ReturnLength
);
1
2
3
4
5
6

此时,所有内容都应能成功链接,运行应用程序后会产生类似以下的输出:

Page size: 4096 bytes
Processors: 16
Physical pages: 33346913
Lowest Physical page: 1
Highest Physical page: 34142207
1
2
3
4
5

以下是完整代码(可在HelloNative项目中找到):

#include <Windows.h>
#include <stdio.h>

typedef enum _SYSTEM_INFORMATION_CLASS {
    SystemBasicInformation,
} SYSTEM_INFORMATION_CLASS;

typedef struct _SYSTEM_BASIC_INFORMATION {
    ULONG Reserved;
    ULONG TimerResolution;
    ULONG PageSize;
    ULONG NumberOfPhysicalPages;
    ULONG LowestPhysicalPageNumber;
    ULONG HighestPhysicalPageNumber;
    ULONG AllocationGranularity;
    ULONG_PTR MinimumUserModeAddress;
    ULONG_PTR MaximumUserModeAddress;
    ULONG_PTR ActiveProcessorsAffinityMask;
    CCHAR NumberOfProcessors;
} SYSTEM_BASIC_INFORMATION, *PSYSTEM_BASIC_INFORMATION;

extern "C" NTSTATUS NTAPI NtQuerySystemInformation(
    _In_  SYSTEM_INFORMATION_CLASS SystemInformationClass,
    _Out_writes_bytes_opt_(SystemInformationLength) PVOID SystemInformation,
    _In_  ULONG SystemInformationLength,
    _Out_opt_ PULONG ReturnLength
);

#pragma comment(lib, "ntdll")

int main()
{
    SYSTEM_BASIC_INFORMATION sysInfo;
    NTSTATUS status = NtQuerySystemInformation(
        SystemBasicInformation,
        &sysInfo,
        sizeof(sysInfo),
        nullptr
    );
    
    if (status == 0)
    {
        //
        // call succeeded
        //
        printf("Page size: %u bytes\n", sysInfo.PageSize);
        printf("Processors: %u\n", (ULONG)sysInfo.NumberOfProcessors);
        printf("Physical pages: %u\n", sysInfo.NumberOfPhysicalPages);
        printf("Lowest Physical page: %u\n", sysInfo.LowestPhysicalPageNumber);
        printf("Highest Physical page: %u\n", sysInfo.HighestPhysicalPageNumber);
    }
    else
    {
        printf("Error calling NtQuerySystemInformation (0x%X)\n", status);
    }
    
    return 0;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58

包含<Windows.h>头文件是必要的(至少目前是这样),因为它提供了ULONG、PVOID等基本定义以及其他标准Windows类型。

图1-3中显示的Visual Studio变量$(CoreLibraryDependencies)包含了许多标准子系统动态链接库(Subsystem DLLs),但不包括NtDll.lib,这也是项目最初链接失败的原因。你可以通过点击最右侧的向下箭头(图1-3)并选择“编辑...”,然后点击“宏”按钮来验证这一点。在展开的窗口顶部的编辑框中输入内容,可以过滤变量列表,查看该特定变量下隐藏的默认导入库列表(图1-4)。

图1-4:Visual Studio变量$(CoreLibraryDependencies)

# 1.4 动态链接到NtDll.Dll

使用导入库NtDll.lib的方法是可行的,但有时可能并非最佳选择。一个潜在的问题是,应用程序可能会使用某些目标Windows版本不支持的API。在这种情况下,应用程序可能编译和链接都正常,但在不支持该API的系统上启动时,会在启动时崩溃。

为了解决这个问题,可以在运行时动态绑定API。如果目标API存在,我们会获取到它的指针;否则,会返回一个空指针(NULL),应用程序可以优雅地处理这种失败,而不会崩溃。

通过Windows API函数GetProcAddress可以实现对函数的动态绑定。以下是一个示例:

auto pNtQuerySystemInformation =
    (decltype(NtQuerySystemInformation)*)GetProcAddress(
        GetModuleHandle(L"ntdll"), "NtQuerySystemInformation"
    );

if (pNtQuerySystemInformation)
{
    SYSTEM_BASIC_INFORMATION sysInfo;
    NTSTATUS status = pNtQuerySystemInformation(
        SystemBasicInformation,
        &sysInfo,
        sizeof(sysInfo),
        nullptr
    );
    // ...
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

使用C++11的auto和decltype关键字可以简化代码,无需定义函数指针。但如果需要,也可以手动定义:

typedef NTSTATUS (NTAPI *PNtQuerySystemInformation)(
    _In_  SYSTEM_INFORMATION_CLASS SystemInformationClass,
    _Out_writes_bytes_opt_(SystemInformationLength) PVOID SystemInformation,
    _In_  ULONG SystemInformationLength,
    _Out_opt_ PULONG ReturnLength
);

int main()
{
    auto NtQuerySystemInformation = (PNtQuerySystemInformation)GetProcAddress(
        GetModuleHandle(L"ntdll"), "NtQuerySystemInformation"
    );

    if (NtQuerySystemInformation)
    {
        SYSTEM_BASIC_INFORMATION sysInfo;
        NTSTATUS status = NtQuerySystemInformation(
            SystemBasicInformation,
            &sysInfo,
            sizeof(sysInfo),
            nullptr
        );
        // ...
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25

在这种情况下,我们可以为函数指针类型添加前缀“P”,这样就可以将“真正的”函数名用作变量名,使代码看起来更“自然”一些。注意,此时不再需要extern "C",因为GetProcAddress始终假设函数名是未被修饰的。

注:该示例的完整代码位于HelloNative2项目中。

GetModuleHandle用于获取进程中NtDll.Dll动态链接库的实例句柄(本质上就是其地址),并将其传递给GetProcAddress。这一操作总能成功,因为内核会将NtDll.Dll映射到每个用户模式(user-mode)进程中,因此在进程生命周期的极早期就可用。

细心的读者可能会疑惑,既然我们的目标之一是使用原生API(Native API)而非标准Windows API,为什么还要使用GetProcAddress和GetModuleHandle?我们将在下一章中使用原生API实现相应功能,因为对应的原生API需要更多背景知识,这些知识将在下一章中介绍。无论如何,我们的目标不一定是在所有情况下都完全避免使用Windows API,而是在有意义的场景下利用原生API(Native API)的优势。

最终,代码引用原生API(Native API)的方式由开发人员决定。也可以结合两种方式:对于所有目标Windows版本都支持的API,使用静态绑定(导入库);对于可能不可用的函数,使用动态绑定。

# 1.5 访问原生API(Native API)

通过查看NtDll.dll的导出函数,可以了解原生API(Native API)的丰富程度。一种方法是使用Visual Studio安装时提供的dumpbin工具:

C:\>dumpbin /exports c:\windows\System32\ntdll.dll
Microsoft (R) COFF/PE Dumper Version 14.37.32705.0
Copyright (C) Microsoft Corporation. All rights reserved.

Dump of file c:\windows\System32\ntdll.dll

File Type: DLL

Section contains the following exports for ntdll.dll

00000000 characteristics
6349A4F2 time date stamp
0.00 version
8 ordinal base
2435 number of functions
2434 number of names

ordinal hint RVA             name

9        0  00040280  A_SHAFinal
10       1  000410B0  A_SHAInit
11       2  000410F0  A_SHAUpdate
12       3  000E09C0  AlpcAdjustCompletionListConcurrencyCount
13       4  00070780  AlpcFreeCompletionListMessage
14       5  000E09F0  AlpcGetCompletionListLastMessageInformation
15       6  000E0A10  AlpcGetCompletionListMessageAttributes
16       7  000704B0  AlpcGetHeaderSize
17       8  00070470  AlpcGetMessageAttribute
18       9  00010A60  AlpcGetMessageFromCompletionList
19       A  00085E00  AlpcGetOutstandingCompletionListMessageCount
...
2439    97E  00097FF0  wcstok_s
2440    97F  00092410  wcstol
2441    980  000924B0  wcstombs
2442    981  00092470  wcstoul
...
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36

你也可以使用任何图形化的可移植可执行文件(Portable Executable,PE)查看器(例如我自己开发的TotalPE)来查看这些导出函数:加载NtDll.Dll并查看导出数据目录(图1-5)。注意,在拍摄该截图的Windows版本中,共有2435个导出函数。

img

图1-5:TotalPE显示NtDll.Dll的导出函数

获取函数原型、结构体和枚举的一种方法是从winsiderss/phnt (opens new window)项目提供的头文件中复制所需内容。不过有一个问题:某些定义是许多函数共用的,因此你必须逐一查找并复制每个定义,以确保所有内容都能正常编译。

另一种方法是克隆上述代码仓库(或将其作为子模块添加),并按照phnt主页上的说明进行使用。基本上,应使用以下两个头文件,而无需包含<Windows.h>:

#include <phnt_windows.h>
#include <phnt.h>
1
2

此外,你还可以使用一些宏来让phnt头文件包含特定Windows版本的API。默认情况下,仅包含Windows 7的函数。在包含上述头文件之前,可通过以下宏定义指定所需的Windows版本:

#define PHNT_VERSION PHNT_WIN2K
#define PHNT_VERSION PHNT_WINXP
#define PHNT_VERSION PHNT_WS03
#define PHNT_VERSION PHNT_VISTA
#define PHNT_VERSION PHNT_WIN7
#define PHNT_VERSION PHNT_WIN8
#define PHNT_VERSION PHNT_WINBLUE
#define PHNT_VERSION PHNT_THRESHOLD
#define PHNT_VERSION PHNT_THRESHOLD2
#define PHNT_VERSION PHNT_REDSTONE
#define PHNT_VERSION PHNT_REDSTONE2
#define PHNT_VERSION PHNT_REDSTONE3
#define PHNT_VERSION PHNT_REDSTONE4
#define PHNT_VERSION PHNT_REDSTONE5
#define PHNT_VERSION PHNT_19H1
#define PHNT_VERSION PHNT_19H2
#define PHNT_VERSION PHNT_20H1

上述名称是微软用于表示各个Windows版本的内部名称。该列表未来可能会扩展。

获取phnt头文件的另一种方法是使用vcpkg (opens new window)包管理器安装phnt包。vcpkg是一个C++包管理器。按照上述链接中的说明安装vcpkg(如果尚未安装),然后可以通过以下命令安装phnt:

C:\vcpkg> .\vcpkg.exe install phnt:x64-windows
1

假设vcpkg已与Visual Studio集成(通过vcpkg integrate install命令),你将能够直接使用phnt头文件,无需在项目中添加任何特殊配置。

以下代码实现了与HelloNative相同的功能,但使用了phnt头文件(假设已通过vcpkg安装、手动复制到项目中或作为Git子模块添加)(示例中的HelloNative3项目):

#include <phnt_windows.h>
#include <phnt.h>
#include <stdio.h>

#pragma comment(lib, "ntdll")

int main()
{
    SYSTEM_BASIC_INFORMATION sysInfo;
    NTSTATUS status = NtQuerySystemInformation(
        SystemBasicInformation,
        &sysInfo,
        sizeof(sysInfo),
        nullptr
    );
    
    if (status == STATUS_SUCCESS)
    {
        printf("Page size: %u bytes\n", sysInfo.PageSize);
        printf("Processors: %u\n", (ULONG)sysInfo.NumberOfProcessors);
        printf("Physical pages: %u\n", sysInfo.NumberOfPhysicalPages);
        printf("Lowest Physical page: %u\n", sysInfo.LowestPhysicalPageNumber);
        printf("Highest Physical page: %u\n", sysInfo.HighestPhysicalPageNumber);
    }
    else
    {
        printf("Error calling NtQuerySystemInformation (0x%X)\n", status);
    }
    
    return 0;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31

注意,代码中可以使用STATUS_SUCCESS(值为0)进行比较,因为该常量已在phnt头文件中正确定义。另外,链接仍然是开发人员的责任。上述示例使用#pragma指令向项目添加了NtDll.lib依赖。

通过vcpkg安装phnt。创建一个新的C++项目,添加上述代码,并确保所有内容都能成功编译、链接和执行。

# 1.5.1 <Winternl.h>头文件

Windows SDK中提供了一个名为<Winternl.h>的头文件,其中包含了一小部分原生API(Native API)及其定义。使用该头文件看似方便,对于非常简单的需求,可能也足够用。

然而,该文件中的定义并不总是完整的。例如,SYSTEM_BASIC_INFORMATION结构体的定义如下:

typedef struct _SYSTEM_BASIC_INFORMATION {
    BYTE Reserved1[24];
    PVOID Reserved2[4];
    CCHAR NumberOfProcessors;
} SYSTEM_BASIC_INFORMATION, *PSYSTEM_BASIC_INFORMATION;
1
2
3
4
5

该定义仅提供了处理器数量相关的信息,没有其他内容。如果尝试将该头文件与phnt头文件一起使用,许多定义会发生冲突。总之,不要在任何涉及原生API(Native API)的正式开发工作中使用<Winternl.h>头文件。

# 1.6 总结

在本章中,我们介绍了原生API(Native API)提供的系统调用的调用方式,并在Visual Studio项目中添加了对phnt项目提供的原生API(Native API)定义的支持。在下一章中,我们将探讨原生API(Native API)使用的基础结构体、枚举和常量,并编写原生应用程序(Native Applications)。

Windows Native API编程 专栏说明
第2章 原生API(Native API)基础

← Windows Native API编程 专栏说明 第2章 原生API(Native API)基础→

最近更新
01
C++语言面试问题集锦 目录与说明
03-27
02
第四章 Lambda函数
03-27
03
第二章 关键字static及其不同用法
03-27
更多文章>
Copyright © 2024-2026 沪ICP备2023015129号 张小方 版权所有
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式