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