.NET Applications on Windows x64 – Easy? Yes and No
When migrating to 64-bit Windows, traditional “unmanaged” applications can pose challenges. That is because unmanaged binaries contain hardware-dependent CPU instructions – and the view on the hardware differs between 32- and 64-bit mode. But .NET? It should be unaffected of a system’s bitness since “managed” binaries contain instructions in a so-called intermediate language that is executed in a virtual machine at run-time and only then translated to machine language. But is it really? This article is about .NET programs that are dependent on OS bitness.
When you create a new C# or VB application in Visual Studio it is created as a bitness-independent program by default. That means that it will run as a 32-bit process on 32-bit Windows, and as a 64-bit process on 64-bit Windows, optimally leveraging each platform’s capabilities. That sounds good, and indeed it is a great achievement of the .NET framework to make this possible. But there are caveats, otherwise this article would be quite pointless.
Under the Hood
Each .NET binary (correct: assembly, an EXE or DLL) stores flags marking it as compiled for either:
- AnyCPU [default]
There is also Itanium, but let us ignore that soon-to-be obsolete platform.
These flags are relevant at runtime. AnyCPU assemblies are independent of the OS bitness. AnyCPU DLLs can be loaded into 32-bit and 64-bit processes while AnyCPU EXEs are started as 32-bit processes on Windows x86 and as 64-bit processes on Windows x64.
x86 assemblies can be loaded into 32-bit processes only (DLLs) respectively are always started as 32-bit processes (EXEs).
x64 assemblies can be loaded into 64-bit processes only (DLLs) respectively are always started as 64-bit processes (EXEs).
Why the Distinction?
The obvious question at this point: why even create three different types of .NET assemblies if .NET code is platform-independent? The answer is simple: While .NET code is platform-independent, legacy code is not. And a lot of .NET applications rely on unmanaged code.
Consider the following (fictional) scenario: You are writing a nice program for transferring files from one computer system to another. At some point you discover that compressing the files transferred by your program would greatly enhance speed and usability. So you decide to compress files. But how? Write a packing algorithm yourself? No way! The easiest solution is to use something existing. So you might end up with the free 6-zip library, written in unmanaged C++ and available as 32-bit only. And here the problem starts.
You call the packing routines from the unmanaged 32-bit DLL, test very thoroughly on your 32-bit develepment machine (where everything works great) and deliver the resulting application to your customers. Another job well done! But then one of your customers decides to migrate his systems to Windows x64 – and discovers that your application does not work there.
Why? On 64-bit Windows your AnyCPU assembly runs as a 64-bit process – and a 64-bit process cannot load 32-bit DLLs. But your application tries to do exactly that and fails.
What to Do – As a Programmer
If you are the guy developing the application, you have two options if you want your program to run on both 32-bit and 64-bit Windows:
- Compile your managed code as “AnyCPU” but have the installer determine that target system’s bitness and install any unmanaged DLLs in the appropriate bitness. This means you need additional logic during setup.
- Compile your managed code as “x86” – that way it will always run as a 32-bit process, regardless of the bitness of the OS. Since your code is always 32-bit, it is safe to only distribute 32-bit versions of unmanaged DLLs.
What to Do – As an Administrator
If you want to get the application to run on 64-bit Windows, the safest way to do so would be to contact the vendor and ask him to make his software compatible with x64. Only by going back to the vendor will your configuration be supported, which is a requirement especially in larger enterprises.
If vendor support is not paramount you can try the following hack. It is made possible by the fact that all three flavors of .NET assemblies described above are basically identical except for two flags in the PE header of the binary file storing whether this is a x86, x64 or AnyCPU assembly. And flags can be changed easily…
First of all you might want to check your .NET assemblies. There are several ways to do that:
- System.Reflection.AssemblyName.GetAssemblyName (use the property “ProcessorArchitecture” of the returned object)
If you find managed AnyCPU EXE files on a 64-bit system, next check if they came with unmanaged 32-bit DLLs. For that you first need to determine whether a DLL is managed or unmanaged, which can be done with CorFlags.exe (see above) – it will complain if it is fed an unmanaged binary. Secondly you need to check if your unmanaged DLL is 32-bit or 64-bit – information that, again, is stored in the PE header. So you need a PE header dumper like PEDump from Matt Pietrek, an oldie but goldie. Unfortunately it crashes when analyzing 64-bit DLLs on 64-bit Windows 7, but only after printing the information we are interested in:
D:\Tools\PEDump>PEDUMP.exe c:\Windows\notepad.exe Dump of file C:\WINDOWS\NOTEPAD.EXE File Header Machine: 8664 (unknown) <<<<<<<<< This means "x64" Number of Sections: 0006 TimeDateStamp: 4A5BC9B3 -> Tue Jul 14 01:56:35 2009 ... D:\PEDump>PEDUMP.exe c:\Windows\syswow64\notepad.exe Dump of file C:\WINDOWS\SYSWOW64\NOTEPAD.EXE File Header Machine: 014C (i386) Number of Sections: 0004 TimeDateStamp: 4A5BC60F -> Tue Jul 14 01:41:03 2009 ...
Once you have verified that you indeed have a managed EXE compiled as AnyCPU that needs 32-bit unmanaged DLLs, you can change the EXE’s type from AnyCPU to x86 with CorFlags:
CoreFlags.exe TheApp.exe /32BIT+
If you want to revert:
CoreFlags.exe TheApp.exe /32BIT-
Please note that modifying a binary file invalidates its digital signature (if available). Reverting back to the original state reverses this – the signature is valid once again.