Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Save MichalStrehovsky/d6bc5e4d459c23d0cf3bd17af9a1bcf5 to your computer and use it in GitHub Desktop.
Save MichalStrehovsky/d6bc5e4d459c23d0cf3bd17af9a1bcf5 to your computer and use it in GitHub Desktop.

Building a universal Windows 7/Windows 10 .NET EXE

The problem with building a .NET (classic) executable that runs on both clean Windows 7 install and on Windows 10 is that Windows 7 only ships with .NET 3.5 inbox and Windows 10 ships with .NET 4.X. A .NET 3.5 executable will not run on a (clean install) Windows 10 directly. It can be coerced to do so in multiple ways, but none of them are "worry-free single file" solutions (config file, registry settings, environment variables, etc.).

One of the solutions is to set COMPLUS_OnlyUseLatestCLR environment variable to 1 before the process starts. This will allow .NET 4.X to take over execution of the program. This still doesn't qualify as "worry-free" because we need a batch file or something else to set the envionment for us before the process start (it's too late once Main is executing).

One weird trick to run the same executable on both Windows 7 and Windows 10

When I said we need to set COMPLUS_OnlyUseLatestCLR environment variable to 1 before process starts, I was imprecise - we need to set it before the process entrypoint starts executing. Windows offers one rarely used way to execute code before entrypoint executes: TLS callbacks. Can we use them to set the environment variable before MSCOREE.DLL starts selecting the CLR runtime to activate? You betcha.

Open a Visual Studio developer command prompt and compile a C# hello world against .NET 3.5:

using System;

class Program
{
    static void Main() { Console.WriteLine("Hello world"); }
}
csc /noconfig /nostdlib /target:module hello35.cs /r:"C:\Program Files (x86)\Reference Assemblies\Microsoft\Framework\.NETFramework\v3.5\Profile\Client\mscorlib.dll"

The important bit in the above is that we set target to module. This will produce a .netmodule file which is as close as one gets to object files in IL.

Next lets write some C for the TLS callback:

#include <windows.h>

VOID WINAPI tls_callback(
    PVOID DllHandle,
    DWORD Reason,
    PVOID Reserved)
{
    if (Reason == DLL_PROCESS_ATTACH)
        SetEnvironmentVariableW(L"COMPLUS_OnlyUseLatestCLR", L"1");
}

#ifdef _M_AMD64
    #pragma comment (linker, "/INCLUDE:_tls_used")
    #pragma comment (linker, "/INCLUDE:p_tls_callback")
    #pragma const_seg(push)
    #pragma const_seg(".CRT$XLAAA")
        EXTERN_C const PIMAGE_TLS_CALLBACK p_tls_callback = tls_callback;
    #pragma const_seg(pop)
#endif
#ifdef _M_IX86
    #pragma comment (linker, "/INCLUDE:__tls_used")
    #pragma comment (linker, "/INCLUDE:_p_tls_callback")
    #pragma data_seg(push)
    #pragma data_seg(".CRT$XLAAA")
        EXTERN_C PIMAGE_TLS_CALLBACK p_tls_callback = tls_callback;
    #pragma data_seg(pop)
#endif

Compile with:

cl /c hellotls.c

Now we just need to merge these together. Mixing C with C# hasn't been a problem since .NET 1 and native tools know how to do that:

link hello35.netmodule hellotls.obj kernel32.lib /entry:Program.Main /subsystem:console /ltcg

I haven't found a way to specify the .NET runtime version of the EXE that link.exe produces, so one last step is to open the produced hello35.exe in a hex editor and search and replace v4.0.30319 with v2.0.50727.

We now have a .NET 3.5 executable that will run on 4.X without additional configuration.

@MichalStrehovsky
Copy link
Author

@Tyrrrz I don't think there's an x-platform way. link.exe is very much Windows only and I don't know of other tools that would be able to target this corner of the file format.

@Tyrrrz
Copy link

Tyrrrz commented Feb 10, 2023

@Tyrrrz I don't think there's an x-platform way. link.exe is very much Windows only and I don't know of other tools that would be able to target this corner of the file format.

That's a shame 😔

It also appears that trying to generate a module file via <OutputType>module</OutputType> fails due to unresolved references to NRT-related types. I found a related issue on the Roslyn repo, but it seems that .NET team doesn't want to explicitly support .netmodules as an output target.

Ultimately, my goal is to encapsulate your approach as much as I can within MSBuild so that I can reuse its existing infrastructure instead of wrapping csc manually, but it seems to be a tough challenge. 🤔

@alexchandel
Copy link

Very nice @MichalStrehovsky. Is there any way to target both .NET Framework and new .NET (formerly Core) with this mechanism? (Basically to produce a .NET Standard-like executable?)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment