Wiki

Clone wiki

chocos / ELF - Notes

  • ELF (Executable and Linkable Format) - powszechnie stosowany zarówno do plików wykonywalnych jak i bibliotek
  • Najpierw nagłówek z podstawowymi informacjami na temat zawartosci a potem juz kolejne sekcje pamieci
  • W headerze jest zapisane gdzie są sekcje programu i jak powinny być załadowane

Relokacja

Relokacja - dzięki niej można dostosować adresy programu tak, aby uniezależnić go od tego w jakim miejscu umieści je system (teoretycznie) * Tabela-Relokacji - to część pliku w formacie ELF, która zawiera informacje potrzebne do procesu relokacji. Tabela ta zawiera wpisy dla każdego fragmentu kodu lub danych, które muszą być zmodyfikowane, gdy plik jest ładowany do pamięci w innym miejscu, niż został pierwotnie skompilowany.

Każdy wpis w tabeli relokacji wskazuje, gdzie w pliku znajduje się odniesienie do adresu, które musi być zmodyfikowane, oraz jakiego rodzaju modyfikacja jest wymagana. Dzięki temu, gdy system operacyjny ładuje plik do pamięci, może przejrzeć tabelę relokacji i odpowiednio zaktualizować wszystkie odniesienia do adresów, zapewniając, że program będzie działał poprawnie, niezależnie od tego, gdzie w pamięci zostanie umieszczony.

Wpis w tablicy relokacji:

//==========================================================================================================================================
/**
 * @brief stores relocation entry (extended from RELA and not from REL)
 */
//==========================================================================================================================================
typedef struct
{
    oC_Elf_Addr_t       Offset;                             //!< Location at which to apply the relocation action.
    oC_Elf_Word_t       Info;                               //!< Symbol table index and type of the relocation
    oC_Elf_Sword_t      Addend;                             //!< Constant addend used to compute the value to be stored into the relocatable field
} oC_Elf_RelocationEntry_t;

Tablice relokacji są tworzone per sekcja, więc np tablica relokacji dla sekcji .text będzie się nazywać .rel.text albo .rela.text

Przykład tablicy relokacji:

rela.png

Dla kodu:

#include <stdio.h>

volatile int globalVar = 10;

int main() {
    globalVar = 11;
    printf("globalVar: %d\n", globalVar);
    return 0;
}

Skompilowanego a flagą -fPIC

Global Offset Table

Global Offset Table (got) zawiera wszystkie referencje do zmiennych i funkcji znajdujących się w programie. Ważne jest, aby zauważyć, że mogą to być zarówno odwołania do samego programu jak i bibliotek współdzielonych. Jako, że ChocoOS nie wspiera bibliotek współdzielonyc, to u nas są to tylko odwołania do symboli tego programu (to upraszcza nam trochę kod dynamicznego linkera). Sekcja got powstaje wtedy, kiedy użyjemy flagi fPIC podczas kompilacji.

Aby zaktualizować sekcję GOT ChocoOS wykorzystuje następujący kod:

// W pliku ELF jest sekcja o nazwie 'sections' (pochodzi ona z linkera)
outFile->sections                   = oC_Elf_GetSectionHeaderByName(outFile,".sections");

...


// Odczytujemy zawartość `sections`
oC_Program_Sections_t * sectionsHeader  = File->Sections.sections.Address;

// BinSize pochodzi z linkera: https://bitbucket.org/chocos/chocos/src/76e42f3b1e912c8679784636815879fcdd9e19bf/program.ld#lines-108
oC_MemorySize_t         sectionsSize    = sectionsHeader->BinSize; 

// Kopiujemy zawartość binarną programu do bufora `SectionsBuffer`
memcpy( ProgramContext->SectionsBuffer, File->FileBuffer, sectionsSize );

// Odczytujemy adres i rozmiar sekcji GOT
uint32_t * gotEntries = ProgramContext->Sections.got.Address;
uint32_t   leftSize   = ProgramContext->Sections.got.Size;

while(leftSize > 0)
{
    // Aktualizujemy wszystkie wpisy w GOT tak, aby wskazywały na bufor w pamięci, który zarezerwowaliśmy i który zainicjalizowaliśmy zawartością sekcji `sections`
    // Możemy sobie pozwolić na takie uproszczenie, ponieważ nie wspieramy bibliotek współdzielonych, więc 
    // wszystkie wpisy odnoszą się do tego bufora
    (*gotEntries) += (uint32_t)ProgramContext->SectionsBuffer;
    leftSize -= sizeof(uint32_t);

    gotEntries++;
}

Ładowanie pliku i proces odczytywania poszczególnych sekcji

W nagłówku pliku mamy coś takiego:

    oC_Elf_Half_t       SectionHeaderTableEntrySize;        //!< Holds the size in bytes of one entry in section header table
    oC_Elf_Half_t       SectionHeaderTableNumberOfEntries;  //!< Number of entries in section header table
    oC_Elf_Half_t       SectionHeaderNameStringTableIndex;  //!< Holds the section header table index of the entry associated with the section name string table

Czyli mamy section header table - to jest tablica, w której każdy element jest jednym headerem sekcji. Każdy header jest tego samego rozmiaru i znajduje się jeden po drugim. W FileHeaderze mamy też zapisane ile tych sekcji w pamięci jest, oraz który wpis w tej section header table zawiera informacje na temat sekcji, która przechowuje nazwy poszczególnych sekcji.

Poruszanie się po nagłówkach sekcji

Czyli jak chcemy odczytać jakąś sekcję, to:

  1. Używamy SectionHeaderTableOffset w nagłówku pliku, aby zlokalizować tablicę sekcji
  2. Używamy SectionHeaderTableEntrySize z nagłówka pliku aby przeszukać kolejne wpisy w tablicy
  3. Używamy SectionHeaderNameStringTableIndex aby zlokalizować nagłówek z nazwami wszystkich sekcji

Każdy section header wygląda tak:

//==========================================================================================================================================
/**
 * @brief holds header of the ELF sections
 */
//==========================================================================================================================================
typedef struct
{
    oC_Elf_Word_t       NameIndex;                          //!< Name of the section - its value is an index into the section header string table
    oC_Elf_Word_t       Type;                               //!< This member categorizes the section’s contents and semantics. See #oC_Elf_SectionType_t for more
    oC_Elf_Word_t       Flags;                              //!< Attributes flags. See #oC_Elf_SectionFlag_t for more
    oC_Elf_Addr_t       Address;                            //!< If the section appear in the memory image of a process, this member gives the address at which the section's first byte should reside
    oC_Elf_Off_t        Offset;                             //!< Byte offset from the beginning of the file to the first byte in the section
    oC_Elf_Word_t       Size;                               //!< The section size in bytes
    oC_Elf_Word_t       Link;                               //!< Section header table index link, interpretation depends on the section type
    oC_Elf_Word_t       Info;                               //!< Extra information that depends on the section type
    oC_Elf_Word_t       AddressAlign;                       //!< Alignment of the address of the section
    oC_Elf_Word_t       EntrySize;                          //!< Some sections holds table of symbols, and this is size of the entry in the table. If it is 0, the section does not hold a table of fixed size entries
} oC_Elf_SectionHeader_t;
Offset wskazuje na miejsce w pliku, gdzie znajdują się dane danej sekcji. Size informuje nas o tym jak duża jest ta sekcja. Jeśli jakaś sekcja przechowuje tablicę o stałym rozmiarze elementu, to EntrySize informuje nas o tym jak duży jest wpis w tej tablicy. Warto zauważyć, że sekcja z nazwami sekcji nie jest tablicą, więc NameIndex jest tak naprawdę offsetem w tej sekcji

Odszukanie sekcji po nazwie:

Iterujemy od pierwszej sekcji do ostatniej, używamy NameIndex z section headera, aby odczytać nazwę sekcji z sekcji nazw. Jak znajdziemy to używamy Offset z section headera, żeby odczytać dane sekcji

Updated