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 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;
17use wasmtime_environ::{FilePos, StackMap, Trap, obj};
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
70fn optional_flag_with_default(flag: Option<Option<bool>>, default: bool) -> bool {
71    match flag {
72        None => default,
73        Some(None) => true,
74        Some(Some(val)) => val,
75    }
76}
77
78impl ObjdumpCommand {
79    fn addrmap(&self) -> bool {
80        optional_flag_with_default(self.addrmap, false)
81    }
82
83    fn traps(&self) -> bool {
84        optional_flag_with_default(self.traps, true)
85    }
86
87    fn stack_maps(&self) -> bool {
88        optional_flag_with_default(self.stack_maps, true)
89    }
90
91    /// Executes the command.
92    pub fn execute(self) -> Result<()> {
93        // Setup stdout handling color options. Also build some variables used
94        // below to configure colors of certain items.
95        let mut choice = self.color;
96        if choice == ColorChoice::Auto && !std::io::stdout().is_terminal() {
97            choice = ColorChoice::Never;
98        }
99        let mut stdout = StandardStream::stdout(choice);
100
101        let mut color_address = ColorSpec::new();
102        color_address.set_bold(true).set_fg(Some(Color::Yellow));
103        let mut color_bytes = ColorSpec::new();
104        color_bytes.set_fg(Some(Color::Magenta));
105
106        let bytes = self.read_cwasm()?;
107
108        // Double-check this is a `*.cwasm`
109        if Engine::detect_precompiled(&bytes).is_none() {
110            bail!("not a `*.cwasm` file from wasmtime: {:?}", self.cwasm);
111        }
112
113        // Parse the input as an ELF file, extract the `.text` section.
114        let elf = ElfFile64::<Endianness>::parse(&bytes)?;
115        let text = elf
116            .section_by_name(".text")
117            .context("missing .text section")?;
118        let text = text.data()?;
119
120        // Build the helper that'll get used to attach decorations/annotations
121        // to various instructions.
122        let mut decorator = Decorator {
123            addrmap: elf
124                .section_by_name(obj::ELF_WASMTIME_ADDRMAP)
125                .and_then(|section| section.data().ok())
126                .and_then(|bytes| wasmtime_environ::iterate_address_map(bytes))
127                .map(|i| (Box::new(i) as Box<dyn Iterator<Item = _>>).peekable()),
128            traps: elf
129                .section_by_name(obj::ELF_WASMTIME_TRAPS)
130                .and_then(|section| section.data().ok())
131                .and_then(|bytes| wasmtime_environ::iterate_traps(bytes))
132                .map(|i| (Box::new(i) as Box<dyn Iterator<Item = _>>).peekable()),
133            stack_maps: elf
134                .section_by_name(obj::ELF_WASMTIME_STACK_MAP)
135                .and_then(|section| section.data().ok())
136                .and_then(|bytes| StackMap::iter(bytes))
137                .map(|i| (Box::new(i) as Box<dyn Iterator<Item = _>>).peekable()),
138            objdump: &self,
139        };
140
141        // Iterate over all symbols which will be functions for a cwasm and
142        // we'll disassemble them all.
143        let mut first = true;
144        for sym in elf.symbols() {
145            let name = match sym.name() {
146                Ok(name) => name,
147                Err(_) => continue,
148            };
149            let bytes = &text[sym.address() as usize..][..sym.size() as usize];
150
151            let kind = if name.starts_with("wasmtime_builtin") {
152                Func::Builtin
153            } else if name.contains("]::function[") {
154                Func::Wasm
155            } else if name.contains("trampoline")
156                || name.ends_with("_array_call")
157                || name.ends_with("_wasm_call")
158            {
159                Func::Trampoline
160            } else if name.contains("libcall") || name.starts_with("component") {
161                Func::Libcall
162            } else {
163                panic!("unknown symbol: {name}")
164            };
165
166            // Apply any filters, if provided, to this function to look at just
167            // one function in the disassembly.
168            if self.funcs.is_empty() {
169                if kind != Func::Wasm {
170                    continue;
171                }
172            } else {
173                if !(self.funcs.contains(&Func::All) || self.funcs.contains(&kind)) {
174                    continue;
175                }
176            }
177            if let Some(filter) = &self.filter {
178                if !name.contains(filter) {
179                    continue;
180                }
181            }
182
183            // Place a blank line between functions.
184            if first {
185                first = false;
186            } else {
187                writeln!(stdout)?;
188            }
189
190            // Print the function's address, if so desired. Then print the
191            // function name.
192            if self.addresses {
193                stdout.set_color(color_address.clone().set_bold(true))?;
194                write!(stdout, "{:08x} ", sym.address())?;
195                stdout.reset()?;
196            }
197            stdout.set_color(ColorSpec::new().set_bold(true).set_fg(Some(Color::Green)))?;
198            write!(stdout, "{name}")?;
199            stdout.reset()?;
200            writeln!(stdout, ":")?;
201
202            // Tracking variables for rough heuristics of printing targets of
203            // jump instructions for `--address-jumps` mode.
204            let mut prev_jump = false;
205            let mut write_offsets = false;
206
207            for inst in self.disas(&elf, bytes, sym.address())? {
208                let Inst {
209                    address,
210                    is_jump,
211                    is_return,
212                    disassembly: disas,
213                    bytes,
214                } = inst;
215
216                // Generate an infinite list of bytes to make printing below
217                // easier, but only limit `inline_bytes` to get printed before
218                // an instruction.
219                let mut bytes = bytes.iter().map(Some).chain(iter::repeat(None));
220                let inline_bytes = 9;
221                let width = self.address_width;
222
223                // Some instructions may disassemble to multiple lines, such as
224                // `br_table` with Pulley. Handle separate lines per-instruction
225                // here.
226                for (i, line) in disas.lines().enumerate() {
227                    let print_address = self.addresses
228                        || (self.address_jumps && (write_offsets || (prev_jump && !is_jump)));
229                    if i == 0 && print_address {
230                        stdout.set_color(&color_address)?;
231                        write!(stdout, "{address:>width$x}: ")?;
232                        stdout.reset()?;
233                    } else {
234                        write!(stdout, "{:width$}  ", "")?;
235                    }
236
237                    // If we're printing inline bytes then print up to
238                    // `inline_bytes` of instruction data, and any remaining
239                    // data will go on the next line, if any, or after the
240                    // instruction below.
241                    if self.bytes {
242                        stdout.set_color(&color_bytes)?;
243                        for byte in bytes.by_ref().take(inline_bytes) {
244                            match byte {
245                                Some(byte) => write!(stdout, "{byte:02x} ")?,
246                                None => write!(stdout, "   ")?,
247                            }
248                        }
249                        write!(stdout, "  ")?;
250                        stdout.reset()?;
251                    }
252
253                    writeln!(stdout, "{line}")?;
254                }
255
256                // Flip write_offsets to true once we've seen a `ret`, as
257                // instructions that follow the return are often related to trap
258                // tables.
259                write_offsets |= is_return;
260                prev_jump = is_jump;
261
262                // After the instruction is printed then finish printing the
263                // instruction bytes if any are present. Still limit to
264                // `inline_bytes` per line.
265                if self.bytes {
266                    let mut inline = 0;
267                    stdout.set_color(&color_bytes)?;
268                    for byte in bytes {
269                        let Some(byte) = byte else { break };
270                        if inline == 0 {
271                            write!(stdout, "{:width$}  ", "")?;
272                        } else {
273                            write!(stdout, " ")?;
274                        }
275                        write!(stdout, "{byte:02x}")?;
276                        inline += 1;
277                        if inline == inline_bytes {
278                            writeln!(stdout)?;
279                            inline = 0;
280                        }
281                    }
282                    stdout.reset()?;
283                    if inline > 0 {
284                        writeln!(stdout)?;
285                    }
286                }
287
288                // And now finally after an instruction is printed try to
289                // collect any "decorations" or annotations for this
290                // instruction. This is for example the address map, stack maps,
291                // etc.
292                //
293                // Once they're collected then print them after the instruction
294                // attempting to use some unicode characters to make it easier
295                // to read/scan.
296                let mut decorations = Vec::new();
297                decorator.decorate(address, &mut decorations);
298
299                let print_whitespace_to_decoration = |stdout: &mut StandardStream| -> Result<()> {
300                    write!(stdout, "{:width$}  ", "")?;
301                    if self.bytes {
302                        for _ in 0..inline_bytes + 1 {
303                            write!(stdout, "   ")?;
304                        }
305                    }
306                    Ok(())
307                };
308                for (i, decoration) in decorations.iter().enumerate() {
309                    print_whitespace_to_decoration(&mut stdout)?;
310                    let mut color = ColorSpec::new();
311                    color.set_fg(Some(Color::Cyan));
312                    stdout.set_color(&color)?;
313                    let final_decoration = i == decorations.len() - 1;
314                    if !final_decoration {
315                        write!(stdout, "├")?;
316                    } else {
317                        write!(stdout, "╰")?;
318                    }
319                    for (i, line) in decoration.lines().enumerate() {
320                        if i == 0 {
321                            write!(stdout, "─╼ ")?;
322                        } else {
323                            print_whitespace_to_decoration(&mut stdout)?;
324                            if final_decoration {
325                                write!(stdout, "    ")?;
326                            } else {
327                                write!(stdout, "│   ")?;
328                            }
329                        }
330                        writeln!(stdout, "{line}")?;
331                    }
332                    stdout.reset()?;
333                }
334            }
335        }
336        Ok(())
337    }
338
339    /// Disassembles `func` contained within `elf` returning a list of
340    /// instructions that represent the function.
341    fn disas(&self, elf: &ElfFile64<'_, Endianness>, func: &[u8], addr: u64) -> Result<Vec<Inst>> {
342        let cranelift_target = match elf.architecture() {
343            Architecture::X86_64 => "x86_64",
344            Architecture::Aarch64 => "aarch64",
345            Architecture::S390x => "s390x",
346            Architecture::Riscv64 => {
347                let e_flags = match elf.flags() {
348                    FileFlags::Elf { e_flags, .. } => e_flags,
349                    _ => bail!("not an ELF file"),
350                };
351                if e_flags & (obj::EF_WASMTIME_PULLEY32 | obj::EF_WASMTIME_PULLEY64) != 0 {
352                    return self.disas_pulley(func, addr);
353                } else {
354                    "riscv64"
355                }
356            }
357            other => bail!("unknown architecture {other:?}"),
358        };
359        let builder =
360            lookup_by_name(cranelift_target).context("failed to load cranelift ISA builder")?;
361        let flags = cranelift_codegen::settings::builder();
362        let isa = builder.finish(Flags::new(flags))?;
363        let isa = &*isa;
364        let capstone = isa
365            .to_capstone()
366            .context("failed to create a capstone disassembler")?;
367
368        let insts = capstone
369            .disasm_all(func, addr)?
370            .into_iter()
371            .map(|inst| {
372                let detail = capstone.insn_detail(&inst).ok();
373                let detail = detail.as_ref();
374                let is_jump = detail
375                    .map(|d| {
376                        d.groups()
377                            .iter()
378                            .find(|g| g.0 as u32 == CS_GRP_JUMP)
379                            .is_some()
380                    })
381                    .unwrap_or(false);
382
383                let is_return = detail
384                    .map(|d| {
385                        d.groups()
386                            .iter()
387                            .find(|g| g.0 as u32 == CS_GRP_RET)
388                            .is_some()
389                    })
390                    .unwrap_or(false);
391
392                let disassembly = match (inst.mnemonic(), inst.op_str()) {
393                    (Some(i), Some(o)) => {
394                        if o.is_empty() {
395                            format!("{i}")
396                        } else {
397                            format!("{i:7} {o}")
398                        }
399                    }
400                    (Some(i), None) => format!("{i}"),
401                    _ => unreachable!(),
402                };
403
404                let address = inst.address();
405                Inst {
406                    address,
407                    is_jump,
408                    is_return,
409                    bytes: inst.bytes().to_vec(),
410                    disassembly,
411                }
412            })
413            .collect::<Vec<_>>();
414        Ok(insts)
415    }
416
417    /// Same as `dias` above, but just for Pulley.
418    fn disas_pulley(&self, func: &[u8], addr: u64) -> Result<Vec<Inst>> {
419        let mut result = vec![];
420
421        let mut disas = Disassembler::new(func);
422        disas.offsets(false);
423        disas.hexdump(false);
424        disas.start_offset(usize::try_from(addr).unwrap());
425        let mut decoder = Decoder::new();
426        let mut last_disas_pos = 0;
427        loop {
428            let start_addr = disas.bytecode().position();
429
430            match decoder.decode_one(&mut disas) {
431                // If we got EOF at the initial position, then we're done disassembling.
432                Err(DecodingError::UnexpectedEof { position }) if position == start_addr => break,
433
434                // Otherwise, propagate the error.
435                Err(e) => {
436                    return Err(e).context("failed to disassembly pulley bytecode");
437                }
438
439                Ok(()) => {
440                    let bytes_range = start_addr..disas.bytecode().position();
441                    let disassembly = disas.disas()[last_disas_pos..].trim();
442                    last_disas_pos = disas.disas().len();
443                    let address = u64::try_from(start_addr).unwrap() + addr;
444                    let is_jump = disassembly.contains("jump") || disassembly.contains("br_");
445                    let is_return = disassembly == "ret";
446                    result.push(Inst {
447                        bytes: func[bytes_range].to_vec(),
448                        address,
449                        is_jump,
450                        is_return,
451                        disassembly: disassembly.to_string(),
452                    });
453                }
454            }
455        }
456
457        Ok(result)
458    }
459
460    /// Helper to read the input bytes of the `*.cwasm` handling stdin
461    /// automatically.
462    fn read_cwasm(&self) -> Result<Vec<u8>> {
463        if let Some(path) = &self.cwasm {
464            if path != Path::new("-") {
465                return std::fs::read(path).with_context(|| format!("failed to read {path:?}"));
466            }
467        }
468
469        let mut stdin = Vec::new();
470        std::io::stdin()
471            .read_to_end(&mut stdin)
472            .context("failed to read stdin")?;
473        Ok(stdin)
474    }
475}
476
477/// Helper structure to package up metadata about an instruction.
478struct Inst {
479    address: u64,
480    is_jump: bool,
481    is_return: bool,
482    disassembly: String,
483    bytes: Vec<u8>,
484}
485
486#[derive(clap::ValueEnum, Clone, Copy, PartialEq, Eq)]
487enum Func {
488    All,
489    Wasm,
490    Trampoline,
491    Builtin,
492    Libcall,
493}
494
495struct Decorator<'a> {
496    objdump: &'a ObjdumpCommand,
497    addrmap: Option<Peekable<Box<dyn Iterator<Item = (u32, FilePos)> + 'a>>>,
498    traps: Option<Peekable<Box<dyn Iterator<Item = (u32, Trap)> + 'a>>>,
499    stack_maps: Option<Peekable<Box<dyn Iterator<Item = (u32, StackMap<'a>)> + 'a>>>,
500}
501
502impl Decorator<'_> {
503    fn decorate(&mut self, address: u64, list: &mut Vec<String>) {
504        self.addrmap(address, list);
505        self.traps(address, list);
506        self.stack_maps(address, list);
507    }
508
509    fn addrmap(&mut self, address: u64, list: &mut Vec<String>) {
510        if !self.objdump.addrmap() {
511            return;
512        }
513        let Some(addrmap) = &mut self.addrmap else {
514            return;
515        };
516        while let Some((addr, pos)) = addrmap.next_if(|(addr, _pos)| u64::from(*addr) <= address) {
517            if u64::from(addr) != address {
518                continue;
519            }
520            if let Some(offset) = pos.file_offset() {
521                list.push(format!("addrmap: {offset:#x}"));
522            }
523        }
524    }
525
526    fn traps(&mut self, address: u64, list: &mut Vec<String>) {
527        if !self.objdump.traps() {
528            return;
529        }
530        let Some(traps) = &mut self.traps else {
531            return;
532        };
533        while let Some((addr, trap)) = traps.next_if(|(addr, _pos)| u64::from(*addr) <= address) {
534            if u64::from(addr) != address {
535                continue;
536            }
537            list.push(format!("trap: {trap:?}"));
538        }
539    }
540
541    fn stack_maps(&mut self, address: u64, list: &mut Vec<String>) {
542        if !self.objdump.stack_maps() {
543            return;
544        }
545        let Some(stack_maps) = &mut self.stack_maps else {
546            return;
547        };
548        while let Some((addr, stack_map)) =
549            stack_maps.next_if(|(addr, _pos)| u64::from(*addr) <= address)
550        {
551            if u64::from(addr) != address {
552                continue;
553            }
554            list.push(format!(
555                "stack_map: frame_size={}, frame_offsets={:?}",
556                stack_map.frame_size(),
557                stack_map.offsets().collect::<Vec<_>>()
558            ));
559        }
560    }
561}