In the previous post, I’ve shown how to write a minimal, but functional, Projected File System provider using C++. I also semi-promised to write a version of that provider in Rust. I thought we should start small, by implementing a command line tool I wrote years ago called objdir. Its purpose is to be a “command line” version of a simplified WinObj from Sysinternals. It should be able to list objects (name and type) within a given object manager namespace directory. Here are a couple of examples:
在上一篇文章中,我展示了如何使用 C++ 编写一个最小但实用的投影文件系统提供程序。我还半承诺用 Rust 编写该提供程序的一个版本。我认为我们应该从小处开始,通过实现我几年前编写的一个名为 objdir 的命令行工具。它的目的是成为 Sysinternals 的简化 WinObj 的“命令行”版本。它应该能够列出给定对象管理器命名空间目录中的对象(名称和类型)。这里有几个例子:
D:\>objdir \
PendingRenameMutex (Mutant)
ObjectTypes (Directory)
storqosfltport (FilterConnectionPort)
MicrosoftMalwareProtectionRemoteIoPortWD (FilterConnectionPort)
Container_Microsoft.OutlookForWindows_1.2024.214.400_x64__8wekyb3d8bbwe-S-1-5-21-3968166439-3083973779-398838822-1001 (Job)
MicrosoftDataLossPreventionPort (FilterConnectionPort)
SystemRoot (SymbolicLink)
exFAT (Device)
Sessions (Directory)
MicrosoftMalwareProtectionVeryLowIoPortWD (FilterConnectionPort)
ArcName (Directory)
PrjFltPort (FilterConnectionPort)
WcifsPort (FilterConnectionPort)
...
D:\>objdir \kernelobjects
MemoryErrors (SymbolicLink)
LowNonPagedPoolCondition (Event)
Session1 (Session)
SuperfetchScenarioNotify (Event)
SuperfetchParametersChanged (Event)
PhysicalMemoryChange (SymbolicLink)
HighCommitCondition (SymbolicLink)
BcdSyncMutant (Mutant)
HighMemoryCondition (SymbolicLink)
HighNonPagedPoolCondition (Event)
MemoryPartition0 (Partition)
...
Since enumerating object manager directories is required for our ProjFS provider, once we implement objdir in Rust, we’ll have good starting point for implementing the full provider in Rust.
由于我们的 ProjFS 提供程序需要枚举对象管理器目录,因此一旦我们在 Rust 中实现了 objdir,我们就有了在 Rust 中实现完整提供程序的良好起点。
This post assumes you are familiar with the fundamentals of Rust. Even if you’re not, the code should still be fairly understandable, as we’re mostly going to use unsafe rust to do the real work.
这篇文章假设您熟悉 Rust 的基础知识。即使你不是,代码仍然应该是相当容易理解的,因为我们主要使用不安全的 Rust 来完成真正的工作。
Unsafe Rust 不安全的生锈
One of the main selling points of Rust is its safety – memory and concurrency safety guaranteed at compile time. However, there are cases where access is needed that cannot be checked by the Rust compiler, such as the need to call external C functions, such as OS APIs. Rust allows this by using unsafe
blocks or functions. Within unsafe blocks, certain operations are allowed which are normally forbidden; it’s up to the developer to make sure the invariants assumed by Rust are not violated – essentially making sure nothing leaks, or otherwise misused.
Rust 的主要卖点之一是它的安全性——在编译时保证内存和并发安全。然而,在某些情况下,Rust 编译器无法检查需要访问的情况,例如需要调用外部 C 函数,例如操作系统 API。 Rust 通过使用 unsafe
块或函数来实现这一点。在不安全块内,允许某些通常被禁止的操作;开发人员有责任确保 Rust 假设的不变量不被违反——本质上是确保没有泄漏或以其他方式被滥用。
The Rust standard library provides some support for calling C functions, mostly in the std::ffi
module (FFI=Foreign Function Interface). This is pretty bare bones, providing a C-string class, for example. That’s not rich enough, unfortunately. First, strings in Windows are mostly UTF-16, which is not the same as a classic C string, and not the same as the Rust standard String
type. More importantly, any C function that needs to be invoked must be properly exposed as an extern "C"
function, using the correct Rust types that provide the same binary representation as the C types.
Rust 标准库提供了一些对调用 C 函数的支持,主要在 std::ffi
模块中(FFI=Foreign Function Interface)。这是非常简单的,例如提供一个 C 字符串类。不幸的是,这还不够丰富。首先,Windows 中的字符串大多是 UTF-16,这与经典的 C 字符串不同,也与 Rust 标准 String
类型不同。更重要的是,任何需要调用的 C 函数都必须正确公开为 extern "C"
函数,使用提供与 C 类型相同的二进制表示形式的正确 Rust 类型。
Doing all this manually is a lot of error-prone, non-trivial, work. It only makes sense for simple and limited sets of functions. In our case, we need to use native APIs, like NtOpenDirectoryObject
and NtQueryDirectoryObject
. To simplify matters, there are crates available in crates.io (the master Rust crates repository) that already provide such declarations.
手动完成所有这些工作是一项非常容易出错且非常重要的工作。它只对简单且有限的函数集有意义。在我们的例子中,我们需要使用本机 API,例如 NtOpenDirectoryObject
和 NtQueryDirectoryObject
。为了简化问题,crates.io(Rust 主箱存储库)中的可用箱已经提供了此类声明。
Adding Dependencies 添加依赖项
Assuming you have Rust installed, open a command window and create a new project named objdir:
假设您安装了 Rust,打开命令窗口并创建一个名为 objdir 的新项目:
cargo new objdir
This will create a subdirectory named objdir, hosting the binary crate created. Now we can open cargo.toml (the manifest) and add dependencies for the following crates:
这将创建一个名为 objdir 的子目录,用于托管创建的二进制包。现在我们可以打开Cargo.toml(清单)并为以下板条箱添加依赖项:
[dependencies]
ntapi = "0.4"
winapi = { version = "0.3.9", features = [ "impl-default" ] }
winapi provides most of the Windows API declarations, but does not provide native APIs. ntapi provides those additional declarations, and in fact depends on winapi for some fundamental types (which we’ll need). The feature “impl-default” indicates we would like the implementations of the standard Rust Default
trait provided – we’ll need that later.
winapi 提供了大部分 Windows API 声明,但不提供本机 API。 ntapi 提供了这些额外的声明,实际上某些基本类型(我们需要)依赖于 winapi。功能“impl-default”表示我们希望提供标准 Rust Default
特征的实现 – 我们稍后会需要它。
The main Function 主要功能
The main
function is going to accept a command line argument to indicate the directory to enumerate. If no parameters are provided, we’ll assume the root directory is requested. Here is one way to get that directory:
main
函数将接受命令行参数来指示要枚举的目录。如果未提供参数,我们将假设请求根目录。这是获取该目录的一种方法:
let dir = std::env::args().skip(1).next().unwrap_or( "\\" .to_owned()); |
(Note that unfortunately the WordPress system I’m using to write this post has no syntax highlighting for Rust, the code might be uglier than expected; I’ve set it to C++).
(请注意,不幸的是,我用来写这篇文章的 WordPress 系统没有 Rust 语法突出显示,代码可能比预期的更难看;我已将其设置为 C++)。
The args
method returns an iterator. We skip the first item (the executable itself), and grab the next one with next
. It returns an Option<String>
, so we grab the string if there is one, or use a fixed backslash as the string.
args
方法返回一个迭代器。我们跳过第一项(可执行文件本身),并使用 next
获取下一项。它返回一个 Option<String>
,因此我们抓取字符串(如果有),或者使用固定的反斜杠作为字符串。
Next, we’ll call a helper function, enum_directory
that does the heavy lifting and get back a Result
where success is a vector of tuples, each containing the object’s name and type (Vec<(String, String)>
). Based on the result, we can display the results or report an error:
接下来,我们将调用一个辅助函数 enum_directory
来完成繁重的工作并返回 Result
,其中 success 是一个元组向量,每个元组包含对象的名称和类型( < b2>)。根据结果,我们可以显示结果或者报错:
let result = enum_directory(&dir); match result { Ok(objects) => { for (name, typename ) in &objects { println!( "{name} ({typename})" ); } println!( "{} objects." , objects.len()); }, Err(status) => println!( "Error: 0x{status:X}" ) }; |
That is it for the main
function.
这就是 main
函数。
Enumerating Objects 枚举对象
Since we need to use APIs defined within the winapi and ntapi crates, let’s bring them into scope for easier access at the top of the file:
由于我们需要使用 winapi 和 ntapi 包中定义的 API,因此我们将它们纳入范围,以便在文件顶部更轻松地访问:
use winapi::shared::ntdef::*; use ntapi::ntobapi::*; use ntapi::ntrtl::*; |
I’m using the “glob” operator (*) to make it easy to just use the function names directly without any prefix. Why these specific modules? Based on the APIs and types we’re going to need, these are where these are defined (check the documentation for these crates).
我使用“glob”运算符 (*) 来方便直接使用函数名称,无需任何前缀。为什么要使用这些特定模块?根据我们需要的 API 和类型,这些是定义的地方(查看这些 crate 的文档)。
enum_directory
is where the real is done. Here its declararion:
enum_directory
是真正完成的地方。这是它的声明:
fn enum_directory(dir: &str) -> Result<Vec<(String, String)>, NTSTATUS> { |
The function accepts a string slice and returns a Result
type, where the Ok
variant is a vector of tuples consisting of two standard Rust strings.
该函数接受字符串切片并返回 Result
类型,其中 Ok
变体是由两个标准 Rust 字符串组成的元组向量。
The following code follows the basic logic of the EnumDirectoryObjects
function from the ProjFS example in the previous post, without the capability of search or filter. We’ll add that when we work on the actual ProjFS project in a future post.
以下代码遵循上一篇文章中 ProjFS 示例中的 EnumDirectoryObjects
函数的基本逻辑,没有搜索或过滤功能。当我们在以后的文章中处理实际的 ProjFS 项目时,我们将添加这一点。
The first thing to do is open the given directory object with NtOpenDirectoryObject
. For that we need to prepare an OBJECT_ATTRIBUTES
and a UNICODE_STRING
. Here is what that looks like:
要做的第一件事是使用 NtOpenDirectoryObject
打开给定的目录对象。为此,我们需要准备一个 OBJECT_ATTRIBUTES
和一个 UNICODE_STRING
。看起来是这样的:
let mut items = vec![]; unsafe { let mut udir = UNICODE_STRING:: default (); let wdir = string_to_wstring(&dir); RtlInitUnicodeString(&mut udir, wdir.as_ptr()); let mut dir_attr = OBJECT_ATTRIBUTES:: default (); InitializeObjectAttributes(&mut dir_attr, &mut udir, OBJ_CASE_INSENSITIVE, NULL, NULL); |
We start by creating an empty vector to hold the results. We don’t need any type annotation because later in the code the compiler would have enough information to deduce it on its own. We then start an unsafe
block because we’re calling C APIs.
我们首先创建一个空向量来保存结果。我们不需要任何类型注释,因为稍后在代码中编译器将有足够的信息来自行推断它。然后我们启动一个 unsafe
块,因为我们正在调用 C API。
Next, we create a default-initialized UNICODE_STRING
and use a helper function to convert a Rust string slice to a UTF-16 string, usable by native APIs. We’ll see this string_to_wstring
helper function once we’re done with this one. The returned value is in fact a Vec<u16>
– an array of UTF-16 characters.
接下来,我们创建一个默认初始化的 UNICODE_STRING
并使用辅助函数将 Rust 字符串切片转换为可供本机 API 使用的 UTF-16 字符串。完成此操作后,我们将看到这个 string_to_wstring
辅助函数。返回的值实际上是一个 Vec<u16>
– UTF-16 字符数组。
The next step is to call RtlInitUnicodeString
, to initialize the UNICODE_STRING
based on the UTF-16 string we just received. Methods such as as_ptr
are necessary to make the Rust compiler happy. Finally, we create a default OBJECT_ATTRIBUTES
and initialize it with the udir
(the UTF-16 directory string). All the types and constants used are provided by the crates we’re using.
下一步是调用 RtlInitUnicodeString
,根据我们刚刚收到的 UTF-16 字符串初始化 UNICODE_STRING
。诸如 as_ptr
之类的方法对于让 Rust 编译器满意是必要的。最后,我们创建一个默认的 OBJECT_ATTRIBUTES
并使用 udir
(UTF-16 目录字符串)对其进行初始化。使用的所有类型和常量均由我们使用的包提供。
The next step is to actually open the directory, which could fail because of insufficient access or a directory that does not exist. In that case, we just return an error. Otherwise, we move to the next step:
下一步是实际打开目录,这可能会因为访问权限不足或目录不存在而失败。在这种情况下,我们只返回一个错误。否则,我们进入下一步:
let mut hdir: HANDLE = NULL; match NtOpenDirectoryObject(&mut hdir, DIRECTORY_QUERY, &mut dir_attr) { 0 => { // do real work... }, err => Err(err), } |
The NULL
here is just a type alias for the Rust provided C void pointer with a value of zero (*mut c_void
). We examine the NTSTATUS
returned using a match
expression: If it’s not zero (STATUS_SUCCESS
), it must be an error and we return an Err
object with the status. if it’s zero, we’re good to go. Now comes the real work.
这里的 NULL
只是 Rust 提供的 C void 指针的类型别名,其值为 0 ( *mut c_void
)。我们使用 match
表达式检查返回的 NTSTATUS
:如果它不为零 ( STATUS_SUCCESS
),则它一定是一个错误,我们返回 Err
We need to allocate a buffer to receive the object information in this directory and be prepared for the case the information is too big for the allocated buffer, so we may need to loop around to get the next “chunk” of data. This is how the NtQueryDirectoryObject
is expected to be used. Let’s allocate a buffer using the standard Vec<>
type and prepare some locals:
我们需要分配一个缓冲区来接收该目录中的对象信息,并为分配的缓冲区中的信息太大的情况做好准备,因此我们可能需要循环以获取下一个“数据块”。这就是 NtQueryDirectoryObject
的预期使用方式。让我们使用标准 Vec<>
类型分配一个缓冲区并准备一些局部变量:
const LEN: u32 = 1 << 16; let mut first = 1; let mut buffer: Vec<u8> = Vec::with_capacity(LEN as usize); let mut index = 0u32; let mut size: u32 = 0; |
We’re allocating 64KB, but could have chosen any number. Now the loop:
我们分配 64KB,但可以选择任何数字。现在循环:
loop { let start = index; if NtQueryDirectoryObject(hdir, buffer.as_mut_ptr().cast(), LEN, 0, first, &mut index, &mut size) < 0 { break ; } first = 0; let mut obuffer = buffer.as_ptr() as * const OBJECT_DIRECTORY_INFORMATION; for _ in 0..index - start { let item = *obuffer; let name = String::from_utf16_lossy(std::slice::from_raw_parts(item.Name.Buffer, (item.Name.Length / 2) as usize)); let typename = String::from_utf16_lossy(std::slice::from_raw_parts(item.TypeName.Buffer, (item.TypeName.Length / 2) as usize)); items.push((name, typename )); obuffer = obuffer.add(1); } } Ok(items) |
There are quite a few things going on here. if NtQueryDirectoryObject
fails, we break out of the loop
. This happens when there are is no more information to give. If there is data, buffer
is cast to a OBJECT_DIRECTORY_INFORMATION
pointer, and we can loop around on the items that were returned. start
is used to keep track of the previous number of items delivered. first
is 1 (true) the first time through the loop to force the NtQueryDirectoryObject
to start from the beginning.
这里发生了很多事情。如果 NtQueryDirectoryObject
失败,我们就会突破 loop
。当没有更多信息可提供时,就会发生这种情况。如果有数据, buffer
会被转换为 OBJECT_DIRECTORY_INFORMATION
指针,我们可以循环返回的项目。 start
用于跟踪先前交付的物品数量。第一次循环时, first
为 1(真),强制 NtQueryDirectoryObject
从头开始。
Once we have an item (item
), its two members are extracted. item
is of type OBJECT_DIRECTORY_INFORMATION
and has two members: Name
and TypeName
(both UNICODE_STRING
). Since we want to return standard Rust strings (which, by the way, are UTF-8 encoded), we must convert the UNICODE_STRING
s to Rust strings. String::from_utf16_lossy
performs such a conversion, but we must specify the number of characters, because a UNICODE_STRING
does not have to be NULL
-terminated. The trick here is std::slice::from_raw_parts
that can have a length, which is half of the number of bytes (Length
member in UNICODE_STRING
).
一旦我们有了一个项目( item
),它的两个成员就会被提取。 item
属于 OBJECT_DIRECTORY_INFORMATION
类型,并且有两个成员: Name
和 TypeName
(都是 UNICODE_STRING
)。由于我们想要返回标准 Rust 字符串(顺便说一句,它们是 UTF-8 编码的),因此我们必须将 UNICODE_STRING
转换为 Rust 字符串。 String::from_utf16_lossy
执行此类转换,但我们必须指定字符数,因为 UNICODE_STRING
不必以 NULL
结尾。这里的技巧是 std::slice::from_raw_parts
可以有一个长度,它是字节数的一半( Length
成员在 UNICODE_STRING
中)。
Finally, Vec<>.push
is called to add the tuple (name, typename)
to the vector. This is what allows the compiler to infer the vector type. Once we exit the loop, the Ok
variant of Result<>
is returned with the vector.
最后,调用 Vec<>.push
将元组 (name, typename)
添加到向量中。这就是编译器能够推断向量类型的原因。一旦我们退出循环, Result<>
的 Ok
变体就会与向量一起返回。
The last function used is the helper to convert a Rust string slice to a UTF-16 null-terminated string:
最后使用的函数是将 Rust 字符串切片转换为 UTF-16 以 null 结尾的字符串的帮助程序:
fn string_to_wstring(s: &str) -> Vec<u16> { let mut wstring: Vec<_> = s.encode_utf16().collect(); wstring.push(0); // null terminator wstring } |
And that is it. The Rust version of objdir is functional.
就是这样。 Rust 版本的 objdir 可以正常运行。
The full source is at zodiacon/objdir-rs: Rust version of the objdir tool (github.com)
完整源代码位于 zodiacon/objdir-rs:Rust 版本的 objdir 工具 (github.com)
If you want to know more about Rust, consider signing up for my upcoming Rust masterclass programming.
如果您想了解更多有关 Rust 的信息,请考虑报名参加我即将推出的 Rust 大师班编程。
原文始发于Pavel Yosifovich:ObjDir – Rust Version