Concept derrière ces quatre lignes de code C délicat

Pourquoi ce code donne-t-il la sortie C++Sucks ? Quel est le concept derrière cela?

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

Testez-le ici .

    Le nombre 7709179928849219.0 a la représentation binary suivante en double 64 bits:

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

    + montre la position du signe; ^ de l’exposant et - de la mantisse (c’est-à-dire la valeur sans exposant).

    Puisque la représentation utilise un exposant binary et une mantisse, doubler le nombre incrémente l’exposant de un. Votre programme le fait précisément 771 fois, donc l’exposant qui a commencé à 1075 (représentation décimale de 10000110011 ) devient 1075 + 771 = 1846 à la fin; la représentation binary de 1846 est 11100110110 . Le motif résultant ressemble à ceci:

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

    Ce motif correspond à la chaîne que vous voyez imprimée, seulement en arrière. Dans le même temps, le deuxième élément du tableau devient zéro, fournissant une terminaison nulle, ce qui rend la chaîne appropriée pour passer à printf() .

    Version plus lisible:

     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); } } 

    Il appelle récursivement main() 771 fois.

    Au début, m[0] = 7709179928849219.0 , qui signifie C++Suc;C Dans chaque appel, m[0] est doublé, pour “réparer” les deux dernières lettres. Dans le dernier appel, m[0] contient une représentation ASCII des caractères C++Sucks et m[1] ne contient que des zéros, il a donc un terminateur nul pour les chaînes C++Sucks . En supposant que m[0] est stocké sur 8 octets, chaque caractère prend 1 octet.

    Sans récursivité et illégale, main() ressemblera à ceci:

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

    Clause de non-responsabilité: Cette réponse a été publiée dans la forme d’origine de la question, qui mentionnait uniquement C ++ et incluait un en-tête C ++. La conversion de la question en C pur a été effectuée par la communauté, sans consortingbution de la part du demandeur initial.


    D’un sharepoint vue formel, il est impossible de raisonner sur ce programme car il est mal formé (c’est-à-dire que ce n’est pas légal en C ++). Il viole C ++ 11 [basic.start.main] p3:

    La fonction main ne doit pas être utilisée dans un programme.

    Cela mis à part, cela repose sur le fait que sur un ordinateur grand public typique, un double fait 8 octets de long et utilise une certaine représentation interne connue. Les valeurs initiales du tableau sont calculées de sorte que lorsque l’algorithme est exécuté, la valeur finale du premier double sera telle que la représentation interne (8 octets) sera le code ASCII des 8 caractères C++Sucks . Le second élément du tableau est alors 0.0 , dont le premier octet est 0 dans la représentation interne, ce qui en fait une chaîne de style C valide. Ceci est ensuite envoyé à la sortie en utilisant printf() .

    L’exécuter sur HW, où une partie de ce qui précède ne résiste pas, se traduirait à la place par du texte de mémoire (ou peut-être même un access hors limites).

    La manière la plus simple de comprendre le code est peut-être de travailler en sens inverse. Nous commencerons par une chaîne à imprimer – pour l’équilibre, nous utiliserons “C ++ Rocks”. Point crucial: tout comme l’original, il compte exactement huit caractères. Comme nous allons le faire (grosso modo) comme l’original, et l’imprimer dans l’ordre inverse, nous commencerons par le mettre en ordre inverse. Pour notre première étape, nous allons simplement voir ce modèle de bit comme un double et imprimer le résultat:

     #include  char ssortingng[] = "skcoR++C"; int main(){ printf("%f\n", *(double*)ssortingng); } 

    Cela produit 3823728713643449.5 . Donc, nous voulons manipuler cela d’une manière qui n’est pas évidente, mais qui est facile à inverser. Je choisirai semi-arbitrairement la multiplication par 256, ce qui nous donne 978874550692723072 . Maintenant, nous avons juste besoin d’écrire du code obscurci pour diviser par 256, puis imprimer les octets individuels dans l’ordre inverse:

     #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); } 

    Maintenant, nous avons beaucoup de transtypage, en passant des arguments à la main (récursive) qui sont complètement ignorés (mais l’évaluation pour obtenir l’incrémentation et la décrémentation sont absolument cruciales), et bien sûr, ce numéro de vue complètement arbitraire pour couvrir le fait que nous sums faire est vraiment simple.

    Bien sûr, puisque l’objective est l’obscurcissement, si nous en avons envie, nous pouvons également prendre davantage de mesures. Par exemple, nous pouvons tirer parti de l’évaluation des courts-circuits, pour transformer notre instruction if en une seule expression, de sorte que le corps de main ressemble à ceci:

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

    Pour quiconque n’est pas habitué au code obscur (et / ou au code golf), cela commence à paraître étrange – calculer et supprimer le nombre logique and un nombre à virgule flottante sans signification et la valeur de retour de main , ce qui n’est même pas le cas. retourner une valeur. Pire encore, sans réaliser (et sans penser) comment fonctionne l’évaluation des courts-circuits, il n’est peut-être même pas évident que cela évite une récursion infinie.

    Notre prochaine étape serait probablement de séparer l’impression de chaque caractère de la recherche de ce caractère. Nous pouvons le faire assez facilement en générant le bon caractère comme valeur de retour de main et en imprimant les main retours:

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

    Au moins pour moi, cela semble assez obscurci, alors je vais en restr là.

    Il ne fait que construire un tableau double (16 octets) qui – s’il est interprété comme un tableau de caractères – construit les codes ASCII pour la chaîne “C ++ Sucks”

    Cependant, le code ne fonctionne pas sur chaque système, il repose sur certains des faits non définis suivants:

    • double a exactement 8 octets
    • endianness

    Le code suivant imprime C++Suc;C , donc la multiplication entière est seulement pour les deux dernières lettres

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

    Les autres ont bien expliqué la question, j’aimerais append une note selon laquelle ce comportement n’est pas défini conformément à la norme.

    C ++ 11 3.6.1 / 3 Fonction principale

    La fonction main ne doit pas être utilisée dans un programme. Le lien (3.5) de main est défini par la mise en œuvre. Un programme qui définit main comme supprimé ou qui déclare principal être en ligne, statique ou constexpr est mal formé. Le nom principal n’est pas autrement réservé. [Exemple: les fonctions membres, les classes et les énumérations peuvent être appelées main, tout comme les entités des autres espaces de noms. —Exemple]

    Le code pourrait être réécrit comme ceci:

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

    Ce qu’il fait, c’est produire un ensemble d’octets dans le double tableau m qui correspondent aux caractères ‘C ++ Sucks’ suivis d’un null-terminateur. Ils ont obscurci le code en choisissant une valeur double qui, doublée 771 fois, produit, dans la représentation standard, cet ensemble d’octets avec le terminateur nul fourni par le second membre du tableau.

    Notez que ce code ne fonctionnerait pas sous une représentation endian différente. En outre, l’appel de main() n’est pas ssortingctement autorisé.

    C’est simplement une manière intelligente de cacher la chaîne “C ++ Sucks” (notez les 8 octets) dans la première valeur double, qui est multipliée de manière récursive par deux jusqu’à ce que les valeurs doubles atteignent zéro (771 fois).

    En multipliant les valeurs doubles 7709179928849219.0 * 2 * 711, vous obtenez “C ++ Sucks” si vous interprétez la valeur d’octet du double comme chaîne, ce que printf () fait avec la dissortingbution. Et printf () n’échoue pas, car la deuxième valeur double est “0” et interprétée comme “\ 0” par printf ().