2 +----------------------------------------------------------------------+
4 +----------------------------------------------------------------------+
5 | Copyright (c) 2010-2013 Facebook, Inc. (http://www.facebook.com) |
6 +----------------------------------------------------------------------+
7 | This source file is subject to version 3.01 of the PHP license, |
8 | that is bundled with this package in the file LICENSE, and is |
9 | available through the world-wide-web at the following url: |
10 | http://www.php.net/license/3_01.txt |
11 | If you did not receive a copy of the PHP license and are unable to |
12 | obtain it through the world-wide-web, please send a note to |
13 | license@php.net so we can mail you a copy immediately. |
14 +----------------------------------------------------------------------+
17 #include "hphp/runtime/vm/debugger_hook.h"
18 #include "hphp/runtime/vm/jit/translator.h"
19 #include "hphp/runtime/debugger/break_point.h"
20 #include "hphp/runtime/debugger/debugger.h"
21 #include "hphp/runtime/debugger/debugger_proxy.h"
22 #include "hphp/runtime/base/file_repository.h"
23 #include "hphp/util/logger.h"
24 #include "hphp/util/util.h"
28 //////////////////////////////////////////////////////////////////////////
30 TRACE_SET_MOD(debuggerflow
);
32 static inline Transl::Translator
* transl() {
33 return Transl::Translator::Get();
36 // Hook called from the bytecode interpreter before every opcode executed while
37 // a debugger is attached. The debugger may choose to hold the thread below
38 // here and execute any number of commands from the client. Return from here
39 // lets the opcode execute.
40 void phpDebuggerOpcodeHook(const uchar
* pc
) {
41 TRACE(5, "in phpDebuggerOpcodeHook()\n");
42 // Short-circuit when we're doing things like evaling PHP for print command,
43 // or conditional breakpoints.
44 if (UNLIKELY(g_vmContext
->m_dbgNoBreak
)) {
45 TRACE(5, "NoBreak flag is on\n");
48 // Short-circuit for cases where we're executing a line of code that we know
49 // we don't need an interrupt for, e.g., stepping over a line of code.
50 if (UNLIKELY(g_vmContext
->m_lastLocFilter
!= nullptr) &&
51 g_vmContext
->m_lastLocFilter
->checkPC(pc
)) {
52 TRACE_RB(5, "Location filter hit at pc %p\n", pc
);
55 // Are we hitting a breakpoint?
56 if (LIKELY(g_vmContext
->m_breakPointFilter
== nullptr ||
57 !g_vmContext
->m_breakPointFilter
->checkPC(pc
))) {
58 TRACE(5, "not in the PC range for any breakpoints\n");
59 if (LIKELY(!DEBUGGER_FORCE_INTR
)) {
62 TRACE_RB(5, "DEBUGGER_FORCE_INTR\n");
64 Eval::Debugger::InterruptVMHook();
65 TRACE(5, "out phpDebuggerOpcodeHook()\n");
68 // Hook called from iopThrow to signal that we are about to throw an exception.
69 void phpDebuggerExceptionThrownHook(ObjectData
* exception
) {
70 TRACE(5, "in phpDebuggerExceptionThrownHook()\n");
71 if (UNLIKELY(g_vmContext
->m_dbgNoBreak
)) {
72 TRACE(5, "NoBreak flag is on\n");
75 Eval::Debugger::InterruptVMHook(Eval::ExceptionThrown
, exception
);
76 TRACE(5, "out phpDebuggerExceptionThrownHook()\n");
79 // Hook called from exception unwind to signal that we are about to handle an
81 void phpDebuggerExceptionHandlerHook() {
82 TRACE(5, "in phpDebuggerExceptionHandlerHook()\n");
83 if (UNLIKELY(g_vmContext
->m_dbgNoBreak
)) {
84 TRACE(5, "NoBreak flag is on\n");
87 Eval::Debugger::InterruptVMHook(Eval::ExceptionHandler
);
88 TRACE(5, "out phpDebuggerExceptionHandlerHook()\n");
91 // Hook called when the VM raises an error.
92 void phpDebuggerErrorHook(const std::string
& message
) {
93 TRACE(5, "in phpDebuggerErrorHook()\n");
94 if (UNLIKELY(g_vmContext
->m_dbgNoBreak
)) {
95 TRACE(5, "NoBreak flag is on\n");
98 Eval::Debugger::InterruptVMHook(Eval::ExceptionThrown
, String(message
));
99 TRACE(5, "out phpDebuggerErrorHook()\n");
102 bool isDebuggerAttachedProcess() {
103 return Eval::Debugger::CountConnectedProxy() > 0;
106 // Ensure we interpret all code at the given offsets. This sets up a guard for
107 // each piece of translated code to ensure we punt to the interpreter when the
108 // debugger is attached.
109 static void blacklistRangesInJit(const Unit
* unit
,
110 const OffsetRangeVec
& offsets
) {
111 for (OffsetRangeVec::const_iterator it
= offsets
.begin();
112 it
!= offsets
.end(); ++it
) {
113 for (PC pc
= unit
->at(it
->m_base
); pc
< unit
->at(it
->m_past
);
114 pc
+= instrLen((Opcode
*)pc
)) {
115 transl()->addDbgBLPC(pc
);
118 if (!transl()->addDbgGuards(unit
)) {
119 Logger::Warning("Failed to set breakpoints in Jitted code");
121 // In this case, we may be setting a breakpoint in a tracelet which could
122 // already be jitted, and present on the stack. Make sure we don't return
123 // to it so we have a chance to honor breakpoints.
124 g_vmContext
->preventReturnsToTC();
127 // Ensure we interpret an entire function when the debugger is attached.
128 static void blacklistFuncInJit(const Func
* f
) {
129 Unit
* unit
= f
->unit();
130 OffsetRangeVec ranges
;
131 ranges
.push_back(OffsetRange(f
->base(), f
->past()));
132 blacklistRangesInJit(unit
, ranges
);
135 static PCFilter
*getBreakPointFilter() {
136 if (!g_vmContext
->m_breakPointFilter
) {
137 g_vmContext
->m_breakPointFilter
= new PCFilter();
139 return g_vmContext
->m_breakPointFilter
;
142 // Looks up the offset range in the given unit, of the given breakpoint.
143 // If the offset cannot be found, the breakpoint is marked as invalid.
144 // Otherwise it is marked as valid and the offset is added to the
145 // breakpoint filter and the offset range is black listed for the JIT.
146 static void addBreakPointInUnit(Eval::BreakPointInfoPtr bp
, Unit
* unit
) {
147 OffsetRangeVec offsets
;
148 if (!unit
->getOffsetRanges(bp
->m_line1
, offsets
) || offsets
.size() == 0) {
149 bp
->m_bindState
= Eval::BreakPointInfo::KnownToBeInvalid
;
152 bp
->m_bindState
= Eval::BreakPointInfo::KnownToBeValid
;
153 TRACE(3, "Add to breakpoint filter for %s:%d, unit %p:\n",
154 unit
->filepath()->data(), bp
->m_line1
, unit
);
155 getBreakPointFilter()->addRanges(unit
, offsets
);
156 if (RuntimeOption::EvalJit
) {
157 blacklistRangesInJit(unit
, offsets
);
161 static void addBreakPointsInFile(Eval::DebuggerProxy
* proxy
,
162 Eval::PhpFile
* efile
) {
163 Eval::BreakPointInfoPtrVec bps
;
164 proxy
->getBreakPoints(bps
);
165 for (unsigned int i
= 0; i
< bps
.size(); i
++) {
166 Eval::BreakPointInfoPtr bp
= bps
[i
];
167 if (Eval::BreakPointInfo::MatchFile(bp
->m_file
, efile
->getFileName(),
168 efile
->getRelPath())) {
169 addBreakPointInUnit(bp
, efile
->unit());
175 static void addBreakPointFuncEntry(const Func
* f
) {
176 PC pc
= f
->unit()->at(f
->base());
177 TRACE(5, "func() break %s : unit %p offset %d)\n",
178 f
->fullName()->data(), f
->unit(), f
->base());
179 getBreakPointFilter()->addPC(pc
);
180 if (RuntimeOption::EvalJit
) {
181 if (transl()->addDbgBLPC(pc
)) {
182 // if a new entry is added in blacklist
183 if (!transl()->addDbgGuard(f
, f
->base())) {
184 Logger::Warning("Failed to set breakpoints in Jitted code");
190 // If the proxy has an enabled breakpoint that matches entry into the given
191 // function, arrange for the VM to stop execution and notify the debugger
192 // whenever execution enters the given function.
193 static void addBreakPointFuncEntry(Eval::DebuggerProxy
* proxy
, const Func
* f
) {
194 Eval::BreakPointInfoPtrVec bps
;
195 proxy
->getBreakPoints(bps
);
196 for (unsigned int i
= 0; i
< bps
.size(); i
++) {
197 Eval::BreakPointInfoPtr bp
= bps
[i
];
198 if (bp
->m_state
== Eval::BreakPointInfo::Disabled
) continue;
199 if (bp
->getFuncName() != f
->fullName()->data()) continue;
200 bp
->m_bindState
= Eval::BreakPointInfo::KnownToBeValid
;
201 addBreakPointFuncEntry(f
);
206 // If the proxy has enabled breakpoints that match entry into methods of
207 // the given class, arrange for the VM to stop execution and notify the debugger
208 // whenever execution enters one of these matched method.
209 // This function is called once, when a class is first loaded, so it is not
210 // performance critical.
211 static void addBreakPointsClass(Eval::DebuggerProxy
* proxy
, const Class
* cls
) {
212 size_t numFuncs
= cls
->numMethods();
213 if (numFuncs
== 0) return;
214 auto clsName
= cls
->name();
215 auto funcs
= cls
->methods();
216 Eval::BreakPointInfoPtrVec bps
;
217 proxy
->getBreakPoints(bps
);
218 for (unsigned int i
= 0; i
< bps
.size(); i
++) {
219 Eval::BreakPointInfoPtr bp
= bps
[i
];
220 if (bp
->m_state
== Eval::BreakPointInfo::Disabled
) continue;
221 // TODO: check name space separately
222 if (bp
->getClass() != clsName
->data()) continue;
223 bp
->m_bindState
= Eval::BreakPointInfo::KnownToBeInvalid
;
224 for (size_t i
= 0; i
< numFuncs
; ++i
) {
226 if (bp
->getFunction() != f
->name()->data()) continue;
227 bp
->m_bindState
= Eval::BreakPointInfo::KnownToBeValid
;
228 addBreakPointFuncEntry(f
);
233 void phpAddBreakPoint(const Unit
* unit
, Offset offset
) {
234 PC pc
= unit
->at(offset
);
235 getBreakPointFilter()->addPC(pc
);
236 if (RuntimeOption::EvalJit
) {
237 if (transl()->addDbgBLPC(pc
)) {
238 // if a new entry is added in blacklist
239 if (!transl()->addDbgGuards(unit
)) {
240 Logger::Warning("Failed to set breakpoints in Jitted code");
242 // In this case, we may be setting a breakpoint in a tracelet which could
243 // already be jitted, and present on the stack. Make sure we don't return
244 // to it so we have a chance to honor breakpoints.
245 g_vmContext
->preventReturnsToTC();
250 void phpRemoveBreakPoint(const Unit
* unit
, Offset offset
) {
251 if (g_vmContext
->m_breakPointFilter
) {
252 PC pc
= unit
->at(offset
);
253 g_vmContext
->m_breakPointFilter
->removePC(pc
);
257 void phpDebuggerEvalHook(const Func
* f
) {
258 if (RuntimeOption::EvalJit
) {
259 blacklistFuncInJit(f
);
263 // Called by the VM when a file is loaded.
264 void phpDebuggerFileLoadHook(Eval::PhpFile
* efile
) {
265 Eval::DebuggerProxyPtr proxy
= Eval::Debugger::GetProxy();
266 if (proxy
== nullptr) return;
267 addBreakPointsInFile(proxy
.get(), efile
);
270 // Called by the VM when a class definition is loaded.
271 void phpDebuggerDefClassHook(const Class
* cls
) {
272 Eval::DebuggerProxyPtr proxy
= Eval::Debugger::GetProxy();
273 if (proxy
== nullptr) return;
274 addBreakPointsClass(proxy
.get(), cls
);
277 // Called by the VM when a function definition is loaded.
278 void phpDebuggerDefFuncHook(const Func
* func
) {
279 Eval::DebuggerProxyPtr proxy
= Eval::Debugger::GetProxy();
280 if (proxy
== nullptr) return;
281 addBreakPointFuncEntry(proxy
.get(), func
);
284 // Called by the proxy whenever its breakpoint list is updated.
285 // Since this intended to be called when user input is received, it is not
286 // performance critical. Also, in typical scenarios, the list is short.
287 void phpSetBreakPoints(Eval::DebuggerProxy
* proxy
) {
288 Eval::BreakPointInfoPtrVec bps
;
289 proxy
->getBreakPoints(bps
);
290 for (unsigned int i
= 0; i
< bps
.size(); i
++) {
291 Eval::BreakPointInfoPtr bp
= bps
[i
];
292 bp
->m_bindState
= Eval::BreakPointInfo::Unknown
;
293 auto className
= bp
->getClass();
294 if (!className
.empty()) {
295 auto clsName
= StringData::GetStaticString(className
);
296 auto cls
= Unit::lookupClass(clsName
);
297 if (cls
== nullptr) continue;
298 bp
->m_bindState
= Eval::BreakPointInfo::KnownToBeInvalid
;
299 size_t numFuncs
= cls
->numMethods();
300 if (numFuncs
== 0) continue;
301 auto methodName
= bp
->getFunction();
302 Func
* const* funcs
= cls
->methods();
303 for (size_t i
= 0; i
< numFuncs
; ++i
) {
305 if (methodName
!= f
->name()->data()) continue;
306 bp
->m_bindState
= Eval::BreakPointInfo::KnownToBeValid
;
307 addBreakPointFuncEntry(f
);
310 //TODO: what about superclass methods accessed via the derived class?
314 auto funcName
= bp
->getFuncName();
315 if (!funcName
.empty()) {
316 auto fName
= StringData::GetStaticString(funcName
);
317 Func
* f
= Unit::lookupFunc(fName
);
318 if (f
== nullptr) continue;
319 bp
->m_bindState
= Eval::BreakPointInfo::KnownToBeValid
;
320 addBreakPointFuncEntry(f
);
323 auto fileName
= bp
->m_file
;
324 if (!fileName
.empty()) {
325 for (EvaledFilesMap::const_iterator it
=
326 g_vmContext
->m_evaledFiles
.begin();
327 it
!= g_vmContext
->m_evaledFiles
.end(); ++it
) {
328 auto efile
= it
->second
;
329 if (!Eval::BreakPointInfo::MatchFile(fileName
, efile
->getFileName(),
330 efile
->getRelPath())) continue;
331 addBreakPointInUnit(bp
, efile
->unit());
336 auto exceptionClassName
= bp
->getExceptionClass();
337 if (exceptionClassName
== "@") {
338 bp
->m_bindState
= Eval::BreakPointInfo::KnownToBeValid
;
340 } else if (!exceptionClassName
.empty()) {
341 auto expClsName
= StringData::GetStaticString(exceptionClassName
);
342 auto cls
= Unit::lookupClass(expClsName
);
343 if (cls
!= nullptr) {
344 auto baseClsName
= StringData::GetStaticString("Exception");
345 auto baseCls
= Unit::lookupClass(baseClsName
);
346 if (baseCls
!= nullptr) {
347 if (cls
->classof(baseCls
)) {
348 bp
->m_bindState
= Eval::BreakPointInfo::KnownToBeValid
;
350 bp
->m_bindState
= Eval::BreakPointInfo::KnownToBeInvalid
;
358 // If we get here, the break point is of a type that does
359 // not need to be explicitly enabled in the VM. For example
360 // a break point that get's triggered when the server starts
361 // to process a page request.
362 bp
->m_bindState
= Eval::BreakPointInfo::KnownToBeValid
;
366 //////////////////////////////////////////////////////////////////////////
368 struct PCFilter::PtrMapNode
{
370 void clearImpl(unsigned short bits
);
373 void PCFilter::PtrMapNode::clearImpl(unsigned short bits
) {
374 // clear all the sub levels and mark all slots NULL
375 if (bits
<= PTRMAP_LEVEL_BITS
) {
376 assert(bits
== PTRMAP_LEVEL_BITS
);
377 // On bottom level, pointers are not PtrMapNode*
378 memset(m_entries
, 0, sizeof(void*) * PTRMAP_LEVEL_ENTRIES
);
381 for (int i
= 0; i
< PTRMAP_LEVEL_ENTRIES
; i
++) {
383 ((PCFilter::PtrMapNode
*)m_entries
[i
])->clearImpl(bits
-
385 free(((PCFilter::PtrMapNode
*)m_entries
[i
])->m_entries
);
387 m_entries
[i
] = nullptr;
392 PCFilter::PtrMapNode
* PCFilter::PtrMap::MakeNode() {
393 PtrMapNode
* node
= (PtrMapNode
*)malloc(sizeof(PtrMapNode
));
395 (void**)calloc(1, PTRMAP_LEVEL_ENTRIES
* sizeof(void*));
399 PCFilter::PtrMap::~PtrMap() {
401 free(m_root
->m_entries
);
405 void* PCFilter::PtrMap::getPointer(void* ptr
) {
406 PtrMapNode
* current
= m_root
;
407 unsigned short cursor
= PTRMAP_PTR_SIZE
;
408 while (current
&& cursor
) {
409 cursor
-= PTRMAP_LEVEL_BITS
;
410 unsigned long index
= ((PTRMAP_LEVEL_MASK
<< cursor
) & (unsigned long)ptr
)
412 assert(index
< PTRMAP_LEVEL_ENTRIES
);
413 current
= (PtrMapNode
*)(current
->m_entries
[index
]);
415 return (void*)current
;
418 void PCFilter::PtrMap::setPointer(void* ptr
, void* val
) {
419 PtrMapNode
* current
= m_root
;
420 unsigned short cursor
= PTRMAP_PTR_SIZE
;
422 cursor
-= PTRMAP_LEVEL_BITS
;
423 unsigned long index
= ((PTRMAP_LEVEL_MASK
<< cursor
) & (unsigned long)ptr
)
425 assert(index
< PTRMAP_LEVEL_ENTRIES
);
427 current
->m_entries
[index
] = val
;
430 if (!current
->m_entries
[index
]) {
431 current
->m_entries
[index
] = (void*) MakeNode();
433 current
= (PtrMapNode
*)(current
->m_entries
[index
]);
437 void PCFilter::PtrMap::clear() {
438 m_root
->clearImpl(PTRMAP_PTR_SIZE
);
441 // Adds a range of PCs to the filter given a collection of offset ranges.
442 // Omit PCs which have opcodes that don't pass the given opcode filter.
443 void PCFilter::addRanges(const Unit
* unit
, const OffsetRangeVec
& offsets
,
444 OpcodeFilter isOpcodeAllowed
) {
445 for (auto range
= offsets
.cbegin(); range
!= offsets
.cend(); ++range
) {
446 TRACE(3, "\toffsets [%d, %d)\n", range
->m_base
, range
->m_past
);
447 for (PC pc
= unit
->at(range
->m_base
); pc
< unit
->at(range
->m_past
);
448 pc
+= instrLen(pc
)) {
449 if (isOpcodeAllowed(*pc
)) {
450 TRACE(3, "\t\tpc %p\n", pc
);
453 TRACE(3, "\t\tpc %p -- skipping (offset %d)\n", pc
, unit
->offsetOf(pc
));
459 void PCFilter::removeOffset(const Unit
* unit
, Offset offset
) {
460 removePC(unit
->at(offset
));
463 //////////////////////////////////////////////////////////////////////////