Skip to main content

wasmtime_cli/commands/
objdump.rs

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