Home > Software design >  Is it safe to re-interpret a smaller data type to Int64 on x64 systems?
Is it safe to re-interpret a smaller data type to Int64 on x64 systems?

Time:12-03

I have a c# program that converts/re-interpret unmanaged type to ulong on x64 machine:

public static unsafe ulong ToUInt64<T>(T value) where T : unmanaged
{
  if(sizeof(T) > sizeof(ulong))
    throw new InvalidOperationException("Can't convert this type to UInt64");
  return *(ulong*)&value;
}

Usage:

int a = 123;
ulong b = ToUInt64(a);
Console.WriteLine(b); // returns 123 as expected.

However, I'm not sure if this is 100% safe. Namely, will the runtime always allocate 8 bytes and zero the memory if a variable have a smaller size? Can I get AccessViolationException or read some garbage memory into the ulong under some situation?

CodePudding user response:

I think this is still based on the runtime.

At first I cannot ensure this example is reproductive on every machine.

static void Main()
{
    var j = Test(0x12345678);
    ulong b = ToUInt64((byte)123);
    Console.WriteLine(b); // returns 123 as expected.
    Console.WriteLine(j);
}

public unsafe static short Test(int value)
{
    return (short)(value - 1);
}

public static unsafe ulong ToUInt64<T>(T value) where T : unmanaged
{
    return *(ulong*)&value;
}

When I debugged (F5) the program (in release mode), I got the following result. You can see the first value is unexpected.

Debug result

The gernerated assembly is:

00007FFDD6148D64  mov         dword ptr [rsp 24h],7Bh
00007FFDD6148D6C  mov         rcx,qword ptr [rsp 24h]

The value 123 (0x7B) is moved as a dword ptr, the returned value is moved as a qword ptr, so unless the runtime always clears the parameter stack, you cannot guarantee the final value will be pure.

CodePudding user response:

Turns out I neglected two things at first.

  1. The value is copied on function call, so it'll be different than "re-interpreting" the variable outright.
  2. If I allocate an array and re-interpret it in scope, it'll always fail. Since it reads adjacent unwanted memory:
int* a = stackalloc int[2];
a[0] = a[1] = 123;
Console.WriteLine(*(ulong*)&a[0]); // 528280977531

Even if I wraps it into a function, it may still fail sometimes. The result vary depending on Debug/Release configuration and other codes. Those may due to the code optimization and what's being written to the call stack (accidentally read some instructions?)

The 100% safe approach is alwasy allocate a new zeroed variable then copy the data:

public static unsafe ulong ToUInt64<T>(T value) where T : unmanaged
{
    if (sizeof(T) > sizeof(TResult))
        throw new InvalidCastException("Can't cast from data type with larger size");
    ulong result = 0;
    *(T*)&result = value;
    return result;
}

A more generic implementation:

public static unsafe TResult UnmanagedCast<T,TResult>(T value) where T : unmanaged where TResult : unmanaged
{
    if (sizeof(T) > sizeof(TResult))
        throw new InvalidCastException("Can't cast from data type with larger size");
    TResult result = default;
    *(T*)&result = value;
    return result;
}

usage:

UnmanagedCast<int,ulong>(123)
  • Related