diff --git a/repos/os/include/monitor/gdb_packet.h b/repos/os/include/monitor/gdb_packet.h new file mode 100644 index 0000000000..2dba076de3 --- /dev/null +++ b/repos/os/include/monitor/gdb_packet.h @@ -0,0 +1,117 @@ +/* + * \brief GDB packet parser + * \author Norman Feske + * \date 2023-05-11 + */ + +/* + * Copyright (C) 2023 Genode Labs GmbH + * + * This file is part of the Genode OS framework, which is distributed + * under the terms of the GNU Affero General Public License version 3. + */ + +#ifndef _MONITOR__GDB_PACKET_H_ +#define _MONITOR__GDB_PACKET_H_ + +#include + +namespace Genode { template struct Gdb_packet; } + + +template +struct Genode::Gdb_packet +{ + enum class State { + IDLE, INCOMPLETE, + EXPECT_CHECKSUM1, EXPECT_CHECKSUM2, + COMPLETE, CORRUPT + }; + + State state { State::IDLE }; + + unsigned cursor = 0; + + struct Checksum + { + uint8_t accumulated; + uint8_t expected; + + bool matches() const { return (accumulated == expected); } + }; + + Checksum checksum { }; + + char buf[MAX_SIZE] { }; + + void reset() + { + state = State::IDLE; + cursor = 0; + checksum = { }; + } + + enum class Append_result { OK, COMPLETE, OVERFLOW, CORRUPT }; + + Append_result append(char const c) + { + if (cursor >= sizeof(buf)) + return Append_result::OVERFLOW; + + auto interpret = [&] + { + auto is_hex_digit = [] (char c) { return is_digit(c, true); }; + auto hex_digit = [] (char c) { return digit (c, true); }; + + if (state == State::EXPECT_CHECKSUM1 || state == State::EXPECT_CHECKSUM2) + if (!is_hex_digit(c)) + return State::CORRUPT; + + switch (state) { + + case State::IDLE: + return (c == '$') ? State::INCOMPLETE + : State::IDLE; + case State::INCOMPLETE: + return (c == '#') ? State::EXPECT_CHECKSUM1 + : State::INCOMPLETE; + case State::EXPECT_CHECKSUM1: + checksum.expected = uint8_t(hex_digit(c) << 4u); + return State::EXPECT_CHECKSUM2; + + case State::EXPECT_CHECKSUM2: + checksum.expected |= uint8_t(hex_digit(c)); + return checksum.matches() ? State::COMPLETE + : State::CORRUPT; + case State::COMPLETE: + case State::CORRUPT: + break; /* expect call of 'reset' */ + }; + return state; + }; + + State const orig_state = state; + + state = interpret(); + + /* capture only the command payload between '$' and '#' */ + if ((orig_state == State::INCOMPLETE) && (state == State::INCOMPLETE)) { + buf[cursor++] = c; + checksum.accumulated = uint8_t(checksum.accumulated + c); + } + + return (state == State::COMPLETE) ? Append_result::COMPLETE : + (state == State::CORRUPT) ? Append_result::CORRUPT : + Append_result::OK; + } + + bool complete() const { return state == State::COMPLETE; } + + void with_command(auto const &fn) const + { + if (complete()) + fn(Const_byte_range_ptr { buf, cursor }); + } +}; + +#endif /* _MONITOR__GDB_PACKET_H_ */ diff --git a/repos/os/include/monitor/output.h b/repos/os/include/monitor/output.h new file mode 100644 index 0000000000..72cb67a72e --- /dev/null +++ b/repos/os/include/monitor/output.h @@ -0,0 +1,61 @@ +/* + * \brief Output utilities + * \author Norman Feske + * \date 2023-06-06 + */ + +/* + * Copyright (C) 2023 Genode Labs GmbH + * + * This file is part of the Genode OS framework, which is distributed + * under the terms of the GNU Affero General Public License version 3. + */ + +#ifndef _MONITOR__OUTPUT_H_ +#define _MONITOR__OUTPUT_H_ + +#include + +namespace Genode { + + struct Gdb_hex; + struct Gdb_checksummed_output; +} + + +struct Genode::Gdb_hex : Hex +{ + template + explicit Gdb_hex(T value) : Hex(value, OMIT_PREFIX, PAD) { } +}; + + +struct Genode::Gdb_checksummed_output : Output +{ + Output &_output; + uint8_t _accumulated = 0; + + Gdb_checksummed_output(Output &output) : _output(output) + { + print(_output, "$"); + } + + ~Gdb_checksummed_output() + { + print(_output, "#", Gdb_hex(_accumulated)); + } + + void out_char(char c) override { out_string(&c, 1); } + + void out_string(char const *str, size_t n) override + { + n = (n == ~0UL) ? strlen(str) : n; + + for (unsigned i = 0; i < n; i++) + _accumulated = uint8_t(_accumulated + str[i]); + + _output.out_string(str, n); + } +}; + +#endif /* _MONITOR__OUTPUT_H_ */ diff --git a/repos/os/include/monitor/string.h b/repos/os/include/monitor/string.h new file mode 100644 index 0000000000..697ab55ea2 --- /dev/null +++ b/repos/os/include/monitor/string.h @@ -0,0 +1,76 @@ +/* + * \brief Parsing utilities + * \author Norman Feske + * \date 2023-06-06 + */ + +/* + * Copyright (C) 2023 Genode Labs GmbH + * + * This file is part of the Genode OS framework, which is distributed + * under the terms of the GNU Affero General Public License version 3. + */ + +#ifndef _MONITOR__STRING_H_ +#define _MONITOR__STRING_H_ + +#include + +namespace Genode { + + static void with_max_bytes(Const_byte_range_ptr const &bytes, + size_t const max, auto const &fn) + { + fn(Const_byte_range_ptr { bytes.start, min(max, bytes.num_bytes) }); + } + + static void with_skipped_bytes(Const_byte_range_ptr const &bytes, + size_t const n, auto const &fn) + { + if (bytes.num_bytes < n) + return; + + Const_byte_range_ptr const remainder { bytes.start + n, + bytes.num_bytes - n }; + fn(remainder); + } + + static void with_skipped_prefix(Const_byte_range_ptr const &bytes, + Const_byte_range_ptr const &prefix, + auto const &fn) + { + if (bytes.num_bytes < prefix.num_bytes) + return; + + if (strcmp(prefix.start, bytes.start, prefix.num_bytes) != 0) + return; + + with_skipped_bytes(bytes, prefix.num_bytes, fn); + } + + static void with_skipped_prefix(Const_byte_range_ptr const &bytes, + char const *prefix, auto const &fn) + { + with_skipped_prefix(bytes, Const_byte_range_ptr { prefix, strlen(prefix) }, fn); + } + + template + static void with_skipped_prefix(Const_byte_range_ptr const &bytes, + String const &prefix, auto const &fn) + { + with_skipped_prefix(bytes, prefix.string(), fn); + } + + /** + * Return true if 'bytes' equals the null-terminated string 'str' + */ + static inline bool equal(Const_byte_range_ptr const &bytes, char const *str) + { + size_t const len = strlen(str); + + return (len == bytes.num_bytes) && (strcmp(bytes.start, str, len) == 0); + } + +} + +#endif /* _MONITOR__STRING_H_ */ diff --git a/repos/os/lib/mk/spec/x86_64/monitor_gdb_arch.mk b/repos/os/lib/mk/spec/x86_64/monitor_gdb_arch.mk new file mode 100644 index 0000000000..019dab69a4 --- /dev/null +++ b/repos/os/lib/mk/spec/x86_64/monitor_gdb_arch.mk @@ -0,0 +1,5 @@ +SRC_BIN = gdb_target.xml +SRC_CC = gdb_arch.cc +INC_DIR += $(REP_DIR)/src/monitor + +vpath % $(REP_DIR)/src/monitor/spec/x86_64 diff --git a/repos/os/recipes/api/monitor/content.mk b/repos/os/recipes/api/monitor/content.mk new file mode 100644 index 0000000000..6f7c0d9220 --- /dev/null +++ b/repos/os/recipes/api/monitor/content.mk @@ -0,0 +1,8 @@ +content: include/monitor LICENSE + +include/monitor: + $(mirror_from_rep_dir) + +LICENSE: + cp $(GENODE_DIR)/LICENSE $@ + diff --git a/repos/os/recipes/api/monitor/hash b/repos/os/recipes/api/monitor/hash new file mode 100644 index 0000000000..6dd74b05f0 --- /dev/null +++ b/repos/os/recipes/api/monitor/hash @@ -0,0 +1 @@ +2023-06-09 132782703fc58b9aa089970fc4ae971c96f37fac diff --git a/repos/os/recipes/src/monitor/content.mk b/repos/os/recipes/src/monitor/content.mk new file mode 100644 index 0000000000..e83b06950e --- /dev/null +++ b/repos/os/recipes/src/monitor/content.mk @@ -0,0 +1,9 @@ +SRC_DIR = src/monitor +include $(GENODE_DIR)/repos/base/recipes/src/content.inc + +MIRROR_FROM_REP_DIR += lib/mk/spec/x86_64/monitor_gdb_arch.mk + +content: $(MIRROR_FROM_REP_DIR) + +$(MIRROR_FROM_REP_DIR): + $(mirror_from_rep_dir) diff --git a/repos/os/recipes/src/monitor/hash b/repos/os/recipes/src/monitor/hash new file mode 100644 index 0000000000..471c179b86 --- /dev/null +++ b/repos/os/recipes/src/monitor/hash @@ -0,0 +1 @@ +2023-06-09 0d4af26c7b3dfb7f197f46bda6ef09f90150666f diff --git a/repos/os/recipes/src/monitor/used_apis b/repos/os/recipes/src/monitor/used_apis new file mode 100644 index 0000000000..ae51077eb7 --- /dev/null +++ b/repos/os/recipes/src/monitor/used_apis @@ -0,0 +1,7 @@ +base +sandbox +os +monitor +report_session +timer_session +terminal_session diff --git a/repos/os/run/monitor.run b/repos/os/run/monitor.run new file mode 100644 index 0000000000..2d810a160c --- /dev/null +++ b/repos/os/run/monitor.run @@ -0,0 +1,80 @@ +proc platform_supported { } { + if {[have_spec x86_64] && [have_board pc]} { + if {![have_spec linux] && ![have_spec foc] && ![have_spec sel4]} { + return 1 } } + return 0 +} + +if {![platform_supported]} { + puts "Run script is not supported on this platform" + exit 0 +} + +build { core lib/ld init timer monitor test/monitor server/terminal_crosslink } + +create_boot_directory + +install_config { + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +} + +build_boot_image [build_artifacts] + +append qemu_args "-nographic " + +run_genode_until {\[init -> monitor\] child "test-monitor" exited with exit value 0.*\n} 30 diff --git a/repos/os/run/monitor_gdb.run b/repos/os/run/monitor_gdb.run new file mode 100644 index 0000000000..45db802f29 --- /dev/null +++ b/repos/os/run/monitor_gdb.run @@ -0,0 +1,120 @@ +proc platform_supported { } { + if {[have_spec x86_64] && [have_board pc]} { + if {![have_spec linux] && ![have_spec foc] && ![have_spec sel4]} { + return 1 } } + return 0 +} + +if {![platform_supported]} { + puts "Run script is not supported on this platform" + exit 0 +} + +build { core lib/ld init timer monitor drivers/uart test/log } + +create_boot_directory + +install_config { + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +} + +build_boot_image [build_artifacts] + +set local_port 5555 + +# qemu config +append qemu_args " -display none " + +# connect comport 0 to stdio +append qemu_args " -serial stdio " + +# connect comport 1 with TCP port $local_port +append qemu_args " -serial chardev:uart " +append qemu_args " -chardev socket,id=uart,port=$local_port,host=localhost,server,nowait,ipv4 " + +run_genode_until {.*\[init -> monitor -> first-test-log\].*} 30 +set genode_id [output_spawn_id] + +# sequence of GDB commands to execute at startup +set gdb_cmds "" +append gdb_cmds {-ex "target remote localhost:$local_port" } +append gdb_cmds {-ex "set style enabled off" } + +# test for dumping memory +append gdb_cmds {-ex "x/x 0x1003e8b" } + +# run GDB +eval spawn [gdb] debug/ld.lib.so -n $gdb_cmds +set gdb_id [list $spawn_id $genode_id] + +# show output of both GDB and Genode, supply user input to GDB +interact -i $gdb_id $genode_id diff --git a/repos/os/src/monitor/README b/repos/os/src/monitor/README new file mode 100644 index 0000000000..d54394a7f3 --- /dev/null +++ b/repos/os/src/monitor/README @@ -0,0 +1,75 @@ +The monitor component is a drop-in replacement for init that allows for the +inspection and debugging of child components. Its configuration mirrors the +configuration of init. + +In contrast to init, the monitor requests the 'platform_info' ROM to detect +the used kernel at runtime. This is needed because low-level mechanisms for +interacting with threads differ between the various kernels supported by +Genode. Note that Linux is not supported by the monitor. + +A component can be selected for monitoring by routing its PD and CPU sessions +to the monitor's local PD and CPU services instead of the routing those +sessions to the parent. For example, the following start node selects the +'test-log' component to be monitored. + +! +! +! +! +! +! +! +! +! + +If monitor's configuration features a sub node, the monitor creates +a terminal session that offers access to the monitored components via the GDB +protocol. Each component appears as a distinct GDB inferior. The following +GDB commands are useful: + +* List all monitored components: + ! (gdb) info inferiors + +* List all threads: + ! (gdb) info threads + +* Select a component for inspection, specifying the component's inferior ID: + ! (gdb) inferior 2 + +* Show 10 words of the component's memory at virtual address 0x100000: + ! x/10x 0x100000 + +The configuration can host optional nodes referring to +inferiors by their respective labels. For example: + +! +! +! + +;;; XXX not implemented, uncomment once completed +; +; By setting the 'wait' attribute to "yes", the execution of the inferior is +; delayed until the monitor receives the GDB command for continuing execution. +; This is useful for inspecting the startup phase of a component. By default, +; inferiors don't wait. +; +; The 'stop' attribute defines the behavior of an inferior when GDB connects +; to the monitor. By default, all inferiors are stopped. By setting the +; attribute to "no", a GDB connection does not interfere with the execution of +; the inferiors while memory can be inspected. + +The enabling of the 'wx' attribute prompts the monitor to map the executable +code of the monitored component as writeable memory, allowing the patching of +text segment by GDB, which is needed for using breakpoints. + + +RAM wiping +---------- + +As a secondary functionality, the monitor wipes all RAM dataspaces allocated +by the monitored components when the RAM is freed. This is useful as a possible +defense against cold-boot attacks. Note however that only RAM allocated by +monitored components is subjected to the wiping. Shared-memory buffers +obtained from a service - think of a GUI server - are freed by the service, +not the client. If the service is not monitored, client information could +prevail in such a shared-memory buffer. diff --git a/repos/os/src/monitor/gdb_arch.h b/repos/os/src/monitor/gdb_arch.h new file mode 100644 index 0000000000..97a3c55aca --- /dev/null +++ b/repos/os/src/monitor/gdb_arch.h @@ -0,0 +1,26 @@ +/* + * \brief Architecture-specific GDB protocol support + * \author Norman Feske + * \date 2023-05-15 + */ + +/* + * Copyright (C) 2023 Genode Labs GmbH + * + * This file is part of the Genode OS framework, which is distributed + * under the terms of the GNU Affero General Public License version 3. + */ + +#ifndef _GDB_ARCH_H_ +#define _GDB_ARCH_H_ + +#include +#include +#include + +namespace Monitor { namespace Gdb { + + void print_registers(Output &out, Cpu_state const &cpu); +} } + +#endif /* _GDB_ARCH_H_ */ diff --git a/repos/os/src/monitor/gdb_command.h b/repos/os/src/monitor/gdb_command.h new file mode 100644 index 0000000000..6790e4181e --- /dev/null +++ b/repos/os/src/monitor/gdb_command.h @@ -0,0 +1,187 @@ +/* + * \brief Interfaces for providing GDB commands + * \author Norman Feske + * \date 2023-05-11 + */ + +/* + * Copyright (C) 2023 Genode Labs GmbH + * + * This file is part of the Genode OS framework, which is distributed + * under the terms of the GNU Affero General Public License version 3. + */ + +#ifndef _GDB_COMMAND_H_ +#define _GDB_COMMAND_H_ + +#include +#include +#include + +namespace Monitor { namespace Gdb { + + struct State; + struct Command; + struct Command_with_separator; + struct Command_without_separator; + + using Commands = Registry; +} } + + +struct Monitor::Gdb::Command : private Commands::Element, Interface +{ + using Name = String<32>; + + Name const name; + + Command(Commands &commands, Name const &name) + : + Commands::Element(commands, *this), name(name) + { } + + void _match_name(Const_byte_range_ptr const &bytes, auto const &match_remainder_fn) const + { + with_skipped_prefix(bytes, name, match_remainder_fn); + } + + struct With_args_fn : Interface + { + virtual void call(Const_byte_range_ptr const &args) const = 0; + }; + + virtual void _with_args(Const_byte_range_ptr const &, With_args_fn const &) const = 0; + + void with_args(Const_byte_range_ptr const &command_bytes, auto const &fn) const + { + using Fn = typeof(fn); + struct Impl : With_args_fn + { + Fn const &_fn; + Impl(Fn const &fn) : _fn(fn) { } + void call(Const_byte_range_ptr const &args) const override { _fn(args); } + }; + _with_args(command_bytes, Impl(fn)); + } + + virtual void execute(State &, Const_byte_range_ptr const &args, Output &) const = 0; + + /** + * Argument-separating character + */ + struct Sep { char value; }; + + /** + * Call 'fn' for each semicolon-separated argument + */ + static void for_each_argument(Const_byte_range_ptr const &args, Sep sep, auto const &fn) + { + char const *start = args.start; + size_t num_bytes = args.num_bytes; + + for (; num_bytes > 0;) { + + auto field_len = [] (char sep, Const_byte_range_ptr const &arg) + { + size_t n = 0; + for ( ; (n < arg.num_bytes) && (arg.start[n] != sep); n++); + return n; + }; + + size_t const arg_len = field_len(sep.value, { start, num_bytes }); + + fn(Const_byte_range_ptr(start, arg_len)); + + auto skip = [&] (size_t n) + { + if (num_bytes >= n) { + start += n; + num_bytes -= n; + } + }; + + skip(arg_len); /* argument */ + skip(1); /* separating semicolon */ + } + } + + /** + * Call 'fn' with the Nth semicolon-separated argument + */ + static void with_argument(Const_byte_range_ptr const &args, Sep const sep, + unsigned const n, auto const &fn) + { + unsigned i = 0; + for_each_argument(args, sep, [&] (Const_byte_range_ptr const &arg) { + if (n == i++) + fn(arg); }); + } + + /** + * Call 'fn' with pointer to 'arg' as null-terminated string + * + * Note that the argument length is limited by the bounds of the locally + * allocated buffer, which is dimensioned for parsing number arguments. + */ + static void with_null_terminated(Const_byte_range_ptr const &arg, auto const &fn) + { + char null_terminated[20] { }; + memcpy(null_terminated, arg.start, + min(sizeof(null_terminated) - 1, arg.num_bytes)); + fn(const_cast(null_terminated)); + } + + /** + * Return Nth comma-separated hexadecimal number from args + */ + template + static T comma_separated_hex_value(Const_byte_range_ptr const &args, + unsigned const n, T const default_value) + { + T result { default_value }; + with_argument(args, Sep {','}, n, [&] (Const_byte_range_ptr const &arg) { + with_null_terminated(arg, [&] (char const *str) { + ascii_to_unsigned(str, result, 16); }); }); + return result; + } +}; + + +struct Monitor::Gdb::Command_with_separator : Command +{ + using Command::Command; + + void _match_separator(Const_byte_range_ptr const &bytes, auto const &match_remainder_fn) const + { + if (bytes.num_bytes == 0) + return; + + char const c = bytes.start[0]; + + if ((c == ',') || (c == ';') || (c || ':')) + with_skipped_bytes(bytes, 1, match_remainder_fn); + } + + void _with_args(Const_byte_range_ptr const &bytes, + With_args_fn const &fn) const override + { + _match_name(bytes, [&] (Const_byte_range_ptr const &bytes) { + _match_separator(bytes, [&] (Const_byte_range_ptr const &args) { + fn.call(args); }); }); + } +}; + + +struct Monitor::Gdb::Command_without_separator : Command +{ + using Command::Command; + + void _with_args(Const_byte_range_ptr const &bytes, + With_args_fn const &fn) const override + { + _match_name(bytes, [&] (Const_byte_range_ptr const &args) { + fn.call(args); }); + } +}; + +#endif /* _GDB_COMMAND_H_ */ diff --git a/repos/os/src/monitor/gdb_packet_handler.h b/repos/os/src/monitor/gdb_packet_handler.h new file mode 100644 index 0000000000..ce70466e65 --- /dev/null +++ b/repos/os/src/monitor/gdb_packet_handler.h @@ -0,0 +1,78 @@ +/* + * \brief GDB packet handler + * \author Norman Feske + * \date 2023-05-11 + */ + +/* + * Copyright (C) 2023 Genode Labs GmbH + * + * This file is part of the Genode OS framework, which is distributed + * under the terms of the GNU Affero General Public License version 3. + */ + +#ifndef _GDB_PACKET_HANDLER_H_ +#define _GDB_PACKET_HANDLER_H_ + +#include + +/* local includes */ +#include + +namespace Monitor { namespace Gdb { struct Packet_handler; } } + + +struct Monitor::Gdb::Packet_handler +{ + using Gdb_packet = Genode::Gdb_packet; + + Gdb_packet _packet { }; + + bool execute(State &state, + Commands const &commands, + Const_byte_range_ptr const &input, + Output &output) + { + bool progress = false; + for (unsigned i = 0; i < input.num_bytes; i++) { + + using Result = Gdb_packet::Append_result; + + switch (_packet.append(input.start[i])) { + + case Result::COMPLETE: + _packet.with_command([&] (Const_byte_range_ptr const &bytes) { + bool handled = false; + commands.for_each([&] (Command const &command) { + command.with_args(bytes, [&] (Const_byte_range_ptr const &args) { + print(output, "+"); /* ack */ + command.execute(state, args, output); + handled = true; }); }); + + if (!handled) + warning("unhandled GDB command: ", + Cstring(bytes.start, bytes.num_bytes)); + }); + _packet.reset(); + break; + + case Result::OVERFLOW: + error("received unexpectedly large GDB command"); + _packet.reset(); + break; + + case Result::CORRUPT: + error("received GDB command that could not be parsed"); + _packet.reset(); + break; + + case Result::OK: + break; + } + progress = true; + } + return progress; + } +}; + +#endif /* _GDB_PACKET_HANDLER_H_ */ diff --git a/repos/os/src/monitor/gdb_response.h b/repos/os/src/monitor/gdb_response.h new file mode 100644 index 0000000000..06b27d8803 --- /dev/null +++ b/repos/os/src/monitor/gdb_response.h @@ -0,0 +1,57 @@ +/* + * \brief Utilities for generating responses for the GDB protocol + * \author Norman Feske + * \date 2023-05-12 + */ + +/* + * Copyright (C) 2023 Genode Labs GmbH + * + * This file is part of the Genode OS framework, which is distributed + * under the terms of the GNU Affero General Public License version 3. + */ + +#ifndef _GDB_RESPONSE_H_ +#define _GDB_RESPONSE_H_ + +#include +#include + +namespace Genode { + + void gdb_response(Output &, auto const &fn); + + static inline void gdb_ok (Output &); + static inline void gdb_error(Output &, uint8_t); +} + + +/** + * Calls 'fn' with an output interface that wraps the date into a GDB packet + */ +void Genode::gdb_response(Output &output, auto const &fn) +{ + Gdb_checksummed_output checksummed_output { output }; + + fn(checksummed_output); +}; + + +/** + * Generate OK response + */ +static inline void Genode::gdb_ok(Output &output) +{ + gdb_response(output, [&] (Output &out) { print(out, "OK"); }); +} + + +/** + * Generate error respones + */ +static inline void Genode::gdb_error(Output &output, uint8_t errno) +{ + gdb_response(output, [&] (Output &out) { print(out, "E", Gdb_hex(errno)); }); +} + +#endif /* _GDB_RESPONSE_H_ */ diff --git a/repos/os/src/monitor/gdb_stub.h b/repos/os/src/monitor/gdb_stub.h new file mode 100644 index 0000000000..849b1116d3 --- /dev/null +++ b/repos/os/src/monitor/gdb_stub.h @@ -0,0 +1,535 @@ +/* + * \brief GDB stub + * \author Norman Feske + * \date 2023-05-11 + */ + +/* + * Copyright (C) 2023 Genode Labs GmbH + * + * This file is part of the Genode OS framework, which is distributed + * under the terms of the GNU Affero General Public License version 3. + */ + +#ifndef _GDB_STUB_H_ +#define _GDB_STUB_H_ + +#include +#include +#include +#include +#include + +namespace Monitor { namespace Gdb { struct State; } } + + +struct Monitor::Gdb::State : Noncopyable +{ + Inferiors &inferiors; + + struct Thread_list + { + char _buf[1024*16] { }; + size_t _len = 0; + + Thread_list(Inferiors const &inferiors) + { + Xml_generator xml(_buf, sizeof(_buf), "threads", [&] { + inferiors.for_each([&] (Inferior_pd const &inferior) { + inferior.for_each_thread([&] (Monitored_thread const &thread) { + xml.node("thread", [&] { + String<32> const id("p", inferior.id(), ".", thread.id()); + xml.attribute("id", id); + xml.attribute("core", 0); + xml.attribute("name", thread._name); }); }); }); }); + + _len = strlen(_buf); + } + + void with_bytes(auto const &fn) const + { + Const_byte_range_ptr const ptr { _buf, _len }; + fn(ptr); + } + }; + + Memory_accessor &_memory_accessor; + + struct Current : Noncopyable + { + Inferior_pd &pd; + + struct Thread + { + Monitored_thread &thread; + + Thread(Monitored_thread &thread) : thread(thread) { } + }; + + Constructible thread { }; + + Current(Inferior_pd &pd) : pd(pd) { } + }; + + Constructible _current { }; + + void flush(Inferior_pd &pd) + { + if (_current.constructed() && _current->pd.id() == pd.id()) + _current.destruct(); + } + + size_t read_memory(Memory_accessor::Virt_addr at, Byte_range_ptr const &dst) + { + if (_current.constructed()) + return _memory_accessor.read(_current->pd, at, dst); + + warning("attempt to read memory without a current target"); + return 0; + } + + bool current_defined() const { return _current.constructed(); } + + void current(Inferiors::Id pid, Threads::Id tid) + { + _current.destruct(); + + inferiors.for_each([&] (Inferior_pd &inferior) { + if (inferior.id() != pid.value) + return; + + _current.construct(inferior); + + inferior._threads.for_each([&] (Monitored_thread &thread) { + if (thread.id() == tid.value) + _current->thread.construct(thread); }); + }); + } + + void with_current_thread_state(auto const &fn) + { + Thread_state thread_state { }; + + if (_current.constructed() && _current->thread.constructed()) { + try { + thread_state = _current->thread->thread._real.call(); + } catch (Cpu_thread::State_access_failed) { + warning("unable to access state of thread ", _current->thread->thread.id()); + } + } + + fn(thread_state); + }; + + State(Inferiors &inferiors, Memory_accessor &memory_accessor) + : + inferiors(inferiors), _memory_accessor(memory_accessor) + { } +}; + + +/* + * The command types within the 'Gdb::Cmd' namespace deliberately do not follow + * Genode's coding style regarding the use of upper/lower case letters. + * + * The types are named precisely atfer the corresponding commands of the GDB + * prototol, which are case sensitive. + */ + +namespace Monitor { namespace Gdb { namespace Cmd { + + +/** + * Protocol negotiation + */ +struct qSupported : Command_with_separator +{ + qSupported(Commands &c) : Command_with_separator(c, "qSupported") { } + + void execute(State &, Const_byte_range_ptr const &args, Output &out) const override + { + /* check for expected GDB features */ + bool gdb_supports_multiprocess = false, + gdb_supports_vcont = false; + + for_each_argument(args, Sep {';'}, [&] (Const_byte_range_ptr const &arg) { + if (equal(arg, "multiprocess+")) gdb_supports_multiprocess = true; + if (equal(arg, "vContSupported+")) gdb_supports_vcont = true; + }); + + if (!gdb_supports_multiprocess) warning("GDB lacks multi-process support"); + if (!gdb_supports_vcont) warning("GDB lacks vcont support"); + + /* tell GDB about our features */ + gdb_response(out, [&] (Output &out) { + print(out, "PacketSize=", Gdb_hex(GDB_PACKET_MAX_SIZE), ";"); + print(out, "vContSupported+;"); + print(out, "qXfer:features:read+;"); /* XML target descriptions */ + print(out, "qXfer:threads:read+;"); + print(out, "multiprocess+;"); + print(out, "QNonStop+;"); + }); + } +}; + + +extern "C" char _binary_gdb_target_xml_start[]; +extern "C" char _binary_gdb_target_xml_end[]; + + +/** + * Query XML-based information + */ +struct qXfer : Command_with_separator +{ + qXfer(Commands &c) : Command_with_separator(c, "qXfer") { } + + struct Raw_data_ptr : Const_byte_range_ptr + { + Raw_data_ptr(char const *start, char const *end) + : + Const_byte_range_ptr(start, end - start) + { } + }; + + struct Window + { + size_t offset, len; + + static Window from_args(Const_byte_range_ptr const &args) + { + return { .offset = comma_separated_hex_value(args, 0, 0UL), + .len = comma_separated_hex_value(args, 1, 0UL) }; + } + }; + + static void _send_window(Output &out, Const_byte_range_ptr const &total_bytes, Window const window) + { + with_skipped_bytes(total_bytes, window.offset, [&] (Const_byte_range_ptr const &bytes) { + with_max_bytes(bytes, window.len, [&] (Const_byte_range_ptr const &bytes) { + gdb_response(out, [&] (Output &out) { + char const *prefix = (window.offset + window.len < total_bytes.num_bytes) + ? "m" : "l"; /* 'last' marker */ + print(out, prefix, Cstring(bytes.start, bytes.num_bytes)); }); }); }); + } + + void execute(State &state, Const_byte_range_ptr const &args, Output &out) const override + { + bool handled = false; + + with_skipped_prefix(args, "features:read:target.xml:", [&] (Const_byte_range_ptr const &args) { + Raw_data_ptr const total_bytes { _binary_gdb_target_xml_start, _binary_gdb_target_xml_end }; + _send_window(out, total_bytes, Window::from_args(args)); + handled = true; + }); + + with_skipped_prefix(args, "threads:read::", [&] (Const_byte_range_ptr const &args) { + State::Thread_list const thread_list(state.inferiors); + thread_list.with_bytes([&] (Const_byte_range_ptr const &bytes) { + _send_window(out, bytes, Window::from_args(args)); }); + handled = true; + }); + + if (!handled) + warning("GDB ", name, " command unsupported: ", Cstring(args.start, args.num_bytes)); + } +}; + + +struct vMustReplyEmpty : Command_without_separator +{ + vMustReplyEmpty(Commands &c) : Command_without_separator(c, "vMustReplyEmpty") { } + + void execute(State &, Const_byte_range_ptr const &, Output &out) const override + { + gdb_response(out, [&] (Output &) { }); + } +}; + + +/** + * Set current thread + */ +struct H : Command_without_separator +{ + H(Commands &commands) : Command_without_separator(commands, "H") { } + + void execute(State &state, Const_byte_range_ptr const &args, Output &out) const override + { + log("H command args: ", Cstring(args.start, args.num_bytes)); + + /* 'g' for other operations, 'p' as prefix of thread-id syntax */ + with_skipped_prefix(args, "gp", [&] (Const_byte_range_ptr const &args) { + + auto dot_separated_arg_value = [&] (unsigned i, auto &value) + { + with_argument(args, Sep { '.' }, i, [&] (Const_byte_range_ptr const &arg) { + with_null_terminated(arg, [&] (char const * const str) { + ascii_to(str, value); }); }); + }; + + unsigned pid = 0, tid = 0; + + dot_separated_arg_value(0, pid); + dot_separated_arg_value(1, tid); + + /* + * GDB initially sends a Hgp0.0 command but assumes that inferior 1 + * is current. Avoid losing the default current inferior as set by + * 'Main::_create_session'. + */ + if (pid > 0) + state.current(Inferiors::Id { pid }, Threads::Id { tid }); + + gdb_ok(out); + }); + + with_skipped_prefix(args, "c-", [&] (Const_byte_range_ptr const &) { + gdb_error(out, 1); }); + } +}; + + +/** + * Enable/disable non-stop mode + */ +struct QNonStop : Command_with_separator +{ + QNonStop(Commands &commands) : Command_with_separator(commands, "QNonStop") { } + + void execute(State &, Const_byte_range_ptr const &args, Output &out) const override + { + log("QNonStop command args: ", Cstring(args.start, args.num_bytes)); + + gdb_ok(out); + } +}; + + +/** + * Symbol-lookup prototol (not used) + */ +struct qSymbol : Command_with_separator +{ + qSymbol(Commands &commands) : Command_with_separator(commands, "qSymbol") { } + + void execute(State &, Const_byte_range_ptr const &, Output &out) const override + { + gdb_ok(out); + } +}; + + +/** + * Query trace status + */ +struct qTStatus : Command_without_separator +{ + qTStatus(Commands &commands) : Command_without_separator(commands, "qTStatus") { } + + void execute(State &, Const_byte_range_ptr const &, Output &out) const override + { + gdb_response(out, [&] (Output &) { }); + } +}; + + +/** + * Query current thread ID + */ +struct qC : Command_without_separator +{ + qC(Commands &commands) : Command_without_separator(commands, "qC") { } + + void execute(State &, Const_byte_range_ptr const &args, Output &out) const override + { + log("qC: ", Cstring(args.start, args.num_bytes)); + + gdb_response(out, [&] (Output &) { }); + } +}; + + +/** + * Query attached state + */ +struct qAttached : Command_without_separator +{ + qAttached(Commands &commands) : Command_without_separator(commands, "qAttached") { } + + void execute(State &, Const_byte_range_ptr const &, Output &out) const override + { + gdb_response(out, [&] (Output &out) { + print(out, "1"); }); + } +}; + + +/** + * Query text/data segment offsets + */ +struct qOffsets : Command_without_separator +{ + qOffsets(Commands &commands) : Command_without_separator(commands, "qOffsets") { } + + void execute(State &, Const_byte_range_ptr const &args, Output &out) const override + { + log("qOffsets: ", Cstring(args.start, args.num_bytes)); + + gdb_response(out, [&] (Output &) { }); + } +}; + + +/** + * Query halt reason + */ +struct ask : Command_without_separator +{ + ask(Commands &c) : Command_without_separator(c, "?") { } + + void execute(State &, Const_byte_range_ptr const &args, Output &out) const override + { + log("? command args: ", Cstring(args.start, args.num_bytes)); + + gdb_response(out, [&] (Output &out) { + print(out, "T05"); }); + } +}; + + +/** + * Read registers + */ +struct g : Command_without_separator +{ + g(Commands &c) : Command_without_separator(c, "g") { } + + void execute(State &state, Const_byte_range_ptr const &, Output &out) const override + { + log("-> execute g"); + + gdb_response(out, [&] (Output &out) { + state.with_current_thread_state([&] (Thread_state const &thread_state) { + print_registers(out, thread_state); }); }); + } +}; + + +/** + * Read memory + */ +struct m : Command_without_separator +{ + m(Commands &c) : Command_without_separator(c, "m") { } + + void execute(State &state, Const_byte_range_ptr const &args, Output &out) const override + { + addr_t const addr = comma_separated_hex_value(args, 0, addr_t(0)); + size_t const len = comma_separated_hex_value(args, 1, 0UL); + + gdb_response(out, [&] (Output &out) { + + for (size_t pos = 0; pos < len; ) { + + char chunk[16*1024] { }; + + size_t const remain_len = len - pos; + size_t const num_bytes = min(sizeof(chunk), remain_len); + + size_t const read_len = + state.read_memory(Memory_accessor::Virt_addr { addr + pos }, + Byte_range_ptr(chunk, num_bytes)); + + for (unsigned i = 0; i < read_len; i++) + print(out, Gdb_hex(chunk[i])); + + pos += read_len; + + if (read_len < num_bytes) + break; + } + }); + } +}; + + +/** + * Query liveliness of thread ID + */ +struct T : Command_without_separator +{ + T(Commands &c) : Command_without_separator(c, "T") { } + + void execute(State &, Const_byte_range_ptr const &args, Output &out) const override + { + log("T command args: ", Cstring(args.start, args.num_bytes)); + + gdb_ok(out); + } +}; + + +/** + * Disconnect + */ +struct D : Command_with_separator +{ + D(Commands &c) : Command_with_separator(c, "D") { } + + void execute(State &, Const_byte_range_ptr const &, Output &out) const override + { + gdb_ok(out); + } +}; + + +} /* namespace Cmd */ } /* namespace Gdb */ } /* namespace Monitor */ + + +/* + * Registry of all supported commands + */ + +namespace Monitor { namespace Gdb { struct Supported_commands; } } + +struct Monitor::Gdb::Supported_commands : Commands +{ + template + struct Instances; + + template + struct Instances + { + LAST _last; + Instances(Commands ®istry) : _last(registry) { }; + }; + + template + struct Instances + { + HEAD _head; + Instances _tail; + Instances(Commands ®istry) : _head(registry), _tail(registry) { } + }; + + Instances< + Cmd::qSupported, + Cmd::qXfer, + Cmd::vMustReplyEmpty, + Cmd::H, + Cmd::QNonStop, + Cmd::qSymbol, + Cmd::qTStatus, + Cmd::qC, + Cmd::qAttached, + Cmd::qOffsets, + Cmd::g, + Cmd::m, + Cmd::D, + Cmd::T, + Cmd::ask + > _instances { *this }; +}; + +#endif /* _GDB_STUB_H_ */ diff --git a/repos/os/src/monitor/inferior_cpu.h b/repos/os/src/monitor/inferior_cpu.h new file mode 100644 index 0000000000..436e7b11bc --- /dev/null +++ b/repos/os/src/monitor/inferior_cpu.h @@ -0,0 +1,101 @@ +/* + * \brief CPU session of monitored child PD + * \author Norman Feske + * \date 2023-05-08 + */ + +/* + * Copyright (C) 2023 Genode Labs GmbH + * + * This file is part of the Genode OS framework, which is distributed + * under the terms of the GNU Affero General Public License version 3. + */ + +#ifndef _INFERIOR_CPU_H_ +#define _INFERIOR_CPU_H_ + +/* local includes */ +#include + +namespace Monitor { struct Inferior_cpu; } + + +struct Monitor::Inferior_cpu : Monitored_cpu_session +{ + Allocator &_alloc; + + Constructible _native_cpu_nova { }; + + enum class Kernel { GENERIC, NOVA }; + + void init_native_cpu(Kernel kernel) + { + if (kernel == Kernel::NOVA) { + Capability native_cpu_cap = + reinterpret_cap_cast(_real.call()); + _native_cpu_nova.construct(_ep, native_cpu_cap, ""); + } + } + + Inferior_cpu(Entrypoint &ep, Capability real, + Name const &name, Allocator &alloc) + : + Monitored_cpu_session(ep, real, name), _alloc(alloc) + { } + + + /*************************** + ** Cpu_session interface ** + ***************************/ + + Thread_capability + create_thread(Capability pd, Cpu_session::Name const &name, + Affinity::Location affinity, Weight weight, addr_t utcb) override + { + Thread_capability result { }; + + Inferior_pd::with_inferior_pd(_ep, pd, + [&] (Inferior_pd &inferior_pd) { + + Capability real_thread = + _real.call(inferior_pd._real, + name, affinity, weight, utcb); + + Monitored_thread &monitored_thread = *new (_alloc) + Monitored_thread(_ep, real_thread, name, inferior_pd._threads, + inferior_pd.alloc_thread_id()); + + result = monitored_thread.cap(); + }, + [&] { + result = _real.call(pd, name, affinity, weight, utcb); + }); + return result; + } + + void kill_thread(Thread_capability thread) override { + _real.call(thread); } + + void exception_sigh(Signal_context_capability sigh) override { + _real.call(sigh); } + + Affinity::Space affinity_space() const override { + return _real.call(); } + + Dataspace_capability trace_control() override { + return _real.call(); } + + Quota quota() override { return _real.call(); } + + Capability native_cpu() override + { + if (_native_cpu_nova.constructed()) { + Untyped_capability cap = _native_cpu_nova->cap(); + return reinterpret_cap_cast(cap); + } + + return _real.call(); + } +}; + +#endif /* _INFERIOR_CPU_H_ */ diff --git a/repos/os/src/monitor/inferior_pd.h b/repos/os/src/monitor/inferior_pd.h new file mode 100644 index 0000000000..c02cfbe499 --- /dev/null +++ b/repos/os/src/monitor/inferior_pd.h @@ -0,0 +1,246 @@ +/* + * \brief Inferior is a monitored child PD + * \author Norman Feske + * \date 2023-05-08 + */ + +/* + * Copyright (C) 2023 Genode Labs GmbH + * + * This file is part of the Genode OS framework, which is distributed + * under the terms of the GNU Affero General Public License version 3. + */ + +#ifndef _INFERIOR_PD_H_ +#define _INFERIOR_PD_H_ + +/* Genode includes */ +#include +#include + +/* local includes */ +#include + +namespace Monitor { struct Inferior_pd; } + + +struct Monitor::Inferior_pd : Monitored_pd_session +{ + Inferiors::Element _inferiors_elem; + + Monitored_region_map _address_space { + _ep, _real.call(), "address space" }; + + Monitored_region_map _stack_area { + _ep, _real.call(), "stack area" }; + + Monitored_region_map _linker_area { + _ep, _real.call(), "linker area" }; + + Threads _threads { }; + + Threads::Id _last_thread_id { }; + + Io_signal_handler _page_fault_handler; + + Region_map &_local_rm; /* for wiping RAM dataspaces on free */ + Allocator &_alloc; /* used for allocating 'Ram_ds' objects */ + Ram_allocator &_wx_ram; /* RAM used for writeable text segments */ + + struct Policy + { + bool wait; /* wait for GDB continue command */ + bool stop; /* stop execution when GDB connects */ + bool wx; /* make text segments writeable */ + + static Policy from_xml(Xml_node const &policy) + { + return { .wait = policy.attribute_value("wait", false), + .stop = policy.attribute_value("stop", true), + .wx = policy.attribute_value("wx", false) }; + } + + static Policy default_policy() { return from_xml(""); } + }; + + Policy _policy = Policy::default_policy(); + + unsigned _page_fault_count = 0; + + void _handle_page_fault() { _page_fault_count++; } + + /** + * Keep track of allocated RAM dataspaces for wiping when freed + */ + struct Ram_ds : Id_space::Element + { + static Id_space::Id id(Dataspace_capability const &cap) + { + return { (unsigned long)cap.local_name() }; + } + + Ram_ds(Id_space &id_space, Dataspace_capability cap) + : + Id_space::Element(*this, id_space, id(cap)), cap(cap) + { } + + Dataspace_capability cap; + }; + + Id_space _ram_dataspaces { }; + + void _wipe_ram_ds(Ram_ds &ram_ds) + { + { + Attached_dataspace ds(_local_rm, ram_ds.cap); + memset(ds.local_addr(), 0, ds.size()); + } + destroy(_alloc, &ram_ds); + } + + + Inferior_pd(Entrypoint &ep, Capability real, Name const &name, + Inferiors &inferiors, Inferiors::Id id, Region_map &local_rm, + Allocator &alloc, Ram_allocator &wx_ram) + : + Monitored_pd_session(ep, real, name), + _inferiors_elem(*this, inferiors, id), + _page_fault_handler(ep, *this, &Inferior_pd::_handle_page_fault), + _local_rm(local_rm), _alloc(alloc), _wx_ram(wx_ram) + { + _address_space._real.call(_page_fault_handler); + _stack_area ._real.call(_page_fault_handler); + _linker_area ._real.call(_page_fault_handler); + } + + ~Inferior_pd() + { + while (_ram_dataspaces.apply_any([&] (Ram_ds &ram_ds) { + _wipe_ram_ds(ram_ds); })); + + while (_threads.apply_any([&] (Monitored_thread &thread) { + destroy(_alloc, &thread); })); + } + + void apply_monitor_config(Xml_node const &monitor) + { + with_matching_policy(_name, monitor, + [&] (Xml_node const policy) { _policy = Policy::from_xml(policy); }, + [&] { _policy = Policy::default_policy(); }); + + _address_space.writeable_text_segments(_alloc, _wx_ram, _local_rm); + _linker_area .writeable_text_segments(_alloc, _wx_ram, _local_rm); + } + + long unsigned id() const { return _inferiors_elem.id().value; } + + unsigned page_fault_count() const { return _page_fault_count; } + + Threads::Id alloc_thread_id() + { + _last_thread_id.value++; + return _last_thread_id; + } + + void for_each_thread(auto const &fn) const + { + _threads.for_each(fn); + } + + static void with_inferior_pd(Entrypoint &ep, Capability pd_cap, + auto const &monitored_fn, auto const &direct_fn) + { + with_monitored(ep, pd_cap, monitored_fn, direct_fn); + } + + + /************************** + ** Pd_session interface ** + **************************/ + + void assign_parent(Capability parent) override { + _real.call(parent); } + + bool assign_pci(addr_t pci_config_memory_address, uint16_t bdf) override { + return _real.call(pci_config_memory_address, bdf); } + + void map(addr_t virt, addr_t size) override { + _real.call(virt, size); } + + Signal_source_capability alloc_signal_source() override { + return _real.call(); } + + void free_signal_source(Signal_source_capability cap) override { + _real.call(cap); } + + Signal_context_capability alloc_context(Signal_source_capability source, + unsigned long imprint) override { + return _real.call(source, imprint); } + + void free_context(Signal_context_capability cap) override { + _real.call(cap); } + + void submit(Signal_context_capability receiver, unsigned cnt = 1) override { + _real.call(receiver, cnt); } + + Native_capability alloc_rpc_cap(Native_capability ep) override { + return _real.call(ep); } + + void free_rpc_cap(Native_capability cap) override { + _real.call(cap); } + + Capability address_space() override { return _address_space.cap(); } + + Capability stack_area() override { return _stack_area.cap(); } + + Capability linker_area() override { return _linker_area.cap(); } + + Cap_quota cap_quota() const override { + return _real.call(); } + + Cap_quota used_caps() const override { + return _real.call(); } + + Alloc_result try_alloc(size_t size, Cache cache = CACHED) override + { + return _real.call(size, cache).convert( + [&] (Ram_dataspace_capability cap) -> Alloc_result { + new (_alloc) Ram_ds(_ram_dataspaces, cap); + return cap; + }, + [&] (Alloc_error e) -> Alloc_result { return e; }); + } + + void free(Ram_dataspace_capability ds) override + { + _ram_dataspaces.apply(Ram_ds::id(ds), [&] (Ram_ds &ram_ds) { + _wipe_ram_ds(ram_ds); }); + + _real.call(ds); + } + + size_t dataspace_size(Ram_dataspace_capability ds) const override + { + return ds.valid() ? Dataspace_client(ds).size() : 0; + } + + Ram_quota ram_quota() const override { + return _real.call(); } + + Ram_quota used_ram() const override { + return _real.call(); } + + Capability native_pd() override { + return _real.call(); } + + Managing_system_state managing_system(Managing_system_state const & state) override { + return _real.call(state); } + + addr_t dma_addr(Ram_dataspace_capability ds) override { + return _real.call(ds); } + + Attach_dma_result attach_dma(Dataspace_capability ds, addr_t at) override { + return _real.call(ds, at); } +}; + +#endif /* _INFERIOR_PD_H_ */ diff --git a/repos/os/src/monitor/main.cc b/repos/os/src/monitor/main.cc new file mode 100644 index 0000000000..8fcac8c1c0 --- /dev/null +++ b/repos/os/src/monitor/main.cc @@ -0,0 +1,345 @@ +/* + * \brief Init component with builtin debug monitor + * \author Norman Feske + * \date 2023-05-08 + */ + +/* + * Copyright (C) 2023 Genode Labs GmbH + * + * This file is part of the Genode OS framework, which is distributed + * under the terms of the GNU Affero General Public License version 3. + */ + +/* Genode includes */ +#include +#include +#include +#include +#include +#include + +/* local includes */ +#include +#include +#include + +namespace Monitor { + + template + struct Local_session_base : Noncopyable + { + CONNECTION _connection; + + Local_session_base(Env &env, Session::Label const &label) + : + _connection(env, label) + { }; + }; + + template + struct Local_session : private Local_session_base, + public MONITORED_SESSION + { + using Local_session_base::_connection; + + template + Local_session(Env &env, Session::Label const &label, ARGS &&... args) + : + Local_session_base(env, label), + MONITORED_SESSION(env.ep(), _connection.cap(), label, args...) + { } + + void upgrade(Session::Resources const &resources) + { + _connection.upgrade(resources); + } + }; +} + + +namespace Monitor { struct Main; } + +struct Monitor::Main : Sandbox::State_handler +{ + using Local_pd_session = Local_session; + using Local_cpu_session = Local_session; + + using Pd_service = Sandbox::Local_service; + using Cpu_service = Sandbox::Local_service; + + Env &_env; + + Heap _heap { _env.ram(), _env.rm() }; + + Pd_intrinsics _pd_intrinsics { _env }; + + Sandbox _sandbox { _env, *this, _pd_intrinsics }; + + Inferior_cpu::Kernel _detect_kernel() + { + Attached_rom_dataspace info { _env, "platform_info" }; + + Inferior_cpu::Kernel result = Inferior_cpu::Kernel::GENERIC; + + info.xml().with_optional_sub_node("kernel", [&] (Xml_node const &kernel) { + if (kernel.attribute_value("name", String<10>()) == "nova") + result = Inferior_cpu::Kernel::NOVA; }); + + return result; + } + + Inferior_cpu::Kernel const _kernel = _detect_kernel(); + + Attached_rom_dataspace _config { _env, "config" }; + + Inferiors::Id _last_inferior_id { }; /* counter for unique inferior IDs */ + + Inferiors _inferiors { }; + + struct Gdb_stub + { + Env &_env; + + Terminal::Connection _terminal { _env }; + + Signal_handler _terminal_read_avail_handler { + _env.ep(), *this, &Gdb_stub::_handle_terminal_read_avail }; + + Memory_accessor _memory_accessor { _env }; + + Gdb::Packet_handler _packet_handler { }; + Gdb::State _state; + Gdb::Supported_commands _commands { }; + + struct Terminal_output + { + struct Write_fn + { + Terminal::Connection &_terminal; + void operator () (char const *str) + { + for (;;) { + size_t const num_bytes = strlen(str); + size_t const written_bytes = _terminal.write(str, num_bytes); + if (written_bytes == num_bytes) + break; + + str = str + written_bytes; + } + } + } _write_fn; + + Buffered_output<1024, Write_fn> buffered { _write_fn }; + }; + + void _handle_terminal_read_avail() + { + Terminal_output output { ._write_fn { _terminal } }; + + for (;;) { + char buffer[1024] { }; + size_t const num_bytes = _terminal.read(buffer, sizeof(buffer)); + if (!num_bytes) + return; + + _packet_handler.execute(_state, _commands, + Const_byte_range_ptr { buffer, num_bytes }, + output.buffered); + } + } + + void flush(Inferior_pd &pd) + { + _state.flush(pd); + _memory_accessor.flush(); + } + + Gdb_stub(Env &env, Inferiors &inferiors) + : + _env(env), _state(inferiors, _memory_accessor) + { + _terminal.read_avail_sigh(_terminal_read_avail_handler); + _handle_terminal_read_avail(); + } + }; + + Constructible _gdb_stub { }; + + void _handle_resource_avail() { } + + Signal_handler
_resource_avail_handler { + _env.ep(), *this, &Main::_handle_resource_avail }; + + Constructible _reporter { }; + + size_t _report_buffer_size = 0; + + template + void _handle_service(SERVICE &); + + void _handle_pd_service() { _handle_service (_pd_service); } + void _handle_cpu_service() { _handle_service(_cpu_service); } + + struct Service_handler : Sandbox::Local_service_base::Wakeup + { + Main &_main; + + using Member = void (Main::*) (); + Member _member; + + void wakeup_local_service() override { (_main.*_member)(); } + + Service_handler(Main &main, Member member) + : _main(main), _member(member) { } + }; + + Service_handler _pd_handler { *this, &Main::_handle_pd_service }; + Service_handler _cpu_handler { *this, &Main::_handle_cpu_service }; + + Pd_service _pd_service { _sandbox, _pd_handler }; + Cpu_service _cpu_service { _sandbox, _cpu_handler }; + + Local_pd_session &_create_session(Pd_service &, Session::Label const &label) + { + _last_inferior_id.value++; + + Inferiors::Id const id = _last_inferior_id; + + Local_pd_session &session = *new (_heap) + Local_pd_session(_env, label, _inferiors, id, _env.rm(), _heap, _env.ram()); + + _apply_monitor_config_to_inferiors(); + + /* set first monitored PD as current inferior */ + if (_gdb_stub.constructed() && !_gdb_stub->_state.current_defined()) + _gdb_stub->_state.current(id, Threads::Id { }); + + return session; + } + + Local_cpu_session &_create_session(Cpu_service &, Session::Label const &label) + { + Local_cpu_session &session = *new (_heap) Local_cpu_session(_env, label, _heap); + + session.init_native_cpu(_kernel); + + return session; + } + + void _destroy_session(Local_pd_session &session) + { + if (_gdb_stub.constructed()) + _gdb_stub->flush(session); + + destroy(_heap, &session); + } + + void _destroy_session(Local_cpu_session &session) + { + destroy(_heap, &session); + } + + void _apply_monitor_config_to_inferiors() + { + _config.xml().with_sub_node("monitor", + [&] (Xml_node const monitor) { + _inferiors.for_each([&] (Inferior_pd &pd) { + pd.apply_monitor_config(monitor); }); }, + [&] { + _inferiors.for_each([&] (Inferior_pd &pd) { + pd.apply_monitor_config(""); }); }); + } + + void _handle_config() + { + _config.update(); + + Xml_node const config = _config.xml(); + + bool reporter_enabled = false; + config.with_optional_sub_node("report", [&] (Xml_node report) { + + reporter_enabled = true; + + /* (re-)construct reporter whenever the buffer size is changed */ + Number_of_bytes const buffer_size = + report.attribute_value("buffer", Number_of_bytes(4096)); + + if (buffer_size != _report_buffer_size || !_reporter.constructed()) { + _report_buffer_size = buffer_size; + _reporter.construct(_env, "state", "state", _report_buffer_size); + } + }); + + if (_reporter.constructed()) + _reporter->enabled(reporter_enabled); + + _gdb_stub.conditional(config.has_sub_node("monitor"), _env, _inferiors); + + _apply_monitor_config_to_inferiors(); + + _sandbox.apply_config(config); + } + + Signal_handler
_config_handler { + _env.ep(), *this, &Main::_handle_config }; + + /** + * Sandbox::State_handler interface + */ + void handle_sandbox_state() override + { + try { + Reporter::Xml_generator xml(*_reporter, [&] () { + _sandbox.generate_state_report(xml); }); + } + catch (Xml_generator::Buffer_exceeded) { + + error("state report exceeds maximum size"); + + /* try to reflect the error condition as state report */ + try { + Reporter::Xml_generator xml(*_reporter, [&] () { + xml.attribute("error", "report buffer exceeded"); }); + } + catch (...) { } + } + } + + Main(Env &env) : _env(env) + { + _config.sigh(_config_handler); + + /* prevent blocking for resource upgrades (never satisfied by core) */ + _env.parent().resource_avail_sigh(_resource_avail_handler); + + _handle_config(); + } +}; + + +template +void Monitor::Main::_handle_service(SERVICE &service) +{ + using Local_session = typename SERVICE::Local_session; + + service.for_each_requested_session([&] (typename SERVICE::Request &request) { + request.deliver_session(_create_session(service, request.label)); + }); + + service.for_each_upgraded_session([&] (Local_session &session, + Session::Resources const &amount) { + session.upgrade(amount); + return SERVICE::Upgrade_response::CONFIRMED; + }); + + service.for_each_session_to_close([&] (Local_session &session) { + _destroy_session(session); + return SERVICE::Close_response::CLOSED; + }); +} + + +void Component::construct(Genode::Env &env) { static Monitor::Main main(env); } + diff --git a/repos/os/src/monitor/memory_accessor.h b/repos/os/src/monitor/memory_accessor.h new file mode 100644 index 0000000000..8bc3da58bf --- /dev/null +++ b/repos/os/src/monitor/memory_accessor.h @@ -0,0 +1,246 @@ +/* + * \brief Mechanism for accessing the virtual memory of inferiors + * \author Norman Feske + * \date 2023-05-24 + * + * The 'Memory_accessor' uses a dedicated "probe" thread of accessing the + * inferior's memory. It keeps a window of the address space locally attached + * and guards the access of this window by the probe thread by watching out for + * page faults. The sacrifice of the probe thread shields the monitor from the + * effects of accessing empty regions of the inferior's address space. + */ + +/* + * Copyright (C) 2023 Genode Labs GmbH + * + * This file is part of the Genode OS framework, which is distributed + * under the terms of the GNU Affero General Public License version 3. + */ + +#ifndef _MEMORY_ACCESSOR_H_ +#define _MEMORY_ACCESSOR_H_ + +/* Genode includes */ +#include +#include + +/* local includes */ +#include +#include + +namespace Monitor { struct Memory_accessor; } + + +class Monitor::Memory_accessor : Noncopyable +{ + public: + + struct Virt_addr { addr_t value; }; + + private: + + Env &_env; + + static constexpr size_t WINDOW_SIZE_LOG2 = 24, /* 16 MiB */ + WINDOW_SIZE = 1 << WINDOW_SIZE_LOG2; + + struct Curr_view + { + Region_map &_local_rm; + Inferior_pd &_pd; + addr_t const _offset; + + struct { uint8_t const *_local_ptr; }; + + Curr_view(Region_map &local_rm, Inferior_pd &pd, addr_t offset) + : + _local_rm(local_rm), _pd(pd), _offset(offset), + _local_ptr(_local_rm.attach(pd._address_space.dataspace(), + WINDOW_SIZE, offset)) + { } + + ~Curr_view() { _local_rm.detach(_local_ptr); } + + bool _in_curr_range(Virt_addr at) const + { + return (at.value >= _offset) && (at.value < _offset + WINDOW_SIZE); + } + + bool contains(Inferior_pd &pd, Virt_addr at) const + { + return (pd.id() == _pd.id()) && _in_curr_range(at); + } + }; + + Constructible _curr_view { }; + + using Probe_response_handler = Io_signal_handler; + + Io_signal_handler _probe_response_handler { + _env.ep(), *this, &Memory_accessor::_handle_probe_response }; + + void _handle_probe_response() { } + + struct Probe : Thread + { + Probe_response_handler &_probe_response_handler; + + Blockade _blockade { }; + + /* exists only temporary during a 'Memory_accessor::read' call */ + struct Read_job + { + Curr_view &curr_view; + Virt_addr at; + Byte_range_ptr const &dst; + + size_t pos = 0; /* number of completed bytes */ + bool done = 0; /* true if 'execute_may_fault' survived */ + + Read_job(Curr_view &curr_view, Virt_addr at, Byte_range_ptr const &dst) + : + curr_view(curr_view), at(at), dst(dst) + { } + + /* called only once */ + void execute_may_fault() + { + /* offset from the start of the window */ + addr_t const window_pos = at.value - curr_view._offset; + addr_t const num_bytes_at_window_pos = WINDOW_SIZE - window_pos; + size_t const len = min(dst.num_bytes, num_bytes_at_window_pos); + + uint8_t const * const src_ptr = curr_view._local_ptr; + + for (pos = 0; pos < len; pos++) + dst.start[pos] = src_ptr[window_pos + pos]; + + done = true; + } + }; + + Constructible _read_job { }; + + /** + * Thread interface + */ + void entry() override + { + for (;;) { + _blockade.block(); + + if (_read_job.constructed()) { + _read_job->execute_may_fault(); + + /* wakeup 'Memory_accessor::read' via I/O signal */ + _probe_response_handler.local_submit(); + } + } + } + + Probe(Env &env, Probe_response_handler &probe_response_handler) + : + Thread(env, "probe", 16*1024), + _probe_response_handler(probe_response_handler) + { + start(); + } + + size_t read(Curr_view &curr_view, + Virt_addr at, Byte_range_ptr const &dst, auto const &block_fn) + { + _read_job.construct(curr_view, at, dst); + _blockade.wakeup(); + + /* block until read is done or a page fault occurred */ + while (!_read_job->done && block_fn()); + + size_t const result = _read_job->pos; + + _read_job.destruct(); + + return result; + } + }; + + Constructible _probe { }; + + Io_signal_handler _timeout_handler { + _env.ep(), *this, &Memory_accessor::_handle_timeout }; + + Timer::Connection _watchdog_timer { _env }; + + unsigned _timeout_count = 0; + + void _handle_timeout() { _timeout_count++; } + + public: + + Memory_accessor(Env &env) : _env(env) + { + _watchdog_timer.sigh(_timeout_handler); + } + + void flush() { _curr_view.destruct(); } + + /** + * Read memory from inferior 'pd' at address 'at' into buffer 'dst' + * + * The 'dst.num_bytes' value denotes the number of bytes to read. + * + * \return number of successfully read bytes, which can be smaller + * than 'dst.num_bytes' when encountering the bounds of + * mapped memory in the inferior's address space. + */ + size_t read(Inferior_pd &pd, Virt_addr at, Byte_range_ptr const &dst) + { + if (_curr_view.constructed() && !_curr_view->contains(pd, at)) + _curr_view.destruct(); + + if (!_curr_view.constructed()) { + addr_t const offset = at.value & ~(WINDOW_SIZE - 1); + try { _curr_view.construct(_env.rm(), pd, offset); } + catch (Region_map::Region_conflict) { + warning("attempt to read outside the virtual address space: ", + Hex(at.value)); + return 0; + } + } + + /* give up after 100 milliseconds */ + _watchdog_timer.trigger_once(1000*100); + + /* drain pending signals to avoid spurious watchdog timeouts */ + while (_env.ep().dispatch_pending_io_signal()); + + unsigned const orig_page_fault_count = pd.page_fault_count(); + unsigned const orig_timeout_count = _timeout_count; + + if (!_probe.constructed()) + _probe.construct(_env, _probe_response_handler); + + auto fault_or_timeout_occurred = [&] { + return (orig_page_fault_count != pd.page_fault_count()) + || (orig_timeout_count != _timeout_count); }; + + auto block_fn = [&] + { + if (fault_or_timeout_occurred()) + return false; /* cancel read */ + + _env.ep().wait_and_dispatch_one_io_signal(); + return true; /* keep trying */ + }; + + size_t const read_num_bytes = + _probe->read(*_curr_view, at, dst, block_fn); + + /* wind down the faulted probe thread, spawn a fresh one on next call */ + if (fault_or_timeout_occurred()) + _probe.destruct(); + + return read_num_bytes; + } +}; + +#endif /* _MEMORY_ACCESSOR_H_ */ diff --git a/repos/os/src/monitor/monitored_cpu.h b/repos/os/src/monitor/monitored_cpu.h new file mode 100644 index 0000000000..856c317ce9 --- /dev/null +++ b/repos/os/src/monitor/monitored_cpu.h @@ -0,0 +1,74 @@ +/* + * \brief Monitored CPU session + * \author Norman Feske + * \date 2023-05-09 + */ + +/* + * Copyright (C) 2023 Genode Labs GmbH + * + * This file is part of the Genode OS framework, which is distributed + * under the terms of the GNU Affero General Public License version 3. + */ + +#ifndef _MONITORED_CPU_H_ +#define _MONITORED_CPU_H_ + +/* Genode includes */ +#include +#include + +/* local includes */ +#include +#include + +namespace Monitor { struct Monitored_cpu_session; } + + +struct Monitor::Monitored_cpu_session : Monitored_rpc_object +{ + using Monitored_rpc_object::Monitored_rpc_object; + + void _with_cpu_arg(Capability cpu_cap, + auto const &monitored_fn, auto const &direct_fn) + { + if (cpu_cap == cap()) { + error("attempt to pass invoked capability as RPC argument"); + return; + } + with_monitored(_ep, cpu_cap, monitored_fn, direct_fn); + } + + + /*************************** + ** Cpu_session interface ** + ***************************/ + + int ref_account(Cpu_session_capability cpu_cap) override + { + int result = 0; + + _with_cpu_arg(cpu_cap, + [&] (Monitored_cpu_session &monitored_cpu) { + result = _real.call(monitored_cpu._real); }, + [&] { + result = _real.call(cpu_cap); }); + + return result; + } + + int transfer_quota(Cpu_session_capability cpu_cap, size_t amount) override + { + int result = 0; + + _with_cpu_arg(cpu_cap, + [&] (Monitored_cpu_session &monitored_cpu) { + result = _real.call(monitored_cpu._real, amount); }, + [&] { + result = _real.call(cpu_cap, amount); }); + + return result; + } +}; + +#endif /* _MONITORED_CPU_H_ */ diff --git a/repos/os/src/monitor/monitored_native_cpu.h b/repos/os/src/monitor/monitored_native_cpu.h new file mode 100644 index 0000000000..3cbed0c926 --- /dev/null +++ b/repos/os/src/monitor/monitored_native_cpu.h @@ -0,0 +1,44 @@ +/* + * \brief Monitored kernel-specific native CPU interface + * \author Norman Feske + * \date 2023-05-16 + */ + +/* + * Copyright (C) 2023 Genode Labs GmbH + * + * This file is part of the Genode OS framework, which is distributed + * under the terms of the GNU Affero General Public License version 3. + */ + +#ifndef _MONITORED_NATIVE_CPU_H_ +#define _MONITORED_NATIVE_CPU_H_ + +/* Genode includes */ +#include +#include + +/* local includes */ +#include +#include +#include + +namespace Monitor { struct Monitored_native_cpu_nova; } + + +struct Monitor::Monitored_native_cpu_nova : Monitored_rpc_object +{ + using Monitored_rpc_object::Monitored_rpc_object; + + void thread_type(Capability cap, Thread_type type, + Exception_base exc) override + { + Monitored_thread::with_thread(_ep, cap, + [&] (Monitored_thread &monitored_thread) { + _real.call(monitored_thread._real, type, exc); }, + [&] /* thread not intercepted */ { + _real.call(cap, type, exc); }); + } +}; + +#endif /* _MONITORED_NATIVE_CPU_H_ */ diff --git a/repos/os/src/monitor/monitored_pd.h b/repos/os/src/monitor/monitored_pd.h new file mode 100644 index 0000000000..a5796b9f3e --- /dev/null +++ b/repos/os/src/monitor/monitored_pd.h @@ -0,0 +1,73 @@ +/* + * \brief Monitored PD session + * \author Norman Feske + * \date 2023-05-08 + */ + +/* + * Copyright (C) 2023 Genode Labs GmbH + * + * This file is part of the Genode OS framework, which is distributed + * under the terms of the GNU Affero General Public License version 3. + */ + +#ifndef _MONITORED_PD_H_ +#define _MONITORED_PD_H_ + +/* Genode includes */ +#include +#include + +/* local includes */ +#include +#include + +namespace Monitor { struct Monitored_pd_session; } + + +struct Monitor::Monitored_pd_session : Monitored_rpc_object +{ + using Monitored_rpc_object::Monitored_rpc_object; + + void _with_pd_arg(Capability pd_cap, + auto const &monitored_fn, auto const &direct_fn) + { + if (pd_cap == cap()) { + error("attempt to pass invoked capability as RPC argument"); + return; + } + with_monitored(_ep, pd_cap, monitored_fn, direct_fn); + } + + + /************************** + ** Pd_session interface ** + **************************/ + + void ref_account(Capability pd_cap) override + { + _with_pd_arg(pd_cap, + [&] (Monitored_pd_session &pd) { _real.call(pd._real); }, + [&] { _real.call(pd_cap); }); + } + + void transfer_quota(Capability pd_cap, Cap_quota amount) override + { + _with_pd_arg(pd_cap, + [&] (Monitored_pd_session &pd) { + _real.call(pd._real, amount); }, + [&] { + _real.call(pd_cap, amount); }); + } + + void transfer_quota(Pd_session_capability pd_cap, Ram_quota amount) override + { + _with_pd_arg(pd_cap, + [&] (Monitored_pd_session &pd) { + _real.call(pd._real, amount); }, + [&] { + _real.call(pd_cap, amount); }); + } +}; + +#endif /* _MONITORED_PD_H_ */ diff --git a/repos/os/src/monitor/monitored_region_map.h b/repos/os/src/monitor/monitored_region_map.h new file mode 100644 index 0000000000..2f0eeddb53 --- /dev/null +++ b/repos/os/src/monitor/monitored_region_map.h @@ -0,0 +1,145 @@ +/* + * \brief Monitored region map + * \author Norman Feske + * \date 2023-05-09 + */ + +/* + * Copyright (C) 2023 Genode Labs GmbH + * + * This file is part of the Genode OS framework, which is distributed + * under the terms of the GNU Affero General Public License version 3. + */ + +#ifndef _MONITORED_REGION_MAP_H_ +#define _MONITORED_REGION_MAP_H_ + +/* Genode includes */ +#include +#include +#include + +/* local includes */ +#include + +namespace Monitor { struct Monitored_region_map; } + + +struct Monitor::Monitored_region_map : Monitored_rpc_object +{ + using Monitored_rpc_object::Monitored_rpc_object; + + /* see the comment in base/include/region_map/client.h */ + Dataspace_capability _rm_ds_cap { }; + + struct Writeable_text_segments + { + Allocator &_alloc; + Ram_allocator &_ram; + Region_map &_local_rm; + + struct Ram_ds : Registry::Element + { + Attached_ram_dataspace ds; + + Ram_ds(Registry ®istry, Ram_allocator &ram, Region_map &local_rm, + Const_byte_range_ptr const &content) + : + Registry::Element(registry, *this), + ds(ram, local_rm, content.num_bytes) + { + memcpy(ds.local_addr(), content.start, content.num_bytes); + } + }; + + Registry _dataspaces { }; + + Writeable_text_segments(Allocator &alloc, + Ram_allocator &ram, + Region_map &local_rm) + : + _alloc(alloc), _ram(ram), _local_rm(local_rm) + { } + + ~Writeable_text_segments() + { + _dataspaces.for_each([&] (Ram_ds &ram_ds) { + destroy(_alloc, &ram_ds); }); + } + + Dataspace_capability create_writable_copy(Dataspace_capability orig_ds, + off_t offset, size_t size) + { + Attached_dataspace ds { _local_rm, orig_ds }; + + if (size_t(offset) >= ds.size()) + return Dataspace_capability(); + + Const_byte_range_ptr const + content_ptr(ds.local_addr() + offset, + min(size, ds.size() - size_t(offset))); + + Ram_ds &ram_ds = *new (_alloc) + Ram_ds(_dataspaces, _ram, _local_rm, content_ptr); + + return ram_ds.ds.cap(); + } + }; + + Constructible _writeable_text_segments { }; + + + void writeable_text_segments(Allocator &alloc, + Ram_allocator &ram, + Region_map &local_rm) + { + if (!_writeable_text_segments.constructed()) + _writeable_text_segments.construct(alloc, ram, local_rm); + } + + + /************************** + ** Region_map interface ** + **************************/ + + Local_addr attach(Dataspace_capability ds, size_t size = 0, + off_t offset = 0, bool use_local_addr = false, + Local_addr local_addr = (void *)0, + bool executable = false, + bool writeable = true) override + { + if (executable && !writeable && _writeable_text_segments.constructed()) { + ds = _writeable_text_segments->create_writable_copy(ds, offset, size); + offset = 0; + writeable = true; + } + + return _real.call(ds, size, offset, use_local_addr, local_addr, + executable, writeable); + } + + void detach(Local_addr local_addr) override + { + _real.call(local_addr); + } + + void fault_handler(Signal_context_capability) override + { + warning("Monitored_region_map: ignoring custom fault_handler for ", _name); + } + + State state() override + { + return _real.call(); + } + + Dataspace_capability dataspace() override + { + if (!_rm_ds_cap.valid()) + _rm_ds_cap = _real.call(); + + return _rm_ds_cap; + } +}; + +#endif /* _MONITORED_REGION_MAP_H_ */ diff --git a/repos/os/src/monitor/monitored_thread.h b/repos/os/src/monitor/monitored_thread.h new file mode 100644 index 0000000000..01c5c4fe8c --- /dev/null +++ b/repos/os/src/monitor/monitored_thread.h @@ -0,0 +1,84 @@ +/* + * \brief Monitored CPU thread + * \author Norman Feske + * \date 2023-05-16 + */ + +/* + * Copyright (C) 2023 Genode Labs GmbH + * + * This file is part of the Genode OS framework, which is distributed + * under the terms of the GNU Affero General Public License version 3. + */ + +#ifndef _MONITORED_THREAD_H_ +#define _MONITORED_THREAD_H_ + +/* Genode includes */ +#include +#include + +/* local includes */ +#include + +namespace Monitor { struct Monitored_thread; } + + +struct Monitor::Monitored_thread : Monitored_rpc_object +{ + static void with_thread(Entrypoint &ep, Capability cap, + auto const &monitored_fn, auto const &direct_fn) + { + with_monitored(ep, cap, monitored_fn, direct_fn); + } + + Threads::Element _threads_elem; + + using Monitored_rpc_object::Monitored_rpc_object; + + Monitored_thread(Entrypoint &ep, Capability real, Name const &name, + Threads &threads, Threads::Id id) + : + Monitored_rpc_object(ep, real, name), _threads_elem(*this, threads, id) + { } + + long unsigned id() const { return _threads_elem.id().value; } + + Dataspace_capability utcb() override { + return _real.call(); } + + void start(addr_t ip, addr_t sp) override { + _real.call(ip, sp); } + + void pause() override { + _real.call(); } + + void resume() override { + _real.call(); } + + Thread_state state() override { + return _real.call(); } + + void state(Thread_state const &state) override { + _real.call(state); } + + void exception_sigh(Signal_context_capability handler) override { + _real.call(handler); } + + void single_step(bool enabled) override { + _real.call(enabled); } + + void affinity(Affinity::Location location) override { + _real.call(location); } + + unsigned trace_control_index() override { + return _real.call(); } + + Dataspace_capability trace_buffer() override { + return _real.call(); } + + Dataspace_capability trace_policy() override { + return _real.call(); } +}; + +#endif /* _MONITORED_THREAD_H_ */ diff --git a/repos/os/src/monitor/native_cpu_nova.h b/repos/os/src/monitor/native_cpu_nova.h new file mode 100644 index 0000000000..11166cb57c --- /dev/null +++ b/repos/os/src/monitor/native_cpu_nova.h @@ -0,0 +1,49 @@ +/* + * \brief NOVA-specific part of the CPU session interface + * \author Norman Feske + * \date 2022-06-02 + * + * Mirrored from 'base-nova/include/nova_native_cpu/nova_native_cpu.h'. + */ + +/* + * Copyright (C) 2016-2023 Genode Labs GmbH + * + * This file is part of the Genode OS framework, which is distributed + * under the terms of the GNU Affero General Public License version 3. + */ + +#ifndef _NATIVE_CPU_NOVA_H_ +#define _NATIVE_CPU_NOVA_H_ + +#include + +/* local includes */ +#include + +namespace Monitor { struct Native_cpu_nova; } + + +struct Monitor::Native_cpu_nova : Interface +{ + enum Thread_type { GLOBAL, LOCAL, VCPU }; + + /* + * Exception base of thread in caller protection domain - not in core! + */ + struct Exception_base { addr_t exception_base; }; + + + virtual void thread_type(Thread_capability, Thread_type, Exception_base) = 0; + + + /********************* + ** RPC declaration ** + *********************/ + + GENODE_RPC(Rpc_thread_type, void, thread_type, Thread_capability, + Thread_type, Exception_base ); + GENODE_RPC_INTERFACE(Rpc_thread_type); +}; + +#endif /* _NATIVE_CPU_NOVA_H_ */ diff --git a/repos/os/src/monitor/pd_intrinsics.h b/repos/os/src/monitor/pd_intrinsics.h new file mode 100644 index 0000000000..022575146b --- /dev/null +++ b/repos/os/src/monitor/pd_intrinsics.h @@ -0,0 +1,143 @@ +/* + * \brief Sandbox::Pd_intrinsics for intercepting the PD access of children + * \author Norman Feske + * \date 2023-05-10 + */ + +/* + * Copyright (C) 2023 Genode Labs GmbH + * + * This file is part of the Genode OS framework, which is distributed + * under the terms of the GNU Affero General Public License version 3. + */ + +#ifndef _PD_INTRINSICS_H_ +#define _PD_INTRINSICS_H_ + +/* Genode includes */ +#include + +/* local includes */ +#include + +namespace Monitor { struct Pd_intrinsics; } + + +struct Monitor::Pd_intrinsics : Sandbox::Pd_intrinsics +{ + Env &_env; + + /* + * The sandbox interacts with the 'Monitored_ref_pd' only for quota + * transfers to the children, in particular during the child creation. + */ + struct Monitored_ref_pd : Monitored_pd_session + { + using Monitored_pd_session::Monitored_pd_session; + + using Sig_src_cap = Signal_source_capability; + using Sig_ctx_cap = Signal_context_capability; + using Ram_ds_cap = Ram_dataspace_capability; + using Mng_sys_state = Managing_system_state; + + void assign_parent(Capability) override { never_called(__func__); }; + bool assign_pci(addr_t, uint16_t) override { never_called(__func__); }; + void map(addr_t, addr_t) override { never_called(__func__); }; + Sig_src_cap alloc_signal_source() override { never_called(__func__); }; + void free_signal_source(Sig_src_cap) override { never_called(__func__); }; + Sig_ctx_cap alloc_context(Sig_src_cap, unsigned long) override { never_called(__func__); }; + void free_context(Sig_ctx_cap) override { never_called(__func__); }; + void submit(Sig_ctx_cap, unsigned) override { never_called(__func__); }; + Native_capability alloc_rpc_cap(Native_capability) override { never_called(__func__); }; + void free_rpc_cap(Native_capability) override { never_called(__func__); }; + Capability address_space() override { never_called(__func__); }; + Capability stack_area() override { never_called(__func__); }; + Capability linker_area() override { never_called(__func__); }; + Cap_quota cap_quota() const override { never_called(__func__); }; + Cap_quota used_caps() const override { never_called(__func__); }; + Alloc_result try_alloc(size_t, Cache) override { never_called(__func__); }; + void free(Ram_ds_cap) override { never_called(__func__); }; + size_t dataspace_size(Ram_ds_cap) const override { never_called(__func__); }; + Ram_quota ram_quota() const override { never_called(__func__); }; + Ram_quota used_ram() const override { never_called(__func__); }; + Capability native_pd() override { never_called(__func__); }; + Mng_sys_state managing_system(Mng_sys_state const &) override { never_called(__func__); }; + addr_t dma_addr(Ram_ds_cap) override { never_called(__func__); }; + Attach_dma_result attach_dma(Dataspace_capability, addr_t) override { never_called(__func__); }; + + } _monitored_ref_pd { _env.ep(), _env.pd_session_cap(), Session::Label { } }; + + struct Monitored_ref_cpu : Monitored_cpu_session + { + using Monitored_cpu_session::Monitored_cpu_session; + + using Sig_ctx_cap = Signal_context_capability; + + Thread_capability + create_thread(Capability, Cpu_session::Name const &, + Affinity::Location, Weight, addr_t) override { never_called(__func__); } + void kill_thread(Thread_capability) override { never_called(__func__); } + void exception_sigh(Sig_ctx_cap) override { never_called(__func__); } + Affinity::Space affinity_space() const override { never_called(__func__); } + Dataspace_capability trace_control() override { never_called(__func__); } + Quota quota() override { never_called(__func__); } + Capability native_cpu() override { never_called(__func__); } + + } _monitored_ref_cpu { _env.ep(), _env.cpu_session_cap(), Session::Label { } }; + + void with_intrinsics(Capability pd_cap, Pd_session &pd, Fn const &fn) override + { + /* + * Depending on the presence of the PD session in our local entrypoint, + * we know whether the child is to be monitored or not. + * + * If not monitored, we provide the default PD intrinsics as done by + * init, using the 'Env' as reference PD session and accessing the + * child's address space via RPC. For such childen, the monitor + * functions exactly like init with no indirection. + * + * For monitored childen, we provide the '_monitored_ref_pd' and + * '_monitored_ref_cpu' to get hold of all interactions between the + * child and its reference PD. Furthermore, the sandbox' interplay with + * the child's address space is redirected to the locally implemented + * 'Monitored_region_map'. + */ + Inferior_pd::with_inferior_pd(_env.ep(), pd_cap, + [&] (Inferior_pd &inferior_pd) { + + Intrinsics intrinsics { .ref_pd = _monitored_ref_pd, + .ref_pd_cap = _monitored_ref_pd.cap(), + .ref_cpu = _monitored_ref_cpu, + .ref_cpu_cap = _monitored_ref_cpu.cap(), + .address_space = inferior_pd._address_space }; + fn.call(intrinsics); + }, + [&] /* PD session not intercepted */ { + + Region_map_client region_map(pd.address_space()); + + Intrinsics intrinsics { .ref_pd = _env.pd(), + .ref_pd_cap = _env.pd_session_cap(), + .ref_cpu = _env.cpu(), + .ref_cpu_cap = _env.cpu_session_cap(), + .address_space = region_map }; + fn.call(intrinsics); + } + ); + } + + void start_initial_thread(Capability cap, addr_t ip) override + { + Monitored_thread::with_thread(_env.ep(), cap, + [&] (Monitored_thread &monitored_thread) { + monitored_thread.start(ip, 0); + }, + [&] /* PD session not intercepted */ { + Cpu_thread_client(cap).start(ip, 0); + }); + } + + Pd_intrinsics(Env &env) : _env(env) { } +}; + +#endif /* _PD_INTRINSICS_H_ */ diff --git a/repos/os/src/monitor/spec/x86_64/gdb_arch.cc b/repos/os/src/monitor/spec/x86_64/gdb_arch.cc new file mode 100644 index 0000000000..0d7ae61eb2 --- /dev/null +++ b/repos/os/src/monitor/spec/x86_64/gdb_arch.cc @@ -0,0 +1,37 @@ +/* + * \brief Architecture-specific GDB protocol support + * \author Norman Feske + * \date 2023-05-15 + */ + +/* + * Copyright (C) 2023 Genode Labs GmbH + * + * This file is part of the Genode OS framework, which is distributed + * under the terms of the GNU Affero General Public License version 3. + */ + +#include +#include + +using namespace Monitor; + + +void Monitor::Gdb::print_registers(Output &out, Cpu_state const &cpu) +{ + uint64_t const values_64bit[] = { + cpu.rax, cpu.rbx, cpu.rcx, cpu.rdx, cpu.rsi, cpu.rdi, cpu.rbp, cpu.sp, + cpu.r8, cpu.r9, cpu.r10, cpu.r11, cpu.r12, cpu.r13, cpu.r14, cpu.r15, + cpu.ip }; + + for (uint64_t value : values_64bit) + print(out, Gdb_hex(host_to_big_endian(value))); + + uint32_t const values_32bit[] = { + uint32_t(cpu.eflags), uint32_t(cpu.cs), uint32_t(cpu.ss), + 0 /* es */, 0 /* fs */, /* gs */ }; + + for (uint32_t value : values_32bit) + print(out, Gdb_hex(host_to_big_endian(value))); +} + diff --git a/repos/os/src/monitor/spec/x86_64/gdb_target.xml b/repos/os/src/monitor/spec/x86_64/gdb_target.xml new file mode 100644 index 0000000000..82e004e7cd --- /dev/null +++ b/repos/os/src/monitor/spec/x86_64/gdb_target.xml @@ -0,0 +1,118 @@ + + + + i386:x86-64 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/repos/os/src/monitor/target.mk b/repos/os/src/monitor/target.mk new file mode 100644 index 0000000000..a053eaf62e --- /dev/null +++ b/repos/os/src/monitor/target.mk @@ -0,0 +1,4 @@ +TARGET = monitor +SRC_CC = main.cc +LIBS = base sandbox monitor_gdb_arch +INC_DIR += $(PRG_DIR) diff --git a/repos/os/src/monitor/types.h b/repos/os/src/monitor/types.h new file mode 100644 index 0000000000..5cca1f27f3 --- /dev/null +++ b/repos/os/src/monitor/types.h @@ -0,0 +1,90 @@ +/* + * \brief Common types used within monitor component + * \author Norman Feske + * \date 2023-05-08 + */ + +/* + * Copyright (C) 2023 Genode Labs GmbH + * + * This file is part of the Genode OS framework, which is distributed + * under the terms of the GNU Affero General Public License version 3. + */ + +#ifndef _TYPES_H_ +#define _TYPES_H_ + +#include +#include +#include +#include +#include + +namespace Monitor { + + using namespace Genode; + + template struct Monitored_rpc_object; + + constexpr static uint16_t GDB_PACKET_MAX_SIZE = 16*1024; + + struct Inferior_pd; + + using Inferiors = Id_space; + + struct Monitored_thread; + + using Threads = Id_space; + + static inline void never_called(char const *) __attribute__((noreturn)); + static inline void never_called(char const *method_name) + { + error("unexpected call of ", method_name); + sleep_forever(); + } + + /** + * Call 'monitored_fn' with local RPC object that belongs to 'cap', or + * 'direct_fn' if 'cap' does not belong to any local RPC object of type + * 'OBJ'. + */ + template + static void with_monitored(Entrypoint &ep, Capability cap, + auto const &monitored_fn, auto const &direct_fn) + { + OBJ *monitored_obj_ptr = nullptr; + ep.rpc_ep().apply(cap, [&] (OBJ *ptr) { + monitored_obj_ptr = ptr; }); + + if (monitored_obj_ptr) + monitored_fn(*monitored_obj_ptr); + else + direct_fn(); + } +} + + +template +struct Monitor::Monitored_rpc_object : Rpc_object +{ + Entrypoint &_ep; + + using Name = String; + + Name const _name; + + using Interface = IF; + + Capability _real; + + Monitored_rpc_object(Entrypoint &ep, Capability real, Name const &name) + : + _ep(ep), _name(name), _real(real) + { + _ep.manage(*this); + } + + ~Monitored_rpc_object() { _ep.dissolve(*this); } +}; + +#endif /* _TYPES_H_ */ diff --git a/repos/os/src/test/monitor/main.cc b/repos/os/src/test/monitor/main.cc new file mode 100644 index 0000000000..5894d512f9 --- /dev/null +++ b/repos/os/src/test/monitor/main.cc @@ -0,0 +1,202 @@ +/* + * \brief Test for accessing memory using the monitor runtime + * \author Norman Feske + * \date 2023-06-06 + * + * This test exercises the memory-access functionality of the monitor component + * by acting as both the monitored inferior and the observer at the same time. + */ + +/* + * Copyright (C) 2023 Genode Labs GmbH + * + * This file is part of the Genode OS framework, which is distributed + * under the terms of the GNU Affero General Public License version 3. + */ + +/* Genode includes */ +#include +#include +#include +#include +#include +#include +#include + +namespace Test { + + using namespace Genode; + + struct Main; +} + + +struct Test::Main +{ + Env &_env; + + Monitor::Controller _monitor { _env }; + + static void assert(bool condition, auto &&... error_message) + { + if (!condition) { + error(error_message...); + sleep_forever(); + } + } + + void test_query_threads() + { + log("-- test_query_threads --"); + using Thread_info = Monitor::Controller::Thread_info; + + bool expected_inferior_detected = false; + _monitor.for_each_thread([&] (Thread_info const &info) { + if (info.name == "test-monitor" && info.pid == 1 && info.tid == 1) + expected_inferior_detected = true; + log("thread inferior:", info.pid, " tid:", info.tid, " name:", info.name); + }); + assert(expected_inferior_detected, "failed to detect myself as inferior"); + } + + void test_survive_nonexisting_memory_access() + { + log("-- test_survive_nonexisting_memory_access --"); + char buffer[32] { }; + size_t const read_bytes = + _monitor.read_memory(0x10, Byte_range_ptr(buffer, sizeof(buffer))); + + assert(read_bytes == 0, + "unexpected read of ", read_bytes, " from nonexisting memory"); + } + + void test_read_memory() + { + log("-- test_read_memory --"); + char const * const s = "Trying to read back this pattern"; + size_t const num_bytes = strlen(s); + char buffer[num_bytes] { }; + + size_t const read_bytes = + _monitor.read_memory((addr_t)s, Byte_range_ptr(buffer, sizeof(buffer))); + + assert(read_bytes == num_bytes, + "unable to read string of ", num_bytes, " bytes"); + assert(equal(Const_byte_range_ptr(buffer, read_bytes), s), + "read bytes don't match expected pattern"); + } + + void test_truncated_mapping() + { + log("-- test_truncated_mapping --"); + + /* + * Attach 4 KiB of RAM at at the beginning of a managed dataspace of 8 + * KiB while leaving second 4 KiB unmapped. + */ + + Rm_connection rm_connection { _env }; + Region_map_client rm { rm_connection.create(8*1024) }; + Attached_ram_dataspace ram_ds { _env.ram(), _env.rm(), 4*1024 }; + rm.attach_at(ram_ds.cap(), 0); + Attached_dataspace managed_ds { _env.rm(), rm.dataspace() }; + + /* try to read 100 bytes at page boundary, expect to stop after 50 bytes */ + char buffer[100] { }; + addr_t const at = (addr_t)managed_ds.local_addr() + + 4*1024 - 50; + size_t const read_bytes = + _monitor.read_memory(at, Byte_range_ptr(buffer, sizeof(buffer))); + + assert(read_bytes == 50, "failed to read from truncated mapping"); + } + + void test_bench() + { + log("-- test_bench --"); + + Timer::Connection timer { _env }; + + Attached_ram_dataspace large_ram_ds { _env.ram(), _env.rm(), 8*1024*1024 }; + + memset(large_ram_ds.local_addr(), 1, large_ram_ds.size()); + + /* + * Dimensioning of the buffer for one round trip: + * + * The terminal_crosslink component uses a buffer of 4 KiB. + * GDB's 'm' command encodes memory as hex, two characters per byte. + * Hence, a dump of max. 2 KiB fits into the terminal-crosslink buffer. + * The GDB command, packet header, and checksum also take a few bytes. + * + * The most effective way to optimize the throughput would be to + * increase the terminal-crosslink's buffer size, reducing the number + * of round trips. + */ + char buffer[2*1024 - 16] { }; + + uint64_t const start_us = timer.elapsed_us(); + uint64_t now_us = start_us; + + uint64_t total_bytes = 0; + addr_t offset = 0; + for (;;) { + + addr_t const at = (addr_t)large_ram_ds.local_addr() + offset; + + size_t const read_bytes = + _monitor.read_memory(at, Byte_range_ptr(buffer, sizeof(buffer))); + + assert(read_bytes == sizeof(buffer), + "failed to read memory during benchmark"); + + total_bytes += read_bytes; + + /* slide read window over large dataspace, wrap at the end */ + offset = offset + sizeof(buffer); + if (offset + sizeof(buffer) >= large_ram_ds.size()) + offset = 0; + + now_us = timer.elapsed_us(); + if (now_us - start_us > 3*1024*1024) + break; + } + + double const seconds = double(now_us - start_us)/double(1000*1000); + double const rate_kib = (double(total_bytes)/1024) / seconds; + log("read ", total_bytes, " bytes at rate of ", rate_kib, " KiB/s"); + } + + void test_writeable_text_segment() + { + log("-- test_writeable_text_segment --"); + + /* + * Test the 'wx' attribute of the , that converts + * executable text segments into writeable RAM. + */ + + char *code_ptr = (char *)&Component::construct; + char const * const pattern = "risky"; + + copy_cstring(code_ptr, pattern, strlen(pattern) + 1); + + assert(strcmp(code_ptr, pattern, strlen(pattern)) == 0, + "unexpected content at patched address"); + } + + Main(Env &env) : _env(env) + { + test_query_threads(); + test_survive_nonexisting_memory_access(); + test_read_memory(); + test_truncated_mapping(); + test_writeable_text_segment(); + test_bench(); + + _env.parent().exit(0); + } +}; + + +void Component::construct(Genode::Env &env) { static Test::Main main(env); } diff --git a/repos/os/src/test/monitor/monitor_controller.h b/repos/os/src/test/monitor/monitor_controller.h new file mode 100644 index 0000000000..e2cf77fc0a --- /dev/null +++ b/repos/os/src/test/monitor/monitor_controller.h @@ -0,0 +1,192 @@ +/* + * \brief Utility for interacting with the monitor runtime + * \author Norman Feske + * \date 2023-06-06 + */ + +/* + * Copyright (C) 2023 Genode Labs GmbH + * + * This file is part of the Genode OS framework, which is distributed + * under the terms of the GNU Affero General Public License version 3. + */ + +#ifndef _MONITOR_CONTROLLER_H_ +#define _MONITOR_CONTROLLER_H_ + +/* Genode includes */ +#include +#include +#include +#include + +namespace Monitor { + + using namespace Genode; + + class Controller; +} + + +/** + * Utility for the synchronous interaction with a GDB stub over a terminal + * + * Note that requests and responses are limited to GDB_PACKET_MAX_SIZE. + */ +class Monitor::Controller : Noncopyable +{ + private: + + static constexpr uint16_t GDB_PACKET_MAX_SIZE = 16*1024; + + Env &_env; + + Terminal::Connection _terminal { _env }; + + char _buffer[GDB_PACKET_MAX_SIZE] { }; + + Io_signal_handler _terminal_read_avail_handler { + _env.ep(), *this, &Controller::_handle_terminal_read_avail }; + + void _handle_terminal_read_avail() { } + + void _request(auto &&... args) + { + struct Terminal_output + { + struct Write_fn + { + Terminal::Connection &_terminal; + void operator () (char const *str) { + _terminal.write(str, strlen(str)); } + } _write_fn; + + Buffered_output<1024, Write_fn> buffered { _write_fn }; + }; + + Terminal_output output { ._write_fn { _terminal } }; + + Gdb_checksummed_output checksummed_output { output.buffered }; + + print(checksummed_output, args...); + }; + + void _with_response(auto const &fn) + { + using Gdb_packet = Genode::Gdb_packet; + + Gdb_packet _packet { }; + + while (!_packet.complete()) { + + size_t const read_num_bytes = _terminal.read(_buffer, sizeof(_buffer)); + + for (unsigned i = 0; i < read_num_bytes; i++) { + + using Result = Gdb_packet::Append_result; + + switch (_packet.append(_buffer[i])) { + + case Result::COMPLETE: + fn(Const_byte_range_ptr(_packet.buf, _packet.cursor)); + _terminal.write("+", 1); /* ack */ + return; + + case Result::OVERFLOW: + error("received unexpectedly large GDB response"); + break; + + case Result::CORRUPT: + error("received GDB response that could not be parsed"); + break; + + case Result::OK: + break; + } + } + if (read_num_bytes == 0) + _env.ep().wait_and_dispatch_one_io_signal(); + } + } + + public: + + Controller(Env &env) : _env(env) + { + _terminal.read_avail_sigh(_terminal_read_avail_handler); + } + + struct Thread_info + { + using Name = String<64>; + Name name; + + unsigned pid; /* inferior ID */ + unsigned tid; /* thread ID */ + + static Thread_info from_xml(Xml_node const &node) + { + using Id = String<16>; + Id const id = node.attribute_value("id", Id()); + char const *s = id.string(); + + /* parse GDB's thread ID format, e.g., 'p1.2' */ + unsigned pid = 0, tid = 0; + if (*s == 'p') s++; + s = s + ascii_to(s, pid); + if (*s == '.') s++; + ascii_to(s, tid); + + return { .name = node.attribute_value("name", Name()), + .pid = pid, + .tid = tid }; + } + }; + + /** + * Call 'fn' for each thread with the 'Thread_info' as argument + */ + void for_each_thread(auto const &fn) + { + _request("qXfer:threads:read::0,1000"); + + _with_response([&] (Const_byte_range_ptr const &response) { + with_skipped_prefix(response, "l", [&] (Const_byte_range_ptr const &payload) { + Xml_node node(payload.start, payload.num_bytes); + node.for_each_sub_node("thread", [&] (Xml_node const &thread) { + fn(Thread_info::from_xml(thread)); }); }); }); + } + + /** + * Read memory 'at' from current inferior + */ + size_t read_memory(addr_t at, Byte_range_ptr const &dst) + { + /* + * Memory dump is in hex format (two digits per byte), also + * account for the protocol overhead (checksum, prefix). + */ + size_t num_bytes = min(dst.num_bytes, size_t(GDB_PACKET_MAX_SIZE)/2 - 16); + + _request("m", Gdb_hex(at), ",", Gdb_hex(num_bytes)); + + size_t read_bytes = 0; + _with_response([&] (Const_byte_range_ptr const &response) { + + read_bytes = min(response.num_bytes/2, dst.num_bytes); + + /* convert ASCII hex to bytes */ + char const *s = response.start; + uint8_t *d = (uint8_t *)dst.start; + for (unsigned i = 0; i < read_bytes; i++) { + char const hi = *s++; + char const lo = *s++; + *d++ = uint8_t(digit(hi, true) << 4) + | uint8_t(digit(lo, true)); + } + }); + return read_bytes; + } +}; + +#endif /* _MONITOR_CONTROLLER_H_ */ diff --git a/repos/os/src/test/monitor/target.mk b/repos/os/src/test/monitor/target.mk new file mode 100644 index 0000000000..3d43912f03 --- /dev/null +++ b/repos/os/src/test/monitor/target.mk @@ -0,0 +1,4 @@ +TARGET := test-monitor +SRC_CC := main.cc +LIBS += base +INC_DIR += $(PRG_DIR) diff --git a/tool/autopilot.list b/tool/autopilot.list index ee2533b175..a6e5069a4b 100644 --- a/tool/autopilot.list +++ b/tool/autopilot.list @@ -32,6 +32,7 @@ lx_hybrid_exception lx_hybrid_pthread_ipc microcode migrate +monitor netperf_lwip netperf_lwip_bridge netperf_lwip_usb