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::{ 18 FilePos, FrameInstPos, FrameStackShape, FrameStateSlot, FrameTable, FrameTableDescriptorIndex, 19 StackMap, Trap, obj, 20 }; 21 use wasmtime_unwinder::{ExceptionHandler, ExceptionTable}; 22 23 /// A helper utility in wasmtime to explore the compiled object file format of 24 /// a `*.cwasm` file. 25 #[derive(Parser)] 26 pub struct ObjdumpCommand { 27 /// The path to a compiled `*.cwasm` file. 28 /// 29 /// If this is `-` or not provided then stdin is used as input. 30 cwasm: Option<PathBuf>, 31 32 /// Whether or not to display function/instruction addresses. 33 #[arg(long)] 34 addresses: bool, 35 36 /// Whether or not to try to only display addresses of instruction jump 37 /// targets. 38 #[arg(long)] 39 address_jumps: bool, 40 41 /// What functions should be printed 42 #[arg(long, default_value = "wasm", value_name = "KIND")] 43 funcs: Vec<Func>, 44 45 /// String filter to apply to function names to only print some functions. 46 #[arg(long, value_name = "STR")] 47 filter: Option<String>, 48 49 /// Whether or not instruction bytes are disassembled. 50 #[arg(long)] 51 bytes: bool, 52 53 /// Whether or not to use color. 54 #[arg(long, default_value = "auto")] 55 color: ColorChoice, 56 57 /// Whether or not to interleave instructions with address maps. 58 #[arg(long, require_equals = true, value_name = "true|false")] 59 addrmap: Option<Option<bool>>, 60 61 /// Column width of how large an address is rendered as. 62 #[arg(long, default_value = "10", value_name = "N")] 63 address_width: usize, 64 65 /// Whether or not to show information about what instructions can trap. 66 #[arg(long, require_equals = true, value_name = "true|false")] 67 traps: Option<Option<bool>>, 68 69 /// Whether or not to show information about stack maps. 70 #[arg(long, require_equals = true, value_name = "true|false")] 71 stack_maps: Option<Option<bool>>, 72 73 /// Whether or not to show information about exception tables. 74 #[arg(long, require_equals = true, value_name = "true|false")] 75 exception_tables: Option<Option<bool>>, 76 77 /// Whether or not to show information about frame tables. 78 #[arg(long, require_equals = true, value_name = "true|false")] 79 frame_tables: Option<Option<bool>>, 80 } 81 82 fn optional_flag_with_default(flag: Option<Option<bool>>, default: bool) -> bool { 83 match flag { 84 None => default, 85 Some(None) => true, 86 Some(Some(val)) => val, 87 } 88 } 89 90 impl ObjdumpCommand { 91 fn addrmap(&self) -> bool { 92 optional_flag_with_default(self.addrmap, false) 93 } 94 95 fn traps(&self) -> bool { 96 optional_flag_with_default(self.traps, true) 97 } 98 99 fn stack_maps(&self) -> bool { 100 optional_flag_with_default(self.stack_maps, true) 101 } 102 103 fn exception_tables(&self) -> bool { 104 optional_flag_with_default(self.exception_tables, true) 105 } 106 107 fn frame_tables(&self) -> bool { 108 optional_flag_with_default(self.frame_tables, true) 109 } 110 111 /// Executes the command. 112 pub fn execute(self) -> Result<()> { 113 // Setup stdout handling color options. Also build some variables used 114 // below to configure colors of certain items. 115 let mut choice = self.color; 116 if choice == ColorChoice::Auto && !std::io::stdout().is_terminal() { 117 choice = ColorChoice::Never; 118 } 119 let mut stdout = StandardStream::stdout(choice); 120 121 let mut color_address = ColorSpec::new(); 122 color_address.set_bold(true).set_fg(Some(Color::Yellow)); 123 let mut color_bytes = ColorSpec::new(); 124 color_bytes.set_fg(Some(Color::Magenta)); 125 126 let bytes = self.read_cwasm()?; 127 128 // Double-check this is a `*.cwasm` 129 if Engine::detect_precompiled(&bytes).is_none() { 130 bail!("not a `*.cwasm` file from wasmtime: {:?}", self.cwasm); 131 } 132 133 // Parse the input as an ELF file, extract the `.text` section. 134 let elf = ElfFile64::<Endianness>::parse(&bytes)?; 135 let text = elf 136 .section_by_name(".text") 137 .context("missing .text section")?; 138 let text = text.data()?; 139 140 // Build the helper that'll get used to attach decorations/annotations 141 // to various instructions. 142 let mut decorator = Decorator { 143 addrmap: elf 144 .section_by_name(obj::ELF_WASMTIME_ADDRMAP) 145 .and_then(|section| section.data().ok()) 146 .and_then(|bytes| wasmtime_environ::iterate_address_map(bytes)) 147 .map(|i| (Box::new(i) as Box<dyn Iterator<Item = _>>).peekable()), 148 traps: elf 149 .section_by_name(obj::ELF_WASMTIME_TRAPS) 150 .and_then(|section| section.data().ok()) 151 .and_then(|bytes| wasmtime_environ::iterate_traps(bytes)) 152 .map(|i| (Box::new(i) as Box<dyn Iterator<Item = _>>).peekable()), 153 stack_maps: elf 154 .section_by_name(obj::ELF_WASMTIME_STACK_MAP) 155 .and_then(|section| section.data().ok()) 156 .and_then(|bytes| StackMap::iter(bytes)) 157 .map(|i| (Box::new(i) as Box<dyn Iterator<Item = _>>).peekable()), 158 exception_tables: elf 159 .section_by_name(obj::ELF_WASMTIME_EXCEPTIONS) 160 .and_then(|section| section.data().ok()) 161 .and_then(|bytes| ExceptionTable::parse(bytes).ok()) 162 .map(|table| table.into_iter()) 163 .map(|i| (Box::new(i) as Box<dyn Iterator<Item = _>>).peekable()), 164 frame_tables: elf 165 .section_by_name(obj::ELF_WASMTIME_FRAMES) 166 .and_then(|section| section.data().ok()) 167 .and_then(|bytes| FrameTable::parse(bytes).ok()) 168 .map(|table| table.into_program_points()) 169 .map(|i| (Box::new(i) as Box<dyn Iterator<Item = _>>).peekable()), 170 171 frame_table_descriptors: elf 172 .section_by_name(obj::ELF_WASMTIME_FRAMES) 173 .and_then(|section| section.data().ok()) 174 .and_then(|bytes| FrameTable::parse(bytes).ok()), 175 176 objdump: &self, 177 }; 178 179 // Iterate over all symbols which will be functions for a cwasm and 180 // we'll disassemble them all. 181 let mut first = true; 182 for sym in elf.symbols() { 183 let name = match sym.name() { 184 Ok(name) => name, 185 Err(_) => continue, 186 }; 187 let bytes = &text[sym.address() as usize..][..sym.size() as usize]; 188 189 let kind = if name.starts_with("wasmtime_builtin") { 190 Func::Builtin 191 } else if name.contains("]::function[") { 192 Func::Wasm 193 } else if name.contains("trampoline") 194 || name.ends_with("_array_call") 195 || name.ends_with("_wasm_call") 196 { 197 Func::Trampoline 198 } else if name.contains("libcall") || name.starts_with("component") { 199 Func::Libcall 200 } else { 201 panic!("unknown symbol: {name}") 202 }; 203 204 // Apply any filters, if provided, to this function to look at just 205 // one function in the disassembly. 206 if self.funcs.is_empty() { 207 if kind != Func::Wasm { 208 continue; 209 } 210 } else { 211 if !(self.funcs.contains(&Func::All) || self.funcs.contains(&kind)) { 212 continue; 213 } 214 } 215 if let Some(filter) = &self.filter { 216 if !name.contains(filter) { 217 continue; 218 } 219 } 220 221 // Place a blank line between functions. 222 if first { 223 first = false; 224 } else { 225 writeln!(stdout)?; 226 } 227 228 // Print the function's address, if so desired. Then print the 229 // function name. 230 if self.addresses { 231 stdout.set_color(color_address.clone().set_bold(true))?; 232 write!(stdout, "{:08x} ", sym.address())?; 233 stdout.reset()?; 234 } 235 stdout.set_color(ColorSpec::new().set_bold(true).set_fg(Some(Color::Green)))?; 236 write!(stdout, "{name}")?; 237 stdout.reset()?; 238 writeln!(stdout, ":")?; 239 240 // Tracking variables for rough heuristics of printing targets of 241 // jump instructions for `--address-jumps` mode. 242 let mut prev_jump = false; 243 let mut write_offsets = false; 244 245 for inst in self.disas(&elf, bytes, sym.address())? { 246 let Inst { 247 address, 248 is_jump, 249 is_return, 250 disassembly: disas, 251 bytes, 252 } = inst; 253 254 // Generate an infinite list of bytes to make printing below 255 // easier, but only limit `inline_bytes` to get printed before 256 // an instruction. 257 let mut bytes = bytes.iter().map(Some).chain(iter::repeat(None)); 258 let inline_bytes = 9; 259 let width = self.address_width; 260 261 // Collect any "decorations" or annotations for this 262 // instruction. This includes the address map, stack 263 // maps, exception handlers, etc. 264 // 265 // Once they're collected then we print them before or 266 // after the instruction attempting to use some 267 // unicode characters to make it easier to read/scan. 268 // 269 // Note that some decorations occur "before" an 270 // instruction: for example, exception handler entries 271 // logically occur at the return point after a call, 272 // so "before" the instruction following the call. 273 let mut pre_decorations = Vec::new(); 274 let mut post_decorations = Vec::new(); 275 decorator.decorate(address, &mut pre_decorations, &mut post_decorations); 276 277 let print_whitespace_to_decoration = |stdout: &mut StandardStream| -> Result<()> { 278 write!(stdout, "{:width$} ", "")?; 279 if self.bytes { 280 for _ in 0..inline_bytes + 1 { 281 write!(stdout, " ")?; 282 } 283 } 284 Ok(()) 285 }; 286 287 let print_decorations = 288 |stdout: &mut StandardStream, decorations: Vec<String>| -> Result<()> { 289 for (i, decoration) in decorations.iter().enumerate() { 290 print_whitespace_to_decoration(stdout)?; 291 let mut color = ColorSpec::new(); 292 color.set_fg(Some(Color::Cyan)); 293 stdout.set_color(&color)?; 294 let final_decoration = i == decorations.len() - 1; 295 if !final_decoration { 296 write!(stdout, "├")?; 297 } else { 298 write!(stdout, "╰")?; 299 } 300 for (i, line) in decoration.lines().enumerate() { 301 if i == 0 { 302 write!(stdout, "─╼ ")?; 303 } else { 304 print_whitespace_to_decoration(stdout)?; 305 if final_decoration { 306 write!(stdout, " ")?; 307 } else { 308 write!(stdout, "│ ")?; 309 } 310 } 311 writeln!(stdout, "{line}")?; 312 } 313 stdout.reset()?; 314 } 315 Ok(()) 316 }; 317 318 print_decorations(&mut stdout, pre_decorations)?; 319 320 // Some instructions may disassemble to multiple lines, such as 321 // `br_table` with Pulley. Handle separate lines per-instruction 322 // here. 323 for (i, line) in disas.lines().enumerate() { 324 let print_address = self.addresses 325 || (self.address_jumps && (write_offsets || (prev_jump && !is_jump))); 326 if i == 0 && print_address { 327 stdout.set_color(&color_address)?; 328 write!(stdout, "{address:>width$x}: ")?; 329 stdout.reset()?; 330 } else { 331 write!(stdout, "{:width$} ", "")?; 332 } 333 334 // If we're printing inline bytes then print up to 335 // `inline_bytes` of instruction data, and any remaining 336 // data will go on the next line, if any, or after the 337 // instruction below. 338 if self.bytes { 339 stdout.set_color(&color_bytes)?; 340 for byte in bytes.by_ref().take(inline_bytes) { 341 match byte { 342 Some(byte) => write!(stdout, "{byte:02x} ")?, 343 None => write!(stdout, " ")?, 344 } 345 } 346 write!(stdout, " ")?; 347 stdout.reset()?; 348 } 349 350 writeln!(stdout, "{line}")?; 351 } 352 353 // Flip write_offsets to true once we've seen a `ret`, as 354 // instructions that follow the return are often related to trap 355 // tables. 356 write_offsets |= is_return; 357 prev_jump = is_jump; 358 359 // After the instruction is printed then finish printing the 360 // instruction bytes if any are present. Still limit to 361 // `inline_bytes` per line. 362 if self.bytes { 363 let mut inline = 0; 364 stdout.set_color(&color_bytes)?; 365 for byte in bytes { 366 let Some(byte) = byte else { break }; 367 if inline == 0 { 368 write!(stdout, "{:width$} ", "")?; 369 } else { 370 write!(stdout, " ")?; 371 } 372 write!(stdout, "{byte:02x}")?; 373 inline += 1; 374 if inline == inline_bytes { 375 writeln!(stdout)?; 376 inline = 0; 377 } 378 } 379 stdout.reset()?; 380 if inline > 0 { 381 writeln!(stdout)?; 382 } 383 } 384 385 print_decorations(&mut stdout, post_decorations)?; 386 } 387 } 388 Ok(()) 389 } 390 391 /// Disassembles `func` contained within `elf` returning a list of 392 /// instructions that represent the function. 393 fn disas(&self, elf: &ElfFile64<'_, Endianness>, func: &[u8], addr: u64) -> Result<Vec<Inst>> { 394 let cranelift_target = match elf.architecture() { 395 Architecture::X86_64 => "x86_64", 396 Architecture::Aarch64 => "aarch64", 397 Architecture::S390x => "s390x", 398 Architecture::Riscv64 => { 399 let e_flags = match elf.flags() { 400 FileFlags::Elf { e_flags, .. } => e_flags, 401 _ => bail!("not an ELF file"), 402 }; 403 if e_flags & (obj::EF_WASMTIME_PULLEY32 | obj::EF_WASMTIME_PULLEY64) != 0 { 404 return self.disas_pulley(func, addr); 405 } else { 406 "riscv64" 407 } 408 } 409 other => bail!("unknown architecture {other:?}"), 410 }; 411 let builder = 412 lookup_by_name(cranelift_target).context("failed to load cranelift ISA builder")?; 413 let flags = cranelift_codegen::settings::builder(); 414 let isa = builder.finish(Flags::new(flags))?; 415 let isa = &*isa; 416 let capstone = isa 417 .to_capstone() 418 .context("failed to create a capstone disassembler")?; 419 420 let insts = capstone 421 .disasm_all(func, addr)? 422 .into_iter() 423 .map(|inst| { 424 let detail = capstone.insn_detail(&inst).ok(); 425 let detail = detail.as_ref(); 426 let is_jump = detail 427 .map(|d| { 428 d.groups() 429 .iter() 430 .find(|g| g.0 as u32 == CS_GRP_JUMP) 431 .is_some() 432 }) 433 .unwrap_or(false); 434 435 let is_return = detail 436 .map(|d| { 437 d.groups() 438 .iter() 439 .find(|g| g.0 as u32 == CS_GRP_RET) 440 .is_some() 441 }) 442 .unwrap_or(false); 443 444 let disassembly = match (inst.mnemonic(), inst.op_str()) { 445 (Some(i), Some(o)) => { 446 if o.is_empty() { 447 format!("{i}") 448 } else { 449 format!("{i:7} {o}") 450 } 451 } 452 (Some(i), None) => format!("{i}"), 453 _ => unreachable!(), 454 }; 455 456 let address = inst.address(); 457 Inst { 458 address, 459 is_jump, 460 is_return, 461 bytes: inst.bytes().to_vec(), 462 disassembly, 463 } 464 }) 465 .collect::<Vec<_>>(); 466 Ok(insts) 467 } 468 469 /// Same as `dias` above, but just for Pulley. 470 fn disas_pulley(&self, func: &[u8], addr: u64) -> Result<Vec<Inst>> { 471 let mut result = vec![]; 472 473 let mut disas = Disassembler::new(func); 474 disas.offsets(false); 475 disas.hexdump(false); 476 disas.start_offset(usize::try_from(addr).unwrap()); 477 let mut decoder = Decoder::new(); 478 let mut last_disas_pos = 0; 479 loop { 480 let start_addr = disas.bytecode().position(); 481 482 match decoder.decode_one(&mut disas) { 483 // If we got EOF at the initial position, then we're done disassembling. 484 Err(DecodingError::UnexpectedEof { position }) if position == start_addr => break, 485 486 // Otherwise, propagate the error. 487 Err(e) => { 488 return Err(e).context("failed to disassembly pulley bytecode"); 489 } 490 491 Ok(()) => { 492 let bytes_range = start_addr..disas.bytecode().position(); 493 let disassembly = disas.disas()[last_disas_pos..].trim(); 494 last_disas_pos = disas.disas().len(); 495 let address = u64::try_from(start_addr).unwrap() + addr; 496 let is_jump = disassembly.contains("jump") || disassembly.contains("br_"); 497 let is_return = disassembly == "ret"; 498 result.push(Inst { 499 bytes: func[bytes_range].to_vec(), 500 address, 501 is_jump, 502 is_return, 503 disassembly: disassembly.to_string(), 504 }); 505 } 506 } 507 } 508 509 Ok(result) 510 } 511 512 /// Helper to read the input bytes of the `*.cwasm` handling stdin 513 /// automatically. 514 fn read_cwasm(&self) -> Result<Vec<u8>> { 515 if let Some(path) = &self.cwasm { 516 if path != Path::new("-") { 517 return std::fs::read(path).with_context(|| format!("failed to read {path:?}")); 518 } 519 } 520 521 let mut stdin = Vec::new(); 522 std::io::stdin() 523 .read_to_end(&mut stdin) 524 .context("failed to read stdin")?; 525 Ok(stdin) 526 } 527 } 528 529 /// Helper structure to package up metadata about an instruction. 530 struct Inst { 531 address: u64, 532 is_jump: bool, 533 is_return: bool, 534 disassembly: String, 535 bytes: Vec<u8>, 536 } 537 538 #[derive(clap::ValueEnum, Clone, Copy, PartialEq, Eq)] 539 enum Func { 540 All, 541 Wasm, 542 Trampoline, 543 Builtin, 544 Libcall, 545 } 546 547 struct Decorator<'a> { 548 objdump: &'a ObjdumpCommand, 549 addrmap: Option<Peekable<Box<dyn Iterator<Item = (u32, FilePos)> + 'a>>>, 550 traps: Option<Peekable<Box<dyn Iterator<Item = (u32, Trap)> + 'a>>>, 551 stack_maps: Option<Peekable<Box<dyn Iterator<Item = (u32, StackMap<'a>)> + 'a>>>, 552 exception_tables: 553 Option<Peekable<Box<dyn Iterator<Item = (u32, Option<u32>, Vec<ExceptionHandler>)> + 'a>>>, 554 frame_tables: Option< 555 Peekable< 556 Box< 557 dyn Iterator< 558 Item = ( 559 u32, 560 FrameInstPos, 561 Vec<(u32, FrameTableDescriptorIndex, FrameStackShape)>, 562 ), 563 > + 'a, 564 >, 565 >, 566 >, 567 568 frame_table_descriptors: Option<FrameTable<'a>>, 569 } 570 571 impl Decorator<'_> { 572 fn decorate(&mut self, address: u64, pre_list: &mut Vec<String>, post_list: &mut Vec<String>) { 573 self.addrmap(address, post_list); 574 self.traps(address, post_list); 575 self.stack_maps(address, post_list); 576 self.exception_table(address, pre_list); 577 self.frame_table(address, pre_list, post_list); 578 } 579 580 fn addrmap(&mut self, address: u64, list: &mut Vec<String>) { 581 if !self.objdump.addrmap() { 582 return; 583 } 584 let Some(addrmap) = &mut self.addrmap else { 585 return; 586 }; 587 while let Some((addr, pos)) = addrmap.next_if(|(addr, _pos)| u64::from(*addr) <= address) { 588 if u64::from(addr) != address { 589 continue; 590 } 591 if let Some(offset) = pos.file_offset() { 592 list.push(format!("addrmap: {offset:#x}")); 593 } 594 } 595 } 596 597 fn traps(&mut self, address: u64, list: &mut Vec<String>) { 598 if !self.objdump.traps() { 599 return; 600 } 601 let Some(traps) = &mut self.traps else { 602 return; 603 }; 604 while let Some((addr, trap)) = traps.next_if(|(addr, _pos)| u64::from(*addr) <= address) { 605 if u64::from(addr) != address { 606 continue; 607 } 608 list.push(format!("trap: {trap:?}")); 609 } 610 } 611 612 fn stack_maps(&mut self, address: u64, list: &mut Vec<String>) { 613 if !self.objdump.stack_maps() { 614 return; 615 } 616 let Some(stack_maps) = &mut self.stack_maps else { 617 return; 618 }; 619 while let Some((addr, stack_map)) = 620 stack_maps.next_if(|(addr, _pos)| u64::from(*addr) <= address) 621 { 622 if u64::from(addr) != address { 623 continue; 624 } 625 list.push(format!( 626 "stack_map: frame_size={}, frame_offsets={:?}", 627 stack_map.frame_size(), 628 stack_map.offsets().collect::<Vec<_>>() 629 )); 630 } 631 } 632 633 fn exception_table(&mut self, address: u64, list: &mut Vec<String>) { 634 if !self.objdump.exception_tables() { 635 return; 636 } 637 let Some(exception_tables) = &mut self.exception_tables else { 638 return; 639 }; 640 while let Some((addr, frame_offset, handlers)) = 641 exception_tables.next_if(|(addr, _, _)| u64::from(*addr) <= address) 642 { 643 if u64::from(addr) != address { 644 continue; 645 } 646 if let Some(frame_offset) = frame_offset { 647 list.push(format!( 648 "exception frame offset: SP = FP - 0x{frame_offset:x}", 649 )); 650 } 651 for handler in &handlers { 652 let tag = match handler.tag { 653 Some(tag) => format!("tag={tag}"), 654 None => "default handler".to_string(), 655 }; 656 let context = match handler.context_sp_offset { 657 Some(offset) => format!("context at [SP+0x{offset:x}]"), 658 None => "no dynamic context".to_string(), 659 }; 660 list.push(format!( 661 "exception handler: {tag}, {context}, handler=0x{:x}", 662 handler.handler_offset 663 )); 664 } 665 } 666 } 667 668 fn frame_table( 669 &mut self, 670 address: u64, 671 pre_list: &mut Vec<String>, 672 post_list: &mut Vec<String>, 673 ) { 674 if !self.objdump.frame_tables() { 675 return; 676 } 677 let (Some(frame_table_iter), Some(frame_tables)) = 678 (&mut self.frame_tables, &self.frame_table_descriptors) 679 else { 680 return; 681 }; 682 683 while let Some((addr, pos, frames)) = 684 frame_table_iter.next_if(|(addr, _, _)| u64::from(*addr) <= address) 685 { 686 if u64::from(addr) != address { 687 continue; 688 } 689 let list = match pos { 690 // N.B.: the "post" position means that we are 691 // attached to the end of the previous instruction 692 // (its "post"); which means that from this 693 // instruction's PoV, we print before the instruction 694 // (the "pre list"). And vice versa for the "pre" 695 // position. Hence the reversal here. 696 FrameInstPos::Post => &mut *pre_list, 697 FrameInstPos::Pre => &mut *post_list, 698 }; 699 let pos = match pos { 700 FrameInstPos::Post => "after previous inst", 701 FrameInstPos::Pre => "before next inst", 702 }; 703 for (wasm_pc, frame_descriptor, stack_shape) in frames { 704 let (frame_descriptor_data, offset) = 705 frame_tables.frame_descriptor(frame_descriptor).unwrap(); 706 let frame_descriptor = FrameStateSlot::parse(frame_descriptor_data).unwrap(); 707 708 let local_shape = Self::describe_local_shape(&frame_descriptor); 709 let stack_shape = Self::describe_stack_shape(&frame_descriptor, stack_shape); 710 let func_key = frame_descriptor.func_key(); 711 list.push(format!("debug frame state ({pos}): func key {func_key:?}, wasm PC {wasm_pc}, slot at FP-0x{offset:x}, locals {local_shape}, stack {stack_shape}")); 712 } 713 } 714 } 715 716 fn describe_local_shape(desc: &FrameStateSlot<'_>) -> String { 717 let mut parts = vec![]; 718 for (offset, ty) in desc.locals() { 719 parts.push(format!("{ty:?} @ slot+0x{:x}", offset.offset())); 720 } 721 parts.join(", ") 722 } 723 724 fn describe_stack_shape(desc: &FrameStateSlot<'_>, shape: FrameStackShape) -> String { 725 let mut parts = vec![]; 726 for (offset, ty) in desc.stack(shape) { 727 parts.push(format!("{ty:?} @ slot+0x{:x}", offset.offset())); 728 } 729 parts.reverse(); 730 parts.join(", ") 731 } 732 } 733