mirror of
https://github.com/ioacademy-jikim/debugging
synced 2025-06-07 16:06:09 +00:00
531 lines
16 KiB
C
531 lines
16 KiB
C
/*--------------------------------------------------------------------*/
|
|
/*--- Implementation of vgdb invoker subsystem on Solaris */
|
|
/* via /proc filesystem and control messages. ---*/
|
|
/*--------------------------------------------------------------------*/
|
|
|
|
/*
|
|
This file is part of Valgrind, a dynamic binary instrumentation
|
|
framework.
|
|
|
|
Copyright (C) 2014-2015 Ivo Raisr <ivosh@ivosh.net>
|
|
|
|
This program is free software; you can redistribute it and/or
|
|
modify it under the terms of the GNU General Public License as
|
|
published by the Free Software Foundation; either version 2 of the
|
|
License, or (at your option) any later version.
|
|
|
|
This program is distributed in the hope that it will be useful, but
|
|
WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
|
General Public License for more details.
|
|
|
|
You should have received a copy of the GNU General Public License
|
|
along with this program; if not, write to the Free Software
|
|
Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA
|
|
02111-1307, USA.
|
|
|
|
The GNU General Public License is contained in the file COPYING.
|
|
*/
|
|
|
|
/* This module implements vgdb-invoker subsystem as per vgdb.h
|
|
on Solaris. It differs significantly from the other ptrace-based
|
|
implementation found in vgdb-invoker-ptrace.c. However the goal
|
|
is the same - to work on the following scenario:
|
|
|
|
- A valgrind process (referred to also as an inferior process)
|
|
is remotely debugged with gdb.
|
|
- All threads of the inferior process are stuck in blocking
|
|
syscalls.
|
|
- Therefore no thread can process packets received from gdb.
|
|
|
|
When module vgdb.c detects this situation then it calls
|
|
function invoker_invoke_gdbserver() within the context of
|
|
invoke_gdbserver_in_valgrind_thread thread. The steps of
|
|
interaction between vgdb and m_gdbserver module are as follows:
|
|
|
|
1. Function invoker_invoke_gdbserver() attaches to the inferior
|
|
process and stops all threads.
|
|
2. It gets registers of the first thread and modifies them
|
|
and the stack so that a call to "invoke_gdbserver" function
|
|
is arranged along with a function parameter.
|
|
3. Then it creates an agent thread within the inferior process
|
|
with these modified registers and waits until the agent thread
|
|
exits.
|
|
4. Meanwhile in the inferior process function
|
|
VG_(invoke_gdbserver)() is invoked within the context of the
|
|
agent thread; all other threads are still stopped.
|
|
5. The agent thread processes packets from gdb relayed by vgdb.
|
|
6. Eventually processing is finished and the agent thread exits
|
|
in function give_control_back_to_vgdb().
|
|
7. vgdb then detaches from the inferior process and thus resumes
|
|
all the stopped threads.
|
|
*/
|
|
|
|
#include "vgdb.h"
|
|
|
|
#include <assert.h>
|
|
#include <errno.h>
|
|
#include <string.h>
|
|
|
|
typedef Addr CORE_ADDR;
|
|
|
|
typedef struct {
|
|
long cmd;
|
|
union {
|
|
long flags;
|
|
prgregset_t regs;
|
|
} arg;
|
|
} ctl_t;
|
|
|
|
/* Process control file /proc/<pid>/ctl.
|
|
Once this file is closed, PR_RLC flag takes effect and
|
|
inferior process resumes automatically. */
|
|
static int ctl_fd = -1;
|
|
|
|
/* Copy LEN bytes of data from vgdb memory at MYADDR
|
|
to valgrind memory at MEMADDR.
|
|
On failure (cannot write the valgrind memory)
|
|
returns the value of errno. */
|
|
static int write_memory(pid_t pid, CORE_ADDR memaddr,
|
|
const void *myaddr, size_t len)
|
|
{
|
|
char procname[PATH_MAX];
|
|
snprintf(procname, sizeof(procname), "/proc/%d/as", pid);
|
|
|
|
/* Open the process address-space file. */
|
|
int as_fd = open(procname, O_WRONLY, 0);
|
|
if (as_fd < 0) {
|
|
int error = errno;
|
|
ERROR(error, "Failed to open %s.\n", procname);
|
|
return error;
|
|
}
|
|
|
|
if (debuglevel >= 1) {
|
|
DEBUG(1, "Writing bytes '");
|
|
size_t i;
|
|
for (i = 0; i < len; i++)
|
|
PDEBUG(1, "%02x", ((const unsigned char *) myaddr)[i]);
|
|
PDEBUG(1, "' to address %#lx.\n", memaddr);
|
|
}
|
|
|
|
ssize_t written = pwrite(as_fd, myaddr, len, memaddr);
|
|
if ((written < 0) || (written != len)) {
|
|
int error = errno;
|
|
ERROR(error, "Failed to write to file %s, memory block of %zu"
|
|
" bytes at %#lx to %#lx.\n",
|
|
procname, len, (Addr) myaddr, memaddr);
|
|
close(as_fd);
|
|
return error;
|
|
}
|
|
|
|
DEBUG(1, "Written ok.\n");
|
|
close(as_fd);
|
|
return 0;
|
|
}
|
|
|
|
/* Attaches to a process identified by pid and stops all threads. */
|
|
static Bool attach(pid_t pid)
|
|
{
|
|
char procname[PATH_MAX];
|
|
snprintf(procname, sizeof(procname), "/proc/%d/ctl", pid);
|
|
|
|
DEBUG(1, "Attaching to pid %d.\n", pid);
|
|
|
|
/* Open the process control file. */
|
|
ctl_fd = open(procname, O_WRONLY, 0);
|
|
if (ctl_fd < 0) {
|
|
ERROR(errno, "Failed to open %s.\n", procname);
|
|
return False;
|
|
}
|
|
|
|
DEBUG(1, "Setting run-on-last-close-flag (PR_RLC) to pid %d.\n", pid);
|
|
|
|
/* Set run-on-last-close flag. */
|
|
ctl_t ctl;
|
|
ctl.cmd = PCSET;
|
|
ctl.arg.flags = PR_RLC;
|
|
size_t bytes = sizeof(ctl.cmd) + sizeof(ctl.arg.flags);
|
|
ssize_t written = write(ctl_fd, (void *) &ctl, bytes);
|
|
if ((written < 0) || (written != bytes)) {
|
|
ERROR(errno, "Failed to write to ctl_fd: PCSET + PR_RLC.\n");
|
|
return False;
|
|
}
|
|
|
|
DEBUG(1, "Stopping process %d.\n", pid);
|
|
|
|
/* Stop the whole process - all threads. */
|
|
ctl.cmd = PCSTOP;
|
|
bytes = sizeof(ctl.cmd);
|
|
written = write(ctl_fd, (void *) &ctl, bytes);
|
|
if ((written < 0) || (written != bytes)) {
|
|
ERROR(errno, "Failed to write to ctl_fd: PCSTOP.\n");
|
|
return False;
|
|
}
|
|
|
|
DEBUG(1, "Process %d stopped.\n", pid);
|
|
|
|
/* Now confirm it is actually the case. */
|
|
snprintf(procname, sizeof(procname), "/proc/%d/status", pid);
|
|
int status_fd = open(procname, O_RDONLY, 0);
|
|
if (status_fd < 0) {
|
|
ERROR(errno, "Failed to open %s.\n", procname);
|
|
return False;
|
|
}
|
|
|
|
pstatus_t pstatus;
|
|
bytes = read(status_fd, &pstatus, sizeof(pstatus));
|
|
if ((bytes < 0) || (bytes != sizeof(pstatus))) {
|
|
ERROR(errno, "Failed to read from %s.\n", procname);
|
|
close(status_fd);
|
|
return False;
|
|
}
|
|
|
|
if (pstatus.pr_flags & PR_RLC) {
|
|
DEBUG(2, "Process %d has run-on-last-close flag set. Good.\n", pid);
|
|
} else {
|
|
ERROR(0, "Process %d does not have run-on-last-close flag set!\n", pid);
|
|
close(status_fd);
|
|
return False;
|
|
}
|
|
|
|
if (pstatus.pr_lwp.pr_flags & PR_STOPPED) {
|
|
DEBUG(3, "Process %d seems to be stopped. Good.\n", pid);
|
|
} else {
|
|
ERROR(0, "Process %d is not stopped!\n", pid);
|
|
close(status_fd);
|
|
return False;
|
|
}
|
|
|
|
close(status_fd);
|
|
return True;
|
|
}
|
|
|
|
static void detach(pid_t pid)
|
|
{
|
|
if (ctl_fd != -1) {
|
|
close(ctl_fd);
|
|
ctl_fd = -1;
|
|
}
|
|
|
|
DEBUG(1, "Detached from pid %d.\n", pid);
|
|
}
|
|
|
|
/* Gets the registers of the first thread. */
|
|
static Bool get_regs(pid_t pid, prgregset_t *regs)
|
|
{
|
|
char procname[PATH_MAX];
|
|
snprintf(procname, sizeof(procname), "/proc/%d/lwp/1/lwpstatus", pid);
|
|
|
|
DEBUG(1, "Getting registers from the first thread of process %d.\n", pid);
|
|
|
|
/* Open the first thread's status file. */
|
|
int status_fd = open(procname, O_RDONLY, 0);
|
|
if (status_fd < 0) {
|
|
ERROR(errno, "Failed to open file %s.\n", procname);
|
|
return False;
|
|
}
|
|
|
|
lwpstatus_t status;
|
|
ssize_t bytes = read(status_fd, &status, sizeof(status));
|
|
if ((bytes < 0) || (bytes != sizeof(status))) {
|
|
ERROR(errno, "Failed to read from %s.\n", procname);
|
|
close(status_fd);
|
|
return False;
|
|
}
|
|
|
|
DEBUG(3, "Registers of thread %d from process %d: ", status.pr_lwpid, pid);
|
|
unsigned int i;
|
|
for (i = 0; i < _NGREG; i++) {
|
|
PDEBUG(3, "%u: %#lx, ", i, (unsigned long) status.pr_reg[i]);
|
|
}
|
|
PDEBUG(3, "\n");
|
|
|
|
memcpy(regs, &status.pr_reg, sizeof(prgregset_t));
|
|
close(status_fd);
|
|
return True;
|
|
}
|
|
|
|
/* Modifies the register set so that a new stack frame is created
|
|
for "invoke_gdbserver" function with an extra argument.
|
|
The argument is written to the stack of the first thread.
|
|
*/
|
|
static Bool setup_stack_frame(pid_t pid, prgregset_t *regs)
|
|
{
|
|
DEBUG(1, "Setting up new stack frame of process %d.\n", pid);
|
|
|
|
/* A specific int value is passed to invoke_gdbserver(), to check
|
|
everything goes according to the plan. */
|
|
const int check = 0x8BADF00D; // ate bad food.
|
|
|
|
/* A bad return address will be pushed on the stack.
|
|
Function invoke_gdbserver() cannot return. If it ever returns,
|
|
a NULL address pushed on the stack should ensure this is
|
|
detected. */
|
|
const Addr bad_return = 0;
|
|
|
|
#if defined(VGA_x86)
|
|
Addr sp = (*regs)[UESP];
|
|
#elif defined(VGA_amd64)
|
|
Addr sp = (*regs)[REG_RSP];
|
|
#else
|
|
I_die_here : (sp) architecture missing in vgdb-invoker-solaris.c
|
|
#endif
|
|
|
|
if (shared32 != NULL) {
|
|
/* vgdb speaking with a 32bit executable. */
|
|
#if defined(VGA_x86) || defined(VGA_amd64)
|
|
const size_t regsize = 4;
|
|
|
|
/* Push check argument on the stack - according to C/ia32 ABI. */
|
|
sp = sp - regsize;
|
|
DEBUG(1, "Pushing check argument to process %d memory.\n", pid);
|
|
assert(regsize == sizeof(check));
|
|
int error = write_memory(pid, sp, &check, regsize);
|
|
if (error != 0) {
|
|
ERROR(error, "Failed to push check argument to process %d memory.\n",
|
|
pid);
|
|
detach(pid);
|
|
return False;
|
|
}
|
|
|
|
sp = sp - regsize;
|
|
DEBUG(1, "Pushing bad_return return address to process %d memory.\n",
|
|
pid);
|
|
/* Note that even for a 64 bits vgdb, only 4 bytes
|
|
of NULL bad_return are written. */
|
|
error = write_memory(pid, sp, &bad_return, regsize);
|
|
if (error != 0) {
|
|
ERROR(error, "Failed to push bad_return return address to process %d "
|
|
"memory.\n", pid);
|
|
detach(pid);
|
|
return False;
|
|
}
|
|
|
|
#if defined(VGA_x86)
|
|
/* Set EBP, ESP, EIP to invoke gdbserver.
|
|
vgdb is 32bits, speaking with a 32bits process. */
|
|
(*regs)[EBP] = sp; // bp set to sp
|
|
(*regs)[UESP] = sp;
|
|
(*regs)[EIP] = shared32->invoke_gdbserver;
|
|
#elif defined(VGA_amd64)
|
|
/* Set RBP, RSP, RIP to invoke gdbserver.
|
|
vgdb is 64bits, speaking with a 32bits process. */
|
|
(*regs)[REG_RBP] = sp; // bp set to sp
|
|
(*regs)[REG_RSP] = sp;
|
|
(*regs)[REG_RIP] = shared32->invoke_gdbserver;
|
|
#else
|
|
I_die_here : not x86 or amd64 in x86/amd64 section/
|
|
#endif
|
|
|
|
#else
|
|
I_die_here : architecture missing in vgdb-invoker-solaris.c
|
|
#endif
|
|
|
|
} else if (shared64 != NULL) {
|
|
#if defined(VGA_x86)
|
|
assert(0); /* 64bits process with a 32bits vgdb - no way */
|
|
#elif defined(VGA_amd64)
|
|
/* 64bits vgdb speaking with a 64 bit process. */
|
|
const int regsize = 8;
|
|
|
|
/* Give check argument in rdi - according to C/amd64 ABI. */
|
|
(*regs)[REG_RDI] = check;
|
|
|
|
/* Push return address on stack: return to breakaddr. */
|
|
sp = sp - regsize;
|
|
DEBUG(1, "Pushing bad_return return address to process %d memory.\n",
|
|
pid);
|
|
int error = write_memory(pid, sp, &bad_return,
|
|
sizeof(bad_return));
|
|
if (error != 0) {
|
|
ERROR(error, "Failed to push bad_return return address to process %d "
|
|
"memory.\n", pid);
|
|
detach(pid);
|
|
return False;
|
|
}
|
|
|
|
/* set RBP, RSP, RIP to invoke gdbserver */
|
|
(*regs)[REG_RBP] = sp; // bp set to sp
|
|
(*regs)[REG_RSP] = sp;
|
|
(*regs)[REG_RIP] = shared64->invoke_gdbserver;
|
|
#else
|
|
I_die_here: architecture missing in vgdb-invoker-solaris.c
|
|
#endif
|
|
} else {
|
|
assert(0);
|
|
}
|
|
|
|
DEBUG(1, "New stack frame set up for process %d.\n", pid);
|
|
return True;
|
|
}
|
|
|
|
/* Creates and starts an agent thread within the inferior process.
|
|
The agent thread is created stopped and with its held signal set
|
|
(the signal mask) having all signals except SIGKILL and SIGSTOP
|
|
blocked. All these signals need to remain blocked while the agent
|
|
thread is running because valgrind syscall/signal machinery expects
|
|
that (remember: all valgrind threads are blocked in VgTs_WaitSys
|
|
- that is the reason why we are invoking the agent, after all).
|
|
It is necessary to resume the agent thread afterwards.
|
|
*/
|
|
static Bool invoke_agent(pid_t pid, prgregset_t *regs, id_t *agent_lwpid)
|
|
{
|
|
assert(ctl_fd != -1);
|
|
|
|
DEBUG(1, "Creating an agent thread within process %d.\n", pid);
|
|
|
|
/* Create the agent thread. */
|
|
ctl_t ctl;
|
|
ctl.cmd = PCAGENT;
|
|
memcpy(&ctl.arg.regs, regs, sizeof(prgregset_t));
|
|
size_t bytes = sizeof(ctl.cmd) + sizeof(ctl.arg.regs);
|
|
ssize_t written = write(ctl_fd, (void *) &ctl, bytes);
|
|
if ((written < 0) || (written != bytes)) {
|
|
ERROR(errno, "Failed to write to ctl_fd: PCAGENT.\n");
|
|
return False;
|
|
}
|
|
|
|
DEBUG(1, "Obtaining agent thread lwpid for process %d.\n", pid);
|
|
|
|
char procname[PATH_MAX];
|
|
snprintf(procname, sizeof(procname),
|
|
"/proc/%d/lwp/agent/lwpstatus", pid);
|
|
|
|
int status_fd = open(procname, O_RDONLY, 0);
|
|
if (status_fd < 0) {
|
|
/* Operation failed but there is no way to get rid of the agent
|
|
thread from outside. We are doomed... */
|
|
ERROR(errno, "Failed to open file %s.\n", procname);
|
|
return False;
|
|
}
|
|
|
|
lwpstatus_t status;
|
|
bytes = read(status_fd, &status, sizeof(status));
|
|
if ((bytes < 0) || (bytes != sizeof(status))) {
|
|
ERROR(errno, "Failed to read from %s.\n", procname);
|
|
close(status_fd);
|
|
return False;
|
|
}
|
|
|
|
close(status_fd);
|
|
*agent_lwpid = status.pr_lwpid;
|
|
|
|
snprintf(procname, sizeof(procname),
|
|
"/proc/%d/lwp/agent/lwpctl", pid);
|
|
|
|
int agent_ctl_fd = open(procname, O_WRONLY, 0);
|
|
if (agent_ctl_fd < 0) {
|
|
/* Resuming failed but there is no way to get rid of the agent
|
|
thread from outside. We are doomed... */
|
|
ERROR(errno, "Failed to open file %s.\n", procname);
|
|
return False;
|
|
}
|
|
|
|
DEBUG(1, "Resuming the agent thread for process %d.\n", pid);
|
|
|
|
/* Resume the agent thread. */
|
|
ctl.cmd = PCRUN;
|
|
ctl.arg.flags = 0;
|
|
bytes = sizeof(ctl.cmd) + sizeof(ctl.arg.flags);
|
|
written = write(agent_ctl_fd, (void *) &ctl, bytes);
|
|
if ((written < 0) || (written != bytes)) {
|
|
/* Resuming failed but there is no way to get rid of the agent
|
|
thread from outside. We are doomed... */
|
|
ERROR(errno, "Failed to write to agent_ctl_fd: PCRUN 0.\n");
|
|
close(agent_ctl_fd);
|
|
return False;
|
|
}
|
|
|
|
DEBUG(1, "Agent thread lwpid %d now running within process %d.\n",
|
|
*agent_lwpid, pid);
|
|
close(agent_ctl_fd);
|
|
return True;
|
|
}
|
|
|
|
/* Waits until the agent thread running inside the inferior
|
|
process exits. */
|
|
static Bool wait_for_agent_exit(pid_t pid, id_t agent_lwpid)
|
|
{
|
|
char procname[PATH_MAX];
|
|
snprintf(procname, sizeof(procname), "/proc/%d/lwp/agent/lwpctl", pid);
|
|
|
|
int agent_ctl_fd = open(procname, O_WRONLY, 0);
|
|
if (agent_ctl_fd < 0) {
|
|
if (errno == ENOENT) {
|
|
DEBUG(1, "Agent control file %s no longer exists. This means "
|
|
"agent thread %d exited meanwhile.\n",
|
|
procname, agent_lwpid);
|
|
return True;
|
|
}
|
|
ERROR(errno, "Failed to open agent control file %s.\n", procname);
|
|
return False;
|
|
}
|
|
|
|
DEBUG(1, "Waiting for agent thread %d to exit.\n", agent_lwpid);
|
|
|
|
/* Wait until the agent thread stops. This covers also the case
|
|
when the thread exited. */
|
|
ctl_t ctl;
|
|
ctl.cmd = PCWSTOP;
|
|
size_t bytes = sizeof(ctl.cmd);
|
|
ssize_t written = write(agent_ctl_fd, (void *) &ctl, bytes);
|
|
if ((written < 0) || (written != bytes)) {
|
|
if (errno == ENOENT) {
|
|
DEBUG(1, "Agent thread lwpid %d has now exited in process %d.\n",
|
|
agent_lwpid, pid);
|
|
} else {
|
|
ERROR(errno, "Failed to write to agent_ctl_fd: PCWSTOP.\n");
|
|
close(agent_ctl_fd);
|
|
return False;
|
|
}
|
|
}
|
|
|
|
close(agent_ctl_fd);
|
|
return True;
|
|
}
|
|
|
|
Bool invoker_invoke_gdbserver(pid_t pid)
|
|
{
|
|
if (attach(pid) != True) {
|
|
return False;
|
|
}
|
|
|
|
prgregset_t regs;
|
|
if (get_regs(pid, ®s) != True) {
|
|
detach(pid);
|
|
return False;
|
|
}
|
|
|
|
if (setup_stack_frame(pid, ®s) != True) {
|
|
detach(pid);
|
|
return False;
|
|
}
|
|
|
|
id_t agent_lwpid;
|
|
if (invoke_agent(pid, ®s, &agent_lwpid) != True) {
|
|
detach(pid);
|
|
return False;
|
|
}
|
|
|
|
if (wait_for_agent_exit(pid, agent_lwpid) != True) {
|
|
detach(pid);
|
|
return False;
|
|
}
|
|
|
|
detach(pid);
|
|
return True;
|
|
}
|
|
|
|
void invoker_cleanup_restore_and_detach(void *v_pid)
|
|
{
|
|
detach(*(int *) v_pid);
|
|
}
|
|
|
|
void invoker_restrictions_msg(void)
|
|
{
|
|
}
|
|
|
|
void invoker_valgrind_dying(void)
|
|
{
|
|
}
|