summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorThomas Bertschinger <tahbertschinger@gmail.com>2024-04-23 21:43:25 -0600
committerThomas Bertschinger <tahbertschinger@gmail.com>2024-05-07 21:29:32 -0400
commit16f2849433aafd33eae8539536169f86df124dfd (patch)
tree6431313e883f0bf3bc9eca36106c23bd48f3fb98
parentfbb223308961067a44d343cbca515aa12a745bde (diff)
WIP: bcachefs: new debug command
Signed-off-by: Thomas Bertschinger <tahbertschinger@gmail.com>
-rw-r--r--Cargo.lock158
-rw-r--r--Cargo.toml4
-rw-r--r--c_src/cmd_debug.c115
-rw-r--r--c_src/cmds.h11
-rw-r--r--src/bcachefs.rs12
-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
-rw-r--r--src/commands/mod.rs2
9 files changed, 778 insertions, 10 deletions
diff --git a/Cargo.lock b/Cargo.lock
index 22ac0e3b..50e39366 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -3,6 +3,12 @@
version = 3
[[package]]
+name = "adler"
+version = "1.0.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe"
+
+[[package]]
name = "aho-corasick"
version = "1.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -83,8 +89,12 @@ dependencies = [
"colored",
"either",
"errno 0.2.8",
+ "gimli",
"libc",
"log",
+ "memmap2",
+ "nom",
+ "object",
"rpassword",
"udev",
"uuid",
@@ -124,7 +134,7 @@ dependencies = [
"regex",
"rustc-hash",
"shlex",
- "syn",
+ "syn 2.0.48",
"which",
]
@@ -228,7 +238,7 @@ dependencies = [
"heck",
"proc-macro2",
"quote",
- "syn",
+ "syn 2.0.48",
]
[[package]]
@@ -254,12 +264,38 @@ dependencies = [
]
[[package]]
+name = "crc32fast"
+version = "1.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b3855a8a784b474f333699ef2bbca9db2c4a1f6d9088a90a2d25b1eb53111eaa"
+dependencies = [
+ "cfg-if",
+]
+
+[[package]]
+name = "derive_more"
+version = "0.99.17"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4fb810d30a7c1953f91334de7244731fc3f3c10d7fe163338a35b9f640960321"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 1.0.109",
+]
+
+[[package]]
name = "either"
version = "1.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a26ae43d7bcc3b814de94796a5e736d4029efb0ee900c12e2d54c993ad1a1e07"
[[package]]
+name = "equivalent"
+version = "1.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5"
+
+[[package]]
name = "errno"
version = "0.2.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -291,12 +327,45 @@ dependencies = [
]
[[package]]
+name = "fallible-iterator"
+version = "0.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2acce4a10f12dc2fb14a218589d4f1f62ef011b2d0cc4b3cb1bba8e94da14649"
+
+[[package]]
+name = "flate2"
+version = "1.0.28"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "46303f565772937ffe1d394a4fac6f411c6013172fadde9dcdb1e147a086940e"
+dependencies = [
+ "crc32fast",
+ "miniz_oxide",
+]
+
+[[package]]
+name = "gimli"
+version = "0.29.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "40ecd4077b5ae9fd2e9e169b102c6c330d0605168eb0e8bf79952b256dbefffd"
+dependencies = [
+ "fallible-iterator",
+ "indexmap",
+ "stable_deref_trait",
+]
+
+[[package]]
name = "glob"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b"
[[package]]
+name = "hashbrown"
+version = "0.14.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "290f1a1d9242c78d09ce40a5e87e7554ee637af1351968159f4952f028f75604"
+
+[[package]]
name = "heck"
version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -312,6 +381,16 @@ dependencies = [
]
[[package]]
+name = "indexmap"
+version = "2.2.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "168fb715dda47215e360912c096649d23d58bf392ac62f73919e831745e40f26"
+dependencies = [
+ "equivalent",
+ "hashbrown",
+]
+
+[[package]]
name = "itertools"
version = "0.12.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -377,6 +456,15 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "523dc4f511e55ab87b694dc30d0f820d60906ef06413f93d4d7a1385599cc149"
[[package]]
+name = "memmap2"
+version = "0.9.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fe751422e4a8caa417e13c3ea66452215d7d63e19e604f4980461212f3ae1322"
+dependencies = [
+ "libc",
+]
+
+[[package]]
name = "memoffset"
version = "0.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -392,6 +480,15 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a"
[[package]]
+name = "miniz_oxide"
+version = "0.7.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9d811f3e15f28568be3407c8e7fdb6514c1cda3cb30683f15b6a1a1dc4ea14a7"
+dependencies = [
+ "adler",
+]
+
+[[package]]
name = "nom"
version = "7.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -402,6 +499,17 @@ dependencies = [
]
[[package]]
+name = "object"
+version = "0.35.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b8ec7ab813848ba4522158d5517a6093db1ded27575b070f4177b8d12b41db5e"
+dependencies = [
+ "flate2",
+ "memchr",
+ "ruzstd",
+]
+
+[[package]]
name = "once_cell"
version = "1.19.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -426,7 +534,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a41cf62165e97c7f814d2221421dbb9afcbcdb0a88068e5ea206e19951c2cbb5"
dependencies = [
"proc-macro2",
- "syn",
+ "syn 2.0.48",
]
[[package]]
@@ -517,12 +625,35 @@ dependencies = [
]
[[package]]
+name = "ruzstd"
+version = "0.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5174a470eeb535a721ae9fdd6e291c2411a906b96592182d05217591d5c5cf7b"
+dependencies = [
+ "byteorder",
+ "derive_more",
+ "twox-hash",
+]
+
+[[package]]
name = "shlex"
version = "1.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
[[package]]
+name = "stable_deref_trait"
+version = "1.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3"
+
+[[package]]
+name = "static_assertions"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f"
+
+[[package]]
name = "strsim"
version = "0.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -530,6 +661,17 @@ checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623"
[[package]]
name = "syn"
+version = "1.0.109"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "unicode-ident",
+]
+
+[[package]]
+name = "syn"
version = "2.0.48"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0f3531638e407dfc0814761abb7c00a5b54992b849452a0646b7f65c9f770f3f"
@@ -550,6 +692,16 @@ dependencies = [
]
[[package]]
+name = "twox-hash"
+version = "1.6.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "97fee6b57c6a41524a810daee9286c02d7752c4253064d0b05472833a438f675"
+dependencies = [
+ "cfg-if",
+ "static_assertions",
+]
+
+[[package]]
name = "udev"
version = "0.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
diff --git a/Cargo.toml b/Cargo.toml
index 853123ee..1efc66be 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -23,3 +23,7 @@ either = "1.5"
rpassword = "7"
bch_bindgen = { path = "bch_bindgen" }
byteorder = "1.3"
+gimli = "0.29.0"
+object = "0.35.0"
+memmap2 = "0.9.4"
+nom = "7.1.3"
diff --git a/c_src/cmd_debug.c b/c_src/cmd_debug.c
new file mode 100644
index 00000000..9d5def1a
--- /dev/null
+++ b/c_src/cmd_debug.c
@@ -0,0 +1,115 @@
+#include <stdio.h>
+
+#include "libbcachefs/bkey_types.h"
+#include "libbcachefs/btree_update.h"
+#include "libbcachefs/printbuf.h"
+#include "libbcachefs/inode.h"
+
+#include "cmds.h"
+
+void write_field(void *base, u64 size, u64 offset, u64 value)
+{
+ u8 *field8;
+ u16 *field16;
+ u32 *field32;
+ u64 *field64;
+
+ switch (size) {
+ case 1:
+ field8 = (u8 *) base + offset;
+ *field8 = (u8) value;
+ break;
+ case 2:
+ field16 = (u16 *) ((u8 *) base + offset);
+ *field16 = (u16) value;
+ break;
+ case 4:
+ field32 = (u32 *) ((u8 *) base + offset);
+ *field32 = (u32) value;
+ break;
+ case 8:
+ field64 = (u64 *) ((u8 *) base + offset);
+ *field64 = value;
+ break;
+ default:
+ fprintf(stderr, "can't handle size %llu\n", size);
+ }
+}
+
+int cmd_dump_bkey(struct bch_fs *c, enum btree_id id, struct bpos pos)
+{
+ struct printbuf buf = PRINTBUF;
+ struct btree_trans *trans = bch2_trans_get(c);
+ struct btree_iter iter = { NULL };
+ int ret = 0;
+
+ bch2_trans_iter_init(trans, &iter, id, pos, BTREE_ITER_ALL_SNAPSHOTS);
+
+ struct bkey_s_c k = bch2_btree_iter_peek(&iter);
+ if (!bpos_eq(pos, k.k->p)) {
+ printf("no such key\n");
+ ret = 1;
+ goto out;
+ }
+
+ bch2_bkey_val_to_text(&buf, c, k);
+ printf("%s\n", buf.buf);
+
+out:
+ bch2_trans_iter_exit(trans, &iter);
+ bch2_trans_put(trans);
+
+ return ret;
+}
+
+int cmd_update_bkey(struct bch_fs *c, struct bkey_update u, struct bpos pos)
+{
+ struct btree_trans *trans = bch2_trans_get(c);
+ struct btree_iter iter = { NULL };
+ int ret = 0;
+
+ set_bit(BCH_FS_no_invalid_checks, &c->flags);
+
+ if (!strcmp(u.bkey, "bch_inode_unpacked")) {
+ bch2_trans_iter_init(trans, &iter, BTREE_ID_inodes, pos,
+ BTREE_ITER_ALL_SNAPSHOTS);
+
+ struct bkey_s_c k = bch2_btree_iter_peek(&iter);
+ if (bkey_err(k)) {
+ // TODO: is this proper error handling?
+ printf("error getting key: %s\n", bch2_err_str(PTR_ERR(k.k)));
+ ret = 1;
+ goto out;
+ }
+ struct bch_inode_unpacked inode;
+ // TODO: check error
+ bch2_inode_unpack(k, &inode);
+
+ write_field(&inode, u.size, u.offset, u.value);
+
+ ret = bch2_inode_write(trans, &iter, &inode) ?:
+ bch2_trans_commit(trans, NULL, NULL, BCH_TRANS_COMMIT_no_invalid_checks);
+ if (ret != 0) {
+ printf("ret = %s (%d)\n", bch2_err_str(ret), ret);
+ }
+ } else {
+ // TODO can this return an error?
+ struct bkey_i *k = bch2_bkey_get_mut(trans, &iter, u.id, pos,
+ BTREE_ITER_ALL_SNAPSHOTS);
+ // TODO: check bkey type to confirm it matches specified type?
+ bch2_trans_unlock(trans);
+
+ write_field(&k->v, u.size, u.offset, u.value);
+
+ ret = bch2_btree_insert(c, u.id, k, NULL, BCH_TRANS_COMMIT_no_invalid_checks);
+ if (ret != 0) {
+ printf("ret = %s (%d)\n", bch2_err_str(ret), ret);
+ }
+ }
+
+out:
+ bch2_trans_iter_exit(trans, &iter);
+ bch2_trans_put(trans);
+
+ return ret;
+}
diff --git a/c_src/cmds.h b/c_src/cmds.h
index 64267dc4..cb76f677 100644
--- a/c_src/cmds.h
+++ b/c_src/cmds.h
@@ -9,6 +9,14 @@
#include "tools-util.h"
+struct bkey_update {
+ enum btree_id id;
+ const char *bkey;
+ u64 offset;
+ u64 size;
+ u64 value;
+};
+
int cmd_format(int argc, char *argv[]);
int cmd_show_super(int argc, char *argv[]);
int cmd_reset_counters(int argc, char *argv[]);
@@ -54,6 +62,9 @@ int cmd_subvolume_snapshot(int argc, char *argv[]);
int cmd_fusemount(int argc, char *argv[]);
+int cmd_dump_bkey(struct bch_fs *, enum btree_id, struct bpos);
+int cmd_update_bkey(struct bch_fs *, struct bkey_update, struct bpos);
+
void bcachefs_usage(void);
int device_cmds(int argc, char *argv[]);
int fs_cmds(int argc, char *argv[]);
diff --git a/src/bcachefs.rs b/src/bcachefs.rs
index e8099ffa..dbe79578 100644
--- a/src/bcachefs.rs
+++ b/src/bcachefs.rs
@@ -1,11 +1,11 @@
-mod wrappers;
mod commands;
mod key;
+mod wrappers;
use std::ffi::{c_char, CString};
-use commands::logger::SimpleLogger;
use bch_bindgen::c;
+use commands::logger::SimpleLogger;
#[derive(Debug)]
pub struct ErrnoError(pub errno::Errno);
@@ -25,10 +25,7 @@ fn handle_c_command(mut argv: Vec<String>, symlink_cmd: Option<&str>) -> i32 {
let argc: i32 = argv.len().try_into().unwrap();
- let argv: Vec<_> = argv
- .into_iter()
- .map(|s| CString::new(s).unwrap())
- .collect();
+ let argv: Vec<_> = argv.into_iter().map(|s| CString::new(s).unwrap()).collect();
let mut argv = argv
.into_iter()
.map(|s| Box::into_raw(s.into_boxed_c_str()) as *mut c_char)
@@ -41,7 +38,7 @@ fn handle_c_command(mut argv: Vec<String>, symlink_cmd: Option<&str>) -> i32 {
"--help" => {
c::bcachefs_usage();
0
- },
+ }
"data" => c::data_cmds(argc, argv),
"device" => c::device_cmds(argc, argv),
"dump" => c::cmd_dump(argc, argv),
@@ -110,6 +107,7 @@ fn main() {
"list" => commands::list(args[1..].to_vec()),
"mount" => commands::mount(args, symlink_cmd),
"subvolume" => commands::subvolume(args[1..].to_vec()),
+ "debug" => commands::debug(args[1..].to_vec()),
_ => handle_c_command(args, symlink_cmd),
};
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,
+ }
+}
diff --git a/src/commands/mod.rs b/src/commands/mod.rs
index c7645926..831aaf5a 100644
--- a/src/commands/mod.rs
+++ b/src/commands/mod.rs
@@ -5,11 +5,13 @@ pub mod mount;
pub mod list;
pub mod completions;
pub mod subvolume;
+pub mod debug;
pub use mount::mount;
pub use list::list;
pub use completions::completions;
pub use subvolume::subvolume;
+pub use debug::debug;
#[derive(clap::Parser, Debug)]
#[command(name = "bcachefs")]