new advisories work and features gaps work

This commit is contained in:
master
2026-01-14 18:39:19 +02:00
parent 95d5898650
commit 15aeac8e8b
148 changed files with 16731 additions and 554 deletions

View File

@@ -0,0 +1,322 @@
// Copyright (c) StellaOps. All rights reserved.
// Licensed under AGPL-3.0-or-later. See LICENSE in the project root.
// Sprint: SPRINT_20260112_004_BINIDX_b2r2_lowuir_perf_cache (BINIDX-OPS-04)
// Task: Add ops endpoints for health, bench, cache, and config
using System.Collections.Immutable;
using System.Diagnostics;
using System.Globalization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Options;
using StellaOps.BinaryIndex.Cache;
using StellaOps.BinaryIndex.Disassembly.B2R2;
namespace StellaOps.BinaryIndex.WebService.Controllers;
/// <summary>
/// Ops endpoints for BinaryIndex health, benchmarking, cache stats, and configuration.
/// </summary>
[ApiController]
[Route("api/v1/ops/binaryindex")]
[Produces("application/json")]
public sealed class BinaryIndexOpsController : ControllerBase
{
private readonly B2R2LifterPool? _lifterPool;
private readonly FunctionIrCacheService? _cacheService;
private readonly IOptions<B2R2LifterPoolOptions> _poolOptions;
private readonly IOptions<FunctionIrCacheOptions> _cacheOptions;
private readonly TimeProvider _timeProvider;
private readonly ILogger<BinaryIndexOpsController> _logger;
public BinaryIndexOpsController(
ILogger<BinaryIndexOpsController> logger,
TimeProvider timeProvider,
IOptions<B2R2LifterPoolOptions> poolOptions,
IOptions<FunctionIrCacheOptions> cacheOptions,
B2R2LifterPool? lifterPool = null,
FunctionIrCacheService? cacheService = null)
{
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
_timeProvider = timeProvider ?? throw new ArgumentNullException(nameof(timeProvider));
_poolOptions = poolOptions ?? throw new ArgumentNullException(nameof(poolOptions));
_cacheOptions = cacheOptions ?? throw new ArgumentNullException(nameof(cacheOptions));
_lifterPool = lifterPool;
_cacheService = cacheService;
}
/// <summary>
/// Gets BinaryIndex health status including lifter warmness and cache availability.
/// </summary>
/// <param name="ct">Cancellation token.</param>
/// <returns>Health response with component status.</returns>
[HttpGet("health")]
[ProducesResponseType<BinaryIndexOpsHealthResponse>(StatusCodes.Status200OK)]
[ProducesResponseType<ProblemDetails>(StatusCodes.Status503ServiceUnavailable)]
public ActionResult<BinaryIndexOpsHealthResponse> GetHealth(CancellationToken ct)
{
var lifterStatus = "unavailable";
var lifterWarm = false;
var lifterPoolStats = ImmutableDictionary<string, int>.Empty;
if (_lifterPool != null)
{
var stats = _lifterPool.GetStats();
lifterStatus = stats.IsWarm ? "warm" : "cold";
lifterWarm = stats.IsWarm;
lifterPoolStats = stats.IsaStats
.ToImmutableDictionary(
kv => kv.Key,
kv => kv.Value.PooledCount + kv.Value.ActiveCount);
}
var cacheStatus = "unavailable";
var cacheEnabled = false;
if (_cacheService != null)
{
var cacheStats = _cacheService.GetStats();
cacheStatus = cacheStats.IsEnabled ? "enabled" : "disabled";
cacheEnabled = cacheStats.IsEnabled;
}
var response = new BinaryIndexOpsHealthResponse(
Status: lifterWarm && cacheEnabled ? "healthy" : "degraded",
Timestamp: _timeProvider.GetUtcNow().ToString("o", CultureInfo.InvariantCulture),
LifterStatus: lifterStatus,
LifterWarm: lifterWarm,
LifterPoolStats: lifterPoolStats,
CacheStatus: cacheStatus,
CacheEnabled: cacheEnabled);
return Ok(response);
}
/// <summary>
/// Runs a quick benchmark and returns latency metrics.
/// </summary>
/// <param name="request">Optional bench parameters.</param>
/// <param name="ct">Cancellation token.</param>
/// <returns>Benchmark response with latency measurements.</returns>
[HttpPost("bench/run")]
[ProducesResponseType<BinaryIndexBenchResponse>(StatusCodes.Status200OK)]
[ProducesResponseType<ProblemDetails>(StatusCodes.Status400BadRequest)]
public ActionResult<BinaryIndexBenchResponse> RunBench(
[FromBody] BinaryIndexBenchRequest? request,
CancellationToken ct)
{
var iterations = request?.Iterations ?? 10;
if (iterations < 1 || iterations > 1000)
{
return BadRequest(new ProblemDetails
{
Title = "Invalid iterations",
Detail = "Iterations must be between 1 and 1000",
Status = StatusCodes.Status400BadRequest
});
}
_logger.LogInformation("Running BinaryIndex benchmark with {Iterations} iterations", iterations);
var lifterLatencies = new List<double>();
var cacheLatencies = new List<double>();
// Benchmark lifter acquisition if available
if (_lifterPool != null)
{
var isa = new B2R2.ISA(B2R2.Architecture.Intel, B2R2.WordSize.Bit64);
for (var i = 0; i < iterations; i++)
{
ct.ThrowIfCancellationRequested();
var sw = Stopwatch.StartNew();
using (var lifter = _lifterPool.Acquire(isa))
{
// Just acquire and release
}
sw.Stop();
lifterLatencies.Add(sw.Elapsed.TotalMilliseconds);
}
}
// Benchmark cache lookup if available
if (_cacheService != null)
{
var dummyKey = new FunctionCacheKey(
Isa: "intel-64",
B2R2Version: "0.9.1",
NormalizationRecipe: "v1",
CanonicalIrHash: "0000000000000000000000000000000000000000000000000000000000000000");
for (var i = 0; i < iterations; i++)
{
ct.ThrowIfCancellationRequested();
var sw = Stopwatch.StartNew();
// Fire and forget the cache lookup
_ = _cacheService.TryGetAsync(dummyKey, ct).ConfigureAwait(false);
sw.Stop();
cacheLatencies.Add(sw.Elapsed.TotalMilliseconds);
}
}
var lifterStats = ComputeLatencyStats(lifterLatencies);
var cacheStats = ComputeLatencyStats(cacheLatencies);
var response = new BinaryIndexBenchResponse(
Timestamp: _timeProvider.GetUtcNow().ToString("o", CultureInfo.InvariantCulture),
Iterations: iterations,
LifterAcquireLatencyMs: lifterStats,
CacheLookupLatencyMs: cacheStats);
return Ok(response);
}
/// <summary>
/// Gets function IR cache statistics.
/// </summary>
/// <param name="ct">Cancellation token.</param>
/// <returns>Cache statistics.</returns>
[HttpGet("cache")]
[ProducesResponseType<BinaryIndexFunctionCacheStats>(StatusCodes.Status200OK)]
public ActionResult<BinaryIndexFunctionCacheStats> GetCacheStats(CancellationToken ct)
{
if (_cacheService == null)
{
return Ok(new BinaryIndexFunctionCacheStats(
Enabled: false,
Hits: 0,
Misses: 0,
Evictions: 0,
HitRate: 0.0,
KeyPrefix: "",
CacheTtlSeconds: 0));
}
var stats = _cacheService.GetStats();
return Ok(new BinaryIndexFunctionCacheStats(
Enabled: stats.IsEnabled,
Hits: stats.Hits,
Misses: stats.Misses,
Evictions: stats.Evictions,
HitRate: stats.HitRate,
KeyPrefix: stats.KeyPrefix,
CacheTtlSeconds: (long)stats.CacheTtl.TotalSeconds));
}
/// <summary>
/// Gets effective BinaryIndex configuration.
/// </summary>
/// <param name="ct">Cancellation token.</param>
/// <returns>Effective configuration (secrets redacted).</returns>
[HttpGet("config")]
[ProducesResponseType<BinaryIndexEffectiveConfig>(StatusCodes.Status200OK)]
public ActionResult<BinaryIndexEffectiveConfig> GetConfig(CancellationToken ct)
{
var poolOptions = _poolOptions.Value;
var cacheOptions = _cacheOptions.Value;
return Ok(new BinaryIndexEffectiveConfig(
LifterPoolMaxSizePerIsa: poolOptions.MaxPoolSizePerIsa,
LifterPoolWarmPreloadEnabled: poolOptions.EnableWarmPreload,
LifterPoolWarmPreloadIsas: poolOptions.WarmPreloadIsas,
LifterPoolAcquireTimeoutSeconds: (long)poolOptions.AcquireTimeout.TotalSeconds,
CacheEnabled: cacheOptions.Enabled,
CacheKeyPrefix: cacheOptions.KeyPrefix,
CacheTtlSeconds: (long)cacheOptions.CacheTtl.TotalSeconds,
CacheMaxTtlSeconds: (long)cacheOptions.MaxTtl.TotalSeconds,
B2R2Version: cacheOptions.B2R2Version,
NormalizationRecipeVersion: cacheOptions.NormalizationRecipeVersion));
}
private static BinaryIndexLatencyStats ComputeLatencyStats(List<double> latencies)
{
if (latencies.Count == 0)
{
return new BinaryIndexLatencyStats(
Min: 0,
Max: 0,
Mean: 0,
P50: 0,
P95: 0,
P99: 0);
}
latencies.Sort();
var count = latencies.Count;
return new BinaryIndexLatencyStats(
Min: latencies[0],
Max: latencies[^1],
Mean: latencies.Average(),
P50: latencies[count / 2],
P95: latencies[(int)(count * 0.95)],
P99: latencies[(int)(count * 0.99)]);
}
}
#region Response Models
/// <summary>
/// BinaryIndex health response.
/// </summary>
public sealed record BinaryIndexOpsHealthResponse(
string Status,
string Timestamp,
string LifterStatus,
bool LifterWarm,
ImmutableDictionary<string, int> LifterPoolStats,
string CacheStatus,
bool CacheEnabled);
/// <summary>
/// Benchmark request parameters.
/// </summary>
public sealed record BinaryIndexBenchRequest(
int Iterations = 10);
/// <summary>
/// Benchmark response with latency measurements.
/// </summary>
public sealed record BinaryIndexBenchResponse(
string Timestamp,
int Iterations,
BinaryIndexLatencyStats LifterAcquireLatencyMs,
BinaryIndexLatencyStats CacheLookupLatencyMs);
/// <summary>
/// Latency statistics.
/// </summary>
public sealed record BinaryIndexLatencyStats(
double Min,
double Max,
double Mean,
double P50,
double P95,
double P99);
/// <summary>
/// Function IR cache statistics.
/// </summary>
public sealed record BinaryIndexFunctionCacheStats(
bool Enabled,
long Hits,
long Misses,
long Evictions,
double HitRate,
string KeyPrefix,
long CacheTtlSeconds);
/// <summary>
/// Effective BinaryIndex configuration.
/// </summary>
public sealed record BinaryIndexEffectiveConfig(
int LifterPoolMaxSizePerIsa,
bool LifterPoolWarmPreloadEnabled,
ImmutableArray<string> LifterPoolWarmPreloadIsas,
long LifterPoolAcquireTimeoutSeconds,
bool CacheEnabled,
string CacheKeyPrefix,
long CacheTtlSeconds,
long CacheMaxTtlSeconds,
string B2R2Version,
string NormalizationRecipeVersion);
#endregion

View File

@@ -22,6 +22,7 @@
<ProjectReference Include="../__Libraries/StellaOps.BinaryIndex.Persistence/StellaOps.BinaryIndex.Persistence.csproj" />
<ProjectReference Include="../__Libraries/StellaOps.BinaryIndex.VexBridge/StellaOps.BinaryIndex.VexBridge.csproj" />
<ProjectReference Include="../__Libraries/StellaOps.BinaryIndex.GoldenSet/StellaOps.BinaryIndex.GoldenSet.csproj" />
<ProjectReference Include="../__Libraries/StellaOps.BinaryIndex.Disassembly.B2R2/StellaOps.BinaryIndex.Disassembly.B2R2.csproj" />
</ItemGroup>
</Project>

View File

@@ -2,6 +2,8 @@
// BinaryCacheServiceExtensions.cs
// Sprint: SPRINT_20251226_014_BINIDX
// Task: SCANINT-21 - Add Valkey cache layer for hot lookups
// Sprint: SPRINT_20260112_004_BINIDX (BINIDX-CACHE-03)
// Task: Function-level cache for canonical IR and semantic fingerprints
// -----------------------------------------------------------------------------
using Microsoft.Extensions.Configuration;
@@ -56,4 +58,49 @@ public static class BinaryCacheServiceExtensions
return services;
}
/// <summary>
/// Adds function IR caching layer to the service collection.
/// Uses Valkey as hot cache for semantic fingerprints.
/// </summary>
/// <param name="services">The service collection.</param>
/// <param name="configuration">Configuration for cache options.</param>
/// <returns>The service collection for chaining.</returns>
public static IServiceCollection AddFunctionIrCaching(
this IServiceCollection services,
IConfiguration configuration)
{
ArgumentNullException.ThrowIfNull(services);
ArgumentNullException.ThrowIfNull(configuration);
services.AddOptions<FunctionIrCacheOptions>()
.Bind(configuration.GetSection(FunctionIrCacheOptions.SectionName))
.ValidateOnStart();
services.TryAddSingleton<FunctionIrCacheService>();
return services;
}
/// <summary>
/// Adds function IR caching layer with explicit options.
/// </summary>
/// <param name="services">The service collection.</param>
/// <param name="configureOptions">Action to configure options.</param>
/// <returns>The service collection for chaining.</returns>
public static IServiceCollection AddFunctionIrCaching(
this IServiceCollection services,
Action<FunctionIrCacheOptions> configureOptions)
{
ArgumentNullException.ThrowIfNull(services);
ArgumentNullException.ThrowIfNull(configureOptions);
services.AddOptions<FunctionIrCacheOptions>()
.Configure(configureOptions)
.ValidateOnStart();
services.TryAddSingleton<FunctionIrCacheService>();
return services;
}
}

View File

@@ -0,0 +1,316 @@
// Copyright (c) StellaOps. All rights reserved.
// Licensed under AGPL-3.0-or-later. See LICENSE in the project root.
// Sprint: SPRINT_20260112_004_BINIDX_b2r2_lowuir_perf_cache (BINIDX-CACHE-03)
// Task: Function-level cache for canonical IR and semantic fingerprints
using System.Collections.Concurrent;
using System.Collections.Immutable;
using System.Globalization;
using System.Security.Cryptography;
using System.Text;
using System.Text.Json;
using Microsoft.Extensions.Caching.Distributed;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
namespace StellaOps.BinaryIndex.Cache;
/// <summary>
/// Configuration options for the function IR cache.
/// </summary>
public sealed class FunctionIrCacheOptions
{
/// <summary>
/// Configuration section name.
/// </summary>
public const string SectionName = "StellaOps:BinaryIndex:FunctionIrCache";
/// <summary>
/// Valkey key prefix for function IR cache entries.
/// </summary>
public string KeyPrefix { get; init; } = "stellaops:binidx:funccache:";
/// <summary>
/// TTL for cached function IR entries.
/// </summary>
public TimeSpan CacheTtl { get; init; } = TimeSpan.FromHours(4);
/// <summary>
/// Maximum TTL for any cache entry.
/// </summary>
public TimeSpan MaxTtl { get; init; } = TimeSpan.FromHours(24);
/// <summary>
/// Whether to enable the cache.
/// </summary>
public bool Enabled { get; init; } = true;
/// <summary>
/// B2R2 version string to include in cache keys.
/// </summary>
public string B2R2Version { get; init; } = "0.9.1";
/// <summary>
/// Normalization recipe version for cache key stability.
/// </summary>
public string NormalizationRecipeVersion { get; init; } = "v1";
}
/// <summary>
/// Cache key components for function IR caching.
/// </summary>
/// <param name="Isa">ISA identifier (e.g., "intel-64").</param>
/// <param name="B2R2Version">B2R2 version string.</param>
/// <param name="NormalizationRecipe">Normalization recipe version.</param>
/// <param name="CanonicalIrHash">SHA-256 hash of the canonical IR bytes.</param>
public sealed record FunctionCacheKey(
string Isa,
string B2R2Version,
string NormalizationRecipe,
string CanonicalIrHash)
{
/// <summary>
/// Converts to a deterministic cache key string.
/// </summary>
public string ToKeyString() =>
string.Format(
CultureInfo.InvariantCulture,
"{0}:{1}:{2}:{3}",
Isa,
B2R2Version,
NormalizationRecipe,
CanonicalIrHash);
}
/// <summary>
/// Cached function IR and semantic fingerprint entry.
/// </summary>
/// <param name="FunctionAddress">Original function address.</param>
/// <param name="FunctionName">Original function name.</param>
/// <param name="SemanticFingerprint">Computed semantic fingerprint.</param>
/// <param name="IrStatementCount">Number of IR statements.</param>
/// <param name="BasicBlockCount">Number of basic blocks.</param>
/// <param name="ComputedAtUtc">When the fingerprint was computed (ISO-8601).</param>
/// <param name="B2R2Version">B2R2 version used.</param>
/// <param name="NormalizationRecipe">Normalization recipe used.</param>
public sealed record CachedFunctionFingerprint(
ulong FunctionAddress,
string FunctionName,
string SemanticFingerprint,
int IrStatementCount,
int BasicBlockCount,
string ComputedAtUtc,
string B2R2Version,
string NormalizationRecipe);
/// <summary>
/// Cache statistics for the function IR cache.
/// </summary>
public sealed record FunctionIrCacheStats(
long Hits,
long Misses,
long Evictions,
double HitRate,
bool IsEnabled,
string KeyPrefix,
TimeSpan CacheTtl);
/// <summary>
/// Service for caching function IR and semantic fingerprints.
/// Uses Valkey as hot cache with deterministic key generation.
/// </summary>
public sealed class FunctionIrCacheService
{
private readonly IDistributedCache _cache;
private readonly ILogger<FunctionIrCacheService> _logger;
private readonly FunctionIrCacheOptions _options;
private readonly TimeProvider _timeProvider;
// Thread-safe statistics
private long _hits;
private long _misses;
private long _evictions;
private static readonly JsonSerializerOptions s_jsonOptions = new()
{
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
WriteIndented = false
};
/// <summary>
/// Creates a new function IR cache service.
/// </summary>
public FunctionIrCacheService(
IDistributedCache cache,
ILogger<FunctionIrCacheService> logger,
IOptions<FunctionIrCacheOptions> options,
TimeProvider timeProvider)
{
_cache = cache ?? throw new ArgumentNullException(nameof(cache));
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
_options = options?.Value ?? new FunctionIrCacheOptions();
_timeProvider = timeProvider ?? throw new ArgumentNullException(nameof(timeProvider));
}
/// <summary>
/// Gets the current cache statistics.
/// </summary>
public FunctionIrCacheStats GetStats()
{
var hits = Interlocked.Read(ref _hits);
var misses = Interlocked.Read(ref _misses);
var total = hits + misses;
var hitRate = total > 0 ? (double)hits / total : 0.0;
return new FunctionIrCacheStats(
Hits: hits,
Misses: misses,
Evictions: Interlocked.Read(ref _evictions),
HitRate: hitRate,
IsEnabled: _options.Enabled,
KeyPrefix: _options.KeyPrefix,
CacheTtl: _options.CacheTtl);
}
/// <summary>
/// Tries to get a cached function fingerprint.
/// </summary>
/// <param name="key">The cache key.</param>
/// <param name="ct">Cancellation token.</param>
/// <returns>The cached fingerprint if found, null otherwise.</returns>
public async Task<CachedFunctionFingerprint?> TryGetAsync(
FunctionCacheKey key,
CancellationToken ct = default)
{
if (!_options.Enabled)
{
return null;
}
var cacheKey = BuildCacheKey(key);
try
{
var bytes = await _cache.GetAsync(cacheKey, ct).ConfigureAwait(false);
if (bytes is null || bytes.Length == 0)
{
Interlocked.Increment(ref _misses);
return null;
}
var result = JsonSerializer.Deserialize<CachedFunctionFingerprint>(bytes, s_jsonOptions);
Interlocked.Increment(ref _hits);
_logger.LogTrace(
"Cache hit for function {FunctionName} at {Address}",
result?.FunctionName,
result?.FunctionAddress);
return result;
}
catch (Exception ex)
{
_logger.LogWarning(ex, "Failed to get cached function fingerprint for key {Key}", cacheKey);
Interlocked.Increment(ref _misses);
return null;
}
}
/// <summary>
/// Sets a function fingerprint in the cache.
/// </summary>
/// <param name="key">The cache key.</param>
/// <param name="fingerprint">The fingerprint to cache.</param>
/// <param name="ct">Cancellation token.</param>
public async Task SetAsync(
FunctionCacheKey key,
CachedFunctionFingerprint fingerprint,
CancellationToken ct = default)
{
if (!_options.Enabled)
{
return;
}
var cacheKey = BuildCacheKey(key);
try
{
var bytes = JsonSerializer.SerializeToUtf8Bytes(fingerprint, s_jsonOptions);
var options = new DistributedCacheEntryOptions
{
AbsoluteExpirationRelativeToNow = _options.CacheTtl
};
await _cache.SetAsync(cacheKey, bytes, options, ct).ConfigureAwait(false);
_logger.LogTrace(
"Cached function {FunctionName} fingerprint with key {Key}",
fingerprint.FunctionName,
cacheKey);
}
catch (Exception ex)
{
_logger.LogWarning(ex, "Failed to cache function fingerprint for key {Key}", cacheKey);
}
}
/// <summary>
/// Removes a cached function fingerprint.
/// </summary>
/// <param name="key">The cache key.</param>
/// <param name="ct">Cancellation token.</param>
public async Task RemoveAsync(FunctionCacheKey key, CancellationToken ct = default)
{
if (!_options.Enabled)
{
return;
}
var cacheKey = BuildCacheKey(key);
try
{
await _cache.RemoveAsync(cacheKey, ct).ConfigureAwait(false);
Interlocked.Increment(ref _evictions);
_logger.LogTrace("Removed cached function fingerprint for key {Key}", cacheKey);
}
catch (Exception ex)
{
_logger.LogWarning(ex, "Failed to remove cached function fingerprint for key {Key}", cacheKey);
}
}
/// <summary>
/// Computes a canonical IR hash from function bytes.
/// </summary>
/// <param name="irBytes">The canonical IR bytes.</param>
/// <returns>Hex-encoded SHA-256 hash.</returns>
public static string ComputeCanonicalIrHash(ReadOnlySpan<byte> irBytes)
{
Span<byte> hashBytes = stackalloc byte[32];
SHA256.HashData(irBytes, hashBytes);
return Convert.ToHexString(hashBytes).ToLowerInvariant();
}
/// <summary>
/// Creates a cache key for a function.
/// </summary>
/// <param name="isa">ISA identifier.</param>
/// <param name="canonicalIrBytes">The canonical IR bytes.</param>
/// <returns>The cache key.</returns>
public FunctionCacheKey CreateKey(string isa, ReadOnlySpan<byte> canonicalIrBytes)
{
var hash = ComputeCanonicalIrHash(canonicalIrBytes);
return new FunctionCacheKey(
Isa: isa,
B2R2Version: _options.B2R2Version,
NormalizationRecipe: _options.NormalizationRecipeVersion,
CanonicalIrHash: hash);
}
private string BuildCacheKey(FunctionCacheKey key) =>
_options.KeyPrefix + key.ToKeyString();
}

View File

@@ -13,6 +13,7 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.Caching.Abstractions" />
<PackageReference Include="StackExchange.Redis" />
<PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" />
<PackageReference Include="Microsoft.Extensions.Configuration.Binder" />

View File

@@ -369,6 +369,7 @@ public sealed class B2R2DisassemblyPlugin : IDisassemblyPlugin
: ImmutableArray<byte>.Empty;
var kind = ClassifyInstruction(instr, mnemonic);
var operands = ParseOperands(operandsText, mnemonic);
return new DisassembledInstruction(
Address: address,
@@ -376,7 +377,266 @@ public sealed class B2R2DisassemblyPlugin : IDisassemblyPlugin
Mnemonic: mnemonic,
OperandsText: operandsText,
Kind: kind,
Operands: ImmutableArray<Operand>.Empty); // Simplified - operand parsing is complex
Operands: operands);
}
private static ImmutableArray<Operand> ParseOperands(string operandsText, string mnemonic)
{
if (string.IsNullOrWhiteSpace(operandsText))
{
return ImmutableArray<Operand>.Empty;
}
var builder = ImmutableArray.CreateBuilder<Operand>();
// Split operands by comma, respecting brackets
var operandStrings = SplitOperands(operandsText);
foreach (var opStr in operandStrings)
{
var trimmed = opStr.Trim();
if (string.IsNullOrEmpty(trimmed)) continue;
var operand = ParseSingleOperand(trimmed);
builder.Add(operand);
}
return builder.ToImmutable();
}
private static IReadOnlyList<string> SplitOperands(string operandsText)
{
var result = new List<string>();
var current = new System.Text.StringBuilder();
var bracketDepth = 0;
foreach (var c in operandsText)
{
if (c == '[' || c == '(' || c == '{')
{
bracketDepth++;
current.Append(c);
}
else if (c == ']' || c == ')' || c == '}')
{
bracketDepth--;
current.Append(c);
}
else if (c == ',' && bracketDepth == 0)
{
if (current.Length > 0)
{
result.Add(current.ToString());
current.Clear();
}
}
else
{
current.Append(c);
}
}
if (current.Length > 0)
{
result.Add(current.ToString());
}
return result;
}
private static Operand ParseSingleOperand(string text)
{
var trimmed = text.Trim();
// Check for memory operand [...]
if (trimmed.StartsWith('[') && trimmed.EndsWith(']'))
{
return ParseMemoryOperand(trimmed);
}
// Check for ARM64 memory operand [...]!
if (trimmed.StartsWith('[') && (trimmed.EndsWith("]!") || trimmed.Contains("],")))
{
return ParseMemoryOperand(trimmed);
}
// Check for immediate value
if (trimmed.StartsWith('#') || trimmed.StartsWith("0x", StringComparison.OrdinalIgnoreCase) ||
trimmed.StartsWith("0X", StringComparison.OrdinalIgnoreCase) ||
(trimmed.Length > 0 && (char.IsDigit(trimmed[0]) || trimmed[0] == '-')))
{
return ParseImmediateOperand(trimmed);
}
// Assume it's a register
return ParseRegisterOperand(trimmed);
}
private static Operand ParseRegisterOperand(string text)
{
var regName = text.ToUpperInvariant();
return new Operand(
Type: OperandType.Register,
Text: text,
Value: null,
Register: regName,
MemoryBase: null,
MemoryIndex: null,
MemoryScale: null,
MemoryDisplacement: null);
}
private static Operand ParseImmediateOperand(string text)
{
var cleanText = text.TrimStart('#');
long? value = null;
if (cleanText.StartsWith("0x", StringComparison.OrdinalIgnoreCase))
{
if (long.TryParse(cleanText.AsSpan(2), System.Globalization.NumberStyles.HexNumber,
System.Globalization.CultureInfo.InvariantCulture, out var hexVal))
{
value = hexVal;
}
}
else if (cleanText.StartsWith("-0x", StringComparison.OrdinalIgnoreCase))
{
if (long.TryParse(cleanText.AsSpan(3), System.Globalization.NumberStyles.HexNumber,
System.Globalization.CultureInfo.InvariantCulture, out var hexVal))
{
value = -hexVal;
}
}
else if (long.TryParse(cleanText, System.Globalization.CultureInfo.InvariantCulture, out var decVal))
{
value = decVal;
}
return new Operand(
Type: OperandType.Immediate,
Text: text,
Value: value,
Register: null,
MemoryBase: null,
MemoryIndex: null,
MemoryScale: null,
MemoryDisplacement: null);
}
private static Operand ParseMemoryOperand(string text)
{
// Extract content between brackets
var start = text.IndexOf('[');
var end = text.LastIndexOf(']');
if (start < 0 || end <= start)
{
return new Operand(
Type: OperandType.Memory,
Text: text,
Value: null,
Register: null,
MemoryBase: null,
MemoryIndex: null,
MemoryScale: null,
MemoryDisplacement: null);
}
var inner = text.Substring(start + 1, end - start - 1);
// Parse components: base, index, scale, displacement
// Common patterns:
// x86: [rax], [rax+rbx], [rax+rbx*4], [rax+0x10], [rax+rbx*4+0x10]
// ARM: [x0], [x0, #8], [x0, x1], [x0, x1, lsl #2]
string? memBase = null;
string? memIndex = null;
int? memScale = null;
long? memDisp = null;
// Split by + or , depending on architecture style
var components = inner.Split(['+', ','], StringSplitOptions.RemoveEmptyEntries);
foreach (var comp in components)
{
var trimmed = comp.Trim();
// Check for scale pattern: reg*N
if (trimmed.Contains('*'))
{
var scaleParts = trimmed.Split('*');
if (scaleParts.Length == 2)
{
memIndex = scaleParts[0].Trim().ToUpperInvariant();
if (int.TryParse(scaleParts[1].Trim(), out var scale))
{
memScale = scale;
}
}
continue;
}
// Check for ARM immediate: #N
if (trimmed.StartsWith('#'))
{
var immText = trimmed.TrimStart('#');
if (immText.StartsWith("0x", StringComparison.OrdinalIgnoreCase))
{
if (long.TryParse(immText.AsSpan(2), System.Globalization.NumberStyles.HexNumber,
System.Globalization.CultureInfo.InvariantCulture, out var hexDisp))
{
memDisp = hexDisp;
}
}
else if (long.TryParse(immText, out var decDisp))
{
memDisp = decDisp;
}
continue;
}
// Check for hex displacement: 0xNN
if (trimmed.StartsWith("0x", StringComparison.OrdinalIgnoreCase))
{
if (long.TryParse(trimmed.AsSpan(2), System.Globalization.NumberStyles.HexNumber,
System.Globalization.CultureInfo.InvariantCulture, out var hexDisp))
{
memDisp = hexDisp;
}
continue;
}
// Check for negative displacement
if (trimmed.StartsWith('-'))
{
if (long.TryParse(trimmed, out var negDisp))
{
memDisp = negDisp;
}
continue;
}
// Must be a register
if (memBase == null)
{
memBase = trimmed.ToUpperInvariant();
}
else if (memIndex == null)
{
memIndex = trimmed.ToUpperInvariant();
}
}
return new Operand(
Type: OperandType.Memory,
Text: text,
Value: null,
Register: null,
MemoryBase: memBase,
MemoryIndex: memIndex,
MemoryScale: memScale,
MemoryDisplacement: memDisp);
}
private static InstructionKind ClassifyInstruction(IInstruction instr, string mnemonic)

View File

@@ -0,0 +1,384 @@
// Copyright (c) StellaOps. All rights reserved.
// Licensed under AGPL-3.0-or-later. See LICENSE in the project root.
// Sprint: SPRINT_20260112_004_BINIDX_b2r2_lowuir_perf_cache (BINIDX-LIFTER-02)
// Task: Bounded lifter pool with warm preload per ISA
using System.Collections.Concurrent;
using System.Collections.Immutable;
using System.Globalization;
using B2R2;
using B2R2.FrontEnd;
using B2R2.FrontEnd.BinLifter;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
namespace StellaOps.BinaryIndex.Disassembly.B2R2;
/// <summary>
/// Configuration options for the B2R2 lifter pool.
/// </summary>
public sealed class B2R2LifterPoolOptions
{
/// <summary>
/// Configuration section name.
/// </summary>
public const string SectionName = "StellaOps:BinaryIndex:B2R2LifterPool";
/// <summary>
/// Maximum number of pooled lifters per ISA.
/// </summary>
public int MaxPoolSizePerIsa { get; set; } = 4;
/// <summary>
/// Whether to warm preload lifters for common ISAs at startup.
/// </summary>
public bool EnableWarmPreload { get; set; } = true;
/// <summary>
/// ISAs to warm preload at startup.
/// </summary>
public ImmutableArray<string> WarmPreloadIsas { get; set; } =
[
"intel-64",
"intel-32",
"armv8-64",
"armv7-32"
];
/// <summary>
/// Timeout for acquiring a lifter from the pool.
/// </summary>
public TimeSpan AcquireTimeout { get; set; } = TimeSpan.FromSeconds(5);
}
/// <summary>
/// Pooled B2R2 BinHandle and LiftingUnit for reuse across calls.
/// </summary>
public sealed class PooledLifter : IDisposable
{
private readonly B2R2LifterPool _pool;
private readonly ISA _isa;
private bool _disposed;
internal PooledLifter(
B2R2LifterPool pool,
ISA isa,
BinHandle binHandle,
LiftingUnit liftingUnit)
{
_pool = pool ?? throw new ArgumentNullException(nameof(pool));
_isa = isa;
BinHandle = binHandle ?? throw new ArgumentNullException(nameof(binHandle));
LiftingUnit = liftingUnit ?? throw new ArgumentNullException(nameof(liftingUnit));
}
/// <summary>
/// The B2R2 BinHandle for this lifter.
/// </summary>
public BinHandle BinHandle { get; }
/// <summary>
/// The B2R2 LiftingUnit for this lifter.
/// </summary>
public LiftingUnit LiftingUnit { get; }
/// <summary>
/// Returns the lifter to the pool.
/// </summary>
public void Dispose()
{
if (_disposed) return;
_disposed = true;
_pool.Return(this, _isa);
}
}
/// <summary>
/// Bounded pool of B2R2 lifters with warm preload per ISA.
/// Thread-safe and designed for reuse in high-throughput scenarios.
/// </summary>
public sealed class B2R2LifterPool : IDisposable
{
private readonly ILogger<B2R2LifterPool> _logger;
private readonly B2R2LifterPoolOptions _options;
private readonly ConcurrentDictionary<string, ConcurrentBag<PooledLifterEntry>> _pools = new();
private readonly ConcurrentDictionary<string, int> _activeCount = new();
private readonly object _warmLock = new();
private bool _warmed;
private bool _disposed;
private sealed record PooledLifterEntry(BinHandle BinHandle, LiftingUnit LiftingUnit, DateTimeOffset CreatedAt);
/// <summary>
/// Creates a new B2R2 lifter pool.
/// </summary>
public B2R2LifterPool(
ILogger<B2R2LifterPool> logger,
IOptions<B2R2LifterPoolOptions> options)
{
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
_options = options?.Value ?? new B2R2LifterPoolOptions();
}
/// <summary>
/// Gets the current pool statistics.
/// </summary>
public B2R2LifterPoolStats GetStats()
{
var isaStats = new Dictionary<string, B2R2IsaPoolStats>();
foreach (var kvp in _pools)
{
var isaKey = kvp.Key;
var poolSize = kvp.Value.Count;
var activeCount = _activeCount.GetValueOrDefault(isaKey, 0);
isaStats[isaKey] = new B2R2IsaPoolStats(
PooledCount: poolSize,
ActiveCount: activeCount,
MaxPoolSize: _options.MaxPoolSizePerIsa);
}
return new B2R2LifterPoolStats(
TotalPooledLifters: _pools.Values.Sum(b => b.Count),
TotalActiveLifters: _activeCount.Values.Sum(),
IsWarm: _warmed,
IsaStats: isaStats.ToImmutableDictionary());
}
/// <summary>
/// Warms the pool by preloading lifters for common ISAs.
/// </summary>
public void WarmPool()
{
if (!_options.EnableWarmPreload) return;
if (_warmed) return;
lock (_warmLock)
{
if (_warmed) return;
_logger.LogInformation(
"Warming B2R2 lifter pool for {IsaCount} ISAs",
_options.WarmPreloadIsas.Length);
foreach (var isaKey in _options.WarmPreloadIsas)
{
try
{
var isa = ParseIsaKey(isaKey);
if (isa is null)
{
_logger.LogWarning("Unknown ISA key for warm preload: {IsaKey}", isaKey);
continue;
}
// Create and pool a lifter for this ISA
var entry = CreateLifterEntry(isa);
var pool = GetOrCreatePool(GetIsaKey(isa));
pool.Add(entry);
_logger.LogDebug("Warmed lifter for ISA: {IsaKey}", isaKey);
}
catch (Exception ex)
{
_logger.LogWarning(ex, "Failed to warm lifter for ISA: {IsaKey}", isaKey);
}
}
_warmed = true;
_logger.LogInformation("B2R2 lifter pool warm complete");
}
}
/// <summary>
/// Acquires a lifter for the specified ISA.
/// </summary>
public PooledLifter Acquire(ISA isa)
{
ObjectDisposedException.ThrowIf(_disposed, this);
var isaKey = GetIsaKey(isa);
var pool = GetOrCreatePool(isaKey);
// Try to get an existing lifter from the pool
if (pool.TryTake(out var entry))
{
IncrementActive(isaKey);
_logger.LogTrace("Acquired pooled lifter for {Isa}", isaKey);
return new PooledLifter(this, isa, entry.BinHandle, entry.LiftingUnit);
}
// Create a new lifter
var newEntry = CreateLifterEntry(isa);
IncrementActive(isaKey);
_logger.LogTrace("Created new lifter for {Isa}", isaKey);
return new PooledLifter(this, isa, newEntry.BinHandle, newEntry.LiftingUnit);
}
/// <summary>
/// Returns a lifter to the pool.
/// </summary>
internal void Return(PooledLifter lifter, ISA isa)
{
var isaKey = GetIsaKey(isa);
DecrementActive(isaKey);
var pool = GetOrCreatePool(isaKey);
// Only return to pool if under limit
if (pool.Count < _options.MaxPoolSizePerIsa)
{
var entry = new PooledLifterEntry(
lifter.BinHandle,
lifter.LiftingUnit,
DateTimeOffset.UtcNow);
pool.Add(entry);
_logger.LogTrace("Returned lifter to pool for {Isa}", isaKey);
}
else
{
_logger.LogTrace("Pool full, discarding lifter for {Isa}", isaKey);
}
}
/// <inheritdoc />
public void Dispose()
{
if (_disposed) return;
_disposed = true;
_pools.Clear();
_activeCount.Clear();
_logger.LogInformation("B2R2 lifter pool disposed");
}
#region Private Helpers
private static string GetIsaKey(ISA isa) =>
string.Format(
CultureInfo.InvariantCulture,
"{0}-{1}",
isa.Arch.ToString().ToLowerInvariant(),
isa.WordSize == WordSize.Bit64 ? "64" : "32");
private static ISA? ParseIsaKey(string key)
{
var parts = key.Split('-');
if (parts.Length != 2) return null;
var archStr = parts[0].ToLowerInvariant();
var bits = parts[1];
var wordSize = bits == "64" ? WordSize.Bit64 : WordSize.Bit32;
return archStr switch
{
"intel" => new ISA(Architecture.Intel, wordSize),
"armv7" => new ISA(Architecture.ARMv7, wordSize),
"armv8" => new ISA(Architecture.ARMv8, wordSize),
"mips" => new ISA(Architecture.MIPS, wordSize),
"riscv" => new ISA(Architecture.RISCV, wordSize),
"ppc" => new ISA(Architecture.PPC, Endian.Big, wordSize),
"sparc" => new ISA(Architecture.SPARC, Endian.Big),
_ => (ISA?)null
};
}
private ConcurrentBag<PooledLifterEntry> GetOrCreatePool(string isaKey) =>
_pools.GetOrAdd(isaKey, _ => new ConcurrentBag<PooledLifterEntry>());
private static PooledLifterEntry CreateLifterEntry(ISA isa)
{
// Create a minimal BinHandle for the ISA
// Use a small NOP sled as placeholder code
var nopBytes = CreateNopSled(isa, 64);
var binHandle = new BinHandle(nopBytes, isa, null, true);
var liftingUnit = binHandle.NewLiftingUnit();
return new PooledLifterEntry(binHandle, liftingUnit, DateTimeOffset.UtcNow);
}
private static byte[] CreateNopSled(ISA isa, int size)
{
var bytes = new byte[size];
// Fill with architecture-appropriate NOP bytes
switch (isa.Arch)
{
case Architecture.Intel:
// x86/x64 NOP = 0x90
Array.Fill(bytes, (byte)0x90);
break;
case Architecture.ARMv7:
case Architecture.ARMv8:
// ARM NOP = 0x00000000 or 0x1F 20 03 D5 (ARM64)
if (isa.WordSize == WordSize.Bit64)
{
for (var i = 0; i + 3 < size; i += 4)
{
bytes[i] = 0x1F;
bytes[i + 1] = 0x20;
bytes[i + 2] = 0x03;
bytes[i + 3] = 0xD5;
}
}
else
{
// ARM32 NOP = 0xE320F000 (big endian) or 0x00 F0 20 E3 (little)
for (var i = 0; i + 3 < size; i += 4)
{
bytes[i] = 0x00;
bytes[i + 1] = 0xF0;
bytes[i + 2] = 0x20;
bytes[i + 3] = 0xE3;
}
}
break;
default:
// Generic zeroes for other architectures
Array.Fill(bytes, (byte)0x00);
break;
}
return bytes;
}
private void IncrementActive(string isaKey)
{
_activeCount.AddOrUpdate(isaKey, 1, (_, v) => v + 1);
}
private void DecrementActive(string isaKey)
{
_activeCount.AddOrUpdate(isaKey, 0, (_, v) => Math.Max(0, v - 1));
}
#endregion
}
/// <summary>
/// Statistics for the B2R2 lifter pool.
/// </summary>
/// <param name="TotalPooledLifters">Total lifters currently in pool.</param>
/// <param name="TotalActiveLifters">Total lifters currently in use.</param>
/// <param name="IsWarm">Whether the pool has been warmed.</param>
/// <param name="IsaStats">Per-ISA pool statistics.</param>
public sealed record B2R2LifterPoolStats(
int TotalPooledLifters,
int TotalActiveLifters,
bool IsWarm,
ImmutableDictionary<string, B2R2IsaPoolStats> IsaStats);
/// <summary>
/// Per-ISA pool statistics.
/// </summary>
/// <param name="PooledCount">Number of lifters in pool for this ISA.</param>
/// <param name="ActiveCount">Number of lifters currently in use for this ISA.</param>
/// <param name="MaxPoolSize">Maximum pool size for this ISA.</param>
public sealed record B2R2IsaPoolStats(
int PooledCount,
int ActiveCount,
int MaxPoolSize);

View File

@@ -0,0 +1,697 @@
// Copyright (c) StellaOps. All rights reserved.
// Licensed under AGPL-3.0-or-later. See LICENSE in the project root.
// Sprint: SPRINT_20260112_004_BINIDX_b2r2_lowuir_perf_cache (BINIDX-LIR-01)
// Task: Implement B2R2 LowUIR adapter for IIrLiftingService
using System.Collections.Immutable;
using System.Globalization;
using B2R2;
using B2R2.FrontEnd;
using Microsoft.Extensions.Logging;
using StellaOps.BinaryIndex.Disassembly;
using StellaOps.BinaryIndex.Semantic;
namespace StellaOps.BinaryIndex.Disassembly.B2R2;
/// <summary>
/// B2R2 LowUIR adapter for the IR lifting service.
/// Maps B2R2 BinIR/LowUIR statements to the StellaOps IR model
/// with deterministic ordering and invariant formatting.
/// </summary>
public sealed class B2R2LowUirLiftingService : IIrLiftingService
{
private readonly ILogger<B2R2LowUirLiftingService> _logger;
/// <summary>
/// Version string for cache key generation.
/// </summary>
public const string AdapterVersion = "1.0.0";
private static readonly ImmutableHashSet<CpuArchitecture> SupportedArchitectures =
[
CpuArchitecture.X86,
CpuArchitecture.X86_64,
CpuArchitecture.ARM32,
CpuArchitecture.ARM64,
CpuArchitecture.MIPS32,
CpuArchitecture.MIPS64,
CpuArchitecture.RISCV64,
CpuArchitecture.PPC32,
CpuArchitecture.SPARC
];
public B2R2LowUirLiftingService(ILogger<B2R2LowUirLiftingService> logger)
{
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
}
/// <inheritdoc />
public bool SupportsArchitecture(CpuArchitecture architecture) =>
SupportedArchitectures.Contains(architecture);
/// <inheritdoc />
public Task<LiftedFunction> LiftToIrAsync(
IReadOnlyList<DisassembledInstruction> instructions,
string functionName,
ulong startAddress,
CpuArchitecture architecture,
LiftOptions? options = null,
CancellationToken ct = default)
{
ArgumentNullException.ThrowIfNull(instructions);
ct.ThrowIfCancellationRequested();
options ??= LiftOptions.Default;
if (!SupportsArchitecture(architecture))
{
throw new NotSupportedException(
$"Architecture {architecture} is not supported for B2R2 LowUIR lifting.");
}
_logger.LogDebug(
"B2R2 LowUIR lifting {InstructionCount} instructions for function {FunctionName} ({Architecture})",
instructions.Count,
functionName,
architecture);
var isa = MapToB2R2Isa(architecture);
var statements = new List<IrStatement>();
var basicBlocks = new List<IrBasicBlock>();
var currentBlockStatements = new List<int>();
var blockStartAddress = startAddress;
var statementId = 0;
var blockId = 0;
var effectiveMaxInstructions = options.MaxInstructions > 0
? options.MaxInstructions
: int.MaxValue;
foreach (var instr in instructions.Take(effectiveMaxInstructions))
{
ct.ThrowIfCancellationRequested();
// Lift instruction to B2R2 LowUIR
var liftedStatements = LiftInstructionToLowUir(isa, instr, ref statementId);
statements.AddRange(liftedStatements);
foreach (var stmt in liftedStatements)
{
currentBlockStatements.Add(stmt.Id);
}
// Check for block-ending instructions
if (IsBlockTerminator(instr))
{
var endAddress = instr.Address + (ulong)instr.RawBytes.Length;
var block = new IrBasicBlock(
Id: blockId,
Label: string.Format(CultureInfo.InvariantCulture, "bb_{0}", blockId),
StartAddress: blockStartAddress,
EndAddress: endAddress,
StatementIds: [.. currentBlockStatements],
Predecessors: ImmutableArray<int>.Empty,
Successors: ImmutableArray<int>.Empty);
basicBlocks.Add(block);
blockId++;
currentBlockStatements.Clear();
blockStartAddress = endAddress;
}
}
// Handle trailing statements not yet in a block
if (currentBlockStatements.Count > 0 && instructions.Count > 0)
{
var lastInstr = instructions[^1];
var endAddress = lastInstr.Address + (ulong)lastInstr.RawBytes.Length;
var block = new IrBasicBlock(
Id: blockId,
Label: string.Format(CultureInfo.InvariantCulture, "bb_{0}", blockId),
StartAddress: blockStartAddress,
EndAddress: endAddress,
StatementIds: [.. currentBlockStatements],
Predecessors: ImmutableArray<int>.Empty,
Successors: ImmutableArray<int>.Empty);
basicBlocks.Add(block);
}
// Build CFG edges deterministically (sorted by address)
var (blocksWithEdges, edges) = BuildCfgEdges([.. basicBlocks]);
var cfg = new ControlFlowGraph(
EntryBlockId: blocksWithEdges.Length > 0 ? 0 : -1,
ExitBlockIds: FindExitBlocks(blocksWithEdges),
Edges: edges);
var lifted = new LiftedFunction(
Name: functionName,
Address: startAddress,
Statements: [.. statements],
BasicBlocks: blocksWithEdges,
Cfg: cfg);
_logger.LogDebug(
"B2R2 LowUIR lifted {StatementCount} statements in {BlockCount} blocks for {FunctionName}",
statements.Count,
blocksWithEdges.Length,
functionName);
return Task.FromResult(lifted);
}
/// <inheritdoc />
public Task<SsaFunction> TransformToSsaAsync(
LiftedFunction lifted,
CancellationToken ct = default)
{
ArgumentNullException.ThrowIfNull(lifted);
ct.ThrowIfCancellationRequested();
_logger.LogDebug(
"Transforming {FunctionName} to SSA form ({StatementCount} statements)",
lifted.Name,
lifted.Statements.Length);
// Build SSA form from lifted function
var ssaStatements = new List<SsaStatement>();
var ssaBlocks = new List<SsaBasicBlock>();
var definitions = new Dictionary<SsaVariable, int>();
var uses = new Dictionary<SsaVariable, HashSet<int>>();
var versionCounters = new Dictionary<string, int>();
foreach (var stmt in lifted.Statements)
{
ct.ThrowIfCancellationRequested();
SsaVariable? destVar = null;
var sourceVars = new List<SsaVariable>();
// Process destination
if (stmt.Destination != null)
{
var varName = stmt.Destination.Name ?? "?";
if (!versionCounters.TryGetValue(varName, out var version))
{
version = 0;
}
versionCounters[varName] = version + 1;
destVar = new SsaVariable(
BaseName: varName,
Version: version + 1,
BitSize: stmt.Destination.BitSize,
Kind: MapOperandKindToSsaKind(stmt.Destination.Kind));
definitions[destVar] = stmt.Id;
}
// Process sources
foreach (var src in stmt.Sources)
{
var varName = src.Name ?? "?";
var currentVersion = versionCounters.GetValueOrDefault(varName, 0);
var ssaVar = new SsaVariable(
BaseName: varName,
Version: currentVersion,
BitSize: src.BitSize,
Kind: MapOperandKindToSsaKind(src.Kind));
sourceVars.Add(ssaVar);
if (!uses.ContainsKey(ssaVar))
{
uses[ssaVar] = [];
}
uses[ssaVar].Add(stmt.Id);
}
var ssaStmt = new SsaStatement(
Id: stmt.Id,
Address: stmt.Address,
Kind: stmt.Kind,
Operation: stmt.Operation,
Destination: destVar,
Sources: [.. sourceVars],
PhiSources: null);
ssaStatements.Add(ssaStmt);
}
// Build SSA basic blocks from lifted blocks
foreach (var block in lifted.BasicBlocks)
{
var blockStatements = ssaStatements
.Where(s => block.StatementIds.Contains(s.Id))
.ToImmutableArray();
var ssaBlock = new SsaBasicBlock(
Id: block.Id,
Label: block.Label,
PhiNodes: ImmutableArray<SsaStatement>.Empty,
Statements: blockStatements,
Predecessors: block.Predecessors,
Successors: block.Successors);
ssaBlocks.Add(ssaBlock);
}
var defUse = new DefUseChains(
Definitions: definitions.ToImmutableDictionary(),
Uses: uses.ToImmutableDictionary(
k => k.Key,
v => v.Value.ToImmutableHashSet()));
var ssaFunction = new SsaFunction(
Name: lifted.Name,
Address: lifted.Address,
Statements: [.. ssaStatements],
BasicBlocks: [.. ssaBlocks],
DefUse: defUse);
_logger.LogDebug(
"SSA transformation complete: {StatementCount} SSA statements, {DefCount} definitions",
ssaStatements.Count,
definitions.Count);
return Task.FromResult(ssaFunction);
}
#region B2R2 LowUIR Mapping
private List<IrStatement> LiftInstructionToLowUir(
ISA isa,
DisassembledInstruction instr,
ref int statementId)
{
var statements = new List<IrStatement>();
try
{
// Create B2R2 BinHandle and lifting unit for the ISA
var bytes = instr.RawBytes.ToArray();
var binHandle = new BinHandle(bytes, isa, null, true);
var lifter = binHandle.NewLiftingUnit();
// Lift to LowUIR using B2R2 - returns Stmt[] directly
var liftResult = lifter.LiftInstruction(instr.Address);
if (liftResult == null || liftResult.Length == 0)
{
// Fallback to simple mapping if B2R2 lift fails
statements.Add(CreateFallbackStatement(instr, statementId++));
return statements;
}
// Map each B2R2 LowUIR statement to our IR model
foreach (var b2r2Stmt in liftResult)
{
var irStmt = MapB2R2Statement(b2r2Stmt, instr.Address, ref statementId);
if (irStmt != null)
{
statements.Add(irStmt);
}
}
// Ensure at least one statement per instruction for determinism
if (statements.Count == 0)
{
statements.Add(CreateFallbackStatement(instr, statementId++));
}
}
catch (Exception ex)
{
_logger.LogWarning(
ex,
"B2R2 lift failed for instruction at {Address}: {Mnemonic}",
instr.Address,
instr.Mnemonic);
statements.Add(CreateFallbackStatement(instr, statementId++));
}
return statements;
}
private IrStatement? MapB2R2Statement(object b2r2Stmt, ulong baseAddress, ref int statementId)
{
// B2R2 LowUIR statement types:
// - Put: register assignment
// - Store: memory write
// - Jmp: unconditional jump
// - CJmp: conditional jump
// - InterJmp: indirect jump
// - InterCJmp: indirect conditional jump
// - LMark: label marker
// - SideEffect: side effects (syscall, fence, etc.)
var stmtType = b2r2Stmt.GetType().Name;
var kind = MapB2R2StmtTypeToKind(stmtType);
if (kind == IrStatementKind.Unknown)
{
return null;
}
var (dest, sources) = ExtractOperandsFromB2R2Stmt(b2r2Stmt);
var operation = stmtType;
return new IrStatement(
Id: statementId++,
Address: baseAddress,
Kind: kind,
Operation: operation,
Destination: dest,
Sources: sources,
Metadata: null);
}
private static IrStatementKind MapB2R2StmtTypeToKind(string stmtType) => stmtType switch
{
"Put" => IrStatementKind.Assign,
"Store" => IrStatementKind.Store,
"Jmp" => IrStatementKind.Jump,
"CJmp" => IrStatementKind.ConditionalJump,
"InterJmp" => IrStatementKind.Jump,
"InterCJmp" => IrStatementKind.ConditionalJump,
"LMark" => IrStatementKind.Nop,
"SideEffect" => IrStatementKind.Syscall,
_ => IrStatementKind.Unknown
};
private static (IrOperand? Dest, ImmutableArray<IrOperand> Sources) ExtractOperandsFromB2R2Stmt(object b2r2Stmt)
{
IrOperand? dest = null;
var sources = new List<IrOperand>();
var type = b2r2Stmt.GetType();
// Try to extract destination
var destProp = type.GetProperty("Dest");
if (destProp != null)
{
var destVal = destProp.GetValue(b2r2Stmt);
if (destVal != null)
{
dest = CreateOperandFromB2R2Expr(destVal);
}
}
// Try to extract source/value
var srcProp = type.GetProperty("Value") ?? type.GetProperty("Src");
if (srcProp != null)
{
var srcVal = srcProp.GetValue(b2r2Stmt);
if (srcVal != null)
{
sources.Add(CreateOperandFromB2R2Expr(srcVal));
}
}
// Try to extract condition for conditional jumps
var condProp = type.GetProperty("Cond");
if (condProp != null)
{
var condVal = condProp.GetValue(b2r2Stmt);
if (condVal != null)
{
sources.Add(CreateOperandFromB2R2Expr(condVal));
}
}
return (dest, [.. sources]);
}
private static IrOperand CreateOperandFromB2R2Expr(object expr)
{
var exprType = expr.GetType().Name;
return exprType switch
{
"Var" => new IrOperand(
Kind: IrOperandKind.Register,
Name: GetVarName(expr),
Value: null,
BitSize: GetVarBitWidth(expr),
IsMemory: false),
"TempVar" => new IrOperand(
Kind: IrOperandKind.Temporary,
Name: GetTempVarName(expr),
Value: null,
BitSize: GetVarBitWidth(expr),
IsMemory: false),
"Num" => new IrOperand(
Kind: IrOperandKind.Immediate,
Name: null,
Value: GetNumValueLong(expr),
BitSize: GetNumBitWidth(expr),
IsMemory: false),
"Load" => new IrOperand(
Kind: IrOperandKind.Memory,
Name: "[mem]",
Value: null,
BitSize: GetLoadBitWidth(expr),
IsMemory: true),
_ => new IrOperand(
Kind: IrOperandKind.Unknown,
Name: exprType,
Value: null,
BitSize: 64,
IsMemory: false)
};
}
private static string GetVarName(object varExpr)
{
var nameProp = varExpr.GetType().GetProperty("Name");
return nameProp?.GetValue(varExpr)?.ToString() ?? "?";
}
private static string GetTempVarName(object tempVarExpr)
{
var numProp = tempVarExpr.GetType().GetProperty("N");
var num = numProp?.GetValue(tempVarExpr) ?? 0;
return string.Format(CultureInfo.InvariantCulture, "T{0}", num);
}
private static int GetVarBitWidth(object varExpr)
{
var typeProp = varExpr.GetType().GetProperty("Type");
if (typeProp == null) return 64;
var regType = typeProp.GetValue(varExpr);
var bitSizeProp = regType?.GetType().GetProperty("BitSize");
return (int?)bitSizeProp?.GetValue(regType) ?? 64;
}
private static long GetNumValueLong(object numExpr)
{
var valueProp = numExpr.GetType().GetProperty("Value");
var value = valueProp?.GetValue(numExpr);
return Convert.ToInt64(value, CultureInfo.InvariantCulture);
}
private static int GetNumBitWidth(object numExpr)
{
var typeProp = numExpr.GetType().GetProperty("Type");
if (typeProp == null) return 64;
var numType = typeProp.GetValue(numExpr);
var bitSizeProp = numType?.GetType().GetProperty("BitSize");
return (int?)bitSizeProp?.GetValue(numType) ?? 64;
}
private static int GetLoadBitWidth(object loadExpr)
{
var typeProp = loadExpr.GetType().GetProperty("Type");
if (typeProp == null) return 64;
var loadType = typeProp.GetValue(loadExpr);
var bitSizeProp = loadType?.GetType().GetProperty("BitSize");
return (int?)bitSizeProp?.GetValue(loadType) ?? 64;
}
private static IrStatement CreateFallbackStatement(DisassembledInstruction instr, int id)
{
var sources = instr.Operands.Skip(1)
.Select(op => new IrOperand(
Kind: MapOperandType(op.Type),
Name: op.Text,
Value: op.Value,
BitSize: 64,
IsMemory: op.Type == OperandType.Memory))
.ToImmutableArray();
var dest = instr.Operands.Length > 0
? new IrOperand(
Kind: MapOperandType(instr.Operands[0].Type),
Name: instr.Operands[0].Text,
Value: instr.Operands[0].Value,
BitSize: 64,
IsMemory: instr.Operands[0].Type == OperandType.Memory)
: null;
return new IrStatement(
Id: id,
Address: instr.Address,
Kind: MapMnemonicToKind(instr.Mnemonic),
Operation: instr.Mnemonic,
Destination: dest,
Sources: sources,
Metadata: ImmutableDictionary<string, object>.Empty.Add("fallback", true));
}
private static SsaVariableKind MapOperandKindToSsaKind(IrOperandKind kind) => kind switch
{
IrOperandKind.Register => SsaVariableKind.Register,
IrOperandKind.Temporary => SsaVariableKind.Temporary,
IrOperandKind.Memory => SsaVariableKind.Memory,
IrOperandKind.Immediate => SsaVariableKind.Constant,
_ => SsaVariableKind.Temporary
};
private static IrOperandKind MapOperandType(OperandType type) => type switch
{
OperandType.Register => IrOperandKind.Register,
OperandType.Immediate => IrOperandKind.Immediate,
OperandType.Memory => IrOperandKind.Memory,
OperandType.Address => IrOperandKind.Label,
_ => IrOperandKind.Unknown
};
#endregion
#region Helper Methods
private static ISA MapToB2R2Isa(CpuArchitecture arch) => arch switch
{
CpuArchitecture.X86 => new ISA(Architecture.Intel, WordSize.Bit32),
CpuArchitecture.X86_64 => new ISA(Architecture.Intel, WordSize.Bit64),
CpuArchitecture.ARM32 => new ISA(Architecture.ARMv7, WordSize.Bit32),
CpuArchitecture.ARM64 => new ISA(Architecture.ARMv8, WordSize.Bit64),
CpuArchitecture.MIPS32 => new ISA(Architecture.MIPS, WordSize.Bit32),
CpuArchitecture.MIPS64 => new ISA(Architecture.MIPS, WordSize.Bit64),
CpuArchitecture.RISCV64 => new ISA(Architecture.RISCV, WordSize.Bit64),
CpuArchitecture.PPC32 => new ISA(Architecture.PPC, Endian.Big, WordSize.Bit32),
CpuArchitecture.SPARC => new ISA(Architecture.SPARC, Endian.Big),
_ => throw new NotSupportedException($"Unsupported architecture: {arch}")
};
private static bool IsBlockTerminator(DisassembledInstruction instr)
{
var mnemonic = instr.Mnemonic.ToUpperInvariant();
return mnemonic.StartsWith("J", StringComparison.Ordinal) ||
mnemonic.StartsWith("B", StringComparison.Ordinal) ||
mnemonic == "RET" ||
mnemonic == "RETN" ||
mnemonic == "RETF" ||
mnemonic == "IRET" ||
mnemonic == "SYSRET" ||
mnemonic == "BLR" ||
mnemonic == "BX" ||
mnemonic == "JR";
}
private static IrStatementKind MapMnemonicToKind(string mnemonic)
{
var upper = mnemonic.ToUpperInvariant();
if (upper.StartsWith("MOV", StringComparison.Ordinal) ||
upper.StartsWith("LEA", StringComparison.Ordinal) ||
upper.StartsWith("LDR", StringComparison.Ordinal))
return IrStatementKind.Assign;
if (upper.StartsWith("ADD", StringComparison.Ordinal) ||
upper.StartsWith("SUB", StringComparison.Ordinal) ||
upper.StartsWith("MUL", StringComparison.Ordinal) ||
upper.StartsWith("DIV", StringComparison.Ordinal))
return IrStatementKind.BinaryOp;
if (upper.StartsWith("AND", StringComparison.Ordinal) ||
upper.StartsWith("OR", StringComparison.Ordinal) ||
upper.StartsWith("XOR", StringComparison.Ordinal) ||
upper.StartsWith("SH", StringComparison.Ordinal))
return IrStatementKind.BinaryOp;
if (upper.StartsWith("CMP", StringComparison.Ordinal) ||
upper.StartsWith("TEST", StringComparison.Ordinal))
return IrStatementKind.Compare;
if (upper.StartsWith("J", StringComparison.Ordinal) ||
upper.StartsWith("B", StringComparison.Ordinal))
return IrStatementKind.ConditionalJump;
if (upper == "CALL" || upper == "BL" || upper == "BLX")
return IrStatementKind.Call;
if (upper == "RET" || upper == "RETN" || upper == "BLR")
return IrStatementKind.Return;
if (upper.StartsWith("PUSH", StringComparison.Ordinal) ||
upper.StartsWith("POP", StringComparison.Ordinal) ||
upper.StartsWith("STR", StringComparison.Ordinal))
return IrStatementKind.Store;
if (upper == "NOP")
return IrStatementKind.Nop;
return IrStatementKind.Unknown;
}
private static (ImmutableArray<IrBasicBlock> Blocks, ImmutableArray<CfgEdge> Edges) BuildCfgEdges(
ImmutableArray<IrBasicBlock> blocks)
{
if (blocks.Length == 0)
return (blocks, ImmutableArray<CfgEdge>.Empty);
var result = new IrBasicBlock[blocks.Length];
var edges = new List<CfgEdge>();
for (var i = 0; i < blocks.Length; i++)
{
var block = blocks[i];
var predecessors = new List<int>();
var successors = new List<int>();
// Fall-through successor (next block in sequence)
if (i < blocks.Length - 1)
{
successors.Add(i + 1);
edges.Add(new CfgEdge(
SourceBlockId: i,
TargetBlockId: i + 1,
Kind: CfgEdgeKind.FallThrough,
Condition: null));
}
// Predecessor from fall-through
if (i > 0)
{
predecessors.Add(i - 1);
}
result[i] = block with
{
Predecessors = [.. predecessors.Distinct().OrderBy(x => x)],
Successors = [.. successors.Distinct().OrderBy(x => x)]
};
}
return ([.. result], [.. edges]);
}
private static ImmutableArray<int> FindExitBlocks(ImmutableArray<IrBasicBlock> blocks)
{
return blocks
.Where(b => b.Successors.Length == 0)
.Select(b => b.Id)
.ToImmutableArray();
}
#endregion
}

View File

@@ -1,8 +1,11 @@
// Copyright (c) StellaOps. All rights reserved.
// Licensed under AGPL-3.0-or-later. See LICENSE in the project root.
// Sprint: SPRINT_20260112_004_BINIDX_b2r2_lowuir_perf_cache (BINIDX-LIFTER-02)
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;
using StellaOps.BinaryIndex.Semantic;
namespace StellaOps.BinaryIndex.Disassembly.B2R2;
@@ -25,4 +28,66 @@ public static class B2R2ServiceCollectionExtensions
return services;
}
/// <summary>
/// Adds the B2R2 lifter pool to the service collection.
/// Provides pooled lifters with warm preload for improved performance.
/// </summary>
/// <param name="services">The service collection.</param>
/// <param name="configuration">Configuration for binding pool options.</param>
/// <returns>The service collection for chaining.</returns>
public static IServiceCollection AddB2R2LifterPool(
this IServiceCollection services,
IConfiguration? configuration = null)
{
ArgumentNullException.ThrowIfNull(services);
if (configuration != null)
{
services.Configure<B2R2LifterPoolOptions>(
configuration.GetSection(B2R2LifterPoolOptions.SectionName));
}
else
{
services.Configure<B2R2LifterPoolOptions>(_ => { });
}
services.TryAddSingleton<B2R2LifterPool>();
return services;
}
/// <summary>
/// Adds the B2R2 LowUIR lifting service to the service collection.
/// Provides IR lifting with B2R2 LowUIR semantics.
/// </summary>
/// <param name="services">The service collection.</param>
/// <returns>The service collection for chaining.</returns>
public static IServiceCollection AddB2R2LowUirLiftingService(this IServiceCollection services)
{
ArgumentNullException.ThrowIfNull(services);
services.TryAddSingleton<IIrLiftingService, B2R2LowUirLiftingService>();
return services;
}
/// <summary>
/// Adds all B2R2 services to the service collection.
/// </summary>
/// <param name="services">The service collection.</param>
/// <param name="configuration">Configuration for binding options.</param>
/// <returns>The service collection for chaining.</returns>
public static IServiceCollection AddB2R2Services(
this IServiceCollection services,
IConfiguration? configuration = null)
{
ArgumentNullException.ThrowIfNull(services);
services.AddB2R2DisassemblyPlugin();
services.AddB2R2LifterPool(configuration);
services.AddB2R2LowUirLiftingService();
return services;
}
}

View File

@@ -11,6 +11,8 @@
<ItemGroup>
<ProjectReference Include="..\StellaOps.BinaryIndex.Disassembly.Abstractions\StellaOps.BinaryIndex.Disassembly.Abstractions.csproj" />
<!-- Sprint: SPRINT_20260112_004_BINIDX_b2r2_lowuir_perf_cache (BINIDX-LIR-01) -->
<ProjectReference Include="..\StellaOps.BinaryIndex.Semantic\StellaOps.BinaryIndex.Semantic.csproj" />
</ItemGroup>
<ItemGroup>