ladybird/Userland/Utilities/wasm.cpp
sin-ack c70f45ff44 Everywhere: Explicitly specify the size in StringView constructors
This commit moves the length calculations out to be directly on the
StringView users. This is an important step towards the goal of removing
StringView(char const*), as it moves the responsibility of calculating
the size of the string to the user of the StringView (which will prevent
naive uses causing OOB access).
2022-07-12 23:11:35 +02:00

533 lines
21 KiB
C++

/*
* Copyright (c) 2021, Ali Mohammad Pur <mpfard@serenityos.org>
* Copyright (c) 2022, the SerenityOS developers.
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#include <LibCore/ArgsParser.h>
#include <LibCore/File.h>
#include <LibCore/FileStream.h>
#include <LibLine/Editor.h>
#include <LibMain/Main.h>
#include <LibWasm/AbstractMachine/AbstractMachine.h>
#include <LibWasm/AbstractMachine/BytecodeInterpreter.h>
#include <LibWasm/Printer/Printer.h>
#include <LibWasm/Types.h>
#include <signal.h>
#include <unistd.h>
RefPtr<Line::Editor> g_line_editor;
static auto g_stdout = Core::OutputFileStream::standard_error();
static Wasm::Printer g_printer { g_stdout };
static bool g_continue { false };
static void (*old_signal)(int);
static Wasm::DebuggerBytecodeInterpreter g_interpreter;
static void sigint_handler(int)
{
if (!g_continue) {
signal(SIGINT, old_signal);
kill(getpid(), SIGINT);
}
g_continue = false;
}
static bool post_interpret_hook(Wasm::Configuration&, Wasm::InstructionPointer& ip, Wasm::Instruction const& instr, Wasm::Interpreter const& interpreter)
{
if (interpreter.did_trap()) {
g_continue = false;
warnln("Trapped when executing ip={}", ip);
g_printer.print(instr);
warnln("Trap reason: {}", interpreter.trap_reason());
const_cast<Wasm::Interpreter&>(interpreter).clear_trap();
}
return true;
}
static bool pre_interpret_hook(Wasm::Configuration& config, Wasm::InstructionPointer& ip, Wasm::Instruction const& instr)
{
static bool always_print_stack = false;
static bool always_print_instruction = false;
if (always_print_stack)
config.dump_stack();
if (always_print_instruction) {
g_stdout.write(String::formatted("{:0>4} ", ip.value()).bytes());
g_printer.print(instr);
}
if (g_continue)
return true;
g_stdout.write(String::formatted("{:0>4} ", ip.value()).bytes());
g_printer.print(instr);
String last_command = "";
for (;;) {
auto result = g_line_editor->get_line("> ");
if (result.is_error()) {
return false;
}
auto str = result.release_value();
g_line_editor->add_to_history(str);
if (str.is_empty())
str = last_command;
else
last_command = str;
auto args = str.split_view(' ');
if (args.is_empty())
continue;
auto& cmd = args[0];
if (cmd.is_one_of("h", "help")) {
warnln("Wasm shell commands");
warnln("Toplevel:");
warnln("- [s]tep Run one instruction");
warnln("- next Alias for step");
warnln("- [c]ontinue Execute until a trap or the program exit point");
warnln("- [p]rint <args...> Print various things (see section on print)");
warnln("- call <fn> <args...> Call the function <fn> with the given arguments");
warnln("- set <args...> Set shell option (see section on settings)");
warnln("- unset <args...> Unset shell option (see section on settings)");
warnln("- [h]elp Print this help");
warnln();
warnln("Print:");
warnln("- print [s]tack Print the contents of the stack, including frames and labels");
warnln("- print [[m]em]ory <index> Print the contents of the memory identified by <index>");
warnln("- print [[i]nstr]uction Print the current instruction");
warnln("- print [[f]unc]tion <index> Print the function identified by <index>");
warnln();
warnln("Settings:");
warnln("- set print stack Make the shell print the stack on every instruction executed");
warnln("- set print [instr]uction Make the shell print the instruction that will be executed next");
warnln();
continue;
}
if (cmd.is_one_of("s", "step", "next")) {
return true;
}
if (cmd.is_one_of("p", "print")) {
if (args.size() < 2) {
warnln("Print what?");
continue;
}
auto& what = args[1];
if (what.is_one_of("s", "stack")) {
config.dump_stack();
continue;
}
if (what.is_one_of("m", "mem", "memory")) {
if (args.size() < 3) {
warnln("print what memory?");
continue;
}
auto value = args[2].to_uint<u64>();
if (!value.has_value()) {
warnln("invalid memory index {}", args[2]);
continue;
}
auto mem = config.store().get(Wasm::MemoryAddress(value.value()));
if (!mem) {
warnln("invalid memory index {} (not found)", args[2]);
continue;
}
warnln("{:>32hex-dump}", mem->data().bytes());
continue;
}
if (what.is_one_of("i", "instr", "instruction")) {
g_printer.print(instr);
continue;
}
if (what.is_one_of("f", "func", "function")) {
if (args.size() < 3) {
warnln("print what function?");
continue;
}
auto value = args[2].to_uint<u64>();
if (!value.has_value()) {
warnln("invalid function index {}", args[2]);
continue;
}
auto fn = config.store().get(Wasm::FunctionAddress(value.value()));
if (!fn) {
warnln("invalid function index {} (not found)", args[2]);
continue;
}
if (auto* fn_value = fn->get_pointer<Wasm::HostFunction>()) {
warnln("Host function at {:p}", &fn_value->function());
continue;
}
if (auto* fn_value = fn->get_pointer<Wasm::WasmFunction>()) {
g_printer.print(fn_value->code());
continue;
}
}
}
if (cmd == "call"sv) {
if (args.size() < 2) {
warnln("call what?");
continue;
}
Optional<Wasm::FunctionAddress> address;
auto index = args[1].to_uint<u64>();
if (index.has_value()) {
address = config.frame().module().functions()[index.value()];
} else {
auto& name = args[1];
for (auto& export_ : config.frame().module().exports()) {
if (export_.name() == name) {
if (auto addr = export_.value().get_pointer<Wasm::FunctionAddress>()) {
address = *addr;
break;
}
}
}
}
if (!address.has_value()) {
failed_to_find:;
warnln("Could not find a function {}", args[1]);
continue;
}
auto fn = config.store().get(*address);
if (!fn)
goto failed_to_find;
auto type = fn->visit([&](auto& value) { return value.type(); });
if (type.parameters().size() + 2 != args.size()) {
warnln("Expected {} arguments for call, but found only {}", type.parameters().size(), args.size() - 2);
continue;
}
Vector<u64> values_to_push;
Vector<Wasm::Value> values;
for (size_t index = 2; index < args.size(); ++index)
values_to_push.append(args[index].to_uint().value_or(0));
for (auto& param : type.parameters())
values.append(Wasm::Value { param, values_to_push.take_last() });
Wasm::Result result { Wasm::Trap {} };
{
Wasm::BytecodeInterpreter::CallFrameHandle handle { g_interpreter, config };
result = config.call(g_interpreter, *address, move(values));
}
if (result.is_trap())
warnln("Execution trapped: {}", result.trap().reason);
if (!result.values().is_empty())
warnln("Returned:");
for (auto& value : result.values()) {
g_stdout.write(" -> "sv.bytes());
g_printer.print(value);
}
continue;
}
if (cmd.is_one_of("set", "unset")) {
auto value = !cmd.starts_with('u');
if (args.size() < 3) {
warnln("(un)set what (to what)?");
continue;
}
if (args[1] == "print"sv) {
if (args[2] == "stack"sv)
always_print_stack = value;
else if (args[2].is_one_of("instr", "instruction"))
always_print_instruction = value;
else
warnln("Unknown print category '{}'", args[2]);
continue;
}
warnln("Unknown set category '{}'", args[1]);
continue;
}
if (cmd.is_one_of("c", "continue")) {
g_continue = true;
return true;
}
warnln("Command not understood: {}", cmd);
}
}
static Optional<Wasm::Module> parse(StringView filename)
{
auto result = Core::File::open(filename, Core::OpenMode::ReadOnly);
if (result.is_error()) {
warnln("Failed to open {}: {}", filename, result.error());
return {};
}
auto stream = Core::InputFileStream(result.release_value());
auto parse_result = Wasm::Module::parse(stream);
if (parse_result.is_error()) {
warnln("Something went wrong, either the file is invalid, or there's a bug with LibWasm!");
warnln("The parse error was {}", Wasm::parse_error_to_string(parse_result.error()));
return {};
}
return parse_result.release_value();
}
static void print_link_error(Wasm::LinkError const& error)
{
for (auto const& missing : error.missing_imports)
warnln("Missing import '{}'", missing);
}
ErrorOr<int> serenity_main(Main::Arguments arguments)
{
StringView filename;
bool print = false;
bool attempt_instantiate = false;
bool debug = false;
bool export_all_imports = false;
bool shell_mode = false;
String exported_function_to_execute;
Vector<u64> values_to_push;
Vector<String> modules_to_link_in;
Core::ArgsParser parser;
parser.add_positional_argument(filename, "File name to parse", "file");
parser.add_option(debug, "Open a debugger", "debug", 'd');
parser.add_option(print, "Print the parsed module", "print", 'p');
parser.add_option(attempt_instantiate, "Attempt to instantiate the module", "instantiate", 'i');
parser.add_option(exported_function_to_execute, "Attempt to execute the named exported function from the module (implies -i)", "execute", 'e', "name");
parser.add_option(export_all_imports, "Export noop functions corresponding to imports", "export-noop", 0);
parser.add_option(shell_mode, "Launch a REPL in the module's context (implies -i)", "shell", 's');
parser.add_option(Core::ArgsParser::Option {
.requires_argument = true,
.help_string = "Extra modules to link with, use to resolve imports",
.long_name = "link",
.short_name = 'l',
.value_name = "file",
.accept_value = [&](char const* str) {
if (auto v = StringView { str, strlen(str) }; !v.is_empty()) {
modules_to_link_in.append(v);
return true;
}
return false;
},
});
parser.add_option(Core::ArgsParser::Option {
.requires_argument = true,
.help_string = "Supply arguments to the function (default=0) (expects u64, casts to required type)",
.long_name = "arg",
.short_name = 0,
.value_name = "u64",
.accept_value = [&](char const* str) -> bool {
if (auto v = StringView { str, strlen(str) }.to_uint<u64>(); v.has_value()) {
values_to_push.append(v.value());
return true;
}
return false;
},
});
parser.parse(arguments);
if (shell_mode) {
debug = true;
attempt_instantiate = true;
}
if (!shell_mode && debug && exported_function_to_execute.is_empty()) {
warnln("Debug what? (pass -e fn)");
return 1;
}
if (debug || shell_mode) {
old_signal = signal(SIGINT, sigint_handler);
}
if (!exported_function_to_execute.is_empty())
attempt_instantiate = true;
auto parse_result = parse(filename);
if (!parse_result.has_value())
return 1;
if (print && !attempt_instantiate) {
auto out_stream = Core::OutputFileStream::standard_output();
Wasm::Printer printer(out_stream);
printer.print(parse_result.value());
}
if (attempt_instantiate) {
Wasm::AbstractMachine machine;
Core::EventLoop main_loop;
if (debug) {
g_line_editor = Line::Editor::construct();
g_interpreter.pre_interpret_hook = pre_interpret_hook;
g_interpreter.post_interpret_hook = post_interpret_hook;
}
// First, resolve the linked modules
NonnullOwnPtrVector<Wasm::ModuleInstance> linked_instances;
Vector<Wasm::Module> linked_modules;
for (auto& name : modules_to_link_in) {
auto parse_result = parse(name);
if (!parse_result.has_value()) {
warnln("Failed to parse linked module '{}'", name);
return 1;
}
linked_modules.append(parse_result.release_value());
Wasm::Linker linker { linked_modules.last() };
for (auto& instance : linked_instances)
linker.link(instance);
auto link_result = linker.finish();
if (link_result.is_error()) {
warnln("Linking imported module '{}' failed", name);
print_link_error(link_result.error());
return 1;
}
auto instantiation_result = machine.instantiate(linked_modules.last(), link_result.release_value());
if (instantiation_result.is_error()) {
warnln("Instantiation of imported module '{}' failed: {}", name, instantiation_result.error().error);
return 1;
}
linked_instances.append(instantiation_result.release_value());
}
Wasm::Linker linker { parse_result.value() };
for (auto& instance : linked_instances)
linker.link(instance);
if (export_all_imports) {
HashMap<Wasm::Linker::Name, Wasm::ExternValue> exports;
for (auto& entry : linker.unresolved_imports()) {
if (!entry.type.has<Wasm::TypeIndex>())
continue;
auto type = parse_result.value().type(entry.type.get<Wasm::TypeIndex>());
auto address = machine.store().allocate(Wasm::HostFunction(
[name = entry.name, type = type](auto&, auto& arguments) -> Wasm::Result {
StringBuilder argument_builder;
bool first = true;
for (auto& argument : arguments) {
DuplexMemoryStream stream;
Wasm::Printer { stream }.print(argument);
if (first)
first = false;
else
argument_builder.append(", "sv);
ByteBuffer buffer = stream.copy_into_contiguous_buffer();
argument_builder.append(StringView(buffer).trim_whitespace());
}
dbgln("[wasm runtime] Stub function {} was called with the following arguments: {}", name, argument_builder.to_string());
Vector<Wasm::Value> result;
result.ensure_capacity(type.results().size());
for (auto& result_type : type.results())
result.append(Wasm::Value { result_type, 0ull });
return Wasm::Result { move(result) };
},
type));
exports.set(entry, *address);
}
linker.link(exports);
}
auto link_result = linker.finish();
if (link_result.is_error()) {
warnln("Linking main module failed");
print_link_error(link_result.error());
return 1;
}
auto result = machine.instantiate(parse_result.value(), link_result.release_value());
if (result.is_error()) {
warnln("Module instantiation failed: {}", result.error().error);
return 1;
}
auto module_instance = result.release_value();
auto launch_repl = [&] {
Wasm::Configuration config { machine.store() };
Wasm::Expression expression { {} };
config.set_frame(Wasm::Frame {
*module_instance,
Vector<Wasm::Value> {},
expression,
0,
});
Wasm::Instruction instr { Wasm::Instructions::nop };
Wasm::InstructionPointer ip { 0 };
g_continue = false;
pre_interpret_hook(config, ip, instr);
};
auto stream = Core::OutputFileStream::standard_output();
auto print_func = [&](auto const& address) {
Wasm::FunctionInstance* fn = machine.store().get(address);
stream.write(String::formatted("- Function with address {}, ptr = {}\n", address.value(), fn).bytes());
if (fn) {
stream.write(String::formatted(" wasm function? {}\n", fn->has<Wasm::WasmFunction>()).bytes());
fn->visit(
[&](Wasm::WasmFunction const& func) {
Wasm::Printer printer { stream, 3 };
stream.write(" type:\n"sv.bytes());
printer.print(func.type());
stream.write(" code:\n"sv.bytes());
printer.print(func.code());
},
[](Wasm::HostFunction const&) {});
}
};
if (print) {
// Now, let's dump the functions!
for (auto& address : module_instance->functions()) {
print_func(address);
}
}
if (shell_mode) {
launch_repl();
return 0;
}
if (!exported_function_to_execute.is_empty()) {
Optional<Wasm::FunctionAddress> run_address;
Vector<Wasm::Value> values;
for (auto& entry : module_instance->exports()) {
if (entry.name() == exported_function_to_execute) {
if (auto addr = entry.value().get_pointer<Wasm::FunctionAddress>())
run_address = *addr;
}
}
if (!run_address.has_value()) {
warnln("No such exported function, sorry :(");
return 1;
}
auto instance = machine.store().get(*run_address);
VERIFY(instance);
if (instance->has<Wasm::HostFunction>()) {
warnln("Exported function is a host function, cannot run that yet");
return 1;
}
for (auto& param : instance->get<Wasm::WasmFunction>().type().parameters()) {
if (values_to_push.is_empty())
values.append(Wasm::Value { param, 0ull });
else
values.append(Wasm::Value { param, values_to_push.take_last() });
}
if (print) {
outln("Executing ");
print_func(*run_address);
outln();
}
auto result = machine.invoke(g_interpreter, run_address.value(), move(values));
if (debug)
launch_repl();
if (result.is_trap()) {
warnln("Execution trapped: {}", result.trap().reason);
} else {
if (!result.values().is_empty())
warnln("Returned:");
for (auto& value : result.values()) {
Wasm::Printer printer { stream };
g_stdout.write(" -> "sv.bytes());
g_printer.print(value);
}
}
}
}
return 0;
}