1use 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::{
18 FilePos, FrameInstPos, FrameStackShape, FrameStateSlot, FrameTable, FrameTableDescriptorIndex,
19 StackMap, Trap, obj,
20};
21use wasmtime_unwinder::{ExceptionHandler, ExceptionTable};
22
23#[derive(Parser)]
26pub struct ObjdumpCommand {
27 cwasm: Option<PathBuf>,
31
32 #[arg(long)]
34 addresses: bool,
35
36 #[arg(long)]
39 address_jumps: bool,
40
41 #[arg(long, default_value = "wasm", value_name = "KIND")]
43 funcs: Vec<Func>,
44
45 #[arg(long, value_name = "STR")]
47 filter: Option<String>,
48
49 #[arg(long)]
51 bytes: bool,
52
53 #[arg(long, default_value = "auto")]
55 color: ColorChoice,
56
57 #[arg(long, require_equals = true, value_name = "true|false")]
59 addrmap: Option<Option<bool>>,
60
61 #[arg(long, default_value = "10", value_name = "N")]
63 address_width: usize,
64
65 #[arg(long, require_equals = true, value_name = "true|false")]
67 traps: Option<Option<bool>>,
68
69 #[arg(long, require_equals = true, value_name = "true|false")]
71 stack_maps: Option<Option<bool>>,
72
73 #[arg(long, require_equals = true, value_name = "true|false")]
75 exception_tables: Option<Option<bool>>,
76
77 #[arg(long, require_equals = true, value_name = "true|false")]
79 frame_tables: Option<Option<bool>>,
80}
81
82fn optional_flag_with_default(flag: Option<Option<bool>>, default: bool) -> bool {
83 match flag {
84 None => default,
85 Some(None) => true,
86 Some(Some(val)) => val,
87 }
88}
89
90impl ObjdumpCommand {
91 fn addrmap(&self) -> bool {
92 optional_flag_with_default(self.addrmap, false)
93 }
94
95 fn traps(&self) -> bool {
96 optional_flag_with_default(self.traps, true)
97 }
98
99 fn stack_maps(&self) -> bool {
100 optional_flag_with_default(self.stack_maps, true)
101 }
102
103 fn exception_tables(&self) -> bool {
104 optional_flag_with_default(self.exception_tables, true)
105 }
106
107 fn frame_tables(&self) -> bool {
108 optional_flag_with_default(self.frame_tables, true)
109 }
110
111 pub fn execute(self) -> Result<()> {
113 let mut choice = self.color;
116 if choice == ColorChoice::Auto && !std::io::stdout().is_terminal() {
117 choice = ColorChoice::Never;
118 }
119 let mut stdout = StandardStream::stdout(choice);
120
121 let mut color_address = ColorSpec::new();
122 color_address.set_bold(true).set_fg(Some(Color::Yellow));
123 let mut color_bytes = ColorSpec::new();
124 color_bytes.set_fg(Some(Color::Magenta));
125
126 let bytes = self.read_cwasm()?;
127
128 if Engine::detect_precompiled(&bytes).is_none() {
130 bail!("not a `*.cwasm` file from wasmtime: {:?}", self.cwasm);
131 }
132
133 let elf = ElfFile64::<Endianness>::parse(&bytes)?;
135 let text = elf
136 .section_by_name(".text")
137 .context("missing .text section")?;
138 let text = text.data()?;
139
140 let mut decorator = Decorator {
143 addrmap: elf
144 .section_by_name(obj::ELF_WASMTIME_ADDRMAP)
145 .and_then(|section| section.data().ok())
146 .and_then(|bytes| wasmtime_environ::iterate_address_map(bytes))
147 .map(|i| (Box::new(i) as Box<dyn Iterator<Item = _>>).peekable()),
148 traps: elf
149 .section_by_name(obj::ELF_WASMTIME_TRAPS)
150 .and_then(|section| section.data().ok())
151 .and_then(|bytes| wasmtime_environ::iterate_traps(bytes))
152 .map(|i| (Box::new(i) as Box<dyn Iterator<Item = _>>).peekable()),
153 stack_maps: elf
154 .section_by_name(obj::ELF_WASMTIME_STACK_MAP)
155 .and_then(|section| section.data().ok())
156 .and_then(|bytes| StackMap::iter(bytes))
157 .map(|i| (Box::new(i) as Box<dyn Iterator<Item = _>>).peekable()),
158 exception_tables: elf
159 .section_by_name(obj::ELF_WASMTIME_EXCEPTIONS)
160 .and_then(|section| section.data().ok())
161 .and_then(|bytes| ExceptionTable::parse(bytes).ok())
162 .map(|table| table.into_iter())
163 .map(|i| (Box::new(i) as Box<dyn Iterator<Item = _>>).peekable()),
164 frame_tables: elf
165 .section_by_name(obj::ELF_WASMTIME_FRAMES)
166 .and_then(|section| section.data().ok())
167 .and_then(|bytes| FrameTable::parse(bytes).ok())
168 .map(|table| table.into_program_points())
169 .map(|i| (Box::new(i) as Box<dyn Iterator<Item = _>>).peekable()),
170
171 frame_table_descriptors: elf
172 .section_by_name(obj::ELF_WASMTIME_FRAMES)
173 .and_then(|section| section.data().ok())
174 .and_then(|bytes| FrameTable::parse(bytes).ok()),
175
176 objdump: &self,
177 };
178
179 let mut first = true;
182 for sym in elf.symbols() {
183 let name = match sym.name() {
184 Ok(name) => name,
185 Err(_) => continue,
186 };
187 let bytes = &text[sym.address() as usize..][..sym.size() as usize];
188
189 let kind = if name.starts_with("wasmtime_builtin") {
190 Func::Builtin
191 } else if name.contains("]::function[") {
192 Func::Wasm
193 } else if name.contains("trampoline")
194 || name.ends_with("_array_call")
195 || name.ends_with("_wasm_call")
196 {
197 Func::Trampoline
198 } else if name.contains("libcall") || name.starts_with("component") {
199 Func::Libcall
200 } else {
201 panic!("unknown symbol: {name}")
202 };
203
204 if self.funcs.is_empty() {
207 if kind != Func::Wasm {
208 continue;
209 }
210 } else {
211 if !(self.funcs.contains(&Func::All) || self.funcs.contains(&kind)) {
212 continue;
213 }
214 }
215 if let Some(filter) = &self.filter {
216 if !name.contains(filter) {
217 continue;
218 }
219 }
220
221 if first {
223 first = false;
224 } else {
225 writeln!(stdout)?;
226 }
227
228 if self.addresses {
231 stdout.set_color(color_address.clone().set_bold(true))?;
232 write!(stdout, "{:08x} ", sym.address())?;
233 stdout.reset()?;
234 }
235 stdout.set_color(ColorSpec::new().set_bold(true).set_fg(Some(Color::Green)))?;
236 write!(stdout, "{name}")?;
237 stdout.reset()?;
238 writeln!(stdout, ":")?;
239
240 let mut prev_jump = false;
243 let mut write_offsets = false;
244
245 for inst in self.disas(&elf, bytes, sym.address())? {
246 let Inst {
247 address,
248 is_jump,
249 is_return,
250 disassembly: disas,
251 bytes,
252 } = inst;
253
254 let mut bytes = bytes.iter().map(Some).chain(iter::repeat(None));
258 let inline_bytes = 9;
259 let width = self.address_width;
260
261 let mut pre_decorations = Vec::new();
274 let mut post_decorations = Vec::new();
275 decorator.decorate(address, &mut pre_decorations, &mut post_decorations);
276
277 let print_whitespace_to_decoration = |stdout: &mut StandardStream| -> Result<()> {
278 write!(stdout, "{:width$} ", "")?;
279 if self.bytes {
280 for _ in 0..inline_bytes + 1 {
281 write!(stdout, " ")?;
282 }
283 }
284 Ok(())
285 };
286
287 let print_decorations =
288 |stdout: &mut StandardStream, decorations: Vec<String>| -> Result<()> {
289 for (i, decoration) in decorations.iter().enumerate() {
290 print_whitespace_to_decoration(stdout)?;
291 let mut color = ColorSpec::new();
292 color.set_fg(Some(Color::Cyan));
293 stdout.set_color(&color)?;
294 let final_decoration = i == decorations.len() - 1;
295 if !final_decoration {
296 write!(stdout, "├")?;
297 } else {
298 write!(stdout, "╰")?;
299 }
300 for (i, line) in decoration.lines().enumerate() {
301 if i == 0 {
302 write!(stdout, "─╼ ")?;
303 } else {
304 print_whitespace_to_decoration(stdout)?;
305 if final_decoration {
306 write!(stdout, " ")?;
307 } else {
308 write!(stdout, "│ ")?;
309 }
310 }
311 writeln!(stdout, "{line}")?;
312 }
313 stdout.reset()?;
314 }
315 Ok(())
316 };
317
318 print_decorations(&mut stdout, pre_decorations)?;
319
320 for (i, line) in disas.lines().enumerate() {
324 let print_address = self.addresses
325 || (self.address_jumps && (write_offsets || (prev_jump && !is_jump)));
326 if i == 0 && print_address {
327 stdout.set_color(&color_address)?;
328 write!(stdout, "{address:>width$x}: ")?;
329 stdout.reset()?;
330 } else {
331 write!(stdout, "{:width$} ", "")?;
332 }
333
334 if self.bytes {
339 stdout.set_color(&color_bytes)?;
340 for byte in bytes.by_ref().take(inline_bytes) {
341 match byte {
342 Some(byte) => write!(stdout, "{byte:02x} ")?,
343 None => write!(stdout, " ")?,
344 }
345 }
346 write!(stdout, " ")?;
347 stdout.reset()?;
348 }
349
350 writeln!(stdout, "{line}")?;
351 }
352
353 write_offsets |= is_return;
357 prev_jump = is_jump;
358
359 if self.bytes {
363 let mut inline = 0;
364 stdout.set_color(&color_bytes)?;
365 for byte in bytes {
366 let Some(byte) = byte else { break };
367 if inline == 0 {
368 write!(stdout, "{:width$} ", "")?;
369 } else {
370 write!(stdout, " ")?;
371 }
372 write!(stdout, "{byte:02x}")?;
373 inline += 1;
374 if inline == inline_bytes {
375 writeln!(stdout)?;
376 inline = 0;
377 }
378 }
379 stdout.reset()?;
380 if inline > 0 {
381 writeln!(stdout)?;
382 }
383 }
384
385 print_decorations(&mut stdout, post_decorations)?;
386 }
387 }
388 Ok(())
389 }
390
391 fn disas(&self, elf: &ElfFile64<'_, Endianness>, func: &[u8], addr: u64) -> Result<Vec<Inst>> {
394 let cranelift_target = match elf.architecture() {
395 Architecture::X86_64 => "x86_64",
396 Architecture::Aarch64 => "aarch64",
397 Architecture::S390x => "s390x",
398 Architecture::Riscv64 => {
399 let e_flags = match elf.flags() {
400 FileFlags::Elf { e_flags, .. } => e_flags,
401 _ => bail!("not an ELF file"),
402 };
403 if e_flags & (obj::EF_WASMTIME_PULLEY32 | obj::EF_WASMTIME_PULLEY64) != 0 {
404 return self.disas_pulley(func, addr);
405 } else {
406 "riscv64"
407 }
408 }
409 other => bail!("unknown architecture {other:?}"),
410 };
411 let builder =
412 lookup_by_name(cranelift_target).context("failed to load cranelift ISA builder")?;
413 let flags = cranelift_codegen::settings::builder();
414 let isa = builder.finish(Flags::new(flags))?;
415 let isa = &*isa;
416 let capstone = isa
417 .to_capstone()
418 .context("failed to create a capstone disassembler")?;
419
420 let insts = capstone
421 .disasm_all(func, addr)?
422 .into_iter()
423 .map(|inst| {
424 let detail = capstone.insn_detail(&inst).ok();
425 let detail = detail.as_ref();
426 let is_jump = detail
427 .map(|d| {
428 d.groups()
429 .iter()
430 .find(|g| g.0 as u32 == CS_GRP_JUMP)
431 .is_some()
432 })
433 .unwrap_or(false);
434
435 let is_return = detail
436 .map(|d| {
437 d.groups()
438 .iter()
439 .find(|g| g.0 as u32 == CS_GRP_RET)
440 .is_some()
441 })
442 .unwrap_or(false);
443
444 let disassembly = match (inst.mnemonic(), inst.op_str()) {
445 (Some(i), Some(o)) => {
446 if o.is_empty() {
447 format!("{i}")
448 } else {
449 format!("{i:7} {o}")
450 }
451 }
452 (Some(i), None) => format!("{i}"),
453 _ => unreachable!(),
454 };
455
456 let address = inst.address();
457 Inst {
458 address,
459 is_jump,
460 is_return,
461 bytes: inst.bytes().to_vec(),
462 disassembly,
463 }
464 })
465 .collect::<Vec<_>>();
466 Ok(insts)
467 }
468
469 fn disas_pulley(&self, func: &[u8], addr: u64) -> Result<Vec<Inst>> {
471 let mut result = vec![];
472
473 let mut disas = Disassembler::new(func);
474 disas.offsets(false);
475 disas.hexdump(false);
476 disas.start_offset(usize::try_from(addr).unwrap());
477 let mut decoder = Decoder::new();
478 let mut last_disas_pos = 0;
479 loop {
480 let start_addr = disas.bytecode().position();
481
482 match decoder.decode_one(&mut disas) {
483 Err(DecodingError::UnexpectedEof { position }) if position == start_addr => break,
485
486 Err(e) => {
488 return Err(e).context("failed to disassembly pulley bytecode");
489 }
490
491 Ok(()) => {
492 let bytes_range = start_addr..disas.bytecode().position();
493 let disassembly = disas.disas()[last_disas_pos..].trim();
494 last_disas_pos = disas.disas().len();
495 let address = u64::try_from(start_addr).unwrap() + addr;
496 let is_jump = disassembly.contains("jump") || disassembly.contains("br_");
497 let is_return = disassembly == "ret";
498 result.push(Inst {
499 bytes: func[bytes_range].to_vec(),
500 address,
501 is_jump,
502 is_return,
503 disassembly: disassembly.to_string(),
504 });
505 }
506 }
507 }
508
509 Ok(result)
510 }
511
512 fn read_cwasm(&self) -> Result<Vec<u8>> {
515 if let Some(path) = &self.cwasm {
516 if path != Path::new("-") {
517 return std::fs::read(path).with_context(|| format!("failed to read {path:?}"));
518 }
519 }
520
521 let mut stdin = Vec::new();
522 std::io::stdin()
523 .read_to_end(&mut stdin)
524 .context("failed to read stdin")?;
525 Ok(stdin)
526 }
527}
528
529struct Inst {
531 address: u64,
532 is_jump: bool,
533 is_return: bool,
534 disassembly: String,
535 bytes: Vec<u8>,
536}
537
538#[derive(clap::ValueEnum, Clone, Copy, PartialEq, Eq)]
539enum Func {
540 All,
541 Wasm,
542 Trampoline,
543 Builtin,
544 Libcall,
545}
546
547struct Decorator<'a> {
548 objdump: &'a ObjdumpCommand,
549 addrmap: Option<Peekable<Box<dyn Iterator<Item = (u32, FilePos)> + 'a>>>,
550 traps: Option<Peekable<Box<dyn Iterator<Item = (u32, Trap)> + 'a>>>,
551 stack_maps: Option<Peekable<Box<dyn Iterator<Item = (u32, StackMap<'a>)> + 'a>>>,
552 exception_tables:
553 Option<Peekable<Box<dyn Iterator<Item = (u32, Option<u32>, Vec<ExceptionHandler>)> + 'a>>>,
554 frame_tables: Option<
555 Peekable<
556 Box<
557 dyn Iterator<
558 Item = (
559 u32,
560 FrameInstPos,
561 Vec<(u32, FrameTableDescriptorIndex, FrameStackShape)>,
562 ),
563 > + 'a,
564 >,
565 >,
566 >,
567
568 frame_table_descriptors: Option<FrameTable<'a>>,
569}
570
571impl Decorator<'_> {
572 fn decorate(&mut self, address: u64, pre_list: &mut Vec<String>, post_list: &mut Vec<String>) {
573 self.addrmap(address, post_list);
574 self.traps(address, post_list);
575 self.stack_maps(address, post_list);
576 self.exception_table(address, pre_list);
577 self.frame_table(address, pre_list, post_list);
578 }
579
580 fn addrmap(&mut self, address: u64, list: &mut Vec<String>) {
581 if !self.objdump.addrmap() {
582 return;
583 }
584 let Some(addrmap) = &mut self.addrmap else {
585 return;
586 };
587 while let Some((addr, pos)) = addrmap.next_if(|(addr, _pos)| u64::from(*addr) <= address) {
588 if u64::from(addr) != address {
589 continue;
590 }
591 if let Some(offset) = pos.file_offset() {
592 list.push(format!("addrmap: {offset:#x}"));
593 }
594 }
595 }
596
597 fn traps(&mut self, address: u64, list: &mut Vec<String>) {
598 if !self.objdump.traps() {
599 return;
600 }
601 let Some(traps) = &mut self.traps else {
602 return;
603 };
604 while let Some((addr, trap)) = traps.next_if(|(addr, _pos)| u64::from(*addr) <= address) {
605 if u64::from(addr) != address {
606 continue;
607 }
608 list.push(format!("trap: {trap:?}"));
609 }
610 }
611
612 fn stack_maps(&mut self, address: u64, list: &mut Vec<String>) {
613 if !self.objdump.stack_maps() {
614 return;
615 }
616 let Some(stack_maps) = &mut self.stack_maps else {
617 return;
618 };
619 while let Some((addr, stack_map)) =
620 stack_maps.next_if(|(addr, _pos)| u64::from(*addr) <= address)
621 {
622 if u64::from(addr) != address {
623 continue;
624 }
625 list.push(format!(
626 "stack_map: frame_size={}, frame_offsets={:?}",
627 stack_map.frame_size(),
628 stack_map.offsets().collect::<Vec<_>>()
629 ));
630 }
631 }
632
633 fn exception_table(&mut self, address: u64, list: &mut Vec<String>) {
634 if !self.objdump.exception_tables() {
635 return;
636 }
637 let Some(exception_tables) = &mut self.exception_tables else {
638 return;
639 };
640 while let Some((addr, frame_offset, handlers)) =
641 exception_tables.next_if(|(addr, _, _)| u64::from(*addr) <= address)
642 {
643 if u64::from(addr) != address {
644 continue;
645 }
646 if let Some(frame_offset) = frame_offset {
647 list.push(format!(
648 "exception frame offset: SP = FP - 0x{frame_offset:x}",
649 ));
650 }
651 for handler in &handlers {
652 let tag = match handler.tag {
653 Some(tag) => format!("tag={tag}"),
654 None => "default handler".to_string(),
655 };
656 let context = match handler.context_sp_offset {
657 Some(offset) => format!("context at [SP+0x{offset:x}]"),
658 None => "no dynamic context".to_string(),
659 };
660 list.push(format!(
661 "exception handler: {tag}, {context}, handler=0x{:x}",
662 handler.handler_offset
663 ));
664 }
665 }
666 }
667
668 fn frame_table(
669 &mut self,
670 address: u64,
671 pre_list: &mut Vec<String>,
672 post_list: &mut Vec<String>,
673 ) {
674 if !self.objdump.frame_tables() {
675 return;
676 }
677 let (Some(frame_table_iter), Some(frame_tables)) =
678 (&mut self.frame_tables, &self.frame_table_descriptors)
679 else {
680 return;
681 };
682
683 while let Some((addr, pos, frames)) =
684 frame_table_iter.next_if(|(addr, _, _)| u64::from(*addr) <= address)
685 {
686 if u64::from(addr) != address {
687 continue;
688 }
689 let list = match pos {
690 FrameInstPos::Post => &mut *pre_list,
697 FrameInstPos::Pre => &mut *post_list,
698 };
699 let pos = match pos {
700 FrameInstPos::Post => "after previous inst",
701 FrameInstPos::Pre => "before next inst",
702 };
703 for (wasm_pc, frame_descriptor, stack_shape) in frames {
704 let (frame_descriptor_data, offset) =
705 frame_tables.frame_descriptor(frame_descriptor).unwrap();
706 let frame_descriptor = FrameStateSlot::parse(frame_descriptor_data).unwrap();
707
708 let local_shape = Self::describe_local_shape(&frame_descriptor);
709 let stack_shape = Self::describe_stack_shape(&frame_descriptor, stack_shape);
710 let func_key = frame_descriptor.func_key();
711 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}"));
712 }
713 }
714 }
715
716 fn describe_local_shape(desc: &FrameStateSlot<'_>) -> String {
717 let mut parts = vec![];
718 for (offset, ty) in desc.locals() {
719 parts.push(format!("{ty:?} @ slot+0x{:x}", offset.offset()));
720 }
721 parts.join(", ")
722 }
723
724 fn describe_stack_shape(desc: &FrameStateSlot<'_>, shape: FrameStackShape) -> String {
725 let mut parts = vec![];
726 for (offset, ty) in desc.stack(shape) {
727 parts.push(format!("{ty:?} @ slot+0x{:x}", offset.offset()));
728 }
729 parts.reverse();
730 parts.join(", ")
731 }
732}