summaryrefslogtreecommitdiff
path: root/src/commands/debug
diff options
context:
space:
mode:
Diffstat (limited to 'src/commands/debug')
-rw-r--r--src/commands/debug/bkey_types.rs289
-rw-r--r--src/commands/debug/mod.rs108
-rw-r--r--src/commands/debug/parser.rs89
3 files changed, 486 insertions, 0 deletions
diff --git a/src/commands/debug/bkey_types.rs b/src/commands/debug/bkey_types.rs
new file mode 100644
index 00000000..340e89cf
--- /dev/null
+++ b/src/commands/debug/bkey_types.rs
@@ -0,0 +1,289 @@
+//! Representation of the bcachefs bkey types, derived from DWARF debug info.
+//!
+//! This is adapted from `gimli/crates/examples/src/bin/simple.rs`.
+
+use gimli::Reader as _;
+use object::{Object, ObjectSection};
+use std::{borrow, error, fs};
+use std::collections::HashSet;
+
+/// A list of the known bcachefs bkey types.
+#[derive(Debug)]
+pub struct BkeyTypes(Vec<BchStruct>);
+
+impl BkeyTypes {
+ pub fn new() -> Self {
+ BkeyTypes(Vec::new())
+ }
+
+ /// Given a struct name and a member name, return the size and offset of
+ /// the member within the struct, or None if it does not exist.
+ pub fn get_member_layout(&self, outer: &str, member: &str) -> Option<(u64, u64)> {
+ for bkey_type in self.0.iter() {
+ if bkey_type.name == *outer {
+ return bkey_type.member_layout(member);
+ }
+ }
+
+ None
+ }
+}
+
+#[derive(Debug)]
+pub struct BchStruct {
+ name: String,
+ pub members: Vec<BchMember>,
+}
+
+impl BchStruct {
+ pub fn member_layout(&self, name: &str) -> Option<(u64, u64)> {
+ for memb in self.members.iter() {
+ if memb.name == *name {
+ return Some((memb.size, memb.offset));
+ }
+ }
+ None
+ }
+}
+
+#[derive(Debug)]
+pub struct BchMember {
+ name: String,
+ size: u64,
+ offset: u64,
+}
+
+// This is a simple wrapper around `object::read::RelocationMap` that implements
+// `gimli::read::Relocate` for use with `gimli::RelocateReader`.
+// You only need this if you are parsing relocatable object files.
+#[derive(Debug, Default)]
+struct RelocationMap(object::read::RelocationMap);
+
+impl<'a> gimli::read::Relocate for &'a RelocationMap {
+ fn relocate_address(&self, offset: usize, value: u64) -> gimli::Result<u64> {
+ Ok(self.0.relocate(offset as u64, value))
+ }
+
+ fn relocate_offset(&self, offset: usize, value: usize) -> gimli::Result<usize> {
+ <usize as gimli::ReaderOffset>::from_u64(self.0.relocate(offset as u64, value as u64))
+ }
+}
+
+// The section data that will be stored in `DwarfSections` and `DwarfPackageSections`.
+#[derive(Default)]
+struct Section<'data> {
+ data: borrow::Cow<'data, [u8]>,
+ relocations: RelocationMap,
+}
+
+// The reader type that will be stored in `Dwarf` and `DwarfPackage`.
+// If you don't need relocations, you can use `gimli::EndianSlice` directly.
+type Reader<'data> =
+ gimli::RelocateReader<gimli::EndianSlice<'data, gimli::RunTimeEndian>, &'data RelocationMap>;
+
+fn process_file(
+ object: &object::File,
+ struct_list: &mut BkeyTypes,
+) -> Result<(), Box<dyn error::Error>> {
+ let endian = if object.is_little_endian() {
+ gimli::RunTimeEndian::Little
+ } else {
+ gimli::RunTimeEndian::Big
+ };
+
+ // Load a `Section` that may own its data.
+ fn load_section<'data>(
+ object: &object::File<'data>,
+ name: &str,
+ ) -> Result<Section<'data>, Box<dyn error::Error>> {
+ Ok(match object.section_by_name(name) {
+ Some(section) => Section {
+ data: section.uncompressed_data()?,
+ relocations: section.relocation_map().map(RelocationMap)?,
+ },
+ None => Default::default(),
+ })
+ }
+
+ // Borrow a `Section` to create a `Reader`.
+ fn borrow_section<'data>(
+ section: &'data Section<'data>,
+ endian: gimli::RunTimeEndian,
+ ) -> Reader<'data> {
+ let slice = gimli::EndianSlice::new(borrow::Cow::as_ref(&section.data), endian);
+ gimli::RelocateReader::new(slice, &section.relocations)
+ }
+
+ // Load all of the sections.
+ let dwarf_sections = gimli::DwarfSections::load(|id| load_section(object, id.name()))?;
+
+ // Create `Reader`s for all of the sections and do preliminary parsing.
+ // Alternatively, we could have used `Dwarf::load` with an owned type such as `EndianRcSlice`.
+ let dwarf = dwarf_sections.borrow(|section| borrow_section(section, endian));
+
+ let mut bkey_types = HashSet::new();
+ load_bkey_types(&mut bkey_types);
+
+ let mut iter = dwarf.units();
+ while let Some(header) = iter.next()? {
+ let unit = dwarf.unit(header)?;
+ process_unit(&dwarf, &unit, struct_list, &mut bkey_types)?;
+ }
+
+ Ok(())
+}
+
+fn load_bkey_types(bkey_types: &mut HashSet<String>) {
+ let mut ptr: *const *const i8 = unsafe { bch_bindgen::c::bch2_bkey_types.as_ptr() };
+ unsafe {
+ while !(*ptr).is_null() {
+ let mut bkey_name = String::from("bch_");
+ bkey_name.push_str(std::ffi::CStr::from_ptr(*ptr).to_str().unwrap());
+ bkey_types.insert(bkey_name);
+ ptr = ptr.offset(1);
+ }
+ }
+
+ // This key type is not included in BCH2_BKEY_TYPES.
+ bkey_types.insert("bch_inode_unpacked".to_string());
+}
+
+fn process_unit(
+ dwarf: &gimli::Dwarf<Reader>,
+ unit: &gimli::Unit<Reader>,
+ struct_list: &mut BkeyTypes,
+ bkey_types: &mut HashSet<String>,
+) -> Result<(), gimli::Error> {
+ let mut tree = unit.entries_tree(None)?;
+
+ process_tree(dwarf, unit, tree.root()?, struct_list, bkey_types)?;
+
+ Ok(())
+}
+
+fn process_tree(
+ dwarf: &gimli::Dwarf<Reader>,
+ unit: &gimli::Unit<Reader>,
+ node: gimli::EntriesTreeNode<Reader>,
+ struct_list: &mut BkeyTypes,
+ bkey_types: &mut HashSet<String>,
+) -> gimli::Result<()> {
+ let entry = node.entry();
+ if entry.tag() == gimli::DW_TAG_structure_type {
+ if let Some(name) = entry.attr(gimli::DW_AT_name)? {
+ if let Ok(name) = dwarf.attr_string(unit, name.value()) {
+ let name = name.to_string_lossy()?.into_owned();
+ if bkey_types.remove(&name.clone()) {
+ process_struct(name, dwarf, unit, node, struct_list)?;
+ }
+ }
+ }
+ } else {
+ let mut children = node.children();
+ while let Some(child) = children.next()? {
+ process_tree(dwarf, unit, child, struct_list, bkey_types)?;
+ }
+ }
+ Ok(())
+}
+
+fn process_struct(
+ name: std::string::String,
+ dwarf: &gimli::Dwarf<Reader>,
+ unit: &gimli::Unit<Reader>,
+ node: gimli::EntriesTreeNode<Reader>,
+ struct_list: &mut BkeyTypes,
+) -> gimli::Result<()> {
+ let mut bch_struct = BchStruct {
+ name,
+ members: Vec::new(),
+ };
+
+ let mut children = node.children();
+ while let Some(child) = children.next()? {
+ if let Some(member) = process_struct_member(dwarf, unit, child) {
+ bch_struct.members.push(member);
+ }
+ }
+ struct_list.0.push(bch_struct);
+
+ Ok(())
+}
+
+fn process_struct_member(
+ dwarf: &gimli::Dwarf<Reader>,
+ unit: &gimli::Unit<Reader>,
+ node: gimli::EntriesTreeNode<Reader>,
+) -> Option<BchMember> {
+ let entry = node.entry();
+
+ let name: Option<String> = entry.attr(gimli::DW_AT_name).ok()?.map(|name| {
+ if let Ok(name) = dwarf.attr_string(unit, name.value()) {
+ Some(name.to_string_lossy().ok()?.into_owned())
+ } else {
+ None
+ }
+ })?;
+ let Some(name) = name else {
+ return None;
+ };
+
+ let offset: Option<u64> = entry
+ .attr(gimli::DW_AT_data_member_location)
+ .ok()?
+ .map(|offset| offset.value().udata_value())?;
+ let Some(offset) = offset else {
+ return None;
+ };
+
+ let size = entry.attr(gimli::DW_AT_type).ok()?.map(|ty| {
+ if let gimli::AttributeValue::UnitRef(offset) = ty.value() {
+ let mut ty_entry = unit.entries_at_offset(offset).ok()?;
+ ty_entry.next_entry().ok()?;
+ if let Some(t) = ty_entry.current() {
+ return get_size(unit, t);
+ }
+ }
+
+ None
+ })?;
+ let Some(size) = size else {
+ return None;
+ };
+
+ Some(BchMember { name, offset, size })
+}
+
+fn get_size(
+ unit: &gimli::Unit<Reader>,
+ entry: &gimli::DebuggingInformationEntry<Reader>,
+) -> Option<u64> {
+ if let Some(size) = entry.attr(gimli::DW_AT_byte_size).ok()? {
+ return size.udata_value();
+ }
+
+ if let Some(ref_type) = entry.attr(gimli::DW_AT_type).ok()? {
+ if let gimli::AttributeValue::UnitRef(offset) = ref_type.value() {
+ let mut type_entry = unit.entries_at_offset(offset).ok()?;
+ type_entry.next_entry().ok()?;
+ if let Some(t) = type_entry.current() {
+ return get_size(unit, t);
+ }
+ }
+ }
+
+ None
+}
+
+/// Return a list of the known bkey types.
+pub fn get_bkey_type_info() -> BkeyTypes {
+ let path = fs::read_link("/proc/self/exe").unwrap();
+
+ let file = fs::File::open(path).unwrap();
+ let mmap = unsafe { memmap2::Mmap::map(&file).unwrap() };
+ let object = object::File::parse(&*mmap).unwrap();
+ let mut struct_list = BkeyTypes::new();
+ process_file(&object, &mut struct_list).unwrap();
+
+ struct_list
+}
diff --git a/src/commands/debug/mod.rs b/src/commands/debug/mod.rs
new file mode 100644
index 00000000..e5651921
--- /dev/null
+++ b/src/commands/debug/mod.rs
@@ -0,0 +1,108 @@
+use clap::Parser;
+use std::ffi::{c_char, CString};
+use std::io::{BufRead, Write};
+
+use bch_bindgen::bcachefs;
+use bch_bindgen::c;
+use bch_bindgen::fs::Fs;
+
+mod bkey_types;
+mod parser;
+
+use bch_bindgen::c::bpos;
+
+/// Debug a bcachefs filesystem.
+#[derive(Parser, Debug)]
+pub struct Cli {
+ #[arg(required(true))]
+ devices: Vec<std::path::PathBuf>,
+}
+
+#[derive(Debug)]
+enum DebugCommand {
+ Dump(DumpCommand),
+ Update(UpdateCommand),
+}
+
+#[derive(Debug)]
+struct DumpCommand {
+ btree: String,
+ bpos: bpos,
+}
+
+#[derive(Debug)]
+struct UpdateCommand {
+ btree: String,
+ bpos: bpos,
+ bkey: String,
+ field: String,
+ value: u64,
+}
+
+fn update(fs: &Fs, type_list: &bkey_types::BkeyTypes, cmd: UpdateCommand) {
+ let bkey = CString::new(cmd.bkey.clone()).unwrap();
+ let bkey = bkey.as_ptr() as *const c_char;
+
+ let id: bch_bindgen::c::btree_id = cmd.btree.parse().expect("no such btree");
+
+ if let Some((size, offset)) = type_list.get_member_layout(&cmd.bkey, &cmd.field) {
+ let update = c::bkey_update {
+ id,
+ bkey,
+ offset,
+ size,
+ value: cmd.value,
+ };
+ unsafe {
+ c::cmd_update_bkey(fs.raw, update, cmd.bpos);
+ }
+ } else {
+ println!("unknown field '{}'", cmd.field);
+ }
+}
+
+fn dump(fs: &Fs, cmd: DumpCommand) {
+ let id: bch_bindgen::c::btree_id = cmd.btree.parse().expect("no such btree");
+
+ unsafe {
+ c::cmd_dump_bkey(fs.raw, id, cmd.bpos);
+ }
+}
+
+pub fn debug(argv: Vec<String>) -> i32 {
+ fn prompt() {
+ print!("bcachefs> ");
+ std::io::stdout().flush().unwrap();
+ }
+
+ let opt = Cli::parse_from(argv);
+
+ let fs_opts: bcachefs::bch_opts = Default::default();
+ let fs = match Fs::open(&opt.devices, fs_opts) {
+ Ok(fs) => fs,
+ Err(_) => {
+ return 1;
+ }
+ };
+
+ let type_list = bkey_types::get_bkey_type_info();
+
+ prompt();
+ let stdin = std::io::stdin();
+ for line in stdin.lock().lines() {
+ let line = line.unwrap();
+ if let Some(cmd) = parser::parse_command(&line) {
+ match cmd {
+ // usage: dump <btree_type> <bpos>
+ DebugCommand::Dump(cmd) => dump(&fs, cmd),
+ // usage: update <btree_type> <bpos> <bkey_type>.<field>=<value>
+ DebugCommand::Update(cmd) => update(&fs, &type_list, cmd),
+ }
+ } else {
+ println!("failed to parse a command");
+ };
+ prompt();
+ }
+
+ 0
+}
diff --git a/src/commands/debug/parser.rs b/src/commands/debug/parser.rs
new file mode 100644
index 00000000..550860c9
--- /dev/null
+++ b/src/commands/debug/parser.rs
@@ -0,0 +1,89 @@
+use nom::branch::alt;
+use nom::bytes::complete::{tag, take_while};
+use nom::character::complete::{alpha1, char, space1, u32, u64};
+use nom::combinator::{all_consuming, value};
+use nom::sequence::tuple;
+use nom::IResult;
+
+use bch_bindgen::c::bpos;
+
+use crate::commands::debug::{DebugCommand, DumpCommand, UpdateCommand};
+
+fn parse_bpos(input: &str) -> IResult<&str, bpos> {
+ let (input, (inode, _, offset, _, snapshot)) = tuple((
+ u64,
+ char(':'),
+ u64,
+ char(':'),
+ alt((u32, value(u32::MAX, tag("U32_MAX")))),
+ ))(input)?;
+
+ Ok((
+ input,
+ bpos {
+ inode,
+ offset,
+ snapshot,
+ },
+ ))
+}
+
+fn parse_dump_cmd(input: &str) -> IResult<&str, DebugCommand> {
+ let (input, (_, btree, _, bpos)) =
+ all_consuming(tuple((space1, alpha1, space1, parse_bpos)))(input)?;
+
+ Ok((
+ input,
+ DebugCommand::Dump(DumpCommand {
+ btree: btree.to_string(),
+ bpos,
+ }),
+ ))
+}
+
+fn symbol_name(input: &str) -> IResult<&str, &str> {
+ take_while(|c: char| c.is_alphabetic() || c == '_')(input)
+}
+
+fn parse_update_cmd(input: &str) -> IResult<&str, DebugCommand> {
+ let (input, (_, btree, _, bpos, _, bkey, _, field, _, value)) = all_consuming(tuple((
+ space1,
+ alpha1,
+ space1,
+ parse_bpos,
+ space1,
+ symbol_name,
+ char('.'),
+ symbol_name,
+ char('='),
+ u64,
+ )))(input)?;
+
+ Ok((
+ input,
+ DebugCommand::Update(UpdateCommand {
+ btree: btree.to_string(),
+ bpos,
+ bkey: bkey.to_string(),
+ field: field.to_string(),
+ value,
+ }),
+ ))
+}
+
+fn parse_command_inner(input: &str) -> IResult<&str, DebugCommand> {
+ let (input, cmd) = alt((tag("dump"), tag("update")))(input)?;
+
+ match cmd {
+ "dump" => parse_dump_cmd(input),
+ "update" => parse_update_cmd(input),
+ _ => unreachable!(),
+ }
+}
+
+pub fn parse_command(input: &str) -> Option<DebugCommand> {
+ match parse_command_inner(input) {
+ Ok((_, c)) => Some(c),
+ Err(_) => None,
+ }
+}