Jak usunąć nieużywane symbole C / C ++ za pomocą GCC i ld?

110

Muszę poważnie zoptymalizować rozmiar mojego pliku wykonywalnego ( ARMprogramowanie) i zauważyłem, że w moim obecnym schemacie kompilacji ( gcc+ ld) nieużywane symbole nie są usuwane.

Użycie arm-strip --strip-unneededdla wynikowych plików wykonywalnych / bibliotek nie zmienia rozmiaru wyjściowego pliku wykonywalnego (nie mam pojęcia dlaczego, może po prostu nie może) .

Jaki byłby sposób (jeśli istnieje) zmodyfikowania potoku budowania, tak aby nieużywane symbole zostały usunięte z pliku wynikowego?


Ja nawet nie myśleć o tym, ale mój obecny osadzony środowisko nie jest bardzo „mocny” i oszczędność nawet 500Kz 2Mwynikami w bardzo ładnym wzrost wydajności załadunku.

Aktualizacja:

Niestety obecna gccwersja, której używam, nie ma takiej -dead-stripopcji, a -ffunction-sections... + --gc-sectionsfor ldnie daje żadnej znaczącej różnicy w wynikowym wyniku.

Jestem zszokowany, że stało się to nawet problemem, ponieważ byłem pewien, że gcc + ldpowinno to automatycznie usunąć nieużywane symbole (dlaczego w ogóle muszą je zachować?).

Yippie-Ki-Yay
źródło
Skąd wiesz, że symbole nie są używane?
zvrba,
Nigdzie nie ma odniesienia => nie jest używany w ostatecznej aplikacji. Zakładam, że budowanie wykresu wywołań podczas łączenia / łączenia nie powinno być trudne.
Yippie-Ki-Yay,
1
Czy próbujesz zmniejszyć rozmiar pliku .o, usuwając martwe symbole , czy też próbujesz zmniejszyć rozmiar rzeczywistego śladu kodu po załadowaniu do pamięci wykonywalnej? Fakt, że mówisz „osadzone”, wskazuje na to drugie; pytanie, które zadajesz, wydaje się skupiać na tym pierwszym.
Ira Baxter
@Ira Próbuję zmniejszyć rozmiar wyjściowego pliku wykonywalnego, ponieważ (jako przykład) jeśli spróbuję przenieść niektóre istniejące aplikacje, które używają boostbibliotek, .exeplik wynikowy zawiera wiele nieużywanych plików obiektowych i ze względu na specyfikacje mojego obecnego osadzonego środowiska wykonawczego uruchomienie 10mbaplikacji trwa znacznie dłużej niż na przykład uruchomienie 500kaplikacji.
Yippie-Ki-Yay,
8
@Yippie: Chcesz pozbyć się kodu, aby zminimalizować czas ładowania; kod, którego chcesz się pozbyć, to nieużywane metody / etc. z bibliotek. Tak, aby to zrobić, musisz zbudować wykres wywołań. To nie jest takie proste; musi to być globalny wykres połączeń, musi być konserwatywny (nie można usunąć czegoś, co mogłoby się przydać) i musi być dokładny (więc masz tak blisko idealnego wykresu połączeń, że naprawdę wiesz, co nie jest używany). Dużym problemem jest zrobienie globalnego, dokładnego wykresu połączeń. Nie znam wielu kompilatorów, które to robią, nie mówiąc już o linkerach.
Ira Baxter,

Odpowiedzi:

131

W przypadku GCC odbywa się to w dwóch etapach:

Najpierw skompiluj dane, ale powiedz kompilatorowi, aby rozdzielił kod na oddzielne sekcje w jednostce tłumaczenia. Zostanie to zrobione dla funkcji, klas i zmiennych zewnętrznych przy użyciu następujących dwóch flag kompilatora:

-fdata-sections -ffunction-sections

Połącz jednostki tłumaczeniowe razem przy użyciu flagi optymalizacji linkera (powoduje to, że linker odrzuca sekcje bez odniesień):

-Wl,--gc-sections

Więc jeśli masz jeden plik o nazwie test.cpp, w którym zadeklarowano dwie funkcje, ale jedna z nich była nieużywana, możesz pominąć nieużywany za pomocą następującego polecenia do gcc (g ++):

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

(Zauważ, że -Os to dodatkowa flaga kompilatora, która mówi GCC, aby zoptymalizować pod kątem rozmiaru)

JT
źródło
3
Należy pamiętać, że spowolni to plik wykonywalny zgodnie z opisami opcji GCC (testowałem).
metamorfoza
1
Z mingwtym nie działa podczas statycznego łączenia bibliotek libstdc ++ i libgcc z flagą -static. Opcja konsolidatora -strip-allpomaga całkiem sporo, ale nadal wygenerowany plik wykonywalny (lub dll) jest około 4 razy większy niż to, co wygenerowałoby Visual Studio. Chodzi o to, że nie mam kontroli nad tym, jak libstdc++został skompilowany. Powinna istnieć ldjedyna opcja.
Fabio,
34

Jeśli ten wątek wierzyć, trzeba dostarczyć -ffunction-sectionsi-fdata-sections do gcc, co wywrze każdej funkcji i obiekt danych w osobnej sekcji. Następnie dajesz i --gc-sectionsGNU ld usuwa nieużywane sekcje.

Nemo
źródło
6
@MSalters: Nie jest to ustawienie domyślne, ponieważ narusza standardy C i C ++. Nagle globalna inicjalizacja nie następuje, co powoduje, że niektórzy bardzo zaskoczeni są programistami.
Ben Voigt,
1
@MSalters: Tylko jeśli przekażesz niestandardowe opcje łamiące zachowanie, które zaproponowałeś jako zachowanie domyślne.
Ben Voigt,
1
@MSalters: Jeśli możesz stworzyć łatkę, która uruchamia statyczne inicjatory wtedy i tylko wtedy, gdy efekty uboczne są konieczne do prawidłowego działania programu, byłoby to niesamowite. Niestety, myślę, że robienie tego doskonale często wymaga rozwiązania problemu zatrzymania, więc prawdopodobnie będziesz musiał czasami pomylić się po stronie dołączania dodatkowych symboli. I tak w zasadzie mówi Ira w swoich komentarzach do pytania. (BTW: „nie jest to konieczne do poprawnego działania programu” jest inna definicja „niewykorzystany” niż jak ten termin jest stosowany w normach)
Ben Voigt
2
@BenVoigt w C, globalna inicjalizacja nie może mieć skutków ubocznych (inicjatory muszą być wyrażeniami stałymi)
MM
2
@Matt: Ale to nieprawda w C ++ ... i mają ten sam linker.
Ben Voigt,
25

Będziesz chciał sprawdzić swoje dokumenty pod kątem wersji gcc & ld:

Jednak dla mnie (OS X gcc 4.0.1) znajduję je dla ld

-dead_strip

Usuń funkcje i dane, które są nieosiągalne dla punktu wejścia lub wyeksportowanych symboli.

-dead_strip_dylibs

Usuń dyliby niedostępne dla punktu wejścia lub wyeksportowanych symboli. Oznacza to, że pomija generowanie poleceń polecenia ładowania dla dylibs, które nie dostarczały żadnych symboli podczas połączenia. Ta opcja nie powinna być używana podczas łączenia z dylib, które jest wymagane w czasie wykonywania z jakiegoś pośredniego powodu, takiego jak dylib ma ważny inicjator.

I ta pomocna opcja

-why_live symbol_name

Rejestruje łańcuch odniesień do symbol_name. Dotyczy tylko z -dead_strip. Może pomóc w debugowaniu, dlaczego coś, co Twoim zdaniem powinno być usunięte, nie jest usuwane.

W man gcc / g ++ jest również uwaga, że ​​pewne rodzaje eliminacji martwego kodu są wykonywane tylko wtedy, gdy optymalizacja jest włączona podczas kompilacji.

Chociaż te opcje / warunki mogą nie odpowiadać Twojemu kompilatorowi, sugeruję, abyś poszukał czegoś podobnego w swoich dokumentach.

Michael Anderson
źródło
Wydaje się, że to nic nie daje mingw.
Fabio,
-dead_stripnie wchodzi w gccgrę.
ar2015,
20

Mogą też pomóc nawyki programistyczne; np. dodaj staticdo funkcji, do których nie można uzyskać dostępu poza określonym plikiem; używaj krótszych nazw symboli (może trochę pomóc, prawdopodobnie nie za bardzo); używać w const char x[]miarę możliwości; ... ten artykuł , chociaż mówi o dynamicznych obiektach współdzielonych, może zawierać sugestie, których przestrzeganie może pomóc zmniejszyć ostateczny rozmiar wyjścia binarnego (jeśli celem jest ELF).

ShinTakezou
źródło
4
Jak pomaga wybór krótszych nazw symboli?
fuz
1
jeśli symbole nie zostaną usunięte, ça va sans dire - ale wydaje się, że trzeba to powiedzieć teraz.
ShinTakezou
@fuz Artykuł mówi o dynamicznych obiektach współdzielonych (np. .sow systemie Linux), więc nazwy symboli muszą zostać zachowane, aby interfejsy API, takie jak ctypesmoduł FFI Pythona, mogły ich używać do wyszukiwania symboli według nazwy w czasie wykonywania.
ssokolow
18

Odpowiedź jest -flto. Musisz przekazać to zarówno do kompilacji, jak i do kroków łączenia, w przeciwnym razie nic nie zrobi.

W rzeczywistości działa bardzo dobrze - zmniejszyłem rozmiar programu mikrokontrolera, który napisałem, do mniej niż 50% jego poprzedniego rozmiaru!

Niestety wydawało się to trochę wadliwe - miałem przypadki, gdy rzeczy nie były poprawnie budowane. Mogło to być spowodowane systemem kompilacji, którego używam (QBS; jest bardzo nowy), ale w każdym razie zalecałbym włączenie go tylko dla ostatecznej kompilacji, jeśli to możliwe, i dokładne przetestowanie tej kompilacji.

Timmmm
źródło
1
"-Wl, - gc-sekcja" nie działa na MinGW-W64, "-flto" działa u mnie. Dzięki
rhbc73
Montaż wyjściowy jest bardzo dziwny -flto, ponieważ nie rozumiem, co robi za kulisami.
ar2015,
Uważam, -fltoże nie kompiluje każdego pliku do asemblacji, kompiluje je do LLVM IR, a następnie końcowe łącze kompiluje je tak, jakby były w jednej jednostce kompilacji. Oznacza to, że może wyeliminować nieużywane funkcje i wbudowane nie- staticjedynki, a prawdopodobnie także inne rzeczy. Zobacz llvm.org/docs/LinkTimeOptimization.html
Timmmm
13

Chociaż nie chodzi tylko o symbole, jeśli chodzi o rozmiar - zawsze kompiluj z flagami -Osi -s. -Osoptymalizuje wynikowy kod pod kątem minimalnego rozmiaru pliku wykonywalnego i -susuwa tablicę symboli i informacje o relokacji z pliku wykonywalnego.

Czasami - jeśli pożądany jest mały rozmiar - zabawa z różnymi flagami optymalizacji może - lub nie - mieć znaczenie. Na przykład przełączanie -ffast-mathi / lub -fomit-frame-pointerczasami może zaoszczędzić nawet dziesiątki bajtów.

zxcdw
źródło
Większość poprawek optymalizacyjnych nadal będzie dawać poprawny kod, o ile będziesz zgodny ze standardem językowym, ale -ffast-mathsiałem spustoszenie w całkowicie zgodnym ze standardami kodzie C ++, więc nigdy nie polecałbym tego.
Raptor007
11

Wydaje mi się, że odpowiedź udzielona przez Nemo jest prawidłowa. Jeśli te instrukcje nie działają, problem może być związany z wersją gcc / ld, której używasz, jako ćwiczenie skompilowałem przykładowy program, korzystając z instrukcji opisanych tutaj

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

Następnie skompilowałem kod przy użyciu coraz bardziej agresywnych przełączników usuwania martwego kodu:

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

Te parametry kompilacji i łączenia generowały pliki wykonywalne o rozmiarze, odpowiednio, 8457, 8164 i 6160 bajtów, co stanowiło największy wkład pochodzący z deklaracji „strip-all”. Jeśli nie możesz stworzyć podobnych redukcji na swojej platformie, być może Twoja wersja gcc nie obsługuje tej funkcjonalności. Używam gcc (4.5.2-8ubuntu4), ld (2.21.0.20110327) na Linux Mint 2.6.38-8-generic x86_64

Gearoid Murphy
źródło
8

strip --strip-unneededdziała tylko na tablicy symboli twojego pliku wykonywalnego. W rzeczywistości nie usuwa żadnego kodu wykonywalnego.

Biblioteki standardowe osiągają oczekiwany rezultat, dzieląc wszystkie swoje funkcje na osobne pliki obiektowe, które są łączone za pomocą ar. Jeśli następnie połączysz powstałe archiwum jako bibliotekę (tj. Dasz opcję -l your_libraryld), wtedy ld będzie zawierał tylko pliki obiektowe, a tym samym symbole, które są faktycznie używane.

Możesz również znaleźć niektóre odpowiedzi na to podobne pytanie dotyczące użytkowania.

Andrew Edgecombe
źródło
2
Oddzielne pliki obiektów w bibliotece są istotne tylko podczas tworzenia łącza statycznego. W przypadku bibliotek współdzielonych cała biblioteka jest ładowana, ale oczywiście nie jest zawarta w pliku wykonywalnym.
Jonathan Leffler,
4

Nie wiem, czy to pomoże w twojej obecnej sytuacji, ponieważ jest to nowa funkcja, ale możesz określić widoczność symboli w sposób globalny. Przekazywanie -fvisibility=hidden -fvisibility-inlines-hiddenpodczas kompilacji może pomóc konsolidatorowi w późniejszym pozbyciu się niepotrzebnych symboli. Jeśli tworzysz plik wykonywalny (w przeciwieństwie do biblioteki współdzielonej), nie ma nic więcej do zrobienia.

Więcej informacji (i szczegółowe podejście dla np. Bibliotek) jest dostępnych na wiki GCC .

Luc Danton
źródło
4

Z podręcznika GCC 4.2.1, sekcja -fwhole-program:

Załóżmy, że bieżąca jednostka kompilacji reprezentuje cały kompilowany program. Wszystkie funkcje i zmienne publiczne, z wyjątkiem maintych połączonych przez atrybut, externally_visiblestają się funkcjami statycznymi iw efekcie są bardziej agresywnie optymalizowane przez optymalizatory międzyprocedurowe. Chociaż ta opcja jest równoważna z prawidłowym użyciem staticsłowa kluczowego dla programów składających się z pojedynczego pliku, w połączeniu z opcją --combineta flaga może być używana do kompilowania większości programów C o mniejszej skali, ponieważ funkcje i zmienne stają się lokalne dla całej połączonej jednostki kompilacji, a nie dla sam plik źródłowy.

awiebe
źródło
Tak, ale to prawdopodobnie nie działa z jakąkolwiek kompilacją przyrostową i prawdopodobnie będzie trochę powolne.
Timmmm
@Timmmm: Podejrzewam, że o tym myślisz -flto.
Ben Voigt,
Tak! Później to odkryłem (dlaczego nie ma żadnej odpowiedzi?). Niestety wydawało się to trochę wadliwe, więc poleciłbym go tylko do ostatecznej kompilacji, a następnie często testował tę kompilację!
Timmmm,
-1

Możesz użyć strip binarnego na pliku obiektowym (np. Wykonywalnym), aby usunąć z niego wszystkie symbole.

Uwaga: zmienia sam plik i nie tworzy kopii.

ton4eg
źródło