Exepack.NET, часть 2
Попробуем усложнить задачу. Возьмем какое-нибудь реальное .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-файлы. Пускай это остается на следующий раз.
