From 7f0c7fb0748addb06fef35989217bb5bd5723961 Mon Sep 17 00:00:00 2001 From: Jakob Kummerow Date: Mon, 4 Jul 2022 23:04:07 +0200 Subject: [PATCH] [tools][wasm] Add "wami", the Wasm Module Inspector Initial feature: list functions in a module, as follows: $ gm x64.release wami $ out/x64.release/wami --list-functions my_module.wasm More to come. Change-Id: I9580437d51153e1b5ccc291fdb6a6a67315be07d Reviewed-on: https://chromium-review.googlesource.com/c/v8/v8/+/3742700 Commit-Queue: Jakob Kummerow Reviewed-by: Manos Koukoutos Cr-Commit-Position: refs/heads/main@{#81515} --- src/wasm/module-decoder-impl.h | 15 +-- src/wasm/module-decoder.cc | 9 ++ src/wasm/module-decoder.h | 4 + src/wasm/names-provider.h | 2 +- tools/BUILD.gn | 4 + tools/dev/gm.py | 8 +- tools/wasm/BUILD.gn | 36 +++++++ tools/wasm/module-inspector.cc | 165 +++++++++++++++++++++++++++++++++ 8 files changed, 232 insertions(+), 11 deletions(-) create mode 100644 tools/wasm/BUILD.gn create mode 100644 tools/wasm/module-inspector.cc diff --git a/src/wasm/module-decoder-impl.h b/src/wasm/module-decoder-impl.h index 555481c14d..c79cc606d8 100644 --- a/src/wasm/module-decoder-impl.h +++ b/src/wasm/module-decoder-impl.h @@ -35,7 +35,7 @@ constexpr char kBranchHintsString[] = "metadata.code.branch_hint"; constexpr char kDebugInfoString[] = ".debug_info"; constexpr char kExternalDebugInfoString[] = "external_debug_info"; -const char* ExternalKindName(ImportExportKindCode kind) { +inline const char* ExternalKindName(ImportExportKindCode kind) { switch (kind) { case kExternalFunction: return "function"; @@ -51,7 +51,7 @@ const char* ExternalKindName(ImportExportKindCode kind) { return "unknown"; } -const char* SectionName(SectionCode code) { +inline const char* SectionName(SectionCode code) { switch (code) { case kUnknownSectionCode: return "Unknown"; @@ -100,7 +100,7 @@ const char* SectionName(SectionCode code) { } } -bool validate_utf8(Decoder* decoder, WireBytesRef string) { +inline bool validate_utf8(Decoder* decoder, WireBytesRef string) { return unibrow::Utf8::ValidateEncoding( decoder->start() + decoder->GetBufferRelativeOffset(string.offset()), string.length()); @@ -110,8 +110,9 @@ enum class StringValidation { kNone, kUtf8, kWtf8 }; // Reads a length-prefixed string, checking that it is within bounds. Returns // the offset of the string, and the length as an out parameter. -WireBytesRef consume_string(Decoder* decoder, StringValidation validation, - const char* name) { +inline WireBytesRef consume_string(Decoder* decoder, + StringValidation validation, + const char* name) { uint32_t length = decoder->consume_u32v("string length"); uint32_t offset = decoder->pc_offset(); const byte* string_start = decoder->pc(); @@ -138,11 +139,11 @@ WireBytesRef consume_string(Decoder* decoder, StringValidation validation, return {offset, decoder->failed() ? 0 : length}; } -WireBytesRef consume_utf8_string(Decoder* decoder, const char* name) { +inline WireBytesRef consume_utf8_string(Decoder* decoder, const char* name) { return consume_string(decoder, StringValidation::kUtf8, name); } -SectionCode IdentifyUnknownSectionInternal(Decoder* decoder) { +inline SectionCode IdentifyUnknownSectionInternal(Decoder* decoder) { WireBytesRef string = consume_utf8_string(decoder, "section name"); if (decoder->failed()) { return kUnknownSectionCode; diff --git a/src/wasm/module-decoder.cc b/src/wasm/module-decoder.cc index 4a09d401f7..5954c9e985 100644 --- a/src/wasm/module-decoder.cc +++ b/src/wasm/module-decoder.cc @@ -76,6 +76,15 @@ ModuleResult DecodeWasmModule( return result; } +ModuleResult DecodeWasmModuleForDisassembler(const byte* module_start, + const byte* module_end, + AccountingAllocator* allocator) { + constexpr bool verify_functions = false; + ModuleDecoderImpl decoder(WasmFeatures::All(), module_start, module_end, + kWasmOrigin); + return decoder.DecodeModule(nullptr, allocator, verify_functions); +} + ModuleDecoder::ModuleDecoder(const WasmFeatures& enabled) : enabled_features_(enabled) {} diff --git a/src/wasm/module-decoder.h b/src/wasm/module-decoder.h index 21c535a396..9c7bd5c0f0 100644 --- a/src/wasm/module-decoder.h +++ b/src/wasm/module-decoder.h @@ -91,6 +91,10 @@ V8_EXPORT_PRIVATE ModuleResult DecodeWasmModule( Counters* counters, std::shared_ptr metrics_recorder, v8::metrics::Recorder::ContextId context_id, DecodingMethod decoding_method, AccountingAllocator* allocator); +// Stripped down version for disassembler needs. +V8_EXPORT_PRIVATE ModuleResult DecodeWasmModuleForDisassembler( + const byte* module_start, const byte* module_end, + AccountingAllocator* allocator); // Exposed for testing. Decodes a single function signature, allocating it // in the given zone. Returns {nullptr} upon failure. diff --git a/src/wasm/names-provider.h b/src/wasm/names-provider.h index 261616455b..14ba899cd8 100644 --- a/src/wasm/names-provider.h +++ b/src/wasm/names-provider.h @@ -22,7 +22,7 @@ namespace wasm { class DecodedNameSection; class StringBuilder; -class NamesProvider { +class V8_EXPORT_PRIVATE NamesProvider { public: // {kWasmInternal}: only return raw name from name section. // {kDevTools}: prepend '$', use import/export names as fallback, diff --git a/tools/BUILD.gn b/tools/BUILD.gn index e168a05d4e..e9b6a0a390 100644 --- a/tools/BUILD.gn +++ b/tools/BUILD.gn @@ -18,6 +18,10 @@ group("gn_all") { if (is_win) { data_deps += [ "v8windbg" ] } + + if (v8_enable_webassembly) { + data_deps += [ "wasm:wami" ] + } } group("v8_check_static_initializers") { diff --git a/tools/dev/gm.py b/tools/dev/gm.py index 47a6d15c9f..57f97c7789 100755 --- a/tools/dev/gm.py +++ b/tools/dev/gm.py @@ -56,9 +56,11 @@ MODES = { # Modes that get built/run when you don't specify any. DEFAULT_MODES = ["release", "debug"] # Build targets that can be manually specified. -TARGETS = ["d8", "cctest", "unittests", "v8_fuzzers", "wasm_api_tests", "wee8", - "mkgrokdump", "generate-bytecode-expectations", "inspector-test", - "bigint_shell"] +TARGETS = [ + "d8", "cctest", "unittests", "v8_fuzzers", "wasm_api_tests", "wee8", + "mkgrokdump", "generate-bytecode-expectations", "inspector-test", + "bigint_shell", "wami" +] # Build targets that get built when you don't specify any (and specified tests # don't imply any other targets). DEFAULT_TARGETS = ["d8"] diff --git a/tools/wasm/BUILD.gn b/tools/wasm/BUILD.gn new file mode 100644 index 0000000000..cd2bb2ca72 --- /dev/null +++ b/tools/wasm/BUILD.gn @@ -0,0 +1,36 @@ +# Copyright 2022 the V8 project authors. All rights reserved. +# Use of this source code is governed by a BSD-style license that can be +# found in the LICENSE file. + +import("../../gni/v8.gni") + +config("internal_config") { + visibility = [ ":*" ] # Only targets in this file can depend on this. + + include_dirs = [ + ".", + "../..", + "../../include/", + "$target_gen_dir/../..", + ] +} + +# Wasm Module Inspector +v8_executable("wami") { + testonly = true + + deps = [ + "../..:v8", + "../..:v8_libbase", + "../..:v8_libplatform", + ] + + sources = [ "module-inspector.cc" ] + + defines = [] + + configs = [ ":internal_config" ] + if (v8_enable_i18n_support) { + configs += [ "//third_party/icu:icu_config" ] + } +} diff --git a/tools/wasm/module-inspector.cc b/tools/wasm/module-inspector.cc new file mode 100644 index 0000000000..4b66f55a5e --- /dev/null +++ b/tools/wasm/module-inspector.cc @@ -0,0 +1,165 @@ +// Copyright 2022 the V8 project authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include +#include +#include +#include + +#include "include/libplatform/libplatform.h" +#include "include/v8-initialization.h" +#include "src/wasm/module-decoder-impl.h" +#include "src/wasm/names-provider.h" +#include "src/wasm/string-builder.h" +#include "src/wasm/wasm-opcodes-inl.h" + +int PrintHelp(char** argv) { + std::cerr << "Usage: Specify an action and a module name in any order.\n" + << "The action can be any of:\n" + + << " --help\n" + << " Print this help and exit.\n" + + << " --list-functions\n" + << " List functions in the given module\n" + + << "The module name must be a file name.\n"; + return 1; +} + +namespace v8 { +namespace internal { +namespace wasm { + +class FormatConverter { + public: + explicit FormatConverter(std::string path) { + std::ifstream input(path, std::ios::binary); + if (!input.is_open()) { + std::cerr << "Failed to open " << path << "!\n"; + return; + } + raw_bytes_ = std::vector(std::istreambuf_iterator(input), {}); + if (raw_bytes_.size() < 8 || raw_bytes_[0] != 0 || raw_bytes_[1] != 'a' || + raw_bytes_[2] != 's' || raw_bytes_[3] != 'm') { + std::cerr << "That's not a Wasm module!\n"; + return; + } + base::Vector wire_bytes(raw_bytes_.data(), raw_bytes_.size()); + ModuleResult result = + DecodeWasmModuleForDisassembler(start(), end(), &allocator_); + if (result.failed()) { + WasmError error = result.error(); + std::cerr << "Decoding error: " << error.message() << " at offset " + << error.offset() << "\n"; + // TODO(jkummerow): Show some disassembly. + return; + } + ok_ = true; + module_ = result.value(); + names_provider_ = + std::make_unique(module_.get(), wire_bytes); + } + + bool ok() const { return ok_; } + + void ListFunctions() { + DCHECK(ok_); + const WasmModule* m = module(); + uint32_t num_functions = static_cast(m->functions.size()); + std::cout << "There are " << num_functions << " functions (" + << m->num_imported_functions << " imported, " + << m->num_declared_functions + << " locally defined); the following have names:\n"; + for (uint32_t i = 0; i < num_functions; i++) { + StringBuilder sb; + names()->PrintFunctionName(sb, i); + if (sb.length() == 0) continue; + std::string name(sb.start(), sb.length()); + std::cout << i << " " << name << "\n"; + } + } + + private: + byte* start() { return raw_bytes_.data(); } + byte* end() { return start() + raw_bytes_.size(); } + const WasmModule* module() { return module_.get(); } + NamesProvider* names() { return names_provider_.get(); } + + AccountingAllocator allocator_; + bool ok_{false}; + std::vector raw_bytes_; + std::shared_ptr module_; + std::unique_ptr names_provider_; +}; + +} // namespace wasm +} // namespace internal +} // namespace v8 + +using FormatConverter = v8::internal::wasm::FormatConverter; + +enum class Action { + kUnset, + kHelp, + kListFunctions, +}; + +struct Options { + const char* filename = nullptr; + Action action = Action::kUnset; +}; + +void ListFunctions(const Options& options) { + FormatConverter fc(options.filename); + if (fc.ok()) fc.ListFunctions(); +} + +int ParseOptions(int argc, char** argv, Options* options) { + for (int i = 1; i < argc; i++) { + if (strcmp(argv[i], "--help") == 0 || strcmp(argv[i], "-h") == 0 || + strcmp(argv[i], "help") == 0) { + options->action = Action::kHelp; + } else if (strcmp(argv[i], "--list-functions") == 0) { + options->action = Action::kListFunctions; + } else if (options->filename != nullptr) { + return PrintHelp(argv); + } else { + options->filename = argv[i]; + } + } + if (options->action == Action::kUnset || options->filename == nullptr) { + return PrintHelp(argv); + } + return 0; +} + +int main(int argc, char** argv) { + Options options; + if (ParseOptions(argc, argv, &options) != 0) return 1; + // Bootstrap the basics. + v8::V8::InitializeICUDefaultLocation(argv[0]); + v8::V8::InitializeExternalStartupData(argv[0]); + std::unique_ptr platform = v8::platform::NewDefaultPlatform(); + v8::V8::InitializePlatform(platform.get()); +#ifdef V8_ENABLE_SANDBOX + if (!v8::V8::InitializeSandbox()) { + fprintf(stderr, "Error initializing the V8 sandbox\n"); + return 1; + } +#endif + v8::V8::Initialize(); + + switch (options.action) { + // clang-format off + case Action::kHelp: PrintHelp(argv); break; + case Action::kListFunctions: ListFunctions(options); break; + case Action::kUnset: UNREACHABLE(); + // clang-format on + } + + v8::V8::Dispose(); + v8::V8::DisposePlatform(); + return 0; +}