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 .
skcuS++C
.
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 .
skcuS++C
.
Antworten:
Die Zahl 7709179928849219.0
hat 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()
.
7709179928849219
Wert eingefügt und die binäre Darstellung zurückbekommen.
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++Sucks
und m[1]
enthält nur Nullen, sodass ein Nullterminator für die C++Sucks
Zeichenfolge 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);
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 double
8 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 double
so 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 0
in 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.
basic.start.main
3.6.1 / 3 mit dem gleichen Wortlaut.
main()
zu entfernen oder ihn durch einen API-Aufruf zum Formatieren der Festplatte oder was auch immer zu ersetzen.
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 double
und 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 if
Aussage 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 and
einer 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 main
und ausdrucken, was main
zurü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.
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:
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);
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]
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 double
Array 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.
f
Rückkehr ein int
?
int
Rückgabe in der Frage hirnlos kopiert habe . Lassen Sie mich das beheben.
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
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.