Séquence d’échappement : comparaison de string et valeurs hexa

C


Aujourd’hui, nous allons parler développement C, des normes ANSI C et d’un fonctionnement surprenant des séquences d’échappement. Oui tout de suite ça ne donne pas forcément envie dit comme ça mais vous verrez que cet article a tout de même un certain intérêt.

Prérequis

L’ensemble du code C présent sur cette page est présent sur le dépôt Github c_compare_string et est compilé avec la commande suivante :

$ gcc  main.c -o c_compare_string

Les séquences d’échappement

Nombre d’entre vous savent ce que sont les séquences d’échappement. Pour les autres, les séquences d’échappement permettent de définir un caractère par sa valeur hexadécimale, sa valeur octale ou encore sa représentation (par exemple \n pour le retour à la ligne) et ce dans beaucoup de langages.

printf("\x31 \061 1\n");
> 1 1 1
print('\x31 \061 1\n');
> 1 1 1

Dans les exemples ci-dessus, nous sortons une chaînes constitué de 3 « 1 » qui se suivent (\x31 \061 1), suivi d’un retour à la ligne (\n).

La comparaison de chaînes

En C, pour comparer 2 chaines de caractères, nous avons 3 solutions, strcmp, strncmp ou encore memcmp.

#include <string.h>
int strcmp(const char *s1, const char *s2);
int strncmp(const char *s1, const char *s2, size_t n);
int memcmp(const void *s1, const void *s2, size_t n);

Les 2 premières comparent les chaines de caractères jusqu’à l’octet 0x00 qui est censé désigner la fin de chaine. Cependant strncmp comparera au maximum le nombre de caractère spécifié dans le paramètre n.
Enfin la dernière memcmp comparera la valeur de l’ensemble des n premiers octets. La page de man spécifie que l’ensemble des octets seront interprété en unsigned char.

Notre problématique

Sur un projet particulier, nous avons eu un soucis de devoir comparer 2 chaînes de caractères qui nous paraissaient identiques mais qui ne l’étaient pas au vue de memcmp etstrncmp. Un code parlant plus que mille mots.

#define STRING1 "\x3123toto"

char buffer[10];
int rmc, rsc;

// Prepare buffer
buffer[0] = 0x31;
buffer[1] = '2';
buffer[2] = '3';
buffer[3] = 't';
buffer[4] = 'o';
buffer[5] = 't';
buffer[6] = 'o';
buffer[7] = 0x00;

// Compare le buffer à STRING1, on attend 0 en retour.
rmc = memcmp(buffer, STRING1, 7);
rsc = strncmp(buffer, STRING1, 7);
printf("rmc: %d - rsc: %d - buffer: %s - STRING1: %s\n", rmc, rsc, buffer, STRING1);
// rmc: 14 - rsc: 14 - buffer: 123toto - STRING1: #toto

On voit dans cet exemple que quelle que soit la fonction utilisée, elles nous renvoient que les chaînes sont différentes et voila la cause de cet article.

Les solutions

Après quelques essais, nous avons remarqué que les solutions suivantes fonctionnaient :

#define STRING2 "\x31" "23toto"
#define STRING4 "\x31\x32\x33toto"
...
/* Compare le buffer à STRING2, on attend 0 en retour. */
rmc = memcmp(buffer, STRING2, 7);
rsc = strncmp(buffer, STRING2, 7);
printf("> rmc: %d - rsc: %d - buffer: %s - STRING2: %s\n", rmc, rsc, buffer, STRING2);
/* > rmc: 0 - rsc: 0 - buffer: 123toto - STRING2: 123toto */

/* Compare le buffer à STRING4, on attend 0 en retour. */
rmc = memcmp(buffer, STRING4, 7);
rsc = strncmp(buffer, STRING4, 7);
printf("rmc: %d - rsc: %d - buffer: %s - STRING4: %s\n", rmc, rsc, buffer, STRING4);
/* > rmc: 0 - rsc: 0 - buffer: 123toto - STRING4: 123toto */

Sur ces 2 essais, les comparaisons fonctionnent. Mais pourquoi ?

Explication

Il s’avère que selon les normes ANSI C (et ce depuis au moins la C89), les séquences d’échappement hexadécimales intègrent tous les caractères pouvant entrer dans une représentation hexadécimale d’un nombre. Quant aux représentations octales, elles ne peuvent être composées de plus de 3 digits.

En gros, les représentations suivantes sont equales :

printf("\x0031 = \x000031 = \x00000031 = \61 = \061\n");
/* > 1 = 1 = 1 = 1 = 1 */

Le plus surprenant est que la représentation n’est finalement enregistrée que sur 1 octet.

char *test = "\x31323334\x00\x00\x00";
int i;
for (i = 0; i < 4; ++i) {
    printf("\n> test[%d] = 0x%02X", i, test[i]);
}
/* > test[0] = 0x34
> test[1] = 0x00
> test[2] = 0x00
> test[3] = 0x00 */

Si vous aussi, vous êtes surpris par un comportement anecdotique de votre compilateur ou du préprocesseur, partagez le avec nous.

Laisser un commentaire

Votre adresse e-mail ne sera pas publiée. Les champs obligatoires sont indiqués avec *

Ce site utilise Akismet pour réduire les indésirables. En savoir plus sur comment les données de vos commentaires sont utilisées.