Skip to main content

wasmtime_cli/commands/
objdump.rs

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