Niveau 2 alt : NaNaNaNaNaNaN, Baaaaatman !

La version alternative du niveau 2 est un piège redoutable :

level2@io:~$ cat /levels/level02_alt.c 
/* submitted by noname */

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>


#define answer 3.141593

void main(int argc, char **argv) {

	float a = (argc - 2)?: strtod(argv[1], 0);

        printf("You provided the number %f which is too ", a);


        if(a < answer)
                 puts("low");
        else if(a > answer)
                puts("high");
        else
                execl("/bin/sh", "sh", "-p", NULL);
}

Il faut donc passer une valeur qui ne soit ni inférieure ni supérieure à 3,141593. Fastoche !

level2@io:~$ /levels/level02_alt 3.141593
You provided the number 3.141593 which is too low

:(

Les fins connaisseurs du standard IEEE754 (ou les gens qui auront compris mon jeu de mots foireux) auront sans doute immédiatement vu l’astuce :

level2@io:~$ /levels/level02_alt nan
sh-4.1$ 

En effet, la valeur NaN (Not a Number) n’est comparable à aucune valeur ; en pratique, toute comparaison d’un NaN avec quoi que ce soit (même un autre NaN) retournera 0.

Cela dit, on peut légitimement se demander pourquoi la première solution ne fonctionnait pas. On pense évidemment à une erreur d’arrondi, mais est-ce qu’il ne serait pas possible d’affiner la valeur donnée pour passer le niveau ? La réponse est non, car il est impossible de donner une valeur suffisamment précise.

Pour s’en rendre compte, il faut savoir qu’une valeur littérale à virgule flottante sera considérée par défaut comme une valeur en double précision (type double). Cela signifie que notre “constante” answer sera en double précision, or la valeur passée en argument est stockée dans une variable de type float, c’est-à-dire en simple précision. Un calcul simple montre que le nombre 3,141593 se représente en binaire par

11.001001000011111101110000010110000101011110101111111

Combien de bits cela fait-il ? On ne va quand même pas les compter…

firas@aoba ~ % echo -n "11.001001000011111101110000010110000101011110101111111" | wc -c
      54

54 caractères. En enlevant le point et le premier 1 (qui est implicite en IEEE754), on voit qu’on a besoin de 52 bits de mantisse pour représenter exactement la valeur 3,141593. 52 bits de mantisse, c’est exactement ce qu’on a dans un double, donc pas de souci. Par contre dans un float on a uniquement 23 bits de mantisse, et donc on ne peut pas représenter exactement 3,141593. Cela signifie que quand on stocke la valeur dans notre variable de type float, elle n’est plus exacte.

Ce qu’on compare, donc, c’est d’un côté la valeur 3,141593 représentée exactement dans un double, et de l’autre la même valeur mais convertie en float, et donc avec de la précision en moins. Quand on fait la comparaison, la valeur float est convertie en un double, mais cela ne nous fait pas retrouver la valeur originale, car la précision perdue ne peut pas être retrouvée. Pour faire une analogie, c’est comme redimensionner une image de 1920×1080 pixels à 640×480, puis de nouveau de 640×480 à 1920×1080 alors que l’image d’origine contenait des détails qui sont perdus lors du premier redimensionnement : l’image finale ne sera évidemment pas identique à l’image originale.

Pour la même raison, aucune des valeurs qu’on pourrait passer en argument ne pourra faire illusion et nous faire passer le niveau : pour reprendre la même analogie, aucune image de 640×480 pixels redimensionnée à 1920×1080 ne pourra donner l’impression d’être une “vraie” image à 1920×1080.

Leave a Reply

Your email address will not be published. Required fields are marked *