[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 <jkummerow@chromium.org>
Reviewed-by: Manos Koukoutos <manoskouk@chromium.org>
Cr-Commit-Position: refs/heads/main@{#81515}
This commit is contained in:
Jakob Kummerow 2022-07-04 23:04:07 +02:00 committed by V8 LUCI CQ
parent 1c8d8d6c6c
commit 7f0c7fb074
8 changed files with 232 additions and 11 deletions

View File

@ -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;

View File

@ -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) {}

View File

@ -91,6 +91,10 @@ V8_EXPORT_PRIVATE ModuleResult DecodeWasmModule(
Counters* counters, std::shared_ptr<metrics::Recorder> 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.

View File

@ -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,

View File

@ -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") {

View File

@ -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"]

36
tools/wasm/BUILD.gn Normal file
View File

@ -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" ]
}
}

View File

@ -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 <fstream>
#include <iomanip>
#include <iostream>
#include <vector>
#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<byte>(std::istreambuf_iterator<char>(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<const byte> 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<NamesProvider>(module_.get(), wire_bytes);
}
bool ok() const { return ok_; }
void ListFunctions() {
DCHECK(ok_);
const WasmModule* m = module();
uint32_t num_functions = static_cast<uint32_t>(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<byte> raw_bytes_;
std::shared_ptr<WasmModule> module_;
std::unique_ptr<NamesProvider> 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<v8::Platform> 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;
}