Ich möchte dies etwas näher erläutern und eine vollständigere Antwort geben. Betrachten wir zunächst diesen Code:
#import <Foundation/Foundation.h>
int main(int argc, char *argv[]) {
void (^block)() = nil;
block();
}
Wenn Sie dies ausführen, sehen Sie einen Absturz in der block()
Zeile, der ungefähr so aussieht (wenn Sie auf einer 32-Bit-Architektur ausgeführt werden - das ist wichtig):
EXC_BAD_ACCESS (Code = 2, Adresse = 0xc)
Warum ist das so? Nun, das 0xc
ist das Wichtigste. Der Absturz bedeutet, dass der Prozessor versucht hat, die Informationen an der Speicheradresse zu lesen 0xc
. Dies ist fast definitiv eine völlig falsche Sache. Es ist unwahrscheinlich, dass dort etwas ist. Aber warum hat es versucht, diesen Speicherort zu lesen? Nun, es liegt an der Art und Weise, wie ein Block tatsächlich unter der Haube aufgebaut ist.
Wenn ein Block definiert ist, erstellt der Compiler tatsächlich eine Struktur auf dem Stapel in der folgenden Form:
struct Block_layout {
void *isa;
int flags;
int reserved;
void (*invoke)(void *, ...);
struct Block_descriptor *descriptor;
};
Der Block ist dann ein Zeiger auf diese Struktur. Das vierte Mitglied invoke
dieser Struktur ist das interessante. Es ist ein Funktionszeiger, der auf den Code zeigt, in dem die Implementierung des Blocks gespeichert ist. Der Prozessor versucht also, zu diesem Code zu springen, wenn ein Block aufgerufen wird. Beachten Sie, dass, wenn Sie die Anzahl der Bytes in der Struktur vor dem invoke
Element zählen, Sie feststellen, dass 12 in Dezimalzahl oder C in Hexadezimalzahl vorhanden sind.
Wenn also ein Block aufgerufen wird, nimmt der Prozessor die Adresse des Blocks, addiert 12 und versucht, den an dieser Speicheradresse gespeicherten Wert zu laden. Es wird dann versucht, zu dieser Adresse zu springen. Wenn der Block jedoch Null ist, wird versucht, die Adresse zu lesen 0xc
. Dies ist eindeutig eine Duff-Adresse, und so erhalten wir den Segmentierungsfehler.
Der Grund, warum es ein Absturz wie dieser sein muss, anstatt stillschweigend zu scheitern, wie es ein Objective-C-Nachrichtenaufruf tut, ist wirklich eine Entwurfsentscheidung. Da der Compiler entscheidet, wie der Block aufgerufen werden soll, muss er überall dort, wo ein Block aufgerufen wird, keinen Prüfcode einfügen. Dies würde die Codegröße erhöhen und zu einer schlechten Leistung führen. Eine andere Möglichkeit wäre die Verwendung eines Trampolins, das die Nullprüfung durchführt. Dies würde jedoch auch zu Leistungseinbußen führen. Objective-C-Nachrichten durchlaufen bereits ein Trampolin, da sie die Methode aufrufen müssen, die tatsächlich aufgerufen wird. Die Laufzeit ermöglicht das verzögerte Einfügen von Methoden und das Ändern von Methodenimplementierungen, sodass sie ohnehin schon ein Trampolin durchläuft. Die zusätzliche Strafe für die Nullprüfung ist in diesem Fall nicht signifikant.
Ich hoffe, das hilft ein wenig, die Gründe zu erklären.
Weitere Informationen finden Sie in meinen Blog- Beiträgen .