Exepack.NET, часть 2

Программа app.exe в дизассемблере ILDASM

Попробуем усложнить задачу. Возьмем какое-нибудь реальное .NET-приложение, которое состоит из нескольких сборок. Как правило, это один EXE-файл и несколько дополнительных DLL-библиотек.

Модули (файлы *.netmodule) я рассматривать не буду, никогда не видел, чтобы ими кто-то пользовался. Я могу ошибаться, но по-моему, в Visual Studio нет для них полноценной поддержки: проекты компилируются в монолитные сборки, а не в набор модулей. Теоретически, конечно, это может быть реализовано по-разному на разных платформах, но я пока не ставил себе цели написать полностью переносимый EXE-упаковщик.

Чтобы не искать готовое приложение, я за минуту написал небольшую программку из двух файлов: app.cs и applib.cs. На картинке показано, как такая программа выглядит в дизассемблере ILDASM (красным выделена ссылка на сборку-библиотеку).

Теперь я модифицирую загрузчик так, чтобы он мог распаковать и запустить программу вместе с библиотекой. Файлы app.exe и applib.dll я присоединяю к ресурсам моего загрузчика, чтобы получить один EXE-файл. Загрузчик работает по старой схеме: вытаскивает ресурс с именем app.exe и выполняет метод Main() с помощью методов отражения. Но теперь при загрузке происходит еще кое-что интересное.

Файл app.exe невозможно загрузить отдельно от applib.dll. Поэтому среда исполнения .NET перед загрузкой сборки app.exe начинает усиленно искать applib.dll везде, где только можно: сначала в текущем каталоге, потом в GAC. Поскольку файла нигде нет (он ведь теперь упакован), среда генерирует исключение: файл не найден.

А раз есть исключение, мы можем написать для него свой обработчик. Обработчик (у меня он называется ExtractAssembly) подключается одной строчкой:

AppDomain.CurrentDomain.AssemblyResolve += ExtractAssembly;

Обработчик работает по той же схеме, что и загрузчик: он вытаскивает из ресурсов файл applib.dll и подсовывает его исполняющей среде .NET вместо файла на диске. Вот как это делается:

static Assembly ExtractAssembly(object sender,
  ResolveEventArgs args)
{
    return GetResourceAssembly(new AssemblyName(
      args.Name.ToLowerInvariant()).Name + ".dll");
}
 
static Assembly GetResourceAssembly(string name)
{
    using (Stream stream = Assembly.GetExecutingAssembly().
      GetManifestResourceStream(name))
    {
        Assembly asm = Assembly.Load(
          new BinaryReader(stream).ReadBytes((int)stream.Length));
        return asm;
    }
}

Тут есть один маленький нюанс. Обработчику передается не имя файла, а имя сборки, в данном случае — «applib». Вообще говоря, тут нужно сохранить табличку соответствия имен сборок именам их файлов, но пока я ограничился самым простым вариантом: к имени сборки я добавляю расширение «.dll».

Собственно, это уже практический нормальный загрузчик, которым уже можно пользоваться. Осталось только добавить к нему распаковку ресурсов перед загрузкой (это делается одной строкой кода, с помощью DeflateStream) и еще пару мелочей:

// csc loader.cs /res:app.deflated /res:applib.deflated
// Written by Y [10-01-09]
 
using System;
using System.IO;
using System.IO.Compression;
using System.Reflection;
 
class Loader
{
  [STAThread]
  static void Main(string[] args)
  {
    AppDomain.CurrentDomain.AssemblyResolve += ExtractAssembly;
 
    Assembly asm = GetResourceAssembly("app.exe");
 
    if (asm.EntryPoint.GetParameters().Length == 0)
      asm.EntryPoint.Invoke(null, new object[0]);
    else
      asm.EntryPoint.Invoke(null, new object[] { args });
  }
 
  static Assembly ExtractAssembly(object sender,
    ResolveEventArgs args)
  {
    return GetResourceAssembly(
      new AssemblyName(args.Name.ToLowerInvariant()).Name + ".dll");
  }
 
  static Assembly GetResourceAssembly(string name)
  {
    using (Stream stream = Assembly.GetExecutingAssembly().
      GetManifestResourceStream(name))
    using (DeflateStream ds =
      new DeflateStream(stream, CompressionMode.Decompress))
    {
      // (int)ds.Length не поддерживается,
      // пока сделаем ограничение на 4 мегабайта
      Assembly asm = Assembly.Load(
        new BinaryReader(ds).ReadBytes(4000000));
      return asm;
    }
  }
}

Теперь, когда загрузчик готов, остается сделать не так уж много. Нужно написать программу, которая упаковывает сборки и компонует их вместе с загрузчиком в монолитные EXE-файлы. Пускай это остается на следующий раз.

Leave a Reply