(lib)ELF for dummies?

Fórumok

Sziasztok!

Egy/tobb ISA szimulatorhoz keresek egy egyszeru megkozelitest amivel *.elf fileokkal is tudnam etetni a rendszert. Neztem a `libelf-dev` csomagot, de ott az RTFM finoman szolva is foghijjas (lasd: `man elf_begin`). Talaltam meg ezt a leirast, most epp ezt nezem, hatha. Ami kell nekem az kb a kovetkezo: az objcopy -j ... -j ... -O binary xyz.elf xyz.bin-nek megfelelo funkcionalitas, illetve az `nm`-nek megfelelo symbol lookup. Persze ilyesmik is jol jonnek hogy az adott szegmens (.text, .rodata) mettol meddig tart es mettol meddig van benne valami. A legjobb lenne egy sima C library, mint a libelf, felteve hogy ez... erre valo. Amiben csak amiatt ketelkedek mert az `ldd` szerint ezek (objcopy, nm) nem linkelik magukhoz a libelf.so-t. 

Barkinek barmi otlet, ilyen getting started jelleggel? A fenti leirast elkezdem nezni, de ott nekem elsore aranytalanul sok a "creating" meg a "modifying" resz, ami itt biztos nem kell, kifejezett dump-olast meg vagy nem lattam ebben vagy vak vagyok. 

thx, A.

Hozzászólások

Szerkesztve: 2025. 08. 02., szo – 16:03

Minek neked a libelf? Miért nem írod meg magad? Kilistázni egy ELF tartalmát pofonegyszerű, csak pár struct lista, tényleg csak pár sor. A structokat megtalálod az /usr/include/elf.h fájlban.

Pár példa:
- POSIX-UEFI, ebben még a struct defek is megtalálhatók
- bootboot, ez meg a szekciókon meg a szimbólumokon iterál végig
- easyboot végignyálazza az ELF szegmenseket
- easyboot plgld, ez a legteljesebb, szegmensek, szekciók, szimbólumok, de még relokációs listára is van benne példa

Tényleg nem nagy kunszt. Ha csak annyi kell, hogy kidumpold, mint ahogy az nm teszi, az 10-20 sor, nem több (ha értelmezni meg ellenőrizni is akarod, na az már más kérdés).

    ehdr = (Elf64_Ehdr *)data;                    // ELF header
    shdr = (Elf64_Shdr *)(data + ehdr->e_shoff);  // section header
    // megkeressük a szimbólum táblát meg a string táblát
    for(i = 0; i < ehdr->e_shnum; i++) {
        s = ((Elf64_Shdr *)((uint8_t *)shdr + i * ehdr->e_shentsize));  // köv. section
        if(s->sh_type == SHT_STRTAB) { if(!strs) strs = s; } else
        if(s->sh_type == SHT_SYMTAB) { if(!syms) syms = s; }
    }
    // kilistázzuk a szimbólumokat
    for(i = 0; i < syms->sh_size; i += syms->sh_entsize) {
        sym = (Elf64_Sym *)(data + syms->sh_offset + i);
        printf("%08x %s\n", sym->st_value, (char*)data + strs->sh_offset + sym->st_name);
    }

Ennyi. Amire figyelni kell, hogy ne léptesd a struct pointert, hanem mindig a fejlécben szereplő "entsize" mezőket használd, hogy jövőbiztos meg multiplatform legyen.

Koszi! Ha csak ennyi az boven jo lehet ;)

Igy kiprobaltam gyorsan, annyi (talan nem is annyira apro?) kulonbseggel hogy 32 bites ELF-eken kell most dolgozzak (erosen embedded cuccok, 32 bit boven eleg). Szoval a tipusokat atirtam Elf32_*-ra, igy is hiba nelkul lefordul, es tenyleg ad egy nm-szeru kimenetet. Vagyis, vannak jo entry-k is, csak azt sejtem igy elsore hogy az a 

(char*)data + strs->sh_offset + sym->st_name

resz az nem feltetlen egy null-terminalt sztring ebben az esetben es/vagy nem jo helyre mutat. Ha ezt kiszedem ugy kb jot ad vissza, de a stringekre meg ki kell talalni valamit. Lehet hogy 32 bites esetben mashogy kell? Ez a fenti pelda teljesen logikusnak tunik, szoval fura lenne hogy 32 bites esetben mashogy kene csinalni mint 64-nel. 

Szerk: a nyers *.elf-ben valoban null-terminalt sztringek vannak. Ugyhogy itt inkabb az lehet hogy a fenti valami nem pont a nevre mutat. Lehet hogy az offset itt 32 biten megis mashogy van? 

Szerk-szerk: Oh, megvan: ket SHT_STRTAB is van ebben a *.elf-ben... es hat nem mindegy hogy melyiket hasznaljuk. Az elsonel zagyvasag jon ki, a masodikra tokeletes es pont azt adja ki ami nekem kell. 

kulonbseggel hogy 32 bites ELF-eken kell most dolgozzak [...] Szoval a tipusokat atirtam Elf32_*-ra

Igen, tényleg csak ennyi.

Oh, megvan: ket SHT_STRTAB is van ebben a *.elf-ben... es hat nem mindegy hogy melyiket hasznaljuk.

Ja, igen, lehet több sztringtábla is, tipikusan ilyenkor van egy, amiben a szekciónevek vannak (ehdr->e_shstrndx indexű szekcióban), meg egy másik csak a szimbólumok neveivel. De ez csak konvenció kérdése, lehet egyben is a kettő, az ELF megengedi.

Ha tutibiztosra akarsz menni, akkor a szekció típuskódja helyett a szekciónevet kell nézni:

    // szekciónevek sztringtáblája
    strt = (Elf32_Shdr *)((uint8_t *)shdr + ehdr->e_shstrndx * ehdr->e_shentsize);
    shstr = (char*)data + strt->sh_offset;
    // nem típuskódot, hanem szekcióneveket nézünk
    for(i = 0; i < ehdr->e_shnum; i++) {
        s = ((Elf32_Shdr *)((uint8_t *)shdr + i * ehdr->e_shentsize));  // köv. section
        if(!memcmp(shstr + shdr->sh_name, ".strtab", 8)) strs = s; else
        if(!memcmp(shstr + shdr->sh_name, ".symtab", 8)) syms = s;
    }
    // a többi már ugyanaz

A szimbólumneveket tartalmazó sztringtábla neve mindig ".strtab" és ilyenből mindig csak egy van. Ha a nevet nézed, akkor működik úgy is, ha csak egy sztringtábla van, akkor is, ha kettő, meg akkor is, ha a másodikban vannak a szekciónevek (ezt sem köti meg az ELF, elvileg bármi lehet a sorrend).

Szuper! :) Mindjart (vagyis kicsit kesobb) megnezem jobban. 

Szerk: most neztem meg. Picit finomhangolni kellett a forrason, de lehet hogy csak az en hulyesegem miatt. Lenyeg a lenyeg hogy ez igy most jonak tunik:

if ( s->sh_type == SHT_STRTAB && memcmp(shstr + s->sh_name, ".strtab", 8) ==0 )
 {      strs = s;
 }
else if ( s->sh_type == SHT_SYMTAB && memcmp(shstr + s->sh_name, ".symtab", 8) == 0 )
 {      syms = s;
 }

Es akkor a tobbi valtozatlan, ahogy mondod. Szepen megy a kod az osszes kritikus/erdekes architektura (msp430, avr, riscv32/rv32eac, armv6-m) toolchain-jei altal generalt *.elf-ekre is meg regebbi x86-os binarisokra is amit talaltam elfekvovben, valtoztatas nelkul. Kiprobalom majd a 32/64 bites megkulonboztetest... van par riscv64-es implementaciom is, azokra sem baj ha megy majd :)

Kiprobalom majd a 32/64 bites megkulonboztetest

Az meg

    if(ehdr->e_ident[EI_CLASS] == ELFCLASS64) {
        // 64 bites, Elf64_* struktok
    } else {
        // 32 bites, Elf32_* struktok
    }

Ja, és csak a teljesség kedvéért, ha tényleg mindenféle architektúrára akarod használni, akkor elfordulhat, hogy big-endian számokba is belefutsz. Ezt

    if(ehdr->e_ident[EI_DATA] == ELFDATA2LSB) {
        // little endian (x86, arm, risc, stb.)
        // le16toh(), le32toh(), le64toh()
    } else {
        // big endian (ősrégi procik, pl. M68K)
        // be16toh(), be32toh(), be64toh()
    }

kóddal lehet egységesen kezelni. Elvileg legalábbis, ugyanis 20 éve láttam utoljára nem little endian masinát, ELF big endian fájlt meg még sosem kellett feldolgoznom.

Biztos tudod, de azért leírom, ha valaki weben keresve találna ide, hogy ilyenkor a megfelelő méretű endian.h függvényt kell hívni az ELF struktok mezőire. Ha azonos a programod és a bemenet ELF endianness-e, akkor erre nincs semmi szükség.

De szerintem felesleges ennyire univerzálisra megírnod, egy

    if(ehdr->e_ident[EI_DATA] != ELFDATA2LSB) {
        printf("bocsi, nem támogatott bájtsorrend\n");
        exit(1);
    }

ellenőrzés bőven elég lesz neked, mert minden általad felsorolt architektúra little-endian.

Illetve meg debuggolas kozben annyi kijott hogy az elso SHT_STRTAB neve az `.shstrtab`

Igen, általában, de ne vegyél arra mérget, hogy mindig az első lesz. Az ehdr->e_shstrndx index a biztos.

Köszi, tök jó!

Igen, hálistennek nálam minden little endian, de egy ilyen teszt jó ötlet. Vagyis az ISA szimulátor ellenőrizheti akár magát az arch-ot is, az még jobb... De majd a post-synthesis szimulációknál is érdekes ez az egész. Azaz ott is hasznos lesz az ELF betöltés, de az endianness ott már (újra) meaningless lesz...

Aztán márcsak a progbits section-ok értelmezése van hátra. De azon is agyalok hogy a memory block-ok automatikusan kialakulnak a progbits/nobits-ekből és így ISA szinten is már tud fault-ot dobni a rendszer bármilyen szoftverhiba esetén... 

Aztán márcsak a progbits section-ok értelmezése van hátra.

Ööö, ez alatt mit értesz pontosan? Ez eléggé bonyi is tud lenni ám... Nem váletlenül írtam, hogy "ha értelmezni meg ellenőrizni is akarod, na az már más kérdés".

De azon is agyalok hogy a memory block-ok automatikusan kialakulnak a progbits/nobits-ekből

Nem, ez nem egészen így működik. Megpróbálom lehető legegyszerűbben elmagyarázni.

Először is, nincs olyan, hogy memory block, a MEMORY csak linker script absztrakció, ELF-ben nincs ilyesmi. Ott segmentek vannak (ELF szóhasználatban program headers, PHDRS, amik PT_LOAD típusúak), és ezek sem automatikusak, hanem a linker script sorolja be a sectionöket valamelyikbe (ezt az összerendelést a readelf egyébként ki is írja). Ilyen segmentek mindig kell, hogy legyenek előre definiálva, ha más nem, akkor a fordító "internal linker script"-jéből jönnek.

Alapvetően kétféle ELF "nézet" van:
- section: Shdr rekordok, ezt használja a fordító és ez a linker bemenete. Semmi köze semmihez, teljesen ad-hoc logikai elrendezés, akár minden függvény külön sectionbe rakható, ha akarod.
- segment: Phdr rekordok, futtatáskor meg csak ezeket használja. Jellemzően lapozási hozzáférési jogosultságonként szokott lenni egy-egy (futtatható szegmens, csak olvasható szegmens, írható-olvasható szegmens). Persze az ELF nagyon rugalmas, bármi elképzelhető. A memory block (linker script MEMORY kulcsszava) ezeknek a címeivel zsonglőrködik.

Namost simán lehet, hogy futtatható fájlban egyáltalán nincs section infó (pl. ha strip parancsot hívtak rá). Ilyenkor Elf64_Shdr helyett az Elf64_Phdr structokat kell nézni:

    for(i = 0; i < ehdr->e_phnum; i++) {
        phdr = (Elf32_Phdr *)(data + ehdr->e_phoff + i * elf->e_phentsize);
        printf("ELF segment %p %d bytes (bss %d bytes)\n", phdr->p_vaddr, phdr->p_filesz,  phdr->p_memsz - phdr->p_filesz);
    }

Ezek jóval egyszerűbbek és kevesebb infót tárolnak egy-egy blokkról (nincs pl láthatóság, szimbólumnév se bennük), van viszont fájloffszet, virtuális cím és kétféle méret (fájlban és memóriában mennyi), valamint rengeteg fajta technikai típus is van ezekből.

Például ha shared object (.so), akkor kell lennie a Phdr között egy PT_DYNAMIC típusúnak, ebbe kerülnek azok a szimbólumok, melyek kívülről hívhatóak (de csak azok!) és amik általában szerpelnek a Global Offset Table-ben is. Futtatható esetén meg egy INTERPRETER szokott lenni, egy kis kód, ami a futás idejű shared object linkelést végzi általában. Ezeken kívül még számos egyéb Phdr bejegyzésfajta is található, amikre igazából nincs szükség a futáshoz, vagy automatikusan generálódnak a kódból, stb. és ezeknek egyértelmű section megfelelőjük sincs (tipikus példa a lazy dynlink PLT rekordjai, a hash tábla, vagy épp a statikus stack, build azonosító, relokációs táblák stb).

Mivel sokkal többféle program header típus van (a PT_LOAD csak egy közülük), ezért nem épp triviális a section leképezés, pl. progbits-e csak a sectionnek van, és az ilyen sectionöket tartalmazó segment mindig PT_LOAD flaget kap, ugyanakkor van egy csomó más PT_LOAD-al megjelölt segment, amikhez nem tartozik progbits-es section egyáltalán, és egy section több program headersbe is bekerülhet (kód, PLT, relokáció, stb.).

Ezért kérdezem, hogy mire gondoltál, mert ha nm-nél részletesebb kimenetet akarsz, akkor amint látod, nagyon gyorsan eszkalálódni tud a helyzet.
Ha elég annyi, hogy a fájl melyik része hova kell kerüljön a memóriába, akkor az viszonylag egyszerű, csak a fentebbi Phdr listázás fog kelleni.

Ööö, ez alatt mit értesz pontosan? 

Amit ottan fentebb is irtam a nyitoban, az objcopy-nak megfelelo dolgot (a .text meg .rodata kinyereset). Plusz persze azt az infot hogy az hol is helyezkedik el :)

Nem, a "memory block" csak az ISA szimulatornak egy sajat belso objektuma, de igen, nagyban megegyezik a linker script MEMORY blokkjaival. Azert csak nagyban es nem teljesen mert maganak a hardvernek vannak MMIO tartomanyai meg egyeb, helllyel-kozzel memoriakent viselkedo tartomanyai, amirol igy a szimulatornak is tudnia kell. A feladat itt csak annyi hogy a szimulator inditasakor legalabb ezeknek egy reszet magatol kitalalja, es ne az "elf -> objcopy -> binaris -> cimek kezzel visszairva -> szimulator" hanem az "elf -> szimulator" vonal legyen a preferalt. De ez inkabb csak finomsag, a szimbolum-alapu debug az fontosabb. A memory block-ok azert lehetnek erdekesek hogy igy akkor a szimulalt hardver pontosan annyi memoriat kap mint amennyire szuksege van. Igy hiaba van akar a fizikai hardverben is tobb, egyes alacsonyszinten bekovetkezo szoftverhibakat mar igy is ki lehet szurni. 

Koszi a tobbit, nagyon hasznos! Igen, en is a .text.* format "hasznalom". Azaz -ffunction-sections, -fdata-sections kapcsoloval forditok mindent, linkelesnel pedig ugy ezek ossze vannak vonva:

.text  { *(.text .text.*)  }  > ROM

Es gondolom az *.elf-ben a legelso .text jelenik meg mint a PROGBITS neve. A "ROM"-ot meg mar nem latom viszont az *.elf-ben, ahogy mondod. 

Kicsit meg tanulnom kell ezeket, igen. Dehat jobb keson mint megkesobb.

az objcopy-nak megfelelo dolgot (a .text meg .rodata kinyereset). Plusz persze azt az infot hogy az hol is helyezkedik el :)

Hát, ha csak ennyi, akkor elég lesz neked a Phdr-ön végigmenni és kész, lásd az előbbi posztomban.

A fájl p_offset pozíciójáról másolódik p_filesz darab bájt a p_vaddr címre. Ha a p_memsz nagyobb, mint a p_filesz, akkor különbségüket ki kell nullázni. Szóval a hol helyezkedik el kérdésre a válasz az, hogy a p_vaddr címen, p_memsz darab bájt, és ilyen blokkból több is van, amik nem biztos, hogy folyamatosak. Illetve a kérdés még az, mely Phdr-öket veszed számításba. Aminek a p_type-ja PT_LOAD, az a klasszikus értelemben vett szegmens, az biztos kelleni fog, a többit meg nézd meg, kell-e.

Illetve ez csak a statikus címekre linkelt ELF esetén igaz (ami az általános embedded környezetben), ha shared objecteket is fel kell dolgoznod, akkor bonyolódik a helyzet, mert azokat dinamikusan bármilyen címre be lehet tölteni általában. Ekkor két variáció lehet, az első, ha csak simán relokálható, ilyenkor ".rela.dyn" Shdr van. A második, ha PIC (position independent code)-nak is lett fordítva, ilyenkor ".rela.plt", ".plt.got" stb. rekordok is vannak. Ezek a dlopen RTLD_LAZY flagje miatt kellenek, ilyenkor a végső cím nem fordításkor, de még csak nem is betöltéskor, hanem futás közben számítódik ki.

De szerintem neked csak a statikus címekre fordított futtathatókkal lesz dolgod, szóval nem kell a lazy dynamic linker cuccaival foglalkoznod. Egyébként sem biztos, hogy a program által betöltött shared object foglalását a program memóriafoglalásának számlájára akarod írni.

Es gondolom az *.elf-ben a legelso .text jelenik meg mint a PROGBITS neve.

Nem, mármint nem biztos. A p_flags bitjelzőket érdemes nézni, a PF_X jelzi, hogy futtatható a szegmens, a PF_W meg hogy írható-e.

PROGBITS-nek jelöltből számtalan van, most hirtelen kilistáztam egy programot, itt az első ilyen neve ".init", a másodiké ".plt", és csak a harmadik neve a ".text". Ezenkívül az ".rodata" szekció szintén PROGBITS-ként jelenik meg, ugyanakkor annak nincs a PF_X flagje beállítva, tehát nem is programkódot tartalmaz. A lényeg, hogy a text szegmens itt fel van darabolva több PT_LOAD rekordra, és értelmezés kérdése, hogy az ".rodata"-át hozzá számítod-e (mint korábban írtam, progbits valójában csak a section-ön van, a segment esetén egy heurisztikai jelöli annak, nincs konkrétan semmiféle egyértelmű "progbits" azonosító a Phdr-ben).

Sajnos az ELF túl rugalmas, ha értelmezni is akarod, akkor érhetnek meglepetések a rugalmassága miatt. Ha csak annyi kell, hova töltődik be és melyik blokk futtatható vagy írható, akkor felejtsd el a PROGBITS-et meg az Shdr-t, és inkább a p_flags mezőre érdemes hagyatkozni a Phdr-ben. Tisztább, szárazabb érzés.

Szerkesztve: 2025. 08. 05., k – 11:18

Én is játszottam mostanában ilyennel, 32 bites x86 ELF-et betöltöttem 64 bites processzbe, és megcsináltam, hogy bele lehessen hívni is. A slusszpoén az, hogy működik. LoL64-nek neveztem el a Wine WOW64 mintájára. Sajnos még nincs annyira kész, hogy publikálni tudjam. Az elf loadert egy kollegám csinálta, és olyasmik vannak benne, mint amit bzt is írt.