xref: /wasmtime-44.0.1/src/commands/objdump.rs (revision 4c8edb95)
1 //! Implementation of the `wasmtime objdump` CLI command.
2 
3 use anyhow::{Context, Result, bail};
4 use capstone::InsnGroupType::{CS_GRP_JUMP, CS_GRP_RET};
5 use clap::Parser;
6 use cranelift_codegen::isa::lookup_by_name;
7 use cranelift_codegen::settings::Flags;
8 use object::read::elf::ElfFile64;
9 use object::{Architecture, Endianness, FileFlags, Object, ObjectSection, ObjectSymbol};
10 use pulley_interpreter::decode::{Decoder, DecodingError, OpVisitor};
11 use pulley_interpreter::disas::Disassembler;
12 use std::io::{IsTerminal, Read, Write};
13 use std::iter::{self, Peekable};
14 use std::path::{Path, PathBuf};
15 use termcolor::{Color, ColorChoice, ColorSpec, StandardStream, WriteColor};
16 use wasmtime::Engine;
17 use 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)]
22 pub 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 
70 fn 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 
78 impl 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.
478 struct 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)]
487 enum Func {
488     All,
489     Wasm,
490     Trampoline,
491     Builtin,
492     Libcall,
493 }
494 
495 struct 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 
502 impl 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 }
562