using System.Collections.Generic;
using System.Globalization;
using System.Reflection;
using Iced.Intel;
namespace WhiteMagic.Assembly;
///
/// Optional backend that assembles arbitrary x86/x64 mnemonic
/// text to machine code using the Iced library, and provides full instruction-boundary
/// decoding for detour prologue validation.
///
///
/// Iced ships a fluent code assembler (typed method calls) and a decoder, but no
/// text parser. This class bridges Intel-syntax text onto Iced's fluent
/// by reflection: each line's mnemonic selects the matching
/// method and its operands are bound to registers, immediates, or
/// labels. Register and immediate operands and label-relative branches are supported;
/// memory operands ([reg+disp]) are not — a caller needing those should emit bytes
/// directly.
/// This backend is entirely optional. Constructing it is the only thing that pulls
/// Iced into a behavioral path; the default never references it.
///
public sealed class IcedAssembler : IAssembler
{
private const int DefaultBitness = 64;
private readonly int _bitness;
// Lowercased register name -> boxed AssemblerRegisterNN value, built once from
// Iced's AssemblerRegisters. Enables binding a text operand like "esp" to a typed
// fluent-API register argument.
private static readonly Dictionary Registers = BuildRegisterMap();
/// Creates an assembler for the given bitness (32 or 64).
/// 32 for x86, 64 for x64. Defaults to 64.
public IcedAssembler(int bitness = DefaultBitness)
{
if (bitness != 32 && bitness != 64)
throw new ArgumentOutOfRangeException(nameof(bitness), "Bitness must be 32 or 64.");
_bitness = bitness;
}
///
public byte[] Assemble(string assemblyText, ulong origin = 0)
{
ArgumentNullException.ThrowIfNull(assemblyText);
var assembler = new Assembler(_bitness);
List<(string Mnemonic, string[] Operands)> lines = Tokenize(assemblyText, out var labelNames);
// Pre-create every label so a forward branch can reference it before its definition.
var labels = new Dictionary(StringComparer.OrdinalIgnoreCase);
foreach (string name in labelNames)
labels[name] = assembler.CreateLabel(name);
foreach ((string mnemonic, string[] operands) in lines)
{
// A pure label definition (e.g. "loop:") marks the current position.
if (mnemonic.EndsWith(':'))
{
Label label = labels[mnemonic[..^1]];
assembler.Label(ref label);
continue;
}
EmitInstruction(assembler, mnemonic, operands, labels);
}
var writer = new ByteListCodeWriter();
assembler.Assemble(writer, origin);
return writer.Bytes.ToArray();
}
///
/// Computes the number of whole prologue-instruction bytes that must be preserved for
/// a splice of bytes, decoding arbitrary instructions
/// (not just the common prologue shapes the built-in decoder covers). Matches the
/// PrologueLengthResolver delegate so it can be assigned to
/// .
///
/// A prologue byte sequence does not decode
/// to a valid instruction.
public int GetPrologueLength(byte[] prologue, int requiredBytes, bool is64Bit)
{
ArgumentNullException.ThrowIfNull(prologue);
var reader = new ByteArrayCodeReader(prologue);
var decoder = Decoder.Create(is64Bit ? 64 : 32, reader);
int total = 0;
while (total < requiredBytes)
{
decoder.Decode(out Instruction instruction);
if (instruction.IsInvalid)
{
throw new InvalidOperationException(
"The target prologue contains a byte sequence that does not decode to a valid instruction.");
}
total += instruction.Length;
}
return total;
}
private void EmitInstruction(
Assembler assembler,
string mnemonic,
string[] operandText,
Dictionary labels)
{
object?[] operands = new object?[operandText.Length];
for (int i = 0; i < operandText.Length; i++)
operands[i] = ParseOperand(operandText[i], labels);
// Find the fluent Assembler method whose name equals the mnemonic and whose
// parameters bind to the parsed operands.
foreach (MethodInfo method in typeof(Assembler).GetMethods(BindingFlags.Public | BindingFlags.Instance))
{
if (!string.Equals(method.Name, mnemonic, StringComparison.OrdinalIgnoreCase))
continue;
ParameterInfo[] parameters = method.GetParameters();
if (parameters.Length != operands.Length)
continue;
if (TryBind(parameters, operands, out object?[]? boundArgs))
{
method.Invoke(assembler, boundArgs);
return;
}
}
throw new NotSupportedException(
$"Cannot assemble '{mnemonic}{(operandText.Length > 0 ? " " + string.Join(", ", operandText) : "")}': " +
"no matching Iced assembler overload for the given operands (registers, immediates and " +
"labels are supported; memory operands are not).");
}
private static bool TryBind(ParameterInfo[] parameters, object?[] operands, out object?[]? boundArgs)
{
var args = new object?[parameters.Length];
for (int i = 0; i < parameters.Length; i++)
{
Type paramType = parameters[i].ParameterType;
object? operand = operands[i];
switch (operand)
{
case Immediate imm when IsNumeric(paramType):
args[i] = Convert.ChangeType(imm.Value, paramType, CultureInfo.InvariantCulture);
break;
case not null when paramType.IsInstanceOfType(operand):
args[i] = operand;
break;
default:
boundArgs = null;
return false;
}
}
boundArgs = args;
return true;
}
private static object ParseOperand(string text, Dictionary labels)
{
string token = text.Trim();
if (Registers.TryGetValue(token, out object? register))
return register;
if (labels.TryGetValue(token, out Label label))
return label;
if (TryParseImmediate(token, out long value))
return new Immediate(value);
throw new NotSupportedException(
$"Unrecognized operand '{token}' (expected a register, an immediate, or a label).");
}
private static bool TryParseImmediate(string token, out long value)
{
bool negative = token.StartsWith('-');
string body = negative ? token[1..] : token;
bool ok;
if (body.StartsWith("0x", StringComparison.OrdinalIgnoreCase))
ok = long.TryParse(body[2..], NumberStyles.HexNumber, CultureInfo.InvariantCulture, out value);
else
ok = long.TryParse(body, NumberStyles.Integer, CultureInfo.InvariantCulture, out value);
if (ok && negative)
value = -value;
return ok;
}
private static bool IsNumeric(Type type) => Type.GetTypeCode(type) is
TypeCode.SByte or TypeCode.Byte or TypeCode.Int16 or TypeCode.UInt16 or
TypeCode.Int32 or TypeCode.UInt32 or TypeCode.Int64 or TypeCode.UInt64;
private static List<(string Mnemonic, string[] Operands)> Tokenize(string text, out List labelNames)
{
var result = new List<(string, string[])>();
labelNames = new List();
foreach (string rawLine in text.Split('\n'))
{
string line = rawLine;
int comment = line.IndexOf(';');
if (comment >= 0)
line = line[..comment];
line = line.Trim();
if (line.Length == 0)
continue;
// A "name:" prefix is a label definition; keep any instruction that follows it
// on the same line as a separate entry.
int colon = line.IndexOf(':');
if (colon >= 0)
{
string labelName = line[..colon].Trim();
labelNames.Add(labelName);
result.Add((labelName + ":", Array.Empty()));
line = line[(colon + 1)..].Trim();
if (line.Length == 0)
continue;
}
int space = line.IndexOfAny([' ', '\t']);
if (space < 0)
{
result.Add((line, Array.Empty()));
continue;
}
string mnemonic = line[..space];
string[] operands = line[(space + 1)..]
.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
result.Add((mnemonic, operands));
}
return result;
}
private static Dictionary BuildRegisterMap()
{
var map = new Dictionary(StringComparer.OrdinalIgnoreCase);
foreach (FieldInfo field in typeof(AssemblerRegisters).GetFields(BindingFlags.Public | BindingFlags.Static))
{
object? value = field.GetValue(null);
if (value is not null)
map[field.Name] = value;
}
return map;
}
// A parsed immediate, distinguished from register/label operands so binding can widen
// or narrow it to whichever integer parameter type the chosen overload expects.
private readonly record struct Immediate(long Value);
private sealed class ByteListCodeWriter : CodeWriter
{
public List Bytes { get; } = new();
public override void WriteByte(byte value) => Bytes.Add(value);
}
}