1 //! Implementation of the `wasmtime objdump` CLI command. 2 3 use anyhow::{Context, Result, bail}; 4 use capstone::InsnGroupType::{CS_GRP_JUMP, CS_GRP_RET}; 5 use clap::Parser; 6 use cranelift_codegen::isa::lookup_by_name; 7 use cranelift_codegen::settings::Flags; 8 use object::read::elf::ElfFile64; 9 use object::{Architecture, Endianness, FileFlags, Object, ObjectSection, ObjectSymbol}; 10 use pulley_interpreter::decode::{Decoder, DecodingError, OpVisitor}; 11 use pulley_interpreter::disas::Disassembler; 12 use std::io::{IsTerminal, Read, Write}; 13 use std::iter::{self, Peekable}; 14 use std::path::{Path, PathBuf}; 15 use termcolor::{Color, ColorChoice, ColorSpec, StandardStream, WriteColor}; 16 use wasmtime::Engine; 17 use wasmtime_environ::{FilePos, StackMap, Trap, obj}; 18 19 /// A helper utility in wasmtime to explore the compiled object file format of 20 /// a `*.cwasm` file. 21 #[derive(Parser)] 22 pub struct ObjdumpCommand { 23 /// The path to a compiled `*.cwasm` file. 24 /// 25 /// If this is `-` or not provided then stdin is used as input. 26 cwasm: Option<PathBuf>, 27 28 /// Whether or not to display function/instruction addresses. 29 #[arg(long)] 30 addresses: bool, 31 32 /// Whether or not to try to only display addresses of instruction jump 33 /// targets. 34 #[arg(long)] 35 address_jumps: bool, 36 37 /// What functions should be printed 38 #[arg(long, default_value = "wasm", value_name = "KIND")] 39 funcs: Vec<Func>, 40 41 /// String filter to apply to function names to only print some functions. 42 #[arg(long, value_name = "STR")] 43 filter: Option<String>, 44 45 /// Whether or not instruction bytes are disassembled. 46 #[arg(long)] 47 bytes: bool, 48 49 /// Whether or not to use color. 50 #[arg(long, default_value = "auto")] 51 color: ColorChoice, 52 53 /// Whether or not to interleave instructions with address maps. 54 #[arg(long, require_equals = true, value_name = "true|false")] 55 addrmap: Option<Option<bool>>, 56 57 /// Column width of how large an address is rendered as. 58 #[arg(long, default_value = "10", value_name = "N")] 59 address_width: usize, 60 61 /// Whether or not to show information about what instructions can trap. 62 #[arg(long, require_equals = true, value_name = "true|false")] 63 traps: Option<Option<bool>>, 64 65 /// Whether or not to show information about stack maps. 66 #[arg(long, require_equals = true, value_name = "true|false")] 67 stack_maps: Option<Option<bool>>, 68 } 69 70 fn optional_flag_with_default(flag: Option<Option<bool>>, default: bool) -> bool { 71 match flag { 72 None => default, 73 Some(None) => true, 74 Some(Some(val)) => val, 75 } 76 } 77 78 impl ObjdumpCommand { 79 fn addrmap(&self) -> bool { 80 optional_flag_with_default(self.addrmap, false) 81 } 82 83 fn traps(&self) -> bool { 84 optional_flag_with_default(self.traps, true) 85 } 86 87 fn stack_maps(&self) -> bool { 88 optional_flag_with_default(self.stack_maps, true) 89 } 90 91 /// Executes the command. 92 pub fn execute(self) -> Result<()> { 93 // Setup stdout handling color options. Also build some variables used 94 // below to configure colors of certain items. 95 let mut choice = self.color; 96 if choice == ColorChoice::Auto && !std::io::stdout().is_terminal() { 97 choice = ColorChoice::Never; 98 } 99 let mut stdout = StandardStream::stdout(choice); 100 101 let mut color_address = ColorSpec::new(); 102 color_address.set_bold(true).set_fg(Some(Color::Yellow)); 103 let mut color_bytes = ColorSpec::new(); 104 color_bytes.set_fg(Some(Color::Magenta)); 105 106 let bytes = self.read_cwasm()?; 107 108 // Double-check this is a `*.cwasm` 109 if Engine::detect_precompiled(&bytes).is_none() { 110 bail!("not a `*.cwasm` file from wasmtime: {:?}", self.cwasm); 111 } 112 113 // Parse the input as an ELF file, extract the `.text` section. 114 let elf = ElfFile64::<Endianness>::parse(&bytes)?; 115 let text = elf 116 .section_by_name(".text") 117 .context("missing .text section")?; 118 let text = text.data()?; 119 120 // Build the helper that'll get used to attach decorations/annotations 121 // to various instructions. 122 let mut decorator = Decorator { 123 addrmap: elf 124 .section_by_name(obj::ELF_WASMTIME_ADDRMAP) 125 .and_then(|section| section.data().ok()) 126 .and_then(|bytes| wasmtime_environ::iterate_address_map(bytes)) 127 .map(|i| (Box::new(i) as Box<dyn Iterator<Item = _>>).peekable()), 128 traps: elf 129 .section_by_name(obj::ELF_WASMTIME_TRAPS) 130 .and_then(|section| section.data().ok()) 131 .and_then(|bytes| wasmtime_environ::iterate_traps(bytes)) 132 .map(|i| (Box::new(i) as Box<dyn Iterator<Item = _>>).peekable()), 133 stack_maps: elf 134 .section_by_name(obj::ELF_WASMTIME_STACK_MAP) 135 .and_then(|section| section.data().ok()) 136 .and_then(|bytes| StackMap::iter(bytes)) 137 .map(|i| (Box::new(i) as Box<dyn Iterator<Item = _>>).peekable()), 138 objdump: &self, 139 }; 140 141 // Iterate over all symbols which will be functions for a cwasm and 142 // we'll disassemble them all. 143 let mut first = true; 144 for sym in elf.symbols() { 145 let name = match sym.name() { 146 Ok(name) => name, 147 Err(_) => continue, 148 }; 149 let bytes = &text[sym.address() as usize..][..sym.size() as usize]; 150 151 let kind = if name.starts_with("wasmtime_builtin") { 152 Func::Builtin 153 } else if name.contains("]::function[") { 154 Func::Wasm 155 } else if name.contains("trampoline") 156 || name.ends_with("_array_call") 157 || name.ends_with("_wasm_call") 158 { 159 Func::Trampoline 160 } else if name.contains("libcall") || name.starts_with("component") { 161 Func::Libcall 162 } else { 163 panic!("unknown symbol: {name}") 164 }; 165 166 // Apply any filters, if provided, to this function to look at just 167 // one function in the disassembly. 168 if self.funcs.is_empty() { 169 if kind != Func::Wasm { 170 continue; 171 } 172 } else { 173 if !(self.funcs.contains(&Func::All) || self.funcs.contains(&kind)) { 174 continue; 175 } 176 } 177 if let Some(filter) = &self.filter { 178 if !name.contains(filter) { 179 continue; 180 } 181 } 182 183 // Place a blank line between functions. 184 if first { 185 first = false; 186 } else { 187 writeln!(stdout)?; 188 } 189 190 // Print the function's address, if so desired. Then print the 191 // function name. 192 if self.addresses { 193 stdout.set_color(color_address.clone().set_bold(true))?; 194 write!(stdout, "{:08x} ", sym.address())?; 195 stdout.reset()?; 196 } 197 stdout.set_color(ColorSpec::new().set_bold(true).set_fg(Some(Color::Green)))?; 198 write!(stdout, "{name}")?; 199 stdout.reset()?; 200 writeln!(stdout, ":")?; 201 202 // Tracking variables for rough heuristics of printing targets of 203 // jump instructions for `--address-jumps` mode. 204 let mut prev_jump = false; 205 let mut write_offsets = false; 206 207 for inst in self.disas(&elf, bytes, sym.address())? { 208 let Inst { 209 address, 210 is_jump, 211 is_return, 212 disassembly: disas, 213 bytes, 214 } = inst; 215 216 // Generate an infinite list of bytes to make printing below 217 // easier, but only limit `inline_bytes` to get printed before 218 // an instruction. 219 let mut bytes = bytes.iter().map(Some).chain(iter::repeat(None)); 220 let inline_bytes = 9; 221 let width = self.address_width; 222 223 // Some instructions may disassemble to multiple lines, such as 224 // `br_table` with Pulley. Handle separate lines per-instruction 225 // here. 226 for (i, line) in disas.lines().enumerate() { 227 let print_address = self.addresses 228 || (self.address_jumps && (write_offsets || (prev_jump && !is_jump))); 229 if i == 0 && print_address { 230 stdout.set_color(&color_address)?; 231 write!(stdout, "{address:>width$x}: ")?; 232 stdout.reset()?; 233 } else { 234 write!(stdout, "{:width$} ", "")?; 235 } 236 237 // If we're printing inline bytes then print up to 238 // `inline_bytes` of instruction data, and any remaining 239 // data will go on the next line, if any, or after the 240 // instruction below. 241 if self.bytes { 242 stdout.set_color(&color_bytes)?; 243 for byte in bytes.by_ref().take(inline_bytes) { 244 match byte { 245 Some(byte) => write!(stdout, "{byte:02x} ")?, 246 None => write!(stdout, " ")?, 247 } 248 } 249 write!(stdout, " ")?; 250 stdout.reset()?; 251 } 252 253 writeln!(stdout, "{line}")?; 254 } 255 256 // Flip write_offsets to true once we've seen a `ret`, as 257 // instructions that follow the return are often related to trap 258 // tables. 259 write_offsets |= is_return; 260 prev_jump = is_jump; 261 262 // After the instruction is printed then finish printing the 263 // instruction bytes if any are present. Still limit to 264 // `inline_bytes` per line. 265 if self.bytes { 266 let mut inline = 0; 267 stdout.set_color(&color_bytes)?; 268 for byte in bytes { 269 let Some(byte) = byte else { break }; 270 if inline == 0 { 271 write!(stdout, "{:width$} ", "")?; 272 } else { 273 write!(stdout, " ")?; 274 } 275 write!(stdout, "{byte:02x}")?; 276 inline += 1; 277 if inline == inline_bytes { 278 writeln!(stdout)?; 279 inline = 0; 280 } 281 } 282 stdout.reset()?; 283 if inline > 0 { 284 writeln!(stdout)?; 285 } 286 } 287 288 // And now finally after an instruction is printed try to 289 // collect any "decorations" or annotations for this 290 // instruction. This is for example the address map, stack maps, 291 // etc. 292 // 293 // Once they're collected then print them after the instruction 294 // attempting to use some unicode characters to make it easier 295 // to read/scan. 296 let mut decorations = Vec::new(); 297 decorator.decorate(address, &mut decorations); 298 299 let print_whitespace_to_decoration = |stdout: &mut StandardStream| -> Result<()> { 300 write!(stdout, "{:width$} ", "")?; 301 if self.bytes { 302 for _ in 0..inline_bytes + 1 { 303 write!(stdout, " ")?; 304 } 305 } 306 Ok(()) 307 }; 308 for (i, decoration) in decorations.iter().enumerate() { 309 print_whitespace_to_decoration(&mut stdout)?; 310 let mut color = ColorSpec::new(); 311 color.set_fg(Some(Color::Cyan)); 312 stdout.set_color(&color)?; 313 let final_decoration = i == decorations.len() - 1; 314 if !final_decoration { 315 write!(stdout, "├")?; 316 } else { 317 write!(stdout, "╰")?; 318 } 319 for (i, line) in decoration.lines().enumerate() { 320 if i == 0 { 321 write!(stdout, "─╼ ")?; 322 } else { 323 print_whitespace_to_decoration(&mut stdout)?; 324 if final_decoration { 325 write!(stdout, " ")?; 326 } else { 327 write!(stdout, "│ ")?; 328 } 329 } 330 writeln!(stdout, "{line}")?; 331 } 332 stdout.reset()?; 333 } 334 } 335 } 336 Ok(()) 337 } 338 339 /// Disassembles `func` contained within `elf` returning a list of 340 /// instructions that represent the function. 341 fn disas(&self, elf: &ElfFile64<'_, Endianness>, func: &[u8], addr: u64) -> Result<Vec<Inst>> { 342 let cranelift_target = match elf.architecture() { 343 Architecture::X86_64 => "x86_64", 344 Architecture::Aarch64 => "aarch64", 345 Architecture::S390x => "s390x", 346 Architecture::Riscv64 => { 347 let e_flags = match elf.flags() { 348 FileFlags::Elf { e_flags, .. } => e_flags, 349 _ => bail!("not an ELF file"), 350 }; 351 if e_flags & (obj::EF_WASMTIME_PULLEY32 | obj::EF_WASMTIME_PULLEY64) != 0 { 352 return self.disas_pulley(func, addr); 353 } else { 354 "riscv64" 355 } 356 } 357 other => bail!("unknown architecture {other:?}"), 358 }; 359 let builder = 360 lookup_by_name(cranelift_target).context("failed to load cranelift ISA builder")?; 361 let flags = cranelift_codegen::settings::builder(); 362 let isa = builder.finish(Flags::new(flags))?; 363 let isa = &*isa; 364 let capstone = isa 365 .to_capstone() 366 .context("failed to create a capstone disassembler")?; 367 368 let insts = capstone 369 .disasm_all(func, addr)? 370 .into_iter() 371 .map(|inst| { 372 let detail = capstone.insn_detail(&inst).ok(); 373 let detail = detail.as_ref(); 374 let is_jump = detail 375 .map(|d| { 376 d.groups() 377 .iter() 378 .find(|g| g.0 as u32 == CS_GRP_JUMP) 379 .is_some() 380 }) 381 .unwrap_or(false); 382 383 let is_return = detail 384 .map(|d| { 385 d.groups() 386 .iter() 387 .find(|g| g.0 as u32 == CS_GRP_RET) 388 .is_some() 389 }) 390 .unwrap_or(false); 391 392 let disassembly = match (inst.mnemonic(), inst.op_str()) { 393 (Some(i), Some(o)) => { 394 if o.is_empty() { 395 format!("{i}") 396 } else { 397 format!("{i:7} {o}") 398 } 399 } 400 (Some(i), None) => format!("{i}"), 401 _ => unreachable!(), 402 }; 403 404 let address = inst.address(); 405 Inst { 406 address, 407 is_jump, 408 is_return, 409 bytes: inst.bytes().to_vec(), 410 disassembly, 411 } 412 }) 413 .collect::<Vec<_>>(); 414 Ok(insts) 415 } 416 417 /// Same as `dias` above, but just for Pulley. 418 fn disas_pulley(&self, func: &[u8], addr: u64) -> Result<Vec<Inst>> { 419 let mut result = vec![]; 420 421 let mut disas = Disassembler::new(func); 422 disas.offsets(false); 423 disas.hexdump(false); 424 disas.start_offset(usize::try_from(addr).unwrap()); 425 let mut decoder = Decoder::new(); 426 let mut last_disas_pos = 0; 427 loop { 428 let start_addr = disas.bytecode().position(); 429 430 match decoder.decode_one(&mut disas) { 431 // If we got EOF at the initial position, then we're done disassembling. 432 Err(DecodingError::UnexpectedEof { position }) if position == start_addr => break, 433 434 // Otherwise, propagate the error. 435 Err(e) => { 436 return Err(e).context("failed to disassembly pulley bytecode"); 437 } 438 439 Ok(()) => { 440 let bytes_range = start_addr..disas.bytecode().position(); 441 let disassembly = disas.disas()[last_disas_pos..].trim(); 442 last_disas_pos = disas.disas().len(); 443 let address = u64::try_from(start_addr).unwrap() + addr; 444 let is_jump = disassembly.contains("jump") || disassembly.contains("br_"); 445 let is_return = disassembly == "ret"; 446 result.push(Inst { 447 bytes: func[bytes_range].to_vec(), 448 address, 449 is_jump, 450 is_return, 451 disassembly: disassembly.to_string(), 452 }); 453 } 454 } 455 } 456 457 Ok(result) 458 } 459 460 /// Helper to read the input bytes of the `*.cwasm` handling stdin 461 /// automatically. 462 fn read_cwasm(&self) -> Result<Vec<u8>> { 463 if let Some(path) = &self.cwasm { 464 if path != Path::new("-") { 465 return std::fs::read(path).with_context(|| format!("failed to read {path:?}")); 466 } 467 } 468 469 let mut stdin = Vec::new(); 470 std::io::stdin() 471 .read_to_end(&mut stdin) 472 .context("failed to read stdin")?; 473 Ok(stdin) 474 } 475 } 476 477 /// Helper structure to package up metadata about an instruction. 478 struct Inst { 479 address: u64, 480 is_jump: bool, 481 is_return: bool, 482 disassembly: String, 483 bytes: Vec<u8>, 484 } 485 486 #[derive(clap::ValueEnum, Clone, Copy, PartialEq, Eq)] 487 enum Func { 488 All, 489 Wasm, 490 Trampoline, 491 Builtin, 492 Libcall, 493 } 494 495 struct Decorator<'a> { 496 objdump: &'a ObjdumpCommand, 497 addrmap: Option<Peekable<Box<dyn Iterator<Item = (u32, FilePos)> + 'a>>>, 498 traps: Option<Peekable<Box<dyn Iterator<Item = (u32, Trap)> + 'a>>>, 499 stack_maps: Option<Peekable<Box<dyn Iterator<Item = (u32, StackMap<'a>)> + 'a>>>, 500 } 501 502 impl Decorator<'_> { 503 fn decorate(&mut self, address: u64, list: &mut Vec<String>) { 504 self.addrmap(address, list); 505 self.traps(address, list); 506 self.stack_maps(address, list); 507 } 508 509 fn addrmap(&mut self, address: u64, list: &mut Vec<String>) { 510 if !self.objdump.addrmap() { 511 return; 512 } 513 let Some(addrmap) = &mut self.addrmap else { 514 return; 515 }; 516 while let Some((addr, pos)) = addrmap.next_if(|(addr, _pos)| u64::from(*addr) <= address) { 517 if u64::from(addr) != address { 518 continue; 519 } 520 if let Some(offset) = pos.file_offset() { 521 list.push(format!("addrmap: {offset:#x}")); 522 } 523 } 524 } 525 526 fn traps(&mut self, address: u64, list: &mut Vec<String>) { 527 if !self.objdump.traps() { 528 return; 529 } 530 let Some(traps) = &mut self.traps else { 531 return; 532 }; 533 while let Some((addr, trap)) = traps.next_if(|(addr, _pos)| u64::from(*addr) <= address) { 534 if u64::from(addr) != address { 535 continue; 536 } 537 list.push(format!("trap: {trap:?}")); 538 } 539 } 540 541 fn stack_maps(&mut self, address: u64, list: &mut Vec<String>) { 542 if !self.objdump.stack_maps() { 543 return; 544 } 545 let Some(stack_maps) = &mut self.stack_maps else { 546 return; 547 }; 548 while let Some((addr, stack_map)) = 549 stack_maps.next_if(|(addr, _pos)| u64::from(*addr) <= address) 550 { 551 if u64::from(addr) != address { 552 continue; 553 } 554 list.push(format!( 555 "stack_map: frame_size={}, frame_offsets={:?}", 556 stack_map.frame_size(), 557 stack_map.offsets().collect::<Vec<_>>() 558 )); 559 } 560 } 561 } 562