Linkeskedünk - mert a méret a lényeg

 ( Chain-Q | 2017. január 6., péntek - 15:40 )

Kezdetben vala a C, ahol minden forrás önálló objektté fordult vala, majd ezekből library-k készültek vala, amelyekből linkeléskor csak a szükséges objektek kerültek a végleges binárisba. És az executable-ek kicsik voltak vala és a developer és az user egyaránt látá, hogy ez jó.

De a törpök élete nem csak játék és mese! Hallottál már a rosszról, a csúf, gonosz C++-ról? (És egyéb magas szintű nyelvekről, főleg OOP nyelvekről...) Az alapvető probléma, hogy míg C-ben a C forrás és a hozzá tartozó .o (objektum) fájl a kód szervezésének egysége, amelyek viszonylag jól, kevés függőséggel elkülöníthetők, magasabb színtű nyelvekben ez egyáltalán nincs így, és a linkeléskori objektumok tartalma különféle nyelvi elemek, osztályok körül szerveződik. Például C++-ban egy osztályt tipikusan egy egyetlen CPP fájl implementál, és így az osztály teljes kódja is egyetlen objektumban végzi, így pedig az egész osztály kódja is bekerül a végleges futtatható állományba, mivel a linker hagyományosan nem vizsgálja az objektumokon belüli függőségeket, csak az objektumok függőségeit. És így készül vala a bloatware. Tonnányi halott kód a végleges binárisban, sok-sok MB felzabált memória és diszk tárhely a semmiért.

Egyébként bár a problémát a magas szintű nyelvekre kenjük, ez a probléma már C-ben is létezik - elég megnézni pl. egy libc forrását, minden függvény külön C fájlban van implementálva, pont ezért, hogy a linker tényleg csak a szükséges függvényeket csapja a végleges binárishoz. Ez viszont elég nyilvánvaló kódkarbantartási rémálomhoz vezet, ha a kód egy bizonyos komplexitás és méret fölé hízik.

De kell, hogy legyen jobb megoldás! Mi lenne, ha a linker ki tudná válogatni a csak a szükséges függvényeket egy objektumból? Mekkora ötlet! Nem lehet olyan nehéz!

Nos, a probléma nem ennyire triviális. A linkelésre váró objektumot ugyanis általában nem a magas szintű fordító készíti elő, hanem az alatta lévő assembler. Egy assemblert pedig hagyományosan csak az érdekli, hogy az assembly forrás mely része kód, mely része statikus adat, és mely része futtatáskor inicializálandó adat. Ennek megfelelően az assemblerek három különböző szekciót ismertek: CODE (vagy más terminológiában TEXT), DATA és BSS. És a lefordítandó kódot szépen össze is szortírozták ebbe a három szekcióba, amelyet így a linker már egyáltalán nem tudott szétválasztani...

Ennek a problémának a kezelésére egyes fordítók (pl. Free Pascal) azt találták ki, hogy maguk válogatják szét külön szekciókba az önálló kódrészleteket, amelyeket aztán önálló assembly fájlokba mentenek le, és önállóan meghívják rájuk az assemblert, végül az AR tool segítségével ezeket egy statikusan linkelhető .a library-ba szervezik. Remek! Vagyis nem annyira. Ugyanis több nagyságrenddel meghosszabítja a fordítási időt. Egy átlagosan bonyolult osztályt tartalmazó forrásra ugyanis több százszor, esetenként több ezerszer, vagy akár tízezerszer kell meghívni az assemblert, ami sok minden, csak nem gyors. (A néhány perces build idők komolyabb alkalmazásoknál órákra nyúltak...) Ideiglenes megoldásnak jó volt, de ezen a ponton túl már muszáj belenyúlni az assemblerbe és a linkerbe is.

A végleges megoldás a named sections lett. Néhány éve a GNU toolchain is implementálta őket, vagyis a fordító elnevezheti a szekciókat önállóan, valamint megadhatja a típusukat - amelyet így az assembler nem fog feltétlenül összevonni egyetlen szekcióba, és a linker a következő lépésben kiválogathatja közülük amelyikre a végleges futtatható állománynak szüksége van. Persze ez sem olyan egyszerű, mert a linkelési lépés meg operációs rendszer függő, főleg ha dinamikus linkelés és egyebek is szerepet játszanak, ráadásul a "section garbage collection" olyan dolgokat is kidobálhat, amiket a binárisban akarsz tudni, szóval "it's complicated". Mindenesetre a GCC egy rakás makróval és paraméterrel támogatja a dolgot. A szimpla technikai stunton kívül amúgy, hogy kisebb binárisokat tudunk csinálni ez sokszor kritikus is - gondoljunk pl. egy embedded rendszer ROM-jára. Ha kifutunk az általában erősen korlátos és fix méretű ROM területből a sok dead code miatt, az elég kínos. (Érdeklődőknek kiindulásnak néhány kulcsszó: -ffunction-sections, -fdata-sections, -Wl,--gc-sections...)

És akkor most jön a probléma - részben pont a named sections okán, a GNU binutils kihajigált egy rakás régi objekt formátumot és ezzel együtt egy rakás legacy platformot is. A régi GNU binutils persze működik tovább, de nem támogatja a named sections - vagy nem támogatja az adott platformon, mivel valamilyen rejtélyes oknál fogva a named sectionshoz tartozó szintaxis kimeneti formátum függő...

Így Amigán és Atarin Free Pascallal (szinte látom magam előtt felsóhajtani az olvasókat, hogy "Nabazz, megint kezdi...") eddig nem volt használható a dolog, így viszont egy Hello World! is a többszáz (300-700) KB méretet karcolgatta, néhány egyéb unit hozzáadásával pedig a pár MB-be kúszott bele, ami pont a dolog lényegét ásta alá - kényelmes, magas szintű nyelven egyszerű fejlesztés legacy platformokra, miközben végleges bináris mérete és futtatási sebessége lehetőleg vállalható marad.

Szóval kb. november óta reszelgettük Frank Wille-lel, a főleg embedded és legacy rendszereket támogató VBCC fordítócsomag egyik írójával a named sectionst a vasm és vlink párosban. Valamint kijavítottunk egy rakás GNU assembler kompatibilitási bugot, és egyéb issue-t. A nightly néhány napja támogatni kezdte mindkettőt, szóval bekapcsolhattam és készre reszelhettem a már létező támogatást Free Pascalban, Amigára, Atarira és MorphOS-re. Mellékhatásként - hogy megismertem az egész procedúra működését - pedig AROS-hoz is megcsináltam a dolgot, a GNU binutils használatával.

Végeredményként persze az user csak annyit lát, hogy "Drágám, a binárisok összementek!", mint ezen a guminő az Amiga emulátor screenshoton is megfigyelhető:


Összement. Biztos a hidegtől. Még jó hogy nem fagyott le!

A nagyobb binárisok közül pedig a fordító maga Amigán 3.5MB-ről 2.1MB-re, a szöveges módú FP IDE pedig 4.5MB-ről 3.1MB-re esett. Innentől most már értelme is van az esetleges további méretoptimalizálásnak, mivel nem kell az exeben benne maradt több megabyte-nyi bloatot kerülgetni.

Köszönöm hogy elmondhattam, leülök magamtól is.

Hozzászólás megjelenítési lehetőségek

A választott hozzászólás megjelenítési mód a „Beállítás” gombbal rögzíthető.

"és így az osztály teljes kódja is egyetlen objektumban végzi, így pedig az egész osztály kódja is bekerül a végleges futtatható állományba, mivel a linker hagyományosan nem vizsgálja az objektumokon belüli függőségeket, csak az objektumok függőségeit."
Miért, a linker vizsgálja a C object fileok egymás közötti függését? Ha az egyik object fileban van 500 belépési pont exportálva meg 800 szimbólum, de abból nem használsz, csak ötöt, akkor mi lesz linkelve?

A C++ object fileok is ugyanolyan ELF objectek, mint a C ELF object fileok, csak a name mangling éppen fordítófüggő.

Idézet:
Miért, a linker vizsgálja a C object fileok egymás közötti függését? Ha az egyik object fileban van 500 belépési pont exportálva meg 800 szimbólum, de abból nem használsz, csak ötöt, akkor mi lesz linkelve?

1., Vizsgálja. Egy statikus .a fájlból, ami több objektumot tartalmaz, csak azokat húzza be a végleges binárisba, amelyek szükségesek, nem a teljes .a fájlt. Ilyen értelemben végez valamilyen függőségkezelést. De ezt kb. meg is említem a postban.

2., Named sections (és bekapcsolt section gc) esetén csak azokat a szekciókat amik a végleges binárisba kerülnek, de mivel named section nélkül az assembler csak egy kód, adat és bss szekciót gyárt objektumonként ezért az egész objektumot. Erről szól a post. :D

Szerk: Oké, úgy tűnik ez is platformfüggő, objektum formátum függő, vagy nemtom, vagy GCC generáció függő, mert most megpróbáltam ARM GCC 5.2-vel, ami külön szekcióba rakja a függvényeket alapból is... Well, whatever. Lehet csak nekem van túl sok túl régi GCC felrakva... :) Viszont a --gc-sections linker paraméter továbbra is kell úgy tűnik, másként minden ami az objektumban van, a binárisban végzi. De lehet hogy ezt is hozzáfűzi a GCC manapság alapból.

Szerk #2: GCC 4.6.2-vel x86_64-en (Ubuntu 12.04LTS) még továbbra is kell az -ffunction-sections:

charlie@scenergy:~/lnk$ gcc -c -ffunction-sections --save-temps -o test.o test.c
charlie@scenergy:~/lnk$ cp test.s test.f.s
charlie@scenergy:~/lnk$ gcc -c --save-temps -o test.o test.c
charlie@scenergy:~/lnk$ diff test.s test.f.s
8c8
<       .text
---
>       .section        .text.x,"ax",@progbits
24a25
>       .section        .text.y,"ax",@progbits

Szóval ha a named sections lett a default, akkor az GCC-ben is viszonylag friss változás.

Idézet:
A C++ object fileok is ugyanolyan ELF objectek, mint a C ELF object fileok, csak a name mangling éppen fordítófüggő.

Tudom, de az assemblert és a linkert nem nagyon érdekli a szimbólumaid name manglingje, ő az objektumban lévő szekciókra kíváncsi.

-=- Mire a programozó: "Na és szerintetek ki csinálta a káoszt?" -=-

https://gcc.gnu.org/onlinedocs/gccint/LTO.html

--
NetBSD - Simplicity is prerequisite for reliability

Kösz. És hogy kontextusba tegyük, itt egy blogpost a közelmúlt történetéről ezzel kapcsolatban GCC szempontból, szóval tényleg az egyik aktívan fejlesztett területről van szó.

-=- Mire a programozó: "Na és szerintetek ki csinálta a káoszt?" -=-

congrats!

Hajrá!

"Jegyezze fel a vádhoz - utasította Metcalf őrnagy a tizedest, aki tudott gyorsírni. - Tiszteletlenül beszélt a feljebbvalójával, amikor nem pofázott közbe."

(feliratkozás)


tr '[:lower:]' '[:upper:]' <<<locsemege
LOCSEMEGE

+1

grat és köszi!

Pocsék napom volt, de ez felüdített. Kösz!

cár

----
"Kb. egy hónapja elkezdtem írni egy Coelho-emulátort, ami kattintásra generál random Coelho-kompatibilis tartalmat."

itt jartam.

--
Vortex Rikers NC114-85EKLS

\o/ _o_