Konzept hinter diesen vier Zeilen kniffligen C-Codes


384

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

#include <stdio.h>

double m[] = {7709179928849219.0, 771};

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

Testen Sie es hier .


1
@BoBTFish technisch ja, aber es läuft trotzdem in C99: ideone.com/IZOkql
nijansen

12
@urettin Ich hatte ähnliche Gedanken. Aber es ist nicht die Schuld von OP, es sind die Leute, die für dieses nutzlose Wissen stimmen. Zugegeben, dieses Code-Verschleierungsmaterial mag interessant sein, aber geben Sie "Verschleierung" in Google ein und Sie erhalten unzählige Ergebnisse in jeder formalen Sprache, die Sie sich vorstellen können. Versteh mich nicht falsch, ich finde es in Ordnung, hier eine solche Frage zu stellen. Es ist nur eine überbewertete, aber nicht sehr nützliche Frage.
TobiMcNamobi

6
@ detonator123 "Sie müssen hier neu sein" - Wenn Sie sich den Schließungsgrund ansehen, können Sie feststellen, dass dies nicht der Fall ist. Das erforderliche minimale Verständnis fehlt eindeutig in Ihrer Frage - "Ich verstehe das nicht, erkläre es" ist bei Stack Overflow nicht erwünscht. Wenn Sie selbst zuerst etwas versucht hätten, wäre die Frage nicht geschlossen worden. Es ist trivial, "doppelte Darstellung C" oder ähnliches zu googeln.

42
Meine Big-Endian-PowerPC-Maschine druckt aus skcuS++C.
Adam Rosenfield

27
Mein Wort, ich hasse solche erfundenen Fragen. Es ist ein Bitmuster im Speicher, das mit einem albernen String identisch ist. Es dient niemandem einem nützlichen Zweck und bringt dennoch Hunderte von Wiederholungspunkten sowohl für den Fragesteller als auch für den Antwortenden ein. In der Zwischenzeit verdienen schwierige Fragen, die für Menschen nützlich sein könnten, vielleicht eine Handvoll Punkte, wenn überhaupt. Dies ist eine Art Aushängeschild dessen, was mit SO nicht stimmt.
Carey Gregory

Antworten:


494

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

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

+zeigt die Position des Zeichens; ^des Exponenten und -der Mantisse (dh des Wertes ohne Exponenten).

Da die Darstellung einen binären Exponenten und eine Mantisse verwendet, erhöht das Verdoppeln der Zahl den Exponenten um eins. Ihr Programm macht es genau 771 Mal, so dass der Exponent, der bei 1075 begann (Dezimaldarstellung von 10000110011), am Ende 1075 + 771 = 1846 wird; 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 der Zeichenfolge, die Sie gedruckt sehen, nur rückwärts. Gleichzeitig wird das zweite Element des Arrays Null, wodurch ein Nullterminator bereitgestellt wird, wodurch die Zeichenfolge zum Übergeben an geeignet wird printf().


22
Warum ist die Saite rückwärts?
Derek

95
@Derek x86 ist Little-Endian
Angew ist nicht mehr stolz auf SO

16
@Derek Dies liegt an der plattformspezifischen Endianness : Die Bytes der abstrakten IEEE 754-Darstellung werden an abnehmenden Adressen gespeichert, sodass die Zeichenfolge korrekt gedruckt wird. Bei Hardware mit großer Ausstattung müsste man mit einer anderen Nummer beginnen.
Dasblinkenlight

14
@AlvinWong Sie haben Recht, der Standard erfordert weder IEEE 754 noch ein anderes spezifisches Format. Dieses Programm ist so wenig portabel wie es nur geht oder sehr nahe daran :-)
dasblinkenlight

10
@GrijeshChauhan Ich habe einen IEEE754-Rechner mit doppelter Genauigkeit verwendet : Ich habe den 7709179928849219Wert eingefügt und die binäre Darstellung zurückbekommen.
Dasblinkenlight

223

Lesbarere 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 wird main()771 Mal rekursiv aufgerufen.

Am Anfang m[0] = 7709179928849219.0, das steht für C++Suc;C. Wird bei jedem Anruf m[0]verdoppelt, um die letzten beiden Buchstaben zu "reparieren". m[0]Enthält im letzten Aufruf die ASCII-Zeichendarstellung von C++Sucksund m[1]enthält nur Nullen, sodass ein Nullterminator für die C++SucksZeichenfolge vorhanden ist. Alles unter der Annahme, dass m[0]es auf 8 Bytes gespeichert ist, sodass jedes Zeichen 1 Byte benötigt.

Ohne Rekursion und illegalen main()Anruf sieht es so aus:

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

8
Es ist Postfix-Dekrement. Es wird also 771 mal aufgerufen.
Jack Aidley

106

Haftungsausschluss: Diese Antwort wurde in der ursprünglichen Form der Frage veröffentlicht, in der nur C ++ erwähnt wurde und die einen C ++ - Header enthielt. Die Konvertierung der Frage in reines C wurde von der Community ohne Eingabe des ursprünglichen Fragestellers durchgeführt.


Formal ist es unmöglich, über dieses Programm nachzudenken, weil es schlecht geformt ist (dh es ist kein legales C ++). Es verstößt gegen C ++ 11 [basic.start.main] p3:

Die Funktion main darf nicht innerhalb eines Programms verwendet werden.

Abgesehen davon beruht es auf der Tatsache, dass auf einem typischen Consumer-Computer a double8 Byte lang ist und eine bestimmte bekannte interne Darstellung verwendet. Die Anfangswerte des Arrays werden so berechnet, dass bei Ausführung des "Algorithmus" der Endwert des ersten doubleso ist, dass die interne Darstellung (8 Bytes) die ASCII-Codes der 8 Zeichen sind C++Sucks. Das zweite Element im Array ist dann 0.0, dessen erstes Byte sich 0in der internen Darstellung befindet, was dies zu einer gültigen Zeichenfolge im C-Stil macht. Dies wird dann mit an die Ausgabe gesendet printf().

Wenn Sie dies auf HW ausführen, wo einige der oben genannten Punkte nicht zutreffen, wird stattdessen Mülltext (oder möglicherweise sogar ein Zugriff außerhalb der Grenzen) ausgegeben.


25
Ich muss hinzufügen, dass dies keine Erfindung von C ++ 11 ist - C ++ 03 hatte auch basic.start.main3.6.1 / 3 mit dem gleichen Wortlaut.
Scharfzahn

1
In diesem kleinen Beispiel soll veranschaulicht werden, was mit C ++ getan werden kann. Magisches Beispiel mit UB-Tricks oder riesigen Softwarepaketen mit "klassischem" Code.
SChepurin

1
@sharptooth Danke, dass du das hinzugefügt hast. Ich wollte nicht anders implizieren, ich habe nur den Standard zitiert, den ich verwendet habe.
Angew ist nicht mehr stolz auf SO

@Angew: Ja, ich verstehe das, wollte nur sagen, dass der Wortlaut ziemlich alt ist.
Scharfzahn

1
@ JimBalter Hinweis Ich sagte "formal gesehen ist es unmöglich zu argumentieren", nicht "es ist unmöglich formal zu argumentieren ". Sie haben Recht, dass es möglich ist, über das Programm nachzudenken, aber Sie müssen die Details des Compilers kennen, der dafür verwendet wird. Es liegt in den Rechten eines Compilers , den Aufruf einfach main()zu entfernen oder ihn durch einen API-Aufruf zum Formatieren der Festplatte oder was auch immer zu ersetzen.
Angew ist nicht mehr stolz auf SO

57

Der einfachste Weg, den Code zu verstehen, besteht darin, die Dinge in umgekehrter Reihenfolge durchzuarbeiten. Wir beginnen mit einem String zum Ausdrucken - zum Ausgleich verwenden wir "C ++ Rocks". Entscheidender Punkt: Genau wie das Original ist es genau acht Zeichen lang. Da wir das Original (ungefähr) mögen und es in umgekehrter Reihenfolge ausdrucken, werden wir es zunächst in umgekehrter Reihenfolge einfügen. In unserem ersten Schritt betrachten wir dieses Bitmuster einfach als doubleund drucken das Ergebnis aus:

#include <stdio.h>

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

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

Dies erzeugt 3823728713643449.5. Wir wollen das also auf eine Weise manipulieren, die nicht offensichtlich ist, aber leicht rückgängig zu machen ist. Ich werde halb willkürlich die Multiplikation mit 256 wählen, was uns gibt 978874550692723072. Jetzt müssen wir nur noch einen verschleierten Code schreiben, um ihn durch 256 zu teilen, und dann die einzelnen Bytes davon in umgekehrter Reihenfolge ausdrucken:

#include <stdio.h>

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, die Argumente an (rekursive) übergeben main, die völlig ignoriert werden (aber die Bewertung, um das Inkrement und Dekrement zu erhalten, sind äußerst wichtig), und natürlich diese völlig willkürlich aussehende Zahl, um die Tatsache zu vertuschen, was wir tun ist wirklich ziemlich einfach.

Da der springende Punkt die Verschleierung ist, können wir natürlich auch weitere Schritte unternehmen, wenn wir Lust dazu haben. Zum Beispiel können wir die Kurzschlussbewertung nutzen, um unsere ifAussage in einen einzigen Ausdruck umzuwandeln, sodass der Hauptteil folgendermaßen 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 dies in der Tat ziemlich seltsam aus - das Berechnen und Verwerfen der Logik andeiner bedeutungslosen Gleitkommazahl und des Rückgabewerts von main, der nicht einmal a zurückgibt Wert. Schlimmer noch, ohne zu erkennen (und darüber nachzudenken), wie die Kurzschlussbewertung funktioniert, ist es möglicherweise nicht sofort offensichtlich, wie eine unendliche Rekursion vermieden wird.

Unser nächster Schritt wäre wahrscheinlich, das Drucken jedes Zeichens vom Finden dieses Zeichens zu trennen. Wir können das ziemlich einfach tun, indem wir das richtige Zeichen als Rückgabewert von generieren mainund ausdrucken, was mainzurückgibt:

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

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


1
Ich liebe den forensischen Ansatz.
Ryyker

24

Es wird lediglich ein doppeltes Array (16 Byte) aufgebaut, das - wenn es als char-Array interpretiert wird - die ASCII-Codes für die Zeichenfolge "C ++ Sucks" aufbaut.

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


12

Der folgende Code wird gedruckt C++Suc;C, sodass die gesamte Multiplikation nur für die letzten beiden Buchstaben gilt

double m[] = {7709179928849219.0, 0};
printf("%s\n", (char *)m);

11

Die anderen haben die Frage ziemlich gründlich erklärt. Ich möchte einen Hinweis hinzufügen, dass dies ein undefiniertes Verhalten gemäß dem Standard ist.

C ++ 11 3.6.1 / 3 Hauptfunktion

Die Funktion main darf nicht innerhalb eines Programms verwendet werden. Die Verknüpfung (3.5) von main ist implementierungsdefiniert. Ein Programm, das main als gelöscht definiert oder main als inline, statisch oder constexpr deklariert, ist fehlerhaft. Der Name main ist nicht anderweitig reserviert. [Beispiel: Elementfunktionen, Klassen und Aufzählungen können als main bezeichnet werden, ebenso wie Entitäten in anderen Namespaces. - Beispiel beenden]


1
Ich würde sagen, es ist sogar schlecht geformt (wie ich es in meiner Antwort getan habe) - es verstößt gegen ein "Soll".
Angew ist nicht mehr stolz auf SO

9

Der Code könnte folgendermaßen umgeschrieben werden:

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

Es erzeugt eine Reihe von Bytes im doubleArray m, die zufällig den Zeichen 'C ++ Sucks' entsprechen, gefolgt von einem Nullterminator. Sie haben den Code verschleiert, indem sie einen doppelten Wert gewählt haben, der, wenn er 771-mal verdoppelt wird, in der Standarddarstellung den Satz von Bytes mit dem vom zweiten Mitglied des Arrays bereitgestellten Nullterminator erzeugt.

Beachten Sie, dass dieser Code unter einer anderen Endian-Darstellung nicht funktioniert. Auch Anrufe main()sind nicht unbedingt erlaubt.


3
Warum kommt Ihre fRückkehr ein int?
Links um den

1
Ähm, weil ich die intRückgabe in der Frage hirnlos kopiert habe . Lassen Sie mich das beheben.
Jack Aidley

1

Zunächst sollten wir daran erinnern, dass Zahlen mit doppelter Genauigkeit im Binärformat wie folgt im Speicher gespeichert sind:

(i) 1 Bit für das Vorzeichen

(ii) 11 Bits für den Exponenten

(iii) 52 Bits für die Größe

Die Reihenfolge der Bits nimmt von (i) auf (iii) ab.

Zuerst wird die dezimale Bruchzahl in eine äquivalente gebrochene Binärzahl umgewandelt und dann als binäre Größenordnungsform ausgedrückt.

So ist die Zahl 7709179928849219,0 wird

(11011011000110111010101010011001010110010101101000011)base 2


=1.1011011000110111010101010011001010110010101101000011 * 2^52

Nun wird unter Berücksichtigung der Größenbits 1 vernachlässigt, da alle Größenordnungsmethoden mit 1 beginnen sollen.

So wird der Magnitudenanteil:

1011011000110111010101010011001010110010101101000011 

Nun ist die Macht der 2 ist 52 , müssen wir Vorbelastungsnummer als hinzuzufügen -1 2 ^ (für Exponenten -1 Bits) , dh 2 ^ (11 -1) -1 = 1023 , so dass unsere Exponenten werden 52 + 1023 = 1075

Jetzt multipliziert unser Code die Zahl mit dem 2 , 771- fachen, wodurch sich der Exponent um 771 erhöht

Unser Exponent ist also (1075 + 771) = 1846, dessen binäres Äquivalent (11100110110) ist.

Jetzt ist unsere Zahl positiv, also ist unser Vorzeichenbit 0 .

So wird unsere modifizierte Nummer:

Vorzeichenbit + Exponent + Größe (einfache Verkettung der Bits)

0111001101101011011000110111010101010011001010110010101101000011 

Da m in einen Zeichenzeiger umgewandelt wird, teilen wir das Bitmuster in 8er-Blöcke vom LSD auf

01110011 01101011 01100011 01110101 01010011 00101011 00101011 01000011 

(dessen Hex-Äquivalent ist :)

 0x73 0x6B 0x63 0x75 0x53 0x2B 0x2B 0x43 

ASCII-TABELLE Welche aus der Charakterkarte wie gezeigt ist:

s   k   c   u      S      +   +   C 

Sobald dies geschehen ist, ist m [1] 0, was ein NULL-Zeichen bedeutet

Angenommen, Sie führen dieses Programm auf einem Little-Endian- Computer aus (das Bit niedrigerer Ordnung wird in der unteren Adresse gespeichert), zeigen Sie also mit dem Zeiger m auf das Bit mit der niedrigsten Adresse und nehmen Sie dann Bits in Spannfuttern von 8 auf (als Typ, der in char * umgewandelt wurde) ) und das printf () stoppt, wenn 00000000 im letzten Chunck ...

Dieser Code ist jedoch nicht portierbar.

Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.