Bilo da se bavite programiranjem na niskom nivou (eng. lowlevel) ili pak razvojem aplikacija na višim nivoima, poznavanje strukture zapisa fajlova je u najmanju ruku poželjno, a neretko i neizbežno. Takođe, bilo da pišete u onom (recimo Rustu) ili ovom (recimo C++) programskom jeziku, struktura izvršnog fajla prati nekakve standarde zapisa. Esencijalno je poznavati iste, jer em nas to odvaja od “običnih” programera, em ćemo lakše prozreti u potencijalne probleme našeg programa. Ti uslovno rečeno problemi, mogu biti i bezbedonosne prirode, jer detaljno poznavanje fajl formata neretko baš neki zlonamerni programeri/neetički hakeri koriste kako bi ostvarili svoje ciljeve i došli do podataka drugih korisnika.
Nadam se da sam vas koliko toliko ubedio sa ovih par rečenica da je dobro poznavati standarde zapisa fajlova, te sada prelazimo na nešto tehničkiji deo. U daljem tekstu ćemo obraditi fajl formate za dva veoma popularna operativna sistema GNU Linux I Windows.
Fajl format ELF
ELF (eng. Executable and Linkable Format) predstavlja generički format zapisa izvršnih fajlova na Linux operativnim sistemima. Ovaj format možemo posmatrati kroz tri celine:
-
ELF zaglavlje (eng. ELF header)
-
sekcije (eng. Sections)
-
segmenti (eng. Segments)
Svaka od navedenih celina ima svoju ulogu ili tokom faze povezivanja/linkovanja ili tokom faze izvršavanja, te hajde da obradimo svaku ponaosob.
ELF zaglavlje
ELF zaglavlje je predstavljeno strukturom Elf{32|64}_Ehdr.
Analizirajući ELF zaglavalje, možemo saznati razne informacije o binarnom fajlu, kao što su procesorska arhitektura za koji je fajl preveden, ulazna tačka/funkcija programa, i mnoge druge informacije. Koristeći alate kao što su `readelf` ili `llvm-readelf` možemo videti sva polja zaglavlja.

Sekcije
Sekcije sadrže sve informacije potrebne tokom faze povezivanja/linkovanja. Za one koji nisu familijarni sa ovim terminima, ukratko faza prevođenja programa je prikazana sledećom slikom:

Fajl `main.o` je potrebno povezati/linkovati sa potencijalno drugim objektnim fajlovima kako bi se dobio jedan izvršni fajl. Biće reči o fazi linkovanja svakako u nastavku.
Vratimo se na sekcije ELF fajla. Svaki ELF fajl sadrži tabelu sekcija (eng. Section Header Table). Ta tabela zapravo sadrži niz Elf{32|64}_Shdr struktura, koje predstavljaju svaku sekciju zasebno, i polja te strukture opisuju neke osibine te sekcije kao što su ime, veličina, tip, neke posebne flegove itd.

Neke od sekcija za koje ste verovatno čuli su:
- `.text` - kod programa
- `.data` - inicijalizovani podaci
- `.bss` - neinicijalizovani podaci
- `.plt` - tabela ulaznih adresa funkcija
- `.got` - sadrži informacije o adresama globalnih promenljivih
- `.symtab` - tabela simbola
- `.dynsym` - tabela dnamičkih simbola
- ...

Segmenti
Segmenti (eng. Program Headers) predstavljaju delove programa koji zasebno mogu biti učitani u memoriju. Za razliku od Sekcija, segmenti se koriste tokom izvršavanja programa i nisu nam od koristi tokom faze povezivanja/linkovanja. Takođe kao i prethodne celine ELF-a, i ovde postoji tabela segmenata (eng. Program Header Table) koja sadrži informacije o svakom segmentu ponaosob. Struktura koja opisuje segment je Elf{32|64}_Phdr i ona izgleda:

Izdvojimo neke tipove segmenata, opisane poljem `p_type`: `PT_LOAD` - segment koji se učitava, `PT_DYNAMIC` - sadrži sekciju `.dynamic`, itd. Spomenimo da samo segmenti sa tipom `PT_LOAD` se učitavaju u memoriju, te svaki ostali segment koji ima neki drugi navedeni tip se smešta u memorijski raspon koji je dodeljen segmentu koji je sadržao `PT_LOAD`.
Na segmente možemo gledati kao skupove sekcija grupisanih u jednu celinu koju možemo učitati u memoriju (pogotovu ako je organizacija memorije sistema organizovana korišćenjem metoda straničenja, jedna stranica može biti korišćena za jedan segment, i onda neka nesusedna stranica može biti korišćena za drugi segment istog programa). Znači, segmenti su zapravo samo način olakšavanja celom operativnom sistemu (i dinamičkom puniocu) prilikom izvršavanja programa. Sledeća slika ilustruje opisanu ideju:

Zanimljivo je da se segmenti poravnavaju na veličinu stranice da ne bismo operativni sistem stavili u iskušenje da dva segmenta učita u istu stranicu, jer segmenti uglavnom imaju različite atribute/flegove pristupa.
Simboli i Relokacije
Tokom procesa pisanja programa definišemo imena promenljivih i funkcija kako bi ih kasnije u programu referisali/koristili. Imena takvih entiteta nazivamo simbolima. Za takve informacije, format ELF odvaja posebne sekcije koje ih čuvaju. Simboli se smeštaju u obliku tabela koje nazivamo Table Simbola (eng. Symbol Tables). Programski prevodioci tokom faze prevođenja popunjavaju te tabele povezivajući sekvence mašinskog koda sa odgovarajućim simbolima iz tabela koje ih čuvaju. Obično se simbolima pristupa preko para {adresa, pomeraj}. Linker je alat koji tokom faza povezivanja/linkovanja koristi ove informacije kako bi razrešio referencu simbola iz mašinskog koda sa konkretnom definicijom tog simbola iz Tabeli simbole. Tokom ove faze recimo linker može detektovati da se koristi funkcija koja nema definiciju itd.
U ELF-u, svaki simbol je predstavljen instancama Elf{32|64}_Sym strukture (smeštene u Tabeli simbola).

Iz ove strukture možemo saznati ime koje opisuje dati simbol, polje st_info sadrži razne korisne osobine simbola - recimo tip entiteta na koji simbol referiše - STT_FUNC predstavlja funkcije, STT_OBJECT ukazuje promenljive itd.
Fajlovi koji prate format ELF mogu imati najviše dve Tabele Simbola. Jedna od njih je .symtab koja sadrži informacije o statičkim simbolima programa i ona se koristi tokom faze linkovanja/povezivanja. Druga tabela simbola je .dynsym i ona se koristi tokom faze dinamičkog povezivanja (eng. dynamic linking). Za one koji nisu familijarni sa ovim terminom, ukratko, nekada želimo da referišemo/koristimo simbole iz eksternih/spoljašnjih objekata poput deljenih biblioteka (eng. shared library), a čija definicija se ne nalazi u kontekstu našeg programa. Informacije iz ove tabele se koriste tokom faze izvršavanja programa, od strane dinamičkog punioca (eng. Runtime dynamic loader). Ova sekcija se ne sme otkloniti iz fajla, te iako pokušamo da je otklonimo uz pomoć nekih sistemskih alata (opcija --strip) simboli će ostati netaknuti (.symtab se može otkloniti nakon faze linkovanja).
Ostao sam dužan da spomenem koju reč o relokacijama i kakve to veze one imaju sa simbolima. Spomenuli smo da se simbolima iz mašinskog koda pristupa putem referenci, te je izazov pronaći definiciju konkretnog simbola na koju ta referenca ukazuje. Baš taj mehanizam povezivanja reference simbola sa samom definicijom simbola nazivamo Relokacijama (npr. želimo da pristupimo funkciji iz nekog spoljašnjeg modula, jasno je da adresa te funkcije mora biti razrešena kako bi je koristili u konkekstu našeg programa). Neke relokacije se razrešavaju tokom faze linkovanja/povezivanja (statički simboli- prilikom povezivanja više objektnih fajlova u jedan), a pak s druge strane postoje i relokacije koje se razrešavaju i tokom izvršavanja programa, tačnije tokom faze dinamičkog punjenja (npr. primer sa pozivom funkcije iz neke deljene biblioteke). Relokacija se prestavlja strukturom Elf{32|64}_Rel ili Elf{32|64}_Rela (arhitekture biraju korišćenje prve ili druge strukture):

Polje r_offset čuva informaciju o tome gde bi relokacija trebalo da bude primenjena, dok r_info ukazuje na to kako bi pozicija simbola u tebeli simbola trebalo da bude izračunata. Postoje razni tipovi relokacija, npr. ako je tip relokacije označen kao R_386_GOT32, to znači da se adresa simbola računa u odnosu na GOT (Globalna Tabela pomeraja - eng. Global Offset Table).
U suštini, kako se izvrašava program koji je dinamički preveden (preciznije - linkovan/povezan):
- u memoriju se učitavaju segmenti označeni sa PT_LOAD
- koristeći .dynamic sekciju, dinamički punioc traži zavisnosti/biblioteke po sistemu
- proces relokacije
- izvršavanje programa od ulazne tačke
To bi bilo to što se tiče ELF formata. Pokrili smo osnovne strukture ovih fajlova, spomenuli kako se struktura fajlova menja i tokom dinamičkog linkovanja (postoji i notacija lenjog dinamičkog povezivanja (eng. lazy linking) - gde se relokacije odlažu do trenutka kada nam određena funkcija zaista treba, ali to ostavljamo za neki naredni blog).
Fajl format PE/COFF
Fajlovi na operativnim sistemima familije Windows su zapisani prateći format `PE/COFF`. Ovaj format deli mnoge karakteristike sa formatom ELF, ali se po mnogome i razlikuje. Spomenuli smo da format ELF deli fajl u nekakve sekcije, pa kasnije iste grupiše u segmente. Kod formata PE/COFF ne postoji pojam segmenata, već samo sekcija. Možemo načelno smatrati da se u slučaju formata PE/COFF sekcije tokom izvršavanja programa koriste kao i segmenti u slučaju formata ELF.
Format
Prva stvar koju opisujemo jeste COFF File Header. Ovaj deo predstavlja zaglavlje fajla i veoma je sličan zaglavlju fajlova formata ELF. Tu se mogu naći razne informacije o samom fajlu, npr. kada je isti generisan, informacije o arhitekturi, pokazivač na tabelu simbola itd. Koristeći alat llvm-readobj možemo istraživati PE/COFF fajlove čak i sa operativnih sistema UNIX familije (tipa sa Ubuntu Linux sistema).
$ llvm-readobj --file-header test.exe
File: test.exe
Format: COFF-x86-64
Arch: x86_64
AddressSize: 64bit
ImageFileHeader {
Machine: IMAGE_FILE_MACHINE_AMD64 (0x8664)
SectionCount: 15
TimeDateStamp: 2022-04-15 18:02:42 (0x6259B342)
PointerToSymbolTable: 0x45400
SymbolCount: 981
StringTableSize: 7522
OptionalHeaderSize: 240
Characteristics [ (0x22)
IMAGE_FILE_EXECUTABLE_IMAGE (0x2)
IMAGE_FILE_LARGE_ADDRESS_AWARE (0x20)
]
}
Nakon COFF zaglavlja, izvršni fajlovi sadrže Opcioano Zaglavlje (eng. Optional Header), koje sadži veoma bitne informacije kako za proces povezivanja/linkovanja, tako i za proces izvršavanja programa. Ovde bih izdvojio polje koje je veoma specifično za Windows operativni sistem, a to je Preferirana adresa (eng. Preferred Address) i ona je predstavljena ImageBase poljem u tabeli informacija koje se nalaze u Opcionom Zaglavlju. Ovo polje zapravo predstavlja adresu prvog bajta gde bi program (ili biblioteka) trebalo da se učita u memoriju. Za deljene biblioteke (DLL) to je adresa 0x10000000, dok mnoge Windows varijante preferiraju adresu 0x00400000. Pored ove informacije, tu možemo naći razne druge informacije, kao što su informacije o veličini zaglavlja, poravnanju fajla i sekcija, itd.
ImageOptionalHeader {
Magic: 0x20B
MajorLinkerVersion: 14
MinorLinkerVersion: 0
SizeOfCode: 7680
SizeOfInitializedData: 274944
SizeOfUninitializedData: 0
AddressOfEntryPoint: 0x14F0
BaseOfCode: 0x1000
ImageBase: 0x140000000
SectionAlignment: 4096
FileAlignment: 512
MajorOperatingSystemVersion: 6
MinorOperatingSystemVersion: 0
MajorImageVersion: 0
MinorImageVersion: 0
MajorSubsystemVersion: 6
MinorSubsystemVersion: 0
SizeOfImage: 319488
SizeOfHeaders: 1024
Subsystem: IMAGE_SUBSYSTEM_WINDOWS_CUI (0x3)
Characteristics [ (0x8160)
IMAGE_DLL_CHARACTERISTICS_DYNAMIC_BASE (0x40)
IMAGE_DLL_CHARACTERISTICS_HIGH_ENTROPY_VA (0x20)
IMAGE_DLL_CHARACTERISTICS_NX_COMPAT (0x100)
IMAGE_DLL_CHARACTERISTICS_TERMINAL_SERVER_AWARE (0x8000)
]
SizeOfStackReserve: 1048576
...
Spomenuli smo iznad da PE/COFF poznaje samo sekcije. Sekcije grupišu nekakve informacije koje imaju istu namenu. Neke od sekcija su: .edata (sadrži informacije o simbolima koje mogu koristiti i drugi programi), .idata (sadrži informacije o simbolima koje koristimo iz deljenih biblioteka), .reloc (sadrži informacije o relokaciji simbola iz različitih objektnih fajlova) itd.
Izdvojićemo sekciju .idata. Ova sekcija se koristi tokom faze dinamičkog povezivanja/linkovanja. Izvršni fajlovi (.exe) u ovoj sekciji sadrže informacije o uveženim simbolima. Na početku ove sekcije možemo pronaći zaglavlje (eng. Directory Table) koje sadrži informacije potrebne za razrešavanje adresa svih uveženih simbola, a nakon toga svaka biblioteka ima svoje zasebne informacije. Za svaku biblioteku srećemo dve tabele:
1. Import Lookup Table - sadrži informacije o simbolima/imenima uveženih funkcija
2. Import Address Table - sadrži informacije o adresama/lokacijama uveženih funkcija (ovu tabelu razrešava dinamički punioc)
Ovu sekciju možemo zamisliti:
Directory Table
DLL 1
ILT
IAT
DLL2
ILT
IAT
...
Kako jedan poziv ka dinamičkoj funkciji radi?
Hajde da ovo uprostimo koliko je to moguće. Sekcija .idata sadrži informacije o svim DLL-ovima korićenim u našem programu. Kako je sekcija .text zadužena za čuvanje koda, kada je ciljani simbol neke instrukcije koja izvršava poziv funkcije neki simbol iz dinamičke biblioteke, tok programa se preusmerava u sekciju .idata, gde tom prilikom dinamički punioc razrešava lokaciju simbola koji treba pozvati. Naredna slika ilustruje to na lepši način.

Zaključak
To bi bilo to, ukratko o formatima fajlova na Unix i Windows operativnim sistemima. Ovim smo samo zagrebali u formu i strukturu ovih fajlova, nadam se da smo bar ukazali na neke interesantne pojedinosti, te bar znate odakle da počnete. Hvala na čitanju!
Add comment