xref: /wasmtime-44.0.1/src/commands/objdump.rs (revision bedbcd85)
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