Fix IcedAssembler immediate overflow and unwrap invoke exceptions

- TryBind wraps Convert.ChangeType in TryChangeType so an immediate that
  overflows a candidate parameter type returns false (letting a wider overload
  be tried) instead of throwing OverflowException out of assembly. Verified:
  "mov eax, 4294967295" and "mov eax, -2147483649" no longer crash.
- Immediate now carries a boxed long OR ulong; TryParseImmediate parses decimal
  values above long.MaxValue via a ulong fallback, and hex via ulong. Previously
  such literals were rejected at parse time.
- Unwrap TargetInvocationException from method.Invoke so callers see the real
  Iced failure, not the reflection wrapper.

Tests: 231 passing, 4 skipped.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
kbe
2026-07-22 11:43:48 +02:00
co-authored by Claude Opus 4.8
parent 0ddd812829
commit e8c84f0ba1
2 changed files with 94 additions and 14 deletions
+68 -14
View File
@@ -129,7 +129,16 @@ public sealed class IcedAssembler : IAssembler
if (TryBind(parameters, operands, out object?[]? boundArgs)) if (TryBind(parameters, operands, out object?[]? boundArgs))
{ {
method.Invoke(assembler, boundArgs); try
{
method.Invoke(assembler, boundArgs);
}
catch (TargetInvocationException ex) when (ex.InnerException is not null)
{
// Surface the real Iced failure rather than the reflection wrapper.
throw ex.InnerException;
}
return; return;
} }
} }
@@ -151,7 +160,16 @@ public sealed class IcedAssembler : IAssembler
switch (operand) switch (operand)
{ {
case Immediate imm when IsNumeric(paramType): case Immediate imm when IsNumeric(paramType):
args[i] = Convert.ChangeType(imm.Value, paramType, CultureInfo.InvariantCulture); // An immediate that overflows this parameter's type means this overload
// is the wrong width; return false so a wider overload can be tried
// instead of crashing the whole assembly.
if (!TryChangeType(imm.Value, paramType, out object? converted))
{
boundArgs = null;
return false;
}
args[i] = converted;
break; break;
case not null when paramType.IsInstanceOfType(operand): case not null when paramType.IsInstanceOfType(operand):
@@ -178,28 +196,63 @@ public sealed class IcedAssembler : IAssembler
if (labels.TryGetValue(token, out Label label)) if (labels.TryGetValue(token, out Label label))
return label; return label;
if (TryParseImmediate(token, out long value)) if (TryParseImmediate(token, out object? value))
return new Immediate(value); return new Immediate(value!);
throw new NotSupportedException( throw new NotSupportedException(
$"Unrecognized operand '{token}' (expected a register, an immediate, or a label)."); $"Unrecognized operand '{token}' (expected a register, an immediate, or a label).");
} }
private static bool TryParseImmediate(string token, out long value) // Parses an immediate as the narrowest of long/ulong that holds it, boxed. Storing the
// widest representation lets TryChangeType later narrow it to whatever integer parameter
// the chosen overload expects — and reject (rather than crash on) values that do not fit.
private static bool TryParseImmediate(string token, out object? value)
{ {
value = null;
bool negative = token.StartsWith('-'); bool negative = token.StartsWith('-');
string body = negative ? token[1..] : token; string body = negative ? token[1..] : token;
bool ok;
if (body.StartsWith("0x", StringComparison.OrdinalIgnoreCase)) if (body.StartsWith("0x", StringComparison.OrdinalIgnoreCase))
ok = long.TryParse(body[2..], NumberStyles.HexNumber, CultureInfo.InvariantCulture, out value); {
if (!ulong.TryParse(body[2..], NumberStyles.HexNumber, CultureInfo.InvariantCulture, out ulong hex))
return false;
value = negative ? -(long)hex : hex;
return true;
}
if (negative)
{
if (!long.TryParse(token, NumberStyles.Integer, CultureInfo.InvariantCulture, out long signed))
return false;
value = signed;
return true;
}
// Non-negative decimal: prefer long, fall back to ulong for values above long.MaxValue.
if (long.TryParse(body, NumberStyles.Integer, CultureInfo.InvariantCulture, out long asLong))
value = asLong;
else if (ulong.TryParse(body, NumberStyles.Integer, CultureInfo.InvariantCulture, out ulong asULong))
value = asULong;
else else
ok = long.TryParse(body, NumberStyles.Integer, CultureInfo.InvariantCulture, out value); return false;
if (ok && negative) return true;
value = -value; }
return ok; private static bool TryChangeType(object value, Type targetType, out object? result)
{
try
{
result = Convert.ChangeType(value, targetType, CultureInfo.InvariantCulture);
return true;
}
catch (Exception ex) when (ex is OverflowException or InvalidCastException or FormatException)
{
result = null;
return false;
}
} }
private static bool IsNumeric(Type type) => Type.GetTypeCode(type) is private static bool IsNumeric(Type type) => Type.GetTypeCode(type) is
@@ -266,9 +319,10 @@ public sealed class IcedAssembler : IAssembler
return map; return map;
} }
// A parsed immediate, distinguished from register/label operands so binding can widen // A parsed immediate (boxed long or ulong), distinguished from register/label operands
// or narrow it to whichever integer parameter type the chosen overload expects. // so binding can narrow it to whichever integer parameter type the chosen overload
private readonly record struct Immediate(long Value); // expects — or reject it when it does not fit.
private readonly record struct Immediate(object Value);
private sealed class ByteListCodeWriter : CodeWriter private sealed class ByteListCodeWriter : CodeWriter
{ {
@@ -82,6 +82,32 @@ public class IcedAssemblerTests
Assert.Equal(expected, jmp.NearBranchTarget); Assert.Equal(expected, jmp.NearBranchTarget);
} }
[Theory]
[InlineData("mov eax, 4294967295")] // 0xFFFFFFFF — needs the uint overload, not int
[InlineData("mov eax, 0xFFFFFFFF")] // same value, hex form
[InlineData("mov rax, 18446744073709551615")] // ulong.MaxValue — decimal above long.MaxValue
public void Assemble_binds_wide_unsigned_immediates(string source)
{
var assembler = new IcedAssembler(64);
byte[] code = assembler.Assemble(source);
Assert.NotEmpty(code);
Instruction[] instructions = Disassemble(code, 64, 0);
Assert.Single(instructions);
Assert.Equal(Mnemonic.Mov, instructions[0].Mnemonic);
}
[Fact]
public void Assemble_rejects_immediate_that_fits_no_overload_without_crashing()
{
var assembler = new IcedAssembler(64);
// -2147483649 is below int.MinValue and eax has no wider signed overload; must be a
// clean NotSupportedException, not an OverflowException escaping from ChangeType.
Assert.Throws<NotSupportedException>(() => assembler.Assemble("mov eax, -2147483649"));
}
[Fact] [Fact]
public void Assemble_throws_on_unsupported_operand() public void Assemble_throws_on_unsupported_operand()
{ {