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

384

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

#include <stdio.h>

double m[] = {7709179928849219.0, 771};

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

Testez-le ici .

codeslayer1
la source
1
@BoBTFish techniquement, oui, mais il fonctionne tout de même en C99: ideone.com/IZOkql
nijansen
12
@nurettin J'ai eu des pensées similaires. Mais ce n'est pas la faute d'OP, ce sont les gens qui votent pour ces connaissances inutiles. Certes, ce truc d'obscurcissement de code peut être intéressant, mais tapez "obfuscation" dans Google et vous obtenez des tonnes de résultats dans chaque langage formel auquel vous pouvez penser. Ne vous méprenez pas, je trouve OK de poser une telle question ici. C'est juste une question surfaite car pas très utile.
TobiMcNamobi
6
@ detonator123 "Vous devez être nouveau ici" - si vous regardez la raison de la fermeture, vous pouvez découvrir que ce n'est pas le cas. La compréhension minimale requise est clairement absente de votre question - "Je ne comprends pas cela, expliquez-le" n'est pas quelque chose de bienvenu sur Stack Overflow. Si vous aviez tenté quelque chose vous-même en premier, la question n'aurait-elle pas été close. Il est trivial de google "double représentation C" ou similaire.
42
Ma machine PowerPC big-endian s'imprime skcuS++C.
Adam Rosenfield
27
Ma parole, je déteste les questions artificielles comme celle-ci. C'est un motif de bits en mémoire qui se trouve être identique à une chaîne stupide. Il ne sert à rien pour quiconque, et pourtant il rapporte des centaines de points de rep à la fois à l'interrogateur et au répondeur. Pendant ce temps, des questions difficiles qui pourraient être utiles aux gens gagnent peut-être une poignée de points, le cas échéant. C'est une sorte d'enfant affiche de ce qui ne va pas avec SO.
Carey Gregory

Réponses:

494

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

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 l'exposant).

Puisque la représentation utilise l'exposant binaire et la 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; représentation binaire 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, uniquement à l'envers. Dans le même temps, le deuxième élément du tableau devient zéro, fournissant un terminateur nul, ce qui rend la chaîne appropriée pour passer à printf().

dasblinkenlight
la source
22
Pourquoi la chaîne est-elle inversée?
Derek
95
@Derek x86 est petit-endian
Angew n'est plus fier de SO
16
@Derek Ceci est dû à la plate-forme spécifique boutisme : les octets de la représentation abstraite IEEE 754 sont stockées dans la mémoire à des adresses décroissantes, de sorte que les impressions de chaîne correctement. Sur le matériel avec une grande endianité, il faudrait commencer par un numéro différent.
dasblinkenlight
14
@AlvinWong Vous avez raison, la norme ne requiert pas IEEE 754 ou tout autre format spécifique. Ce programme est à peu près aussi portable que possible, ou très proche de lui :-)
dasblinkenlight
10
@GrijeshChauhan J'ai utilisé une calculatrice IEEE754 double précision : j'ai collé la 7709179928849219valeur et récupéré la représentation binaire.
dasblinkenlight
223

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 se dresse pour C++Suc;C. À chaque appel, m[0]est doublé, pour "réparer" les deux dernières lettres. Dans le dernier appel, m[0]contient une représentation de caractères ASCII C++Suckset m[1]contient uniquement des zéros, il a donc un terminateur nul pour la C++Suckschaîne. Tous sous l'hypothèse qui m[0]est stockée sur 8 octets, donc chaque caractère prend 1 octet.

Sans récursivité et illégal main() appel cela ressemblera à ceci:

double m[] = {7709179928849219.0, 0};
for (int i = 0; i < 771; i++)
{
    m[0] *= 2;
}
printf((char*) m);
Adam Stelmaszczyk
la source
8
C'est décrémenter le suffixe. Il sera donc appelé 771 fois.
Jack Aidley
106

Avertissement: Cette réponse a été publiée sous la forme originale de la question, qui ne mentionnait que C ++ et comprenait un en-tête C ++. La conversion de la question en C pur a été effectuée par la communauté, sans contribution du demandeur initial.


Formellement parlant, il est impossible de raisonner sur ce programme car il est mal formé (c'est-à-dire qu'il 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.

Ceci mis à part, il repose sur le fait que sur un ordinateur grand public typique, a doublefait 8 octets de long et utilise une certaine représentation interne bien connue. Les valeurs initiales du tableau sont calculées de telle sorte que lorsque "l'algorithme" est exécuté, la valeur finale du premier doublesera telle que la représentation interne (8 octets) sera les codes ASCII des 8 caractères C++Sucks. Le deuxième élément du tableau est alors 0.0, dont le premier octet est 0dans la représentation interne, ce qui en fait une chaîne de style C valide. Il est ensuite envoyé à la sortie en utilisantprintf() .

L'exécution de cela sur HW où certains des éléments ci-dessus ne tiennent pas entraînerait à la place un texte incorrect (ou peut-être même un accès hors limites).

Angew n'est plus fier de SO
la source
25
Je dois ajouter que ce n'est pas une invention de C ++ 11 - C ++ 03 avait également basic.start.main3.6.1 / 3 avec le même libellé.
sharpptooth
1
Le but de ce petit exemple est d'illustrer ce qui peut être fait avec C ++. Échantillon magique utilisant des astuces UB ou d'énormes progiciels de code "classique".
SChepurin
1
@sharptooth Merci d'avoir ajouté ceci. Je ne voulais pas impliquer le contraire, je viens de citer la norme que j'ai utilisée.
Angew n'est plus fier de SO
@Angew: Oui, je comprends cela, je voulais juste dire que la formulation est assez ancienne.
sharpptooth
1
@JimBalter Remarque J'ai dit "formellement, il est impossible de raisonner" et non "il est impossible de raisonner formellement". Vous avez raison, il est possible de raisonner sur le programme, mais vous devez connaître les détails du compilateur utilisé pour le faire. Il serait tout à fait dans les droits d'un compilateur de simplement éliminer l'appel à main(), ou de le remplacer par un appel API pour formater le disque dur, ou autre chose.
Angew n'est plus fier de SO
57

La façon la plus simple de comprendre le code est peut-être de travailler à l'envers. Nous allons commencer par une chaîne à imprimer - pour l'équilibre, nous utiliserons "C ++ Rocks". Point crucial: tout comme l'original, il fait exactement huit caractères. Puisque nous allons faire (à peu près) comme l'original et l'imprimer dans l'ordre inverse, nous allons commencer par le mettre dans l'ordre inverse. Pour notre première étape, nous allons simplement afficher ce motif binaire comme un double, et imprimer le résultat:

#include <stdio.h>

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

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

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

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

Maintenant, nous avons beaucoup de cast, passant des arguments à (récursif) main qui sont complètement ignorés (mais l'évaluation pour obtenir l'incrémentation et la décrémentation est absolument cruciale), et bien sûr ce nombre à la recherche complètement arbitraire pour couvrir le fait que ce que nous faisons est vraiment assez simple.

Bien sûr, étant donné que tout le problème est l'obscurcissement, si nous en avons envie, nous pouvons également prendre plus de mesures. Par exemple, nous pouvons profiter de l'évaluation des courts-circuits, pour transformer notre ifdéclaration 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 quelqu'un qui n'est pas habitué au code obscurci (et / ou au golf de code), cela commence à sembler assez étrange en effet - calculer et rejeter la logique andd'un certain nombre à virgule flottante sans signification et la valeur de retour demain , qui ne renvoie même pas un valeur. Pire encore, sans réaliser (et réfléchir) au fonctionnement de l'évaluation des courts-circuits, il n'est peut-être même pas immédiatement évident comment elle é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 mainet en imprimant ce qui mainretourne:

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 rester là.

Jerry Coffin
la source
1
J'adore l'approche médico-légale.
ryyker
24

Il s'agit simplement de créer un double tableau (16 octets) qui - s'il est interprété comme un tableau de caractères - crée les codes ASCII pour la chaîne "C ++ Sucks"

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

DR
la source
12

Le code suivant s'imprime C++Suc;C, donc toute la multiplication ne concerne que les deux dernières lettres

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

Les autres ont expliqué la question de manière assez approfondie, je voudrais ajouter une note qu'il s'agit d' un comportement indéfini selon 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 l'implémentation. Un programme qui définit main comme supprimé ou qui déclare main comme étant 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 dans d'autres espaces de noms. —Fin exemple]

Yu Hao
la source
1
Je dirais qu'il est même mal formé (comme je l'ai fait dans ma réponse) - il viole un "doit".
Angew n'est plus fier de SO
9

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 doubletableau mqui correspondent aux caractères «C ++ Sucks» suivi d'un terminateur nul. Ils ont obscurci le code en choisissant une valeur double qui, lorsqu'elle est doublée 771 fois, produit, dans la représentation standard, cet ensemble d'octets avec le terminateur nul fourni par le deuxième membre du tableau.

Notez que ce code ne fonctionnerait pas sous une représentation endienne différente. De plus, les appels main()ne sont pas strictement autorisés.

Jack Aidley
la source
3
Pourquoi votre fretour est-il un int?
leftaroundabout
1
Euh, parce que j'étais sans cervelle copiant le intretour dans la question. Permettez-moi de résoudre ce problème.
Jack Aidley
1

Rappelons d'abord que les nombres en double précision sont stockés dans la mémoire au format binaire comme suit:

(i) 1 bit pour le signe

(ii) 11 bits pour l'exposant

(iii) 52 bits pour la grandeur

L'ordre des bits décroît de (i) à (iii).

Tout d'abord, le nombre décimal fractionnaire est converti en nombre binaire fractionnaire équivalent, puis il est exprimé sous forme d'ordre de grandeur en binaire.

Ainsi, le numéro 7709179928849219.0 devient

(11011011000110111010101010011001010110010101101000011)base 2


=1.1011011000110111010101010011001010110010101101000011 * 2^52

Maintenant, tout en considérant les bits de magnitude 1. est négligé car toute la méthode de l'ordre de grandeur doit commencer par 1.

Donc, la partie magnitude devient:

1011011000110111010101010011001010110010101101000011 

Maintenant, la puissance de 2 est 52 , nous devons lui ajouter un nombre de biais comme 2 ^ (bits pour l'exposant -1) -1, c'est-à-dire 2 ^ (11 -1) -1 = 1023 , donc notre exposant devient 52 + 1023 = 1075

Maintenant, notre code multiplie le nombre par 2 , 771 fois ce qui fait que l'exposant augmente de 771

Donc, notre exposant est (1075 + 771) = 1846 dont l'équivalent binaire est (11100110110)

Maintenant, notre nombre est positif, donc notre bit de signe est 0 .

Notre numéro modifié devient donc:

signe bit + exposant + amplitude (concaténation simple des bits)

0111001101101011011000110111010101010011001010110010101101000011 

puisque m est converti en pointeur char, nous diviserons le motif binaire en morceaux de 8 à partir du LSD

01110011 01101011 01100011 01110101 01010011 00101011 00101011 01000011 

(dont l'équivalent Hex est :)

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

TABLEAU ASCII Lequel de la carte de caractères comme indiqué est:

s   k   c   u      S      +   +   C 

Maintenant, une fois que cela a été fait, m [1] est 0, ce qui signifie un caractère NULL

Supposons maintenant que vous exécutez ce programme sur une machine petit-boutiste (le bit de poids faible est stocké dans une adresse inférieure). ) et printf () s'arrête lorsqu'il rencontre 00000000 dans le dernier chunck ...

Ce code n'est cependant pas portable.

Abhishek Ghosh
la source