2012-04-25 5 views
17

Die folgende Frage wurde in einem College-Programmierwettbewerb gegeben. Wir wurden gebeten, die Ausgabe zu erraten und/oder ihre Funktionsweise zu erklären. Unnötig zu sagen, keiner von uns war erfolgreich.Verständnis eines ungewöhnlichen Arguments für Haupt

main(_){write(read(0,&_,1)&&main());} 

Einige kurze Googeln führte mich zu genau dieser Frage, fragte in codegolf.stackexchange.com:

https://codegolf.stackexchange.com/a/1336/4085

Dort seine was erklärt es tut: Reverse stdin and place on stdout, aber nicht wie.

fand ich auch etwas Hilfe in dieser Frage: Three arguments to main, and other obfuscating tricks aber es erklärt immer noch nicht, wie main(_), &_ und &&main() funktioniert.

Meine Frage ist, wie funktionieren diese Syntaxen? Sind sie etwas, über das ich Bescheid wissen sollte? Sind sie noch relevant?

Ich wäre dankbar für alle Hinweise (auf Ressourcen-Links, etc.), wenn nicht sogar Antworten.

+0

Dieses Programm wird nicht in C++ kompilieren. Entfernen des C++ - Tags –

+0

@ Robo Ah danke. Ich war sorglos. – RaunakS

+10

Sogar in C ruft dieses Programm mehrfach undefiniertes Verhalten auf. Das Ergebnis ist nur für bestimmte Compiler vorhersagbar, die auf bestimmte Arten von CPUs abzielen (selbst bei Codegolf macht dieses Programm nur bei einer bestimmten Optimierungsebene etwas Interessantes). Richtige Antworten zu "Was macht dieses Programm?" schließe ein: "Es kommt darauf an", "Was immer es will" und "Es wird dich entlassen". –

Antwort

26

Was macht dieses Programm?

main(_){write(read(0,&_,1)&&main());} 

Bevor wir es analysieren, machen wir es prettify:

main(_) { 
    write (read(0, &_, 1) && main()); 
} 

Zunächst sollten Sie wissen, dass _ eine gültige Variablenname ist, wenn auch eine hässliche ein.Sagen wir es ändern:

main(argc) { 
    write(read(0, &argc, 1) && main()); 
} 

Als nächstes erkennen, dass der Rückgabetyp einer Funktion und der Typ eines Parameters in C optional sind (aber nicht in C++):

int main(int argc) { 
    write(read(0, &argc, 1) && main()); 
} 

Als nächstes verstehen, wie Rückgabewerte funktionieren. Bei bestimmten CPU-Typen wird der Rückgabewert immer in denselben Registern gespeichert (z. B. EAX auf x86). Wenn Sie also eine return-Anweisung weglassen, ist der Rückgabewert wahrscheinlich, was auch immer die zuletzt zurückgegebene Funktion sein wird.

int main(int argc) { 
    int result = write(read(0, &argc, 1) && main()); 
    return result; 
} 

Der Aufruf von read ist mehr oder weniger deutlich: sie liest aus Standard-in (Dateideskriptor 0) ist, in den Speicher an &argc befindet, für 1 Byte. Es gibt 1 zurück, wenn das Lesen erfolgreich war, und andernfalls 0.

&& ist der logische "und" -Operator. Es bewertet seine rechte Seite genau dann, wenn es auf der linken Seite "wahr" ist (technisch ist jeder Wert ungleich null). Das Ergebnis des Ausdrucks && ist ein int, der immer 1 (für "wahr") oder 0 (für falsch) ist. In diesem Fall ruft die rechte Seite main ohne Argumente auf. Das Aufrufen von main ohne Argumente nach der Deklaration mit 1 Argument ist ein nicht definiertes Verhalten. Trotzdem funktioniert es oft, solange Sie sich nicht um den Anfangswert des Parameters argc kümmern. Das Ergebnis des && wird dann an write() übergeben. So sieht unser Code jetzt wie folgt aus:

int main(int argc) { 
    int read_result = read(0, &argc, 1) && main(); 
    int result = write(read_result); 
    return result; 
} 

Hmm. Ein kurzer Blick auf die man-Seiten zeigt, dass write drei Argumente braucht, nicht eins. Ein weiterer Fall von undefiniertem Verhalten. Genauso wie wir main mit zu wenigen Argumenten aufrufen, können wir nicht vorhersagen, was write für sein zweites und drittes Argument erhalten wird. Auf typischen Computern bekommen sie etwas, aber wir können nicht sicher was. (Auf atypischen Computern können seltsame Dinge passieren.) Der Autor verlässt sich auf write Empfangen, was zuvor auf dem Speicherstapel gespeichert wurde. Und er verlässt sich auf , die ist das zweite und dritte Argument zu lesen.

int main(int argc) { 
    int read_result = read(0, &argc, 1) && main(); 
    int result = write(read_result, &argc, 1); 
    return result; 
} 

Befestigung der ungültigen Aufruf main und Header hinzugefügt, und die Erweiterung des && haben wir:

#include <unistd.h> 
int main(int argc, int argv) { 
    int result; 
    result = read(0, &argc, 1); 
    if(result) result = main(argc, argv); 
    result = write(result, &argc, 1); 
    return result; 
} 


Schlussfolgerungen

Dieses Programm wird nicht wie erwartet auf viele Computer. Selbst wenn Sie denselben Computer wie den ursprünglichen Autor verwenden, funktioniert es möglicherweise nicht auf einem anderen Betriebssystem. Selbst wenn Sie denselben Computer und dasselbe Betriebssystem verwenden, wird es bei vielen Compilern nicht funktionieren. Selbst wenn Sie denselben Computer-Compiler und dasselbe Betriebssystem verwenden, funktioniert es möglicherweise nicht, wenn Sie die Befehlszeilen-Flags des Compilers ändern.

Wie ich in den Kommentaren sagte, hat die Frage keine gültige Antwort.Wenn Sie einen Veranstalter oder einen Wettkampfrichter gefunden haben, der etwas anderes sagt, laden Sie ihn nicht zu Ihrem nächsten Wettbewerb ein.

+1

Oh wow, das war sehr, _sehr_ umfassend. Eine Klarstellung: Die 'write()' Syntax ist 'int write (int fd, char * Buff, int NumBytes)'. Also wird der Rückgabewert von 'read()' zum Schreiben auf die Standardausgabe '1'? – RaunakS

+1

0 ist Standardeingang, 1 ist Standardausgang, 2 ist Standardfehler. Eine erfolgreiche Rückgabe von read (kombiniert mit einer erfolgreichen Rückkehr von dem rekursiven Aufruf zu main) ergibt somit einen Schreibvorgang nach stdout. Eine fehlgeschlagene Rückgabe aus dem Lesevorgang führt zu einem Schreibvorgang in stdin. Das ist noch ein anderes undefiniertes Verhalten. –

+0

Ah ja, ich hätte vor dem Fragen wiki'd. Dieser Code wäre ein sehr guter IOCCC-Anwärter. Und ist undefiniertes Verhalten wie dieses replizierbar? Ich meine, auf dem gleichen Compiler (gcc 4.4.1), wird dies immer das gleiche Ergebnis geben? – RaunakS

8

Ok, _ ist nur eine Variable, die in der frühen K & R C -Syntax mit einem Standardtyp von int deklariert wurde. Es fungiert als temporärer Speicher.

Das Programm versucht, ein Byte vom Standardeingang zu lesen. Wenn eine Eingabe vorliegt, wird main rekursiv aufgerufen, wobei weiterhin ein Byte gelesen wird.

Am Ende der Eingabe wird read(2) 0 zurückgeben, der Ausdruck wird 0 zurückgeben, der Systemaufruf write(2) wird ausgeführt, und die Aufrufkette wird wahrscheinlich abwickeln.

Ich sage "wahrscheinlich" hier, weil von diesem Punkt an die Ergebnisse in hohem Maße implementierungsabhängig sind. Die anderen Parameter zu write(2) fehlen, aber etwas wird in den Registern und auf dem Stapel sein, so etwas wird in den Kernel übergeben werden. Das gleiche undefinierte Verhalten gilt für den Rückgabewert aus den verschiedenen rekursiven Aktivierungen von main.

Auf meinem x86_64 Mac liest das Programm Standard-Eingabe bis EOF und dann beendet, nichts zu schreiben.

+0

Irgendwelche Zitate über was ist '_'? Neugierig darüber zu wissen –

+0

Es ist nur ein formaler Parameter * ("Variable") * Name. Es ist äquivalent zu 'main (int _)' ... stell dir vor, dass sie es * argc * nannten und es wird alles klar sein. Das heißt: 'main (argc)' wäre früh C mit Default ** int, ** die * Prototyp * Deklarationen wurden später hinzugefügt. Sie deklarieren nicht das übliche * argv *, aber als Ergebnis wird nichts Drastisches passieren. – DigitalRoss

+0

Ja, ein einfaches '_' ist ein zulässiger Variablenname. –