Что в CLR может выполниться до метода Main()?
Вы задумывались когда-нибудь, может-ли некоторый код выполняться до вызова метода Main()
(точки входа приложения) в .NET-приложении?
- Самый очевидный вариант - статический конструктор типа, определяющего метод точки входа. Не смотря на различные правила порядка инициализации статических данных в разных версиях CLR, на практике инициализатор типа всегда вызывается до вызова точки входа, вне зависимости от наличия/доступа к статическим полям:
class Program {
static Program() {
System.Console.WriteLine("type initializer");
}
static void Main() {
System.Console.WriteLine("entry point");
}
}
- Второй способ вторгнуться до вызова точки входа - CAS-атрибуты. Механизм CAS объявлен deprecated в .NET 4.0, однако продолжает работать (мне на радость), правда вызывая дичайшие тормоза (вызов метода с CAS-атрибутом на 3-4 порядка медленнее, чем вызов обычного метода!). Атрибутом-наследником
CodeAccessSecurityAttribute
можно отметить методы и типы, а так же сборки. Например, можно определить такой атрибут в сборкеCasAssembly.dll
:
using System;
using System.Security;
using System.Security.Permissions;
[AttributeUsage(
AttributeTargets.Assembly | AttributeTargets.Class |
AttributeTargets.Struct | AttributeTargets.Constructor |
AttributeTargets.Method, AllowMultiple=true, Inherited=false)]
public sealed class FooAttribute : CodeAccessSecurityAttribute {
public FooAttribute(SecurityAction action = SecurityAction.Demand)
: base(action) { }
public string Level { get; set; }
public override IPermission CreatePermission() {
Console.WriteLine("cas: {0} level", Level);
return null; // не возвращаем ничего
}
}
А затем его использовать в приложении ConsoleApplication.exe
:
using System;
using System.Security.Permissions;
[assembly: Foo(SecurityAction.RequestMinimum, Level="assembly")]
[Foo(Level="type")]
class Program {
[Foo(Level="constructor")]
static Program() {
Console.WriteLine("type initializer");
}
[Foo(Level="method")]
static void Main() {
Console.WriteLine("entry point");
}
}
То вывод будет следующим:
cas: assembly level
cas: type level
cas: type level
cas: constructor level
type initializer
cas: type level
cas: type level
cas: method level
entry point
Отдельная сборка нужна из-за того, что CAS-атрибут уровня сборки требует, чтобы сборка с типом данного атрибута могла быть полностью загружена в момент проверки прав. Проверка уровня типа случается дважды из-за того, что вызов метода требует два права - на обращение к типу и на обращение к конкретному методу, однако обращение к методу возможно только если вызывающая сторона имеет право на доступ к типу.
Итак мы смогли исполнить произвольный код до статического конструктора Program, в момент загрузки сборки. Однако есть ещё один вариант…
- Третий способ - конструктора модуля. Что это за зверь, спросите вы? :)) Любое .NET-приложение состоит из сборок, каждая из которых может состоять из модулей. VS позволяет работать только с одномодульными сборками, поэтому многие .NET-программисты не подозревают о существовании модулей. Нужны они для многофайловых сборок, представляющих собой
exe
/dll
-файл + набор файлов с расширением.netmodule
(наверняка вы первый раз про такие слышите). Единственный их юзкейс - модули подгружаются отложенно, что может давать бенефиты, если у вас в сборке много ресурсов и они не всегда сразу нужны во время выполнения.
Так вот, внутри главного из модулей (dll
/exe
-файла) всегда существует специальный тип (обычно названный <Module>
), занимающий первое место в таблице токенов типов. Этот может использоваться (и используется ilasm
‘ом) для глобальных переменных и глобальных методов, если язык предоставляет таковые. Так вот, этот тип может иметь статический конструктор - этот метод и называется конструктором модуля. Исполняется он после загрузки сборки, но до всякого обращения к типу, определяющего точку входа приложения.
Проверить это мы можем, сгенерировав сборку с конструктором модуля через программный интерфейс генерации кода - System.Reflection.Emit
(или с помощью компилятора ilasm
):
using System;
using System.Reflection;
using System.Reflection.Emit;
static class Program {
static void Main() {
// создаём динамическую сборку в текущем домене
var assemblyBuilder = AppDomain.CurrentDomain
.DefineDynamicAssembly(new AssemblyName("FooAssembly"),
AssemblyBuilderAccess.RunAndSave);
// в динамической сборке определяем единственный модуль
const string moduleName = "test.exe";
var moduleBuilder = assemblyBuilder
.DefineDynamicModule("FooModule", moduleName);
// создаём глобальный метод (контруктор модуля)
var cctor = moduleBuilder.DefineGlobalMethod(".cctor",
MethodAttributes.RTSpecialName |
MethodAttributes.SpecialName |
MethodAttributes.Static, null, null);
var il2 = cctor.GetILGenerator();
il2.EmitWriteLine("module initializer");
il2.Emit(OpCodes.Ret);
moduleBuilder.CreateGlobalFunctions();
// определяем статический класс (в IL это abstract sealed)
var fooType = moduleBuilder.DefineType("Program",
TypeAttributes.Abstract | TypeAttributes.Sealed);
// определяем статический конструктор
var ctorBuilder = fooType.DefineTypeInitializer();
var il = ctorBuilder.GetILGenerator();
il.EmitWriteLine("type initializer");
il.Emit(OpCodes.Ret);
// определяем статический метод Main() - точку входа
var mainBuilder = fooType.DefineMethod("Main",
MethodAttributes.Public | MethodAttributes.Static,
CallingConventions.Standard, typeof(void), null);
il = mainBuilder.GetILGenerator();
il.EmitWriteLine("entry point");
il.Emit(OpCodes.Ret);
fooType.CreateType();
// задаём точку входа и сохраняем сборку в exe-файл
assemblyBuilder.SetEntryPoint(
mainBuilder, PEFileKinds.ConsoleApplication);
assemblyBuilder.Save(moduleName);
}
}
Получаем сборку, запуск которой выводит на экран:
module initializer
type initializer
entry point
— update —
- Четвёртый способ подсказал знаток недр CLR @tr_tr_mitya - менеджер доменов. Куда лучше меня про эту штуку подробно расскажет msdn (и подобные записи в блогах), а я лишь скажу, что это это управляемый аналог хоста CLR. Некоторый специальный тип, управляющий загрузкой сборок и созданием доменов .NET-приложений, а так же отвечающий за некоторые вопросы безопасности и Remoting’а.
Менеджер доменов представляет собой тип-наследник System.AppDomainManager
, определённый в подписанной строгим именем сборке, находящейся в GAC. Например:
using System;
public class FooDomainManager : AppDomainManager {
public override void InitializeNewDomain(AppDomainSetup info) {
Console.WriteLine("init domain");
}
}
Далее различными можно указать данный менеджер для использования в файле App.config
любого приложения (эта возможность доступна только начиная с .NET 4.0) следующим образом:
<?xml version="1.0"?>
<configuration>
<runtime>
<!-- полное имя сборки с AppDomainManager'ом -->
<appDomainManagerAssembly value="DomainManager,
Version=1.0.0.0, Culture=neutral, PublicKeyToken=679fb76896252e34"/>
<!-- полное имя типа AppDomainManager'а -->
<appDomainManagerType value="FooDomainManager"/>
</runtime>
</configuration>
До .NET 4.0 можно было удобно задать AppDomainManager
лишь глобально - для всей системы, установив переменные окружения (так же возможно задать менеджер через реестр windows или через unmanaged API в случае хостинга CLR внутри неуправляемого приложения):
set APPDOMAIN_MANAGER_ASM=DomainManager, Version=1.0.0.0, PublicKeyToken=679fb7...
set APPDOMAIN_MANAGER_TYPE=FooDomainManager
Код менеджера доменов исполняется вовсе до загрузки сборки нашего приложения, до проверки CAS-атрибутов уровня сборки.
Если вы знаете ещё способы вклиниться в процесс запуска .NET-приложения, то не стесняйтесь написать комментарий :))