-
Notifications
You must be signed in to change notification settings - Fork 4.9k
Description
Tour of .NET Behavior on Windows 11 Arm64
We're going to take a tour of the basic behavior of .NET 6 and .NET Framework 4.8.1 on Windows 11 Arm64. This tour of the experience is going to give us a sense of which .NET variants can be installed on the machine, how to use them to target various architectures, and what various key APIs report. This tour will provide a good sense of the experience that you an expect and that is on offer.
The primary questions it answers are:
- When I build an app, which architecture does it target/support by default?
- How do I build my app for a different/particular architecture?
- Is there a way to get my app to run a different architecture than what it was built for as part of launching it?
A subset of this document will be moved to official docs. In fact, it was created as a set of working notes to do just that. The author of those notes is sharing them with the hope that they will be a benefit for someone trying to figure out how best to target Windows Arm64.
The tour is on Windows 11 Arm64, however, you could just as easily use these instructions to build for Windows Arm64 on Windows x64 and then run the built apps on Windows Arm64. Also, much of what you'll see equally applies to macOS x64 and Arm64.
The key -- potentially surprising -- experience is that .NET Framework
AnyCPU
apps run as emulatedx64
on Windows Arm64. That choice was made for the benefit of compatibility. The document describes multiple ways to get your .NET Framework apps to run natively, either as a build-time or launch-time decision.
I'm using .NET 6.0.8 (.NET SDK 6.0.400) and .NET Framework 4.8.1. I'll be using Arm64, x64, and x86 variants of both. .NET 6 is not included in Windows 11 (or any other Windows version), while all three .NET Framework architecture variants are included. I'm using a Windows 11 Insider build (10.0.22621). The machine and OS are Arm64.
There is also one honorable mention of .NET 7, where its behavior differs from .NET 6.
Much of the tour relies on the Developer Command Prompt
that comes with Visual Studio 2022. Visual Studio 2022 will soon be supported on Windows 11 Arm64.
Test samples
We'll be using the following samples to test our environment:
- https://github.com/dotnet/dotnet-docker/blob/main/samples/dotnetapp/Program.cs
- https://github.com/microsoft/dotnet-framework-docker/blob/main/samples/dotnetapp/Program.cs
Those are samples we maintain for containers, but they are also great for this use case.
PATH
All the .NET variants mentioned are installed on the Windows 11 Arm64 test machine that will be used for the tour.
The PATH
is a key aspect of any dev experience and is critical to understand to using platform components (particularly tools).
Let's take a look at the PATH
:
$env:PATH
C:\Windows\system32;C:\Windows;C:\Windows\System32\Wbem;C:\Windows\System32\WindowsPowerShell\v1.0\;C:\Windows\System32\OpenSSH\;C:\Program Files\dotnet\;C:\Program Files\Git\cmd;C:\Program Files\Microsoft SQL Server\150\Tools\Binn\;C:\Program Files\Microsoft SQL Server\Client SDK\ODBC\170\Tools\Binn\;C:\Users\rich\AppData\Local\Microsoft\WindowsApps;C:\Users\rich\.dotnet\tools;C:\Users\rich\AppData\Local\Programs\Microsoft VS Code\bin
The following path segments relate to .NET:
C:\Program Files\dotnet\
C:\Users\rich\.dotnet\tools
There are two interesting aspects here:
- Only the native architecture .NET (Core) location is registered. That's Arm64.
- There is just one location for .NET tools. That feature is built in such a way that tools from multiple architectures can be co-located without issue.
There is no PATH
entry for .NET Framework. That's nothing new. There has never been a PATH
entry for .NET Framework. It doesn't explicitly need one. If you want to use .NET Framework tools, you need to rely on the Developer Command Prompt for Visual Studio
.
Building and running by default
First, .NET 6:
PS C:\Users\rich\git\dotnet-docker\samples\dotnetapp> dotnet run
42
42 ,d ,d
42 42 42
,adPPYb,42 ,adPPYba, MM42MMM 8b,dPPYba, ,adPPYba, MM42MMM
a8" `Y42 a8" "8a 42 42P' `"8a a8P_____42 42
8b 42 8b d8 42 42 42 8PP""""""" 42
"8a, ,d42 "8a, ,a8" 42, 42 42 "8b, ,aa 42,
`"8bbdP"Y8 `"YbbdP"' "Y428 42 42 `"Ybbd8"' "Y428
.NET 6.0.8
Microsoft Windows 10.0.22621
OSArchitecture: Arm64
ProcessArchitecture: Arm64
ProcessorCount: 8
TotalAvailableMemoryBytes: 31.41 GiB
The app is running on Arm64, as expected.
Next, .NET Framework 4.8.1:
C:\Users\rich\git\dotnet-framework-docker\samples\dotnetapp>msbuild /t:restore;build
C:\Users\rich\git\dotnet-framework-docker\samples\dotnetapp>bin\Debug\net48\dotnetapp.exe
ad88
,d d8"
88 88
8b,dPPYba, ,adPPYba, MM88MMM MM88MMM 8b, ,d8
88P' `"8a a8P_____88 88 88 `Y8, ,8P'
88 88 8PP""""""" 88 88 )888(
88 88 "8b, ,aa 88, 88 ,d8" "8b,
88 88 `"Ybbd8"' "Y888 88 8P' `Y8
.NET Framework 4.8.9065.0
Microsoft Windows 10.0.22621
OSArchitecture: X64
ProcessArchitecture: X64
ProcessorCount: 8
In contrast, the .NET Framework app is running on x64.
.NET Framework apps run as x64 by default on Windows Arm64. That was a key design decision made to aid compatibility. .NET Framework apps have been primarily running as x64 for over a decade on pervasively deployed x64 desktops, laptops, and servers. It's quite likely that many .NET Framework apps have x64 dependencies (like native libraries). Running them as emulated seemed like the safest bet, and to make Arm64 opt-in.
Targeting Arm64
Targeting Arm64 is easy with .NET 6. Your app will target the same architecture as the SDK you are using. You'll produce an Arm64 app by default as long as you are using the Arm64 SDK. That was just demonstrated above.
However, if you want to force targeting Arm64 (which will be more important later on the tour), you can use -a arm64
as demonstrated in the following:
PS C:\Users\rich\git\dotnet-docker\samples\dotnetapp> dotnet run -a arm64
42
42 ,d ,d
42 42 42
,adPPYb,42 ,adPPYba, MM42MMM 8b,dPPYba, ,adPPYba, MM42MMM
a8" `Y42 a8" "8a 42 42P' `"8a a8P_____42 42
8b 42 8b d8 42 42 42 8PP""""""" 42
"8a, ,d42 "8a, ,a8" 42, 42 42 "8b, ,aa 42,
`"8bbdP"Y8 `"YbbdP"' "Y428 42 42 `"Ybbd8"' "Y428
.NET 6.0.8
Microsoft Windows 10.0.22621
OSArchitecture: Arm64
ProcessArchitecture: Arm64
ProcessorCount: 8
TotalAvailableMemoryBytes: 31.41 GiB
There is a similar option with MSBuild, for .NET Framework, using the Platform
property:
C:\Users\rich\git\dotnet-framework-docker\samples\dotnetapp>msbuild /t:restore;build /p:Platform=arm64
C:\Users\rich\git\dotnet-framework-docker\samples\dotnetapp>bin\arm64\Debug\net48\dotnetapp.exe
ad88
,d d8"
88 88
8b,dPPYba, ,adPPYba, MM88MMM MM88MMM 8b, ,d8
88P' `"8a a8P_____88 88 88 `Y8, ,8P'
88 88 8PP""""""" 88 88 )888(
88 88 "8b, ,aa 88, 88 ,d8" "8b,
88 88 `"Ybbd8"' "Y888 88 8P' `Y8
.NET Framework 4.8.9065.0
Microsoft Windows 10.0.22621
OSArchitecture: Arm64
ProcessArchitecture: Arm64
ProcessorCount: 8
You can also use the .NET SDK for the same purpose:
C:\Users\rich\git\dotnet-framework-docker\samples\dotnetapp>dotnet run -a arm64
ad88
,d d8"
88 88
8b,dPPYba, ,adPPYba, MM88MMM MM88MMM 8b, ,d8
88P' `"8a a8P_____88 88 88 `Y8, ,8P'
88 88 8PP""""""" 88 88 )888(
88 88 "8b, ,aa 88, 88 ,d8" "8b,
88 88 `"Ybbd8"' "Y888 88 8P' `Y8
.NET Framework 4.8.9065.0
Microsoft Windows 10.0.22621
OSArchitecture: Arm64
ProcessArchitecture: Arm64
ProcessorCount: 8
I'm using dotnet run
for this demonstration, but dotnet build
works the same way.
Targeting x64
Targeting x64 is just a variation on what you've just seen (mostly).
For .NET 6, we can continue using the Arm64 SDK, but target x64 with the same -a
switch you just saw. That's the easiest and highest performance approach (the SDK is a lot of code and will run slowly when emulation).
PS C:\Users\rich\git\dotnet-docker\samples\dotnetapp> dotnet run -a x64
42
42 ,d ,d
42 42 42
,adPPYb,42 ,adPPYba, MM42MMM 8b,dPPYba, ,adPPYba, MM42MMM
a8" `Y42 a8" "8a 42 42P' `"8a a8P_____42 42
8b 42 8b d8 42 42 42 8PP""""""" 42
"8a, ,d42 "8a, ,a8" 42, 42 42 "8b, ,aa 42,
`"8bbdP"Y8 `"YbbdP"' "Y428 42 42 `"Ybbd8"' "Y428
.NET 6.0.8
Microsoft Windows 10.0.22621
OSArchitecture: X64
ProcessArchitecture: X64
ProcessorCount: 8
TotalAvailableMemoryBytes: 31.41 GiB
We can also use the x64 SDK, which will produce x64 apps by default. That's not the recommended approach (due to the emulation performance cost), but works fine.
C:\Users\rich\git\dotnet-docker\samples\dotnetapp>"c:\Program Files\dotnet\x64\dotnet" run
42
42 ,d ,d
42 42 42
,adPPYb,42 ,adPPYba, MM42MMM 8b,dPPYba, ,adPPYba, MM42MMM
a8" `Y42 a8" "8a 42 42P' `"8a a8P_____42 42
8b 42 8b d8 42 42 42 8PP""""""" 42
"8a, ,d42 "8a, ,a8" 42, 42 42 "8b, ,aa 42,
`"8bbdP"Y8 `"YbbdP"' "Y428 42 42 `"Ybbd8"' "Y428
.NET 6.0.8
Microsoft Windows 10.0.22621
OSArchitecture: X64
ProcessArchitecture: X64
ProcessorCount: 8
TotalAvailableMemoryBytes: 31.41 GiB
There isn't a C:\Program Files (x64)
on Windows Arm64. As a result, the x64 variant of .NET is installed in C:\Program Files\dotnet
in the child x64
directory. That's what you see demonstrated above. the x64
directory will only be there if you install the x64 .NET SDK. It doesn't come with the Arm64 SDK.
To complete the demonstration, we can use the x64 SDK to produce Arm64 apps. That's also not recommended, but works.
C:\Users\rich\git\dotnet-docker\samples\dotnetapp>"c:\Program Files\dotnet\x64\dotnet" run -a arm64
42
42 ,d ,d
42 42 42
,adPPYb,42 ,adPPYba, MM42MMM 8b,dPPYba, ,adPPYba, MM42MMM
a8" `Y42 a8" "8a 42 42P' `"8a a8P_____42 42
8b 42 8b d8 42 42 42 8PP""""""" 42
"8a, ,d42 "8a, ,a8" 42, 42 42 "8b, ,aa 42,
`"8bbdP"Y8 `"YbbdP"' "Y428 42 42 `"Ybbd8"' "Y428
.NET 6.0.8
Microsoft Windows 10.0.22621
OSArchitecture: Arm64
ProcessArchitecture: Arm64
ProcessorCount: 8
TotalAvailableMemoryBytes: 31.41 GiB
For .NET Framework, x64 is the default target as demonstrated above. There isn't anything more to show.
Targeting x86
For .NET 6, this experience is very similar to targeting x64. We'll use the same approach.
First, using Arm64 SDK, again using the -a
switch:
C:\Users\rich\git\dotnet-docker\samples\dotnetapp>dotnet run -a x86
42
42 ,d ,d
42 42 42
,adPPYb,42 ,adPPYba, MM42MMM 8b,dPPYba, ,adPPYba, MM42MMM
a8" `Y42 a8" "8a 42 42P' `"8a a8P_____42 42
8b 42 8b d8 42 42 42 8PP""""""" 42
"8a, ,d42 "8a, ,a8" 42, 42 42 "8b, ,aa 42,
`"8bbdP"Y8 `"YbbdP"' "Y428 42 42 `"Ybbd8"' "Y428
.NET 6.0.8
Microsoft Windows 10.0.22621
OSArchitecture: X64
ProcessArchitecture: X86
ProcessorCount: 8
TotalAvailableMemoryBytes: 2.00 GiB
Huh. Why did we drop from (near) 32GB to 2GB? Are 32-bit apps running in some small virtual machine used for emulation? How do I make it bigger? That's not it. 32-bit processes are limited to 4GB of memory and it's split 50/50 between kernel and user mode. That's why we're seeing 2GB.
We see slightly difference behavior with .NET 7 (specifically for OSArchitecture
).
C:\Users\rich\git\dotnet-docker\samples\dotnetapp>dotnet run -a x86
42
42 ,d ,d
42 42 42
,adPPYb,42 ,adPPYba, MM42MMM 8b,dPPYba, ,adPPYba, MM42MMM
a8" `Y42 a8" "8a 42 42P' `"8a a8P_____42 42
8b 42 8b d8 42 42 42 8PP""""""" 42
"8a, ,d42 "8a, ,a8" 42, 42 42 "8b, ,aa 42,
`"8bbdP"Y8 `"YbbdP"' "Y428 42 42 `"Ybbd8"' "Y428
.NET 7.0.0-preview.7.22375.6
Microsoft Windows 10.0.22621
OSArchitecture: Arm64
ProcessArchitecture: X86
ProcessorCount: 8
TotalAvailableMemoryBytes: 2.00 GiB
Let's try with .NET Framework.
C:\Users\rich\git\dotnet-framework-docker\samples\dotnetapp>msbuild /t:restore;build /p:Platform=x86
C:\Users\rich\git\dotnet-framework-docker\samples\dotnetapp>bin\x86\Debug\net48\dotnetapp.exe
ad88
,d d8"
88 88
8b,dPPYba, ,adPPYba, MM88MMM MM88MMM 8b, ,d8
88P' `"8a a8P_____88 88 88 `Y8, ,8P'
88 88 8PP""""""" 88 88 )888(
88 88 "8b, ,aa 88, 88 ,d8" "8b,
88 88 `"Ybbd8"' "Y888 88 8P' `Y8
.NET Framework 4.8.9065.0
Microsoft Windows 10.0.22621
OSArchitecture: Arm64
ProcessArchitecture: X86
ProcessorCount: 8
We can try the same thing with the .NET SDK:
C:\Users\rich\git\dotnet-framework-docker\samples\dotnetapp>dotnet run -a x86
ad88
,d d8"
88 88
8b,dPPYba, ,adPPYba, MM88MMM MM88MMM 8b, ,d8
88P' `"8a a8P_____88 88 88 `Y8, ,8P'
88 88 8PP""""""" 88 88 )888(
88 88 "8b, ,aa 88, 88 ,d8" "8b,
88 88 `"Ybbd8"' "Y888 88 8P' `Y8
.NET Framework 4.8.9065.0
Microsoft Windows 10.0.22621
OSArchitecture: Arm64
ProcessArchitecture: X86
ProcessorCount: 8
Hmmm. That's interesting. OSArchitecture
is reported differently for .NET 6 on one side and .NET 7 and .NET Framework 4.8.1 on the other when running x86 apps. They must use different Windows APIs to report OSArchitecture
. They don't all report the same thing.
Coercing already-built apps to Arm64
With .NET 6, you can (in some cases) run apps targeted for one architecture to another. We won't go into all the details on that, but will demonstrate the mechanism.
We'll first build and run the app natively, as has been demonstrated earlier.
C:\Users\rich\git\dotnet-docker\samples\dotnetapp>dotnet run
42
42 ,d ,d
42 42 42
,adPPYb,42 ,adPPYba, MM42MMM 8b,dPPYba, ,adPPYba, MM42MMM
a8" `Y42 a8" "8a 42 42P' `"8a a8P_____42 42
8b 42 8b d8 42 42 42 8PP""""""" 42
"8a, ,d42 "8a, ,a8" 42, 42 42 "8b, ,aa 42,
`"8bbdP"Y8 `"YbbdP"' "Y428 42 42 `"Ybbd8"' "Y428
.NET 6.0.8
Microsoft Windows 10.0.22621
OSArchitecture: Arm64
ProcessArchitecture: Arm64
ProcessorCount: 8
TotalAvailableMemoryBytes: 31.41 GiB
Now, we'll use the x64 dotnet
to run the app.
C:\Users\rich\git\dotnet-docker\samples\dotnetapp>"C:\Program Files\dotnet\x64\dotnet" bin\Debug\net6.0\dotnetapp.dll
42
42 ,d ,d
42 42 42
,adPPYb,42 ,adPPYba, MM42MMM 8b,dPPYba, ,adPPYba, MM42MMM
a8" `Y42 a8" "8a 42 42P' `"8a a8P_____42 42
8b 42 8b d8 42 42 42 8PP""""""" 42
"8a, ,d42 "8a, ,a8" 42, 42 42 "8b, ,aa 42,
`"8bbdP"Y8 `"YbbdP"' "Y428 42 42 `"Ybbd8"' "Y428
.NET 6.0.8
Microsoft Windows 10.0.22621
OSArchitecture: X64
ProcessArchitecture: X64
ProcessorCount: 8
TotalAvailableMemoryBytes: 31.41 GiB
Every .NET (Core) app has a native launcher. It's a component of every app and ties each app to an operating system and architecture. However, you don't need to launch an app with the native launcher that the SDK provides. You can launch it with the dotnet
host instead. That's what is being demonstrated above.
.NET Framework works differently. It has special executables that are not tied to an architecture in quite as direct a way. Lets try some techniques to coerce an x64 app to Arm64.
We'll rebuild the app again to ensure we're starting from a clean slate.
C:\Users\rich\git\dotnet-framework-docker\samples\dotnetapp>msbuild /t:restore;build
C:\Users\rich\git\dotnet-framework-docker\samples\dotnetapp>bin\Debug\net48\dotnetapp.exe
ad88
,d d8"
88 88
8b,dPPYba, ,adPPYba, MM88MMM MM88MMM 8b, ,d8
88P' `"8a a8P_____88 88 88 `Y8, ,8P'
88 88 8PP""""""" 88 88 )888(
88 88 "8b, ,aa 88, 88 ,d8" "8b,
88 88 `"Ybbd8"' "Y888 88 8P' `Y8
.NET Framework 4.8.9065.0
Microsoft Windows 10.0.22621
OSArchitecture: X64
ProcessArchitecture: X64
ProcessorCount: 8
That looks good for our baseline.
Let's first try start
:
C:\Users\rich\git\dotnet-framework-docker\samples\dotnetapp>start /MACHINE arm64 /B /WAIT bin\Debug\net48\dotnetapp.exe
ad88
,d d8"
88 88
8b,dPPYba, ,adPPYba, MM88MMM MM88MMM 8b, ,d8
88P' `"8a a8P_____88 88 88 `Y8, ,8P'
88 88 8PP""""""" 88 88 )888(
88 88 "8b, ,aa 88, 88 ,d8" "8b,
88 88 `"Ybbd8"' "Y888 88 8P' `Y8
.NET Framework 4.8.9065.0
Microsoft Windows 10.0.22621
OSArchitecture: Arm64
ProcessArchitecture: Arm64
ProcessorCount: 8
Our app now runs as Arm64.
runas
is another option that you can use to get a .NET Framework app to run as Arm64.
C:\Users\rich\git\dotnet-framework-docker\samples\dotnetapp>runas /machine:arm64 /trustlevel:0x20000 bin\Debug\net48\dotnetapp.exe
This launches another console windows, which follows:
ad88
,d d8"
88 88
8b,dPPYba, ,adPPYba, MM88MMM MM88MMM 8b, ,d8
88P' `"8a a8P_____88 88 88 `Y8, ,8P'
88 88 8PP""""""" 88 88 )888(
88 88 "8b, ,aa 88, 88 ,d8" "8b,
88 88 `"Ybbd8"' "Y888 88 8P' `Y8
.NET Framework 4.8.9065.0
Microsoft Windows 10.0.22621
OSArchitecture: Arm64
ProcessArchitecture: Arm64
ProcessorCount: 8
In this case, I had to add a Console.ReadLine()
to the app. Otherwise, it would just open and close immediately. start
offers /wait
and /b
to work around that. For UI apps, that won't be a problem.
You can also specify that you want your app to always run in the registry as Arm64.
Create the following registry key (rename for your app):
HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Image File Execution Options\dotnetapp.exe
It would have this DWORD
item and hex value:
PreferredMachine=aa64
or as decimal:
PreferredMachine=43620
You can see my registy entry.
Note: Is there a way to specify the absolute path for this key, not just the filename?
Let's try that:
C:\Users\rich\git\dotnet-framework-docker\samples\dotnetapp>bin\Debug\net48\dotnetapp.exe
ad88
,d d8"
88 88
8b,dPPYba, ,adPPYba, MM88MMM MM88MMM 8b, ,d8
88P' `"8a a8P_____88 88 88 `Y8, ,8P'
88 88 8PP""""""" 88 88 )888(
88 88 "8b, ,aa 88, 88 ,d8" "8b,
88 88 `"Ybbd8"' "Y888 88 8P' `Y8
.NET Framework 4.8.9065.0
Microsoft Windows 10.0.22621
OSArchitecture: Arm64
ProcessArchitecture: Arm64
ProcessorCount: 8
That works as expected.
For folks that start processes from code, you can specify machine type by using the UpdateProcThreadAttribute function. See PROC_THREAD_ATTRIBUTE_MACHINE_TYPE
.
Visual Studio
The targeting experience is the same in Visual Studio as it is for MSBuild.