Qt
Internal/Contributor docs for the Qt SDK. Note: These are NOT official API docs; those are found at https://doc.qt.io/
Loading...
Searching...
No Matches
qv4debugger.cpp
Go to the documentation of this file.
1// Copyright (C) 2016 The Qt Company Ltd.
2// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
3// Qt-Security score:significant
4
5#include "qv4debugger.h"
6#include "qv4debugjob.h"
8
9#include <private/qqmlcontext_p.h>
10#include <private/qqmlengine_p.h>
11#include <private/qv4compileddata_p.h>
12#include <private/qv4function_p.h>
13#include <private/qv4scopedvalue_p.h>
14#include <private/qv4script_p.h>
15#include <private/qv4stackframe_p.h>
16
17#include <algorithm>
18
19QT_BEGIN_NAMESPACE
20
21QV4Debugger::BreakPoint::BreakPoint(const QString &fileName, int line)
22 : fileName(fileName), lineNumber(line)
23{}
24
25inline size_t qHash(const QV4Debugger::BreakPoint &b, size_t seed = 0) noexcept
26{
27 return qHash(b.fileName, seed) ^ b.lineNumber;
28}
29
30inline bool operator==(const QV4Debugger::BreakPoint &a,
31 const QV4Debugger::BreakPoint &b)
32{
33 return a.lineNumber == b.lineNumber && a.fileName == b.fileName;
34}
35
36QV4Debugger::QV4Debugger(QV4::ExecutionEngine *engine)
38 , m_state(Running)
39 , m_stepping(NotStepping)
40 , m_pauseRequested(false)
41 , m_haveBreakPoints(false)
42 , m_breakOnThrow(false)
44 , m_gatherSources(nullptr)
45 , m_runningJob(nullptr)
47{
48 static int debuggerId = qRegisterMetaType<QV4Debugger*>();
49 static int pauseReasonId = qRegisterMetaType<QV4Debugger::PauseReason>();
50 Q_UNUSED(debuggerId);
51 Q_UNUSED(pauseReasonId);
52 connect(this, &QV4Debugger::scheduleJob,
53 this, &QV4Debugger::runJobUnpaused, Qt::QueuedConnection);
54}
55
57{
58 return m_engine;
59}
60
62{
63 return &m_collector;
64}
65
67{
68 return &m_collector;
69}
70
72{
73 QMutexLocker locker(&m_lock);
74 if (m_state == Paused)
75 return;
76 m_pauseRequested = true;
77}
78
80{
81 QMutexLocker locker(&m_lock);
82 if (m_state != Paused)
83 return;
84
85 if (!m_returnedValue.isUndefined())
86 m_returnedValue.set(m_engine, QV4::Encode::undefined());
87
88 m_currentFrame = m_engine->currentStackFrame;
89 m_stepping = speed;
90 m_runningCondition.wakeAll();
91}
92
94{
95 return m_state;
96}
97
98void QV4Debugger::addBreakPoint(const QString &fileName, int lineNumber, const QString &condition)
99{
100 QMutexLocker locker(&m_lock);
101 m_breakPoints.insert(BreakPoint(fileName.mid(fileName.lastIndexOf('/') + 1),
102 lineNumber), {fileName, condition});
103 m_haveBreakPoints = true;
104 m_breakPointsChanged = true;
105}
106
107void QV4Debugger::removeBreakPoint(const QString &fileName, int lineNumber)
108{
109 QMutexLocker locker(&m_lock);
110 m_breakPoints.remove(BreakPoint(fileName.mid(fileName.lastIndexOf('/') + 1),
111 lineNumber));
112 m_haveBreakPoints = !m_breakPoints.isEmpty();
113 m_breakPointsChanged = true;
114}
115
117{
118 QMutexLocker locker(&m_lock);
119
120 m_breakOnThrow = onoff;
121}
122
124{
125 QMutexLocker locker(&m_lock);
126 m_pauseRequested = false;
127}
128
130{
131 QMutexLocker locker(&m_lock);
132 return m_executableLines.size();
133}
134
136{
137 ExecutionState state;
138 state.fileName = QUrl(getFunction()->sourceFile()).fileName();
139 state.lineNumber = engine()->currentStackFrame->lineNumber();
140
141 return state;
142}
143
145 // A change of breakpoints has to reach maybeBreakAtInstruction() even when
146 // none are left, so that the cache of executable lines gets pruned.
147 return m_pauseRequested || m_haveBreakPoints || m_breakPointsChanged || m_gatherSources
148 || m_stepping >= StepOver;
149}
150
151QList<QV4::StackFrame> QV4Debugger::stackTrace(int frameLimit) const
152{
153 return m_engine->stackTrace(frameLimit);
154}
155
157{
158 if (m_runningJob) // do not re-enter when we're doing a job for the debugger.
159 return;
160
161 QMutexLocker locker(&m_lock);
162
163 if (m_gatherSources) {
164 m_gatherSources->run();
165 delete m_gatherSources;
166 m_gatherSources = nullptr;
167 }
168
169 if (m_breakPointsChanged) {
170 m_breakPointsChanged = false;
171 pruneExecutableLines();
172 }
173
174 switch (m_stepping) {
175 case StepOver:
176 if (m_currentFrame != m_engine->currentStackFrame)
177 break;
178 Q_FALLTHROUGH();
179 case StepIn:
180 pauseAndWait(Step);
181 return;
182 case StepOut:
183 case NotStepping:
184 break;
185 }
186
187 if (m_pauseRequested) { // Serve debugging requests from the agent
188 m_pauseRequested = false;
189 pauseAndWait(PauseRequest);
190 } else if (m_haveBreakPoints) {
191 if (QV4::Function *f = getFunction()) {
192 // lineNumber will be negative for Ret instructions, so those won't match
193 const int lineNumber = engine()->currentStackFrame->lineNumber();
194 if (reallyHitTheBreakPoint(f, f->sourceFile(), lineNumber))
195 pauseAndWait(BreakPointHit);
196 }
197 }
198}
199
201{
202 if (m_runningJob)
203 return;
204 QMutexLocker locker(&m_lock);
205
206 if (m_stepping == StepIn)
207 m_currentFrame = m_engine->currentStackFrame;
208}
209
210void QV4Debugger::leavingFunction(const QV4::ReturnedValue &retVal)
211{
212 if (m_runningJob)
213 return;
214 Q_UNUSED(retVal); // TODO
215
216 QMutexLocker locker(&m_lock);
217
218 if (m_stepping != NotStepping && m_currentFrame == m_engine->currentStackFrame) {
219 m_currentFrame = m_currentFrame->parentFrame();
220 m_stepping = StepOver;
221 m_returnedValue.set(m_engine, retVal);
222 }
223}
224
226{
227 if (!m_breakOnThrow)
228 return;
229
230 if (m_runningJob) // do not re-enter when we're doing a job for the debugger.
231 return;
232
233 QMutexLocker locker(&m_lock);
234 pauseAndWait(Throwing);
235}
236
238{
239 if (m_engine->currentStackFrame)
240 return m_engine->currentStackFrame->v4Function;
241 else
242 return m_engine->globalCode;
243}
244
245void QV4Debugger::runJobUnpaused()
246{
247 QMutexLocker locker(&m_lock);
248 if (m_runningJob)
249 m_runningJob->run();
250 m_jobIsRunning.wakeAll();
251}
252
253void QV4Debugger::pauseAndWait(PauseReason reason)
254{
255 if (m_runningJob)
256 return;
257
258 m_state = Paused;
259 emit debuggerPaused(this, reason);
260
261 while (true) {
262 m_runningCondition.wait(&m_lock);
263 if (m_runningJob) {
264 m_runningJob->run();
265 m_jobIsRunning.wakeAll();
266 } else {
267 break;
268 }
269 }
270
271 m_state = Running;
272}
273
274/*!
275 \internal
276
277 Collects the executable lines of every function in \a unit, i.e. of the
278 whole source file. Resolution has to be file-global: a breakpoint on a
279 comment inside a nested function must not also match the outer function's
280 first line after the comment.
281*/
282QV4Debugger::ExecutableLines QV4Debugger::collectExecutableLines(
283 const QQmlRefPointer<QV4::CompiledData::CompilationUnit> &unit)
284{
285 ExecutableLines result;
286 const QV4::CompiledData::Unit *data = unit ? unit->unitData() : nullptr;
287 if (!data)
288 return result;
289 for (uint f = 0, fn = data->functionTableSize; f < fn; ++f) {
290 const QV4::CompiledData::Function *cf = data->functionAt(f);
291 const int start = cf->location.line();
292 result.functionStarts.append(start);
293 const QV4::CompiledData::CodeOffsetToLineAndStatement *table
294 = cf->lineAndStatementNumberTable();
295 for (uint i = 0, n = cf->nLineAndStatementNumbers; i < n; ++i) {
296 const int line = table[i].line;
297 if (line > 0) // debug instructions get negative line numbers
298 result.lines.append({line, start});
299 }
300 }
301 std::sort(result.lines.begin(), result.lines.end());
302 result.lines.erase(std::unique(result.lines.begin(), result.lines.end()),
303 result.lines.end());
304 std::sort(result.functionStarts.begin(), result.functionStarts.end());
305 return result;
306}
307
308int QV4Debugger::firstLineAtOrAfter(const ExecutableLines &executable, int requestedLine)
309{
310 // Lines are sorted by line, then by the start of their function, so the
311 // first one at or after the request is from the earliest starting function.
312 const auto it = std::lower_bound(executable.lines.cbegin(), executable.lines.cend(),
313 ExecutableLine{requestedLine, 0});
314 if (it == executable.lines.cend())
315 return -1;
316 // Only move within code that starts at or before the requested line, and
317 // not past the start of another function or binding. A function starting
318 // later runs at an unrelated time, and in QML the requested line may be a
319 // declaration without code, such as an id or a literal binding. A unit may
320 // also cover only a fragment of its file (a binding compiled from a
321 // string, a chunk passed to QJSEngine::evaluate() with a line offset), and
322 // a line before its code belongs to some other unit.
323 if (it->functionStart > requestedLine)
324 return -1;
325 const QList<int> &starts = executable.functionStarts;
326 const auto start = std::upper_bound(starts.cbegin(), starts.cend(), requestedLine);
327 if (start != starts.cend() && *start < it->line)
328 return -1;
329 return it->line;
330}
331
332void QV4Debugger::pruneExecutableLines()
333{
334 // Drop the units of files without breakpoints, so that the cache does not
335 // keep them from being trimmed. This runs on the engine thread, which is
336 // the only one allowed to release a unit.
337 m_executableLines.removeIf([this](const UnitLines &cached) {
338 const QString file = QUrl(cached.unit->fileName()).fileName();
339 for (auto it = m_breakPoints.keyBegin(), end = m_breakPoints.keyEnd(); it != end; ++it) {
340 if (it->fileName == file)
341 return false;
342 }
343 return true;
344 });
345}
346
347/*!
348 \internal
349
350 Returns the executable lines of the compilation unit \a function belongs
351 to, cached for the most recent units.
352*/
353const QV4Debugger::ExecutableLines &QV4Debugger::executableLines(QV4::Function *function)
354{
355 // Enough for execution moving back and forth between the files of a scene
356 // that all have breakpoints, few enough for a linear search.
357 constexpr qsizetype MaxCachedUnits = 8;
358
359 const QQmlRefPointer<QV4::CompiledData::CompilationUnit> &unit
360 = function->executableCompilationUnit()->baseCompilationUnit();
361 for (qsizetype i = 0, n = m_executableLines.size(); i < n; ++i) {
362 if (m_executableLines.at(i).unit == unit) {
363 if (i > 0)
364 m_executableLines.move(i, 0);
365 return m_executableLines.first().lines;
366 }
367 }
368 if (m_executableLines.size() == MaxCachedUnits)
369 m_executableLines.removeLast();
370 m_executableLines.prepend({unit, collectExecutableLines(unit)});
371 return m_executableLines.first().lines;
372}
373
374/*!
375 \internal
376
377 While paused, returns the line a breakpoint requested on \a requestedLine
378 resolves to in the unit currently being executed, or -1. Lets the agent
379 attribute a hit that landed on a shifted line back to the breakpoint the
380 user placed on a non-executable line.
381*/
382int QV4Debugger::resolvedBreakpointLine(int requestedLine)
383{
384 QMutexLocker locker(&m_lock);
385 QV4::Function *f = getFunction();
386 if (!f)
387 return -1;
388 // This runs on the agent's thread, so it must not change the cache: dropping
389 // the reference to an evicted unit could destroy that unit here.
390 const QQmlRefPointer<QV4::CompiledData::CompilationUnit> &unit
391 = f->executableCompilationUnit()->baseCompilationUnit();
392 for (const UnitLines &cached : std::as_const(m_executableLines)) {
393 if (cached.unit == unit)
394 return firstLineAtOrAfter(cached.lines, requestedLine);
395 }
396 return firstLineAtOrAfter(collectExecutableLines(unit), requestedLine);
397}
398
399bool QV4Debugger::reallyHitTheBreakPoint(QV4::Function *function, const QString &filename,
400 int linenr)
401{
402 const QString file = QUrl(filename).fileName();
403
404 // Besides a breakpoint exactly on this line, a breakpoint requested on a
405 // non-executable line (e.g. a comment or blank line) counts here if this is
406 // the first executable line at or after it. Any of them can stop execution,
407 // so all of them have to be checked.
408 QStringList conditions;
409 for (auto bp = m_breakPoints.constBegin(), end = m_breakPoints.constEnd(); bp != end; ++bp) {
410 if (bp.key().fileName != file || bp.key().lineNumber > linenr)
411 continue;
412 // Only relocate in a unit the client's target identifies: a same-named
413 // file elsewhere has unrelated executable lines.
414 if (bp.key().lineNumber != linenr
415 && (!filename.endsWith(bp.value().target)
416 || firstLineAtOrAfter(executableLines(function), bp.key().lineNumber)
417 != linenr)) {
418 continue;
419 }
420 if (bp.value().condition.isEmpty())
421 return true;
422 conditions.append(bp.value().condition);
423 }
424
425 for (const QString &condition : std::as_const(conditions)) {
426 Q_ASSERT(m_runningJob == nullptr);
427 EvalJob evilJob(m_engine, condition);
428 m_runningJob = &evilJob;
429 m_runningJob->run();
430 m_runningJob = nullptr;
431 if (evilJob.resultAsBoolean())
432 return true;
433 }
434 return false;
435}
436
438{
439 QMutexLocker locker(&m_lock);
440 runInEngine_havingLock(job);
441}
442
443void QV4Debugger::runInEngine_havingLock(QV4DebugJob *job)
444{
445 Q_ASSERT(job);
446 Q_ASSERT(m_runningJob == nullptr);
447
448 m_runningJob = job;
449 if (state() == Paused)
450 m_runningCondition.wakeAll();
451 else
452 emit scheduleJob();
453 m_jobIsRunning.wait(&m_lock);
454 m_runningJob = nullptr;
455}
456
457QT_END_NAMESPACE
458
459#include "moc_qv4debugger.cpp"
friend bool operator==(const QByteArray::FromBase64Result &lhs, const QByteArray::FromBase64Result &rhs) noexcept
Returns true if lhs and rhs are equal, otherwise returns false.
Definition qbytearray.h:842
virtual void run()=0
qsizetype executableLinesCacheSize()
void maybeBreakAtInstruction() override
QV4::Function * getFunction() const
void enteringFunction() override
void scheduleJob()
void runInEngine(QV4DebugJob *job)
int resolvedBreakpointLine(int requestedLine)
QV4DataCollector * collector()
void removeBreakPoint(const QString &fileName, int lineNumber)
QV4::ExecutionEngine * engine() const
void resume(Speed speed)
void setBreakOnThrow(bool onoff)
bool pauseAtNextOpportunity() const override
void leavingFunction(const QV4::ReturnedValue &retVal) override
State state() const
ExecutionState currentExecutionState() const
const QV4DataCollector * collector() const
QV4Debugger(QV4::ExecutionEngine *engine)
void aboutToThrow() override
void addBreakPoint(const QString &fileName, int lineNumber, const QString &condition=QString())
void clearPauseRequest()
QList< QV4::StackFrame > stackTrace(int frameLimit=-1) const
constexpr size_t qHash(const QSize &s, size_t seed=0) noexcept
Definition qsize.h:192