mirror of
https://github.com/baldurk/renderdoc.git
synced 2026-05-12 21:10:42 +00:00
551 lines
14 KiB
C++
551 lines
14 KiB
C++
/******************************************************************************
|
|
* The MIT License (MIT)
|
|
*
|
|
* Copyright (c) 2019-2020 Baldur Karlsson
|
|
*
|
|
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
* of this software and associated documentation files (the "Software"), to deal
|
|
* in the Software without restriction, including without limitation the rights
|
|
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
* copies of the Software, and to permit persons to whom the Software is
|
|
* furnished to do so, subject to the following conditions:
|
|
*
|
|
* The above copyright notice and this permission notice shall be included in
|
|
* all copies or substantial portions of the Software.
|
|
*
|
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
* THE SOFTWARE.
|
|
******************************************************************************/
|
|
|
|
#include <elf.h>
|
|
#include <sys/ptrace.h>
|
|
#include <sys/types.h>
|
|
#include <sys/user.h>
|
|
#include <sys/wait.h>
|
|
#include <time.h>
|
|
#include <unistd.h>
|
|
#include <algorithm>
|
|
#include "api/replay/data_types.h"
|
|
#include "common/common.h"
|
|
#include "common/formatting.h"
|
|
#include "core/core.h"
|
|
#include "core/settings.h"
|
|
#include "os/os_specific.h"
|
|
|
|
RDOC_CONFIG(bool, Linux_PtraceChildProcesses, true,
|
|
"Use ptrace(2) to trace child processes at startup to ensure connection is made as "
|
|
"early as possible.");
|
|
|
|
extern char **environ;
|
|
|
|
// we wait 1ns, then 2ns, then 4ns, etc so our total is 0xfff etc
|
|
// 0xfffff == ~1s
|
|
#define INITIAL_WAIT_TIME 1
|
|
#define MAX_WAIT_TIME 0xfffff
|
|
|
|
char **GetCurrentEnvironment()
|
|
{
|
|
return environ;
|
|
}
|
|
|
|
rdcarray<int> getSockets(pid_t childPid)
|
|
{
|
|
rdcarray<int> sockets;
|
|
rdcstr dirPath = StringFormat::Fmt("/proc/%d/fd", (int)childPid);
|
|
rdcarray<PathEntry> files;
|
|
FileIO::GetFilesInDirectory(dirPath.c_str(), files);
|
|
if(files.empty())
|
|
return sockets;
|
|
|
|
for(const PathEntry &file : files)
|
|
{
|
|
rdcstr target = StringFormat::Fmt("%s/%s", dirPath.c_str(), file.filename.c_str());
|
|
char linkname[1024];
|
|
ssize_t length = readlink(target.c_str(), linkname, 1023);
|
|
if(length == -1)
|
|
continue;
|
|
|
|
linkname[length] = '\0';
|
|
uint32_t inode = 0;
|
|
int num = sscanf(linkname, "socket:[%u]", &inode);
|
|
if(num == 1)
|
|
sockets.push_back(inode);
|
|
}
|
|
return sockets;
|
|
}
|
|
|
|
int GetIdentPort(pid_t childPid)
|
|
{
|
|
int ret = 0;
|
|
|
|
rdcstr procfile = StringFormat::Fmt("/proc/%d/net/tcp", (int)childPid);
|
|
|
|
int waitTime = INITIAL_WAIT_TIME;
|
|
|
|
// try for a little while for the /proc entry to appear
|
|
while(ret == 0 && waitTime <= MAX_WAIT_TIME)
|
|
{
|
|
// back-off for each retry
|
|
usleep(waitTime);
|
|
|
|
waitTime *= 2;
|
|
|
|
FILE *f = FileIO::fopen(procfile.c_str(), "r");
|
|
|
|
if(f == NULL)
|
|
{
|
|
// try again in a bit
|
|
continue;
|
|
}
|
|
|
|
rdcarray<int> sockets = getSockets(childPid);
|
|
|
|
// read through the proc file to check for an open listen socket
|
|
while(ret == 0 && !feof(f))
|
|
{
|
|
const size_t sz = 512;
|
|
char line[sz];
|
|
line[sz - 1] = 0;
|
|
fgets(line, sz - 1, f);
|
|
|
|
// an example for a line:
|
|
// sl local_address rem_address st tx_queue rx_queue tr tm->when retrnsmt uid timeout inode
|
|
// 0: 00000000:9808 00000000:0000 0A 00000000:00000000 00:00000000 00000000 1000 0 109747
|
|
|
|
int socketnum = 0, hexip = 0, hexport = 0, inode = 0;
|
|
int num = sscanf(line, " %d: %x:%x %*x:%*x %*x %*x:%*x %*x:%*x %*x %*d %*d %d", &socketnum,
|
|
&hexip, &hexport, &inode);
|
|
|
|
// find open listen socket on 0.0.0.0:port
|
|
if(num == 4 && hexip == 0 && hexport >= RenderDoc_FirstTargetControlPort &&
|
|
hexport <= RenderDoc_LastTargetControlPort && sockets.contains(inode))
|
|
{
|
|
ret = hexport;
|
|
}
|
|
}
|
|
|
|
FileIO::fclose(f);
|
|
}
|
|
|
|
if(ret == 0)
|
|
{
|
|
RDCWARN("Couldn't locate renderdoc target control listening port between %u and %u in %s",
|
|
(uint32_t)RenderDoc_FirstTargetControlPort, (uint32_t)RenderDoc_LastTargetControlPort,
|
|
procfile.c_str());
|
|
|
|
if(!FileIO::exists(procfile.c_str()))
|
|
{
|
|
RDCWARN("Process %u is no longer running - did it exit during initialisation or fail to run?",
|
|
childPid);
|
|
}
|
|
}
|
|
|
|
return ret;
|
|
}
|
|
|
|
static bool ptrace_scope_ok()
|
|
{
|
|
if(!Linux_PtraceChildProcesses())
|
|
return false;
|
|
|
|
rdcstr contents;
|
|
FileIO::ReadAll("/proc/sys/kernel/yama/ptrace_scope", contents);
|
|
contents.trim();
|
|
if(!contents.empty())
|
|
{
|
|
int ptrace_scope = atoi(contents.c_str());
|
|
if(ptrace_scope > 1)
|
|
{
|
|
if(RenderDoc::Inst().IsReplayApp())
|
|
{
|
|
static bool warned = false;
|
|
if(!warned)
|
|
{
|
|
warned = true;
|
|
RDCWARN(
|
|
"ptrace_scope value %d means ptrace can't be used to pause child processes while "
|
|
"attaching.",
|
|
ptrace_scope);
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
static uint64_t get_nanotime()
|
|
{
|
|
timespec ts;
|
|
clock_gettime(CLOCK_MONOTONIC, &ts);
|
|
uint64_t ret = uint64_t(ts.tv_sec) * 1000000000ULL + uint32_t(ts.tv_nsec & 0xffffffff);
|
|
return ret;
|
|
}
|
|
|
|
#if ENABLED(RDOC_X64)
|
|
#define INST_PTR_REG rip
|
|
#else
|
|
#define INST_PTR_REG eip
|
|
#endif
|
|
|
|
static uint64_t get_child_ip(pid_t childPid)
|
|
{
|
|
user_regs_struct regs = {};
|
|
|
|
long ptraceRet = ptrace(PTRACE_GETREGS, childPid, NULL, ®s);
|
|
if(ptraceRet == 0)
|
|
return uint64_t(regs.INST_PTR_REG);
|
|
|
|
return 0;
|
|
}
|
|
|
|
static bool wait_traced_child(pid_t childPid, uint32_t timeoutMS, int &status)
|
|
{
|
|
// spin waiting for the traced child, with a 100ms timeout
|
|
status = 0;
|
|
uint64_t start_nano = get_nanotime();
|
|
uint64_t end_nano = 0;
|
|
int ret = 0;
|
|
|
|
const uint64_t timeoutNanoseconds = uint64_t(timeoutMS) * 1000 * 1000;
|
|
|
|
while((ret = waitpid(childPid, &status, WNOHANG)) == 0)
|
|
{
|
|
status = 0;
|
|
|
|
// if we're in a capturing process then the process itself might have done waitpid(-1) and
|
|
// swallowed the wait for our child. So as an alternative we check to see if we can query the
|
|
// instruction pointer, which is only possible if the child is stopped.
|
|
uint64_t ip = get_child_ip(childPid);
|
|
if(ip != 0)
|
|
{
|
|
// do waitpid again in case we raced and the child stopped in between the call to waitpid and
|
|
// get_child_ip.
|
|
ret = waitpid(childPid, &status, WNOHANG);
|
|
|
|
// if it still didn't succeed, set status to 0 so we know we're earlying out and don't check
|
|
// the status codes.
|
|
if(ret == 0)
|
|
status = 0;
|
|
return true;
|
|
}
|
|
|
|
usleep(10);
|
|
|
|
// check the timeout
|
|
end_nano = get_nanotime();
|
|
if(end_nano - start_nano > timeoutNanoseconds)
|
|
break;
|
|
}
|
|
|
|
return WIFSTOPPED(status);
|
|
}
|
|
|
|
bool StopChildAtMain(pid_t childPid)
|
|
{
|
|
// don't do this unless the ptrace scope is OK.
|
|
if(!ptrace_scope_ok())
|
|
return false;
|
|
|
|
int childStatus = 0;
|
|
|
|
// we have a low timeout for this stop since it should happen almost immediately (right after the
|
|
// fork). If it didn't then we want to fail relatively fast.
|
|
if(!wait_traced_child(childPid, 100, childStatus))
|
|
{
|
|
RDCERR("Didn't get initial stop from child PID %u", childPid);
|
|
return false;
|
|
}
|
|
|
|
if(childStatus > 0 && WSTOPSIG(childStatus) != SIGSTOP)
|
|
{
|
|
RDCERR("Initial signal from child PID %u was %x, expected %x", childPid, WSTOPSIG(childStatus),
|
|
SIGSTOP);
|
|
return false;
|
|
}
|
|
|
|
long ptraceRet = 0;
|
|
|
|
// continue until exec
|
|
ptraceRet = ptrace(PTRACE_SETOPTIONS, childPid, NULL, PTRACE_O_TRACEEXEC);
|
|
RDCASSERTEQUAL(ptraceRet, 0);
|
|
|
|
// continue
|
|
ptraceRet = ptrace(PTRACE_CONT, childPid, NULL, NULL);
|
|
RDCASSERTEQUAL(ptraceRet, 0);
|
|
|
|
// we're not under control of when the application calls exec() after fork() in the case of child
|
|
// processes, so be a little more generous with the timeout
|
|
if(!wait_traced_child(childPid, 250, childStatus))
|
|
{
|
|
RDCERR("Didn't get to execve in child PID %u", childPid);
|
|
return false;
|
|
}
|
|
|
|
if(childStatus > 0 && (childStatus >> 8) != (SIGTRAP | (PTRACE_EVENT_EXEC << 8)))
|
|
{
|
|
RDCERR("Exec wait event from child PID %u was status %x, expected %x", childPid,
|
|
(childStatus >> 8), (SIGTRAP | (PTRACE_EVENT_EXEC << 8)));
|
|
return false;
|
|
}
|
|
|
|
rdcstr exepath;
|
|
long basePointer = 0;
|
|
uint32_t sectionOffset = 0;
|
|
|
|
rdcstr mapsName = StringFormat::Fmt("/proc/%u/maps", childPid);
|
|
|
|
FILE *maps = FileIO::fopen(mapsName.c_str(), "r");
|
|
|
|
if(!maps)
|
|
{
|
|
RDCERR("Couldn't open %s", mapsName.c_str());
|
|
return false;
|
|
}
|
|
|
|
while(!feof(maps))
|
|
{
|
|
char line[512] = {0};
|
|
if(fgets(line, 511, maps))
|
|
{
|
|
if(strstr(line, "r-xp"))
|
|
{
|
|
RDCCOMPILE_ASSERT(sizeof(long) == sizeof(void *), "Expected long to be pointer sized");
|
|
int pathOffset = 0;
|
|
int num = sscanf(line, "%lx-%*x r-xp %x %*x:%*x %*u %n", &basePointer, §ionOffset,
|
|
&pathOffset);
|
|
|
|
if(num != 2 || pathOffset == 0)
|
|
{
|
|
RDCERR("Couldn't parse first executable mapping '%s'", rdcstr(line).trimmed().c_str());
|
|
return false;
|
|
}
|
|
|
|
exepath = line + pathOffset;
|
|
exepath.trim();
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
if(basePointer == 0)
|
|
{
|
|
RDCERR("Couldn't find executable mapping in maps file");
|
|
return false;
|
|
}
|
|
|
|
FileIO::fclose(maps);
|
|
|
|
FILE *elf = FileIO::fopen(exepath.c_str(), "r");
|
|
|
|
if(!elf)
|
|
{
|
|
RDCERR("Couldn't open %s to parse ELF header", exepath.c_str());
|
|
return false;
|
|
}
|
|
|
|
Elf64_Ehdr elf_header;
|
|
size_t read = FileIO::fread(&elf_header, sizeof(elf_header), 1, elf);
|
|
FileIO::fclose(elf);
|
|
|
|
if(read != 1)
|
|
{
|
|
RDCERR("Couldn't read ELF header from %s", exepath.c_str());
|
|
return false;
|
|
}
|
|
|
|
void *entry = (void *)(basePointer + elf_header.e_entry - sectionOffset);
|
|
|
|
long origEntryWord = ptrace(PTRACE_PEEKTEXT, childPid, entry, 0);
|
|
|
|
long breakpointWord = (origEntryWord & 0xffffff00) | 0xcc;
|
|
ptraceRet = ptrace(PTRACE_POKETEXT, childPid, entry, breakpointWord);
|
|
RDCASSERTEQUAL(ptraceRet, 0);
|
|
|
|
// continue
|
|
ptraceRet = ptrace(PTRACE_CONT, childPid, NULL, NULL);
|
|
RDCASSERTEQUAL(ptraceRet, 0);
|
|
|
|
// it could take a long time to hit main so we have a large timeout here
|
|
if(!wait_traced_child(childPid, 2000, childStatus))
|
|
{
|
|
RDCERR("Didn't hit breakpoint in PID %u (%x)", childPid, childStatus);
|
|
return false;
|
|
}
|
|
|
|
// we're now at main! now just need to clean up after ourselves
|
|
|
|
user_regs_struct regs = {};
|
|
|
|
ptraceRet = ptrace(PTRACE_GETREGS, childPid, NULL, ®s);
|
|
RDCASSERTEQUAL(ptraceRet, 0);
|
|
|
|
// step back past the byte we inserted the breakpoint on
|
|
regs.INST_PTR_REG--;
|
|
ptraceRet = ptrace(PTRACE_SETREGS, childPid, NULL, ®s);
|
|
RDCASSERTEQUAL(ptraceRet, 0);
|
|
|
|
// restore the function
|
|
ptraceRet = ptrace(PTRACE_POKETEXT, childPid, entry, origEntryWord);
|
|
RDCASSERTEQUAL(ptraceRet, 0);
|
|
|
|
// we'll resume after reading the ident port in the calling function
|
|
return true;
|
|
}
|
|
|
|
void StopAtMainInChild()
|
|
{
|
|
// don't do this unless the ptrace scope is OK.
|
|
if(!ptrace_scope_ok())
|
|
return;
|
|
|
|
// allow parent tracing, and immediately stop so the parent process can attach
|
|
ptrace(PTRACE_TRACEME, 0, 0, 0);
|
|
raise(SIGSTOP);
|
|
}
|
|
|
|
void ResumeProcess(pid_t childPid, uint32_t delaySeconds)
|
|
{
|
|
if(childPid != 0)
|
|
{
|
|
// if we have a delay, see if the process is paused. If so then detach it but keep it stopped
|
|
// and wait to see if someone attaches
|
|
if(delaySeconds > 0)
|
|
{
|
|
uint64_t ip = get_child_ip(childPid);
|
|
|
|
if(ip != 0)
|
|
{
|
|
// detach but stop, to allow a debugger to attach
|
|
ptrace(PTRACE_DETACH, childPid, NULL, SIGSTOP);
|
|
|
|
rdcstr filename = StringFormat::Fmt("/proc/%u/status", childPid);
|
|
|
|
uint64_t start_nano = get_nanotime();
|
|
uint64_t end_nano = 0;
|
|
|
|
const uint64_t timeoutNanoseconds = uint64_t(delaySeconds) * 1000 * 1000 * 1000;
|
|
|
|
bool connected = false;
|
|
|
|
// watch for a tracer to attach
|
|
do
|
|
{
|
|
usleep(10);
|
|
|
|
rdcstr status;
|
|
FileIO::ReadAll(filename, status);
|
|
|
|
int32_t offs = status.find("TracerPid:");
|
|
|
|
if(offs < 0)
|
|
break;
|
|
|
|
status.erase(0, offs + sizeof("TracerPid:"));
|
|
status.trim();
|
|
|
|
end_nano = get_nanotime();
|
|
|
|
if(status[0] != '0')
|
|
{
|
|
RDCLOG("Debugger PID %u attached after %f seconds", atoi(status.c_str()),
|
|
double(end_nano - start_nano) / 1000000000.0);
|
|
connected = true;
|
|
break;
|
|
}
|
|
} while(end_nano - start_nano < timeoutNanoseconds);
|
|
|
|
if(!connected)
|
|
{
|
|
RDCLOG("Timed out waiting for debugger, resuming");
|
|
kill(childPid, SIGCONT);
|
|
}
|
|
return;
|
|
}
|
|
else
|
|
{
|
|
RDCERR("Can't delay for debugger without ptrace, check ptrace_scope value");
|
|
}
|
|
}
|
|
|
|
// try to detach and resume the process, ignoring any errors if we weren't tracing
|
|
ptrace(PTRACE_DETACH, childPid, NULL, NULL);
|
|
}
|
|
}
|
|
|
|
// because OSUtility::DebuggerPresent is called often we want it to be
|
|
// cheap. Opening and parsing a file would cause high overhead on each
|
|
// call, so instead we just cache it at startup. This fails in the case
|
|
// of attaching to processes
|
|
bool debuggerPresent = false;
|
|
|
|
void CacheDebuggerPresent()
|
|
{
|
|
FILE *f = FileIO::fopen("/proc/self/status", "r");
|
|
|
|
if(f == NULL)
|
|
{
|
|
RDCWARN("Couldn't open /proc/self/status");
|
|
return;
|
|
}
|
|
|
|
// read through the proc file to check for TracerPid
|
|
while(!feof(f))
|
|
{
|
|
const size_t sz = 512;
|
|
char line[sz];
|
|
line[sz - 1] = 0;
|
|
fgets(line, sz - 1, f);
|
|
|
|
int tracerpid = 0;
|
|
int num = sscanf(line, "TracerPid: %d", &tracerpid);
|
|
|
|
// found TracerPid line
|
|
if(num == 1)
|
|
{
|
|
debuggerPresent = (tracerpid != 0);
|
|
break;
|
|
}
|
|
}
|
|
|
|
FileIO::fclose(f);
|
|
}
|
|
|
|
bool OSUtility::DebuggerPresent()
|
|
{
|
|
return debuggerPresent;
|
|
}
|
|
|
|
const char *Process::GetEnvVariable(const char *name)
|
|
{
|
|
return getenv(name);
|
|
}
|
|
|
|
uint64_t Process::GetMemoryUsage()
|
|
{
|
|
FILE *f = FileIO::fopen("/proc/self/statm", "r");
|
|
|
|
if(f == NULL)
|
|
{
|
|
RDCWARN("Couldn't open /proc/self/statm");
|
|
return 0;
|
|
}
|
|
|
|
char line[512] = {};
|
|
fgets(line, 511, f);
|
|
|
|
uint32_t vmPages = 0;
|
|
int num = sscanf(line, "%u", &vmPages);
|
|
|
|
if(num == 1 && vmPages > 0)
|
|
return vmPages * (uint64_t)sysconf(_SC_PAGESIZE);
|
|
|
|
return 0;
|
|
}
|