[MEGOLDVA] Újabb Clang bug

Fórumok

Megpróbálom érthetően leírni, hogy mi is a gond, nem triviális.

Szóval, a régebbi Clang fordítók, és a distro-mban évő 17.0.6 is freestanding módban helyesen .text, .data és .bss szekciókat generál.

Azonban a legfrissebb (forrásból telepített) Clang nem, az ilyent eredményez:

readelf -S exfat.o 
There are 9 section headers, starting at offset 0x1968:

Section Headers:
  [Nr] Name              Type             Address           Offset
       Size              EntSize          Flags  Link  Info  Align
  [ 0]                   NULL             0000000000000000  00000000
       0000000000000000  0000000000000000           0     0     0
  [ 1] .strtab           STRTAB           0000000000000000  00001830
       0000000000000132  0000000000000000           0     0     1
  [ 2] .text             PROGBITS         0000000000000000  00000040
       0000000000000d10  0000000000000000  AX       0     0     16
  [ 3] .rela.text        RELA             0000000000000000  00001110
       0000000000000708  0000000000000018           8     2     8
  [ 4] .note             NOTE             0000000000000000  00000d50
       0000000000000084  0000000000000000   A       0     0     4
  [ 5] .comment          PROGBITS         0000000000000000  00000dd4
       0000000000000020  0000000000000001  MS       0     0     1
  [ 6] .note.GNU-stack   PROGBITS         0000000000000000  00000df4
       0000000000000000  0000000000000000           0     0     1
  [ 7] .llvm_addrsig     LOOS+0xfff4c03   0000000000000000  00001818
       0000000000000018  0000000000000000   E       8     0     1
  [ 8] .symtab           SYMTAB           0000000000000000  00000df8
       0000000000000318  0000000000000018           1     7     8
Key to Flags:
  W (write), A (alloc), X (execute), M (merge), S (strings), I (info),
  L (link order), O (extra OS processing required), G (group), T (TLS),
  C (compressed), x (unknown), o (OS specific), E (exclude),
  D (mbind), l (large), p (processor specific)

Amint látható, BSS szekciónak hűlt helye, van viszont helyette LOOS szekció (ami OS-specifikust jelent), és aminek freestanding módban nem is szabadna léteznie, tehát ez egyértelműen Clang fordító bug.

A kérdésem az, hogy próbáltam a doksit bújni, de nem találtam, viszont hátha valaki itt a HUP-on ismer olyan Clang parancssori kapcsolót, esetleg fordítási opciót, amivel a korábbi működés helyreállítható (azaz hogy freestanding módban NE legyenek OS-specifikus szekciók, csak a hagyományos .text, .data és .bss).

Bármi tipp?

MEGOLDÁS: ezer hála lacos-nak, aki megtalálta az egyik kapcsolót! A hiányzó bss-re is találtam megoldást, az -fno-addrsig -fno-common kapcsolók együttesen eltűntetik a LOOS szekciót és visszahozzák a BSS-t, azaz egy-az-egyben visszaállítják a korábbi működést. Azt nem értem, hogy freestanding módban miért nem ezek az alapértelmezettek, de sebaj, megadható, működik!

Hozzászólások

Szerkesztve: 2024. 05. 28., k – 22:58

Elvileg erre a -ffreestanging kapcsoló való. Ha úgy se jó, próbáld meg -ansi kapcsolóval, csak próbából, nehogy valami modern standard miatt próbálja kikapcsolni, mert megváltozott a default.

Lehet tévedek, de elvileg az sem baj, ha nincs ott a BSS szekció, attól még futnia kéne OS-től függetlenül.

The world runs on Excel spreadsheets. (Dylan Beattie)

Elvileg erre a -ffreestanging kapcsoló való. Ha úgy se jó, próbáld meg -ansi kapcsolóval

Mindkettő már meg van adva, de azért kössz!

Lehet tévedek, de elvileg az sem baj, ha nincs ott a BSS szekció

De nekem kéne... Ráadásul amit korábban a BSS-be rakott, az került most ebbe az LOOS-be. Legalábbis az egyik eltűnt, a másik megjelent és egyforma a méretük.

Hasznos információt nem a Type=LOOS alapján találtam, hanem a Section Name=.llvm_addrsig alapján.

Ez szemre egy lld linker feature, nem bug; úgy hívják, hogy "Identical Code Folding". Ennek a gcc BZ-nek a legelső kommentjében jól összefoglalják a feature-t:

https://gcc.gnu.org/bugzilla/show_bug.cgi?id=105625#c0

A .llvm_addrsig section ahhoz kell, hogy ezt az optimalizálási lépést "biztonságosan" végre lehessen hajtani, amit pedig az --icf=safe kapcsolóval lehet kérni:

https://llvm.org/docs/Extensions.html#sht-llvm-addrsig-section-address-…

https://maskray.me/blog/2024-03-09-a-new-relocation-format-for-elf#llvm…

Tipp: próbáld meg az lld-nek megadni az --icf=none kapcsolót:

https://man.archlinux.org/man/extra/lld/ld.lld.1.en#icf~3

Általános tipp: a readelf olvashatóbb kimenetet produkál a --wide kapcsolóval.

Ez szemre egy lld linker feature, nem bug; úgy hívják, hogy "Identical Code Folding"

Nem lehet linker feature, mert linkert nem is használok hozzá, csak fordítót. Ráadásul nem is kódot rakja bele (az a .text szekcióba került), hanem a BSS adatotokat (0x18 bájtnyit, pont ekkora volt a régi verzióval a BSS).

https://llvm.org/docs/Extensions.html#sht-llvm-addrsig-section-address-…

Amit itt írnak, az teljes hülyeség. Egyrészt milyen extension az, amit nem lehet kikapcsolni (mert ha nem lehet, akkor az nem is lehet extension, ugye), másrészt asszongya: "This section is used to mark symbols as address-significant, i.e. the address of the symbol is used in a comparison or leaks outside the translation unit.", ami alapján üresnek kéne lennie ennek a szekciónak (mivel csak helyi változókról van eleve szó, tehát nem lehet "leaks outside the translation unit", a mondat másik felének meg nincs is értelme, max. ha dinamikusan linkelnék, ami meg freestanding módban nem is lehetséges).

Azt is írja, hogy "Without this directive, all symbols are considered address-significant.", magyarán mi a francnak van ez a szekció, ha alapból úgyis az összes szimbólumot bele kell rakni??? Normális? És miért nem a szimbólumindexek vannak benne, és miért pont akkora, mint a hiányzó BSS? Ehh... esszerint két bugról beszélünk: a hiányzó BSS, és a freestanding ellenére megjelenő LOOS, de a kettőnek semmi köze egymáshoz, csak véletlenül egyforma a méretük?

Tipp: próbáld meg az lld-nek megadni

Ennél bonyoltultabb a dolog, mert nem használok se LLVM lld-t, se GNU ld-t. Speciális formátumú kimenetre van szükségem, ezért igazából én írtam a linkert hozzá, ami beolvassa az ELF objektumokat és előállítja ez a formátumot.

Ezért kéne tudnom, pontosan hol van és mekkora a BSS, és ezért baj, ha valami gányolt LOOS szekció van helyette. (Eredetileg a.out-ot akartam volna, de a gccből újabban kivették alapból, a Clang-ba meg soha nem is implementálták, ezért a szopóroller a saját formátummal.)

Kis rant: ahogy én látom, ennek a Rui Ueyama nevű faszjancsinak fingja sincs, hogy mi is a linker feladata, és valószínűleg még soha életében nem fordított bare metalra, de még csak statikusan sem linkelt soha, csakis kizárólag mindenféle fos bloated frameworköket linkelt dinamikusan, ezért azt hiszi, csak az létezik... Hogy nem küldték még el a P-be az öreg motorosok? Mármint oké, hogy belerak egy ilyen szart a fosai kedvéért, na de hogy úgy, hogy még ki sem lehet kapcsolni, és minden mást tör vele...?

Ez azért van, mert a C nyelv való rendszerprogramozásra és ezért ha egyszer leírtál egy programot, vagy összeraktál egy buildet akkor az nem törik el. Nem kell folyton hülyeségek miatt újrapeccselni minden régi kódot minden fordítóverzióval, mert ez C, nem csiligány újhullámos szar.

Az azért ugye megvan, hogy ennek itt SEMMI KÖZE magához a C nyelvhez, de még csak a C nyelvű források sem változtak egyetlen bitnyit sem, és például gcc fordítóval továbbra is tökéletesen működik a linkerem, de a Clang linkerével sem okoz gondot?
Mindössze egy fordítóprogram egy opciójának alapértelmezését baszták el az egyik fordítási módban, és amit valószínűleg hamarosan javítani fognak, ha többen is reklamálnak érte. De mégegyszer, a saját linkerével használva továbbra sem lenne gond, csak azért jött ez elő, mert saját linkert kellett írnom.

Ha azt hiszed, ennek bármi köze van a C nyelvhez, akkor add vissza az IT diplomád!

Ezek az esetek amikor valamit újra elő kell venni, guglizni napokat, fórumozgatni és a végén megjavítani mindig úgy szoktak lenni, hogy valójában nem is hiba, meg nem hibája annak amit szeretünk, csak éppen elment vele három nap amit ki kell fizetni és mégis annyival később lesz kész a termék is. De a C nyelvnek semmi köze hozzá, az tökéletes, csak a fordítóval van valami apróság, lényegtelen semmiség.

Lassan írom, hogy még az olyan együgyűek is biztos megértsék, mint Te.

1. Ha Ada-ból vagy Rust-ból fordítottam volna ezt az ELF-et, akkor is pontosan ugyanez lett volna, mert semmi köze a nyelvhez, csakis az ELF specifikációhoz van köze.

2. Mindössze az egyik fordító egyik alapbeállítása változott, ami csak azért volt itt gond, mert a szabvány fordítási környezet HELYETT saját linkert írtam, amibe nem volt még beleimplementálva ez az ELF opció.

Szóval a Clang fordító csinált neked egy problémát, de semmi köze a C-hez az egésznek. Emlékszel, hogy nem is olyan régen egy egész topikot elfloodoltál azzal a gondolattal, hogy ha írsz egy C programot, akkor az működni fog 10 év múlva is változatlanul? Mert "szabvány". Kivéve például a szegmensek nevei: az nem szabvány. Talán megpróbálhatnád átírni Rustra.

Valóban irónikus, és a C sem tökéletes, ezt aláírom. Nem annyira felhőtlen, mint azt mi C fanboyok előadjuk idealizáltan, megvannak a saját problémái, a nyelvnek, szabványainak, fordítóknak is. Másik oldalról meg semmi nem lesz soha hibátlan, egyik nyelv, fordító sem, a Rust sem lesz mindenhatóbb, annak is megvannak a hátrányai.

A C legalább még mindig a legegyszerűbbek, legkonzervatívabbak között van, még általában azzal a legkisebb a fordított bináris mérete, azzal a legjobb a futási sebessége (hacsak nem speciális műfajról van szó, pl. CUDA számításokra optimalizált Fortran fordítóval vetjük össze, és linpack-szerű felhasználás a cél).

Nyilván a Clang/LLVM mindig is erőteljesen újító volt, aki konzervativizmust akar (mind pl. a Linux kernelesek), az inkább marad gcc-n, meg régebbi C-szabványon (C89, 90, 99), meg linkeléskor se újítgat be túl modern módszerekkel, túl agresszív optimalizációkkal (LTO és társai).

Ez a 10 év múlva is változatlanul lefordul, az kb. sose volt igaz C-nél sem, míg nem volt szabványosítva, a C fordítók gyakran egyedi megoldásoztak, aztán meg mikor lett szabvány, azok állandóan változtak, egymással nem feltétlen kompatibilisen. Pl. én a múlt héten belefutottam egy csomó olyan régi CLI program projektjébe, amik már nem fordultak le, pedig nagyon egyszerű, alap programok, de már olyan blőd dolgok miatt nem fordulnak le, hogy már nem tudja rendesen beemelni függőségnek a libcurses-t, meg pl. olyan régi kód, hogy még nincs a függvényeknek visszatérési típusa definiálva, amire a modern fordító nem is warningot dob, hanem kifejezett error-ral bukik el az egész. Ráadásul az -ansi kapcsoló se segített, kézzel kellett a problémás kódrészeket áthegeszteni, pedig ráadásul ne is ilyen ultramodern fordítóval mentem neki, mint a Clang, hanem konzervatív gcc-vel.

Meg pl. most néztem pár napja Tannenbaumnak az egyik régebbi BSDCan-előadását, amiben a modern Minix helyzetét taglalta, meg hogy miért a NetBSD userland-et, pkg/ports, Clang ökoszisztémát vették át alapul a Minix 3+ esetében, miért nem a GNU-s, linuxos toolokat, aminél megjegyezte, hogy pedig próbálkoztak az utóbbival is, de egy csomó minden nem fordult le, mert kiderült, hogy a GNU/linuxos kódok igazából nem C-ben, hanem gcc-ben vannak írva (meg én részemről hibának látom, hogy sokszor x86-ra vannak alapozva, és nem kellően portolhatóan megírva, igaz ez nem mindegyikre igaz, de vannak kivételek sajnos, főleg GPU drivernél, meg egyes nem GNU-s, de linuxos szoftvereknél). Hallatszott rajta, a maró gúny, amivel mondta, a közönség fel is röhögött rajta.

The world runs on Excel spreadsheets. (Dylan Beattie)

linkert nem is használok hozzá, csak fordítót

Az llvm/clang forrásfában bányászva eljutottam a következő kapcsolóig: -fno-addrsig.

Miután a forrásban megtaláltam, és tudtam, hogy pontosan mire kell keresni a neten, meglett a dokumentáció is:

https://clang.llvm.org/docs/UsersManual.html#cmdoption-faddrsig

Azt írja:

Controls whether Clang emits an address-significance table into the object file. Address-significance tables allow linkers to implement safe ICF without the false positives that can result from other implementation techniques such as relocation scanning. Address-significance tables are enabled by default on ELF targets when using the integrated assembler. This flag currently only has an effect on ELF targets.

Ha megadod a clang-nak az -fno-addrsig-et, változik valami?

(A kapcsoló egyébként elég régi; én most git-blame-mel a következő commit-ig nyomoztam le: 14b468bab620 ("Re-land r337333, "Teach Clang to emit address-significance tables.", which was reverted in r337336.", 2018-07-18). Azért nem könnyű megtalálni, mert mostanra a clang opciók reprezentációja a forrásban megváltozott. Például a 7694b571d9fd ("[Driver] Add multiclass OptInFlag and OptOutFlag to simplify boolean option definition", 2020-06-02) után az faddrsig sztringre keresve nincs az Options.td file-ban találat.)

Nekem ez a Rui Ueyama nem tűnik annyira hülye gyereknek. Legalábbis nem egy átlag soydev, de abban igazad lehet, hogy bare metalban nem utazott eddig. Védelmére legyen mondva, hogy abba nagyon kevesen is utaznak, lényegében már csak kernel, driver, firmware-fejlesztők, meg esetleg Xen-fejlesztők. A többiek, kb. az összes fejlesztők 99,999999999999%-a tényleg ilyen-olyan framworkökbe, meg dinamikus linkeléssel, bytecode-ra fordítással, stb. tolja.

Ez a bare metal fejlesztési tudás kihalóban lévő műfaj, fekete mágiának számít már.

The world runs on Excel spreadsheets. (Dylan Beattie)

Az -fno-common-nal kapcsolatban: őszintén szólva nem értem, hogy miért nem az -fno-common az alapértelmezett beállítás. Ugyanis az -fcommon definiál egy olyan viselkedést, ami a C szabvány szerint definitálatlan -- és ez minimum zavaró. Például:

// a.c
int x;
int main(void) { return x; }

// b.c
int x;

Bármelyik file-t önmagában tekintve az int x; deklaráció tentative definition (kísérleti definíció), amely a file végét elérve external definition-né (külső definícióvá) válik. Ugyanis idézzük pl. a C99 szabványt (6.9.2 External object definitions, 2. bekezdés):

A declaration of an identifier for an object that has file scope without an initializer, and without a storage-class specifier or with the storage-class specifier static, constitutes a tentative definition. If a translation unit contains one or more tentative definitions for an identifier, and the translation unit contains no external definition for that identifier, then the behavior is exactly as if the translation unit contains a file scope declaration of that identifier, with the composite type as of the end of the translation unit, with an initializer equal to 0.

Más szóval a szabvány a két fenti forrásfile viselkedését úgy szabja meg, hogy az egyenrangú legyen a következőkkel:

// a2.c
int x = 0;
int main(void) { return x; }

// b2.c
int x = 0;

Ezek viszont már (külön-külön) external definition-nek számítanak (6.9.2 External object definitions, 1. bekezdés)::

If the declaration of an identifier for an object has file scope and an initializer, the declaration is an external definition for the identifier.

Továbbá az x változónak mindkét file-ban (és mindkét változatban) external linkage-e van (6.2.2 Linkages of identifiers, 5. bekezdés 2. mondat):

[...] If the declaration of an identifier for an object has file scope and no storage-class specifier, its linkage is external.

A két eredeti forrásfile-t egyetlen programba összelinkelve pedig megszegnénk a szabványt (6.9 External definitions, 5. bekezdés 2. mondat; aláhúzásos kiemelés tőlem):

[...] If an identifier declared with external linkage is used in an expression (other than as part of the operand of a sizeof operator whose result is an integer constant), somewhere in the entire program there shall be exactly one external definition for the identifier [...]

És akkor nézzük meg pl. a gcc viselkedését:

gcc -std=c99 -Wall -Wextra -pedantic a.c b.c

A parancs nem jelez hibát. Itt jön a képbe az, hogy a shall hogyan értelmezendő:

  • 4. Conformance, 1. és 2. bekezdésekből:

    In this International Standard, ‘‘shall’’ is to be interpreted as a requirement on an implementation or on a program [...]

    If a ‘‘shall’’ or ‘‘shall not’’ requirement that appears outside of a constraint is violated, the behavior is undefined. [...]

  • 5.1.1.3 Diagnostics, 1. bekezdés 1. mondat:

    A conforming implementation shall produce at least one diagnostic message (identified in an implementation-defined manner) if a preprocessing translation unit or translation unit contains a violation of any syntax rule or constraint, even if the behavior is also explicitly specified as undefined or implementation-defined. [...]

A gcc-re csak azért nem mondhatjuk, hogy megszegi a szabványt, met a 6.9 External definitions, 5. bekezdés -- amely a "pontosan egy külső definíció" előírást tartalmazza -- nem egy Constraints szakaszban található, hanem egy Semantics szakaszban. Az előbbi esetben a gcc viselkedése megszegné a szabványt, így viszont csak az a helyzet, hogy a gcc definiál egy olyan viselkedést, amelyet a szabvány nem definiál. Ennek ellenére a gcc működése szerintem nagyon zavaró.

És akkor az -fno-common kapcsolóval ugyanez:

gcc -std=c99 -Wall -Wextra -pedantic -fno-common a.c b.c

Itt már van hibaüzenet:

/tmp/cc96ba6O.o:(.bss+0x0): multiple definition of `x'
/tmp/ccL5BbT8.o:(.bss+0x0): first defined here
collect2: error: ld returned 1 exit status

Csak hogy három projektet említsek, amivel dolgom volt: mind az edk2, mind a qemu, mind a libvirt projektek megadják az -fno-common-t. Szerintem állíthatjuk azt, hogy az -fcommon mint alapértelmezés hiba (vagy minimum tévedés) a fordítókban.

Köszönöm a kimerítő választ! Tökéletesen egyetértek, szerintem is az -fno-common-nak kéne az alapértelmezettnek lennie. Sőt, igazából én sem tudok elképzelni olyan gyakorlati esetet, ahol lenne értelme a common-nak (mégis, mi baj lenne már azzal, hogy az inicializálatlan változók a bss-be kerülnek és nem egy fiktív nemlétező szekcióba?), ráadásul ahogy írod, valóban a C-ből fordított objektfájlok esetében még UB is.

Nálam ráadásul le sem fordul az a.c,b.c példád, pontosan ugyanazt a hibát dobja, mint az a2.c,b2c példa. Egyébként megjegyzem, nem a gcc parancs jelez hibát, hanem a linker (ezt ugye azért fontos itt kiemelni, mert ennél a konkrét esetnél én írtam a linkert, szóval nincs se collect2, se ld, plgld van helyettük).

Én inkább nem is azt nem értem, hogy az alapértelmezett viselkedés miért változott, hanem hogy ez miért nincs rendesen lekommunikálva, dokumentálva, aztán meg a felhasználó észreveszi, hogy valami nem stimmel. Ez így elég gáz, hogy random verzióban sunyiban ilyenek húznak meg.

The world runs on Excel spreadsheets. (Dylan Beattie)

őszintén szólva nem értem, hogy miért nem az -fno-common az alapértelmezett beállítás

Hm... most ugy latom hogy ujabban itten (gcc-12 videken) mar ez az alapertelmezett. Legalabbis a manual szerint. De az ketsegtelen hogy beagyazott cuccoknal mindig megadom, foleg hogy ott vannak regebbi forditok (pl riscv64-unknown-elf-gcc-8.3.0) ahol tenyleg nem az az alapertelmezett... De ujabban (riscv64-unknown-elf-gcc-12.2.0) mar ott is... 

Erdekes nagyon, koszi a reszletes leirast :) Tudtam csak nem sejtettem hogy ez az -fno-common azert fontos :)