From d19f5e85ee26c3d6604798d30ab821d15731cada Mon Sep 17 00:00:00 2001 From: KyleMiles Date: Wed, 19 May 2021 20:40:24 -0400 Subject: DebugInfo - plugable debug information importers - C++, Rust, and Python APIs --- python/__init__.py | 1 + python/binaryview.py | 29 ++- python/debuginfo.py | 426 ++++++++++++++++++++++++++++++++++++++++ python/examples/debug_info.py | 269 +++++++++++++++++++++++++ python/examples/test_debug_info | Bin 0 -> 14336 bytes 5 files changed, 724 insertions(+), 1 deletion(-) create mode 100644 python/debuginfo.py create mode 100755 python/examples/debug_info.py create mode 100755 python/examples/test_debug_info (limited to 'python') diff --git a/python/__init__.py b/python/__init__.py index 1b199c9a..4f8a373f 100644 --- a/python/__init__.py +++ b/python/__init__.py @@ -36,6 +36,7 @@ from binaryninja.databuffer import * from binaryninja.filemetadata import * from binaryninja.fileaccessor import * from binaryninja.binaryview import * +from binaryninja.debuginfo import * from binaryninja.transform import * from binaryninja.architecture import * from binaryninja.basicblock import * diff --git a/python/binaryview.py b/python/binaryview.py index 30b763a0..ae88b4c0 100644 --- a/python/binaryview.py +++ b/python/binaryview.py @@ -28,6 +28,7 @@ import abc import numbers import json import inspect +from typing import Union from collections import defaultdict, OrderedDict @@ -500,6 +501,15 @@ class DataVariable(object): self._view = value +class DataVariableAndName(DataVariable): + def __init__(self, addr: int, var_type: types.Type, var_name: str, auto_discovered: bool, view: "BinaryView" = None) -> None: + super(DataVariableAndName, self).__init__(addr, var_type, auto_discovered, view) + self.name = var_name + + def __repr__(self) -> str: + return "" % (self.address, str(self.type), self.name) + + class BinaryDataNotificationCallbacks(object): def __init__(self, view, notify): self._view = view @@ -1483,7 +1493,6 @@ class BinaryView(object): finally: core.BNFreeFunctionList(funcs, count.value) - def __getitem__(self, i): if isinstance(i, tuple): result = bytes() @@ -6251,6 +6260,24 @@ class BinaryView(object): """ core.BNSetGlobalCommentForAddress(self.handle, addr, comment) + @property + def debug_info(self) -> "binaryninja.debuginfo.DebugInfo": + """The current debug info object for this binary view""" + return binaryninja.debuginfo.DebugInfo(core.BNNewDebugInfoReference(core.BNGetDebugInfo(self.handle))) + + @debug_info.setter + def debug_info(self, value: "binaryninja.debuginfo.DebugInfo") -> Union[None, 'NotImplemented']: + """Sets the debug info for the current binary view""" + if not isinstance(value, binaryninja.debuginfo.DebugInfo): + return NotImplemented + core.BNSetDebugInfo(self.handle, value.handle) + + def apply_debug_info(self, value: "binaryninja.debuginfo.DebugInfo") -> Union[None, 'NotImplemented']: + """Sets the debug info and applies its contents to the current binary view""" + if not isinstance(value, binaryninja.debuginfo.DebugInfo): + return NotImplemented + core.BNApplyDebugInfo(self.handle, value.handle) + def query_metadata(self, key): """ `query_metadata` retrieves a metadata associated with the given key stored in the current BinaryView. diff --git a/python/debuginfo.py b/python/debuginfo.py new file mode 100644 index 00000000..15ac4efb --- /dev/null +++ b/python/debuginfo.py @@ -0,0 +1,426 @@ +# Copyright (c) 2015-2021 Vector 35 Inc +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to +# deal in the Software without restriction, including without limitation the +# rights to use, copy, modify, merge, publish, distribute, sublicense, and/or +# sell copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS +# IN THE SOFTWARE. + +import ctypes +from typing import Optional, List, Iterator, Callable, Tuple +import traceback + +# Binary Ninja components +import binaryninja +from binaryninja import _binaryninjacore as core +from binaryninja import callingconvention +from binaryninja import platform +from binaryninja import types +from binaryninja import log + + +_debug_info_parsers = {} + + +class _DebugInfoParserMetaClass(type): + @property + def list(self) -> List["DebugInfoParser"]: + """List all debug-info parsers (read-only)""" + binaryninja._init_plugins() + count = ctypes.c_ulonglong() + parsers = core.BNGetDebugInfoParsers(count) + result = [] + for i in range(0, count.value): + result.append(DebugInfoParser(core.BNNewDebugInfoParserReference(parsers[i]))) + core.BNFreeDebugInfoParserList(parsers, count.value) + return result + + def __iter__(self) -> Iterator["DebugInfoParser"]: + """Generator of all debug-info parsers""" + binaryninja._init_plugins() + count = ctypes.c_ulonglong() + parsers = core.BNGetDebugInfoParsers(count) + try: + for i in range(0, count.value): + yield DebugInfoParser(core.BNNewDebugInfoParserReference(parsers[i])) + finally: + core.BNFreeDebugInfoParserList(parsers, count.value) + + def __getitem__(cls, value: str) -> "DebugInfoParser": + """Returns debug info parser of the given name, if it exists""" + binaryninja._init_plugins() + parser = core.BNGetDebugInfoParserByName(str(value)) + if parser is None: + raise KeyError(f"'{str(value)}' is not a valid debug-info parser") + return DebugInfoParser(core.BNNewDebugInfoParserReference(parser)) + + @staticmethod + def get_parsers_for_view(view: binaryninja.binaryview.BinaryView) -> List["DebugInfoParser"]: + """Returns a list of debug-info parsers that are valid for the provided binary view""" + binaryninja._init_plugins() + + count = ctypes.c_ulonglong() + parsers = core.BNGetDebugInfoParsersForView(view.handle, count) + result = [] + for i in range(0, count.value): + result.append(DebugInfoParser(core.BNNewDebugInfoParserReference(parsers[i]))) + core.BNFreeDebugInfoParserList(parsers, count.value) + return result + + @staticmethod + def _is_valid(view: core.BNBinaryView, callback: Callable[[binaryninja.binaryview.BinaryView], bool]) -> bool: + try: + file_metadata = binaryninja.filemetadata.FileMetadata(handle = core.BNGetFileForView(view)) + view_obj = binaryninja.binaryview.BinaryView(file_metadata = file_metadata, handle = core.BNNewViewReference(view)) + return callback(view_obj) + except: + log.log_error(traceback.format_exc()) + + @staticmethod + def _parse_info(debug_info: core.BNDebugInfo, view: core.BNBinaryView, callback: Callable[["DebugInfo", binaryninja.binaryview.BinaryView], None]) -> None: + try: + file_metadata = binaryninja.filemetadata.FileMetadata(handle = core.BNGetFileForView(view)) + view_obj = binaryninja.binaryview.BinaryView(file_metadata = file_metadata, handle = core.BNNewViewReference(view)) + callback(DebugInfo(core.BNNewDebugInfoReference(debug_info)), view_obj) + except: + log.log_error(traceback.format_exc()) + + @classmethod + def register(cls, name: str, is_valid: Callable[[binaryninja.binaryview.BinaryView], bool], parse_info: Callable[["DebugInfo", binaryninja.binaryview.BinaryView], None]) -> "DebugInfoParser": + """Registers a DebugInfoParser. See ``binaryninja.debuginfo.DebugInfoParser`` for more details.""" + binaryninja._init_plugins() + + is_valid_cb = ctypes.CFUNCTYPE(ctypes.c_bool, ctypes.c_void_p, ctypes.POINTER(core.BNBinaryView))(lambda ctxt, view: cls._is_valid(view, is_valid)) + parse_info_cb = ctypes.CFUNCTYPE(None, ctypes.c_void_p, ctypes.POINTER(core.BNDebugInfo), ctypes.POINTER(core.BNBinaryView))(lambda ctxt, debug_info, view: cls._parse_info(debug_info, view, parse_info)) + + # Don't let our callbacks get garbage collected + global _debug_info_parsers + _debug_info_parsers[len(_debug_info_parsers)] = (is_valid_cb, parse_info_cb) + + return DebugInfoParser(core.BNNewDebugInfoParserReference(core.BNRegisterDebugInfoParser(name, is_valid_cb, parse_info_cb, None))) + + +class DebugInfoParser(object, metaclass=_DebugInfoParserMetaClass): + """ + ``DebugInfoParser``s represent the registered parsers and providers of debug information to Binary Ninja. + + The debug information is used by Binary Ninja as ground-truth information about the attributes of functions, + types, and variables that Binary Ninja's analysis pipeline would otherwise work to deduce. By providing + debug info, Binary Ninja's output can be generated quicker, more accurately, and more completely. + + A DebugInfoParser consists of: + 1. A name + 2. An ``is_valid`` function which takes a BV and returns a bool + 3. A ``parse`` function which takes a ``DebugInfo`` object and uses the member functions ``add_type``, ``add_function``, and ``add_data_variable`` to populate all the info it can. + And finally calling ``bn.debuginfo.DebugInfoParser.register`` to register it with the core. + + Here's a minimal, complete example boilerplate-plugin: + ``` + import binaryninja as bn + + def is_valid(bv: bn.binaryview.BinaryView) -> bool: + return bv.view_type == "Raw" + + def parse_info(debug_info: bn.debuginfo.DebugInfo, bv: bn.binaryview.BinaryView) -> None: + debug_info.add_type("name", bn.types.Type.int(4, True)) + debug_info.add_data_variable(0x1234, bn.types.Type.int(4, True), "name") + + function_info = bn.debuginfo.DebugFunctionInfo(0xdead1337, "short_name", "full_name", "raw_name", bn.types.Type.int(4, False), []) + debug_info.add_function(function_info) + + bn.debuginfo.DebugInfoParser.register("debug info parser", is_valid, parse_info) + ``` + + ``DebugInfo`` can then be automatically applied to valid binary views (via the "Parse and Apply Debug Info" setting), or manually fetched/applied as bellow: + ``` + valid_parsers = bn.debuginfo.DebugInfoParser.get_parsers_for_view(bv) + parser = valid_parsers[0] + debug_info = parser.parse_debug_info(bv) + bv.apply_debug_info(debug_info) + ``` + + Multiple debug-info parsers can manually contribute debug info for a binary view by simply calling ``parse_debug_info`` with the + ``DebugInfo`` object just returned. This is automatic when opening a binary view with multiple valid debug info parsers. If you + wish to set the debug info for a binary view without applying it as well, you can call ``binaryninja.binaryview.BinaryView.set_debug_info``. + """ + def __init__(self, handle: core.BNDebugInfoParser) -> None: + self.handle = core.handle_of_type(handle, core.BNDebugInfoParser) + + def __del__(self) -> None: + core.BNFreeDebugInfoParserReference(self.handle) + + def __repr__(self) -> str: + return f"" + + def __eq__(self, other: "DebugInfoParser") -> bool: + if not isinstance(other, self.__class__): + return NotImplemented + return ctypes.addressof(self.handle.contents) == ctypes.addressof(other.handle.contents) + + def __ne__(self, other: "DebugInfoParser") -> bool: + if not isinstance(other, self.__class__): + return NotImplemented + return not (self == other) + + def __hash__(self) -> int: + return hash(ctypes.addressof(self.handle.contents)) + + @property + def name(self) -> str: + """Debug-info parser's name (read-only)""" + return core.BNGetDebugInfoParserName(self.handle) + + def is_valid_for_view(self, view: binaryninja.binaryview.BinaryView) -> bool: + """Returns whether this debug-info parser is valid for the provided binary view""" + return core.BNIsDebugInfoParserValidForView(self.handle, view.handle) + + def parse_debug_info(self, view: binaryninja.binaryview.BinaryView, debug_info: Optional["DebugInfo"] = None) -> "DebugInfo": + """Returns a ``DebugInfo`` object populated with debug info by this debug-info parser. Only provide a ``DebugInfo`` object if you wish to append to the existing debug info""" + if isinstance(debug_info, DebugInfo): + return DebugInfo(core.BNNewDebugInfoReference(core.BNParseDebugInfo(self.handle, view.handle, debug_info.handle))) + else: + return DebugInfo(core.BNParseDebugInfo(self.handle, view.handle, None)) + + +class DebugFunctionInfo(object): + """ + ``DebugFunctionInfo`` collates ground-truth function-external attributes for use in BinaryNinja's internal analysis. + + When contributing function info, provide only what you know - BinaryNinja will figure out everything else that it can, as it usually does. + + Functions will not be created if an address is not provided, but will be able to be queried from debug info for later user analysis. + """ + def __init__(self, address: Optional[int] = 0, short_name: Optional[str] = None, full_name: Optional[str] = None, raw_name: Optional[str] = None, return_type: Optional[types.Type] = None, parameters: Optional[List[Tuple[str, types.Type]]] = None, variable_parameters: Optional[bool] = False, calling_convention: Optional[callingconvention.CallingConvention] = None, platform: Optional[platform.Platform] = None) -> None: + self._short_name = short_name + self._full_name = full_name + self._raw_name = raw_name + self._address = address + self._return_type = return_type + self._parameters = parameters + self._variable_parameters = variable_parameters + self._calling_convention = calling_convention + self._platform = platform + + def __repr__(self) -> str: + suffix = f"@{hex(self._address)}>" if self._address != 0 else ">" + if self._short_name is not None: + return f" Optional[str]: + return self._short_name + + @property + def full_name(self) -> Optional[str]: + return self._full_name + + @property + def raw_name(self) -> Optional[str]: + return self._raw_name + + @property + def address(self) -> Optional[int]: + return self._address + + @property + def return_type(self) -> Optional[int]: + return self._return_type + + @property + def parameters(self) -> Optional[List[Tuple[str, types.Type]]]: + return self._parameters + + @property + def variable_parameters(self) -> Optional[bool]: + return self._variable_parameters + + @property + def calling_convention(self) -> Optional[callingconvention.CallingConvention]: + return self._calling_convention + + @property + def platform(self) -> Optional[platform.Platform]: + return self._platform + + +class DebugInfo(object): + """ + ``class DebugInfo`` provides an interface to both provide and query debug info. The DebugInfo object is used + internally by the binary view to which it is applied to determine the attributes of functions, types, and variables + that would otherwise be costly to deduce. + + DebugInfo objects themselves are independent of binary views; their data can be sourced from any arbitrary binary + views and be applied to any other arbitrary binary view. A DebugInfo object can also contain debug info from multiple + DebugInfoParsers. This makes it possible to gather debug info that may be distributed across several different + formats and files. + + DebugInfo cannot be instantiated by the user, instead get it from either the binary view (see ``binaryninja.binaryview.BinaryView.debug_info``) + or a debug-info parser (see ``binaryninja.debuginfo.DebugInfoParser.parse_debug_info``). + + .. note:: Please note that calling one of ``add_*`` functions will not work outside of a debuginfo plugin. + """ + def __init__(self, handle: core.BNDebugInfo) -> None: + self.handle = core.handle_of_type(handle, core.BNDebugInfo) + + def __del__(self) -> None: + core.BNFreeDebugInfoReference(self.handle) + + def types_from_parser(self, name: Optional[str] = None) -> Iterator[Tuple[str, types.Type]]: + """Returns a generator of all types provided by a named DebugInfoParser""" + count = ctypes.c_ulonglong(0) + name_and_types = core.BNGetDebugTypes(self.handle, name, count) + try: + for i in range(0, count.value): + yield (name_and_types[i].name, types.Type(core.BNNewTypeReference(name_and_types[i].type))) + finally: + core.BNFreeDebugTypes(name_and_types, count.value) + + @property + def types(self) -> Iterator[Tuple[str, types.Type]]: + """A generator of all types provided by DebugInfoParsers""" + return self.types_from_parser() + + def functions_from_parser(self, name: Optional[str] = None) -> Iterator[DebugFunctionInfo]: + """Returns a generator of all functions provided by a named DebugInfoParser""" + count = ctypes.c_ulonglong(0) + functions = core.BNGetDebugFunctions(self.handle, name, count) + try: + for i in range(0, count.value): + + parameters: List[Tuple[str, binaryninja.types.Type]] = [] + for j in range(functions[i].parameterCount): + parameters.append((functions[i].parameterNames[j], binaryninja.types.Type(core.BNNewTypeReference(functions[i].parameterTypes[j])))) + + if functions[i].returnType: + return_type = binaryninja.types.Type(core.BNNewTypeReference(functions[i].returnType)) + else: + return_type = None + + if functions[i].callingConvention: + calling_convention = callingconvention.CallingConvention(handle=core.BNNewCallingConventionReference(functions[i].callingConvention)) + else: + calling_convention = None + + if functions[i].platform: + func_platform = platform.Platform(handle=core.BNNewPlatformReference(functions[i].platform)) + else: + func_platform = None + + yield DebugFunctionInfo( + functions[i].address, + functions[i].shortName, + functions[i].fullName, + functions[i].rawName, + return_type, + parameters, + functions[i].variableParameters, + calling_convention, + func_platform + ) + finally: + core.BNFreeDebugFunctions(functions, count.value) + + @property + def functions(self) -> Iterator[DebugFunctionInfo]: + """A generator of all functions provided by DebugInfoParsers""" + return self.functions_from_parser() + + def data_variables_from_parser(self, name: Optional[str] = None) -> Iterator[binaryninja.binaryview.DataVariableAndName]: + """Returns a generator of all data variables provided by a named DebugInfoParser""" + count = ctypes.c_ulonglong(0) + data_variables = core.BNGetDebugDataVariables(self.handle, name, count) + try: + for i in range(0, count.value): + yield binaryninja.binaryview.DataVariableAndName( + data_variables[i].address, + binaryninja.types.Type(core.BNNewTypeReference(data_variables[i].type)), + data_variables[i].name, + data_variables[i].autoDiscovered, + data_variables[i].typeConfidence + ) + finally: + core.BNFreeDataVariablesAndName(data_variables, count.value) + + @property + def data_variables(self) -> Iterator[binaryninja.binaryview.DataVariableAndName]: + """A generator of all data variables provided by DebugInfoParsers""" + return self.data_variables_from_parser() + + def add_type(self, name: str, new_type: binaryninja.types.Type) -> bool: + """Adds a type scoped under the current parser's name to the debug info""" + if isinstance(new_type, binaryninja.types.Type): + return core.BNAddDebugType(self.handle, name, new_type.handle) + return NotImplemented + + def add_function(self, new_func: DebugFunctionInfo) -> bool: + """Adds a function scoped under the current parser's name to the debug info""" + if not isinstance(new_func, DebugFunctionInfo): + return NotImplemented + + parameter_count = 0 + if new_func.parameters is not None: + parameter_count = len(new_func.parameters) + + func_info = core.BNDebugFunctionInfo() + + if new_func.return_type is None: + func_info.returnType = None + elif isinstance(new_func.return_type, binaryninja.types.Type): + func_info.returnType = new_func.return_type.handle + else: + return NotImplemented + + if new_func.calling_convention is None: + func_info.callingConvention = None + elif isinstance(new_func.calling_convention, callingconvention.CallingConvention): + func_info.callingConvention = new_func.calling_convention.handle + else: + return NotImplemented + + if new_func.platform is None: + func_info.platform = None + elif isinstance(new_func.platform, platform.Platform): + func_info.platform = new_func.platform.handle + else: + return NotImplemented + + func_info.shortName = new_func.short_name + func_info.fullName = new_func.full_name + func_info.rawName = new_func.raw_name + func_info.address = new_func.address + func_info.variableParameters = new_func.variable_parameters + + if parameter_count == 0: + func_info.parameterNames = None + func_info.parameterTypes = None + func_info.parameterCount = parameter_count + else: + func_info.parameterNames = (ctypes.c_char_p * parameter_count)(*map(lambda pair: binaryninja.cstr(pair[0]), new_func.parameters)) + func_info.parameterTypes = (ctypes.POINTER(core.BNType) * parameter_count)(*map(lambda pair: pair[1].handle, new_func.parameters)) + func_info.parameterCount = parameter_count + + return core.BNAddDebugFunction(self.handle, func_info) + + def add_data_variable(self, address: int, new_type: binaryninja.types.Type, name: Optional[str] = None) -> bool: + """Adds a data variable scoped under the current parser's name to the debug info""" + if isinstance(address, int) and isinstance(new_type, binaryninja.types.Type): + return core.BNAddDebugDataVariable(self.handle, address, new_type.handle, name) + return NotImplemented diff --git a/python/examples/debug_info.py b/python/examples/debug_info.py new file mode 100755 index 00000000..8f379544 --- /dev/null +++ b/python/examples/debug_info.py @@ -0,0 +1,269 @@ +#!/usr/bin/env python3 + +# If you're here, you're likely looking for boilerplate code. Here it is: +# ``` +# import binaryninja as bn +# +# def is_valid(bv: bn.binaryview.BinaryView): +# return bv.view_type == "Raw" +# +# def parse_info(debug_info: bn.debuginfo.DebugInfo, bv: bn.binaryview.BinaryView): +# debug_info.add_type("name", bn.types.Type.int(4, True)) +# +# debug_info.add_data_variable(0x1234, bn.types.Type.int(4, True), "name") +# debug_info.add_data_variable(0x4321, bn.types.Type.int(4, True)) # Names are optional +# +# # Just provide the information you can; we can't create the function without an address, but we'll +# # figure out what we can and you can query this info later when you have a better idea of things +# function_info = bn.debuginfo.DebugFunctionInfo(0xdead1337, "short_name", "full_name", "raw_name", bn.types.Type.int(4, False), []) +# debug_info.add_function(function_info) +# +# bn.debuginfo.DebugInfoParser.register("debug info parser", is_valid, parse_info) +# ``` + +# If you're interesting in applying debug info to existing BNDBs or otherwise manipulating debug info more directally, consider: +# ``` +# valid_parsers = bn.debuginfo.DebugInfoParser.get_parsers_for_view(bv) +# parser = bn.debuginfo.DebugInfoParser[name] +# debug_info = parser.parse_debug_info(bv) +# bv.apply_debug_info(debug_info) +# ``` + + +# The rest of this file serves as a test and example of implementing debug info parsers, and the resultant debug info. +# +# All that is required is to provide functions similar to "is_valid" and "parse_info" below, and call +# `binaryninja.debuginfo.DebugInfoParser.register` with a name for your parser; your parser will be made +# available for all valid binary views, with the ability to parse and apply debug info to existing BNDBs. +# +# For the purposes of this example, the following test program was compiled and the symbol `__elf_interp` +# overwritten to provide some magic for us to key on. This example should prove sufficient to +# demonstraight the capabilities of a debug info parser; providing function prototypes, local variables, +# data variables, and new types. It also highlights some limitations of BN at time of writing which +# should be fixed (see github.com/Vector35/binaryninja-api/issues/2399). +# ``` +# #include +# #include +# +# struct test_type_1 { +# int a; +# char b[4]; +# uint64_t c; +# bool d; +# } test_var_1; +# +# struct test_type_2 { +# struct test_type_1 a; +# struct test_type_1* b; +# struct test_type_2* c; +# }; +# +# int test_var_2 = 0x1232; +# const int test_var_3 = 0x1233; +# static int test_var_4 = 0x1234; +# +# void no_return_type_no_parameters() { } +# +# bool used_parameter(bool value) +# { +# return !value; +# } +# +# int unused_parameters(bool value_1, int value_2, char* value_3) +# { +# return 8*16-12/32+7|13; +# } +# +# int used_and_unused_parameters_1(int value_1, int value_2, char* value_3, bool value_4) +# { +# return value_1 + value_2; +# } +# +# uint8_t used_and_unused_parameters_2(bool value_1, uint8_t value_2, char* value_3, uint8_t value_4, char value_5) +# { +# return value_2 + value_4; +# } +# +# void local_parameters(bool value_1, uint8_t value_2, char* value_3, uint8_t value_4, char value_5) +# { +# char local_var_1 = value_1 ? value_3[15] : value_5; +# uint8_t local_var_2 = value_2 + 25; +# } +# +# int main() +# { +# int a = 0b01010101; +# int b = 0b10101010; +# return ~(a | b | test_var_2); +# } +# ``` + + +import binaryninja as bn +import os + + +filename = os.path.join(os.path.dirname(os.path.abspath(__file__)), "test_debug_info") + + +# Some setup code not just for informative printing + + +print = print +if __name__ != "__main__": + print = bn.log_error + + +def pretty_print_add_data_variable(debug_info: bn.debuginfo.DebugInfo, address: int, t: bn.types.Type, name: str = None) -> None: + print(f" Adding data variable of type `{t}` at {hex(address)} : {debug_info.add_data_variable(address, t, name)}") + + +def pretty_print_add_function(debug_info: bn.debuginfo.DebugInfo, address: int, short_name: str = None, full_name: str = None, raw_name: str = None, return_type = None, parameters = None) -> None: + function_info = bn.debuginfo.DebugFunctionInfo(address, short_name, full_name, raw_name, return_type, parameters) + if parameters is not None: + print(f" Adding function `{return_type} {short_name}({', '.join(f'{t} {name}' for name, t in parameters)})` at {hex(address)} : {debug_info.add_function(function_info)}") + else: + print(f" Adding function `{return_type} {short_name}()` at {hex(address)} : {debug_info.add_function(function_info)}") + + +# The beginning of the actual debug info plugin + + +def is_valid(bv: bn.binaryview.BinaryView): + sym = bv.get_symbol_by_raw_name("__elf_interp") + if sym is None: + return False + else: + var = bv.get_data_var_at(sym.address) + return b"test_debug_info_parsing" == bv.read(sym.address, var.type.width-1) + + +def parse_info(debug_info: bn.debuginfo.DebugInfo, bv: bn.binaryview.BinaryView): + print("Adding types") + types = [] + for name, t in bv.parse_types_from_string(""" + struct test_type_1 { + int a; + char b[4]; + uint64_t c; + bool d; + }; + + struct test_type_2 { + struct test_type_1 a; + struct test_type_1* b; + struct test_type_2* c; + };""").types.items(): + print(f" Adding type \"{name}\" `{t}` : {debug_info.add_type(str(name), t)}") + types.append(t) + + print("Adding data variables") + pretty_print_add_data_variable(debug_info, 0x4030, types[0], "test_var_1") + pretty_print_add_data_variable(debug_info, 0x4010, bn.types.Type.int(4, True), "test_var_2") + # Names are optional + pretty_print_add_data_variable(debug_info, 0x4014, bn.types.Type.int(4, True)) + + t = bn.types.Type.int(4, True) + t.const = True + pretty_print_add_data_variable(debug_info, 0x2004, t, "test_var_3") + + print("Adding functions") + char_star = bv.parse_type_string("char*")[0] + pretty_print_add_function(debug_info, 0x1129, "no_return_type_no_parameters", None, None, bn.types.Type.void(), None) + pretty_print_add_function(debug_info, 0x1134, "used_parameter", None, None, bn.types.Type.bool(), [("value", bn.types.Type.bool())]) + pretty_print_add_function(debug_info, 0x1155, "unused_parameters", None, None, bn.types.Type.int(4, True), [("value_1", bn.types.Type.bool()), ("value_2", bn.types.Type.int(4, True)), ("value_3", char_star)]) + pretty_print_add_function(debug_info, 0x1170, "used_and_unused_parameters_1", None, None, bn.types.Type.int(4, True), [("value_1", bn.types.Type.int(4, True)), ("value_2", bn.types.Type.int(4, True)), ("value_3", char_star), ("value_4", bn.types.Type.bool())]) + pretty_print_add_function(debug_info, 0x1191, "used_and_unused_parameters_2", None, None, bn.types.Type.int(1, False), [("value_1", bn.types.Type.bool()), ("value_2", bn.types.Type.int(1, False)), ("value_3", char_star), ("value_4", bn.types.Type.int(1, False)), ("value_5", bn.types.Type.char())]) + pretty_print_add_function(debug_info, 0x11c0, "local_parameters", None, None, bn.types.Type.void(), [("value_1", bn.types.Type.bool()), ("value_2", bn.types.Type.int(1, False)), ("value_3", char_star), ("value_4", bn.types.Type.int(1, False)), ("value_5", bn.types.Type.char())]) + + +parser = bn.debuginfo.DebugInfoParser.register("test debug info parser", is_valid, parse_info) +print(f"Registered parser: {parser.name}") + + +# The above is all that is needed for a DebugInfo plugin +# The below serves to test the correctness of (the Python bindings' implementation of) debug info parsers' functionality. + + +bn.debuginfo.DebugInfoParser.register("dummy extra debug parser 1", lambda bv: False, lambda di, bv: None) +bn.debuginfo.DebugInfoParser.register("dummy extra debug parser 2", lambda bv: bv.view_type != "Raw", lambda di, bv: None) + +# Test fetching parser list and fetching by name +print(f"Availible parsers: {len(list(bn.debuginfo.DebugInfoParser))}") +for p in bn.debuginfo.DebugInfoParser: + if p == parser: + print(f" {bn.debuginfo.DebugInfoParser[p.name].name} (the one we just registered)") + else: + print(f" {bn.debuginfo.DebugInfoParser[p.name].name}") + +# Test calling our `is_valid` callback +bv = bn.open_view(filename, options={"analysis.experimental.parseDebugInfo": False}) +if parser.is_valid_for_view(bv): + print("Parser is valid") +else: + print("Parser is NOT valid!") + quit() + + +# Test getting list of valid parsers, and DebugInfoParser's repr +print("") +for p in bn.debuginfo.DebugInfoParser.get_parsers_for_view(bv): + print(f"`{p.name}` is valid for `{bv}`") +print("") + +# Test calling our `parse_info` callback +debug_info = parser.parse_debug_info(bv) +# debug_info = bv.debug_info + +print("\nEach of the following pairs of prints should be the same\n") + +print("All types:") +for name, t in debug_info.types: + print(f" \"{name}\": `{t}`") + +print("Types from parser:") +for name, t in debug_info.types_from_parser(parser.name): + print(f" \"{name}\": `{t}`") + +print("") + +print("All functions:") +for func in debug_info.functions: + print(f" {func}") + +print("Functions from parser:") +for func in debug_info.functions_from_parser(parser.name): + print(f" {func}") + +print("") + +print("All data variables:") +for data_var in debug_info.data_variables: + print(f" {data_var}") + +print("Data variables from parser:") +for data_var in debug_info.data_variables_from_parser(parser.name): + print(f" {data_var}") + + +print("Appling debug info!") +bv.apply_debug_info(debug_info) +bv.update_analysis_and_wait() + +# Checking applied debug info +print("") +print("Types:") +for name, t in debug_info.types: + print(f" {bv.get_type_by_name(name)}") + +print("") + +print("Functions:") +for func in debug_info.functions: + print(f" {bv.get_function_at(func.address)}") + +print("") + +print("Data variables:") +for data_var in debug_info.data_variables: + print(f" {bv.get_data_var_at(data_var.address)}") diff --git a/python/examples/test_debug_info b/python/examples/test_debug_info new file mode 100755 index 00000000..8a650411 Binary files /dev/null and b/python/examples/test_debug_info differ -- cgit v1.3.1