diff --git a/NavigationExtensions.cs b/NavigationExtensions.cs new file mode 100644 index 0000000..fde7504 --- /dev/null +++ b/NavigationExtensions.cs @@ -0,0 +1,418 @@ +using System; +using System.Collections.Generic; +using System.Runtime.InteropServices; + +namespace BinaryNinja +{ + // Hand-written, durable object-navigation conveniences that mirror the ergonomic + // navigation surface of the official Python binding over the types this binding + // already exposes. This file lives at the repository root (NOT inside the generated + // Handle/Type/Struct tree) on purpose: the generator regenerates the wrapper classes + // from the upstream C++ header on every resync, so anything added there is wiped, + // while a root file like UnsafeUtils.cs survives. + // + // Every member here is either pure C# composition over existing, working accessors or + // a thin wrapper over a P/Invoke whose export is already present in the installed core, + // so each one is fully end-to-end validatable. No edits are made to generated files. + + // ---- BNDerivedString interop (binaryninjacore.h :1830 / :1837) ------------------------- + // + // The generator emits BNGetDerivedStrings with an opaque IntPtr for BNDerivedString, so + // defining the struct here does not collide with generated interop. Field offsets are + // pinned with LayoutKind.Explicit to reproduce the C ABI byte-for-byte; the non-obvious + // case is BNDerivedString, where the embedded `bool locationValid` is one byte and the + // following BNDerivedStringLocation is 8-byte aligned, so `location` lands at offset 16 + // (7 bytes of padding after the bool). A runtime probe asserts the managed size matches + // the C size (BNDerivedString == 48, BNDerivedStringLocation == 24) to lock the layout. + + [StructLayout(LayoutKind.Explicit)] + internal struct BNDerivedStringLocation + { + // BNDerivedStringLocationType (C enum = int, 4 bytes); 4 bytes pad before addr. + [FieldOffset(0)] + public int locationType; + + // uint64_t addr + [FieldOffset(8)] + public ulong addr; + + // uint64_t len + [FieldOffset(16)] + public ulong len; + } + + [StructLayout(LayoutKind.Explicit)] + internal struct BNDerivedString + { + // BNStringRef* value + [FieldOffset(0)] + public IntPtr value; + + // bool locationValid (C _Bool, 1 byte); 7 bytes pad before the aligned `location`. + [FieldOffset(8)] + [MarshalAs(UnmanagedType.I1)] + public bool locationValid; + + // BNDerivedStringLocation location (8-byte aligned -> offset 16) + [FieldOffset(16)] + public BNDerivedStringLocation location; + + // BNCustomStringType* customType + [FieldOffset(40)] + public IntPtr customType; + } + + // Managed projection of a BNDerivedString. The text is read eagerly (via + // BNGetStringRefContents, decoded UTF-8) because the caller frees the native array, + // including its embedded BNStringRef* values, immediately after marshaling (Python passes + // owned = False to BNFreeDerivedStringList). The location is surfaced only when + // locationValid is true, matching the Python DerivedString contract. + public sealed class DerivedString + { + public string Value { get; } + + public bool LocationValid { get; } + + public DerivedStringLocationType LocationType { get; } + + public ulong Address { get; } + + public ulong Length { get; } + + private DerivedString( + string value, + bool locationValid, + int locationType, + ulong addr, + ulong len) + { + this.Value = value; + this.LocationValid = locationValid; + this.LocationType = (DerivedStringLocationType)locationType; + this.Address = addr; + this.Length = len; + } + + // Reads one BNDerivedString out of a by-value native array. Must run before the array + // (and its BNStringRef* values) is freed; TakeStructArrayEx calls this per element + // before invoking the array free callback, so the order is safe. + internal static DerivedString FromNative(BNDerivedString raw) + { + string value = ReadStringRef(raw.value); + + return new DerivedString( + value, + raw.locationValid, + raw.location.locationType, + raw.location.addr, + raw.location.len + ); + } + + // Decodes a BNStringRef* as a UTF-8 string. The core hands back a NUL-terminated C + // string; UTF-8 decoding covers the non-ASCII case the rest of the binding targets. + private static string ReadStringRef(IntPtr reference) + { + if (IntPtr.Zero == reference) + { + return string.Empty; + } + + IntPtr contents = NativeMethods.BNGetStringRefContents(reference); + + return UnsafeUtils.ReadUtf8String(contents); + } + } + + // The navigation conveniences themselves. Grouped by the type they extend. + public static class NavigationExtensions + { + // ---- Enumeration / EnumerationBuilder ---------------------------------------------- + + /// + /// The first enumeration member whose equals + /// , or null when no such member exists. Mirrors the + /// string branch of Python Enumeration.__getitem__ over + /// . + /// + public static EnumerationMember? GetMemberByName(this Enumeration enumeration, string name) + { + if (null == name) + { + throw new ArgumentNullException(nameof(name)); + } + + foreach (EnumerationMember member in enumeration.Members) + { + if (member.Name == name) + { + return member; + } + } + + return null; + } + + /// + /// The first enumeration member whose equals + /// , or null when no such member exists. A C# + /// ergonomic companion to (Python exposes name and + /// index lookup only, not value lookup). + /// + public static EnumerationMember? GetMemberByValue(this Enumeration enumeration, ulong value) + { + foreach (EnumerationMember member in enumeration.Members) + { + if (member.Value == value) + { + return member; + } + } + + return null; + } + + /// + /// The first builder member whose equals + /// , or null. Mirrors Python + /// EnumerationBuilder.__getitem__ over . + /// + public static EnumerationMember? GetMemberByName(this EnumerationBuilder builder, string name) + { + if (null == name) + { + throw new ArgumentNullException(nameof(name)); + } + + foreach (EnumerationMember member in builder.Members) + { + if (member.Name == name) + { + return member; + } + } + + return null; + } + + /// + /// The first builder member whose equals + /// , or null. C# ergonomic companion to + /// . + /// + public static EnumerationMember? GetMemberByValue(this EnumerationBuilder builder, ulong value) + { + foreach (EnumerationMember member in builder.Members) + { + if (member.Value == value) + { + return member; + } + } + + return null; + } + + // ---- StructureType ----------------------------------------------------------------- + + /// + /// The structure member with the given name, or null. Forwards the underlying + /// (Python StructureType.__getitem__) + /// so callers need not drop to . The + /// offset-based lookup already exists as . + /// + public static StructureMember? GetMemberByName(this StructureType structureType, string name) + { + return structureType.Structure.GetMemberByName(name); + } + + // ---- Type -------------------------------------------------------------------------- + + /// + /// The child types of this type, mirroring Python Type.children: a pointer + /// yields its target, an array its element type, a function its return type followed + /// by each parameter type, and a structure each member type. Leaf classes (void, + /// integer, float, enumeration, named-type-reference, ...) yield an empty array. + /// + /// + /// Each returned is an independently owned reference (the accessors + /// take a fresh BNNewTypeReference), so the caller is responsible for disposing + /// them, exactly as for . + /// + public static BinaryNinja.Type[] GetChildren(this BinaryNinja.Type type) + { + List children = new List(); + + switch (type.TypeClass) + { + case TypeClass.PointerTypeClass: + { + using (PointerType? pointer = type.AsPointer()) + { + if (null != pointer) + { + children.Add(pointer.Target.Type); + } + } + break; + } + case TypeClass.ArrayTypeClass: + { + using (ArrayType? array = type.AsArray()) + { + if (null != array) + { + children.Add(array.ElementType.Type); + } + } + break; + } + case TypeClass.FunctionTypeClass: + { + using (FunctionType? function = type.AsFunction()) + { + if (null != function) + { + children.Add(function.ReturnType.Type); + + foreach (FunctionParameter parameter in function.Parameters) + { + children.Add(parameter.Type); + } + } + } + break; + } + case TypeClass.StructureTypeClass: + { + using (StructureType? structure = type.AsStructure()) + { + if (null != structure) + { + foreach (StructureMember member in structure.Members) + { + children.Add(member.Type); + } + } + } + break; + } + default: + { + // Void/Bool/Char/WideChar/Integer/Float/Enumeration/NamedTypeReference are + // leaves: they carry no child types (matches Python Type.children base case). + break; + } + } + + return children.ToArray(); + } + + // ---- Function ---------------------------------------------------------------------- + + /// + /// Every Low Level IL exit target of the instruction starting at + /// under (or the function's own + /// architecture when null). Composes + /// with + /// ; mirrors Python + /// Function.get_low_level_il_exits_at. Returns an empty array when the + /// function has no LLIL or no instruction starts at the address. + /// + public static LowLevelILInstruction[] GetLowLevelILExitsAt( + this Function function, + ulong address, + Architecture? arch = null) + { + LowLevelILFunction? llil = function.GetLowLevelIL(); + + if (null == llil) + { + return Array.Empty(); + } + + LowLevelILInstruction? instruction = llil.GetInstructionStart(address, arch); + + if (null == instruction) + { + return Array.Empty(); + } + + return llil.GetExitsForInstruction(instruction.InstructionIndex); + } + + // ---- BinaryView -------------------------------------------------------------------- + + /// + /// The derived strings the analysis produced for this view, mirroring Python + /// BinaryView.get_derived_strings (C++ BinaryView::GetDerivedStrings). + /// + /// + /// The BNGetDerivedStrings stub declares its count out-parameter as + /// IntPtr (size_t*), so the count is read back from a native slot rather + /// than via out. BNFreeDerivedStringList frees the array and its embedded + /// BNStringRef* values, so must not free them. + /// + public static DerivedString[] GetDerivedStrings(this BinaryView view) + { + VerifyDerivedStringLayout(); + + IntPtr countSlot = Marshal.AllocHGlobal(IntPtr.Size); + + try + { + IntPtr head = NativeMethods.BNGetDerivedStrings( + view.DangerousGetHandle(), + countSlot + ); + + ulong count = (ulong)Marshal.ReadInt64(countSlot); + + return UnsafeUtils.TakeStructArrayEx( + head, + count, + DerivedString.FromNative, + FreeDerivedStringList + ); + } + finally + { + Marshal.FreeHGlobal(countSlot); + } + } + + // Adapts BNFreeDerivedStringList (which takes a UIntPtr count) to the + // Action signature TakeStructArrayEx expects. Named method, not a + // lambda, per the project's C# style rules. + private static void FreeDerivedStringList(IntPtr head, ulong count) + { + NativeMethods.BNFreeDerivedStringList(head, (UIntPtr)count); + } + + // One-shot check that the managed BNDerivedString layout reproduces the C ABI from + // binaryninjacore.h (48 bytes for BNDerivedString, 24 for BNDerivedStringLocation). + // The bool-padding offset is the one non-obvious case, so this locks it rather than + // assuming the [FieldOffset] values are correct. Runs once per process. + private static int s_derivedStringLayoutSize = -1; + + private static void VerifyDerivedStringLayout() + { + if (s_derivedStringLayoutSize >= 0) + { + return; + } + + int outerSize = Marshal.SizeOf(); + int innerSize = Marshal.SizeOf(); + + if (48 != outerSize || 24 != innerSize) + { + throw new InvalidOperationException( + "BNDerivedString interop layout mismatch: outer=" + outerSize + + " (expected 48), inner=" + innerSize + " (expected 24)"); + } + + s_derivedStringLayoutSize = outerSize; + } + } +}