Как изменить размер exe файла

Обновлено: 07.07.2024


В интернетах есть несколько старых статей (тыц, тыц) про то, как уменьшить размер exe-файла, который генерирует Visual C++. Я взялся проверить актуальность рецептов, которые там приводятся, для Visual Studio 2010.

Задачу я взял классическую: найти 10 самых популярных слов в текстовом файле. Мой исходный файл был размером 73728 байт.

Начало

Вот мое изначальное решение, написанное за 8 минут и заработавшее со второй компиляции:

int main()
try string s = "input.txt" ;
ifstream inf(s.c_str(), ios::binary);
if (inf.good()) string word;
map<string, int > wc;
while (inf >> word) ++wc[word];
>
inf.close();
vector< pair< int , string> > wcvec;
for ( auto i = wc.begin(), iend = wc.end(); i != iend; ++i) wcvec.push_back(make_pair(i->second, i->first));
>
sort(wcvec.begin(), wcvec.end());
for ( size_t i = wcvec.size() - 1 , j = 0 ; i >= 0 && j < 10 ; --i, ++j) cout << boost::format( "%6d %s \n " ) % wcvec[i].first % wcvec[i].second;
>
> else throw runtime_error( "No such file" );
>
> catch (exception& e) cerr << e.what() << endl;
> catch (. ) cerr << "Unknown error" << endl;
>
>

VC2010 в конфигурации Release выдал exe-файл размером 73728 байт. Посмотрим внутрь, что там столько жрет:

Заголовок0x400
.text0xC400 = 50176 байтВ этой секции находится код
.rdata0x3A00строки, манглированные имена
.data0x0A00еще немного манглированных имен
.rsrc0x0200манифест
.reloc0x1200рассказ загрузчику как менять адреса в программе, если файл загружен не по тому адресу, по какому ожидал линкер

Наш файл зависит от библиотек рантайма Visual C: msvcp100.dll, msvcr100.dll (10.0 здесь это версия, XP SP2 в стандартной поставке имеет только до 7.0), и, как и каждый исполнимый файл в системе, от kernel32.dll (тот, в свою очередь всегда тянет за собой ntdll.dll).

В памяти программа занимает максимум 2.5 мегабайта на входном файле в полмегабайта. В адресное пространство мапятся нужные dll, кодовые страницы и .nls, по странице (4 KB) жрут переменные окружения, параметры процесса, PEB и TIB, 574 страницы съедает heap и, вопреки опасениям, стек выделяется не весь сразу, а по мере надобности, в пике 3 страницы на сам стек и 1 на его guard page.

Сборка

Для начала попробуем уменьшить файл ключами компилятора и линкера. Я провел небольшое исследование, воспользовавшись, в том числе, утилитой, которая собирала программу, перебирая найденные ключи. Каких-то 8000 exe-файлов за ночь сборки, и я уже могу поделиться результатами :)

Отмечу, что с теми же опциями + /NODEFAULTLIB минимальный hello world с одним MessageBoxA занимает 640 байт (против 7186 байт в Release по дефолту и 28160 в Debug).

Воспользовавшись Function list в одном известном дизассемблере и самописным скриптом, взвесим различные смысловые части кода в изначальном файле:

Init and deinit 889
main 1832
Exceptions 4537
RTTI 82
Runtime checks 527
PE functions 332
Float arithmetics 222
Memory allocation 413
Locks 12
8846
std memory fns 1524
std::allocator 368
std::exception types 152
std::iostream 2022
Locale and facets 700
std::fstream 528
std bufs 3142
std::map 400
std::_Tree 2992
std::vector 1984
std::basic_string 3607
std::char_traits 128
std::pair 400
std::sort 4192
std::heap 993
std comparisons 160
23292
boost::format 4128
boost::io 9216
boost::optional 48
boost::base_from_member 64
boost::exception 4184
boost::detail 234
17874
Overall: 50012

Легко видеть, что boost, который здесь почти ничего не делает (чего не смог бы сделать банальный printf ), занимает почти треть кода. Заменяем, 42544 → 19744 байт.

Сказал Э, скажи Ю. Заменим весь iostream на fopen, printf и K°. 10865 байт.

Уберем sort и vector, заменив на insertion sort в простом массиве размера 10. 8336 байт.

Заменим map на unordered_map. 8896 байт. Ой, я пошутил, откатимся обратно. Внезапно хэш-таблица, которую бы я писал, если бы фашисты отобрали у меня STL, оказалась толще красно-черного дерева.

Выпилим исключения, все равно они здесь почти ничего не делают, и заменим на обработку ошибок в старом мерзком си-стиле. 8112 байта.

Да, поддержку C++ исключений теперь можно и отключить. 7360 байт. Все, больше ничего не смог придумать :)

Теперь можно расслабиться и проверить энтропию. Популярнейший упаковщик исполнимых файлов UPX пожал исходный файл до 32256 байт, с исправленным не справился из-за /align; проставил /align:512, upx пожал до 6144 байт, но побил файл. 7z пожал исходный файл до 29591 байт, а исправленный до 3955 байт. Есть еще простор для сжатия :)

Напомню, исходный код был написан за 8 минут, а оптимизация ключей компилятора и линкера с последующим переписыванием в более компактный вид заняла 18 часов.

UPD: По совету xproger и wizzard0 попробовал Crinkler, хитрый линкер для интрописателей. Как линкеру, ему нужен .obj, который генерирует студия в одну из папок проекта. Для начала следует собрать проект с отключенной Whole program optimization (/GL, оно немного увеличит студийный exe), затем исполнить команду вида:

crinkler.exe /ENTRY:main /SUBSYSTEM:CONSOLE /COMPMODE:SLOW kernel32.lib user32.lib msvcrt.lib msvcprt.lib main.obj

Подумав минуту, Crinkler выплюнет exe размером 3907 байт, который вообще не сжимается 7z! Теперь, думаю, простор для сжатия закончился :)

Читайте также: