Comment est-il possible que des fichiers puissent contenir des octets nuls dans des systèmes d'exploitation écrits dans un langage avec des chaînes se terminant par null (à savoir, C)?
Par exemple, si j'exécute ce code Shell:
$ printf "Hello\00, World!" > test.txt
$ xxd test.txt
0000000: 4865 6c6c 6f00 2c20 576f 726c 6421 Hello., World!
Je vois un octet nul dans test.txt
(au moins sous OS X). Si C utilise des chaînes se terminant par null et OS X est écrit en C, alors comment se fait-il que le fichier ne se termine pas à l'octet nul, ce qui entraîne le fichier contenant Hello
au lieu de Hello\00, World!
? Existe-t-il une différence fondamentale entre les fichiers et les chaînes?
Les chaînes à terminaison nulle sont une construction C utilisée pour déterminer la fin d'une séquence de caractères destinée à être utilisée comme chaîne. Les fonctions de manipulation de chaînes telles que strcmp
, strcpy
, strchr
et d'autres utilisent cette construction pour effectuer leurs tâches.
Mais vous pouvez toujours lire et écrire des données binaires qui contiennent des octets nuls dans votre programme ainsi que vers et depuis des fichiers. Vous ne pouvez tout simplement pas les traiter comme des chaînes.
Voici un exemple de comment cela fonctionne:
#include <stdio.h>
#include <stdlib.h>
int main()
{
FILE *fp = fopen("out1","w");
if (fp == NULL) {
perror("fopen failed");
exit(1);
}
int a1[] = { 0x12345678, 0x33220011, 0x0, 0x445566 };
char a2[] = { 0x22, 0x33, 0x0, 0x66 };
char a3[] = "Hello\x0World";
// this writes the whole array
fwrite(a1, sizeof(a1[0]), 4, fp);
// so does this
fwrite(a2, sizeof(a2[0]), 4, fp);
// this does not write the whole array -- only "Hello" is written
fprintf(fp, "%s\n", a3);
// but this does
fwrite(a3, sizeof(a3[0]), 12, fp);
fclose(fp);
return 0;
}
Contenu de out1:
[dbush@db-centos tmp]$ xxd out1
0000000: 7856 3412 1100 2233 0000 0000 6655 4400 xV4..."3....fUD.
0000010: 2233 0066 4865 6c6c 6f0a 4865 6c6c 6f00 "3.fHello.Hello.
0000020: 576f 726c 6400 World.
Pour le premier tableau, parce que nous utilisons la fonction fwrite
et lui disons d'écrire 4 éléments de la taille d'un int
, toutes les valeurs du tableau apparaissent dans le fichier. Vous pouvez voir à partir de la sortie que toutes les valeurs sont écrites, les valeurs sont 32 bits et chaque valeur est écrite dans l'ordre des octets en petits caractères. Nous pouvons également voir que les deuxième et quatrième éléments du tableau contiennent chacun un octet nul, tandis que la troisième valeur étant 0 a 4 octets nuls, et tous apparaissent dans le fichier.
Nous utilisons également fwrite
sur le deuxième tableau, qui contient des éléments de type char
, et nous constatons à nouveau que tous les éléments du tableau apparaissent dans le fichier. En particulier, la troisième valeur du tableau est 0, qui consiste en un seul octet nul qui apparaît également dans le fichier.
Le troisième tableau est d'abord écrit avec la fonction fprintf
en utilisant un spécificateur de format %s
Qui attend une chaîne. Il écrit les 5 premiers octets de ce tableau dans le fichier avant de rencontrer l'octet nul, après quoi il arrête de lire le tableau. Il imprime ensuite un caractère de nouvelle ligne (0x0a
) Selon le format.
Le troisième tableau, il a écrit à nouveau dans le fichier, cette fois en utilisant fwrite
. La constante de chaîne "Hello\x0World"
Contient 12 octets: 5 pour "Bonjour", un pour l'octet nul explicite, 5 pour "Monde" et un pour l'octet nul qui termine implicitement la constante de chaîne. Puisque fwrite
a la taille complète du tableau (12), il écrit tous ces octets. En effet, en regardant le contenu du fichier, nous voyons chacun de ces octets.
En guise de remarque, dans chacun des appels fwrite
, j'ai codé en dur la taille du tableau pour le troisième paramètre au lieu d'utiliser une expression plus dynamique telle que sizeof(a1)/sizeof(a1[0])
pour le rendre plus effacer exactement combien d'octets sont écrits dans chaque cas.
Les chaînes à terminaison nulle sont certainement pas la seule chose que vous pouvez mettre dans un fichier. Le code du système d'exploitation ne considère pas un fichier comme un véhicule pour stocker des chaînes terminées par un caractère nul: un système d'exploitation présente un fichier comme une collection d'octets arbitraires.
En ce qui concerne C, des API d'E/S existent pour écrire des fichiers en mode binaire. Voici un exemple:
char buffer[] = {0, 1, 0, 2, 0, 3, 0, 4, 0, 5};
FILE *f = fopen("data.bin","wb"); // "w" is for write, "b" is for binary
fwrite(buffer, 1, sizeof(buffer), f);
Ce code C crée un fichier appelé "data.bin" et y écrit dix octets. Notez que bien que buffer
soit un tableau de caractères, il s'agit pas d'une chaîne terminée par null.
Parce qu'un fichier n'est qu'un flux d'octets, de n'importe quel octet y compris l'octet nul. Certains fichiers sont appelés fichiers texte lorsqu'ils ne contiennent qu'un sous-ensemble de tous les octets possibles: les imprimables (grossièrement alphanumériques, espaces, ponctuation).
Les chaînes C sont une séquence d'octets terminée par un octet nul, juste une question de convention. Ils sont trop souvent source de confusion; juste une séquence terminée par null, signifie que tout octet non nul terminé par null est une chaîne C correcte! Même celui qui contient un octet non imprimable ou un caractère de contrôle. Soyez prudent car votre exemple n'est pas un C! Dans C printf("dummy\000foo");
n'imprimera jamais foo
car printf
considérera la chaîne C commençant par d
et se terminant à l'octet nul au milieu. Certains compilateurs se plaignent d'un tel littéral de chaîne C.
Maintenant, il n'y a pas de lien direct entre les chaînes C (qui ne contiennent généralement que des caractères imprimables) et le fichier texte. L'impression d'une chaîne C dans un fichier consiste généralement à ne stocker que sa sous-séquence d'octets non nuls.
Alors que les octets nuls sont utilisés pour terminer les chaînes et nécessaires aux fonctions de manipulation de chaînes (afin qu'ils sachent où se termine la chaîne), dans les fichiers binaires \0
les octets peuvent être partout.
Considérons un fichier binaire avec des nombres 32 bits par exemple, ils contiendront tous des octets nuls si leurs valeurs sont inférieures à 2 ^ 24 (par exemple: 0x 00 1a 00 c7, ou 64 bits 0x 000000 0a 0000 1a4d).
Idem pour Unicode-16 où tous les caractères ASCII ont un _ ou un début \0
, en fonction de leur endianness , et les chaînes doivent se terminer par \0\0
.
De nombreux fichiers ont même des blocs remplis (jusqu'à 4 Ko ou même 64 Ko) avec \0
octets, pour avoir un accès rapide aux blocs souhaités.
Pour encore plus d'octets nuls dans un fichier, jetez un œil à fichiers épars , où tous les octets sont \0
par défaut, et les blocs pleins d'octets nuls ne sont même pas stockés sur le disque pour économiser de l'espace.
Considérez les appels de fonction C habituels pour écrire des données dans des fichiers - write(2)
:
ssize_t
write(int fildes, const void *buf, size_t nbyte);
… Et fwrite(3)
:
size_t
fwrite(const void *restrict ptr, size_t size, size_t nitems, FILE *restrict stream);
Aucune de ces fonctions n'accepte une chaîne terminée par const char *
NUL. Ils prennent plutôt un tableau d'octets (un const void *
) Avec une taille explicite. Ces fonctions traitent les octets NUL comme n'importe quelle autre valeur d'octet.