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::{FilePos, StackMap, Trap, obj};
18
19#[derive(Parser)]
22pub struct ObjdumpCommand {
23 cwasm: Option<PathBuf>,
27
28 #[arg(long)]
30 addresses: bool,
31
32 #[arg(long)]
35 address_jumps: bool,
36
37 #[arg(long, default_value = "wasm", value_name = "KIND")]
39 funcs: Vec<Func>,
40
41 #[arg(long, value_name = "STR")]
43 filter: Option<String>,
44
45 #[arg(long)]
47 bytes: bool,
48
49 #[arg(long, default_value = "auto")]
51 color: ColorChoice,
52
53 #[arg(long, require_equals = true, value_name = "true|false")]
55 addrmap: Option<Option<bool>>,
56
57 #[arg(long, default_value = "10", value_name = "N")]
59 address_width: usize,
60
61 #[arg(long, require_equals = true, value_name = "true|false")]
63 traps: Option<Option<bool>>,
64
65 #[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 pub fn execute(self) -> Result<()> {
93 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 if Engine::detect_precompiled(&bytes).is_none() {
110 bail!("not a `*.cwasm` file from wasmtime: {:?}", self.cwasm);
111 }
112
113 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 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 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 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 if first {
185 first = false;
186 } else {
187 writeln!(stdout)?;
188 }
189
190 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 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 let mut bytes = bytes.iter().map(Some).chain(iter::repeat(None));
220 let inline_bytes = 9;
221 let width = self.address_width;
222
223 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 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 write_offsets |= is_return;
260 prev_jump = is_jump;
261
262 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 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 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 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 Err(DecodingError::UnexpectedEof { position }) if position == start_addr => break,
433
434 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 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
477struct 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}