Rust语言生态系统每天都在增长,它的受欢迎程度越来越高,这是有充分理由的.它是唯一在编译时提供内存和并发安全性的主流语言,有强大而丰富的构建系统(cargo)和越来越多的包(箱).
操作者的日常驱动仍是C++,因为大部分工作都是窗口C和COMAPI很容易使用的低级系统和内核编程的.然而,Rust是一个系统语言,即可,或至少可在与C/C++相同场景中应用.
主要问题是按Rust期望的冗长转换C类型时.该"冗长"可通过适当的包装器和宏来缓解.
我决定试编写简单有用的WDM驱动.它是我在书中说明的"Booster"驱动的Rust版本(窗口内核编程),它允许按任何值更改任何线程的优先级.
开始
要准备生成驱动,请查阅这里,但基本上应该安装WDK(正常或EWDK).此外,文档需要安装LLVM,才能访问Clang编译器,这里,这里.
如果想尝试以下操作,我假设你已安装了这些.
可从创建一个新的Rust库项目开始(因为驱动在技术上是一个,在内核空间中加载的DLL):
cpp
cargo new -lib booster
可在VSCode中打开booster目录,然后开始编码.首先,为了成功编译和链接实际代码,需要做一些准备工作.
需要一个告诉货物在CRT静态链接的build.rs文件.在根助推器目录添加build.rs文件,代码如下:
cpp
fn main() -> Result<(), wdk_build::ConfigError> {
std::env::set_var("CARGO_CFG_TARGET_FEATURE", "crtstatic");
wdk_build::configure_wdk_binary_build()
}
接着,需要编辑cargo.toml并添加各种依赖.以下是最低依赖:
cpp
[package]
name = "booster"
version = "0.1.0"
edition = "2021"
[package.metadata.wdk.drivermodel]
drivertype = "WDM"
[lib]
cratetype = ["cdylib"]
test = false
[builddependencies]
wdkbuild = "0.3.0"
[dependencies]
wdk = "0.3.0"
wdkmacros = "0.3.0"
wdkalloc = "0.3.0"
wdkpanic = "0.3.0"
wdksys = "0.3.0"
[features]
default = []
nightly = ["wdk/nightly", "wdksys/nightly"]
[profile.dev]
panic = "abort"
lto = true
[profile.release]
panic = "abort"
lto = true
重要的部分是WDK依赖.该在lib.rs中取实际代码了.
代码
首先删除标准库,因为内核中没有它:
cpp
#![no_std]
接着,添加一些use语句以减少代码的冗长性:
cpp
use core::ffi::c_void;
use core::ptr::null_mut;
use alloc::vec::Vec;
use alloc::{slice, string::String};
use wdk::*;
use wdk_alloc::WdkAllocator;
use wdk_sys::ntddk::*;
use wdk_sys::*;
wdk_sys包提供低级互操作内核函数.WDK包提供更高级的包装器.alloc::vec::Vec是有趣的.因为不能使用标准库,你或会认为std::vec::Vec<>不可用,这是正确的.
但是,Vec实际上是在,可在标准库之外使用的叫alloc::vec的较低级模块中定义的.这工作,因为Vec的唯一要求是有一个分配和释放内存的方法.
Rust通过任何人都可提供的全局分配器对象公开了这一方面.因为没有标准库,因此没有全局分配器,因此必须提供一个.
然后,Vec(和串)就可正常工作了:
cpp
#[global_allocator]
static GLOBAL_ALLOCATOR: WdkAllocator = WdkAllocator;
这是与手动一样使用ExAllocatePool2和ExFreePool来管理分配的WDK包提供的全局分配器.
这里
这里
接着,添加两个extern包以支持分配器和一个恐慌处理器的支持,因为不包括标准库,这是必须提供的另一件事.Cargo.toml有一个,如果任何代码出现恐慌,则中止驱动(使系统崩溃)的设置:
cpp
extern 包 wdk_panic;
extern 包 alloc;
现在该编写实际的代码了.从DriverEntry开始,它是任何窗口内核驱动的入口:
cpp
#[export_name = "DriverEntry"]
pub unsafe extern "system" fn driver_entry(
driver: &mut DRIVER_OBJECT,
registry_path: PUNICODE_STRING,
) -> NTSTATUS {
熟悉内核驱动的人会识别函数签名.函数名,driver_entry符合函数的snake_case(蛇形)的Rust命名约定,但因为链接器会查找DriverEntry,因此使用export_name(导出名)属性装饰函数.
如果愿意,可用DriverEntry并忽略或禁止编译器的警告.
与使用C/C++一样,可用熟悉的调用DbgPrint,重新实现的println!宏.注意,你仍可调用DbgPrint,但println!更简单:
cpp
println!("DriverEntry from Rust! {:p}", &driver);
let registry_path = unicode_to_string(registry_path);
println!("Registry Path: {}", registry_path);
可惜,它似乎是println!还不支持UNICODE_STRING,所以可编写叫unicode_to_string的函数来按普通的Rust串转换UNICODE_STRING:
cpp
fn unicode_to_string(str: PCUNICODE_STRING) -> String {
String::from_utf16_lossy(unsafe {
slice::from_raw_parts((*str).Buffer, (*str).Length as usize / 2)
})
}
回到DriverEntry,下个业务的顺序是创建叫"\Device\Booster"的设备对象:
cpp
let mut dev = null_mut();
let mut dev_name = UNICODE_STRING::default();
string_to_ustring("\\Device\\Booster", &mut dev_name);
let status = IoCreateDevice(
driver,
0,
&mut dev_name,
FILE_DEVICE_UNKNOWN,
0,
0u8,
&mut dev,
);
string_to_ustring函数按UNICODE_STRING转换Rust串:
cpp
fn string_to_ustring(s: &str, uc: &mut UNICODE_STRING) -> Vec<u16> {
let mut wstring: Vec<_> = s.encode_utf16().collect();
uc.Length = wstring.len() as u16 * 2;
uc.MaximumLength = wstring.len() as u16 * 2;
uc.Buffer = wstring.as_mut_ptr();
wstring
}
比想要的更复杂,但可按编写一次然后可随处使用的函数对待它.
如果创建设备失败,将返回失败状态:
cpp
if !nt_success(status) {
println!("Error creating device 0x{:X}", status);
return status;
}
nt_success类似WDK头文件提供的NT_SUCCESS宏.
接着,将创建一个符号链接,这样标准CreateFile调用可打开设备句柄:
cpp
let mut sym_name = UNICODE_STRING::default();
let _ = string_to_ustring("\\??\\Booster", &mut sym_name);
let status = IoCreateSymbolicLink(&mut sym_name, &mut dev_name);
if !nt_success(status) {
println!("Error creating symbolic link 0x{:X}", status);
IoDeleteDevice(dev);
return status;
}
剩下就是初化设备对象,以支持缓冲I/O(为了简单,将使用IRP_MJ_WRITE),设置驱动卸载例程及要支持的主函数:
cpp
(*dev).Flags |= DO_BUFFERED_IO;
driver.DriverUnload = Some(boost_unload);
driver.MajorFunction[IRP_MJ_CREATE as usize] = Some(boost_create_close);
driver.MajorFunction[IRP_MJ_CLOSE as usize] = Some(boost_create_close);
driver.MajorFunction[IRP_MJ_WRITE as usize] = Some(boost_write);
STATUS_SUCCESS
}
注意使用Rust的Option<>类型来指示有回调.
卸载例程如下:
cpp
unsafe extern "C" fn boost_unload(driver: *mut DRIVER_OBJECT) {
let mut sym_name = UNICODE_STRING::default();
string_to_ustring("\\??\\Booster", &mut sym_name);
let _ = IoDeleteSymbolicLink(&mut sym_name);
IoDeleteDevice((*driver).DeviceObject);
}
与普通的内核驱动一样,只调用IoDeleteSymbolicLink和IoDeleteDevice了.
处理请求
有三个请求类型需要处理:IRP_MJ_CREATE,IRP_MJ_CLOSE和IRP_MJ_WRITE.创建和关闭是很简单的,只需成功完成IRP:
cpp
unsafe extern "C" fn boost_create_close(_device: *mut DEVICE_OBJECT, irp: *mut IRP) -> NTSTATUS {
(*irp).IoStatus.__bindgen_anon_1.Status = STATUS_SUCCESS;
(*irp).IoStatus.Information = 0;
IofCompleteRequest(irp, 0);
STATUS_SUCCESS
}
IoStatus是一个但用包含状态和Pointer的联定义的IO_STATUS_BLOCK.这似乎是错误的,因为Information应该在带Pointer(而不是状态)的联中.
代码通过"自动生成"的联访问状态成员,这很难看.绝对是需要进一步研究的东西.但它管用.
真正有趣的函数是IRP_MJ_WRITE处理器,它会更改实际的线程优先级.首先,将声明一个表示驱动请求的结构:
cpp
#[repr(C)]
struct ThreadData {
pub thread_id: u32,
pub priority: i32,
}
使用repr(C)很重要,以确保字段按C/C++一样在内存中布局.这允许非Rust客户与驱动通信.事实上,我将使用我有的C++版本驱动的C++客户测试驱动.
驱动接受要更改的线程ID和要使用的优先级.现在可从boost_write开始:
cpp
unsafe extern "C" fn boost_write(_device: *mut DEVICE_OBJECT, irp: *mut IRP) -> NTSTATUS {
let data = (*irp).AssociatedIrp.SystemBuffer as *const ThreadData;
首先,因为请求缓冲I/O支持,从IRP中的SystemBuffer中取数据指针.这是客户缓冲的内核副本.接着,检查错误:
cpp
let status;
loop {
if data == null_mut() {
status = STATUS_INVALID_PARAMETER;
break;
}
if (*data).priority < 1 || (*data).priority > 31 {
status = STATUS_INVALID_PARAMETER;
break;
}
循环语句创建一个可通过中断退出的无限块.一旦验证了优先级在范围内,就可找线程对象了:
cpp
let mut thread = null_mut();
status = PsLookupThreadByThreadId(((*data).thread_id) as *mut c_void, &mut thread);
if !nt_success(status) {
break;
}
使用的是PsLookupThreadByThreadId.如果失败,则表明可能没有线程ID,会退出.剩下就是设置优先级并以拥有的任何状态完成请求:
这里
cpp
KeSetPriorityThread(thread, (*data).priority);
ObfDereferenceObject(thread as *mut c_void);
break;
}
(*irp).IoStatus.__bindgen_anon_1.Status = status;
(*irp).IoStatus.Information = 0;
IofCompleteRequest(irp, 0);
status
}
就这样!
只剩下签名驱动.如果有INF或INX文件,则箱似乎支持签名驱动,但此驱动未使用INF.
所以需要在部署前手动签名.可从项目的根目录中使用以下内容:
cpp
signtool sign /n wdk /fd sha256 target\debug\booster.dll
/n wdk使用一般由VS在生成驱动时自动创建的WDK测试证书.我只是抓住存储中第一个以"wdk"开头的并使用它.
文件扩展名,是一个DLL,当前无法在货物构建过程中自动更改它.如果使用INF/INX,则会按SYS更改文件扩展名.
文件扩展名并不重要,可手动重命名它,或直接改成DLL.
安装驱动
对软件驱动可按"正常"方式安装生成的文件,如在有测试登录的计算机上使用sc.exe工具(从提升的命令窗口).然后可用sc start在系统中加载驱动:
cpp
sc.exe sc create booster type= kernel binPath= c:\path_to_driver_file
sc.exe start booster
测试驱动
我使用了一个现有的与驱动通信,并期望传递正确的结构的C++应用.它像这样:
cpp
#include <Windows.h>
#include <stdio.h>
struct ThreadData {
int ThreadId;
int Priority;
};
int main(int argc, const char* argv[]) {
if (argc < 3) {
printf("Usage: boost <tid> <priority>\n");
return 0;
}
int tid = atoi(argv[1]);
int priority = atoi(argv[2]);
HANDLE hDevice = CreateFile(L"\\\\.\\Booster",
GENERIC_WRITE, 0, nullptr, OPEN_EXISTING, 0,
nullptr);
if (hDevice == INVALID_HANDLE_VALUE) {
printf("Failed in CreateFile: %u\n", GetLastError());
return 1;
}
ThreadData data;
data.ThreadId = tid;
data.Priority = priority;
DWORD ret;
if (WriteFile(hDevice, &data, sizeof(data),
&ret, nullptr))
printf("Success!!\n");
else
printf("Error (%u)\n", GetLastError());
CloseHandle(hDevice);
return 0;
}
结论
可以用Rust编写内核驱动.WDK仓库的版本为0.3,还有很长的路要走.
为了在该空间中充分利用Rust,应该创建安全包装器,这样代码不那么冗长,没有不安全的块,并享受Rust可提供的好处.
注意,我可能在该简单的实现中遗漏了一些包装器.