Skip to content

Commit 7e18415

Browse files
committed
Add a wasmtime objdump subcommand
This commit adds an `objdump` subcommand to the `wasmtime` CLI. Like all other subcommands this can be disabled for a more minimal build of the CLI as well. The purpose of this subcommand is to provide a Wasmtime-specific spin on the venerable native `objdump` itself. Notably this brings Wasmtime-specific knowledge for filtering functions, showing Wasmtime metadata, etc. This command is intended to look like `objdump` roughly but also has configurable output with various flags and things that can be printed. For now the main Wasmtime additions are showing the address map section, stack map section, and trap section of a `*.cwasm` file. This new subcommand replaces the infrastructure of the `disas` test suite, and now that test suite uses `wasmtime objdump` to generate test expectations. Additionally the subcommand replaces the Pulley `objdump` example as a more full-featured objdump that also works natively with Pulley. The hope is that if we add more binary metadata in the future (such as unwinding tables) that can be relatively easily added here for exploration as well. Otherwise this is mostly just a developer convenience for Wasmtime developers as well and hopefully doesn't cost too much in maintenance burden. Closes bytecodealliance#10336
1 parent d2c9d2a commit 7e18415

File tree

22 files changed

+823
-341
lines changed

22 files changed

+823
-341
lines changed

.github/workflows/main.yml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -393,6 +393,7 @@ jobs:
393393
-p wasmtime-cli --no-default-features --features compile
394394
-p wasmtime-cli --no-default-features --features compile,cranelift
395395
-p wasmtime-cli --no-default-features --features compile,cranelift,component-model
396+
-p wasmtime-cli --no-default-features --features objdump
396397
-p wasmtime-cli --all-features
397398
-p wasmtime-cli --features component-model
398399
@@ -405,7 +406,7 @@ jobs:
405406
checks: |
406407
-p cranelift-entity --no-default-features
407408
-p cranelift-entity --no-default-features --features enable-serde
408-
409+
409410
- name: wasmtime-bench-api
410411
checks: |
411412
-p wasmtime-bench-api

Cargo.lock

Lines changed: 2 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,12 @@ wasmparser = { workspace = true }
7171
tracing = { workspace = true }
7272
log = { workspace = true }
7373
tempfile = { workspace = true, optional = true }
74+
object = { workspace = true, optional = true }
75+
cranelift-codegen = { workspace = true, optional = true, features = ['disas'] }
76+
capstone = { workspace = true, optional = true }
77+
termcolor = { workspace = true, optional = true }
78+
gimli = { workspace = true, optional = true }
79+
pulley-interpreter = { workspace = true, optional = true }
7480

7581
async-trait = { workspace = true }
7682
trait-variant = { workspace = true }
@@ -381,6 +387,7 @@ libm = "0.2.7"
381387
tokio-rustls = "0.25.0"
382388
rustls = "0.22.0"
383389
webpki-roots = "0.26.0"
390+
termcolor = "1.4.1"
384391

385392
# =============================================================================
386393
#
@@ -406,6 +413,7 @@ default = [
406413
"wast",
407414
"config",
408415
"completion",
416+
"objdump",
409417

410418
# On-by-default WASI features
411419
"wasi-nn",
@@ -513,6 +521,14 @@ run = [
513521
"wasmtime-cli-flags/async",
514522
]
515523
completion = ["dep:clap_complete"]
524+
objdump = [
525+
'dep:object',
526+
'dep:cranelift-codegen',
527+
'dep:capstone',
528+
'dep:termcolor',
529+
'dep:gimli',
530+
'pulley-interpreter/disas',
531+
]
516532

517533
[[test]]
518534
name = "disas"

crates/environ/src/compile/mod.rs

Lines changed: 26 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -285,34 +285,41 @@ pub trait Compiler: Send + Sync {
285285
use target_lexicon::Architecture::*;
286286

287287
let triple = self.triple();
288+
let (arch, flags) = match triple.architecture {
289+
X86_32(_) => (Architecture::I386, 0),
290+
X86_64 => (Architecture::X86_64, 0),
291+
Arm(_) => (Architecture::Arm, 0),
292+
Aarch64(_) => (Architecture::Aarch64, 0),
293+
S390x => (Architecture::S390x, 0),
294+
Riscv64(_) => (Architecture::Riscv64, 0),
295+
// XXX: the `object` crate won't successfully build an object
296+
// with relocations and such if it doesn't know the
297+
// architecture, so just pretend we are riscv64. Yolo!
298+
//
299+
// Also note that we add some flags to `e_flags` in the object file
300+
// to indicate that it's pulley, not actually riscv64. This is used
301+
// by `wasmtime objdump` for example.
302+
Pulley32 | Pulley32be => (Architecture::Riscv64, obj::EF_WASMTIME_PULLEY32),
303+
Pulley64 | Pulley64be => (Architecture::Riscv64, obj::EF_WASMTIME_PULLEY64),
304+
architecture => {
305+
anyhow::bail!("target architecture {:?} is unsupported", architecture,);
306+
}
307+
};
288308
let mut obj = Object::new(
289309
BinaryFormat::Elf,
290-
match triple.architecture {
291-
X86_32(_) => Architecture::I386,
292-
X86_64 => Architecture::X86_64,
293-
Arm(_) => Architecture::Arm,
294-
Aarch64(_) => Architecture::Aarch64,
295-
S390x => Architecture::S390x,
296-
Riscv64(_) => Architecture::Riscv64,
297-
// XXX: the `object` crate won't successfully build an object
298-
// with relocations and such if it doesn't know the
299-
// architecture, so just pretend we are riscv64. Yolo!
300-
Pulley32 | Pulley64 | Pulley32be | Pulley64be => Architecture::Riscv64,
301-
architecture => {
302-
anyhow::bail!("target architecture {:?} is unsupported", architecture,);
303-
}
304-
},
310+
arch,
305311
match triple.endianness().unwrap() {
306312
target_lexicon::Endianness::Little => object::Endianness::Little,
307313
target_lexicon::Endianness::Big => object::Endianness::Big,
308314
},
309315
);
310316
obj.flags = FileFlags::Elf {
311317
os_abi: obj::ELFOSABI_WASMTIME,
312-
e_flags: match kind {
313-
ObjectKind::Module => obj::EF_WASMTIME_MODULE,
314-
ObjectKind::Component => obj::EF_WASMTIME_COMPONENT,
315-
},
318+
e_flags: flags
319+
| match kind {
320+
ObjectKind::Module => obj::EF_WASMTIME_MODULE,
321+
ObjectKind::Component => obj::EF_WASMTIME_COMPONENT,
322+
},
316323
abi_version: 0,
317324
};
318325
Ok(obj)

crates/environ/src/obj.rs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,14 @@ pub const EF_WASMTIME_MODULE: u32 = 1 << 0;
1717
/// component.
1818
pub const EF_WASMTIME_COMPONENT: u32 = 1 << 1;
1919

20+
/// Flag for the `e_flags` field in the ELF header indicating compiled code for
21+
/// pulley32
22+
pub const EF_WASMTIME_PULLEY32: u32 = 1 << 2;
23+
24+
/// Flag for the `e_flags` field in the ELF header indicating compiled code for
25+
/// pulley64
26+
pub const EF_WASMTIME_PULLEY64: u32 = 1 << 3;
27+
2028
/// Flag for the `sh_flags` field in the ELF text section that indicates that
2129
/// the text section does not itself need to be executable. This is used for the
2230
/// Pulley target, for example, to indicate that it does not need to be made

crates/environ/src/trap_encoding.rs

Lines changed: 24 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -174,13 +174,7 @@ impl core::error::Error for Trap {}
174174
/// `TrapEncodingBuilder` above. Additionally the `offset` should be a relative
175175
/// offset within the text section of the compilation image.
176176
pub fn lookup_trap_code(section: &[u8], offset: usize) -> Option<Trap> {
177-
let mut section = Bytes(section);
178-
// NB: this matches the encoding written by `append_to` above.
179-
let count = section.read::<U32Bytes<LittleEndian>>().ok()?;
180-
let count = usize::try_from(count.get(LittleEndian)).ok()?;
181-
let (offsets, traps) =
182-
object::slice_from_bytes::<U32Bytes<LittleEndian>>(section.0, count).ok()?;
183-
debug_assert_eq!(traps.len(), count);
177+
let (offsets, traps) = parse(section)?;
184178

185179
// The `offsets` table is sorted in the trap section so perform a binary
186180
// search of the contents of this section to find whether `offset` is an
@@ -202,3 +196,26 @@ pub fn lookup_trap_code(section: &[u8], offset: usize) -> Option<Trap> {
202196
debug_assert!(trap.is_some(), "missing mapping for {byte}");
203197
trap
204198
}
199+
200+
fn parse(section: &[u8]) -> Option<(&[U32Bytes<LittleEndian>], &[u8])> {
201+
let mut section = Bytes(section);
202+
// NB: this matches the encoding written by `append_to` above.
203+
let count = section.read::<U32Bytes<LittleEndian>>().ok()?;
204+
let count = usize::try_from(count.get(LittleEndian)).ok()?;
205+
let (offsets, traps) =
206+
object::slice_from_bytes::<U32Bytes<LittleEndian>>(section.0, count).ok()?;
207+
debug_assert_eq!(traps.len(), count);
208+
Some((offsets, traps))
209+
}
210+
211+
/// Returns an iterator over all of the traps encoded in `section`, which should
212+
/// have been produced by `TrapEncodingBuilder`.
213+
pub fn iterate_traps(section: &[u8]) -> Option<impl Iterator<Item = (u32, Trap)> + '_> {
214+
let (offsets, traps) = parse(section)?;
215+
Some(
216+
offsets
217+
.iter()
218+
.zip(traps)
219+
.map(|(offset, trap)| (offset.get(LittleEndian), Trap::from_u8(*trap).unwrap())),
220+
)
221+
}

crates/wasmtime/src/engine.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -226,13 +226,13 @@ impl Engine {
226226
/// [`Engine::precompile_module`], or [`Engine::precompile_component`], then
227227
/// this will return `Some(...)` indicating so. Otherwise `None` is
228228
/// returned.
229-
pub fn detect_precompiled(&self, bytes: &[u8]) -> Option<Precompiled> {
229+
pub fn detect_precompiled(bytes: &[u8]) -> Option<Precompiled> {
230230
serialization::detect_precompiled_bytes(bytes)
231231
}
232232

233233
/// Like [`Engine::detect_precompiled`], but performs the detection on a file.
234234
#[cfg(feature = "std")]
235-
pub fn detect_precompiled_file(&self, path: impl AsRef<Path>) -> Result<Option<Precompiled>> {
235+
pub fn detect_precompiled_file(path: impl AsRef<Path>) -> Result<Option<Precompiled>> {
236236
serialization::detect_precompiled_file(path)
237237
}
238238

crates/wasmtime/src/engine/serialization.rs

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,7 @@ pub fn check_compatible(engine: &Engine, mmap: &[u8], expected: ObjectKind) -> R
6868
os_abi: obj::ELFOSABI_WASMTIME,
6969
abi_version: 0,
7070
e_flags,
71-
} if e_flags == expected_e_flags => {}
71+
} if e_flags & expected_e_flags == expected_e_flags => {}
7272
_ => bail!("incompatible object file format"),
7373
}
7474

@@ -149,13 +149,13 @@ fn detect_precompiled<'data, R: object::ReadRef<'data>>(
149149
FileFlags::Elf {
150150
os_abi: obj::ELFOSABI_WASMTIME,
151151
abi_version: 0,
152-
e_flags: obj::EF_WASMTIME_MODULE,
153-
} => Some(Precompiled::Module),
152+
e_flags,
153+
} if e_flags & obj::EF_WASMTIME_MODULE != 0 => Some(Precompiled::Module),
154154
FileFlags::Elf {
155155
os_abi: obj::ELFOSABI_WASMTIME,
156156
abi_version: 0,
157-
e_flags: obj::EF_WASMTIME_COMPONENT,
158-
} => Some(Precompiled::Component),
157+
e_flags,
158+
} if e_flags & obj::EF_WASMTIME_COMPONENT != 0 => Some(Precompiled::Component),
159159
_ => None,
160160
}
161161
}

docs/cli-install.md

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -19,13 +19,13 @@ Windows users will want to visit our [releases page][releases] and can download
1919
the MSI installer (`wasmtime-dev-x86_64-windows.msi` for example) and use that
2020
to install.
2121

22-
[releases]: https://github.com/bytecodealliance/wasmtime/releases
22+
[releases]: https://github.com/bytecodealliance/wasmtime/releases/latest
2323

2424
You can confirm your installation works by executing:
2525

2626
```sh
2727
$ wasmtime -V
28-
wasmtime 0.12.0
28+
wasmtime 30.0.0 (ede663c2a 2025-02-19)
2929
```
3030

3131
And now you're off to the races! Be sure to check out the [various CLI
@@ -56,9 +56,9 @@ the `dev` release)
5656
Each of these archives has a `wasmtime` binary placed inside which can be
5757
executed normally as the CLI would.
5858

59-
[wasmtime-dev-x86_64-linux.tar.xz`]: https://github.com/bytecodealliance/wasmtime/releases/download/dev/wasmtime-dev-x86_64-linux.tar.xz
60-
[wasmtime-dev-x86_64-macos.tar.xz`]: https://github.com/bytecodealliance/wasmtime/releases/download/dev/wasmtime-dev-x86_64-macos.tar.xz
61-
[wasmtime-dev-x86_64-windows.zip`]: https://github.com/bytecodealliance/wasmtime/releases/download/dev/wasmtime-dev-x86_64-windows.zip
59+
[`wasmtime-dev-x86_64-linux.tar.xz`]: https://github.com/bytecodealliance/wasmtime/releases/download/dev/wasmtime-dev-x86_64-linux.tar.xz
60+
[`wasmtime-dev-x86_64-macos.tar.xz`]: https://github.com/bytecodealliance/wasmtime/releases/download/dev/wasmtime-dev-x86_64-macos.tar.xz
61+
[`wasmtime-dev-x86_64-windows.zip`]: https://github.com/bytecodealliance/wasmtime/releases/download/dev/wasmtime-dev-x86_64-windows.zip
6262

6363
## Compiling from Source
6464

docs/cli-options.md

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,57 @@ display what Cranelift settings are inferred for the host:
127127
$ wasmtime settings
128128
```
129129

130+
## `explore`
131+
132+
This subcommand can be used to explore a `*.cwasm` file and see how it connects
133+
to the original wasm file in a web browser. This will compile an input wasm
134+
file and emit an HTML file that can be opened in a web browser:
135+
136+
```
137+
$ wasmtime explore foo.wasm
138+
Exploration written to foo.explore.html
139+
```
140+
141+
The output HTML file can be used to compare what WebAssembly instruction
142+
compiles to what native instruction. Compilation options can be passed to
143+
`wasmtime explore` to see the effect of compilation options on generated code.
144+
145+
## `objdump`
146+
147+
Primarily intended as a debugging utility the `objdump` subcommand can be used
148+
to explore a `*.cwasm` file locally on your terminal. This is roughly modeled
149+
after native `objdump` binaries themselves:
150+
151+
```
152+
$ wasmtime objdump foo.cwasm
153+
wasm[0]::function[0]:
154+
stp x29, x30, [sp, #-0x10]!
155+
mov x29, sp
156+
ldr x5, [x2, #0x50]
157+
lsl w6, w4, #2
158+
ldr w2, [x5, w6, uxtw]
159+
ldp x29, x30, [sp], #0x10
160+
ret
161+
```
162+
163+
You can also pass various options to configure and annotate the output:
164+
165+
```
166+
$ wasmtime objdump foo.cwasm --addresses --bytes --addrma
167+
00000000 wasm[0]::function[0]:
168+
0: fd 7b bf a9 stp x29, x30, [sp, #-0x10]!
169+
4: fd 03 00 91 mov x29, sp
170+
8: 45 28 40 f9 ldr x5, [x2, #0x50]
171+
╰─╼ addrmap: 0x23
172+
c: 86 74 1e 53 lsl w6, w4, #2
173+
╰─╼ addrmap: 0x22
174+
10: a2 48 66 b8 ldr w2, [x5, w6, uxtw]
175+
╰─╼ addrmap: 0x23
176+
14: fd 7b c1 a8 ldp x29, x30, [sp], #0x10
177+
╰─╼ addrmap: 0x26
178+
18: c0 03 5f d6 ret
179+
```
180+
130181
# Additional options
131182
Many of the above subcommands also take additional options. For example,
132183
- run

0 commit comments

Comments
 (0)