Skip to content

Commit 3a38234

Browse files
authored
Add UI and C++/Python API to time travel to a given timestamp with custom icon (#848)
1 parent 208ee1d commit 3a38234

File tree

10 files changed

+454
-52
lines changed

10 files changed

+454
-52
lines changed

api/python/debuggercontroller.py

Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -494,6 +494,62 @@ def _notify(event: dbgcore.BNDebuggerEvent, callback: DebuggerEventCallback) ->
494494
binaryninja.log_error(traceback.format_exc())
495495

496496

497+
class TTDPosition:
498+
"""
499+
TTDPosition represents a position in a time travel debugging trace.
500+
501+
It has the following fields:
502+
503+
* ``sequence``: the sequence number (as hex string or int)
504+
* ``step``: the step number within the sequence (as hex string or int)
505+
"""
506+
507+
def __init__(self, sequence, step):
508+
if isinstance(sequence, str):
509+
self.sequence = int(sequence, 16)
510+
else:
511+
self.sequence = int(sequence)
512+
513+
if isinstance(step, str):
514+
self.step = int(step, 16)
515+
else:
516+
self.step = int(step)
517+
518+
def __eq__(self, other):
519+
if not isinstance(other, self.__class__):
520+
return NotImplemented
521+
return self.sequence == other.sequence and self.step == other.step
522+
523+
def __ne__(self, other):
524+
if not isinstance(other, self.__class__):
525+
return NotImplemented
526+
return not (self == other)
527+
528+
def __hash__(self):
529+
return hash((self.sequence, self.step))
530+
531+
def __repr__(self):
532+
return f"<TTDPosition: {self.sequence:x}:{self.step:x}>"
533+
534+
def __str__(self):
535+
return f"{self.sequence:x}:{self.step:x}"
536+
537+
@classmethod
538+
def from_string(cls, timestamp_str):
539+
"""
540+
Create a TTDPosition from a string in format "sequence:step"
541+
Both sequence and step can be in hex or decimal format.
542+
"""
543+
if ':' not in timestamp_str:
544+
raise ValueError("Timestamp must be in format 'sequence:step'")
545+
546+
parts = timestamp_str.strip().split(':')
547+
if len(parts) != 2:
548+
raise ValueError("Timestamp must be in format 'sequence:step'")
549+
550+
return cls(parts[0], parts[1])
551+
552+
497553
class DebuggerController:
498554
"""
499555
The ``DebuggerController`` object is the core of the debugger. Most debugger operations can be performed on it.
@@ -1636,6 +1692,87 @@ def is_first_launch(self):
16361692
def is_ttd(self):
16371693
return dbgcore.BNDebuggerIsTTD(self.handle)
16381694

1695+
@property
1696+
def current_ttd_position(self):
1697+
"""
1698+
Get the current position in the TTD trace.
1699+
1700+
Returns:
1701+
TTDPosition: Current position, or None if not in TTD mode
1702+
"""
1703+
if not self.is_ttd:
1704+
return None
1705+
1706+
pos = dbgcore.BNDebuggerGetCurrentTTDPosition(self.handle)
1707+
return TTDPosition(pos.sequence, pos.step)
1708+
1709+
@current_ttd_position.setter
1710+
def current_ttd_position(self, position):
1711+
"""
1712+
Navigate to a specific position in the TTD trace.
1713+
1714+
Args:
1715+
position: TTDPosition object or string in format "sequence:step"
1716+
"""
1717+
if not self.is_ttd:
1718+
raise RuntimeError("TTD is not active")
1719+
1720+
if isinstance(position, str):
1721+
position = TTDPosition.from_string(position)
1722+
elif not isinstance(position, TTDPosition):
1723+
raise TypeError("Position must be TTDPosition object or string")
1724+
1725+
# Create the C structure
1726+
pos = dbgcore.BNDebuggerTTDPosition()
1727+
pos.sequence = position.sequence
1728+
pos.step = position.step
1729+
1730+
success = dbgcore.BNDebuggerSetTTDPosition(self.handle, pos)
1731+
if not success:
1732+
raise RuntimeError("Failed to navigate to the specified TTD position")
1733+
1734+
def set_ttd_position(self, position):
1735+
"""
1736+
Navigate to a specific position in the TTD trace.
1737+
1738+
Args:
1739+
position: TTDPosition object or string in format "sequence:step"
1740+
1741+
Returns:
1742+
bool: True if navigation succeeded, False otherwise
1743+
"""
1744+
if not self.is_ttd:
1745+
return False
1746+
1747+
if isinstance(position, str):
1748+
position = TTDPosition.from_string(position)
1749+
elif not isinstance(position, TTDPosition):
1750+
raise TypeError("Position must be TTDPosition object or string")
1751+
1752+
# Create the C structure
1753+
pos = dbgcore.BNDebuggerTTDPosition()
1754+
pos.sequence = position.sequence
1755+
pos.step = position.step
1756+
1757+
return dbgcore.BNDebuggerSetTTDPosition(self.handle, pos)
1758+
1759+
def navigate_to_timestamp(self, timestamp_str):
1760+
"""
1761+
Convenience method to navigate to a timestamp string.
1762+
1763+
Args:
1764+
timestamp_str: String in format "sequence:step" (hex or decimal)
1765+
1766+
Returns:
1767+
bool: True if navigation succeeded, False otherwise
1768+
"""
1769+
try:
1770+
position = TTDPosition.from_string(timestamp_str)
1771+
return self.set_ttd_position(position)
1772+
except ValueError as e:
1773+
binaryninja.log_error(f"Invalid timestamp format: {e}")
1774+
return False
1775+
16391776
def __del__(self):
16401777
if dbgcore is not None:
16411778
dbgcore.BNDebuggerFreeController(self.handle)

core/adapters/dbgengttdadapter.cpp

Lines changed: 62 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -379,75 +379,85 @@ TTDPosition DbgEngTTDAdapter::GetCurrentTTDPosition()
379379
return position;
380380
}
381381

382-
// Use data model API to get current TTD position
383-
std::string output = EvaluateDataModelExpression("@$cursession.TTD.Position");
382+
// Always use the !position command to retrieve the current timestamp
383+
std::string output = InvokeBackendCommand("!position");
384384

385-
if (!output.empty() && output != "complex_result")
385+
if (!output.empty())
386386
{
387-
// Parse the position output (format like "1A0:12F")
388-
size_t colonPos = output.find(':');
389-
if (colonPos != std::string::npos)
387+
// Parse the position output (format like "Time Travel Position: 602C:0")
388+
size_t prefixPos = output.find("Time Travel Position:");
389+
if (prefixPos != std::string::npos)
390390
{
391-
try
391+
// Find the position data after the prefix
392+
size_t dataStart = prefixPos + strlen("Time Travel Position:");
393+
std::string positionData = output.substr(dataStart);
394+
395+
// Find the colon in the position data
396+
size_t colonPos = positionData.find(':');
397+
if (colonPos != std::string::npos)
392398
{
393-
std::string seqStr = output.substr(0, colonPos);
394-
std::string stepStr = output.substr(colonPos + 1);
395-
396-
// Remove any non-hex characters
397-
seqStr.erase(std::remove_if(seqStr.begin(), seqStr.end(),
398-
[](char c) { return !std::isxdigit(c); }), seqStr.end());
399-
stepStr.erase(std::remove_if(stepStr.begin(), stepStr.end(),
400-
[](char c) { return !std::isxdigit(c); }), stepStr.end());
401-
402-
if (!seqStr.empty() && !stepStr.empty())
399+
try
403400
{
404-
position.sequence = std::stoull(seqStr, nullptr, 16);
405-
position.step = std::stoull(stepStr, nullptr, 16);
401+
std::string seqStr = positionData.substr(0, colonPos);
402+
std::string stepStr = positionData.substr(colonPos + 1);
403+
404+
// Remove any non-hex characters
405+
seqStr.erase(std::remove_if(seqStr.begin(), seqStr.end(),
406+
[](char c) { return !std::isxdigit(c); }), seqStr.end());
407+
stepStr.erase(std::remove_if(stepStr.begin(), stepStr.end(),
408+
[](char c) { return !std::isxdigit(c); }), stepStr.end());
409+
410+
if (!seqStr.empty() && !stepStr.empty())
411+
{
412+
position.sequence = std::stoull(seqStr, nullptr, 16);
413+
position.step = std::stoull(stepStr, nullptr, 16);
414+
}
415+
}
416+
catch (const std::exception& e)
417+
{
418+
LogError("Failed to parse TTD position: %s", e.what());
406419
}
407-
}
408-
catch (const std::exception& e)
409-
{
410-
LogError("Failed to parse TTD position: %s", e.what());
411420
}
412421
}
413-
}
414-
else
415-
{
416-
// Fallback to command interface if data model doesn't work
417-
LogWarn("Data model evaluation failed, falling back to command interface");
418-
std::string output = InvokeBackendCommand("!position");
419-
420-
// Parse the position output (format like "1A0:12F")
421-
size_t colonPos = output.find(':');
422-
if (colonPos != std::string::npos)
422+
else
423423
{
424-
try
424+
// Fallback: try to find a simple "XXXX:Y" pattern in the output
425+
size_t colonPos = output.find(':');
426+
if (colonPos != std::string::npos)
425427
{
426-
std::string seqStr = output.substr(0, colonPos);
427-
std::string stepStr = output.substr(colonPos + 1);
428-
429-
// Remove any non-hex characters
430-
seqStr.erase(std::remove_if(seqStr.begin(), seqStr.end(),
431-
[](char c) { return !std::isxdigit(c); }), seqStr.end());
432-
stepStr.erase(std::remove_if(stepStr.begin(), stepStr.end(),
433-
[](char c) { return !std::isxdigit(c); }), stepStr.end());
434-
435-
if (!seqStr.empty() && !stepStr.empty())
428+
try
436429
{
437-
position.sequence = std::stoull(seqStr, nullptr, 16);
438-
position.step = std::stoull(stepStr, nullptr, 16);
430+
// Look backwards from colon to find start of hex sequence
431+
size_t seqStart = colonPos;
432+
while (seqStart > 0 && std::isxdigit(output[seqStart - 1]))
433+
seqStart--;
434+
435+
// Look forwards from colon to find end of hex step
436+
size_t stepEnd = colonPos + 1;
437+
while (stepEnd < output.length() && std::isxdigit(output[stepEnd]))
438+
stepEnd++;
439+
440+
if (seqStart < colonPos && stepEnd > colonPos + 1)
441+
{
442+
std::string seqStr = output.substr(seqStart, colonPos - seqStart);
443+
std::string stepStr = output.substr(colonPos + 1, stepEnd - colonPos - 1);
444+
445+
if (!seqStr.empty() && !stepStr.empty())
446+
{
447+
position.sequence = std::stoull(seqStr, nullptr, 16);
448+
position.step = std::stoull(stepStr, nullptr, 16);
449+
}
450+
}
451+
}
452+
catch (const std::exception& e)
453+
{
454+
LogError("Failed to parse TTD position from fallback parsing: %s", e.what());
439455
}
440-
}
441-
catch (const std::exception& e)
442-
{
443-
LogError("Failed to parse TTD position: %s", e.what());
444456
}
445457
}
446458
}
447459

448460
return position;
449-
450-
return position;
451461
}
452462

453463
bool DbgEngTTDAdapter::SetTTDPosition(const TTDPosition& position)

debuggerui.qrc

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,5 +24,6 @@
2424
<file alias="stop">icons/stop.png</file>
2525
<file alias="ttd-memory">icons/ttd-memory.png</file>
2626
<file alias="ttd-calls">icons/ttd-calls.png</file>
27+
<file alias="ttd-timestamp">icons/ttd-timestamp.png</file>
2728
</qresource>
2829
</RCC>

icons/ttd-timestamp.png

3.27 KB
Loading

ui/controlswidget.cpp

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ limitations under the License.
1616

1717
#include "controlswidget.h"
1818
#include "adaptersettings.h"
19+
#include "timestampnavigationdialog.h"
1920
#include <QPixmap>
2021
#include <QInputDialog>
2122
#include <QMessageBox>
@@ -134,6 +135,11 @@ DebugControlsWidget::DebugControlsWidget(QWidget* parent, const std::string name
134135
performStepReturnReverse();
135136
});
136137
m_actionStepReturnBack->setToolTip(getToolTip("Step Return Backwards"));
138+
139+
m_actionTimestampNavigation = addAction(getColoredIcon(":/debugger/ttd-timestamp", cyan), "Navigate to Timestamp", [this]() {
140+
performTimestampNavigation();
141+
});
142+
m_actionTimestampNavigation->setToolTip(getToolTip("Navigate to TTD Timestamp..."));
137143
updateButtons();
138144
}
139145

@@ -507,6 +513,8 @@ void DebugControlsWidget::setReverseSteppingEnabled(bool enabled)
507513
m_actionStepOverBack->setVisible(enabled);
508514
m_actionStepReturnBack->setEnabled(enabled);
509515
m_actionStepReturnBack->setVisible(enabled);
516+
m_actionTimestampNavigation->setEnabled(enabled);
517+
m_actionTimestampNavigation->setVisible(enabled);
510518
}
511519

512520

@@ -566,3 +574,16 @@ void DebugControlsWidget::updateButtons()
566574
m_actionGoBack->setVisible(m_controller->IsTTD());
567575
}
568576
}
577+
578+
579+
void DebugControlsWidget::performTimestampNavigation()
580+
{
581+
if (!m_controller->IsTTD())
582+
{
583+
QMessageBox::warning(this, "Error", "Time travel debugging is not active.");
584+
return;
585+
}
586+
587+
auto* dialog = new TimestampNavigationDialog(this, m_controller);
588+
dialog->show();
589+
}

ui/controlswidget.h

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ class DebugControlsWidget : public QToolBar
5353

5454
QAction* m_actionSettings;
5555
QAction* m_actionToggleBreakpoint;
56+
QAction* m_actionTimestampNavigation;
5657

5758
bool canExec();
5859
bool canConnect();
@@ -90,4 +91,5 @@ public Q_SLOTS:
9091

9192
void performSettings();
9293
void toggleBreakpoint();
94+
void performTimestampNavigation();
9395
};

0 commit comments

Comments
 (0)