Skip to content
220 changes: 215 additions & 5 deletions core/adapters/gdbmiadapter.cpp
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
#include "gdbmiadapter.h"
#include <regex>
#include <sstream>
#include <map>
#include "../debuggercontroller.h" // Assuming this path is correct for your project structure
#include "../../cli/log.h" // For Log::print

Expand Down Expand Up @@ -195,10 +197,38 @@ void GdbMiAdapter::AsyncRecordHandler(const MiRecord& record)
// Update target state BEFORE posting events
m_targetRunningAtomic.store(false, std::memory_order_release);

// Kick a background refresh so we don’t block the reader
ScheduleStateRefresh();

m_eventCV.notify_all();
// Check if the process has exited
if (m_lastStopReason == ProcessExited)
{
// Parse exit code if available
if (value.Exists("exit-code"))
{
try {
m_exitCode = std::stoull(value["exit-code"].GetString(), nullptr, 0);
} catch(...) {
LogWarn("Failed to parse exit code");
m_exitCode = 0;
}
}
else
{
m_exitCode = 0;
}

// Post target exited event
DebuggerEvent dbgevt;
dbgevt.type = TargetExitedEventType;
dbgevt.data.exitData.exitCode = m_exitCode;
PostDebuggerEvent(dbgevt);

m_eventCV.notify_all();
}
else
{
// Normal stop - kick a background refresh so we don't block the reader
ScheduleStateRefresh();
m_eventCV.notify_all();
}
}
else if (record.command == "running")
{
Expand Down Expand Up @@ -243,6 +273,15 @@ void GdbMiAdapter::AsyncRecordHandler(const MiRecord& record)
}
}

// If we're collecting console output, buffer it
{
std::lock_guard<std::mutex> lock(m_consoleBufferMutex);
if (m_collectConsoleOutput && record.type == '~')
{
m_consoleBuffer += message;
}
}

DebuggerEvent event;
event.type = BackendMessageEventType;
event.data.messageData.message = message;
Expand Down Expand Up @@ -774,7 +813,178 @@ std::vector<DebugModule> GdbMiAdapter::GetModuleList()
if (!m_mi)
return {};

// Acquire the command mutex to prevent other threads from sending commands
// while we're waiting for the response to "info proc mappings"
std::unique_lock lock(m_gdbCommandMutex);

// Enable console output buffering
{
std::lock_guard<std::mutex> bufferLock(m_consoleBufferMutex);
m_consoleBuffer.clear();
m_collectConsoleOutput = true;
}

// Use -interpreter-exec to run the console command "info proc mappings"
// Use a longer timeout since this command can generate a lot of output
auto result = m_mi->SendCommand("-interpreter-exec console \"info proc mappings\"", 5000);

// Disable console output buffering and get the collected output
std::string output;
{
std::lock_guard<std::mutex> bufferLock(m_consoleBufferMutex);
m_collectConsoleOutput = false;
output = m_consoleBuffer;
m_consoleBuffer.clear();
}

if (result.command != "done")
{
LogWarn("Failed to get process mappings: %s", result.fullLine.c_str());
return {};
}

if (output.empty())
{
LogWarn("No console output received from info proc mappings");
return {};
}

std::vector<DebugModule> modules;
std::map<std::string, int> moduleNameCount; // Track module name occurrences for duplicates
std::map<std::string, std::vector<std::pair<uint64_t, uint64_t>>> moduleRanges; // path -> list of (start, end)
std::vector<std::string> moduleOrder; // Track the order in which modules are first seen

// Parse the output line by line
// Expected format (from the issue):
// process 25443
// Mapped address spaces:
//
// Start Addr End Addr Size Offset Perms objfile
// 0x555555554000 0x555555555000 0x1000 0x0 r--p /path/to/file

std::istringstream stream(output);
std::string line;
bool headerFound = false;

while (std::getline(stream, line))
{
// Skip until we find the header line
if (!headerFound)
{
if (line.find("Start Addr") != std::string::npos &&
line.find("End Addr") != std::string::npos)
{
headerFound = true;
}
continue;
}

// Parse data lines
// Format: Start_Addr End_Addr Size Offset Perms objfile
std::istringstream lineStream(line);
std::string startStr, endStr, sizeStr, offsetStr, perms, objfile;

lineStream >> startStr >> endStr >> sizeStr >> offsetStr >> perms;

// Rest of the line is the objfile (path)
std::getline(lineStream, objfile);

// Trim leading and trailing whitespace from objfile
size_t firstNonSpace = objfile.find_first_not_of(" \t");
if (firstNonSpace == std::string::npos)
{
// Line is all whitespace, skip it
continue;
}
objfile = objfile.substr(firstNonSpace);

// Trim trailing whitespace
size_t lastNonSpace = objfile.find_last_not_of(" \t");
if (lastNonSpace != std::string::npos)
{
objfile = objfile.substr(0, lastNonSpace + 1);
}

// Skip lines without valid addresses or without objfile
if (startStr.empty() || endStr.empty() || objfile.empty())
continue;

// Skip special mappings like [stack], [heap], [vvar], [vdso], etc.
if (objfile[0] == '[')
continue;

try
{
uint64_t start = std::stoull(startStr, nullptr, 16);
uint64_t end = std::stoull(endStr, nullptr, 16);

// Track the order of first occurrence
if (moduleRanges.find(objfile) == moduleRanges.end())
{
moduleOrder.push_back(objfile);
}

// Accumulate ranges for each object file
moduleRanges[objfile].emplace_back(start, end);
}
catch (const std::exception& e)
{
LogDebug("Failed to parse address range: %s", e.what());
continue;
}
}

// Now create DebugModule entries in the order they were first encountered
// For each unique object file, we need to determine its overall address range
for (const auto& path : moduleOrder)
{
const auto& ranges = moduleRanges[path];
if (ranges.empty())
continue;

// Find the minimum start and maximum end
uint64_t minStart = ranges[0].first;
uint64_t maxEnd = ranges[0].second;

for (const auto& [start, end] : ranges)
{
minStart = std::min(minStart, start);
maxEnd = std::max(maxEnd, end);
}

// Extract the base name from the path for m_short_name
std::string shortName = path;
size_t lastSlash = path.find_last_of("/\\");
if (lastSlash != std::string::npos)
{
shortName = path.substr(lastSlash + 1);
}

// Handle duplicate names by appending -1, -2, etc.
// The first occurrence gets the original name, subsequent ones get -1, -2, etc.
std::string finalName = path;
if (moduleNameCount.find(path) != moduleNameCount.end())
{
// This is a duplicate (shouldn't happen with current logic, but keep for safety)
int count = ++moduleNameCount[path];
finalName = path + "-" + std::to_string(count);
}
else
{
// First occurrence
moduleNameCount[path] = 0;
}

DebugModule module;
module.m_name = finalName;
module.m_short_name = shortName;
module.m_address = minStart;
module.m_size = maxEnd - minStart;
module.m_loaded = true;

modules.push_back(module);
}

return modules;
}

Expand Down Expand Up @@ -825,7 +1035,7 @@ std::string GdbMiAdapter::InvokeBackendCommand(const std::string& command) {
return (result.command == "done") ? result.payload : result.command;
}

uint64_t GdbMiAdapter::ExitCode() { return 0; }
uint64_t GdbMiAdapter::ExitCode() { return m_exitCode; }

DebugStopReason GdbMiAdapter::StopReason() { return m_lastStopReason; }

Expand Down
6 changes: 6 additions & 0 deletions core/adapters/gdbmiadapter.h
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,17 @@ class GdbMiAdapter : public BinaryNinjaDebugger::DebugAdapter
std::vector<std::string> m_registerNames; // In GDB's order
BinaryNinjaDebugger::DebugStopReason m_lastStopReason;
std::atomic<bool> m_targetRunningAtomic{false};
uint64_t m_exitCode = 0;

std::mutex m_eventMutex;
std::mutex m_gdbCommandMutex;
std::condition_variable m_eventCV;

// --- Console output buffering for commands that need it ---
std::mutex m_consoleBufferMutex;
std::string m_consoleBuffer;
bool m_collectConsoleOutput = false;

// --- Cached Target State ---
std::mutex m_cacheMutex; // To protect access to cached data
std::vector<BinaryNinjaDebugger::DebugThread> m_cachedThreads;
Expand Down