web-dev-qa-db-fra.com

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

Pourquoi ce code donne-t-il le résultat 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 .

378
codeslayer1

Le nombre 7709179928849219.0 a la représentation binaire suivante sous la forme d'un double 64 bits:

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

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

Étant donné que la représentation utilise les exposants binaires et la mantisse, doubler le nombre incrémente l'exposant de un. Votre programme le fait précisément 771 fois. L’exposant qui a commencé à 1075 (représentation décimale de 10000110011) devient 1075 + 771 = 1846 à la fin; La représentation binaire de 1846 est 11100110110. Le modèle 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 permet à la chaîne de passer à printf().

492
dasblinkenlight

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 de manière récursive main() 771 fois.

Au début, m[0] = 7709179928849219.0, qui signifie 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 ASCII la représentation des caractères de C++Sucks et m[1] ne contient que des zéros, ce qui signifie que terminateur nul = pour C++Sucks chaîne. Tout en supposant que m[0] soit stocké sur 8 octets, chaque caractère prend 1 octet.

Sans récursion et appels illégaux main() cela ressemblera à ceci:

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

Disclaimer: Cette réponse a été postée dans le formulaire original de la question, qui ne mentionnait que le C++ et incluait un en-tête C++. La conversion de la question en C pur a été effectuée par la communauté, sans la contribution du demandeur initial.


Formellement parlant, il est impossible de raisonner sur ce programme car il est mal formé (c’est-à-dire que ce n’est pas du C++ légal). Cela viole C++ 11 [basic.start.main] p3:

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

Ceci mis à part, cela repose sur le fait que sur un ordinateur grand public typique, une double a une longueur de 8 octets et utilise une certaine représentation interne bien connue. Les valeurs initiales du tableau sont calculées de sorte que, lorsque "l'algorithme" est exécuté, la valeur finale du premier double soit telle que la représentation interne (8 octets) soit le ASCII. codes des 8 caractères C++Sucks. Le deuxième é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().

Exécuter ceci sur HW où certains des éléments ci-dessus ne sont pas conservés aurait pour résultat un texte saccadé (ou peut-être même un accès hors limites).

104
Angew

Le moyen le plus simple de comprendre le code est peut-être de travailler à l'envers. Nous allons commencer par une chaîne à imprimer - pour la balance, nous utiliserons "C++ Rocks". Point crucial: comme l’original, il a exactement huit caractères. Puisque nous allons faire (à peu près) comme l'original et l'imprimer dans l'ordre inverse, nous commencerons par le mettre dans l'ordre inverse. Pour notre première étape, nous allons simplement voir ce motif de bits sous la forme d'un double, et afficher le résultat:

#include <stdio.h>

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

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

Ceci produit 3823728713643449.5. Nous voulons donc manipuler cela d’une manière qui n’est pas évidente, mais qui est facile à inverser. Je choisirai de manière semi-arbitraire la multiplication par 256, ce qui nous donne 978874550692723072. Maintenant, il suffit d’écrire du code obscurci pour le diviser par 256, puis d’en imprimer les octets individuels 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 transtypages, en passant des arguments à (récursifs) main qui sont complètement ignorés (mais l'évaluation pour obtenir l'incrément et le décrément est tout à fait cruciale), et bien sûr ce nombre complètement arbitraire pour dissimuler le fait que ce que nous faisons est vraiment très simple.

Bien sûr, étant donné qu’il s’agit bien d’obscurcissement, nous pouvons prendre des mesures supplémentaires si nous en avons le sentiment. Par exemple, nous pouvons tirer parti de l’évaluation des courts-circuits pour transformer notre déclaration 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 ceux qui ne sont pas habitués au code obscurci (et/ou au code golf), cela commence à paraître vraiment étrange - calculer et abandonner la logique and d'un nombre à virgule flottante sans signification et la valeur de retour de main, qui ne renvoie même pas de valeur. Pire encore, sans se rendre compte (et sans penser à) comment fonctionne l'évaluation de court-circuit, il peut ne pas même être évident de voir 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 en tant que valeur de retour de main et en affichant ce que main renvoie:

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

Au moins, cela me semble assez obscur, alors je vais en rester là.

56
Jerry Coffin

Il s'agit simplement de construire un double tableau (16 octets) qui, s'il est interprété comme un tableau de caractères, construit les codes ASCII de la chaîne "Cucks Sucks".

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

23
D.R.

Le code suivant imprime C++Suc;C, de sorte que la multiplication complète ne concerne que les deux dernières lettres.

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

Les autres ont expliqué la question de manière assez détaillée. J'aimerais ajouter une note indiquant que c'est 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 la mise en oeuvre. Un programme qui définit main en tant que supprimé ou qui déclare main en ligne, statique ou constexpr est mal formé. Le nom principal n'est pas réservé autrement. [Exemple: les fonctions membres, les classes et les énumérations peuvent être appelées main, de même que les entités situées dans d'autres espaces de noms. —Fin exemple]

10
Yu Hao

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

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

Cela produit un ensemble d'octets dans le tableau double _ m correspondant aux caractères 'C++ Sucks' suivis d'un terminateur nul. Ils ont obscurci le code en choisissant une valeur double qui, lorsqu'elle est doublée 771 fois, génère, 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 Endian différente. De plus, l'appel de main() n'est pas strictement autorisé.

9
Jack Aidley

Rappelons tout 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 magnitude

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

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

Donc, le nombre 7709179928849219. devient

(11011011000110111010101010011001010110010101101000011)base 2


=1.1011011000110111010101010011001010110010101101000011 * 2^52

Maintenant, tout en considérant la magnitude, les bits 1. sont négligés car toute la méthode d'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 numéro de polarisation sous la forme 2 ^ (bits pour l'exposant -1) -1 ie 2 ^ (11 -1) -1 = 102, notre exposant devient donc 52 + 1023 = 1075

Maintenant notre code multiplie le nombre avec 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 et notre bit de signe est .

Donc, notre numéro modifié devient:

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

0111001101101011011000110111010101010011001010110010101101000011 

puisque m est converti en pointeur de caractères, nous allons scinder le modèle de bits 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 

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

s   k   c   u      S      +   +   C 

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

Supposons maintenant que vous exécutiez ce programme sur une machine little-endian (le bit d’ordre inférieur est stocké dans l’adresse inférieure), donc pointeur m pointeur vers le bit d’adresse le plus bas, puis reprise en prenant des bits dans des mandrins de 8. (en tant que type converti en char *) et printf () s’arrête lorsqu’il est confronté à 00000000 dans le dernier tronçon ...

Ce code n'est cependant pas portable.

1
Abhishek Ghosh