diff --git a/api/debuggerapi.h b/api/debuggerapi.h index 0fad836f..2a17d0dd 100644 --- a/api/debuggerapi.h +++ b/api/debuggerapi.h @@ -698,6 +698,22 @@ namespace BinaryNinjaDebuggerAPI { std::vector GetModules(); std::vector GetMemoryMap(); + + // Read the symbols the debugger backend knows about for the named module and add them to the + // BinaryView as auto symbols. Returns the number of symbols added. + size_t LoadSymbolsForModule(const std::string& module); + // Load the backend symbols for every currently-loaded module. Returns the total number added. + size_t LoadSymbolsForAllModules(); + // Remove the backend symbols previously added for the named module. Returns the number removed. + size_t RemoveSymbolsForModule(const std::string& module); + // Remove every backend symbol the debugger has added. Returns the number removed. + size_t RemoveAllLoadedSymbols(); + // The base names of the modules for which backend symbols have been loaded. + std::vector GetModulesWithLoadedSymbols(); + // The number of backend symbols currently loaded for the named module (0 if none). The module may + // be given as either its base name or its full path. + size_t GetLoadedSymbolCountForModule(const std::string& module); + 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 71720ac8..35dd6d8d 100644 --- a/api/debuggercontroller.cpp +++ b/api/debuggercontroller.cpp @@ -300,6 +300,51 @@ std::vector DebuggerController::GetMemoryMap() } +size_t DebuggerController::LoadSymbolsForModule(const std::string& module) +{ + return BNDebuggerLoadSymbolsForModule(m_object, module.c_str()); +} + + +size_t DebuggerController::LoadSymbolsForAllModules() +{ + return BNDebuggerLoadSymbolsForAllModules(m_object); +} + + +size_t DebuggerController::RemoveSymbolsForModule(const std::string& module) +{ + return BNDebuggerRemoveSymbolsForModule(m_object, module.c_str()); +} + + +size_t DebuggerController::RemoveAllLoadedSymbols() +{ + return BNDebuggerRemoveAllLoadedSymbols(m_object); +} + + +std::vector DebuggerController::GetModulesWithLoadedSymbols() +{ + size_t count; + char** modules = BNDebuggerGetModulesWithLoadedSymbols(m_object, &count); + + std::vector result; + result.reserve(count); + for (size_t i = 0; i < count; i++) + result.emplace_back(modules[i]); + + BNDebuggerFreeStringList(modules, count); + return result; +} + + +size_t DebuggerController::GetLoadedSymbolCountForModule(const std::string& module) +{ + return BNDebuggerGetLoadedSymbolCountForModule(m_object, module.c_str()); +} + + std::vector DebuggerController::GetRegisters() { size_t count; diff --git a/api/ffi.h b/api/ffi.h index c9655f83..8836217e 100644 --- a/api/ffi.h +++ b/api/ffi.h @@ -533,6 +533,23 @@ extern "C" DEBUGGER_FFI_API BNDebugMemoryRegion* BNDebuggerGetMemoryMap(BNDebuggerController* controller, size_t* count); DEBUGGER_FFI_API void BNDebuggerFreeMemoryRegions(BNDebugMemoryRegion* regions, size_t count); + // Read the symbols the debugger backend knows about for the named module and add them to the + // BinaryView as auto symbols. Returns the number of symbols added. + DEBUGGER_FFI_API size_t BNDebuggerLoadSymbolsForModule(BNDebuggerController* controller, const char* module); + // Load the backend symbols for every currently-loaded module. Returns the total number added. + DEBUGGER_FFI_API size_t BNDebuggerLoadSymbolsForAllModules(BNDebuggerController* controller); + // Remove the backend symbols previously added for the named module. Returns the number removed. + DEBUGGER_FFI_API size_t BNDebuggerRemoveSymbolsForModule(BNDebuggerController* controller, const char* module); + // Remove every backend symbol the debugger has added. Returns the number removed. + DEBUGGER_FFI_API size_t BNDebuggerRemoveAllLoadedSymbols(BNDebuggerController* controller); + // The base names of the modules for which backend symbols have been loaded. Free with + // BNDebuggerFreeStringList. + DEBUGGER_FFI_API char** BNDebuggerGetModulesWithLoadedSymbols(BNDebuggerController* controller, size_t* count); + // The number of backend symbols currently loaded for the named module (0 if none). The module may be + // given as either its base name or its full path. + DEBUGGER_FFI_API size_t BNDebuggerGetLoadedSymbolCountForModule( + BNDebuggerController* controller, const char* module); + 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 c312abb7..b0c11852 100644 --- a/api/python/debuggercontroller.py +++ b/api/python/debuggercontroller.py @@ -1529,6 +1529,74 @@ def memory_map(self) -> List[DebugMemoryRegion]: dbgcore.BNDebuggerFreeMemoryRegions(regions, count.value) return result + def load_symbols_for_module(self, module: str) -> int: + """ + Read the symbols that the debugger backend knows about for the given module and add them to + the BinaryView as auto symbols. + + By default the debugger loads no symbols from the backend. Call this to load, on demand, all + symbols of a module (e.g., the exports of ``kernel32.dll``) so that the annotation process + becomes aware of them, e.g., when a register points to a Windows API function. The added + symbols are tracked internally and can be removed later with ``remove_symbols_for_module`` or + ``remove_all_loaded_symbols``. + + Loading the same module again is idempotent: any symbols previously loaded for it are removed + first, so no duplicate symbols are created. + + :param module: the module to load symbols for; either its short name or full path + :return: the number of symbols added + """ + return dbgcore.BNDebuggerLoadSymbolsForModule(self.handle, module) + + def load_symbols_for_all_modules(self) -> int: + """ + Load the backend symbols for every currently-loaded module. + + :return: the total number of symbols added + """ + return dbgcore.BNDebuggerLoadSymbolsForAllModules(self.handle) + + def remove_symbols_for_module(self, module: str) -> int: + """ + Remove the backend symbols previously added for the given module. + + :param module: the module to remove symbols for; either its short name or full path + :return: the number of symbols removed + """ + return dbgcore.BNDebuggerRemoveSymbolsForModule(self.handle, module) + + def remove_all_loaded_symbols(self) -> int: + """ + Remove every backend symbol the debugger has added. + + :return: the number of symbols removed + """ + return dbgcore.BNDebuggerRemoveAllLoadedSymbols(self.handle) + + @property + def modules_with_loaded_symbols(self) -> List[str]: + """ + The base names of the modules for which backend symbols have been loaded. + + :return: a list of module base names + """ + count = ctypes.c_ulonglong() + modules = dbgcore.BNDebuggerGetModulesWithLoadedSymbols(self.handle, count) + result = [] + for i in range(count.value): + result.append(modules[i].decode('utf-8')) + dbgcore.BNDebuggerFreeStringList(modules, count.value) + return result + + def loaded_symbol_count_for_module(self, module: str) -> int: + """ + The number of backend symbols currently loaded for the given module. + + :param module: the module to query; either its short name or full path + :return: the number of loaded symbols, or 0 if none have been loaded for the module + """ + return dbgcore.BNDebuggerGetLoadedSymbolCountForModule(self.handle, module) + 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 5101e28f..a6f5e0b4 100644 --- a/core/adapters/dbgengadapter.cpp +++ b/core/adapters/dbgengadapter.cpp @@ -1819,6 +1819,53 @@ std::vector DbgEngAdapter::GetMemoryMap() } +std::vector DbgEngAdapter::GetSymbolsForModule(const DebugModule& module) +{ + std::vector result; + if (!m_debugSymbols) + return result; + + // Build the symbol-match pattern "!*". dbgeng identifies modules in the bang syntax by their + // base name without extension. + std::string moduleName = + module.m_short_name.empty() ? DebugModule::GetPathBaseName(module.m_name) : module.m_short_name; + auto dot = moduleName.find_last_of('.'); + if (dot != std::string::npos) + moduleName = moduleName.substr(0, dot); + if (moduleName.empty()) + return result; + + std::string pattern = moduleName + "!*"; + + uint64_t handle = 0; + if (m_debugSymbols->StartSymbolMatch(pattern.c_str(), &handle) != S_OK) + return result; + + char nameBuffer[2048]; + uint64_t offset = 0; + unsigned long matchSize = 0; + while (m_debugSymbols->GetNextSymbolMatch(handle, nameBuffer, sizeof(nameBuffer), &matchSize, &offset) == S_OK) + { + // GetNextSymbolMatch returns the fully-qualified "module!symbol" name. + std::string fullName = nameBuffer; + std::string shortName = fullName; + auto bang = fullName.find('!'); + if (bang != std::string::npos) + shortName = fullName.substr(bang + 1); + + if (shortName.empty() || (offset == 0)) + continue; + + // TODO: dbgeng's symbol match does not report whether a symbol is code or data; classify all as + // functions for now, which is correct for the common case of API exports. + result.emplace_back(shortName, fullName, fullName, offset, 0, true); + } + + m_debugSymbols->EndSymbolMatch(handle); + return result; +} + + bool DbgEngAdapter::BreakInto() { if (ExecStatus() == DEBUG_STATUS_BREAK || ExecStatus() == DEBUG_STATUS_NO_DEBUGGEE) @@ -2238,6 +2285,8 @@ bool DbgEngAdapter::SupportFeature(DebugAdapterCapacity feature) return true; case DebugAdapterSupportThreads: return true; + case DebugAdapterSupportSymbols: + return true; default: return false; } diff --git a/core/adapters/dbgengadapter.h b/core/adapters/dbgengadapter.h index 6fc097fe..7f007033 100644 --- a/core/adapters/dbgengadapter.h +++ b/core/adapters/dbgengadapter.h @@ -225,6 +225,8 @@ namespace BinaryNinjaDebugger { std::vector GetMemoryMap() override; + std::vector GetSymbolsForModule(const DebugModule& module) override; + std::string GetTargetArchitecture() override; DebugStopReason StopReason() override; diff --git a/core/adapters/lldbadapter.cpp b/core/adapters/lldbadapter.cpp index 523e7015..71461f44 100644 --- a/core/adapters/lldbadapter.cpp +++ b/core/adapters/lldbadapter.cpp @@ -1685,6 +1685,81 @@ std::vector LldbAdapter::GetMemoryMap() } +std::vector LldbAdapter::GetSymbolsForModule(const DebugModule& module) +{ + std::vector result; + + // Locate the SBModule that corresponds to the requested DebugModule. We match on the base file + // name so that host/guest path differences (see DebugModule::IsSameBaseModule) do not matter. + uint32_t numModules = m_target.GetNumModules(); + for (uint32_t i = 0; i < numModules; i++) + { + SBModule sbModule = m_target.GetModuleAtIndex(i); + if (!sbModule.IsValid()) + continue; + + SBFileSpec fileSpec = sbModule.GetFileSpec(); + char path[1024]; + size_t len = fileSpec.GetPath(path, 1024); + std::string modulePath(path, len); + if (!module.IsSameBaseModule(modulePath)) + continue; + + size_t numSymbols = sbModule.GetNumSymbols(); + result.reserve(numSymbols); + for (size_t j = 0; j < numSymbols; j++) + { + SBSymbol symbol = sbModule.GetSymbolAtIndex(j); + if (!symbol.IsValid()) + continue; + + SymbolType type = symbol.GetType(); + bool isFunction; + switch (type) + { + case eSymbolTypeCode: + case eSymbolTypeResolver: + isFunction = true; + break; + case eSymbolTypeData: + isFunction = false; + break; + default: + // Skip everything else (e.g. compile units, line entries, trampolines), which do not + // correspond to a useful named address in the target. + continue; + } + + SBAddress startAddress = symbol.GetStartAddress(); + if (!startAddress.IsValid()) + continue; + + uint64_t address = startAddress.GetLoadAddress(m_target); + if ((address == 0) || (address == LLDB_INVALID_ADDRESS)) + continue; + + const char* name = symbol.GetName(); + if ((name == nullptr) || (name[0] == '\0')) + continue; + + std::string shortName = name; + std::string fullName = module.m_short_name.empty() ? shortName : module.m_short_name + "!" + shortName; + std::string rawName; + if (const char* mangled = symbol.GetMangledName()) + rawName = mangled; + if (rawName.empty()) + rawName = shortName; + + result.emplace_back(shortName, fullName, rawName, address, symbol.GetSize(), isFunction); + } + + break; + } + + return result; +} + + std::string LldbAdapter::GetTargetArchitecture() { SBPlatform platform = m_target.GetPlatform(); @@ -2144,6 +2219,8 @@ bool LldbAdapter::SupportFeature(DebugAdapterCapacity feature) return true; case DebugAdapterSupportThreads: return true; + case DebugAdapterSupportSymbols: + return true; case DebugAdapterSupportTTD: return false; default: diff --git a/core/adapters/lldbadapter.h b/core/adapters/lldbadapter.h index 1ce16128..cb012815 100644 --- a/core/adapters/lldbadapter.h +++ b/core/adapters/lldbadapter.h @@ -127,6 +127,8 @@ namespace BinaryNinjaDebugger { std::vector GetMemoryMap() override; + std::vector GetSymbolsForModule(const DebugModule& module) override; + std::string GetTargetArchitecture() override; DebugStopReason StopReason() override; diff --git a/core/adapters/windowsnativeadapter.cpp b/core/adapters/windowsnativeadapter.cpp index 1896f5ef..3d4c106b 100644 --- a/core/adapters/windowsnativeadapter.cpp +++ b/core/adapters/windowsnativeadapter.cpp @@ -3078,6 +3078,8 @@ bool WindowsNativeAdapter::SupportFeature(DebugAdapterCapacity feature) return true; case DebugAdapterSupportThreads: return true; + case DebugAdapterSupportSymbols: + return true; case DebugAdapterSupportStepOverReverse: case DebugAdapterSupportTTD: return false; @@ -3087,6 +3089,150 @@ bool WindowsNativeAdapter::SupportFeature(DebugAdapterCapacity feature) } +namespace { + struct EnumSymbolsContext + { + std::vector* result; + std::string moduleName; + // Executable [start, end) address ranges of the module, used to classify symbols as code or data. + const std::vector>* execRanges; + }; + + static BOOL CALLBACK EnumSymbolsCallback(PSYMBOL_INFO pSymInfo, ULONG symbolSize, PVOID userContext) + { + auto* ctx = reinterpret_cast(userContext); + if (!pSymInfo || (pSymInfo->NameLen == 0)) + return TRUE; + + std::string shortName(pSymInfo->Name, pSymInfo->NameLen); + std::string fullName = ctx->moduleName.empty() ? shortName : ctx->moduleName + "!" + shortName; + // dbghelp sets SYMFLAG_FUNCTION for symbols from full debug info, but for the export-table symbols + // we get for system DLLs (no PDB) it usually does not. Without this, every API export would be + // added as a data symbol -- rendering in a different color than the DbgEng backend, which reports + // them as functions. Fall back to the module's executable sections to recover the classification so + // the two adapters agree and code exports show as functions. + bool isFunction = (pSymInfo->Flags & SYMFLAG_FUNCTION) != 0; + if (!isFunction && ctx->execRanges) + { + for (const auto& [start, end] : *ctx->execRanges) + { + if (pSymInfo->Address >= start && pSymInfo->Address < end) + { + isFunction = true; + break; + } + } + } + ctx->result->emplace_back(shortName, fullName, shortName, pSymInfo->Address, symbolSize, isFunction); + return TRUE; + } +} + + +std::vector> WindowsNativeAdapter::GetExecutableRanges(uint64_t moduleBase) +{ + // NOTE: Once we read the target's memory map via VirtualQueryEx (planned), a region's executability is + // available directly from its protection (PAGE_EXECUTE_*), so this per-module PE-section parsing could be + // replaced by -- or cross-checked against -- that map. Better still, symbol code/data classification + // could move up into the controller and use the debugger BinaryView's segment executability, so every + // adapter (LLDB, DbgEng, native) agrees instead of each guessing on its own. All of these only answer + // "is this address executable memory", not "function entry vs. code-adjacent data" -- that needs a PDB. + std::vector> ranges; + if (!m_processHandle || moduleBase == 0) + return ranges; + + IMAGE_DOS_HEADER dosHeader {}; + if (!ReadProcessMemory(m_processHandle, (LPCVOID)moduleBase, &dosHeader, sizeof(dosHeader), nullptr) + || dosHeader.e_magic != IMAGE_DOS_SIGNATURE) + return ranges; + + // Read the NT headers. Reading the 64-bit layout for a 32-bit (WOW64) image over-reads a few bytes into + // the section table, which is harmless -- we only use the Signature and FileHeader, whose layout is + // identical for PE32 and PE32+, plus FileHeader.SizeOfOptionalHeader to locate the section table. + IMAGE_NT_HEADERS ntHeaders {}; + uint64_t ntHeaderAddr = moduleBase + dosHeader.e_lfanew; + if (!ReadProcessMemory(m_processHandle, (LPCVOID)ntHeaderAddr, &ntHeaders, sizeof(ntHeaders), nullptr) + || ntHeaders.Signature != IMAGE_NT_SIGNATURE) + return ranges; + + uint64_t sectionTable = + ntHeaderAddr + FIELD_OFFSET(IMAGE_NT_HEADERS, OptionalHeader) + ntHeaders.FileHeader.SizeOfOptionalHeader; + for (WORD i = 0; i < ntHeaders.FileHeader.NumberOfSections; i++) + { + IMAGE_SECTION_HEADER section {}; + if (!ReadProcessMemory(m_processHandle, (LPCVOID)(sectionTable + (uint64_t)i * sizeof(section)), §ion, + sizeof(section), nullptr)) + break; + if (section.Characteristics & IMAGE_SCN_MEM_EXECUTE) + { + uint64_t start = moduleBase + section.VirtualAddress; + DWORD size = section.Misc.VirtualSize ? section.Misc.VirtualSize : section.SizeOfRawData; + ranges.emplace_back(start, start + size); + } + } + return ranges; +} + + +std::vector WindowsNativeAdapter::GetSymbolsForModule(const DebugModule& module) +{ + std::vector result; + if (!m_processHandle) + return result; + + std::string moduleName = + module.m_short_name.empty() ? DebugModule::GetPathBaseName(module.m_name) : module.m_short_name; + + // Start from a clean symbol handler and, crucially, do NOT invade the process (fInvadeProcess = FALSE). + // The previous fInvadeProcess = TRUE relied on dbghelp enumerating the target's loader list to register + // modules; when that registration did not line up with the base we enumerate at, SymEnumSymbols found no + // module and returned zero symbols. Instead we load exactly the one module we need, from its on-disk + // image at the base it occupies in the target, and enumerate at the base dbghelp actually loaded it at. + // This is both faster and deterministic. SymCleanup on an uninitialized handle is a harmless no-op. + SymCleanup(m_processHandle); + SymSetOptions(SYMOPT_UNDNAME | SYMOPT_DEFERRED_LOADS); + if (!SymInitialize(m_processHandle, nullptr, FALSE)) + { + LogWarn("SymInitialize failed for module %s: error %lu", moduleName.c_str(), GetLastError()); + return result; + } + + // SymLoadModuleEx returns the load base, or 0 both when the module is already loaded + // (GetLastError == ERROR_SUCCESS) and on genuine failure; distinguish the two by GetLastError. + SetLastError(ERROR_SUCCESS); + DWORD64 base = SymLoadModuleEx(m_processHandle, nullptr, module.m_name.c_str(), moduleName.c_str(), + module.m_address, (DWORD)module.m_size, nullptr, 0); + if (base == 0) + { + DWORD err = GetLastError(); + if (err != ERROR_SUCCESS) + { + LogWarn("SymLoadModuleEx failed for module %s at 0x%llX (error %lu)", moduleName.c_str(), + (unsigned long long)module.m_address, err); + SymCleanup(m_processHandle); + return result; + } + } + DWORD64 moduleBase = base ? base : module.m_address; + + // Read the module's executable section ranges from its mapped PE headers so the callback can classify + // export-table symbols (which dbghelp does not flag as SYMFLAG_FUNCTION) as functions rather than data, + // matching the DbgEng backend so both show up the same way in the symbols list. + std::vector> execRanges = GetExecutableRanges(moduleBase); + + EnumSymbolsContext ctx {&result, moduleName, &execRanges}; + if (!SymEnumSymbols(m_processHandle, moduleBase, "*", EnumSymbolsCallback, &ctx)) + { + LogWarn("SymEnumSymbols found no symbols for module %s (base 0x%llX): error %lu. The debugger backend " + "may not have symbols available for this module.", + moduleName.c_str(), (unsigned long long)moduleBase, GetLastError()); + } + + SymCleanup(m_processHandle); + return result; +} + + std::vector WindowsNativeAdapter::GetFramesOfThread(uint32_t tid) { std::vector frames; @@ -3114,7 +3260,12 @@ std::vector WindowsNativeAdapter::GetFramesOfThread(uint32_t tid) // 32-bit process on 64-bit Windows ctx32.ContextFlags = WOW64_CONTEXT_FULL; if (!Wow64GetThreadContext(threadHandle, &ctx32)) + { + // Balance the SymInitialize above; leaving the handler initialized would make the next + // SymInitialize (here or in GetSymbolsForModule) a no-op and leak the session. + SymCleanup(m_processHandle); return frames; + } machineType = IMAGE_FILE_MACHINE_I386; stackFrame.AddrPC.Offset = ctx32.Eip; @@ -3130,7 +3281,11 @@ std::vector WindowsNativeAdapter::GetFramesOfThread(uint32_t tid) // Native 64-bit process ctx64.ContextFlags = CONTEXT_FULL; if (!GetThreadContext(threadHandle, &ctx64)) + { + // Balance the SymInitialize above (see the WOW64 branch). + SymCleanup(m_processHandle); return frames; + } machineType = IMAGE_FILE_MACHINE_AMD64; stackFrame.AddrPC.Offset = ctx64.Rip; diff --git a/core/adapters/windowsnativeadapter.h b/core/adapters/windowsnativeadapter.h index ee01b5a9..f6c78c86 100644 --- a/core/adapters/windowsnativeadapter.h +++ b/core/adapters/windowsnativeadapter.h @@ -151,6 +151,9 @@ namespace BinaryNinjaDebugger { bool HandleOutputDebugString(const OUTPUT_DEBUG_STRING_INFO& info); std::string GetModuleNameFromHandle(HANDLE fileHandle, LPVOID baseAddress); + // The module's executable [start, end) address ranges, read from its mapped PE section headers. + // Used to classify backend symbols (see GetSymbolsForModule / EnumSymbolsCallback) as code or data. + std::vector> GetExecutableRanges(uint64_t moduleBase); bool ApplyBreakpoint(uint64_t address, unsigned long id); bool RemoveBreakpointInternal(uint64_t address); void ApplyPendingBreakpoints(); @@ -228,6 +231,8 @@ namespace BinaryNinjaDebugger { std::vector GetMemoryMap() override; + std::vector GetSymbolsForModule(const DebugModule& module) override; + std::string GetTargetArchitecture() override; DebugStopReason StopReason() override; diff --git a/core/debugadapter.h b/core/debugadapter.h index 2644175b..5909a602 100644 --- a/core/debugadapter.h +++ b/core/debugadapter.h @@ -58,6 +58,9 @@ namespace BinaryNinjaDebugger { DebugAdapterSupportModules, DebugAdapterSupportThreads, DebugAdapterSupportTTD, + // The adapter can read the symbols a debugger backend knows about for a module. See + // DebugAdapter::GetSymbolsForModule. + DebugAdapterSupportSymbols, }; @@ -236,6 +239,33 @@ namespace BinaryNinjaDebugger { {} }; + // A symbol the debugger backend (e.g. LLDB, dbgeng) knows about for a loaded module, but which the + // static BinaryView analysis is not aware of. These are read on demand (see + // DebugAdapter::GetSymbolsForModule and DebuggerController::LoadSymbolsForModule) and added to the + // debugger BinaryView as auto symbols so that the annotation process becomes aware of them. + struct DebugSymbol + { + // Short name, e.g. "CreateFileA" + std::string m_name {}; + // Fully-qualified name, e.g. "kernel32!CreateFileA" + std::string m_fullName {}; + // Raw (possibly mangled) name. May be empty if the backend does not provide one. + std::string m_rawName {}; + // Absolute load address of the symbol in the target. + std::uintptr_t m_address {}; + std::size_t m_size {}; + // True if the symbol refers to code (a function), false if it refers to data. + bool m_isFunction {}; + + DebugSymbol() = default; + + DebugSymbol(std::string name, std::string fullName, std::string rawName, std::uintptr_t address, + std::size_t size, bool isFunction) : + m_name(std::move(name)), m_fullName(std::move(fullName)), m_rawName(std::move(rawName)), + m_address(address), m_size(size), m_isFunction(isFunction) + {} + }; + struct DebugFrame { size_t m_index = 0; @@ -368,6 +398,12 @@ namespace BinaryNinjaDebugger { // that do not (yet) support it. See issue #96. virtual std::vector GetMemoryMap() { return {}; } + // Read the symbols that the debugger backend knows about for the given module. These are the + // symbols (e.g. exports like CreateFileA) that the static BinaryView analysis is not aware of. + // The default implementation returns an empty list; adapters that support reading symbols from + // the backend should override this and report DebugAdapterSupportSymbols in SupportFeature. + virtual std::vector GetSymbolsForModule(const DebugModule& module) { return {}; } + virtual std::string GetTargetArchitecture() = 0; virtual DebugStopReason StopReason() = 0; diff --git a/core/debuggercontroller.cpp b/core/debuggercontroller.cpp index 573eb9d7..173bd3fb 100644 --- a/core/debuggercontroller.cpp +++ b/core/debuggercontroller.cpp @@ -2101,6 +2101,11 @@ void DebuggerController::ApplyOwnStateForEvent(const DebuggerEvent& event) // harmless. void DebuggerController::FinalizeTargetGoneCleanup() { + // The backend symbols we added are at absolute target addresses that are meaningless once the target + // is gone, so remove them. Idempotent: the map is cleared, so a second call is a no-op. Pass + // updateAnalysis = false: we are about to remove the debugger memory region below, so we must not + // schedule an async analysis pass that could read from it mid-teardown. + RemoveAllLoadedSymbols(false); m_state->MarkDirty(); // Remove the region from the BinaryView's MemoryMap BEFORE disposing of m_accessor: the // MemoryMap holds a raw pointer to it (see AddRemoteMemoryRegion in DebuggerController::Start), @@ -2509,6 +2514,274 @@ std::vector DebuggerController::GetMemoryMap() return m_state->GetMemoryMap()->GetAllRegions(); } + +size_t DebuggerController::LoadSymbolsForModule(const std::string& moduleName) +{ + DebugModule module = m_state->GetModules()->GetModuleByName(moduleName); + if (module.m_name.empty() && module.m_short_name.empty()) + { + LogWarn("Cannot load symbols: no module named \"%s\" is loaded in the target", moduleName.c_str()); + return 0; + } + return LoadSymbolsForModule(module); +} + + +size_t DebuggerController::LoadSymbolsForModule(const DebugModule& module) +{ + if (!m_adapter) + return 0; + + if (!m_adapter->SupportFeature(DebugAdapterSupportSymbols)) + { + LogWarn("The current debug adapter does not support reading symbols from the backend"); + return 0; + } + + auto data = GetData(); + if (!data) + return 0; + + std::vector symbols; + { + std::lock_guard adapterLock(m_state->AdapterAccessMutex()); + symbols = m_adapter->GetSymbolsForModule(module); + } + + if (symbols.empty()) + return 0; + + std::lock_guard lock(m_loadedModuleSymbolsMutex); + + auto id = data->BeginUndoActions(); + // Adding a large module's symbols one at a time generates a per-symbol analysis notification; disable + // the updates while we add them in bulk and re-enable afterwards (see the design notes on issue #210). + data->SetFunctionAnalysisUpdateDisabled(true); + size_t count = ApplyModuleSymbolsLocked(data, module, symbols); + data->SetFunctionAnalysisUpdateDisabled(false); + data->ForgetUndoActions(id); + // The data variables above were defined while function-analysis updates were disabled, so nothing has + // processed them into the view yet. Without this, the newly added symbols show up "bare" (no data + // variable) in the symbols/linear views until the user manually refreshes. Kick an async update so the + // pending data variables are materialized and the views are notified. + data->UpdateAnalysis(); + + LogInfo("Loaded %zu symbols for module %s from the debugger backend", count, + module.m_short_name.empty() ? module.m_name.c_str() : module.m_short_name.c_str()); + return count; +} + + +size_t DebuggerController::LoadSymbolsForAllModules() +{ + if (!m_adapter) + return 0; + + if (!m_adapter->SupportFeature(DebugAdapterSupportSymbols)) + { + LogWarn("The current debug adapter does not support reading symbols from the backend"); + return 0; + } + + auto data = GetData(); + if (!data) + return 0; + + // Read every module's symbols from the backend first (this needs the adapter lock), then apply them + // all inside a single analysis-update window below. Toggling function-analysis updates once for the + // whole batch -- rather than once per module -- is what makes loading "all modules" behave the same as + // loading each module on its own: re-enabling analysis starts an async update, and a per-module disable + // would supersede the previous module's still-pending update, leaving only the last module's references + // (e.g. IAT pointers to freshly-named API functions) re-resolved. See ApplyModuleSymbolsLocked / #210. + std::vector>> moduleSymbols; + { + std::lock_guard adapterLock(m_state->AdapterAccessMutex()); + for (const DebugModule& module : GetAllModules()) + { + std::vector symbols = m_adapter->GetSymbolsForModule(module); + if (!symbols.empty()) + moduleSymbols.emplace_back(module, std::move(symbols)); + } + } + + if (moduleSymbols.empty()) + return 0; + + std::lock_guard lock(m_loadedModuleSymbolsMutex); + + auto id = data->BeginUndoActions(); + data->SetFunctionAnalysisUpdateDisabled(true); + size_t total = 0; + for (auto& [module, symbols] : moduleSymbols) + total += ApplyModuleSymbolsLocked(data, module, symbols); + data->SetFunctionAnalysisUpdateDisabled(false); + data->ForgetUndoActions(id); + // Materialize the data variables added under the disabled-update window and notify the views; see + // LoadSymbolsForModule for why this is needed (otherwise the symbols render "bare" until a refresh). + data->UpdateAnalysis(); + + LogInfo("Loaded %zu symbols across %zu modules from the debugger backend", total, moduleSymbols.size()); + return total; +} + + +size_t DebuggerController::ApplyModuleSymbolsLocked( + BinaryViewRef data, const DebugModule& module, const std::vector& symbols) +{ + // If symbols were already loaded for this module, remove them first so that re-loading the same + // module is idempotent and does not create duplicate symbols. + for (auto it = m_loadedModuleSymbols.begin(); it != m_loadedModuleSymbols.end(); ++it) + { + if (module.IsSameBaseModule(it->first)) + { + RemoveTrackedSymbolsLocked(data, it->second); + m_loadedModuleSymbols.erase(it); + break; + } + } + + std::string key = !module.m_name.empty() ? DebugModule::GetPathBaseName(module.m_name) : module.m_short_name; + std::vector>* symbolList = &m_loadedModuleSymbols[key]; + + auto voidType = Type::VoidType(); + for (const DebugSymbol& sym : symbols) + { + BNSymbolType symbolType = sym.m_isFunction ? FunctionSymbol : DataSymbol; + std::string rawName = sym.m_rawName.empty() ? sym.m_name : sym.m_rawName; + // Use DefineAutoSymbol (not DefineUserSymbol) so these never override the user's own symbols. + Ref symbol = new Symbol(symbolType, sym.m_name, sym.m_fullName, rawName, sym.m_address); + data->DefineAutoSymbol(symbol); + // A data variable is needed for BN to actually render the symbol in the views. Define it with a + // void type, mirroring the design notes on issue #210. + data->DefineDataVariable(sym.m_address, Confidence>(voidType)); + // Track the exact symbol object so we can remove precisely it later, even when the linker folds + // several symbols onto one address (GetSymbolByAddress would only return one of them). + symbolList->push_back(symbol); + } + return symbols.size(); +} + + +size_t DebuggerController::RemoveTrackedSymbolsLocked(BinaryViewRef data, const std::vector>& symbols) +{ + for (const Ref& symbol : symbols) + { + if (!symbol) + continue; + // Undo in the inverse order of ApplyModuleSymbolsLocked (which defines the symbol, then the data + // variable). Removing the data variable first avoids leaving it briefly symbol-less, which on + // some platforms makes the core auto-create an anonymous "data_..." symbol that would then leak. + // Undefining a data variable is keyed on the address; calling it more than once for an address + // shared by several folded symbols is harmless (the later calls are no-ops). + // + // Pass blacklist = false: the default (true) blacklists the address so auto analysis will not + // recreate an auto data variable there. Since ApplyModuleSymbolsLocked adds these as *auto* data + // variables, blacklisting would make a later re-load's DefineDataVariable a no-op -- the symbol + // would then have no data variable and would not render in the linear view. We manage these + // variables ourselves, so removal must not blacklist them. + data->UndefineDataVariable(symbol->GetAddress(), false); + data->UndefineAutoSymbol(symbol); + } + return symbols.size(); +} + + +size_t DebuggerController::UndefineTrackedSymbols(const std::vector>& symbols) +{ + auto data = GetData(); + if (!data) + return 0; + + auto id = data->BeginUndoActions(); + data->SetFunctionAnalysisUpdateDisabled(true); + size_t count = RemoveTrackedSymbolsLocked(data, symbols); + data->SetFunctionAnalysisUpdateDisabled(false); + data->ForgetUndoActions(id); + // Flush the undefines to the views (mirrors the load path); otherwise the removed symbols/data + // variables linger in the views until a manual refresh. + data->UpdateAnalysis(); + return count; +} + + +size_t DebuggerController::RemoveSymbolsForModule(const DebugModule& module) +{ + return RemoveSymbolsForModule(module.m_name.empty() ? module.m_short_name : module.m_name); +} + + +size_t DebuggerController::RemoveSymbolsForModule(const std::string& moduleName) +{ + std::lock_guard lock(m_loadedModuleSymbolsMutex); + for (auto it = m_loadedModuleSymbols.begin(); it != m_loadedModuleSymbols.end(); ++it) + { + if (DebugModule::IsSameBaseModule(it->first, moduleName)) + { + size_t count = UndefineTrackedSymbols(it->second); + m_loadedModuleSymbols.erase(it); + return count; + } + } + return 0; +} + + +size_t DebuggerController::RemoveAllLoadedSymbols(bool updateAnalysis) +{ + std::lock_guard lock(m_loadedModuleSymbolsMutex); + + auto data = GetData(); + if (!data) + { + // The view is already gone (e.g. shutdown); there is nothing to undefine, just drop our tracking. + m_loadedModuleSymbols.clear(); + return 0; + } + + // Remove every module's symbols inside one analysis-update window, for the same reason the load path + // batches them (see LoadSymbolsForAllModules). + auto id = data->BeginUndoActions(); + data->SetFunctionAnalysisUpdateDisabled(true); + size_t count = 0; + for (auto& [key, symbols] : m_loadedModuleSymbols) + count += RemoveTrackedSymbolsLocked(data, symbols); + data->SetFunctionAnalysisUpdateDisabled(false); + data->ForgetUndoActions(id); + // Flush the undefines to the views so they update without a manual refresh. The teardown caller + // (FinalizeTargetGoneCleanup) passes updateAnalysis = false: it is about to remove the debugger memory + // region, and scheduling an async analysis pass here could trigger a linear-view read against memory + // that is being torn down (see the ordering note in FinalizeTargetGoneCleanup). + if (updateAnalysis) + data->UpdateAnalysis(); + + m_loadedModuleSymbols.clear(); + return count; +} + + +std::vector DebuggerController::GetModulesWithLoadedSymbols() +{ + std::lock_guard lock(m_loadedModuleSymbolsMutex); + std::vector result; + result.reserve(m_loadedModuleSymbols.size()); + for (const auto& [key, symbols] : m_loadedModuleSymbols) + result.push_back(key); + return result; +} + + +size_t DebuggerController::GetLoadedSymbolCountForModule(const std::string& module) +{ + std::lock_guard lock(m_loadedModuleSymbolsMutex); + for (const auto& [key, symbols] : m_loadedModuleSymbols) + { + if (DebugModule::IsSameBaseModule(key, module)) + return symbols.size(); + } + return 0; +} + + std::vector DebuggerController::GetProcessList() { if (!m_adapter) diff --git a/core/debuggercontroller.h b/core/debuggercontroller.h index ad20450b..ee474462 100644 --- a/core/debuggercontroller.h +++ b/core/debuggercontroller.h @@ -241,6 +241,27 @@ namespace BinaryNinjaDebugger { void ProcessOneVariable(uint64_t address, Confidence> type, const std::string& name); void DefineVariablesRecursive(uint64_t address, Confidence> type); + // Tracks the symbols the debugger has added to the BinaryView from the debugger backend, keyed by + // the module's base file name. The value is the exact auto symbols that were defined, so they can + // later be removed -- either per module on user request or in bulk when the target is gone. We keep + // the Symbol objects (rather than just their addresses) because the linker can fold several distinct + // symbols onto the same address (e.g. identical .cold stubs), and GetSymbolByAddress only returns + // one of them. See LoadSymbolsForModule / RemoveSymbolsForModule. + std::map>> m_loadedModuleSymbols; + std::recursive_mutex m_loadedModuleSymbolsMutex; + // Undefine the given auto symbols and their data variables in a self-contained analysis-update / + // undo-action window. Returns the number of symbols processed. m_loadedModuleSymbolsMutex must be held. + size_t UndefineTrackedSymbols(const std::vector>& symbols); + // Define / undefine a module's symbols directly in the BinaryView. The caller must hold + // m_loadedModuleSymbolsMutex, have already disabled function-analysis updates, and manage the + // undo-action scope. Applying every module inside one such shared window -- rather than opening one + // per module -- lets a single analysis pass re-resolve every module's references (e.g. IAT pointers + // to freshly-named API functions); a per-module window would let each module's async re-analysis be + // superseded by the next module's disable, so only the last module loaded would resolve. See #210. + size_t ApplyModuleSymbolsLocked( + BinaryViewRef data, const DebugModule& module, const std::vector& symbols); + size_t RemoveTrackedSymbolsLocked(BinaryViewRef data, const std::vector>& symbols); + void ApplyBreakpoints(); std::string m_lastAdapterName; @@ -446,6 +467,32 @@ namespace BinaryNinjaDebugger { // memory map std::vector GetMemoryMap(); + // symbols (read from the debugger backend on demand) + // Read the symbols that the debugger backend knows about for the given module and add them to the + // BinaryView as auto symbols (along with a data variable at each address so they are rendered). + // By default no backend symbols are loaded; the user requests this explicitly per module. The + // added symbols are tracked internally so they can be removed later. Returns the number of + // symbols added, or 0 if the adapter does not support reading symbols or the module is unknown. + // Loading the same module again is idempotent: any symbols previously loaded for it are removed + // first, so no duplicates are created. + size_t LoadSymbolsForModule(const DebugModule& module); + size_t LoadSymbolsForModule(const std::string& module); + // Load the backend symbols for every currently-loaded module. Returns the total number added. + size_t LoadSymbolsForAllModules(); + // Remove the backend symbols previously added for the given module. Returns the number removed. + size_t RemoveSymbolsForModule(const DebugModule& module); + size_t RemoveSymbolsForModule(const std::string& module); + // Remove every backend symbol the debugger has added. Returns the number removed. updateAnalysis + // controls whether an async analysis update is scheduled afterwards to refresh the views; the + // target-gone teardown path passes false because it is about to remove the debugger memory region + // and must not schedule a pass that could read from it mid-teardown. + size_t RemoveAllLoadedSymbols(bool updateAnalysis = true); + // The base names of the modules for which backend symbols have been loaded. + std::vector GetModulesWithLoadedSymbols(); + // The number of backend symbols currently loaded for the given module (0 if none). The module may be + // given as either its base name or its full path. + size_t GetLoadedSymbolCountForModule(const std::string& module); + // rebasing // Note: Returns true immediately in UI mode (rebase completes asynchronously via UI callback) bool RebaseToRemoteBase(); diff --git a/core/ffi.cpp b/core/ffi.cpp index 40b596a6..516c0adb 100644 --- a/core/ffi.cpp +++ b/core/ffi.cpp @@ -376,6 +376,50 @@ void BNDebuggerFreeMemoryRegions(BNDebugMemoryRegion* regions, size_t count) } +size_t BNDebuggerLoadSymbolsForModule(BNDebuggerController* controller, const char* module) +{ + return controller->object->LoadSymbolsForModule(std::string(module)); +} + + +size_t BNDebuggerLoadSymbolsForAllModules(BNDebuggerController* controller) +{ + return controller->object->LoadSymbolsForAllModules(); +} + + +size_t BNDebuggerRemoveSymbolsForModule(BNDebuggerController* controller, const char* module) +{ + return controller->object->RemoveSymbolsForModule(std::string(module)); +} + + +size_t BNDebuggerRemoveAllLoadedSymbols(BNDebuggerController* controller) +{ + return controller->object->RemoveAllLoadedSymbols(); +} + + +char** BNDebuggerGetModulesWithLoadedSymbols(BNDebuggerController* controller, size_t* count) +{ + std::vector modules = controller->object->GetModulesWithLoadedSymbols(); + *count = modules.size(); + + std::vector cstrings; + cstrings.reserve(modules.size()); + for (auto& str : modules) + cstrings.push_back(str.c_str()); + + return BNDebuggerAllocStringList(cstrings.data(), *count); +} + + +size_t BNDebuggerGetLoadedSymbolCountForModule(BNDebuggerController* controller, const char* module) +{ + return controller->object->GetLoadedSymbolCountForModule(std::string(module)); +} + + BNDebugRegister* BNDebuggerGetRegisters(BNDebuggerController* controller, size_t* size) { std::vector registers = controller->object->GetAllRegisters(); diff --git a/docs/guide/index.md b/docs/guide/index.md index 1a82c543..78e2209c 100644 --- a/docs/guide/index.md +++ b/docs/guide/index.md @@ -247,12 +247,31 @@ The context menu offers to suspend and resume each thread individually. A conven ![](../../img/debugger/modulewidget.png) The module widget shows the start/end address, size, name, and path information of the target's modules. +The `Symbols` column shows how many backend symbols have been loaded for each module (blank when none; see below). Double-clicking the addresses navigates to the address. Note: on macOS 13, the size of system dylib are calculated wrong. The bizarrely huge size is caused by dyld_shared_cache on macOS, which will be addressed in the future. The size of the main executable is still calculated correctly. +##### Loading Symbols from the Debugger Backend + +The debugger backend (LLDB, DbgEng, Windows Native) often knows about symbols that Binary Ninja's static analysis does not — for example the exported functions of system libraries like `kernel32.dll` or `libsystem_c.dylib`. Loading them is useful when, for instance, a register points to a Windows API function that is not present in the analyzed binary. See [issue #210](https://github.com/Vector35/debugger/issues/210). + +By default, no backend symbols are loaded. You can load them on demand, per module, from the Module Widget's context menu: + +![](../../img/debugger/module_load_symbols.png) + +- `Load Symbols` reads all the symbols the backend knows about for the selected module and adds them to the debugger's Binary View. When the module already has symbols loaded, this action instead reads `Reload Symbols`. +- `Remove Symbols` removes the backend symbols previously loaded for the selected module. +- `Load Symbols (All Modules)` and `Remove All Symbols` do the same for every loaded module at once. + +The symbols are added as *auto* symbols, so they never override your own (user-defined) symbols. The debugger tracks exactly which symbols it added, so removing them is clean, and they are removed automatically when the target exits or you detach. Once loaded, they participate in analysis and annotation like any other symbol: + +![](../../img/debugger/loaded_module_symbols.png) + +The same operations are available from the [Python API](#reading-symbols-from-the-backend). + #### Debugger Info Widget @@ -752,10 +771,36 @@ details - `process save-core ` +### Reading Symbols from the Backend + +The debugger can [read the symbols](https://github.com/Vector35/debugger/issues/210) that the backend knows about for a loaded module and add them to the Binary View on demand. For the UI, see [Loading Symbols from the Debugger Backend](#loading-symbols-from-the-debugger-backend). From the Python console: + +```python +from binaryninja.debugger import DebuggerController + +controller = DebuggerController(bv) +# ... launch or attach, and stop the target ... + +# Load all backend symbols for a module (identified by short name or full path). +# Returns the number of symbols added. Loading the same module again is idempotent. +count = controller.load_symbols_for_module("libsystem_c.dylib") + +# Load the symbols for every currently-loaded module. +controller.load_symbols_for_all_modules() + +# The base names of the modules that currently have backend symbols loaded. +print(controller.modules_with_loaded_symbols) + +# Remove the symbols again (per module, or all at once). +controller.remove_symbols_for_module("libsystem_c.dylib") +controller.remove_all_loaded_symbols() +``` + +The symbols are added as auto symbols and are tracked internally, so they can be removed cleanly and are cleared automatically when the target exits or you detach. + ### Listing Symbol At/Near an Address -Before we have the capacity to [read symbols](https://github.com/Vector35/debugger/issues/210) from the backend, as a -workaround, we can check the symbols at or near a specific address. +To check the symbol at or near a specific address without loading a module's symbols into the Binary View, you can run a backend command directly. #### WinDbg/DbgEng diff --git a/docs/img/debugger/loaded_module_symbols.png b/docs/img/debugger/loaded_module_symbols.png new file mode 100644 index 00000000..7942fe51 Binary files /dev/null and b/docs/img/debugger/loaded_module_symbols.png differ diff --git a/docs/img/debugger/module_load_symbols.png b/docs/img/debugger/module_load_symbols.png new file mode 100644 index 00000000..07e1b1c5 Binary files /dev/null and b/docs/img/debugger/module_load_symbols.png differ diff --git a/docs/img/debugger/modulewidget.png b/docs/img/debugger/modulewidget.png index 2a1ff7ec..cc939cde 100644 Binary files a/docs/img/debugger/modulewidget.png and b/docs/img/debugger/modulewidget.png differ diff --git a/test/debugger_test.py b/test/debugger_test.py index b171b334..823d66e6 100644 --- a/test/debugger_test.py +++ b/test/debugger_test.py @@ -138,6 +138,128 @@ def test_debug_shared_library(self): if dbg.connected: dbg.quit_and_wait() + def test_load_module_symbols(self): + # Load symbols from the debugger backend on demand, then remove them, checking that the number + # of symbols in the BinaryView increases when they are loaded and returns to the original value + # when they are removed (i.e. nothing is left behind). See + # https://github.com/Vector35/debugger/issues/210 + fpath = name_to_fpath('helloworld', self.arch) + bv = load(fpath) + dbg = self.create_debugger(bv) + self.assertNotIn(dbg.launch_and_wait(), [DebugStopReason.ProcessExited, DebugStopReason.InternalError]) + try: + self.assertGreater(len(dbg.modules), 0) + # No backend symbols are loaded by default. + self.assertEqual(len(dbg.modules_with_loaded_symbols), 0) + + def symbol_count(): + return len(dbg.data.get_symbols()) + + before = symbol_count() + # Track the feature's *own* data variables by address. Loading symbols defines a data variable + # at each symbol address; pinning those down lets the removal check ignore data variables that + # appear for reasons unrelated to this feature -- e.g. the null-pointer data variable at 0x0 that + # stack-variable annotation creates, or reference-site variables that analysis materializes -- + # which otherwise make the global data-variable count an unstable, environment-dependent oracle. + sym_addrs_before = {s.address for s in dbg.data.get_symbols()} + data_var_addrs_before = {v.address for v in dbg.data.data_vars.values()} + + def describe(addrs): + # Render an address set for an assertion failure message: each address with its data + # variable type (if any) and the symbols defined there. + lines = [] + for a in sorted(addrs): + dv = dbg.data.data_vars.get(a) + type_desc = repr(dv.type) if dv is not None else None + syms = [(s.type.name, s.name) for s in dbg.data.get_symbols(a, 1)] + lines.append(f" {a:#x} type={type_desc} symbols={syms}") + return "\n".join(lines) + + # We do not know up front which module the backend has symbols for, so try each one until a + # module actually contributes symbols. Skip the main executable so the symbols are added into + # otherwise-unannotated address space, making the add/remove counts unambiguous. + main_path = os.path.realpath(fpath) + loaded_module = None + added = 0 + for m in dbg.modules: + name = m.name or m.short_name + if not name: + continue + if os.path.realpath(name) == main_path: + continue + count = dbg.load_symbols_for_module(name) + if count > 0: + loaded_module = name + added = count + break + + if loaded_module is None: + self.skipTest('no non-main module reported backend symbols for this adapter') + + self.assertGreater(added, 0) + self.assertEqual(len(dbg.modules_with_loaded_symbols), 1) + # The per-module count (surfaced in the Modules widget's Symbols column) matches what was added. + self.assertEqual(dbg.loaded_symbol_count_for_module(loaded_module), added) + + # The addresses where this load introduced symbols. The feature defines one data variable per + # symbol address; restrict to addresses that did not already have a data variable so the checks + # below concern only what the feature itself created. + loaded_addrs = {s.address for s in dbg.data.get_symbols()} - sym_addrs_before + feature_dv_addrs = loaded_addrs - data_var_addrs_before + self.assertGreater(len(feature_dv_addrs), 0) + + # Loading symbols increases the number of symbols in the BinaryView, and each gets a data + # variable so it renders in the views. + after_load = symbol_count() + self.assertGreater(after_load, before) + current_dv_addrs = {v.address for v in dbg.data.data_vars.values()} + self.assertTrue(feature_dv_addrs.issubset(current_dv_addrs), + "loaded symbols did not all get a data variable:\n" + + describe(feature_dv_addrs - current_dv_addrs)) + + # Removing the symbols must undefine every data variable the feature created, leaving nothing + # behind. Data variables that exist for unrelated reasons (stack-variable annotation, analysis + # reference sites, ...) are ignored by construction. + removed = dbg.remove_symbols_for_module(loaded_module) + self.assertEqual(removed, added) + self.assertLess(symbol_count(), after_load) + # Every symbol the feature added must be gone. As with data variables, check the feature's own + # addresses rather than the global symbol count: that count drifts with symbols created for + # unrelated reasons (analysis, stack-variable annotation) and with background analysis that is + # still settling when the baseline is captured, which is stable on some platforms but not others. + leaked_syms = {a for a in loaded_addrs if dbg.data.get_symbols(a, 1)} + self.assertEqual(leaked_syms, set(), + "symbols the feature added were not removed:\n" + describe(leaked_syms)) + leaked = feature_dv_addrs & {v.address for v in dbg.data.data_vars.values()} + self.assertEqual(leaked, set(), + "data variables the feature created were not removed:\n" + describe(leaked)) + self.assertEqual(len(dbg.modules_with_loaded_symbols), 0) + self.assertEqual(dbg.loaded_symbol_count_for_module(loaded_module), 0) + + # Regression: a load/remove/load cycle must recreate the data variables. Removal undefines them + # without blacklisting their addresses; if it blacklisted them, this re-load's auto data + # variables would be suppressed and the reloaded symbols would not render in the linear view. + self.assertGreater(dbg.load_symbols_for_module(loaded_module), 0) + # A correct re-load recreates a data variable at each of the feature's addresses; the blacklist + # bug would leave them undefined. + reloaded_dv_addrs = {v.address for v in dbg.data.data_vars.values()} + self.assertTrue(feature_dv_addrs.issubset(reloaded_dv_addrs), + "re-load did not recreate the feature's data variables:\n" + + describe(feature_dv_addrs - reloaded_dv_addrs)) + self.assertEqual(dbg.loaded_symbol_count_for_module(loaded_module), added) + + # Loading the same module twice must not register it twice or accumulate duplicate tracking. + # This is checked via the debugger's own tracking rather than the BinaryView's global symbol + # count: some backends (e.g. DbgEng) resolve a module's symbols lazily and may enumerate them + # slightly differently across calls, so the global count is not a stable idempotency oracle. + self.assertGreater(dbg.load_symbols_for_module(loaded_module), 0) + self.assertEqual(len(dbg.modules_with_loaded_symbols), 1) + self.assertGreater(dbg.remove_symbols_for_module(loaded_module), 0) + self.assertEqual(len(dbg.modules_with_loaded_symbols), 0) + finally: + if dbg.connected: + dbg.quit_and_wait() + def test_return_code(self): # return code tests fpath = name_to_fpath('exitcode', self.arch) diff --git a/ui/moduleswidget.cpp b/ui/moduleswidget.cpp index 28c77e32..035a6543 100644 --- a/ui/moduleswidget.cpp +++ b/ui/moduleswidget.cpp @@ -28,8 +28,8 @@ using namespace std; constexpr int SortFilterRole = Qt::UserRole + 1; -ModuleItem::ModuleItem(uint64_t address, size_t size, std::string name, std::string path) : - m_address(address), m_size(size), m_name(name), m_path(path) +ModuleItem::ModuleItem(uint64_t address, size_t size, std::string name, std::string path, size_t symbolCount) : + m_address(address), m_size(size), m_name(name), m_path(path), m_symbolCount(symbolCount) {} @@ -138,6 +138,19 @@ QVariant DebugModulesListModel::data(const QModelIndex& index, int role) const return QVariant(text); } + case DebugModulesListModel::SymbolsColumn: + { + // Show how many backend symbols are loaded for the module, e.g. "1024 symbols"; blank when none. + QString text; + if (item->symbolCount() > 0) + text = QString("%1 symbol%2") + .arg((qulonglong)item->symbolCount()) + .arg(item->symbolCount() == 1 ? "" : "s"); + if (role == Qt::SizeHintRole) + return QVariant((qulonglong)text.size()); + + return QVariant(text); + } case DebugModulesListModel::PathColumn: { QString text = QString::fromStdString(item->path()); @@ -169,6 +182,8 @@ QVariant DebugModulesListModel::headerData(int column, Qt::Orientation orientati return "Size"; case DebugModulesListModel::NameColumn: return "Name"; + case DebugModulesListModel::SymbolsColumn: + return "Symbols"; case DebugModulesListModel::PathColumn: return "Path"; } @@ -176,13 +191,25 @@ QVariant DebugModulesListModel::headerData(int column, Qt::Orientation orientati } -void DebugModulesListModel::updateRows(std::vector newModules) +void DebugModulesListModel::updateRows( + std::vector newModules, const std::map& moduleSymbolCounts) { beginResetModel(); std::vector newRows; for (const DebugModule& module : newModules) { - newRows.emplace_back(module.m_address, module.m_size, module.m_short_name, module.m_name); + uint64_t symbolCount = 0; + for (const auto& [name, count] : moduleSymbolCounts) + { + // Note: DebugModule::IsSameBaseModule is declared in the API but not linked here, so compare + // via the exported FFI helper, which matches the base file name case-insensitively. + if (BNDebuggerIsSameBaseModule(module.m_name.c_str(), name.c_str())) + { + symbolCount = count; + break; + } + } + newRows.emplace_back(module.m_address, module.m_size, module.m_short_name, module.m_name, symbolCount); } std::sort(newRows.begin(), newRows.end(), [=](const ModuleItem& a, const ModuleItem& b) { @@ -234,6 +261,7 @@ void DebugModulesItemDelegate::paint( painter->drawText(textRect, data.toString()); break; case DebugModulesListModel::NameColumn: + case DebugModulesListModel::SymbolsColumn: case DebugModulesListModel::PathColumn: { painter->setPen(option.palette.color(QPalette::WindowText).rgba()); @@ -335,6 +363,28 @@ DebugModulesWidget::DebugModulesWidget(ViewFrame* view, BinaryViewRef data) : QT m_menu.addAction("Copy All", "Options", MENU_ORDER_NORMAL); m_actionHandler.bindAction("Copy All", UIAction([&]() { copyAll(); }, [&]() { return canCopyAll(); })); + UIAction::registerAction("Load Symbols"); + m_menu.addAction("Load Symbols", "Symbols", MENU_ORDER_NORMAL); + m_actionHandler.bindAction( + "Load Symbols", UIAction([&]() { loadSymbols(); }, [&]() { return canLoadSymbols(); })); + m_actionHandler.setActionDisplayName( + "Load Symbols", [&]() { return selectedModuleSymbolsLoaded() ? "Reload Symbols" : "Load Symbols"; }); + + UIAction::registerAction("Remove Symbols"); + m_menu.addAction("Remove Symbols", "Symbols", MENU_ORDER_NORMAL); + m_actionHandler.bindAction( + "Remove Symbols", UIAction([&]() { removeSymbols(); }, [&]() { return canLoadSymbols(); })); + + UIAction::registerAction("Load Symbols (All Modules)"); + m_menu.addAction("Load Symbols (All Modules)", "Symbols", MENU_ORDER_NORMAL); + m_actionHandler.bindAction( + "Load Symbols (All Modules)", UIAction([&]() { loadAllSymbols(); }, [&]() { return canLoadAllSymbols(); })); + + UIAction::registerAction("Remove All Symbols"); + m_menu.addAction("Remove All Symbols", "Symbols", MENU_ORDER_NORMAL); + m_actionHandler.bindAction( + "Remove All Symbols", UIAction([&]() { removeAllSymbols(); }, [&]() { return canLoadAllSymbols(); })); + connect(this, &QTableView::doubleClicked, this, &DebugModulesWidget::onDoubleClicked); connect(this, &DebugModulesWidget::debuggerEvent, this, &DebugModulesWidget::onDebuggerEvent); @@ -358,13 +408,18 @@ void DebugModulesWidget::updateColumnWidths() resizeColumnToContents(DebugModulesListModel::EndAddressColumn); resizeColumnToContents(DebugModulesListModel::SizeColumn); resizeColumnToContents(DebugModulesListModel::NameColumn); + resizeColumnToContents(DebugModulesListModel::SymbolsColumn); resizeColumnToContents(DebugModulesListModel::PathColumn); } void DebugModulesWidget::notifyModulesChanged(std::vector modules) { - m_model->updateRows(modules); + std::map moduleSymbolCounts; + for (const std::string& name : m_controller->GetModulesWithLoadedSymbols()) + moduleSymbolCounts[name] = m_controller->GetLoadedSymbolCountForModule(name); + + m_model->updateRows(modules, moduleSymbolCounts); updateColumnWidths(); } @@ -472,6 +527,82 @@ bool DebugModulesWidget::canCopyAll() } +bool DebugModulesWidget::canLoadSymbols() +{ + if (!m_controller->IsConnected()) + return false; + + QModelIndexList sel = selectionModel()->selectedIndexes(); + return !sel.empty(); +} + + +bool DebugModulesWidget::canLoadAllSymbols() +{ + return m_controller->IsConnected(); +} + + +bool DebugModulesWidget::selectedModuleSymbolsLoaded() +{ + QModelIndexList sel = selectionModel()->selectedIndexes(); + if (sel.empty()) + return false; + + auto sourceIndex = m_filter->mapToSource(sel[0]); + if (!sourceIndex.isValid()) + return false; + + return m_model->getRow(sourceIndex.row()).symbolsLoaded(); +} + + +void DebugModulesWidget::loadSymbols() +{ + QModelIndexList sel = selectionModel()->selectedIndexes(); + if (sel.empty()) + return; + + auto sourceIndex = m_filter->mapToSource(sel[0]); + if (!sourceIndex.isValid()) + return; + + auto module = m_model->getRow(sourceIndex.row()); + m_controller->LoadSymbolsForModule(module.path()); + updateContent(); +} + + +void DebugModulesWidget::removeSymbols() +{ + QModelIndexList sel = selectionModel()->selectedIndexes(); + if (sel.empty()) + return; + + auto sourceIndex = m_filter->mapToSource(sel[0]); + if (!sourceIndex.isValid()) + return; + + auto module = m_model->getRow(sourceIndex.row()); + m_controller->RemoveSymbolsForModule(module.path()); + updateContent(); +} + + +void DebugModulesWidget::loadAllSymbols() +{ + m_controller->LoadSymbolsForAllModules(); + updateContent(); +} + + +void DebugModulesWidget::removeAllSymbols() +{ + m_controller->RemoveAllLoadedSymbols(); + updateContent(); +} + + void DebugModulesWidget::copy() { QModelIndexList sel = selectionModel()->selectedIndexes(); diff --git a/ui/moduleswidget.h b/ui/moduleswidget.h index c88b06c6..e8a13895 100644 --- a/ui/moduleswidget.h +++ b/ui/moduleswidget.h @@ -16,6 +16,7 @@ limitations under the License. #pragma once +#include #include #include #include @@ -41,14 +42,18 @@ class ModuleItem size_t m_size; std::string m_name; std::string m_path; + size_t m_symbolCount; public: - ModuleItem(uint64_t address, size_t size, std::string name, std::string path); + ModuleItem(uint64_t address, size_t size, std::string name, std::string path, size_t symbolCount = 0); uint64_t address() const { return m_address; } uint64_t endAddress() const { return m_address + m_size; } size_t size() const { return m_size; } std::string name() const { return m_name; } std::string path() const { return m_path; } + // Number of backend symbols loaded for this module (0 if none). + size_t symbolCount() const { return m_symbolCount; } + bool symbolsLoaded() const { return m_symbolCount > 0; } bool operator==(const ModuleItem& other) const; bool operator!=(const ModuleItem& other) const; bool operator<(const ModuleItem& other) const; @@ -73,6 +78,7 @@ class DebugModulesListModel : public QAbstractTableModel EndAddressColumn, SizeColumn, NameColumn, + SymbolsColumn, PathColumn, }; @@ -89,12 +95,13 @@ class DebugModulesListModel : public QAbstractTableModel virtual int columnCount(const QModelIndex& parent = QModelIndex()) const override { (void)parent; - return 5; + return 6; } ModuleItem 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 newModules); + void updateRows( + std::vector newModules, const std::map& moduleSymbolCounts); }; @@ -154,6 +161,10 @@ class DebugModulesWidget : public QTableView, public FilterTarget bool canCopy(); bool canCopyAll(); + bool canLoadSymbols(); + bool canLoadAllSymbols(); + // Whether the currently-selected module already has backend symbols loaded. + bool selectedModuleSymbolsLoaded(); virtual void setFilter(const std::string& filter, FilterOptions options) override; virtual void scrollToFirstItem() override; @@ -179,6 +190,10 @@ private slots: void jumpToEnd(); void copy(); void copyAll(); + void loadSymbols(); + void removeSymbols(); + void loadAllSymbols(); + void removeAllSymbols(); void onDoubleClicked(); public slots: