webentwicklung-frage-antwort-db.com.de

Warum verhält sich dieser Funktionsaufruf sinnvoll, nachdem er über einen typisierten Funktionszeiger aufgerufen wurde?

Ich habe den folgenden Code. Es gibt eine Funktion, die zwei int32 benötigt. Dann nehme ich einen Zeiger darauf und wandle ihn in eine Funktion um, die drei int8 benötigt, und rufe ihn auf. Ich habe einen Laufzeitfehler erwartet, aber das Programm funktioniert einwandfrei. Warum ist das überhaupt möglich?

main.cpp:

#include <iostream>

using namespace std;

void f(int32_t a, int32_t b) {
    cout << a << " " << b << endl;
}

int main() {
    cout << typeid(&f).name() << endl;
    auto g = reinterpret_cast<void(*)(int8_t, int8_t, int8_t)>(&f);
    cout << typeid(g).name() << endl;
    g(10, 20, 30);
    return 0;
}

Ausgabe:

PFviiE
PFvaaaE
10 20

Wie ich sehen kann, erfordert die Signatur der ersten Funktion zwei Ints und die zweite Funktion drei Zeichen. Char ist kleiner als int und ich habe mich gefragt, warum a und b immer noch gleich 10 und 20 sind.

35
Divano

Wie andere bereits betont haben, handelt es sich hierbei um ein undefiniertes Verhalten. Daher sind alle Wetten ungültig, was im Prinzip passieren kann. Unter der Annahme, dass Sie sich auf einem x86-Computer befinden, gibt es eine plausible Erklärung dafür, warum Sie dies sehen.

Unter x86 übergibt der g ++ - Compiler nicht immer Argumente, indem er sie auf den Stapel schiebt. Stattdessen werden die ersten Argumente in Registern gespeichert. Wenn wir die Funktion f zerlegen, beachten Sie, dass die ersten Anweisungen die Argumente aus den Registern in den Stapel verschieben:

    Push    rbp
    mov     rbp, rsp
    sub     rsp, 16
    mov     DWORD PTR [rbp-4], edi  # <--- Here
    mov     DWORD PTR [rbp-8], esi  # <--- Here
    # (many lines skipped)

Beachten Sie auch, wie der Aufruf in main generiert wird. Die Argumente werden in diese Register gestellt:

    mov     rax, QWORD PTR [rbp-8]
    mov     edx, 30      # <--- Here
    mov     esi, 20      # <--- Here
    mov     edi, 10      # <--- Here
    call    rax

Da das gesamte Register zum Speichern der Argumente verwendet wird, ist die Größe der Argumente hier nicht relevant.

Da diese Argumente über Register übergeben werden, besteht keine Bedenken, dass die Größe des Stapels falsch geändert wird. Einige Aufrufkonventionen (cdecl) überlassen es dem Anrufer, eine Bereinigung durchzuführen, während andere (stdcall) den Angerufenen bitten, eine Bereinigung durchzuführen. Beides spielt hier jedoch keine Rolle, da der Stapel nicht berührt wird.

36
templatetypedef

Wie andere bereits betont haben, ist es wahrscheinlich ndefiniertes Verhalten, aber C-Programmierer der alten Schule wissen, dass solche Dinge funktionieren.

Auch weil ich spüren kann, wie die Sprachanwälte ihre Prozessdokumente und Gerichtsanträge für das, was ich sagen werde, verfassen, werde ich einen Zauber von undefined behavior discussion. Es wird mit den Worten undefined behavior dreimal beim zusammenklopfen meiner schuhe. Und das lässt die Sprachanwälte verschwinden, damit ich erklären kann, warum seltsame Dinge einfach funktionieren, ohne verklagt zu werden.

Zurück zu meiner Antwort:

Alles, was ich unten diskutiere, ist compilerspezifisches Verhalten. Alle meine Simulationen werden mit Visual Studio als 32-Bit-x86-Code kompiliert. Ich vermute, dass es mit gcc und g ++ auf einer ähnlichen 32-Bit-Architektur genauso funktionieren wird.

Hier ist, warum Ihr Code zufällig funktioniert und einige Einschränkungen.

  1. Wenn Funktionsaufrufargumente auf den Stapel verschoben werden, werden sie in umgekehrter Reihenfolge verschoben. Wenn f normal aufgerufen wird, generiert der Compiler Code, um das Argument b vor dem Argument a auf den Stapel zu verschieben. Dies erleichtert verschiedene Argumentfunktionen wie printf. Wenn Ihre Funktion f auf a und b zugreift, greift sie nur auf Argumente oben im Stapel zu. Beim Aufrufen über g wurde ein zusätzliches Argument an den Stapel (30) gesendet, das jedoch zuerst verschoben wurde. Als nächstes wurde 20 gedrückt, gefolgt von 10, die sich oben auf dem Stapel befindet. f betrachtet nur die beiden obersten Argumente auf dem Stapel.

  2. IIRC, zumindest in klassischem ANSI C, Zeichen und Shorts, werden immer zu int befördert, bevor sie auf den Stapel gelegt werden. Wenn Sie es mit g aufrufen, werden die Literale 10 und 20 daher als Ints in voller Größe anstelle von 8-Bit-Ints auf dem Stapel abgelegt. Sobald Sie jedoch f neu definieren, um 64-Bit-Longs anstelle von 32-Bit-Ints zu verwenden, ändert sich die Ausgabe Ihres Programms.

    void  f(int64_t a, int64_t b) {
        cout << a << " " << b << endl;
    }

Dies führt dazu, dass dies von Ihrem Main (mit meinem Compiler) ausgegeben wird.

85899345930 48435561672736798

Und wenn Sie in hex konvertieren:

140000000a effaf00000001e

14 ist 20 und 0A ist 10. Und ich vermute, dass 1e ist dein 30 auf den Stapel geschoben werden. Daher wurden die Argumente beim Aufrufen über g auf den Stapel verschoben, aber auf eine compilerspezifische Weise aufgemischt. (ndefiniertes Verhalten wieder, aber Sie können sehen, dass die Argumente gepusht wurden).

  1. Wenn Sie eine Funktion aufrufen, ist das übliche Verhalten, dass der aufrufende Code den Stapelzeiger bei der Rückkehr von einer aufgerufenen Funktion repariert. Dies ist wiederum aus Gründen variabler Funktionen und anderer älterer Gründe für die Kompatibilität mit K & R C. printf hat keine Ahnung, wie viele Argumente Sie tatsächlich an K & R übergeben haben, und es ist darauf angewiesen, dass der Aufrufer den Stapel repariert, wenn er ausgeführt wird kehrt zurück. Wenn Sie also über g aufgerufen haben, hat der Compiler Code generiert, um 3 Ganzzahlen in den Stapel zu verschieben, die Funktion aufzurufen und dann Code, um dieselben Werte zu entfernen. In dem Moment ändern Sie Ihre Compiler-Option, damit der Angerufene den Stapel bereinigt (ala __stdcall in Visual Studio):
    void  __stdcall f(int32_t a, int32_t b) {
        cout << a << " " << b << endl;
    }

Jetzt befinden Sie sich eindeutig in einem undefinierten Verhaltensgebiet. Durch Aufrufen von g wurden drei int-Argumente auf den Stapel verschoben, aber der Compiler hat nur Code für f generiert, um zwei int-Argumente vom Stapel zu entfernen, wenn er zurückgegeben wird. Der Stapelzeiger ist bei der Rückkehr beschädigt.

9
selbie

Wie andere bereits betont haben, handelt es sich um ein völlig undefiniertes Verhalten, und was Sie erhalten, hängt vom Compiler ab. Dies funktioniert nur, wenn Sie eine bestimmte Aufrufkonvention haben, die nicht den Stapel verwendet, sondern Register zum Übergeben der Parameter.

Ich habe Godbolt verwendet, um zu sehen, wie die Versammlung generiert wurde, die Sie vollständig einchecken können hier

Der entsprechende Funktionsaufruf ist hier:

mov     edi, 10
mov     esi, 20
mov     edx, 30
call    f(int, int) #clang totally knows you're calling f by the way

Es werden keine Parameter auf den Stapel übertragen, sondern einfach in Register eingetragen. Am interessantesten ist, dass der Befehl mov nicht nur die unteren 8 Bits des Registers ändert, sondern alle, da es sich um eine 32-Bit-Bewegung handelt. Dies bedeutet auch, dass Sie unabhängig davon, was zuvor im Register war, immer den richtigen Wert erhalten, wenn Sie 32 Bit zurücklesen, wie dies bei f der Fall ist.

Wenn Sie sich fragen, warum die 32-Bit-Verschiebung erfolgt, stellt sich heraus, dass Compiler in einer x86- oder AMD64-Architektur in fast allen Fällen immer entweder 32-Bit-Literalverschiebungen oder 64-Bit-Literalverschiebungen verwenden (genau dann, wenn der Wert zu groß ist für 32 Bit). Das Verschieben eines 8-Bit-Werts setzt die oberen Bits (8-31) des Registers nicht auf Null und kann Probleme verursachen, wenn der Wert am Ende heraufgestuft wird. Die Verwendung eines 32-Bit-Literalbefehls ist einfacher als ein zusätzlicher Befehl, um das Register zuerst auf Null zu setzen.

Eine Sache, an die Sie sich erinnern müssen, ist, dass wirklich versucht wird, f aufzurufen, als ob es 8-Bit-Parameter hätte. Wenn Sie also einen großen Wert eingeben, wird das Literal abgeschnitten. Zum Beispiel, 1000 wird werden -24, als die unteren Bits von 1000 sind E8, welches ist -24 bei Verwendung vorzeichenbehafteter Ganzzahlen. Sie erhalten auch eine Warnung

<source>:13:7: warning: implicit conversion from 'int' to 'signed char' changes value from 1000 to -24 [-Wconstant-conversion]
1
meneldal