Common Object File Format

Das Common Object File Format (COFF; deutsch allgemeines Objektdateiformat) ist ein Binärformat für Programme und Objektdateien. Es wurde von AT&T für das Betriebssystem Unix System V eingeführt[1] und findet heutzutage vor allem im darauf aufbauenden Format PE für Windows Verwendung (siehe Portable Executable). Für Dateiendungen wird, falls vorhanden und abgesehen von den für PE genutzten Endungen, oft „cof“, „obj“ oder „lib“ verwendet.

Geschichte

Ursprünglich wurde das Format a.out für ausführbare Dateien unter Unix verwendet. Dieses unterstützte jedoch moderne Entwicklungen wie eingebettete Debugging-Informationen oder dynamische Bibliotheken nicht. Deshalb entwickelte AT&T für Release 3 vom Unix System V das Common Object File Format.[2] Da das originale COFF designtechnisch beschränkt war, entwickelten sich unterschiedliche Varianten unter den Unix-Herstellern (z. B. XCOFF von IBM für AIX[3], ECOFF von SGI und anderen). Mit dem Release 4 von System V im Jahre 1989 ersetzte AT&T COFF durch das neue, gemeinsam mit Sun Microsystems entwickelte Format ELF (Executable and Linking Format).[4]

Eigenschaften

Mit COFF wurde es möglich, Debugging-Informationen direkt in eine Binärdatei einzubetten. Bibliotheken können dynamisch gelinkt und als separate Dateien gehandhabt werden, brauchen also nicht zum unveränderlichen, unaustauschbaren Bestandteil einer Programmdatei zu werden. Dazu werden alle Adressen in den Relokationseinträgen relativ zur eigentlichen Adresse der Sektion in den virtuellen Speicher der Anwendung geladen. Dadurch braucht die Adresse der Sektion erst zur Übersetzungszeit festgelegt zu werden anstatt bereits bei der Programmierung. Nach COFF entwickelte Formate besitzen diese Fähigkeiten ebenfalls.

Verwendung

Moderne Unix- und Linux-Versionen unterstützen COFF nicht mehr, allerdings wird es für Eingebettete Systeme noch verwendet[5]. Unter Windows NT (und früher) ist die COFF-Variante Portable Executable (PE, manchmal auch PE/COFF) das Standarddateiformat für Bibliotheken und ausführbare Dateien, allerdings unterscheidet sich diese Variante geringfügig vom ursprünglichen COFF.[6]

Struktur

Eine COFF-Datei besteht aus mehreren Teilen. Sie beginnt mit dem File Header und einem Optional Header. Dann folgt eine Anzahl von Sektionen, bestehend aus Header, einer Datensektion sowie einem Bereich für Zeilennummerneinträge und einem Bereich für Relokationseinträge. Am Dateiende folgen eine Symboltabelle und eine Zeichenkettentabelle.

File Header

Der File Header steht am Anfang einer Datei. Dort sind Daten gespeichert, die den Aufbau der gesamten Datei beschreiben. Dazu gehört die Magische Zahl, die für die unterschiedlichen Varianten (PE, XCOFF etc.) unterschiedlich ist, ein Unix-Timestamp mit dem Zeitpunkt der Erstellung der Datei, sowie die Position und Größe anderer Sektionen. Zudem können mittels Flag verschiedene Eigenschaften der Datei definiert werden (z. B. ob sie ausführbar ist).

struct filehdr {
    unsigned short  f_magic;        /* Magische Zahl */
    unsigned short  f_nscns;        /* Anzahl der Sektionen in der Datei */
    long            f_timdat;       /* Zeitstempel der Erstellung */
    long            f_symptr;       /* Zeiger zur Symboltabelle */
    long            f_nsyms;        /* Größe der Symboltabelle */
    unsigned short  f_opthdr;       /* Größe der "optional header" */
    unsigned short  f_flags;        /* Flags */
};

Optional Header

Der Optional Header enthält je nach COFF-Variante unterschiedliche Daten. Oft wird er für weitere zur Ausführung benötigte Informationen (z. B. die Einstiegsadresse) verwendet. Da er unterschiedlich lang sein kann, ist seine Größe im "File Header" gespeichert.

Section Header

Der Section Header enthält Daten über eine Sektion, insbesondere wie groß diese ist und wohin sie in den virtuellen Speicher geladen werden sollte. Für ausführbare Dateien in der Regel der Anfang des Speichers, d. h. die erste Sektion wird an die Adresse 0 geladen, für gelinkte Daten kann dies anders sein. Zudem enthalten sie einen Zeiger auf und die Größe der Zeilennummerneinträge und der Relokationseinträge.

struct sectionhdr {
    char           s_name[8];  /* Name der Sektion */
    unsigned long  s_paddr;    /* Speicheradresse, an die diese Sektion geladen werden soll*/
    unsigned long  s_vaddr;    /* virtuelle Adresse, an die diese Sektion geladen werden soll */
    unsigned long  s_size;     /* Größe der Sektion (inklusive Header)*/
    unsigned long  s_scnptr;   /* Zeiger zu den Daten dieser Sektion */
    unsigned long  s_relptr;   /* Zeiger zu den Relokationseinträgen dieser Sektion */
    unsigned long  s_lnnoptr;  /* Zeiger zu dem Zeilennummerneinträgen dieser Sektion */
    unsigned short s_nreloc;   /* Anzahl der Relokationseinträge */
    unsigned short s_nlnno;    /* Anzahl der Zeilennummerneinträge */
    unsigned long  s_flags;    /* Flags */
};

Datensektion

Die Datensektion kann unterschiedlich lang sein. Sie enthält die eigentlichen Daten in der Datei. Dies sind in der Regel Anweisungen in Maschinencode, Platz für Variablen und Daten, die für die Ausführung benötigt werden – kurzum, das eigentliche Programm.

Relokationseintrag

Ein Relokationseintrag definiert, wo die Symbole in der Datensektion gefunden werden können. Dies wird für jedes Symbol einzeln definiert.

typedef struct reloc{
    unsigned long  r_vaddr;   /* Adresse für die Relokation */
    unsigned long  r_symndx;  /* Symbol, für das die Relokation gilt */
    unsigned short r_type;    /* Type der Relokation*/
};

Zeilennummerneintrag

Ein Zeilennummerneintrag definiert, welche Zeile im Quellcode welcher Anweisung im Maschinencode entspricht. Dies ist insbesondere zum Debuggen von Anwendungen wichtig. Jede Sektion hat ihre eigene Tabelle mit Zeilennummern. Die Zeilen werden dabei für jede Funktion in der Sektion einzeln gezählt.

typedef struct lineno{
    union l_addr{
        unsigned long l_symndx;  /* Index des Namens der Funktion */
        unsigned long l_paddr;   /* Adresse der Zeilennummer */
    };
    unsigned short l_lnno;  /* Zeilennummer */
};

Zeilennummern werden ab Anfang jeder Funktion ab 0 hochgezählt. Für eine Zeile, auf der eine Funktion beginnt, wird also ein Eintrag mit l_lnno = 0 und dem Symbol der Funktion als l_symndx erstellt. Für jede weitere Zeile in der Funktion wird ein Eintrag mit der Anzahl an Zeilen seit dem Funktionsbeginn als l_lnno erstellt und der Adresse der ersten Anweisung aus dieser Zeile als l_paddr.

Symboltabelle

Die Symboltabelle enthält Informationen über die in der Datei vorhandenen Symbole. Symbole sind z. B. Funktionen oder Variablen, die von anderen Programmen verwendet werden können. Die Größe und die Position der Symboltabelle wird im File Header festgelegt. Die Symboltabelle besteht aus Einträgen der Form

typedef struct sysent{
  union e {
    char e_name[8];             /* Name des Symbols */
    struct e {
      unsigned long e_zeroes;   /* Falls 0, ist der Name des Symbols in der Zeichenkettentabelle angelegt*/
      unsigned long e_offset;   /* Position des Symbols in der Zeichenkettentabelle */
    };
  };
  unsigned long e_value;        /* Wert (in der Regel Adresse) des Symbols */
  short e_scnum;                /* Sektion */
  unsigned short e_type;        /* Datentyp */
  unsigned char e_sclass;       /* Speicherklasse */
  unsigned char e_numaux;       /* Anzahl zusätzlicher Einträge*/
};

Der Name des Symbols wird in e_name gespeichert, wenn er höchstens acht Zeichen lang ist. Ansonsten wird er in der Zeichenkettentabelle abgelegt, dann ist e_zeros = 0, und e_offset gibt die Position dieses Eintrags in der Zeichenkettentabelle an. Der „Wert“ des Symbols wird in e_value gespeichert. Dies ist in der Regel die Adresse, an der dieses Symbol abgelegt ist, welche wiederum vom Datentyp und der Speicherklasse abhängt, die in e_sclass abgelegt ist. e_type definiert den Datentypen des Symbols. Dies kann entweder ein elementarer Typ (int, float etc.) oder ein zusammengesetzter Typ (struct, union) sein. Zudem kann das Symbol einen Wert, einen Zeiger ("pointer"), ein Feld ("array") oder eine Funktion, die diesen Wert zurückgibt, definieren. e_class definiert die Speicherklasse, also wo und wie das Symbol abgelegt ist (z. B. kann es ein externes Symbol sein, ein Funktionsargument, eine globale oder statische Variable etc.). Abhängig von Typen des Symbols können zusätzliche Einträge folgen. Die Anzahl dieser Einträge ist mit e_numaux angegeben.

Zeichenkettentabelle

Die Zeichenkettentabelle folgt am Schluss der Datei. Sie beginnt mit einer Ganzzahl ("integer"), in der die Länge der Tabelle gespeichert ist. Danach folgen alle Zeichenketten hintereinander. Um eine Zeichenkette zu lesen, muss man deren Position kennen und kann an dieser Stelle mit dem Lesen beginnen. Die Zeichenketten sind nullterminiert.

Einzelnachweise

  1. Common Object File Format Texas Instruments, aufgerufen am 8. März 2014
  2. Overview over SCO System V Release 3 (Memento des Originals vom 9. März 2014 im Internet Archive)  Info: Der Archivlink wurde automatisch eingesetzt und noch nicht geprüft. Bitte prüfe Original- und Archivlink gemäß Anleitung und entferne dann diesen Hinweis.@1@2Vorlage:Webachiv/IABot/h10025.www1.hp.com HP, aufgerufen am 8. März 2014
  3. XCOFF Object File Format IBM, aufgerufen am 8. März 2013
  4. Object File / Symbol Table Format Specification Compaq/HP, aufgerufen am 8. März 2014
  5. Typer of Executable (Memento des Originals vom 9. März 2014 im Internet Archive)  Info: Der Archivlink wurde automatisch eingesetzt und noch nicht geprüft. Bitte prüfe Original- und Archivlink gemäß Anleitung und entferne dann diesen Hinweis.@1@2Vorlage:Webachiv/IABot/www.linux.org Linux.org, aufgerufen am 8. März 2014
  6. PE and COFF Specification, Microsoft Developer Network, aufgerufen am 8. März 2014

Weblinks