第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)等,所有这些操作都需要内核介入。

图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
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
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所示。

图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
);
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;
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;
}
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
尽管只显示了一个错误,但实际上存在两个问题。第一个问题是链接器不知道NtQuerySystemInformation的实现位置。我们知道它位于NtDll.dll中,但链接器并不知道,因此我们需要告知链接器。
一种方法是将Visual Studio安装时提供的导入库(import library)ntdll.lib添加到链接器的“输入”节点中(可在此添加额外的导入库),如图1-3所示。

图1-3:Visual Studio中链接器的“输入”节点
注:最好在图1-3所示对话框的顶部选择“所有配置”和“所有平台”,这样就无需在每个配置和平台中重复设置。
另一种实现相同效果的方法是在源代码中使用#pragma指令添加相同的导入库:
#pragma comment(lib, "ntdll")
即使进行了上述修改(无论使用哪种方法),我们仍然会遇到相同的链接器错误。问题在于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
);
2
3
4
5
6
此时,所有内容都应能成功链接,运行应用程序后会产生类似以下的输出:
Page size: 4096 bytes
Processors: 16
Physical pages: 33346913
Lowest Physical page: 1
Highest Physical page: 34142207
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;
}
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
);
// ...
}
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
);
// ...
}
}
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
...
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个导出函数。

图1-5:TotalPE显示NtDll.Dll的导出函数
获取函数原型、结构体和枚举的一种方法是从winsiderss/phnt (opens new window)项目提供的头文件中复制所需内容。不过有一个问题:某些定义是许多函数共用的,因此你必须逐一查找并复制每个定义,以确保所有内容都能正常编译。
另一种方法是克隆上述代码仓库(或将其作为子模块添加),并按照phnt主页上的说明进行使用。基本上,应使用以下两个头文件,而无需包含<Windows.h>:
#include <phnt_windows.h>
#include <phnt.h>
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
假设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;
}
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;
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)。