Files
whitemagic/WhiteMagicTest/StubAssemblerTests.cs
kbeandClaude Opus 4.8 75ff4a2320 Guard diagnostic stub builder against argument-count overflow
The x64 frame math (0x20 + 8*stackArgs) and x86 arg buffer size grow with the
argument count. An absurdly large count could overflow int and produce a bogus
or negative frame. Add a MaxArguments (256) bound checked at the public entry —
far above any real calling convention — so the arithmetic stays in range. Add a
test asserting the cap is inclusive and count+1 throws.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-21 22:30:11 +02:00

473 lines
21 KiB
C#
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
using WhiteMagic.Assembly;
namespace WhiteMagicTest;
public class StubAssemblerTests
{
private static StubAssembler Create() => new();
// ── Emit primitives ────────────────────────────────────────────────────
[Fact] public void EmitU8_appends_a_single_byte() { var s=Create(); var b=new List<byte>(); s.EmitU8(b,0xAB); Assert.Equal([0xAB],b); }
[Fact] public void EmitU32_appends_little_endian() { var s=Create(); var b=new List<byte>(); s.EmitU32(b,0x11223344); Assert.Equal([0x44,0x33,0x22,0x11],b); }
[Fact] public void EmitU32_appends_zero() { var s=Create(); var b=new List<byte>(); s.EmitU32(b,0); Assert.Equal([0,0,0,0],b); }
[Fact] public void EmitU64_appends_little_endian() { var s=Create(); var b=new List<byte>(); s.EmitU64(b,0x1122334455667788); Assert.Equal([0x88,0x77,0x66,0x55,0x44,0x33,0x22,0x11],b); }
[Fact] public void EmitU64_appends_high_bits() { var s=Create(); var b=new List<byte>(); s.EmitU64(b,0xDEADBEEF_CAFEBABE); Assert.Equal([0xBE,0xBA,0xFE,0xCA,0xEF,0xBE,0xAD,0xDE],b); }
[Fact] public void StubAssembler_is_IAssembler() { Assert.IsAssignableFrom<IAssembler>(Create()); }
[Fact] public void Assemble_throws() { Assert.Throws<NotSupportedException>(()=>Create().Assemble("nop",0)); }
// ── x86 cdecl ──────────────────────────────────────────────────────────
[Fact]
public void Cdecl_0args()
{
uint r = 0x12345678u-(0x10000000u+5);
Assert.Equal([0xE8,(byte)r,(byte)(r>>8),(byte)(r>>16),(byte)(r>>24),0xC3],
Create().BuildCallStub((IntPtr)0x10000000,(IntPtr)0x12345678,[],4,CallConvention.Cdecl));
}
[Fact]
public void Cdecl_1arg()
{
uint ca=0x10000000u+5,r=0x12345678u-(ca+5);
Assert.Equal([
0x68,0xDD,0xCC,0xBB,0xAA,
0xE8,(byte)r,(byte)(r>>8),(byte)(r>>16),(byte)(r>>24),
0x83,0xC4,0x04,0xC3],
Create().BuildCallStub((IntPtr)0x10000000,(IntPtr)0x12345678,[0xAABBCCDD],4,CallConvention.Cdecl));
}
[Fact]
public void Cdecl_2args()
{
uint ca=0x10000000u+10,r=0x12345678u-(ca+5);
Assert.Equal([
0x68,0x22,0x22,0x22,0x22,
0x68,0x11,0x11,0x11,0x11,
0xE8,(byte)r,(byte)(r>>8),(byte)(r>>16),(byte)(r>>24),
0x83,0xC4,0x08,0xC3],
Create().BuildCallStub((IntPtr)0x10000000,(IntPtr)0x12345678,[0x11111111,0x22222222],4,CallConvention.Cdecl));
}
// ── x86 stdcall ──────────────────────────────────────────────────────
[Fact]
public void Stdcall_1arg()
{
uint ca=0x10000000u+5,r=0x12345678u-(ca+5);
Assert.Equal([
0x68,0xDD,0xCC,0xBB,0xAA,
0xE8,(byte)r,(byte)(r>>8),(byte)(r>>16),(byte)(r>>24),0xC3],
Create().BuildCallStub((IntPtr)0x10000000,(IntPtr)0x12345678,[0xAABBCCDD],4,CallConvention.Stdcall));
}
[Fact]
public void Stdcall_2args()
{
uint ca=0x10000000u+10,r=0x12345678u-(ca+5);
Assert.Equal([
0x68,0x22,0x22,0x22,0x22,
0x68,0x11,0x11,0x11,0x11,
0xE8,(byte)r,(byte)(r>>8),(byte)(r>>16),(byte)(r>>24),0xC3],
Create().BuildCallStub((IntPtr)0x10000000,(IntPtr)0x12345678,[0x11111111,0x22222222],4,CallConvention.Stdcall));
}
// ── x86 thiscall ─────────────────────────────────────────────────────
[Fact]
public void Thiscall_ecx_then_stack()
{
uint ca=0x10000000u+10,r=0x12345678u-(ca+5);
Assert.Equal([
0xB9,0x55,0x55,0xAA,0xAA,
0x68,0x66,0x66,0xBB,0xBB,
0xE8,(byte)r,(byte)(r>>8),(byte)(r>>16),(byte)(r>>24),0xC3],
Create().BuildCallStub((IntPtr)0x10000000,(IntPtr)0x12345678,[0xAAAA5555,0xBBBB6666],4,CallConvention.Thiscall));
}
[Fact]
public void Thiscall_1arg_ecx_only()
{
uint ca=0x10000000u+5,r=0x12345678u-(ca+5);
byte[] s=Create().BuildCallStub((IntPtr)0x10000000,(IntPtr)0x12345678,[0xCAFEBABE],4,CallConvention.Thiscall);
Assert.Equal(11,s.Length); Assert.Equal(0xB9,s[0]); Assert.Equal(0xCAFEBABEu,BitConverter.ToUInt32(s,1));
Assert.Equal(0xE8,s[5]); Assert.Equal(r,BitConverter.ToUInt32(s,6)); Assert.Equal(0xC3,s[10]);
}
[Fact]
public void Thiscall_0args_throws()
{
Assert.Throws<ArgumentOutOfRangeException>(() =>
Create().BuildCallStub((IntPtr)0x10000000,(IntPtr)0x12345678,[],4,CallConvention.Thiscall));
}
// ── x86 fastcall ────────────────────────────────────────────────────
[Fact]
public void Fastcall_ecx_edx_stack()
{
uint ca=0x10000000u+15,r=0x12345678u-(ca+5);
Assert.Equal([
0xB9,0x11,0x11,0x11,0x11,
0xBA,0x22,0x22,0x22,0x22,
0x68,0x33,0x33,0x33,0x33,
0xE8,(byte)r,(byte)(r>>8),(byte)(r>>16),(byte)(r>>24),0xC3],
Create().BuildCallStub((IntPtr)0x10000000,(IntPtr)0x12345678,[0x11111111,0x22222222,0x33333333],4,CallConvention.Fastcall));
}
[Fact]
public void Fastcall_2args_registers_only()
{
uint ca=0x10000000u+10,r=0x12345678u-(ca+5);
byte[] s=Create().BuildCallStub((IntPtr)0x10000000,(IntPtr)0x12345678,[0xAAAAAAAA,0xBBBBBBBB],4,CallConvention.Fastcall);
Assert.Equal(16,s.Length); Assert.Equal(0xB9,s[0]); Assert.Equal(0xAAAAAAAAu,BitConverter.ToUInt32(s,1));
Assert.Equal(0xBA,s[5]); Assert.Equal(0xBBBBBBBBu,BitConverter.ToUInt32(s,6));
Assert.Equal(0xE8,s[10]); Assert.Equal(r,BitConverter.ToUInt32(s,11)); Assert.Equal(0xC3,s[15]);
}
[Fact]
public void Fastcall_0args_is_valid()
{
uint r=0x12345678u-(0x10000000u+5);
Assert.Equal([0xE8,(byte)r,(byte)(r>>8),(byte)(r>>16),(byte)(r>>24),0xC3],
Create().BuildCallStub((IntPtr)0x10000000,(IntPtr)0x12345678,[],4,CallConvention.Fastcall));
}
// ── x64 (Microsoft x64 ABI — shadow space + 16-byte alignment + 64-bit loads) ──
//
// Stub frame layout:
// bytes 0..6 sub rsp, K (7 bytes — K = 32 + 8·stackArgs, rounded so K ≡ 8 mod 16)
// bytes 7..N mov r64, imm64 ... (10 bytes per reg move: 2-byte prefix + 8-byte imm)
// mov rax, imm64 / mov [rsp+0x20+8*(i-4)], rax for stack args (15 bytes each)
// E8 rel32 call target (5 bytes)
// 48 81 C4 K 00... add rsp, K (7 bytes)
// C3 ret (1 byte)
//
// Each `mov rNN, imm64` is 10 bytes regardless of the target register:
// RCX REX.W+opcode B9 (0x48 0xB9)
// RDX REX.W+opcode BA (0x48 0xBA)
// R8 REX.WB+opcode B8 (0x49 0xB8, REX.R needed for r8)
// R9 REX.WB+opcode B9 (0x49 0xB9, REX.R needed for r9)
// RAX REX.W+opcode B8 (0x48 0xB8)
[Fact]
public void X64_0args_allocates_shadow_space_and_aligns()
{
var s = Create();
ulong a = 0x100000000, t = 0x123456788;
byte[] stub = s.BuildCallStub(
(IntPtr)(nint)a, (IntPtr)(nint)t, [], 8, CallConvention.Cdecl);
// K = 0x20 + 0·8 = 0x20; round up to ≡ 8 mod 16 → K = 0x28.
// Frame = sub(7) + call(5) + add(7) + ret(1) = 20
Assert.Equal(20, stub.Length);
uint rel = (uint)(t - (a + 7 + 5)); // = t - a - 12
Assert.Equal([0x48, 0x81, 0xEC, 0x28, 0x00, 0x00, 0x00], stub[..7]); // sub rsp, 0x28 (K ≡ 8 mod 16)
Assert.Equal(0xE8, stub[7]);
Assert.Equal(rel, BitConverter.ToUInt32(stub, 8));
Assert.Equal([0x48, 0x81, 0xC4, 0x28, 0x00, 0x00, 0x00], stub[12..19]); // add rsp, 0x28
Assert.Equal(0xC3, stub[19]); // ret
}
[Fact]
public void X64_1arg_loads_rcx_as_64bit()
{
var s = Create();
ulong a = 0x100000000, t = 0x123456788;
byte[] stub = s.BuildCallStub(
(IntPtr)(nint)a, (IntPtr)(nint)t, [0xAABBCCDDu], 8, CallConvention.Cdecl);
// K = 0x28. Total = sub(7) + mov(10) + call(5) + add(7) + ret(1) = 30
Assert.Equal(30, stub.Length);
uint rel = (uint)(t - (a + 7 + 10 + 5)); // = t - a - 22
// stub[0..6] = sub rsp, 0x28 (K ≡ 8 mod 16, 16-aligned call-site for SSE safety)
Assert.Equal([0x48, 0x81, 0xEC, 0x28, 0x00, 0x00, 0x00], stub[..7]);
// stub[7..16] = mov rcx, 0x00000000_AABBCCDD (zero-extended)
Assert.Equal(0x48, stub[7]); Assert.Equal(0xB9, stub[8]);
Assert.Equal(0xDD, stub[9]); Assert.Equal(0xCC, stub[10]);
Assert.Equal(0xBB, stub[11]); Assert.Equal(0xAA, stub[12]);
Assert.Equal(0x00, stub[13]); Assert.Equal(0x00, stub[14]);
Assert.Equal(0x00, stub[15]); Assert.Equal(0x00, stub[16]);
// stub[17..21] = call rel32
Assert.Equal(0xE8, stub[17]);
Assert.Equal(rel, BitConverter.ToUInt32(stub, 18));
// stub[22..28] = add rsp, 0x28
Assert.Equal([0x48, 0x81, 0xC4, 0x28, 0x00, 0x00, 0x00], stub[22..29]);
Assert.Equal(0xC3, stub[29]); // ret
}
[Fact]
public void X64_4args_loads_rcx_rdx_r8_r9_as_64bit()
{
var s = Create();
ulong a = 0x100000000, t = 0x123456788;
byte[] stub = s.BuildCallStub(
(IntPtr)(nint)a, (IntPtr)(nint)t,
[0x11111111u, 0x22222222u, 0x33333333u, 0x44444444u], 8, CallConvention.Cdecl);
// K = 0x28. Total = sub(7) + 4×mov(40) + call(5) + add(7) + ret(1) = 60
Assert.Equal(60, stub.Length);
uint rel = (uint)(t - (a + 7 + 40 + 5)); // = t - a - 52
Assert.Equal([0x48, 0x81, 0xEC, 0x28, 0x00, 0x00, 0x00], stub[..7]); // sub rsp, 0x28
// mov rcx, 0x11111111 (48 B9 + 8 imm) at [7..16]
Assert.Equal(0x48, stub[7]); Assert.Equal(0xB9, stub[8]);
Assert.Equal(0x11, stub[9]); Assert.Equal(0x11, stub[10]);
Assert.Equal(0x11, stub[11]); Assert.Equal(0x11, stub[12]);
Assert.Equal(0x00, stub[13]); Assert.Equal(0x00, stub[14]);
Assert.Equal(0x00, stub[15]); Assert.Equal(0x00, stub[16]);
// mov rdx, 0x22222222 (48 BA + 8 imm) at [17..26]
Assert.Equal(0x48, stub[17]); Assert.Equal(0xBA, stub[18]);
Assert.Equal(0x22, stub[19]); Assert.Equal(0x22, stub[20]);
Assert.Equal(0x22, stub[21]); Assert.Equal(0x22, stub[22]);
Assert.Equal(0x00, stub[23]); Assert.Equal(0x00, stub[24]);
Assert.Equal(0x00, stub[25]); Assert.Equal(0x00, stub[26]);
// mov r8, 0x33333333 (49 B8 + 8 imm) at [27..36]
Assert.Equal(0x49, stub[27]); Assert.Equal(0xB8, stub[28]);
Assert.Equal(0x33, stub[29]); Assert.Equal(0x33, stub[30]);
Assert.Equal(0x33, stub[31]); Assert.Equal(0x33, stub[32]);
Assert.Equal(0x00, stub[33]); Assert.Equal(0x00, stub[34]);
Assert.Equal(0x00, stub[35]); Assert.Equal(0x00, stub[36]);
// mov r9, 0x44444444 (49 B9 + 8 imm) at [37..46]
Assert.Equal(0x49, stub[37]); Assert.Equal(0xB9, stub[38]);
Assert.Equal(0x44, stub[39]); Assert.Equal(0x44, stub[40]);
Assert.Equal(0x44, stub[41]); Assert.Equal(0x44, stub[42]);
Assert.Equal(0x00, stub[43]); Assert.Equal(0x00, stub[44]);
Assert.Equal(0x00, stub[45]); Assert.Equal(0x00, stub[46]);
// call rel32 at [47..51]
Assert.Equal(0xE8, stub[47]);
Assert.Equal(rel, BitConverter.ToUInt32(stub, 48));
// add rsp, 0x28 at [52..58]
Assert.Equal([0x48, 0x81, 0xC4, 0x28, 0x00, 0x00, 0x00], stub[52..59]);
// ret at [59]
Assert.Equal(0xC3, stub[59]);
}
[Fact]
public void X64_5args_places_first_stack_arg_in_shadow_plus_0x20()
{
var s = Create();
ulong a = 0x100000000, t = 0x123456788;
byte[] stub = s.BuildCallStub(
(IntPtr)(nint)a, (IntPtr)(nint)t,
[(nuint)1, (nuint)2, (nuint)3, (nuint)4, (nuint)5], 8, CallConvention.Cdecl);
// K = 0x20 + 1·8 = 0x28. Round-up rule: 0x28 % 16 = 8 → no extra padding.
// sub(7) + 4 reg moves (40) + stack arg (mov rax 10 + mov [rsp+0x20],rax 5 = 15)
// + call (5) + add (7) + ret (1) = 75
Assert.Equal(75, stub.Length);
Assert.Equal([0x48, 0x81, 0xEC, 0x28, 0x00, 0x00, 0x00], stub[..7]); // sub rsp, 0x28 (K=0x20+8=0x28, 0x28 % 16 = 8 ✓)
// mov rcx, 1 at [7..16]
Assert.Equal(0x48, stub[7]); Assert.Equal(0xB9, stub[8]);
Assert.Equal(0x01, stub[9]); Assert.Equal(0x00, stub[10]);
Assert.Equal(0x00, stub[11]); Assert.Equal(0x00, stub[12]);
// mov rdx, 2 at [17..26]
Assert.Equal(0x48, stub[17]); Assert.Equal(0xBA, stub[18]);
Assert.Equal(0x02, stub[19]);
// mov r8, 3 at [27..36]
Assert.Equal(0x49, stub[27]); Assert.Equal(0xB8, stub[28]);
Assert.Equal(0x03, stub[29]);
// mov r9, 4 at [37..46]
Assert.Equal(0x49, stub[37]); Assert.Equal(0xB9, stub[38]);
Assert.Equal(0x04, stub[39]);
// mov rax, 5 (48 B8 + 8-byte imm) at [47..56]
Assert.Equal(0x48, stub[47]); Assert.Equal(0xB8, stub[48]);
Assert.Equal(0x05, stub[49]);
for (int k = 50; k <= 56; k++) Assert.Equal(0x00, stub[k]);
// mov [rsp + 0x20], rax (48 89 44 24 20) at [57..61]
Assert.Equal(0x48, stub[57]); Assert.Equal(0x89, stub[58]);
Assert.Equal(0x44, stub[59]); Assert.Equal(0x24, stub[60]);
Assert.Equal(0x20, stub[61]);
// call rel32 at [62..66]; distance = t - (a + 62 + 5) = t - a - 67
uint rel = (uint)(t - (a + 62 + 5));
Assert.Equal(0xE8, stub[62]);
Assert.Equal(rel, BitConverter.ToUInt32(stub, 63));
// add rsp, 0x28 at [67..73]
Assert.Equal([0x48, 0x81, 0xC4, 0x28, 0x00, 0x00, 0x00], stub[67..74]);
// ret at [74]
Assert.Equal(0xC3, stub[74]);
}
[Fact]
public void X64_frame_alignment_property_for_arg_counts()
{
// ABI invariant: for every arg count the sub operand K must satisfy
// K ≡ 8 (mod 16), and the same K must appear in the matching 'add rsp, K'
// just before the ret. Violating this misaligns the inner call, which
// #GP-faults the next time an SSE-using callee executes movaps/movdqa.
var s = Create();
// Keep stub/target within E8 rel32 range (< 2 GiB) so the property check
// exercises the frame math, not the distance guard.
ulong a = 0x140000000, t = 0x140100000;
for (int argc = 0; argc <= 12; argc++)
{
nuint[] args = new nuint[argc];
for (int i = 0; i < argc; i++) args[i] = (nuint)(i + 1);
byte[] stub = s.BuildCallStub(
(IntPtr)(nint)a, (IntPtr)(nint)t, args, 8, CallConvention.Cdecl);
// sub rsp, imm32: 48 81 EC K0 K1 K2 K3
Assert.Equal(0x48, stub[0]);
Assert.Equal(0x81, stub[1]);
Assert.Equal(0xEC, stub[2]);
uint subK = BitConverter.ToUInt32(stub, 3);
Assert.True(subK % 16 == 8,
$"argc={argc}: sub K=0x{subK:X} must satisfy K % 16 == 8");
// add rsp, imm32 is 7 bytes immediately before the trailing C3
int last = stub.Length - 1;
Assert.Equal(0xC3, stub[last]);
int addIdx = last - 7;
Assert.Equal(0x48, stub[addIdx]);
Assert.Equal(0x81, stub[addIdx + 1]);
Assert.Equal(0xC4, stub[addIdx + 2]);
uint addK = BitConverter.ToUInt32(stub, addIdx + 3);
Assert.True(subK == addK,
$"argc={argc}: add K=0x{addK:X} must match sub K=0x{subK:X}");
}
}
[Fact]
public void X64_6args_frame_grows_to_0x38()
{
// Six args: frameBytes = 0x20 + 2·8 = 0x30. 0x30 % 16 = 0, so the
// pad-to-≡8 rule adds 8 more bytes → K = 0x38. Args 5 and 6 still live
// at [rsp+0x20] and [rsp+0x28]; the extra 8 bytes of padding at [rsp+0x30]
// are unused but necessary for alignment.
var s = Create();
ulong a = 0x100000000, t = 0x123456788;
nuint[] args = [(nuint)1, (nuint)2, (nuint)3, (nuint)4, (nuint)5, (nuint)6];
byte[] stub = s.BuildCallStub(
(IntPtr)(nint)a, (IntPtr)(nint)t, args, 8, CallConvention.Cdecl);
// sub rsp, 0x38 (7 bytes)
Assert.Equal([0x48, 0x81, 0xEC, 0x38, 0x00, 0x00, 0x00], stub[..7]);
// add rsp, 0x38 occupies the 7 bytes immediately before ret
int addIdx = stub.Length - 8;
Assert.Equal([0x48, 0x81, 0xC4, 0x38, 0x00, 0x00, 0x00], stub[addIdx..(addIdx + 7)]);
Assert.Equal(0xC3, stub[stub.Length - 1]);
}
[Fact]
public void X64_full_64bit_args_are_preserved_not_truncated()
{
// The bug this catches: an earlier stub emitted "mov r32d, imm32" which zero-extended
// a 32-bit immediate into the lower half of the 64-bit register, silently dropping
// the high bits of any pointer-sized argument above 4 GiB.
var s = Create();
ulong a = 0x100000000, t = 0x123456788;
nuint wideArg = unchecked((nuint)0xDEADBEEF_CAFEBABEUL);
byte[] stub = s.BuildCallStub(
(IntPtr)(nint)a, (IntPtr)(nint)t, [wideArg], 8, CallConvention.Cdecl);
// The 8-byte immediate for arg0 lives inside `mov rcx, imm64` at bytes [9..16].
ulong read = BitConverter.ToUInt64(stub, 9);
Assert.Equal(0xDEADBEEF_CAFEBABEul, read);
}
[Fact]
public void X86_target_rejects_arg_value_larger_than_32_bits()
{
if (!Environment.Is64BitProcess)
{
// On a 32-bit host, nuint cannot exceed uint.MaxValue — the precondition
// cannot be exercised. Mark the test as an intentional no-op.
Assert.True(true);
return;
}
var s = Create();
nuint tooBig = unchecked((nuint)0x1_00000000UL);
Assert.Throws<ArgumentOutOfRangeException>(() =>
s.BuildCallStub((IntPtr)0x10000000, (IntPtr)0x12345678, [tooBig], 4, CallConvention.Cdecl));
}
// ── Edge cases ────────────────────────────────────────────────────────
[Fact]
public void Far_target_throws()
{
Assert.Throws<ArgumentOutOfRangeException>(() =>
Create().BuildCallStub(IntPtr.Zero, unchecked((IntPtr)(nint)0xC0000000), [], 4, CallConvention.Cdecl));
}
[Fact]
public void Many_args_cleanup_uses_imm32_form()
{
// x86 path: 33 args, stack cleanup > 127 bytes → must emit add esp, imm32 (81 C4)
var args = new nuint[33];
for (int i = 0; i < 33; i++) args[i] = (nuint)(uint)(i * 0x10000 + i);
byte[] stub = Create().BuildCallStub(
(IntPtr)0x10000000, (IntPtr)0x12345678, args, 4, CallConvention.Cdecl);
for (int i = 0; i < stub.Length - 5; i++)
{
if (stub[i] == 0x81 && stub[i + 1] == 0xC4)
{
Assert.Equal(132, BitConverter.ToInt32(stub, i + 2));
return;
}
}
Assert.Fail("Expected 0x81 0xC4 (add esp, imm32) not found");
}
[Fact]
public void Invalid_pointerSize_throws()
{
Assert.Throws<ArgumentOutOfRangeException>(() =>
Create().BuildCallStub((IntPtr)0x10000000, (IntPtr)0x12345678, [], 2, CallConvention.Cdecl));
}
[Fact]
public void Invalid_calling_convention_throws()
{
Assert.Throws<ArgumentOutOfRangeException>(() =>
Create().BuildCallStub((IntPtr)0x10000000, (IntPtr)0x12345678, [], 4, (CallConvention)99));
}
[Fact]
public void Too_many_arguments_throws_before_frame_math_overflows()
{
// Guards the 0x20 + 8*stackArgs frame arithmetic against int overflow.
// MaxArguments is honored (accepted) and MaxArguments+1 is rejected.
var atCap = new nuint[StubAssembler.MaxArguments];
// At the cap the call still builds (cdecl x64), proving the bound is inclusive.
_ = Create().BuildCallStub((IntPtr)0x10000000, (IntPtr)0x10001000, atCap, 8, CallConvention.Cdecl);
var overCap = new nuint[StubAssembler.MaxArguments + 1];
Assert.Throws<ArgumentOutOfRangeException>(() =>
Create().BuildCallStub((IntPtr)0x10000000, (IntPtr)0x10001000, overCap, 8, CallConvention.Cdecl));
}
// ── No-FASM ─────────────────────────────────────────────────────────
[Fact]
public void No_fasm_reference_in_output()
{
var asm = typeof(StubAssembler).Assembly;
var refs = asm.GetReferencedAssemblies();
Assert.DoesNotContain(refs, r =>
r.Name!.Contains("Fasm", StringComparison.OrdinalIgnoreCase) ||
r.Name!.Contains("ManagedFasm", StringComparison.OrdinalIgnoreCase));
}
}