5

Betrachten Sie das folgende Programm:Ruft dieser Missbrauch von Funktionsdeklarationen undefiniertes Verhalten auf?

int main() 
{ 
    int exit(); 
    ((void(*)())exit)(0); 
} 

Wie Sie sehen können, exit mit dem falschen Rückgabetyp deklariert, aber nie mit der falschen Funktionstyp genannt. Ist das Verhalten dieses Programms klar definiert?

+0

gcc 4.7.2 denkt dies: exit.c: 6: 22: Hinweis: Wenn dieser Code erreicht ist, wird das Programm abgebrochen. Also muss ich davon ausgehen, dass die gcc-Entwickler dies als undefiniertes Verhalten ansehen. Da Sie in der Vergangenheit eine negative Sicht auf gcc geäußert haben, können Sie die Meinung von gcc ignorieren. Es gibt weitere Fehler bei der Definition von exit(). –

+0

Eigentlich sagt es in meinem Fall sogar: 'Fehler: zu viele Argumente zu funktionieren', was natürlich stimmt. –

+1

@Haroogan: C und C++ sind verschiedene Sprachen. –

Antwort

4

MSVC hat kein Problem mit diesem Programm, aber gcc tut (mindestens gcc 4.6.1). Es gibt die folgenden Warnungen aus:

test.c: In function 'main': 
test.c:3:9: warning: conflicting types for built-in function 'exit' [enabled by default] 
test.c:4:22: warning: function called through a non-compatible type [enabled by default] 
test.c:4:22: note: if this code is reached, the program will abort 

Und wie versprochen, stürzt es ab, wenn es ausgeführt wird.Der Absturz ist kein Zufall bei einer inkorrekten Aufrufkonvention oder etwas - gcc erzeugt tatsächlich eine undefinierte Anweisung mit dem Opcode 0x0b0f, um explizit einen Absturz zu erzwingen (gdb zerlegt es als ud2 - Ich habe nicht nachgeschaut, was das CPU - Handbuch dazu sagen könnte opcode):

main: 
.LFB0: 
    .cfi_startproc 
    push ebp 
    .cfi_def_cfa_offset 8 
    .cfi_offset 5, -8 
    mov ebp, esp 
    .cfi_def_cfa_register 5 
     .value 0x0b0f 
    .cfi_endproc 

ich bin nur ungern, dass gcc ist dabei die falsch zu sagen, denn ich bin sicher, die Leute, die diese Compiler schreiben viel mehr über C wissen als ich. Aber hier lese ich, was der Standard dazu sagt; Ich bin sicher, dass jemand darauf hinweisen, was mir fehlt:

C99 sagt über Umwandlungen von Funktionszeigern (6.3.2.3/8 „Pointers“):

A pointer to a function of one type may be converted to a pointer to a function of another type and back again; the result shall compare equal to the original pointer. If a converted pointer is used to call a function whose type is not compatible with the pointed-to type, the behavior is undefined.

In einem Ausdruck, der Kennung 10 wertet einen Funktionszeiger aus.

Der Unterausdruck ((void(*)())exit) konvertiert den Funktionszeiger, den auswertet, in einen Funktionszeiger vom Typ void (*)(). Dann wird ein Funktionsaufruf wird durch diesen Zeiger gemacht, vorbei an der int Argument 0.

Die Standardbibliothek enthält eine Funktion exit benannt, die den folgenden Prototyp hat:

void exit(int status); 

Die Norm sagt auch (7.1.4/2 „Verwendung von Bibliotheksfunktionen“):

Provided that a library function can be declared without reference to any type defined in a header, it is also permissible to declare the function and use it without including its associated header.

Ihr Programm enthält nicht den Header, den Prototyp enthält, sondern den Funktionsaufruf durch die umgewandelten Zeiger gemacht verwendet die ‚Erklärung‘ in der Besetzung zur Verfügung gestellt. Die Deklaration in der Umwandlung ist keine Prototyp-Deklaration, daher müssen wir feststellen, ob der Funktionstyp , wie von der Standardbibliothek definiert, und der Funktionstyp des konvertierten Funktionszeigers in Ihrem Programm kompatibel sind. Die Norm sagt (6.7.5.3/15 „Funktionsdeklaratoren (einschließlich Prototypen)“)

For two function types to be compatible, both shall specify compatible return types. ... If one type has a parameter type list and the other type is specified by a function declarator that is not part of a function definition and that contains an empty identifier list, the parameter list shall not have an ellipsis terminator and the type of each parameter shall be compatible with the type that results from the application of the default argument promotions

Es scheint mir, dass die konvertierte Funktionszeiger eine kompatible Funktionstyp hat - der Rückgabetyp ist das gleiche (void) und Der Typ des einzelnen Parameters ist int nach den Standardargumentenpromotions. So scheint es mir, dass es hier kein undefiniertes Verhalten gibt.


Update: Nach ein wenig mehr Gedanken darüber, könnte es sinnvoll sein 7.1.4/2 zu interpretieren, dass ein ‚selbsterklärte‘ Bibliothek Funktionsname korrekt (wenn auch nicht unbedingt deklariert werden muß mit einem Prototyp, aber mit einem korrekten Rückgabetyp). Zumal der Standard auch besagt, dass "alle Bezeichner mit externer Verknüpfung in einem der folgenden Unterklauseln ... immer für die Verwendung als Bezeichner mit externer Verknüpfung reserviert sind" (7.1.3).

Also ich denke, ein vernünftiges Argument kann gemacht werden, dass das Programm undefiniert Verhalten hat.

+0

+1 für Ihr Update, ich denke, das ist die Antwort. 7.1.3 enthält auch explizit genug "Wenn das Programm einen Bezeichner in einem Kontext deklariert oder definiert, in dem es reserviert ist (anders als gemäß 7.1.4 zulässig) oder einen reservierten Bezeichner als Makronamen definiert, ist das Verhalten nicht definiert . " Wenn also kein Argument dafür gemacht werden kann, dass 7.1.4 dies zulässt, ist der Code ungültig. – hvd

+0

ud2 ist ein 'undefinierter Befehl' http://asm.inightmare.org/opcodelst/index.php?op=UD2, der einen ungültigen Opcode-Fehler auslöst. –

+0

OK, es scheint, als ob meine Wahl, 'exit' zu verwenden, um Einfachheit, die damals sinnvoll war, die Ursache für übermäßige Probleme sein könnte. Was ist, wenn wir das Problem ändern (sollte ich eine neue Frage öffnen oder einfach an das Ende dieser Zeile hinzufügen), wo die Funktion "void foo (int x)" in einer separaten Übersetzungseinheit definiert ist, so gibt es keine Frage zu Standardbibliotheksfunktionen Gib einfach Mismatch ein. –

0

Ich würde sagen, dass dies in beiden Fällen möglicherweise schlecht definiert ist.

Mein Argument wäre, dass der resultierende generierte Code von zwei Anrufen ((void(*)())exit)(0); und exit(); gut anders sein könnte. Wenn also int exit() nur deklariert wird (der, an dem Sie interessiert sind), könnte das primäre Problem sein, dass die binären Layouts von int exit(void) und void exit(int) nicht notwendigerweise identisch sind.

Wenn int exit() auch definiert wäre, würde höchstwahrscheinlich aus dem folgenden Grund abstürzen. Es gibt eine Reihe von Aufrufkonventionen, und das Problem kann zum Beispiel auftreten, wenn der Platz für den Rückgabewert auf dem Stack reserviert ist. Wenn also ((void(*)())exit)(0); verwendet wird, dann würde natürlich kein Platz auf dem Stapel vom Compiler reserviert werden (speziell für den Rückgabewert), während die Funktion selbst (int exit()) nichts davon weiß und daher versuchen wird, den int zu drücken Rückgabewert in die erwartete Speicherzelle in der Laufzeit (die, die hätte reserviert werden müssen, war aber nicht), die definitiv als Absturz enden würde.

+0

Vielleicht die Frage war nicht klar. Der eigentliche Typ von 'exit' ist' void exit (int) '. Dies ist unveränderlich. Die Frage enthält eine Erklärung von 'exit' mit dem falschen Typ, aber sie nennt sie nicht mit dem falschen Typ. Stattdessen wird die Adresse zurück an den richtigen Funktionszeigertyp übergeben, um sie aufzurufen. –

+0

Ja, das verstehe ich, aber wollten Sie versuchen, die Tatsache zu missbrauchen, dass das binäre Layout zwischen 'int exit (void)' und 'void exit (int)' oder 'int exit (void)' dasselbe sein könnte nur eine zufällige Auswahl und Sie sind zusätzlich interessiert, was passieren kann, wenn Sie zum Beispiel "void exit (void)" deklarieren? Mit anderen Worten, es ist Ihnen egal, wie inkompatibel die "falsche" Erklärung ist, oder? –

0

Ich denke, das hat Verhalten definiert. Die relevanten Teile der Norm sind über die Parameter (p6, etwas langatmig) und über die Art:

If the function is defined with a type that is not compatible with the type (of the expression) pointed to by the expression that denotes the called function, the behavior is undefined.

All dies immer spricht über zwei verschiedene Einheiten, eine der Funktionsausdruck, der ausgewertet wird und die zweite die Funktion Das wird .. genannt. Der Bezeichner, der den Ausdruck erhöht (Ihr fälschlicherweise 10), tritt niemals in das Spiel ein. Also in Ihrem Fall wird die Funktion korrekt aufgerufen und es gibt kein UB.

Im Allgemeinen würde es viel Code kaputtmachen, wenn das UB wäre, nämlich für Code, der Funktionszeiger in Arrays speichert, und dann ruft die Funktionen durch Umwandlungen als Ihre auf, abhängig von etwas zusätzlichem Wissen über den Kontext.

Nur ein Nitpick, ich denke, dass Sie dem Compiler den Gefallen tun sollten und einen Prototyp in einem solchen Fall geben sollten. Das Argument Konvertierungsformular 0 ist trivial, und in diesem Fall korrekt, aber wirklich sehr fehleranfällig.

((void(*)(int))exit)(0); 

wäre besser.

Update: Im Hinblick auf Michaels Antwort stimme ich zu, dass das Obige wahr gewesen wäre, wenn Sie all das mit einer Nicht-Bibliotheksfunktion getan hätten. Aber 7.1.3 p1 verbietet eindeutig die ID exit, die sich von dem in der Kopfzeile deklarierten Prototyp unterscheidet, und dann p. 2 Zustände

If the program declares or defines an identifier in a context in which it is reserved (other than as allowed by 7.1.4), or defines a reserved identifier as a macro name, the behavior is undefined.

+0

Das ist sehr zerbrechlich. Sobald er eine andere TU hat, die stdlib.h enthält oder den Exit korrekt deklariert, wird das Verhalten durch 6.2.7p2 undefiniert. –

+0

@ JohannesSchaub-littb, ich habe wohl die Tatsache unterschätzt, dass dies Standard-Bibliotheksfunktion ist. Siehe mein Update. –

+0

@JensGustedt: Ja, die Tatsache, dass es eine Standardbibliotheksfunktion ist, macht es problematisch. Ich war eher an dem Fall interessiert, in dem das nicht der Fall ist, aber ich habe die Frage mit "exit" geschrieben, um zu vermeiden, dass mehrere Übersetzungseinheiten benötigt werden. –