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