При программировании на C# обычно не нужно беспокоиться о базовой целевой платформе. Однако есть несколько случаев, когда архитектура приложения и ОС может повлиять на логику программы, изменить функциональность и вызвать непредвиденные исключения. В этой статье мы рассмотрим, как .NET обрабатывает системы x64 и x86, и предоставим несколько примеров C#, чтобы помочь.

Сборник

В отличие от двоичного кода Native C или C++, созданного для определенной архитектуры, приложение .NET компилируется в независимый от платформы промежуточный язык (IL). Несмотря на это, проект .NET содержит конфигурацию сборки «Platform Target»:

Распространено заблуждение, что выбор конкретной цели приведет к тому, что компилятор сгенерирует код, специфичный для платформы. Это не так, и вместо этого он просто устанавливает флаг в заголовке CLR сборки. Эту информацию можно легко извлечь и изменить с помощью инструмента Microsoft CoreFlags:

В следующей таблице показано, как установка целевой платформы проекта (и предпочтения 32-разрядной версии в AnyCPU) влияет на информацию о состоянии платформы:

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

В C# внешнюю сборку .NET можно проверить аналогичным образом, используя GetAssemblyName() следующим образом:

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

Это возвращает перечисление ProcessorArchitecture, которое идентифицирует процессор и количество битов в слове платформы, на которую нацелен исполняемый файл.

Архитектура времени выполнения

Мы видели, что независимая от платформы сборка .NET может быть скомпилирована как AnyCPU, x86 или x64. Итак, что это означает для архитектуры времени выполнения:

На 32-разрядной машине:

  • Сборки, скомпилированные как AnyCPU или x86, будут работать как 32-разрядный процесс. Во время выполнения они могут загружать AnyCPU и сборки x86, но не x64(BadImageFormatException).
  • Сборки, скомпилированные как x64, всегда будут выдавать ошибку BadImageFormatException.

На 64-разрядной машине:

  • Сборки, скомпилированные как AnyCPU или x64, будут работать как 64-разрядный процесс. Во время выполнения они могут загружать AnyCPU и сборки x64, но не x86(BadImageFormatException).
  • Сборки, скомпилированные как x86, будут работать как 32-разрядный процесс и могут загружать любой процессор и x86 сборки, но не x64(BadImageFormatException).

Определение архитектуры процесса

Базовым механизмом определения текущей архитектуры процесса является проверка размера указателя. Значение равно 4 в 32-разрядном процессе и 8 в 64-разрядном процессе.

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

В качестве альтернативы, начиная с .NET Standard 1.1, существует вспомогательная функция, которая возвращает информацию о том, является ли архитектура приложения x64, x86, Arm или Arm64.

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

Определение системной архитектуры

Простое знание архитектуры процесса не всегда дает полную картину. В некоторых случаях также требуется системная архитектура, которую можно определить простой проверкой для систем x64:

bool Is64BitOperatingSystem = Environment.Is64BitOperatingSystem;

Когда архитектура имеет значение?

Теперь мы понимаем, как целевые платформы влияют на сборки .NET, давайте посмотрим, когда эта разница имеет значение.

Справочные сборки .NET

Как мы видели, приложение, использующее внешние или сторонние сборки, может столкнуться с неожиданными сбоями, если архитектура среды выполнения не соответствует архитектуре импортированной сборки.

Например, если приложение .NET запускается как 64-разрядный процесс и пытается загрузить скомпилированную сборку x86, будет выдано исключение. Поскольку приложение, скомпилированное с помощью AnyCPU, может быть запущено на любой платформе, могут потребоваться дополнительные проверки, чтобы убедиться, что вы выбираете правильную зависимость. Мы можем использовать функции, показанные ниже, для выборочной загрузки правильной сборки:

// 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");
    }
}

Совместимость

В идеальном мире вы могли бы просто программировать на своем любимом языке .NET и никогда больше не касаться нативного кода. К сожалению, это не всегда так, и вам может понадобиться использовать существующие неуправляемые библиотеки.

Совместимость позволяет сохранить и использовать существующие инвестиции в неуправляемый код. [Майкрософт]

P/Invoke (Platform Invoke) предоставляет коду .NET возможность вызывать собственные неуправляемые библиотеки. В большинстве случаев это используется для вызова Windows API, однако таким образом можно вызвать любую собственную библиотеку DLL.

Подобно тому, как ссылки на сборки .NET могут вызвать проблемы, вызов собственных библиотек также зависит от архитектуры среды выполнения и будет определять, какую версию собственного двоичного файла нам нужно загрузить. Если это не соответствует, будет выдано BadImageFormatException. Для поддержки вызова собственных двоичных файлов нам может потребоваться скомпилировать наше приложение .NET для конкретной платформы.

К счастью, при работе с Windows API в 64-битной системе нам предоставляются двоичные файлы для обеих архитектур. Следовательно, их вызов может быть таким же простым, как использование DllImport с именем файла, и система выберет правильную неуправляемую 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);
}

Даже когда Windows предоставляет нам обе версии своих собственных двоичных файлов, другие проблемы с платформой все еще могут возникать из-за сортировки собственных указателей и структур. Структура из нативного кода может иметь разные размеры и смещения в зависимости от архитектуры запущенного процесса. Это происходит из-за того, что значения IntPtr меняются между 32-битными или 64-битными в зависимости от платформы.

Перенаправление системы Windows

Наша последняя потенциальная проблема — перенаправление Windows. 64-разрядная версия Windows прозрачно перенаправляет доступ к файловой системе и реестру в зависимости от архитектуры приложения.

Перенаправление файлов происходит, когда 32-разрядное приложение пытается получить доступ к системным каталогам, таким как каталог c:\Windows\System32, который будет сопоставлен с c:\Windows\SysWOW64. Доступ к реестру также перехватывается для определенных ключей, позволяющих 32-разрядному приложению получить доступ к реестру так же, как если бы оно было на 32-разрядной машине.

В большинстве случаев такое перенаправление является желательным поведением, поскольку именно операционная система обеспечивает доступ к правильным зависимостям во время выполнения. Однако, если приложение преднамеренно пытается скопировать или получить доступ к ресурсам в одном из этих перенаправленных расположений, фактическое расположение на диске может отличаться от ожидаемого.

Встроенная функция Wow64DisableWow64FsRedirection может быть вызвана для отключения перенаправления файловой системы в текущем потоке. Так как он не предусмотрен .NET, его нужно будет вызывать с помощью P/Invoke:

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

Ключи реестра, перенаправляемые Windows, задокументированы здесь. Чтобы избежать этого перенаправления, OpenBaseKey можно использовать для выбора конкретного 64-битного или 32-битного представления:

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

У нас есть больше в нашей серии внутренностей .NET на Medium: