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 .
skcuS++C
.
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 .
skcuS++C
.
Réponses:
Le nombre 7709179928849219.0
a 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()
.
7709179928849219
valeur et récupéré la représentation binaire.
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++Sucks
et m[1]
contient uniquement des zéros, il a donc un terminateur nul pour la C++Sucks
chaî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);
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 double
fait 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 double
sera 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 0
dans 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).
basic.start.main
3.6.1 / 3 avec le même libellé.
main()
, ou de le remplacer par un appel API pour formater le disque dur, ou autre chose.
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 if
dé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 and
d'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 main
et en imprimant ce qui main
retourne:
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à.
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:
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);
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]
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» 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.
f
retour est-il un int
?
int
retour dans la question. Permettez-moi de résoudre ce problème.
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
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.