Skip to main content

wasmtime_cli/commands/
objdump.rs

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