/* * Copyright (c) 2021, Ali Mohammad Pur * Copyright (c) 2022, the SerenityOS developers. * * SPDX-License-Identifier: BSD-2-Clause */ #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include RefPtr g_line_editor; static OwnPtr g_stdout {}; static OwnPtr g_printer {}; static bool g_continue { false }; static void (*old_signal)(int); static StackInfo g_stack_info; static Wasm::DebuggerBytecodeInterpreter g_interpreter(g_stack_info); struct ParsedValue { Wasm::Value value; Wasm::ValueType type; }; static void sigint_handler(int) { if (!g_continue) { signal(SIGINT, old_signal); kill(getpid(), SIGINT); } g_continue = false; } static Optional convert_to_uint(StringView string) { if (string.is_empty()) return {}; u128 value = 0; auto const characters = string.characters_without_null_termination(); for (size_t i = 0; i < string.length(); i++) { if (characters[i] < '0' || characters[i] > '9') return {}; value *= 10; value += u128 { static_cast(characters[i] - '0'), 0 }; } return value; } static Optional convert_to_uint_from_hex(StringView string) { if (string.is_empty()) return {}; u128 value = 0; auto const count = string.length(); auto const upper_bound = NumericLimits::max(); for (size_t i = 0; i < count; i++) { char digit = string[i]; if (value > (upper_bound >> 4)) return {}; auto digit_val = decode_hex_digit(digit); if (digit_val == 255) return {}; value = (value << 4) + digit_val; } return value; } static ErrorOr parse_value(StringView spec) { constexpr auto is_sep = [](char c) { return is_ascii_space(c) || c == ':'; }; // Scalar: 'T.const[:\s]v' (i32.const 42) auto parse_scalar = [](StringView text) -> ErrorOr { if constexpr (IsFloatingPoint) { if (text.trim_whitespace().equals_ignoring_ascii_case("nan"sv)) { if constexpr (IsSame) return Wasm::Value { nanf("") }; else return Wasm::Value { nan("") }; } if (text.trim_whitespace().equals_ignoring_ascii_case("inf"sv)) { if constexpr (IsSame) return Wasm::Value { HUGE_VALF }; else return Wasm::Value { HUGE_VAL }; } } if (auto v = text.to_number(); v.has_value()) return Wasm::Value { *v }; return Error::from_string_literal("Invalid scalar value"); }; // Vector: 'v128.const[:\s]v' (v128.const 0x01000000020000000300000004000000) or 'v(T.const[:\s]v, ...)' (v(i32.const 1, i32.const 2, i32.const 3, i32.const 4)) auto parse_u128 = [](StringView text) -> ErrorOr { u128 value; if (text.starts_with("0x"sv)) { if (auto v = convert_to_uint_from_hex(text); v.has_value()) value = *v; else return Error::from_string_literal("Invalid hex v128 value"); } else { if (auto v = convert_to_uint(text); v.has_value()) value = *v; else return Error::from_string_literal("Invalid v128 value"); } return Wasm::Value { value }; }; GenericLexer lexer(spec); if (lexer.consume_specific("v128.const"sv)) { lexer.ignore_while(is_sep); // The rest of the string is the value auto text = lexer.consume_all(); return ParsedValue { .value = TRY(parse_u128(text)), .type = Wasm::ValueType(Wasm::ValueType::Kind::V128) }; } if (lexer.consume_specific("i8.const"sv)) { lexer.ignore_while(is_sep); auto text = lexer.consume_all(); return ParsedValue { .value = TRY(parse_scalar.operator()(text)), .type = Wasm::ValueType(Wasm::ValueType::Kind::I32) }; } if (lexer.consume_specific("i16.const"sv)) { lexer.ignore_while(is_sep); auto text = lexer.consume_all(); return ParsedValue { .value = TRY(parse_scalar.operator()(text)), .type = Wasm::ValueType(Wasm::ValueType::Kind::I32) }; } if (lexer.consume_specific("i32.const"sv)) { lexer.ignore_while(is_sep); auto text = lexer.consume_all(); return ParsedValue { .value = TRY(parse_scalar.operator()(text)), .type = Wasm::ValueType(Wasm::ValueType::Kind::I32) }; } if (lexer.consume_specific("i64.const"sv)) { lexer.ignore_while(is_sep); auto text = lexer.consume_all(); return ParsedValue { .value = TRY(parse_scalar.operator()(text)), .type = Wasm::ValueType(Wasm::ValueType::Kind::I64) }; } if (lexer.consume_specific("f32.const"sv)) { lexer.ignore_while(is_sep); auto text = lexer.consume_all(); return ParsedValue { .value = TRY(parse_scalar.operator()(text)), .type = Wasm::ValueType(Wasm::ValueType::Kind::F32) }; } if (lexer.consume_specific("f64.const"sv)) { lexer.ignore_while(is_sep); auto text = lexer.consume_all(); return ParsedValue { .value = TRY(parse_scalar.operator()(text)), .type = Wasm::ValueType(Wasm::ValueType::Kind::F64) }; } if (lexer.consume_specific("v("sv)) { Vector values; for (;;) { lexer.ignore_while(is_sep); if (lexer.consume_specific(")"sv)) break; if (lexer.is_eof()) { warnln("Expected ')' to close vector"); break; } auto value = parse_value(lexer.consume_until(is_any_of(",)"sv))); if (value.is_error()) return value.release_error(); lexer.consume_specific(','); values.append(value.release_value()); } if (values.is_empty()) return Error::from_string_literal("Empty vector"); auto element_type = values.first().type; for (auto& value : values) { if (value.type != element_type) return Error::from_string_literal("Mixed types in vector"); } unsigned total_size = 0; unsigned width = 0; u128 result = 0; u128 last_value = 0; for (auto& parsed : values) { if (total_size >= 128) return Error::from_string_literal("Vector too large"); switch (parsed.type.kind()) { case Wasm::ValueType::F32: case Wasm::ValueType::I32: width = sizeof(u32); break; case Wasm::ValueType::F64: case Wasm::ValueType::I64: width = sizeof(u64); break; case Wasm::ValueType::V128: case Wasm::ValueType::FunctionReference: case Wasm::ValueType::ExternReference: VERIFY_NOT_REACHED(); } last_value = parsed.value.value(); result |= last_value << total_size; total_size += width * 8; } if (total_size < 128) warnln("Vector value '{}' is only {} bytes wide, repeating last element", spec, total_size); while (total_size < 128) { // Repeat the last value until we fill the 128 bits result |= last_value << total_size; total_size += width * 8; } return ParsedValue { .value = Wasm::Value { result }, .type = Wasm::ValueType(Wasm::ValueType::Kind::V128) }; } return Error::from_string_literal("Invalid value"); } 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(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_until_depleted(ByteString::formatted("{:0>4} ", ip.value())).release_value_but_fixme_should_propagate_errors(); g_printer->print(instr); } if (g_continue) return true; g_stdout->write_until_depleted(ByteString::formatted("{:0>4} ", ip.value())).release_value_but_fixme_should_propagate_errors(); g_printer->print(instr); ByteString 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 Print various things (see section on print)"); warnln("- call Call the function with the given arguments"); warnln("- set Set shell option (see section on settings)"); warnln("- unset 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 Print the contents of the memory identified by "); warnln("- print [[i]nstr]uction Print the current instruction"); warnln("- print [[f]unc]tion Print the function identified by "); 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_number(); 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_number(); 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()) { warnln("Host function at {:p}", &fn_value->function()); continue; } if (auto* fn_value = fn->get_pointer()) { g_printer->print(fn_value->code()); continue; } } } if (cmd == "call"sv) { if (args.size() < 2) { warnln("call what?"); continue; } Optional address; auto index = args[1].to_number(); 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()) { 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 values_to_push; Vector values; auto ok = true; for (size_t index = 2; index < args.size(); ++index) { auto r = parse_value(args[index]); if (r.is_error()) { warnln("Failed to parse argument {}: {}", args[index], r.error()); ok = false; break; } values_to_push.append(r.release_value()); } if (!ok) continue; for (auto& param : type.parameters()) { auto v = values_to_push.take_last(); if (v.type != param) { warnln("Type mismatch in argument: expected {}, but got {}", Wasm::ValueType::kind_name(param.kind()), Wasm::ValueType::kind_name(v.type.kind())); ok = false; break; } values.append(v.value); } if (!ok) continue; Wasm::Result result { Wasm::Trap {} }; { Wasm::BytecodeInterpreter::CallFrameHandle handle { g_interpreter, config }; result = config.call(g_interpreter, *address, move(values)).assert_wasm_result(); } if (result.is_trap()) { warnln("Execution trapped: {}", result.trap().reason); } else { if (!result.values().is_empty()) warnln("Returned:"); size_t index = 0; for (auto& value : result.values()) { g_stdout->write_until_depleted(" -> "sv.bytes()).release_value_but_fixme_should_propagate_errors(); g_printer->print(value, type.results()[index]); ++index; } } 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 RefPtr parse(StringView filename) { auto result = Core::MappedFile::map(filename); if (result.is_error()) { warnln("Failed to open {}: {}", filename, result.error()); return {}; } auto parse_result = Wasm::Module::parse(*result.value()); 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_byte_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 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; bool wasi = false; ByteString exported_function_to_execute; Vector values_to_push; Vector modules_to_link_in; Vector args_if_wasi; Vector wasi_preopened_mappings; 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"); parser.add_option(shell_mode, "Launch a REPL in the module's context (implies -i)", "shell", 's'); parser.add_option(wasi, "Enable WASI", "wasi", 'w'); parser.add_option(Core::ArgsParser::Option { .argument_mode = Core::ArgsParser::OptionArgumentMode::Required, .help_string = "Directory mappings to expose via WASI", .long_name = "wasi-map-dir", .short_name = 0, .value_name = "path[:path]", .accept_value = [&](StringView str) { if (!str.is_empty()) { wasi_preopened_mappings.append(str); return true; } return false; }, }); parser.add_option(Core::ArgsParser::Option { .argument_mode = Core::ArgsParser::OptionArgumentMode::Required, .help_string = "Extra modules to link with, use to resolve imports", .long_name = "link", .short_name = 'l', .value_name = "file", .accept_value = [&](StringView str) { if (!str.is_empty()) { modules_to_link_in.append(str); return true; } return false; }, }); parser.add_option(Core::ArgsParser::Option { .argument_mode = Core::ArgsParser::OptionArgumentMode::Required, .help_string = "Supply arguments to the function (default=0) (T.const:v or v(T.const:v, ...))", .long_name = "arg", .short_name = 0, .value_name = "value", .accept_value = [&](StringView str) -> bool { auto result = parse_value(str); if (result.is_error()) { warnln("Failed to parse value: {}", result.error()); return false; } values_to_push.append(result.release_value()); return true; }, }); parser.add_positional_argument(args_if_wasi, "Arguments to pass to the WASI module", "args", Core::ArgsParser::Required::No); 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.is_null()) return 1; g_stdout = TRY(Core::File::standard_output()); g_printer = TRY(try_make(*g_stdout)); if (print && !attempt_instantiate) { Wasm::Printer printer(*g_stdout); printer.print(*parse_result); } if (attempt_instantiate) { Wasm::AbstractMachine machine; Optional wasi_impl; if (wasi) { wasi_impl.emplace(Wasm::Wasi::Implementation::Details { .provide_arguments = [&] { Vector strings; for (auto& string : args_if_wasi) strings.append(String::from_utf8(string).release_value_but_fixme_should_propagate_errors()); return strings; }, .provide_environment = {}, .provide_preopened_directories = [&] { Vector paths; for (auto& string : wasi_preopened_mappings) { auto split_index = string.find(':'); if (split_index.has_value()) { LexicalPath host_path { FileSystem::real_path(string.substring_view(0, *split_index)).release_value_but_fixme_should_propagate_errors() }; LexicalPath mapped_path { string.substring_view(*split_index + 1) }; paths.append({move(host_path), move(mapped_path)}); } else { LexicalPath host_path { FileSystem::real_path(string).release_value_but_fixme_should_propagate_errors() }; LexicalPath mapped_path { string }; paths.append({move(host_path), move(mapped_path)}); } } return paths; }, }); } 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 Vector> linked_instances; Vector> linked_modules; for (auto& name : modules_to_link_in) { auto parse_result = parse(name); if (parse_result.is_null()) { warnln("Failed to parse linked module '{}'", name); return 1; } linked_modules.append(parse_result.release_nonnull()); 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 }; for (auto& instance : linked_instances) linker.link(*instance); if (wasi) { HashMap wasi_exports; for (auto& entry : linker.unresolved_imports()) { if (entry.module != "wasi_snapshot_preview1"sv) continue; auto function = wasi_impl->function_by_name(entry.name); if (function.is_error()) { dbgln("wasi function {} not implemented :(", entry.name); continue; } auto address = machine.store().allocate(function.release_value()); wasi_exports.set(entry, *address); } linker.link(wasi_exports); } if (export_all_imports) { HashMap exports; for (auto& entry : linker.unresolved_imports()) { if (!entry.type.has()) continue; auto type = parse_result->type_section().types()[entry.type.get().value()]; auto address = machine.store().allocate(Wasm::HostFunction( [name = entry.name, type = type](auto&, auto& arguments) -> Wasm::Result { StringBuilder argument_builder; bool first = true; size_t index = 0; for (auto& argument : arguments) { AllocatingMemoryStream stream; auto value_type = type.parameters()[index]; Wasm::Printer { stream }.print(argument, value_type); if (first) first = false; else argument_builder.append(", "sv); auto buffer = ByteBuffer::create_uninitialized(stream.used_buffer_size()).release_value_but_fixme_should_propagate_errors(); stream.read_until_filled(buffer).release_value_but_fixme_should_propagate_errors(); argument_builder.append(StringView(buffer).trim_whitespace()); ++index; } dbgln("[wasm runtime] Stub function {} was called with the following arguments: {}", name, argument_builder.to_byte_string()); Vector result; result.ensure_capacity(type.results().size()); for (auto expect_result : type.results()) result.append(Wasm::Value(expect_result)); return Wasm::Result { move(result) }; }, type, entry.name)); 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, 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 {}, expression, 0, }); Wasm::Instruction instr { Wasm::Instructions::nop }; Wasm::InstructionPointer ip { 0 }; g_continue = false; pre_interpret_hook(config, ip, instr); }; auto print_func = [&](auto const& address) { Wasm::FunctionInstance* fn = machine.store().get(address); g_stdout->write_until_depleted(ByteString::formatted("- Function with address {}, ptr = {}\n", address.value(), fn)).release_value_but_fixme_should_propagate_errors(); if (fn) { g_stdout->write_until_depleted(ByteString::formatted(" wasm function? {}\n", fn->has())).release_value_but_fixme_should_propagate_errors(); fn->visit( [&](Wasm::WasmFunction const& func) { Wasm::Printer printer { *g_stdout, 3 }; g_stdout->write_until_depleted(" type:\n"sv).release_value_but_fixme_should_propagate_errors(); printer.print(func.type()); g_stdout->write_until_depleted(" code:\n"sv).release_value_but_fixme_should_propagate_errors(); 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 run_address; Vector values; for (auto& entry : module_instance->exports()) { if (entry.name() == exported_function_to_execute) { if (auto addr = entry.value().get_pointer()) 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()) { warnln("Exported function is a host function, cannot run that yet"); return 1; } for (auto& param : instance->get().type().parameters()) { if (values_to_push.is_empty()) { values.append(Wasm::Value(param)); } else if (param == values_to_push.last().type) { values.append(values_to_push.take_last().value); } else { warnln("Type mismatch in argument: expected {}, but got {}", Wasm::ValueType::kind_name(param.kind()), Wasm::ValueType::kind_name(values_to_push.last().type.kind())); return 1; } } if (print) { outln("Executing "); print_func(*run_address); outln(); } auto result = machine.invoke(g_interpreter, run_address.value(), move(values)).assert_wasm_result(); if (debug) launch_repl(); if (result.is_trap()) { if (result.trap().reason.starts_with("exit:"sv)) return -result.trap().reason.substring_view(5).to_number().value_or(-1); warnln("Execution trapped: {}", result.trap().reason); } else { if (!result.values().is_empty()) warnln("Returned:"); auto result_type = instance->get().type().results(); size_t index = 0; for (auto& value : result.values()) { g_stdout->write_until_depleted(" -> "sv.bytes()).release_value_but_fixme_should_propagate_errors(); g_printer->print(value, result_type[index]); ++index; } } } } return 0; }