diff --git a/api/debuggerapi.h b/api/debuggerapi.h index 8a237305..0fad836f 100644 --- a/api/debuggerapi.h +++ b/api/debuggerapi.h @@ -349,6 +349,17 @@ namespace BinaryNinjaDebuggerAPI { static std::string GetPathBaseName(const std::string& path); }; + struct DebugMemoryRegion + { + std::uintptr_t m_start {}; + std::size_t m_size {}; + std::string m_name {}; + bool m_read {}; + bool m_write {}; + bool m_execute {}; + bool m_shared {}; + }; + struct DebugRegister { @@ -686,6 +697,7 @@ namespace BinaryNinjaDebuggerAPI { bool ResumeThread(std::uint32_t tid); std::vector GetModules(); + std::vector GetMemoryMap(); std::vector GetRegisters(); intx::uint512 GetRegisterValue(const std::string& name); bool SetRegisterValue(const std::string& name, const intx::uint512& value); diff --git a/api/debuggercontroller.cpp b/api/debuggercontroller.cpp index dec0bf76..71720ac8 100644 --- a/api/debuggercontroller.cpp +++ b/api/debuggercontroller.cpp @@ -275,6 +275,31 @@ std::vector DebuggerController::GetModules() } +std::vector DebuggerController::GetMemoryMap() +{ + size_t count; + BNDebugMemoryRegion* regions = BNDebuggerGetMemoryMap(m_object, &count); + + vector result; + result.reserve(count); + for (size_t i = 0; i < count; i++) + { + DebugMemoryRegion region; + region.m_start = regions[i].m_start; + region.m_size = regions[i].m_size; + region.m_name = regions[i].m_name; + region.m_read = regions[i].m_read; + region.m_write = regions[i].m_write; + region.m_execute = regions[i].m_execute; + region.m_shared = regions[i].m_shared; + result.push_back(region); + } + BNDebuggerFreeMemoryRegions(regions, count); + + return result; +} + + std::vector DebuggerController::GetRegisters() { size_t count; diff --git a/api/ffi.h b/api/ffi.h index edd6e0d3..c9655f83 100644 --- a/api/ffi.h +++ b/api/ffi.h @@ -115,6 +115,17 @@ extern "C" bool m_loaded; } BNDebugModule; + typedef struct BNDebugMemoryRegion + { + char* m_name; + uint64_t m_start; + size_t m_size; + bool m_read; + bool m_write; + bool m_execute; + bool m_shared; + } BNDebugMemoryRegion; + typedef struct BNDebugRegister { @@ -519,6 +530,9 @@ extern "C" DEBUGGER_FFI_API BNDebugModule* BNDebuggerGetModules(BNDebuggerController* controller, size_t* count); DEBUGGER_FFI_API void BNDebuggerFreeModules(BNDebugModule* modules, size_t count); + DEBUGGER_FFI_API BNDebugMemoryRegion* BNDebuggerGetMemoryMap(BNDebuggerController* controller, size_t* count); + DEBUGGER_FFI_API void BNDebuggerFreeMemoryRegions(BNDebugMemoryRegion* regions, size_t count); + DEBUGGER_FFI_API BNDebugRegister* BNDebuggerGetRegisters(BNDebuggerController* controller, size_t* count); DEBUGGER_FFI_API void BNDebuggerFreeRegisters(BNDebugRegister* modules, size_t count); DEBUGGER_FFI_API bool BNDebuggerSetRegisterValue( diff --git a/api/python/debuggercontroller.py b/api/python/debuggercontroller.py index 000f3c5a..c312abb7 100644 --- a/api/python/debuggercontroller.py +++ b/api/python/debuggercontroller.py @@ -179,6 +179,62 @@ def __repr__(self): return f"" +class DebugMemoryRegion: + """ + DebugMemoryRegion represents a single mapped region of the target's virtual address space (a + memory-map entry). It has the following fields: + + * ``start``: the start address of the region + * ``size``: the size of the region, in bytes + * ``name``: the backing of the region -- a file path for file-backed mappings, a well-known name \ +such as ``[stack]`` or ``[heap]`` where the backend provides one, or an empty string for anonymous \ +mappings + * ``read``: whether the region is readable + * ``write``: whether the region is writable + * ``execute``: whether the region is executable + * ``shared``: whether the mapping is shared between processes (as opposed to private/copy-on-write) + + """ + def __init__(self, start, size, name, read, write, execute, shared): + self.start = start + self.size = size + self.name = name + self.read = read + self.write = write + self.execute = execute + self.shared = shared + + def __eq__(self, other): + if not isinstance(other, self.__class__): + return NotImplemented + return self.start == other.start and self.size == other.size and self.name == other.name \ + and self.read == other.read and self.write == other.write and self.execute == other.execute \ + and self.shared == other.shared + + def __ne__(self, other): + if not isinstance(other, self.__class__): + return NotImplemented + return not (self == other) + + def __hash__(self): + return hash((self.start, self.size, self.name, self.read, self.write, self.execute, self.shared)) + + def __setattr__(self, name, value): + try: + object.__setattr__(self, name, value) + except AttributeError: + raise AttributeError(f"attribute '{name}' is read only") + + @property + def permissions(self) -> str: + """A string like ``rwx`` (or ``r-x``) describing the region's permissions""" + return f"{'r' if self.read else '-'}{'w' if self.write else '-'}{'x' if self.execute else '-'}" + + def __repr__(self): + return f"" + + class DebugRegister: """ DebugRegister represents a register in the target. It has the following fields: @@ -1450,6 +1506,29 @@ def modules(self) -> DebugModules: dbgcore.BNDebuggerFreeModules(modules, count.value) return DebugModules(result) + @property + def memory_map(self) -> List[DebugMemoryRegion]: + """ + The memory map of the target: every mapped region of the virtual address space with its + permissions. + + The map is refreshed when the target stops. Not every adapter reports a memory map yet; for + those, this returns an empty list. See issue #96. + + :return: a list of ``DebugMemoryRegion`` + """ + count = ctypes.c_ulonglong() + regions = dbgcore.BNDebuggerGetMemoryMap(self.handle, count) + result = [] + for i in range(0, count.value): + region = DebugMemoryRegion(regions[i].m_start, regions[i].m_size, regions[i].m_name, + regions[i].m_read, regions[i].m_write, regions[i].m_execute, + regions[i].m_shared) + result.append(region) + + dbgcore.BNDebuggerFreeMemoryRegions(regions, count.value) + return result + def rebase_to_remote_base(self) -> bool: """ Rebase the input binary view to match the remote base address. diff --git a/core/adapters/dbgengadapter.cpp b/core/adapters/dbgengadapter.cpp index 19bcd0bd..5101e28f 100644 --- a/core/adapters/dbgengadapter.cpp +++ b/core/adapters/dbgengadapter.cpp @@ -330,6 +330,7 @@ bool DbgEngAdapter::ConnectToDebugServerInternal(const std::string& connectionSt QUERY_DEBUG_INTERFACE(IDebugControl7, &this->m_debugControl); QUERY_DEBUG_INTERFACE(IDebugDataSpaces, &this->m_debugDataSpaces); + QUERY_DEBUG_INTERFACE(IDebugDataSpaces2, &this->m_debugDataSpaces2); QUERY_DEBUG_INTERFACE(IDebugRegisters, &this->m_debugRegisters); QUERY_DEBUG_INTERFACE(IDebugSymbols3, &this->m_debugSymbols); QUERY_DEBUG_INTERFACE(IDebugSystemObjects, &this->m_debugSystemObjects); @@ -422,6 +423,7 @@ void DbgEngAdapter::Reset() { SAFE_RELEASE(this->m_debugControl); SAFE_RELEASE(this->m_debugDataSpaces); + SAFE_RELEASE(this->m_debugDataSpaces2); SAFE_RELEASE(this->m_debugRegisters); SAFE_RELEASE(this->m_debugSymbols); SAFE_RELEASE(this->m_debugSystemObjects); @@ -1749,6 +1751,74 @@ std::vector DbgEngAdapter::GetModuleList() return modules; } + +std::vector DbgEngAdapter::GetMemoryMap() +{ + if (!this->m_debugDataSpaces2) + return {}; + + std::vector result; + + // Walk the whole virtual address space with QueryVirtual (the DbgEng wrapper over VirtualQueryEx), + // starting at 0 and advancing by each region's size. QueryVirtual fails once we walk past the end + // of the address space, which terminates the loop. Free/reserved regions are reported too (with a + // size that spans the gap), so skipping them still advances efficiently. + ULONG64 address = 0; + while (true) + { + MEMORY_BASIC_INFORMATION64 info = {}; + if (this->m_debugDataSpaces2->QueryVirtual(address, &info) != S_OK) + break; + + if (info.RegionSize == 0) + break; + + // Only committed pages are actually mapped. Guard pages and no-access pages are committed but + // cannot be read, so we exclude them from the "readable" map. + const ULONG protect = info.Protect & 0xff; // strip PAGE_GUARD / PAGE_NOCACHE / PAGE_WRITECOMBINE + if (info.State == MEM_COMMIT && !(info.Protect & PAGE_GUARD) && protect != PAGE_NOACCESS) + { + DebugMemoryRegion region; + region.m_start = info.BaseAddress; + region.m_size = info.RegionSize; + region.m_read = true; // any committed, non-no-access, non-guard page is readable on x86/x64 + region.m_write = (protect == PAGE_READWRITE) || (protect == PAGE_WRITECOPY) + || (protect == PAGE_EXECUTE_READWRITE) || (protect == PAGE_EXECUTE_WRITECOPY); + region.m_execute = (protect == PAGE_EXECUTE) || (protect == PAGE_EXECUTE_READ) + || (protect == PAGE_EXECUTE_READWRITE) || (protect == PAGE_EXECUTE_WRITECOPY); + // MEM_MAPPED sections (file/pagefile-backed) can be shared between processes; MEM_IMAGE is + // copy-on-write and MEM_PRIVATE is private. + region.m_shared = (info.Type == MEM_MAPPED); + + // For image-backed regions, resolve the backing module's image path for the name column. + if (info.Type == MEM_IMAGE && this->m_debugSymbols) + { + ULONG moduleIndex = 0; + ULONG64 moduleBase = 0; + if (this->m_debugSymbols->GetModuleByOffset(info.BaseAddress, 0, &moduleIndex, &moduleBase) == S_OK) + { + char imageName[1024]; + if (this->m_debugSymbols->GetModuleNames(moduleIndex, 0, imageName, 1024, nullptr, nullptr, 0, + nullptr, nullptr, 0, nullptr) + == S_OK) + region.m_name = imageName; + } + } + + result.push_back(region); + } + + // Advance past this region; stop if the address would wrap around at the top of the space. + ULONG64 next = info.BaseAddress + info.RegionSize; + if (next <= address) + break; + address = next; + } + + return result; +} + + bool DbgEngAdapter::BreakInto() { if (ExecStatus() == DEBUG_STATUS_BREAK || ExecStatus() == DEBUG_STATUS_NO_DEBUGGEE) diff --git a/core/adapters/dbgengadapter.h b/core/adapters/dbgengadapter.h index 10402f06..6fc097fe 100644 --- a/core/adapters/dbgengadapter.h +++ b/core/adapters/dbgengadapter.h @@ -128,6 +128,7 @@ namespace BinaryNinjaDebugger { IDebugClient7* m_debugClient {nullptr}; IDebugControl7* m_debugControl {nullptr}; IDebugDataSpaces* m_debugDataSpaces {nullptr}; + IDebugDataSpaces2* m_debugDataSpaces2 {nullptr}; IDebugRegisters* m_debugRegisters {nullptr}; IDebugSymbols3* m_debugSymbols {nullptr}; IDebugSystemObjects* m_debugSystemObjects {nullptr}; @@ -222,6 +223,8 @@ namespace BinaryNinjaDebugger { // bool WriteMemory(std::uintptr_t address, const void* out, std::size_t size) override; std::vector GetModuleList() override; + std::vector GetMemoryMap() override; + std::string GetTargetArchitecture() override; DebugStopReason StopReason() override; diff --git a/core/adapters/gdbadapter.cpp b/core/adapters/gdbadapter.cpp index 161d4673..4e6ab3bb 100644 --- a/core/adapters/gdbadapter.cpp +++ b/core/adapters/gdbadapter.cpp @@ -891,6 +891,63 @@ std::vector GdbAdapter::GetModuleList() } +std::vector GdbAdapter::GetMemoryMap() +{ + if (m_isTargetRunning) + return {}; + + if (!m_rspConnector) + return {}; + + const auto path = "/proc/" + std::to_string(this->m_lastActiveThreadId) + "/maps"; + std::string data = GetRemoteFile(path); + if (data.empty()) + return {}; + + // Each line of /proc/[pid]/maps describes one mapped region: + // start-end perms offset dev inode pathname + // e.g. "7ffff7dc5000-7ffff7de7000 r-xp 00000000 08:01 1234 /usr/lib/libc.so" + // Unlike GetModuleList (which aggregates the regions of each file into one module) we keep every + // region and record its permissions. The pathname is optional and may be a pseudo-name such as + // "[stack]", "[heap]" or "[vdso]". + std::vector result; + const std::regex region_regex( + "^([0-9a-f]+)-([0-9a-f]+)\\s+([rwxsp-]{4})\\s+[0-9a-f]+\\s+\\S+\\s+[0-9]+\\s*(.*)$"); + for (const std::string& line : RspConnector::Split(data, "\n")) + { + std::string_view v = line; + v.remove_prefix(std::min(v.find_first_not_of(" "), v.size())); + auto trimPosition = v.find_last_not_of(" \r"); + if (trimPosition != v.npos) + v.remove_suffix(v.size() - trimPosition - 1); + + const std::string trimmedLine = std::string(v); + + std::smatch match; + if (!std::regex_match(trimmedLine, match, region_regex) || match.size() != 5) + continue; + + uint64_t start = std::strtoull(match[1].str().c_str(), nullptr, 16); + uint64_t end = std::strtoull(match[2].str().c_str(), nullptr, 16); + if (end <= start) + continue; + + const std::string perms = match[3].str(); // e.g. "r-xp" + DebugMemoryRegion region; + region.m_start = start; + region.m_size = end - start; + region.m_name = match[4].str(); + region.m_read = perms[0] == 'r'; + region.m_write = perms[1] == 'w'; + region.m_execute = perms[2] == 'x'; + region.m_shared = perms[3] == 's'; + result.push_back(region); + } + + return result; +} + + std::string GdbAdapter::GetTargetArchitecture() { return m_remoteArch; diff --git a/core/adapters/gdbadapter.h b/core/adapters/gdbadapter.h index 5d9896b1..5dc4a71f 100644 --- a/core/adapters/gdbadapter.h +++ b/core/adapters/gdbadapter.h @@ -130,6 +130,8 @@ namespace BinaryNinjaDebugger std::string GetRemoteFile(const std::string& path); std::vector GetModuleList() override; + std::vector GetMemoryMap() override; + std::string GetTargetArchitecture() override; DebugStopReason StopReason() override; diff --git a/core/adapters/gdbmiadapter.cpp b/core/adapters/gdbmiadapter.cpp index 888989f0..0170b083 100644 --- a/core/adapters/gdbmiadapter.cpp +++ b/core/adapters/gdbmiadapter.cpp @@ -1109,6 +1109,89 @@ std::vector GdbMiAdapter::GetModuleList() return result; } + +std::vector GdbMiAdapter::GetMemoryMap() +{ + if (!m_mi || m_targetRunningAtomic) + return {}; + + std::unique_lock cmdLock(m_gdbCommandMutex); + + // "info proc mappings" lists every mapped region. On GDB 8.0+ it includes a "Perms" column, e.g.: + // Start Addr End Addr Size Offset Perms objfile + // 0x555555554000 0x555555556000 0x2000 0x0 r--p /usr/bin/cat + // Older GDB omits the Perms column; in that case we can only report readability. + std::string output = InvokeBackendCommand("info proc mappings"); + if (output.empty() || output == "error, transport not ready") + return {}; + + auto isPermsToken = [](const std::string& s) { + return s.size() == 4 && (s[0] == 'r' || s[0] == '-') && (s[1] == 'w' || s[1] == '-') + && (s[2] == 'x' || s[2] == '-') && (s[3] == 'p' || s[3] == 's' || s[3] == '-'); + }; + + std::vector result; + std::istringstream stream(output); + std::string line; + while (std::getline(stream, line)) + { + if (line.empty() || line.find("Start Addr") != std::string::npos + || line.find("process") != std::string::npos + || line.find("Mapped address spaces") != std::string::npos) + continue; + + std::vector columns; + std::istringstream lineStream(line); + std::string column; + while (lineStream >> column) + columns.push_back(column); + + // Need at least start and end addresses. + if (columns.size() < 2 || columns[0].substr(0, 2) != "0x" || columns[1].substr(0, 2) != "0x") + continue; + + uint64_t start = std::strtoull(columns[0].c_str(), nullptr, 16); + uint64_t end = std::strtoull(columns[1].c_str(), nullptr, 16); + if (end <= start) + continue; + + // Find the permissions token (position varies across GDB versions), and treat the trailing + // column as the objfile/pseudo-name when it is neither the perms token nor a hex number. + std::string perms; + for (size_t i = 2; i < columns.size(); i++) + { + if (isPermsToken(columns[i])) + perms = columns[i]; + } + + std::string name; + const std::string& last = columns.back(); + if (last != perms && last.substr(0, 2) != "0x") + name = last; + + DebugMemoryRegion region; + region.m_start = start; + region.m_size = end - start; + region.m_name = name; + if (!perms.empty()) + { + region.m_read = perms[0] == 'r'; + region.m_write = perms[1] == 'w'; + region.m_execute = perms[2] == 'x'; + region.m_shared = perms[3] == 's'; + } + else + { + // Older GDB does not report permissions here; the region is mapped, so assume readable. + region.m_read = true; + } + result.push_back(region); + } + + return result; +} + + bool GdbMiAdapter::Go() { if (!m_mi || m_targetRunningAtomic) return false; diff --git a/core/adapters/gdbmiadapter.h b/core/adapters/gdbmiadapter.h index dc9005a1..afb19e0e 100644 --- a/core/adapters/gdbmiadapter.h +++ b/core/adapters/gdbmiadapter.h @@ -94,6 +94,7 @@ class GdbMiAdapter : public BinaryNinjaDebugger::DebugAdapter bool WriteMemory(std::uintptr_t address, const BinaryNinja::DataBuffer& buffer) override; std::vector GetModuleList() override; + std::vector GetMemoryMap() override; std::string GetTargetArchitecture() override; BinaryNinjaDebugger::DebugStopReason StopReason() override; uint64_t ExitCode() override; diff --git a/core/adapters/lldbadapter.cpp b/core/adapters/lldbadapter.cpp index 3ff043ad..523e7015 100644 --- a/core/adapters/lldbadapter.cpp +++ b/core/adapters/lldbadapter.cpp @@ -1648,6 +1648,43 @@ std::vector LldbAdapter::GetModuleList() } +std::vector LldbAdapter::GetMemoryMap() +{ + std::vector result; + if (!m_process.IsValid()) + return result; + + SBMemoryRegionInfoList regions = m_process.GetMemoryRegions(); + SBMemoryRegionInfo region; + for (uint32_t i = 0; i < regions.GetSize(); i++) + { + if (!regions.GetMemoryRegionAtIndex(i, region)) + continue; + // The list also describes unmapped gaps between regions; skip those. + if (!region.IsMapped()) + continue; + + uint64_t start = region.GetRegionBase(); + uint64_t end = region.GetRegionEnd(); + if (end <= start) + continue; + + DebugMemoryRegion m; + m.m_start = start; + m.m_size = end - start; + if (const char* name = region.GetName()) + m.m_name = name; + m.m_read = region.IsReadable(); + m.m_write = region.IsWritable(); + m.m_execute = region.IsExecutable(); + // LLDB's SBMemoryRegionInfo does not expose shared/private mapping information. + m.m_shared = false; + result.push_back(m); + } + return result; +} + + std::string LldbAdapter::GetTargetArchitecture() { SBPlatform platform = m_target.GetPlatform(); diff --git a/core/adapters/lldbadapter.h b/core/adapters/lldbadapter.h index b5e5d4df..1ce16128 100644 --- a/core/adapters/lldbadapter.h +++ b/core/adapters/lldbadapter.h @@ -125,6 +125,8 @@ namespace BinaryNinjaDebugger { std::vector GetModuleList() override; + std::vector GetMemoryMap() override; + std::string GetTargetArchitecture() override; DebugStopReason StopReason() override; diff --git a/core/adapters/windowsnativeadapter.cpp b/core/adapters/windowsnativeadapter.cpp index f3145521..1896f5ef 100644 --- a/core/adapters/windowsnativeadapter.cpp +++ b/core/adapters/windowsnativeadapter.cpp @@ -2630,6 +2630,64 @@ std::vector WindowsNativeAdapter::GetModuleList() } +std::vector WindowsNativeAdapter::GetMemoryMap() +{ + if (!m_processHandle) + return {}; + + std::vector result; + + // Walk the whole virtual address space with VirtualQueryEx, starting at 0 and advancing by each + // region's size. The query fails once we walk past the end of the user address space, which + // terminates the loop. Free/reserved regions are reported too (with a size that spans the gap), so + // skipping them still advances efficiently. + uintptr_t address = 0; + MEMORY_BASIC_INFORMATION info = {}; + while (VirtualQueryEx(m_processHandle, (LPCVOID)address, &info, sizeof(info)) == sizeof(info)) + { + if (info.RegionSize == 0) + break; + + // Only committed pages are actually mapped. Guard pages and no-access pages are committed but + // cannot be read, so we exclude them from the "readable" map. + const DWORD protect = info.Protect & 0xff; // strip PAGE_GUARD / PAGE_NOCACHE / PAGE_WRITECOMBINE + if (info.State == MEM_COMMIT && !(info.Protect & PAGE_GUARD) && protect != PAGE_NOACCESS) + { + DebugMemoryRegion region; + region.m_start = (uint64_t)info.BaseAddress; + region.m_size = info.RegionSize; + region.m_read = true; // any committed, non-no-access, non-guard page is readable on x86/x64 + region.m_write = (protect == PAGE_READWRITE) || (protect == PAGE_WRITECOPY) + || (protect == PAGE_EXECUTE_READWRITE) || (protect == PAGE_EXECUTE_WRITECOPY); + region.m_execute = (protect == PAGE_EXECUTE) || (protect == PAGE_EXECUTE_READ) + || (protect == PAGE_EXECUTE_READWRITE) || (protect == PAGE_EXECUTE_WRITECOPY); + // MEM_MAPPED sections (file/pagefile-backed) can be shared between processes; MEM_IMAGE is + // copy-on-write and MEM_PRIVATE is private. + region.m_shared = (info.Type == MEM_MAPPED); + + // Image- and file-backed regions have a backing file we can name. Leave the name empty + // (rather than the helper's "" sentinel) for mappings with no resolvable file. + if (info.Type == MEM_IMAGE || info.Type == MEM_MAPPED) + { + std::string name = GetModuleNameFromHandle(nullptr, info.BaseAddress); + if (name != "") + region.m_name = name; + } + + result.push_back(region); + } + + // Advance past this region; stop if the address would wrap around at the top of the space. + uintptr_t next = (uintptr_t)info.BaseAddress + info.RegionSize; + if (next <= address) + break; + address = next; + } + + return result; +} + + std::string WindowsNativeAdapter::GetTargetArchitecture() { // Use cached WOW64 detection result diff --git a/core/adapters/windowsnativeadapter.h b/core/adapters/windowsnativeadapter.h index 35ef5a94..ee01b5a9 100644 --- a/core/adapters/windowsnativeadapter.h +++ b/core/adapters/windowsnativeadapter.h @@ -226,6 +226,8 @@ namespace BinaryNinjaDebugger { std::vector GetModuleList() override; + std::vector GetMemoryMap() override; + std::string GetTargetArchitecture() override; DebugStopReason StopReason() override; diff --git a/core/debugadapter.h b/core/debugadapter.h index 116a9c26..2644175b 100644 --- a/core/debugadapter.h +++ b/core/debugadapter.h @@ -210,6 +210,32 @@ namespace BinaryNinjaDebugger { static std::string GetPathBaseName(const std::string& path); }; + // A single mapped region of the target's virtual address space (a memory-map entry), e.g. the + // backing of one segment of a module, an anonymous mapping, the stack, or the heap. Unlike a + // DebugModule (one entry per loaded binary), a memory region is the OS page-protection view: a + // module typically spans several regions with different permissions, and many regions (stack, + // heap, anonymous mmap) belong to no module at all. + struct DebugMemoryRegion + { + std::uintptr_t m_start {}; + std::size_t m_size {}; + // The backing of the region: a file path for file-backed mappings, a well-known name such as + // "[stack]" or "[heap]" where the backend provides one, or empty for anonymous mappings. + std::string m_name {}; + bool m_read {}; + bool m_write {}; + bool m_execute {}; + // Whether the mapping is shared between processes (as opposed to a private/copy-on-write map). + bool m_shared {}; + + DebugMemoryRegion() = default; + DebugMemoryRegion(std::uintptr_t start, std::size_t size, std::string name, bool read, bool write, + bool execute, bool shared) : + m_start(start), m_size(size), m_name(std::move(name)), m_read(read), m_write(write), + m_execute(execute), m_shared(shared) + {} + }; + struct DebugFrame { size_t m_index = 0; @@ -337,6 +363,11 @@ namespace BinaryNinjaDebugger { virtual std::vector GetModuleList() = 0; + // Return the target's memory map: every mapped region of the virtual address space with its + // permissions. Adapters opt in by overriding this; the default is an empty map for backends + // that do not (yet) support it. See issue #96. + virtual std::vector GetMemoryMap() { return {}; } + virtual std::string GetTargetArchitecture() = 0; virtual DebugStopReason StopReason() = 0; diff --git a/core/debuggercontroller.cpp b/core/debuggercontroller.cpp index 1526a624..573eb9d7 100644 --- a/core/debuggercontroller.cpp +++ b/core/debuggercontroller.cpp @@ -2504,6 +2504,11 @@ std::vector DebuggerController::GetAllModules() return m_state->GetModules()->GetAllModules(); } +std::vector DebuggerController::GetMemoryMap() +{ + return m_state->GetMemoryMap()->GetAllRegions(); +} + std::vector DebuggerController::GetProcessList() { if (!m_adapter) diff --git a/core/debuggercontroller.h b/core/debuggercontroller.h index 4d50d4a9..ad20450b 100644 --- a/core/debuggercontroller.h +++ b/core/debuggercontroller.h @@ -443,6 +443,9 @@ namespace BinaryNinjaDebugger { ModuleNameAndOffset AbsoluteAddressToRelative(uint64_t absoluteAddress); uint64_t RelativeAddressToAbsolute(const ModuleNameAndOffset& relativeAddress); + // memory map + std::vector GetMemoryMap(); + // rebasing // Note: Returns true immediately in UI mode (rebase completes asynchronously via UI callback) bool RebaseToRemoteBase(); diff --git a/core/debuggerstate.cpp b/core/debuggerstate.cpp index ba958894..663fb3be 100644 --- a/core/debuggerstate.cpp +++ b/core/debuggerstate.cpp @@ -540,6 +540,70 @@ std::vector DebuggerModules::GetAllModules() } +DebuggerMemoryMap::DebuggerMemoryMap(DebuggerState* state) : m_state(state) +{ + MarkDirty(); +} + + +void DebuggerMemoryMap::MarkDirty() +{ + std::unique_lock lock(m_regionsMutex); + m_dirty = true; + m_regions.clear(); +} + + +void DebuggerMemoryMap::Update() +{ + DebugAdapter* adapter = m_state->GetAdapter(); + if (!adapter) + return; + + if (!m_state->IsConnected()) + return; + + std::unique_lock lock(m_regionsMutex); + { + std::lock_guard adapterLock(m_state->AdapterAccessMutex()); + m_regions = adapter->GetMemoryMap(); + } + m_dirty = false; +} + + +std::vector DebuggerMemoryMap::GetAllRegions() +{ + std::unique_lock lock(m_regionsMutex); + + if (IsDirty()) + Update(); + + return m_regions; +} + + +DebugMemoryRegion DebuggerMemoryMap::GetRegionForAddress(uint64_t remoteAddress, bool& found) +{ + std::unique_lock lock(m_regionsMutex); + + if (IsDirty()) + Update(); + + for (const DebugMemoryRegion& region : m_regions) + { + if (remoteAddress >= region.m_start && remoteAddress < region.m_start + region.m_size) + { + found = true; + return region; + } + } + + found = false; + return {}; +} + + DebuggerBreakpoints::DebuggerBreakpoints(DebuggerState* state, std::vector initial) : m_state(state) { @@ -1390,6 +1454,7 @@ DebuggerState::DebuggerState(BinaryViewRef data, DebuggerController* controller) m_adapter = nullptr; m_modules = new DebuggerModules(this); + m_memoryMap = new DebuggerMemoryMap(this); m_registers = new DebuggerRegisters(this); m_threads = new DebuggerThreads(this); m_breakpoints = new DebuggerBreakpoints(this); @@ -1406,6 +1471,7 @@ DebuggerState::~DebuggerState() { delete m_adapter; delete m_modules; + delete m_memoryMap; delete m_registers; delete m_threads; delete m_breakpoints; @@ -1572,6 +1638,7 @@ void DebuggerState::MarkDirty() m_registers->MarkDirty(); m_threads->MarkDirty(); m_modules->MarkDirty(); + m_memoryMap->MarkDirty(); m_memory->MarkDirty(); } diff --git a/core/debuggerstate.h b/core/debuggerstate.h index 57e58004..075282c1 100644 --- a/core/debuggerstate.h +++ b/core/debuggerstate.h @@ -78,6 +78,27 @@ namespace BinaryNinjaDebugger { }; + class DebuggerMemoryMap + { + private: + DebuggerState* m_state; + std::vector m_regions; + bool m_dirty; + std::recursive_mutex m_regionsMutex; + + public: + DebuggerMemoryMap(DebuggerState* state); + void MarkDirty(); + void Update(); + bool IsDirty() const { return m_dirty; } + + std::vector GetAllRegions(); + // Return the region that contains the given remote address, if any. `found` is set to false + // when no mapped region covers the address. + DebugMemoryRegion GetRegionForAddress(uint64_t remoteAddress, bool& found); + }; + + struct BreakpointEntry { ModuleNameAndOffset location; @@ -230,6 +251,7 @@ namespace BinaryNinjaDebugger { DebugAdapter* m_adapter; DebuggerModules* m_modules; + DebuggerMemoryMap* m_memoryMap; DebuggerRegisters* m_registers; DebuggerThreads* m_threads; DebuggerBreakpoints* m_breakpoints; @@ -260,6 +282,7 @@ namespace BinaryNinjaDebugger { DebuggerController* GetController() const { return m_controller; } DebuggerModules* GetModules() const { return m_modules; } + DebuggerMemoryMap* GetMemoryMap() const { return m_memoryMap; } DebuggerBreakpoints* GetBreakpoints() const { return m_breakpoints; } DebuggerRegisters* GetRegisters() const { return m_registers; } DebuggerThreads* GetThreads() const { return m_threads; } diff --git a/core/ffi.cpp b/core/ffi.cpp index 17c0cdd5..40b596a6 100644 --- a/core/ffi.cpp +++ b/core/ffi.cpp @@ -344,6 +344,38 @@ void BNDebuggerFreeModules(BNDebugModule* modules, size_t count) } +BNDebugMemoryRegion* BNDebuggerGetMemoryMap(BNDebuggerController* controller, size_t* size) +{ + std::vector regions = controller->object->GetMemoryMap(); + + *size = regions.size(); + BNDebugMemoryRegion* results = new BNDebugMemoryRegion[regions.size()]; + + for (size_t i = 0; i < regions.size(); i++) + { + results[i].m_name = BNDebuggerAllocString(regions[i].m_name.c_str()); + results[i].m_start = regions[i].m_start; + results[i].m_size = regions[i].m_size; + results[i].m_read = regions[i].m_read; + results[i].m_write = regions[i].m_write; + results[i].m_execute = regions[i].m_execute; + results[i].m_shared = regions[i].m_shared; + } + + return results; +} + + +void BNDebuggerFreeMemoryRegions(BNDebugMemoryRegion* regions, size_t count) +{ + for (size_t i = 0; i < count; i++) + { + BNDebuggerFreeString(regions[i].m_name); + } + delete[] regions; +} + + BNDebugRegister* BNDebuggerGetRegisters(BNDebuggerController* controller, size_t* size) { std::vector registers = controller->object->GetAllRegisters(); diff --git a/ui/memorymapwidget.cpp b/ui/memorymapwidget.cpp new file mode 100644 index 00000000..9678bf84 --- /dev/null +++ b/ui/memorymapwidget.cpp @@ -0,0 +1,839 @@ +/* +Copyright 2020-2026 Vector 35 Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include "ui.h" +#include "memorymapwidget.h" +#include "clickablelabel.h" + +using namespace BinaryNinja; +using namespace std; + +constexpr int SortFilterRole = Qt::UserRole + 1; + +MemoryRegionItem::MemoryRegionItem( + uint64_t start, size_t size, std::string name, bool read, bool write, bool execute, bool shared) : + m_start(start), m_size(size), m_name(name), m_read(read), m_write(write), m_execute(execute), m_shared(shared) +{} + + +std::string MemoryRegionItem::permissions() const +{ + std::string result; + result += m_read ? 'r' : '-'; + result += m_write ? 'w' : '-'; + result += m_execute ? 'x' : '-'; + result += m_shared ? 's' : 'p'; + return result; +} + + +bool MemoryRegionItem::operator==(const MemoryRegionItem& other) const +{ + return (m_start == other.start()) && (m_size == other.size()) && (m_name == other.name()) + && (m_read == other.read()) && (m_write == other.write()) && (m_execute == other.execute()) + && (m_shared == other.shared()); +} + + +bool MemoryRegionItem::operator!=(const MemoryRegionItem& other) const +{ + return !(*this == other); +} + + +bool MemoryRegionItem::operator<(const MemoryRegionItem& other) const +{ + if (m_start < other.start()) + return true; + else if (m_start > other.start()) + return false; + return m_size < other.size(); +} + + +DebugMemoryMapListModel::DebugMemoryMapListModel(QWidget* parent, ViewFrame* view) : + QAbstractTableModel(parent), m_view(view) +{} + + +DebugMemoryMapListModel::~DebugMemoryMapListModel() {} + + +MemoryRegionItem DebugMemoryMapListModel::getRow(int row) const +{ + if ((size_t)row >= m_items.size()) + throw std::runtime_error("row index out-of-bound"); + + return m_items[row]; +} + + +QModelIndex DebugMemoryMapListModel::index(int row, int column, const QModelIndex&) const +{ + if (row < 0 || (size_t)row >= m_items.size() || column >= columnCount()) + { + return QModelIndex(); + } + + return createIndex(row, column, (void*)&m_items[row]); +} + + +QVariant DebugMemoryMapListModel::data(const QModelIndex& index, int role) const +{ + if (index.column() >= columnCount() || (size_t)index.row() >= m_items.size()) + return QVariant(); + + MemoryRegionItem* item = static_cast(index.internalPointer()); + if (!item) + return QVariant(); + + if ((role != Qt::DisplayRole) && (role != Qt::SizeHintRole) && (role != SortFilterRole)) + return QVariant(); + + switch (index.column()) + { + case DebugMemoryMapListModel::StartColumn: + { + QString text = QString::asprintf("0x%" PRIx64, item->start()); + if (role == Qt::SizeHintRole) + return QVariant((qulonglong)text.size()); + + return QVariant(text); + } + case DebugMemoryMapListModel::EndColumn: + { + QString text = QString::asprintf("0x%" PRIx64, item->endAddress()); + if (role == Qt::SizeHintRole) + return QVariant((qulonglong)text.size()); + + return QVariant(text); + } + case DebugMemoryMapListModel::SizeColumn: + { + QString text = QString::asprintf("0x%" PRIx64, (uint64_t)item->size()); + if (role == Qt::SizeHintRole) + return QVariant((qulonglong)text.size()); + + return QVariant(text); + } + case DebugMemoryMapListModel::PermissionsColumn: + { + QString text = QString::fromStdString(item->permissions()); + if (role == Qt::SizeHintRole) + return QVariant((qulonglong)text.size()); + + return QVariant(text); + } + case DebugMemoryMapListModel::NameColumn: + { + QString text = QString::fromStdString(item->name()); + if (role == Qt::SizeHintRole) + return QVariant((qulonglong)text.size()); + + return QVariant(text); + } + } + return QVariant(); +} + + +QVariant DebugMemoryMapListModel::headerData(int column, Qt::Orientation orientation, int role) const +{ + if (role != Qt::DisplayRole) + return QVariant(); + + if (orientation == Qt::Vertical) + return QVariant(); + + switch (column) + { + case DebugMemoryMapListModel::StartColumn: + return "Start"; + case DebugMemoryMapListModel::EndColumn: + return "End"; + case DebugMemoryMapListModel::SizeColumn: + return "Size"; + case DebugMemoryMapListModel::PermissionsColumn: + return "Permissions"; + case DebugMemoryMapListModel::NameColumn: + return "Name"; + } + return QVariant(); +} + + +void DebugMemoryMapListModel::updateRows(std::vector newRegions) +{ + beginResetModel(); + std::vector newRows; + for (const DebugMemoryRegion& region : newRegions) + { + newRows.emplace_back(region.m_start, region.m_size, region.m_name, region.m_read, region.m_write, + region.m_execute, region.m_shared); + } + + std::sort(newRows.begin(), newRows.end(), [=](const MemoryRegionItem& a, const MemoryRegionItem& b) { + return a.start() < b.start(); + }); + + m_items = newRows; + endResetModel(); +} + + +DebugMemoryMapItemDelegate::DebugMemoryMapItemDelegate(QWidget* parent) : QStyledItemDelegate(parent) +{ + updateFonts(); +} + + +void DebugMemoryMapItemDelegate::paint( + QPainter* painter, const QStyleOptionViewItem& option, const QModelIndex& idx) const +{ + painter->setFont(m_font); + + bool selected = (option.state & QStyle::State_Selected) != 0; + if (selected) + painter->setBrush(getThemeColor(SelectionColor)); + else + painter->setBrush(option.backgroundBrush); + + painter->setPen(Qt::NoPen); + painter->setFont(m_font); + + QRect textRect = option.rect; + textRect.setBottom(textRect.top() + m_charHeight + 2); + painter->drawRect(textRect); + + auto data = idx.data(Qt::DisplayRole); + switch (idx.column()) + { + case DebugMemoryMapListModel::StartColumn: + case DebugMemoryMapListModel::EndColumn: + painter->setPen(getThemeColor(AddressColor).rgba()); + painter->drawText(textRect, data.toString()); + break; + case DebugMemoryMapListModel::SizeColumn: + painter->setPen(getThemeColor(NumberColor).rgba()); + painter->drawText(textRect, data.toString()); + break; + case DebugMemoryMapListModel::PermissionsColumn: + case DebugMemoryMapListModel::NameColumn: + { + painter->setPen(option.palette.color(QPalette::WindowText).rgba()); + painter->drawText(textRect, data.toString()); + break; + } + default: + break; + } +} + + +void DebugMemoryMapItemDelegate::updateFonts() +{ + // Get font and compute character sizes + m_font = getMonospaceFont(dynamic_cast(parent())); + m_font.setKerning(false); + m_baseline = (int)QFontMetricsF(m_font).ascent(); + m_charWidth = getFontWidthAndAdjustSpacing(m_font); + m_charHeight = (int)(QFontMetricsF(m_font).height() + getExtraFontSpacing()); + m_charOffset = getFontVerticalOffset(); +} + + +QSize DebugMemoryMapItemDelegate::sizeHint(const QStyleOptionViewItem& option, const QModelIndex& idx) const +{ + auto totalWidth = (idx.data(Qt::SizeHintRole).toInt() + 2) * m_charWidth + 4; + return QSize(totalWidth, m_charHeight + 2); +} + + +DebugMemoryMapWidget::DebugMemoryMapWidget(ViewFrame* view, BinaryViewRef data) : QTableView(view), m_view(view) +{ + m_controller = DebuggerController::GetController(data); + if (!m_controller) + return; + + m_model = new DebugMemoryMapListModel(this, view); + m_filter = new DebugMemoryMapFilterProxyModel(this); + m_filter->setSourceModel(m_model); + setModel(m_filter); + setShowGrid(false); + + m_delegate = new DebugMemoryMapItemDelegate(this); + setItemDelegate(m_delegate); + + setSelectionBehavior(QAbstractItemView::SelectItems); + + verticalHeader()->setSectionResizeMode(QHeaderView::ResizeToContents); + verticalHeader()->setVisible(false); + + horizontalHeader()->setStretchLastSection(true); + horizontalHeader()->setSectionResizeMode(QHeaderView::ResizeToContents); + + setHorizontalScrollMode(QAbstractItemView::ScrollPerPixel); + setVerticalScrollMode(QAbstractItemView::ScrollPerPixel); + + resizeColumnsToContents(); + resizeRowsToContents(); + + m_actionHandler.setupActionHandler(this); + m_contextMenuManager = new ContextMenuManager(this); + + QString actionName = QString::fromStdString("Jump To Start"); + UIAction::registerAction(actionName); + m_menu.addAction(actionName, "Options", MENU_ORDER_FIRST); + m_actionHandler.bindAction(actionName, UIAction([this]() { jumpToStart(); })); + + actionName = QString::fromStdString("Jump To End"); + UIAction::registerAction(actionName); + m_menu.addAction(actionName, "Options", MENU_ORDER_FIRST); + m_actionHandler.bindAction(actionName, UIAction([this]() { jumpToEnd(); })); + + m_menu.addAction("Copy", "Options", MENU_ORDER_NORMAL); + m_actionHandler.bindAction("Copy", UIAction([&]() { copy(); }, [&]() { return canCopy(); })); + m_actionHandler.setActionDisplayName("Copy", [&]() { + QModelIndexList sel = selectionModel()->selectedIndexes(); + if (sel.empty()) + return "Copy"; + + switch (sel[0].column()) + { + case DebugMemoryMapListModel::StartColumn: + return "Copy Start"; + case DebugMemoryMapListModel::EndColumn: + return "Copy End"; + case DebugMemoryMapListModel::SizeColumn: + return "Copy Size"; + case DebugMemoryMapListModel::PermissionsColumn: + return "Copy Permissions"; + case DebugMemoryMapListModel::NameColumn: + return "Copy Name"; + default: + return "Copy"; + } + }); + + UIAction::registerAction("Copy All"); + m_menu.addAction("Copy All", "Options", MENU_ORDER_NORMAL); + m_actionHandler.bindAction("Copy All", UIAction([&]() { copyAll(); }, [&]() { return canCopyAll(); })); + + actionName = QString::fromStdString("Select In Binary View"); + UIAction::registerAction(actionName); + m_menu.addAction(actionName, "Options", MENU_ORDER_NORMAL); + m_actionHandler.bindAction( + actionName, UIAction([this]() { selectInView(); }, [this]() { return canSelectRegion(); })); + + actionName = QString::fromStdString("Save Region To Disk..."); + UIAction::registerAction(actionName); + m_menu.addAction(actionName, "Options", MENU_ORDER_LAST); + m_actionHandler.bindAction( + actionName, UIAction([this]() { saveToDisk(); }, [this]() { return canSaveRegion(); })); + + connect(this, &QTableView::doubleClicked, this, &DebugMemoryMapWidget::onDoubleClicked); + connect(this, &DebugMemoryMapWidget::debuggerEvent, this, &DebugMemoryMapWidget::onDebuggerEvent); + + m_debuggerEventCallback = m_controller->RegisterEventCallback( + [&](const DebuggerEvent& event) { emit debuggerEvent(event); }, "Memory Map Widget"); + + updateContent(); +} + + +DebugMemoryMapWidget::~DebugMemoryMapWidget() +{ + if (m_controller) + m_controller->RemoveEventCallback(m_debuggerEventCallback); +} + + +void DebugMemoryMapWidget::updateColumnWidths() +{ + resizeColumnToContents(DebugMemoryMapListModel::StartColumn); + resizeColumnToContents(DebugMemoryMapListModel::EndColumn); + resizeColumnToContents(DebugMemoryMapListModel::SizeColumn); + resizeColumnToContents(DebugMemoryMapListModel::PermissionsColumn); + resizeColumnToContents(DebugMemoryMapListModel::NameColumn); +} + + +void DebugMemoryMapWidget::notifyRegionsChanged(std::vector regions) +{ + m_model->updateRows(regions); + updateColumnWidths(); +} + + +void DebugMemoryMapWidget::onDebuggerEvent(const DebuggerEvent& event) +{ + switch (event.type) + { + case TargetStoppedEventType: + case TargetExitedEventType: + // These updates ensure the widgets become empty after the target stops + case DetachedEventType: + updateContent(); + break; + default: + break; + } +} + + +void DebugMemoryMapWidget::updateContent() +{ + if (!m_controller->IsConnected()) + return; + + std::vector regions = m_controller->GetMemoryMap(); + notifyRegionsChanged(regions); +} + + +void DebugMemoryMapWidget::contextMenuEvent(QContextMenuEvent* event) +{ + showContextMenu(); +} + + +void DebugMemoryMapWidget::showContextMenu() +{ + m_contextMenuManager->show(&m_menu, &m_actionHandler); +} + + +void DebugMemoryMapWidget::jumpToStart() +{ + QModelIndexList sel = selectionModel()->selectedIndexes(); + if (sel.empty()) + return; + + auto sourceIndex = m_filter->mapToSource(sel[0]); + if (!sourceIndex.isValid()) + return; + + auto region = m_model->getRow(sourceIndex.row()); + uint64_t address = region.start(); + + UIContext* context = UIContext::contextForWidget(this); + if (!context) + return; + + ViewFrame* frame = context->getCurrentViewFrame(); + if (!frame) + return; + + if (m_controller->GetData()) + frame->navigate(m_controller->GetData(), address, true, true); +} + + +void DebugMemoryMapWidget::jumpToEnd() +{ + QModelIndexList sel = selectionModel()->selectedIndexes(); + if (sel.empty()) + return; + + auto sourceIndex = m_filter->mapToSource(sel[0]); + if (!sourceIndex.isValid()) + return; + + auto region = m_model->getRow(sourceIndex.row()); + uint64_t address = region.endAddress(); + + UIContext* context = UIContext::contextForWidget(this); + if (!context) + return; + + ViewFrame* frame = context->getCurrentViewFrame(); + if (!frame) + return; + + if (m_controller->GetData()) + frame->navigate(m_controller->GetData(), address, true, true); +} + + +bool DebugMemoryMapWidget::canCopy() +{ + QModelIndexList sel = selectionModel()->selectedIndexes(); + return !sel.empty(); +} + + +bool DebugMemoryMapWidget::canCopyAll() +{ + return m_model->rowCount() > 0; +} + + +void DebugMemoryMapWidget::copy() +{ + QModelIndexList sel = selectionModel()->selectedIndexes(); + if (sel.empty()) + return; + + // Sort into visual order (top-to-bottom, then left-to-right) so a multi-cell selection is + // copied the way it is laid out on screen. + std::sort(sel.begin(), sel.end(), [](const QModelIndex& a, const QModelIndex& b) { + if (a.row() != b.row()) + return a.row() < b.row(); + return a.column() < b.column(); + }); + + auto cellText = [this](const QModelIndex& proxyIndex) -> QString { + auto sourceIndex = m_filter->mapToSource(proxyIndex); + if (!sourceIndex.isValid()) + return QString(); + + auto region = m_model->getRow(sourceIndex.row()); + switch (proxyIndex.column()) + { + case DebugMemoryMapListModel::StartColumn: + return QString::asprintf("0x%" PRIx64, region.start()); + case DebugMemoryMapListModel::EndColumn: + return QString::asprintf("0x%" PRIx64, region.endAddress()); + case DebugMemoryMapListModel::SizeColumn: + return QString::asprintf("0x%" PRIx64, (uint64_t)region.size()); + case DebugMemoryMapListModel::PermissionsColumn: + return QString::fromStdString(region.permissions()); + case DebugMemoryMapListModel::NameColumn: + return QString::fromStdString(region.name()); + default: + return QString(); + } + }; + + // Group the selected cells by row: tab-separate columns within a row, newline between rows. + QStringList lines; + QStringList currentRow; + int lastRow = sel[0].row(); + for (const auto& index : sel) + { + if (index.row() != lastRow) + { + lines.append(currentRow.join('\t')); + currentRow.clear(); + lastRow = index.row(); + } + currentRow.append(cellText(index)); + } + lines.append(currentRow.join('\t')); + + QString text = lines.join('\n'); + + auto* clipboard = QGuiApplication::clipboard(); + clipboard->clear(); + auto* mime = new QMimeData(); + mime->setText(text); + clipboard->setMimeData(mime); +} + + +void DebugMemoryMapWidget::copyAll() +{ + int rowCount = m_model->rowCount(); + if (rowCount == 0) + return; + + QStringList lines; + + // Add header + lines.append("Start\tEnd\tSize\tPermissions\tName"); + + // Add all region rows + for (int row = 0; row < rowCount; row++) + { + auto region = m_model->getRow(row); + QString line = QString::asprintf("0x%" PRIx64 "\t0x%" PRIx64 "\t0x%" PRIx64 "\t%s\t%s", region.start(), + region.endAddress(), (uint64_t)region.size(), region.permissions().c_str(), region.name().c_str()); + lines.append(line); + } + + QString text = lines.join("\n"); + + auto* clipboard = QGuiApplication::clipboard(); + clipboard->clear(); + auto* mime = new QMimeData(); + mime->setText(text); + clipboard->setMimeData(mime); +} + + +bool DebugMemoryMapWidget::canSelectRegion() +{ + return m_controller->IsConnected() && !selectionModel()->selectedIndexes().empty(); +} + + +bool DebugMemoryMapWidget::canSaveRegion() +{ + return m_controller->IsConnected() && !selectionModel()->selectedIndexes().empty(); +} + + +void DebugMemoryMapWidget::selectInView() +{ + QModelIndexList sel = selectionModel()->selectedIndexes(); + if (sel.empty()) + return; + + auto sourceIndex = m_filter->mapToSource(sel[0]); + if (!sourceIndex.isValid()) + return; + + auto region = m_model->getRow(sourceIndex.row()); + + UIContext* context = UIContext::contextForWidget(this); + if (!context) + return; + + ViewFrame* frame = context->getCurrentViewFrame(); + if (!frame) + return; + + BinaryViewRef data = m_controller->GetData(); + if (!data) + return; + + // Navigate to the start of the region first, then select the whole range in the resulting view. + frame->navigate(data, region.start(), true, true); + + View* view = frame->getCurrentViewInterface(); + if (view) + view->setSelectionOffsets({region.start(), region.endAddress()}); +} + + +void DebugMemoryMapWidget::saveToDisk() +{ + QModelIndexList sel = selectionModel()->selectedIndexes(); + if (sel.empty()) + return; + + auto sourceIndex = m_filter->mapToSource(sel[0]); + if (!sourceIndex.isValid()) + return; + + auto region = m_model->getRow(sourceIndex.row()); + + if (!m_controller->IsConnected()) + return; + + DataBuffer buffer = m_controller->ReadMemory(region.start(), region.size()); + if (buffer.GetLength() == 0) + { + QMessageBox::critical(this, "Save Failed", + QString::asprintf("Could not read any memory from the region at 0x%" PRIx64 ".", region.start())); + return; + } + + QString defaultName = QString::asprintf("region_0x%" PRIx64 "-0x%" PRIx64 ".bin", region.start(), region.endAddress()); + QString savePath = QFileDialog::getSaveFileName(this, "Save Memory Region", defaultName, "All Files (*)"); + if (savePath.isEmpty()) + return; + + QFile file(savePath); + if (!file.open(QIODevice::WriteOnly)) + { + QMessageBox::critical(this, "Save Failed", "Could not open the destination file for writing."); + return; + } + + qint64 written = file.write((const char*)buffer.GetData(), buffer.GetLength()); + file.close(); + + if (written != (qint64)buffer.GetLength()) + { + QMessageBox::critical(this, "Save Failed", "Failed to write the full memory region to disk."); + return; + } + + // The region may be only partially readable; if so we saved fewer bytes than the region's nominal size. + if (buffer.GetLength() < region.size()) + { + QMessageBox::warning(this, "Partial Save", + QString::asprintf("Only 0x%" PRIx64 " of 0x%" PRIx64 " bytes were readable and saved.", + (uint64_t)buffer.GetLength(), (uint64_t)region.size())); + } +} + + +void DebugMemoryMapWidget::onDoubleClicked() +{ + QModelIndexList sel = selectionModel()->selectedIndexes(); + if (sel.empty()) + return; + + if (sel[0].column() != DebugMemoryMapListModel::StartColumn + && sel[0].column() != DebugMemoryMapListModel::EndColumn) + return; + + auto sourceIndex = m_filter->mapToSource(sel[0]); + if (!sourceIndex.isValid()) + return; + + auto region = m_model->getRow(sourceIndex.row()); + uint64_t address; + + if (sourceIndex.column() == DebugMemoryMapListModel::StartColumn) + address = region.start(); + else + address = region.endAddress(); + + UIContext* context = UIContext::contextForWidget(this); + if (!context) + return; + + ViewFrame* frame = context->getCurrentViewFrame(); + if (!frame) + return; + + if (m_controller->GetData()) + frame->navigate(m_controller->GetData(), address, true, true); +}; + + +void DebugMemoryMapWidget::setFilter(const string& filter, FilterOptions options) +{ + if (options.testFlag(UseRegexOption)) + m_filter->setFilterRegularExpression(QString::fromStdString(filter)); + else + m_filter->setFilterFixedString(QString::fromStdString(filter)); + m_filter->setFilterCaseSensitivity( + options.testFlag(CaseSensitiveOption) ? Qt::CaseSensitive : Qt::CaseInsensitive); + updateColumnWidths(); +} + + +void DebugMemoryMapWidget::updateFonts() +{ + m_delegate->updateFonts(); +} + + +void DebugMemoryMapWidget::scrollToFirstItem() {} + + +void DebugMemoryMapWidget::scrollToCurrentItem() {} + + +void DebugMemoryMapWidget::ensureSelection() {} + + +void DebugMemoryMapWidget::activateSelection() {} + + +DebugMemoryMapWithFilter::DebugMemoryMapWithFilter(ViewFrame* view, BinaryViewRef data) : m_view(view) +{ + m_memoryMap = new DebugMemoryMapWidget(view, data); + m_separateEdit = new FilterEdit(m_memoryMap); + m_separateEdit->showRegexToggle(true); + m_filter = new FilteredView(this, m_memoryMap, m_memoryMap, m_separateEdit); + m_filter->setFilterPlaceholderText("Search memory regions"); + + auto headerLayout = new QHBoxLayout; + headerLayout->addWidget(m_separateEdit, 1); + + // Vertically-align the hamburger icon with the text field and give the + // layout just a bit more breathing room since it's really close to + // the surrounding elements. + headerLayout->setContentsMargins(1, 1, 6, 0); + headerLayout->setAlignment(Qt::AlignBaseline); + + auto* icon = new ClickableIcon(QImage(":/debugger/menu"), QSize(16, 16)); + connect(icon, &ClickableIcon::clicked, m_memoryMap, &DebugMemoryMapWidget::showContextMenu); + headerLayout->addWidget(icon); + + auto* layout = new QVBoxLayout(this); + layout->setContentsMargins(0, 0, 0, 0); + layout->addLayout(headerLayout); + layout->addWidget(m_filter, 1); +} + + +void DebugMemoryMapWithFilter::updateFonts() +{ + m_memoryMap->updateFonts(); +} + + +DebugMemoryMapContainer::DebugMemoryMapContainer(ViewFrame* frame, BinaryViewRef data) : + SidebarWidget("Debugger Memory Map") +{ + m_widget = new DebugMemoryMapWithFilter(frame, data); + + auto* layout = new QVBoxLayout(this); + layout->setContentsMargins(0, 0, 0, 0); + layout->addWidget(m_widget); +} + + +void DebugMemoryMapContainer::notifyFontChanged() +{ + m_widget->updateFonts(); +} + + +DebugMemoryMapFilterProxyModel::DebugMemoryMapFilterProxyModel(QObject* parent) : QSortFilterProxyModel(parent) +{ + setFilterCaseSensitivity(Qt::CaseInsensitive); +} + + +bool DebugMemoryMapFilterProxyModel::filterAcceptsRow(int sourceRow, const QModelIndex& sourceParent) const +{ + QRegularExpression regExp = filterRegularExpression(); + if (!regExp.isValid()) + return true; + + for (int column = 0; column < sourceModel()->columnCount(sourceParent); column++) + { + QModelIndex index = sourceModel()->index(sourceRow, column, sourceParent); + QString data = index.data(SortFilterRole).toString(); + if (data.indexOf(regExp) != -1) + return true; + } + return false; +} + + +DebugMemoryMapSidebarWidgetType::DebugMemoryMapSidebarWidgetType() : + SidebarWidgetType(QImage(":/icons/images/squares-bug.png"), "Debugger Memory Map") +{} + + +SidebarWidget* DebugMemoryMapSidebarWidgetType::createWidget(ViewFrame* frame, BinaryViewRef data) +{ + return new DebugMemoryMapContainer(frame, data); +} + + +SidebarContentClassifier* DebugMemoryMapSidebarWidgetType::contentClassifier(ViewFrame*, BinaryViewRef data) +{ + return new ActiveDebugSessionSidebarContentClassifier(data); +} diff --git a/ui/memorymapwidget.h b/ui/memorymapwidget.h new file mode 100644 index 00000000..c82b7510 --- /dev/null +++ b/ui/memorymapwidget.h @@ -0,0 +1,228 @@ +/* +Copyright 2020-2026 Vector 35 Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +#pragma once + +#include +#include +#include +#include +#include +#include "inttypes.h" +#include "binaryninjaapi.h" +#include "viewframe.h" +#include "fontsettings.h" +#include "theme.h" +#include "globalarea.h" +#include "filter.h" +#include "debuggerapi.h" + +using namespace BinaryNinjaDebuggerAPI; +using namespace BinaryNinja; +using namespace std; + +class MemoryRegionItem +{ +private: + uint64_t m_start; + size_t m_size; + std::string m_name; + bool m_read; + bool m_write; + bool m_execute; + bool m_shared; + +public: + MemoryRegionItem(uint64_t start, size_t size, std::string name, bool read, bool write, bool execute, bool shared); + uint64_t start() const { return m_start; } + uint64_t endAddress() const { return m_start + m_size; } + size_t size() const { return m_size; } + std::string name() const { return m_name; } + bool read() const { return m_read; } + bool write() const { return m_write; } + bool execute() const { return m_execute; } + bool shared() const { return m_shared; } + // A string like "r-xp" / "rw-s" describing the region's permissions and sharing. + std::string permissions() const; + bool operator==(const MemoryRegionItem& other) const; + bool operator!=(const MemoryRegionItem& other) const; + bool operator<(const MemoryRegionItem& other) const; +}; + +Q_DECLARE_METATYPE(MemoryRegionItem); + + +class DebugMemoryMapListModel : public QAbstractTableModel +{ + Q_OBJECT + +protected: + QWidget* m_owner; + ViewFrame* m_view; + std::vector m_items; + +public: + enum ColumnHeaders + { + StartColumn, + EndColumn, + SizeColumn, + PermissionsColumn, + NameColumn, + }; + + DebugMemoryMapListModel(QWidget* parent, ViewFrame* view); + virtual ~DebugMemoryMapListModel(); + + virtual QModelIndex index(int row, int col, const QModelIndex& parent = QModelIndex()) const override; + + virtual int rowCount(const QModelIndex& parent = QModelIndex()) const override + { + (void)parent; + return (int)m_items.size(); + } + virtual int columnCount(const QModelIndex& parent = QModelIndex()) const override + { + (void)parent; + return 5; + } + MemoryRegionItem getRow(int row) const; + virtual QVariant data(const QModelIndex& i, int role) const override; + virtual QVariant headerData(int column, Qt::Orientation orientation, int role) const override; + void updateRows(std::vector newRegions); +}; + + +class DebugMemoryMapItemDelegate : public QStyledItemDelegate +{ + Q_OBJECT + + QFont m_font; + int m_baseline, m_charWidth, m_charHeight, m_charOffset; + +public: + DebugMemoryMapItemDelegate(QWidget* parent); + void updateFonts(); + void paint(QPainter* painter, const QStyleOptionViewItem& option, const QModelIndex& idx) const; + QSize sizeHint(const QStyleOptionViewItem& option, const QModelIndex& idx) const; +}; + + +class DebugMemoryMapFilterProxyModel : public QSortFilterProxyModel +{ + Q_OBJECT + +public: + DebugMemoryMapFilterProxyModel(QObject* parent); + +protected: + virtual bool filterAcceptsRow(int sourceRow, const QModelIndex& sourceParent) const override; +}; + + +class DebugMemoryMapWidget : public QTableView, public FilterTarget +{ + Q_OBJECT + + ViewFrame* m_view; + DbgRef m_controller; + + DebugMemoryMapListModel* m_model; + DebugMemoryMapItemDelegate* m_delegate; + DebugMemoryMapFilterProxyModel* m_filter; + + size_t m_debuggerEventCallback; + + UIActionHandler m_actionHandler; + ContextMenuManager* m_contextMenuManager; + Menu m_menu; + + virtual void contextMenuEvent(QContextMenuEvent* event) override; + + bool canCopy(); + bool canCopyAll(); + bool canSaveRegion(); + bool canSelectRegion(); + + virtual void setFilter(const std::string& filter, FilterOptions options) override; + virtual void scrollToFirstItem() override; + virtual void scrollToCurrentItem() override; + virtual void ensureSelection() override; + virtual void activateSelection() override; + + void updateContent(); + +public: + DebugMemoryMapWidget(ViewFrame* view, BinaryViewRef data); + ~DebugMemoryMapWidget(); + + void updateColumnWidths(); + void notifyRegionsChanged(std::vector regions); + void updateFonts(); + +signals: + void debuggerEvent(const DebuggerEvent& event); + +private slots: + void jumpToStart(); + void jumpToEnd(); + void copy(); + void copyAll(); + void saveToDisk(); + void selectInView(); + void onDoubleClicked(); + +public slots: + void onDebuggerEvent(const DebuggerEvent& event); + void showContextMenu(); +}; + + +class DebugMemoryMapWithFilter : public QWidget +{ + Q_OBJECT + + ViewFrame* m_view; + DebugMemoryMapWidget* m_memoryMap; + FilteredView* m_filter; + FilterEdit* m_separateEdit = nullptr; + +public: + DebugMemoryMapWithFilter(ViewFrame* view, BinaryViewRef data); + void updateFonts(); +}; + + +class DebugMemoryMapContainer : public SidebarWidget +{ + DebugMemoryMapWithFilter* m_widget; + +public: + DebugMemoryMapContainer(ViewFrame* frame, BinaryViewRef data); + void notifyFontChanged() override; +}; + + +class DebugMemoryMapSidebarWidgetType : public SidebarWidgetType +{ +public: + DebugMemoryMapSidebarWidgetType(); + SidebarWidget* createWidget(ViewFrame* frame, BinaryViewRef data) override; + SidebarWidgetLocation defaultLocation() const override { return SidebarWidgetLocation::RightBottom; } + SidebarContextSensitivity contextSensitivity() const override { return PerViewTypeSidebarContext; } + SidebarIconVisibility defaultIconVisibility() const override { return HideSidebarIconIfNoContent; } + SidebarContentClassifier* contentClassifier(ViewFrame*, BinaryViewRef) override; +}; diff --git a/ui/ui.cpp b/ui/ui.cpp index f60c4f94..74e2b51b 100644 --- a/ui/ui.cpp +++ b/ui/ui.cpp @@ -19,6 +19,7 @@ limitations under the License. #include "breakpointswidget.h" #include "hardwarebreakpointdialog.h" #include "moduleswidget.h" +#include "memorymapwidget.h" #include "renderlayer.h" #include "uinotification.h" #include "platformdialog.h" @@ -2041,6 +2042,7 @@ void GlobalDebuggerUI::InitializeUI() { Sidebar::addSidebarWidgetType(new DebuggerWidgetType(QImage(":/debugger/debugger"), "Debugger")); Sidebar::addSidebarWidgetType(new DebugModulesSidebarWidgetType()); + Sidebar::addSidebarWidgetType(new DebugMemoryMapSidebarWidgetType()); Sidebar::addSidebarWidgetType(new ThreadFramesSidebarWidgetType()); Sidebar::addSidebarWidgetType(new DebugInfoWidgetType()); Sidebar::addSidebarWidgetType(new TTDMemoryWidgetType());