Arquivos
hhvm/hphp/runtime/debugger/debugger.cpp
T
Mike Magruder 96d6bab291 Cleanup flow control around exceptions
There were multiple issues with flow control when exceptions occur. Fixed these by ditching the reliance on the exception thrown interrupt and introduce an exception handler interrupt, which indicates control is about to pass to a catch clause. This gives us much better insight into how execution is flowing and how we might need to adjust an in-flight stepping operation.
2013-06-10 10:14:11 -07:00

659 linhas
22 KiB
C++

/*
+----------------------------------------------------------------------+
| HipHop for PHP |
+----------------------------------------------------------------------+
| Copyright (c) 2010-2013 Facebook, Inc. (http://www.facebook.com) |
+----------------------------------------------------------------------+
| This source file is subject to version 3.01 of the PHP license, |
| that is bundled with this package in the file LICENSE, and is |
| available through the world-wide-web at the following url: |
| http://www.php.net/license/3_01.txt |
| If you did not receive a copy of the PHP license and are unable to |
| obtain it through the world-wide-web, please send a note to |
| license@php.net so we can mail you a copy immediately. |
+----------------------------------------------------------------------+
*/
#include "hphp/runtime/debugger/debugger.h"
#include "hphp/runtime/debugger/debugger_server.h"
#include "hphp/runtime/debugger/debugger_client.h"
#include "hphp/runtime/debugger/cmd/cmd_interrupt.h"
#include "hphp/runtime/base/hphp_system.h"
#include "hphp/runtime/vm/jit/translator.h"
#include "hphp/runtime/vm/jit/translator-inline.h"
#include "hphp/util/text_color.h"
#include "hphp/util/util.h"
#include "hphp/util/logger.h"
namespace HPHP { namespace Eval {
///////////////////////////////////////////////////////////////////////////////
TRACE_SET_MOD(debugger);
Debugger Debugger::s_debugger;
bool Debugger::s_clientStarted = false;
bool Debugger::StartServer() {
TRACE(2, "Debugger::StartServer\n");
return DebuggerServer::Start();
}
DebuggerProxyPtr Debugger::StartClient(const DebuggerClientOptions &options) {
TRACE(2, "Debugger::StartClient\n");
SmartPtr<Socket> localProxy = DebuggerClient::Start(options);
if (localProxy.get()) {
s_clientStarted = true;
return CreateProxy(localProxy, true);
}
return DebuggerProxyPtr();
}
void Debugger::Stop() {
TRACE(2, "Debugger::Stop\n");
LogShutdown(ShutdownKind::Normal);
s_debugger.m_proxyMap.clear();
DebuggerServer::Stop();
if (s_clientStarted) {
DebuggerClient::Stop();
}
}
///////////////////////////////////////////////////////////////////////////////
void Debugger::RegisterSandbox(const DSandboxInfo &sandbox) {
TRACE(2, "Debugger::RegisterSandbox\n");
s_debugger.registerSandbox(sandbox);
}
void Debugger::UnregisterSandbox(CStrRef id) {
TRACE(2, "Debugger::UnregisterSandbox\n");
s_debugger.unregisterSandbox(id.get());
}
void Debugger::RegisterThread() {
TRACE(2, "Debugger::RegisterThread\n");
s_debugger.registerThread();
}
DebuggerProxyPtr Debugger::CreateProxy(SmartPtr<Socket> socket, bool local) {
TRACE(2, "Debugger::CreateProxy\n");
return s_debugger.createProxy(socket, local);
}
void Debugger::RemoveProxy(DebuggerProxyPtr proxy) {
TRACE(2, "Debugger::RemoveProxy\n");
s_debugger.removeProxy(proxy);
}
int Debugger::CountConnectedProxy() {
TRACE(2, "Debugger::CountConnectedProxy\n");
return s_debugger.countConnectedProxy();
}
DebuggerProxyPtr Debugger::GetProxy() {
TRACE(2, "Debugger::GetProxy\n");
CStrRef sandboxId = g_context->getSandboxId();
return s_debugger.findProxy(sandboxId.get());
}
bool Debugger::SwitchSandbox(DebuggerProxyPtr proxy,
const std::string &newId,
bool force) {
TRACE(2, "Debugger::SwitchSandbox\n");
return s_debugger.switchSandbox(proxy, newId, force);
}
void Debugger::GetRegisteredSandboxes(DSandboxInfoPtrVec &sandboxes) {
TRACE(2, "Debugger::GetRegisteredSandboxes\n");
s_debugger.getSandboxes(sandboxes);
}
bool Debugger::IsThreadDebugging(int64_t id) {
TRACE(2, "Debugger::IsThreadDebugging\n");
return s_debugger.isThreadDebugging(id);
}
void Debugger::RequestInterrupt(DebuggerProxyPtr proxy) {
TRACE(2, "Debugger::RequestInterrupt\n");
s_debugger.requestInterrupt(proxy);
}
void Debugger::RetireDummySandboxThread(DummySandbox* toRetire) {
TRACE(2, "Debugger::RetireDummySandboxThread\n");
s_debugger.retireDummySandboxThread(toRetire);
}
void Debugger::CleanupDummySandboxThreads() {
TRACE(7, "Debugger::CleanupDummySandboxThreads\n");
s_debugger.cleanupDummySandboxThreads();
}
void Debugger::DebuggerSession(const DebuggerClientOptions& options,
const std::string& file, bool restart) {
TRACE(2, "Debugger::DebuggerSession: restart=%d\n", restart);
if (options.extension.empty()) {
// even if it's empty, still need to call for warmup
hphp_invoke_simple("", true); // not to run the 1st file if compiled
} else {
hphp_invoke_simple(options.extension);
}
ThreadInfo *ti = ThreadInfo::s_threadInfo.getNoCheck();
ti->m_reqInjectionData.setDebugger(true);
if (!restart) {
DebuggerDummyEnv dde;
Debugger::InterruptSessionStarted(file.c_str());
}
if (!file.empty()) {
hphp_invoke_simple(file);
}
{
DebuggerDummyEnv dde;
Debugger::InterruptSessionEnded(file.c_str());
}
}
void Debugger::LogShutdown(ShutdownKind shutdownKind) {
int proxyCount = s_debugger.countConnectedProxy();
if (proxyCount > 0) {
Logger::Warning(DEBUGGER_LOG_TAG "%s with connected debuggers!",
shutdownKind == ShutdownKind::Normal ?
"Normal shutdown" : "Unexpected crash",
proxyCount);
for (const auto& proxyEntry: s_debugger.m_proxyMap) {
auto sid = proxyEntry.first;
auto proxy = proxyEntry.second;
auto dummySid = StringData::GetStaticString(proxy->getDummyInfo().id());
if (sid != dummySid) {
auto sandbox = proxy->getSandbox();
if (sandbox.valid()) {
Logger::Warning(DEBUGGER_LOG_TAG "Debugging %s\n",
sandbox.desc().c_str());
}
}
}
}
}
///////////////////////////////////////////////////////////////////////////////
void Debugger::InterruptSessionStarted(const char *file,
const char *error /* = NULL */) {
TRACE(2, "Debugger::InterruptSessionStarted\n");
ThreadInfo::s_threadInfo->m_reqInjectionData.setDebugger(true);
Interrupt(SessionStarted, file, nullptr, error);
}
void Debugger::InterruptSessionEnded(const char *file) {
TRACE(2, "Debugger::InterruptSessionEnded\n");
Interrupt(SessionEnded, file);
}
void Debugger::InterruptWithUrl(int type, const char *url) {
// Build a site to represent the URL. Note it won't have any source info
// in it, because this event is raised with no PHP on the stack.
InterruptSite site(false, null_variant);
site.url() = url ? url : "";
Interrupt(type, url, &site);
}
void Debugger::InterruptRequestStarted(const char *url) {
TRACE(2, "Debugger::InterruptRequestStarted\n");
if (ThreadInfo::s_threadInfo->m_reqInjectionData.getDebugger()) {
InterruptWithUrl(RequestStarted, url);
}
}
void Debugger::InterruptRequestEnded(const char *url) {
TRACE(2, "Debugger::InterruptRequestEnded: url=%s\n", url);
if (ThreadInfo::s_threadInfo->m_reqInjectionData.getDebugger()) {
InterruptWithUrl(RequestEnded, url);
}
CStrRef sandboxId = g_context->getSandboxId();
s_debugger.unregisterSandbox(sandboxId.get());
}
void Debugger::InterruptPSPEnded(const char *url) {
TRACE(2, "Debugger::InterruptPSPEnded\n");
if (ThreadInfo::s_threadInfo->m_reqInjectionData.getDebugger()) {
InterruptWithUrl(PSPEnded, url);
}
}
// Called directly from exception handling to indicate a user error handler
// failed to handle an exeption. NB: this is quite distinct from the hook called
// from iopThrow named phpDebuggerExceptionHook().
bool Debugger::InterruptException(CVarRef e) {
TRACE(2, "Debugger::InterruptException\n");
if (RuntimeOption::EnableDebugger) {
ThreadInfo *ti = ThreadInfo::s_threadInfo.getNoCheck();
if (ti->m_reqInjectionData.getDebugger()) {
HPHP::Transl::VMRegAnchor _;
InterruptVMHook(ExceptionThrown, e);
}
}
return true;
}
// Primary entrypoint for the debugger from the VM. Called in response to a host
// of VM events that the debugger is interested in. The debugger will execute
// any logic needed to handle the event, and will block below this to wait for
// and process more commands from the debugger client. This function will only
// return when the debugger is letting the thread continue execution, e.g., for
// flow control command like continue, next, etc.
void Debugger::Interrupt(int type, const char *program,
InterruptSite *site /* = NULL */,
const char *error /* = NULL */) {
assert(RuntimeOption::EnableDebugger);
TRACE_RB(2, "Debugger::Interrupt type %d\n", type);
DebuggerProxyPtr proxy = GetProxy();
if (proxy) {
TRACE(3, "proxy != null\n");
RequestInjectionData &rjdata = ThreadInfo::s_threadInfo->m_reqInjectionData;
// The proxy will only service an interrupt if we've previously setup some
// form of flow control command (steps, breakpoints, etc.) or if it's
// an interrupt related to something like the session or request.
if (proxy->needInterrupt() || type != BreakPointReached) {
// Interrupts may execute some PHP code, causing another interruption.
std::stack<void *> &interrupts = rjdata.interrupts;
CmdInterrupt cmd((InterruptType)type, program, site, error);
interrupts.push(&cmd);
proxy->interrupt(cmd);
interrupts.pop();
}
// Some cmds require us to interpret all instructions until the cmd
// completes. Setting this will ensure we stay out of JIT code and in the
// interpreter so phpDebuggerOpcodeHook has a chance to work.
rjdata.setDebuggerIntr(proxy->needVMInterrupts());
} else {
TRACE(3, "proxy == null\n");
// Debugger clients are disconnected abnormally, or this sandbox is not
// being debugged.
if (type == SessionStarted || type == SessionEnded) {
// for command line programs, we need this exception to exit from
// the infinite execution loop
throw DebuggerClientExitException();
}
}
}
// Primary entrypoint from the set of "debugger hooks", which the VM calls in
// response to various events. While this function is quite general wrt. the
// type of interrupt, practically the type will be one of the following:
// - ExceptionThrown
// - ExceptionHandler
// - BreakPointReached
// - HardBreakPoint
//
// Note: it is indeed a bit odd that interrupts due to single stepping come in
// as "BreakPointReached". Currently this results in spurious work in the
// debugger.
void Debugger::InterruptVMHook(int type /* = BreakPointReached */,
CVarRef e /* = null_variant */) {
TRACE(2, "Debugger::InterruptVMHook\n");
// Computing the interrupt site here pulls in more data from the Unit to
// describe the current execution point.
InterruptSite site(type == HardBreakPoint, e);
if (!site.valid()) {
// An invalid site is missing something like an ActRec, a func, or a
// Unit. Currently the debugger has no action to take at such sites.
return;
}
Interrupt(type, nullptr, &site);
}
///////////////////////////////////////////////////////////////////////////////
void Debugger::SetTextColors() {
TRACE(2, "Debugger::SetTextColors\n");
s_stdout_color = ANSI_COLOR_CYAN;
s_stderr_color = ANSI_COLOR_RED;
}
String Debugger::ColorStdout(CStrRef s) {
TRACE(2, "Debugger::ColorStdout\n");
if (s_stdout_color) {
return String(s_stdout_color) + s + String(ANSI_COLOR_END);
}
return s;
}
String Debugger::ColorStderr(CStrRef s) {
TRACE(2, "Debugger::ColorStderr\n");
if (s_stderr_color) {
return String(s_stderr_color) + s + String(ANSI_COLOR_END);
}
return s;
}
///////////////////////////////////////////////////////////////////////////////
bool Debugger::isThreadDebugging(int64_t tid) {
TRACE(2, "Debugger::isThreadDebugging\n");
ThreadInfoMap::const_accessor acc;
if (m_threadInfos.find(acc, tid)) {
ThreadInfo* ti = acc->second;
return (ti->m_reqInjectionData.getDebugger());
}
return false;
}
void Debugger::registerThread() {
TRACE(2, "Debugger::registerThread\n");
ThreadInfo* ti = ThreadInfo::s_threadInfo.getNoCheck();
int64_t tid = (int64_t)Process::GetThreadId();
ThreadInfoMap::accessor acc;
m_threadInfos.insert(acc, tid);
acc->second = ti;
}
void Debugger::updateSandbox(const DSandboxInfo &sandbox) {
TRACE(2, "Debugger::updateSandbox\n");
const StringData* sd = StringData::GetStaticString(sandbox.id());
SandboxMap::accessor acc;
if (m_sandboxMap.insert(acc, sd)) {
DSandboxInfoPtr sb(new DSandboxInfo());
*sb = sandbox;
acc->second = sb;
} else {
acc->second->update(sandbox);
}
}
void Debugger::getSandboxes(DSandboxInfoPtrVec &sandboxes) {
TRACE(2, "Debugger::getSandboxes\n");
sandboxes.reserve(m_sandboxMap.size());
for (SandboxMap::const_iterator iter =
m_sandboxMap.begin(); iter != m_sandboxMap.end(); ++iter) {
if (!iter->second->m_user.empty()) {
DSandboxInfoPtr sandbox(new DSandboxInfo());
*sandbox = *iter->second;
sandboxes.push_back(sandbox);
}
}
}
void Debugger::registerSandbox(const DSandboxInfo &sandbox) {
TRACE(2, "Debugger::registerSandbox\n");
// update sandbox first
updateSandbox(sandbox);
// add thread do m_sandboxThreadInfoMap
const StringData* sid = StringData::GetStaticString(sandbox.id());
ThreadInfo* ti = ThreadInfo::s_threadInfo.getNoCheck();
{
SandboxThreadInfoMap::accessor acc;
m_sandboxThreadInfoMap.insert(acc, sid);
ThreadInfoSet& set = acc->second;
set.insert(ti);
}
// find out whether this sandbox is being debugged
DebuggerProxyPtr proxy = findProxy(sid);
if (proxy) {
ti->m_reqInjectionData.setDebugger(true);
proxy->writeInjTablesToThread();
}
}
void Debugger::unregisterSandbox(const StringData* sandboxId) {
TRACE(2, "Debugger::unregisterSandbox\n");
ThreadInfo *ti = ThreadInfo::s_threadInfo.getNoCheck();
SandboxThreadInfoMap::accessor acc;
if (m_sandboxThreadInfoMap.find(acc, sandboxId)) {
ThreadInfoSet& set = acc->second;
set.erase(ti);
}
}
#define FOREACH_SANDBOX_THREAD_BEGIN(sid, ti) { \
SandboxThreadInfoMap::const_accessor acc; \
if (m_sandboxThreadInfoMap.find(acc, sid)) { \
const ThreadInfoSet& set = acc->second; \
for (std::set<ThreadInfo*>::iterator iter = set.begin(); \
iter != set.end(); ++iter) { \
ThreadInfo* ti = (*iter); \
assert(ThreadInfo::valid(ti)); \
#define FOREACH_SANDBOX_THREAD_END() } } } \
// Ask every thread in this proxy's sandbox and the dummy sandbox to "stop".
// Gaining control of these threads is the intention... the mechanism is to
// force them all to start interpreting all of their code in an effort to gain
// control in phpDebuggerOpcodeHook().
void Debugger::requestInterrupt(DebuggerProxyPtr proxy) {
TRACE(2, "Debugger::requestInterrupt\n");
const StringData* sid = StringData::GetStaticString(proxy->getSandboxId());
FOREACH_SANDBOX_THREAD_BEGIN(sid, ti)
ti->m_reqInjectionData.setDebuggerIntr(true);
FOREACH_SANDBOX_THREAD_END()
sid = StringData::GetStaticString(proxy->getDummyInfo().id());
FOREACH_SANDBOX_THREAD_BEGIN(sid, ti)
ti->m_reqInjectionData.setDebuggerIntr(true);
FOREACH_SANDBOX_THREAD_END()
}
void Debugger::setDebuggerFlag(const StringData* sandboxId, bool flag) {
TRACE(2, "Debugger::setDebuggerFlag\n");
FOREACH_SANDBOX_THREAD_BEGIN(sandboxId, ti)
ti->m_reqInjectionData.setDebugger(flag);
FOREACH_SANDBOX_THREAD_END()
}
#undef FOREACH_SANDBOX_THREAD_BEGIN
#undef FOREACH_SANDBOX_THREAD_END
DebuggerProxyPtr Debugger::createProxy(SmartPtr<Socket> socket, bool local) {
TRACE(2, "Debugger::createProxy\n");
// Creates a proxy and threads needed to handle it. At this point, there is
// not enough information to attach a sandbox.
DebuggerProxyPtr proxy(new DebuggerProxy(socket, local));
const StringData* sid =
StringData::GetStaticString(proxy->getDummyInfo().id());
{
assert(sid);
ProxyMap::accessor acc;
m_proxyMap.insert(acc, sid);
acc->second = proxy;
}
if (!local) {
proxy->startDummySandbox();
}
proxy->startSignalThread();
return proxy;
}
void Debugger::retireDummySandboxThread(DummySandbox* toRetire) {
TRACE(2, "Debugger::retireDummySandboxThread\n");
m_cleanupDummySandboxQ.push(toRetire);
}
void Debugger::cleanupDummySandboxThreads() {
TRACE(7, "Debugger::cleanupDummySandboxThreads\n");
DummySandbox* ptr = nullptr;
while (m_cleanupDummySandboxQ.try_pop(ptr)) {
ptr->notify();
try {
// we can't block the server for waiting
if (ptr->waitForEnd(1)) {
delete ptr;
} else {
Logger::Error("Dummy sandbox %p refused to stop", ptr);
}
} catch (Exception &e) {
Logger::Error("Dummy sandbox exception: " + e.getMessage());
}
}
}
void Debugger::removeProxy(DebuggerProxyPtr proxy) {
TRACE(2, "Debugger::removeProxy\n");
if (proxy->getSandbox().valid()) {
const StringData* sid = StringData::GetStaticString(proxy->getSandboxId());
setDebuggerFlag(sid, false);
m_proxyMap.erase(sid);
}
const StringData* dummySid =
StringData::GetStaticString(proxy->getDummyInfo().id());
m_proxyMap.erase(dummySid);
// Clear the debugger blacklist PC upon last detach if JIT is used
if (RuntimeOption::EvalJit && countConnectedProxy() == 0) {
Transl::Translator::Get()->clearDbgBL();
}
}
DebuggerProxyPtr Debugger::findProxy(const StringData* sandboxId) {
TRACE(2, "Debugger::findProxy\n");
if (sandboxId) {
ProxyMap::const_accessor acc;
if (m_proxyMap.find(acc, sandboxId)) {
return acc->second;
}
}
return DebuggerProxyPtr();
}
bool Debugger::switchSandbox(DebuggerProxyPtr proxy,
const std::string &newId,
bool force) {
TRACE(2, "Debugger::switchSandbox\n");
const StringData* newSid = StringData::GetStaticString(newId);
// Lock the proxy during the switch
Lock l(proxy.get());
if (proxy->getSandboxId() != newId) {
if (!switchSandboxImpl(proxy, newSid, force)) {
// failed to switch
return false;
}
}
updateProxySandbox(proxy, newSid);
return true;
}
bool Debugger::switchSandboxImpl(DebuggerProxyPtr proxy,
const StringData* newSid,
bool force) {
TRACE(2, "Debugger::switchSandboxImpln");
// Take the new sandbox
DebuggerProxyPtr dpp;
{
ProxyMap::accessor acc;
if (m_proxyMap.insert(acc, newSid)) {
acc->second = proxy;
} else {
// the sandbox indicated by newId is already attached by another proxy
if (!force) {
return false;
}
// Delay the destruction of the proxy originally attached to the sandbox
// to avoid calling a DebuggerProxy destructor (which can sleep) while
// holding m_mutex.
dpp = acc->second;
acc->second = proxy;
}
}
if (proxy->getSandbox().valid()) {
// Detach from the old sandbox
const StringData* oldSid =
StringData::GetStaticString(proxy->getSandboxId());
setDebuggerFlag(oldSid, false);
m_proxyMap.erase(oldSid);
}
setDebuggerFlag(newSid, true);
if (dpp) {
dpp->forceQuit();
}
return true;
}
void Debugger::updateProxySandbox(DebuggerProxyPtr proxy,
const StringData* sandboxId) {
TRACE(2, "Debugger::updateProxySandbox\n");
// update proxy's sandbox info from what is on file
SandboxMap::const_accessor acc;
if (m_sandboxMap.find(acc, sandboxId)) {
proxy->updateSandbox(acc->second);
} else {
// don't have the sandbox on file yet. create a sandbox info with
// no path for now. Sandbox path will be updated upon first request
// with that sandbox arrives
DSandboxInfoPtr sb(new DSandboxInfo(sandboxId->toCPPString()));
proxy->updateSandbox(sb);
}
}
///////////////////////////////////////////////////////////////////////////////
// Helpers for usage logging
// NB: the usage logger is not owned by the Debugger. The caller will call this
// again with nullptr before destroying the given usage logger.
void Debugger::SetUsageLogger(DebuggerUsageLogger *usageLogger) {
TRACE(1, "Debugger::SetUsageLogger\n");
s_debugger.m_usageLogger = usageLogger;
}
void Debugger::InitUsageLogging() {
TRACE(1, "Debugger::InitUsageLogging\n");
if (s_debugger.m_usageLogger) s_debugger.m_usageLogger->init();
}
void Debugger::UsageLog(const std::string &mode, const std::string &cmd,
const std::string &data) {
if (s_debugger.m_usageLogger) s_debugger.m_usageLogger->log(mode, cmd, data);
}
const char *Debugger::InterruptTypeName(CmdInterrupt &cmd) {
switch (cmd.getInterruptType()) {
case SessionStarted: return "SessionStarted";
case SessionEnded: return "SessionEnded";
case RequestStarted: return "RequestStarted";
case RequestEnded: return "RequestEnded";
case PSPEnded: return "PSPEnded";
case HardBreakPoint: return "HardBreakPoint";
case BreakPointReached: return "BreakPointReached";
case ExceptionThrown: return "ExceptionThrown";
case ExceptionHandler: return "ExceptionHandler";
default:
return "unknown";
}
}
void Debugger::UsageLogInterrupt(const std::string &mode, CmdInterrupt &cmd) {
UsageLog(mode, "interrupt", InterruptTypeName(cmd));
}
///////////////////////////////////////////////////////////////////////////////
DebuggerDummyEnv::DebuggerDummyEnv() {
TRACE(2, "DebuggerDummyEnv::DebuggerDummyEnv\n");
g_vmContext->enterDebuggerDummyEnv();
}
DebuggerDummyEnv::~DebuggerDummyEnv() {
TRACE(2, "DebuggerDummyEnv::~DebuggerDummyEnv\n");
g_vmContext->exitDebuggerDummyEnv();
}
///////////////////////////////////////////////////////////////////////////////
EvalBreakControl::EvalBreakControl(bool noBreak) {
TRACE(2, "EvalBreakControl::EvalBreakControl\n");
m_noBreakSave = g_vmContext->m_dbgNoBreak;
g_vmContext->m_dbgNoBreak = noBreak;
}
EvalBreakControl::~EvalBreakControl() {
TRACE(2, "EvalBreakControl::~EvalBreakControl\n");
g_vmContext->m_dbgNoBreak = m_noBreakSave;
}
///////////////////////////////////////////////////////////////////////////////
}}