diff --git a/include/asmgrader/api/test_context.hpp b/include/asmgrader/api/test_context.hpp index d07a5af..260266a 100644 --- a/include/asmgrader/api/test_context.hpp +++ b/include/asmgrader/api/test_context.hpp @@ -14,6 +14,7 @@ #include #include #include +#include #include #include @@ -81,6 +82,9 @@ class TestContext /// Get all stdout from since the beginning of the test invokation std::string get_full_stdout(); + Subprocess::OutputResult get_output(Subprocess::WhichOutput which = Subprocess::WhichOutput::StdoutAndStderr); + Subprocess::OutputResult get_full_output(Subprocess::WhichOutput which = Subprocess::WhichOutput::StdoutAndStderr); + /// Flushes any reamaining unread data in the stdin buffer /// Returns: number of bytes flushed, or error kind if failure occured std::size_t flush_stdin(); diff --git a/include/asmgrader/common/expected.hpp b/include/asmgrader/common/expected.hpp index cf6e852..d7561ab 100644 --- a/include/asmgrader/common/expected.hpp +++ b/include/asmgrader/common/expected.hpp @@ -160,6 +160,7 @@ class [[nodiscard]] Expected } template + requires(!std::is_void_v) constexpr Expected, E> transform(const Func& func) { if (!has_value()) { return error(); @@ -168,6 +169,16 @@ class [[nodiscard]] Expected return func(value()); } + template + requires(std::is_void_v) + constexpr Expected, E> transform(const Func& func) { + if (!has_value()) { + return error(); + } + + return func(); + } + private: template struct ExpectedData diff --git a/include/asmgrader/subprocess/subprocess.hpp b/include/asmgrader/subprocess/subprocess.hpp index 69fdc63..ec8c940 100644 --- a/include/asmgrader/subprocess/subprocess.hpp +++ b/include/asmgrader/subprocess/subprocess.hpp @@ -1,10 +1,13 @@ #pragma once +#include #include #include #include #include +#include + #include #include #include @@ -28,17 +31,35 @@ class Subprocess : NonCopyable Subprocess(Subprocess&&) noexcept; Subprocess& operator=(Subprocess&&) noexcept; + /// Which output file descriptor to read from + enum class WhichOutput : u8 { None = 0, Stdout = 1, Stderr = 2, StdoutAndStderr = Stdout | Stderr }; + + /// stdout_str is only valid if WhichOutput::Stdout was included in the request + /// stderr_str is only valid if WhichOutput::Stderr was included in the request + struct OutputResult + { + std::string stdout_str; + std::string stderr_str; + }; + + /// Read buffered output since the last call to this function + OutputResult read_output(WhichOutput which = WhichOutput::StdoutAndStderr); + + /// Get all output since the program has launched + OutputResult read_full_output(WhichOutput which = WhichOutput::StdoutAndStderr); + template - Result read_stdout(const std::chrono::duration& timeout) { - return read_stdout_poll_impl(std::chrono::duration_cast(timeout).count()); + [[deprecated]] Result read_stdout(const std::chrono::duration& timeout) { + read_pipe_poll(stdout_, std::chrono::duration_cast(timeout).count()); + return new_output(WhichOutput::Stdout).stdout_str; } - Result read_stdout(); + /// Updates output result based on cursor positions + /// If the cursor is located before the end of the string, then a substring is used + /// (starting at the cursor position) + void get_new_output(OutputResult& res); - /// Get all stdout since the program has launched - const std::string& get_full_stdout(); - - Result send_stdin(std::string_view str); + Result send_stdin(std::string_view str) const; // Forks the current process to start a new subprocess as specified virtual Result start(); @@ -78,12 +99,20 @@ class Subprocess : NonCopyable /// pipes to communicate with subprocess' stdout and stdin respectively /// The parent process will only make use of the write end of stdin_pipe_, and the read end of stdout_pipe_ linux::Pipe stdin_pipe_{}; - linux::Pipe stdout_pipe_{}; - std::string stdout_buffer_; - std::size_t stdout_cursor_{}; + struct OutputPipe + { + linux::Pipe pipe; + std::string buffer; + std::size_t cursor; + }; - Result read_stdout_poll_impl(int timeout_ms); + OutputPipe stdout_{}; + OutputPipe stderr_{}; + + /// Marks all open fds (other than 0,1,2) as FD_CLOEXEC so that they get closed in the child proc + /// Run in the PARENT process. + Expected<> mark_cloexec_all() const; /// Marks all open fds (other than 0,1,2) as FD_CLOEXEC so that they get closed in the child proc /// Run in the PARENT process. @@ -92,10 +121,49 @@ class Subprocess : NonCopyable /// Reads any data on the stdout pipe to stdout_buffer_ Result read_stdout_impl(); + /// Reads any immediately available data available on the specified pipe's read end + /// Writes any new data to that OutputPipe's buffer + /// Throws a std::logic_error if any syscalls fail + static void read_pipe_nonblock(OutputPipe& pipe); + + /// Polls the pipe's read end for timeout_ms millis, or until available input arrives + /// Writes any new data to that OutputPipe's buffer + /// \returns true if a successful read occurred, false if timed out + static bool read_pipe_poll(OutputPipe& pipe, int timeout_ms); + + /// Obtain new output based on cursor positions and buffers, + /// and set cursors to the end of their resp. buffers + OutputResult new_output(WhichOutput which); + std::optional exit_code_; std::string exec_; std::vector args_; }; +// TODO: macro for bitfield enums + +constexpr std::string_view format_as(const Subprocess::WhichOutput& from) { + switch (from) { + case Subprocess::WhichOutput::None: + return "none"; + case Subprocess::WhichOutput::Stdout: + return "stdout"; + case Subprocess::WhichOutput::Stderr: + return "stderr"; + case Subprocess::WhichOutput::StdoutAndStderr: + return "stdout&stderr"; + default: + return ""; + } +} + +constexpr Subprocess::WhichOutput operator&(const Subprocess::WhichOutput& lhs, const Subprocess::WhichOutput& rhs) { + return static_cast(fmt::underlying(lhs) & fmt::underlying(rhs)); +} + +constexpr Subprocess::WhichOutput operator|(const Subprocess::WhichOutput& lhs, const Subprocess::WhichOutput& rhs) { + return static_cast(fmt::underlying(lhs) | fmt::underlying(rhs)); +} + } // namespace asmgrader diff --git a/src/api/test_context.cpp b/src/api/test_context.cpp index af73d1d..7404412 100644 --- a/src/api/test_context.cpp +++ b/src/api/test_context.cpp @@ -17,6 +17,7 @@ #include "logging.hpp" #include "program/program.hpp" #include "subprocess/run_result.hpp" +#include "subprocess/subprocess.hpp" #include "subprocess/syscall_record.hpp" #include @@ -83,11 +84,19 @@ std::string_view TestContext::get_name() const { } std::string TestContext::get_stdout() { - return TRY_OR_THROW(prog_.get_subproc().read_stdout(), "failed to read stdout"); + return get_output(Subprocess::WhichOutput::Stdout).stdout_str; } std::string TestContext::get_full_stdout() { - return prog_.get_subproc().get_full_stdout(); + return get_full_output(Subprocess::WhichOutput::Stdout).stdout_str; +} + +Subprocess::OutputResult TestContext::get_output(Subprocess::WhichOutput which) { + return prog_.get_subproc().read_output(which); +} + +Subprocess::OutputResult TestContext::get_full_output(Subprocess::WhichOutput which) { + return prog_.get_subproc().read_full_output(which); } void TestContext::send_stdin(std::string_view input) { diff --git a/src/subprocess/subprocess.cpp b/src/subprocess/subprocess.cpp index 260ded8..1511352 100644 --- a/src/subprocess/subprocess.cpp +++ b/src/subprocess/subprocess.cpp @@ -9,16 +9,16 @@ #include #include -#include #include -#include #include #include #include +#include #include #include #include #include +#include #include #include @@ -129,15 +129,20 @@ Result Subprocess::wait_for_exit(std::chrono::microseconds timeout) { Result Subprocess::close_pipes() { // Make sure all available data is read before pipes are closed - std::ignore = read_stdout_impl(); + read_pipe_nonblock(stdout_); + read_pipe_nonblock(stderr_); if (stdin_pipe_.write_fd != -1) { TRYE(linux::close(stdin_pipe_.write_fd), SyscallFailure); stdin_pipe_.write_fd = -1; } - if (stdout_pipe_.read_fd != -1) { - TRYE(linux::close(stdout_pipe_.read_fd), SyscallFailure); - stdout_pipe_.read_fd = -1; + if (stdout_.pipe.read_fd != -1) { + TRYE(linux::close(stdout_.pipe.read_fd), SyscallFailure); + stdout_.pipe.read_fd = -1; + } + if (stderr_.pipe.read_fd != -1) { + TRYE(linux::close(stderr_.pipe.read_fd), SyscallFailure); + stderr_.pipe.read_fd = -1; } return {}; @@ -146,16 +151,14 @@ Result Subprocess::close_pipes() { Subprocess::Subprocess(Subprocess&& other) noexcept : child_pid_{std::exchange(other.child_pid_, 0)} , stdin_pipe_{std::exchange(other.stdin_pipe_, {})} - , stdout_pipe_{std::exchange(other.stdout_pipe_, {})} - , stdout_buffer_{std::exchange(other.stdout_buffer_, {})} - , stdout_cursor_{std::exchange(other.stdout_cursor_, 0)} {} + , stdout_{std::exchange(other.stdout_, {})} + , stderr_{std::exchange(other.stderr_, {})} {} Subprocess& Subprocess::operator=(Subprocess&& rhs) noexcept { child_pid_ = std::exchange(rhs.child_pid_, 0); stdin_pipe_ = std::exchange(rhs.stdin_pipe_, {}); - stdout_pipe_ = std::exchange(rhs.stdout_pipe_, {}); - stdout_buffer_ = std::exchange(rhs.stdout_buffer_, {}); - stdout_cursor_ = std::exchange(rhs.stdout_cursor_, 0); + stdout_ = std::exchange(rhs.stdout_, {}); + stderr_ = std::exchange(rhs.stderr_, {}); return *this; } @@ -164,72 +167,89 @@ bool Subprocess::is_alive() const { return linux::kill(child_pid_, 0) != std::make_error_code(std::errc::no_such_process); } -Result Subprocess::read_stdout_poll_impl(int timeout_ms) { - // If the pipe is already closed, all we can do is try reading from the buffer - if (stdout_pipe_.read_fd == -1) { - return read_stdout(); +Subprocess::OutputResult Subprocess::read_output(WhichOutput which) { + if ((which & WhichOutput::Stdout) != WhichOutput::None) { + read_pipe_nonblock(stdout_); + } + if ((which & WhichOutput::Stderr) != WhichOutput::None) { + read_pipe_nonblock(stderr_); } - struct pollfd poll_struct = {.fd = stdout_pipe_.read_fd, .events = POLLIN, .revents = 0}; + return new_output(which); +} - // TODO: Create wrapper in linux.hpp - int res = poll(&poll_struct, 1, timeout_ms); - // Error - if (res == -1) { - LOG_WARN("Error polling for read from stdout pipe: '{}'", get_err_msg()); - return ErrorKind::SyscallFailure; +Subprocess::OutputResult Subprocess::read_full_output(WhichOutput which) { + if ((which & WhichOutput::Stdout) != WhichOutput::None) { + read_pipe_nonblock(stdout_); } - // Timeout occured - if (res == 0) { - return ""; + if ((which & WhichOutput::Stderr) != WhichOutput::None) { + read_pipe_nonblock(stderr_); } - return read_stdout(); + return OutputResult{.stdout_str = stdout_.buffer, .stderr_str = stderr_.buffer}; } -Result Subprocess::read_stdout() { - TRY(read_stdout_impl()); +void Subprocess::read_pipe_nonblock(OutputPipe& pipe) { + std::size_t num_bytes_avail = 0; - // Cursor is still at the end of the buffer -> no data was read - if (stdout_cursor_ == stdout_buffer_.size()) { - return ""; + if (pipe.pipe.read_fd == -1) { + LOG_TRACE("Attempted to read from a fd that's already closed ({})", pipe.pipe.read_fd); + return; } - auto res = stdout_buffer_.substr(stdout_cursor_); - stdout_cursor_ = stdout_buffer_.size(); + if (!linux::ioctl(pipe.pipe.read_fd, FIONREAD, &num_bytes_avail)) { + throw std::logic_error("ioctl for pipe failed"); + } - return res; -} + LOG_DEBUG("{} bytes available from fd ({})", num_bytes_avail, pipe.pipe.read_fd); -const std::string& Subprocess::get_full_stdout() { - std::ignore = read_stdout_impl(); + if (num_bytes_avail == 0) { + return; + } - return stdout_buffer_; + if (auto res = linux::read(pipe.pipe.read_fd, num_bytes_avail)) { + pipe.buffer += res.value(); + } else { + throw std::logic_error("read from pipe failed"); + } } -Result Subprocess::read_stdout_impl() { - std::size_t num_bytes_avail = 0; +bool Subprocess::read_pipe_poll(Subprocess::OutputPipe& pipe, int timeout_ms) { + struct pollfd poll_struct = {.fd = pipe.pipe.read_fd, .events = POLLIN, .revents = 0}; - if (stdout_pipe_.read_fd == -1) { - return {}; + // TODO: Create wrapper in linux.hpp + int res = poll(&poll_struct, 1, timeout_ms); + // Syscall error + if (res == -1) { + throw std::logic_error("poll for pipe failed"); + } + // Timeout occured + if (res == 0) { + return false; } - TRYE(linux::ioctl(stdout_pipe_.read_fd, FIONREAD, &num_bytes_avail), SyscallFailure); - - LOG_DEBUG("{} bytes available from stdout_pipe", num_bytes_avail); + read_pipe_nonblock(pipe); - if (num_bytes_avail == 0) { - return {}; - } + return true; +} - std::string res = TRYE(linux::read(stdout_pipe_.read_fd, num_bytes_avail), SyscallFailure); +Subprocess::OutputResult Subprocess::new_output(WhichOutput which) { + OutputResult res; - stdout_buffer_ += res; + // Update res and cursor positions + if ((which & WhichOutput::Stdout) != WhichOutput::None && stdout_.cursor < stdout_.buffer.size()) { + res.stdout_str = stdout_.buffer.substr(stdout_.cursor); + stdout_.cursor = stdout_.buffer.size(); + } + if ((which & WhichOutput::Stderr) != WhichOutput::None && stderr_.cursor < stderr_.buffer.size()) { + res.stderr_str = stderr_.buffer.substr(stderr_.cursor); + stderr_.cursor = stderr_.buffer.size(); + } - return {}; + return res; } -Result Subprocess::send_stdin(std::string_view str) { +Result Subprocess::send_stdin(std::string_view str) const { // TODO: more abstract write wrapper that ensures all bytes were sent TRYE(linux::write(stdin_pipe_.write_fd, str), SyscallFailure); @@ -237,8 +257,13 @@ Result Subprocess::send_stdin(std::string_view str) { } Result Subprocess::create(const std::string& exec, const std::vector& args) { - stdout_pipe_ = TRYE(linux::pipe2(), SyscallFailure); stdin_pipe_ = TRYE(linux::pipe2(), SyscallFailure); + stdout_.pipe = TRYE(linux::pipe2(), SyscallFailure); + stderr_.pipe = TRYE(linux::pipe2(), SyscallFailure); + + if (!mark_cloexec_all()) { + LOG_WARN("Failed to set flags for fds; some fds will likely remain open in child proc"); + } if (!mark_cloexec_all()) { LOG_WARN("Failed to set flags for fds; some fds will likely remain open in child proc"); @@ -264,13 +289,33 @@ Result Subprocess::create(const std::string& exec, const std::vector Subprocess::init_child() { TRYE(linux::dup2(stdin_pipe_.read_fd, STDIN_FILENO), SyscallFailure); - TRYE(linux::dup2(stdout_pipe_.write_fd, STDOUT_FILENO), SyscallFailure); + TRYE(linux::dup2(stdout_.pipe.write_fd, STDOUT_FILENO), SyscallFailure); + TRYE(linux::dup2(stderr_.pipe.write_fd, STDERR_FILENO), SyscallFailure); // Close the pipe ends not being used in the child proc // - read end for stdout - // - write end for stdin + // - write end for stdin and stderr TRYE(linux::close(stdin_pipe_.write_fd), SyscallFailure); - TRYE(linux::close(stdout_pipe_.read_fd), SyscallFailure); + TRYE(linux::close(stdout_.pipe.read_fd), SyscallFailure); + TRYE(linux::close(stderr_.pipe.read_fd), SyscallFailure); + + namespace fs = std::filesystem; + + for (const auto& entry : fs::directory_iterator("/proc/self/fd")) { + int fd = std::stoi(entry.path().filename().string()); + + // skip stdin, stdout, stderr + if (fd <= 2) { + continue; + } + + // auto res = linux::close(fd); + // + // // If close(2) failed for a reason other than the fd not existing, return an error + // if (!res && res != linux::make_error_code(EBADF)) { + // return ErrorKind::SyscallFailure; + // } + } namespace fs = std::filesystem; @@ -296,16 +341,20 @@ Result Subprocess::init_child() { Result Subprocess::init_parent() { // Close the pipe ends being used in the parent proc // - write end for stdout - // - read end for stdin + // - read end for stdin and stderr TRYE(linux::close(stdin_pipe_.read_fd), SyscallFailure); - TRYE(linux::close(stdout_pipe_.write_fd), SyscallFailure); + TRYE(linux::close(stdout_.pipe.write_fd), SyscallFailure); + TRYE(linux::close(stderr_.pipe.write_fd), SyscallFailure); // stdin_pipefd_ = stdin_pipe.write_fd; // write end of stdin pipe // stdout_pipefd_ = stdout_pipe.read_fd; // read end of stdout pipe - // Make reading from stdout non-blocking - int pre_flags = TRYE(linux::fcntl(stdout_pipe_.read_fd, F_GETFL), SyscallFailure); + // Make reading from stdout and stderr non-blocking + int pre_flags_stdout = TRYE(linux::fcntl(stdout_.pipe.read_fd, F_GETFL), SyscallFailure); + int pre_flags_stderr = TRYE(linux::fcntl(stderr_.pipe.read_fd, F_GETFL), SyscallFailure); - TRYE(linux::fcntl(stdout_pipe_.read_fd, F_SETFL, pre_flags | O_NONBLOCK), // NOLINT + TRYE(linux::fcntl(stdout_.pipe.read_fd, F_SETFL, pre_flags_stdout | O_NONBLOCK), // NOLINT + SyscallFailure); + TRYE(linux::fcntl(stderr_.pipe.read_fd, F_SETFL, pre_flags_stderr | O_NONBLOCK), // NOLINT SyscallFailure); return {}; diff --git a/tests/resources/simple_asm_aarch64.s b/tests/resources/simple_asm_aarch64.s index 29d1c80..12c191d 100644 --- a/tests/resources/simple_asm_aarch64.s +++ b/tests/resources/simple_asm_aarch64.s @@ -32,7 +32,7 @@ sum: /// x0 + x1 written to stdout as 8 bytes. sum_and_write: add x0, x0, x1 // x0 += x1 - STR x0, [sp, -8]! + str x0, [sp, -8]! // save x0 onto the stack mov x8, 64 // SYS_write mov x0, 1 // fd param = stdout @@ -43,6 +43,26 @@ sum_and_write: add sp, sp, 8 // pop x0 off of the stack ret + +/// write_to +/// sums two numbers and writes the result to stdout. Overflow may occur. +/// Parameters: +/// x0 (const char*) - string to write +/// x1 (int) - the fd to write to +/// x2 (size_t) - length of the string +/// Result: +/// length bytes of string written to fd +write_to: + mov x3, x0 // save x0 (str) into x3 + + mov x8, 64 // SYS_write + mov x0, x1 // fd param = fd + mov x1, x3 // str param = string + // len param [already set by param] + svc 0 // SYS_write + + ret + /// This subroutine will timeout in an infinitely recurring loop timeout_fn: b timeout_fn diff --git a/tests/resources/simple_asm_x86_64.s b/tests/resources/simple_asm_x86_64.s index 7f3eb61..8473bcd 100644 --- a/tests/resources/simple_asm_x86_64.s +++ b/tests/resources/simple_asm_x86_64.s @@ -13,7 +13,6 @@ _start: mov rdi, 42 # retcode syscall - /// sum /// sums two numbers and returns the result /// Parameters: @@ -46,6 +45,26 @@ sum_and_write: pop rsi # pop rsi off the stack ret +/// write_to +/// sums two numbers and writes the result to specified fd. Overflow may occur. +/// Parameters: +/// rdi (const char*) - string to write +/// rsi (int) - the fd to write to +/// rdx (size_t) - length of the string +/// Result: +/// length bytes of string written to fd +write_to: + mov r10, rdi # save rdi (str) into r10 + + mov rax, 1 # SYS_write + mov rdi, rsi # fd + mov rsi, r10 # rsi = str + mov rdx, rdx # rdx (len) = length + syscall # SYS_write + + ret + + /// This subroutine will timeout in an infinitely recurring loop timeout_fn: jmp timeout_fn diff --git a/tests/test_program.cpp b/tests/test_program.cpp index d27d884..e85d5cb 100644 --- a/tests/test_program.cpp +++ b/tests/test_program.cpp @@ -3,14 +3,18 @@ #include "common/aliases.hpp" #include "common/error_types.hpp" #include "program/program.hpp" +#include "subprocess/subprocess.hpp" #include #include +#include + using namespace asmgrader::aliases; using sum = u64(std::uint64_t, std::uint64_t); using sum_and_write = void(u64, std::uint64_t); +using write_to = void(const char*, int, size_t); using timeout_fn = void(); using segfaulting_fn = void(); using exiting_fn = void(u64); @@ -46,16 +50,34 @@ TEST_CASE("Call sum_and_write function") { asmgrader::Program prog(ASM_TESTS_EXEC, {}); REQUIRE(prog.call_function("sum_and_write", 0, 0)); - REQUIRE(prog.get_subproc().read_stdout() == std::string{"\0\0\0\0\0\0\0\0", 8}); + REQUIRE(prog.get_subproc().read_output(asmgrader::Subprocess::WhichOutput::Stdout).stdout_str == + std::string{"\0\0\0\0\0\0\0\0", 8}); REQUIRE(prog.call_function("sum_and_write", 'a', 5)); // 'a' + 5 = 'f' - REQUIRE(prog.get_subproc().read_stdout() == std::string{"f\0\0\0\0\0\0\0", 8}); + REQUIRE(prog.get_subproc().read_output(asmgrader::Subprocess::WhichOutput::Stdout).stdout_str == + std::string{"f\0\0\0\0\0\0\0", 8}); REQUIRE(prog.call_function("sum_and_write", 0x1010101010101010, 0x1010101010101010)); static_assert(' ' == 0x10 + 0x10, "Somehow not ASCII encoded???"); // 0x10 + 0x10 = 0x20 (space ' ') - REQUIRE(prog.get_subproc().read_stdout() == " "); + REQUIRE(prog.get_subproc().read_output(asmgrader::Subprocess::WhichOutput::Stdout).stdout_str == " "); +} + +TEST_CASE("Call write_to function") { + asmgrader::Program prog(ASM_TESTS_EXEC, {}); + + std::string test_str = "I am a test string\nNOPE\n123897g51%%~"; + + REQUIRE(prog.call_function("write_to", test_str, STDOUT_FILENO, test_str.size())); + REQUIRE(prog.get_subproc().read_output().stdout_str == test_str); + + REQUIRE(prog.call_function("write_to", test_str, STDERR_FILENO, test_str.size())); + REQUIRE(prog.get_subproc().read_output().stderr_str == test_str); + + REQUIRE(prog.call_function("write_to", test_str, 0, test_str.size())); + REQUIRE(prog.get_subproc().read_output().stdout_str == ""); + REQUIRE(prog.get_subproc().read_output().stderr_str == ""); } TEST_CASE("Test that timeouts are handled properly with timeout_fn") { diff --git a/tests/test_subprocess.cpp b/tests/test_subprocess.cpp index b8fdf7d..22e0086 100644 --- a/tests/test_subprocess.cpp +++ b/tests/test_subprocess.cpp @@ -40,7 +40,7 @@ TEST_CASE("Read /bin/echo stdout") { proc.wait_for_exit(); - REQUIRE(proc.read_stdout() == "Hello world!"); + REQUIRE(proc.read_output(asmgrader::Subprocess::WhichOutput::Stdout).stdout_str == "Hello world!"); } TEST_CASE("Interact with /bin/cat") { @@ -68,7 +68,7 @@ TEST_CASE("Get results of asm program") { REQUIRE(run_res->get_code() == 42); REQUIRE(proc.get_exit_code() == 42); - REQUIRE(proc.read_stdout() == "Hello, from assembly!\n"); + REQUIRE(proc.read_output(asmgrader::Subprocess::WhichOutput::Stdout).stdout_str == "Hello, from assembly!\n"); auto syscall_records = proc.get_tracer().get_records();