| /* -*- Mode: C++; tab-width: 8; c-basic-offset: 2; indent-tabs-mode: nil; -*- */ |
| |
| #ifndef RR_GDB_CONNECTION_H_ |
| #define RR_GDB_CONNECTION_H_ |
| |
| #include <stddef.h> |
| #include <sys/types.h> |
| |
| #include <memory> |
| #include <ostream> |
| #include <string> |
| #include <unordered_set> |
| #include <vector> |
| |
| #include "GdbRegister.h" |
| #include "Registers.h" |
| #include "ReplaySession.h" |
| #include "ReplayTimeline.h" |
| #include "core.h" |
| |
| namespace rr { |
| |
| /** |
| * Descriptor for task. Note: on linux, we can uniquely identify any thread |
| * by its |tid| (in rr's pid namespace). |
| */ |
| struct GdbThreadId { |
| GdbThreadId(pid_t pid = -1, pid_t tid = -1) : pid(pid), tid(tid) {} |
| |
| pid_t pid; |
| pid_t tid; |
| |
| bool operator==(const GdbThreadId& o) const { |
| return pid == o.pid && tid == o.tid; |
| } |
| |
| static const GdbThreadId ANY; |
| static const GdbThreadId ALL; |
| }; |
| |
| inline std::ostream& operator<<(std::ostream& o, const GdbThreadId& t) { |
| o << t.pid << "." << t.tid; |
| return o; |
| } |
| |
| /** |
| * Represents a possibly-undefined register |name|. |size| indicates how |
| * many bytes of |value| are valid, if any. |
| */ |
| struct GdbRegisterValue { |
| enum { MAX_SIZE = Registers::MAX_SIZE }; |
| GdbRegister name; |
| union { |
| uint8_t value[MAX_SIZE]; |
| uint8_t value1; |
| uint16_t value2; |
| uint32_t value4; |
| uint64_t value8; |
| }; |
| size_t size; |
| bool defined; |
| }; |
| |
| enum GdbRequestType { |
| DREQ_NONE = 0, |
| |
| /* None of these requests have parameters. */ |
| DREQ_GET_CURRENT_THREAD, |
| DREQ_GET_OFFSETS, |
| DREQ_GET_REGS, |
| DREQ_GET_STOP_REASON, |
| DREQ_GET_THREAD_LIST, |
| DREQ_INTERRUPT, |
| DREQ_DETACH, |
| |
| /* These use params.target. */ |
| DREQ_GET_AUXV, |
| DREQ_GET_EXEC_FILE, |
| DREQ_GET_IS_THREAD_ALIVE, |
| DREQ_GET_THREAD_EXTRA_INFO, |
| DREQ_SET_CONTINUE_THREAD, |
| DREQ_SET_QUERY_THREAD, |
| // TLS lookup, uses params.target and params.tls. |
| DREQ_TLS, |
| // gdb wants to write back siginfo_t to a tracee. More |
| // importantly, this packet arrives before an experiment |
| // session for a |call foo()| is about to be torn down. |
| // |
| // TODO: actual interface NYI. |
| DREQ_WRITE_SIGINFO, |
| |
| /* These use params.mem. */ |
| DREQ_GET_MEM, |
| DREQ_SET_MEM, |
| // gdb wants to read the current siginfo_t for a stopped |
| // tracee. More importantly, this packet arrives at the very |
| // beginning of a |call foo()| experiment. |
| // |
| // Uses .mem for offset/len. |
| DREQ_READ_SIGINFO, |
| DREQ_SEARCH_MEM, |
| DREQ_MEM_FIRST = DREQ_GET_MEM, |
| DREQ_MEM_LAST = DREQ_SEARCH_MEM, |
| |
| DREQ_REMOVE_SW_BREAK, |
| DREQ_REMOVE_HW_BREAK, |
| DREQ_REMOVE_WR_WATCH, |
| DREQ_REMOVE_RD_WATCH, |
| DREQ_REMOVE_RDWR_WATCH, |
| DREQ_SET_SW_BREAK, |
| DREQ_SET_HW_BREAK, |
| DREQ_SET_WR_WATCH, |
| DREQ_SET_RD_WATCH, |
| DREQ_SET_RDWR_WATCH, |
| DREQ_WATCH_FIRST = DREQ_REMOVE_SW_BREAK, |
| DREQ_WATCH_LAST = DREQ_SET_RDWR_WATCH, |
| |
| /* Use params.reg. */ |
| DREQ_GET_REG, |
| DREQ_SET_REG, |
| DREQ_REG_FIRST = DREQ_GET_REG, |
| DREQ_REG_LAST = DREQ_SET_REG, |
| |
| /* Use params.cont. */ |
| DREQ_CONT, |
| |
| /* gdb host detaching from stub. No parameters. */ |
| |
| /* Uses params.restart. */ |
| DREQ_RESTART, |
| |
| /* Uses params.text. */ |
| DREQ_RR_CMD, |
| |
| // qSymbol packet, uses params.sym. |
| DREQ_QSYMBOL, |
| |
| // vFile:setfs packet, uses params.file_setfs. |
| DREQ_FILE_SETFS, |
| // vFile:open packet, uses params.file_open. |
| DREQ_FILE_OPEN, |
| // vFile:pread packet, uses params.file_pread. |
| DREQ_FILE_PREAD, |
| // vFile:close packet, uses params.file_close. |
| DREQ_FILE_CLOSE, |
| }; |
| |
| enum GdbRestartType { |
| RESTART_FROM_PREVIOUS, |
| RESTART_FROM_EVENT, |
| RESTART_FROM_CHECKPOINT, |
| RESTART_FROM_TICKS |
| }; |
| |
| enum GdbActionType { ACTION_CONTINUE, ACTION_STEP }; |
| |
| struct GdbContAction { |
| GdbContAction(GdbActionType type = ACTION_CONTINUE, |
| const GdbThreadId& target = GdbThreadId::ANY, |
| int signal_to_deliver = 0) |
| : type(type), target(target), signal_to_deliver(signal_to_deliver) {} |
| GdbActionType type; |
| GdbThreadId target; |
| int signal_to_deliver; |
| }; |
| |
| /** |
| * These requests are made by the debugger host and honored in proxy |
| * by rr, the target. |
| */ |
| struct GdbRequest { |
| GdbRequest(GdbRequestType type = DREQ_NONE) |
| : type(type), suppress_debugger_stop(false) {} |
| GdbRequest(const GdbRequest& other) |
| : type(other.type), |
| target(other.target), |
| suppress_debugger_stop(other.suppress_debugger_stop), |
| mem_(other.mem_), |
| watch_(other.watch_), |
| reg_(other.reg_), |
| restart_(other.restart_), |
| cont_(other.cont_), |
| text_(other.text_), |
| tls_(other.tls_), |
| sym_(other.sym_), |
| file_setfs_(other.file_setfs_), |
| file_open_(other.file_open_), |
| file_pread_(other.file_pread_), |
| file_close_(other.file_close_) {} |
| GdbRequest& operator=(const GdbRequest& other) { |
| this->~GdbRequest(); |
| new (this) GdbRequest(other); |
| return *this; |
| } |
| |
| const GdbRequestType type; |
| GdbThreadId target; |
| bool suppress_debugger_stop; |
| |
| struct Mem { |
| uintptr_t addr; |
| size_t len; |
| // For SET_MEM requests, the |len| raw bytes that are to be written. |
| // For SEARCH_MEM requests, the bytes to search for. |
| std::vector<uint8_t> data; |
| } mem_; |
| struct Watch { |
| uintptr_t addr; |
| int kind; |
| std::vector<std::vector<uint8_t>> conditions; |
| } watch_; |
| GdbRegisterValue reg_; |
| struct Restart { |
| int64_t param; |
| std::string param_str; |
| GdbRestartType type; |
| } restart_; |
| struct Cont { |
| RunDirection run_direction; |
| std::vector<GdbContAction> actions; |
| } cont_; |
| std::string text_; |
| struct Tls { |
| size_t offset; |
| remote_ptr<void> load_module; |
| } tls_; |
| struct Symbol { |
| bool has_address; |
| remote_ptr<void> address; |
| std::string name; |
| } sym_; |
| struct FileSetfs { |
| pid_t pid; |
| } file_setfs_; |
| struct FileOpen { |
| std::string file_name; |
| // In system format, not gdb's format |
| int flags; |
| int mode; |
| } file_open_; |
| struct FilePread { |
| int fd; |
| size_t size; |
| uint64_t offset; |
| } file_pread_; |
| struct FileClose { |
| int fd; |
| } file_close_; |
| |
| Mem& mem() { |
| DEBUG_ASSERT(type >= DREQ_MEM_FIRST && type <= DREQ_MEM_LAST); |
| return mem_; |
| } |
| const Mem& mem() const { |
| DEBUG_ASSERT(type >= DREQ_MEM_FIRST && type <= DREQ_MEM_LAST); |
| return mem_; |
| } |
| Watch& watch() { |
| DEBUG_ASSERT(type >= DREQ_WATCH_FIRST && type <= DREQ_WATCH_LAST); |
| return watch_; |
| } |
| const Watch& watch() const { |
| DEBUG_ASSERT(type >= DREQ_WATCH_FIRST && type <= DREQ_WATCH_LAST); |
| return watch_; |
| } |
| GdbRegisterValue& reg() { |
| DEBUG_ASSERT(type >= DREQ_REG_FIRST && type <= DREQ_REG_LAST); |
| return reg_; |
| } |
| const GdbRegisterValue& reg() const { |
| DEBUG_ASSERT(type >= DREQ_REG_FIRST && type <= DREQ_REG_LAST); |
| return reg_; |
| } |
| Restart& restart() { |
| DEBUG_ASSERT(type == DREQ_RESTART); |
| return restart_; |
| } |
| const Restart& restart() const { |
| DEBUG_ASSERT(type == DREQ_RESTART); |
| return restart_; |
| } |
| Cont& cont() { |
| DEBUG_ASSERT(type == DREQ_CONT); |
| return cont_; |
| } |
| const Cont& cont() const { |
| DEBUG_ASSERT(type == DREQ_CONT); |
| return cont_; |
| } |
| const std::string& text() const { |
| DEBUG_ASSERT(type == DREQ_RR_CMD); |
| return text_; |
| } |
| Tls& tls() { |
| DEBUG_ASSERT(type == DREQ_TLS); |
| return tls_; |
| } |
| const Tls& tls() const { |
| DEBUG_ASSERT(type == DREQ_TLS); |
| return tls_; |
| } |
| Symbol& sym() { |
| DEBUG_ASSERT(type == DREQ_QSYMBOL); |
| return sym_; |
| } |
| const Symbol& sym() const { |
| DEBUG_ASSERT(type == DREQ_QSYMBOL); |
| return sym_; |
| } |
| FileSetfs& file_setfs() { |
| DEBUG_ASSERT(type == DREQ_FILE_SETFS); |
| return file_setfs_; |
| } |
| const FileSetfs& file_setfs() const { |
| DEBUG_ASSERT(type == DREQ_FILE_SETFS); |
| return file_setfs_; |
| } |
| FileOpen& file_open() { |
| DEBUG_ASSERT(type == DREQ_FILE_OPEN); |
| return file_open_; |
| } |
| const FileOpen& file_open() const { |
| DEBUG_ASSERT(type == DREQ_FILE_OPEN); |
| return file_open_; |
| } |
| FilePread& file_pread() { |
| DEBUG_ASSERT(type == DREQ_FILE_PREAD); |
| return file_pread_; |
| } |
| const FilePread& file_pread() const { |
| DEBUG_ASSERT(type == DREQ_FILE_PREAD); |
| return file_pread_; |
| } |
| FileClose& file_close() { |
| DEBUG_ASSERT(type == DREQ_FILE_CLOSE); |
| return file_close_; |
| } |
| const FileClose& file_close() const { |
| DEBUG_ASSERT(type == DREQ_FILE_CLOSE); |
| return file_close_; |
| } |
| |
| /** |
| * Return nonzero if this requires that program execution be resumed |
| * in some way. |
| */ |
| bool is_resume_request() const { return type == DREQ_CONT; } |
| }; |
| |
| /** |
| * This struct wraps up the state of the gdb protocol, so that we can |
| * offer a (mostly) stateless interface to clients. |
| */ |
| class GdbConnection { |
| public: |
| struct Features { |
| Features() : reverse_execution(true) {} |
| bool reverse_execution; |
| }; |
| |
| /** |
| * Call this when the target of |req| is needed to fulfill the |
| * request, but the target is dead. This situation is a symptom of a |
| * gdb or rr bug. |
| */ |
| void notify_no_such_thread(const GdbRequest& req); |
| |
| /** |
| * Finish a DREQ_RESTART request. Should be invoked after replay |
| * restarts and prior GdbConnection has been restored. |
| */ |
| void notify_restart(); |
| |
| /** |
| * Return the current request made by the debugger host, that needs to |
| * be satisfied. This function will block until either there's a |
| * debugger host request that needs a response, or until a request is |
| * made to resume execution of the target. In the latter case, |
| * calling this function multiple times will return an appropriate |
| * resume request each time (see above). |
| * |
| * The target should peek at the debugger request in between execution |
| * steps. A new request may need to be serviced. |
| */ |
| GdbRequest get_request(); |
| |
| /** |
| * Notify the host that this process has exited with |code|. |
| */ |
| void notify_exit_code(int code); |
| |
| /** |
| * Notify the host that this process has exited from |sig|. |
| */ |
| void notify_exit_signal(int sig); |
| |
| /** |
| * Notify the host that a resume request has "finished", i.e., the |
| * target has stopped executing for some reason. |sig| is the signal |
| * that stopped execution, or 0 if execution stopped otherwise. |
| */ |
| void notify_stop(GdbThreadId which, int sig, const char *reason=nullptr); |
| |
| /** Notify the debugger that a restart request failed. */ |
| void notify_restart_failed(); |
| |
| /** |
| * Tell the host that |thread| is the current thread. |
| */ |
| void reply_get_current_thread(GdbThreadId thread); |
| |
| /** |
| * Reply with the target thread's |auxv| pairs. |auxv.empty()| |
| * if there was an error reading the auxiliary vector. |
| */ |
| void reply_get_auxv(const std::vector<uint8_t>& auxv); |
| |
| /** |
| * Reply with the target thread's executable file name |
| */ |
| void reply_get_exec_file(const std::string& exec_file); |
| |
| /** |
| * |alive| is true if the requested thread is alive, false if dead. |
| */ |
| void reply_get_is_thread_alive(bool alive); |
| |
| /** |
| * |info| is a string containing data about the request target that |
| * might be relevant to the debugger user. |
| */ |
| void reply_get_thread_extra_info(const std::string& info); |
| |
| /** |
| * |ok| is true if req->target can be selected, false otherwise. |
| */ |
| void reply_select_thread(bool ok); |
| |
| /** |
| * The first |mem.size()| bytes of the request were read into |mem|. |
| * |mem.size()| must be less than or equal to the length of the request. |
| */ |
| void reply_get_mem(const std::vector<uint8_t>& mem); |
| |
| /** |
| * |ok| is true if a SET_MEM request succeeded, false otherwise. This |
| * function *must* be called whenever a SET_MEM request is made, |
| * regardless of success/failure or special interpretation. |
| */ |
| void reply_set_mem(bool ok); |
| |
| /** |
| * Reply to the DREQ_SEARCH_MEM request. |
| * |found| is true if we found the searched-for bytes starting at address |
| * |addr|. |
| */ |
| void reply_search_mem(bool found, remote_ptr<void> addr); |
| |
| /** |
| * Reply to the DREQ_GET_OFFSETS request. |
| */ |
| void reply_get_offsets(/* TODO */); |
| |
| /** |
| * Send |value| back to the debugger host. |value| may be undefined. |
| */ |
| void reply_get_reg(const GdbRegisterValue& value); |
| |
| /** |
| * Send |file| back to the debugger host. |file| may contain |
| * undefined register values. |
| */ |
| void reply_get_regs(const std::vector<GdbRegisterValue>& file); |
| |
| /** |
| * Pass |ok = true| iff the requested register was successfully set. |
| */ |
| void reply_set_reg(bool ok); |
| |
| /** |
| * Reply to the DREQ_GET_STOP_REASON request. |
| */ |
| void reply_get_stop_reason(GdbThreadId which, int sig); |
| |
| /** |
| * |threads| contains the list of live threads, of which there are |
| * |len|. |
| */ |
| void reply_get_thread_list(const std::vector<GdbThreadId>& threads); |
| |
| /** |
| * |ok| is true if the request was successfully applied, false if |
| * not. |
| */ |
| void reply_watchpoint_request(bool ok); |
| |
| /** |
| * DREQ_DETACH was processed. |
| * |
| * There's no functional reason to reply to the detach request. |
| * However, some versions of gdb expect a response and time out |
| * awaiting it, wasting developer time. |
| */ |
| void reply_detach(); |
| |
| /** |
| * Pass the siginfo_t and its size (as requested by the debugger) in |
| * |si_bytes| and |num_bytes| if successfully read. Otherwise pass |
| * |si_bytes = nullptr|. |
| */ |
| void reply_read_siginfo(const std::vector<uint8_t>& si_bytes); |
| /** |
| * Not yet implemented, but call this after a WRITE_SIGINFO request |
| * anyway. |
| */ |
| void reply_write_siginfo(/* TODO*/); |
| |
| /** |
| * Send a manual text response to a rr cmd (maintenance) packet. |
| */ |
| void reply_rr_cmd(const std::string& text); |
| |
| /** |
| * Send a qSymbol response to gdb, requesting the address of the |
| * symbol |name|. |
| */ |
| void send_qsymbol(const std::string& name); |
| |
| /** |
| * The "all done" response to a qSymbol packet from gdb. |
| */ |
| void qsymbols_finished(); |
| |
| /** |
| * Respond to a qGetTLSAddr packet. If |ok| is true, then respond |
| * with |address|. If |ok| is false, respond with an error. |
| */ |
| void reply_tls_addr(bool ok, remote_ptr<void> address); |
| |
| /** |
| * Respond to a vFile:setfs |
| */ |
| void reply_setfs(int err); |
| /** |
| * Respond to a vFile:open |
| */ |
| void reply_open(int fd, int err); |
| /** |
| * Respond to a vFile:pread |
| */ |
| void reply_pread(const uint8_t* bytes, ssize_t len, int err); |
| /** |
| * Respond to a vFile:close |
| */ |
| void reply_close(int err); |
| |
| /** |
| * Create a checkpoint of the given Session with the given id. Delete the |
| * existing checkpoint with that id if there is one. |
| */ |
| void created_checkpoint(ReplaySession::shr_ptr& checkpoint, |
| int checkpoint_id); |
| |
| /** |
| * Delete the checkpoint with the given id. Silently fail if the checkpoint |
| * does not exist. |
| */ |
| void delete_checkpoint(int checkpoint_id); |
| |
| /** |
| * Get the checkpoint with the given id. Return null if not found. |
| */ |
| ReplaySession::shr_ptr get_checkpoint(int checkpoint_id); |
| |
| /** |
| * Return true if there's a new packet to be read/process (whether |
| * incomplete or not), and false if there isn't one. |
| */ |
| bool sniff_packet(); |
| |
| const Features& features() { return features_; } |
| |
| enum { |
| CPU_X86_64 = 0x1, |
| CPU_AVX = 0x2, |
| CPU_AARCH64 = 0x4, |
| CPU_PKU = 0x8 |
| }; |
| void set_cpu_features(uint32_t features) { cpu_features_ = features; } |
| uint32_t cpu_features() const { return cpu_features_; } |
| |
| GdbConnection(pid_t tgid, const Features& features); |
| |
| /** |
| * Wait for a debugger client to connect to |dbg|'s socket. Blocks |
| * indefinitely. |
| */ |
| void await_debugger(ScopedFd& listen_fd); |
| |
| /** |
| * Returns false if the connection has been closed |
| */ |
| bool is_connection_alive(); |
| |
| bool hwbreak_supported() { return hwbreak_supported_; } |
| bool swbreak_supported() { return swbreak_supported_; } |
| |
| bool is_pass_signal(int sig); |
| |
| private: |
| /** |
| * read() incoming data exactly one time, successfully. May block. |
| */ |
| void read_data_once(); |
| /** |
| * Send all pending output to gdb. May block. |
| */ |
| void write_flush(); |
| void write_data_raw(const uint8_t* data, ssize_t len); |
| void write_hex(unsigned long hex); |
| void write_packet_bytes(const uint8_t* data, size_t num_bytes); |
| void write_packet(const char* data); |
| void write_binary_packet(const char* pfx, const uint8_t* data, |
| ssize_t num_bytes); |
| void write_hex_bytes_packet(const char* prefix, const uint8_t* bytes, |
| size_t len); |
| void write_hex_bytes_packet(const uint8_t* bytes, size_t len); |
| void write_xfer_response(const void* data, size_t size, uint64_t offset, |
| uint64_t len); |
| /** |
| * Consume bytes in the input buffer until start-of-packet ('$') or |
| * the interrupt character is seen. Does not block. Return true if |
| * seen, false if not. |
| */ |
| bool skip_to_packet_start(); |
| /** |
| * Block until the sequence of bytes |
| * |
| * "[^$]*\$[^#]*#.*" |
| * |
| * has been read from the client fd. This is one (or more) gdb |
| * packet(s). |
| */ |
| void read_packet(); |
| /** |
| * Return true if we need to do something in a debugger request, |
| * false if we already handled the packet internally. |
| */ |
| bool xfer(const char* name, char* args); |
| /** |
| * Return true if we need to do something in a debugger request, |
| * false if we already handled the packet internally. |
| */ |
| bool query(char* payload); |
| /** |
| * Return true if we need to do something in a debugger request, |
| * false if we already handled the packet internally. |
| */ |
| bool set_var(char* payload); |
| /** |
| * Return true if we need to do something in a debugger request, |
| * false if we already handled the packet internally. |
| */ |
| bool process_vpacket(char* payload); |
| /** |
| * Return true if we need to do something in a debugger request, |
| * false if we already handled the packet internally. |
| */ |
| bool process_bpacket(char* payload); |
| /** |
| * Return true if we need to do something in a debugger request, |
| * false if we already handled the packet internally. |
| */ |
| bool process_packet(); |
| void consume_request(); |
| void send_stop_reply_packet(GdbThreadId thread, int sig, |
| const char *reason); |
| void send_file_error_reply(int system_errno); |
| |
| // Current request to be processed. |
| GdbRequest req; |
| // Thread to be resumed. |
| GdbThreadId resume_thread; |
| // Thread for get/set requests. |
| GdbThreadId query_thread; |
| // gdb and rr don't work well together in multi-process and |
| // multi-exe-image debugging scenarios, so we pretend only |
| // this thread group exists when interfacing with gdb |
| pid_t tgid; |
| uint32_t cpu_features_; |
| // true when "no-ack mode" enabled, in which we don't have |
| // to send ack packets back to gdb. This is a huge perf win. |
| bool no_ack; |
| // contains signals (gdb not native) which should be passed directly to the |
| // debuggee without gdb being informed, speeding up |
| // reverse execution |
| std::unordered_set<int> pass_signals; |
| ScopedFd sock_fd; |
| std::vector<uint8_t> inbuf; /* buffered input from gdb */ |
| size_t packetend; /* index of '#' character */ |
| std::vector<uint8_t> outbuf; /* buffered output for gdb */ |
| Features features_; |
| bool connection_alive_; |
| bool multiprocess_supported_; // client supports multiprocess extension |
| bool hwbreak_supported_; // client supports hwbreak extension |
| bool swbreak_supported_; // client supports swbreak extension |
| }; |
| |
| } // namespace rr |
| |
| #endif /* RR_GDB_CONNECTION_H_ */ |