Как удалить неиспользуемые символы 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 оптимизировать размер)

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

Вы захотите проверить свои документы на наличие версии 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кода, и это может быть достигнуто без какого-либо компилирующего кода, разделенного на разделы.

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

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

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

Другие вопросы по тегам