MacOS Universal Binaries

Apple machte vor ein paar Jahren einen wagenden Zug: es stellte seine Laptops und Computer von Intel-x86_64 Prozessoren auf ARM Prozessoren um. Prozessorarchitektur-Änderungen sind eher schwer zu machen; jedes Programm, das auf dem Computer laufen soll, muss für die neue Architektur kompiliert werden. Dieses Verfahren ist für die Erstanbieterprogramme genug Arbeit, aber die Entwickler von Drittanbietern sind dafür bekannt, langsam zu sein. Es kann lang dauern, bis Drittanbieterprogramme auf der neuen Architektur funktionieren. Benutzer hassen es, wenn ihre Software nach einem einfachen Computerupgrade nicht mehr funktioniert.

Aber Apple machte diesen Zug zuvor. Apple hatte einmal seine Computer mit PowerPC Prozessoren ausgerüstet und stellte sie von 2005 bis 2006 auf Intel Prozessoren um. Deshalb hat Apple diesmal ein paar Asse im Ärmel. Eins davon ist «Universal Binaries» (auf Deutsch, universelle Binärdateien), Binärdateien, die auf mehreren Architekturen laufen können. Die meisten universelle Binärdateien können nur auf einer Architektur laufen, also sind diese universelle Binärdateien beeindruckend. Lass uns sehen, wie sie funktionieren.

Die interessanteste Tatsache über Apples Universal Binaries ist, dass sie so einfach sind. Sie sind nichts mehr als mehrere ausführbare Mach-O-dateien in derselben Datei mit einer kleinen Kopfzeile. Mach-O-Dateien sind das normale ausführbare Dateiformat, das Apples Betriebssysteme seit Jahren verwendet haben. Eine Universal Binary packt einfach die ausführbare Dateien für die einzelnen Architekturen zusammen und das Betriebssystem wählt die richtige ausführbare Datei, wenn die Universal Binary ausführt wird.

Alle Universal Binaries, auch «Fat Binaries» (Englisch für «dicke Binärdateien») beginnen mit einer einfachen Kopfzeile. Die ersten vier Bytes sind eine magische Zahl, die dieses Dateiformat kennzeichnet. Sie sind entweder die hexadezimalen Werte CA FE BA BE, die eine 32-bit Universal Binary kennzeichnet, oder CA FE BA BF, die eine 64-bit Universal Binary kennzeichnet. Die magische Zahl wird von weiteren vier big-endian vorzeichenlosen bytes gefolgt, die die Anzahl der Architekturen in der Datei darstellen.

// von fat.h im MacOSX-Software-Entwicklungskit 
struct fat_header {
    uint32_t    magic;      /* FAT_MAGIC or FAT_MAGIC_64 */
    uint32_t    nfat_arch;  /* number of structs that follow */
};

Direkt nach der Kopfzeile liegen die nfat_arch fat_arch (oder fat_arch64) Strukturen, die die einzelnen Architekturen in den Binärdateien definieren. Die Größe der Mitglieder dieser Strukturen hängt davon ab, ob die Binärdatei 32-bit oder 64-bit ist, aber die Reihenfolge der Mitglieder ist dieselbe. Alle Ganzzahlen sind Big-Endian.

Zuerst kommt cputype, eine 32-bit vorzeichenbehaftete Zahl, die die Architektur kennzeichnet. Die möglichen Werte können in der Datei machine.h gefunden werden. Als Nächstes ist cpusubtype, eine weitere 32-bit vorzeichenbehaftete Zahl, die die genaue Version der Architektur kennzeichnet. Zum Beispiel bedeutet eine cputype von 0x00000007, oder CPU_TYPE_X86, und eine cpusubtype von 0x00000005, oder CPU_SUBTYPE_PENT, dass dieser Teil der Binärdatei auf einem Intel-Pentium-Prozessor laufen soll.

Das dritte Mitglied der fat_arch / fat_arch64 Struktur ist der vorzeichenlose Offset in der Binärdatei, offset, wo die ursprüngliche Mach-O-Objektdatei dieser Architektur gefunden werden kann. Das nächste Mitglied ist die vorzeichenlose Größe des Objekt, size. Zusammen erlauben es diese zwei Mitglieder, das ursprüngliche Objekt dieser Architektur von der Binärdatei zu extrahieren. Diese Mitglieder sind entweder 32-bit oder 64-bit, es hängt vom Typ des Universal Binarys an, die diese Datei ist.

Der letzte Mitglied der fat_arch und fat_arch64 Strukturen ist align, die vorzeichenlose Zweierpotenz, an der das Objekt ausgerichtet sein müssen, wenn es in den Speicher geladen wird. Die fat_arch64 Struktur hat auch eine weitere 32-bit vorzeichenlose Zahl namens reserved, die zur künftigen Verwendung reserviert ist.

// auch von fat.h
struct fat_arch {
    int32_t    cputype; /* cpu specifier (int) */
    int32_t    cpusubtype; /* machine specifier (int) */
    uint32_t   offset;  /* file offset to this object file */
    uint32_t   size;  /* size of this object file */
    uint32_t   align;  /* alignment as a power of 2 */
};

struct fat_arch_64 {
    int32_t    cputype; /* cpu specifier (int) */
    int32_t    cpusubtype; /* machine specifier (int) */
    uint64_t   offset;  /* file offset to this object file */
    uint64_t   size;  /* size of this object file */
    uint32_t   align;  /* alignment as a power of 2 */
    uint32_t   reserved; /* reserved */
};

Mit der Information, die in diesen Strukturen enthalten ist, kann man endlich die richtige Mach-O-Datei im Universal Binary finden. Das Mach-O-Datei selber bleibt völlig unverändert. Es gibt zwischen Architekturen kein Aufteilen der Daten und keine Datenkompression. Als ich diese Dateien erforschte, war ich eigentlich enttäuscht zu finden, dass sie so einfach sind. Aber die Mach-O-Dateien darin sind viel interessanter. Als jemand, der von der Linux-Welt kommt und mit ihren ELF-Dateien (Executable Linux Format, oder ausführbares Linux-Format) vertraut ist, war ich von den Unterschiede zwischen den Mach-O- und ELF-Dateiformaten sehr überrascht. Mein nächster Beitrag wird die Einzelheiten des Mach-O-Formats schildern.

Wähle eine Sprache:

Switch to English LanguageSwitch to German Language