Frage

Ich fange gerade erst an, meinen Kopf um Funktionszeiger in C zu wickeln, um zu verstehen, wie das Gießen von Funktionszeigern funktioniert, ich habe das folgende Programm geschrieben. Es erstellt im Grunde genommen einen Funktionszeiger auf eine Funktion, die einen Parameter aufnimmt, ihn mit drei Parametern auf einen Funktionszeiger verlässt und die Funktion aufruft und drei Parameter liefert. Ich war neugierig, was passieren würde:

#include <stdio.h>

int square(int val){
  return val*val;
}

void printit(void* ptr){
  int (*fptr)(int,int,int) = (int (*)(int,int,int)) (ptr);
  printf("Call function with parameters 2,4,8.\n");
  printf("Result: %d\n", fptr(2,4,8));
}


int main(void)
{
    printit(square);
    return 0;
}

Dies kompiliert und läuft ohne Fehler oder Warnungen (GCC -Wall auf Linux / x86). Die Ausgabe auf meinem System ist:

Call function with parameters 2,4,8.
Result: 4

Anscheinend werden die überflüssigen Argumente einfach schweigend verworfen.

Jetzt möchte ich verstehen, was hier wirklich passiert.

  1. Legalität: Wenn ich die Antwort auf verstehe Gießen Sie einen Funktionszeiger auf einen anderen Typ Richtig, dies ist einfach ein undefiniertes Verhalten. Die Tatsache, dass dies ein vernünftiges Ergebnis läuft und hervorbringt, ist nur reines Glück, richtig? (oder Freundlichkeit der Compiler -Autoren)
  2. Warum wird GCC mich auch mit Mauer davor warnen? Ist das etwas, was der Compiler einfach nicht erkennen kann? Wieso den?

Ich komme aus Java, wo typisches Typeck viel strenger ist, also verwirrte mich dieses Verhalten ein wenig. Vielleicht erlebe ich einen kulturellen Schock :-).

War es hilfreich?

Lösung

Die zusätzlichen Parameter werden nicht verworfen. Sie sind ordnungsgemäß auf dem Stapel platziert, als ob der Anruf an eine Funktion erfolgt, die drei Parameter erwartet. Da sich Ihre Funktion jedoch nur um einen Parameter kümmert, sieht sie nur ganz oben auf dem Stapel aus und berührt nicht die anderen Parameter.

Die Tatsache, dass dieser Anruf funktioniert hat, ist reine Glück, basierend auf den beiden Fakten:

  • Der Typ des ersten Parameters ist für die Funktion und den Gusszeiger gleich. Wenn Sie die Funktion ändern, um einen Zeiger in die Zeichenfolge zu nehmen und diese Zeichenfolge zu drucken, erhalten Sie einen schönen Absturz, da der Code versucht, Zeiger auf den Speicher 2 zu adressieren 2.
  • Die standardmäßige Anrufkonvention besteht darin, dass der Anrufer den Stapel aufräumt. Wenn Sie die Anrufkonvention ändern, so dass der Callee den Stapel aufräumt, werden Sie den Anrufer am Ende auf den Stapel drücken und dann die Callee -Aufräumarbeiten (oder eher versuchen) einen Parameter aufzuräumen. Dies würde wahrscheinlich zu Stapelkorruption führen.

Der Compiler kann Sie aus einem einfachen Grund nicht vor potenziellen Problemen wie diesen warnen - im allgemeinen Fall kennt er den Wert des Zeigers zur Kompilierung nicht, sodass er nicht bewerten kann, worauf er weist. Stellen Sie sich vor, der Funktionszeiger zeigt auf eine Methode in einer virtuellen Klassentabelle, die zur Laufzeit erstellt wurde? Sie sagen also dem Compiler, dass es sich um einen Zeiger auf eine Funktion mit drei Parametern handelt, der Compiler wird Ihnen glauben.

Andere Tipps

Wenn Sie ein Auto nehmen und es als Hammer werfen, nimmt Sie der Compiler Sie mit dem Wort, dass das Auto ein Hammer ist, das das Auto jedoch nicht in einen Hammer verwandelt. Der Compiler kann das Auto zum Fahren eines Nagels erfolgreich verwenden, aber das ist das Glück des implementierungsabhängigen Glücks. Es ist immer noch unklug.

  1. Ja, es ist ein undefiniertes Verhalten - alles könnte passieren, auch wenn es "funktioniert".

  2. Die Besetzung verhindert, dass der Compiler eine Warnung herausgibt. Außerdem sind Compiler nicht erforderlich, mögliche Ursachen undefiniertes Verhalten zu diagnostizieren. Der Grund dafür ist, dass es entweder unmöglich ist, dies zu tun oder dass dies zu schwierig und/oder zu viel Aufwand führen würde.

Die schlimmste Straftat Ihrer Besetzung ist es, einen Datenzeiger auf einen Funktionszeiger zu werfen. Es ist schlechter als die Signaturänderung, da es keine Garantie gibt, dass die Größen der Funktionszeiger und der Datenzeiger gleich sind. Und im Gegensatz zu viel theoretisch Undefinierte Verhaltensweisen können in freier Wildbahn (nicht nur auf eingebetteten Systemen) in freier Wildbahn auftreten.

Sie können auf eingebettete Plattformen leicht auf unterschiedliche Größenzeiger stoßen. Es gibt sogar Prozessoren, bei denen Datenzeiger und Funktionszeiger unterschiedliche Dinge ansprechen (zum einen RAM, ROM für den anderen), die sogenannte Harvard-Architektur. Auf x86 im realen Modus können Sie 16 Bit und 32 Bit gemischt haben. WATCOM-C hatte einen speziellen Modus für DOS Extender, in dem Datenzeiger 48 Bit breit waren. Insbesondere bei C sollte man wissen, dass nicht alles POSIX ist, da C möglicherweise die einzige Sprache für exotische Hardware ist.

Einige Compiler ermöglichen gemischte Speichermodelle, bei denen der Code innerhalb von 32 Bitgröße garantiert wird, und die Daten sind mit 64 -Bit -Zeigern oder dem Gegenteil adressierbar.

Bearbeiten: Schlussfolgerung, werfen Sie niemals einen Datenzeiger auf einen Funktionszeiger.

Das Verhalten wird durch die Anrufkonvention definiert. Wenn Sie eine Anrufkonvention verwenden, bei der der Anrufer den Stapel drückt und gepackt, würde dies in diesem Fall gut funktionieren, da dies nur ein paar zusätzliche Bytes auf dem Stapel während des Anrufs gibt. Ich habe momentan nicht GCC praktisch, aber mit dem Microsoft -Compiler dieser Code:

int ( __cdecl * fptr)(int,int,int) = (int (__cdecl * ) (int,int,int)) (ptr);

Die folgende Baugruppe wird für den Anruf erstellt:

push        8
push        4
push        2
call        dword ptr [ebp-4]
add         esp,0Ch

Beachten Sie die 12 Bytes (0Ch), die nach dem Anruf zum Stapel hinzugefügt wurden. Danach ist der Stapel in Ordnung (vorausgesetzt, der Callee ist in diesem Fall __CDECL, sodass er nicht auch versucht, den Stapel zu beseitigen). Aber mit dem folgenden Code:

int ( __stdcall * fptr)(int,int,int) = (int (__stdcall * ) (int,int,int)) (ptr);

Das add esp,0Ch wird in der Baugruppe nicht erzeugt. Wenn der Callee in diesem Fall __CDECl ist, würde der Stapel beschädigt.

  1. Zugegeben, ich weiß es nicht genau, aber Sie möchten das Verhalten definitiv nicht nutzen, wenn es Glück ist oder Wenn es Compiler spezifisch ist.

  2. Es verdient keine Warnung, weil die Besetzung explizit ist. Durch das Casting informieren Sie den Compiler, dass Sie besser wissen. Insbesondere wirken Sie a void*, und als solches sagen Sie "Nehmen Sie die von diesem Zeiger dargestellte Adresse und machen Sie ihn mit diesem anderen Zeiger" - die Besetzung informiert einfach den Compiler, dass Sie sicher sind, was sich an der Zieladresse befindet, tatsächlich ist. das Gleiche. Obwohl wir hier wissen, dass das falsch ist.

Ich sollte meine Erinnerung an das binäre Layout der C -Calling Convention irgendwann erfrischen, aber ich bin mir ziemlich sicher, dass dies das passiert:

  • 1: Es ist kein reines Glück. Die C-Calling Convention ist gut definiert, und zusätzliche Daten auf dem Stapel sind kein Faktor für die Anrufe, obwohl sie möglicherweise von den Callee überschrieben wird, da die Callee nichts davon weiß.
  • 2: Eine "harte" Besetzung mit Klammern sagt dem Compiler, dass Sie wissen, was Sie tun. Da sich alle erforderlichen Daten in einer Kompilierungseinheit befinden, könnte der Compiler klug genug sein, um herauszufinden, dass dies eindeutig illegal ist, aber Cs Designer (en) hat sich nicht darauf konzentriert, eine überprüfbare Falschheit der Ecke zu fangen. Einfach gesagt, der Compiler vertraut darauf, dass Sie wissen, was Sie tun (vielleicht unklug bei vielen C/C ++ - Programmierern!)

Um Ihre Fragen zu beantworten:

  1. Reines Glück - Sie können den Stapel leicht miteinander treten und den Rückgaberoter zum nächsten ausführenden Code überschreiben. Da Sie den Funktionszeiger mit 3 Parametern angegeben und den Funktionszeiger aufgerufen haben, wurden die verbleibenden zwei Parameter „verworfen“ und daher ist das Verhalten undefiniert. Stellen Sie sich vor, dieser 2. oder 3. Parameter enthielt eine binäre Anweisung und steckte den Anrufprozedurstapel aus ....

  2. Es gibt keine Warnung, da Sie a verwendet haben void * Zeiger und wirkt es. Das ist ein ziemlich legitimer Code in den Augen des Compilers, auch wenn Sie explizit angegeben haben -Wall Schalter. Der Compiler geht davon aus, dass Sie wissen, was Sie tun! Das ist das Geheimnis.

Hoffe das hilft, am besten, Tom.

Lizenziert unter: CC-BY-SA mit Zuschreibung
Nicht verbunden mit StackOverflow
scroll top