Подтвердить что ты не робот

Как файл может содержать нулевые байты?

Как возможно, что файлы могут содержать пустые байты в операционных системах, написанных на языке с нулевыми терминами (а именно, C)?

Например, если я запустил этот код оболочки:

$ printf "Hello\00, World!" > test.txt
$ xxd test.txt
0000000: 4865 6c6c 6f00 2c20 576f 726c 6421       Hello., World!

Я вижу нулевой байт в test.txt (по крайней мере, в OS X). Если C использует нуль-завершающие строки, а OS X записывается на C, то почему файл не заканчивается в нулевом байте, в результате получается файл, содержащий Hello вместо Hello\00, World!? Существует ли принципиальное различие между файлами и строками?

4b9b3361

Ответ 1

Строки с нулевым завершением представляют собой конструкцию C, используемую для определения конца последовательности символов, предназначенных для использования в качестве строки. Функции манипуляции строками, такие как strcmp, strcpy, strchr, и другие используют эту конструкцию для выполнения своих обязанностей.

Но вы все равно можете читать и записывать двоичные данные, содержащие нулевые байты в вашей программе, а также в файлы и из них. Вы просто не можете рассматривать их как строки.

Вот пример того, как это работает:

#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;
}

Содержимое out1:

[[email protected] 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.

Для первого массива, поскольку мы используем функцию fwrite и говорим ему, чтобы записать 4 элемента размером int, все значения в массиве отображаются в файле. Вы можете видеть на выходе, что все значения записаны, значения 32-битные, и каждое значение написано в порядке порядка байтов. Мы также можем видеть, что второй и четвертый элементы массива содержат один нулевой байт, а третье значение - 0 пустых байтов, и все они появляются в файле.

Мы также используем fwrite для второго массива, который содержит элементы типа char, и мы снова видим, что все элементы массива появляются в файле. В частности, третье значение в массиве равно 0, которое состоит из одного нулевого байта, который также отображается в файле.

Третий массив сначала записывается с помощью функции fprintf с использованием спецификатора формата %s, который ожидает строку. Он записывает первые 5 байтов этого массива в файл перед встречей с нулевым байтом, после чего он перестает читать массив. Затем он печатает символ новой строки (0x0a) в соответствии с форматом.

Третий массив, который он записал в файл снова, на этот раз с помощью fwrite. Строковая константа "Hello\x0World" содержит 12 байтов: 5 для "Hello", один для явного байт-байта, 5 для "World" и один для нулевого байта, который неявно завершает строчную константу. Поскольку fwrite задан полный размер массива (12), он записывает все эти байты. Действительно, глядя на содержимое файла, мы видим каждый из этих байтов.

В качестве побочного примечания в каждом вызове fwrite я жестко запрограммировал размер массива для третьего параметра вместо использования более динамического выражения, такого как sizeof(a1)/sizeof(a1[0]), чтобы сделать его более ясным, сколько байты записываются в каждом случае.

Ответ 2

Строки с нулевым завершением - это, конечно, не единственное, что вы можете поместить в файл. Код операционной системы не рассматривает файл в качестве носителя для хранения строк с нулевым завершением: операционная система представляет файл как набор произвольных байтов.

Что касается C, существуют API ввода-вывода для записи файлов в двоичном режиме. Вот пример:

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);

Этот код C создает файл с именем "data.bin" и записывает в него десять байтов. Обратите внимание, что хотя buffer является символьным массивом, это не строка с нулевым завершением.

Ответ 3

Поскольку файл представляет собой просто поток байтов, любой байт, включая нулевой байт. Некоторые файлы называются текстовыми файлами, когда они содержат только подмножество всех возможных байтов: печатные (примерно буквенно-цифровые, пробелы, знаки препинания).

Строки C - это последовательность байтов, заканчивающихся нулевым байтом, просто вопрос конвенции. Они слишком часто становятся источником путаницы; просто последовательность, завершаемая нулем, означает, что любой ненулевой байт, завершенный нулем, является правильной строкой С! Даже тот, который содержит непечатаемый байт, или элемент управления char. Будьте осторожны, потому что ваш пример не является C! В C printf("dummy\000foo"); никогда не будет печатать foo, поскольку printf рассмотрит строку C, начинающуюся с d и заканчивающуюся нулевым байтом в середине. Некоторые компиляторы жалуются на такой строковый литерал C.

Теперь нет прямой ссылки между строками C (которые обычно также содержат только печатные char) и текстовый файл. При печати строки C в файл обычно состоит в сохранении только ее подпоследовательности непустых байтов.

Ответ 4

В то время как null-байты используются для прерывания строк и необходимы для функций манипуляции строками (поэтому они знают, где заканчивается строка), в двоичных файлах \0 байты могут быть повсюду.

Рассмотрим двоичный файл с 32-битными номерами, например, все они будут содержать нуль-байты, если их значения меньше 2 ^ 24 (например: 0x 00 1a 00 c7 или 64-бит 0x 000000 0a 0000 1a4d).

Идем для Unicode-16, где все символы ASCII имеют ведущий или конечный \0, в зависимости от их endianness, а строки нужны для завершения с помощью \0\0.

У большого количества файлов даже есть блоки (до 4 КБ или даже 64 КБ) с \0 байтами, чтобы иметь быстрый доступ к желаемым блокам.

Для еще большего количества нулевых байтов в файле просмотрите разреженные файлы, где по умолчанию все байты \0, и блоки, заполненные нулевыми байтами, даже не сохраняются на диске для экономии места.

Ответ 5

Рассмотрим обычные вызовы функций C для записи данных в файлы - write(2):

ssize_t
write(int fildes, const void *buf, size_t nbyte);

... и fwrite(3):

size_t
fwrite(const void *restrict ptr, size_t size, size_t nitems, FILE *restrict stream);

Ни одна из этих функций не принимает строку const char * NUL-terminated. Скорее, они берут массив байтов (a const void *) с явным размером. Эти функции обрабатывают байты NUL так же, как и любое другое значение байта.

Ответ 6

Прежде чем отвечать на все, обратите внимание, что

( примечание: в соответствии с nm (см. комментарий в OP) " Байт - это наименьшее количество, доступное для записи на диск с помощью стандартной библиотеки C, нестандартные библиотеки могут иметь дело с битами или чем-то еще". Итак, то, что я сказал ниже о размерах WORD, являющихся наименьшим количеством, вероятно, не очень верно, но тем не менее обеспечивает понимание, тем не менее).

NULL всегда 0_decimal (практически)

dec: 0
hex: 0x00000000
bin: 00000000 00000000 00000000 00000000

хотя его фактическое значение определяется спецификацией языка программирования, поэтому используйте везде NULL константу NULL, а не hardcoding 0 (в случае ее изменения, когда ад замерзает).

ASCII кодировка для символа '0' равна 48_decimal

dec: 48
hex: 0x00000030
bin: 00000000 00000000 00000000 00110000

Концепция NULL не существует в файле, но в пределах генерирующего языка программирования приложений. В файле существует только числовая кодировка/значение NULL.

Как возможно, что файлы могут содержать нулевые байты при работе системы, написанные на языке с нуль-завершающими строками (а именно, С)?

С изложенным выше этот вопрос становится , как файл может содержать 0?. Ответ теперь тривиален.

Например, если я запустил этот код оболочки:

$ printf "Hello\00, World!" 
test.txt $ xxd test.txt 0000000: 4865
6c6c 6f00 2c20 576f 726c 6421            Hello., World!

Я вижу нулевой байт в test.txt(по крайней мере, в OS X). Если C использует нуль-завершающие строки, а OS X записывается на C, тогда как файл не завершается в нулевом байте, в результате чего файл содержащий Hello вместо Hello\00, World!?

Есть ли принципиальное различие между файлами и строками?

Предполагая кодировку ASCII (1-байтные/8-битные символы в десятичном диапазоне 0 и 127):

  • Строки - буферы / char -аррамы из 1 байтовых символов (где NULL = 0_decimal и '0' = 48_decimal)).
  • Файлы - это последовательности 32-разрядных или 64-разрядных " СЛОВ." (зависит от ОС и оборудования, то есть от x86 или x64 соответственно).

Следовательно, 32-разрядный файл ОС, содержащий только строки ASCII, будет состоять из 32 (4 байта), которые находятся между десятичными значениями 0 и 127, по существу используя только первый байт 4-байтового слова (b2: base-2, decimal - base-10 и hex base-16, fyi)

  0_b2: 00000000 00000000 00000000 00000000
 32_b2: 00000000 00000000 00000000 00100000
 64_b2: 00000000 00000000 00000000 01000000
 96_b2: 00000000 00000000 00000000 01100000
127_b2: 00000000 00000000 00000000 11111111
128_b2: 00000000 00000000 00000001 00000000

Погода в этом байте слева или больше зависит от ОС endianness.

Но чтобы ответить на ваш вопрос о отсутствующем NULL после Hello\00, World!, я собираюсь предположить, что он был заменен значением EOL/EOF (конец файла), которое больше всего вероятно, не подлежит печати, и поэтому вы не видите его в окне вывода.

Примечание.. Я уверен, что современные ОС (и классические системы на базе Unix) оптимизируют хранение ASCII, так что 1 слово (4 байта) может упаковываться в 4 символа. Все меняется с помощью UTF, поскольку эти кодировки используют больше битов для хранения символов, поскольку у них больше алфавитов/наборы символов для представления (например, 50k кандзи/японские символы). Я думаю, UTF-8 является аналогом ASCII и переименован для единообразия (с UTF-16 и UTF-32).

Примечание. C/С++ фактически "упаковывает" 4 символа в одно 4-байтовое слово, используя массивы символов (т.е. строки). Поскольку каждый char является 1 байтом, компилятор будет распределять и обрабатывать его как 1-байтовое, арифметически, в стеке или куче. Поэтому, если вы объявляете массив в функции (т.е. Автоматическую переменную), например

char[] str1[7] = {'H','e','l','l','o','!','\0'};

где стек функций начинается с адреса 1000_b10 (base-10/decimal), тогда ya имеет:

072 101 108 108 111 033

addr  char        binary   decimal
----  ----------- -------- -------
1000: str1[0] 'H' ‭01001000‬ (072)
1001: str1[1] 'e' ‭01100101‬ (101)
1002: str1[2] 'l' ‭01101100‬ (108)
1003: str1[3] 'l' ‭01101100‬ (108)
1004: str1[4] 'o' ‭01101111‬ (111)
1005: str1[5] '!' ‭00100001‬ (033)
1006: str1[6] '0' 00000000 (000)

Так как RAM имеет адрес байта, каждый адрес ссылается на один байт.