C# Target Platforms | x64 vs x86 vs AnyCPU

TRAPDOOR Labs
5 min readAug 22, 2020

--

When programming with C# you don’t usually need to worry about the underlying target platform. There are however a few cases when the Application and OS architecture can affect program logic, change functionality and cause unexpected exceptions. In this article we’re going to look into how .NET handles x64 vs x86 systems and provide some C# examples to help out.

Photo by Christian Wiediger on Unsplash

Compilation

Unlike a Native C or C++ binary, which is built to a specific architecture, a .NET application is compiled to platform independent Intermediate Language (IL). In spite of this, a .NET project contains a “Platform Target” build configuration:

Selecting a Platform target in a C# .NET Core Project

It is common misconception that selecting a specific target will result in the compiler generating platform specific code. This is not the case and instead it simply sets a flag in the assembly’s CLR header. This information can be easily extracted, and modified, using Microsoft’s CoreFlags tool:

Output from coreflags.exe on a x64 .NET Assembly

The following table demonstrates how setting the project’s target platform (and 32-bit preference in AnyCPU) affects the the platform status information:

                   x64    x86    AnyCPU    AnyCPU(Prefer 32)       
───────────────────────────────────────────────────────────────
PE PE32+ PE32 PE32 PE32
32BITREQ 0 1 0 0
32BITPREF 0 0 0 1

In C# an external .NET Assembly can be checked in a similar way using GetAssemblyName() as follows:

var asmInfo = System.Reflection.AssemblyName.GetAssemblyName("assembly.dll");
Console.WriteLine(asmInfo.ProcessorArchitecture);

This returns a ProcessorArchitecture enum which identifies the processor and bits-per-word of the platform targeted by the executable.

Runtime Architecture

We have seen that a platform independent .NET assembly can be compiled as AnyCPU, x86 or x64. So what does this mean for the runtime architecture:

On a 32-bit machine:

  • Assemblies compiled as AnyCPU or x86 will run as 32-bit process. At runtime they can can load AnyCPU and x86 assemblies but not x64 (BadImageFormatException).
  • Assemblies compiled as x64 will always throw a BadImageFormatException.

On a 64-bit machine:

  • Assemblies compiled as AnyCPU or x64 will run as a 64-bit process. At runtime they can load AnyCPU and x64 assemblies but not x86(BadImageFormatException).
  • Assemblies compiled as x86 will run as a 32-bit process and can load Any CPU and x86 assemblies but not x64 (BadImageFormatException).

Identifying Process Architecture

A basic mechanism to determine the current process architecture is by inspecting the pointer size. The value is 4 in a 32-bit process, and 8 in a 64-bit process.

string platform = IntPtr.Size == 4 ? "x86" : "x64";

Alternatively since .NET Standard 1.1 a helper function exists that returns whether the application architecture is x64, x86, Arm or Arm64.

// using System.Runtime.InteropServices;
var architecture = RuntimeInformation.ProcessArchitecture;

Identifying System Architecture

Just knowing the process architecture doesn’t always provide the full picture. In some cases the system architecture is also required and can be identified with a simple check for x64 systems:

bool Is64BitOperatingSystem = Environment.Is64BitOperatingSystem;

When does Architecture Matter?

Now we understand how the target platforms affect .NET assemblies let’s take a look at when this difference matters.

Reference .NET Assemblies

As we’ve seen, an application that makes use of external or third party assemblies can encounter unexpected failures, if the runtime architecture doesn’t match that of the imported assembly.

For example if a .NET application runs as a 64-bit process and attempts to load a x86 compiled assembly an exception will be thrown. As an application compiled using AnyCPU may be run as either platform additional checks may be required to ensure you’re picking the correct dependency. We can use the functions shown below to selectively load the correct assembly:

// If we're running as 32-bit process
if (Environment.Is64BitProcess == false)
{
// ...and the assembly is x86 or neutral
if (asmInfo.ProcessorArchitecture == ProcessorArchitecture.X86
|| asmInfo.ProcessorArchitecture == ProcessorArchitecture.MSIL)
{
// Load it
assembly = Assembly.LoadFile(@"assembly-86.dll");
}
}

Interoperability

In an ideal world you could just program in your favourite .NET language and never have to touch native code again. Unfortunately, this isn’t always the case and you may need to make use of existing unmanaged libraries.

Interoperability enables you to preserve and take advantage of existing investments in unmanaged code. [Microsoft]

P/Invoke (Platform Invoke) provides the ability for .NET code to call through to native, unmanaged libraries. In the majority of cases this is used to call through to Windows APIs, however any native DLL can be invoked in this way.

Similar to how referencing .NET assemblies can cause issues, invoking native libraries is also affected by the runtime architecture and will determine what version of the native binary we need to load. If this is mismatched, a BadImageFormatException will be thrown. To support invoking native binaries we may need to compile our .NET application for a specific platform.

Fortunately when dealing with Windows APIs on a 64-bit system, we are provided with binaries for both architectures. Invoking these can therefore be as simple as using DllImport with the file name and the system will select the correct unmanaged DLL.

// Import the native DLL and define the method required
[DllImport("user32.dll")]
[return: MarshalAs(UnmanagedType.Bool)]
static extern bool MessageBeep(uint beepType);
static void Main(string[] args)
{
// Call our native function
MessageBeep((uint)0x00);
}

Even when Windows provides us with both versions of their native binaries other platform issues can still occur due to marshalling native pointers and structures. A structure from native code may have different sizes and offsets depending on the running process architecture. This occurs due to IntPtr values changing between 32-bit or 64-bit wide depending on the platform.

Windows System Redirection

Our final potential issue is Windows redirection. Windows 64-bit transparently redirects file system and registry access depending on the application architecture.

File redirection occurs when a a 32-bit application attempts to access system directories such as the c:\Windows\System32 directory which will be mapped to c:\Windows\SysWOW64. Registry access is also intercepted for specific keys allowing a 32-bit application to access the registry in the same way as if it was on a 32-bit machine.

In most case this redirection is desired behaviour as it’s the operating system ensuring the correct dependencies are accessed at runtime. However if an application is deliberately trying to copy or access resources in one of these redirected locations the actual location on disk may be different to what you expect.

The native function ‘Wow64DisableWow64FsRedirection’ can be called to disable file system redirection on the current thread. As it is not provided by .NET it will need to be called using P/Invoke:

[DllImport("kernel32.dll", SetLastError=true)]
public static extern bool Wow64DisableWow64FsRedirection(ref IntPtr ptr);

The Registry keys redirected by Windows are documented here. To avoid this redirection ‘OpenBaseKey’ can be used to select a specific a 64-bit or 32-bit view:

using var registryKey = RegistryKey.OpenBaseKey(RegistryHive.LocalMachine, RegistryView.Registry64);

--

--