390 Stimmen

Konzept hinter diesen vier Zeilen kniffligen C-Codes

Warum gibt dieser Code die Ausgabe C++Sucks? Was ist das Konzept dahinter?

#include 

double m[] = {7709179928849219.0, 771};

int main() {
    m[1]--?m[0]*=2,main():printf((char*)m);    
}

Teste es hier.

501voto

Sergey Kalinichenko Punkte 694383

Die Nummer 7709179928849219.0 hat die folgende binäre Darstellung als 64-Bit double:

01000011 00111011 01100011 01110101 01010011 00101011 00101011 01000011
+^^^^^^^ ^^^^---- -------- -------- -------- -------- -------- --------

+ zeigt die Position des Vorzeichens an; ^ des Exponenten und - der Mantisse (d.h. der Wert ohne den Exponenten).

Da die Darstellung den binären Exponenten und die Mantisse verwendet, erhöht sich der Exponent bei Verdopplung des Werts um eins. Ihr Programm tut dies genau 771 Mal, sodass der Exponent, der bei 1075 begann (dezimale Darstellung von 10000110011), am Ende 1075 + 771 = 1846 beträgt; die binäre Darstellung von 1846 ist 11100110110. Das resultierende Muster sieht folgendermaßen aus:

01110011 01101011 01100011 01110101 01010011 00101011 00101011 01000011
-------- -------- -------- -------- -------- -------- -------- --------
0x73 's' 0x6B 'k' 0x63 'c' 0x75 'u' 0x53 'S' 0x2B '+' 0x2B '+' 0x43 'C'

Dieses Muster entspricht dem String, den Sie rückwärts gedruckt sehen. Gleichzeitig wird das zweite Element des Arrays auf null gesetzt, wodurch ein Nullterminator bereitgestellt wird, der den String geeignet für die Übergabe an printf() macht.

226voto

Adam Stelmaszczyk Punkte 19275

Mehr lesbare Version:

double m[2] = {7709179928849219.0, 771};
// m[0] = 7709179928849219.0;
// m[1] = 771;    

int main()
{
    if (m[1]-- != 0)
    {
        m[0] *= 2;
        main();
    }
    else
    {
        printf((char*) m);
    }
}

Es ruft rekursiv main() 771 Mal auf.

Zu Beginn ist m[0] = 7709179928849219.0, was für steht für C++Suc;C. Bei jedem Aufruf wird m[0] verdoppelt, um die letzten beiden Buchstaben zu "reparieren". Im letzten Aufruf enthält m[0] die ASCII-Zeichenfolge von C++Sucks und m[1] enthält nur Nullen, daher hat es einen Null Terminator für die Zeichenfolge C++Sucks. Alles unter der Annahme, dass m[0] auf 8 Bytes gespeichert ist, sodass jedes Zeichen 1 Byte einnimmt.

Ohne Rekursion und illegalem Aufruf von main() sieht es wie folgt aus:

double m[] = {7709179928849219.0, 0};
for (int i = 0; i < 771; i++)
{
    m[0] *= 2;
}
printf((char*) m);

107voto

Haftungsausschluss: Diese Antwort wurde auf der Originalversion der Frage veröffentlicht, die nur C++ erwähnte und einen C++-Header enthielt. Die Umwandlung der Frage in reinem C wurde von der Community ohne die Mitwirkung des Ursprungsfragenden durchgeführt.


Formell betrachtet ist es unmöglich, über dieses Programm zu argumentieren, da es fehlerhaft ist (d.h. es handelt sich nicht um gültiges C++). Es verstößt gegen C++11[basic.start.main]p3:

Die Funktion main darf innerhalb eines Programms nicht verwendet werden.

Abgesehen davon beruht es darauf, dass auf einem typischen Verbrauchercomputer ein double 8 Bytes lang ist und eine bestimmte, weit verbreitete interne Darstellung verwendet wird. Die Anfangswerte des Arrays werden so berechnet, dass beim Ausführen des "Algorithmus" der Endwert des ersten double so ist, dass die interne Darstellung (8 Bytes) den ASCII-Codes der 8 Zeichen C++Sucks entspricht. Das zweite Element im Array ist dann 0.0, dessen ersten Byte in der internen Darstellung 0 ist, wodurch dies zu einem gültigen C-Style-String wird. Dies wird dann mit printf() ausgegeben.

Wenn dies auf Hardware ausgeführt wird, auf der einige der oben genannten Punkte nicht zutreffen, würde stattdessen Mülltext (oder vielleicht sogar ein Zugriff außerhalb der Grenzen) angezeigt.

57voto

Jerry Coffin Punkte 452852

Vielleicht ist der einfachste Weg, den Code zu verstehen, das Ganze rückwärts durchzugehen. Wir beginnen mit einem String zum Ausgeben - für das Gleichgewicht verwenden wir "C++Rocks". Wichtiger Punkt: genau wie im Original hat er genau acht Zeichen. Da wir ungefähr wie im Original vorgehen und ihn in umgekehrter Reihenfolge ausgeben werden, fangen wir damit an, ihn umgekehrt einzugeben. Als ersten Schritt betrachten wir diesen Bitmuster einfach als double und geben das Ergebnis aus:

#include 

char string[] = "skcoR++C";

int main(){
    printf("%f\n", *(double*)string);
}

Dies ergibt 3823728713643449.5. Nun möchten wir das auf eine Weise manipulieren, die nicht offensichtlich ist, aber einfach umgekehrt werden kann. Ich wähle semiwillkürlich die Multiplikation mit 256, was uns 978874550692723072 ergibt. Jetzt müssen wir nur noch etwas verschleierten Code schreiben, um durch 256 zu dividieren und dann die einzelnen Bytes davon in umgekehrter Reihenfolge auszugeben:

#include 

double x [] = { 978874550692723072, 8 };
char *y = (char *)x;

int main(int argc, char **argv){
    if (x[1]) {
        x[0] /= 2;  
        main(--x[1], (char **)++y);
    }
    putchar(*--y);
}

Jetzt haben wir viele Castings, Argumente, die an (rekursiven) main übergeben werden, die vollständig ignoriert werden (aber die Ausführung für die Inkrementierung und Dekrementierung ist entscheidend), und natürlich diese völlig willkürlich aussehende Zahl, um zu vertuschen, dass das, was wir tun, wirklich ziemlich einfach ist.

Natürlich, da es nur darum geht, den Code zu verschleiern, können wir, wenn wir wollen, auch weitere Schritte unternehmen. Nur als Beispiel können wir die Kurzschlussschaltung nutzen, um unsere if-Anweisung in einen einzigen Ausdruck umzuwandeln, sodass der Hauptteil von main wie folgt aussieht:

x[1] && (x[0] /= 2,  main(--x[1], (char **)++y));
putchar(*--y);

Für jeden, der nicht an verschleierten Code (und/oder Code-Golf) gewöhnt ist, sieht das jetzt ziemlich seltsam aus - die logische Verknüpfung einer sinnlosen Gleitkommazahl und dem Rückgabewert von main berechnen und verwerfen, der nicht einmal einen Wert zurückgibt. Schlimmer noch, ohne zu erkennen (und darüber nachzudenken), wie die Kurzschlussschaltung funktioniert, ist möglicherweise nicht sofort offensichtlich, wie sie eine unendliche Rekursion vermeidet.

Unser nächster Schritt wäre wahrscheinlich das Trennen des Druckens jedes Zeichens vom Finden dieses Zeichens. Das können wir ziemlich einfach tun, indem wir das richtige Zeichen als Rückgabewert von main generieren und ausgeben, was main zurückgibt:

x[1] && (x[0] /= 2,  putchar(main(--x[1], (char **)++y)));
return *--y;

Zumindest mir scheint das jetzt verschleiert genug zu sein, also lasse ich es dabei.

24voto

D.R. Punkte 18820

Es baut einfach ein doppeltes Array (16 Bytes) auf, das - wenn es als Zeichenarray interpretiert wird - die ASCII-Codes für den String "C++Sucks" aufbaut

Der Code funktioniert jedoch nicht auf jedem System, er stützt sich auf einige der folgenden undefinierten Fakten:

CodeJaeger.com

CodeJaeger ist eine Gemeinschaft für Programmierer, die täglich Hilfe erhalten..
Wir haben viele Inhalte, und Sie können auch Ihre eigenen Fragen stellen oder die Fragen anderer Leute lösen.

Powered by:

X