Power Off Driver
We will try to build a driver that powers off QEMU.
ISA Debug Exit​
QEMU provides a virual peripheral called ISA Debug Exit that maps an IO port. Writing a numeric value to this port will power off QEMU and return the value as an error code.
Attaching this device to QEMU is done by adding -device isa-debug-exit,iobase=0x10f4,iosize=0x04 to the command line.
In this example, the debug device is mapped on port 0x10f4 and is 4 bytes long.
Simple Power Off Driver​
The simplest driver that we can write is one that power off QEMU when it is loaded. We need to:
- reserve the port
- write to it using x86 assembly
Reserving the port region​
The Kernel Rust API does not have (yet) and safe abstraction to reserve a port. This means that we have to use the C binding directly,
unsafe { bindings::request_region(start, len, name.as_char_ptr()) }
startis the port numberlenis the gthe length of the portnameis a name that will be displayed in/proc/ioportsand is a C stringchar*
The function return NULL if the port mapping fails. In Rust, we can use the .is_null() function
to check if the port reservation worked.
Writing to the port​
Writing to the IO port is done using inline assembly. There are two possible syntaxes:
Kernel AT&T Syntax​
// kernel API
use kernel::asm;
unsafe {
asm!("
outb %al, %dx
";
in("dx") port as u16,
in("al") value as u8
);
}
Intel Sytnax​
// core API
use core::arch::asm;
unsafe {
asm!("
out dx, al
",
in("dx") port as u16,
in("al") value as u8
);
}
The kernel does not forbid writing to a port without reserving it, but this is bad practice,
as we might override another driver's ports. if we cannot reserve a port, we just back off
and fail to load the driver returning a EBUSY error.
Realeasing the port reagion​
If we have reserved a port region, we must make sure to release it when we do not need it anymore or when we unload the driver.
unsafe { bindings::release_region(start, len); }
Exercises​
- Write an empty module called
Reset - Reserve the
0x10f4port with 4 bytes long in theModule::initfunction.
- Print the contents of
/proc/ioportsto see if your port has been reserved - Try reserving a port that already exists and fail to initialize the module
- Release the
0x10f4port with 4 bytes long in theDrop::dropfunction.
Bonus​
-
Automate the release of the IOPort by using a structure called
IoPortRegionthat reserves the port in itsnew. Implement theDroptrait forIoPortRegionand release the IOPort. Store the structure in theResetdriver structure so that it gets dropped when the driver is unloaded. -
Use a
Taskto schedule a reset after a certain amount of time defined in a module parameter. Use a workqueue.
/// Stores all the possible tasks (Work)
#[pin_data]
struct DeferredWork {
module: &'static ThisModule,
#[pin]
work: Work<DeferredWork>,
}
/// Initializes the possible tasks
impl DeferredWork {
fn new(module: &'static ThisModule) -> Result<Arc<DeferredWork>> {
Arc::pin_init(
pin_init!(DeferredWork {
module,
work <- new_work!("DeferredWork::work"),
}),
GFP_KERNEL,
)
}
}
/// Must be implemented
impl_has_work! {
impl HasWork<Self> for DeferredWork { self.work }
}
/// Implement the task
impl WorkItem for DeferredWork {
type Pointer = Arc<DeferredWork>;
fn run(_this: Self::Pointer) {
// ...
}
}
/// Setup the work
fn setup_work() {
match DeferredWork::new(module) {
Ok(work) => {
let _ = workqueue::system().enqueue(work);
}
Err(err) => {
pr_warn!("Failed to setup work: {:?}", err);
}
}
}