Skip to content

Commit

Permalink
feat(x86_64): debug commands to print CPUID & MSRs (#492)
Browse files Browse the repository at this point in the history
This commit adds kernel debug shell commands `dump arch msr` and dump
arch cpuid`, to print the value of x86 model-specific registers and
CPUID leaves, respectively. I also added a bunch of argument-parsing
utilities to the kernel debug shell.

Closes #491 

![image](https://github.com/user-attachments/assets/815118d5-90f1-4a6f-ac76-30290f870cac)
  • Loading branch information
hawkw authored Dec 29, 2024
1 parent a6f9ba0 commit 8c2cc10
Show file tree
Hide file tree
Showing 2 changed files with 260 additions and 13 deletions.
73 changes: 72 additions & 1 deletion src/arch/x86_64/shell.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
use crate::shell::Command;
use crate::shell::{Command, NumberFormat};
use mycelium_util::fmt;

pub const DUMP_ARCH: Command = Command::new("arch")
.with_help("dump architecture-specific structures")
Expand All @@ -17,4 +18,74 @@ pub const DUMP_ARCH: Command = Command::new("arch")
tracing::info!(IDT = ?idt);
Ok(())
}),
Command::new("msr")
.with_help(
"print the value of the specified model-specific register (MSR)\n \
MSR_NUM: the MSR number in hexadecimal or binary\n \
-f, --fmt <hex|bin|dec>: format the value of the MSR in hexadecimal, \
decimal, or binary.",
)
.with_usage("[-f|--fmt] <MSR_NUM>")
.with_fn(|mut ctx| {
let fmt = ctx
.parse_optional_flag::<NumberFormat>(&["-f", "--fmt"])?
.unwrap_or(NumberFormat::Hex);
let num = ctx.parse_u32_hex_or_dec("<MSR_NUM>")?;

let msr = hal_x86_64::cpu::msr::Msr::try_new(num).ok_or_else(|| {
ctx.other_error(
"CPU does not support model-specific registers (must be pre-pentium...)",
)
})?;

let val = msr.read_raw();
match fmt {
NumberFormat::Binary => tracing::info!("MSR {num:#x} = {val:#b}"),
NumberFormat::Decimal => tracing::info!("MSR {num:#x} = {val}"),
NumberFormat::Hex => tracing::info!("MSR {num:#x} = {val:#x}"),
}
Ok(())
}),
Command::new("cpuid")
.with_help(
"print the value of the specified CPUID leaf (and subleaf)\n \
LEAF: the CPUID leaf number in hexadecimal or binary\n \
SUBLEAF: the CPUID subleaf number in hexadecimal or binary\n \
-f, --fmt <hex|bin|dec>: format the values of the CPUID registers in hexadecimal, \
decimal, or binary.",
)
.with_usage("[-f|--fmt] <LEAF> [SUBLEAF]")
.with_fn(|mut ctx| {
use core::arch::x86_64::{CpuidResult, __cpuid_count};
let fmt = ctx
.parse_optional_flag::<NumberFormat>(&["-f", "--fmt"])?
.unwrap_or(NumberFormat::Hex);
let leaf = ctx.parse_u32_hex_or_dec("<LEAF>")?;
let subleaf = ctx.parse_optional_u32_hex_or_dec("[SUBLEAF]")?.unwrap_or(0);

let CpuidResult { eax, ebx, ecx, edx } = unsafe { __cpuid_count(leaf, subleaf) };
match fmt {
NumberFormat::Binary => tracing::info!(
target: "shell",
eax = fmt::bin(eax),
ebx = fmt::bin(ebx),
ecx = fmt::bin(ecx),
edx = fmt::bin(edx),
"CPUID {leaf:#x}:{subleaf:x}",
),
NumberFormat::Decimal => tracing::info!(
target: "shell", eax, ebx, ecx, edx,
"CPUID {leaf:#x}:{subleaf:x}",
),
NumberFormat::Hex => tracing::info!(
target: "shell",
eax = fmt::hex(eax),
ebx = fmt::hex(ebx),
ecx = fmt::hex(ecx),
edx = fmt::hex(edx),
"CPUID {leaf:#x}:{subleaf:x}",
),
}
Ok(())
}),
]);
200 changes: 188 additions & 12 deletions src/shell.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
//! purposes.
//!
use crate::rt;
use core::str::FromStr;
use mycelium_util::fmt::{self, Write};

/// Defines a shell command, including its name, help text, and how the command
Expand All @@ -21,23 +22,30 @@ pub struct Error<'a> {
kind: ErrorKind<'a>,
}

pub type Result<'a> = core::result::Result<(), Error<'a>>;
pub type CmdResult<'a> = core::result::Result<(), Error<'a>>;

pub trait Run: Send + Sync {
fn run<'ctx>(&'ctx self, ctx: Context<'ctx>) -> Result<'ctx>;
fn run<'ctx>(&'ctx self, ctx: Context<'ctx>) -> CmdResult<'ctx>;
}

#[derive(Debug)]
enum ErrorKind<'a> {
UnknownCommand(&'a [Command<'a>]),

SubcommandRequired(&'a [Command<'a>]),
InvalidArguments { help: &'a str, arg: &'a str },
InvalidArguments {
help: &'a str,
arg: &'a str,
flag: Option<&'a str>,
},
FlagRequired {
flags: &'a [&'a str],
},
Other(&'static str),
}

enum RunKind<'a> {
Fn(fn(Context<'_>) -> Result<'_>),
Fn(fn(Context<'_>) -> CmdResult<'_>),
Runnable(&'a dyn Run),
}

Expand Down Expand Up @@ -75,7 +83,7 @@ pub struct Context<'cmd> {
current: &'cmd str,
}

pub fn handle_command<'cmd>(ctx: Context<'cmd>, commands: &'cmd [Command]) -> Result<'cmd> {
pub fn handle_command<'cmd>(ctx: Context<'cmd>, commands: &'cmd [Command]) -> CmdResult<'cmd> {
let chunk = ctx.current.trim();
for cmd in commands {
if let Some(current) = chunk.strip_prefix(cmd.name) {
Expand Down Expand Up @@ -310,7 +318,7 @@ impl<'cmd> Command<'cmd> {
/// If [`Command::with_fn`] or [`Command::with_runnable`] was previously
/// called, this overwrites the previously set value.
#[must_use]
pub const fn with_fn(self, func: fn(Context<'_>) -> Result<'_>) -> Self {
pub const fn with_fn(self, func: fn(Context<'_>) -> CmdResult<'_>) -> Self {
Self {
run: Some(RunKind::Fn(func)),
..self
Expand All @@ -332,7 +340,7 @@ impl<'cmd> Command<'cmd> {
}

/// Run this command in the provided [`Context`].
pub fn run<'ctx>(&'cmd self, ctx: Context<'ctx>) -> Result<'ctx>
pub fn run<'ctx>(&'cmd self, ctx: Context<'ctx>) -> CmdResult<'ctx>
where
'cmd: 'ctx,
{
Expand Down Expand Up @@ -414,15 +422,30 @@ impl fmt::Display for Error<'_> {
.chain(core::iter::once("help"))
}

fn fmt_flag_names(f: &mut fmt::Formatter<'_>, flags: &[&str]) -> fmt::Result {
let mut names = flags.iter();
if let Some(name) = names.next() {
f.write_str(name)?;
for name in names {
write!(f, "|{name}")?;
}
}
Ok(())
}

let Self { line, kind } = self;
match kind {
ErrorKind::UnknownCommand(commands) => {
write!(f, "unknown command {line:?}, expected one of: [")?;
fmt::comma_delimited(&mut f, command_names(commands))?;
f.write_char(']')?;
}
ErrorKind::InvalidArguments { arg, help } => {
write!(f, "invalid argument {arg:?}: {help}")?
ErrorKind::InvalidArguments { arg, help, flag } => {
f.write_str("invalid argument")?;
if let Some(flag) = flag {
write!(f, " {flag}")?;
}
write!(f, " {arg:?}: {help}")?;
}
ErrorKind::SubcommandRequired(subcommands) => {
writeln!(
Expand All @@ -432,6 +455,11 @@ impl fmt::Display for Error<'_> {
fmt::comma_delimited(&mut f, command_names(subcommands))?;
f.write_char(']')?;
}
ErrorKind::FlagRequired { flags } => {
write!(f, "the '{line}' command requires the ")?;
fmt_flag_names(f, flags)?;
write!(f, " flag")?;
}
ErrorKind::Other(msg) => write!(f, "could not execute {line:?}: {msg}")?,
}

Expand Down Expand Up @@ -472,6 +500,18 @@ impl<'cmd> Context<'cmd> {
line: self.line,
kind: ErrorKind::InvalidArguments {
arg: self.current,
flag: None,
help,
},
}
}

pub fn invalid_argument_named(&self, name: &'static str, help: &'static str) -> Error<'cmd> {
Error {
line: self.line,
kind: ErrorKind::InvalidArguments {
arg: self.current,
flag: Some(name),
help,
},
}
Expand All @@ -483,13 +523,130 @@ impl<'cmd> Context<'cmd> {
kind: ErrorKind::Other(msg),
}
}

pub fn parse_bool_flag(&mut self, flag: &str) -> bool {
if let Some(rest) = self.command().trim().strip_prefix(flag) {
self.current = rest.trim();
true
} else {
false
}
}

pub fn parse_optional_u32_hex_or_dec(
&mut self,
name: &'static str,
) -> Result<Option<u32>, Error<'cmd>> {
let (chunk, rest) = match self.command().split_once(" ") {

Check warning on line 540 in src/shell.rs

View workflow job for this annotation

GitHub Actions / clippy

warning: single-character string constant used as pattern --> src/shell.rs:540:61 | 540 | let (chunk, rest) = match self.command().split_once(" ") { | ^^^ help: try using a `char` instead: `' '` | = help: for further information visit https://rust-lang.github.io/rust-clippy/master/index.html#single_char_pattern = note: `#[warn(clippy::single_char_pattern)]` on by default

Check warning on line 540 in src/shell.rs

View workflow job for this annotation

GitHub Actions / clippy

warning: single-character string constant used as pattern --> src/shell.rs:540:61 | 540 | let (chunk, rest) = match self.command().split_once(" ") { | ^^^ help: try using a `char` instead: `' '` | = help: for further information visit https://rust-lang.github.io/rust-clippy/master/index.html#single_char_pattern = note: `#[warn(clippy::single_char_pattern)]` on by default
Some((chunk, rest)) => (chunk.trim(), rest),
None => (self.command(), ""),
};

if chunk.is_empty() {
return Ok(None);
}

let val = if let Some(hex_num) = chunk.strip_prefix("0x") {
u32::from_str_radix(hex_num.trim(), 16).map_err(|_| Error {
line: self.line,
kind: ErrorKind::InvalidArguments {
arg: chunk,
flag: Some(name),
help: "expected a 32-bit hex number",
},
})?
} else {
u32::from_str(chunk).map_err(|_| Error {
line: self.line,
kind: ErrorKind::InvalidArguments {
arg: chunk,
flag: Some(name),
help: "expected a 32-bit decimal number",
},
})?
};

self.current = rest;
Ok(Some(val))
}

pub fn parse_u32_hex_or_dec(&mut self, name: &'static str) -> Result<u32, Error<'cmd>> {
self.parse_optional_u32_hex_or_dec(name).and_then(|val| {
val.ok_or_else(|| self.invalid_argument_named(name, "expected a number"))
})
}

pub fn parse_optional_flag<T>(
&mut self,
names: &'static [&'static str],
) -> Result<Option<T>, Error<'cmd>>
where
T: FromStr,
T::Err: core::fmt::Display,
{
for name in names {
if let Some(rest) = self.command().strip_prefix(name) {
let (chunk, rest) = match rest.trim().split_once(" ") {

Check warning on line 589 in src/shell.rs

View workflow job for this annotation

GitHub Actions / clippy

warning: single-character string constant used as pattern --> src/shell.rs:589:66 | 589 | let (chunk, rest) = match rest.trim().split_once(" ") { | ^^^ help: try using a `char` instead: `' '` | = help: for further information visit https://rust-lang.github.io/rust-clippy/master/index.html#single_char_pattern

Check warning on line 589 in src/shell.rs

View workflow job for this annotation

GitHub Actions / clippy

warning: single-character string constant used as pattern --> src/shell.rs:589:66 | 589 | let (chunk, rest) = match rest.trim().split_once(" ") { | ^^^ help: try using a `char` instead: `' '` | = help: for further information visit https://rust-lang.github.io/rust-clippy/master/index.html#single_char_pattern
Some((chunk, rest)) => (chunk.trim(), rest),
None => (rest, ""),
};

if chunk.is_empty() {
return Err(Error {
line: self.line,
kind: ErrorKind::InvalidArguments {
arg: chunk,
flag: Some(name),
help: "expected a value",
},
});
}

match chunk.parse() {
Ok(val) => {
self.current = rest;
return Ok(Some(val));
}
Err(e) => {
tracing::warn!(target: "shell", "invalid value {chunk:?} for flag {name}: {e}");
return Err(Error {
line: self.line,
kind: ErrorKind::InvalidArguments {
arg: chunk,
flag: Some(name),
help: "invalid value",
},
});
}
}
}
}

Ok(None)
}

pub fn parse_required_flag<T>(
&mut self,
names: &'static [&'static str],
) -> Result<T, Error<'cmd>>
where
T: FromStr,
T::Err: core::fmt::Display,
{
self.parse_optional_flag(names).and_then(|val| {
val.ok_or_else(|| Error {
line: self.line,
kind: ErrorKind::FlagRequired { flags: names },
})

Check warning on line 640 in src/shell.rs

View workflow job for this annotation

GitHub Actions / clippy

warning: unnecessary closure used to substitute value for `Option::None` --> src/shell.rs:637:13 | 637 | / val.ok_or_else(|| Error { 638 | | line: self.line, 639 | | kind: ErrorKind::FlagRequired { flags: names }, 640 | | }) | |______________^ | = help: for further information visit https://rust-lang.github.io/rust-clippy/master/index.html#unnecessary_lazy_evaluations = note: `#[warn(clippy::unnecessary_lazy_evaluations)]` on by default help: use `ok_or(..)` instead | 637 ~ val.ok_or(Error { 638 + line: self.line, 639 + kind: ErrorKind::FlagRequired { flags: names }, 640 + }) |

Check warning on line 640 in src/shell.rs

View workflow job for this annotation

GitHub Actions / clippy

warning: unnecessary closure used to substitute value for `Option::None` --> src/shell.rs:637:13 | 637 | / val.ok_or_else(|| Error { 638 | | line: self.line, 639 | | kind: ErrorKind::FlagRequired { flags: names }, 640 | | }) | |______________^ | = help: for further information visit https://rust-lang.github.io/rust-clippy/master/index.html#unnecessary_lazy_evaluations = note: `#[warn(clippy::unnecessary_lazy_evaluations)]` on by default help: use `ok_or(..)` instead | 637 ~ val.ok_or(Error { 638 + line: self.line, 639 + kind: ErrorKind::FlagRequired { flags: names }, 640 + }) |
})
}
}

// === impl RunKind ===

impl RunKind<'_> {
#[inline]
fn run<'ctx>(&'ctx self, ctx: Context<'ctx>) -> Result<'ctx> {
fn run<'ctx>(&'ctx self, ctx: Context<'ctx>) -> CmdResult<'ctx> {
match self {
Self::Fn(func) => func(ctx),
Self::Runnable(runnable) => runnable.run(ctx),
Expand All @@ -513,9 +670,9 @@ impl fmt::Debug for RunKind<'_> {

impl<F> Run for F
where
F: Fn(Context<'_>) -> Result<'_> + Send + Sync,
F: Fn(Context<'_>) -> CmdResult<'_> + Send + Sync,
{
fn run<'ctx>(&'ctx self, ctx: Context<'ctx>) -> Result<'ctx> {
fn run<'ctx>(&'ctx self, ctx: Context<'ctx>) -> CmdResult<'ctx> {
self(ctx)
}
}
Expand All @@ -527,3 +684,22 @@ fn print_help(parent_cmd: &str, commands: &[Command]) {
}
tracing::info!(target: "shell", " {parent_cmd}{parent_cmd_pad}help --- prints this help message");
}

#[derive(Copy, Clone, Debug, Eq, PartialEq)]
pub enum NumberFormat {
Binary,
Hex,
Decimal,
}

impl FromStr for NumberFormat {
type Err = &'static str;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s.trim() {
"b" | "bin" | "binary" => Ok(Self::Binary),
"h" | "hex" => Ok(Self::Hex),
"d" | "dec" | "decimal" => Ok(Self::Decimal),
_ => Err("expected one of: [b, bin, binary, h, hex, d, decimal]"),
}
}
}

0 comments on commit 8c2cc10

Please sign in to comment.