Skip to main content

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​

note

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()) }
  • start is the port number
  • len is the gthe length of the port
  • name is a name that will be displayed in /proc/ioports and is a C string char*

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
);
}
warning

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​

  1. Write an empty module called Reset
  2. Reserve the 0x10f4 port with 4 bytes long in the Module::init function.
  • Print the contents of /proc/ioports to see if your port has been reserved
  • Try reserving a port that already exists and fail to initialize the module
  1. Release the 0x10f4 port with 4 bytes long in the Drop::drop function.

Bonus​

  1. Automate the release of the IOPort by using a structure called IoPortRegion that reserves the port in its new. Implement the Drop trait for IoPortRegion and release the IOPort. Store the structure in the Reset driver structure so that it gets dropped when the driver is unloaded.

  2. Use a Task to 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);
}
}
}