2507rust,rust写驱动
原文
Rust
语言生态系统每天都在增长,它的受欢迎程度越来越高
,这是有充分理由的.它是唯一在编译时
提供内存和并发安全性
的主流语言
,有强大而丰富
的构建系统
(cargo
)和越来越多
的包(箱
).
操作者的日常驱动
仍是C++
,因为大部分
工作都是窗口C
和COMAPI
很容易使用的低级系统和内核编程
的.然而,Rust
是一个系统语言
,即可,或至少可在与C/C++
相同场景中应用.
主要问题
是按Rust
期望的冗长
转换C类型
时.该"冗长"可通过适当的包装器和宏
来缓解.
我决定试编写简单有用的WDM
驱动.它是我在书中说明的"Booster"
驱动的Rust
版本(窗口
内核编程),它允许按任何值
更改任何线程的优先级
.
开始
要准备生成驱动,请查阅这里,但基本上应该安装WDK
(正常或EWDK
).此外,文档需要安装LLVM
,才能访问Clang编译器
,这里,这里.
如果想尝试以下操作,我假设你已安装了这些.
可从创建一个新的Rust
库项目开始(因为驱动在技术上是一个,在内核空间
中加载的DLL
):
cargo new -lib booster
可在VSCode
中打开booster
目录,然后开始编码.首先,为了成功编译和链接实际代码,需要做一些准备工作.
需要一个告诉货物在CRT
静态链接的build.rs
文件.在根助推器目录
添加build.rs
文件,代码如下:
fn main() -> Result<(), wdk_build::ConfigError> {std::env::set_var("CARGO_CFG_TARGET_FEATURE", "crtstatic");wdk_build::configure_wdk_binary_build()
}
接着,需要编辑cargo.toml
并添加各种依赖
.以下是最低依赖:
[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
中取实际代码了.
代码
首先删除标准库
,因为内核中没有它:
#![no_std]
接着,添加一些use
语句以减少代码的冗长性
:
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
(和串
)就可正常工作
了:
#[global_allocator]
static GLOBAL_ALLOCATOR: WdkAllocator = WdkAllocator;
这是与手动一样使用ExAllocatePool2
和ExFreePool
来管理分配的WDK包
提供的全局分配器
.
这里
这里
接着,添加两个extern包
以支持分配器
和一个恐慌处理器
的支持,因为不包括标准库
,这是必须提供的另一件事
.Cargo.toml
有一个,如果任何代码出现恐慌
,则中止驱动(使系统崩溃)的设置
:
extern 包 wdk_panic;
extern 包 alloc;
现在该编写实际的代码
了.从DriverEntry
开始,它是任何窗口
内核驱动的入口:
#[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!更简单
:
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
:
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"
的设备对象
:
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
串:
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
}
比想要的更复杂
,但可按编写一次
然后可随处使用
的函数对待它.
如果创建设备
失败,将返回失败状态
:
if !nt_success(status) {println!("Error creating device 0x{:X}", status);return status;
}
nt_success
类似WDK头文件
提供的NT_SUCCESS
宏.
接着,将创建一个符号链接
,这样标准CreateFile
调用可打开设备句柄
:
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
),设置驱动卸载例程
及要支持的主函数
:
(*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<>
类型来指示有回调
.
卸载例程
如下:
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
:
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处理器
,它会更改实际的线程优先级
.首先,将声明一个表示驱动请求
的结构:
#[repr(C)]
struct ThreadData {pub thread_id: u32,pub priority: i32,
}
使用repr(C)
很重要,以确保字段按C/C++
一样在内存中布局.这允许非Rust
客户与驱动
通信.事实上,我将使用我有的C++
版本驱动的C++
客户测试驱动
.
驱动接受要更改的线程ID
和要使用的优先级
.现在可从boost_write
开始:
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
中取数据
指针.这是客户缓冲
的内核副本
.接着,检查错误:
let status;
loop {if data == null_mut() {status = STATUS_INVALID_PARAMETER;break;}if (*data).priority < 1 || (*data).priority > 31 {status = STATUS_INVALID_PARAMETER;break;}
循环
语句创建一个可通过中断退出
的无限块.一旦验证了优先级
在范围内,就可找线程对象
了:
let mut thread = null_mut();
status = PsLookupThreadByThreadId(((*data).thread_id) as *mut c_void, &mut thread);
if !nt_success(status) {break;
}
使用的是PsLookupThreadByThreadId
.如果失败,则表明可能没有线程ID
,会退出.剩下就是设置优先级
并以拥有的任何状态
完成请求:
这里
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
.
所以需要在部署前手动签名
.可从项目的根目录
中使用以下内容:
signtool sign /n wdk /fd sha256 target\debug\booster.dll
/n wdk
使用一般由VS
在生成驱动时自动创建
的WDK
测试证书.我只是抓住存储中第一个以"wdk"
开头的并使用它.
文件扩展名
,是一个DLL
,当前无法在货物构建
过程中自动更改
它.如果使用INF/INX
,则会按SYS
更改文件扩展名
.
文件扩展名
并不重要,可手动重命名它
,或直接改成DLL
.
安装驱动
对软件驱动
可按"正常"
方式安装生成的文件
,如在有测试登录的计算机
上使用sc.exe
工具(从提升的命令窗口
).然后可用sc start
在系统中加载驱动:
sc.exe sc create booster type= kernel binPath= c:\path_to_driver_file
sc.exe start booster
测试驱动
我使用了一个现有的与驱动
通信,并期望传递正确
的结构的C++
应用.它像这样:
#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");elseprintf("Error (%u)\n", GetLastError());CloseHandle(hDevice);return 0;
}
结论
可以用Rust
编写内核驱动
.WDK
仓库的版本为0.3
,还有很长的路要走.
为了在该空间中充分利用Rust
,应该创建安全包装器
,这样代码不那么冗长,没有不安全的块
,并享受Rust
可提供的好处.
注意,我可能在该简单的实现中遗漏了一些包装器.
可在这里,找到更多KMDF
的Rust
驱动示例.
文章的代码.