blob: a4003d86a0db1b2373f2eeb48639e96a9f731d8a [file] [log] [blame]
/* -*- Mode: C++; tab-width: 8; c-basic-offset: 2; indent-tabs-mode: nil; -*- */
#include "GdbServer.h"
#include <elf.h>
#include <limits.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/sysmacros.h>
#include <sys/wait.h>
#include <unistd.h>
#include <algorithm>
#include <limits>
#include <map>
#include <sstream>
#include <string>
#include <vector>
#include "BreakpointCondition.h"
#include "ElfReader.h"
#include "Event.h"
#include "GdbCommandHandler.h"
#include "GdbExpression.h"
#include "ReplaySession.h"
#include "ReplayTask.h"
#include "ScopedFd.h"
#include "StringVectorToCharArray.h"
#include "Task.h"
#include "ThreadGroup.h"
#include "core.h"
#include "kernel_metadata.h"
#include "log.h"
#include "util.h"
using namespace std;
namespace rr {
GdbServer::GdbServer(std::unique_ptr<GdbConnection>& dbg, Task* t)
: dbg(std::move(dbg)),
debuggee_tguid(t->thread_group()->tguid()),
last_continue_tuid(t->tuid()),
last_query_tuid(t->tuid()),
final_event(UINT32_MAX),
stop_replaying_to_target(false),
interrupt_pending(false),
exit_sigkill_pending(false),
emergency_debug_session(&t->session()),
file_scope_pid(0) {
memset(&stop_siginfo, 0, sizeof(stop_siginfo));
}
// Special-sauce macros defined by rr when launching the gdb client,
// which implement functionality outside of the gdb remote protocol.
// (Don't stare at them too long or you'll go blind ;).)
static const string& gdb_rr_macros() {
static string s;
if (s.empty()) {
stringstream ss;
ss << GdbCommandHandler::gdb_macros()
<< "define restart\n"
<< " run c$arg0\n"
<< "end\n"
<< "document restart\n"
<< "restart at checkpoint N\n"
<< "checkpoints are created with the 'checkpoint' command\n"
<< "end\n"
<< "define seek-ticks\n"
<< " run t$arg0\n"
<< "end\n"
<< "document seek-ticks\n"
<< "restart at given ticks value\n"
<< "end\n"
<< "define jump\n"
<< " rr-denied jump\n"
<< "end\n"
// In gdb version "Fedora 7.8.1-30.fc21", a raw "run" command
// issued before any user-generated resume-execution command
// results in gdb hanging just after the inferior hits an internal
// gdb breakpoint. This happens outside of rr, with gdb
// controlling gdbserver, as well. We work around that by
// ensuring *some* resume-execution command has been issued before
// restarting the session. But, only if the inferior hasn't
// already finished execution ($_thread != 0). If it has and we
// issue the "stepi" command, then gdb refuses to restart
// execution.
<< "define hook-run\n"
<< " rr-hook-run\n"
<< "end\n"
<< "define hookpost-continue\n"
<< " rr-set-suppress-run-hook 1\n"
<< "end\n"
<< "define hookpost-step\n"
<< " rr-set-suppress-run-hook 1\n"
<< "end\n"
<< "define hookpost-stepi\n"
<< " rr-set-suppress-run-hook 1\n"
<< "end\n"
<< "define hookpost-next\n"
<< " rr-set-suppress-run-hook 1\n"
<< "end\n"
<< "define hookpost-nexti\n"
<< " rr-set-suppress-run-hook 1\n"
<< "end\n"
<< "define hookpost-finish\n"
<< " rr-set-suppress-run-hook 1\n"
<< "end\n"
<< "define hookpost-reverse-continue\n"
<< " rr-set-suppress-run-hook 1\n"
<< "end\n"
<< "define hookpost-reverse-step\n"
<< " rr-set-suppress-run-hook 1\n"
<< "end\n"
<< "define hookpost-reverse-stepi\n"
<< " rr-set-suppress-run-hook 1\n"
<< "end\n"
<< "define hookpost-reverse-finish\n"
<< " rr-set-suppress-run-hook 1\n"
<< "end\n"
<< "define hookpost-run\n"
<< " rr-set-suppress-run-hook 0\n"
<< "end\n"
<< "set unwindonsignal on\n"
<< "handle SIGURG stop\n"
<< "set prompt (rr) \n"
// Try both "set target-async" and "maint set target-async" since
// that changed recently.
<< "python\n"
<< "import re\n"
<< "m = re.compile("
<< "'[^0-9]*([0-9]+)\\.([0-9]+)(\\.([0-9]+))?'"
<< ").match(gdb.VERSION)\n"
<< "ver = int(m.group(1))*10000 + int(m.group(2))*100\n"
<< "if m.group(4):\n"
<< " ver = ver + int(m.group(4))\n"
<< "\n"
<< "if ver == 71100:\n"
<< " gdb.write("
<< "'This version of gdb (7.11.0) has known bugs that break rr. "
<< "Install 7.11.1 or later.\\n', gdb.STDERR)\n"
<< "\n"
<< "if ver < 71101:\n"
<< " gdb.execute('set target-async 0')\n"
<< " gdb.execute('maint set target-async 0')\n"
<< "end\n";
s = ss.str();
}
return s;
}
/**
* Attempt to find the value of |regname| (a DebuggerRegister
* name), and if so (i) write it to |buf|; (ii)
* set |*defined = true|; (iii) return the size of written
* data. If |*defined == false|, the value of |buf| is
* meaningless.
*
* This helper can fetch the values of both general-purpose
* and "extra" registers.
*
* NB: |buf| must be large enough to hold the largest register
* value that can be named by |regname|.
*/
static size_t get_reg(const Registers& regs, const ExtraRegisters& extra_regs,
uint8_t* buf, GdbRegister regname, bool* defined) {
size_t num_bytes = regs.read_register(buf, regname, defined);
if (!*defined) {
num_bytes = extra_regs.read_register(buf, regname, defined);
}
return num_bytes;
}
static bool set_reg(Task* target, const GdbRegisterValue& reg) {
if (!reg.defined) {
return false;
}
Registers regs = target->regs();
if (regs.write_register(reg.name, reg.value, reg.size)) {
target->set_regs(regs);
return true;
}
ExtraRegisters extra_regs = target->extra_regs();
if (extra_regs.write_register(reg.name, reg.value, reg.size)) {
target->set_extra_regs(extra_regs);
return true;
}
LOG(warn) << "Unhandled register name " << reg.name;
return false;
}
/**
* Return the register |which|, which may not have a defined value.
*/
GdbRegisterValue GdbServer::get_reg(const Registers& regs,
const ExtraRegisters& extra_regs,
GdbRegister which) {
GdbRegisterValue reg;
memset(&reg, 0, sizeof(reg));
reg.name = which;
reg.size = rr::get_reg(regs, extra_regs, &reg.value[0], which, &reg.defined);
return reg;
}
static GdbThreadId get_threadid(const Session& session, const TaskUid& tuid) {
Task* t = session.find_task(tuid);
pid_t pid = t ? t->tgid() : GdbThreadId::ANY.pid;
return GdbThreadId(pid, tuid.tid());
}
static GdbThreadId get_threadid(Task* t) {
return GdbThreadId(t->tgid(), t->rec_tid);
}
static bool matches_threadid(const GdbThreadId& tid,
const GdbThreadId& target) {
return (target.pid <= 0 || target.pid == tid.pid) &&
(target.tid <= 0 || target.tid == tid.tid);
}
static bool matches_threadid(Task* t, const GdbThreadId& target) {
GdbThreadId tid = get_threadid(t);
return matches_threadid(tid, target);
}
static WatchType watchpoint_type(GdbRequestType req) {
switch (req) {
case DREQ_SET_HW_BREAK:
case DREQ_REMOVE_HW_BREAK:
return WATCH_EXEC;
case DREQ_SET_WR_WATCH:
case DREQ_REMOVE_WR_WATCH:
return WATCH_WRITE;
case DREQ_REMOVE_RDWR_WATCH:
case DREQ_SET_RDWR_WATCH:
// NB: x86 doesn't support read-only watchpoints (who would
// ever want to use one?) so we treat them as readwrite
// watchpoints and hope that gdb can figure out what's going
// on. That is, if a user ever tries to set a read
// watchpoint.
case DREQ_REMOVE_RD_WATCH:
case DREQ_SET_RD_WATCH:
return WATCH_READWRITE;
default:
FATAL() << "Unknown dbg request " << req;
return WatchType(-1); // not reached
}
}
static void maybe_singlestep_for_event(Task* t, GdbRequest* req) {
if (!t->session().is_replaying()) {
return;
}
auto rt = static_cast<ReplayTask*>(t);
if (trace_instructions_up_to_event(
rt->session().current_trace_frame().time())) {
fputs("Stepping: ", stderr);
t->regs().print_register_file_compact(stderr);
fprintf(stderr, " ticks:%" PRId64 "\n", t->tick_count());
*req = GdbRequest(DREQ_CONT);
req->suppress_debugger_stop = true;
req->cont().actions.push_back(
GdbContAction(ACTION_STEP, get_threadid(t->session(), t->tuid())));
}
}
void GdbServer::dispatch_regs_request(const Registers& regs,
const ExtraRegisters& extra_regs) {
GdbRegister end;
// Send values for all the registers we sent XML register descriptions for.
// Those descriptions are controlled by GdbConnection::cpu_features().
bool have_PKU = dbg->cpu_features() & GdbConnection::CPU_PKU;
bool have_AVX = dbg->cpu_features() & GdbConnection::CPU_AVX;
switch (regs.arch()) {
case x86:
end = have_PKU ? DREG_PKRU : (have_AVX ? DREG_YMM7H : DREG_ORIG_EAX);
break;
case x86_64:
end = have_PKU ? DREG_64_PKRU : (have_AVX ? DREG_64_YMM15H : DREG_GS_BASE);
break;
case aarch64:
end = DREG_FPCR;
break;
default:
FATAL() << "Unknown architecture";
return;
}
vector<GdbRegisterValue> rs;
for (GdbRegister r = GdbRegister(0); r <= end; r = GdbRegister(r + 1)) {
rs.push_back(get_reg(regs, extra_regs, r));
}
dbg->reply_get_regs(rs);
}
class GdbBreakpointCondition : public BreakpointCondition {
public:
GdbBreakpointCondition(const vector<vector<uint8_t>>& bytecodes) {
for (auto& b : bytecodes) {
expressions.push_back(GdbExpression(b.data(), b.size()));
}
}
virtual bool evaluate(Task* t) const override {
for (auto& e : expressions) {
GdbExpression::Value v;
// Break if evaluation fails or the result is nonzero
if (!e.evaluate(t, &v) || v.i != 0) {
return true;
}
}
return false;
}
private:
vector<GdbExpression> expressions;
};
static unique_ptr<BreakpointCondition> breakpoint_condition(
const GdbRequest& request) {
if (request.watch().conditions.empty()) {
return nullptr;
}
return unique_ptr<BreakpointCondition>(
new GdbBreakpointCondition(request.watch().conditions));
}
static bool search_memory(Task* t, const MemoryRange& where,
const vector<uint8_t>& find,
remote_ptr<void>* result) {
vector<uint8_t> buf;
buf.resize(page_size() + find.size() - 1);
for (const auto& m : t->vm()->maps()) {
MemoryRange r = MemoryRange(m.map.start(), m.map.end() + find.size() - 1)
.intersect(where);
// We basically read page by page here, but we read past the end of the
// page to handle the case where a found string crosses page boundaries.
// This approach isn't great for handling long search strings but gdb's find
// command isn't really suited to that.
// Reading page by page lets us avoid problems where some pages in a
// mapping aren't readable (e.g. reading beyond end of file).
while (r.size() >= find.size()) {
ssize_t nread = t->read_bytes_fallible(
r.start(), std::min(buf.size(), r.size()), buf.data());
if (nread >= ssize_t(find.size())) {
void* found = memmem(buf.data(), nread, find.data(), find.size());
if (found) {
*result = r.start() + (static_cast<uint8_t*>(found) - buf.data());
return true;
}
}
r = MemoryRange(
std::min(r.end(), floor_page_size(r.start()) + page_size()), r.end());
}
}
return false;
}
static bool is_in_patch_stubs(Task* t, remote_code_ptr ip) {
auto p = ip.to_data_ptr<void>();
return t->vm()->has_mapping(p) &&
(t->vm()->mapping_flags_of(p) & AddressSpace::Mapping::IS_PATCH_STUBS);
}
void GdbServer::maybe_intercept_mem_request(Task* target, const GdbRequest& req,
vector<uint8_t>* result) {
DEBUG_ASSERT(req.mem_.len >= result->size());
/* Crazy hack!
* When gdb tries to read the word at the top of the stack, and we're in our
* dynamically-generated stub code, tell it the value is zero, so that gdb's
* stack-walking code doesn't find a bogus value that it treats as a return
* address and sets a breakpoint there, potentially corrupting program data.
* gdb sometimes reads a whole block of memory around the stack pointer so
* handle cases where the top-of-stack word is contained in a larger range.
*/
size_t size = word_size(target->arch());
if (target->regs().sp().as_int() >= req.mem_.addr &&
target->regs().sp().as_int() + size <= req.mem_.addr + result->size() &&
is_in_patch_stubs(target, target->ip())) {
memset(result->data() + target->regs().sp().as_int() - req.mem_.addr, 0,
size);
}
}
void GdbServer::dispatch_debugger_request(Session& session,
const GdbRequest& req,
ReportState state) {
DEBUG_ASSERT(!req.is_resume_request());
// These requests don't require a target task.
switch (req.type) {
case DREQ_RESTART:
DEBUG_ASSERT(false);
return; // unreached
case DREQ_GET_CURRENT_THREAD:
dbg->reply_get_current_thread(get_threadid(session, last_continue_tuid));
return;
case DREQ_GET_OFFSETS:
/* TODO */
dbg->reply_get_offsets();
return;
case DREQ_GET_THREAD_LIST: {
vector<GdbThreadId> tids;
if (state != REPORT_THREADS_DEAD) {
for (auto& kv : session.tasks()) {
tids.push_back(get_threadid(session, kv.second->tuid()));
}
}
dbg->reply_get_thread_list(tids);
return;
}
case DREQ_INTERRUPT: {
Task* t = session.find_task(last_continue_tuid);
ASSERT(t, session.is_diversion())
<< "Replay interrupts should be handled at a higher level";
DEBUG_ASSERT(!t || t->thread_group()->tguid() == debuggee_tguid);
dbg->notify_stop(t ? get_threadid(t) : GdbThreadId(), 0);
memset(&stop_siginfo, 0, sizeof(stop_siginfo));
if (t) {
last_query_tuid = last_continue_tuid = t->tuid();
}
return;
}
case DREQ_GET_EXEC_FILE: {
// We shouldn't normally receive this since we try to pass the exe file
// name on gdb's command line, but the user might start gdb manually
// and this is easy to support in case some other debugger or
// configuration needs it.
Task* t = nullptr;
if (req.target.tid) {
ThreadGroup* tg = session.find_thread_group(req.target.tid);
if (tg) {
t = *tg->task_set().begin();
}
} else {
t = session.find_task(last_continue_tuid);
}
if (t) {
dbg->reply_get_exec_file(t->vm()->exe_image());
} else {
dbg->reply_get_exec_file(string());
}
return;
}
case DREQ_FILE_SETFS:
// Only the filesystem as seen by the remote stub is supported currently
file_scope_pid = req.file_setfs().pid;
dbg->reply_setfs(0);
return;
case DREQ_FILE_OPEN:
// We only support reading files
if (req.file_open().flags == O_RDONLY) {
Task* t = session.find_task(last_continue_tuid);
int fd = open_file(session, t, req.file_open().file_name);
dbg->reply_open(fd, fd >= 0 ? 0 : ENOENT);
} else {
dbg->reply_open(-1, EACCES);
}
return;
case DREQ_FILE_PREAD: {
GdbRequest::FilePread read_req = req.file_pread();
{
auto it = files.find(read_req.fd);
if (it != files.end()) {
size_t size = min<uint64_t>(read_req.size, 1024 * 1024);
vector<uint8_t> data;
data.resize(size);
ssize_t bytes =
read_to_end(it->second, read_req.offset, data.data(), size);
dbg->reply_pread(data.data(), bytes, bytes >= 0 ? 0 : -errno);
return;
}
}
{
auto it = memory_files.find(read_req.fd);
if (it != memory_files.end() && timeline.is_running()) {
// Search our mmap stream for a record that can satisfy this request
TraceReader tmp_reader(timeline.current_session().trace_reader());
tmp_reader.rewind();
while (true) {
TraceReader::MappedData data;
bool found;
KernelMapping km = tmp_reader.read_mapped_region(
&data, &found, TraceReader::DONT_VALIDATE, TraceReader::ANY_TIME);
if (!found)
break;
if (it->second == FileId(km)) {
if (data.source != TraceReader::SOURCE_FILE) {
LOG(warn) << "Not serving file because it is not a file source";
break;
}
ScopedFd fd(data.file_name.c_str(), O_RDONLY);
vector<uint8_t> data;
data.resize(read_req.size);
LOG(debug) << "Reading " << read_req.size << " bytes at offset " <<
read_req.offset;
ssize_t bytes =
read_to_end(fd, read_req.offset, data.data(), read_req.size);
if (bytes < (ssize_t)read_req.size) {
LOG(warn) << "Requested " << read_req.size << " bytes but only got " << bytes;
}
dbg->reply_pread(data.data(), bytes, bytes >= 0 ? 0 : -errno);
return;
}
}
LOG(warn) << "No mapping found";
}
}
LOG(warn) << "Unknown file descriptor requested";
dbg->reply_pread(nullptr, 0, EIO);
return;
}
case DREQ_FILE_CLOSE: {
{
auto it = files.find(req.file_close().fd);
if (it != files.end()) {
files.erase(it);
dbg->reply_close(0);
return;
}
} {
auto it = memory_files.find(req.file_close().fd);
if (it != memory_files.end()) {
memory_files.erase(it);
dbg->reply_close(0);
return;
}
}
LOG(warn) << "Unable to find file descriptor for close";
dbg->reply_close(EBADF);
return;
}
default:
/* fall through to next switch stmt */
break;
}
bool is_query = req.type != DREQ_SET_CONTINUE_THREAD;
Task* target =
req.target.tid > 0
? session.find_task(req.target.tid)
: session.find_task(is_query ? last_query_tuid : last_continue_tuid);
if (target) {
if (is_query) {
last_query_tuid = target->tuid();
} else {
last_continue_tuid = target->tuid();
}
}
// These requests query or manipulate which task is the
// target, so it's OK if the task doesn't exist.
switch (req.type) {
case DREQ_GET_IS_THREAD_ALIVE:
dbg->reply_get_is_thread_alive(target != nullptr);
return;
case DREQ_GET_THREAD_EXTRA_INFO:
dbg->reply_get_thread_extra_info(target->name());
return;
case DREQ_SET_CONTINUE_THREAD:
dbg->reply_select_thread(target != nullptr);
return;
case DREQ_SET_QUERY_THREAD:
dbg->reply_select_thread(target != nullptr);
return;
default:
// fall through to next switch stmt
break;
}
// These requests require a valid target task. We don't trust
// the debugger to use the information provided above to only
// query valid tasks.
if (!target) {
dbg->notify_no_such_thread(req);
return;
}
switch (req.type) {
case DREQ_GET_AUXV: {
dbg->reply_get_auxv(target->vm()->saved_auxv());
return;
}
case DREQ_GET_MEM: {
vector<uint8_t> mem;
mem.resize(req.mem().len);
ssize_t nread = target->read_bytes_fallible(req.mem().addr, req.mem().len,
mem.data());
mem.resize(max(ssize_t(0), nread));
target->vm()->replace_breakpoints_with_original_values(
mem.data(), mem.size(), req.mem().addr);
maybe_intercept_mem_request(target, req, &mem);
dbg->reply_get_mem(mem);
return;
}
case DREQ_SET_MEM: {
// gdb has been observed to send requests of length 0 at
// odd times
// (e.g. before sending the magic write to create a checkpoint)
if (req.mem().len == 0) {
dbg->reply_set_mem(true);
return;
}
// If an address is recognised as belonging to a SystemTap semaphore it's
// because it was detected by the audit library during recording and
// pre-incremented.
if (target->vm()->is_stap_semaphore(req.mem().addr)) {
LOG(info) << "Suppressing write to SystemTap semaphore";
dbg->reply_set_mem(true);
return;
}
// We only allow the debugger to write memory if the
// memory will be written to an diversion session.
// Arbitrary writes to replay sessions cause
// divergence.
if (!session.is_diversion()) {
LOG(error) << "Attempt to write memory outside diversion session";
dbg->reply_set_mem(false);
return;
}
LOG(debug) << "Writing " << req.mem().len << " bytes to "
<< HEX(req.mem().addr);
// TODO fallible
target->write_bytes_helper(req.mem().addr, req.mem().len,
req.mem().data.data());
dbg->reply_set_mem(true);
return;
}
case DREQ_SEARCH_MEM: {
remote_ptr<void> addr;
bool found =
search_memory(target, MemoryRange(req.mem().addr, req.mem().len),
req.mem().data, &addr);
dbg->reply_search_mem(found, addr);
return;
}
case DREQ_GET_REG: {
GdbRegisterValue reg =
get_reg(target->regs(), target->extra_regs(), req.reg().name);
dbg->reply_get_reg(reg);
return;
}
case DREQ_GET_REGS: {
dispatch_regs_request(target->regs(), target->extra_regs());
return;
}
case DREQ_SET_REG: {
if (!session.is_diversion()) {
// gdb sets orig_eax to -1 during a restart. For a
// replay session this is not correct (we might be
// restarting from an rr checkpoint inside a system
// call, and we must not tamper with replay state), so
// just ignore it.
if ((target->arch() == x86 && req.reg().name == DREG_ORIG_EAX) ||
(target->arch() == x86_64 && req.reg().name == DREG_ORIG_RAX)) {
dbg->reply_set_reg(true);
return;
}
LOG(error) << "Attempt to write register outside diversion session";
dbg->reply_set_reg(false);
return;
}
if (!set_reg(target, req.reg())) {
LOG(warn) << "Attempt to set register " << req.reg().name << " failed";
}
dbg->reply_set_reg(true /*currently infallible*/);
return;
}
case DREQ_GET_STOP_REASON: {
dbg->reply_get_stop_reason(get_threadid(session, last_continue_tuid),
stop_siginfo.si_signo);
return;
}
case DREQ_SET_SW_BREAK: {
ASSERT(target, req.watch().kind == bkpt_instruction_length(target->arch()))
<< "Debugger setting bad breakpoint insn";
// Mirror all breakpoint/watchpoint sets/unsets to the target process
// if it's not part of the timeline (i.e. it's a diversion).
ReplayTask* replay_task =
timeline.current_session().find_task(target->tuid());
bool ok = timeline.add_breakpoint(replay_task, req.watch().addr,
breakpoint_condition(req));
if (ok && &session != &timeline.current_session()) {
bool diversion_ok =
target->vm()->add_breakpoint(req.watch().addr, BKPT_USER);
ASSERT(target, diversion_ok);
}
dbg->reply_watchpoint_request(ok);
return;
}
case DREQ_SET_HW_BREAK:
case DREQ_SET_RD_WATCH:
case DREQ_SET_WR_WATCH:
case DREQ_SET_RDWR_WATCH: {
ReplayTask* replay_task =
timeline.current_session().find_task(target->tuid());
bool ok = timeline.add_watchpoint(
replay_task, req.watch().addr, req.watch().kind,
watchpoint_type(req.type), breakpoint_condition(req));
if (ok && &session != &timeline.current_session()) {
bool diversion_ok = target->vm()->add_watchpoint(
req.watch().addr, req.watch().kind, watchpoint_type(req.type));
ASSERT(target, diversion_ok);
}
dbg->reply_watchpoint_request(ok);
return;
}
case DREQ_REMOVE_SW_BREAK: {
ReplayTask* replay_task =
timeline.current_session().find_task(target->tuid());
timeline.remove_breakpoint(replay_task, req.watch().addr);
if (&session != &timeline.current_session()) {
target->vm()->remove_breakpoint(req.watch().addr, BKPT_USER);
}
dbg->reply_watchpoint_request(true);
return;
}
case DREQ_REMOVE_HW_BREAK:
case DREQ_REMOVE_RD_WATCH:
case DREQ_REMOVE_WR_WATCH:
case DREQ_REMOVE_RDWR_WATCH: {
ReplayTask* replay_task =
timeline.current_session().find_task(target->tuid());
timeline.remove_watchpoint(replay_task, req.watch().addr,
req.watch().kind, watchpoint_type(req.type));
if (&session != &timeline.current_session()) {
target->vm()->remove_watchpoint(req.watch().addr, req.watch().kind,
watchpoint_type(req.type));
}
dbg->reply_watchpoint_request(true);
return;
}
case DREQ_READ_SIGINFO: {
vector<uint8_t> si_bytes;
si_bytes.resize(req.mem().len);
memset(si_bytes.data(), 0, si_bytes.size());
memcpy(si_bytes.data(), &stop_siginfo,
min(si_bytes.size(), sizeof(stop_siginfo)));
dbg->reply_read_siginfo(si_bytes);
return;
}
case DREQ_WRITE_SIGINFO:
LOG(warn) << "WRITE_SIGINFO request outside of diversion session";
dbg->reply_write_siginfo();
return;
case DREQ_RR_CMD:
dbg->reply_rr_cmd(
GdbCommandHandler::process_command(*this, target, req.text()));
return;
#ifdef PROC_SERVICE_H
case DREQ_QSYMBOL: {
// When gdb sends "qSymbol::", it means that gdb is ready to
// respond to symbol requests. This can be sent multiple times
// during the course of a session -- gdb sends it whenever
// something in the inferior has changed, making it possible
// that previous failed symbol lookups could now succeed. In
// response to a qSymbol request from gdb, we either send back a
// qSymbol response, requesting the address of a symbol; or we
// send back OK. We have to do this as an ordinary response and
// maintain our own state explicitly, as opposed to simply
// reading another packet from gdb, because when gdb looks up a
// symbol it might send other requests that must be served. So,
// we keep a copy of the symbol names, and an iterator into this
// copy. When gdb sends a plain "qSymbol::" packet, because gdb
// has detected some change in the inferior state that might
// enable more symbol lookups, we restart the iterator.
if (!thread_db) {
thread_db =
std::unique_ptr<ThreadDb>(new ThreadDb(debuggee_tguid.tid()));
}
const string& name = req.sym().name;
if (req.sym().has_address) {
// Got a response holding a previously-requested symbol's name
// and address.
thread_db->register_symbol(name, req.sym().address);
} else if (name == "") {
// Plain "qSymbol::" request.
symbols =
thread_db->get_symbols_and_clear_map(target->thread_group().get());
symbols_iter = symbols.begin();
}
if (symbols_iter == symbols.end()) {
dbg->qsymbols_finished();
} else {
string symbol = *symbols_iter++;
dbg->send_qsymbol(symbol);
}
return;
}
case DREQ_TLS: {
if (!thread_db) {
thread_db =
std::unique_ptr<ThreadDb>(new ThreadDb(debuggee_tguid.tid()));
}
remote_ptr<void> address;
bool ok = thread_db->get_tls_address(target->thread_group().get(),
target->rec_tid, req.tls().offset,
req.tls().load_module, &address);
dbg->reply_tls_addr(ok, address);
return;
}
#endif
default:
FATAL() << "Unknown debugger request " << req.type;
}
}
static bool any_action_targets_match(const Session& session,
const TaskUid& tuid,
const vector<GdbContAction>& actions) {
GdbThreadId tid = get_threadid(session, tuid);
return any_of(actions.begin(), actions.end(), [tid](GdbContAction action) {
return matches_threadid(tid, action.target);
});
}
static Task* find_first_task_matching_target(
const Session& session, const vector<GdbContAction>& actions) {
const Session::TaskMap& tasks = session.tasks();
auto it = find_first_of(
tasks.begin(), tasks.end(),
actions.begin(), actions.end(),
[](Session::TaskMap::value_type task_pair, GdbContAction action) {
return matches_threadid(task_pair.second, action.target);
});
return it != tasks.end() ? it->second : nullptr;
}
bool GdbServer::diverter_process_debugger_requests(
DiversionSession& diversion_session, uint32_t& diversion_refcount,
GdbRequest* req) {
while (true) {
*req = dbg->get_request();
if (req->is_resume_request()) {
const vector<GdbContAction>& actions = req->cont().actions;
DEBUG_ASSERT(actions.size() > 0);
// GDB may ask us to resume more than one task, so we have to
// choose one. We give priority to the task last resumed, as
// this is likely to be the context in which GDB is executing
// code; selecting any other task runs the risk of resuming
// replay, denying the diverted code an opportunity to complete
// and end the diversion session.
if (!any_action_targets_match(diversion_session, last_continue_tuid,
actions)) {
// If none of the resumption targets match the task last
// resumed, we simply choose any matching task. This ensures
// that GDB (and the user) can choose an arbitrary thread to
// serve as the context of the code being evaluated.
// TODO: maybe it makes sense to try and select the matching
// task that was most recently resumed, or possibly the
// matching task with an event in the replay trace nearest to
// 'now'.
Task* task =
find_first_task_matching_target(diversion_session, actions);
DEBUG_ASSERT(task != nullptr);
last_continue_tuid = task->tuid();
}
return diversion_refcount > 0;
}
switch (req->type) {
case DREQ_RESTART:
case DREQ_DETACH:
diversion_refcount = 0;
return false;
case DREQ_READ_SIGINFO: {
LOG(debug) << "Adding ref to diversion session";
++diversion_refcount;
// TODO: maybe share with replayer.cc?
vector<uint8_t> si_bytes;
si_bytes.resize(req->mem().len);
memset(si_bytes.data(), 0, si_bytes.size());
dbg->reply_read_siginfo(si_bytes);
continue;
}
case DREQ_SET_QUERY_THREAD: {
if (req->target.tid) {
Task* next = diversion_session.find_task(req->target.tid);
if (next) {
last_query_tuid = next->tuid();
}
}
break;
}
case DREQ_WRITE_SIGINFO:
LOG(debug) << "Removing reference to diversion session ...";
DEBUG_ASSERT(diversion_refcount > 0);
--diversion_refcount;
if (diversion_refcount == 0) {
LOG(debug) << " ... dying at next continue request";
}
dbg->reply_write_siginfo();
continue;
case DREQ_RR_CMD: {
DEBUG_ASSERT(req->type == DREQ_RR_CMD);
Task* task = diversion_session.find_task(last_continue_tuid);
if (task) {
std::string reply =
GdbCommandHandler::process_command(*this, task, req->text());
// Certain commands cause the diversion to end immediately
// while other commands must work within a diversion.
if (reply == GdbCommandHandler::cmd_end_diversion()) {
diversion_refcount = 0;
return false;
}
dbg->reply_rr_cmd(reply);
continue;
} else {
diversion_refcount = 0;
return false;
}
break;
}
default:
break;
}
dispatch_debugger_request(diversion_session, *req, REPORT_NORMAL);
}
}
static bool is_last_thread_exit(const BreakStatus& break_status) {
// The task set may be empty if the task has already exited.
return break_status.task_exit &&
break_status.task_context.thread_group->task_set().size() <= 1;
}
static Task* is_in_exec(ReplayTimeline& timeline) {
Task* t = timeline.current_session().current_task();
if (!t) {
return nullptr;
}
return timeline.current_session().next_step_is_successful_exec_syscall_exit()
? t
: nullptr;
}
void GdbServer::maybe_notify_stop(const GdbRequest& req,
const BreakStatus& break_status) {
bool do_stop = false;
remote_ptr<void> watch_addr;
char watch[1024];
watch[0] = '\0';
if (!break_status.watchpoints_hit.empty()) {
do_stop = true;
memset(&stop_siginfo, 0, sizeof(stop_siginfo));
stop_siginfo.si_signo = SIGTRAP;
watch_addr = break_status.watchpoints_hit[0].addr;
bool any_hw_break = false;
for (const auto& w : break_status.watchpoints_hit) {
if (w.type == WATCH_EXEC) {
any_hw_break = true;
}
}
if (dbg->hwbreak_supported() && any_hw_break) {
snprintf(watch, sizeof(watch) - 1, "hwbreak:;");
} else if (watch_addr) {
snprintf(watch, sizeof(watch) - 1, "watch:%" PRIxPTR ";", watch_addr.as_int());
}
LOG(debug) << "Stopping for watchpoint at " << watch_addr;
}
if (break_status.breakpoint_hit || break_status.singlestep_complete) {
do_stop = true;
memset(&stop_siginfo, 0, sizeof(stop_siginfo));
stop_siginfo.si_signo = SIGTRAP;
if (break_status.breakpoint_hit) {
if (dbg->swbreak_supported()) {
snprintf(watch, sizeof(watch) - 1, "swbreak:;");
}
LOG(debug) << "Stopping for breakpoint";
} else {
LOG(debug) << "Stopping for singlestep";
}
}
if (break_status.signal) {
do_stop = true;
stop_siginfo = *break_status.signal;
LOG(debug) << "Stopping for signal " << stop_siginfo;
}
if (is_last_thread_exit(break_status)) {
if (break_status.task_context.session->is_diversion()) {
// If the last task of a diversion session has exited, we need
// to make sure GDB knows it's unrecoverable. There's no good
// way to do this: a stop is insufficient, but an inferior exit
// typically signals the end of a debugging session. Using the
// latter approach appears to work, but stepping through GDB's
// processing of the event seems to indicate it isn't really
// supposed to. FIXME.
LOG(debug) << "Last task of diversion exiting. "
<< "Notifying exit with synthetic SIGKILL";
dbg->notify_exit_signal(SIGKILL);
return;
} else if (dbg->features().reverse_execution) {
do_stop = true;
memset(&stop_siginfo, 0, sizeof(stop_siginfo));
if (req.cont().run_direction == RUN_FORWARD) {
// The exit of the last task in a thread group generates a fake SIGKILL,
// when reverse-execution is enabled, because users often want to run
// backwards from the end of the task.
stop_siginfo.si_signo = SIGKILL;
LOG(debug) << "Stopping for synthetic SIGKILL";
} else {
// The start of the debuggee task-group should trigger a silent stop.
stop_siginfo.si_signo = 0;
LOG(debug) << "Stopping at start of execution while running backwards";
}
}
}
Task* t = break_status.task();
Task* in_exec_task = is_in_exec(timeline);
if (in_exec_task) {
do_stop = true;
memset(&stop_siginfo, 0, sizeof(stop_siginfo));
t = in_exec_task;
LOG(debug) << "Stopping at exec";
}
if (do_stop && t->thread_group()->tguid() == debuggee_tguid) {
/* Notify the debugger and process any new requests
* that might have triggered before resuming. */
dbg->notify_stop(get_threadid(t), stop_siginfo.si_signo,
watch);
last_query_tuid = last_continue_tuid = t->tuid();
}
}
static RunCommand compute_run_command_from_actions(Task* t,
const GdbRequest& req,
int* signal_to_deliver) {
for (auto& action : req.cont().actions) {
if (matches_threadid(t, action.target)) {
// We can only run task |t|; neither diversion nor replay sessions
// support running multiple threads. So even if gdb tells us to continue
// multiple threads, we don't do that.
*signal_to_deliver = action.signal_to_deliver;
return action.type == ACTION_STEP ? RUN_SINGLESTEP : RUN_CONTINUE;
}
}
// gdb told us to run (or step) some thread that's not |t|, without resuming
// |t|. It sometimes does this even though its target thread is entering a
// blocking syscall and |t| must run before gdb's target thread can make
// progress. So, allow |t| to run anyway.
*signal_to_deliver = 0;
return RUN_CONTINUE;
}
struct AllowedTasks {
TaskUid task; // tid 0 means 'any member of debuggee_tguid'
RunCommand command;
};
static RunCommand compute_run_command_for_reverse_exec(
Session& session, const ThreadGroupUid& debuggee_tguid,
const GdbRequest& req, vector<AllowedTasks>& allowed_tasks) {
// Singlestep if any of the actions request singlestepping.
RunCommand result = RUN_CONTINUE;
for (auto& action : req.cont().actions) {
if (action.target.pid > 0 && action.target.pid != debuggee_tguid.tid()) {
continue;
}
AllowedTasks allowed;
allowed.command = RUN_CONTINUE;
if (action.type == ACTION_STEP) {
allowed.command = result = RUN_SINGLESTEP;
}
if (action.target.tid > 0) {
Task* t = session.find_task(action.target.tid);
if (t) {
allowed.task = t->tuid();
}
}
allowed_tasks.push_back(allowed);
}
return result;
}
/**
* Create a new diversion session using |replay| session as the
* template. The |replay| session isn't mutated.
*
* Execution begins in the new diversion session under the control of
* |dbg| starting with initial thread target |task|. The diversion
* session ends at the request of |dbg|, and |req| returns the first
* request made that wasn't handled by the diversion session. That
* is, the first request that should be handled by |replay| upon
* resuming execution in that session.
*/
GdbRequest GdbServer::divert(ReplaySession& replay) {
GdbRequest req;
LOG(debug) << "Starting debugging diversion for " << &replay;
if (timeline.is_running()) {
// Ensure breakpoints and watchpoints are applied before we fork the
// diversion, to ensure the diversion is consistent with the timeline
// breakpoint/watchpoint state.
timeline.apply_breakpoints_and_watchpoints();
}
DiversionSession::shr_ptr diversion_session = replay.clone_diversion();
uint32_t diversion_refcount = 1;
TaskUid saved_query_tuid = last_query_tuid;
TaskUid saved_continue_tuid = last_continue_tuid;
while (diverter_process_debugger_requests(*diversion_session,
diversion_refcount, &req)) {
DEBUG_ASSERT(req.is_resume_request());
if (req.cont().run_direction == RUN_BACKWARD) {
// We don't support reverse execution in a diversion. Just issue
// an immediate stop.
dbg->notify_stop(get_threadid(*diversion_session, last_continue_tuid), 0);
memset(&stop_siginfo, 0, sizeof(stop_siginfo));
last_query_tuid = last_continue_tuid;
continue;
}
Task* t = diversion_session->find_task(last_continue_tuid);
DEBUG_ASSERT(t != nullptr);
int signal_to_deliver;
RunCommand command =
compute_run_command_from_actions(t, req, &signal_to_deliver);
auto result =
diversion_session->diversion_step(t, command, signal_to_deliver);
if (result.status == DiversionSession::DIVERSION_EXITED) {
diversion_refcount = 0;
maybe_notify_stop(req, result.break_status);
if (timeline.is_running()) {
// gdb assumes that the process is gone and all its
// breakpoints have gone with it. It will set new breakpoints.
timeline.remove_breakpoints_and_watchpoints();
}
req = GdbRequest(DREQ_NONE);
break;
}
DEBUG_ASSERT(result.status == DiversionSession::DIVERSION_CONTINUE);
maybe_notify_stop(req, result.break_status);
}
LOG(debug) << "... ending debugging diversion";
DEBUG_ASSERT(diversion_refcount == 0);
diversion_session->kill_all_tasks();
last_query_tuid = saved_query_tuid;
last_continue_tuid = saved_continue_tuid;
return req;
}
/**
* Reply to debugger requests until the debugger asks us to resume
* execution, detach, restart, or interrupt.
*/
GdbRequest GdbServer::process_debugger_requests(ReportState state) {
while (true) {
GdbRequest req = dbg->get_request();
req.suppress_debugger_stop = false;
try_lazy_reverse_singlesteps(req);
if (req.type == DREQ_READ_SIGINFO) {
vector<uint8_t> si_bytes;
si_bytes.resize(req.mem().len);
memset(si_bytes.data(), 0, si_bytes.size());
memcpy(si_bytes.data(), &stop_siginfo,
min(si_bytes.size(), sizeof(stop_siginfo)));
dbg->reply_read_siginfo(si_bytes);
// READ_SIGINFO is usually the start of a diversion. It can also be
// triggered by "print $_siginfo" but that is rare so we just assume it's
// a diversion start; if "print $_siginfo" happens we'll print the correct
// siginfo and then incorrectly start a diversion and go haywire :-(.
// Ideally we'd come up with a better way to detect diversions so that
// "print $_siginfo" works.
req = divert(timeline.current_session());
if (req.type == DREQ_NONE) {
continue;
}
// Carry on to process the request that was rejected by
// the diversion session
}
if (req.is_resume_request()) {
Task* t = current_session().find_task(last_continue_tuid);
if (t) {
maybe_singlestep_for_event(t, &req);
}
return req;
}
if (req.type == DREQ_INTERRUPT) {
LOG(debug) << " request to interrupt";
return req;
}
if (req.type == DREQ_RESTART) {
// Debugger client requested that we restart execution
// from the beginning. Restart our debug session.
LOG(debug) << " request to restart at event " << req.restart().param;
return req;
}
if (req.type == DREQ_DETACH) {
LOG(debug) << " debugger detached";
dbg->reply_detach();
return req;
}
dispatch_debugger_request(current_session(), req, state);
}
}
void GdbServer::try_lazy_reverse_singlesteps(GdbRequest& req) {
if (!timeline.is_running()) {
return;
}
ReplayTimeline::Mark now;
bool need_seek = false;
ReplayTask* t = timeline.current_session().current_task();
while (t && req.type == DREQ_CONT &&
req.cont().run_direction == RUN_BACKWARD &&
req.cont().actions.size() == 1 &&
req.cont().actions[0].type == ACTION_STEP &&
req.cont().actions[0].signal_to_deliver == 0 &&
matches_threadid(t, req.cont().actions[0].target) &&
!req.suppress_debugger_stop) {
if (!now) {
now = timeline.mark();
}
ReplayTimeline::Mark previous = timeline.lazy_reverse_singlestep(now, t);
if (!previous) {
break;
}
now = previous;
need_seek = true;
BreakStatus break_status;
break_status.task_context = TaskContext(t);
break_status.singlestep_complete = true;
LOG(debug) << " using lazy reverse-singlestep";
maybe_notify_stop(req, break_status);
while (true) {
req = dbg->get_request();
req.suppress_debugger_stop = false;
if (req.type != DREQ_GET_REGS) {
break;
}
LOG(debug) << " using lazy reverse-singlestep registers";
dispatch_regs_request(now.regs(), now.extra_regs());
}
}
if (need_seek) {
timeline.seek_to_mark(now);
}
}
bool GdbServer::detach_or_restart(const GdbRequest& req, ContinueOrStop* s) {
if (DREQ_RESTART == req.type) {
restart_session(req);
*s = CONTINUE_DEBUGGING;
return true;
}
if (DREQ_DETACH == req.type) {
*s = STOP_DEBUGGING;
return true;
}
return false;
}
GdbServer::ContinueOrStop GdbServer::handle_exited_state(
GdbRequest& last_resume_request) {
// TODO return real exit code, if it's useful.
dbg->notify_exit_code(0);
final_event = timeline.current_session().trace_reader().time();
GdbRequest req = process_debugger_requests(REPORT_THREADS_DEAD);
ContinueOrStop s;
if (detach_or_restart(req, &s)) {
last_resume_request = GdbRequest();
return s;
}
FATAL() << "Received continue/interrupt request after end-of-trace.";
return STOP_DEBUGGING;
}
GdbServer::ContinueOrStop GdbServer::debug_one_step(
GdbRequest& last_resume_request) {
ReplayResult result;
GdbRequest req;
if (in_debuggee_end_state) {
// Treat the state where the last thread is about to exit like
// termination.
req = process_debugger_requests();
// If it's a forward execution request, fake the exited state.
if (req.is_resume_request() && req.cont().run_direction == RUN_FORWARD) {
if (interrupt_pending) {
// Just process this. We're getting it after a restart.
} else {
return handle_exited_state(last_resume_request);
}
} else {
if (req.type != DREQ_DETACH) {
in_debuggee_end_state = false;
}
}
// Otherwise (e.g. detach, restart, interrupt or reverse-exec) process
// the request as normal.
} else if (!interrupt_pending || last_resume_request.type == DREQ_NONE) {
req = process_debugger_requests();
} else {
req = last_resume_request;
}
ContinueOrStop s;
if (detach_or_restart(req, &s)) {
last_resume_request = GdbRequest();
return s;
}
if (req.is_resume_request()) {
last_resume_request = req;
} else {
DEBUG_ASSERT(req.type == DREQ_INTERRUPT);
interrupt_pending = true;
req = last_resume_request;
DEBUG_ASSERT(req.is_resume_request());
}
if (interrupt_pending) {
Task* t = timeline.current_session().current_task();
if (t->thread_group()->tguid() == debuggee_tguid) {
interrupt_pending = false;
dbg->notify_stop(get_threadid(t), in_debuggee_end_state ? SIGKILL : 0);
memset(&stop_siginfo, 0, sizeof(stop_siginfo));
return CONTINUE_DEBUGGING;
}
}
if (exit_sigkill_pending) {
Task* t = timeline.current_session().current_task();
if (t->thread_group()->tguid() == debuggee_tguid) {
exit_sigkill_pending = false;
if (req.cont().run_direction == RUN_FORWARD) {
dbg->notify_stop(get_threadid(t), SIGKILL);
memset(&stop_siginfo, 0, sizeof(stop_siginfo));
return CONTINUE_DEBUGGING;
}
}
}
if (req.cont().run_direction == RUN_FORWARD) {
if (is_in_exec(timeline) &&
timeline.current_session().current_task()->thread_group()->tguid() ==
debuggee_tguid) {
// Don't go any further forward. maybe_notify_stop will generate a
// stop.
result = ReplayResult();
} else {
int signal_to_deliver;
RunCommand command = compute_run_command_from_actions(
timeline.current_session().current_task(), req, &signal_to_deliver);
// Ignore gdb's |signal_to_deliver|; we just have to follow the replay.
result = timeline.replay_step_forward(command);
}
if (result.status == REPLAY_EXITED) {
return handle_exited_state(last_resume_request);
}
} else {
vector<AllowedTasks> allowed_tasks;
// Convert the tids in GdbContActions into TaskUids to avoid issues
// if tids get reused.
RunCommand command = compute_run_command_for_reverse_exec(
timeline.current_session(), debuggee_tguid, req, allowed_tasks);
auto stop_filter = [&](ReplayTask* t, const BreakStatus &break_status) -> bool {
if (t->thread_group()->tguid() != debuggee_tguid) {
return false;
}
// don't stop for a signal that has been specified by QPassSignal
if (break_status.signal && dbg->is_pass_signal(break_status.signal->si_signo)) {
LOG(debug) << "Filtering out event for signal " << break_status.signal->si_signo;
return false;
}
// If gdb's requested actions don't allow the task to run, we still
// let it run (we can't do anything else, since we're replaying), but
// we won't report stops in that task.
for (auto& a : allowed_tasks) {
if (a.task.tid() == 0 || a.task == t->tuid()) {
return true;
}
}
return false;
};
auto interrupt_check = [&]() { return dbg->sniff_packet(); };
switch (command) {
case RUN_CONTINUE:
result = timeline.reverse_continue(stop_filter, interrupt_check);
break;
case RUN_SINGLESTEP: {
Task* t = timeline.current_session().find_task(last_continue_tuid);
DEBUG_ASSERT(t);
result = timeline.reverse_singlestep(
last_continue_tuid, t->tick_count(), stop_filter, interrupt_check);
break;
}
default:
DEBUG_ASSERT(0 && "Unknown RunCommand");
}
if (result.status == REPLAY_EXITED) {
return handle_exited_state(last_resume_request);
}
}
if (!req.suppress_debugger_stop) {
maybe_notify_stop(req, result.break_status);
}
if (req.cont().run_direction == RUN_FORWARD &&
is_last_thread_exit(result.break_status) &&
result.break_status.task_context.thread_group->tguid() == debuggee_tguid) {
in_debuggee_end_state = true;
}
return CONTINUE_DEBUGGING;
}
static bool target_event_reached(const ReplayTimeline& timeline, const GdbServer::Target& target, const ReplayResult& result) {
if (target.event == -1) {
return is_last_thread_exit(result.break_status) &&
(target.pid <= 0 || result.break_status.task_context.thread_group->tgid == target.pid);
} else {
return timeline.current_session().current_trace_frame().time() > target.event;
}
}
bool GdbServer::at_target(ReplayResult& result) {
// Don't launch the debugger for the initial rr fork child.
// No one ever wants that to happen.
if (!timeline.current_session().done_initial_exec()) {
return false;
}
Task* t = timeline.current_session().current_task();
if (!t) {
return false;
}
bool target_is_exit = target.event == -1;
if (!(timeline.can_add_checkpoint() || target_is_exit)) {
return false;
}
if (stop_replaying_to_target) {
return true;
}
// When we decide to create the debugger, we may end up
// creating a checkpoint. In that case, we want the
// checkpoint to retain the state it had *before* we started
// replaying the next frame. Otherwise, the TraceIfstream
// will be one frame ahead of its tracee tree.
//
// So we make the decision to create the debugger based on the
// frame we're *about to* replay, without modifying the
// TraceIfstream.
// NB: we'll happily attach to whichever task within the
// group happens to be scheduled here. We don't take
// "attach to process" to mean "attach to thread-group
// leader".
return target_event_reached(timeline, target, result) &&
(!target.pid || t->tgid() == target.pid) &&
(!target.require_exec || t->execed()) &&
// Ensure we're at the start of processing an event. We don't
// want to attach while we're finishing an exec() since that's a
// slightly confusing state for ReplayTimeline's reverse execution.
(!timeline.current_session().current_step_key().in_execution() || target_is_exit);
}
/**
* The trace has reached the event at which the user wanted to start debugging.
* Set up the appropriate state.
*/
void GdbServer::activate_debugger() {
TraceFrame next_frame = timeline.current_session().current_trace_frame();
FrameTime event_now = next_frame.time();
Task* t = timeline.current_session().current_task();
if (target.event || target.pid) {
if (stop_replaying_to_target) {
fprintf(stderr, "\a\n"
"--------------------------------------------------\n"
" ---> Interrupted; attached to NON-TARGET process %d at event %llu.\n"
"--------------------------------------------------\n",
t->tgid(), (long long)event_now);
} else if (target.event >= 0) {
fprintf(stderr, "\a\n"
"--------------------------------------------------\n"
" ---> Reached target process %d at event %llu.\n"
"--------------------------------------------------\n",
t->tgid(), (long long)event_now);
} else {
ASSERT(t, target.event == -1);
fprintf(stderr, "\a\n"
"--------------------------------------------------\n"
" ---> Reached exit of target process %d at event %llu.\n"
"--------------------------------------------------\n",
t->tgid(), (long long)event_now);
exit_sigkill_pending = true;
}
}
// Store the current tgid and event as the "execution target"
// for the next replay session, if we end up restarting. This
// allows us to determine if a later session has reached this
// target without necessarily replaying up to this point.
target.pid = t->tgid();
target.require_exec = false;
target.event = event_now;
last_query_tuid = last_continue_tuid = t->tuid();
// Have the "checkpoint" be the original replay
// session, and then switch over to using the cloned
// session. The cloned tasks will look like children
// of the clonees, so this scheme prevents |pstree|
// output from getting /too/ far out of whack.
const char* where = "???";
if (timeline.can_add_checkpoint()) {
debugger_restart_checkpoint =
Checkpoint(timeline, last_continue_tuid, Checkpoint::EXPLICIT, where);
} else {
debugger_restart_checkpoint = Checkpoint(timeline, last_continue_tuid,
Checkpoint::NOT_EXPLICIT, where);
}
}
void GdbServer::restart_session(const GdbRequest& req) {
DEBUG_ASSERT(req.type == DREQ_RESTART);
DEBUG_ASSERT(dbg);
in_debuggee_end_state = false;
timeline.remove_breakpoints_and_watchpoints();
Checkpoint checkpoint_to_restore;
if (req.restart().type == RESTART_FROM_CHECKPOINT) {
auto it = checkpoints.find(req.restart().param);
if (it == checkpoints.end()) {
cout << "Checkpoint " << req.restart().param_str << " not found.\n";
cout << "Valid checkpoints:";
for (auto& c : checkpoints) {
cout << " " << c.first;
}
cout << "\n";
dbg->notify_restart_failed();
return;
}
checkpoint_to_restore = it->second;
} else if (req.restart().type == RESTART_FROM_PREVIOUS) {
checkpoint_to_restore = debugger_restart_checkpoint;
} else if (req.restart().type == RESTART_FROM_TICKS) {
Ticks target = req.restart().param;
ReplaySession &session = timeline.current_session();
Task* task = session.current_task();
FrameTime current_time = session.current_frame_time();
TraceReader tmp_reader(session.trace_reader());
FrameTime last_time = current_time;
if (session.ticks_at_start_of_current_event() > target) {
tmp_reader.rewind();
FrameTime task_time;
// EXEC and CLONE reset the ticks counter. Find the first event
// where the tuid matches our current task.
// We'll always hit at least one CLONE/EXEC event for a task
// (we can't debug the time before the initial exec)
// but set this to 0 anyway to silence compiler warnings.
FrameTime ticks_start_time = 0;
while (true) {
TraceTaskEvent r = tmp_reader.read_task_event(&task_time);
if (task_time >= current_time) {
break;
}
if (r.type() == TraceTaskEvent::CLONE || r.type() == TraceTaskEvent::EXEC) {
if (r.tid() == task->tuid().tid()) {
ticks_start_time = task_time;
}
}
}
// Forward the frame reader to the current event
last_time = ticks_start_time + 1;
while (true) {
TraceFrame frame = tmp_reader.read_frame();
if (frame.time() >= ticks_start_time) {
break;
}
}
}
while (true) {
if (tmp_reader.at_end()) {
cout << "No event found matching specified ticks target.\n";
dbg->notify_restart_failed();
return;
}
TraceFrame frame = tmp_reader.read_frame();
if (frame.tid() == task->tuid().tid() && frame.ticks() >= target) {
break;
}
last_time = frame.time() + 1;
}
timeline.seek_to_ticks(last_time, target);
}
interrupt_pending = true;
if (checkpoint_to_restore.mark) {
timeline.seek_to_mark(checkpoint_to_restore.mark);
last_query_tuid = last_continue_tuid =
checkpoint_to_restore.last_continue_tuid;
if (debugger_restart_checkpoint.is_explicit == Checkpoint::EXPLICIT) {
timeline.remove_explicit_checkpoint(debugger_restart_checkpoint.mark);
}
debugger_restart_checkpoint = checkpoint_to_restore;
if (timeline.can_add_checkpoint()) {
timeline.add_explicit_checkpoint();
}
return;
}
stop_replaying_to_target = false;
if (req.restart().type == RESTART_FROM_EVENT) {
// Note that we don't reset the target pid; we intentionally keep targeting
// the same process no matter what is running when we hit the event.
target.event = req.restart().param;
target.event = min(final_event - 1, target.event);
timeline.seek_to_before_event(target.event);
ReplayResult result;
do {
result = timeline.replay_step_forward(RUN_CONTINUE);
// We should never reach the end of the trace without hitting the stop
// condition below.
DEBUG_ASSERT(result.status != REPLAY_EXITED);
if (is_last_thread_exit(result.break_status) &&
result.break_status.task_context.thread_group->tgid == target.pid) {
// Debuggee task is about to exit. Stop here.
in_debuggee_end_state = true;
break;
}
} while (!at_target(result));
}
activate_debugger();
}
static uint32_t get_cpu_features(SupportedArch arch) {
uint32_t cpu_features;
switch (arch) {
case x86:
case x86_64: {
cpu_features = arch == x86_64 ? GdbConnection::CPU_X86_64 : 0;
unsigned int AVX_cpuid_flags = AVX_FEATURE_FLAG | OSXSAVE_FEATURE_FLAG;
auto cpuid_data = cpuid(CPUID_GETEXTENDEDFEATURES, 0);
if ((cpuid_data.ecx & PKU_FEATURE_FLAG) == PKU_FEATURE_FLAG) {
// PKU (Skylake) implies AVX (Sandy Bridge).
cpu_features |= GdbConnection::CPU_AVX | GdbConnection::CPU_PKU;
break;
}
cpuid_data = cpuid(CPUID_GETFEATURES, 0);
// We're assuming here that AVX support on the system making the recording
// is the same as the AVX support during replay. But if that's not true,
// rr is totally broken anyway.
if ((cpuid_data.ecx & AVX_cpuid_flags) == AVX_cpuid_flags) {
cpu_features |= GdbConnection::CPU_AVX;
}
break;
}
case aarch64:
cpu_features = GdbConnection::CPU_AARCH64;
break;
default:
FATAL() << "Unknown architecture";
return 0;
}
return cpu_features;
}
struct DebuggerParams {
char exe_image[PATH_MAX];
char host[16]; // INET_ADDRSTRLEN, omitted for header churn
short port;
};
static void push_default_gdb_options(vector<string>& vec, bool serve_files) {
// The gdb protocol uses the "vRun" packet to reload
// remote targets. The packet is specified to be like
// "vCont", in which gdb waits infinitely long for a
// stop reply packet. But in practice, gdb client
// expects the vRun to complete within the remote-reply
// timeout, after which it issues vCont. The timeout
// causes gdb<-->rr communication to go haywire.
//
// rr can take a very long time indeed to send the
// stop-reply to gdb after restarting replay; the time
// to reach a specified execution target is
// theoretically unbounded. Timing out on vRun is
// technically a gdb bug, but because the rr replay and
// the gdb reload models don't quite match up, we'll
// work around it on the rr side by disabling the
// remote-reply timeout.
vec.push_back("-l");
vec.push_back("10000");
if (!serve_files) {
// For now, avoid requesting binary files through vFile. That is slow and
// hard to make work correctly, because gdb requests files based on the
// names it sees in memory and in ELF, and those names may be symlinks to
// the filenames in the trace, so it's hard to match those names to files in
// the trace.
vec.push_back("-ex");
vec.push_back("set sysroot /");
}
}
static void push_target_remote_cmd(vector<string>& vec, const string& host,
unsigned short port) {
vec.push_back("-ex");
stringstream ss;
// If we omit the address, then gdb can try to resolve "localhost" which
// in some broken environments may not actually resolve to the local host
ss << "target extended-remote " << host << ":" << port;
vec.push_back(ss.str());
}
/**
* Wait for exactly one gdb host to connect to this remote target on
* the specified IP address |host|, port |port|. If |probe| is nonzero,
* a unique port based on |start_port| will be searched for. Otherwise,
* if |port| is already bound, this function will fail.
*
* Pass the |tgid| of the task on which this debug-connection request
* is being made. The remaining debugging session will be limited to
* traffic regarding |tgid|, but clients don't need to and shouldn't
* need to assume that.
*
* If we're opening this connection on behalf of a known client, pass
* an fd in |client_params_fd|; we'll write the allocated port and |exe_image|
* through the fd before waiting for a connection. |exe_image| is the
* process that will be debugged by client, or null ptr if there isn't
* a client.
*
* This function is infallible: either it will return a valid
* debugging context, or it won't return.
*/
static unique_ptr<GdbConnection> await_connection(
Task* t, ScopedFd& listen_fd, const GdbConnection::Features& features) {
auto dbg = unique_ptr<GdbConnection>(new GdbConnection(t->tgid(), features));
dbg->set_cpu_features(get_cpu_features(t->arch()));
dbg->await_debugger(listen_fd);
return dbg;
}
static void print_debugger_launch_command(Task* t, const string& host,
unsigned short port,
bool serve_files,
const char* debugger_name,
FILE* out) {
vector<string> options;
push_default_gdb_options(options, serve_files);
push_target_remote_cmd(options, host, port);
fprintf(out, "%s ", debugger_name);
for (auto& opt : options) {
fprintf(out, "'%s' ", opt.c_str());
}
fprintf(out, "%s\n", t->vm()->exe_image().c_str());
}
void GdbServer::serve_replay(const ConnectionFlags& flags) {
ReplayResult result;
do {
result = timeline.replay_step_forward(RUN_CONTINUE);
if (result.status == REPLAY_EXITED) {
LOG(info) << "Debugger was not launched before end of trace";
return;
}
} while (!at_target(result));
unsigned short port = flags.dbg_port > 0 ? flags.dbg_port : getpid();
// Don't probe if the user specified a port. Explicitly
// selecting a port is usually done by scripts, which would
// presumably break if a different port were to be selected by
// rr (otherwise why would they specify a port in the first
// place). So fail with a clearer error message.
auto probe = flags.dbg_port > 0 ? DONT_PROBE : PROBE_PORT;
Task* t = timeline.current_session().current_task();
ScopedFd listen_fd = open_socket(flags.dbg_host.c_str(), &port, probe);
if (flags.debugger_params_write_pipe) {
DebuggerParams params;
memset(&params, 0, sizeof(params));
strncpy(params.exe_image, t->vm()->exe_image().c_str(),
sizeof(params.exe_image) - 1);
strncpy(params.host, flags.dbg_host.c_str(), sizeof(params.host) - 1);
params.port = port;
ssize_t nwritten =
write(*flags.debugger_params_write_pipe, &params, sizeof(params));
DEBUG_ASSERT(nwritten == sizeof(params));
} else {
fputs("Launch gdb with\n ", stderr);
print_debugger_launch_command(t, flags.dbg_host, port, flags.serve_files,
flags.debugger_name.c_str(), stderr);
}
if (flags.debugger_params_write_pipe) {
flags.debugger_params_write_pipe->close();
}
debuggee_tguid = t->thread_group()->tguid();
FrameTime first_run_event = std::max(t->vm()->first_run_event(),
t->thread_group()->first_run_event());
if (first_run_event) {
timeline.set_reverse_execution_barrier_event(first_run_event);
}
do {
LOG(debug) << "initializing debugger connection";
dbg = await_connection(t, listen_fd, GdbConnection::Features());
activate_debugger();
GdbRequest last_resume_request;
while (debug_one_step(last_resume_request) == CONTINUE_DEBUGGING) {
}
timeline.remove_breakpoints_and_watchpoints();
} while (flags.keep_listening);
LOG(debug) << "debugger server exiting ...";
}
static string create_gdb_command_file(const string& macros) {
TempFile file = create_temporary_file("rr-gdb-commands-XXXXXX");
// This fd is just leaked. That's fine since we only call this once
// per rr invocation at the moment.
int fd = file.fd.extract();
unlink(file.name.c_str());
ssize_t len = macros.size();
int written = write(fd, macros.c_str(), len);
if (written != len) {
FATAL() << "Failed to write gdb command file";
}
stringstream procfile;
procfile << "/proc/" << getpid() << "/fd/" << fd;
return procfile.str();
}
static string to_string(const vector<string>& args) {
stringstream ss;
for (auto& a : args) {
ss << "'" << a << "' ";
}
return ss.str();
}
static bool needs_target(const string& option) {
return !strncmp(option.c_str(), "continue", option.size());
}
/**
* Exec gdb using the params that were written to
* |params_pipe_fd|. Optionally, pre-define in the gdb client the set
* of macros defined in |macros| if nonnull.
*/
void GdbServer::launch_gdb(ScopedFd& params_pipe_fd,
const string& gdb_binary_file_path,
const vector<string>& gdb_options,
bool serve_files) {
auto macros = gdb_rr_macros();
string gdb_command_file = create_gdb_command_file(macros);
DebuggerParams params;
ssize_t nread;
while (true) {
nread = read(params_pipe_fd, &params, sizeof(params));
if (nread == 0) {
// pipe was closed. Probably rr failed/died.
return;
}
if (nread != -1 || errno != EINTR) {
break;
}
}
DEBUG_ASSERT(nread == sizeof(params));
vector<string> args;
args.push_back(gdb_binary_file_path);
push_default_gdb_options(args, serve_files);
args.push_back("-x");
args.push_back(gdb_command_file);
bool did_set_remote = false;
for (size_t i = 0; i < gdb_options.size(); ++i) {
if (!did_set_remote && gdb_options[i] == "-ex" &&
i + 1 < gdb_options.size() && needs_target(gdb_options[i + 1])) {
push_target_remote_cmd(args, string(params.host), params.port);
did_set_remote = true;
}
args.push_back(gdb_options[i]);
}
if (!did_set_remote) {
push_target_remote_cmd(args, string(params.host), params.port);
}
args.push_back(params.exe_image);
vector<string> env = current_env();
env.push_back("GDB_UNDER_RR=1");
LOG(debug) << "launching " << to_string(args);
StringVectorToCharArray c_args(args);
StringVectorToCharArray c_env(env);
execvpe(gdb_binary_file_path.c_str(), c_args.get(), c_env.get());
CLEAN_FATAL() << "Failed to exec " << gdb_binary_file_path << ".";
}
void GdbServer::emergency_debug(Task* t) {
// See the comment in |guard_overshoot()| explaining why we do
// this. Unlike in that context though, we don't know if |t|
// overshot an internal breakpoint. If it did, cover that
// breakpoint up.
if (t->vm()) {
t->vm()->remove_all_breakpoints();
}
// Don't launch a debugger on fatal errors; the user is most
// likely already in a debugger, and wouldn't be able to
// control another session. Instead, launch a new GdbServer and wait for
// the user to connect from another window.
GdbConnection::Features features;
// Don't advertise reverse_execution to gdb because a) it won't work and
// b) some gdb versions will fail if the user doesn't turn off async
// mode (and we don't want to require users to do that)
features.reverse_execution = false;
unsigned short port = t->tid;
ScopedFd listen_fd = open_socket(localhost_addr.c_str(), &port, PROBE_PORT);
dump_rr_stack();
char* test_monitor_pid = getenv("RUNNING_UNDER_TEST_MONITOR");
if (test_monitor_pid) {
pid_t pid = atoi(test_monitor_pid);
// Tell test-monitor to wake up and take a snapshot. It will also
// connect the emergency debugger so let that happen.
FILE* gdb_cmd = fopen("gdb_cmd", "w");
if (gdb_cmd) {
print_debugger_launch_command(t, localhost_addr, port, false, "gdb",
gdb_cmd);
fclose(gdb_cmd);
}
kill(pid, SIGURG);
} else {
fputs("Launch gdb with\n ", stderr);
print_debugger_launch_command(t, localhost_addr, port, false, "gdb",
stderr);
}
unique_ptr<GdbConnection> dbg = await_connection(t, listen_fd, features);
GdbServer(dbg, t).process_debugger_requests();
}
string GdbServer::init_script() { return gdb_rr_macros(); }
static ScopedFd generate_fake_proc_maps(Task* t) {
TempFile file = create_temporary_file("rr-fake-proc-maps-XXXXXX");
unlink(file.name.c_str());
int fd = dup(file.fd);
if (fd < 0) {
FATAL() << "Cannot dup";
}
FILE* f = fdopen(fd, "w");
int addr_min_width = word_size(t->arch()) == 8 ? 10 : 8;
for (AddressSpace::Maps::iterator it = t->vm()->maps().begin();
it != t->vm()->maps().end(); ++it) {
// If this is the mapping just before the rr page and it's still librrpage,
// merge this mapping with the subsequent one. We'd like gdb to treat
// librrpage as the vdso, but it'll only do so if the entire vdso is one
// mapping.
auto m = *it;
uintptr_t map_end = (long long)m.recorded_map.end().as_int();
if (m.recorded_map.end() == t->vm()->rr_page_start()) {
auto it2 = it;
if (++it2 != t->vm()->maps().end()) {
auto m2 = *it2;
if (m2.flags & AddressSpace::Mapping::IS_RR_PAGE) {
// Extend this mapping
map_end += PRELOAD_LIBRARY_PAGE_SIZE;
// Skip the rr page
++it;
}
}
}
int len =
fprintf(f, "%0*llx-%0*llx %s%s%s%s %08llx %02x:%02x %lld",
addr_min_width, (long long)m.recorded_map.start().as_int(),
addr_min_width, (long long)map_end,
(m.recorded_map.prot() & PROT_READ) ? "r" : "-",
(m.recorded_map.prot() & PROT_WRITE) ? "w" : "-",
(m.recorded_map.prot() & PROT_EXEC) ? "x" : "-",
(m.recorded_map.flags() & MAP_SHARED) ? "s" : "p",
(long long)m.recorded_map.file_offset_bytes(),
major(m.recorded_map.device()), minor(m.recorded_map.device()),
(long long)m.recorded_map.inode());
while (len < 72) {
fputc(' ', f);
++len;
}
fputc(' ', f);
string name;
const string& fsname = m.recorded_map.fsname();
for (size_t i = 0; i < fsname.size(); ++i) {
if (fsname[i] == '\n') {
name.append("\\012");
} else {
name.push_back(fsname[i]);
}
}
fputs(name.c_str(), f);
fputc('\n', f);
}
if (ferror(f) || fclose(f)) {
FATAL() << "Can't write";
}
return std::move(file.fd);
}
static bool is_ld_mapping(string map_name) {
char ld_start[] = "ld-";
size_t matchpos = map_name.find_last_of('/');
string fname = map_name.substr(matchpos == string::npos ? 0 : matchpos + 1);
return memcmp(fname.c_str(), ld_start,
sizeof(ld_start)-1) == 0;
}
static bool is_likely_interp(string fsname) {
#ifdef __aarch64__
return fsname == "/lib/ld-linux-aarch64.so.1";
#else
return fsname == "/lib64/ld-linux-x86-64.so.2" || fsname == "/lib/ld-linux.so.2";
#endif
}
static remote_ptr<void> base_addr_from_rendezvous(Task* t, string fname)
{
remote_ptr<void> interpreter_base = t->vm()->saved_interpreter_base();
if (!interpreter_base || !t->vm()->has_mapping(interpreter_base)) {
return nullptr;
}
string ld_path = t->vm()->saved_ld_path();
if (ld_path.length() == 0) {
FATAL() << "Failed to retrieve interpreter name with interpreter_base=" << interpreter_base;
}
ScopedFd ld(ld_path.c_str(), O_RDONLY);
if (ld < 0) {
FATAL() << "Open failed: " << ld_path;
}
ElfFileReader reader(ld);
auto syms = reader.read_symbols(".dynsym", ".dynstr");
static const char r_debug[] = "_r_debug";
bool found = false;
uintptr_t r_debug_offset = 0;
for (size_t i = 0; i < syms.size(); ++i) {
if (!syms.is_name(i, r_debug)) {
continue;
}
r_debug_offset = syms.addr(i);
found = true;
}
if (!found) {
return nullptr;
}
bool ok = true;
remote_ptr<NativeArch::r_debug> r_debug_remote = interpreter_base.as_int()+r_debug_offset;
remote_ptr<NativeArch::link_map> link_map = t->read_mem(REMOTE_PTR_FIELD(r_debug_remote, r_map), &ok);
while (ok && link_map != nullptr) {
if (fname == t->read_c_str(t->read_mem(REMOTE_PTR_FIELD(link_map, l_name), &ok), &ok)) {
remote_ptr<void> result = t->read_mem(REMOTE_PTR_FIELD(link_map, l_addr), &ok);
return ok ? result : nullptr;
}
link_map = t->read_mem(REMOTE_PTR_FIELD(link_map, l_next), &ok);
}
return nullptr;
}
int GdbServer::open_file(Session& session, Task* continue_task, const std::string& file_name) {
// XXX should we require file_scope_pid == 0 here?
ScopedFd contents;
if (file_name.empty()) {
return -1;
}
LOG(debug) << "Trying to open " << file_name;
if (file_name.substr(0, 6) == "/proc/") {
char* tid_end;
long tid = strtol(file_name.c_str() + 6, &tid_end, 10);
if (*tid_end != '/') {
return -1;
}
if (!strncmp(tid_end, "/task/", 6)) {
tid = strtol(tid_end + 6, &tid_end, 10);
if (*tid_end != '/') {
return -1;
}
}
if (tid != (pid_t)tid) {
return -1;
}
Task* t = session.find_task(tid);
if (!t) {
return -1;
}
if (!strcmp(tid_end, "/maps")) {
contents = generate_fake_proc_maps(t);
} else {
return -1;
}
} else if (file_name == continue_task->vm()->interp_name()) {
remote_ptr<void> interp_base = continue_task->vm()->interp_base();
auto m = continue_task->vm()->mapping_of(interp_base);
LOG(debug) << "Found dynamic linker as memory mapping " << m.recorded_map;
int ret_fd = 0;
while (files.find(ret_fd) != files.end() ||
memory_files.find(ret_fd) != memory_files.end()) {
++ret_fd;
}
memory_files.insert(make_pair(ret_fd, FileId(m.recorded_map)));
return ret_fd;
} else {
// See if we can find the file by serving one of our mappings
std::string normalized_file_name = file_name;
normalize_file_name(normalized_file_name);
for (const auto& m : continue_task->vm()->maps()) {
// The dynamic linker is generally a symlink that is resolved by the
// kernel when the process image gets loaded. We add a special case to
// substitute the correct mapping, so gdb can find the dynamic linker
// rendezvous structures.
// Use our old hack for ld from before we read PT_INTERP for backwards
// compat with older traces.
if (m.recorded_map.fsname().compare(0, normalized_file_name.length(), normalized_file_name) == 0
|| m.map.fsname().compare(0, normalized_file_name.length(), normalized_file_name) == 0
|| (is_ld_mapping(m.recorded_map.fsname()) &&
is_likely_interp(normalized_file_name)))
{
int ret_fd = 0;
while (files.find(ret_fd) != files.end() ||
memory_files.find(ret_fd) != memory_files.end()) {
++ret_fd;
}
LOG(debug) << "Found as memory mapping " << m.recorded_map;
memory_files.insert(make_pair(ret_fd, FileId(m.recorded_map)));
return ret_fd;
}
}
// Last ditch attempt: Dig through the tracee's libc rendezvous struct to
// see if we can find this file by a different name (e.g. if it was opened
// via symlink)
remote_ptr<void> base = base_addr_from_rendezvous(continue_task, file_name);
if (base != nullptr && continue_task->vm()->has_mapping(base)) {
int ret_fd = 0;
while (files.find(ret_fd) != files.end() ||
memory_files.find(ret_fd) != memory_files.end()) {
++ret_fd;
}
memory_files.insert(make_pair(ret_fd, FileId(continue_task->vm()->mapping_of(base).recorded_map)));
return ret_fd;
}
LOG(debug) << "... not found";
return -1;
}
int ret_fd = 0;
while (files.find(ret_fd) != files.end()) {
++ret_fd;
}
files.insert(make_pair(ret_fd, std::move(contents)));
return ret_fd;
}
} // namespace rr