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