понедельник, 28 марта 2011 г.

Проблемы запуска .Net приложений под 64-битными ОС

Я уже второй раз за последнее время сталкиваюсь с разными проблемами запуска «управляемых» приложений под 64-битным операционками, так что думаю, пришло время немного об этом рассказать.

Итак, программирование на платформе .Net делает еще один шаг по отделению прикладного программиста от всяких низкоуровневых платформенно-зависимых вещей типа размеров указателя, выравнивания, размеров целочисленных типов данных и других проблем, характерных для программирования на неуправляемых С/С++. Теперь все становится значительно проще и компиляторы таких языков программирования как C# или VB.Net не генерируют ассемблерные инструкции напрямую (хотя такая возможность и присутствует), вместо этого, результатом компиляции является код на промежуточном языке программирования, который затем компилируется во время выполнения JIT-компилятором в набор инструкций для конкретной платформы.

Если говорить за .Net 1.0 и 1.1, то в те старые добрые времена существовала только лишь 32-х разрядная версия CLR, что приводило к запуску .Net приложений на 64-х битных операционных системах в режиме совместимости (WOW64). Начиная с .Net 2.0 у разработчика появилась возможность выбора «целевой платформы» (Target platform), для которой это приложение предназначено. Обычно, этот выбор сводится к одному из следующих вариантов:

  • Any CPU – конкретная платформа будет выбрана автоматически при запуске приложения;
  • x86 – это старый добрый 32-х битный режим для процессоров семейства x86;
  • x64 – это новый модный режим для 64-х битных процессоров;
  • Itanium – а это старый и немодный режим для процессоров Itanium.

Все эти новшества полезны и замечательны, пока речь не заходит о взаимодействии с неуправляемым кодом, написанным на «чистом» С/С++. В таком случае, ваше приложение может сколько угодно говорить, что оно предназначено для “Any CPU”, но если одна из сборок использует неуправляемую dll, заточенную под определенную платформу, то чтобы заставить работать его под другой платформой придется попотеть.

64-х битная ОС и 32-х битная неуправляемая dll

Давайте рассмотрим следующую ситуацию: у нас есть «управляемое приложение», скомпилированное под “Any CPU”, при этом одна из сборок этого приложения использует неуправляемую 32-х битную dll (рисунок 1). За примером далеко ходить не нужно, поскольку многие существующие библиотеки все еще опираются на «нативные» реализации; так, например, многие драйвера доступа к базам данных, а также компоненты для работы с картами, реализованы именно таким образом.

image

Рисунок 1 – Запуск приложения с 32-х битной неуправляемой dll

Итак, при запуске приложения, операционная система проверяет информацию в заголовке исполняемого файла, чтобы определить в каком режиме его запускать: в режиме совместимости с 32-х битными приложения (т.е. в WOW64 режиме), или запускать его, как полноценное 64-х битное приложение. Если «управляемое» приложение собрано под “Any CPU”, то логично предположить, что будет выбран «честный» 64-х битный режим, который не требует никаких дополнительных «прослоек» (какой является режим WOW64), что позволит полностью воспользоваться всеми возможностями 64-х битной операционной системы. В результате будет запущена 64-х битная версия CLR, которая будет компилировать IL-код в соответствующий набор инструкций процессора. Но вот беда, процесс может быть запущен только в одном режиме и переключаться между ними он не в состоянии, поэтому, как только ваше приложение попытается загрузить неуправляемую библиотеку, оно рухнет со страшными криками в виде BadImageFormatException.

Решается эта проблема весьма просто, достаточно это приложение скомпилировать под платформу x86, явно указав это в настройках проекта (установив Platform target в x86). Это явным образом скажет операционной системе, что это приложение нужно запускать в режиме совместимости и никаких ошибок во время выполнения вы больше не получите. Именно поэтому в Visual Studio 2010 изменены настройки по умолчанию у всех «управляемых» исполняемых файлов. Теперь, у всех приложений вместо Any CPU Target platform установлен как x86. Это не касается сборок (Assembly), у них в настройках по-прежнему используется Any CPU, но это и не важно, поскольку операционная система все равно отталкивается от информации в заголовке исполняемого файла.

64-х битная ОС и 64-х битная неуправляемая dll

С первого взгляда может показаться, что на 64-х разрядной операционной системе не может быть проблем с 64-х битными неуправляемыми dll, однако две бессонные ночи (*) показали, что это совсем не так.

Итак, давайте рассмотрим следующий сценарий: у нас есть сборка, скомпилированная под Any CPU, которая использует неуправляемую 64-х битную dll, и все это дело используется из powershell-а (рисунок 2).

image

Рисунок 2 – Архитектура приложения

С первого взгляда, в такой архитектуре нет ничего криминального, и действительно, так оно и было, пока мы не попытались запустить все это дело через нестандартный планировщик (**), который оказался 32-х разрядным приложением. Все дело в том, что при попытке запустить управляемое приложение, скомпилированное под Any CPU из 32-х битного приложения, работаюего в режиме WOW64, ваше приложение также будет запущено в этом же 32-х битном режиме (рисунок 3).

 image

Рисунок 3 – Запуск PowerShell.exe из 32-х битного планировщика

Теперь мы получаем аналогичную проблему, но уже вывернутую на изнанку: мы получаем BadImageFormatException при попытке использовать неуправляемую 64-х битную dll из 32-х битного процесса на 64-х битной операционной системе! Звучит неправдоподобно, но это факт! Причем самое интересное, что существует 32-х битная версия PowerShell.exe, которая располагается C:\Windows\SysWOW64\WindowsPowerShell\v1.0, но вот «чистой» 64-х битной версии не существует.

Решение проблемы состоит в использовании дополнительного приложения, принудительно скомпилированного в режиме x64, которое будет запускаться из 32-х битного планировщика, и которое, в свою очередь, будет запускать PowerShell.exe в 64-х битном режиме (рисунок 4). Другим вариантом решения является использование утилиты CorFlags, которая может изменить информацию в заголовке исполняемого файла и заставить запускаться исполняемый файл в указанном формате. Однако эта славная утилита была найдена слишком поздно, и исправлять что-то на продакшне уже не было никакого желания.

image

Рисунок 4 – Запуск PowerShell.exe с помощью дополнительного приложения

Ну что ж, на сегодня все. Я надеюсь, что на практике вы не столкнетесь с подобными проблемами, а если и столкнетесь, то не будете под таким прессом времени и ответственности, под каким оказалась наша команда. Но, в любом случае, я надеюсь, что эта небольшая заметка сможет помочь вам решить проблемы запуска «управляемых» приложений под 64-х битными операционными системами, если такая проблема вдруг возникнет.

--------------------------

(*) Причина двух бессонных ночей была не только в этом, там получился целый ряд проблем, и эта – была лишь одной из них.

(**) Что это за планировщик и почему используется именно он, совершенно не важно, важно только то, что он оказался «чистым» 32-х битным приложением.

Несколько дополнительных ссылок

4 комментария:

  1. да, сколько я задолбался такой проблемой под win2008 r2 x64, IIS и MS CRM

    ОтветитьУдалить
  2. Небольшая ремарка. Достаточно стартап проект скомпилировать под x86, и тогда все приложение будет запущено в режиме совместимости и неуправляемые сборки будут корректно работать

    ОтветитьУдалить
  3. @cyrillyun: Мне казалось, что я об этом писал:
    Решается эта проблема весьма просто, достаточно это приложение скомпилировать под платформу x86, явно указав это в настройках проекта (установив Platform target в x86). Это явным образом скажет операционной системе, что это приложение нужно запускать в режиме совместимости и никаких ошибок во время выполнения вы больше не получите.
    Но это самый простой случай, а вот второй рассмотренный случай оказался несколько сложнее.

    ОтветитьУдалить
  4. Да у меня была такая проблема, когда я использовал библиотеку sqlite3.dll. В тот момент мне повезло и я угадал с настройками компилятора. А благодаря этой статье, стала понятна причина. Автору спасибо.

    ОтветитьУдалить