Exepack.NET, часть 1
Давно мечтал написать exe-packer — упаковщик для исполняемых файлов. Дурацкая мечта, прямо скажем, но у меня в запасе еще много таких.
Как недавно мне удалось выяснить, написать такой упаковщик для .NET-программ можно за три часа. Наверное, можно и быстрее, но у меня получилось за три с копейками. Это такая приятная особенность .NET: почти все уже сделано до нас.
А сделано до нас вот что:
- управляемый загрузчик Assembly.Load() или Assembly.LoadFrom()
- загрузчик ресурсов Assembly.GetManifestResourceStream()
- класс DeflateStream для упаковки и распаковки данных.
В простейшем случае .NET-программа — это один EXE-файл, который ссылается на системные сборки типа mscorlib, System.dll и так далее. Когда я обработаю этот файл упаковщиком, у меня должен получиться другой EXE-файл, который делает то же самое, но места занимает меньше. Естественно, это будет тоже .NET-программа, так называемый загрузчик.
Для начала я прикинул, как будет выглядеть этот самый загрузчик. Он будет загружать откуда-нибудь исходную сборку и запускать ее в своей же виртуальной машине CLR, в дефолтовом домене приложений. Сказано — сделано:
using System; using System.Reflection; class Loader { static void Main(string[] args) { Assembly asm = Assembly.LoadFrom("app.exe"); if (asm.EntryPoint.GetParameters().Length == 0) asm.EntryPoint.Invoke(null, new object[0]); else asm.EntryPoint.Invoke(null, new object[] { args }); } }
В данном случае сборка грузится из файла app.exe. Единственная тонкость, на которую тут надо обратить внимание: метод Main может быть с параметрами, а может — без. Поэтому перед запуском приходится проверять, с каким вариантом мы имеем дело. Более того, этот метод может быть void, а может возвращать значение типа int, это тоже надо бы учитывать (у меня тут, как видите, не учитывается).
Идем дальше. Загружать сборку из файла, понятно, некрасиво. У нас в результате должен быть один-единственный EXE-шник, в котором упаковано все. Поэтому сборку нужно грузить из наших же ресурсов. Когда я буду компилировать новую версию загрузчика, я попрошу компилятор C# присобачить файлик app.exe к моей программе в качестве ресурса. Есть такой удобный ключик у консольного компилятора — /res:app.exe. Ресурс в результате так и будет называться — app.exe.
Осталось разобраться, как загрузить сборку из ресурса. Выясняется, что это тоже довольно просто: вытаскиваем нужный Stream из ресурса с именем hello.exe, считываем в буфер (массив byte[]) и загружаем из него сборку с помощью Assembly.Load():
// csc loader2.cs /res:app.exe using System; using System.IO; using System.Reflection; class Loader { static void Main(string[] args) { using (Stream stream = Assembly.GetExecutingAssembly(). GetManifestResourceStream("app.exe")) { Assembly asm = Assembly.Load( new BinaryReader(stream).ReadBytes((int)stream.Length)); if (asm.EntryPoint.GetParameters().Length == 0) asm.EntryPoint.Invoke(null, new object[0]); else asm.EntryPoint.Invoke(null, new object[] { args }); } } }
Проверяем — работает! Как нетрудно догадаться, если перед компиляцией загрузчика файл app.exe упаковать, а после загрузки, соответственно, распаковать — это и будет практически готовым упаковщиком.
В реальной жизни .NET-программы редко представляют собой единственный EXE-файл. Обычно это целая пачка сборок — файлов EXE и DLL. Что делать с такими программами, я расскажу в следующий раз.
Добрый день!
Полезная информация, спасибо.
Мне, лично было очень интересно, пригодится.
Спасибо.