new advisories work and features gaps work
This commit is contained in:
@@ -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
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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" />
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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);
|
||||
@@ -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
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user