mirror of
https://github.com/JakeHillion/object-introspection.git
synced 2024-11-09 21:24:14 +00:00
9e2b48d713
Summary: Changes jitlog to use a memfd, an anonymous in memory file descriptor, rather than a file on disk. Also clean up this fd at the end of an OID run rather than leaving it in the hope it's valid next time. A previous attempt to land this used a `char*` from the OID process space in the remote target syscall. Somehow this works with our integration test target, but not globally. Changed to use the previous behaviour of putting the syscall arg in the empty text segment. In doing this I noticed that the text segment wouldn't be initialised at this point on a fresh process, so we were copying into effectively an uninitialised address. Move the jit log fd setup to after the segment setup accordingly. Test plan: - CI - Tested on an integration test target as before. Works. - Created a new target that definitely doesn't have this string in (simple for loop). Failed before, works now. Example: ```sh $ OID_TEST_ARGS='-fjit-logging' stest OidIntegration.simple_struct ... I1121 02:57:36.136890 500897 OIDebugger.cpp:269] Outputting JIT logs: I1121 02:57:36.136896 500897 OIDebugger.cpp:272] JITLOG: SimpleStruct @00007ffc639be180 I1121 02:57:36.136899 500897 OIDebugger.cpp:272] JITLOG: a @00007ffc639be180 I1121 02:57:36.136901 500897 OIDebugger.cpp:272] JITLOG: obj @00007ffc639be180 I1121 02:57:36.136904 500897 OIDebugger.cpp:272] JITLOG: b @00007ffc639be184 I1121 02:57:36.136905 500897 OIDebugger.cpp:272] JITLOG: obj @00007ffc639be184 I1121 02:57:36.136907 500897 OIDebugger.cpp:272] JITLOG: c @00007ffc639be188 I1121 02:57:36.136909 500897 OIDebugger.cpp:272] JITLOG: obj @00007ffc639be188 I1121 02:57:36.136911 500897 OIDebugger.cpp:278] Finished outputting JIT logs. ... ```
3020 lines
99 KiB
C++
3020 lines
99 KiB
C++
/*
|
|
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
*
|
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
* you may not use this file except in compliance with the License.
|
|
* You may obtain a copy of the License at
|
|
*
|
|
* http://www.apache.org/licenses/LICENSE-2.0
|
|
*
|
|
* Unless required by applicable law or agreed to in writing, software
|
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
* See the License for the specific language governing permissions and
|
|
* limitations under the License.
|
|
*/
|
|
#include "oi/OIDebugger.h"
|
|
|
|
#include <folly/Varint.h>
|
|
|
|
#include <algorithm>
|
|
#include <boost/algorithm/string/classification.hpp>
|
|
#include <boost/algorithm/string/join.hpp>
|
|
#include <boost/algorithm/string/split.hpp>
|
|
#include <boost/format.hpp>
|
|
#include <boost/property_tree/ini_parser.hpp>
|
|
#include <boost/property_tree/ptree.hpp>
|
|
#include <boost/scope_exit.hpp>
|
|
#include <cassert>
|
|
#include <cerrno>
|
|
#include <chrono>
|
|
#include <cstddef>
|
|
#include <cstdio>
|
|
#include <cstring>
|
|
#include <numeric>
|
|
#include <span>
|
|
|
|
extern "C" {
|
|
#include <fcntl.h>
|
|
#include <sys/mman.h>
|
|
#include <sys/ptrace.h>
|
|
#include <sys/uio.h>
|
|
#include <sys/user.h>
|
|
#include <sys/wait.h>
|
|
}
|
|
|
|
#include <glog/logging.h>
|
|
|
|
#include "oi/CodeGen.h"
|
|
#include "oi/Config.h"
|
|
#include "oi/ContainerInfo.h"
|
|
#include "oi/Headers.h"
|
|
#include "oi/Metrics.h"
|
|
#include "oi/OILexer.h"
|
|
#include "oi/PaddingHunter.h"
|
|
#include "oi/Portability.h"
|
|
#include "oi/Syscall.h"
|
|
#include "oi/type_graph/DrgnParser.h"
|
|
#include "oi/type_graph/TypeGraph.h"
|
|
|
|
#if OI_PORTABILITY_META_INTERNAL()
|
|
#include "object-introspection/internal/GobsService.h"
|
|
#endif
|
|
|
|
using namespace std;
|
|
|
|
namespace oi::detail {
|
|
|
|
constexpr int oidMagicId = 0x01DE8;
|
|
|
|
bool OIDebugger::isGlobalDataProbeEnabled(void) const {
|
|
return std::any_of(cbegin(pdata), cend(pdata), [](const auto& r) {
|
|
return r.type == "global";
|
|
});
|
|
}
|
|
|
|
bool OIDebugger::parseScript(std::istream& script) {
|
|
metrics::Tracing _("parse_script");
|
|
|
|
OIScanner scanner(&script);
|
|
OIParser parser(scanner, pdata);
|
|
|
|
#if 0
|
|
if (VLOG_IS_ON(1)) {
|
|
scanner.set_debug(1);
|
|
parser.set_debug_level(1);
|
|
}
|
|
#endif
|
|
|
|
if (parser.parse() != 0) {
|
|
return false;
|
|
}
|
|
|
|
if (pdata.numReqs() == 0) {
|
|
LOG(ERROR) << "No valid introspection data specified";
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
bool OIDebugger::patchFunctions(void) {
|
|
assert(pdata.numReqs() != 0);
|
|
metrics::Tracing _("patch_functions");
|
|
|
|
for (const auto& preq : pdata) {
|
|
VLOG(1) << "Type " << preq.type << " Func " << preq.func
|
|
<< " Args: " << boost::join(preq.args, ",");
|
|
|
|
if (preq.type == "global") {
|
|
/*
|
|
* processGlobal() - this function should do everything apart from
|
|
* continue the target thread.
|
|
*/
|
|
processGlobal(preq.func);
|
|
} else {
|
|
if (!functionPatch(preq)) {
|
|
LOG(ERROR) << "Failed to patch function";
|
|
return false;
|
|
}
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
/*
|
|
* Single step an instruction in the target process 'pid' and leave the target
|
|
* thread stopped. Returns the current rip.
|
|
*/
|
|
uint64_t OIDebugger::singlestepInst(pid_t pid, struct user_regs_struct& regs) {
|
|
int status = 0;
|
|
|
|
metrics::Tracing _("single_step_inst");
|
|
|
|
if (ptrace(PTRACE_SINGLESTEP, pid, 0, 0) == -1) {
|
|
LOG(ERROR) << "Error in singlestep!";
|
|
return 0xdeadbeef;
|
|
}
|
|
|
|
/* Error check... */
|
|
waitpid(pid, &status, 0);
|
|
|
|
if (!WIFSTOPPED(status)) {
|
|
LOG(ERROR) << "process not stopped!";
|
|
}
|
|
|
|
errno = 0;
|
|
if (ptrace(PTRACE_GETREGS, pid, NULL, ®s) < 0) {
|
|
LOG(ERROR) << "Couldn't read registers: " << strerror(errno);
|
|
}
|
|
VLOG(1) << "rip after singlestep: " << std::hex << regs.rip;
|
|
|
|
return regs.rip;
|
|
}
|
|
|
|
void OIDebugger::dumpRegs(const char* text,
|
|
pid_t pid,
|
|
struct user_regs_struct* regs) {
|
|
VLOG(1) << "(" << text << ")"
|
|
<< " dumpRegs: pid: " << std::dec << pid << std::hex << " rip "
|
|
<< regs->rip << " rbp: " << regs->rbp << " rsp " << regs->rsp
|
|
<< " rdi " << regs->rdi << " rsi " << regs->rsi << " rdx "
|
|
<< regs->rdx << " rbx " << regs->rbx << " rcx " << regs->rcx << " r8 "
|
|
<< regs->r8 << " r9 " << regs->r9 << " r10 " << regs->r10 << " r11 "
|
|
<< regs->r11 << " r12 " << regs->r12 << " r13 " << regs->r13
|
|
<< " r14 " << regs->r14 << " r15 " << regs->r15 << " rax "
|
|
<< regs->rax << " orig_rax " << regs->orig_rax << " eflags "
|
|
<< regs->eflags;
|
|
}
|
|
|
|
/*
|
|
* singlestepFunc is a debugging aid for when times get desperate! It will
|
|
* single step the tgid specified in pid until it reached the address
|
|
* specified in 'real_end'. This means you'll get a register dump for
|
|
* every instruction which can be extremely useful for getting to those
|
|
* hard to reach places.
|
|
*
|
|
* Note that while this will single step a function nicely, it will also
|
|
* caue the target thread to spin forever and therefore fail the process
|
|
* because of the way we use breakpoint traps . It needs improving.
|
|
*/
|
|
bool OIDebugger::singleStepFunc(pid_t pid, uint64_t real_end) {
|
|
uint64_t addr = 0x0;
|
|
struct user_regs_struct regs {};
|
|
uint64_t prev = 0;
|
|
|
|
do {
|
|
prev = addr;
|
|
VLOG(1) << "singlestepFunc addr: " << std::hex << addr << " real_end "
|
|
<< real_end;
|
|
addr = singlestepInst(pid, regs);
|
|
dumpRegs("singlestep", pid, ®s);
|
|
} while (addr != 0xdeadbeef && addr != real_end && prev != addr);
|
|
|
|
return true;
|
|
}
|
|
|
|
bool OIDebugger::setupLogFile(void) {
|
|
// 1. Copy the anonymous file name into our text segment.
|
|
// 2. Open an anonymous memfd in the target with `memfd_create`.
|
|
// 3. Duplicate that fd to the debugger using `pidfd_getfd`.
|
|
// 4. Store the resulting fds in OIDebugger.
|
|
bool ret = true;
|
|
|
|
static const std::string kAnonFileName{"jit.log"};
|
|
|
|
// Copy the file name into the text segment so it can be referenced by the
|
|
// remote syscall. This will be overwritten later during relocation.
|
|
if (!writeTargetMemory((void*)(kAnonFileName.c_str()),
|
|
reinterpret_cast<void*>(segConfig.textSegBase),
|
|
kAnonFileName.length() + 1)) {
|
|
LOG(ERROR) << "Failed to write log file's name into target process";
|
|
return false;
|
|
}
|
|
|
|
auto traceeFd = remoteSyscall<MemfdCreate>(segConfig.textSegBase, 0);
|
|
if (!traceeFd.has_value()) {
|
|
LOG(ERROR) << "Failed to create memory log file";
|
|
return false;
|
|
}
|
|
logFds.traceeFd = *traceeFd;
|
|
|
|
auto traceePidFd = syscall(SYS_pidfd_open, traceePid, 0);
|
|
if (traceePidFd == -1) {
|
|
PLOG(ERROR) << "Failed to open child pidfd";
|
|
return false;
|
|
}
|
|
auto debuggerFd = syscall(SYS_pidfd_getfd, traceePidFd, *traceeFd, 0);
|
|
if (close(static_cast<int>(traceePidFd)) != 0) {
|
|
PLOG(ERROR) << "Failed to close pidfd";
|
|
ret = false;
|
|
}
|
|
if (debuggerFd == -1) {
|
|
PLOG(ERROR) << "Failed to duplicate child memfd to debugger";
|
|
return false;
|
|
}
|
|
|
|
logFds.debuggerFd = static_cast<int>(debuggerFd);
|
|
return ret;
|
|
}
|
|
|
|
bool OIDebugger::cleanupLogFile(void) {
|
|
bool ret = true;
|
|
if (logFds.traceeFd == -1)
|
|
return ret;
|
|
|
|
if (!remoteSyscall<SysClose>(logFds.traceeFd).has_value()) {
|
|
LOG(ERROR) << "Remote close failed";
|
|
ret = false;
|
|
}
|
|
|
|
if (logFds.debuggerFd == -1)
|
|
return ret;
|
|
|
|
FILE* logs = fdopen(logFds.debuggerFd, "r");
|
|
if (logs == NULL) {
|
|
PLOG(ERROR) << "Failed to fdopen jitlog";
|
|
return false;
|
|
}
|
|
if (fseek(logs, 0, SEEK_SET) != 0) {
|
|
PLOG(ERROR) << "Failed to fseek jitlog";
|
|
return false;
|
|
}
|
|
|
|
char* line = nullptr;
|
|
size_t read = 0;
|
|
VLOG(1) << "Outputting JIT logs:";
|
|
errno = 0;
|
|
while ((read = getline(&line, &read, logs)) != (size_t)-1) {
|
|
VLOG(1) << "JITLOG: " << line;
|
|
}
|
|
if (errno) {
|
|
PLOG(ERROR) << "getline";
|
|
return false;
|
|
}
|
|
VLOG(1) << "Finished outputting JIT logs.";
|
|
|
|
free(line);
|
|
if (fclose(logs) == -1) {
|
|
PLOG(ERROR) << "fclose";
|
|
return false;
|
|
}
|
|
|
|
return ret;
|
|
}
|
|
|
|
/* Set up traced process results and text segments */
|
|
bool OIDebugger::segmentInit(void) {
|
|
/*
|
|
* TODO: change this. If setup_results_segment() fails we have to remove
|
|
* the text segment.
|
|
*/
|
|
if (!segConfig.existingConfig || segConfig.dataSegSize != dataSegSize) {
|
|
if (!segConfig.existingConfig) {
|
|
if (!setupSegment(SegType::text) || !setupSegment(SegType::data)) {
|
|
LOG(ERROR) << "setUpSegment failed!!!";
|
|
return false;
|
|
}
|
|
} else {
|
|
if (!unmapSegment(SegType::data)) {
|
|
LOG(ERROR) << "Failed to unmmap data segment";
|
|
return false;
|
|
}
|
|
if (!setupSegment(SegType::data)) {
|
|
LOG(ERROR) << "Failed to setup data segment";
|
|
return false;
|
|
}
|
|
}
|
|
|
|
segConfig.existingConfig = true;
|
|
segmentConfigFile.seekg(0);
|
|
segmentConfigFile.write((char*)&segConfig, sizeof(segConfig));
|
|
|
|
VLOG(1) << "segConfig size " << sizeof(segConfig);
|
|
|
|
if (segmentConfigFile.fail()) {
|
|
LOG(ERROR) << "init: error in writing configFile" << segConfigFilePath
|
|
<< strerror(errno);
|
|
}
|
|
VLOG(1) << "About to flush segment config file";
|
|
segmentConfigFile.flush();
|
|
}
|
|
|
|
// Using nanoseconds since epoch as the cookie value
|
|
using namespace std::chrono;
|
|
auto now = high_resolution_clock::now().time_since_epoch();
|
|
segConfig.cookie = duration_cast<duration<uintptr_t, std::nano>>(now).count();
|
|
|
|
if (generatorConfig.features[Feature::JitLogging]) {
|
|
if (!setupLogFile()) {
|
|
LOG(ERROR) << "setUpLogFile failed!!!";
|
|
return false;
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
/*
|
|
* Temporary config file with settings for this debugging "session". The
|
|
* notion of "session" is a bit fuzzy at the minute.
|
|
*/
|
|
void OIDebugger::createSegmentConfigFile(void) {
|
|
/*
|
|
* For now we'll just use the pid of the traced process to name the temp
|
|
* file so we can find it
|
|
*/
|
|
assert(traceePid != 0);
|
|
|
|
segConfigFilePath = fs::temp_directory_path() /
|
|
("oid-segconfig-" + std::to_string(traceePid));
|
|
|
|
if (!fs::exists(segConfigFilePath) || fs::file_size(segConfigFilePath) == 0) {
|
|
segmentConfigFile.open(segConfigFilePath,
|
|
ios::in | ios::out | ios::binary | ios::trunc);
|
|
|
|
if (!segmentConfigFile.is_open()) {
|
|
LOG(ERROR) << "Failed to open " << segConfigFilePath << " : "
|
|
<< strerror(errno);
|
|
}
|
|
|
|
VLOG(1) << "createSegmentConfigFile: " << segConfigFilePath << " created";
|
|
} else {
|
|
VLOG(1) << "createSegmentConfigFile: config file " << segConfigFilePath
|
|
<< " already exists";
|
|
|
|
/* Read config */
|
|
segmentConfigFile =
|
|
std::fstream(segConfigFilePath, ios::in | ios::out | ios::binary);
|
|
segmentConfigFile.read((char*)&segConfig, sizeof(c));
|
|
|
|
if (segmentConfigFile.fail()) {
|
|
LOG(ERROR) << "createSegmentConfigFile: failed to read from "
|
|
"existing config file"
|
|
<< strerror(errno);
|
|
}
|
|
|
|
VLOG(1) << "Existing Config File read: " << std::hex
|
|
<< " textSegBase: " << segConfig.textSegBase
|
|
<< " textSegSize: " << segConfig.textSegSize
|
|
<< " jitCodeStart: " << segConfig.jitCodeStart
|
|
<< " existingConfig: " << segConfig.existingConfig
|
|
<< " dataSegBase: " << segConfig.dataSegBase
|
|
<< " dataSegSize: " << segConfig.dataSegSize
|
|
<< " replayInstBase: " << segConfig.replayInstBase
|
|
<< " cookie: " << segConfig.cookie;
|
|
|
|
assert(segConfig.existingConfig);
|
|
}
|
|
}
|
|
|
|
void OIDebugger::deleteSegmentConfig(bool deleteSegConfigFile) {
|
|
if (!segConfig.existingConfig) {
|
|
return;
|
|
}
|
|
|
|
if (deleteSegConfigFile) {
|
|
std::error_code ec;
|
|
if (!fs::remove(segConfigFilePath, ec)) {
|
|
VLOG(1) << "Failed to remove segment config file "
|
|
<< segConfigFilePath.c_str() << " value: " << ec.value()
|
|
<< " message: " << ec.message();
|
|
}
|
|
}
|
|
|
|
segConfig.textSegBase = 0;
|
|
segConfig.textSegSize = 0;
|
|
segConfig.jitCodeStart = 0;
|
|
segConfig.replayInstBase = 0;
|
|
segConfig.existingConfig = false;
|
|
segConfig.dataSegBase = 0;
|
|
segConfig.dataSegSize = 0;
|
|
segConfig.cookie = 0;
|
|
}
|
|
|
|
/*
|
|
* C++ enums are terrible, we know. There are a number of bodges that can make
|
|
* mapping enums values to strings more C++'esque * (read that as 'more
|
|
* grotesque') but this is the simple approach.
|
|
*/
|
|
std::string OIDebugger::taskStateToString(OIDebugger::StatusType status) {
|
|
/* Must reflect the order of OIDebugger::StatusType enum */
|
|
static const std::array enumMapping{"SLEEP",
|
|
"TRACED",
|
|
"RUNNING",
|
|
"ZOMBIE",
|
|
"DEAD",
|
|
"DISK SLEEP",
|
|
"STOPPED",
|
|
"OTHER",
|
|
"BAD"};
|
|
|
|
return enumMapping[static_cast<int>(status)];
|
|
}
|
|
|
|
OIDebugger::StatusType OIDebugger::getTaskState(pid_t pid) {
|
|
/*
|
|
* We often need to know (primarily for debugging) what the contents
|
|
* a tasks /proc/<tid>/status "State" field is set to so we know if it
|
|
* is stopped, sleeping etc. Obviously, this is racy but is better than
|
|
* nothing.
|
|
*/
|
|
auto path = fs::path("/proc") / std::to_string(pid) / "stat";
|
|
std::ifstream stat{path};
|
|
if (!stat) {
|
|
return StatusType::bad;
|
|
}
|
|
|
|
// Ignore pid and comm
|
|
stat.ignore(std::numeric_limits<std::streamsize>::max(), ' ');
|
|
stat.ignore(std::numeric_limits<std::streamsize>::max(), ' ');
|
|
|
|
char state = '\0';
|
|
stat >> state;
|
|
|
|
/* XXX Need to add other states */
|
|
OIDebugger::StatusType ret = StatusType::other;
|
|
if (state == 'S') {
|
|
ret = StatusType::sleep;
|
|
} else if (state == 't') {
|
|
ret = StatusType::traced;
|
|
} else if (state == 'R') {
|
|
ret = StatusType::running;
|
|
} else if (state == 'X') {
|
|
ret = StatusType::dead;
|
|
} else if (state == 'Z') {
|
|
ret = StatusType::zombie;
|
|
} else if (state == 'D') {
|
|
ret = StatusType::diskSleep;
|
|
} else if (state == 'T') {
|
|
ret = StatusType::stopped;
|
|
}
|
|
|
|
return ret;
|
|
}
|
|
|
|
/* For debug - do not remove */
|
|
void OIDebugger::dumpAlltaskStates(void) {
|
|
VLOG(1) << "Task State Dump";
|
|
for (auto const& p : threadList) {
|
|
auto state = getTaskState(p);
|
|
VLOG(1) << "Task " << p << " state: " << taskStateToString(state) << " ("
|
|
<< static_cast<int>(state) << ")";
|
|
}
|
|
}
|
|
|
|
int OIDebugger::getExtendedWaitEventType(int status) {
|
|
return (status >> 16);
|
|
}
|
|
|
|
bool OIDebugger::isExtendedWait(int status) {
|
|
return (getExtendedWaitEventType(status) != 0);
|
|
}
|
|
|
|
/*
|
|
* Continue the main traced thread either by a detach request (default)
|
|
* or via a continue. No provision is made for passing a signal.
|
|
*/
|
|
bool OIDebugger::contTargetThread(bool detach) const {
|
|
VLOG(4) << "contTargetThread: About to " << (detach ? "detach" : "continue")
|
|
<< " pid " << std::dec << traceePid;
|
|
|
|
if (!detach) {
|
|
return contTargetThread(traceePid);
|
|
}
|
|
|
|
errno = 0;
|
|
if ((ptrace(PTRACE_DETACH, traceePid, nullptr, nullptr)) < 0) {
|
|
LOG(ERROR) << "contTargetThread failed to detach pid " << traceePid << " "
|
|
<< strerror(errno) << "(" << errno << ")";
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
/*
|
|
* Continue the thread specified in 'targetPid' passing an optional
|
|
* signal via the optional 'data' parameter. The ptrace(2) data parameter
|
|
* is a 'void*' but used for many purposes. Here the sole pupose in detach
|
|
* or continue is to pass a signal value for the target thread and although
|
|
* it's not explicitly stated in the man page, a value of 0 (default) is
|
|
* treated as no signal passed.
|
|
*/
|
|
bool OIDebugger::contTargetThread(pid_t targetPid, unsigned long data) const {
|
|
bool ret = true;
|
|
|
|
VLOG(4) << "contTargetThread: About to continue pid " << std::dec
|
|
<< targetPid;
|
|
|
|
errno = 0;
|
|
if ((ptrace(PTRACE_CONT, targetPid, nullptr, data)) < 0) {
|
|
LOG(ERROR) << "contTargetThread failed to continue pid " << targetPid << " "
|
|
<< strerror(errno) << "(" << errno << ")";
|
|
ret = false;
|
|
}
|
|
|
|
return ret;
|
|
}
|
|
|
|
bool OIDebugger::replayTrappedInstr(const trapInfo& t,
|
|
pid_t pid,
|
|
struct user_regs_struct& regs,
|
|
struct user_fpregs_struct& fpregs) const {
|
|
/*
|
|
* Single step the original instruction which has been patched over
|
|
* with a breakpoint trap. The original instruction now resides in
|
|
* our private text mapping in the target process (see
|
|
* getReplayInstrAddr())
|
|
*/
|
|
auto origRip = std::exchange(regs.rip, t.replayInstAddr);
|
|
|
|
errno = 0;
|
|
if (ptrace(PTRACE_SETREGS, pid, nullptr, ®s) < 0) {
|
|
LOG(ERROR) << "Execute: Couldn't restore registers: " << strerror(errno);
|
|
}
|
|
|
|
errno = 0;
|
|
if (ptrace(PTRACE_SETFPREGS, pid, nullptr, &fpregs) < 0) {
|
|
LOG(ERROR) << "Execute: Couldn't restore fp registers: " << strerror(errno);
|
|
}
|
|
|
|
uintptr_t rip = singlestepInst(pid, regs);
|
|
|
|
/*
|
|
* On entry to this function the current threads %rip is pointing straight
|
|
* after the int3 instruction which is a single byte opcode. The
|
|
* actual instruction that this is patched over will most likely be
|
|
* larger than that (e.g., a 'push <reg>' is 2 bytes) and therefore
|
|
* we need to set the return %rip to point to the next instruction after
|
|
* that (we have just single stepped it). This assumes that the patched
|
|
* instruction at entry can't be a CTI which I'm not sure is valid. The
|
|
* assert() below will hopefully catch that though.
|
|
*/
|
|
|
|
/*
|
|
* Hmm. Not sure what to do here. If the instruction is a control transfer
|
|
* instruction (CTI) (e.g., jmp, ret, call) then we need to just go with
|
|
* the rip that results from the single step. If it isn't a CTI then I
|
|
* think we should just go with the original rip. Sounds OK on the surface
|
|
* but not sure it is correct for all scenarios.
|
|
*
|
|
* The disassembler can probably tell us if this is a CTI so probably a
|
|
* cleaner solution. For now we'll just see if the current rip is
|
|
* outside of t->replayInstAddr + MAX_INST_SIZE then we assume a CTI.
|
|
*/
|
|
|
|
if (rip > t.replayInstAddr && rip < t.replayInstAddr + 16) {
|
|
/* See comment above in processFuncEntry() for this */
|
|
VLOG(4) << "Hit rip adjustment code in replayTrappedInstr";
|
|
size_t origInstSize = rip - t.replayInstAddr;
|
|
/*
|
|
* x64 instructions can be 16 bytes but we only use 8 bytes of patch. I
|
|
* think things will have gone bad already but put this check here in case
|
|
* we are lucky enough to make it here with such an instruction.
|
|
*/
|
|
assert(origInstSize <= 8);
|
|
regs.rip = origRip - sizeofInt3 + origInstSize;
|
|
}
|
|
|
|
errno = 0;
|
|
if (ptrace(PTRACE_SETREGS, pid, NULL, ®s) < 0) {
|
|
LOG(ERROR) << "Execute: Couldn't restore registers: " << strerror(errno);
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
bool OIDebugger::locateObjectsAddresses(const trapInfo& tInfo,
|
|
struct user_regs_struct& regs) {
|
|
/*
|
|
* Write objects into prologue in target.
|
|
*/
|
|
bool ret = true;
|
|
for (const auto& arg : tInfo.args) {
|
|
auto remoteObjAddr = remoteObjAddrs.find(arg);
|
|
if (remoteObjAddr == remoteObjAddrs.end()) {
|
|
LOG(ERROR) << "Entry: failed to find remoteObjAddr! Skipping...";
|
|
ret = false;
|
|
continue;
|
|
}
|
|
|
|
auto addr = arg->findAddress(®s, tInfo.trapAddr);
|
|
if (!addr.has_value()) {
|
|
LOG(ERROR) << "Entry: failed to locate arg! Skipping...";
|
|
ret = false;
|
|
continue;
|
|
}
|
|
|
|
VLOG(4) << "Entry: arg addr: " << std::hex << *addr;
|
|
if (!writeTargetMemory((void*)(&addr.value()),
|
|
(void*)remoteObjAddr->second,
|
|
sizeof(*addr))) {
|
|
LOG(ERROR) << "Entry: writeTargetMemory remoteObjAddr failed!";
|
|
ret = false;
|
|
continue;
|
|
}
|
|
}
|
|
|
|
return ret;
|
|
}
|
|
|
|
OIDebugger::processTrapRet OIDebugger::processFuncTrap(
|
|
const trapInfo& tInfo,
|
|
pid_t pid,
|
|
struct user_regs_struct& regs,
|
|
struct user_fpregs_struct& fpregs) {
|
|
assert(tInfo.trapKind != OID_TRAP_JITCODERET);
|
|
|
|
processTrapRet ret = OID_CONT;
|
|
|
|
VLOG(4) << "\nProcess Function Trap for pid=" << pid << ": " << tInfo
|
|
<< "\n\n";
|
|
|
|
auto t = std::make_shared<trapInfo>(tInfo);
|
|
|
|
/* Save interrupted registers into trap information */
|
|
memcpy((void*)&t->savedRegs, (void*)®s, sizeof(t->savedRegs));
|
|
|
|
/* Save fpregs into trap information */
|
|
memcpy((void*)&t->savedFPregs, (void*)&fpregs, sizeof(t->savedFPregs));
|
|
|
|
/* Start by locating each Target Object's address */
|
|
if (!locateObjectsAddresses(*t, regs)) {
|
|
LOG(ERROR) << "Failed to locate all objects addresses. Aborting...";
|
|
replayTrappedInstr(*t, pid, t->savedRegs, t->savedFPregs);
|
|
|
|
contTargetThread(pid);
|
|
|
|
return OIDebugger::OID_ERR;
|
|
}
|
|
|
|
/*
|
|
* This is a function return site introspection trap. For now
|
|
* just grab the object address from rax but we will support
|
|
* using entry parameters as well.
|
|
*
|
|
* Return probes need handling slightly differently to entry
|
|
* probes. We may have instrumented multiple return sites and
|
|
* all of them need returning to their original instructions
|
|
* here - not just the one that caused the trap. For now just
|
|
* iterate and locate all OID_TRAP_VECT_ENTRYRET and
|
|
* OID_TRAP_VECT_RET entries and sort them out. This assumes
|
|
* that we only have single enablings which is not the long term
|
|
* strategy but OK for the short term.
|
|
*/
|
|
|
|
/*
|
|
* If this entry trap needs a corresponding entry trap then we need it
|
|
* should be active so let's search the 'threadTrapState' map for it.
|
|
* It *must* have a corresponding active entry in 'threadTrapState'
|
|
* for this pid.
|
|
*
|
|
* XXX Think on this. We may have many threads executing in a function at
|
|
* one point in time:
|
|
* T: Thread A hits foo() entry site
|
|
* T+1: Thread B hits foo() entry site
|
|
* T+2: Thread B hits foo() return site
|
|
* T+3: Thread A hits foo() return site
|
|
*
|
|
* Currently this would be bad for several reasons but the most important
|
|
* is that we can't have one thread messing with another threads data while
|
|
* it is still manipulating it. We have to ensure we match pairs correctly
|
|
* or at least ensure we are using the correct data when we introspect
|
|
* the argument. This means that we have to get rid of that stupid
|
|
* 'segConfig.remoteObjAddr' mechanism which means the OID_TRAP_VECT_ENTRYRET
|
|
* trap needs to stash the argument address somewhere that the return
|
|
* trap can easily access it. We also need to be able to record data from
|
|
* multiple return threads executing JIT'd code for a given function.
|
|
*/
|
|
|
|
/* Execute prologue, if the trap kind probes the target objects */
|
|
if (t->trapKind == OID_TRAP_VECT_ENTRYRET) {
|
|
/* EntryRet traps locate Target Objects, but the capture is deferred until
|
|
* return */
|
|
|
|
/*
|
|
* If this is an entry point site that is a helper for a return probe
|
|
* site then all we need to do is squirel away the contents of the
|
|
* specified register and execute the patched instruction. The content
|
|
* of the register has already been saved above.
|
|
*
|
|
* First we need to ensure that there exists an associated return trap. If
|
|
* we are inconsistent for some reason then we should do nothing. Should we
|
|
* remove the trap though? If we instrument entry sites before return for
|
|
* argument processing returns then we may be in that window. We would need
|
|
* to ensure that this didn't keep happening though owing to a bug. Think
|
|
* on it.
|
|
*/
|
|
t->lifetime.rename("entry_arg");
|
|
replayTrappedInstr(*t, pid, t->savedRegs, t->savedFPregs);
|
|
} else {
|
|
assert(t->trapKind == OID_TRAP_VECT_ENTRY ||
|
|
t->trapKind == OID_TRAP_VECT_RET);
|
|
|
|
if (t->trapKind == OID_TRAP_VECT_ENTRY) {
|
|
t->lifetime.rename("entry_jit");
|
|
} else {
|
|
t->lifetime.rename("return_jit");
|
|
}
|
|
|
|
/* Execute from the start of the prologue */
|
|
regs.rip = t->prologueObjAddr;
|
|
t->fromVect = true;
|
|
|
|
VLOG(4) << "processTrap: redirect pid " << std::dec << pid << " to address "
|
|
<< std::hex << (void*)tInfo.prologueObjAddr << " tInfo: " << tInfo
|
|
<< " " << tInfo.prologueObjAddr << " " << tInfo.fromVect;
|
|
|
|
errno = 0;
|
|
if (ptrace(PTRACE_SETREGS, pid, nullptr, ®s) < 0) {
|
|
LOG(ERROR) << "Execute: Couldn't restore registers: " << strerror(errno);
|
|
}
|
|
}
|
|
|
|
VLOG(4) << "Inserting Trapinfo for pid " << std::dec << pid;
|
|
threadTrapState.insert_or_assign(pid, std::move(t));
|
|
|
|
contTargetThread(pid);
|
|
|
|
VLOG(1) << "Finished Function Trap processing\n";
|
|
|
|
return ret;
|
|
}
|
|
|
|
OIDebugger::processTrapRet OIDebugger::processJitCodeRet(
|
|
const trapInfo& tInfo __attribute__((unused)), pid_t pid) {
|
|
OIDebugger::processTrapRet ret = OIDebugger::OID_CONT;
|
|
|
|
assert(tInfo.trapKind == OID_TRAP_JITCODERET);
|
|
|
|
VLOG(4) << "Process JIT Code Return Trap";
|
|
|
|
/*
|
|
* This is a trap from a non-vectored route. If an entry for this thread
|
|
* exists in the threadTrapState map then this trap is the servicing of a
|
|
* previously handled vector OID_TRAP_VECT_ENTRY. If so, redirect the thread
|
|
* back to the original call site. If not, just let into continue.
|
|
*/
|
|
if (auto iter = threadTrapState.find(pid);
|
|
iter != std::end(threadTrapState)) {
|
|
/*
|
|
* This is the return path from JIT code. Before we re-vector the target
|
|
* thread back to its original code path we must replay the instruction
|
|
* that has been patched over by the breakpoint trap so that its effects
|
|
* are visible to the target thread.
|
|
*/
|
|
auto t{iter->second};
|
|
t->lifetime.stop();
|
|
|
|
auto jitTrapProcessTime = metrics::Tracing("jit_ret");
|
|
|
|
VLOG(4) << "Hit the return path from vector. Redirect to " << std::hex
|
|
<< t->trapAddr;
|
|
|
|
/*
|
|
* Global variable handling sits outside the regular scheme and requires
|
|
* a lot less work than other traps.
|
|
*/
|
|
if (t->trapAddr != GLOBAL_VARIABLE_TRAP_ADDR) {
|
|
replayTrappedInstr(*t, pid, t->savedRegs, t->savedFPregs);
|
|
} else {
|
|
VLOG(4) << "processJitCodeRet processing global variable return";
|
|
|
|
errno = 0;
|
|
if (ptrace(PTRACE_SETREGS, pid, nullptr, &t->savedRegs) < 0) {
|
|
LOG(ERROR) << "Execute: Couldn't restore registers: "
|
|
<< strerror(errno);
|
|
}
|
|
|
|
errno = 0;
|
|
if (ptrace(PTRACE_SETFPREGS, pid, nullptr, &t->savedFPregs) < 0) {
|
|
LOG(ERROR) << "Execute: Couldn't restore fp registers: "
|
|
<< strerror(errno);
|
|
}
|
|
|
|
if (VLOG_IS_ON(4)) {
|
|
errno = 0;
|
|
struct user_regs_struct newregs {};
|
|
if (ptrace(PTRACE_GETREGS, pid, NULL, &newregs) < 0) {
|
|
LOG(ERROR) << "Execute: Couldn't restore registers: "
|
|
<< strerror(errno);
|
|
}
|
|
|
|
dumpRegs("processJitCodeRet global: ", pid, &newregs);
|
|
}
|
|
}
|
|
|
|
VLOG(4) << "Erasing Trapinfo for pid " << std::dec << iter->first;
|
|
threadTrapState.erase(iter);
|
|
|
|
jitTrapProcessTime.stop();
|
|
|
|
contTargetThread(pid);
|
|
|
|
if (++count == 1 || isInterrupted()) {
|
|
VLOG(1) << "count: " << count << " oid done";
|
|
ret = OIDebugger::OID_DONE;
|
|
} else {
|
|
ret = OIDebugger::OID_CONT;
|
|
}
|
|
}
|
|
|
|
VLOG(1) << "Finished JIT Code Return trap processing\n";
|
|
|
|
return ret;
|
|
}
|
|
|
|
/*
|
|
* Although we follow the same naming scheme as for other probe types
|
|
* (e.g., entry/return), this function is never called from trap handling
|
|
* code. Global variables do not rely on a specific entry point to be
|
|
* instrumented but instead we can simply hijack a thread (the main thread
|
|
* in this case) and introspect the global data. It would be good if we had
|
|
* a cheap way of asserting that the global thread is stopped.
|
|
*/
|
|
bool OIDebugger::processGlobal(const std::string& varName) {
|
|
assert(mode == OID_MODE_THREAD);
|
|
|
|
VLOG(1) << "Introspecting global variable: " << varName;
|
|
|
|
errno = 0;
|
|
if (ptrace(PTRACE_SYSCALL, traceePid, nullptr, nullptr) < 0) {
|
|
LOG(ERROR) << "Couldn't attach to target pid " << traceePid
|
|
<< " (Reason: " << strerror(errno) << ")";
|
|
return false;
|
|
}
|
|
|
|
VLOG(1) << "About to wait for process on syscall entry/exit";
|
|
int status = 0;
|
|
waitpid(traceePid, &status, 0);
|
|
|
|
if (!WIFSTOPPED(status)) {
|
|
LOG(ERROR) << "process not stopped!";
|
|
}
|
|
|
|
errno = 0;
|
|
struct user_regs_struct regs {};
|
|
if (ptrace(PTRACE_GETREGS, traceePid, nullptr, ®s) < 0) {
|
|
LOG(ERROR) << "processGlobal: failed to read registers" << strerror(errno);
|
|
return false;
|
|
}
|
|
|
|
errno = 0;
|
|
struct user_fpregs_struct fpregs {};
|
|
if (ptrace(PTRACE_GETFPREGS, traceePid, nullptr, &fpregs) < 0) {
|
|
LOG(ERROR) << "processGlobal: Couldn't get fp registers: "
|
|
<< strerror(errno);
|
|
}
|
|
|
|
dumpRegs("After syscall stop", traceePid, ®s);
|
|
|
|
auto t = std::make_shared<trapInfo>(OID_TRAP_JITCODERET,
|
|
GLOBAL_VARIABLE_TRAP_ADDR);
|
|
t->lifetime.rename("global_jit");
|
|
threadTrapState.emplace(traceePid, t);
|
|
|
|
regs.rip -= 2;
|
|
/* Save interrupted registers into trap information */
|
|
memcpy((void*)&t->savedRegs, (void*)®s, sizeof(t->savedRegs));
|
|
|
|
/* Save fpregs into trap information */
|
|
memcpy((void*)&t->savedFPregs, (void*)&fpregs, sizeof(t->savedFPregs));
|
|
regs.rip = segConfig.textSegBase;
|
|
|
|
dumpRegs("processGlobal2", traceePid, ®s);
|
|
|
|
/*
|
|
* Get the variable address and push it into the target process patch area.
|
|
*/
|
|
auto sym = symbols->locateSymbol(varName);
|
|
if (!sym.has_value()) {
|
|
LOG(ERROR) << "processGlobal: failed to get global's address!";
|
|
return false;
|
|
}
|
|
|
|
uint64_t addr = sym->addr;
|
|
|
|
auto gd = symbols->findGlobalDesc(varName);
|
|
if (!gd) {
|
|
LOG(ERROR) << "processGlobal: failed to find GlobalDesc!";
|
|
return false;
|
|
}
|
|
|
|
auto remoteObjAddr = remoteObjAddrs.find(gd);
|
|
if (remoteObjAddr == remoteObjAddrs.end()) {
|
|
LOG(ERROR) << "processGlobal: no remote object addr for " << varName;
|
|
return false;
|
|
}
|
|
|
|
if (!writeTargetMemory(
|
|
(void*)&addr, (void*)remoteObjAddr->second, sizeof(addr))) {
|
|
LOG(ERROR) << "processGlobal: writeTargetMemory remoteObjAddr failed!";
|
|
}
|
|
|
|
VLOG(1) << varName << " addr: " << std::hex << addr;
|
|
|
|
/* Main target thread should already be stopped */
|
|
|
|
errno = 0;
|
|
if (ptrace(PTRACE_SETREGS, traceePid, nullptr, ®s) < 0) {
|
|
LOG(ERROR) << "Execute: Couldn't restore registers: " << strerror(errno);
|
|
}
|
|
|
|
contTargetThread(traceePid);
|
|
|
|
return true;
|
|
}
|
|
|
|
bool OIDebugger::canProcessTrapForThread(pid_t thread_pid) const {
|
|
/*
|
|
* We want to prevent multiple threads from running the JIT code at the same
|
|
* time. We have only one data segment, so we don't support concurrent
|
|
* probes, for the moment. This method checks if we have a probe already
|
|
* running. It uses the threadTrapState map to do so. If the map is empty,
|
|
* no probe is running, so we return true. Otherwise, we must check if the
|
|
* the thread which encountered the trap is part of the map. If the new trap
|
|
* is from the same thread, we also return true. This is to handle entry-ret
|
|
* probes and the JIT code ret trap.
|
|
*/
|
|
if (threadTrapState.empty()) {
|
|
return true;
|
|
}
|
|
return threadTrapState.find(thread_pid) != end(threadTrapState);
|
|
}
|
|
|
|
/*
|
|
* Wait for a thread to stop and process whatever caused it to stop. Which
|
|
* thread is waited for is controlled by the 'anyPid' parameter: if true
|
|
* (default) we wait for any thread to stop, if false we wait for the thread
|
|
* specified by the first parameter 'pid' to stop.
|
|
*/
|
|
OIDebugger::processTrapRet OIDebugger::processTrap(pid_t pid,
|
|
bool blocking,
|
|
bool anyPid) {
|
|
int status = 0;
|
|
pid_t newpid = 0;
|
|
processTrapRet ret = OIDebugger::OID_CONT;
|
|
pid_t pidToWaitFor = -1;
|
|
|
|
if (threadList.empty()) {
|
|
LOG(ERROR)
|
|
<< "Thread list is empty: the target process must have died. Abort...";
|
|
return OID_DONE;
|
|
}
|
|
|
|
if (!anyPid) {
|
|
pidToWaitFor = pid;
|
|
VLOG(1) << "About to wait for pid " << std::dec << pidToWaitFor;
|
|
} else {
|
|
VLOG(1) << "About to wait for any child process ";
|
|
}
|
|
|
|
errno = 0;
|
|
int options = blocking ? 0 : WNOHANG;
|
|
if ((newpid = waitpid(pidToWaitFor, &status, options)) < 0) {
|
|
LOG(ERROR) << "processTrap: Error in waitpid (pid " << pidToWaitFor << ")"
|
|
<< " " << strerror(errno);
|
|
|
|
/* SIGINT handling */
|
|
if (errno == EINTR && isInterrupted()) {
|
|
return OIDebugger::OID_DONE;
|
|
}
|
|
}
|
|
|
|
if (newpid == 0) {
|
|
/* WNOHANG handling */
|
|
VLOG(4) << "No child waiting for processTrap";
|
|
return OIDebugger::OID_DONE;
|
|
}
|
|
|
|
if (!WIFSTOPPED(status)) {
|
|
LOG(ERROR) << "contAndExecute: process not stopped!";
|
|
return blocking ? OID_ERR : OID_CONT;
|
|
}
|
|
siginfo_t info;
|
|
|
|
auto stopsig = WSTOPSIG(status);
|
|
VLOG(4) << "Stop sig: " << std::dec << stopsig;
|
|
if (ptrace(PTRACE_GETSIGINFO, newpid, nullptr, &info) < 0) {
|
|
LOG(ERROR) << "PTRACE_GETSIGINFO failed with " << strerror(errno);
|
|
} else {
|
|
bool stopped = !!info.si_status;
|
|
VLOG(4) << "PTRACE_GETSIGINFO pid " << newpid << " signo " << info.si_signo
|
|
<< " code " << info.si_code << " status " << info.si_status
|
|
<< " stopped: " << stopped;
|
|
}
|
|
|
|
/*
|
|
* Process PTRACE_EVENT stops (extended waits).
|
|
*/
|
|
if (isExtendedWait(status)) {
|
|
int type = getExtendedWaitEventType(status);
|
|
|
|
if (type == PTRACE_EVENT_CLONE || type == PTRACE_EVENT_FORK ||
|
|
type == PTRACE_EVENT_VFORK) {
|
|
unsigned long ptraceMsg = 0;
|
|
if (ptrace(PTRACE_GETEVENTMSG, newpid, NULL, &ptraceMsg) < 0) {
|
|
LOG(ERROR) << "processTrap: PTRACE_GETEVENTMSG failed: "
|
|
<< strerror(errno);
|
|
}
|
|
|
|
pid_t childPid = static_cast<pid_t>(ptraceMsg);
|
|
VLOG(4) << "New child created. pid " << std::dec << childPid
|
|
<< " (type: " << type << ")";
|
|
threadList.push_back(childPid);
|
|
|
|
/*
|
|
* ptrace() semantics are opaque to say the least. The new child
|
|
* has a pending SIGSTOP so we need to relieve it of that burden.
|
|
*/
|
|
int tstatus = 0;
|
|
int tret = 0;
|
|
tret = waitpid(childPid, &tstatus, __WALL | WSTOPPED | WEXITED);
|
|
|
|
if (tret == -1) {
|
|
VLOG(4) << "processTrap: Error waiting for new child " << std::dec
|
|
<< childPid << " (Reason: " << strerror(errno) << ")";
|
|
} else if (tret != childPid) {
|
|
VLOG(4) << "processTrap: Unexpected pid waiting for child: " << std::dec
|
|
<< tret;
|
|
} else if (!WIFSTOPPED(tstatus)) {
|
|
VLOG(4) << "processTrap: Unexpected status returned when "
|
|
"waiting for child: "
|
|
<< std::hex << tstatus;
|
|
}
|
|
|
|
if (WIFSTOPPED(tstatus)) {
|
|
VLOG(4) << "child was stopped with: " << WSTOPSIG(tstatus);
|
|
}
|
|
|
|
ptrace(PTRACE_SETOPTIONS,
|
|
childPid,
|
|
NULL,
|
|
PTRACE_O_TRACESYSGOOD | PTRACE_O_TRACEFORK | PTRACE_O_TRACECLONE |
|
|
PTRACE_O_TRACEVFORK);
|
|
|
|
contTargetThread(childPid);
|
|
} else if (type == PTRACE_EVENT_EXIT) {
|
|
VLOG(4) << "Thread exiting!! pid " << std::dec << newpid;
|
|
threadList.erase(
|
|
std::remove(threadList.begin(), threadList.end(), newpid),
|
|
threadList.end());
|
|
} else if (type == PTRACE_EVENT_STOP) {
|
|
VLOG(4) << "PTRACE_EVENT_STOP!";
|
|
} else {
|
|
VLOG(4) << "unhandled extended wait event type: " << type;
|
|
}
|
|
|
|
contTargetThread(newpid);
|
|
|
|
return ret;
|
|
}
|
|
|
|
int sig = WSTOPSIG(status);
|
|
switch (sig) {
|
|
/*
|
|
* One of the massive advantages of controlling a target via the ptrace
|
|
* interface is that fatal signals are not delivered to the target but
|
|
* come to us first. This means that we can pretty much do whatever we
|
|
* want in our jitted code and get away with it...
|
|
*/
|
|
case SIGALRM: {
|
|
VLOG(4) << "SIGALRM received!!";
|
|
contTargetThread(newpid, SIGALRM);
|
|
break;
|
|
}
|
|
|
|
case SIGSEGV: {
|
|
{
|
|
errno = 0;
|
|
struct user_regs_struct regs {};
|
|
if (ptrace(PTRACE_GETREGS, newpid, nullptr, ®s) < 0) {
|
|
LOG(ERROR) << "SIGSEGV handling: failed to read registers"
|
|
<< strerror(errno);
|
|
}
|
|
|
|
dumpRegs("SIGSEGV", newpid, ®s);
|
|
LOG(ERROR) << "SIGSEGV: faulting addr " << std::hex << info.si_addr
|
|
<< " rip " << regs.rip << " (pid " << std::dec << newpid
|
|
<< ")";
|
|
}
|
|
|
|
if (auto iter{threadTrapState.find(newpid)};
|
|
iter != std::end(threadTrapState)) {
|
|
auto t{iter->second};
|
|
|
|
LOG(ERROR) << "SEGV handling trap state found for " << std::dec
|
|
<< newpid;
|
|
|
|
errno = 0;
|
|
if (ptrace(PTRACE_SETREGS, newpid, NULL, &t->savedRegs) < 0) {
|
|
LOG(ERROR) << "processTrap: Couldn't restore registers: "
|
|
<< strerror(errno);
|
|
}
|
|
dumpRegs("After1", newpid, &t->savedRegs);
|
|
|
|
errno = 0;
|
|
if (ptrace(PTRACE_SETFPREGS, newpid, NULL, &t->savedFPregs) < 0) {
|
|
LOG(ERROR) << "processTrap: Couldn't restore fp registers: "
|
|
<< strerror(errno);
|
|
}
|
|
|
|
replayTrappedInstr(*t, newpid, t->savedRegs, t->savedFPregs);
|
|
threadTrapState.erase(newpid);
|
|
|
|
contTargetThread(newpid);
|
|
} else {
|
|
// We are not the source of the SIGSEGV so forward it to the target.
|
|
contTargetThread(newpid, sig);
|
|
}
|
|
return OIDebugger::OID_DONE;
|
|
}
|
|
|
|
case SIGBUS: {
|
|
LOG(ERROR) << "SIGBUS: " << std::hex << info.si_addr << "(pid " << newpid
|
|
<< ")";
|
|
return OIDebugger::OID_DONE;
|
|
}
|
|
|
|
case SIGCHLD: {
|
|
VLOG(4) << "SIGCHLD processing for pid " << newpid;
|
|
contTargetThread(newpid, sig);
|
|
|
|
break;
|
|
}
|
|
|
|
case SIGTRAP: {
|
|
/*
|
|
* Posix dictates that siginfo.si_addr is only used for SIGILL,
|
|
* SIGFPE, SIGSEGV and SIGBUS. Use PTRACE_GETREGS for SIGTRAP.
|
|
*
|
|
* We use INT3 for several purposes: in segment setup,
|
|
* instrumentation code execution and in pid provider style
|
|
* entry/return point vectoring. We therefore must be able to match
|
|
* which interrupt this is for and act accordingly.
|
|
*/
|
|
struct user_regs_struct regs {};
|
|
struct user_fpregs_struct fpregs {};
|
|
|
|
errno = 0;
|
|
if (ptrace(PTRACE_GETREGS, newpid, nullptr, ®s) < 0) {
|
|
LOG(ERROR) << "processTrap failed to read registers for process "
|
|
<< traceePid << " " << strerror(errno);
|
|
|
|
return OIDebugger::OID_ERR;
|
|
}
|
|
|
|
errno = 0;
|
|
if (ptrace(PTRACE_GETFPREGS, newpid, nullptr, &fpregs) < 0) {
|
|
LOG(ERROR) << "processTrap failed to read fp regs for process "
|
|
<< traceePid << " " << strerror(errno);
|
|
|
|
return OIDebugger::OID_ERR;
|
|
}
|
|
|
|
/*
|
|
* If we do not remove the breakpoint trap instructions then we
|
|
* need to not adjust the %rip and ensure we replay the original
|
|
* instruction that has been patched before we return the thread
|
|
* to it's next instruction.
|
|
*/
|
|
uint64_t bpaddr = regs.rip - sizeofInt3;
|
|
|
|
if (auto it{activeTraps.find(bpaddr)}; it != std::end(activeTraps)) {
|
|
/*
|
|
* This HAS TO BE a COPY and not a reference! removeTrap() would
|
|
* otherwise delete the only instance of the std::shared_ptr, leading
|
|
* to the destruction of the tInfo and many use-after-free!
|
|
*/
|
|
auto tInfo = it->second;
|
|
assert(bpaddr == tInfo->trapAddr);
|
|
|
|
if (!blocking || !canProcessTrapForThread(newpid)) {
|
|
/*
|
|
* Only one probe is allowed to run at a time. We skip the other
|
|
* traps by replaying their instruction and continuing their thread.
|
|
*/
|
|
if (blocking) {
|
|
VLOG(4)
|
|
<< "Another probe already running, skipping trap for thread "
|
|
<< newpid;
|
|
} else {
|
|
VLOG(4) << "Resuming thread " << newpid;
|
|
}
|
|
|
|
replayTrappedInstr(*tInfo, newpid, regs, fpregs);
|
|
contTargetThread(newpid);
|
|
|
|
break;
|
|
}
|
|
|
|
/* Remove the trap right before we process it */
|
|
removeTrap(newpid, *tInfo);
|
|
|
|
if (tInfo->trapKind == OID_TRAP_JITCODERET) {
|
|
ret = processJitCodeRet(*tInfo, newpid);
|
|
} else {
|
|
ret = processFuncTrap(*tInfo, newpid, regs, fpregs);
|
|
}
|
|
} else {
|
|
LOG(ERROR) << "Error! SIGTRAP: " << std::hex << bpaddr
|
|
<< " No activeTraps entry found, resuming thread "
|
|
<< std::dec << newpid;
|
|
|
|
// Resuming at the breakpoint
|
|
regs.rip = bpaddr;
|
|
|
|
errno = 0;
|
|
if (ptrace(PTRACE_SETREGS, newpid, NULL, ®s) < 0) {
|
|
LOG(ERROR) << "processTrap failed to set registers for process "
|
|
<< newpid << " " << strerror(errno);
|
|
}
|
|
|
|
contTargetThread(newpid);
|
|
}
|
|
|
|
break;
|
|
}
|
|
|
|
case SIGILL: {
|
|
VLOG(4) << "OOPS! SIGILL received for pid " << newpid
|
|
<< " addr: " << std::hex << info.si_addr;
|
|
|
|
break;
|
|
}
|
|
|
|
default:
|
|
VLOG(4) << "contAndExecute: Explictly unhandled signal. Forwarding "
|
|
<< strsignal(WSTOPSIG(status));
|
|
|
|
contTargetThread(newpid, sig);
|
|
break;
|
|
}
|
|
|
|
return ret;
|
|
}
|
|
|
|
std::optional<std::vector<uintptr_t>> OIDebugger::findRetLocs(FuncDesc& fd) {
|
|
size_t maxSize = std::accumulate(
|
|
fd.ranges.begin(), fd.ranges.end(), size_t(0), [](auto currMax, auto& r) {
|
|
return std::max(currMax, r.size());
|
|
});
|
|
|
|
std::vector<uintptr_t> retLocs;
|
|
std::vector<std::byte> text(maxSize);
|
|
for (const auto& range : fd.ranges) {
|
|
/*
|
|
* We already have enough capacity to accomodate any range from the function
|
|
* But we must ensure the actual `size` of the vector matches what is being
|
|
* put into it. `locateOpcode` uses the vector's size to know for how long
|
|
* it must decode instructions. As resize doesn't never reduces the capacity
|
|
* of the vector, there is no risk of multiple memory allocations impacting
|
|
* the performances.
|
|
*/
|
|
text.resize(range.size());
|
|
|
|
/* Copy the range of instruction into the text vector to be disassembled */
|
|
if (!readTargetMemory((void*)range.start, text.data(), text.size())) {
|
|
LOG(ERROR) << "Could not read function range " << fd.symName << "@"
|
|
<< range;
|
|
return std::nullopt;
|
|
}
|
|
|
|
/*
|
|
* Locate the returns within the function instructions.
|
|
* Immediate values have a size of 16-bits/2-bytes. Both 0xC2 and 0xCA have
|
|
* a total size of 3 bytes, which fit in the 8 bytes window we capture with
|
|
* PTRACE_PEEKTEXT. So we'll be able to capture and replay the instraction
|
|
* without problem. We assume a flat memory model, meaning we only have 1
|
|
* segment and we don't have returns across segments. So we don't have to
|
|
* convert RETN into RETF during replay. We can't probe the return fo
|
|
* tail-recursive functions as they typically ends with a JMP.
|
|
*/
|
|
// clang-format off
|
|
const std::array retInsts = {
|
|
std::array{uint8_t(0xC2)}, /* Return from near procedure with immediate value */
|
|
std::array{uint8_t(0xC3)}, /* Return from near procedure */
|
|
std::array{uint8_t(0xCA)}, /* Return from far procedure with immediate value */
|
|
std::array{uint8_t(0xCB)}, /* Return from far procedure */
|
|
};
|
|
// clang-format on
|
|
auto locs = OICompiler::locateOpcodes(text, retInsts);
|
|
if (!locs.has_value()) {
|
|
LOG(ERROR)
|
|
<< "Failed to locate all Return instructions in function range "
|
|
<< fd.symName << "@" << range;
|
|
return std::nullopt;
|
|
}
|
|
|
|
/* Add the range.start to the offsets returned above to get the absolute
|
|
* address of the return instructions */
|
|
for (auto loc : *locs) {
|
|
retLocs.push_back(range.start + loc);
|
|
}
|
|
}
|
|
|
|
return retLocs;
|
|
}
|
|
|
|
/*
|
|
* Locate the address in the target address space where the patched
|
|
* instruction that was at address 'addr' is found. We currently don't handle
|
|
* instructions larger than 8 bytes.
|
|
*
|
|
* If it's not in the replayInstMap, return the address to the next free entry
|
|
* in the cache and put the entry in the map.
|
|
*/
|
|
std::optional<uintptr_t> OIDebugger::nextReplayInstrAddr(const trapInfo& t) {
|
|
if (auto it = replayInstMap.find(t.trapAddr); it != end(replayInstMap)) {
|
|
VLOG(1) << "Found instruction for trap " << (void*)t.trapAddr
|
|
<< " at address " << (void*)it->second;
|
|
return it->second;
|
|
}
|
|
|
|
/*
|
|
* Not found so allocate the instruction into the replay instruction area and
|
|
* create a map entry.
|
|
*/
|
|
|
|
VLOG(1) << "Replay Instruction Base " << std::hex << segConfig.replayInstBase;
|
|
auto newInstrAddr = segConfig.replayInstBase + replayInstsCurIdx++ * 8;
|
|
if (newInstrAddr >= segConfig.textSegBase + segConfig.textSegSize) {
|
|
LOG(ERROR) << "Text Segment's Replay Instruction buffer is full. Increase "
|
|
"replayInstSize in OIDebugger.h";
|
|
return std::nullopt;
|
|
}
|
|
|
|
VLOG(1) << "Orig instructions for trap " << (void*)t.trapAddr
|
|
<< " will get saved at " << (void*)newInstrAddr;
|
|
replayInstMap.emplace(t.trapAddr, newInstrAddr);
|
|
|
|
return newInstrAddr;
|
|
}
|
|
|
|
/*
|
|
* Insert a breakpoint trap at the first instruction of the function
|
|
* supplied as the argument.
|
|
*
|
|
* This is a hybrid instrumentation function for entry-return patching. Pulling
|
|
* the entry and return patching together for this case does create a
|
|
* maintenance burden as we now have code doing exactly the same thing in
|
|
* two different places However, it allows us to reduce the time window
|
|
* between enabling entry and return sites (i.e., when we actually install
|
|
* breakpoint traps).
|
|
*/
|
|
|
|
/*
|
|
* This is a first pass at function return tracing. What does that mean
|
|
* exactly. Well, we allow introspection of the actual return value or we
|
|
* allow introspection of one of the input parameters *at return*. This
|
|
* causes significant complications to what is already looking like a very
|
|
* problematic problem (at least from a concurrency perspective).
|
|
*
|
|
* Currently I can't see a way to obtain function parameters without using
|
|
* breakpoints at the function entry and of course this is a real pain.
|
|
* Another problem is that we may well have multiple return locations
|
|
* within a function so we need multiple points of instrumentation. The
|
|
* main issue I see with this is how to do "atomically" and this has multiple
|
|
* meanings according to context:
|
|
* - a thread executing instructions. Currently the only option we have
|
|
* for modifying read/execute text is via ptrace and this does 8 bytes
|
|
* at a time. How atomic is that 8 byte write w.r.t threads that are
|
|
* executing code within that region? Needs investigation but I'd be very
|
|
* surprised if it was truly atomic.
|
|
* - w.r.t Object Introspection. This is easier to deal with than the above
|
|
* problem but we have to have a consistent view within OI. This means that
|
|
* until all our ret's are instrumented (and possibly our entry point also)
|
|
* then we can't allow a thread to go down the introspection path. All
|
|
* instrumentation much be done as a single unit.
|
|
*/
|
|
|
|
bool OIDebugger::functionPatch(const prequest& req) {
|
|
assert(req.type != "global");
|
|
|
|
auto fd = symbols->findFuncDesc(req.getReqForArg(0));
|
|
if (!fd) {
|
|
LOG(ERROR) << "Could not find FuncDesc for " << req.func;
|
|
return false;
|
|
}
|
|
|
|
/*
|
|
* Patch the function according to the given request:
|
|
* 1. Locate all TRAP points and create a corresponding empty trapInfo in
|
|
* tiVec.
|
|
* 2. Read the original instructions in their corresponding trapInfo.
|
|
* 3. Finish building the trapInfo with the info collected above.
|
|
* 4. Save the original instructions in our Replay Instruction buffer.
|
|
* 5. Insert the traps in the target process.
|
|
*/
|
|
|
|
std::vector<std::shared_ptr<trapInfo>> tiVec;
|
|
|
|
/* 1. Locate all TRAP points and create a corresponding empty trapInfo in
|
|
* tiVec */
|
|
bool hasArg = std::any_of(begin(req.args), end(req.args), [](auto& arg) {
|
|
return arg != "retval";
|
|
});
|
|
|
|
if (req.type == "entry" || hasArg) {
|
|
trapType tType =
|
|
req.type == "return" ? OID_TRAP_VECT_ENTRYRET : OID_TRAP_VECT_ENTRY;
|
|
uintptr_t trapAddr = fd->ranges[0].start;
|
|
if (req.args[0].starts_with("arg") || req.args[0] == "this") {
|
|
auto* argument =
|
|
dynamic_cast<FuncDesc::Arg*>(fd->getArgument(req.args[0]).get());
|
|
if (argument->locator.locations_size > 0) {
|
|
/*
|
|
* The `std::max` is necessary because sometimes when a binary is
|
|
* compiled in debug-mode, the compiler will state that the variable
|
|
* becomes live at address 0, which is clearly not what we want.
|
|
*/
|
|
trapAddr = std::max(trapAddr, argument->locator.locations[0].start);
|
|
}
|
|
}
|
|
tiVec.push_back(
|
|
std::make_shared<trapInfo>(tType, trapAddr, segConfig.textSegBase));
|
|
}
|
|
|
|
if (req.type == "return") {
|
|
auto retLocs = findRetLocs(*fd);
|
|
if (!retLocs.has_value() || retLocs.value().empty()) {
|
|
return false;
|
|
}
|
|
|
|
for (auto addr : *retLocs) {
|
|
tiVec.push_back(std::make_shared<trapInfo>(
|
|
OID_TRAP_VECT_RET, addr, segConfig.textSegBase));
|
|
}
|
|
}
|
|
|
|
assert(!tiVec.empty());
|
|
|
|
/* 2. Read the original instructions in their corresponding trapInfo */
|
|
std::vector<struct iovec> localIov;
|
|
std::vector<struct iovec> remoteIov;
|
|
localIov.reserve(tiVec.size());
|
|
remoteIov.reserve(tiVec.size());
|
|
|
|
for (auto& ti : tiVec) {
|
|
localIov.push_back({(void*)ti->origTextBytes, sizeof(ti->origTextBytes)});
|
|
remoteIov.push_back({(void*)ti->trapAddr, sizeof(ti->origTextBytes)});
|
|
}
|
|
|
|
errno = 0;
|
|
auto readBytes = process_vm_readv(traceePid,
|
|
localIov.data(),
|
|
localIov.size(),
|
|
remoteIov.data(),
|
|
remoteIov.size(),
|
|
0);
|
|
if (readBytes < 0) {
|
|
LOG(ERROR) << "Failed to get original instructions: " << strerror(errno);
|
|
return false;
|
|
}
|
|
|
|
if ((size_t)readBytes != (tiVec.size() * sizeof(trapInfo::origTextBytes))) {
|
|
LOG(ERROR) << "Failed to get all original instructions!";
|
|
return false;
|
|
}
|
|
|
|
/* 3. Finish building the trapInfo with the info collected above */
|
|
|
|
/* Re-use remoteIov to write the original instructions in our textSegment */
|
|
remoteIov.clear();
|
|
|
|
for (auto& trap : tiVec) {
|
|
trap->patchedText = trap->origText;
|
|
trap->patchedTextBytes[0] = int3Inst;
|
|
|
|
auto replayInstrAddr = nextReplayInstrAddr(*trap);
|
|
if (!replayInstrAddr.has_value()) {
|
|
LOG(ERROR)
|
|
<< "Failed to allocate room for replay instructions in text segment";
|
|
return false;
|
|
}
|
|
|
|
trap->replayInstAddr = *replayInstrAddr;
|
|
remoteIov.push_back(
|
|
{(void*)trap->replayInstAddr, sizeof(trap->origTextBytes)});
|
|
|
|
if (trap->trapKind == OID_TRAP_VECT_ENTRY ||
|
|
trap->trapKind == OID_TRAP_VECT_ENTRYRET) {
|
|
/* Capture the arguments to probe */
|
|
trap->args.reserve(req.args.size());
|
|
for (const auto& arg : req.args) {
|
|
if (auto targetObj = fd->getArgument(arg)) {
|
|
trap->args.push_back(std::move(targetObj));
|
|
} else {
|
|
LOG(ERROR) << "Failed to get argument " << arg << " for function "
|
|
<< req.func;
|
|
return false;
|
|
}
|
|
}
|
|
} else if (trap->trapKind == OID_TRAP_VECT_RET) {
|
|
/* Capture retval, if this is a return-retval req */
|
|
if (std::find(begin(req.args), end(req.args), "retval") !=
|
|
end(req.args)) {
|
|
trap->args.push_back(fd->retval);
|
|
}
|
|
}
|
|
}
|
|
|
|
/* 4. Save the original instructions in our Replay Instruction buffer */
|
|
errno = 0;
|
|
auto writtenBytes = process_vm_writev(traceePid,
|
|
localIov.data(),
|
|
localIov.size(),
|
|
remoteIov.data(),
|
|
remoteIov.size(),
|
|
0);
|
|
if (writtenBytes < 0) {
|
|
LOG(ERROR) << "Failed to save original instructions: " << strerror(errno);
|
|
return false;
|
|
}
|
|
|
|
if ((size_t)writtenBytes !=
|
|
(tiVec.size() * sizeof(trapInfo::origTextBytes))) {
|
|
LOG(ERROR) << "Failed to save all original instructions!";
|
|
return false;
|
|
}
|
|
|
|
/* 5. Insert the traps in the target process */
|
|
for (const auto& trap : tiVec) {
|
|
VLOG(1) << "Patching function " << req.func << " @"
|
|
<< (void*)trap->trapAddr;
|
|
activeTraps.emplace(trap->trapAddr, trap);
|
|
|
|
errno = 0;
|
|
if (ptrace(PTRACE_POKETEXT, traceePid, trap->trapAddr, trap->patchedText) <
|
|
0) {
|
|
/* We'll let our cleanup handling restore the original instructions */
|
|
LOG(ERROR) << "functionPatch POKETEXT failed: " << strerror(errno);
|
|
return false;
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
/* See "syscall.h" for an explanation on the `Sys` template argument */
|
|
template <typename Sys, typename... Args>
|
|
std::optional<typename Sys::RetType> OIDebugger::remoteSyscall(Args... _args) {
|
|
/* Check the number of arguments received match the syscall's requirement */
|
|
static_assert(sizeof...(_args) == Sys::ArgsCount,
|
|
"Wrong number of arguments");
|
|
|
|
metrics::Tracing _("syscall");
|
|
VLOG(1) << "syscall enter " << Sys::Name;
|
|
|
|
auto sym = symbols->locateSymbol("main");
|
|
if (!sym.has_value()) {
|
|
LOG(ERROR) << "Failed to get address for 'main'";
|
|
return std::nullopt;
|
|
}
|
|
|
|
uint64_t patchAddr = sym->addr;
|
|
VLOG(1) << "Address of main: " << (void*)patchAddr;
|
|
|
|
/* Saving current registers states */
|
|
errno = 0;
|
|
struct user_regs_struct oldregs {};
|
|
if (ptrace(PTRACE_GETREGS, traceePid, nullptr, &oldregs) < 0) {
|
|
LOG(ERROR) << "syscall: GETREGS failed for process " << traceePid << ": "
|
|
<< strerror(errno);
|
|
return std::nullopt;
|
|
}
|
|
|
|
errno = 0;
|
|
struct user_fpregs_struct oldfpregs {};
|
|
if (ptrace(PTRACE_GETFPREGS, traceePid, nullptr, &oldfpregs) < 0) {
|
|
LOG(ERROR) << "syscall: GETFPREGS failed for process " << traceePid << ": "
|
|
<< strerror(errno);
|
|
return std::nullopt;
|
|
}
|
|
|
|
/*
|
|
* Ensure we restore the registers before returning! BOOST_SCOPE_EXIT_ALL
|
|
* sets up an object whose destructor executes the block below. Thanks to
|
|
* RAII, it ensures that the block always gets executed, independently of
|
|
* early returns or exception being thrown! This allow us to keep writing
|
|
* code without having to keep "restoring the registers" in mind.
|
|
*/
|
|
BOOST_SCOPE_EXIT_ALL(&) {
|
|
errno = 0;
|
|
if (ptrace(PTRACE_SETREGS, traceePid, nullptr, &oldregs) < 0) {
|
|
VLOG(1) << "syscall: restore SETREGS failed: " << strerror(errno);
|
|
}
|
|
|
|
errno = 0;
|
|
if (ptrace(PTRACE_SETFPREGS, traceePid, nullptr, &oldfpregs) < 0) {
|
|
LOG(ERROR) << "syscall: restore SETFPREGS failed: " << strerror(errno);
|
|
}
|
|
};
|
|
|
|
/* Prepare new registers to hijack the thread and execute the syscall */
|
|
struct user_regs_struct newregs = oldregs;
|
|
newregs.rip = patchAddr + 1; // +1 to skip the INT3 safeguard
|
|
newregs.rax = Sys::SysNum;
|
|
|
|
{
|
|
/*
|
|
* See syscall(2) for the list of registers used for syscalls
|
|
* arch/ABI arg1 arg2 arg3 arg4 arg5 arg6 arg7
|
|
* x86-64 rdi rsi rdx r10 r8 r9 -
|
|
*/
|
|
const std::array<unsigned long long*, 6> argToReg = {
|
|
&newregs.rdi,
|
|
&newregs.rsi,
|
|
&newregs.rdx,
|
|
&newregs.r10,
|
|
&newregs.r8,
|
|
&newregs.r9,
|
|
};
|
|
|
|
unsigned long long args[] = {(unsigned long long)_args...};
|
|
for (size_t argIdx = 0; argIdx < Sys::ArgsCount; ++argIdx) {
|
|
*argToReg[argIdx] = args[argIdx];
|
|
}
|
|
}
|
|
|
|
/* Set the new registers with our syscall arguments */
|
|
errno = 0;
|
|
if (ptrace(PTRACE_SETREGS, traceePid, nullptr, &newregs)) {
|
|
LOG(ERROR) << "syscall: SETREGS failed: " << strerror(errno);
|
|
return std::nullopt;
|
|
}
|
|
|
|
/* Save the instructions so they can be restored */
|
|
errno = 0;
|
|
long oldinsts = ptrace(PTRACE_PEEKTEXT, traceePid, patchAddr, nullptr);
|
|
if (errno != 0) {
|
|
LOG(ERROR) << "syscall: PEEKTEXT failed: " << strerror(errno);
|
|
return std::nullopt;
|
|
}
|
|
|
|
/*
|
|
* Ensure we restore the instructions before returning!
|
|
* See the BOOST_SCOPE_EXIT_ALL above for more explanations...
|
|
*/
|
|
BOOST_SCOPE_EXIT_ALL(&) {
|
|
errno = 0;
|
|
if (ptrace(PTRACE_POKETEXT, traceePid, patchAddr, oldinsts) < 0) {
|
|
LOG(ERROR) << "syscall: restore POKETEXT failed: " << strerror(errno);
|
|
}
|
|
};
|
|
|
|
/*
|
|
* Prepare our instructions to run the syscall.
|
|
* This is little-endian, so read bytes from right to left.
|
|
* We start with an INT3 to stop any other thread that might enter `main`
|
|
* while we're fiddling with it (highly unlikely). We rely on `processTrap`
|
|
* to resume them later on. Then we have the two bytes for the SYSCALL
|
|
* instruction and the rest is filled with NOPs.
|
|
*/
|
|
long newinsts = syscallInsts;
|
|
|
|
/* Insert the SYSCALL instruction into the target */
|
|
errno = 0;
|
|
if (ptrace(PTRACE_POKETEXT, traceePid, patchAddr, newinsts) < 0) {
|
|
LOG(ERROR) << "syscall: POKETEXT failed: " << strerror(errno);
|
|
return std::nullopt;
|
|
}
|
|
|
|
{ /* SINGLESTEP once to run the syscall */
|
|
errno = 0;
|
|
if (ptrace(PTRACE_SINGLESTEP, traceePid, nullptr, nullptr) < 0) {
|
|
LOG(ERROR) << "syscall: SYSCALL " << Sys::Name
|
|
<< " failed: " << strerror(errno);
|
|
return std::nullopt;
|
|
}
|
|
|
|
int status = 0;
|
|
waitpid(traceePid, &status, 0);
|
|
|
|
if (!WIFSTOPPED(status)) {
|
|
LOG(ERROR) << "process not stopped!";
|
|
}
|
|
}
|
|
|
|
/* Read the new register state, so we can get the syscall's return value */
|
|
errno = 0;
|
|
if (ptrace(PTRACE_GETREGS, traceePid, nullptr, &newregs) < 0) {
|
|
LOG(ERROR) << "syscall: return GETREGS failed: " << strerror(errno);
|
|
return std::nullopt;
|
|
}
|
|
|
|
if ((long long)newregs.rax < 0) {
|
|
LOG(ERROR) << "syscall: Syscall " << Sys::Name
|
|
<< " failed with error: " << strerror((int)-newregs.rax);
|
|
return std::nullopt;
|
|
}
|
|
|
|
typename Sys::RetType retval = (typename Sys::RetType)newregs.rax;
|
|
|
|
VLOG(1) << "syscall " << Sys::Name << " returned " << std::hex << retval;
|
|
return retval;
|
|
}
|
|
|
|
bool OIDebugger::setupSegment(SegType seg) {
|
|
metrics::Tracing _("setup_segment");
|
|
|
|
std::optional<void*> segAddr;
|
|
if (seg == SegType::text) {
|
|
segAddr =
|
|
remoteSyscall<SysMmap>(nullptr,
|
|
textSegSize, // addr & size
|
|
PROT_READ | PROT_WRITE | PROT_EXEC, // prot
|
|
MAP_PRIVATE | MAP_ANONYMOUS, // flags
|
|
-1,
|
|
0); // fd & offset
|
|
} else {
|
|
segAddr = remoteSyscall<SysMmap>(nullptr,
|
|
dataSegSize, // addr & size
|
|
PROT_READ | PROT_WRITE, // prot
|
|
MAP_SHARED | MAP_ANONYMOUS, // flags
|
|
-1,
|
|
0); // fd & offset
|
|
}
|
|
|
|
if (!segAddr.has_value()) {
|
|
return false;
|
|
}
|
|
|
|
if (seg == SegType::text) {
|
|
segConfig.textSegBase = (uintptr_t)segAddr.value();
|
|
segConfig.constStart = segConfig.textSegBase + prologueLength;
|
|
segConfig.jitCodeStart = segConfig.constStart + constLength;
|
|
segConfig.textSegSize = textSegSize;
|
|
|
|
/*
|
|
* For the minute we'll use the last 512 bytes of text for storing
|
|
* replay instructions.
|
|
*/
|
|
segConfig.replayInstBase =
|
|
segConfig.textSegBase + textSegSize - replayInstSize;
|
|
VLOG(1) << "replayInstBase addr " << std::hex << segConfig.replayInstBase;
|
|
} else {
|
|
segConfig.dataSegBase = (uint64_t)*segAddr;
|
|
segConfig.dataSegSize = dataSegSize;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
bool OIDebugger::unmapSegment(SegType seg) {
|
|
metrics::Tracing _("unmap_segment");
|
|
auto addr =
|
|
(seg == SegType::text) ? segConfig.textSegBase : segConfig.dataSegBase;
|
|
auto size = (seg == SegType::text) ? textSegSize : dataSegSize;
|
|
return remoteSyscall<SysMunmap>(addr, size).has_value();
|
|
}
|
|
|
|
bool OIDebugger::unmapSegments(bool deleteSegConfFile) {
|
|
bool ret = true;
|
|
|
|
if (!unmapSegment(SegType::text)) {
|
|
LOG(ERROR) << "Problem unmapping target process text segment";
|
|
ret = false;
|
|
}
|
|
|
|
if (ret && !unmapSegment(SegType::data)) {
|
|
LOG(ERROR) << "Problem unmapping target process data segment";
|
|
ret = false;
|
|
}
|
|
|
|
deleteSegmentConfig(deleteSegConfFile);
|
|
|
|
return ret;
|
|
}
|
|
|
|
/*
|
|
* The debugger is shutting things down so all breakpoint traps need to be
|
|
* removed from the target process. As oid is currently single threaded
|
|
* there isn't a whole lot to race over so its pretty easy. We just need
|
|
* to be careful that no target threads end up waiting for signals to be
|
|
* be dealt with by us.
|
|
*
|
|
* NOTE: This code can be called from a signal handler context so keep it
|
|
* async-signal-safe. Obviously any output in debug or error modes are not
|
|
* async-signal-safe but we'll live with that for now.
|
|
*
|
|
* The calling thread *must* be stopped before calling this interface.
|
|
* Unfortunately there is no cheap way to assert this.
|
|
*/
|
|
bool OIDebugger::removeTraps(pid_t pid) {
|
|
metrics::Tracing removeTrapsTracing("remove_traps");
|
|
|
|
pid_t targetPid = pid ? pid : traceePid;
|
|
|
|
/* Hijack the main thread to remove the traps and flush the JIT logs */
|
|
errno = 0;
|
|
if (ptrace(PTRACE_INTERRUPT, targetPid, nullptr, nullptr) < 0) {
|
|
LOG(ERROR) << "Couldn't interrupt target pid " << targetPid << ": "
|
|
<< strerror(errno);
|
|
return false;
|
|
}
|
|
|
|
errno = 0;
|
|
if (waitpid(targetPid, 0, WSTOPPED) != targetPid) {
|
|
LOG(ERROR) << "Failed to stop the target pid " << targetPid << ": "
|
|
<< strerror(errno);
|
|
}
|
|
|
|
for (auto it = activeTraps.begin(); it != activeTraps.end();) {
|
|
const auto& tInfo = it->second;
|
|
|
|
/* We don't care about our own traps */
|
|
if (tInfo->trapKind == OID_TRAP_JITCODERET) {
|
|
++it;
|
|
continue;
|
|
}
|
|
|
|
VLOG(1) << "removeTraps removing int3 at " << std::hex << tInfo->trapAddr;
|
|
|
|
errno = 0;
|
|
if (ptrace(PTRACE_POKETEXT, targetPid, tInfo->trapAddr, tInfo->origText) <
|
|
0) {
|
|
LOG(ERROR) << "Execute: Couldn't poke text: " << strerror(errno);
|
|
}
|
|
|
|
it = activeTraps.erase(it);
|
|
}
|
|
|
|
/* Resume the main thread now, so it doesn't have to wait on restoreState */
|
|
if (!contTargetThread(targetPid)) {
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
bool OIDebugger::removeTrap(pid_t pid, const trapInfo& t) {
|
|
std::array<std::byte, 8> repatchedBytes{};
|
|
memcpy(repatchedBytes.data(), t.origTextBytes, repatchedBytes.size());
|
|
|
|
if (auto it = activeTraps.find(t.trapAddr); it != end(activeTraps)) {
|
|
/*
|
|
* `ptrace(POKETEXT)` writes 8 bytes in the target process. If multiple
|
|
* traps are close to each others, the 8 bytes window might overlap with
|
|
* multiple of them. We iter on the items that are within this window and
|
|
* copy their patched bytes over ensuring we don't lose their INT3.
|
|
*/
|
|
constexpr auto windowSize = repatchedBytes.size();
|
|
while (++it != end(activeTraps)) {
|
|
uintptr_t off = it->first - t.trapAddr;
|
|
if (off >= windowSize) {
|
|
break;
|
|
}
|
|
|
|
memcpy(repatchedBytes.data() + off,
|
|
it->second->patchedTextBytes,
|
|
windowSize - off);
|
|
}
|
|
}
|
|
|
|
VLOG(4) << "removeTrap removing int3 at " << std::hex << t.trapAddr;
|
|
if (ptrace(PTRACE_POKETEXT,
|
|
(!pid ? traceePid : pid),
|
|
t.trapAddr,
|
|
*reinterpret_cast<uintptr_t*>(repatchedBytes.data())) < 0) {
|
|
LOG(ERROR) << "Execute: Couldn't poke text: " << strerror(errno);
|
|
return false;
|
|
}
|
|
|
|
activeTraps.erase(t.trapAddr);
|
|
|
|
return true;
|
|
}
|
|
|
|
OIDebugger::OIDebugger(const OICodeGen::Config& genConfig,
|
|
OICompiler::Config ccConfig,
|
|
TreeBuilder::Config tbConfig)
|
|
: cache{genConfig},
|
|
compilerConfig{std::move(ccConfig)},
|
|
generatorConfig{genConfig},
|
|
treeBuilderConfig{std::move(tbConfig)} {
|
|
debug = true;
|
|
|
|
VLOG(1) << "CodeGen config: " << generatorConfig.toString();
|
|
}
|
|
|
|
OIDebugger::OIDebugger(pid_t pid,
|
|
const OICodeGen::Config& genConfig,
|
|
OICompiler::Config ccConfig,
|
|
TreeBuilder::Config tbConfig)
|
|
: OIDebugger(genConfig, std::move(ccConfig), std::move(tbConfig)) {
|
|
traceePid = pid;
|
|
symbols = std::make_shared<SymbolService>(traceePid);
|
|
setDataSegmentSize(dataSegSize);
|
|
createSegmentConfigFile();
|
|
cache.symbols = symbols;
|
|
}
|
|
|
|
OIDebugger::OIDebugger(fs::path debugInfo,
|
|
const OICodeGen::Config& genConfig,
|
|
OICompiler::Config ccConfig,
|
|
TreeBuilder::Config tbConfig)
|
|
: OIDebugger(genConfig, std::move(ccConfig), std::move(tbConfig)) {
|
|
symbols = std::make_shared<SymbolService>(std::move(debugInfo));
|
|
cache.symbols = symbols;
|
|
}
|
|
|
|
/*
|
|
* Copy local buffer from specified address into the remote address in
|
|
* the target process. Note that this only works for mappings that have
|
|
* write permissions set (i.e., mappings that we have created and not
|
|
* standard text mappings).
|
|
*
|
|
* @param[in] local_buffer - buffer containing data to be written
|
|
* @param[in] target_addr - target address where new data are to be written
|
|
* @param[in] bufsz - length of 'target_addr' buffer in bytes
|
|
*/
|
|
bool OIDebugger::writeTargetMemory(void* local_buffer,
|
|
void* target_addr,
|
|
size_t bufsz) const {
|
|
VLOG(1) << "Writing buffer " << std::hex << local_buffer << ", bufsz "
|
|
<< std::dec << bufsz << " into target " << std::hex << target_addr;
|
|
|
|
struct iovec liovecs[] = {{
|
|
.iov_base = local_buffer,
|
|
.iov_len = bufsz,
|
|
}};
|
|
size_t liovcnt = sizeof(liovecs) / sizeof(struct iovec);
|
|
|
|
struct iovec riovecs[] = {{
|
|
.iov_base = target_addr,
|
|
.iov_len = bufsz,
|
|
}};
|
|
size_t riovcnt = sizeof(riovecs) / sizeof(struct iovec);
|
|
|
|
auto writtenBytesCount =
|
|
process_vm_writev(traceePid, liovecs, liovcnt, riovecs, riovcnt, 0);
|
|
if (writtenBytesCount == -1) {
|
|
LOG(ERROR) << "process_vm_writev() error: " << strerror(errno) << " ("
|
|
<< errno << ")";
|
|
return false;
|
|
}
|
|
|
|
if (static_cast<size_t>(writtenBytesCount) != bufsz) {
|
|
LOG(ERROR) << "process_vm_writev() wrote only " << writtenBytesCount
|
|
<< " bytes, expected " << bufsz << " bytes";
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
/*
|
|
* Copy remote buffer from specified address in the target process into
|
|
* the local address.
|
|
*
|
|
* @param[in] remote_buffer - buffer in the target process where the data is
|
|
* read from
|
|
* @param[in] local_addr - local address where new data are to be written
|
|
* @param[in] bufsz - length of 'local_addr' buffer in bytes
|
|
*/
|
|
bool OIDebugger::readTargetMemory(void* remote_buffer,
|
|
void* local_addr,
|
|
size_t bufsz) const {
|
|
VLOG(1) << "Reading buffer " << std::hex << remote_buffer << ", bufsz "
|
|
<< std::dec << bufsz << " into local " << std::hex << local_addr;
|
|
|
|
struct iovec liovecs[] = {{
|
|
.iov_base = local_addr,
|
|
.iov_len = bufsz,
|
|
}};
|
|
size_t liovcnt = sizeof(liovecs) / sizeof(struct iovec);
|
|
|
|
struct iovec riovecs[] = {{
|
|
.iov_base = remote_buffer,
|
|
.iov_len = bufsz,
|
|
}};
|
|
size_t riovcnt = sizeof(riovecs) / sizeof(struct iovec);
|
|
|
|
auto readBytesCount =
|
|
process_vm_readv(traceePid, liovecs, liovcnt, riovecs, riovcnt, 0);
|
|
if (readBytesCount == -1) {
|
|
LOG(ERROR) << "process_vm_readv() error: " << strerror(errno) << " ("
|
|
<< errno << ")";
|
|
return false;
|
|
}
|
|
|
|
if (static_cast<size_t>(readBytesCount) != bufsz) {
|
|
LOG(ERROR) << "process_vm_readv() wrote only " << readBytesCount
|
|
<< " bytes, expected " << bufsz << " bytes";
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
std::optional<std::pair<OIDebugger::ObjectAddrMap::key_type, uintptr_t>>
|
|
OIDebugger::locateJitCodeStart(
|
|
const irequest& req, const OICompiler::RelocResult::SymTable& jitSymbols) {
|
|
// Get type of probed object to locate the JIT code start
|
|
OIDebugger::ObjectAddrMap::key_type targetObj;
|
|
if (req.type == "global") {
|
|
const auto& gd = symbols->findGlobalDesc(req.func);
|
|
if (!gd) {
|
|
LOG(ERROR) << "Failed to find GlobalDesc for " << req.func;
|
|
return std::nullopt;
|
|
}
|
|
|
|
targetObj = gd;
|
|
} else {
|
|
const auto& fd = symbols->findFuncDesc(req);
|
|
if (!fd) {
|
|
LOG(ERROR) << "Failed to find FuncDesc for " << req.func;
|
|
return std::nullopt;
|
|
}
|
|
|
|
const auto& farg = fd->getArgument(req.arg);
|
|
if (!farg) {
|
|
LOG(ERROR) << "Failed to get argument for " << req.func << ':' << req.arg;
|
|
return std::nullopt;
|
|
}
|
|
|
|
targetObj = farg;
|
|
}
|
|
|
|
auto& typeName = std::visit(
|
|
[](auto&& obj) -> std::string& { return obj->typeName; }, targetObj);
|
|
auto typeHash = std::hash<std::string_view>{}(typeName);
|
|
auto jitCodeName = (boost::format("_Z24getSize_%016x") % typeHash).str();
|
|
|
|
uintptr_t jitCodeStart = 0;
|
|
for (const auto& [symName, symAddr] : jitSymbols) {
|
|
if (symName.starts_with(jitCodeName)) {
|
|
jitCodeStart = symAddr;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (jitCodeStart == 0) {
|
|
LOG(ERROR) << "Couldn't find " << jitCodeName << " in symbol table";
|
|
return std::nullopt;
|
|
}
|
|
|
|
return std::make_pair(std::move(targetObj), jitCodeStart);
|
|
}
|
|
|
|
/*
|
|
* Hmmm. I'm pretty sure the JIT'd code sequence is always going to be a
|
|
* non-leaf function at the top level and therefore it's going to finish
|
|
* with a 'ret'. This means that we're going to need to get into it
|
|
* with a 'call' (obviously!). I want to keep the generated code all
|
|
* contained within the mmap'd text area and therefore I need to manually
|
|
* craft a call sequence which will do the following:
|
|
*
|
|
* movabs addr_of_object, %rdi
|
|
* movabs addr_of_entry_top_level_func, %rax
|
|
* call (*rax)
|
|
* movabs regs.rip, %rdi
|
|
* jmpq *(%rdi)
|
|
*
|
|
* Maybe we can do better than this shenanigans but not sure how at the
|
|
* minute. This above sequence will go as a 'prologue' at the start of the
|
|
* mmaped section, before our JIT'd sequence and this will affect the base
|
|
* address of the relocations. NOTE: be very careful that we pass in an
|
|
* address to setBaseRelocAddr() above that takes into account the prologue
|
|
* sequence we are constructing here otherwise the relocations will be
|
|
* wrong. Assume the prologue is 64 bytes.
|
|
*
|
|
* Note that the movabs is a whopper of an instruction at 10 bytes. I'm
|
|
* sure I could do it with less if I need to. Absolute addressing keeps
|
|
* things simple though.
|
|
*
|
|
* A note on rounding. The ptrace peek and poke commands deal with word
|
|
* size operations and this means that we need to round up to a word
|
|
* boundary for our instruction buffers. Therefore we need to pad out the
|
|
* extra space in our new text buffer with NOP instructions to avoid side
|
|
* effects.
|
|
*/
|
|
|
|
bool OIDebugger::writePrologue(
|
|
const prequest& preq, const OICompiler::RelocResult::SymTable& jitSymbols) {
|
|
size_t off = 0;
|
|
uint8_t newInsts[prologueLength];
|
|
|
|
/*
|
|
* Global probes don't have multiple arguments, but calling `getReqForArg(X)`
|
|
* on them still returns the corresponding irequest. We take advantage of that
|
|
* to re-use the same code to generate prologue for both global and func
|
|
* probes.
|
|
*/
|
|
size_t argCount = preq.type == "global" ? 1 : preq.args.size();
|
|
|
|
for (size_t i = 0; i < argCount; i++) {
|
|
const auto& req = preq.getReqForArg(i);
|
|
|
|
auto jitCodeStart = locateJitCodeStart(req, jitSymbols);
|
|
if (!jitCodeStart.has_value()) {
|
|
LOG(ERROR) << "Failed to locate JIT code start for " << req.func << ':'
|
|
<< req.arg;
|
|
return false;
|
|
}
|
|
|
|
VLOG(1) << "Generating prologue for argument '" << req.arg
|
|
<< "', using probe at " << (void*)jitCodeStart->second;
|
|
|
|
newInsts[off++] = movabsrdi0Inst;
|
|
newInsts[off++] = movabsrdi1Inst;
|
|
remoteObjAddrs.emplace(std::move(jitCodeStart->first),
|
|
segConfig.textSegBase + off);
|
|
std::visit([](auto&& obj) { obj = nullptr; },
|
|
jitCodeStart->first); // Invalidate ptr after move
|
|
memcpy(newInsts + off, &objectAddr, sizeof(objectAddr));
|
|
off += sizeof(objectAddr);
|
|
|
|
newInsts[off++] = movabsrax0Inst;
|
|
newInsts[off++] = movabsrax1Inst;
|
|
memcpy(newInsts + off, &jitCodeStart->second, sizeof(jitCodeStart->second));
|
|
off += sizeof(jitCodeStart->second);
|
|
|
|
newInsts[off++] = callRaxInst0Inst;
|
|
newInsts[off++] = callRaxInst1Inst;
|
|
}
|
|
|
|
VLOG(1) << "INT3 at offset " << std::hex << off;
|
|
|
|
auto t = std::make_shared<trapInfo>(OID_TRAP_JITCODERET,
|
|
segConfig.textSegBase + off);
|
|
auto ret = activeTraps.emplace(t->trapAddr, t);
|
|
if (ret.second == false) {
|
|
LOG(ERROR) << "activeTrap element for " << std::hex << t->trapAddr
|
|
<< " already exists (writePrologue error!)";
|
|
}
|
|
|
|
newInsts[off++] = int3Inst;
|
|
|
|
while (off <= prologueLength - sizeofUd2) {
|
|
newInsts[off++] = ud2Inst0;
|
|
newInsts[off++] = ud2Inst1;
|
|
}
|
|
|
|
assert(off <= prologueLength);
|
|
|
|
return writeTargetMemory(
|
|
&newInsts, (void*)segConfig.textSegBase, prologueLength);
|
|
}
|
|
|
|
/*
|
|
* Compile the code that the OICompiler layer knows about. The result of this
|
|
* is that the target processes text segment is populated and ready to go.
|
|
*/
|
|
bool OIDebugger::compileCode() {
|
|
assert(pdata.numReqs() == 1);
|
|
const auto& preq = pdata.getReq();
|
|
|
|
OICompiler compiler{symbols, compilerConfig};
|
|
std::set<fs::path> objectFiles{};
|
|
|
|
/*
|
|
* Global probes don't have multiple arguments, but calling `getReqForArg(X)`
|
|
* on them still returns the corresponding irequest. We take advantage of that
|
|
* to re-use the same code to generate prologue for both global and func
|
|
* probes.
|
|
*/
|
|
size_t argCount = preq.type == "global" ? 1 : preq.args.size();
|
|
for (size_t i = 0; i < argCount; i++) {
|
|
const auto& req = preq.getReqForArg(i);
|
|
|
|
if (cache.isEnabled()) {
|
|
// try to download cache artifacts if present
|
|
if (!downloadCache()) {
|
|
#if OI_PORTABILITY_META_INTERNAL()
|
|
// Send a request to the GOBS service
|
|
char buf[PATH_MAX];
|
|
const std::string procpath =
|
|
"/proc/" + std::to_string(traceePid) + "/exe";
|
|
size_t buf_size;
|
|
if ((buf_size = readlink(procpath.c_str(), buf, PATH_MAX)) == -1) {
|
|
LOG(ERROR)
|
|
<< "Failed to get binary path for tracee, could not call GOBS: "
|
|
<< strerror(errno);
|
|
} else {
|
|
LOG(INFO) << "Attempting to get cache request from gobs";
|
|
ObjectIntrospection::GobsService::requestCache(
|
|
procpath,
|
|
std::string(buf, buf_size),
|
|
req.toString(),
|
|
generatorConfig.toOptions());
|
|
}
|
|
#endif
|
|
LOG(ERROR) << "No cache file found, exiting!";
|
|
return false;
|
|
}
|
|
|
|
if (req.type == "global") {
|
|
decltype(symbols->globalDescs) gds;
|
|
if (cache.load(req, OICache::Entity::GlobalDescs, gds)) {
|
|
symbols->globalDescs.merge(std::move(gds));
|
|
}
|
|
} else {
|
|
decltype(symbols->funcDescs) fds;
|
|
if (cache.load(req, OICache::Entity::FuncDescs, fds)) {
|
|
symbols->funcDescs.merge(std::move(fds));
|
|
}
|
|
}
|
|
}
|
|
|
|
auto sourcePath = cache.getPath(req, OICache::Entity::Source);
|
|
auto objectPath = cache.getPath(req, OICache::Entity::Object);
|
|
auto typeHierarchyPath = cache.getPath(req, OICache::Entity::TypeHierarchy);
|
|
auto paddingInfoPath = cache.getPath(req, OICache::Entity::PaddingInfo);
|
|
|
|
if (!sourcePath || !objectPath || !typeHierarchyPath || !paddingInfoPath) {
|
|
LOG(ERROR) << "Failed to get all cache paths, aborting!";
|
|
return false;
|
|
}
|
|
|
|
bool skipCodeGen = cache.isEnabled() && fs::exists(*objectPath) &&
|
|
fs::exists(*typeHierarchyPath) &&
|
|
fs::exists(*paddingInfoPath);
|
|
if (skipCodeGen) {
|
|
std::pair<RootInfo, TypeHierarchy> th;
|
|
skipCodeGen =
|
|
skipCodeGen && cache.load(req, OICache::Entity::TypeHierarchy, th);
|
|
|
|
std::map<std::string, PaddingInfo> pad;
|
|
skipCodeGen =
|
|
skipCodeGen && cache.load(req, OICache::Entity::PaddingInfo, pad);
|
|
|
|
if (skipCodeGen) {
|
|
typeInfos.emplace(req, std::make_tuple(th.first, th.second, pad));
|
|
}
|
|
}
|
|
|
|
if (!skipCodeGen) {
|
|
VLOG(2) << "Compiling probe for '" << req.arg
|
|
<< "' into: " << *objectPath;
|
|
|
|
auto code = generateCode(req);
|
|
if (!code.has_value()) {
|
|
LOG(ERROR) << "Failed to generate code";
|
|
return false;
|
|
}
|
|
|
|
bool doCompile = !cache.isEnabled() || !fs::exists(*objectPath);
|
|
if (doCompile) {
|
|
if (!compiler.compile(*code, *sourcePath, *objectPath)) {
|
|
LOG(ERROR) << "Failed to compile code";
|
|
return false;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (cache.isEnabled() && !skipCodeGen) {
|
|
if (req.type == "global") {
|
|
cache.store(req, OICache::Entity::GlobalDescs, symbols->globalDescs);
|
|
} else {
|
|
cache.store(req, OICache::Entity::FuncDescs, symbols->funcDescs);
|
|
}
|
|
|
|
const auto& [rootType, typeHierarchy, paddingInfo] = typeInfos.at(req);
|
|
cache.store(req,
|
|
OICache::Entity::TypeHierarchy,
|
|
std::make_pair(rootType, typeHierarchy));
|
|
cache.store(req, OICache::Entity::PaddingInfo, paddingInfo);
|
|
}
|
|
|
|
objectFiles.insert(*objectPath);
|
|
}
|
|
|
|
if (traceePid) { // we attach to a process
|
|
std::unordered_map<std::string, uintptr_t> syntheticSymbols{
|
|
{"dataBase", segConfig.constStart + 0 * sizeof(uintptr_t)},
|
|
{"dataSize", segConfig.constStart + 1 * sizeof(uintptr_t)},
|
|
{"cookieValue", segConfig.constStart + 2 * sizeof(uintptr_t)},
|
|
{"logFile", segConfig.constStart + 3 * sizeof(uintptr_t)},
|
|
};
|
|
|
|
VLOG(2) << "Relocating...";
|
|
for (const auto& o : objectFiles) {
|
|
VLOG(2) << " * " << o;
|
|
}
|
|
auto relocRes = compiler.applyRelocs(
|
|
segConfig.jitCodeStart, objectFiles, syntheticSymbols);
|
|
if (!relocRes.has_value()) {
|
|
LOG(ERROR) << "Failed to relocate object code";
|
|
return false;
|
|
}
|
|
|
|
const auto& [_, segments, jitSymbols] = relocRes.value();
|
|
for (const auto& [symName, symAddr] : jitSymbols) {
|
|
VLOG(2) << "sym " << symName << '@' << std::hex << symAddr;
|
|
}
|
|
|
|
const auto& lastSeg = segments.back();
|
|
auto segmentsLimit = lastSeg.RelocAddr + lastSeg.Size;
|
|
auto remoteSegmentLimit = segConfig.textSegBase + segConfig.textSegSize;
|
|
if (segmentsLimit > remoteSegmentLimit) {
|
|
size_t totalSegmentsSize = segmentsLimit - segConfig.textSegBase;
|
|
LOG(ERROR) << "Generated instruction sequence too large for currently "
|
|
"mapped text segment. Instruction size: "
|
|
<< totalSegmentsSize << " text mapping size: " << textSegSize;
|
|
return false;
|
|
}
|
|
|
|
for (const auto& [BaseAddr, RelocAddr, Size] : segments) {
|
|
if (!writeTargetMemory((void*)BaseAddr, (void*)RelocAddr, Size)) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
if (!writeTargetMemory(&segConfig.dataSegBase,
|
|
(void*)syntheticSymbols["dataBase"],
|
|
sizeof(segConfig.dataSegBase))) {
|
|
LOG(ERROR) << "Failed to write dataSegBase in probe's dataBase";
|
|
return false;
|
|
}
|
|
|
|
if (!writeTargetMemory(&dataSegSize,
|
|
(void*)syntheticSymbols["dataSize"],
|
|
sizeof(dataSegSize))) {
|
|
LOG(ERROR) << "Failed to write dataSegSize in probe's dataSize";
|
|
return false;
|
|
}
|
|
|
|
if (!writeTargetMemory(&segConfig.cookie,
|
|
(void*)syntheticSymbols["cookieValue"],
|
|
sizeof(segConfig.cookie))) {
|
|
LOG(ERROR) << "Failed to write cookie in probe's cookieValue";
|
|
return false;
|
|
}
|
|
|
|
int logFile =
|
|
generatorConfig.features[Feature::JitLogging] ? logFds.traceeFd : 0;
|
|
if (!writeTargetMemory(
|
|
&logFile, (void*)syntheticSymbols["logFile"], sizeof(logFile))) {
|
|
LOG(ERROR) << "Failed to write logFile in probe's cookieValue";
|
|
return false;
|
|
}
|
|
|
|
if (!writePrologue(preq, jitSymbols)) {
|
|
LOG(ERROR) << "Failed to write prologue";
|
|
return false;
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
/* TODO: Needs some cleanup and generally making more resilient */
|
|
void OIDebugger::restoreState(void) {
|
|
/*
|
|
* We are about to detach from the target process.
|
|
* Ensure we don't have any trap in the target process still active.
|
|
*/
|
|
const size_t activeTrapsCount = std::count_if(
|
|
activeTraps.cbegin(), activeTraps.cend(), [](const auto& t) {
|
|
return t.second->trapKind != OID_TRAP_JITCODERET;
|
|
});
|
|
VLOG(1) << "Active traps still within the target process: "
|
|
<< activeTrapsCount;
|
|
assert(activeTrapsCount == 0);
|
|
|
|
metrics::Tracing _("restore_state");
|
|
|
|
int status = 0;
|
|
|
|
/*
|
|
* (1) Check if the thread is already stopped (because it hit a SIGTRAP),
|
|
* (2) If thread is not already stopped, interrupt it so that we can detach
|
|
* from it,
|
|
* (3) If the thread was already stopped because of a SIGTRAP, reset register
|
|
* state to that of the thread when it first came under oid control. The
|
|
* thread could still be in oid JIT code here or trapping in from it normal
|
|
* execution path.
|
|
*/
|
|
for (auto const& p : threadList) {
|
|
auto state = getTaskState(p);
|
|
VLOG(1) << "Task " << p << " state: " << taskStateToString(state) << " ("
|
|
<< static_cast<int>(state) << ")";
|
|
pid_t ret = waitpid(p, &status, WNOHANG | WSTOPPED);
|
|
if (ret < 0) {
|
|
LOG(ERROR) << "Error in waitpid (pid " << p << ")" << strerror(errno);
|
|
} else if (ret == 0) {
|
|
if (WIFSTOPPED(status)) {
|
|
VLOG(1) << "Process " << p << " signal-delivery-stopped "
|
|
<< WSTOPSIG(status);
|
|
} else {
|
|
VLOG(1) << "Process " << p << " not signal-delivery-stopped state";
|
|
}
|
|
|
|
if (WIFSIGNALED(status)) {
|
|
VLOG(1) << "Process " << p << " terminated with signal "
|
|
<< WTERMSIG(status);
|
|
} else {
|
|
VLOG(1) << "Process " << p << " WIFSIGNALED is false ";
|
|
}
|
|
VLOG(1) << "Stopping PID : " << p;
|
|
|
|
if (ptrace(PTRACE_INTERRUPT, p, NULL, NULL) < 0) {
|
|
VLOG(1) << "Couldn't interrupt target pid " << p
|
|
<< " (Reason: " << strerror(errno) << ")";
|
|
}
|
|
VLOG(1) << "Waiting to stop PID : " << p;
|
|
|
|
if (waitpid(p, 0, WSTOPPED) != p) {
|
|
LOG(ERROR) << "failed to wait for process " << p
|
|
<< " (Reason: " << strerror(errno) << ")";
|
|
}
|
|
|
|
VLOG(1) << "Stopped PID : " << p;
|
|
} else if (WSTOPSIG(status) == SIGTRAP) {
|
|
VLOG(1) << "Thread already stopped PID : " << p << " signal is "
|
|
<< WSTOPSIG(status);
|
|
|
|
/*
|
|
* PTRACE_EVENT stop. As noted previously, not sure if this is
|
|
* the correct way for testing for the existence of a ptrace event
|
|
* stop but it seems to work.
|
|
*/
|
|
if (status >> 16) {
|
|
int type = (status >> 16);
|
|
|
|
if (type == PTRACE_EVENT_CLONE || type == PTRACE_EVENT_FORK ||
|
|
type == PTRACE_EVENT_VFORK) {
|
|
/*
|
|
* Detach the new child as it appears to be under our control
|
|
* at this point in its life.
|
|
*/
|
|
unsigned long childPid = 0;
|
|
ptrace(PTRACE_GETEVENTMSG, p, NULL, &childPid);
|
|
|
|
VLOG(1) << "New child being created!! pid " << std::dec << childPid;
|
|
|
|
if (ptrace(PTRACE_DETACH, childPid, 0L, 0L) < 0) {
|
|
LOG(ERROR) << "Couldn't detach target pid " << childPid
|
|
<< " (Reason: " << strerror(errno) << ")";
|
|
} else {
|
|
VLOG(1) << "Successfully detached from pid " << childPid;
|
|
}
|
|
}
|
|
|
|
if (ptrace(PTRACE_DETACH, p, 0L, 0L) < 0) {
|
|
LOG(ERROR) << "Couldn't detach target pid " << p
|
|
<< " (Reason: " << strerror(errno) << ")";
|
|
} else {
|
|
VLOG(1) << "Successfully detached from pid " << p;
|
|
}
|
|
|
|
continue;
|
|
}
|
|
|
|
struct user_regs_struct regs {};
|
|
|
|
/* Find the trapInfo for this tgid */
|
|
if (auto iter{threadTrapState.find(p)};
|
|
iter != std::end(threadTrapState)) {
|
|
auto t{iter->second};
|
|
|
|
/* Paranoia really */
|
|
assert(p == iter->first);
|
|
|
|
struct user_fpregs_struct fpregs {};
|
|
|
|
if (VLOG_IS_ON(1)) {
|
|
errno = 0;
|
|
if (ptrace(PTRACE_GETREGS, p, NULL, ®s) < 0) {
|
|
LOG(ERROR) << "restoreState failed to read registers: "
|
|
<< strerror(errno);
|
|
}
|
|
dumpRegs("Before1", p, ®s);
|
|
}
|
|
|
|
memcpy((void*)®s, (void*)&t->savedRegs, sizeof(regs));
|
|
memcpy((void*)&fpregs, (void*)&t->savedFPregs, sizeof(fpregs));
|
|
|
|
/*
|
|
* Note that we need to rewind the original %rip as it has trapped
|
|
* on an INT3 (which has now been replaced by the original
|
|
* instruction.
|
|
*/
|
|
regs.rip -= sizeofInt3;
|
|
|
|
errno = 0;
|
|
if (ptrace(PTRACE_SETREGS, p, NULL, ®s) < 0) {
|
|
LOG(ERROR) << "restoreState: Couldn't restore registers: "
|
|
<< strerror(errno);
|
|
}
|
|
dumpRegs("After1", p, ®s);
|
|
|
|
errno = 0;
|
|
if (ptrace(PTRACE_SETFPREGS, p, NULL, &fpregs) < 0) {
|
|
LOG(ERROR) << "restorState: Couldn't restore fp registers: "
|
|
<< strerror(errno);
|
|
}
|
|
|
|
VLOG(1) << "Set registers for pid " << std::dec << iter->first;
|
|
} else {
|
|
/*
|
|
* If no trapinfo exists for this thread then it must have just trapped
|
|
* as a result of hitting a traced function and not JIT code. As so,
|
|
* just rewind the %rip register to back it up and let it continue.
|
|
* We're safe to just back it up as the original instructions have
|
|
* been replaced at this point.
|
|
*/
|
|
VLOG(1) << "Couldn't find trapInfo for pid " << std::dec << p;
|
|
|
|
errno = 0;
|
|
if (ptrace(PTRACE_GETREGS, p, NULL, ®s) < 0) {
|
|
LOG(ERROR) << "restoreState SIGTRAP handling: getregs failed - "
|
|
<< strerror(errno);
|
|
}
|
|
|
|
dumpRegs("Before2", p, ®s);
|
|
regs.rip -= sizeofInt3;
|
|
dumpRegs("After2", p, ®s);
|
|
|
|
errno = 0;
|
|
if (ptrace(PTRACE_SETREGS, p, NULL, ®s) < 0) {
|
|
LOG(ERROR) << "restoreState SIGTRAP handling: setregs failed - "
|
|
<< strerror(errno);
|
|
}
|
|
VLOG(1) << "Set registers for thread " << std::dec << p;
|
|
}
|
|
} else {
|
|
LOG(WARNING) << "Thread " << p
|
|
<< " stopped with unknown signal : " << WSTOPSIG(status)
|
|
<< "ret: " << ret << " state: " << taskStateToString(state)
|
|
<< "(" << static_cast<int>(state) << ")";
|
|
|
|
if (state == StatusType::zombie || state == StatusType::dead) {
|
|
/*
|
|
* Pretty sure we don't need to do anything else if the thread
|
|
* is a zombie or dead.
|
|
*/
|
|
continue;
|
|
}
|
|
|
|
if (VLOG_IS_ON(1)) {
|
|
errno = 0;
|
|
struct user_regs_struct regs {};
|
|
if (ptrace(PTRACE_GETREGS, p, NULL, ®s) < 0) {
|
|
LOG(ERROR) << "restoreState unknown sig handling: getregs failed- "
|
|
<< strerror(errno);
|
|
}
|
|
dumpRegs("Unknown Sig", p, ®s);
|
|
}
|
|
}
|
|
|
|
if (!cleanupLogFile())
|
|
LOG(ERROR) << "failed to cleanup log file!";
|
|
|
|
if (ptrace(PTRACE_DETACH, p, 0L, 0L) < 0) {
|
|
LOG(ERROR) << "restoreState Couldn't detach target pid " << p
|
|
<< " (Reason: " << strerror(errno) << ")";
|
|
} else {
|
|
VLOG(1) << "Successfully detached from pid " << p;
|
|
}
|
|
}
|
|
}
|
|
|
|
bool OIDebugger::targetAttach() {
|
|
/*
|
|
* ptrace sucks. It doesn't have a mechanism for grabbing all the threads in
|
|
* a process (even if it's a SEIZE operation). Grabbing all threads seems to
|
|
* be a huge race condition so unless we find a better way to do it, we need
|
|
* to be as defensive as we can.
|
|
*/
|
|
if (mode == OID_MODE_THREAD) {
|
|
if (ptrace(PTRACE_ATTACH, traceePid, NULL, NULL) < 0) {
|
|
LOG(ERROR) << "Couldn't attach to target pid " << traceePid
|
|
<< " (Reason: " << strerror(errno) << ")";
|
|
return false;
|
|
}
|
|
std::cout << "Attached to pid " << traceePid << std::endl;
|
|
} else {
|
|
/* TODO - Handle exceptions */
|
|
auto pidPath = fs::path("/proc") / std::to_string(traceePid) / "task";
|
|
try {
|
|
for (const auto& entry : fs::directory_iterator(pidPath)) {
|
|
auto file = entry.path().filename().string();
|
|
auto pid = std::stoi(file);
|
|
|
|
VLOG(1) << "Seizing thread: " << pid;
|
|
|
|
/*
|
|
* As mentioned above, this code is terribly racy and we need a better
|
|
* solution. If the seize operation fails we will carry on and assume
|
|
* that the thread has exited in between readinf the directory and
|
|
* here (note: ptrace(2) overloads the ESRCH return but with a seize
|
|
* I think it can only mean one thing).
|
|
*/
|
|
if (ptrace(PTRACE_SEIZE,
|
|
pid,
|
|
NULL,
|
|
PTRACE_O_TRACECLONE | PTRACE_O_TRACEFORK |
|
|
PTRACE_O_TRACEVFORK | PTRACE_O_TRACEEXIT) < 0) {
|
|
LOG(ERROR) << "Couldn't seize thread " << pid
|
|
<< " (Reason: " << strerror(errno) << ")";
|
|
} else {
|
|
threadList.push_back(pid);
|
|
}
|
|
}
|
|
} catch (std::filesystem::filesystem_error const& ex) {
|
|
LOG(ERROR) << "directory_iterator exception: " << ex.path1()
|
|
<< ex.code().message();
|
|
|
|
/*
|
|
* As we haven't interrupted any threads yet, any threads which have
|
|
* been seized should be released explicitly when this process exits.
|
|
*/
|
|
return false;
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
bool OIDebugger::stopTarget(void) {
|
|
assert(traceePid > 0);
|
|
|
|
/*
|
|
* Note: PTRACE_ATTACH sends a SIGSTOP to the traced process but PTRACE_SEIZE
|
|
* doesn't do this. I think that both of these interfaces will still end up
|
|
* with the tracee reparented to us for the duration of the trace session
|
|
* though. To interrupt a traced process if PTRACE_SEIZE has been used just
|
|
* use PTRACE_INTERRUPT (this means no signals are delivered which seems
|
|
* like goodness to me).
|
|
*/
|
|
|
|
/*
|
|
* If we are in OID_MODE_FUNC then all threads should have been seized
|
|
* but now but we are only going to stop the main target (as supplied to us).
|
|
* This will be the thread that we will use to set everything up. I would
|
|
* kill for an lwp-agent thread.
|
|
*/
|
|
if (!targetAttach()) {
|
|
return false;
|
|
}
|
|
|
|
if (mode == OID_MODE_FUNC) {
|
|
if (VLOG_IS_ON(1)) {
|
|
OIDebugger::StatusType s = getTaskState(traceePid);
|
|
VLOG(1) << "stopTarget: Interrupting pid " << traceePid
|
|
<< " state: " << static_cast<int>(s);
|
|
} else {
|
|
VLOG(1) << "stopTarget: Interrupting pid " << traceePid;
|
|
}
|
|
|
|
if (ptrace(PTRACE_INTERRUPT, traceePid, NULL, NULL) < 0) {
|
|
LOG(ERROR) << "Couldn't interrupt target pid " << traceePid
|
|
<< " (Reason: " << strerror(errno) << ")";
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/*
|
|
* The PTRACE_ATTACH/INTERRUPT cmds aren't synchronous so the tracee may
|
|
* not be stopped by the time we are here. Wait for it to actually stop.
|
|
*/
|
|
|
|
if (waitpid(traceePid, 0, WSTOPPED) != traceePid) {
|
|
LOG(ERROR) << "stopTarget failed to wait for process " << traceePid
|
|
<< " (Reason: " << strerror(errno) << ")";
|
|
|
|
if (ptrace(PTRACE_DETACH, traceePid, NULL, NULL) < 0) {
|
|
LOG(ERROR) << "stopTarget failed to detach from process " << traceePid
|
|
<< "before exiting";
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
if (VLOG_IS_ON(1)) {
|
|
errno = 0;
|
|
struct user_regs_struct stopregs {};
|
|
if (ptrace(PTRACE_GETREGS, traceePid, NULL, &stopregs) < 0) {
|
|
LOG(ERROR) << "stopTarget getregs failed for process " << traceePid
|
|
<< " : " << strerror(errno);
|
|
|
|
return false;
|
|
}
|
|
|
|
dumpRegs("stopregs: ", traceePid, &stopregs);
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
/*
|
|
* Used by SIGINT handler to close down debugging of the target as the
|
|
* debugger must exit. This means that all tracing must be removed from the
|
|
* target when it is safe to do so.
|
|
*/
|
|
void OIDebugger::stopAll() {
|
|
oidShouldExit = true;
|
|
}
|
|
|
|
void OIDebugger::setDataSegmentSize(size_t size) {
|
|
/* round up to the next page boundary if not aligned */
|
|
int pgsz = getpagesize();
|
|
dataSegSize = ((size + pgsz - 1) & ~(pgsz - 1));
|
|
|
|
VLOG(1) << "setDataSegmentSize: segment size: " << dataSegSize;
|
|
}
|
|
|
|
bool OIDebugger::decodeTargetData(const DataHeader& dataHeader,
|
|
std::vector<uint64_t>& outVec) const {
|
|
VLOG(1) << "== magicId: " << std::hex << dataHeader.magicId;
|
|
VLOG(1) << "== cookie: " << std::hex << dataHeader.cookie;
|
|
VLOG(1) << "== size: " << dataHeader.size;
|
|
|
|
if (dataHeader.magicId != oidMagicId) {
|
|
LOG(ERROR) << "Got a wrong magic ID: " << std::hex << dataHeader.magicId;
|
|
return false;
|
|
}
|
|
|
|
if (dataHeader.cookie != segConfig.cookie) {
|
|
LOG(ERROR) << "Got a wrong cookie: " << std::hex << dataHeader.cookie;
|
|
return false;
|
|
}
|
|
|
|
VLOG(1) << "Total bytes in data segment " << dataHeader.size;
|
|
if (dataHeader.size == 0) {
|
|
LOG(ERROR)
|
|
<< "Data segment is empty. Something went wrong while probing...";
|
|
return false;
|
|
}
|
|
|
|
if (dataSegSize < dataHeader.size) {
|
|
LOG(ERROR) << "Error: Data segment is too small. Needed: "
|
|
<< dataHeader.size << " bytes, dataseg size " << dataSegSize
|
|
<< " bytes";
|
|
return false;
|
|
}
|
|
|
|
if (generatorConfig.features[Feature::JitTiming]) {
|
|
LOG(INFO) << "JIT Timing: " << dataHeader.timeTakenNs << "ns";
|
|
}
|
|
|
|
VLOG(1) << "Pointer tracking stats: " << dataHeader.pointersCapacity << "/"
|
|
<< dataHeader.pointersSize;
|
|
|
|
if (dataHeader.pointersCapacity == dataHeader.pointersSize) {
|
|
VLOG(1) << "Pointer tracking array is exhausted! Results may be"
|
|
" partial.";
|
|
}
|
|
|
|
/*
|
|
* Currently we use MAX_INT to indicate two things:
|
|
* - a single MAX_INT indicates the end of results for the current object
|
|
* - two consecutive MAX_INT's indicate we have finished completely.
|
|
*/
|
|
folly::ByteRange range(dataHeader.data, dataHeader.size - sizeof(dataHeader));
|
|
|
|
outVec.push_back(0);
|
|
outVec.push_back(0);
|
|
outVec.push_back(0);
|
|
outVec.push_back(0);
|
|
uint64_t prevVal = 0;
|
|
|
|
while (true) {
|
|
/* XXX Sort out the sentinel value!!! */
|
|
auto expected = tryDecodeVarint(range);
|
|
if (!expected) {
|
|
std::string s =
|
|
(expected.error() == folly::DecodeVarintError::TooManyBytes)
|
|
? "Invalid varint value: too many bytes."
|
|
: "Invalid varint value: too few bytes.";
|
|
LOG(ERROR) << s;
|
|
return false;
|
|
}
|
|
uint64_t currVal = expected.value();
|
|
|
|
if (currVal == 123456789) {
|
|
if (prevVal == 123456789) {
|
|
break;
|
|
}
|
|
} else {
|
|
outVec.push_back(currVal);
|
|
}
|
|
prevVal = currVal;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
static bool dumpDataSegment(const irequest& req,
|
|
const std::vector<uint64_t>& dataSeg) {
|
|
char dumpPath[PATH_MAX] = {0};
|
|
auto dumpPathSize = snprintf(dumpPath,
|
|
sizeof(dumpPath),
|
|
"/tmp/dataseg.%d.%s.dump",
|
|
getpid(),
|
|
req.arg.c_str());
|
|
if (dumpPathSize < 0 || (size_t)dumpPathSize > sizeof(dumpPath)) {
|
|
LOG(ERROR) << "Failed to generate data-segment path";
|
|
return false;
|
|
}
|
|
|
|
std::ofstream dumpFile{dumpPath, std::ios_base::binary};
|
|
if (!dumpFile) {
|
|
LOG(ERROR) << "Failed to open data-segment file '" << dumpPath
|
|
<< "': " << strerror(errno);
|
|
return false;
|
|
}
|
|
|
|
const auto outVecBytes = std::as_bytes(std::span{dataSeg});
|
|
dumpFile.write((const char*)outVecBytes.data(), outVecBytes.size());
|
|
if (!dumpFile) {
|
|
LOG(ERROR) << "Failed to write to data-segment file '" << dumpPath
|
|
<< "': " << strerror(errno);
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
bool OIDebugger::processTargetData() {
|
|
metrics::Tracing _("process_target_data");
|
|
|
|
std::vector<std::byte> buf{dataSegSize};
|
|
if (!readTargetMemory(reinterpret_cast<void*>(segConfig.dataSegBase),
|
|
buf.data(),
|
|
dataSegSize)) {
|
|
LOG(ERROR) << "Failed to read data segment from target process";
|
|
return false;
|
|
}
|
|
|
|
auto res = reinterpret_cast<uintptr_t>(buf.data());
|
|
|
|
assert(pdata.numReqs() == 1);
|
|
const auto& preq = pdata.getReq();
|
|
|
|
PaddingHunter paddingHunter{};
|
|
TreeBuilder typeTree(treeBuilderConfig);
|
|
|
|
/*
|
|
* Global probes don't have multiple arguments, but calling `getReqForArg(X)`
|
|
* on them still returns the corresponding irequest. We take advantage of that
|
|
* to re-use the same code to generate prologue for both global and func
|
|
* probes.
|
|
*/
|
|
size_t argCount = preq.type == "global" ? 1 : preq.args.size();
|
|
|
|
std::vector<uint64_t> outVec{};
|
|
for (size_t i = 0; i < argCount; i++) {
|
|
const auto& req = preq.getReqForArg(i);
|
|
LOG(INFO) << "Processing data for argument: " << req.arg;
|
|
|
|
const auto& dataHeader = *reinterpret_cast<DataHeader*>(res);
|
|
res += dataHeader.size;
|
|
|
|
outVec.clear();
|
|
if (!decodeTargetData(dataHeader, outVec)) {
|
|
LOG(ERROR) << "Failed to decode target data for arg: " << req.arg;
|
|
return false;
|
|
}
|
|
|
|
if (treeBuilderConfig.dumpDataSegment) {
|
|
if (!dumpDataSegment(req, outVec)) {
|
|
LOG(ERROR) << "Failed to dump data-segment for " << req.arg;
|
|
}
|
|
|
|
// Skip running Tree Builder
|
|
continue;
|
|
}
|
|
|
|
auto typeInfo = typeInfos.find(req);
|
|
if (typeInfo == end(typeInfos)) {
|
|
LOG(ERROR) << "Failed to find corresponding typeInfo for arg: "
|
|
<< req.arg;
|
|
return false;
|
|
}
|
|
|
|
const auto& [rootType, typeHierarchy, paddingInfos] = typeInfo->second;
|
|
VLOG(1) << "Root type addr: " << (void*)rootType.type.type;
|
|
|
|
if (treeBuilderConfig.features[Feature::GenPaddingStats]) {
|
|
paddingHunter.localPaddedStructs = paddingInfos;
|
|
typeTree.setPaddedStructs(&paddingHunter.localPaddedStructs);
|
|
}
|
|
|
|
try {
|
|
typeTree.build(
|
|
outVec, rootType.varName, rootType.type.type, typeHierarchy);
|
|
} catch (std::exception& e) {
|
|
LOG(ERROR) << "Failed to run TreeBuilder for " << req.arg;
|
|
LOG(ERROR) << e.what();
|
|
|
|
if (treeBuilderConfig.dumpDataSegment) {
|
|
LOG(ERROR) << "Data-segment has been dumped for " << req.arg;
|
|
} else {
|
|
LOG(ERROR) << "Dumping data-segment for " << req.arg;
|
|
if (!dumpDataSegment(req, outVec)) {
|
|
LOG(ERROR) << "Failed to dump data-segment for " << req.arg;
|
|
}
|
|
}
|
|
|
|
continue;
|
|
}
|
|
|
|
if (treeBuilderConfig.features[Feature::GenPaddingStats]) {
|
|
paddingHunter.processLocalPaddingInfo();
|
|
}
|
|
}
|
|
|
|
if (treeBuilderConfig.dumpDataSegment) {
|
|
// Tree Builder was not run
|
|
return true;
|
|
}
|
|
|
|
if (typeTree.emptyOutput()) {
|
|
LOG(FATAL)
|
|
<< "Nothing to output: failed to run TreeBuilder on any argument";
|
|
}
|
|
|
|
if (treeBuilderConfig.jsonPath.has_value()) {
|
|
typeTree.dumpJson();
|
|
}
|
|
|
|
if (treeBuilderConfig.features[Feature::GenPaddingStats]) {
|
|
paddingHunter.outputPaddingInfo();
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
std::optional<std::string> OIDebugger::generateCode(const irequest& req) {
|
|
auto root = symbols->getRootType(req);
|
|
if (!root.has_value()) {
|
|
return std::nullopt;
|
|
}
|
|
|
|
std::string code(headers::oi_OITraceCode_cpp);
|
|
|
|
auto codegen = OICodeGen::buildFromConfig(generatorConfig, *symbols);
|
|
if (!codegen) {
|
|
return nullopt;
|
|
}
|
|
|
|
RootInfo rootInfo = *root;
|
|
codegen->setRootType(rootInfo.type);
|
|
if (!codegen->generate(code)) {
|
|
LOG(ERROR) << "Failed to generate code for probe: " << req.type << ":"
|
|
<< req.func << ":" << req.arg;
|
|
return std::nullopt;
|
|
}
|
|
|
|
typeInfos.emplace(
|
|
req,
|
|
std::make_tuple(RootInfo{rootInfo.varName, codegen->getRootType()},
|
|
codegen->getTypeHierarchy(),
|
|
codegen->getPaddingInfo()));
|
|
|
|
if (generatorConfig.features[Feature::TypeGraph]) {
|
|
CodeGen codegen2{generatorConfig, *symbols};
|
|
codegen2.codegenFromDrgn(root->type.type, code);
|
|
}
|
|
|
|
if (auto sourcePath = cache.getPath(req, OICache::Entity::Source)) {
|
|
std::ofstream(*sourcePath) << code;
|
|
}
|
|
|
|
if (!customCodeFile.empty()) {
|
|
auto ifs = std::ifstream(customCodeFile);
|
|
code.assign(std::istreambuf_iterator<char>(ifs),
|
|
std::istreambuf_iterator<char>());
|
|
}
|
|
|
|
// Output JIT source-code after codegen has appended the necessary macros
|
|
// TODO: Maybe accept file path as an arg to dump the generated code
|
|
// instead of stdout
|
|
if (VLOG_IS_ON(3)) {
|
|
// VLOG truncates output, so use std::cout
|
|
VLOG(3) << "Trace code is \n";
|
|
std::cout << code << std::endl;
|
|
VLOG(3) << "Trace code dump finished";
|
|
}
|
|
|
|
return code;
|
|
}
|
|
|
|
} // namespace oi::detail
|