C-Funktionszeiger-Casting in Void-Zeiger

Lesezeit: 8 Minuten

Benutzer-Avatar
Manoj

Ich versuche, das folgende Programm auszuführen, bekomme aber einige seltsame Fehler:

Datei 1.c:

typedef unsigned long (*FN_GET_VAL)(void);

FN_GET_VAL gfnPtr;

void setCallback(const void *fnPointer)
{
    gfnPtr = *((FN_GET_VAL*) (&fnPointer));
}

Datei 2.c:

extern FN_GET_VAL gfnPtr;

unsigned long myfunc(void)
{
    return 0;
}

main()
{
   setCallback((void*)myfunc);
   gfnPtr(); /* Crashing as value was not properly 
                assigned in setCallback function */
}

Hier stürzt gfnPtr() auf 64-Bit-Suse-Linux ab, wenn es mit gcc kompiliert wird. Aber es ruft erfolgreich gfnPtr() VC6 und SunOS auf.

Aber wenn ich die Funktion wie unten angegeben ändere, funktioniert es erfolgreich.

void setCallback(const void *fnPointer)
{
    int i; // put any statement here
    gfnPtr = *((FN_GET_VAL*) (&fnPointer));
}

Bitte helfen Sie bei der Problemursache. Vielen Dank.

Benutzer-Avatar
Pascal Cuoq

Der C-Standard erlaubt es nicht, auf Funktionszeiger umzuwandeln void*. Sie können nur in einen anderen Funktionszeigertyp umwandeln. In dem C11-Standard, 6.3.2.3 §8:

Ein Zeiger auf eine Funktion eines Typs kann in einen Zeiger auf eine Funktion eines anderen Typs umgewandelt werden und wieder zurück

Wichtig ist, dass Sie auf den ursprünglichen Typ zurücksetzen müssen, bevor Sie den Zeiger verwenden, um die Funktion aufzurufen (technisch gesehen auf einen kompatiblen Typ. Definition von “kompatibel” bei 6.2.7).

Beachten Sie, dass der POSIX-Standard, dem viele (aber nicht alle) C-Compiler aufgrund des Kontexts, in dem sie verwendet werden, ebenfalls folgen müssen, vorschreibt, dass ein Funktionszeiger konvertiert werden kann void* und zurück. Dies ist für einige Systemfunktionen erforderlich (z dlsym).

  • Vielen Dank für Ihre Antworten. Ich würde darauf achten, Daten- und Funktionszeiger nicht zu mischen. Aber in diesem Fall kann ich nicht herausfinden, warum dies geschieht. Wenn ich mit -m32 (32 Bit) kompiliere und ausführe, funktioniert es einwandfrei, aber wenn ich mit -m64 (64 Bit) kompiliere, gibt es das Problem. Auch wenn ich eine einzelne Aussage wie hinzufüge int ich; obige Aufgabe dann funktioniert es gut. Ich bin mir nicht sicher, warum der Stapel möglicherweise beschädigt ist, aber wie man das überprüft.

    – Manoj

    7. April 2011 um 11:28 Uhr

  • Ich stimme zu, dass der Standard es nicht erlaubt, Funktionszeiger auf zu werfen void*, nach 6.3.2.3; aber ich frage mich, warum 6.3.2.3p3 “auf einen Zeiger auf ein Objekt oder eine Funktion” sagt, da dies nicht vorkommen darf. Übrigens, J.5.7 sagt: “Ein Zeiger auf ein Objekt oder auf void kann in einen Zeiger auf eine Funktion umgewandelt werden, wodurch Daten als Funktion aufgerufen werden können” und umgekehrt, aber das sind allgemeine J.5-Erweiterungen, daher nicht portierbar.

    – vinc17

    22. November 2018 um 8:31 Uhr

  • @Manoj Der Grund könnte eine Optimierung sein (auch beim Kompilieren ohne -O). Vielleicht denkt der Compiler irgendwann an diese Funktion myfunc wird nicht verwendet oder so.

    – vinc17

    22. November 2018 um 8:37 Uhr

  • Es hängt davon ab, welchen Standard Sie verwenden – der POSIX-Standard schreibt vor, dass der C-Compiler das sichere Casting von Funktionszeigern nach void * und wieder zurück unterstützt.

    – Chris Dodd

    28. März 2019 um 19:57 Uhr

  • @ChrisDodd Das ist wahr. Die Frage ist mit C gekennzeichnet, nicht mit POSIX, aber ich werde eine Notiz hinzufügen.

    – Pascal Cuoq

    28. März 2019 um 20:01 Uhr

Der Standard erlaubt leider kein Casting zwischen Datenzeigern und Funktionszeigern (weil das auf einigen wirklich obskuren Plattformen keinen Sinn machen könnte), obwohl POSIX und andere solche Casts erfordern. Eine Problemumgehung besteht darin, den Zeiger nicht umzuwandeln, sondern einen Zeiger auf den Zeiger umzuwandeln (dies ist für den Compiler in Ordnung und wird die Arbeit auf allen normalen Plattformen erledigen).

typedef void (*FPtr)(void); // Hide the ugliness
FPtr f = someFunc;          // Function pointer to convert
void* ptr = *(void**)(&f);  // Data pointer
FPtr f2 = *(FPtr*)(&ptr);   // Function pointer restored

  • „weil das auf einigen wirklich obskuren Plattformen möglicherweise keinen Sinn macht“ Wirklich obskure Plattformen wie Linux, Mac OS X, iOS, Microsoft Windows und Android, die über eine Datenausführungsverhinderung verfügen. Mit Data Execution Prevention sind Code und Daten NICHT austauschbar. Hinweis: POSIX erfordert die Umwandlung in einen void-Zeiger, aber es erfordert nicht, dass ein solcher resultierender Zeiger verwendbar ist, außer zum Zurückwandeln in einen Funktionszeiger.

    – Jonathan Baldwin

    8. September 2013 um 2:45 Uhr

  • @JonathanBaldwin Was Sie sagen, macht nur auf Plattformen Sinn, auf denen der Codezeiger eine andere Größe als der Datenzeiger hat. Ein Zeiger ist nur eine Adresse und befindet sich als solche niemals im ausführbaren Bereich. Die Daten, auf die die Adresse zeigt, können sich jedoch in einem ausführbaren Bereich befinden. Daher können Sie den Speicher, auf den der Zeiger zeigt, möglicherweise nicht lesen. Aber Sie können es mit dieser Methode sicherlich von Funktionszeiger auf void * und von void * auf Funktionszeiger umwandeln. Wenn die Plattform keine andere Größe für Funktionszeiger als für Datenzeiger hat, wird es funktionieren.

    – Rxantos

    20. Februar 2014 um 8:09 Uhr

  • @rxantos Sicher, eine Umwandlung von Daten in einen Funktionszeiger wäre auf einem ia32-System mit DXP trivial zu implementieren, aber es macht keinen Sinn, da ein solcher Zeiger unbrauchbar wird, ohne ihn in einen Datenzeiger umzuwandeln oder einen Systemaufruf zu verwenden Löcher in DXP lochen (d. h. den Bereich ausführbar machen.) Soweit es C betrifft, lohnt sich die Unterstützung nicht, die Plattformen, die Sie erwähnt haben, inkonsistent zu unterstützen, einschließlich 16-Bit-DOS und Windows (erinnern Sie sich an nahe und ferne Zeiger?) Wenn eine Plattform wie POSIX dies direkt zulassen möchte, könnten sie schließlich eine Erweiterung daraus machen.

    – Jonathan Baldwin

    21. Februar 2014 um 8:51 Uhr


Ich habe drei Faustregeln, wenn es um Datenzeiger und Codezeiger geht:

  • Tun nicht Mischen Sie Datenzeiger und Codezeiger
  • Unterlassen Sie mischen Datenzeiger und Codezeiger
  • Unterlassen Sie je Mischen Sie Datenzeiger und Codezeiger!

In der folgenden Funktion:

void setCallback(const void *fnPointer)
{
    gfnPtr = *((FN_GET_VAL*) (&fnPointer));
}

Du hast ein Datenzeiger dass Sie Fall zu einem Funktionszeiger. (Ganz zu schweigen davon, dass Sie dies tun, indem Sie zuerst die Adresse des Zeigers selbst nehmen, ihn in einen Zeiger auf einen Zeiger umwandeln, bevor Sie ihn dereferenzieren).

Versuchen Sie es umzuschreiben als:

void setCallback(FN_GET_VAL fnPointer)
{
     gfnPtr = fnPointer;
}

Außerdem können (oder sollten) Sie die Besetzung fallen lassen, wenn Sie den Zeiger setzen:

main()
{
   setCallback(myfunc);
   gfnPtr();
}

Als zusätzlichen Bonus können Sie jetzt die normalen Typprüfungen verwenden, die vom Compiler durchgeführt werden.

  • Vielen Dank für Ihre Antworten. Ich würde darauf achten, Daten- und Funktionszeiger nicht zu mischen. Aber in diesem Fall kann ich nicht herausfinden, warum dies geschieht. Wenn ich mit -m32 (32 Bit) kompiliere und ausführe, funktioniert es einwandfrei, aber wenn ich mit -m64 (64 Bit) kompiliere, gibt es das Problem. Auch wenn ich eine einzelne Aussage wie hinzufüge int ich; obige Aufgabe dann funktioniert es gut. Ich bin mir nicht sicher, warum der Stapel möglicherweise beschädigt ist, aber wie man das überprüft.

    – Manoj

    7. April 2011 um 11:25 Uhr

  • +1 für die Analyse, dass das Nehmen der Adresse eines Funktionszeigers nicht dasselbe ist wie das Anwenden & zu einem Funktionsnamen (was ein No-Op ist).

    – Jonathan Leffler

    22. Mai 2013 um 4:43 Uhr

Ich werde eine mögliche vorschlagen teilweise Erläuterung.

@Manoj Wenn Sie die von beiden Compilern generierte Assemblyliste für SetCallback untersuchen (oder bereitstellen können), können wir eine endgültige Antwort erhalten.

Erstens stimmen die Aussagen von Pascal Couq, und Lindydancer zeigt, wie man den Callback richtig setzt. Meine Antwort ist nur ein Versuch, das eigentliche Problem zu erklären.

Ich denke, das Problem rührt von der Tatsache her, dass Linux und die andere Plattform unterschiedliche 64-Bit-Modelle verwenden (siehe 64-Bit-Modelle auf Wikipedia). Beachten Sie, dass Linux LP64 verwendet (int ist 32 Bit). Wir brauchen mehr Details auf der anderen Plattform. Wenn es SPARC64 ist, verwendet es ILP64 (int ist 64 Bit).

Wie ich Sie verstehe, wurde das Problem nur unter Linux beobachtet und verschwand, wenn Sie eine lokale Variable int einführten. Haben Sie dies mit deaktivierten oder aktivierten Optimierungen versucht? Höchstwahrscheinlich hätte dieser Hack keine positiven Auswirkungen mit eingeschalteten Optimierungen.

Bei beiden 64-Bit-Modellen sollten Zeiger 64-Bit sein, unabhängig davon, ob sie auf Code oder Daten zeigen. Es ist jedoch möglich, dass dies nicht der Fall wäre (z. B. segmentierte Speichermodelle); daher die Ermahnungen von Pascal und Lindydancer.

Wenn die Zeiger die gleiche Größe haben, bleibt ein mögliches Stapelausrichtungsproblem. Die Einführung eines lokalen int (das unter Linux 32 Bit ist) könnte die Ausrichtung ändern. Dies würde sich nur auswirken, wenn void*- und Funktionszeiger unterschiedliche Ausrichtungsanforderungen haben. Ein zweifelhaftes Szenario.

Dennoch sind die unterschiedlichen 64-Bit-Speichermodelle höchstwahrscheinlich die Ursache für das, was Sie beobachtet haben. Gerne können Sie uns die Bestückungslisten zur Analyse zur Verfügung stellen.

Benutzer-Avatar
Nergal

Im Gegensatz zu dem, was andere sagen, ja, Sie können einen haben void* Zeiger als Funktionszeiger, aber die Semantik ist sehr schwierig, ihn damit zu verwenden.

Wie Sie sehen können, müssen Sie es nicht als umwandeln void*, weisen Sie es einfach wie gewohnt zu. Ich habe Ihren Code so ausgeführt und bearbeitet, damit er funktioniert.

Datei1.c:

typedef unsigned long (*FN_GET_VAL)(void);

extern FN_GET_VAL gfnPtr;

void setCallback(const void *fnPointer)
{
    gfnPtr = ((FN_GET_VAL) fnPointer);
}

Datei2.c:

int main(void)
{
    setCallback(myfunc);
    (( unsigned long(*)(void) )gfnPtr)();
}

  • Eine Zuweisung beinhaltet eine Konvertierung, genau wie bei einer Umwandlung (die per Definition eine explizite Konvertierung ist), daher ist dies ein undefiniertes Verhalten. Dies kann mit einigen C-Implementierungen funktionieren, entweder als Erweiterung oder nur zufällig (z. B. wegen der gleichen Darstellung und keiner Optimierungen, die den Code beschädigen), aber es gibt keine Garantie durch den C-Standard.

    – vinc17

    22. November 2018 um 8:50 Uhr

  • Eine Zuweisung beinhaltet eine Konvertierung, genau wie bei einer Umwandlung (die per Definition eine explizite Konvertierung ist), daher ist dies ein undefiniertes Verhalten. Dies kann mit einigen C-Implementierungen funktionieren, entweder als Erweiterung oder nur zufällig (z. B. wegen der gleichen Darstellung und keiner Optimierungen, die den Code beschädigen), aber es gibt keine Garantie durch den C-Standard.

    – vinc17

    22. November 2018 um 8:50 Uhr

1365750cookie-checkC-Funktionszeiger-Casting in Void-Zeiger

This website is using cookies to improve the user-friendliness. You agree by using the website further.

Privacy policy