Как удалить неиспользуемые символы C/C++ с помощью GCC и ld?
Мне нужно строго оптимизировать размер моего исполняемого файла (ARM
развитие), и я заметил, что в моей текущей схеме сборки (gcc
+ ld
) неиспользованные символы не удаляются.
Использование arm-strip --strip-unneeded
поскольку получающиеся исполняемые файлы / библиотеки не изменяют выходной размер исполняемого файла (я понятия не имею, почему, возможно, это просто не может).
Каким образом (если он существует) можно изменить мой строительный конвейер, чтобы неиспользуемые символы удалялись из полученного файла?
Я бы даже не подумал об этом, но моя текущая встроенная среда не очень "мощная" и экономит даже 500K
снаружи 2M
приводит к очень хорошему повышению производительности загрузки.
Обновить:
К сожалению нынешний gcc
версия, которую я использую, не имеет -dead-strip
вариант и -ffunction-sections... + --gc-sections
за ld
не дает какой-либо существенной разницы для полученного результата.
Я в шоке, что это даже стало проблемой, потому что я был уверен, что gcc + ld
должен автоматически убирать неиспользуемые символы (почему они вообще должны их оставлять?).
13 ответов
Для 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 оптимизировать размер)
Вы захотите проверить свои документы на наличие версии gcc & ld:
Однако для меня (OS X gcc 4.0.1) я нахожу это для ld
-dead_strip
Удалите функции и данные, которые недоступны для точки входа или экспортируемых символов.
-dead_strip_dylibs
Удалите дизели, которые недоступны точкой входа или экспортированными символами. То есть подавляет генерацию команд загрузки команд для dylib, которые не указывали символы во время ссылки. Эта опция не должна использоваться при связывании с dylib, который требуется во время выполнения по некоторой косвенной причине, такой как dylib имеет важный инициализатор.
И этот полезный вариант
-why_live symbol_name
Регистрирует цепочку ссылок на имя_символа. Применимо только с
-dead_strip
, Это может помочь отладить, почему то, что, по вашему мнению, должно быть удалено, не удалено.
В gcc/g++ man также есть примечание, что определенные виды удаления мертвого кода выполняются только в том случае, если при компиляции включена оптимизация.
Хотя эти параметры / условия могут не подходить для вашего компилятора, я предлагаю вам найти что-то похожее в ваших документах.
Ответ -flto
, Вы должны передать его как на этапы компиляции, так и на этапы компоновки, иначе он ничего не сделает.
Это на самом деле работает очень хорошо - уменьшил размер написанной мной программы микроконтроллера до менее 50% от ее предыдущего размера!
К сожалению, это казалось немного глючным - у меня были случаи, когда вещи создавались неправильно. Возможно, это произошло из-за системы сборки, которую я использую (QBS; она очень новая), но в любом случае я бы порекомендовал вам включить ее только для окончательной сборки, если это возможно, и тщательно протестировать эту сборку.
Навыки программирования тоже могут помочь; например добавить static
к функциям, к которым нет доступа вне определенного файла; используйте более короткие имена для символов (может помочь немного, вероятно, не слишком); использование const char x[]
где возможно; ... этот документ, хотя и рассказывает о динамических общих объектах, может содержать предложения, которые, если им следовать, могут помочь уменьшить конечный размер двоичного вывода (если ваша цель - ELF).
Мне кажется, что ответ, предоставленный Немо, является правильным. Если эти инструкции не работают, проблема может быть связана с версией 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
Хотя не строго о символах, если идет на размер - всегда компилировать с -Os
а также -s
флаги. -Os
оптимизирует полученный код для минимального размера исполняемого файла и -s
удаляет таблицу символов и информацию о перемещении из исполняемого файла.
Иногда - если требуется небольшой размер - игра с разными флагами оптимизации может иметь, а может и нет, иметь значение. Например переключение -ffast-math
и / или -fomit-frame-pointer
иногда может спасти вас даже десятки байтов.
strip --strip-unneeded
работает только с таблицей символов вашего исполняемого файла. На самом деле он не удаляет исполняемый код.
Стандартные библиотеки достигают желаемого результата, разбивая все свои функции на отдельные объектные файлы, которые объединяются с использованием ar
, Если вы затем свяжете результирующий архив как библиотеку (т.е. дайте опцию -l your_library
to ld) тогда ld будет включать только объектные файлы и, следовательно, символы, которые фактически используются.
Вы также можете найти некоторые ответы на этот похожий вопрос использования.
Из руководства GCC 4.2.1, раздел -fwhole-program
:
Предположим, что текущий модуль компиляции представляет целую компилируемую программу. Все публичные функции и переменные, за исключением
main
и те, которые объединены по атрибутуexternally_visible
становятся статическими функциями и в аффекте становятся более агрессивно оптимизированными межпроцедурными оптимизаторами Хотя эта опция эквивалентна правильному использованиюstatic
ключевое слово для программ, состоящих из одного файла, в сочетании с опцией--combine
Этот флаг можно использовать для компиляции большинства небольших программ на Си, поскольку функции и переменные становятся локальными для всего объединенного блока компиляции, а не для одного исходного файла.
Я не знаю, поможет ли это с вашим текущим затруднением, так как это недавняя функция, но вы можете указать видимость символов в глобальном порядке. Переходя -fvisibility=hidden -fvisibility-inlines-hidden
при компиляции может помочь компоновщику позже избавиться от ненужных символов. Если вы создаете исполняемый файл (в отличие от разделяемой библиотеки), вам больше нечего делать.
Более подробная информация (и детальный подход, например, к библиотекам) доступна в вики GCC.
минимальный анализ примера
Эти параметры были упомянуты по адресу: /questions/2012709/kak-udalit-neispolzuemyie-simvolyi-cc-s-pomoschyu-gcc-i-ld/2012714#2012714 , и я просто хотел подтвердить, что они работают, и немного проверить, как с ними работать.objdump
.
Выводы, которые мы делаем, аналогичны тем, что упоминались в других сообщениях:
- если используется какой-либо символ раздела, то включается весь раздел, даже если некоторые другие символы вообще не используются
- при встраивании символ не считается использованным
- приводит к удалению неиспользуемых символов, даже если в той же единице компиляции используются другие символы
Отдельные файлы, только
notmain.c
int i1 = 1;
int i2 = 2;
int f1(int i) {
return i + 1;
}
int f2(int i) {
return i + 2;
}
main.c
Компилировать только с-O3
:
gcc -c -O3 notmain.c
gcc -O3 notmain.o main.c
Разобрать:
Вывод содержит:
Disassembly of section .text:
0000000000000000 <f1>:
0: f3 0f 1e fa endbr64
4: 8d 47 01 lea 0x1(%rdi),%eax
7: c3 ret
8: 0f 1f 84 00 00 00 00 nopl 0x0(%rax,%rax,1)
f: 00
0000000000000010 <f2>:
10: f3 0f 1e fa endbr64
14: 8d 47 02 lea 0x2(%rdi),%eax
17: c3 ret
Disassembly of section .data:
0000000000000000 <i2>:
0: 02 00 add (%rax),%al
...
0000000000000004 <i1>:
4: 01 00 add %eax,(%rax)
...
Разобрать:
Вывод содержит:
Disassembly of section .text:
0000000000001040 <main>:
1040: f3 0f 1e fa endbr64
1044: 48 83 ec 08 sub $0x8,%rsp
1048: e8 03 01 00 00 call 1150 <f1>
104d: 03 05 c1 2f 00 00 add 0x2fc1(%rip),%eax # 4014 <i1>
1053: 48 83 c4 08 add $0x8,%rsp
1057: c3 ret
1058: 0f 1f 84 00 00 00 00 nopl 0x0(%rax,%rax,1)
105f: 00
0000000000001150 <f1>:
1150: f3 0f 1e fa endbr64
1154: 8d 47 01 lea 0x1(%rdi),%eax
1157: c3 ret
1158: 0f 1f 84 00 00 00 00 nopl 0x0(%rax,%rax,1)
115f: 00
0000000000001160 <f2>:
1160: f3 0f 1e fa endbr64
1164: 8d 47 02 lea 0x2(%rdi),%eax
1167: c3 ret
Disassembly of section .data:
0000000000004010 <i2>:
4010: 02 00 add (%rax),%al
...
0000000000004014 <i1>:
4014: 01 00 add %eax,(%rax)
Вывод: оба и присутствовали в конечном выходном файле, хотя и не использовались.
Даже если бы мы добавили:
gcc -O3 -Wl,--gc-sections notmain.o main.c
попробовать удалить неиспользуемые разделы, это ничего бы не изменило, так как в объектном файле фигурирует в том же разделе, что и (.data
), и появляется в том же разделе, что и (), которые использовались, и поэтому все их разделы переносятся в конечный файл.
Мы модифицируем команды компиляции, чтобы:
gcc -c -O3 -fdata-sections -ffunction-sections notmain.c
gcc -O3 -Wl,--gc-sections notmain.o main.c
Разобрать:
objdump -D notmain.o
Вывод содержит:
Disassembly of section .text.f1:
0000000000000000 <f1>:
0: f3 0f 1e fa endbr64
4: 8d 47 01 lea 0x1(%rdi),%eax
7: c3 ret
Disassembly of section .text.f2:
0000000000000000 <f2>:
0: f3 0f 1e fa endbr64
4: 8d 47 02 lea 0x2(%rdi),%eax
7: c3 ret
Disassembly of section .data.i2:
0000000000000000 <i2>:
0: 02 00 add (%rax),%al
...
Disassembly of section .data.i1:
0000000000000000 <i1>:
0: 01 00 add %eax,(%rax)
Итак, мы видим, как все получает свой собственный раздел, названный на основе самого имени символа.
Разобратьnotmain.o
:
objdump -D a.out
Вывод содержит:
Disassembly of section .text:
0000000000001040 <main>:
1040: f3 0f 1e fa endbr64
1044: 48 83 ec 08 sub $0x8,%rsp
1048: e8 03 01 00 00 call 1150 <f1>
104d: 03 05 b5 2f 00 00 add 0x2fb5(%rip),%eax # 4008 <i1>
1053: 48 83 c4 08 add $0x8,%rsp
1057: c3 ret
1058: 0f 1f 84 00 00 00 00 nopl 0x0(%rax,%rax,1)
105f: 00
0000000000001150 <f1>:
1150: f3 0f 1e fa endbr64
1154: 8d 47 01 lea 0x1(%rdi),%eax
1157: c3 ret
Disassembly of section .data:
0000000000004008 <i1>:
4008: 01 00 add %eax,(%rax)
и он не содержит ни . Это потому, что на этот раз каждый символ находился в своем разделе, и поэтому-Wl,--gc-sections
смог удалить каждый неиспользуемый символ.
Встраивание означает, что символ не будет считаться использованным.
Чтобы проверить эффект встраивания, давайте переместим наши тестовые символы в тот же файл, что иmain.c
:
main2.c
int i1 = 1;
int i2 = 2;
int f1(int i) {
return i + 1;
}
int f2(int i) {
return i + 2;
}
int main(int argc, char **argv) {
return f1(argc) + i1;
}
А потом:
gcc -c -O3 main2.c
gcc -O3 -Wl,--gc-sections -o main2.out main2.o
Разобратьmain2.o
:
objdump -D main2.o
Вывод содержит:
Disassembly of section .text:
0000000000000000 <f1>:
0: f3 0f 1e fa endbr64
4: 8d 47 01 lea 0x1(%rdi),%eax
7: c3 ret
8: 0f 1f 84 00 00 00 00 nopl 0x0(%rax,%rax,1)
f: 00
0000000000000010 <f2>:
10: f3 0f 1e fa endbr64
14: 8d 47 02 lea 0x2(%rdi),%eax
17: c3 ret
Disassembly of section .data:
0000000000000000 <i2>:
0: 02 00 add (%rax),%al
...
0000000000000004 <i1>:
4: 01 00 add %eax,(%rax)
...
Disassembly of section .text.startup:
0000000000000000 <main>:
0: f3 0f 1e fa endbr64
4: 8b 05 00 00 00 00 mov 0x0(%rip),%eax # a <main+0xa>
a: 8d 44 38 01 lea 0x1(%rax,%rdi,1),%eax
e: c3 ret
Интересно, какmain
находится в отдельном разделе.text.startup
, возможно, чтобы разрешить сборку остального текста.
Мы также видим, что это было полностью встроено вlea 0x1(%rax,%rdi,1),%eax
(непосредственно добавляет 1), хотя по непонятным мне причинам все еще используется вmov 0x0(%rip),%eax
в ожидании перемещения, см. также: Что делают компоновщики?Перемещение будет понятно после разборки ниже.
Разобрать:
objdump -D main2.out
Вывод содержит:
Disassembly of section .text:
0000000000001040 <main>:
1040: f3 0f 1e fa endbr64
1044: 8b 05 c2 2f 00 00 mov 0x2fc2(%rip),%eax # 400c <i1>
104a: 8d 44 38 01 lea 0x1(%rax,%rdi,1),%eax
104e: c3 ret
104f: 90 nop
Disassembly of section .data:
0000000000004008 <i2>:
4008: 02 00 add (%rax),%al
...
000000000000400c <i1>:
400c: 01 00 add %eax,(%rax)
и были полностью удалены, поскольку были встроены и, следовательно, больше не помечались как используемые, поэтому весь раздел был удален.
Если бы мы заставили не быть встроенными с помощью:
int __attribute__ ((noinline)) f1(int i) {
return i + 1;
}
тогда оба и появятся наmain2.out
.
Разделы разных объектных файлов разделены, даже если они имеют одно и то же имя.
Очевидно, например:
notmain2.c
int i3 = 3;
int i4 = 4;
int f3(int i) {
return i + 3;
}
int f4(int i) {
return i + 4;
}
а потом:
gcc -c -O3 notmain.c
gcc -c -O3 notmain2.c
gcc -O3 -Wl,--gc-sections notmain.o notmain2.o main.c
objdump -D a.out
не содержитf3
иf4
, хотя и были включены, и оба раздела не называются.text
.
Возможный недостаток:
-fdata-sections -ffunction-sections -Wl,--gc-sections
: более медленная скорость соединения
Мы должны найти какой-то ориентир, но это вполне вероятно, так как это потребует большего количества перемещений, когда один символ ссылается на другой символ из той же единицы компиляции, поскольку они больше не присутствуют в независимом разделе.
-flto
приводит к удалению символов, даже если используются другие символы в той же единице компиляции
Кроме того, это происходит независимо от того, приведет ли LTO к встроенному событию. Учитывать:
notmain.c
int i1 = 1;
int i2 = 2;
int __attribute__ ((noinline)) f1(int i) {
return i + 1;
}
int f2(int i) {
return i + 2;
}
main.c
extern int i1;
int f1(int i);
int main(int argc, char **argv) {
return f1(argc) + i1;
}
Компилируем и дизассемблируем:
gcc -c -O3 -flto notmain.c
gcc -O3 -flto notmain.o main.c
objdump -D a.out
Разборка содержит:
Disassembly of section .text:
0000000000001040 <main>:
1040: f3 0f 1e fa endbr64
1044: e8 f7 00 00 00 call 1140 <f1>
1049: 83 c0 01 add $0x1,%eax
104c: c3 ret
0000000000001140 <f1>:
1140: 8d 47 01 lea 0x1(%rdi),%eax
1143: c3 ret
и нет. Такf2
был удален, хотяf1
используется.
Отметим также, что иi2
пропали. Компилятор, кажется, признает, чтоi1
на самом деле никогда не модифицируется, а просто «встраивает» его как константу1
в:add $0x1,%eax
.
Связанный с этим вопрос: выполняет ли GCC LTO устранение мертвого кода между файлами?По какой-то причине удаление кода не происходит, если вы скомпилируете объектный файл с помощью-O0
: Почему GCC не выполняет устранение мертвого кода функции с помощью LTO при компиляции объектного файла с -O0?
Протестировано на Ubuntu 23.04 amd64, GCC 12.2.0.
Устаревшее поведение рекомендуется для создания статических библиотек со всем дополнительным кодом и сокращения единицы компиляции до минимума, необходимого для выполнения небольшой задачи (также рекомендуется в качестве шаблона в дизайне Unix).
Когда вы связываете код и указываете статическую библиотеку (файл.a
архив) компоновщик обрабатывает только все скомпилированные модули, на которые есть ссылки из исходного файла.crt0.o
кода, и это может быть достигнуто без какого-либо компилирующего кода, разделенного на разделы.
Мы сделали это в нашем коде, получив, возможно, не оптимальную выгоду, но позволив нам продолжить разработку с хорошим использованием памяти и сохранением большого количества неиспользуемого кода, но никогда не сталкиваясь с такими проблемами, как необходимость исследовать это компилятором. Я всегда использую эту лемму: если функция не нужна, не привязывайтесь к ней.
Вы можете использовать бинарный файл в объектном файле (например, исполняемый файл), чтобы удалить из него все символы.
Примечание: он изменяет сам файл и не создает копию.