Как удалить неиспользуемые символы C / C ++ с помощью GCC и ld?

111

Мне нужно сильно оптимизировать размер исполняемого файла ( ARMразработка), и я заметил, что в моей текущей схеме сборки ( gcc+ ld) неиспользуемые символы не удаляются.

Использование arm-strip --strip-unneededдля результирующих исполняемых файлов / библиотек не меняет выходной размер исполняемого файла (я понятия не имею, почему, возможно, он просто не может) .

Каким будет способ (если он существует) изменить мой строительный конвейер, чтобы неиспользуемые символы были удалены из результирующего файла?


Я бы даже не подумал об этом, но моя текущая встраиваемая среда не очень "мощная" и экономия даже 500Kна 2Mрезультатах приводит к очень хорошему увеличению производительности загрузки.

Обновить:

К сожалению, текущая gccверсия, которую я использую, не имеет -dead-stripопции, и -ffunction-sections... + --gc-sectionsfor ldне дает существенной разницы для конечного результата.

Я шокирован тем, что это даже стало проблемой, потому что я был уверен, что это gcc + ldдолжно автоматически удалять неиспользуемые символы (зачем им вообще их сохранять?).

Yippie-Ki-Yay
источник
Откуда вы знаете, что символы не используются?
zvrba
Нигде не упоминается => не используется в окончательном приложении. Я предполагаю, что построение графа вызовов при компиляции / связывании не должно быть очень сложным.
Yippie-Ki-Yay
1
Вы пытаетесь уменьшить размер файла .o, удаляя мертвые символы , или вы пытаетесь уменьшить размер фактического отпечатка кода после загрузки в исполняемую память? Тот факт, что вы говорите «встроенный», намекает на последнее; вопрос, который вы задаете, похоже, сосредоточен на первом.
Ira Baxter
@Ira Я пытаюсь уменьшить размер выходного исполняемого файла, потому что (в качестве примера), если я попытаюсь перенести некоторые существующие приложения, которые используют boostбиблиотеки, полученный .exeфайл будет содержать много неиспользуемых объектных файлов и из-за спецификаций моей текущей встроенной среды выполнения , запуск 10mbприложения занимает гораздо больше времени, чем, например, запуск 500kприложения.
Yippie-Ki-Yay
8
@Yippie: вы хотите избавиться от кода, чтобы минимизировать время загрузки; код, от которого вы хотите избавиться, - это неиспользуемые методы / etc. из библиотек. Да, для этого вам нужно построить граф вызовов. Это не так просто; это должен быть глобальный граф вызовов, он должен быть консервативным (не может удалить то, что может быть использовано) и должен быть точным (чтобы у вас был как можно более близок к идеальному графу вызовов, чтобы вы действительно знали, что не используемый). Большая проблема заключается в создании глобального точного графика звонков. Не знаю многих компиляторов, которые это делают, не говоря уже о компоновщиках.
Ira Baxter

Ответы:

131

Для GCC это выполняется в два этапа:

Сначала скомпилируйте данные, но скажите компилятору разделить код на отдельные разделы в блоке перевода. Это будет сделано для функций, классов и внешних переменных с использованием следующих двух флагов компилятора:

-fdata-sections -ffunction-sections

Свяжите единицы перевода вместе с помощью флага оптимизации компоновщика (это заставляет компоновщик отбрасывать разделы, на которые нет ссылок):

-Wl,--gc-sections

Итак, если у вас был один файл с именем test.cpp, в котором были объявлены две функции, но одна из них не использовалась, вы можете опустить неиспользуемый файл с помощью следующей команды gcc (g ++):

gcc -Os -fdata-sections -ffunction-sections test.cpp -o test -Wl,--gc-sections

(Обратите внимание, что -Os - это дополнительный флаг компилятора, который сообщает GCC об оптимизации по размеру)

JT
источник
3
Обратите внимание, что это замедлит исполняемый файл в соответствии с описанием опций GCC (я тестировал).
метаморфоза
1
С mingwэтим не работает статически статическая линковка libstdc ++ и libgcc с флагом -static. Параметр компоновщика -strip-allнемного помогает, но все же сгенерированный исполняемый файл (или dll) примерно в 4 раза больше, чем то, что сгенерирует Visual Studio. Дело в том, что я не могу контролировать, как libstdc++был скомпилирован. Должен быть ldединственный вариант.
Фабио
34

Если верить этому потоку , вам необходимо передать -ffunction-sectionsи-fdata-sections в gcc, который поместит каждую функцию и объект данных в отдельный раздел. Затем вы даете и --gc-sectionsGNU ld удалить неиспользуемые разделы.

Nemo
источник
6
@MSalters: это не по умолчанию, потому что это нарушает стандарты C и C ++. Внезапно глобальная инициализация не происходит, что очень удивляет некоторых программистов.
Ben Voigt
1
@MSalters: только если вы передадите нестандартные параметры нарушения поведения, которые вы предложили сделать поведение по умолчанию.
Ben Voigt
1
@MSalters: если вы можете сделать патч, который запускает статические инициализаторы, если и только если побочные эффекты необходимы для правильной работы программы, это было бы здорово. К сожалению, я думаю, что для этого часто требуется решить проблему остановки, поэтому вам, вероятно, придется иногда ошибаться, добавляя некоторые дополнительные символы. Именно так Ира отвечает в своих комментариях к вопросу. (Кстати: «не обязательно для правильной работы программы» - это другое определение термина «неиспользованный», чем то, как этот термин используется в стандартах)
Бен Фойгт
2
@BenVoigt в C, глобальная инициализация не может иметь побочных эффектов (инициализаторы должны быть постоянными выражениями)
MM
2
@Matt: Но это не так в C ++ ... и они используют один и тот же компоновщик.
Ben Voigt
25

Вы захотите проверить свои документы для вашей версии gcc & ld:

Однако для меня (OS X gcc 4.0.1) я нахожу их для ld

-dead_strip

Удалите функции и данные, недоступные для точки входа или экспортируемых символов.

-dead_strip_dylibs

Удалите дилибы, недоступные для точки входа или экспортируемых символов. То есть подавляет создание команд загрузки для дилибов, которые не предоставили символы во время ссылки. Этот параметр не следует использовать при компоновке с dylib, который требуется во время выполнения по какой-то косвенной причине, такой как dylib имеет важный инициализатор.

И этот полезный вариант

-why_live symbol_name

Регистрирует цепочку ссылок на имя_символа. Применимо только с -dead_strip. Это может помочь отладить, почему то, что, по вашему мнению, должно быть удалено, не удаляется.

Также в gcc / g ++ man есть примечание о том, что определенные виды удаления мертвого кода выполняются только в том случае, если при компиляции включена оптимизация.

Хотя эти параметры / условия могут не подходить для вашего компилятора, я предлагаю вам поискать что-то подобное в своих документах.

Майкл Андерсон
источник
Кажется, это не имеет никакого отношения к mingw.
Фабио
-dead_stripне gccвариант.
ar2015
21

Привычки программирования тоже могут помочь; например, добавить staticк функциям, доступ к которым не осуществляется за пределами определенного файла; используйте более короткие имена для символов (может немного помочь, скорее всего, не слишком); использовать по const char x[]возможности; ... этот документ , хотя и говорит о динамических общих объектах, может содержать предложения, которые, если им следовать, могут помочь уменьшить конечный размер двоичного вывода (если ваша цель - ELF).

ShinTakezou
источник
4
Как это помогает выбирать более короткие названия для символов?
fuz
1
если не убрать символы, ça va sans dire - но, похоже, об этом нужно сказать сейчас.
ShinTakezou
@fuz В документе говорится о динамических общих объектах (например, .soв Linux), поэтому имена символов должны быть сохранены, чтобы API, такие как ctypesмодуль FFI Python, могли использовать их для поиска символов по имени во время выполнения.
ssokolow
18

Ответ есть -flto. Вы должны передать его как на этапах компиляции, так и на этапах связывания, иначе он ничего не сделает.

На самом деле он работает очень хорошо - уменьшил размер написанной мной программы микроконтроллера до менее чем 50% от ее предыдущего размера!

К сожалению, это показалось немного глючным - у меня были случаи, когда что-то строилось неправильно. Возможно, это произошло из-за используемой мной системы сборки (QBS; она очень новая), но в любом случае я бы рекомендовал вам включить ее только для вашей окончательной сборки, если это возможно, и тщательно протестировать эту сборку.

Тимммм
источник
1
«-Wl, - gc-section» не работает на MinGW-W64, «-flto» у меня работает. Спасибо
rhbc73
Выходная сборка очень странная, -fltoя не понимаю, что она делает за сценой.
ar2015
Я считаю, -fltoчто он не компилирует каждый файл в сборку, он компилирует их в LLVM IR, а затем последняя ссылка компилирует их, как если бы они все были в одной единице компиляции. Это означает, что он может удалить неиспользуемые функции и встроенные не- staticединицы, а также, возможно, другие вещи. См. Llvm.org/docs/LinkTimeOptimization.html
Timmmm
13

Хотя это и не о символах, если идти по размеру - всегда компилировать с -Osи -sфлаги. -Osоптимизирует результирующий код для достижения минимального размера исполняемого файла и -sудаляет таблицу символов и информацию о перемещении из исполняемого файла.

Иногда - если желателен небольшой размер - игра с разными флагами оптимизации может иметь - а может и не иметь - значение. Например, переключение -ffast-mathи / или -fomit-frame-pointerвременами может сэкономить вам даже десятки байтов.

zxcdw
источник
Большинство настроек оптимизации по-прежнему дадут правильный код, если вы соблюдаете языковой стандарт, но я -ffast-mathнанес серьезный ущерб полностью совместимому со стандартами программному коду C ++, поэтому я бы никогда не рекомендовал его.
Raptor007
11

Мне кажется, что ответ Nemo правильный. Если эти инструкции не работают, проблема может быть связана с версией gcc / ld, которую вы используете, в качестве упражнения я скомпилировал пример программы, используя инструкции, подробно описанные здесь.

#include <stdio.h>
void deadcode() { printf("This is d dead codez\n"); }
int main(void) { printf("This is main\n"); return 0 ; }

Затем я скомпилировал код, используя все более агрессивные переключатели удаления мертвого кода:

gcc -Os test.c -o test.elf
gcc -Os -fdata-sections -ffunction-sections test.c -o test.elf -Wl,--gc-sections
gcc -Os -fdata-sections -ffunction-sections test.c -o test.elf -Wl,--gc-sections -Wl,--strip-all

Эти параметры компиляции и связывания привели к созданию исполняемых файлов размером 8457, 8164 и 6160 байтов, соответственно, наиболее существенный вклад вносит объявление «strip-all». Если вы не можете произвести аналогичные сокращения на своей платформе, возможно, ваша версия gcc не поддерживает эту функцию. Я использую gcc (4.5.2-8ubuntu4), ld (2.21.0.20110327) в Linux Mint 2.6.38-8-generic x86_64

Героид Мерфи
источник
8

strip --strip-unneededработает только с таблицей символов вашего исполняемого файла. На самом деле он не удаляет исполняемый код.

Стандартные библиотеки достигают желаемого результата, разделяя все свои функции на отдельные объектные файлы, которые объединяются с помощью ar. Если вы затем свяжете полученный архив как библиотеку (т. -l your_libraryЕ. Дадите опцию ld), тогда ld будет включать только объектные файлы и, следовательно, символы, которые фактически используются.

Вы также можете найти некоторые ответы на этот аналогичный вопрос использования.

Эндрю Эджкомб
источник
2
Отдельные объектные файлы в библиотеке актуальны только при статической ссылке. С разделяемыми библиотеками загружается вся библиотека, но, конечно, не включается в исполняемый файл.
Джонатан Леффлер
4

Я не знаю, поможет ли это в вашем текущем затруднительном положении, поскольку это новая функция, но вы можете указать видимость символов глобально. Передача -fvisibility=hidden -fvisibility-inlines-hiddenпри компиляции может помочь компоновщику впоследствии избавиться от ненужных символов. Если вы создаете исполняемый файл (в отличие от общей библиотеки), делать больше нечего.

Дополнительная информация (и подробный подход, например, для библиотек) доступна в вики GCC .

Люк Дантон
источник
4

Из руководства GCC 4.2.1, раздел -fwhole-program:

Предположим, что текущая единица компиляции представляет собой компилируемую программу целиком. Все общедоступные функции и переменные, за исключением mainтех, которые объединены атрибутом, externally_visibleстановятся статическими функциями и в результате более агрессивно оптимизируются межпроцедурными оптимизаторами. Хотя этот параметр эквивалентен правильному использованию staticключевого слова для программ, состоящих из одного файла, в сочетании с параметром --combineэтот флаг можно использовать для компиляции большинства программ на C меньшего масштаба, поскольку функции и переменные становятся локальными для всей объединенной единицы компиляции, а не для сам единственный исходный файл.

awiebe
источник
Да, но это предположительно не работает с какой-либо инкрементальной компиляцией и, вероятно, будет немного медленным.
Timmmm
@Timmmm: Я подозреваю, что вы думаете об этом -flto.
Ben Voigt
Да! Впоследствии я нашел это (почему это не один из ответов?). К сожалению, он казался немного глючным, поэтому я бы рекомендовал его только для окончательной сборки, а затем много тестировал эту сборку!
Timmmm
-2

Вы можете использовать двоичный файл удаления в объектном файле (например, исполняемом файле), чтобы удалить из него все символы.

Примечание: он изменяет сам файл и не создает копию.

ton4eg
источник