6d91f8195a
Because stronger types are better types, and this will make future refactoring easier. I considered trying to purge the Opcode type from the codebase too but that would be a much bigger project.
465 linhas
16 KiB
C++
465 linhas
16 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/vm/debugger_hook.h"
|
|
#include "hphp/runtime/vm/jit/translator.h"
|
|
#include "hphp/runtime/debugger/break_point.h"
|
|
#include "hphp/runtime/debugger/debugger.h"
|
|
#include "hphp/runtime/debugger/debugger_proxy.h"
|
|
#include "hphp/runtime/base/file_repository.h"
|
|
#include "hphp/util/logger.h"
|
|
#include "hphp/util/util.h"
|
|
|
|
namespace HPHP {
|
|
|
|
//////////////////////////////////////////////////////////////////////////
|
|
|
|
TRACE_SET_MOD(debuggerflow);
|
|
|
|
static inline Transl::Translator* transl() {
|
|
return Transl::Translator::Get();
|
|
}
|
|
|
|
// Hook called from the bytecode interpreter before every opcode executed while
|
|
// a debugger is attached. The debugger may choose to hold the thread below
|
|
// here and execute any number of commands from the client. Return from here
|
|
// lets the opcode execute.
|
|
void phpDebuggerOpcodeHook(const uchar* pc) {
|
|
TRACE(5, "in phpDebuggerOpcodeHook()\n");
|
|
// Short-circuit when we're doing things like evaling PHP for print command,
|
|
// or conditional breakpoints.
|
|
if (UNLIKELY(g_vmContext->m_dbgNoBreak)) {
|
|
TRACE(5, "NoBreak flag is on\n");
|
|
return;
|
|
}
|
|
// Short-circuit for cases where we're executing a line of code that we know
|
|
// we don't need an interrupt for, e.g., stepping over a line of code.
|
|
if (UNLIKELY(g_vmContext->m_lastLocFilter != nullptr) &&
|
|
g_vmContext->m_lastLocFilter->checkPC(pc)) {
|
|
TRACE_RB(5, "Location filter hit at pc %p\n", pc);
|
|
return;
|
|
}
|
|
// Are we hitting a breakpoint?
|
|
if (LIKELY(g_vmContext->m_breakPointFilter == nullptr ||
|
|
!g_vmContext->m_breakPointFilter->checkPC(pc))) {
|
|
TRACE(5, "not in the PC range for any breakpoints\n");
|
|
if (LIKELY(!DEBUGGER_FORCE_INTR)) {
|
|
return;
|
|
}
|
|
TRACE_RB(5, "DEBUGGER_FORCE_INTR\n");
|
|
}
|
|
Eval::Debugger::InterruptVMHook();
|
|
TRACE(5, "out phpDebuggerOpcodeHook()\n");
|
|
}
|
|
|
|
// Hook called from iopThrow to signal that we are about to throw an exception.
|
|
void phpDebuggerExceptionThrownHook(ObjectData* exception) {
|
|
TRACE(5, "in phpDebuggerExceptionThrownHook()\n");
|
|
if (UNLIKELY(g_vmContext->m_dbgNoBreak)) {
|
|
TRACE(5, "NoBreak flag is on\n");
|
|
return;
|
|
}
|
|
Eval::Debugger::InterruptVMHook(Eval::ExceptionThrown, exception);
|
|
TRACE(5, "out phpDebuggerExceptionThrownHook()\n");
|
|
}
|
|
|
|
// Hook called from exception unwind to signal that we are about to handle an
|
|
// exception.
|
|
void phpDebuggerExceptionHandlerHook() {
|
|
TRACE(5, "in phpDebuggerExceptionHandlerHook()\n");
|
|
if (UNLIKELY(g_vmContext->m_dbgNoBreak)) {
|
|
TRACE(5, "NoBreak flag is on\n");
|
|
return;
|
|
}
|
|
Eval::Debugger::InterruptVMHook(Eval::ExceptionHandler);
|
|
TRACE(5, "out phpDebuggerExceptionHandlerHook()\n");
|
|
}
|
|
|
|
// Hook called when the VM raises an error.
|
|
void phpDebuggerErrorHook(const std::string& message) {
|
|
TRACE(5, "in phpDebuggerErrorHook()\n");
|
|
if (UNLIKELY(g_vmContext->m_dbgNoBreak)) {
|
|
TRACE(5, "NoBreak flag is on\n");
|
|
return;
|
|
}
|
|
Eval::Debugger::InterruptVMHook(Eval::ExceptionThrown, String(message));
|
|
TRACE(5, "out phpDebuggerErrorHook()\n");
|
|
}
|
|
|
|
bool isDebuggerAttachedProcess() {
|
|
return Eval::Debugger::CountConnectedProxy() > 0;
|
|
}
|
|
|
|
// Ensure we interpret all code at the given offsets. This sets up a guard for
|
|
// each piece of translated code to ensure we punt to the interpreter when the
|
|
// debugger is attached.
|
|
static void blacklistRangesInJit(const Unit* unit,
|
|
const OffsetRangeVec& offsets) {
|
|
for (OffsetRangeVec::const_iterator it = offsets.begin();
|
|
it != offsets.end(); ++it) {
|
|
for (PC pc = unit->at(it->m_base); pc < unit->at(it->m_past);
|
|
pc += instrLen((Op*)pc)) {
|
|
transl()->addDbgBLPC(pc);
|
|
}
|
|
}
|
|
if (!transl()->addDbgGuards(unit)) {
|
|
Logger::Warning("Failed to set breakpoints in Jitted code");
|
|
}
|
|
// In this case, we may be setting a breakpoint in a tracelet which could
|
|
// already be jitted, and present on the stack. Make sure we don't return
|
|
// to it so we have a chance to honor breakpoints.
|
|
g_vmContext->preventReturnsToTC();
|
|
}
|
|
|
|
// Ensure we interpret an entire function when the debugger is attached.
|
|
static void blacklistFuncInJit(const Func* f) {
|
|
Unit* unit = f->unit();
|
|
OffsetRangeVec ranges;
|
|
ranges.push_back(OffsetRange(f->base(), f->past()));
|
|
blacklistRangesInJit(unit, ranges);
|
|
}
|
|
|
|
static PCFilter *getBreakPointFilter() {
|
|
if (!g_vmContext->m_breakPointFilter) {
|
|
g_vmContext->m_breakPointFilter = new PCFilter();
|
|
}
|
|
return g_vmContext->m_breakPointFilter;
|
|
}
|
|
|
|
// Looks up the offset range in the given unit, of the given breakpoint.
|
|
// If the offset cannot be found, the breakpoint is marked as invalid.
|
|
// Otherwise it is marked as valid and the offset is added to the
|
|
// breakpoint filter and the offset range is black listed for the JIT.
|
|
static void addBreakPointInUnit(Eval::BreakPointInfoPtr bp, Unit* unit) {
|
|
OffsetRangeVec offsets;
|
|
if (!unit->getOffsetRanges(bp->m_line1, offsets) || offsets.size() == 0) {
|
|
bp->m_bindState = Eval::BreakPointInfo::KnownToBeInvalid;
|
|
return;
|
|
}
|
|
bp->m_bindState = Eval::BreakPointInfo::KnownToBeValid;
|
|
TRACE(3, "Add to breakpoint filter for %s:%d, unit %p:\n",
|
|
unit->filepath()->data(), bp->m_line1, unit);
|
|
getBreakPointFilter()->addRanges(unit, offsets);
|
|
if (RuntimeOption::EvalJit) {
|
|
blacklistRangesInJit(unit, offsets);
|
|
}
|
|
}
|
|
|
|
static void addBreakPointsInFile(Eval::DebuggerProxy* proxy,
|
|
Eval::PhpFile* efile) {
|
|
Eval::BreakPointInfoPtrVec bps;
|
|
proxy->getBreakPoints(bps);
|
|
for (unsigned int i = 0; i < bps.size(); i++) {
|
|
Eval::BreakPointInfoPtr bp = bps[i];
|
|
if (Eval::BreakPointInfo::MatchFile(bp->m_file, efile->getFileName())) {
|
|
addBreakPointInUnit(bp, efile->unit());
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
static void addBreakPointFuncEntry(const Func* f) {
|
|
PC pc = f->unit()->at(f->base());
|
|
TRACE(5, "func() break %s : unit %p offset %d)\n",
|
|
f->fullName()->data(), f->unit(), f->base());
|
|
getBreakPointFilter()->addPC(pc);
|
|
if (RuntimeOption::EvalJit) {
|
|
if (transl()->addDbgBLPC(pc)) {
|
|
// if a new entry is added in blacklist
|
|
if (!transl()->addDbgGuard(f, f->base())) {
|
|
Logger::Warning("Failed to set breakpoints in Jitted code");
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// If the proxy has an enabled breakpoint that matches entry into the given
|
|
// function, arrange for the VM to stop execution and notify the debugger
|
|
// whenever execution enters the given function.
|
|
static void addBreakPointFuncEntry(Eval::DebuggerProxy* proxy, const Func* f) {
|
|
Eval::BreakPointInfoPtrVec bps;
|
|
proxy->getBreakPoints(bps);
|
|
for (unsigned int i = 0; i < bps.size(); i++) {
|
|
Eval::BreakPointInfoPtr bp = bps[i];
|
|
if (bp->m_state == Eval::BreakPointInfo::Disabled) continue;
|
|
if (bp->getFuncName() != f->fullName()->data()) continue;
|
|
bp->m_bindState = Eval::BreakPointInfo::KnownToBeValid;
|
|
addBreakPointFuncEntry(f);
|
|
return;
|
|
}
|
|
}
|
|
|
|
// If the proxy has enabled breakpoints that match entry into methods of
|
|
// the given class, arrange for the VM to stop execution and notify the debugger
|
|
// whenever execution enters one of these matched method.
|
|
// This function is called once, when a class is first loaded, so it is not
|
|
// performance critical.
|
|
static void addBreakPointsClass(Eval::DebuggerProxy* proxy, const Class* cls) {
|
|
size_t numFuncs = cls->numMethods();
|
|
if (numFuncs == 0) return;
|
|
auto clsName = cls->name();
|
|
auto funcs = cls->methods();
|
|
Eval::BreakPointInfoPtrVec bps;
|
|
proxy->getBreakPoints(bps);
|
|
for (unsigned int i = 0; i < bps.size(); i++) {
|
|
Eval::BreakPointInfoPtr bp = bps[i];
|
|
if (bp->m_state == Eval::BreakPointInfo::Disabled) continue;
|
|
// TODO: check name space separately
|
|
if (bp->getClass() != clsName->data()) continue;
|
|
bp->m_bindState = Eval::BreakPointInfo::KnownToBeInvalid;
|
|
for (size_t i = 0; i < numFuncs; ++i) {
|
|
auto f = funcs[i];
|
|
if (bp->getFunction() != f->name()->data()) continue;
|
|
bp->m_bindState = Eval::BreakPointInfo::KnownToBeValid;
|
|
addBreakPointFuncEntry(f);
|
|
}
|
|
}
|
|
}
|
|
|
|
void phpAddBreakPoint(const Unit* unit, Offset offset) {
|
|
PC pc = unit->at(offset);
|
|
getBreakPointFilter()->addPC(pc);
|
|
if (RuntimeOption::EvalJit) {
|
|
if (transl()->addDbgBLPC(pc)) {
|
|
// if a new entry is added in blacklist
|
|
if (!transl()->addDbgGuards(unit)) {
|
|
Logger::Warning("Failed to set breakpoints in Jitted code");
|
|
}
|
|
// In this case, we may be setting a breakpoint in a tracelet which could
|
|
// already be jitted, and present on the stack. Make sure we don't return
|
|
// to it so we have a chance to honor breakpoints.
|
|
g_vmContext->preventReturnsToTC();
|
|
}
|
|
}
|
|
}
|
|
|
|
void phpRemoveBreakPoint(const Unit* unit, Offset offset) {
|
|
if (g_vmContext->m_breakPointFilter) {
|
|
PC pc = unit->at(offset);
|
|
g_vmContext->m_breakPointFilter->removePC(pc);
|
|
}
|
|
}
|
|
|
|
void phpDebuggerEvalHook(const Func* f) {
|
|
if (RuntimeOption::EvalJit) {
|
|
blacklistFuncInJit(f);
|
|
}
|
|
}
|
|
|
|
// Called by the VM when a file is loaded.
|
|
void phpDebuggerFileLoadHook(Eval::PhpFile* efile) {
|
|
Eval::DebuggerProxyPtr proxy = Eval::Debugger::GetProxy();
|
|
if (proxy == nullptr) return;
|
|
addBreakPointsInFile(proxy.get(), efile);
|
|
}
|
|
|
|
// Called by the VM when a class definition is loaded.
|
|
void phpDebuggerDefClassHook(const Class* cls) {
|
|
Eval::DebuggerProxyPtr proxy = Eval::Debugger::GetProxy();
|
|
if (proxy == nullptr) return;
|
|
addBreakPointsClass(proxy.get(), cls);
|
|
}
|
|
|
|
// Called by the VM when a function definition is loaded.
|
|
void phpDebuggerDefFuncHook(const Func* func) {
|
|
Eval::DebuggerProxyPtr proxy = Eval::Debugger::GetProxy();
|
|
if (proxy == nullptr) return;
|
|
addBreakPointFuncEntry(proxy.get(), func);
|
|
}
|
|
|
|
// Called by the proxy whenever its breakpoint list is updated.
|
|
// Since this intended to be called when user input is received, it is not
|
|
// performance critical. Also, in typical scenarios, the list is short.
|
|
void phpSetBreakPoints(Eval::DebuggerProxy* proxy) {
|
|
Eval::BreakPointInfoPtrVec bps;
|
|
proxy->getBreakPoints(bps);
|
|
for (unsigned int i = 0; i < bps.size(); i++) {
|
|
Eval::BreakPointInfoPtr bp = bps[i];
|
|
bp->m_bindState = Eval::BreakPointInfo::Unknown;
|
|
auto className = bp->getClass();
|
|
if (!className.empty()) {
|
|
auto clsName = StringData::GetStaticString(className);
|
|
auto cls = Unit::lookupClass(clsName);
|
|
if (cls == nullptr) continue;
|
|
bp->m_bindState = Eval::BreakPointInfo::KnownToBeInvalid;
|
|
size_t numFuncs = cls->numMethods();
|
|
if (numFuncs == 0) continue;
|
|
auto methodName = bp->getFunction();
|
|
Func* const* funcs = cls->methods();
|
|
for (size_t i = 0; i < numFuncs; ++i) {
|
|
auto f = funcs[i];
|
|
if (methodName != f->name()->data()) continue;
|
|
bp->m_bindState = Eval::BreakPointInfo::KnownToBeValid;
|
|
addBreakPointFuncEntry(f);
|
|
break;
|
|
}
|
|
//TODO: what about superclass methods accessed via the derived class?
|
|
//Task 2527229.
|
|
continue;
|
|
}
|
|
auto funcName = bp->getFuncName();
|
|
if (!funcName.empty()) {
|
|
auto fName = StringData::GetStaticString(funcName);
|
|
Func* f = Unit::lookupFunc(fName);
|
|
if (f == nullptr) continue;
|
|
bp->m_bindState = Eval::BreakPointInfo::KnownToBeValid;
|
|
addBreakPointFuncEntry(f);
|
|
continue;
|
|
}
|
|
auto fileName = bp->m_file;
|
|
if (!fileName.empty()) {
|
|
for (EvaledFilesMap::const_iterator it =
|
|
g_vmContext->m_evaledFiles.begin();
|
|
it != g_vmContext->m_evaledFiles.end(); ++it) {
|
|
auto efile = it->second;
|
|
if (!Eval::BreakPointInfo::MatchFile(fileName, efile->getFileName())) {
|
|
continue;
|
|
}
|
|
addBreakPointInUnit(bp, efile->unit());
|
|
break;
|
|
}
|
|
continue;
|
|
}
|
|
auto exceptionClassName = bp->getExceptionClass();
|
|
if (exceptionClassName == "@") {
|
|
bp->m_bindState = Eval::BreakPointInfo::KnownToBeValid;
|
|
continue;
|
|
} else if (!exceptionClassName.empty()) {
|
|
auto expClsName = StringData::GetStaticString(exceptionClassName);
|
|
auto cls = Unit::lookupClass(expClsName);
|
|
if (cls != nullptr) {
|
|
auto baseClsName = StringData::GetStaticString("Exception");
|
|
auto baseCls = Unit::lookupClass(baseClsName);
|
|
if (baseCls != nullptr) {
|
|
if (cls->classof(baseCls)) {
|
|
bp->m_bindState = Eval::BreakPointInfo::KnownToBeValid;
|
|
} else {
|
|
bp->m_bindState = Eval::BreakPointInfo::KnownToBeInvalid;
|
|
}
|
|
}
|
|
}
|
|
continue;
|
|
} else {
|
|
continue;
|
|
}
|
|
// If we get here, the break point is of a type that does
|
|
// not need to be explicitly enabled in the VM. For example
|
|
// a break point that get's triggered when the server starts
|
|
// to process a page request.
|
|
bp->m_bindState = Eval::BreakPointInfo::KnownToBeValid;
|
|
}
|
|
}
|
|
|
|
//////////////////////////////////////////////////////////////////////////
|
|
|
|
struct PCFilter::PtrMapNode {
|
|
void **m_entries;
|
|
void clearImpl(unsigned short bits);
|
|
};
|
|
|
|
void PCFilter::PtrMapNode::clearImpl(unsigned short bits) {
|
|
// clear all the sub levels and mark all slots NULL
|
|
if (bits <= PTRMAP_LEVEL_BITS) {
|
|
assert(bits == PTRMAP_LEVEL_BITS);
|
|
// On bottom level, pointers are not PtrMapNode*
|
|
memset(m_entries, 0, sizeof(void*) * PTRMAP_LEVEL_ENTRIES);
|
|
return;
|
|
}
|
|
for (int i = 0; i < PTRMAP_LEVEL_ENTRIES; i++) {
|
|
if (m_entries[i]) {
|
|
((PCFilter::PtrMapNode*)m_entries[i])->clearImpl(bits -
|
|
PTRMAP_LEVEL_BITS);
|
|
free(((PCFilter::PtrMapNode*)m_entries[i])->m_entries);
|
|
free(m_entries[i]);
|
|
m_entries[i] = nullptr;
|
|
}
|
|
}
|
|
}
|
|
|
|
PCFilter::PtrMapNode* PCFilter::PtrMap::MakeNode() {
|
|
PtrMapNode* node = (PtrMapNode*)malloc(sizeof(PtrMapNode));
|
|
node->m_entries =
|
|
(void**)calloc(1, PTRMAP_LEVEL_ENTRIES * sizeof(void*));
|
|
return node;
|
|
}
|
|
|
|
PCFilter::PtrMap::~PtrMap() {
|
|
clear();
|
|
free(m_root->m_entries);
|
|
free(m_root);
|
|
}
|
|
|
|
void* PCFilter::PtrMap::getPointer(void* ptr) {
|
|
PtrMapNode* current = m_root;
|
|
unsigned short cursor = PTRMAP_PTR_SIZE;
|
|
while (current && cursor) {
|
|
cursor -= PTRMAP_LEVEL_BITS;
|
|
unsigned long index = ((PTRMAP_LEVEL_MASK << cursor) & (unsigned long)ptr)
|
|
>> cursor;
|
|
assert(index < PTRMAP_LEVEL_ENTRIES);
|
|
current = (PtrMapNode*)(current->m_entries[index]);
|
|
}
|
|
return (void*)current;
|
|
}
|
|
|
|
void PCFilter::PtrMap::setPointer(void* ptr, void* val) {
|
|
PtrMapNode* current = m_root;
|
|
unsigned short cursor = PTRMAP_PTR_SIZE;
|
|
while (true) {
|
|
cursor -= PTRMAP_LEVEL_BITS;
|
|
unsigned long index = ((PTRMAP_LEVEL_MASK << cursor) & (unsigned long)ptr)
|
|
>> cursor;
|
|
assert(index < PTRMAP_LEVEL_ENTRIES);
|
|
if (!cursor) {
|
|
current->m_entries[index] = val;
|
|
break;
|
|
}
|
|
if (!current->m_entries[index]) {
|
|
current->m_entries[index] = (void*) MakeNode();
|
|
}
|
|
current = (PtrMapNode*)(current->m_entries[index]);
|
|
}
|
|
}
|
|
|
|
void PCFilter::PtrMap::clear() {
|
|
m_root->clearImpl(PTRMAP_PTR_SIZE);
|
|
}
|
|
|
|
// Adds a range of PCs to the filter given a collection of offset ranges.
|
|
// Omit PCs which have opcodes that don't pass the given opcode filter.
|
|
void PCFilter::addRanges(const Unit* unit, const OffsetRangeVec& offsets,
|
|
OpcodeFilter isOpcodeAllowed) {
|
|
for (auto range = offsets.cbegin(); range != offsets.cend(); ++range) {
|
|
TRACE(3, "\toffsets [%d, %d)\n", range->m_base, range->m_past);
|
|
for (PC pc = unit->at(range->m_base); pc < unit->at(range->m_past);
|
|
pc += instrLen((Op*)pc)) {
|
|
if (isOpcodeAllowed(toOp(*pc))) {
|
|
TRACE(3, "\t\tpc %p\n", pc);
|
|
addPC(pc);
|
|
} else {
|
|
TRACE(3, "\t\tpc %p -- skipping (offset %d)\n", pc, unit->offsetOf(pc));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
void PCFilter::removeOffset(const Unit* unit, Offset offset) {
|
|
removePC(unit->at(offset));
|
|
}
|
|
|
|
//////////////////////////////////////////////////////////////////////////
|
|
}
|