Das primär ausführbare Dateiformat unter MacOS ist das Mach-O-Dateiformat. Fast jedes Programm, das auf einem Mac-Rechner ausführt, ist eine Mach-O-Datei, einschließlich Applications, die vom App Store heruntergeladen werden. Heute lernen wir, wie diese Dateien organisiert sind.
Wie so viele andere Dateiformate fangen Mach-O-Dateien mit einem Header an. Auf diesem Header stehen verschiedene Informationen über die Datei, darunter die Art des Prozessors, die Anzahl von Load-Commands und die gesamte Größe der Load-Commands. Das Format dieses Headers ist typisch, außer einem Merkmal: Die Byte-Reihenfolge passt zu der des Rechners, unter dem die Datei ausführen soll. Das heißt, die Zahlen sind auf x86 / 0x86_64 Prozessoren Little-Endian und auf PowerPC Prozessoren Big-Endian. Dazu gehört auch die magische Zahl. Die Struktur des Headers für 32-Bit Architekturen in C-Syntax ist hier angeführt:
// von loader.h im MacOSX-Software-Entwicklungskit
struct mach_header {
uint32_t magic; /* mach magic number identifier */
int32_t cputype; /* cpu specifier */
int32_t cpusubtype; /* machine specifier */
uint32_t filetype; /* type of file */
uint32_t ncmds; /* number of load commands */
uint32_t sizeofcmds; /* the size of all the load commands */
uint32_t flags; /* flags */
};
Auf 64-Bit Systemen ist der Header fast derselbe, allerdings mit einer zusätzlichen Zahl, die für die zukünftige Verwendung reserviert ist. 32-Bit und 64-Bit Mach-O-Dateien können durch die magische Zahl, magic
, voneinander unterschieden werden. 32-Bit-Big-Endian Mach-O-Dateien haben die magische Zahl 0xfeedface
in Hexadezimal, 32-Bit-Little-Endian Dateien haben 0xcefaedfe
, 64-Bit-Big-Endian Dateien haben 0xfeedfacf
, und 64-bit-Little-Endian Dateien haben 0xcffaedfe
. Genau wie in den Universal-Binary-Dateien, stellen cputype
die Prozessorarchitektur und cpusubtype
den genauen Prozessor fest.
Der Wert von filetype
zeigt, was für eine Mach-O-Datei die Datei eigentlich ist. Es gibt als Konstante fünfzehn mögliche Werte, unter anderem MH_EXECUTE
, die eine ganz normale, ausführbare Datei bedeutet, und MH_DYLIB
, die eine dynamische Codebibliothek-Datei bedeutet.
´ncmds´ steht für die Anzahl von Load-Commands in der Datei, und sizeofcmds
für die gesamte Größe den Load-Commands. Load-Commands sind ein wichtiges und tiefes Thema, auf das wir in Kürze zu sprechen kommen.
Das letzte Mitglied des Headers ist flags
, ein Bitfeld, das das Verhalten des Datei-Laders kontrolliert. Manche Funktionen, die diese Flags kontrollieren, bestimmen zum Beispiel, ob das Programm in einer bestimmten oder zufälligen Speicheradresse geladen wird, oder wie die Symbole gebunden werden.
Nach dem Header kommen die Load-Commands (oder "Ladebefehle" auf Deutsch). Diese Befehle sind der wichtigste Teil der Datei. Ihr Job ist unter anderem die eigentlichen Daten, Maschinenbefehle und so weiter zu laden. Es gibt viele verschiedene Befehle, 54 um genau zu sein, und fast jeder Befehl hat eine andere Funktion.
Jeder Befehl beginnt mit einem kleinen Header, der die Art von Befehl und die Größe des Befehls in Bytes enthält.
// von loader.h im MacOSX-Software-Entwicklungskit
struct load_command {
uint32_t cmd; /* type of load command */
uint32_t cmdsize; /* total size of command in bytes */
};
cmd
muss eine von den vierundfünfzig Konstanten in der loader.h
Datei sein, wie zum Beispiel LC_SEGMENT
. Diese Datei enthält auch die Konstante LC_REQ_DYLD
. Jede Ladebefehlkonstante, die nach MacOS X 10.1 hinzugefügt ist, muss mit dieser Konstante bitweise oder'd werden. Wenn der Linker eine unbekannte Ladebefehlkonstante sieht, in der dieses Bit gesetzt ist, wird der Linker einen Fehler melden und sich weigern, die Datei zu laden. ´cmdsize´ ist die gesamte Größe des Befehls, einschließlich dieses Headers. Der Rest des Ladebefehls folgt direkt auf diesem Header und hängt von der Befehlsart ab.
Lasst uns ein paar Ladebefehlbeispiele genau ansehen.
Die vielleicht wichtigsten Ladebefehle sind LC_SEGMENT
und sein größerer Bruder, LC_SEGMENT64
. ´LC_SEGMENT´ ist für 32-Bit Architekturen gemeint, und LC_SEGMENT64
für 64-Bit gemeint. Diese Befehle zeigen, dass ein Teil dieser Datei direkt in dem Adressraum dieses Prozesses abgebildet werden soll. Diese Befehle entsprechen den Segmenten in ELF- oder PE-Dateien. Die Strukturen der beiden Befehlen sind ähnlich:
// von loader.h im MacOSX-Software-Entwicklungskit
struct segment_command { /* for 32-bit architectures */
uint32_t cmd; /* LC_SEGMENT */
uint32_t cmdsize; /* includes sizeof section structs */
char segname[16]; /* segment name */
uint32_t vmaddr; /* memory address of this segment */
uint32_t vmsize; /* memory size of this segment */
uint32_t fileoff; /* file offset of this segment */
uint32_t filesize; /* amount to map from the file */
int32_t maxprot; /* maximum VM protection */
int32_t initprot; /* initial VM protection */
uint32_t nsects; /* number of sections in segment */
uint32_t flags; /* flags */
};
struct segment_command_64 { /* for 64-bit architectures */
uint32_t cmd; /* LC_SEGMENT_64 */
uint32_t cmdsize; /* includes sizeof section_64 structs */
char segname[16]; /* segment name */
uint64_t vmaddr; /* memory address of this segment */
uint64_t vmsize; /* memory size of this segment */
uint64_t fileoff; /* file offset of this segment */
uint64_t filesize; /* amount to map from the file */
int32_t maxprot; /* maximum VM protection */
int32_t initprot; /* initial VM protection */
uint32_t nsects; /* number of sections in segment */
uint32_t flags; /* flags */
};
Die Struktur beginnt mit dem Header, über den wir bereits gesprochen haben. Als nächstes kommt segname
, eine einfache Zeichenkette, die den Namen des Segments enthält. segname
muss sechzehn oder weniger Zeichen enthalten. Nach dem Namen kommen vmaddr
und vmsize
, die zusammen für den Speicherbereich stehen, wo dieses Segment geladen werden soll. fileoff
und filesize
stellen fest, von wo in der Datei dieses Segment geladen werden soll.
initprot
und maxprot
zeigen den anfänglichen, bezeihungsweise maximalen Speicherschutz, der verwendet werden soll. Das vorletzte Mitglied der Struktur ist nsects
, das zeigt, wie viele Sektionstrukturen auf diese Struktur folgen. Als letztes ist flags
, das verschiedene Einstellungen des Segments kontrolliert.
Die 64-Bit Version der Struktur enthält die gleichen Mitglieder. Der einzige Unterschied ist, dass vmaddr
, vmsize
, fileoff
und filesize
64-Bit sind.
Direkt nach der Segmentstruktur kommen nsects
Sektionstrukturen:
// von loader.h im MacOSX-Software-Entwicklungskit
struct section { /*for 32-bit architectures */
char sectname[16]; /* name of this section */
char segname[16]; /* segment this section goes in */
uint32_t addr; /* memory address of this section */
uint32_t size; /* size in bytes of this section */
uint32_t offset; /* file offset of this section */
uint32_t align; /* section alignment (power of 2) */
uint32_t reloff; /* file offset of relocation entries */
uint32_t nreloc; /* number of relocation entries */
uint32_t flags; /* flags (section type and attributes)*/
uint32_t reserved1; /*reserved (for offset or index) */
uint32_t reserved2; /* reserved (for count or sizeof)*/
};
struct section_64 { /*for 64-bit architectures */
char sectname[16]; /* name of this section */
char segname[16]; /* segment this section goes in */
uint64_t addr; /* memory address of this section */
uint64_t size; /* size in bytes of this section */
uint32_t offset; /* file offset of this section */
uint32_t align; /* section alignment (power of 2) */
uint32_t reloff; /* file offset of relocation entries */
uint32_t nreloc; /* number of relocation entries */
uint32_t flags; /* flags (section type and attributes)*/
uint32_t reserved1; /*reserved (for offset or index) */
uint32_t reserved2; /* reserved (for count or sizeof) */
uint32_t reserved3; /* reserved*/
};
Diese Strukturen beschreiben die Sektionen, die das Segment bilden, und haben viele Mitglieder. Zuerst gibt es sectname
, eine einfache Zeichenkette von höchstens sechzehn Zeichen, die den Namen der Sektion enthält. segname
ist derselbe als das Mitglied segname
von der segment_command
oder segment_command_64
Struktur.
addr
und size
sind die Speicheradresse und Größe dieser Sektion, ähnlich wie die Mitglieder vmaddr
und vmsize
von der Segmentstruktur. Die gleiche Geschichte gilt für offset
: Dieses Mitglied ist ähnlich wie fileoff
in der Segmentstruktur, aber es gibt in der Sektionstruktur kein filesize
-Mitglied.
Als Nächstes kommt was Neues: align
. align
ist die vorzeichenlose Zweierpotenz, auf die die Sektion ausgerichtet sein müss, wenn es in den Speicher geladen wird.
Die nächsten zwei Mitglieder sind auch anders. reloff
ist ein weiteres Offset in der Datei, das die Position der »Relocation Entries« (auf Deutsch Relokationseinträge) zeigt. nrelocs
zeigt, wie viele »Relocation Entries« diese Sektion hat. Diese »Relocation Entries« werden verwendet, um positionsabhängige Machinenbefehle auf die eigentliche Speicheradresse einzustellen. Diese »Relocations« sind ziemlich kompliziert und sprengen leider den Rahmen dieses Blog-Beitrags.
Zuletzt gibt es flags
, das die Flags für diese Sektion enthält und verschiedene Einstellungen der Sektion kontrolliert. Die zwei weiteren Mitglieder sind nur für die zukünftige Verwendung reserviert.
Die 64-Bit Struktur hat als addr
und size
64-Bit vorzeichenlose Zahlen, statt 32-Bit vorzeichenlose, und am Ende eine weitere reservierte Zahl.
Dieses Beispiel zeigt wie Ladebefehle gewöhnlich funktionieren: Jeder Ladebefehl hat eine oder mehrere Strukturen, die die notwendigen Daten enthalten, um den Befehl durchzuführen.
Aber manchmal ist ein Ladebefehl überhaupt kein Befehl. Einige Befehle, wie LC_UUID
, existieren nur um Daten zu enthalten. Die Struktur des LC_UUID
-Befehls ist total einfach:
// von loader.h im MacOSX-Software-Entwicklungskit
struct uuid_command {
uint32_t cmd; /* LC_UUID */
uint32_t cmdsize; /* sizeof(struct uuid_command) */
uint8_t uuid[16]; /* the 128-bit uuid */
};
Der Header sind nur die ersten zwei Mitglieder des Ladebefehls. Das letzte Mitglied, uuid
, ist ein UUID: ein »Universally Unique Identifier«, auf Deutsch universell einzigartiger Identifikator. Dieser Identifikator kann diese Datei einzigartig identifizieren. Diese Fähigkeit kann hilfreich sein, um die korrekte Version einer Datei zu laden.
Mit dem Beispiel von LC_UUID
wird dieser Blog-Beitrag am Schluss kommen. Mach-O-Dateien haben eine große Tiefe, und man kann viele Stunden damit verbringen, die Details zu studieren. Ich, und somit auch dieser Beitrag, haben nur an der Oberfläche gekratzt; nur drei von vierundfünfzig Ladebefehlen wurden besprochen! Mach-O-Dateien können nicht nur ausführbare Dateien, sondern auch Objektdateien oder »DyLib«-Dateien (»Dynamic Library«, Englisch für »dynamische Bibliothek«) sein!
Mach-O-Dateien sind für mich faszinierend, weil sie so anders als ELF- und PE-Dateien sind. ELF- und PE-Dateien verlassen sich auf Tabellen, die Einträge mit fester Größe haben, während Mach-O Dateien diese Ladebefehle mit verschiedenen Größen haben. Aber welches Format ist besser? Spielt das überhaupt eine Rolle? Vielleicht, aber vielleicht auch nicht.